liu
3 天以前 fe2d6627b8f1002af7a31b08f6f43bec034d04ee
src/views/wxcp/notice/modules/push-modal.vue
@@ -1,15 +1,17 @@
<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' });
@@ -21,6 +23,33 @@
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) {
@@ -29,11 +58,32 @@
      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) {
@@ -43,6 +93,20 @@
    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 {
@@ -50,6 +114,13 @@
      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('推送成功');
@@ -71,12 +142,64 @@
    <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