| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpNoticeApi } from '#/api/wxcp/notice'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, message } from 'ant-design-vue'; |
| | | 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 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) { |
| | |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ notice: WxCpNoticeApi.Notice }>(); |
| | | notice.value = data.notice ?? {}; |
| | | scope.value = data.notice?.pushScope ?? PUSH_SCOPE.ALL; |
| | | target.value = data.notice?.pushTarget ?? ''; |
| | | 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) { |
| | |
| | | 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 { |
| | |
| | | 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('推送成功'); |
| | |
| | | <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"> |
| | | 推送后成员的企业微信会收到纯文本消息,内容超过 600 字会自动截断。 |
| | | </div> |
| | | <div class="text-gray-500">{{ pushHint }}</div> |
| | | </div> |
| | | |
| | | <div class="mb-2 text-sm font-medium">推送范围</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 |