yyb
16 小时以前 a1df9699594b0a0e46d26a0394eafb1eb030c68b
src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue
@@ -1,29 +1,33 @@
<!--OA模块:EnterpriseNews 企业新闻-->
<!--OA模块:EnterpriseNews 企业新闻(listPage|save|update|delete,新建保留审批模板)-->
<template>
  <div class="app-container enterprise-news-page">
    <div class="search_form mb20">
      <div class="search_fields">
        <span class="search_title">关键词:</span>
        <el-input
          v-model="searchForm.keyword"
          style="width: 200px"
          placeholder="标题 / 编号 / 摘要"
          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" />
        <el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 120px">
          <el-option
            v-for="opt in ENTERPRISE_NEWS_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,41 @@
      </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 +110,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 +142,384 @@
        <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-form-item>
        <template v-if="hasApprovalTemplate">
          <el-divider content-position="left">审批流程</el-divider>
          <el-form-item label="审批模板">
            <span class="template-name">{{ approvalTemplateLabel }}</span>
          </el-form-item>
          <el-form-item v-if="activeTemplate" label="审批流程" required>
            <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" />
            <p class="section-tip">流程与审批人由模板预置,可按需微调节点审批人。</p>
          </el-form-item>
        </template>
        <el-alert
          v-else-if="!isNewsEdit"
          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="newsSaving" @click="onNewsSave('draft')">存草稿</el-button>
        <el-button type="warning" :loading="newsSaving" @click="onNewsSave('submit_review')">
          提交审核
        </el-button>
        <el-button type="primary" :loading="newsSaving" @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" />
      <template #footer>
        <el-button
          v-if="detailRow.publishStatus === 'published' && getUnreadEmployees(detailRow).length"
          type="warning"
          @click="openUnreadFromDetail"
        >
          未读提醒
        <el-button v-if="canEditEnterpriseNewsRow(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 { ElMessage } from "element-plus";
import { Plus, RefreshRight, Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
  deleteEnterpriseNews,
  saveEnterpriseNews,
  updateEnterpriseNews,
} from "@/api/officeProcessAutomation/enterpriseNews.js";
import { computed, onMounted, reactive, ref } from "vue";
import Editor from "@/components/Editor/index.vue";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { newsTypeColor } from "./enterpriseNewsUtils.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 { createEmptySubmitForm } from "../../ApproveManage/approve-list/approveListConstants.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import {
  applyBindingToForm,
  validateTemplateBinding,
} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.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,
  ENTERPRISE_NEWS_STATUS_SEARCH_OPTIONS,
  newsTypeColor,
  newsTypeLabel,
  searchForm,
  tableLoading,
  page,
  tableData,
  tableColumn,
  formDialog,
  form,
  formRef,
  formRules,
  detailDialog,
  detailRow,
  unreadDialog,
  unreadList,
  versionDialog,
  getUnreadEmployees,
  handleQuery,
  resetSearch,
  pagination,
  openFormDialog,
  openDetail,
  openUnreadRemind,
  openVersionHistory,
  saveForm,
  sendUnreadRemind,
  toggleLike,
  addComment,
} = useEnterpriseNews();
  validateNewsForm,
} from "./enterpriseNewsUtils.js";
import {
  buildEnterpriseNewsSaveDto,
  buildEnterpriseNewsTableColumns,
  canEditEnterpriseNewsRow,
  mapApiRowToNewsForm,
} from "./enterpriseNewsMappers.js";
import { useEnterpriseNewsList } from "./useEnterpriseNewsList.js";
const galleryInput = ref("");
const unreadSelected = ref([]);
const versionPreview = reactive({ visible: false, data: null });
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 searchForm = reactive({
  keyword: "",
  newsType: "",
  status: "",
  createTimeRange: null,
});
const newsFormDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
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 newsList = useEnterpriseNewsList();
const { tableData, tableLoading, page, handleQuery: fetchNewsList, pagination: paginateNewsList } =
  newsList;
const submitForm = reactive(createEmptySubmitForm(""));
const templateBindVisible = ref(false);
const pendingTemplateBinding = ref(null);
const newsSaving = ref(false);
const isNewsEdit = computed(() => newsFormDialog.mode === "edit");
const activeTemplate = computed(() => submitForm.templateSnapshot || null);
const hasApprovalTemplate = computed(
  () => Boolean(activeTemplate.value || newsForm.templateId)
);
const approvalTemplateLabel = computed(
  () =>
    activeTemplate.value?.label ||
    newsForm.templateName ||
    submitForm.templateName ||
    "—"
);
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
function openAddWithTemplate() {
  pendingTemplateBinding.value = null;
  templateBindVisible.value = true;
}
function onTemplateBound(binding) {
  pendingTemplateBinding.value = binding;
}
function resetSubmitForm() {
  Object.assign(submitForm, createEmptySubmitForm(""));
}
const detailNewsRow = computed(() => mapApiRowToNewsForm(detailRow.value));
const tableColumn = ref(
  buildEnterpriseNewsTableColumns(() => [
    { name: "详情", type: "text", clickFun: (row) => openNewsDetail(row) },
    {
      name: "修改",
      type: "text",
      disabled: (row) => !canEditEnterpriseNewsRow(row),
      clickFun: (row) => openNewsEdit(row),
    },
    {
      name: "删除",
      type: "danger",
      disabled: (row) => !canEditEnterpriseNewsRow(row),
      clickFun: (row) => handleNewsDelete(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();
  } else if (row) {
    resetNewsForm(mapApiRowToNewsForm(row));
  }
  newsFormDialog.visible = true;
}
function onTemplateBindClosed() {
  const binding = pendingTemplateBinding.value;
  if (!binding) return;
  pendingTemplateBinding.value = null;
  resetSubmitForm();
  applyBindingToForm(submitForm, binding);
  if (binding.templateId) {
    newsForm.templateId = binding.templateId;
    newsForm.templateName = binding.templateName || "";
  }
  openNewsFormDialog("add");
}
function openNewsEdit(row) {
  if (!canEditEnterpriseNewsRow(row)) {
    ElMessage.warning("当前状态不可修改");
    return;
  }
  resetSubmitForm();
  if (row?.templateId != null) {
    submitForm.templateId = row.templateId;
    submitForm.templateName = row.templateName || "";
  }
  openNewsFormDialog("edit", row);
}
function openNewsDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
function openNewsEditFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  openNewsEdit(row);
}
async function handleNewsDelete(row) {
  if (!canEditEnterpriseNewsRow(row)) {
    ElMessage.warning("当前状态不可删除");
    return;
  }
  if (row?.id == null || row.id === "") {
    ElMessage.warning("无法删除:缺少新闻 ID");
    return;
  }
  const title = (row.title || "").trim() || "该条新闻";
  try {
    await ElMessageBox.confirm(
      `确定要删除「${title}」吗?删除后不可恢复。`,
      "删除确认",
      {
        type: "warning",
        confirmButtonText: "确定删除",
        cancelButtonText: "取消",
        distinguishCancelAndClose: true,
        autofocus: false,
      }
    );
  } catch {
    return;
  }
  try {
    await deleteEnterpriseNews([row.id]);
    ElMessage.success("删除成功");
    await fetchNewsList(searchForm);
  } catch {
    /* 错误由请求拦截器提示 */
  }
}
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: `/mock/${name}` });
  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" ? "已提交审核" : "已保存");
  }
}
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 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);
  const v = validateNewsForm(newsForm);
  if (!v.ok) {
    ElMessage.warning(v.message);
    return;
  }
  if (ret?.ok) ElMessage.success(`已向 ${ret.count} 名员工发送阅读提醒`);
  const status = action === "draft" ? "DRAFT" : "PENDING";
  newsForm.publishStatus = status;
  if (!isNewsEdit.value) {
    const templateId = newsForm.templateId || submitForm.templateId;
    if (!templateId) {
      ElMessage.warning("请先选择审批模板");
      return;
    }
    if (!newsForm.templateId) newsForm.templateId = templateId;
    if (!newsForm.templateName && submitForm.templateName) {
      newsForm.templateName = submitForm.templateName;
    }
    if (action !== "draft") {
      const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
      if (!bindingCheck.ok) {
        ElMessage.warning(bindingCheck.message);
        return;
      }
    }
  } else if (!newsForm.templateId && submitForm.templateId) {
    newsForm.templateId = submitForm.templateId;
    newsForm.templateName = submitForm.templateName || newsForm.templateName;
  }
  const dto = buildEnterpriseNewsSaveDto(newsForm, { status });
  if (isNewsEdit.value) {
    if (dto.id == null) {
      ElMessage.warning("无法修改:缺少新闻 ID");
      return;
    }
  }
  if (newsSaving.value) return;
  newsSaving.value = true;
  try {
    if (isNewsEdit.value) {
      await updateEnterpriseNews(dto);
    } else {
      await saveEnterpriseNews(dto);
    }
    newsFormDialog.visible = false;
    const msg =
      action === "draft" ? "已保存草稿" : isNewsEdit.value ? "修改成功" : "已提交审核";
    ElMessage.success(msg);
    if (!isNewsEdit.value) page.current = 1;
    await fetchNewsList(searchForm);
  } catch {
    /* 错误由请求拦截器提示 */
  } finally {
    newsSaving.value = false;
  }
}
function previewVersion(ver) {
  versionPreview.data = ver;
  versionPreview.visible = true;
function onSearch() {
  fetchNewsList(searchForm);
}
function resetSearch() {
  searchForm.keyword = "";
  searchForm.newsType = "";
  searchForm.status = "";
  searchForm.createTimeRange = null;
  onSearch();
}
function onPagination(obj) {
  paginateNewsList(obj, searchForm);
}
onMounted(() => {
  handleQuery();
  loadFlowUsers();
  fetchNewsList(searchForm);
});
</script>
@@ -421,6 +540,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 +551,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;