yyb
14 小时以前 352f7bbb74f1b6c57b3d3e576849d0565932fbd4
src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue
@@ -1,7 +1,6 @@
<!--OA模块:EnterpriseNews 企业新闻-->
<!--OA模块:EnterpriseNews 企业新闻(列表走审批实例,新增/修改保留原表单 + 模板审批流程)-->
<template>
  <div class="app-container enterprise-news-page">
    <div class="search_form mb20">
      <div class="search_fields">
        <span class="search_title">关键词:</span>
@@ -11,19 +10,24 @@
          placeholder="标题 / 编号 / 摘要"
          clearable
          :prefix-icon="Search"
          @keyup.enter="handleQuery"
          @keyup.enter="onSearch"
        />
        <span class="search_title" style="margin-left: 12px">分类:</span>
        <el-select v-model="searchForm.newsType" placeholder="全部" clearable style="width: 140px">
          <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
        </el-select>
        <span class="search_title" style="margin-left: 12px">状态:</span>
        <el-select v-model="searchForm.publishStatus" placeholder="全部" clearable style="width: 120px">
          <el-option v-for="opt in PUBLISH_STATUS_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
        <span class="search_title" style="margin-left: 12px">审批状态:</span>
        <el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 120px">
          <el-option
            v-for="opt in APPROVAL_STATUS_SEARCH_OPTIONS"
            :key="opt.value"
            :label="opt.label"
            :value="opt.value"
          />
        </el-select>
        <span class="search_title" style="margin-left: 12px">发布时间:</span>
        <span class="search_title" style="margin-left: 12px">申请日期:</span>
        <el-date-picker
          v-model="searchForm.publishTimeRange"
          v-model="searchForm.createTimeRange"
          type="daterange"
          range-separator="-"
          start-placeholder="开始"
@@ -33,11 +37,11 @@
          style="width: 260px"
          clearable
        />
        <el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">搜索</el-button>
        <el-button type="primary" :icon="Search" class="ml10" @click="onSearch">搜索</el-button>
        <el-button :icon="RefreshRight" @click="resetSearch">重置</el-button>
      </div>
      <div class="search_actions">
        <el-button type="primary" :icon="Plus" @click="openFormDialog('add')">新建新闻</el-button>
        <el-button type="primary" :icon="Plus" @click="openAddWithTemplate">新建新闻</el-button>
      </div>
    </div>
@@ -50,7 +54,7 @@
        :isSelection="false"
        :tableLoading="tableLoading"
        :total="page.total"
        @pagination="pagination"
        @pagination="onPagination"
      >
        <template #newsType="{ row }">
          <span class="news-type-tag" :style="{ color: newsTypeColor(row.newsType) }">
@@ -60,34 +64,42 @@
      </PIMTable>
    </div>
    <!-- 新建 / 编辑 -->
    <ApprovalTemplateBindDialog
      v-model:visible="templateBindVisible"
      :module-key="APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS"
      skip-form-confirm
      @confirm="onTemplateBound"
      @closed="onTemplateBindClosed"
    />
    <!-- 新建 / 编辑:原企业新闻表单 + 模板审批流程 -->
    <el-dialog
      v-model="formDialog.visible"
      :title="formDialog.title"
      v-model="newsFormDialog.visible"
      :title="newsFormDialog.title"
      width="960px"
      append-to-body
      destroy-on-close
      class="news-form-dialog"
      @closed="formRef?.resetFields?.()"
      @closed="onNewsFormClosed"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="formRules"
        ref="newsFormRef"
        :model="newsForm"
        :rules="newsFormRules"
        label-width="110px"
        :disabled="formDialog.readonly"
        :disabled="newsFormDialog.readonly"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="新闻分类" prop="newsType">
              <el-select v-model="form.newsType" placeholder="请选择" style="width: 100%">
              <el-select v-model="newsForm.newsType" placeholder="请选择" style="width: 100%">
                <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="排版模板">
              <el-select v-model="form.layoutTemplate" style="width: 100%">
              <el-select v-model="newsForm.layoutTemplate" style="width: 100%">
                <el-option
                  v-for="opt in LAYOUT_TEMPLATE_OPTIONS"
                  :key="opt.value"
@@ -99,29 +111,29 @@
          </el-col>
        </el-row>
        <el-form-item label="标题" prop="title">
          <el-input v-model="form.title" placeholder="新闻标题" maxlength="100" show-word-limit />
          <el-input v-model="newsForm.title" placeholder="新闻标题" maxlength="100" show-word-limit />
        </el-form-item>
        <el-form-item label="摘要">
          <el-input v-model="form.summary" type="textarea" :rows="2" maxlength="300" show-word-limit />
          <el-input v-model="newsForm.summary" type="textarea" :rows="2" maxlength="300" show-word-limit />
        </el-form-item>
        <el-form-item label="正文" prop="contentHtml">
          <Editor v-model="form.contentHtml" :min-height="280" />
          <Editor v-model="newsForm.contentHtml" :min-height="280" />
        </el-form-item>
        <el-form-item label="附件">
          <FileUpload v-model:file-list="form.attachmentList" :limit="10" button-text="上传 PDF / 文档" />
          <FileUpload v-model:file-list="newsForm.attachmentList" :limit="10" button-text="上传 PDF / 文档" />
        </el-form-item>
        <el-form-item v-if="form.layoutTemplate === 'gallery'" label="图集/视频">
        <el-form-item v-if="newsForm.layoutTemplate === 'gallery'" label="图集/视频">
          <el-input
            v-model="galleryInput"
            placeholder="输入资源名称后回车添加(演示)"
            @keyup.enter="addGalleryItem"
          />
          <el-tag
            v-for="(m, i) in form.mediaList"
            v-for="(m, i) in newsForm.mediaList"
            :key="i"
            closable
            class="media-tag"
            @close="form.mediaList.splice(i, 1)"
            @close="newsForm.mediaList.splice(i, 1)"
          >
            {{ m.name }}
          </el-tag>
@@ -131,276 +143,326 @@
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="编辑角色">
              <el-select v-model="form.editorRole" style="width: 100%">
              <el-select v-model="newsForm.editorRole" style="width: 100%">
                <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="审核角色">
              <el-select v-model="form.reviewerRole" style="width: 100%">
              <el-select v-model="newsForm.reviewerRole" style="width: 100%">
                <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="阅读范围" prop="readScope">
          <el-radio-group v-model="form.readScope">
          <el-radio-group v-model="newsForm.readScope">
            <el-radio v-for="opt in READ_SCOPE_OPTIONS" :key="opt.value" :value="opt.value">
              {{ opt.label }}
            </el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item v-if="form.readScope === 'department'" label="可见部门">
          <el-select v-model="form.targetDeptIds" multiple placeholder="选择部门" style="width: 100%">
        <el-form-item v-if="newsForm.readScope === 'department'" label="可见部门">
          <el-select v-model="newsForm.targetDeptIds" multiple placeholder="选择部门" style="width: 100%">
            <el-option v-for="d in DEPT_OPTIONS" :key="d.value" :label="d.label" :value="d.value" />
          </el-select>
        </el-form-item>
        <el-form-item label="政策类必读">
          <el-switch v-model="form.requireReadConfirm" active-text="需阅读确认(便于统计未读)" />
          <el-switch v-model="newsForm.requireReadConfirm" active-text="需阅读确认(便于统计未读)" />
        </el-form-item>
        <el-form-item label="发布人">
          <el-input v-model="form.publisherName" placeholder="如:人力资源部" maxlength="50" />
          <el-input v-model="newsForm.publisherName" placeholder="如:人力资源部" maxlength="50" />
        </el-form-item>
        <template v-if="activeTemplate">
          <el-divider content-position="left">审批流程</el-divider>
          <el-form-item label="审批模板">
            <span class="template-name">{{ activeTemplate.label || submitForm.templateName }}</span>
          </el-form-item>
          <el-form-item label="审批流程" required>
            <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" />
            <p class="section-tip">流程与审批人由模板预置,可按需微调节点审批人。</p>
          </el-form-item>
        </template>
        <el-alert v-else type="warning" show-icon :closable="false" title="请先通过「新建新闻」选择审批模板" />
      </el-form>
      <template v-if="!formDialog.readonly" #footer>
        <el-button @click="formDialog.visible = false">取 消</el-button>
        <el-button @click="onSave('save')">存草稿</el-button>
        <el-button type="warning" @click="onSave('submit_review')">提交审核</el-button>
        <el-button type="primary" @click="onSave('publish')">直接发布</el-button>
      <template v-if="!newsFormDialog.readonly" #footer>
        <el-button @click="newsFormDialog.visible = false">取 消</el-button>
        <el-button :loading="submitSaving" @click="onNewsSave('draft')">存草稿</el-button>
        <el-button type="warning" :loading="submitSaving" @click="onNewsSave('submit_review')">
          提交审核
        </el-button>
        <el-button type="primary" :loading="submitSaving" @click="onNewsSave('submit_review')">
          保 存
        </el-button>
      </template>
    </el-dialog>
    <!-- 详情 -->
    <el-dialog v-model="detailDialog.visible" title="新闻详情" width="880px" append-to-body destroy-on-close>
      <NewsDetailPanel
        :row="detailRow"
        @like="onDetailLike"
        @comment="onDetailComment"
      />
      <NewsDetailPanel :row="detailNewsRow" @like="onDetailLike" @comment="onDetailComment" />
      <el-divider content-position="left">审批信息</el-divider>
      <ApproveDetailPanel :row="detailRow" />
      <template #footer>
        <el-button
          v-if="detailRow.publishStatus === 'published' && getUnreadEmployees(detailRow).length"
          type="warning"
          @click="openUnreadFromDetail"
          v-if="canEditBusinessInstanceRow(detailRow)"
          type="primary"
          @click="openNewsEditFromDetail"
        >
          未读提醒
          修改
        </el-button>
        <el-button @click="openVersionFromDetail">版本留证</el-button>
        <el-button @click="detailDialog.visible = false">关 闭</el-button>
      </template>
    </el-dialog>
    <!-- 未读提醒 -->
    <el-dialog
      v-model="unreadDialog.visible"
      :title="`未阅读员工 · ${unreadDialog.row?.title || ''}`"
      width="720px"
      append-to-body
      destroy-on-close
    >
      <el-alert type="warning" show-icon :closable="false" class="mb12">
        政策传达场景:发布新考勤制度等必读信息后,可勾选未读员工由 HR 定向提醒(演示数据,后期对接消息中心)。
      </el-alert>
      <div class="unread-toolbar mb12">
        <el-button size="small" @click="selectAllUnread">全选未读</el-button>
        <span class="unread-stat">共 {{ unreadList.length }} 人未读</span>
      </div>
      <el-table
        :data="unreadList"
        border
        size="small"
        max-height="360"
        @selection-change="onUnreadSelectionChange"
      >
        <el-table-column type="selection" width="48" />
        <el-table-column prop="employeeNo" label="工号" width="100" />
        <el-table-column prop="name" label="姓名" width="90" />
        <el-table-column prop="deptName" label="部门" min-width="120" />
      </el-table>
      <el-divider v-if="unreadDialog.row?.remindLogs?.length" content-position="left">提醒记录</el-divider>
      <el-timeline v-if="unreadDialog.row?.remindLogs?.length">
        <el-timeline-item
          v-for="(log, i) in unreadDialog.row.remindLogs"
          :key="i"
          :timestamp="log.time"
        >
          {{ log.operator }} 已向 {{ log.count }} 人发送阅读提醒
        </el-timeline-item>
      </el-timeline>
      <template #footer>
        <el-button type="primary" @click="onSendRemind">发送定向提醒</el-button>
        <el-button @click="unreadDialog.visible = false">关 闭</el-button>
      </template>
    </el-dialog>
    <!-- 版本留证 -->
    <el-dialog
      v-model="versionDialog.visible"
      :title="`历史版本留证 · ${versionDialog.row?.title || ''}`"
      width="800px"
      append-to-body
      destroy-on-close
    >
      <el-alert type="info" show-icon :closable="false" class="mb12">
        争议发生时可查阅历史版本,证明当时发布内容与发布时间(合规留证)。
      </el-alert>
      <el-descriptions :column="2" border class="mb16">
        <el-descriptions-item label="当前版本">v{{ versionDialog.row?.versionNo || 1 }}</el-descriptions-item>
        <el-descriptions-item label="最近发布">{{ versionDialog.row?.publishTime || "—" }}</el-descriptions-item>
      </el-descriptions>
      <el-table :data="versionList" border size="small" empty-text="暂无历史版本">
        <el-table-column prop="versionNo" label="版本" width="70" align="center" />
        <el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
        <el-table-column prop="changeNote" label="变更说明" width="120" />
        <el-table-column prop="publishTime" label="发布时间" width="170" />
        <el-table-column prop="archivedAt" label="归档时间" width="170" />
        <el-table-column label="操作" width="90" align="center">
          <template #default="{ row: ver }">
            <el-button type="primary" link @click="previewVersion(ver)">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <el-button @click="versionDialog.visible = false">关 闭</el-button>
      </template>
    </el-dialog>
    <!-- 版本预览 -->
    <el-dialog v-model="versionPreview.visible" title="历史版本内容" width="640px" append-to-body>
      <p class="version-meta">
        v{{ versionPreview.data?.versionNo }} · {{ versionPreview.data?.changeNote }} ·
        {{ versionPreview.data?.publishTime }}
      </p>
      <div class="version-html" v-html="versionPreview.data?.contentHtml || ''" />
    </el-dialog>
  </div>
</template>
<script setup>
import { Plus, RefreshRight } from "@element-plus/icons-vue";
import { Plus, RefreshRight, Search } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { computed, onMounted, reactive, ref } from "vue";
import useUserStore from "@/store/modules/user";
import Editor from "@/components/Editor/index.vue";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { newsTypeColor } from "./enterpriseNewsUtils.js";
import { APPROVAL_STATUS_SEARCH_OPTIONS } from "../../ApproveManage/approve-list/approveListConstants.js";
import ApproveDetailPanel from "../../ApproveManage/approve-list/components/ApproveDetailPanel.vue";
import { buildEditFormFromInstanceRow } from "../../ApproveManage/approve-list/approveListConstants.js";
import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
import TemplateFlowEditor from "../../ApproveManage/approve-template/components/TemplateFlowEditor.vue";
import {
  applyBindingToForm,
  validateTemplateBinding,
} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
import NewsDetailPanel from "./components/NewsDetailPanel.vue";
import { useEnterpriseNews } from "./useEnterpriseNews.js";
const {
  Search,
import {
  NEWS_TYPE_OPTIONS,
  PUBLISH_STATUS_OPTIONS,
  LAYOUT_TEMPLATE_OPTIONS,
  READ_SCOPE_OPTIONS,
  PUBLISH_ROLE_OPTIONS,
  DEPT_OPTIONS,
  createEmptyForm,
  newsTypeColor,
  newsTypeLabel,
  searchForm,
  validateNewsForm,
} from "./enterpriseNewsUtils.js";
import {
  enrichEnterpriseNewsListRow,
  extractEnterpriseNewsFromRow,
  syncNewsFormToSubmitPayload,
  buildEnterpriseNewsTableColumns,
} from "./enterpriseNewsApprovalBridge.js";
const userStore = useUserStore();
const searchForm = reactive({
  keyword: "",
  newsType: "",
  status: "",
  createTimeRange: null,
});
const newsFormDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
const newsForm = reactive(createEmptyForm());
const newsFormRef = ref();
const galleryInput = ref("");
const newsFormRules = {
  title: [{ required: true, message: "请输入新闻标题", trigger: "blur" }],
  newsType: [{ required: true, message: "请选择新闻分类", trigger: "change" }],
  readScope: [{ required: true, message: "请选择阅读范围", trigger: "change" }],
};
const mod = useApprovalInstanceModule({
  moduleKey: APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS,
  enrichListRow: enrichEnterpriseNewsListRow,
  buildExtraListParams(sf) {
    const extra = {};
    const kw = (sf?.keyword || "").trim();
    if (kw) extra.title = kw;
    if (sf?.newsType) extra.newsType = sf.newsType;
    return extra;
  },
  async beforeSave(submitForm) {
    const v = validateNewsForm(newsForm);
    if (!v.ok) {
      ElMessage.warning(v.message);
      throw new Error(v.message);
    }
    if (!activeTemplate.value) {
      ElMessage.warning("请先选择审批模板");
      throw new Error("no template");
    }
    const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
    if (!bindingCheck.ok) {
      ElMessage.warning(bindingCheck.message);
      throw new Error(bindingCheck.message);
    }
    syncNewsFormToSubmitPayload(newsForm, submitForm);
  },
});
const {
  tableData,
  tableLoading,
  page,
  tableData,
  tableColumn,
  formDialog,
  form,
  formRef,
  formRules,
  detailDialog,
  detailRow,
  unreadDialog,
  unreadList,
  versionDialog,
  getUnreadEmployees,
  submitDialog,
  submitForm,
  submitSaving,
  isSubmitEdit,
  activeTemplate,
  templateBindVisible,
  pendingTemplateBinding,
  submitEditRow,
  handleQuery,
  resetSearch,
  initModuleList,
  pagination,
  openFormDialog,
  openDetail,
  openUnreadRemind,
  openVersionHistory,
  saveForm,
  sendUnreadRemind,
  toggleLike,
  addComment,
} = useEnterpriseNews();
  openAddWithTemplate,
  onTemplateBound,
  resetSubmitForm,
  submitInstanceForm,
  removeInstance,
  canEditBusinessInstanceRow,
} = mod;
const galleryInput = ref("");
const unreadSelected = ref([]);
const versionPreview = reactive({ visible: false, data: null });
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
const versionList = computed(() => {
  const row = versionDialog.row;
  if (!row) return [];
  const history = [...(row.versions || [])];
  return history.sort((a, b) => (b.versionNo || 0) - (a.versionNo || 0));
const detailNewsRow = computed(() => {
  if (!detailRow.value?.id) return {};
  return extractEnterpriseNewsFromRow(detailRow.value);
});
const tableColumn = ref(
  buildEnterpriseNewsTableColumns(() => [
    { name: "详情", type: "text", clickFun: (row) => openNewsDetail(row) },
    {
      name: "修改",
      type: "text",
      disabled: (row) => !canEditBusinessInstanceRow(row),
      clickFun: (row) => openNewsEdit(row),
    },
    {
      name: "删除",
      type: "danger",
      clickFun: (row) => removeInstance(row),
    },
  ])
);
function resetNewsForm(target = createEmptyForm()) {
  Object.assign(newsForm, createEmptyForm(), target);
}
function openNewsFormDialog(mode, row) {
  newsFormDialog.mode = mode;
  newsFormDialog.readonly = mode === "view";
  newsFormDialog.title =
    mode === "add" ? "新建企业新闻" : mode === "edit" ? "编辑企业新闻" : "查看企业新闻";
  if (mode === "add") {
    resetNewsForm({
      publisherName: userStore?.nickName || userStore?.name || "当前用户",
    });
  } else if (row) {
    resetNewsForm(extractEnterpriseNewsFromRow(row));
  }
  newsFormDialog.visible = true;
}
function onTemplateBindClosed() {
  const binding = pendingTemplateBinding.value;
  if (!binding) return;
  pendingTemplateBinding.value = null;
  resetSubmitForm();
  applyBindingToForm(submitForm, binding);
  submitDialog.mode = "add";
  submitEditRow.value = null;
  openNewsFormDialog("add");
}
function openNewsEdit(row) {
  if (!canEditBusinessInstanceRow(row)) {
    ElMessage.warning("进行中或已完成的审批不可修改");
    return;
  }
  submitDialog.mode = "edit";
  submitEditRow.value = { ...row };
  Object.assign(submitForm, buildEditFormFromInstanceRow(row));
  openNewsFormDialog("edit", row);
}
function openNewsDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
function openNewsEditFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  openNewsEdit(row);
}
function onNewsFormClosed() {
  newsFormRef.value?.resetFields?.();
}
function addGalleryItem() {
  const name = (galleryInput.value || "").trim();
  if (!name) return;
  form.mediaList = form.mediaList || [];
  form.mediaList.push({ type: "image", name, url: "" });
  newsForm.mediaList = newsForm.mediaList || [];
  newsForm.mediaList.push({ type: "image", name, url: "" });
  galleryInput.value = "";
}
function onSave(action) {
  const ret = saveForm(action);
  if (ret?.message) {
    ElMessage.warning(ret.message);
async function onNewsSave(action = "submit_review") {
  try {
    await newsFormRef.value?.validate();
  } catch {
    ElMessage.warning("请完善表单必填项后再保存");
    return;
  }
  if (ret?.ok) {
    ElMessage.success(action === "publish" ? "已发布" : action === "submit_review" ? "已提交审核" : "已保存");
  if (action === "draft") newsForm.publishStatus = "draft";
  else newsForm.publishStatus = "pending_review";
  const ok = await submitInstanceForm({ skipValidate: true });
  if (ok) {
    newsFormDialog.visible = false;
    const msg =
      action === "draft" ? "已保存草稿" : isSubmitEdit.value ? "修改成功" : "已提交审核";
    ElMessage.success(msg);
  }
}
function onSearch() {
  handleQuery(searchForm);
}
function resetSearch() {
  searchForm.keyword = "";
  searchForm.newsType = "";
  searchForm.status = "";
  searchForm.createTimeRange = null;
  onSearch();
}
function onPagination(obj) {
  pagination(obj, searchForm);
}
function onDetailLike() {
  toggleLike(detailRow.value);
  /* 详情互动仍走行内数据,刷新列表后更新 */
}
function onDetailComment(text) {
  const ret = addComment(detailRow.value, text);
  if (ret?.message) ElMessage.warning(ret.message);
  else if (ret?.ok) ElMessage.success("评论已发布");
function onDetailComment() {
  ElMessage.info("评论已记录(演示)");
}
function openUnreadFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  openUnreadRemind(row);
}
function openVersionFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  openVersionHistory(row);
}
function onUnreadSelectionChange(rows) {
  unreadSelected.value = rows.map((r) => r.userId);
}
function selectAllUnread() {
  unreadSelected.value = unreadList.value.map((u) => u.userId);
}
function onSendRemind() {
  const ids = unreadSelected.value;
  const ret = sendUnreadRemind(ids);
  if (ret?.message) {
    ElMessage.warning(ret.message);
    return;
  }
  if (ret?.ok) ElMessage.success(`已向 ${ret.count} 名员工发送阅读提醒`);
}
function previewVersion(ver) {
  versionPreview.data = ver;
  versionPreview.visible = true;
}
onMounted(() => {
  handleQuery();
onMounted(async () => {
  loadFlowUsers();
  await initModuleList(searchForm);
});
</script>
@@ -421,6 +483,10 @@
.search_actions {
  flex-shrink: 0;
}
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
.news-type-tag {
  font-weight: 600;
  font-size: 13px;
@@ -428,32 +494,18 @@
.media-tag {
  margin: 6px 8px 0 0;
}
.unread-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
.template-name {
  font-weight: 600;
  color: var(--el-text-color-primary);
}
.unread-stat {
.section-tip {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  font-size: 13px;
  margin: 8px 0 0;
  line-height: 1.5;
}
.version-meta {
  color: var(--el-text-color-secondary);
  font-size: 13px;
  margin-bottom: 12px;
}
.version-html {
  padding: 12px;
  background: var(--el-fill-color-light);
  border-radius: 6px;
  max-height: 400px;
  overflow-y: auto;
}
.mb16 {
  margin-bottom: 16px;
}
.mb12 {
  margin-bottom: 12px;
.mb20 {
  margin-bottom: 20px;
}
.ml10 {
  margin-left: 10px;