yyb
2 天以前 dacc95761cf7090c628fc37a5d4f8bb825ccbbb0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
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 };
}