committed by
GitHub
619 changed files with 19602 additions and 1218 deletions
@ -0,0 +1,200 @@ |
|||
<script lang="ts" setup> |
|||
import type { CSSProperties, PropType } from 'vue'; |
|||
|
|||
import type { Nullable } from '@vben/types'; |
|||
|
|||
import { |
|||
computed, |
|||
onMounted, |
|||
onUnmounted, |
|||
ref, |
|||
unref, |
|||
useAttrs, |
|||
useTemplateRef, |
|||
} from 'vue'; |
|||
|
|||
import { useNamespace } from '@vben/hooks'; |
|||
|
|||
import { useDebounceFn } from '@vueuse/core'; |
|||
import Cropper from 'cropperjs'; |
|||
|
|||
import 'cropperjs/dist/cropper.css'; |
|||
|
|||
type Options = Cropper.Options; |
|||
type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>; |
|||
|
|||
const props = defineProps({ |
|||
src: { type: String, required: true }, |
|||
alt: { type: String, default: '' }, |
|||
circled: { type: Boolean, default: false }, |
|||
realTimePreview: { type: Boolean, default: true }, |
|||
height: { type: [String, Number], default: '360px' }, |
|||
crossorigin: { |
|||
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>, |
|||
default: undefined, |
|||
}, |
|||
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) }, |
|||
options: { type: Object as PropType<Options>, default: () => ({}) }, |
|||
}); |
|||
const emits = defineEmits(['cropend', 'ready', 'cropendError']); |
|||
const defaultOptions: Options = { |
|||
aspectRatio: 1, |
|||
zoomable: true, |
|||
zoomOnTouch: true, |
|||
zoomOnWheel: true, |
|||
cropBoxMovable: true, |
|||
cropBoxResizable: true, |
|||
toggleDragModeOnDblclick: true, |
|||
autoCrop: true, |
|||
background: true, |
|||
highlight: true, |
|||
center: true, |
|||
responsive: true, |
|||
restore: true, |
|||
checkCrossOrigin: true, |
|||
checkOrientation: true, |
|||
scalable: true, |
|||
modal: true, |
|||
guides: true, |
|||
movable: true, |
|||
rotatable: true, |
|||
}; |
|||
|
|||
const attrs = useAttrs(); |
|||
|
|||
const imgElRef = useTemplateRef<ElRef<HTMLImageElement>>('imgElRef'); |
|||
const cropper = ref<Nullable<Cropper>>(); |
|||
const isReady = ref(false); |
|||
|
|||
const { b, is } = useNamespace('cropper-image'); |
|||
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80); |
|||
|
|||
const getImageStyle = computed((): CSSProperties => { |
|||
return { |
|||
height: props.height, |
|||
maxWidth: '100%', |
|||
...props.imageStyle, |
|||
}; |
|||
}); |
|||
|
|||
const getClass = computed(() => { |
|||
return [b(), attrs.class, is('circled', props.circled)]; |
|||
}); |
|||
|
|||
const getWrapperStyle = computed((): CSSProperties => { |
|||
return { height: `${`${props.height}`.replace(/px/, '')}px` }; |
|||
}); |
|||
|
|||
onMounted(init); |
|||
|
|||
onUnmounted(() => { |
|||
cropper.value?.destroy(); |
|||
}); |
|||
|
|||
async function init() { |
|||
const imgEl = unref(imgElRef); |
|||
if (!imgEl) { |
|||
return; |
|||
} |
|||
cropper.value = new Cropper(imgEl, { |
|||
...defaultOptions, |
|||
ready: () => { |
|||
isReady.value = true; |
|||
realTimeCroppered(); |
|||
emits('ready', cropper.value); |
|||
}, |
|||
crop() { |
|||
debounceRealTimeCroppered(); |
|||
}, |
|||
zoom() { |
|||
debounceRealTimeCroppered(); |
|||
}, |
|||
cropmove() { |
|||
debounceRealTimeCroppered(); |
|||
}, |
|||
...props.options, |
|||
}); |
|||
} |
|||
|
|||
// Real-time display preview |
|||
function realTimeCroppered() { |
|||
props.realTimePreview && croppered(); |
|||
} |
|||
|
|||
// event: return base64 and width and height information after cropping |
|||
function croppered() { |
|||
if (!cropper.value) { |
|||
return; |
|||
} |
|||
const imgInfo = cropper.value.getData(); |
|||
const canvas = props.circled |
|||
? getRoundedCanvas() |
|||
: cropper.value.getCroppedCanvas(); |
|||
canvas.toBlob((blob) => { |
|||
if (!blob) { |
|||
return; |
|||
} |
|||
const fileReader: FileReader = new FileReader(); |
|||
fileReader.readAsDataURL(blob); |
|||
fileReader.onloadend = (e) => { |
|||
emits('cropend', { |
|||
imgBase64: e.target?.result ?? '', |
|||
imgInfo, |
|||
}); |
|||
}; |
|||
// eslint-disable-next-line unicorn/prefer-add-event-listener |
|||
fileReader.onerror = () => { |
|||
emits('cropendError'); |
|||
}; |
|||
}, 'image/png'); |
|||
} |
|||
|
|||
// Get a circular picture canvas |
|||
function getRoundedCanvas() { |
|||
const sourceCanvas = cropper.value!.getCroppedCanvas(); |
|||
const canvas = document.createElement('canvas'); |
|||
const context = canvas.getContext('2d')!; |
|||
const width = sourceCanvas.width; |
|||
const height = sourceCanvas.height; |
|||
canvas.width = width; |
|||
canvas.height = height; |
|||
context.imageSmoothingEnabled = true; |
|||
context.drawImage(sourceCanvas, 0, 0, width, height); |
|||
context.globalCompositeOperation = 'destination-in'; |
|||
context.beginPath(); |
|||
context.arc( |
|||
width / 2, |
|||
height / 2, |
|||
Math.min(width, height) / 2, |
|||
0, |
|||
2 * Math.PI, |
|||
true, |
|||
); |
|||
context.fill(); |
|||
return canvas; |
|||
} |
|||
</script> |
|||
<template> |
|||
<div :class="getClass" :style="getWrapperStyle"> |
|||
<img |
|||
v-show="isReady" |
|||
ref="imgElRef" |
|||
:src="src" |
|||
:alt="alt" |
|||
:crossorigin="crossorigin" |
|||
:style="getImageStyle" |
|||
/> |
|||
</div> |
|||
</template> |
|||
<style scoped lang="scss"> |
|||
$namespace: vben; |
|||
|
|||
.#{$namespace}-cropper-image { |
|||
&.is-circled { |
|||
.cropper-view-box, |
|||
.cropper-face { |
|||
border-radius: 50%; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,159 @@ |
|||
<script setup lang="ts"> |
|||
import type { ButtonProps } from 'ant-design-vue/es/button'; |
|||
|
|||
import type { CSSProperties, PropType } from 'vue'; |
|||
|
|||
import { computed, ref, unref, watch, watchEffect } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { useNamespace } from '@vben/hooks'; |
|||
import { createIconifyIcon } from '@vben/icons'; |
|||
import { useI18n } from '@vben/locales'; |
|||
|
|||
import { Button } from 'ant-design-vue'; |
|||
|
|||
import CropperModal from './CropperModal.vue'; |
|||
|
|||
interface File { |
|||
file: Blob; |
|||
fileName?: string; |
|||
name: string; |
|||
} |
|||
|
|||
const props = defineProps({ |
|||
width: { type: [String, Number], default: '200px' }, |
|||
value: { type: String, default: '' }, |
|||
showBtn: { type: Boolean, default: true }, |
|||
btnProps: { type: Object as PropType<ButtonProps>, default: undefined }, |
|||
btnText: { type: String, default: '' }, |
|||
uploadApi: { |
|||
type: Function as PropType<(file: File) => Promise<void>>, |
|||
default: undefined, |
|||
}, |
|||
}); |
|||
|
|||
const emits = defineEmits(['update:value', 'change']); |
|||
|
|||
const UploadIcon = createIconifyIcon('ant-design:cloud-upload-outlined'); |
|||
|
|||
const sourceValue = ref(props.value || ''); |
|||
const { b, e } = useNamespace('cropper-avatar'); |
|||
const [Modal, modalApi] = useVbenModal({ |
|||
connectedComponent: CropperModal, |
|||
}); |
|||
const { t } = useI18n(); |
|||
|
|||
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`); |
|||
|
|||
const getIconWidth = computed( |
|||
() => `${Number.parseInt(`${props.width}`.replace(/px/, '')) / 2}px`, |
|||
); |
|||
|
|||
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) })); |
|||
|
|||
const getImageWrapperStyle = computed( |
|||
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }), |
|||
); |
|||
|
|||
watchEffect(() => { |
|||
sourceValue.value = props.value || ''; |
|||
}); |
|||
|
|||
watch( |
|||
() => sourceValue.value, |
|||
(v: string) => { |
|||
emits('update:value', v); |
|||
}, |
|||
); |
|||
|
|||
function handleUploadSuccess(url: string) { |
|||
sourceValue.value = url; |
|||
emits('change', url); |
|||
} |
|||
function openModal() { |
|||
modalApi.open(); |
|||
} |
|||
function closeModal() { |
|||
modalApi.close(); |
|||
} |
|||
|
|||
defineExpose({ openModal, closeModal }); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="b()" :style="getStyle"> |
|||
<div |
|||
:class="e(`image-wrapper`)" |
|||
:style="getImageWrapperStyle" |
|||
@click="openModal" |
|||
> |
|||
<div :class="e(`image-mask`)" :style="getImageWrapperStyle"> |
|||
<UploadIcon |
|||
:width="getIconWidth" |
|||
:style="getImageWrapperStyle" |
|||
color="#d6d6d6" |
|||
/> |
|||
</div> |
|||
<img :src="sourceValue" v-if="sourceValue" alt="avatar" /> |
|||
</div> |
|||
<Button |
|||
:class="e(`upload-btn`)" |
|||
@click="openModal" |
|||
v-if="showBtn" |
|||
v-bind="btnProps" |
|||
> |
|||
{{ btnText ? btnText : t('cropper.selectImage') }} |
|||
</Button> |
|||
|
|||
<Modal |
|||
@upload-success="handleUploadSuccess" |
|||
:upload-api="uploadApi" |
|||
:src="sourceValue" |
|||
/> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
$namespace: vben; |
|||
|
|||
.#{$namespace}-cropper-avatar { |
|||
display: inline-block; |
|||
text-align: center; |
|||
|
|||
&__image-wrapper { |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
border: var(--border); |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
// height: 100%; |
|||
} |
|||
} |
|||
|
|||
&__image-mask { |
|||
position: absolute; |
|||
width: inherit; |
|||
height: inherit; |
|||
cursor: pointer; |
|||
background: rgb(0 0 0 / 40%); |
|||
border: inherit; |
|||
border-radius: inherit; |
|||
opacity: 0; |
|||
transition: opacity 0.4s; |
|||
|
|||
::v-deep(svg) { |
|||
margin: auto; |
|||
} |
|||
} |
|||
|
|||
&__image-mask:hover { |
|||
opacity: 40; |
|||
} |
|||
|
|||
&__upload-btn { |
|||
margin: 10px auto; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,309 @@ |
|||
<script setup lang="ts"> |
|||
import type { UploadProps } from 'ant-design-vue'; |
|||
|
|||
import type { PropType } from 'vue'; |
|||
|
|||
import type { CropendResult, Cropper } from './types'; |
|||
|
|||
import { h, ref } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { useNamespace } from '@vben/hooks'; |
|||
import { createIconifyIcon } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
import { isFunction } from '@vben/utils'; |
|||
|
|||
import { dataURLtoBlob } from '@abp/core'; |
|||
import { Avatar, Button, Space, Tooltip, Upload } from 'ant-design-vue'; |
|||
|
|||
import CropperImage from './Cropper.vue'; |
|||
|
|||
type ApiFunParams = { file: Blob; fileName: string; name: string }; |
|||
|
|||
const props = defineProps({ |
|||
circled: { type: Boolean, default: true }, |
|||
uploadApi: { |
|||
type: Function as PropType<(params: ApiFunParams) => Promise<any>>, |
|||
default: undefined, |
|||
}, |
|||
src: { type: String, default: '' }, |
|||
}); |
|||
|
|||
const emits = defineEmits<{ |
|||
(event: 'uploadSuccess', url: string): void; |
|||
}>(); |
|||
|
|||
const UploadIcon = createIconifyIcon('ant-design:upload-outlined'); |
|||
const ResetIcon = createIconifyIcon('ant-design:reload-outlined'); |
|||
const RotateLeftIcon = createIconifyIcon('ant-design:rotate-left-outlined'); |
|||
const RotateRightIcon = createIconifyIcon('ant-design:rotate-right-outlined'); |
|||
const ScaleXIcon = createIconifyIcon('vaadin:arrows-long-h'); |
|||
const ScaleYIcon = createIconifyIcon('vaadin:arrows-long-v'); |
|||
const ZoomInIcon = createIconifyIcon('ant-design:zoom-in-outlined'); |
|||
const ZoomOutIcon = createIconifyIcon('ant-design:zoom-out-outlined'); |
|||
|
|||
let fileName = ''; |
|||
const src = ref(props.src || ''); |
|||
const previewSource = ref(''); |
|||
const fileList = ref<UploadProps['fileList']>([]); |
|||
const cropper = ref<Cropper>(); |
|||
let scaleX = 1; |
|||
let scaleY = 1; |
|||
|
|||
const { b, e } = useNamespace('cropper-am'); |
|||
const [Modal, modalApi] = useVbenModal({ |
|||
class: 'w-[800px]', |
|||
fullscreen: false, |
|||
fullscreenButton: false, |
|||
confirmText: $t('cropper.confirmText'), |
|||
onConfirm: handleOk, |
|||
title: $t('cropper.title'), |
|||
}); |
|||
function handleBeforeUpload(file: File) { |
|||
const reader = new FileReader(); |
|||
reader.readAsDataURL(file); |
|||
src.value = ''; |
|||
previewSource.value = ''; |
|||
reader.addEventListener('load', (e) => { |
|||
src.value = (e.target?.result as string) ?? ''; |
|||
fileName = file.name; |
|||
}); |
|||
return false; |
|||
} |
|||
function handleCropend({ imgBase64 }: CropendResult) { |
|||
previewSource.value = imgBase64; |
|||
} |
|||
|
|||
function handleReady(cropperInstance: Cropper) { |
|||
cropper.value = cropperInstance; |
|||
} |
|||
function handlerToolbar(event: string, arg?: number) { |
|||
if (!cropper.value) { |
|||
return; |
|||
} |
|||
if (event === 'scaleX') { |
|||
scaleX = arg = scaleX === -1 ? 1 : -1; |
|||
} |
|||
if (event === 'scaleY') { |
|||
scaleY = arg = scaleY === -1 ? 1 : -1; |
|||
} |
|||
switch (event) { |
|||
case 'reset': { |
|||
return cropper.value.reset(); |
|||
} |
|||
case 'rotate': { |
|||
return cropper.value.rotate(arg!); |
|||
} |
|||
case 'scaleX': { |
|||
return cropper.value.scaleX(scaleX); |
|||
} |
|||
case 'scaleY': { |
|||
return cropper.value.scaleY(scaleY); |
|||
} |
|||
case 'zoom': { |
|||
return cropper.value.zoom(arg!); |
|||
} |
|||
} |
|||
} |
|||
async function handleOk() { |
|||
const uploadApi = props.uploadApi; |
|||
if (uploadApi && isFunction(uploadApi)) { |
|||
const blob = dataURLtoBlob(previewSource.value); |
|||
try { |
|||
modalApi.setState({ submitting: true }); |
|||
await uploadApi({ name: 'file', file: blob, fileName }); |
|||
emits('uploadSuccess', previewSource.value); |
|||
modalApi.close(); |
|||
} finally { |
|||
modalApi.setState({ submitting: false }); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal> |
|||
<div :class="b()"> |
|||
<div :class="e('left')"> |
|||
<div :class="e('cropper')"> |
|||
<CropperImage |
|||
v-if="src" |
|||
:src="src" |
|||
height="300px" |
|||
:circled="circled" |
|||
@cropend="handleCropend" |
|||
@ready="handleReady" |
|||
/> |
|||
</div> |
|||
<div :class="e('toolbar')"> |
|||
<Upload |
|||
:file-list="fileList" |
|||
accept="image/*" |
|||
:before-upload="handleBeforeUpload" |
|||
> |
|||
<Tooltip :title="$t('cropper.selectImage')" placement="bottom"> |
|||
<Button size="small" :icon="h(UploadIcon)" type="primary" /> |
|||
</Tooltip> |
|||
</Upload> |
|||
<Space> |
|||
<Tooltip :title="$t('cropper.btn_reset')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ResetIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('reset')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_rotate_left')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(RotateLeftIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', -45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_rotate_right')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(RotateRightIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', 45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_scale_x')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ScaleXIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleX')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_scale_y')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ScaleYIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleY')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_zoom_in')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ZoomInIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', 0.1)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_zoom_out')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ZoomOutIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', -0.1)" |
|||
/> |
|||
</Tooltip> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<div :class="e('right')"> |
|||
<div :class="e(`preview`)"> |
|||
<img |
|||
:src="previewSource" |
|||
v-if="previewSource" |
|||
:alt="$t('cropper.preview')" |
|||
/> |
|||
</div> |
|||
<template v-if="previewSource"> |
|||
<div :class="e(`group`)"> |
|||
<Avatar :src="previewSource" size="large" /> |
|||
<Avatar :src="previewSource" :size="48" /> |
|||
<Avatar :src="previewSource" :size="64" /> |
|||
<Avatar :src="previewSource" :size="80" /> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
$namespace: vben; |
|||
|
|||
.#{$namespace}-cropper-am { |
|||
display: flex; |
|||
|
|||
&__left, |
|||
&__right { |
|||
height: 340px; |
|||
} |
|||
|
|||
&__left { |
|||
width: 55%; |
|||
} |
|||
|
|||
&__right { |
|||
width: 45%; |
|||
} |
|||
|
|||
&__cropper { |
|||
height: 300px; |
|||
background: #eee; |
|||
background-image: |
|||
linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
), |
|||
linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
); |
|||
background-position: |
|||
0 0, |
|||
12px 12px; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
&__toolbar { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
&__preview { |
|||
width: 220px; |
|||
height: 220px; |
|||
margin: 0 auto; |
|||
overflow: hidden; |
|||
border: var(--border); |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
&__group { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
padding-top: 8px; |
|||
margin-top: 8px; |
|||
border-top: var(--border); |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,2 @@ |
|||
export { default as CropperAvatar } from './CropperAvatar.vue'; |
|||
export { default as CropperModal } from './CropperModal.vue'; |
|||
@ -0,0 +1,8 @@ |
|||
import type Cropper from 'cropperjs'; |
|||
|
|||
export interface CropendResult { |
|||
imgBase64: string; |
|||
imgInfo: Cropper.Data; |
|||
} |
|||
|
|||
export type { Cropper }; |
|||
@ -0,0 +1,20 @@ |
|||
import type { SupportedLanguagesType } from '@vben/locales'; |
|||
|
|||
import { loadLocalesMapFromDir } from '@vben/locales'; |
|||
|
|||
const modules = import.meta.glob('./langs/**/*.json'); |
|||
|
|||
const localesMap = loadLocalesMapFromDir( |
|||
/\.\/langs\/([^/]+)\/(.*)\.json$/, |
|||
modules, |
|||
); |
|||
|
|||
/** |
|||
* 加载自定义组件本地化资源 |
|||
* @param lang 当前语言 |
|||
* @returns 资源集合 |
|||
*/ |
|||
export async function loadComponentMessages(lang: SupportedLanguagesType) { |
|||
const locales = localesMap[lang]?.(); |
|||
return locales; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"confirmText": "Confirm and upload", |
|||
"title": "Avatar upload", |
|||
"selectImage": "Select Image", |
|||
"btn_rotate_left": "Counterclockwise rotation", |
|||
"btn_rotate_right": "Clockwise rotation", |
|||
"btn_scale_x": "Flip horizontal", |
|||
"btn_scale_y": "Flip vertical", |
|||
"btn_zoom_in": "Zoom in", |
|||
"btn_zoom_out": "Zoom out", |
|||
"btn_reset": "Reset", |
|||
"preview": "Preivew", |
|||
"uploadSuccess": "Uploaded success!" |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"confirmText": "确认并上传", |
|||
"title": "头像上传", |
|||
"selectImage": "选择图片", |
|||
"btn_rotate_left": "逆时针旋转", |
|||
"btn_rotate_right": "顺时针旋转", |
|||
"btn_scale_x": "水平翻转", |
|||
"btn_scale_y": "垂直翻转", |
|||
"btn_zoom_in": "放大", |
|||
"btn_zoom_out": "缩小", |
|||
"btn_reset": "重置", |
|||
"preview": "预览", |
|||
"uploadSuccess": "上传成功!" |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ |
|||
/** |
|||
* @description: base64 to blob |
|||
*/ |
|||
export function dataURLtoBlob(base64Buf: string): Blob { |
|||
const arr = base64Buf.split(','); |
|||
const typeItem = arr[0]; |
|||
const mime = typeItem?.match(/:(.*?);/)?.[1]; |
|||
const bstr = window.atob(arr[1]!); |
|||
let n = bstr.length; |
|||
const u8arr = new Uint8Array(n); |
|||
while (n--) { |
|||
u8arr[n] = bstr.codePointAt(n)!; |
|||
} |
|||
return new Blob([u8arr], { type: mime }); |
|||
} |
|||
|
|||
/** |
|||
* img url to base64 |
|||
* @param url |
|||
*/ |
|||
export function urlToBase64(url: string, mineType?: string): Promise<string> { |
|||
return new Promise((resolve, reject) => { |
|||
let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null; |
|||
const ctx = canvas!.getContext('2d'); |
|||
|
|||
const img = new Image(); |
|||
img.crossOrigin = ''; |
|||
img.addEventListener('load', () => { |
|||
if (!canvas || !ctx) { |
|||
return reject(new Error('canvas or ctx is null!')); |
|||
} |
|||
canvas.height = img.height; |
|||
canvas.width = img.width; |
|||
ctx.drawImage(img, 0, 0); |
|||
const dataURL = canvas.toDataURL(mineType || 'image/png'); |
|||
canvas = null; |
|||
resolve(dataURL); |
|||
}); |
|||
img.src = url; |
|||
}); |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
namespace LINGYUN.Abp.BlobStoring.Tencent; |
|||
internal static class BlobStoringTencentConsts |
|||
{ |
|||
public const string HttpClient = "BlobStoring.Tencent"; |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using LINGYUN.Abp.BlobStoring.Tencent; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
internal static class BlobStoringTencentHttpClientFactoryServiceCollectionExtensions |
|||
{ |
|||
public static IServiceCollection AddTenantOssClient(this IServiceCollection services) |
|||
{ |
|||
services.AddHttpClient(BlobStoringTencentConsts.HttpClient); |
|||
|
|||
return services; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using LINGYUN.Abp.BlobStoring.Tencent; |
|||
|
|||
namespace System.Net.Http; |
|||
public static class BlobStoringTencentHttpClientFactoryExtenssions |
|||
{ |
|||
public static HttpClient CreateTenantOssClient( |
|||
this IHttpClientFactory httpClientFactory) |
|||
{ |
|||
return httpClientFactory.CreateClient(BlobStoringTencentConsts.HttpClient); ; |
|||
} |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
using DotNetCore.CAP; |
|||
using DotNetCore.CAP.Internal; |
|||
using DotNetCore.CAP.Persistence; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.EventBus.CAP; |
|||
|
|||
public class AbpCAPBootstrapper : IBootstrapper |
|||
{ |
|||
private readonly ILogger<IBootstrapper> _logger; |
|||
private readonly IServiceProvider _serviceProvider; |
|||
|
|||
private CancellationTokenSource _cts; |
|||
private bool _disposed; |
|||
private IEnumerable<IProcessingServer> _processors = default!; |
|||
|
|||
public bool IsStarted => !_cts?.IsCancellationRequested ?? false; |
|||
|
|||
public AbpCAPBootstrapper(IServiceProvider serviceProvider, ILogger<IBootstrapper> logger) |
|||
{ |
|||
_serviceProvider = serviceProvider; |
|||
_logger = logger; |
|||
} |
|||
|
|||
public async Task BootstrapAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
if (_cts != null) |
|||
{ |
|||
_logger.LogInformation("### CAP background task is already started!"); |
|||
|
|||
return; |
|||
} |
|||
|
|||
_logger.LogDebug("### CAP background task is starting."); |
|||
|
|||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); |
|||
|
|||
CheckRequirement(); |
|||
|
|||
_processors = _serviceProvider.GetServices<IProcessingServer>(); |
|||
|
|||
try |
|||
{ |
|||
await _serviceProvider.GetRequiredService<IStorageInitializer>().InitializeAsync(_cts.Token).ConfigureAwait(false); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
if (e is InvalidOperationException) throw; |
|||
_logger.LogError(e, "Initializing the storage structure failed!"); |
|||
} |
|||
|
|||
_cts.Token.Register(() => |
|||
{ |
|||
_logger.LogDebug("### CAP background task is stopping."); |
|||
|
|||
|
|||
foreach (var item in _processors) |
|||
try |
|||
{ |
|||
item.Dispose(); |
|||
} |
|||
catch (OperationCanceledException ex) |
|||
{ |
|||
_logger.LogWarning(ex, $"Expected an OperationCanceledException, but found '{ex.Message}'."); |
|||
} |
|||
}); |
|||
|
|||
await BootstrapCoreAsync().ConfigureAwait(false); |
|||
|
|||
_disposed = false; |
|||
_logger.LogInformation("### CAP started!"); |
|||
} |
|||
|
|||
protected virtual async Task BootstrapCoreAsync() |
|||
{ |
|||
foreach (var item in _processors) |
|||
{ |
|||
try |
|||
{ |
|||
_cts!.Token.ThrowIfCancellationRequested(); |
|||
|
|||
await item.Start(_cts!.Token); |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
// ignore
|
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "Starting the processors throw an exception."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public virtual void Dispose() |
|||
{ |
|||
if (_disposed) return; |
|||
|
|||
_cts?.Cancel(); |
|||
_cts?.Dispose(); |
|||
_cts = null; |
|||
_disposed = true; |
|||
} |
|||
|
|||
public virtual async Task ExecuteAsync(CancellationToken stoppingToken) |
|||
{ |
|||
await BootstrapAsync(stoppingToken).ConfigureAwait(false); |
|||
} |
|||
|
|||
public virtual Task StopAsync(CancellationToken cancellationToken) |
|||
{ |
|||
_cts?.Cancel(); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
private void CheckRequirement() |
|||
{ |
|||
var marker = _serviceProvider.GetService<CapMarkerService>(); |
|||
if (marker == null) |
|||
throw new InvalidOperationException( |
|||
"AddCap() must be added on the service collection. eg: services.AddCap(...)"); |
|||
|
|||
var messageQueueMarker = _serviceProvider.GetService<CapMessageQueueMakerService>(); |
|||
if (messageQueueMarker == null) |
|||
throw new InvalidOperationException( |
|||
"You must be config transport provider for CAP!" + Environment.NewLine + |
|||
"==================================================================================" + |
|||
Environment.NewLine + |
|||
"======== eg: services.AddCap( options => { options.UseRabbitMQ(...) }); ========" + |
|||
Environment.NewLine + |
|||
"=================================================================================="); |
|||
|
|||
var databaseMarker = _serviceProvider.GetService<CapStorageMarkerService>(); |
|||
if (databaseMarker == null) |
|||
throw new InvalidOperationException( |
|||
"You must be config storage provider for CAP!" + Environment.NewLine + |
|||
"===================================================================================" + |
|||
Environment.NewLine + |
|||
"======== eg: services.AddCap( options => { options.UseSqlServer(...) }); ========" + |
|||
Environment.NewLine + |
|||
"==================================================================================="); |
|||
} |
|||
|
|||
public ValueTask DisposeAsync() |
|||
{ |
|||
Dispose(); |
|||
|
|||
return ValueTask.CompletedTask; |
|||
} |
|||
} |
|||
@ -1,397 +0,0 @@ |
|||
<?xml version="1.0"?> |
|||
<doc> |
|||
<assembly> |
|||
<name>LINGYUN.Abp.EventBus.CAP</name> |
|||
</assembly> |
|||
<members> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.AbpCAPConsumerServiceSelector"> |
|||
<summary> |
|||
消费者查找器 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.AbpCAPConsumerServiceSelector.CapOptions"> |
|||
<summary> |
|||
CAP配置 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.AbpCAPConsumerServiceSelector.AbpDistributedEventBusOptions"> |
|||
<summary> |
|||
Abp分布式事件配置 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.AbpCAPConsumerServiceSelector.ServiceProvider"> |
|||
<summary> |
|||
服务提供者 |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPConsumerServiceSelector.#ctor(System.IServiceProvider,Microsoft.Extensions.Options.IOptions{DotNetCore.CAP.CapOptions},Microsoft.Extensions.Options.IOptions{Volo.Abp.EventBus.Distributed.AbpDistributedEventBusOptions})"> |
|||
<summary> |
|||
Creates a new <see cref="T:DotNetCore.CAP.Internal.ConsumerServiceSelector" />. |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPConsumerServiceSelector.FindConsumersFromInterfaceTypes(System.IServiceProvider)"> |
|||
<summary> |
|||
查找消费者集合 |
|||
</summary> |
|||
<param name="provider"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPConsumerServiceSelector.GetHandlerDescription(System.Type,System.Type)"> |
|||
<summary> |
|||
获取事件处理器集合 |
|||
</summary> |
|||
<param name="eventType"></param> |
|||
<param name="typeInfo"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.AbpCAPEventBusModule"> |
|||
<summary> |
|||
AbpCAPEventBusModule |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPEventBusModule.ConfigureServices(Volo.Abp.Modularity.ServiceConfigurationContext)"> |
|||
<summary> |
|||
ConfigureServices |
|||
</summary> |
|||
<param name="context"></param> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.AbpCAPEventBusOptions"> |
|||
<summary> |
|||
过期消息清理配置项 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.AbpCAPEventBusOptions.NotifyFailedCallback"> |
|||
<summary> |
|||
发布消息处理失败通知 |
|||
default: false |
|||
</summary> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.AbpCAPExecutionFailedException"> |
|||
<summary> |
|||
AbpECAPExecutionFailedException |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.AbpCAPExecutionFailedException.MessageType"> |
|||
<summary> |
|||
MessageType |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.AbpCAPExecutionFailedException.Origin"> |
|||
<summary> |
|||
Message |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPExecutionFailedException.#ctor(DotNetCore.CAP.Messages.MessageType,DotNetCore.CAP.Messages.Message)"> |
|||
<summary> |
|||
constructor |
|||
</summary> |
|||
<param name="messageType"></param> |
|||
<param name="origin"></param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPExecutionFailedException.#ctor(DotNetCore.CAP.Messages.MessageType,DotNetCore.CAP.Messages.Message,System.String)"> |
|||
<summary> |
|||
constructor |
|||
</summary> |
|||
<param name="messageType"></param> |
|||
<param name="origin"></param> |
|||
<param name="message"></param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPExecutionFailedException.#ctor(DotNetCore.CAP.Messages.MessageType,DotNetCore.CAP.Messages.Message,System.String,System.Exception)"> |
|||
<summary> |
|||
constructor |
|||
</summary> |
|||
<param name="messageType"></param> |
|||
<param name="origin"></param> |
|||
<param name="message"></param> |
|||
<param name="innerException"></param> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.AbpCAPMessageExtensions"> |
|||
<summary> |
|||
CAP消息扩展 |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPMessageExtensions.TryGetTenantId(DotNetCore.CAP.Messages.Message,System.Nullable{System.Guid}@)"> |
|||
<summary> |
|||
尝试获取消息标头中的租户标识 |
|||
</summary> |
|||
<param name="message"></param> |
|||
<param name="tenantId"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPMessageExtensions.GetTenantIdOrNull(DotNetCore.CAP.Messages.Message)"> |
|||
<summary> |
|||
获取消息标头中的租户标识 |
|||
</summary> |
|||
<param name="message"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPMessageExtensions.TryGetCorrelationId(DotNetCore.CAP.Messages.Message,System.String@)"> |
|||
<summary> |
|||
尝试获取消息标头中的链路标识 |
|||
</summary> |
|||
<param name="message"></param> |
|||
<param name="correlationId"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPMessageExtensions.GetCorrelationIdOrNull(DotNetCore.CAP.Messages.Message)"> |
|||
<summary> |
|||
获取消息标头中的链路标识 |
|||
</summary> |
|||
<param name="message"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.AbpCAPSubscribeInvoker"> |
|||
<summary> |
|||
重写 ISubscribeInvoker 实现 Abp 租户集成 |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPSubscribeInvoker.#ctor(Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider,Volo.Abp.Tracing.ICorrelationIdProvider,DotNetCore.CAP.Serialization.ISerializer,Volo.Abp.MultiTenancy.ICurrentTenant)"> |
|||
<summary> |
|||
AbpCAPSubscribeInvoker |
|||
</summary> |
|||
<param name="loggerFactory"></param> |
|||
<param name="serviceProvider"></param> |
|||
<param name="correlationIdProvider"></param> |
|||
<param name="serializer"></param> |
|||
<param name="currentTenant"></param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPSubscribeInvoker.InvokeAsync(DotNetCore.CAP.Internal.ConsumerContext,System.Threading.CancellationToken)"> |
|||
<summary> |
|||
调用订阅者方法 |
|||
</summary> |
|||
<param name="context"></param> |
|||
<param name="cancellationToken"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPSubscribeInvoker.GetCapProvidedParameter(DotNetCore.CAP.Internal.ParameterDescriptor,DotNetCore.CAP.Messages.Message,System.Threading.CancellationToken)"> |
|||
<summary> |
|||
|
|||
</summary> |
|||
<param name="parameterDescriptor"></param> |
|||
<param name="message"></param> |
|||
<param name="cancellationToken"></param> |
|||
<returns></returns> |
|||
<exception cref="T:System.ArgumentException"></exception> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPSubscribeInvoker.GetInstance(System.IServiceProvider,DotNetCore.CAP.Internal.ConsumerContext)"> |
|||
<summary> |
|||
获取事件处理类实例 |
|||
</summary> |
|||
<param name="provider"></param> |
|||
<param name="context"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.AbpCAPSubscribeInvoker.ExecuteWithParameterAsync(LINGYUN.Abp.EventBus.CAP.Internal.ObjectMethodExecutor,System.Object,System.Object[])"> |
|||
<summary> |
|||
通过给定的类型实例与参数调用订阅者方法 |
|||
</summary> |
|||
<param name="executor"></param> |
|||
<param name="class"></param> |
|||
<param name="parameter"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus"> |
|||
<summary> |
|||
CAP分布式事件总线 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.CapPublisher"> |
|||
<summary> |
|||
CAP消息发布接口 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.CustomDistributedEventSubscriber"> |
|||
<summary> |
|||
自定义事件注册接口 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.HandlerFactories"> |
|||
<summary> |
|||
本地事件处理器工厂对象集合 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.EventTypes"> |
|||
<summary> |
|||
本地事件集合 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.CurrentUser"> |
|||
<summary> |
|||
当前用户 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.CurrentClient"> |
|||
<summary> |
|||
当前客户端 |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.JsonSerializer"> |
|||
<summary> |
|||
typeof <see cref="T:Volo.Abp.Json.IJsonSerializer"/> |
|||
</summary> |
|||
</member> |
|||
<member name="P:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.CancellationTokenProvider"> |
|||
<summary> |
|||
取消令牌 |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.#ctor(Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,Microsoft.Extensions.Options.IOptions{Volo.Abp.EventBus.Distributed.AbpDistributedEventBusOptions},DotNetCore.CAP.ICapPublisher,Volo.Abp.Users.ICurrentUser,Volo.Abp.Clients.ICurrentClient,Volo.Abp.MultiTenancy.ICurrentTenant,Volo.Abp.Json.IJsonSerializer,Volo.Abp.Uow.IUnitOfWorkManager,Volo.Abp.Guids.IGuidGenerator,Volo.Abp.Timing.IClock,Volo.Abp.Threading.ICancellationTokenProvider,LINGYUN.Abp.EventBus.CAP.ICustomDistributedEventSubscriber,Volo.Abp.EventBus.IEventHandlerInvoker,Volo.Abp.EventBus.Local.ILocalEventBus,Volo.Abp.Tracing.ICorrelationIdProvider)"> |
|||
<summary> |
|||
constructor |
|||
</summary> |
|||
<param name="serviceScopeFactory"></param> |
|||
<param name="distributedEventBusOptions"></param> |
|||
<param name="capPublisher"></param> |
|||
<param name="currentUser"></param> |
|||
<param name="currentClient"></param> |
|||
<param name="currentTenant"></param> |
|||
<param name="jsonSerializer"></param> |
|||
<param name="unitOfWorkManager"></param> |
|||
<param name="cancellationTokenProvider"></param> |
|||
<param name="guidGenerator"></param> |
|||
<param name="clock"></param> |
|||
<param name="customDistributedEventSubscriber"></param> |
|||
<param name="eventHandlerInvoker"></param> |
|||
<param name="localEventBus"></param> |
|||
<param name="correlationIdProvider"></param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.Subscribe(System.Type,Volo.Abp.EventBus.IEventHandlerFactory)"> |
|||
<summary> |
|||
订阅事件 |
|||
</summary> |
|||
<param name="eventType"></param> |
|||
<param name="factory"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.Unsubscribe``1(System.Func{``0,System.Threading.Tasks.Task})"> |
|||
<summary> |
|||
退订事件 |
|||
</summary> |
|||
<typeparam name="TEvent">事件类型</typeparam> |
|||
<param name="action"></param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.Unsubscribe(System.Type,Volo.Abp.EventBus.IEventHandler)"> |
|||
<summary> |
|||
退订事件 |
|||
</summary> |
|||
<param name="eventType">事件类型</param> |
|||
<param name="handler">事件处理器</param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.Unsubscribe(System.Type,Volo.Abp.EventBus.IEventHandlerFactory)"> |
|||
<summary> |
|||
退订事件 |
|||
</summary> |
|||
<param name="eventType">事件类型</param> |
|||
<param name="factory">事件处理器工厂</param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.UnsubscribeAll(System.Type)"> |
|||
<summary> |
|||
退订所有事件 |
|||
</summary> |
|||
<param name="eventType">事件类型</param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.PublishToEventBusAsync(System.Type,System.Object)"> |
|||
<summary> |
|||
发布事件 |
|||
</summary> |
|||
<param name="eventType">事件类型</param> |
|||
<param name="eventData">事件数据对象</param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.CAPDistributedEventBus.GetHandlerFactories(System.Type)"> |
|||
<summary> |
|||
获取事件处理器工厂列表 |
|||
</summary> |
|||
<param name="eventType"></param> |
|||
<returns></returns> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.ICustomDistributedEventSubscriber"> |
|||
<summary> |
|||
自定义事件订阅者 |
|||
</summary> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.ICustomDistributedEventSubscriber.Subscribe(System.Type,Volo.Abp.EventBus.IEventHandlerFactory)"> |
|||
<summary> |
|||
订阅事件 |
|||
</summary> |
|||
<param name="eventType"></param> |
|||
<param name="factory"></param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.ICustomDistributedEventSubscriber.UnSubscribe(System.Type,Volo.Abp.EventBus.IEventHandlerFactory)"> |
|||
<summary> |
|||
取消订阅 |
|||
</summary> |
|||
<param name="eventType"></param> |
|||
<param name="factory"></param> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.Internal.ObjectMethodExecutor.Execute(System.Object,System.Object[])"> |
|||
<summary> |
|||
Executes the configured method on <paramref name="target" />. This can be used whether or not |
|||
the configured method is asynchronous. |
|||
</summary> |
|||
<remarks> |
|||
Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than |
|||
ExecuteAsync if you know at compile time what the return type is, because then you can directly |
|||
"await" that value (via a cast), and then the generated code will be able to reference the |
|||
resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated |
|||
code will have to treat the resulting awaitable as a boxed object, because it doesn't know at |
|||
compile time what type it would be. |
|||
</remarks> |
|||
<param name="target">The object whose method is to be executed.</param> |
|||
<param name="parameters">Parameters to pass to the method.</param> |
|||
<returns>The method return value.</returns> |
|||
</member> |
|||
<member name="M:LINGYUN.Abp.EventBus.CAP.Internal.ObjectMethodExecutor.ExecuteAsync(System.Object,System.Object[])"> |
|||
<summary> |
|||
Executes the configured method on <paramref name="target" />. This can only be used if the configured |
|||
method is asynchronous. |
|||
</summary> |
|||
<remarks> |
|||
If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync, |
|||
which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations |
|||
as compared with using Execute and then using "await" on the result value typecasted to the known |
|||
awaitable type. The possible extra heap allocations are for: |
|||
1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally |
|||
it's a reference type, and you normally create a new instance per call). |
|||
2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance |
|||
of it, and if it is, it will have to be boxed so the calling code can reference it as an object). |
|||
3. The async result value, if it's a value type (it has to be boxed as an object, since the calling |
|||
code doesn't know what type it's going to be). |
|||
</remarks> |
|||
<param name="target">The object whose method is to be executed.</param> |
|||
<param name="parameters">Parameters to pass to the method.</param> |
|||
<returns>An object that you can "await" to get the method return value.</returns> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.Internal.ObjectMethodExecutorAwaitable"> |
|||
<summary> |
|||
Provides a common awaitable structure that <see cref="M:LINGYUN.Abp.EventBus.CAP.Internal.ObjectMethodExecutor.ExecuteAsync(System.Object,System.Object[])" /> can |
|||
return, regardless of whether the underlying value is a System.Task, an FSharpAsync, or an |
|||
application-defined custom awaitable. |
|||
</summary> |
|||
</member> |
|||
<member name="T:LINGYUN.Abp.EventBus.CAP.Internal.ObjectMethodExecutorFSharpSupport"> |
|||
<summary> |
|||
Helper for detecting whether a given type is FSharpAsync`1, and if so, supplying |
|||
an <see cref="T:System.Linq.Expressions.Expression" /> for mapping instances of that type to a C# awaitable. |
|||
</summary> |
|||
<remarks> |
|||
The main design goal here is to avoid taking a compile-time dependency on |
|||
FSharp.Core.dll, because non-F# applications wouldn't use it. So all the references |
|||
to FSharp types have to be constructed dynamically at runtime. |
|||
</remarks> |
|||
</member> |
|||
<member name="T:Microsoft.Extensions.DependencyInjection.ServiceCollectionExtensions"> |
|||
<summary> |
|||
CAP ServiceCollectionExtensions |
|||
</summary> |
|||
</member> |
|||
<member name="M:Microsoft.Extensions.DependencyInjection.ServiceCollectionExtensions.AddCAPEventBus(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{DotNetCore.CAP.CapOptions})"> |
|||
<summary> |
|||
Adds and configures the consistence services for the consistency. |
|||
</summary> |
|||
<param name="services"></param> |
|||
<param name="capAction"></param> |
|||
<returns></returns> |
|||
</member> |
|||
</members> |
|||
</doc> |
|||
@ -0,0 +1,40 @@ |
|||
namespace LINGYUN.Abp.Sms.Aliyun; |
|||
public class AliyunSmsVerifyCodeResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 请求状态码, OK代表请求成功
|
|||
/// </summary>
|
|||
public string Code { get; set; } |
|||
/// <summary>
|
|||
/// 状态码的描述
|
|||
/// </summary>
|
|||
public string Message { get; set; } |
|||
/// <summary>
|
|||
/// 请求是否成功
|
|||
/// </summary>
|
|||
public bool Success { get; set; } |
|||
/// <summary>
|
|||
/// 请求结果数据
|
|||
/// </summary>
|
|||
public AliyunSmsVerifyCodeModel Model { get; set; } |
|||
} |
|||
|
|||
public class AliyunSmsVerifyCodeModel |
|||
{ |
|||
/// <summary>
|
|||
/// 请求Id
|
|||
/// </summary>
|
|||
public string RequestId { get; set; } |
|||
/// <summary>
|
|||
/// 业务Id
|
|||
/// </summary>
|
|||
public string BizId { get; set; } |
|||
/// <summary>
|
|||
/// 外部流水号
|
|||
/// </summary>
|
|||
public string OutId { get; set; } |
|||
/// <summary>
|
|||
/// 验证码, 仅当使用阿里云短信验证服务生成验证码时携带
|
|||
/// </summary>
|
|||
public string VerifyCode { get; set; } |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Sms.Aliyun; |
|||
/// <summary>
|
|||
/// 阿里云发送短信验证码接口
|
|||
/// </summary>
|
|||
public interface IAliyunSmsVerifyCodeSender |
|||
{ |
|||
/// <summary>
|
|||
/// 发送短信验证码
|
|||
/// </summary>
|
|||
/// <param name="message"></param>
|
|||
/// <returns></returns>
|
|||
Task SendAsync(SmsVerifyCodeMessage message); |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
namespace LINGYUN.Abp.Sms.Aliyun; |
|||
public class SmsVerifyCodeMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 方案名称,如果不填则为“默认方案”。最多不超过 20 个字符。
|
|||
/// </summary>
|
|||
public string SchemeName { get; set; } |
|||
/// <summary>
|
|||
/// 号码国家编码。默认为 86,目前也仅支持中国国内号码发送。
|
|||
/// </summary>
|
|||
public string CountryCode { get; set; } |
|||
/// <summary>
|
|||
/// 上行短信扩展码。上行短信指发送给通信服务提供商的短信,用于定制某种服务、完成查询,或是办理某种业务等,需要收费,按运营商普通短信资费进行扣费。
|
|||
/// </summary>
|
|||
public string SmsUpExtendCode { get; set; } |
|||
/// <summary>
|
|||
/// 外部流水号。
|
|||
/// </summary>
|
|||
public string OutId { get; set; } |
|||
/// <summary>
|
|||
/// 验证码长度支持 4~8 位长度,默认是 4 位。
|
|||
/// </summary>
|
|||
public long? CodeLength { get; set; } |
|||
/// <summary>
|
|||
/// 验证码有效时长,单位秒,默认为 300 秒。
|
|||
/// </summary>
|
|||
public long? ValidTime { get; set; } |
|||
/// <summary>
|
|||
/// 核验规则,当有效时间内对同场景内的同号码重复发送验证码时,旧验证码如何处理。
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 1 - 覆盖处理(默认),即旧验证码会失效掉。<br />
|
|||
/// 2 - 保留,即多个验证码都是在有效期内都可以校验通过。
|
|||
/// </remarks>
|
|||
public long? DuplicatePolicy { get; set; } |
|||
/// <summary>
|
|||
/// 时间间隔,单位:秒。即多久间隔可以发送一次验证码,用于频控,默认 60 秒。
|
|||
/// </summary>
|
|||
public long? Interval { get; set; } |
|||
/// <summary>
|
|||
/// 生成的验证码类型。当参数 TemplateParam 传入占位符时,此参数必填,将由系统根据指定的规则生成验证码。
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 1 - 纯数字(默认)。
|
|||
/// 2 - 纯大写字母。
|
|||
/// 3 - 纯小写字母。
|
|||
/// 4 - 大小字母混合。
|
|||
/// 5 - 数字+大写字母混合。
|
|||
/// 6 - 数字+小写字母混合。
|
|||
/// 7 - 数字+大小写字母混合。
|
|||
/// </remarks>
|
|||
public long? CodeType { get; set; } |
|||
/// <summary>
|
|||
/// 是否返回验证码。
|
|||
/// </summary>
|
|||
public bool? ReturnVerifyCode { get; set; } |
|||
/// <summary>
|
|||
/// 是否自动替换签名重试(默认开启。
|
|||
/// </summary>
|
|||
public bool? AutoRetry { get; set; } |
|||
/// <summary>
|
|||
/// 短信接收方手机号。
|
|||
/// </summary>
|
|||
public string PhoneNumber { get; } |
|||
/// <summary>
|
|||
/// 签名名称。暂不支持使用自定义签名,请使用系统赠送的签名。
|
|||
/// </summary>
|
|||
public string SignName { get; } |
|||
/// <summary>
|
|||
/// 短信模板 CODE。参数SignName选择赠送签名时,必须搭配赠送模板下发短信。您可在赠送模板配置页面选择适用您业务场景的模板。
|
|||
/// </summary>
|
|||
public string TemplateCode { get; } |
|||
/// <summary>
|
|||
/// 短信模板参数。
|
|||
/// </summary>
|
|||
public SmsVerifyCodeMessageParam TemplateParam { get; } |
|||
public SmsVerifyCodeMessage( |
|||
string phoneNumber, |
|||
SmsVerifyCodeMessageParam templateParam, |
|||
string signName = null, |
|||
string templateCode = null) |
|||
{ |
|||
PhoneNumber = phoneNumber; |
|||
TemplateParam = templateParam; |
|||
SignName = signName; |
|||
TemplateCode = templateCode; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
namespace LINGYUN.Abp.Sms.Aliyun; |
|||
public class SmsVerifyCodeMessageParam |
|||
{ |
|||
public string Code { get; } |
|||
public string Min { get; } |
|||
public SmsVerifyCodeMessageParam(string code, string min = "5") |
|||
{ |
|||
Code = code; |
|||
Min = min; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,21 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net9.0</TargetFramework> |
|||
<AssemblyName>LINGYUN.Abp.AspNetCore.MultiTenancy</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.AspNetCore.MultiTenancy</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<OutputType>Library</OutputType> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.MultiTenancy" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Modularity; |
|||
using VoloAbpAspNetCoreMultiTenancyModule = Volo.Abp.AspNetCore.MultiTenancy.AbpAspNetCoreMultiTenancyModule; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.MultiTenancy; |
|||
|
|||
[DependsOn(typeof(VoloAbpAspNetCoreMultiTenancyModule))] |
|||
public class AbpAspNetCoreMultiTenancyModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace LINGYUN.Abp.AspNetCore.MultiTenancy; |
|||
|
|||
public class AbpAspNetCoreMultiTenancyResolveOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 仅解析域名中的租户, 默认: true
|
|||
/// </summary>
|
|||
public bool OnlyResolveDomain { get; set; } |
|||
public AbpAspNetCoreMultiTenancyResolveOptions() |
|||
{ |
|||
OnlyResolveDomain = true; |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.MultiTenancy; |
|||
|
|||
public static class AbpMultiTenancyOptionsExtensions |
|||
{ |
|||
public static void AddOnlyDomainTenantResolver(this AbpTenantResolveOptions options, string domainFormat) |
|||
{ |
|||
options.TenantResolvers.InsertAfter( |
|||
r => r is CurrentUserTenantResolveContributor, |
|||
new OnlyDomainTenantResolveContributor(domainFormat) |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Net; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.MultiTenancy; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Text.Formatting; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.MultiTenancy; |
|||
|
|||
public class OnlyDomainTenantResolveContributor : HttpTenantResolveContributorBase |
|||
{ |
|||
public const string ContributorName = "Domain"; |
|||
|
|||
public override string Name => ContributorName; |
|||
|
|||
private static readonly string[] ProtocolPrefixes = { "http://", "https://" }; |
|||
|
|||
private readonly string _domainFormat; |
|||
|
|||
public OnlyDomainTenantResolveContributor(string domainFormat) |
|||
{ |
|||
_domainFormat = domainFormat.RemovePreFix(ProtocolPrefixes); |
|||
} |
|||
|
|||
protected override Task<string?> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext) |
|||
{ |
|||
if (!httpContext.Request.Host.HasValue) |
|||
{ |
|||
return Task.FromResult<string?>(null); |
|||
} |
|||
|
|||
var options = httpContext.RequestServices.GetRequiredService<IOptions<AbpAspNetCoreMultiTenancyResolveOptions>>(); |
|||
if (options.Value.OnlyResolveDomain) |
|||
{ |
|||
// 仅仅解析域名, 如果请求的是IP地址, 则不使用这个解析贡献者
|
|||
if (IPAddress.TryParse(httpContext.Request.Host.Host, out var _)) |
|||
{ |
|||
return Task.FromResult<string?>(null); |
|||
} |
|||
} |
|||
|
|||
var hostName = httpContext.Request.Host.Value.RemovePreFix(ProtocolPrefixes); |
|||
var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, ignoreCase: true); |
|||
|
|||
context.Handled = true; |
|||
|
|||
return Task.FromResult(extractResult.IsMatch ? extractResult.Matches[0].Value : null); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
{ |
|||
"profiles": { |
|||
"LINGYUN.Abp.AspNetCore.MultiTenancy": { |
|||
"commandName": "Project", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
}, |
|||
"applicationUrl": "https://localhost:61811;http://localhost:61812" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using LINGYUN.Abp.WeChat.Work.Authorize; |
|||
using LINGYUN.Abp.WeChat.Work.Security.Claims; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Security.Principal; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Security.Claims; |
|||
|
|||
namespace LINGYUN.Abp.Identity.WeChat.Work; |
|||
public class AbpWeChatWorkClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency |
|||
{ |
|||
public async virtual Task ContributeAsync(AbpClaimsPrincipalContributorContext context) |
|||
{ |
|||
var claimsIdentity = context.ClaimsPrincipal.Identities.First(); |
|||
if (claimsIdentity.HasClaim(x => x.Type == AbpWeChatWorkClaimTypes.UserId)) |
|||
{ |
|||
return; |
|||
} |
|||
var userId = claimsIdentity.FindUserId(); |
|||
if (userId.HasValue) |
|||
{ |
|||
var userClaimProvider = context.ServiceProvider.GetService<IWeChatWorkUserClaimProvider>(); |
|||
|
|||
var weChatWorkUserId = await userClaimProvider?.FindUserIdentifierAsync(userId.Value); |
|||
if (!weChatWorkUserId.IsNullOrWhiteSpace()) |
|||
{ |
|||
claimsIdentity.AddOrReplace(new Claim(AbpWeChatWorkClaimTypes.UserId, weChatWorkUserId)); |
|||
|
|||
context.ClaimsPrincipal.AddIdentityIfNotContains(claimsIdentity); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public class AbpWeChatMessageResolveOptions |
|||
{ |
|||
public List<IMessageResolveContributor> MessageResolvers { get; } |
|||
|
|||
public AbpWeChatMessageResolveOptions() |
|||
{ |
|||
MessageResolvers = new List<IMessageResolveContributor>(); |
|||
} |
|||
} |
|||
@ -1,7 +0,0 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public interface IMessageResolver |
|||
{ |
|||
Task<MessageResolveResult> ResolveMessageAsync(MessageResolveData messageData); |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages; |
|||
/// <summary>
|
|||
/// 微信公众号消息解析器
|
|||
/// </summary>
|
|||
public interface IWeChatOfficialMessageResolver |
|||
{ |
|||
/// <summary>
|
|||
/// 解析微信公众号消息
|
|||
/// </summary>
|
|||
/// <param name="messageData">公众号消息</param>
|
|||
/// <returns></returns>
|
|||
Task<MessageResolveResult> ResolveAsync(MessageResolveData messageData); |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Crypto; |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages; |
|||
public class WeChatOfficialMessageResolver : MessageResolverBase, IWeChatOfficialMessageResolver |
|||
{ |
|||
private readonly AbpWeChatOfficialMessageResolveOptions _options; |
|||
public WeChatOfficialMessageResolver( |
|||
IWeChatCryptoService cryptoService, |
|||
IServiceProvider serviceProvider, |
|||
IOptions<AbpWeChatOfficialMessageResolveOptions> options) |
|||
: base(cryptoService, serviceProvider) |
|||
{ |
|||
_options = options.Value; |
|||
} |
|||
|
|||
protected async override Task<MessageResolveResult> ResolveMessageAsync(MessageResolveContext context) |
|||
{ |
|||
var result = new MessageResolveResult(context.Origin); |
|||
|
|||
foreach (var messageResolver in _options.MessageResolvers) |
|||
{ |
|||
await messageResolver.ResolveAsync(context); |
|||
|
|||
result.AppliedResolvers.Add(messageResolver.Name); |
|||
|
|||
if (context.HasResolvedMessage()) |
|||
{ |
|||
result.Message = context.Message; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.Common.Messages; |
|||
/// <summary>
|
|||
/// 企业微信消息解析器
|
|||
/// </summary>
|
|||
public interface IWeChatWorkMessageResolver |
|||
{ |
|||
/// <summary>
|
|||
/// 解析企业微信消息
|
|||
/// </summary>
|
|||
/// <param name="messageData">企业微信消息</param>
|
|||
/// <returns></returns>
|
|||
Task<MessageResolveResult> ResolveAsync(MessageResolveData messageData); |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.Common.Messages.Models; |
|||
/// <summary>
|
|||
/// 会议室预定事件
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 当用户在企业微信预定会议室时,会触发该事件回调给会议室系统应用。
|
|||
/// </remarks>
|
|||
[EventName("book_meeting_room")] |
|||
public class BookMeetingRoomEvent : WeChatWorkEventMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 会议室id
|
|||
/// </summary>
|
|||
[XmlElement("MeetingRoomId")] |
|||
public int MeetingRoomId { get; set; } |
|||
/// <summary>
|
|||
/// 预定id,可根据该ID查询具体的会议预定情况
|
|||
/// </summary>
|
|||
[XmlElement("BookingId")] |
|||
public string BookingId { get; set; } |
|||
|
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatWorkEventMessageEto<BookMeetingRoomEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.Common.Messages.Models; |
|||
/// <summary>
|
|||
/// 会议室取消事件
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 当用户在企业微信取消预定会议室时,会触发该事件回调给会议室系统应用;如果该会议室由自建应用预定,除了会议室系统应用外,也会回调给对应的自建应用。
|
|||
/// </remarks>
|
|||
[EventName("cancel_meeting_room")] |
|||
public class CancelMeetingRoomEvent : WeChatWorkEventMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 会议室id
|
|||
/// </summary>
|
|||
[XmlElement("MeetingRoomId")] |
|||
public int MeetingRoomId { get; set; } |
|||
/// <summary>
|
|||
/// 预定id,可根据该ID查询具体的会议预定情况
|
|||
/// </summary>
|
|||
[XmlElement("BookingId")] |
|||
public string BookingId { get; set; } |
|||
|
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatWorkEventMessageEto<CancelMeetingRoomEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,272 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.Common.Messages.Models; |
|||
/// <summary>
|
|||
/// 企业微信“审批应用”审批状态通知事件
|
|||
/// </summary>
|
|||
[EventName("sys_approval_change")] |
|||
public class SysApprovalStatusChangeEvent : WeChatWorkEventMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 审批信息
|
|||
/// </summary>
|
|||
[XmlElement("ApprovalInfo")] |
|||
public SysApprovalInfo ApprovalInfo { get; set; } |
|||
|
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatWorkEventMessageEto<SysApprovalStatusChangeEvent>(this); |
|||
} |
|||
} |
|||
|
|||
public class SysApprovalInfo |
|||
{ |
|||
/// <summary>
|
|||
/// 审批编号(字符串类型)
|
|||
/// </summary>
|
|||
[XmlElement("SpNoStr")] |
|||
public string SpNoStr { get; set; } |
|||
/// <summary>
|
|||
/// 审批申请类型名称(审批模板名称)
|
|||
/// </summary>
|
|||
[XmlElement("SpName")] |
|||
public string SpName { get; set; } |
|||
/// <summary>
|
|||
/// 申请单状态:1-审批中;2-已通过;3-已驳回;4-已撤销;6-通过后撤销;7-已删除;10-已支付
|
|||
/// </summary>
|
|||
[XmlElement("SpStatus")] |
|||
public byte SpStatus { get; set; } |
|||
/// <summary>
|
|||
/// 审批模板id。可在“获取审批申请详情”、“审批状态变化回调通知”中获得,也可在审批模板的模板编辑页面链接中获得。
|
|||
/// </summary>
|
|||
[XmlElement("TemplateId")] |
|||
public string TemplateId { get; set; } |
|||
/// <summary>
|
|||
/// 审批申请提交时间,Unix时间戳
|
|||
/// </summary>
|
|||
[XmlElement("ApplyTime")] |
|||
public int ApplyTime { get; set; } |
|||
/// <summary>
|
|||
/// 申请人信息
|
|||
/// </summary>
|
|||
[XmlElement("Applyer")] |
|||
public SysApprovalApplyer Applyer { get; set; } |
|||
/// <summary>
|
|||
/// 审批流程信息,可能有多个审批节点。
|
|||
/// </summary>
|
|||
[XmlElement("SpRecord")] |
|||
public List<SysApprovalRecord> SpRecord { get; set; } |
|||
/// <summary>
|
|||
/// 抄送信息,可能有多个抄送节点
|
|||
/// </summary>
|
|||
[XmlElement("Notifyer")] |
|||
public List<SysApprovalNotifyer> Notifyer { get; set; } |
|||
/// <summary>
|
|||
/// 审批申请备注信息,可能有多个备注节点
|
|||
/// </summary>
|
|||
[XmlElement("Comments")] |
|||
public List<SysApprovalComment> Comments { get; set; } |
|||
/// <summary>
|
|||
/// 审批流程列表
|
|||
/// </summary>
|
|||
[XmlElement("ProcessList")] |
|||
public List<SysApprovalProcess> ProcessList { get; set; } |
|||
/// <summary>
|
|||
/// 审批申请状态变化类型:1-提单;2-同意;3-驳回;4-转审;5-催办;6-撤销;8-通过后撤销;10-添加备注;11-回退给指定审批人;12-添加审批人;13-加签并同意; 14-已办理; 15-已转交
|
|||
/// </summary>
|
|||
[XmlElement("StatuChangeEvent")] |
|||
public byte StatuChangeEvent { get; set; } |
|||
/// <summary>
|
|||
/// 审批编号
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 局校审批单不返回此字段,其他类型审批单会返回此字段,不推荐使用此字段
|
|||
/// </remarks>
|
|||
[XmlElement("SpNo")] |
|||
[Obsolete("局校审批单不返回此字段,其他类型审批单会返回此字段,不推荐使用此字段")] |
|||
public string SpNo { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalApplyer |
|||
{ |
|||
/// <summary>
|
|||
/// 申请人userid
|
|||
/// </summary>
|
|||
[XmlElement("UserId")] |
|||
public string UserId { get; set; } |
|||
/// <summary>
|
|||
/// 申请人所在部门pid
|
|||
/// </summary>
|
|||
[XmlElement("Party")] |
|||
public string Party { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalRecord |
|||
{ |
|||
/// <summary>
|
|||
/// 审批节点状态:1-审批中;2-已同意;3-已驳回;4-已转审
|
|||
/// </summary>
|
|||
[XmlElement("SpStatus")] |
|||
public byte SpStatus { get; set; } |
|||
/// <summary>
|
|||
/// 节点审批方式:1-或签;2-会签
|
|||
/// </summary>
|
|||
[XmlElement("ApproverAttr")] |
|||
public byte ApproverAttr { get; set; } |
|||
/// <summary>
|
|||
/// 节点审批方式:1-或签;2-会签
|
|||
/// </summary>
|
|||
[XmlElement("Details")] |
|||
public List<SysApprovalRecordDetail> Details { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalRecordDetail |
|||
{ |
|||
/// <summary>
|
|||
/// 分支审批人
|
|||
/// </summary>
|
|||
[XmlElement("Approver")] |
|||
public SysApprovalApplyer Approver { get; set; } |
|||
/// <summary>
|
|||
/// 审批意见字段
|
|||
/// </summary>
|
|||
[XmlElement("Speech")] |
|||
public string Speech { get; set; } |
|||
/// <summary>
|
|||
/// 分支审批人审批状态:1-审批中;2-已同意;3-已驳回;4-已转审
|
|||
/// </summary>
|
|||
[XmlElement("SpStatus")] |
|||
public byte SpStatus { get; set; } |
|||
/// <summary>
|
|||
/// 节点分支审批人审批操作时间,0为尚未操作
|
|||
/// </summary>
|
|||
[XmlElement("SpTime")] |
|||
public int SpTime { get; set; } |
|||
/// <summary>
|
|||
/// 节点分支审批人审批意见附件,赋值为media_id具体使用请参考:文档-获取临时素材
|
|||
/// </summary>
|
|||
[XmlElement("Attach")] |
|||
public string Attach { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalNotifyer |
|||
{ |
|||
/// <summary>
|
|||
/// 节点抄送人userid
|
|||
/// </summary>
|
|||
[XmlElement("UserId")] |
|||
public string UserId { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalComment |
|||
{ |
|||
/// <summary>
|
|||
/// 备注人信息
|
|||
/// </summary>
|
|||
[XmlElement("CommentUserInfo")] |
|||
public SysApprovalCommenter CommentUserInfo { get; set; } |
|||
/// <summary>
|
|||
/// 备注提交时间
|
|||
/// </summary>
|
|||
[XmlElement("CommentTime")] |
|||
public int CommentTime { get; set; } |
|||
/// <summary>
|
|||
/// 备注文本内容
|
|||
/// </summary>
|
|||
[XmlElement("CommentContent")] |
|||
public string CommentContent { get; set; } |
|||
/// <summary>
|
|||
/// 备注id
|
|||
/// </summary>
|
|||
[XmlElement("CommentId")] |
|||
public string CommentId { get; set; } |
|||
/// <summary>
|
|||
/// 备注意见附件,值是附件media_id
|
|||
/// </summary>
|
|||
[XmlElement("Attach")] |
|||
public string Attach { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalCommenter |
|||
{ |
|||
/// <summary>
|
|||
/// 节点抄送人userid
|
|||
/// </summary>
|
|||
[XmlElement("UserId")] |
|||
public string UserId { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalProcess |
|||
{ |
|||
/// <summary>
|
|||
/// 流程节点
|
|||
/// </summary>
|
|||
[XmlElement("NodeList")] |
|||
public List<SysApprovalProcessNode> NodeList { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalProcessNode |
|||
{ |
|||
/// <summary>
|
|||
/// 节点类型 1 审批人 2 抄送人 3办理人
|
|||
/// </summary>
|
|||
[XmlElement("NodeType")] |
|||
public byte NodeType { get; set; } |
|||
/// <summary>
|
|||
/// 节点状态 1-审批中;2-同意;3-驳回;4-转审;11-退回给指定审批人;12-加签;13-同意并加签;14-办理;15-转交
|
|||
/// </summary>
|
|||
[XmlElement("SpStatus")] |
|||
public byte SpStatus { get; set; } |
|||
/// <summary>
|
|||
/// 多人办理方式 1-会签;2-或签 3-依次审批
|
|||
/// </summary>
|
|||
[XmlElement("ApvRel")] |
|||
public byte ApvRel { get; set; } |
|||
/// <summary>
|
|||
/// 子节点列表
|
|||
/// </summary>
|
|||
[XmlElement("SubNodeList")] |
|||
public List<SysApprovalProcessSubNode> SubNodeList { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalProcessSubNode |
|||
{ |
|||
/// <summary>
|
|||
/// 处理人信息
|
|||
/// </summary>
|
|||
[XmlElement("UserInfo")] |
|||
public SysApprovalProcesser UserInfo { get; set; } |
|||
/// <summary>
|
|||
/// 审批/办理意见
|
|||
/// </summary>
|
|||
[XmlElement("Speech")] |
|||
public string Speech { get; set; } |
|||
/// <summary>
|
|||
/// 子节点状态 1-审批中;2-同意;3-驳回;4-转审;11-退回给指定审批人;12-加签;13-同意并加签;14-办理;15-转交
|
|||
/// </summary>
|
|||
[XmlElement("SpYj")] |
|||
public byte SpYj { get; set; } |
|||
/// <summary>
|
|||
/// 操作时间
|
|||
/// </summary>
|
|||
[XmlElement("Sptime")] |
|||
public int Sptime { get; set; } |
|||
/// <summary>
|
|||
/// 备注意见附件,值是附件media_id
|
|||
/// </summary>
|
|||
[XmlElement("MediaIds")] |
|||
public string MediaIds { get; set; } |
|||
} |
|||
|
|||
public class SysApprovalProcesser |
|||
{ |
|||
/// <summary>
|
|||
/// 处理人userid
|
|||
/// </summary>
|
|||
[XmlElement("UserId")] |
|||
public string UserId { get; set; } |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Crypto; |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.Common.Messages; |
|||
public class WeChatWorkMessageResolver : MessageResolverBase, IWeChatWorkMessageResolver |
|||
{ |
|||
private readonly AbpWeChatWorkMessageResolveOptions _options; |
|||
public WeChatWorkMessageResolver( |
|||
IWeChatCryptoService cryptoService, |
|||
IServiceProvider serviceProvider, |
|||
IOptions<AbpWeChatWorkMessageResolveOptions> options) |
|||
: base(cryptoService, serviceProvider) |
|||
{ |
|||
_options = options.Value; |
|||
} |
|||
|
|||
protected async override Task<MessageResolveResult> ResolveMessageAsync(MessageResolveContext context) |
|||
{ |
|||
var result = new MessageResolveResult(context.Origin); |
|||
|
|||
foreach (var messageResolver in _options.MessageResolvers) |
|||
{ |
|||
await messageResolver.ResolveAsync(context); |
|||
|
|||
result.AppliedResolvers.Add(messageResolver.Name); |
|||
|
|||
if (context.HasResolvedMessage()) |
|||
{ |
|||
result.Message = context.Message; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,27 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.WeChat.Work.ExternalContact</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.WeChat.Work.ExternalContact</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<GenerateDocumentationFile>True</GenerateDocumentationFile> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\WeChat\Work\ExternalContact\Localization\Resources\*.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\WeChat\Work\ExternalContact\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,78 @@ |
|||
using LINGYUN.Abp.WeChat.Common; |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using LINGYUN.Abp.WeChat.Work.Common.Messages; |
|||
using LINGYUN.Abp.WeChat.Work.ExternalContact.Messages.Models; |
|||
using LINGYUN.Abp.WeChat.Work.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.ExternalContact; |
|||
|
|||
[DependsOn(typeof(AbpWeChatWorkModule))] |
|||
public class AbpWeChatWorkExternalContactModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpWeChatWorkMessageResolveOptions>(options => |
|||
{ |
|||
// 企业客户变更事件
|
|||
options.MapEvent("change_external_contact", context => |
|||
{ |
|||
var changeType = context.GetMessageData("ChangeType"); |
|||
return changeType switch |
|||
{ |
|||
"add_external_contact" => context.GetWeChatMessage<ExternalContactCreateEvent>(), |
|||
"edit_external_contact" => context.GetWeChatMessage<ExternalContactUpdateEvent>(), |
|||
"add_half_external_contact" => context.GetWeChatMessage<ExternalContactCreateHalfEvent>(), |
|||
"del_external_contact" => context.GetWeChatMessage<ExternalContactDeleteEvent>(), |
|||
"del_follow_user" => context.GetWeChatMessage<ExternalContactDeleteFollowUserEvent>(), |
|||
"transfer_fail" => context.GetWeChatMessage<ExternalContactTransferFailEvent>(), |
|||
_ => throw new AbpWeChatException($"Contact change event change_external_contact:{changeType} is not mounted!"), |
|||
}; |
|||
}); |
|||
// 客户群变更事件
|
|||
options.MapEvent("change_external_chat", context => |
|||
{ |
|||
var changeType = context.GetMessageData("ChangeType"); |
|||
switch (changeType) |
|||
{ |
|||
case "create": return context.GetWeChatMessage<ExternalChatCreateEvent>(); |
|||
case "update": |
|||
// 客户群变更事件
|
|||
var updateDetail = context.GetMessageData("UpdateDetail"); |
|||
return updateDetail switch |
|||
{ |
|||
"add_member" => context.GetWeChatMessage<ExternalChatAddMemberEvent>(), |
|||
"del_member" => context.GetWeChatMessage<ExternalChaDelMemberEvent>(), |
|||
"change_owner" => context.GetWeChatMessage<ExternalChatChangeOwnerEvent>(), |
|||
"change_name" => context.GetWeChatMessage<ExternalChatChangeNameEvent>(), |
|||
"change_notice" => context.GetWeChatMessage<ExternalChatChangeNoticeEvent>(), |
|||
_ => throw new AbpWeChatException($"Contact change event change_external_chat:{changeType}:{updateDetail} is not mounted!"), |
|||
}; |
|||
case "dismiss": return context.GetWeChatMessage<ExternalChatDismissEvent>(); |
|||
default: throw new AbpWeChatException($"Contact change event change_external_chat:{changeType} is not mounted!"); |
|||
} |
|||
}); |
|||
// 企业客户标签事件
|
|||
options.MapEvent("change_external_tag", context => |
|||
{ |
|||
var changeType = context.GetMessageData("ChangeType"); |
|||
return changeType switch |
|||
{ |
|||
"create" => context.GetWeChatMessage<ExternalTagCreateEvent>(), |
|||
"update" => context.GetWeChatMessage<ExternalTagUpdateEvent>(), |
|||
"delete" => context.GetWeChatMessage<ExternalTagDeleteEvent>(), |
|||
"shuffle" => context.GetWeChatMessage<ExternalTagShuffleEvent>(), |
|||
_ => throw new AbpWeChatException($"Contact change event change_external_tag:{changeType} is not mounted!"), |
|||
}; |
|||
}); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<WeChatWorkResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/WeChat/Work/ExternalContact/Localization/Resources"); |
|||
}); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue