<script lang="ts" setup>
|
import type { WxCpNoticeApi } from '#/api/wxcp/notice';
|
|
import { computed, ref } from 'vue';
|
|
import { useVbenModal } from '@vben/common-ui';
|
import { Alert, Button, Input, Select, Textarea, message } from 'ant-design-vue';
|
|
import { pushNotice } from '#/api/wxcp/notice';
|
import FileUpload from '#/components/upload/file-upload.vue';
|
|
import PushScopePicker from '../../components/push-scope-picker.vue';
|
import { PUSH_SCOPE } from '../../data';
|
import { NOTICE_TYPE, NOTICE_TYPE_OPTIONS } from '../data';
|
|
defineOptions({ name: 'WxCpNoticePushModal' });
|
|
const emit = defineEmits<{ success: [] }>();
|
|
const notice = ref<WxCpNoticeApi.Notice>({});
|
const scope = ref<number>(PUSH_SCOPE.ALL);
|
const target = ref<string>('');
|
const pushing = ref(false);
|
const resultMessage = ref('');
|
|
// 推送时选择的消息模板与参数(默认取通知上已保存的配置)
|
const noticeType = ref<number>(NOTICE_TYPE.TEXT);
|
const summary = ref('');
|
const linkUrl = ref('');
|
const linkBtnText = ref('');
|
const coverUrl = ref('');
|
const fileName = ref('');
|
const fileList = ref<number[]>([]);
|
|
const isCard = computed(() => noticeType.value === NOTICE_TYPE.CARD);
|
const isNews = computed(() => noticeType.value === NOTICE_TYPE.NEWS);
|
const isFile = computed(() => noticeType.value === NOTICE_TYPE.FILE);
|
const showLink = computed(() => isCard.value || isNews.value);
|
|
const pushHint = computed(() => {
|
switch (noticeType.value) {
|
case NOTICE_TYPE.CARD:
|
return '成员会收到图文卡片,点击按钮可跳转到填报或详情页面。';
|
case NOTICE_TYPE.FILE:
|
return '成员会收到一条说明文本和一个文件,文件上限 20MB。';
|
case NOTICE_TYPE.NEWS:
|
return '成员会收到图文消息,展示标题、摘要、封面并可点击跳转。';
|
default:
|
return '成员会收到纯文本消息,内容超过 600 字会自动截断。';
|
}
|
});
|
|
const [Modal, modalApi] = useVbenModal({
|
onOpenChange(isOpen: boolean) {
|
if (!isOpen) {
|
notice.value = {};
|
resultMessage.value = '';
|
return;
|
}
|
const data = modalApi.getData<{ notice: WxCpNoticeApi.Notice }>();
|
const item = data.notice ?? {};
|
notice.value = item;
|
scope.value = item.pushScope ?? PUSH_SCOPE.ALL;
|
target.value = item.pushTarget ?? '';
|
noticeType.value = item.noticeType ?? NOTICE_TYPE.TEXT;
|
summary.value = item.summary ?? '';
|
linkUrl.value = item.linkUrl ?? '';
|
linkBtnText.value = item.linkBtnText ?? '';
|
coverUrl.value = item.coverUrl ?? '';
|
fileName.value = item.fileName ?? '';
|
fileList.value = item.fileBlobId ? [item.fileBlobId] : [];
|
},
|
});
|
|
/** 上传组件可能返回数组或对象,统一取出 blobId */
|
function resolveFileBlobId(): number | undefined {
|
const raw = fileList.value as unknown;
|
if (Array.isArray(raw)) {
|
const first = raw[0];
|
if (typeof first === 'number') {
|
return first;
|
}
|
return (first as { id?: number } | undefined)?.id;
|
}
|
return typeof raw === 'number' ? raw : undefined;
|
}
|
|
async function handlePush() {
|
if (!notice.value.id) {
|
return;
|
}
|
if (scope.value !== PUSH_SCOPE.ALL && !target.value.trim()) {
|
message.warning('选择「指定部门」或「指定成员」时,请先选择推送目标');
|
return;
|
}
|
if (noticeType.value === NOTICE_TYPE.TEXT && !notice.value.content?.trim()) {
|
message.warning('纯文本通知的正文不能为空');
|
return;
|
}
|
if (showLink.value && !linkUrl.value.trim()) {
|
message.warning('图文卡片/图文消息的跳转链接不能为空');
|
return;
|
}
|
const fileBlobId = resolveFileBlobId();
|
if (isFile.value && !fileBlobId) {
|
message.warning('文件通知必须上传文件');
|
return;
|
}
|
|
pushing.value = true;
|
resultMessage.value = '';
|
try {
|
const res = await pushNotice({
|
id: notice.value.id,
|
pushScope: scope.value,
|
pushTarget: scope.value === PUSH_SCOPE.ALL ? '' : target.value,
|
noticeType: noticeType.value,
|
summary: summary.value,
|
linkUrl: linkUrl.value,
|
linkBtnText: linkBtnText.value,
|
coverUrl: coverUrl.value,
|
fileBlobId,
|
fileName: fileName.value || undefined,
|
});
|
if (res?.success) {
|
message.success('推送成功');
|
emit('success');
|
await modalApi.close();
|
} else {
|
resultMessage.value = res?.message || '推送失败';
|
}
|
} catch (error: unknown) {
|
resultMessage.value = (error as Error)?.message || '推送失败';
|
} finally {
|
pushing.value = false;
|
}
|
}
|
</script>
|
|
<template>
|
<Modal title="推送通知" class="w-2/5">
|
<div class="px-4">
|
<div class="mb-4 rounded bg-gray-50 p-3 text-sm dark:bg-gray-800">
|
<div class="mb-1 font-medium">{{ notice.title }}</div>
|
<div class="text-gray-500">{{ pushHint }}</div>
|
</div>
|
|
<div class="mb-2 text-sm font-medium">消息模板</div>
|
<Select
|
v-model:value="noticeType"
|
:options="NOTICE_TYPE_OPTIONS"
|
class="mb-4 w-full"
|
placeholder="请选择推送模板类型"
|
/>
|
|
<template v-if="showLink">
|
<div class="mb-2 text-sm font-medium">摘要</div>
|
<Textarea
|
v-model:value="summary"
|
class="mb-3"
|
:rows="2"
|
placeholder="图文卡片/图文消息展示的摘要"
|
/>
|
<div class="mb-2 text-sm font-medium">跳转链接</div>
|
<Input
|
v-model:value="linkUrl"
|
class="mb-3"
|
placeholder="填报页面或详情页地址,如 https://..."
|
/>
|
</template>
|
|
<template v-if="isCard">
|
<div class="mb-2 text-sm font-medium">按钮文案</div>
|
<Input
|
v-model:value="linkBtnText"
|
class="mb-3"
|
placeholder="如:去填报 / 查看详情"
|
/>
|
</template>
|
|
<template v-if="isNews">
|
<div class="mb-2 text-sm font-medium">封面图地址</div>
|
<Input
|
v-model:value="coverUrl"
|
class="mb-3"
|
placeholder="封面图地址(选填)"
|
/>
|
</template>
|
|
<template v-if="isFile">
|
<div class="mb-2 text-sm font-medium">通知文件</div>
|
<FileUpload
|
v-model:value="fileList"
|
class="mb-3"
|
:max-number="1"
|
:multiple="false"
|
value-key="id"
|
:show-description="true"
|
/>
|
</template>
|
|
<div class="mb-2 mt-4 text-sm font-medium">推送范围</div>
|
<PushScopePicker v-model:scope="scope" v-model:target="target" />
|
|
<Alert
|
v-if="resultMessage"
|
class="mt-4"
|
type="error"
|
:message="resultMessage"
|
show-icon
|
/>
|
</div>
|
|
<template #footer>
|
<div class="flex justify-end gap-2">
|
<Button @click="modalApi.close()">取消</Button>
|
<Button type="primary" :loading="pushing" @click="handlePush">
|
确认推送
|
</Button>
|
</div>
|
</template>
|
</Modal>
|
</template>
|