liuyu
4 小时以前 82a74ba0402ab546ba524d23ce62198c4d00d2f7
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
import type {
  RecordListMode,
  TmsRecordDetail,
  TmsRecordListItem,
  TmsRecordPageQuery,
} from './types';
 
function delay<T>(data: T, ms = 220): Promise<T> {
  return new Promise((resolve) => setTimeout(() => resolve(data), ms));
}
 
const now = Date.now();
const day = 24 * 60 * 60 * 1000;
 
function fmt(ts: number, withSec = false) {
  const d = new Date(ts);
  const p = (n: number) => String(n).padStart(2, '0');
  const base = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
  return withSec ? `${base}:00` : base;
}
 
/** mock 数据:培训记录(pending) + 可归档(ready) + 已归档(archived) */
const store: TmsRecordListItem[] = [
  {
    id: 'rc0a',
    recordNo: 'TT2026091801',
    category: '临时培训',
    subject: '本周质量专题宣贯',
    trainerName: '王敏',
    trainType: '集中授课',
    startTime: fmt(now + 1 * day + 9 * 60 * 60 * 1000),
    evalMode: '提问',
    archiveStatus: 'pending',
  },
  {
    id: 'rc0b',
    recordNo: 'TT2026091702',
    category: '文件生效培训',
    subject: '(SOP-PR-0008) 批记录填写规范 Rev.02',
    trainerName: '陈刚',
    trainType: '在线学习',
    startTime: fmt(now - 0.5 * day + 10 * 60 * 60 * 1000),
    evalMode: '无需考核',
    archiveStatus: 'pending',
    signRate: 60,
  },
  {
    id: 'rc0c',
    recordNo: 'TT2026091603',
    category: '岗位培训计划',
    subject: '取样员岗位操作再培训',
    trainerName: '赵丽',
    trainType: '操作培训',
    startTime: fmt(now - 2 * day + 14 * 60 * 60 * 1000),
    evalMode: '实操考核',
    archiveStatus: 'pending',
    signRate: 70,
  },
  {
    id: 'rc1',
    recordNo: 'TT2026090525',
    category: '文件生效培训',
    subject: '(JY-03-0042-B2449) 文件发放与回收管理规程 Rev.03',
    trainerName: '梁波',
    trainType: '在线学习',
    startTime: fmt(now - 5 * day + 9.8 * 60 * 60 * 1000),
    evalMode: '无需考核',
    archiveStatus: 'ready',
    signRate: 100,
    passRate: 100,
  },
  {
    id: 'rc2',
    recordNo: 'TT2026090339',
    category: '岗位培训计划',
    subject: '分析实验室岗位再培训(HPLC 操作)',
    trainerName: '孔德敏',
    trainType: '集中授课',
    startTime: fmt(now - 8 * day + 14 * 60 * 60 * 1000),
    evalMode: '提问',
    archiveStatus: 'ready',
    signRate: 95,
    passRate: 90,
  },
  {
    id: 'rc3',
    recordNo: 'TT2026090201',
    category: '临时培训',
    subject: '偏差调查流程宣贯',
    trainerName: '季远达',
    trainType: '在线学习',
    startTime: fmt(now - 12 * day + 10 * 60 * 60 * 1000),
    evalMode: '无需考核',
    archiveStatus: 'ready',
    signRate: 88,
    passRate: 88,
  },
  {
    id: 'rc4',
    recordNo: 'TT2026082812',
    category: '文件生效培训',
    subject: '(SOP-QA-0012) 变更控制管理程序 Rev.05',
    trainerName: '梁波',
    trainType: '在线学习',
    startTime: fmt(now - 20 * day + 9 * 60 * 60 * 1000),
    evalMode: '提问',
    archiveStatus: 'ready',
    signRate: 100,
    passRate: 96,
  },
  {
    id: 'rc5',
    recordNo: 'TT2026090638',
    category: '临时培训',
    subject: '硝酸奥司他韦干混悬剂稳定性标准培训',
    trainerName: '周莉华',
    trainType: '在线学习',
    startTime: '2020-09-17 16:00',
    endTime: '2020-09-17 17:30',
    evalMode: '无需考核',
    archiveStatus: 'archived',
    trainMode: '在线学习',
    signRate: 100,
    passRate: 100,
  },
  {
    id: 'rc6',
    recordNo: 'TT2026090610',
    category: '文件生效培训',
    subject: '(JY-05-0011) 稳定性考察管理规程 Rev.01',
    trainerName: '黄援花',
    trainType: '集中授课',
    startTime: fmt(now - 3 * day + 9.5 * 60 * 60 * 1000),
    evalMode: '无需考核',
    archiveStatus: 'archived',
    signRate: 100,
    passRate: 100,
  },
  {
    id: 'rc7',
    recordNo: 'TT2026081508',
    category: '岗位培训计划',
    subject: '实验室安全再培训',
    trainerName: '李四',
    trainType: '在线学习',
    startTime: fmt(now - 40 * day + 10 * 60 * 60 * 1000),
    evalMode: '在线考试',
    archiveStatus: 'archived',
    signRate: 100,
    passRate: 92,
  },
  {
    id: 'rc8',
    recordNo: 'TT2026072203',
    category: '临时培训',
    subject: '测试在线考试0415',
    trainerName: 'mkj',
    trainType: '在线学习',
    startTime: fmt(now - 60 * day),
    evalMode: '在线考试',
    archiveStatus: 'archived',
    signRate: 80,
    passRate: 75,
  },
];
 
const signStore: Record<string, TmsRecordDetail['signList']> = {
  rc0b: [
    { id: 's0', userName: '周杰', deptName: '生产部', signTime: fmt(now - 0.5 * day, true), signMode: 'APP扫码' },
  ],
  rc1: [
    { id: 's1', userName: '张三', deptName: '质量管理部', signTime: fmt(now - 5 * day, true), signMode: 'APP扫码' },
    { id: 's2', userName: '李四', deptName: '分析实验室', signTime: fmt(now - 5 * day, true), signMode: '微信扫码' },
  ],
  rc5: [
    { id: 's3', userName: '王五', deptName: '生产部', signTime: '2020-09-17 16:05:00', signMode: '在线签到' },
  ],
};
 
type Participant = NonNullable<TmsRecordDetail['participants']>[number];
 
const participantStore: Record<string, Participant[]> = {
  rc0a: [
    { id: 'p0a1', userName: '陈晓', deptName: '质量管理部', signDate: '', examScore: '', passFlag: '' },
    { id: 'p0a2', userName: '林娜', deptName: '生产一部', signDate: '', examScore: '', passFlag: '' },
    { id: 'p0a3', userName: '黄伟', deptName: '生产二部', signDate: '', examScore: '', passFlag: '' },
  ],
  rc0b: [
    { id: 'p0b1', userName: '周杰', deptName: '生产部', signDate: fmt(now - 0.5 * day, true), examScore: '', passFlag: '' },
    { id: 'p0b2', userName: '何静', deptName: '质量保证部', signDate: fmt(now - 0.5 * day, true), examScore: '', passFlag: '' },
    { id: 'p0b3', userName: '罗明', deptName: '工程部', signDate: fmt(now - 0.5 * day + 8 * 60 * 1000, true), examScore: '', passFlag: '' },
    { id: 'p0b4', userName: '邓丽', deptName: '仓储部', signDate: '', examScore: '', passFlag: '' },
  ],
  rc0c: [
    { id: 'p0c1', userName: '赵丽', deptName: '分析实验室', signDate: fmt(now - 2 * day + 14 * 60 * 60 * 1000, true), examScore: 92, passFlag: '1' },
    { id: 'p0c2', userName: '孙浩', deptName: '取样组', signDate: fmt(now - 2 * day + 14.1 * 60 * 60 * 1000, true), examScore: 88, passFlag: '1' },
    { id: 'p0c3', userName: '钱芳', deptName: '取样组', signDate: fmt(now - 2 * day + 14.2 * 60 * 60 * 1000, true), examScore: 76, passFlag: '1' },
    { id: 'p0c4', userName: '吴磊', deptName: 'QC实验室', signDate: fmt(now - 2 * day + 14.3 * 60 * 60 * 1000, true), examScore: 58, passFlag: '0' },
    { id: 'p0c5', userName: '郑雪', deptName: '取样组', signDate: fmt(now - 2 * day + 14.4 * 60 * 60 * 1000, true), examScore: 85, passFlag: '1' },
    { id: 'p0c6', userName: '冯涛', deptName: '质量保证部', signDate: '', examScore: '', passFlag: '' },
  ],
  rc1: [
    { id: 'p11', userName: '张三', deptName: '质量管理部', signDate: fmt(now - 5 * day, true), examScore: '', passFlag: '' },
    { id: 'p12', userName: '李四', deptName: '分析实验室', signDate: fmt(now - 5 * day, true), examScore: '', passFlag: '' },
    { id: 'p13', userName: '王芳', deptName: '文件管理组', signDate: fmt(now - 5 * day + 6 * 60 * 1000, true), examScore: '', passFlag: '' },
    { id: 'p14', userName: '刘洋', deptName: '质量保证部', signDate: fmt(now - 5 * day + 12 * 60 * 1000, true), examScore: '', passFlag: '' },
    { id: 'p15', userName: '陈明', deptName: '生产一部', signDate: fmt(now - 5 * day + 15 * 60 * 1000, true), examScore: '', passFlag: '' },
  ],
  rc2: [
    { id: 'p21', userName: '孔德敏', deptName: '分析实验室', signDate: fmt(now - 8 * day + 14 * 60 * 60 * 1000, true), examScore: 90, passFlag: '1' },
    { id: 'p22', userName: '马超', deptName: '分析实验室', signDate: fmt(now - 8 * day + 14.1 * 60 * 60 * 1000, true), examScore: 82, passFlag: '1' },
    { id: 'p23', userName: '徐倩', deptName: '方法开发组', signDate: fmt(now - 8 * day + 14.2 * 60 * 60 * 1000, true), examScore: 65, passFlag: '0' },
    { id: 'p24', userName: '蔡磊', deptName: '分析实验室', signDate: fmt(now - 8 * day + 14.3 * 60 * 60 * 1000, true), examScore: 95, passFlag: '1' },
  ],
  rc3: [
    { id: 'p31', userName: '季远达', deptName: '质量保证部', signDate: fmt(now - 12 * day + 10 * 60 * 60 * 1000, true), examScore: '', passFlag: '' },
    { id: 'p32', userName: '唐敏', deptName: '偏差调查组', signDate: fmt(now - 12 * day + 10.1 * 60 * 60 * 1000, true), examScore: '', passFlag: '' },
    { id: 'p33', userName: '潘杰', deptName: '生产一部', signDate: fmt(now - 12 * day + 10.2 * 60 * 60 * 1000, true), examScore: '', passFlag: '' },
  ],
  rc4: [
    { id: 'p41', userName: '梁波', deptName: '质量保证部', signDate: fmt(now - 20 * day + 9 * 60 * 60 * 1000, true), examScore: 88, passFlag: '1' },
    { id: 'p42', userName: '谢娜', deptName: '变更控制组', signDate: fmt(now - 20 * day + 9.1 * 60 * 60 * 1000, true), examScore: 91, passFlag: '1' },
    { id: 'p43', userName: '韩冬', deptName: '生产二部', signDate: fmt(now - 20 * day + 9.2 * 60 * 60 * 1000, true), examScore: 70, passFlag: '1' },
    { id: 'p44', userName: '曹颖', deptName: '文件管理组', signDate: fmt(now - 20 * day + 9.3 * 60 * 60 * 1000, true), examScore: 55, passFlag: '0' },
  ],
  rc5: [
    { id: 'p51', userName: '李俊佳', deptName: '稳定性组', signDate: '2020-09-17 16:02:00', examScore: '', passFlag: '' },
    { id: 'p52', userName: '周莉', deptName: '分析实验室', signDate: '2020-09-17 16:03:00', examScore: '', passFlag: '' },
    { id: 'p53', userName: '梁伟星', deptName: '质量保证部', signDate: '2020-09-17 16:05:00', examScore: '', passFlag: '' },
    { id: 'p54', userName: '陈晓明', deptName: '研发中心', signDate: '2020-09-17 16:08:00', examScore: '', passFlag: '' },
    { id: 'p55', userName: '王芳', deptName: '稳定性组', signDate: '2020-09-17 16:10:00', examScore: '', passFlag: '' },
    { id: 'p56', userName: '张敏', deptName: 'QC实验室', signDate: '2020-09-17 16:12:00', examScore: '', passFlag: '' },
    { id: 'p57', userName: '刘洋', deptName: '生产一部', signDate: '2020-09-17 16:15:00', examScore: '', passFlag: '' },
    { id: 'p58', userName: '赵强', deptName: '仓储部', signDate: '', examScore: '', passFlag: '' },
  ],
  rc6: [
    { id: 'p61', userName: '黄援花', deptName: '稳定性组', signDate: fmt(now - 3 * day + 9.5 * 60 * 60 * 1000, true), examScore: '', passFlag: '' },
    { id: 'p62', userName: '宋佳', deptName: '稳定性组', signDate: fmt(now - 3 * day + 9.6 * 60 * 60 * 1000, true), examScore: '', passFlag: '' },
    { id: 'p63', userName: '姜涛', deptName: '质量保证部', signDate: fmt(now - 3 * day + 9.7 * 60 * 60 * 1000, true), examScore: '', passFlag: '' },
  ],
  rc7: [
    { id: 'p71', userName: '李四', deptName: '分析实验室', signDate: fmt(now - 40 * day + 10 * 60 * 60 * 1000, true), examScore: 86, passFlag: '1' },
    { id: 'p72', userName: '周强', deptName: 'EHS', signDate: fmt(now - 40 * day + 10.1 * 60 * 60 * 1000, true), examScore: 78, passFlag: '1' },
    { id: 'p73', userName: '吴倩', deptName: '生产一部', signDate: fmt(now - 40 * day + 10.2 * 60 * 60 * 1000, true), examScore: 62, passFlag: '0' },
    { id: 'p74', userName: '郑浩', deptName: '工程部', signDate: fmt(now - 40 * day + 10.3 * 60 * 60 * 1000, true), examScore: 90, passFlag: '1' },
    { id: 'p75', userName: '冯蕾', deptName: '仓储部', signDate: fmt(now - 40 * day + 10.4 * 60 * 60 * 1000, true), examScore: 84, passFlag: '1' },
  ],
  rc8: [
    { id: 'p81', userName: 'mkj', deptName: '测试组', signDate: fmt(now - 60 * day, true), examScore: 0, passFlag: '0' },
    { id: 'p82', userName: '测试员A', deptName: '测试组', signDate: fmt(now - 60 * day, true), examScore: 80, passFlag: '1' },
    { id: 'p83', userName: '测试员B', deptName: '质量管理部', signDate: fmt(now - 60 * day + 5 * 60 * 1000, true), examScore: 72, passFlag: '1' },
  ],
};
 
/** 查看页扩展明细(按 id) */
const detailExtra: Record<string, Partial<TmsRecordDetail>> = {
  rc0c: {
    closeTime: fmt(now - 1 * day + 18 * 60 * 60 * 1000),
    keyPoints: '取样操作规范、样品标识、取样记录填写要点',
    placeName: '取样室 / 在线学习平台',
    trainees: '赵丽,孙浩,钱芳,吴磊,郑雪,冯涛',
    remark: '',
    attachments: [
      { id: 'a0c1', name: '取样员岗位操作规程.pdf', url: '#' },
      { id: 'a0c2', name: '取样记录填写示例.pdf', url: '#' },
    ],
  },
  rc5: {
    subject:
      '硝酸奥司他韦干混悬剂, 6mg/ml (360mg/瓶) 稳定性标准 (ST1921B6m360ct, Rev01)\n硝酸奥司他韦干混悬剂, 6mg/ml (15mg/袋) 稳定性标准 (ST1921B6m15ct, Rev01)',
    startTime: '2020-09-17 16:00',
    endTime: '2020-09-17 17:30',
    closeTime: '2020-09-24 17:30',
    category: '临时培训',
    trainMode: '在线学习',
    trainType: '在线学习',
    trainerName: '周莉华',
    evalMode: '无需考核',
    keyPoints: '',
    placeName: '',
    trainees:
      '李俊佳,周莉,梁伟星,陈晓明,王芳,张敏,刘洋,赵强,孙丽,周杰,吴倩,郑浩,冯蕾,曹阳,彭静,徐磊',
    remark: '',
    attachments: [
      {
        id: 'a1',
        name: '硝酸奥司他韦干混悬剂, 6mg/ml (360mg/瓶) 稳定性标准 (ST1921B6m360ct, Rev01).pdf',
        url: '#',
      },
      {
        id: 'a2',
        name: '硝酸奥司他韦干混悬剂, 6mg/ml (15mg/袋) 稳定性标准 (ST1921B6m15ct, Rev01).pdf',
        url: '#',
      },
    ],
  },
};
 
function matchMode(item: TmsRecordListItem, mode?: RecordListMode) {
  if (mode === 'ready') return item.archiveStatus === 'ready';
  if (mode === 'archived') return item.archiveStatus === 'archived';
  // 培训记录主列表:进行中 / 待归档
  return item.archiveStatus === 'pending';
}
 
export function mockQueryRecords(params: TmsRecordPageQuery = {}) {
  const {
    currentPage = 1,
    pageSize = 20,
    keyword = '',
    recordNo = '',
    category = '',
    trainerName = '',
    listMode = 'main',
    archiveStatus,
  } = params;
 
  let list = store.filter((x) => matchMode(x, listMode));
 
  if (archiveStatus) list = list.filter((x) => x.archiveStatus === archiveStatus);
  if (recordNo) list = list.filter((x) => x.recordNo.includes(recordNo));
  if (category) list = list.filter((x) => x.category.includes(category));
  if (trainerName) list = list.filter((x) => x.trainerName.includes(trainerName));
  if (keyword) {
    const k = keyword.trim().toLowerCase();
    list = list.filter(
      (x) =>
        x.recordNo.toLowerCase().includes(k) ||
        x.subject.toLowerCase().includes(k) ||
        x.trainerName.toLowerCase().includes(k) ||
        x.category.toLowerCase().includes(k),
    );
  }
 
  const start = (currentPage - 1) * pageSize;
  const pageList = list.slice(start, start + pageSize);
  return delay({
    list: pageList,
    pagination: { total: list.length, currentPage, pageSize },
  });
}
 
export async function mockGetRecordDetail(id: string) {
  await delay(null);
  const row = store.find((x) => x.id === id);
  if (!row) throw new Error('培训记录不存在');
  const extra = detailExtra[id] || {};
  const signs = signStore[id] || [];
  const participants = participantStore[id] || [];
  const trainees =
    extra.trainees || participants.map((p) => p.userName).join(',') || '';
 
  return {
    ...row,
    taskId: `task-${id}`,
    taskNo: row.recordNo,
    closeTime: row.endTime || '',
    keyPoints: '',
    placeName: '',
    remark: '',
    attachments: [],
    trainMode: row.trainMode || row.trainType,
    signList: signs,
    ...extra,
    // 名单与培训对象以 store 为准,避免被 extra 空值覆盖
    trainees,
    participants,
  } as TmsRecordDetail;
}
 
export async function mockUpdateRecord(id: string, data: Partial<TmsRecordListItem>) {
  await delay(null);
  const row = store.find((x) => x.id === id);
  if (!row) throw new Error('培训记录不存在');
  Object.assign(row, data);
  return true;
}
 
export async function mockArchiveRecords(ids: string[]) {
  await delay(null);
  let count = 0;
  for (const id of ids) {
    const row = store.find((x) => x.id === id);
    if (!row) continue;
    if (row.archiveStatus === 'archived') continue;
    row.archiveStatus = 'archived';
    count += 1;
  }
  if (!count) throw new Error('没有可归档的记录');
  return count;
}
 
export async function mockDeleteRecords(ids: string[]) {
  await delay(null);
  let count = 0;
  for (const id of ids) {
    const idx = store.findIndex((x) => x.id === id);
    if (idx < 0) continue;
    if (store[idx].archiveStatus === 'archived') {
      throw new Error(`已归档记录「${store[idx].recordNo}」不可删除`);
    }
    store.splice(idx, 1);
    count += 1;
  }
  if (!count) throw new Error('请选择要删除的记录');
  return count;
}