<!--OA模块:EnterpriseNews 企业新闻-->
|
<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="标题 / 编号 / 摘要"
|
clearable
|
:prefix-icon="Search"
|
@keyup.enter="handleQuery"
|
/>
|
<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>
|
<span class="search_title" style="margin-left: 12px">发布时间:</span>
|
<el-date-picker
|
v-model="searchForm.publishTimeRange"
|
type="daterange"
|
range-separator="-"
|
start-placeholder="开始"
|
end-placeholder="结束"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
style="width: 260px"
|
clearable
|
/>
|
<el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">搜索</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>
|
</div>
|
</div>
|
|
<div class="table_list">
|
<PIMTable
|
rowKey="id"
|
:column="tableColumn"
|
:tableData="tableData"
|
:page="page"
|
:isSelection="false"
|
:tableLoading="tableLoading"
|
:total="page.total"
|
@pagination="pagination"
|
>
|
<template #newsType="{ row }">
|
<span class="news-type-tag" :style="{ color: newsTypeColor(row.newsType) }">
|
{{ newsTypeLabel(row.newsType) }}
|
</span>
|
</template>
|
</PIMTable>
|
</div>
|
|
<!-- 新建 / 编辑 -->
|
<el-dialog
|
v-model="formDialog.visible"
|
:title="formDialog.title"
|
width="960px"
|
append-to-body
|
destroy-on-close
|
class="news-form-dialog"
|
@closed="formRef?.resetFields?.()"
|
>
|
<el-form
|
ref="formRef"
|
:model="form"
|
:rules="formRules"
|
label-width="110px"
|
:disabled="formDialog.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-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-option
|
v-for="opt in LAYOUT_TEMPLATE_OPTIONS"
|
:key="opt.value"
|
:label="opt.label"
|
:value="opt.value"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-form-item label="标题" prop="title">
|
<el-input v-model="form.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-form-item>
|
<el-form-item label="正文" prop="contentHtml">
|
<Editor v-model="form.contentHtml" :min-height="280" />
|
</el-form-item>
|
<el-form-item label="附件">
|
<FileUpload v-model:file-list="form.attachmentList" :limit="10" button-text="上传 PDF / 文档" />
|
</el-form-item>
|
<el-form-item v-if="form.layoutTemplate === 'gallery'" label="图集/视频">
|
<el-input
|
v-model="galleryInput"
|
placeholder="输入资源名称后回车添加(演示)"
|
@keyup.enter="addGalleryItem"
|
/>
|
<el-tag
|
v-for="(m, i) in form.mediaList"
|
:key="i"
|
closable
|
class="media-tag"
|
@close="form.mediaList.splice(i, 1)"
|
>
|
{{ m.name }}
|
</el-tag>
|
</el-form-item>
|
|
<el-divider content-position="left">权限管控</el-divider>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="编辑角色">
|
<el-select v-model="form.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-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 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-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-form-item>
|
<el-form-item label="发布人">
|
<el-input v-model="form.publisherName" placeholder="如:人力资源部" maxlength="50" />
|
</el-form-item>
|
</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>
|
</el-dialog>
|
|
<!-- 详情 -->
|
<el-dialog v-model="detailDialog.visible" title="新闻详情" width="880px" append-to-body destroy-on-close>
|
<NewsDetailPanel
|
:row="detailRow"
|
@like="onDetailLike"
|
@comment="onDetailComment"
|
/>
|
<template #footer>
|
<el-button
|
v-if="detailRow.publishStatus === 'published' && getUnreadEmployees(detailRow).length"
|
type="warning"
|
@click="openUnreadFromDetail"
|
>
|
未读提醒
|
</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 { 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 NewsDetailPanel from "./components/NewsDetailPanel.vue";
|
import { useEnterpriseNews } from "./useEnterpriseNews.js";
|
|
const {
|
Search,
|
NEWS_TYPE_OPTIONS,
|
PUBLISH_STATUS_OPTIONS,
|
LAYOUT_TEMPLATE_OPTIONS,
|
READ_SCOPE_OPTIONS,
|
PUBLISH_ROLE_OPTIONS,
|
DEPT_OPTIONS,
|
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();
|
|
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));
|
});
|
|
function addGalleryItem() {
|
const name = (galleryInput.value || "").trim();
|
if (!name) return;
|
form.mediaList = form.mediaList || [];
|
form.mediaList.push({ type: "image", name, url: `/mock/${name}` });
|
galleryInput.value = "";
|
}
|
|
function onSave(action) {
|
const ret = saveForm(action);
|
if (ret?.message) {
|
ElMessage.warning(ret.message);
|
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);
|
return;
|
}
|
if (ret?.ok) ElMessage.success(`已向 ${ret.count} 名员工发送阅读提醒`);
|
}
|
|
function previewVersion(ver) {
|
versionPreview.data = ver;
|
versionPreview.visible = true;
|
}
|
|
onMounted(() => {
|
handleQuery();
|
});
|
</script>
|
|
<style scoped>
|
.enterprise-news-page .search_form {
|
display: flex;
|
flex-wrap: wrap;
|
justify-content: space-between;
|
align-items: flex-start;
|
gap: 12px;
|
}
|
.search_fields {
|
display: flex;
|
flex-wrap: wrap;
|
align-items: center;
|
gap: 4px;
|
}
|
.search_actions {
|
flex-shrink: 0;
|
}
|
.news-type-tag {
|
font-weight: 600;
|
font-size: 13px;
|
}
|
.media-tag {
|
margin: 6px 8px 0 0;
|
}
|
.unread-toolbar {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
}
|
.unread-stat {
|
color: var(--el-text-color-secondary);
|
font-size: 13px;
|
}
|
.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;
|
}
|
.ml10 {
|
margin-left: 10px;
|
}
|
</style>
|