| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- EnterpriseNewsï¼è¯¦æ
åªè¯»é¢æ¿ï¼å«äºå¨ï¼ --> |
| | | <template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="æ°é»ç¼å·">{{ row.newsNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå¸ç¶æ"> |
| | | <el-tag :type="publishStatusTag(row.publishStatus)" size="small"> |
| | | {{ publishStatusLabel(row.publishStatus) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ°é»åç±»"> |
| | | <span class="type-badge" :style="{ color: newsTypeColor(row.newsType) }"> |
| | | {{ newsTypeLabel(row.newsType) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æç模æ¿">{{ layoutTemplateLabel(row.layoutTemplate) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ é¢" :span="2">{{ row.title || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æè¦" :span="2">{{ row.summary || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="é
读èå´">{{ readScopeLabel(row.readScope) }}</el-descriptions-item> |
| | | <el-descriptions-item label="é
读ç"> |
| | | {{ readRate(row) }}%ï¼æªè¯» {{ unreadCount }} äººï¼ |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ç¼è¾æé">{{ publishRoleLabel(row.editorRole) }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸è§è²">{{ publishRoleLabel(row.reviewerRole) }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå¸äºº">{{ row.publisherName || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å叿¶é´">{{ row.publishTime || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½åçæ¬">v{{ row.versionNo || 1 }}</el-descriptions-item> |
| | | <el-descriptions-item label="éé
读确认"> |
| | | {{ row.requireReadConfirm ? "æ¯" : "å¦" }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-divider content-position="left">æ£æå
容</el-divider> |
| | | <div v-if="row.contentHtml" class="news-html-body" v-html="row.contentHtml" /> |
| | | <el-empty v-else description="ææ æ£æ" :image-size="48" /> |
| | | |
| | | <template v-if="row.mediaList?.length"> |
| | | <el-divider content-position="left">å¾é / è§é¢</el-divider> |
| | | <div class="media-grid"> |
| | | <div v-for="(m, i) in row.mediaList" :key="i" class="media-item"> |
| | | <el-tag size="small" type="info">{{ m.type === "video" ? "è§é¢" : "å¾ç" }}</el-tag> |
| | | <span class="media-name">{{ m.name }}</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-divider content-position="left">éä»¶</el-divider> |
| | | <template v-if="row.attachmentList?.length"> |
| | | <el-tag |
| | | v-for="(f, i) in row.attachmentList" |
| | | :key="i" |
| | | class="file-tag" |
| | | type="info" |
| | | @click="openFile(f)" |
| | | > |
| | | {{ f.name }} |
| | | </el-tag> |
| | | </template> |
| | | <el-empty v-else description="ææ éä»¶" :image-size="48" /> |
| | | |
| | | <template v-if="row.newsType === 'culture' && row.publishStatus === 'published'"> |
| | | <el-divider content-position="left">äºå¨ï¼ç¹èµ {{ likeCount }} · è¯è®º {{ commentCount }}ï¼</el-divider> |
| | | <div class="interaction-bar"> |
| | | <el-button type="primary" plain size="small" @click="$emit('like')"> |
| | | {{ likedByMe ? "åæ¶ç¹èµ" : "ç¹èµ" }} |
| | | </el-button> |
| | | </div> |
| | | <el-input |
| | | v-model="commentDraft" |
| | | type="textarea" |
| | | :rows="2" |
| | | maxlength="300" |
| | | show-word-limit |
| | | placeholder="åä¸ä½ çè¯è®ºâ¦" |
| | | class="mb8" |
| | | /> |
| | | <el-button type="primary" size="small" @click="submitComment">å表è¯è®º</el-button> |
| | | <el-timeline v-if="row.comments?.length" class="comment-timeline mt12"> |
| | | <el-timeline-item v-for="c in row.comments" :key="c.id" :timestamp="c.time"> |
| | | <strong>{{ c.name }}</strong>ï¼{{ c.content }} |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ è¯è®º" :image-size="40" /> |
| | | </template> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { |
| | | newsTypeLabel, |
| | | newsTypeColor, |
| | | publishStatusLabel, |
| | | publishStatusTag, |
| | | layoutTemplateLabel, |
| | | readScopeLabel, |
| | | publishRoleLabel, |
| | | readRate, |
| | | getUnreadEmployees, |
| | | } from "../enterpriseNewsUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["like", "comment"]); |
| | | |
| | | const commentDraft = ref(""); |
| | | |
| | | const unreadCount = computed(() => getUnreadEmployees(props.row).length); |
| | | const likeCount = computed(() => props.row?.likes?.length || 0); |
| | | const commentCount = computed(() => props.row?.comments?.length || 0); |
| | | const likedByMe = computed(() => (props.row?.likes || []).some((l) => l.userId === "u1")); |
| | | |
| | | function openFile(f) { |
| | | const url = f?.url || f?.downloadURL; |
| | | if (url) window.open(url, "_blank"); |
| | | } |
| | | |
| | | function submitComment() { |
| | | emit("comment", commentDraft.value); |
| | | commentDraft.value = ""; |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .type-badge { |
| | | font-weight: 600; |
| | | } |
| | | .news-html-body { |
| | | padding: 12px 16px; |
| | | background: var(--el-fill-color-light); |
| | | border-radius: 6px; |
| | | line-height: 1.7; |
| | | max-height: 320px; |
| | | overflow-y: auto; |
| | | } |
| | | .media-grid { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 12px; |
| | | } |
| | | .media-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 8px 12px; |
| | | background: var(--el-fill-color-lighter); |
| | | border-radius: 4px; |
| | | } |
| | | .media-name { |
| | | font-size: 13px; |
| | | } |
| | | .file-tag { |
| | | margin: 0 8px 8px 0; |
| | | cursor: pointer; |
| | | } |
| | | .interaction-bar { |
| | | margin-bottom: 8px; |
| | | } |
| | | .comment-timeline { |
| | | max-height: 200px; |
| | | overflow-y: auto; |
| | | } |
| | | .mb8 { |
| | | margin-bottom: 8px; |
| | | } |
| | | .mt12 { |
| | | margin-top: 12px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | /** æ°é»åç±»ï¼ç»ä¸ä¿¡æ¯åºå£ */ |
| | | export const NEWS_TYPE_OPTIONS = [ |
| | | { value: "announcement", label: "ä¼ä¸å
Œ", color: "#409eff" }, |
| | | { value: "policy", label: "æ¿ç解读", color: "#e6a23c" }, |
| | | { value: "industry", label: "è¡ä¸å¨æ", color: "#909399" }, |
| | | { value: "culture", label: "æåæ´»å¨", color: "#67c23a" }, |
| | | ]; |
| | | |
| | | /** åå¸ç¶æ */ |
| | | export const PUBLISH_STATUS_OPTIONS = [ |
| | | { value: "draft", label: "è稿", tag: "info" }, |
| | | { value: "pending_review", label: "å¾
å®¡æ ¸", tag: "warning" }, |
| | | { value: "published", label: "å·²åå¸", tag: "success" }, |
| | | { value: "archived", label: "已彿¡£", tag: "" }, |
| | | ]; |
| | | |
| | | /** æçæ¨¡æ¿ */ |
| | | export const LAYOUT_TEMPLATE_OPTIONS = [ |
| | | { value: "standard", label: "æ å徿" }, |
| | | { value: "policy", label: "æ¿çæ¡æ" }, |
| | | { value: "gallery", label: "å¾éç¸å" }, |
| | | { value: "briefing", label: "ç®æ¥æè¦" }, |
| | | ]; |
| | | |
| | | /** é
读å¯è§èå´ */ |
| | | export const READ_SCOPE_OPTIONS = [ |
| | | { value: "all", label: "å
¨åå¯è§" }, |
| | | { value: "management", label: "管çå±" }, |
| | | { value: "department", label: "æå®é¨é¨" }, |
| | | { value: "custom", label: "èªå®ä¹åå" }, |
| | | ]; |
| | | |
| | | /** ç¼è¾/å®¡æ ¸è§è²ï¼å叿éï¼ */ |
| | | export const PUBLISH_ROLE_OPTIONS = [ |
| | | { value: "hr", label: "HRï¼äººäºæ¿çï¼" }, |
| | | { value: "admin", label: "管çåï¼å¤é¨æ°é»å®¡æ ¸ï¼" }, |
| | | { value: "dept_manager", label: "é¨é¨è´è´£äºº" }, |
| | | { value: "editor", label: "å
容ç¼è¾" }, |
| | | ]; |
| | | |
| | | export const STORAGE_KEY = "oa_enterprise_news_v1"; |
| | | |
| | | /** æ¼ç¤ºç¨ç®æ åä¼ï¼åæå¯¹æ¥ç»ç»æ¶æï¼ */ |
| | | export const MOCK_AUDIENCE = [ |
| | | { userId: "u1", employeeNo: "zhangsan", name: "å¼ ä¸", deptName: "ç åé¨", isManagement: false }, |
| | | { userId: "u2", employeeNo: "lisi", name: "æå", deptName: "ç åé¨", isManagement: false }, |
| | | { userId: "u3", employeeNo: "wangwu", name: "çäº", deptName: "è¡æ¿é¨", isManagement: false }, |
| | | { userId: "u4", employeeNo: "zhaoliu", name: "èµµå
", deptName: "éå®é¨", isManagement: false }, |
| | | { userId: "u5", employeeNo: "sunqi", name: "åä¸", deptName: "è´¢å¡é¨", isManagement: false }, |
| | | { userId: "u6", employeeNo: "zhouba", name: "å¨å
«", deptName: "æ»ç»å", isManagement: true }, |
| | | { userId: "u7", employeeNo: "wujiu", name: "å´ä¹", deptName: "æ»ç»å", isManagement: true }, |
| | | { userId: "u8", employeeNo: "zhengshi", name: "éå", deptName: "人åèµæºé¨", isManagement: false }, |
| | | ]; |
| | | |
| | | const DEPT_OPTIONS = [ |
| | | { value: "101", label: "ç åé¨" }, |
| | | { value: "102", label: "éå®é¨" }, |
| | | { value: "103", label: "è¡æ¿é¨" }, |
| | | { value: "104", label: "è´¢å¡é¨" }, |
| | | { value: "105", label: "æ»ç»å" }, |
| | | { value: "106", label: "人åèµæºé¨" }, |
| | | ]; |
| | | |
| | | export { DEPT_OPTIONS }; |
| | | |
| | | export function newsTypeLabel(v) { |
| | | return NEWS_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function newsTypeColor(v) { |
| | | return NEWS_TYPE_OPTIONS.find((x) => x.value === v)?.color || "#909399"; |
| | | } |
| | | |
| | | export function publishStatusLabel(v) { |
| | | return PUBLISH_STATUS_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function publishStatusTag(v) { |
| | | return PUBLISH_STATUS_OPTIONS.find((x) => x.value === v)?.tag || "info"; |
| | | } |
| | | |
| | | export function layoutTemplateLabel(v) { |
| | | return LAYOUT_TEMPLATE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function readScopeLabel(v) { |
| | | return READ_SCOPE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function publishRoleLabel(v) { |
| | | return PUBLISH_ROLE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function createEmptyForm() { |
| | | return { |
| | | id: "", |
| | | newsNo: "", |
| | | title: "", |
| | | summary: "", |
| | | newsType: "announcement", |
| | | layoutTemplate: "standard", |
| | | contentHtml: "", |
| | | coverImage: "", |
| | | mediaList: [], |
| | | attachmentList: [], |
| | | editorRole: "hr", |
| | | reviewerRole: "admin", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | targetUserIds: [], |
| | | publishStatus: "draft", |
| | | publisherName: "", |
| | | publishTime: "", |
| | | readRecords: [], |
| | | remindLogs: [], |
| | | likes: [], |
| | | comments: [], |
| | | versions: [], |
| | | versionNo: 1, |
| | | requireReadConfirm: false, |
| | | }; |
| | | } |
| | | |
| | | function buildReadRecords(readUserIds = []) { |
| | | const set = new Set(readUserIds); |
| | | return MOCK_AUDIENCE.map((u) => ({ |
| | | userId: u.userId, |
| | | employeeNo: u.employeeNo, |
| | | name: u.name, |
| | | deptName: u.deptName, |
| | | readAt: set.has(u.userId) ? dayjs().subtract(2, "day").format("YYYY-MM-DD HH:mm:ss") : "", |
| | | lastRemindAt: "", |
| | | })); |
| | | } |
| | | |
| | | function createVersionSnapshot(row, changeNote = "åå¸") { |
| | | return { |
| | | versionNo: row.versionNo || 1, |
| | | title: row.title, |
| | | summary: row.summary, |
| | | contentHtml: row.contentHtml, |
| | | newsType: row.newsType, |
| | | publishTime: row.publishTime || dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | archivedAt: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | changeNote, |
| | | publisherName: row.publisherName || "ç³»ç»", |
| | | }; |
| | | } |
| | | |
| | | export function createInitialMockNews() { |
| | | const policyContent = |
| | | "<p><strong>2026 å¹´èå¤ç®¡çå¶åº¦ï¼è¯è¡ï¼</strong></p><p>ä¸ãä¸çæ¶é´ 9:00ï¼å¼¹æ§æå¡çªå£ 8:30â9:30ã</p><p>äºã请åé¡»æåå¨ OA æäº¤å®¡æ¹ã</p><p>ä¸ãæ¬å¶åº¦èª 2026-06-01 èµ·æ§è¡ã</p>"; |
| | | const cultureContent = |
| | | "<p>2026 ä¼ä¸å¹´ä¼å满è½å¹ï¼æè°¢æ¯ä¸ä½åäºçåä¸ï¼ä»¥ä¸ä¸ºç²¾å½©ç¬é´å¾éã</p>"; |
| | | const strategyContent = |
| | | "<p><strong>2026 ä¸åå¹´æç¥æ¹åï¼å
é¨ï¼</strong></p><p>èç¦æ ¸å¿äº§å线åçº§ä¸æµ·å¤å¸åºæå±ï¼å
·ä½ææ è§éä»¶ã</p>"; |
| | | |
| | | const policyRow = { |
| | | id: "news_1", |
| | | newsNo: "EN202605150001", |
| | | title: "å
³äºå叿°èå¤å¶åº¦çéç¥", |
| | | summary: "请å
¨ä½å工认çé
è¯»å¹¶ç¡®è®¤ç¥æï¼èª 2026-06-01 èµ·æ§è¡ã", |
| | | newsType: "policy", |
| | | layoutTemplate: "policy", |
| | | contentHtml: policyContent, |
| | | coverImage: "", |
| | | mediaList: [], |
| | | attachmentList: [{ name: "èå¤å¶åº¦2026.pdf", url: "/mock/attendance-policy.pdf" }], |
| | | editorRole: "hr", |
| | | reviewerRole: "admin", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | targetUserIds: [], |
| | | publishStatus: "published", |
| | | publisherName: "人åèµæºé¨", |
| | | publishTime: "2026-05-15 10:00:00", |
| | | readRecords: buildReadRecords(["u6", "u7", "u8"]), |
| | | remindLogs: [], |
| | | likes: [], |
| | | comments: [], |
| | | versions: [ |
| | | { |
| | | versionNo: 1, |
| | | title: "å
³äºå叿°èå¤å¶åº¦çéç¥ï¼å¾æ±æè§ç¨¿ï¼", |
| | | summary: "徿±æè§ç¨¿", |
| | | contentHtml: "<p>徿±æè§ç¨¿ï¼ä¸çæ¶é´ 9:00â¦â¦</p>", |
| | | newsType: "policy", |
| | | publishTime: "2026-05-10 09:00:00", |
| | | archivedAt: "2026-05-15 09:55:00", |
| | | changeNote: "å®ç¨¿åå¸", |
| | | publisherName: "人åèµæºé¨", |
| | | }, |
| | | ], |
| | | versionNo: 2, |
| | | requireReadConfirm: true, |
| | | createTime: "2026-05-10 09:00:00", |
| | | updateTime: "2026-05-15 10:00:00", |
| | | }; |
| | | |
| | | const cultureRow = { |
| | | id: "news_2", |
| | | newsNo: "EN202605200002", |
| | | title: "2026 ä¼ä¸å¹´ä¼ç²¾å½©ç¬é´", |
| | | summary: "å¹´ä¼å¾éä¸çº¿ï¼æ¬¢è¿ç¹èµçè¨ï¼å
±å»ºä¼ä¸æåã", |
| | | newsType: "culture", |
| | | layoutTemplate: "gallery", |
| | | contentHtml: cultureContent, |
| | | coverImage: "/mock/annual-cover.jpg", |
| | | mediaList: [ |
| | | { type: "image", name: "å¼åº.jpg", url: "/mock/annual-1.jpg" }, |
| | | { type: "image", name: "é¢å¥.jpg", url: "/mock/annual-2.jpg" }, |
| | | { type: "video", name: "å¹´ä¼è±çµ®.mp4", url: "/mock/annual.mp4" }, |
| | | ], |
| | | attachmentList: [], |
| | | editorRole: "dept_manager", |
| | | reviewerRole: "admin", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | targetUserIds: [], |
| | | publishStatus: "published", |
| | | publisherName: "è¡æ¿é¨", |
| | | publishTime: "2026-05-20 14:30:00", |
| | | readRecords: buildReadRecords(["u1", "u2", "u3", "u4", "u5", "u6", "u7"]), |
| | | remindLogs: [], |
| | | likes: [ |
| | | { userId: "u1", name: "å¼ ä¸", time: "2026-05-20 15:01:00" }, |
| | | { userId: "u2", name: "æå", time: "2026-05-20 15:05:00" }, |
| | | { userId: "u4", name: "èµµå
", time: "2026-05-20 16:20:00" }, |
| | | ], |
| | | comments: [ |
| | | { id: "c1", userId: "u1", name: "å¼ ä¸", content: "èç®å¤ªç²¾å½©äºï¼", time: "2026-05-20 15:10:00" }, |
| | | { id: "c2", userId: "u3", name: "çäº", content: "æå¾
æå¹´åèï¼", time: "2026-05-20 17:00:00" }, |
| | | ], |
| | | versions: [], |
| | | versionNo: 1, |
| | | requireReadConfirm: false, |
| | | createTime: "2026-05-20 14:00:00", |
| | | updateTime: "2026-05-20 14:30:00", |
| | | }; |
| | | |
| | | const strategyRow = { |
| | | id: "news_3", |
| | | newsNo: "EN202605220003", |
| | | title: "2026 ä¸åå¹´æç¥è§åè¦ç¹", |
| | | summary: "ä»
é管çå±é
读ï¼è¯·å¿å¯¹å¤ä¼ æã", |
| | | newsType: "announcement", |
| | | layoutTemplate: "briefing", |
| | | contentHtml: strategyContent, |
| | | coverImage: "", |
| | | mediaList: [], |
| | | attachmentList: [{ name: "æç¥ææ .pdf", url: "/mock/strategy.pdf" }], |
| | | editorRole: "admin", |
| | | reviewerRole: "admin", |
| | | readScope: "management", |
| | | targetDeptIds: [], |
| | | targetUserIds: [], |
| | | publishStatus: "published", |
| | | publisherName: "æ»ç»å", |
| | | publishTime: "2026-05-22 09:00:00", |
| | | readRecords: buildReadRecords(["u6", "u7"]), |
| | | remindLogs: [], |
| | | likes: [], |
| | | comments: [], |
| | | versions: [], |
| | | versionNo: 1, |
| | | requireReadConfirm: false, |
| | | createTime: "2026-05-22 08:30:00", |
| | | updateTime: "2026-05-22 09:00:00", |
| | | }; |
| | | |
| | | const industryDraft = { |
| | | id: "news_4", |
| | | newsNo: "EN202605250004", |
| | | title: "å¶é 䏿°åå转åè¶å¿ç®æ¥", |
| | | summary: "è¡ä¸å¨æè稿ï¼å¾
管çåå®¡æ ¸ååå¸ã", |
| | | newsType: "industry", |
| | | layoutTemplate: "standard", |
| | | contentHtml: "<p>æ¬æç®æ¥æ¢³çå·¥ä¸äºèç½ä¸ AI è´¨æ£åºç¨æ¡ä¾â¦â¦</p>", |
| | | coverImage: "", |
| | | mediaList: [], |
| | | attachmentList: [], |
| | | editorRole: "editor", |
| | | reviewerRole: "admin", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | targetUserIds: [], |
| | | publishStatus: "pending_review", |
| | | publisherName: "å¸åºé¨", |
| | | publishTime: "", |
| | | readRecords: [], |
| | | remindLogs: [], |
| | | likes: [], |
| | | comments: [], |
| | | versions: [], |
| | | versionNo: 1, |
| | | requireReadConfirm: false, |
| | | createTime: "2026-05-25 11:00:00", |
| | | updateTime: "2026-05-25 11:00:00", |
| | | }; |
| | | |
| | | return [policyRow, cultureRow, strategyRow, industryDraft]; |
| | | } |
| | | |
| | | export function loadStoredNews() { |
| | | try { |
| | | const raw = localStorage.getItem(STORAGE_KEY); |
| | | if (!raw) return null; |
| | | const data = JSON.parse(raw); |
| | | return Array.isArray(data) ? data : null; |
| | | } catch { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | export function saveStoredNews(rows) { |
| | | try { |
| | | localStorage.setItem(STORAGE_KEY, JSON.stringify(rows)); |
| | | } catch { |
| | | /* ignore */ |
| | | } |
| | | } |
| | | |
| | | /** æé
读èå´è§£æç®æ åä¼ */ |
| | | export function resolveTargetAudience(row) { |
| | | const scope = row.readScope || "all"; |
| | | if (scope === "management") { |
| | | return MOCK_AUDIENCE.filter((u) => u.isManagement); |
| | | } |
| | | if (scope === "department" && row.targetDeptIds?.length) { |
| | | const names = DEPT_OPTIONS.filter((d) => row.targetDeptIds.includes(d.value)).map((d) => d.label); |
| | | return MOCK_AUDIENCE.filter((u) => names.includes(u.deptName)); |
| | | } |
| | | if (scope === "custom" && row.targetUserIds?.length) { |
| | | return MOCK_AUDIENCE.filter((u) => row.targetUserIds.includes(u.userId)); |
| | | } |
| | | return [...MOCK_AUDIENCE]; |
| | | } |
| | | |
| | | export function getUnreadEmployees(row) { |
| | | const audience = resolveTargetAudience(row); |
| | | const readSet = new Set( |
| | | (row.readRecords || []).filter((r) => r.readAt).map((r) => r.userId) |
| | | ); |
| | | return audience.filter((u) => !readSet.has(u.userId)); |
| | | } |
| | | |
| | | export function readRate(row) { |
| | | const audience = resolveTargetAudience(row); |
| | | if (!audience.length) return 0; |
| | | const readCount = (row.readRecords || []).filter((r) => r.readAt).length; |
| | | return Math.round((readCount / audience.length) * 100); |
| | | } |
| | | |
| | | export function nextNewsNo() { |
| | | return `EN${dayjs().format("YYYYMMDD")}${String(Math.floor(Math.random() * 9000) + 1000)}`; |
| | | } |
| | | |
| | | export function pushVersionBeforeUpdate(row, changeNote) { |
| | | const versions = row.versions || []; |
| | | versions.unshift(createVersionSnapshot(row, changeNote)); |
| | | row.versions = versions; |
| | | row.versionNo = (row.versionNo || 1) + 1; |
| | | } |
| | | |
| | | export function validateNewsForm(form) { |
| | | const title = (form.title || "").trim(); |
| | | if (!title) return { ok: false, message: "è¯·å¡«åæ°é»æ é¢" }; |
| | | if (!form.newsType) return { ok: false, message: "è¯·éæ©æ°é»åç±»" }; |
| | | if (form.readScope === "department" && !(form.targetDeptIds || []).length) { |
| | | return { ok: false, message: "è¯·éæ©å¯è§é¨é¨" }; |
| | | } |
| | | return { ok: true, title }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { |
| | | NEWS_TYPE_OPTIONS, |
| | | PUBLISH_STATUS_OPTIONS, |
| | | LAYOUT_TEMPLATE_OPTIONS, |
| | | READ_SCOPE_OPTIONS, |
| | | PUBLISH_ROLE_OPTIONS, |
| | | DEPT_OPTIONS, |
| | | createEmptyForm, |
| | | createInitialMockNews, |
| | | loadStoredNews, |
| | | saveStoredNews, |
| | | getUnreadEmployees, |
| | | readRate, |
| | | nextNewsNo, |
| | | pushVersionBeforeUpdate, |
| | | validateNewsForm, |
| | | newsTypeLabel, |
| | | publishStatusLabel, |
| | | } from "./enterpriseNewsUtils.js"; |
| | | |
| | | export function useEnterpriseNews() { |
| | | const stored = loadStoredNews(); |
| | | const allRows = ref(stored?.length ? stored : createInitialMockNews()); |
| | | |
| | | const searchForm = reactive({ |
| | | keyword: "", |
| | | newsType: "", |
| | | publishStatus: "", |
| | | publishTimeRange: [], |
| | | }); |
| | | |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | |
| | | const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false }); |
| | | const form = reactive(createEmptyForm()); |
| | | const formRef = ref(); |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | |
| | | const unreadDialog = reactive({ visible: false, row: null }); |
| | | const unreadSelection = ref([]); |
| | | |
| | | const versionDialog = reactive({ visible: false, row: null }); |
| | | |
| | | const filteredList = computed(() => { |
| | | let list = [...allRows.value]; |
| | | const kw = (searchForm.keyword || "").trim().toLowerCase(); |
| | | if (kw) { |
| | | list = list.filter((r) => { |
| | | const title = (r.title || "").toLowerCase(); |
| | | const summary = (r.summary || "").toLowerCase(); |
| | | const no = (r.newsNo || "").toLowerCase(); |
| | | return title.includes(kw) || summary.includes(kw) || no.includes(kw); |
| | | }); |
| | | } |
| | | if (searchForm.newsType) { |
| | | list = list.filter((r) => r.newsType === searchForm.newsType); |
| | | } |
| | | if (searchForm.publishStatus) { |
| | | list = list.filter((r) => r.publishStatus === searchForm.publishStatus); |
| | | } |
| | | const range = searchForm.publishTimeRange; |
| | | if (range?.length === 2 && range[0] && range[1]) { |
| | | const start = dayjs(range[0]).startOf("day"); |
| | | const end = dayjs(range[1]).endOf("day"); |
| | | list = list.filter((r) => { |
| | | if (!r.publishTime) return false; |
| | | const t = dayjs(r.publishTime); |
| | | return t.isAfter(start) && t.isBefore(end); |
| | | }); |
| | | } |
| | | return list.sort((a, b) => (String(a.updateTime) < String(b.updateTime) ? 1 : -1)); |
| | | }); |
| | | |
| | | watch( |
| | | filteredList, |
| | | (list) => { |
| | | page.total = list.length; |
| | | const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | const tableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return filteredList.value.slice(start, start + page.size); |
| | | }); |
| | | |
| | | const unreadList = computed(() => { |
| | | if (!unreadDialog.row) return []; |
| | | return getUnreadEmployees(unreadDialog.row); |
| | | }); |
| | | |
| | | const formRules = { |
| | | title: [{ required: true, message: "请è¾å
¥æ°é»æ é¢", trigger: "blur" }], |
| | | newsType: [{ required: true, message: "è¯·éæ©æ°é»åç±»", trigger: "change" }], |
| | | readScope: [{ required: true, message: "è¯·éæ©é
读èå´", trigger: "change" }], |
| | | }; |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "ç¼å·", prop: "newsNo", width: 150 }, |
| | | { label: "æ é¢", prop: "title", minWidth: 180, showOverflowTooltip: true }, |
| | | { |
| | | label: "åç±»", |
| | | prop: "newsType", |
| | | width: 100, |
| | | dataType: "slot", |
| | | slot: "newsType", |
| | | }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "publishStatus", |
| | | width: 90, |
| | | dataType: "tag", |
| | | formatData: (v) => publishStatusLabel(v), |
| | | formatType: (v) => { |
| | | const hit = PUBLISH_STATUS_OPTIONS.find((x) => x.value === v); |
| | | return hit?.tag || "info"; |
| | | }, |
| | | }, |
| | | { |
| | | label: "é
读ç", |
| | | prop: "readRecords", |
| | | width: 90, |
| | | align: "center", |
| | | formatData: (_, row) => `${readRate(row)}%`, |
| | | }, |
| | | { |
| | | label: "æªè¯»", |
| | | prop: "id", |
| | | width: 70, |
| | | align: "center", |
| | | formatData: (_, row) => { |
| | | if (row.publishStatus !== "published") return "â"; |
| | | return getUnreadEmployees(row).length; |
| | | }, |
| | | }, |
| | | { label: "åå¸äºº", prop: "publisherName", width: 110 }, |
| | | { label: "å叿¶é´", prop: "publishTime", width: 170 }, |
| | | { label: "æ´æ°æ¶é´", prop: "updateTime", width: 170 }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 280, |
| | | operation: [ |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | disabled: (row) => row.publishStatus === "archived", |
| | | clickFun: (row) => openFormDialog("edit", row), |
| | | }, |
| | | { |
| | | name: "å®¡æ ¸", |
| | | type: "text", |
| | | disabled: (row) => row.publishStatus !== "pending_review", |
| | | clickFun: (row) => openReview(row), |
| | | }, |
| | | { |
| | | name: "æªè¯»æé", |
| | | type: "text", |
| | | disabled: (row) => |
| | | row.publishStatus !== "published" || getUnreadEmployees(row).length === 0, |
| | | clickFun: (row) => openUnreadRemind(row), |
| | | }, |
| | | { name: "çæ¬çè¯", type: "text", clickFun: (row) => openVersionHistory(row) }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | function persist() { |
| | | saveStoredNews(allRows.value); |
| | | } |
| | | |
| | | function handleQuery() { |
| | | tableLoading.value = true; |
| | | page.current = 1; |
| | | setTimeout(() => { |
| | | tableLoading.value = false; |
| | | }, 200); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.keyword = ""; |
| | | searchForm.newsType = ""; |
| | | searchForm.publishStatus = ""; |
| | | searchForm.publishTimeRange = []; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function pagination({ page: p, limit }) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | } |
| | | |
| | | function resetForm(target = createEmptyForm()) { |
| | | Object.assign(form, createEmptyForm(), target); |
| | | } |
| | | |
| | | function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.readonly = mode === "view"; |
| | | formDialog.title = |
| | | mode === "add" ? "æ°å»ºä¼ä¸æ°é»" : mode === "edit" ? "ç¼è¾ä¼ä¸æ°é»" : "æ¥çä¼ä¸æ°é»"; |
| | | if (mode === "add") { |
| | | resetForm({ publisherName: "å½åç¨æ·" }); |
| | | } else { |
| | | resetForm({ |
| | | ...JSON.parse(JSON.stringify(row)), |
| | | targetDeptIds: [...(row.targetDeptIds || [])], |
| | | targetUserIds: [...(row.targetUserIds || [])], |
| | | mediaList: [...(row.mediaList || [])], |
| | | attachmentList: [...(row.attachmentList || [])], |
| | | }); |
| | | } |
| | | formDialog.visible = true; |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function openUnreadRemind(row) { |
| | | unreadDialog.row = row; |
| | | unreadSelection.value = []; |
| | | unreadDialog.visible = true; |
| | | } |
| | | |
| | | function openVersionHistory(row) { |
| | | versionDialog.row = row; |
| | | versionDialog.visible = true; |
| | | } |
| | | |
| | | async function openReview(row) { |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `ç¡®è®¤å®¡æ ¸éè¿å¹¶åå¸ã${row.title}ãï¼å¤é¨/è¡ä¸ç±»æ°é»é管çåå®¡æ ¸ã`, |
| | | "å®¡æ ¸åå¸", |
| | | { type: "warning", confirmButtonText: "éè¿å¹¶åå¸", cancelButtonText: "åæ¶" } |
| | | ); |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (!hit) return; |
| | | hit.publishStatus = "published"; |
| | | hit.publishTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | hit.updateTime = hit.publishTime; |
| | | if (!hit.readRecords?.length) { |
| | | hit.readRecords = []; |
| | | } |
| | | persist(); |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | function saveForm(submitAction = "save") { |
| | | const v = validateNewsForm(form); |
| | | if (!v.ok) return { ok: false, message: v.message }; |
| | | |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | const payload = { |
| | | ...JSON.parse(JSON.stringify(form)), |
| | | title: v.title, |
| | | updateTime: now, |
| | | }; |
| | | |
| | | if (formDialog.mode === "add") { |
| | | payload.id = `news_${Date.now()}`; |
| | | payload.newsNo = nextNewsNo(); |
| | | payload.createTime = now; |
| | | if (submitAction === "submit_review") { |
| | | payload.publishStatus = "pending_review"; |
| | | } else if (submitAction === "publish") { |
| | | payload.publishStatus = "published"; |
| | | payload.publishTime = now; |
| | | } else { |
| | | payload.publishStatus = "draft"; |
| | | } |
| | | allRows.value.unshift(payload); |
| | | } else { |
| | | const idx = allRows.value.findIndex((r) => r.id === form.id); |
| | | if (idx < 0) return { ok: false, message: "è®°å½ä¸åå¨" }; |
| | | const prev = allRows.value[idx]; |
| | | if (prev.publishStatus === "published" && submitAction !== "draft") { |
| | | pushVersionBeforeUpdate(prev, submitAction === "publish" ? "修订åå¸" : "å
å®¹æ´æ°"); |
| | | } |
| | | if (submitAction === "submit_review") { |
| | | payload.publishStatus = "pending_review"; |
| | | } else if (submitAction === "publish") { |
| | | payload.publishStatus = "published"; |
| | | payload.publishTime = payload.publishTime || now; |
| | | } |
| | | payload.versions = prev.versions || []; |
| | | payload.versionNo = prev.versionNo || 1; |
| | | if (prev.publishStatus === "published" && submitAction === "publish") { |
| | | payload.versionNo = (prev.versionNo || 1) + 1; |
| | | } |
| | | allRows.value[idx] = { ...prev, ...payload }; |
| | | } |
| | | persist(); |
| | | formDialog.visible = false; |
| | | return { ok: true }; |
| | | } |
| | | |
| | | function archiveNews(row) { |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (hit) { |
| | | hit.publishStatus = "archived"; |
| | | hit.updateTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | persist(); |
| | | } |
| | | } |
| | | |
| | | function sendUnreadRemind(selectedIds) { |
| | | const row = unreadDialog.row; |
| | | if (!row || !selectedIds?.length) return { ok: false, message: "è¯·éæ©è¦æéçåå·¥" }; |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (!hit) return { ok: false }; |
| | | |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | hit.remindLogs = hit.remindLogs || []; |
| | | hit.remindLogs.push({ |
| | | time: now, |
| | | count: selectedIds.length, |
| | | operator: "HR", |
| | | userIds: [...selectedIds], |
| | | }); |
| | | |
| | | const records = hit.readRecords || []; |
| | | selectedIds.forEach((uid) => { |
| | | let rec = records.find((r) => r.userId === uid); |
| | | if (!rec) { |
| | | const emp = getUnreadEmployees(hit).find((e) => e.userId === uid); |
| | | rec = { |
| | | userId: uid, |
| | | employeeNo: emp?.employeeNo || "", |
| | | name: emp?.name || "", |
| | | deptName: emp?.deptName || "", |
| | | readAt: "", |
| | | lastRemindAt: now, |
| | | }; |
| | | records.push(rec); |
| | | } else { |
| | | rec.lastRemindAt = now; |
| | | } |
| | | }); |
| | | hit.readRecords = records; |
| | | hit.updateTime = now; |
| | | persist(); |
| | | unreadDialog.visible = false; |
| | | return { ok: true, count: selectedIds.length }; |
| | | } |
| | | |
| | | function toggleLike(row, userId = "u1", userName = "å¼ ä¸") { |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (!hit) return; |
| | | hit.likes = hit.likes || []; |
| | | const idx = hit.likes.findIndex((l) => l.userId === userId); |
| | | if (idx >= 0) { |
| | | hit.likes.splice(idx, 1); |
| | | } else { |
| | | hit.likes.push({ userId, name: userName, time: dayjs().format("YYYY-MM-DD HH:mm:ss") }); |
| | | } |
| | | persist(); |
| | | if (detailRow.value?.id === row.id) { |
| | | detailRow.value = { ...hit }; |
| | | } |
| | | } |
| | | |
| | | function addComment(row, content, userId = "u1", userName = "å¼ ä¸") { |
| | | const text = (content || "").trim(); |
| | | if (!text) return { ok: false, message: "请è¾å
¥è¯è®ºå
容" }; |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (!hit) return { ok: false }; |
| | | hit.comments = hit.comments || []; |
| | | hit.comments.push({ |
| | | id: `c_${Date.now()}`, |
| | | userId, |
| | | name: userName, |
| | | content: text, |
| | | time: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | persist(); |
| | | if (detailRow.value?.id === row.id) { |
| | | detailRow.value = { ...hit }; |
| | | } |
| | | return { ok: true }; |
| | | } |
| | | |
| | | return { |
| | | 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, |
| | | unreadSelection, |
| | | versionDialog, |
| | | getUnreadEmployees, |
| | | readRate, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | openFormDialog, |
| | | openDetail, |
| | | openUnreadRemind, |
| | | openVersionHistory, |
| | | openReview, |
| | | saveForm, |
| | | archiveNews, |
| | | sendUnreadRemind, |
| | | toggleLike, |
| | | addComment, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- NoticeAnnouncementï¼å
¬å详æ
åªè¯»é¢æ¿ --> |
| | | <template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="å
¬åç¼å·">{{ row.noticeNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå¸ç¶æ"> |
| | | <el-tag :type="statusTag" size="small">{{ statusText }}</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å
¬åç±»å"> |
| | | <span class="type-badge" :style="{ color: noticeTypeColor(row.noticeType) }"> |
| | | {{ noticeTypeLabel(row.noticeType) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ä¼å
级"> |
| | | <el-tag :type="priorityTag(row.priority)" size="small">{{ priorityLabel(row.priority) }}</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ é¢" :span="2">{{ row.title || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å叿¥æ">{{ row.publishDate || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="è¿ææ¥æ">{{ row.expireDate || "é¿æææ" }}</el-descriptions-item> |
| | | <el-descriptions-item label="é
读èå´">{{ readScopeLabel(row.readScope) }}</el-descriptions-item> |
| | | <el-descriptions-item label="éé
读确认">{{ row.requireReadConfirm ? "æ¯" : "å¦" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå¸äºº">{{ row.publisherName || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å叿¶é´">{{ row.publishTime || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="é
读é">{{ row.readCount ?? 0 }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-divider content-position="left">å
¬åå
容</el-divider> |
| | | <div v-if="row.priority === 'urgent'" class="urgent-banner"> |
| | | <el-alert title="ç´§æ¥éç¥" type="error" :closable="false" show-icon /> |
| | | </div> |
| | | <div v-if="row.contentHtml" class="notice-html-body" v-html="row.contentHtml" /> |
| | | <el-empty v-else description="ææ å
容" :image-size="48" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { |
| | | noticeTypeLabel, |
| | | noticeTypeColor, |
| | | priorityLabel, |
| | | priorityTag, |
| | | publishStatusLabel, |
| | | publishStatusTag, |
| | | readScopeLabel, |
| | | isExpired, |
| | | } from "../noticeAnnouncementUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const statusText = computed(() => { |
| | | if (isExpired(props.row) && props.row.publishStatus === "published") return "å·²è¿æ"; |
| | | return publishStatusLabel(props.row.publishStatus); |
| | | }); |
| | | |
| | | const statusTag = computed(() => { |
| | | if (isExpired(props.row) && props.row.publishStatus === "published") return ""; |
| | | return publishStatusTag(props.row.publishStatus); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .type-badge { |
| | | font-weight: 600; |
| | | } |
| | | .urgent-banner { |
| | | margin-bottom: 12px; |
| | | } |
| | | .notice-html-body { |
| | | padding: 12px; |
| | | background: var(--el-fill-color-light); |
| | | border-radius: 6px; |
| | | max-height: 400px; |
| | | overflow-y: auto; |
| | | line-height: 1.7; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼NoticeAnnouncement éç¥å
Œ--> |
| | | <template> |
| | | <div class="app-container notice-announcement-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.noticeType" placeholder="å
¨é¨" clearable style="width: 130px"> |
| | | <el-option v-for="opt in NOTICE_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.priority" placeholder="å
¨é¨" clearable style="width: 110px"> |
| | | <el-option v-for="opt in PRIORITY_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: 110px"> |
| | | <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.publishDateRange" |
| | | 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 #noticeType="{ row }"> |
| | | <span class="notice-type-tag" :style="{ color: noticeTypeColor(row.noticeType) }"> |
| | | {{ noticeTypeLabel(row.noticeType) }} |
| | | </span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <!-- æ·»å / ä¿®æ¹ --> |
| | | <el-dialog |
| | | v-model="formDialog.visible" |
| | | :title="formDialog.title" |
| | | width="800px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="notice-form-dialog" |
| | | @closed="formRef?.resetFields?.()" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="formRules" |
| | | label-width="100px" |
| | | :disabled="formDialog.readonly" |
| | | > |
| | | <el-form-item label="æ é¢" prop="title"> |
| | | <el-input v-model="form.title" placeholder="请è¾å
¥å
¬åæ é¢" maxlength="100" show-word-limit /> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å
¬åç±»å" prop="noticeType"> |
| | | <el-select v-model="form.noticeType" placeholder="è¯·éæ©" style="width: 100%" @change="onNoticeTypeChange"> |
| | | <el-option v-for="opt in NOTICE_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.priority" placeholder="è¯·éæ©" style="width: 100%"> |
| | | <el-option v-for="opt in PRIORITY_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å叿¥æ" prop="publishDate"> |
| | | <el-date-picker |
| | | v-model="form.publishDate" |
| | | type="date" |
| | | placeholder="å叿¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è¿ææ¥æ"> |
| | | <el-date-picker |
| | | v-model="form.expireDate" |
| | | type="date" |
| | | placeholder="å¯éï¼çç©ºä¸ºé¿æææ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="é
读èå´"> |
| | | <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 v-if="form.noticeType === 'emergency'" label="å¿
读确认"> |
| | | <el-switch v-model="form.requireReadConfirm" active-text="ç´§æ¥éç¥éå工确认已读" /> |
| | | </el-form-item> |
| | | <el-form-item label="å
容" prop="contentHtml"> |
| | | <Editor v-model="form.contentHtml" :min-height="280" placeholder="请è¾å
¥å
容" /> |
| | | </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(false)">åè稿</el-button> |
| | | <el-button type="primary" @click="onSave(true)">ç¡® å®</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 详æ
--> |
| | | <el-dialog v-model="detailDialog.visible" title="å
¬å详æ
" width="800px" append-to-body destroy-on-close> |
| | | <NoticeDetailPanel :row="detailRow" /> |
| | | <template #footer> |
| | | <el-button @click="detailDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { onMounted } from "vue"; |
| | | import Editor from "@/components/Editor/index.vue"; |
| | | import { noticeTypeColor } from "./noticeAnnouncementUtils.js"; |
| | | import NoticeDetailPanel from "./components/NoticeDetailPanel.vue"; |
| | | import { useNoticeAnnouncement } from "./useNoticeAnnouncement.js"; |
| | | |
| | | const { |
| | | Search, |
| | | NOTICE_TYPE_OPTIONS, |
| | | PRIORITY_OPTIONS, |
| | | PUBLISH_STATUS_OPTIONS, |
| | | READ_SCOPE_OPTIONS, |
| | | DEPT_OPTIONS, |
| | | noticeTypeLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | formDialog, |
| | | form, |
| | | formRef, |
| | | formRules, |
| | | detailDialog, |
| | | detailRow, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | openFormDialog, |
| | | saveForm, |
| | | } = useNoticeAnnouncement(); |
| | | |
| | | function onNoticeTypeChange(type) { |
| | | if (type === "emergency") { |
| | | form.priority = "urgent"; |
| | | form.requireReadConfirm = true; |
| | | } |
| | | } |
| | | |
| | | function onSave(publish) { |
| | | const ret = saveForm(publish); |
| | | if (ret?.message) { |
| | | ElMessage.warning(ret.message); |
| | | return; |
| | | } |
| | | if (ret?.ok) { |
| | | ElMessage.success(publish ? "å
¬åå·²åå¸" : "å·²ä¿åè稿"); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | handleQuery(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .notice-announcement-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; |
| | | } |
| | | .notice-type-tag { |
| | | font-weight: 600; |
| | | font-size: 13px; |
| | | } |
| | | .ml10 { |
| | | margin-left: 10px; |
| | | } |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | /** å
¬åç±»å */ |
| | | export const NOTICE_TYPE_OPTIONS = [ |
| | | { value: "emergency", label: "ç´§æ¥éç¥", color: "#f56c6c" }, |
| | | { value: "employee", label: "åå·¥å
Œ", color: "#409eff" }, |
| | | { value: "company", label: "ä¼ä¸å
Œ", color: "#e6a23c" }, |
| | | ]; |
| | | |
| | | /** ä¼å
级 */ |
| | | export const PRIORITY_OPTIONS = [ |
| | | { value: "urgent", label: "ç´§æ¥", tag: "danger" }, |
| | | { value: "high", label: "éè¦", tag: "warning" }, |
| | | { value: "normal", label: "æ®é", tag: "info" }, |
| | | ]; |
| | | |
| | | /** åå¸ç¶æ */ |
| | | export const PUBLISH_STATUS_OPTIONS = [ |
| | | { value: "draft", label: "è稿", tag: "info" }, |
| | | { value: "published", label: "å·²åå¸", tag: "success" }, |
| | | { value: "withdrawn", label: "å·²æ¤å", tag: "warning" }, |
| | | { value: "expired", label: "å·²è¿æ", tag: "" }, |
| | | ]; |
| | | |
| | | /** é
读èå´ */ |
| | | export const READ_SCOPE_OPTIONS = [ |
| | | { value: "all", label: "å
¨åå¯è§" }, |
| | | { value: "department", label: "æå®é¨é¨" }, |
| | | { value: "management", label: "管çå±" }, |
| | | ]; |
| | | |
| | | export const DEPT_OPTIONS = [ |
| | | { value: "101", label: "ç åé¨" }, |
| | | { value: "102", label: "éå®é¨" }, |
| | | { value: "103", label: "è¡æ¿é¨" }, |
| | | { value: "104", label: "è´¢å¡é¨" }, |
| | | { value: "105", label: "æ»ç»å" }, |
| | | { value: "106", label: "人åèµæºé¨" }, |
| | | ]; |
| | | |
| | | export const STORAGE_KEY = "oa_notice_announcement_v1"; |
| | | |
| | | export function noticeTypeLabel(v) { |
| | | return NOTICE_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function noticeTypeColor(v) { |
| | | return NOTICE_TYPE_OPTIONS.find((x) => x.value === v)?.color || "#909399"; |
| | | } |
| | | |
| | | export function priorityLabel(v) { |
| | | return PRIORITY_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function priorityTag(v) { |
| | | return PRIORITY_OPTIONS.find((x) => x.value === v)?.tag || "info"; |
| | | } |
| | | |
| | | export function publishStatusLabel(v) { |
| | | return PUBLISH_STATUS_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function publishStatusTag(v) { |
| | | return PUBLISH_STATUS_OPTIONS.find((x) => x.value === v)?.tag || "info"; |
| | | } |
| | | |
| | | export function readScopeLabel(v) { |
| | | return READ_SCOPE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function createEmptyForm() { |
| | | return { |
| | | id: "", |
| | | noticeNo: "", |
| | | title: "", |
| | | noticeType: "employee", |
| | | priority: "normal", |
| | | contentHtml: "", |
| | | publishDate: dayjs().format("YYYY-MM-DD"), |
| | | expireDate: "", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | requireReadConfirm: false, |
| | | publishStatus: "draft", |
| | | publisherName: "", |
| | | publishTime: "", |
| | | readCount: 0, |
| | | createTime: "", |
| | | updateTime: "", |
| | | }; |
| | | } |
| | | |
| | | export function createInitialMockNotices() { |
| | | return [ |
| | | { |
| | | id: "notice_1", |
| | | noticeNo: "NA202605100001", |
| | | title: "å
³äºå°é£å¤©æ°å±
å®¶åå
¬çç´§æ¥éç¥", |
| | | noticeType: "emergency", |
| | | priority: "urgent", |
| | | contentHtml: |
| | | "<p><strong>ç´§æ¥éç¥</strong></p><p>åå°é£å½±åï¼ææ¥ï¼5æ17æ¥ï¼å
¨ä½åå·¥å±
å®¶åå
¬ï¼è¯·åé¨é¨è´è´£äººå好工ä½å®æä¸åå·¥èç»ã</p>", |
| | | publishDate: "2026-05-16", |
| | | expireDate: "2026-05-20", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | requireReadConfirm: true, |
| | | publishStatus: "published", |
| | | publisherName: "è¡æ¿é¨", |
| | | publishTime: "2026-05-16 08:30:00", |
| | | readCount: 128, |
| | | createTime: "2026-05-16 08:00:00", |
| | | updateTime: "2026-05-16 08:30:00", |
| | | }, |
| | | { |
| | | id: "notice_2", |
| | | noticeNo: "NA202605120002", |
| | | title: "2026年端åèæ¾å宿å
Œ", |
| | | noticeType: "employee", |
| | | priority: "high", |
| | | contentHtml: |
| | | "<p>æ ¹æ®å½å®¶æ³å®è忥宿ï¼ç«¯åèæ¾åæ¶é´ä¸º 6æ8æ¥è³6æ10æ¥ï¼å
±3天ã6æ7æ¥ï¼å¨å
ï¼æ£å¸¸ä¸çã</p>", |
| | | publishDate: "2026-05-12", |
| | | expireDate: "2026-06-15", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | requireReadConfirm: false, |
| | | publishStatus: "published", |
| | | publisherName: "人åèµæºé¨", |
| | | publishTime: "2026-05-12 10:00:00", |
| | | readCount: 256, |
| | | createTime: "2026-05-12 09:30:00", |
| | | updateTime: "2026-05-12 10:00:00", |
| | | }, |
| | | { |
| | | id: "notice_3", |
| | | noticeNo: "NA202605140003", |
| | | title: "åå
¬åºåæ¶é²æ¼ç»éç¥", |
| | | noticeType: "company", |
| | | priority: "normal", |
| | | contentHtml: "<p>å®äº 5æ25æ¥ 14:00 卿»é¨å¤§æ¥¼è¿è¡æ¶é²æ¼ç»ï¼è¯·åé¨é¨æåå®æäººååå ã</p>", |
| | | publishDate: "2026-05-14", |
| | | expireDate: "2026-05-26", |
| | | readScope: "department", |
| | | targetDeptIds: ["101", "102", "103"], |
| | | requireReadConfirm: false, |
| | | publishStatus: "draft", |
| | | publisherName: "è¡æ¿é¨", |
| | | publishTime: "", |
| | | readCount: 0, |
| | | createTime: "2026-05-14 15:00:00", |
| | | updateTime: "2026-05-14 15:00:00", |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | export function loadStoredNotices() { |
| | | try { |
| | | const raw = localStorage.getItem(STORAGE_KEY); |
| | | if (!raw) return null; |
| | | const data = JSON.parse(raw); |
| | | return Array.isArray(data) ? data : null; |
| | | } catch { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | export function saveStoredNotices(rows) { |
| | | try { |
| | | localStorage.setItem(STORAGE_KEY, JSON.stringify(rows)); |
| | | } catch { |
| | | /* ignore */ |
| | | } |
| | | } |
| | | |
| | | export function nextNoticeNo() { |
| | | return `NA${dayjs().format("YYYYMMDD")}${String(Math.floor(Math.random() * 9000) + 1000)}`; |
| | | } |
| | | |
| | | export function validateNoticeForm(form) { |
| | | const title = (form.title || "").trim(); |
| | | if (!title) return { ok: false, message: "请è¾å
¥å
¬åæ é¢" }; |
| | | if (!form.publishDate) return { ok: false, message: "è¯·éæ©å叿¥æ" }; |
| | | if (!form.noticeType) return { ok: false, message: "è¯·éæ©å
¬åç±»å" }; |
| | | if (form.readScope === "department" && !(form.targetDeptIds || []).length) { |
| | | return { ok: false, message: "è¯·éæ©å¯è§é¨é¨" }; |
| | | } |
| | | return { ok: true, title }; |
| | | } |
| | | |
| | | export function isExpired(row) { |
| | | if (!row.expireDate) return false; |
| | | return dayjs(row.expireDate).endOf("day").isBefore(dayjs()); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { |
| | | NOTICE_TYPE_OPTIONS, |
| | | PRIORITY_OPTIONS, |
| | | PUBLISH_STATUS_OPTIONS, |
| | | READ_SCOPE_OPTIONS, |
| | | DEPT_OPTIONS, |
| | | createEmptyForm, |
| | | createInitialMockNotices, |
| | | loadStoredNotices, |
| | | saveStoredNotices, |
| | | nextNoticeNo, |
| | | validateNoticeForm, |
| | | noticeTypeLabel, |
| | | priorityLabel, |
| | | publishStatusLabel, |
| | | isExpired, |
| | | } from "./noticeAnnouncementUtils.js"; |
| | | |
| | | export function useNoticeAnnouncement() { |
| | | const stored = loadStoredNotices(); |
| | | const allRows = ref(stored?.length ? stored : createInitialMockNotices()); |
| | | |
| | | const searchForm = reactive({ |
| | | keyword: "", |
| | | noticeType: "", |
| | | priority: "", |
| | | publishStatus: "", |
| | | publishDateRange: [], |
| | | }); |
| | | |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | |
| | | const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false }); |
| | | const form = reactive(createEmptyForm()); |
| | | const formRef = ref(); |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | |
| | | const filteredList = computed(() => { |
| | | let list = [...allRows.value]; |
| | | const kw = (searchForm.keyword || "").trim().toLowerCase(); |
| | | if (kw) { |
| | | list = list.filter((r) => (r.title || "").toLowerCase().includes(kw) || (r.noticeNo || "").toLowerCase().includes(kw)); |
| | | } |
| | | if (searchForm.noticeType) list = list.filter((r) => r.noticeType === searchForm.noticeType); |
| | | if (searchForm.priority) list = list.filter((r) => r.priority === searchForm.priority); |
| | | if (searchForm.publishStatus) list = list.filter((r) => r.publishStatus === searchForm.publishStatus); |
| | | const range = searchForm.publishDateRange; |
| | | if (range?.length === 2 && range[0] && range[1]) { |
| | | const start = dayjs(range[0]).startOf("day"); |
| | | const end = dayjs(range[1]).endOf("day"); |
| | | list = list.filter((r) => { |
| | | if (!r.publishDate) return false; |
| | | const t = dayjs(r.publishDate); |
| | | return !t.isBefore(start) && !t.isAfter(end); |
| | | }); |
| | | } |
| | | return list.sort((a, b) => (String(a.updateTime) < String(b.updateTime) ? 1 : -1)); |
| | | }); |
| | | |
| | | watch( |
| | | filteredList, |
| | | (list) => { |
| | | page.total = list.length; |
| | | const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | const tableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return filteredList.value.slice(start, start + page.size); |
| | | }); |
| | | |
| | | const formRules = { |
| | | title: [{ required: true, message: "请è¾å
¥å
¬åæ é¢", trigger: "blur" }], |
| | | publishDate: [{ required: true, message: "è¯·éæ©å叿¥æ", trigger: "change" }], |
| | | noticeType: [{ required: true, message: "è¯·éæ©å
¬åç±»å", trigger: "change" }], |
| | | }; |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "ç¼å·", prop: "noticeNo", width: 150 }, |
| | | { label: "æ é¢", prop: "title", minWidth: 200, showOverflowTooltip: true }, |
| | | { |
| | | label: "ç±»å", |
| | | prop: "noticeType", |
| | | width: 100, |
| | | dataType: "slot", |
| | | slot: "noticeType", |
| | | }, |
| | | { |
| | | label: "ä¼å
级", |
| | | prop: "priority", |
| | | width: 90, |
| | | dataType: "tag", |
| | | formatData: (v) => priorityLabel(v), |
| | | formatType: (v) => { |
| | | const hit = PRIORITY_OPTIONS.find((x) => x.value === v); |
| | | return hit?.tag || "info"; |
| | | }, |
| | | }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "publishStatus", |
| | | width: 90, |
| | | dataType: "tag", |
| | | formatData: (v, row) => (isExpired(row) && v === "published" ? "å·²è¿æ" : publishStatusLabel(v)), |
| | | formatType: (v, row) => { |
| | | if (isExpired(row) && v === "published") return ""; |
| | | const hit = PUBLISH_STATUS_OPTIONS.find((x) => x.value === v); |
| | | return hit?.tag || "info"; |
| | | }, |
| | | }, |
| | | { label: "å叿¥æ", prop: "publishDate", width: 120 }, |
| | | { label: "åå¸äºº", prop: "publisherName", width: 110 }, |
| | | { label: "é
读é", prop: "readCount", width: 80, align: "center" }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 220, |
| | | operation: [ |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "ä¿®æ¹", |
| | | type: "text", |
| | | disabled: (row) => row.publishStatus === "withdrawn", |
| | | clickFun: (row) => openFormDialog("edit", row), |
| | | }, |
| | | { |
| | | name: "åå¸", |
| | | type: "text", |
| | | disabled: (row) => row.publishStatus === "published", |
| | | clickFun: (row) => publishNotice(row), |
| | | }, |
| | | { |
| | | name: "æ¤å", |
| | | type: "text", |
| | | disabled: (row) => row.publishStatus !== "published", |
| | | clickFun: (row) => withdrawNotice(row), |
| | | }, |
| | | { name: "å é¤", type: "text", clickFun: (row) => deleteNotice(row) }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | function persist() { |
| | | saveStoredNotices(allRows.value); |
| | | } |
| | | |
| | | function handleQuery() { |
| | | tableLoading.value = true; |
| | | page.current = 1; |
| | | setTimeout(() => { |
| | | tableLoading.value = false; |
| | | }, 200); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.keyword = ""; |
| | | searchForm.noticeType = ""; |
| | | searchForm.priority = ""; |
| | | searchForm.publishStatus = ""; |
| | | searchForm.publishDateRange = []; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function pagination({ page: p, limit }) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | } |
| | | |
| | | function resetForm(target = createEmptyForm()) { |
| | | Object.assign(form, createEmptyForm(), target); |
| | | } |
| | | |
| | | function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.readonly = mode === "view"; |
| | | formDialog.title = |
| | | mode === "add" ? "æ·»å å
¬å" : mode === "edit" ? "ä¿®æ¹å
¬å" : "æ¥çå
Œ"; |
| | | if (mode === "add") { |
| | | resetForm({ publisherName: "å½åç¨æ·", priority: "normal" }); |
| | | } else { |
| | | resetForm({ |
| | | ...JSON.parse(JSON.stringify(row)), |
| | | targetDeptIds: [...(row.targetDeptIds || [])], |
| | | }); |
| | | } |
| | | formDialog.visible = true; |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function saveForm(publish = false) { |
| | | const v = validateNoticeForm(form); |
| | | if (!v.ok) return { ok: false, message: v.message }; |
| | | |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | const payload = { |
| | | ...JSON.parse(JSON.stringify(form)), |
| | | title: v.title, |
| | | updateTime: now, |
| | | }; |
| | | |
| | | if (form.noticeType === "emergency" && payload.priority === "normal") { |
| | | payload.priority = "urgent"; |
| | | } |
| | | |
| | | if (formDialog.mode === "add") { |
| | | payload.id = `notice_${Date.now()}`; |
| | | payload.noticeNo = nextNoticeNo(); |
| | | payload.createTime = now; |
| | | payload.readCount = 0; |
| | | if (publish) { |
| | | payload.publishStatus = "published"; |
| | | payload.publishTime = now; |
| | | } else { |
| | | payload.publishStatus = "draft"; |
| | | } |
| | | allRows.value.unshift(payload); |
| | | } else { |
| | | const idx = allRows.value.findIndex((r) => r.id === form.id); |
| | | if (idx < 0) return { ok: false, message: "è®°å½ä¸åå¨" }; |
| | | const prev = allRows.value[idx]; |
| | | if (publish) { |
| | | payload.publishStatus = "published"; |
| | | payload.publishTime = payload.publishTime || now; |
| | | } |
| | | allRows.value[idx] = { ...prev, ...payload }; |
| | | } |
| | | persist(); |
| | | formDialog.visible = false; |
| | | return { ok: true }; |
| | | } |
| | | |
| | | async function publishNotice(row) { |
| | | try { |
| | | await ElMessageBox.confirm(`确认åå¸ã${row.title}ãï¼`, "åå¸å
Œ", { |
| | | type: "warning", |
| | | confirmButtonText: "åå¸", |
| | | cancelButtonText: "åæ¶", |
| | | }); |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (!hit) return; |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | hit.publishStatus = "published"; |
| | | hit.publishTime = now; |
| | | hit.updateTime = now; |
| | | if (hit.noticeType === "emergency") hit.priority = "urgent"; |
| | | persist(); |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | async function withdrawNotice(row) { |
| | | try { |
| | | await ElMessageBox.confirm(`确认æ¤åã${row.title}ãï¼æ¤ååå工端å°ä¸åå±ç¤ºã`, "æ¤åå
Œ", { |
| | | type: "warning", |
| | | confirmButtonText: "æ¤å", |
| | | cancelButtonText: "åæ¶", |
| | | }); |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (!hit) return; |
| | | hit.publishStatus = "withdrawn"; |
| | | hit.updateTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | persist(); |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | async function deleteNotice(row) { |
| | | try { |
| | | await ElMessageBox.confirm(`确认å é¤ã${row.title}ãï¼æ¤æä½ä¸å¯æ¢å¤ã`, "å é¤å
Œ", { |
| | | type: "warning", |
| | | confirmButtonText: "å é¤", |
| | | cancelButtonText: "åæ¶", |
| | | }); |
| | | allRows.value = allRows.value.filter((r) => r.id !== row.id); |
| | | persist(); |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | return { |
| | | Search, |
| | | NOTICE_TYPE_OPTIONS, |
| | | PRIORITY_OPTIONS, |
| | | PUBLISH_STATUS_OPTIONS, |
| | | READ_SCOPE_OPTIONS, |
| | | DEPT_OPTIONS, |
| | | noticeTypeLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | formDialog, |
| | | form, |
| | | formRef, |
| | | formRules, |
| | | detailDialog, |
| | | detailRow, |
| | | isExpired, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | openFormDialog, |
| | | openDetail, |
| | | saveForm, |
| | | publishNotice, |
| | | withdrawNotice, |
| | | deleteNotice, |
| | | }; |
| | | } |