/** * OKR 模块 Mock 数据 * ============================================================ * 本文件仅用于前端开发阶段的模拟数据。 * 接口对接完成后,将各页面中的 mockXxx() 函数替换为真实的 request.post() 调用即可, * 页面渲染逻辑无需改动。 * * ── 数据结构说明 ──────────────────────────────────────────── * * Objective(目标 O)字段说明: * id string 目标唯一ID,服务端生成 * title string 目标标题,如"完成 App 2.0 发布" * period string 周期标识,如 "Q1" "Q2" "2026-05" (月度) * period_type string 周期类型: "quarter"(季度) | "month"(月度) * dept_code string 所属部门 code(对应 /api/dept/listed 返回的 dept_code) * dept_name string 所属部门名称(冗余字段,方便前端展示) * owner_code string 负责人 user_code(对应 /api/user/listed 返回的 user_code) * owner_name string 负责人姓名(冗余字段) * progress number 总进度 0~1,由所有 KR 进度平均值计算,服务端可返回也可前端算 * status string 状态: "not_started" | "in_progress" | "done" | "canceled" * created_at string 创建时间 ISO 格式 * key_results KeyResult[] 嵌套的关键结果列表(接口返回时一并带上,减少请求次数) * * KeyResult(关键结果 KR)字段说明: * id string KR 唯一ID * objective_id string 所属目标 ID * title string KR 标题,如"完成核心路径开发及发布" * start_value number 起始值,默认 0 * target_value number 目标值,如 100 * current_value number 当前值,由负责人手动更新 * unit string 单位,如 "%" "次" "万元",可为空 * progress number 进度 0~1 = current_value / target_value * priority string 优先级: "P0" | "P1" | "P2" * owner_code string 负责人 user_code * owner_name string 负责人姓名 * dept_code string 所属部门 code * dept_name string 所属部门名称 * period string 周期,同 Objective.period * start_date string 开始日期 "YYYY-MM-DD" * end_date string 截止日期 "YYYY-MM-DD" * ai_comment string AI 点评(预留字段,暂为空) * steps Step[] 协作步骤列表(嵌套在 KR 内,与 KR 一并返回) * * Step(协作步骤)字段说明: * id string 步骤唯一ID * kr_id string 所属 KR ID * title string 步骤标题,如"完成接口文档" * description string 分工说明,说清楚这步要做什么 * collaborator_code string 协作人 user_code * collaborator_name string 协作人姓名 * status string "todo" | "in_progress" | "done" * due_date string 截止日期 YYYY-MM-DD(可选) * * 步骤接口(待服务端实现): * POST /api/okr/kr/step/add * 请求体: { kr_id, title, description, collaborator_code, due_date? } * 返回: { code: 0, data: { id: "STEP_xxx" } } * POST /api/okr/kr/step/modify * 请求体: { id, title?, description?, collaborator_code?, status?, due_date? } * 返回: { code: 0 } * POST /api/okr/kr/step/remove * 请求体: { id } * 返回: { code: 0 } * * Review(复盘)字段说明: * id string 复盘唯一ID * objective_id string 关联目标 ID * objective_title string 目标标题(冗余) * period string 所属周期 * owner_code string 负责人 * owner_name string 负责人姓名 * dept_name string 部门名称 * score number 目标完成评分 0~1.0(0.7 算达成,对标飞书) * kr_scores [{ kr_id, score }] 各 KR 评分 * went_well string 做得好的(复盘问答1) * to_improve string 做得不好的(复盘问答2) * next_steps string 下次如何改进(复盘问答3) * reviewed_at string 复盘时间 * * ── 接口约定(待服务端实现)────────────────────────────────── * 所有接口统一 POST,token 放 Header: Authorization: Bearer {token} * * 目标接口: * POST /api/okr/objective/listed * 请求体: { period?: string, dept_code?: string, owner_code?: string } * 返回: { code: 0, data: Objective[] } * * POST /api/okr/objective/add * 请求体: { title, period, period_type, dept_code, owner_code } * 返回: { code: 0, data: { id: "O_xxx" } } * * POST /api/okr/objective/modify * 请求体: { id, title?, period?, dept_code?, owner_code?, status? } * 返回: { code: 0 } * * POST /api/okr/objective/remove * 请求体: { id } * 返回: { code: 0 } * * 关键结果接口: * POST /api/okr/kr/add * 请求体: { objective_id, title, start_value, target_value, unit, priority, owner_code, start_date, end_date } * 返回: { code: 0, data: { id: "KR_xxx" } } * * POST /api/okr/kr/modify * 请求体: { id, title?, current_value?, priority?, start_date?, end_date?, owner_code? } * 返回: { code: 0 } * * POST /api/okr/kr/remove * 请求体: { id } * 返回: { code: 0 } * * 复盘接口: * POST /api/okr/review/listed * 请求体: { period?: string } * 返回: { code: 0, data: Review[] } * * POST /api/okr/review/modify * 请求体: { objective_id, score, kr_scores, went_well, to_improve, next_steps } * 返回: { code: 0 } * * 用户/部门(复用已有接口,无需新增): * POST /api/user/listed 获取用户列表 * POST /api/dept/listed 获取部门列表 * ============================================================ */ import { resolvePublicFetchUrl } from '@/utils/apiPath' // ───────────────────────────────────────────── // Mock 部门数据(对应真实 /api/dept/listed) // ───────────────────────────────────────────── export const MOCK_DEPTS = [ { dept_code: 'DEPT_RD', full_name: '研发部' }, { dept_code: 'DEPT_MK', full_name: '市场部' }, { dept_code: 'DEPT_CS', full_name: '客服部' }, { dept_code: 'DEPT_FN', full_name: '财务部' }, ] // ───────────────────────────────────────────── // Mock 用户数据(对应真实 /api/user/listed) // ───────────────────────────────────────────── export const MOCK_USERS = [ { user_code: 'USER_ZBB', realname: '周北北', dept_code: 'DEPT_RD', leader_level: 2, phone: '13800001001', is_valid: true }, { user_code: 'USER_ZXS', realname: '邹小杉', dept_code: 'DEPT_RD', leader_level: 1, phone: '13800001002', is_valid: true }, { user_code: 'USER_YXN', realname: '于小宁', dept_code: 'DEPT_MK', leader_level: 1, phone: '13800002001', is_valid: true }, { user_code: 'USER_HPP', realname: '黄泡泡', dept_code: 'DEPT_MK', leader_level: 2, phone: '13800002002', is_valid: true }, { user_code: 'USER_WXM', realname: '王小铭', dept_code: 'DEPT_CS', leader_level: 1, phone: '13800003001', is_valid: true }, { user_code: 'USER_LDJ', realname: '刘大建', dept_code: 'DEPT_CS', leader_level: 2, phone: '13800003002', is_valid: false }, // 已离职 ] // ───────────────────────────────────────────── // Mock OKR 目标+关键结果数据 // 注:key_results 嵌套在各 Objective 内,接口建议一起返回 // ───────────────────────────────────────────── export const MOCK_OBJECTIVES = [ // ── 研发部 ──────────────────────────────── { id: 'O_001', title: '完成 App 2.0 发布', period: 'Q2', period_type: 'quarter', dept_code: 'DEPT_RD', dept_name: '研发部', owner_code: 'USER_ZBB', owner_name: '周北北', progress: 0.47, // 由 KR 进度平均计算:(1.0 + 0.35 + 0.06) / 3 ≈ 0.47 status: 'in_progress', created_at: '2026-04-01T08:00:00', key_results: [ { id: 'KR_001_1', objective_id: 'O_001', title: '完成关键路径功能开发及发布', start_value: 0, target_value: 100, current_value: 100, unit: '%', progress: 1.0, priority: 'P0', owner_code: 'USER_ZBB', owner_name: '周北北', dept_code: 'DEPT_RD', dept_name: '研发部', period: 'Q2', start_date: '2026-04-01', end_date: '2026-05-15', ai_comment: '', steps: [ { id: 'STEP_001_1', kr_id: 'KR_001_1', title: '完成登录/注册模块开发', description: '覆盖手机号+密码、短信验证码两种方式,通过测试用例', collaborator_code: 'USER_ZXS', collaborator_name: '邹小杉', status: 'done', due_date: '2026-04-15' }, { id: 'STEP_001_2', kr_id: 'KR_001_1', title: '完成首页核心展示模块', description: '按设计稿还原,接入真实接口,兼容主流机型', collaborator_code: 'USER_ZBB', collaborator_name: '周北北', status: 'done', due_date: '2026-04-25' }, { id: 'STEP_001_3', kr_id: 'KR_001_1', title: '完成提交并通过应用商店审核', description: '准备上架材料,跟进苹果/安卓审核流程', collaborator_code: 'USER_ZBB', collaborator_name: '周北北', status: 'done', due_date: '2026-05-15' }, ], }, { id: 'KR_001_2', objective_id: 'O_001', title: '支持 App 2.0 迭代需求,完善 App 代码覆盖率至 80%', start_value: 50, target_value: 80, current_value: 60, unit: '%', progress: 0.35, priority: 'P1', owner_code: 'USER_ZXS', owner_name: '邹小杉', dept_code: 'DEPT_RD', dept_name: '研发部', period: 'Q2', start_date: '2026-04-01', end_date: '2026-06-30', ai_comment: '', steps: [ { id: 'STEP_002_1', kr_id: 'KR_001_2', title: '梳理当前未覆盖的核心路径', description: '整理测试缺口清单,优先覆盖支付/下单流程', collaborator_code: 'USER_ZXS', collaborator_name: '邹小杉', status: 'done', due_date: '2026-04-10' }, { id: 'STEP_002_2', kr_id: 'KR_001_2', title: '补充单元测试至 70%', description: '重点补充 utils / hooks 层,目标覆盖率 70%', collaborator_code: 'USER_ZBB', collaborator_name: '周北北', status: 'in_progress', due_date: '2026-05-31' }, { id: 'STEP_002_3', kr_id: 'KR_001_2', title: '集成测试补充至 80%', description: '结合 E2E 框架,覆盖主流程端到端测试', collaborator_code: 'USER_ZXS', collaborator_name: '邹小杉', status: 'todo', due_date: '2026-06-30' }, ], }, { id: 'KR_001_3', objective_id: 'O_001', title: '完成性能优化,首屏加载时长降至 2 秒以内', start_value: 5, target_value: 2, current_value: 4.7, unit: '秒', progress: 0.1, priority: 'P1', owner_code: 'USER_ZXS', owner_name: '邹小杉', dept_code: 'DEPT_RD', dept_name: '研发部', period: 'Q2', start_date: '2026-05-01', end_date: '2026-06-30', ai_comment: '', steps: [ { id: 'STEP_003_1', kr_id: 'KR_001_3', title: '完成首屏资源分析报告', description: '使用 Lighthouse 分析,找出加载瓶颈', collaborator_code: 'USER_ZXS', collaborator_name: '邹小杉', status: 'in_progress', due_date: '2026-05-15' }, { id: 'STEP_003_2', kr_id: 'KR_001_3', title: '图片懒加载 + CDN 接入', description: '接入 OSS CDN,首屏图片走懒加载', collaborator_code: 'USER_ZBB', collaborator_name: '周北北', status: 'todo', due_date: '2026-06-15' }, ], }, ], }, { id: 'O_002', title: '提升 App 性能稳定性', period: 'Q2', period_type: 'quarter', dept_code: 'DEPT_RD', dept_name: '研发部', owner_code: 'USER_ZXS', owner_name: '邹小杉', progress: 0.30, status: 'in_progress', created_at: '2026-04-02T09:00:00', key_results: [ { id: 'KR_002_1', objective_id: 'O_002', title: 'Crash 率降至 0.1% 以下', start_value: 0.8, target_value: 0.1, current_value: 0.3, unit: '%', progress: 0.71, priority: 'P0', owner_code: 'USER_ZXS', owner_name: '邹小杉', dept_code: 'DEPT_RD', dept_name: '研发部', period: 'Q2', start_date: '2026-04-01', end_date: '2026-06-30', ai_comment: '', }, { id: 'KR_002_2', objective_id: 'O_002', title: 'API 平均响应时间 ≤ 300ms', start_value: 800, target_value: 300, current_value: 700, unit: 'ms', progress: 0.20, priority: 'P1', owner_code: 'USER_ZBB', owner_name: '周北北', dept_code: 'DEPT_RD', dept_name: '研发部', period: 'Q2', start_date: '2026-04-01', end_date: '2026-06-30', ai_comment: '', }, ], }, // ── 市场部 ──────────────────────────────── { id: 'O_003', title: '拓展市场份额', period: 'Q2', period_type: 'quarter', dept_code: 'DEPT_MK', dept_name: '市场部', owner_code: 'USER_YXN', owner_name: '于小宁', progress: 0.25, status: 'in_progress', created_at: '2026-04-03T10:00:00', key_results: [ { id: 'KR_003_1', objective_id: 'O_003', title: '调研 100+ 行业客户,完成需求报告', start_value: 0, target_value: 100, current_value: 28, unit: '家', progress: 0.28, priority: 'P0', owner_code: 'USER_YXN', owner_name: '于小宁', dept_code: 'DEPT_MK', dept_name: '市场部', period: 'Q2', start_date: '2026-04-01', end_date: '2026-05-31', ai_comment: '', steps: [ { id: 'STEP_004_1', kr_id: 'KR_003_1', title: '设计调研问卷', description: '覆盖采购决策、价格敏感度、现有方案痛点三个维度', collaborator_code: 'USER_HPP', collaborator_name: '黄泡泡', status: 'done', due_date: '2026-04-05' }, { id: 'STEP_004_2', kr_id: 'KR_003_1', title: '完成 50 家客户访谈', description: '电话/上门访谈,每份记录上传共享文档', collaborator_code: 'USER_YXN', collaborator_name: '于小宁', status: 'in_progress', due_date: '2026-05-15' }, { id: 'STEP_004_3', kr_id: 'KR_003_1', title: '整理并输出需求分析报告', description: '汇总共性需求,按优先级排序,产出 PPT 与文档', collaborator_code: 'USER_HPP', collaborator_name: '黄泡泡', status: 'todo', due_date: '2026-05-31' }, ], }, { id: 'KR_003_2', objective_id: 'O_003', title: '推进关键信息并推出七项关键策略', start_value: 0, target_value: 7, current_value: 1, unit: '项', progress: 0.14, priority: 'P1', owner_code: 'USER_HPP', owner_name: '黄泡泡', dept_code: 'DEPT_MK', dept_name: '市场部', period: 'Q2', start_date: '2026-04-15', end_date: '2026-06-30', ai_comment: '', }, ], }, { id: 'O_004', title: '完成品牌 App 功能宣发', period: 'Q1', period_type: 'quarter', dept_code: 'DEPT_MK', dept_name: '市场部', owner_code: 'USER_HPP', owner_name: '黄泡泡', progress: 0.70, status: 'in_progress', created_at: '2026-01-05T08:00:00', key_results: [ { id: 'KR_004_1', objective_id: 'O_004', title: '策划并执行 3 场线上发布活动', start_value: 0, target_value: 3, current_value: 2, unit: '场', progress: 0.67, priority: 'P0', owner_code: 'USER_HPP', owner_name: '黄泡泡', dept_code: 'DEPT_MK', dept_name: '市场部', period: 'Q1', start_date: '2026-01-10', end_date: '2026-03-31', ai_comment: '', }, { id: 'KR_004_2', objective_id: 'O_004', title: '创建并发布功能宣发内容,有效触达 10 万用户', start_value: 0, target_value: 100000, current_value: 73000, unit: '人', progress: 0.73, priority: 'P1', owner_code: 'USER_YXN', owner_name: '于小宁', dept_code: 'DEPT_MK', dept_name: '市场部', period: 'Q1', start_date: '2026-01-10', end_date: '2026-03-31', ai_comment: '', }, ], }, // ── 客服部 ──────────────────────────────── { id: 'O_005', title: '提升客户满意度与响应效率', period: 'Q2', period_type: 'quarter', dept_code: 'DEPT_CS', dept_name: '客服部', owner_code: 'USER_WXM', owner_name: '王小铭', progress: 0.43, status: 'in_progress', created_at: '2026-04-04T09:00:00', key_results: [ { id: 'KR_005_1', objective_id: 'O_005', title: '改进客户满意度问卷,客满分达到 4.5/5.0', start_value: 3.8, target_value: 4.5, current_value: 4.1, unit: '分', progress: 0.43, priority: 'P0', owner_code: 'USER_WXM', owner_name: '王小铭', dept_code: 'DEPT_CS', dept_name: '客服部', period: 'Q2', start_date: '2026-04-01', end_date: '2026-06-30', ai_comment: '', }, { id: 'KR_005_2', objective_id: 'O_005', title: '全面梳理问题库,完善常见问题(FAQ)至 200 条', start_value: 80, target_value: 200, current_value: 120, unit: '条', progress: 0.33, priority: 'P1', owner_code: 'USER_WXM', owner_name: '王小铭', dept_code: 'DEPT_CS', dept_name: '客服部', period: 'Q2', start_date: '2026-04-01', end_date: '2026-05-31', ai_comment: '', }, { id: 'KR_005_3', objective_id: 'O_005', title: '优化工单响应流程,工单平均处理时长降至 4 小时', start_value: 12, target_value: 4, current_value: 8, unit: '小时', progress: 0.50, priority: 'P2', owner_code: 'USER_WXM', owner_name: '王小铭', dept_code: 'DEPT_CS', dept_name: '客服部', period: 'Q2', start_date: '2026-04-15', end_date: '2026-06-30', ai_comment: '', }, ], }, ] // ───────────────────────────────────────────── // Mock 复盘数据(已完成季度的复盘记录) // ───────────────────────────────────────────── export const MOCK_REVIEWS = [ { id: 'RV_001', objective_id: 'O_004', objective_title: '完成品牌 App 功能宣发', period: 'Q1', owner_code: 'USER_HPP', owner_name: '黄泡泡', dept_name: '市场部', score: 0.7, // 0.7 = 达成,对标飞书评分标准 kr_scores: [ { kr_id: 'KR_004_1', score: 0.7 }, { kr_id: 'KR_004_2', score: 0.7 }, ], went_well: '活动执行节奏稳定,用户反馈积极,创意素材质量较高。', to_improve: '线上活动宣传预热不足,导致首日流量不及预期。', next_steps: '提前 2 周做预热推广,增加 KOL 合作渠道覆盖。', reviewed_at: '2026-04-10T14:00:00', }, ] // ───────────────────────────────────────────── // Mock API 函数(替换为真实接口时只改这里) // ───────────────────────────────────────────── /** * 获取目标列表(含嵌套 KR) * 真实接口:POST /api/okr/objective/listed * @param {{ period?: string, dept_code?: string, owner_code?: string }} params */ export async function mockGetObjectives(params = {}) { await _fakeDelay(300) let list = [...MOCK_OBJECTIVES] if (params.period) list = list.filter(o => o.period === params.period) if (params.dept_code) list = list.filter(o => o.dept_code === params.dept_code) if (params.owner_code) list = list.filter(o => o.owner_code === params.owner_code) return list } /** * 获取复盘列表 * 真实接口:POST /api/okr/review/listed * @param {{ period?: string }} params */ export async function mockGetReviews(params = {}) { await _fakeDelay(200) let list = [...MOCK_REVIEWS] if (params.period) list = list.filter(r => r.period === params.period) return list } let _rvIdCounter = 100 /** 新建复盘(写回 MOCK_REVIEWS) */ export async function mockAddReview(data) { await _fakeDelay(150) const id = `RV_${++_rvIdCounter}` MOCK_REVIEWS.unshift({ id, ...data }) return { id } } /** 修改复盘(写回 MOCK_REVIEWS) */ export async function mockModifyReview(patch) { await _fakeDelay(150) const rv = MOCK_REVIEWS.find(r => r.id === patch.id) if (rv) Object.assign(rv, patch) } /** * 获取用户列表 —— 调用真实接口 /api/user/listed * 成功后同步覆盖 MOCK_USERS,并把 mock 目标的负责人也替换成真实用户 */ export async function mockGetUsers() { try { const res = await fetch(resolvePublicFetchUrl('/api/user/listed'), { method: 'POST', headers: { 'Content-Type': 'application/json', token: localStorage.getItem('token') || '' }, body: JSON.stringify({ page: 1, limit: 500 }), }).then(r => r.json()) const list = Array.isArray(res?.data) ? res.data : (res?.data?.list || []) if (list.length > 0) { const normalized = list.map(u => ({ ...u, user_code: String(u.user_code ?? u.userCode ?? ''), realname: u.realname || u.username || u.user_code || '', })) // 覆盖本地 mock 用户表,之后所有查询都用真实用户 MOCK_USERS.splice(0, MOCK_USERS.length, ...normalized) // 把 mock 目标里的假 owner_code / owner_name 替换成真实用户 // 循环分配:第 i 个目标用第 i % n 个真实用户 MOCK_OBJECTIVES.forEach((obj, i) => { const u = normalized[i % normalized.length] obj.owner_code = u.user_code obj.owner_name = u.realname || u.username || u.user_code obj.key_results?.forEach((kr, j) => { const ku = normalized[(i + j + 1) % normalized.length] kr.owner_code = ku.user_code kr.owner_name = ku.realname || ku.username || ku.user_code // 同步更新步骤协作人(循环分配真实用户) kr.steps?.forEach((step, k) => { const su = normalized[(i + j + k + 2) % normalized.length] step.collaborator_code = su.user_code step.collaborator_name = su.realname || su.username || su.user_code }) }) }) MOCK_REVIEWS.forEach((rv) => { const obj = MOCK_OBJECTIVES.find(o => o.id === rv.objective_id) if (obj) { rv.owner_code = obj.owner_code rv.owner_name = obj.owner_name rv.dept_name = obj.dept_name } }) return normalized } } catch {} return [...MOCK_USERS] } /** * 获取部门列表 —— 调用真实接口 /api/dept/listed */ export async function mockGetDepts() { try { // 先拿公司列表,再拿部门 const compRes = await fetch(resolvePublicFetchUrl('/api/company/listed'), { method: 'POST', headers: { 'Content-Type': 'application/json', token: localStorage.getItem('token') || '' }, body: JSON.stringify({}), }).then(r => r.json()) const companies = Array.isArray(compRes?.data) ? compRes.data : (compRes?.data?.list || []) if (companies.length > 0) { const deptRes = await fetch(resolvePublicFetchUrl('/api/dept/listed'), { method: 'POST', headers: { 'Content-Type': 'application/json', token: localStorage.getItem('token') || '' }, body: JSON.stringify({ company_code: companies[0]?.company_code || companies[0]?.code }), }).then(r => r.json()) const list = Array.isArray(deptRes?.data) ? deptRes.data : (deptRes?.data?.list || []) if (list.length > 0) { // 统一字段:dept_code / dept_name(注意:必须先展开 d 再覆盖,否则 ...d 会把上面两行冲掉) return list.map(d => { const code = d.dept_code ?? d.code const name = d.dept_name ?? d.name ?? d.full_name ?? '' return { ...d, dept_code: code != null && code !== '' ? String(code) : '', dept_name: name, full_name: d.full_name || name, } }) } } } catch {} // 降级到 mock return [...MOCK_DEPTS] } // ── Objective / KR CRUD(mock,对接时替换为 request.post)──────────── /** 修改 Objective(写回 MOCK_OBJECTIVES 共享源数据,各页面下次读取都能感知) */ export async function mockModifyObjective(patch) { await _fakeDelay(150) const obj = MOCK_OBJECTIVES.find(o => o.id === patch.id) if (obj) Object.assign(obj, patch) } /** 修改 KR(写回 MOCK_OBJECTIVES 中对应的 key_results 条目) */ export async function mockModifyKr(patch) { await _fakeDelay(150) for (const obj of MOCK_OBJECTIVES) { const kr = (obj.key_results || []).find(k => k.id === patch.id) if (kr) { Object.assign(kr, patch); return } } } /** 删除 Objective */ export async function mockRemoveObjective({ id }) { await _fakeDelay(150) const idx = MOCK_OBJECTIVES.findIndex(o => o.id === id) if (idx !== -1) MOCK_OBJECTIVES.splice(idx, 1) } /** 删除 KR */ export async function mockRemoveKr({ id }) { await _fakeDelay(150) for (const obj of MOCK_OBJECTIVES) { const idx = (obj.key_results || []).findIndex(k => k.id === id) if (idx !== -1) { obj.key_results.splice(idx, 1); return } } } let _objIdCounter = 100 let _krIdCounter = 100 /** 新建 Objective */ export async function mockAddObjective(data) { await _fakeDelay(200) const id = `O_${++_objIdCounter}` const obj = { id, key_results: [], progress: 0, status: 'not_started', created_at: new Date().toISOString(), ...data } MOCK_OBJECTIVES.push(obj) return { id } } /** 新建 KR */ export async function mockAddKr(data) { await _fakeDelay(200) const id = `KR_${++_krIdCounter}` const kr = { id, steps: [], progress: 0, current_value: data.start_value ?? 0, ...data } const obj = MOCK_OBJECTIVES.find(o => o.id === data.objective_id) if (obj) obj.key_results = [...(obj.key_results || []), kr] return { id } } // ── 步骤 CRUD(mock,对接时替换为 request.post)───────────────────── let _stepIdCounter = 100 export async function mockAddStep({ kr_id, title, description, collaborator_code, collaborator_name, due_date }) { await _fakeDelay(150) const id = `STEP_${++_stepIdCounter}` const step = { id, kr_id, title: title || '', description: description || '', collaborator_code: collaborator_code || '', collaborator_name: collaborator_name || '', status: 'todo', due_date: due_date || '' } // 写入 mock 数据 for (const obj of MOCK_OBJECTIVES) { const kr = obj.key_results?.find(k => k.id === kr_id) if (kr) { kr.steps = [...(kr.steps || []), step]; break } } return { id } } export async function mockModifyStep({ id, ...patch }) { await _fakeDelay(150) for (const obj of MOCK_OBJECTIVES) { for (const kr of (obj.key_results || [])) { const step = (kr.steps || []).find(s => s.id === id) if (step) { Object.assign(step, patch); return } } } } export async function mockRemoveStep({ id }) { await _fakeDelay(150) for (const obj of MOCK_OBJECTIVES) { for (const kr of (obj.key_results || [])) { const idx = (kr.steps || []).findIndex(s => s.id === id) if (idx !== -1) { kr.steps.splice(idx, 1); return } } } } /** * 获取当前用户参与的所有步骤(含所属 KR/O 信息) * @param {string} userCode */ export function mockGetMySteps(userCode) { const result = [] for (const obj of MOCK_OBJECTIVES) { for (const kr of (obj.key_results || [])) { for (const step of (kr.steps || [])) { if (step.collaborator_code === userCode) { result.push({ ...step, kr_title: kr.title, kr_id: kr.id, obj_title: obj.title, obj_id: obj.id, period: kr.period, end_date: kr.end_date }) } } } } return result } // 模拟网络延迟 function _fakeDelay(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } // ───────────────────────────────────────────── // 工具函数(前端通用) // ───────────────────────────────────────────── /** * 根据进度值返回对应颜色 * 0~0.3: 红色(落后) 0.3~0.7: 橙色(进行中) 0.7~1.0: 绿色(良好) */ export function progressColor(val) { if (val >= 0.7) return '#52c41a' if (val >= 0.3) return '#fa8c16' return '#ff4d4f' } /** * 进度百分比文字 */ export function progressText(val) { return Math.round((val ?? 0) * 100) + '%' } /** * 优先级配置 */ export const PRIORITY_MAP = { P0: { label: 'P0', color: '#ff4d4f', bg: '#fff1f0', border: '#ffa39e' }, P1: { label: 'P1', color: '#fa8c16', bg: '#fff7e6', border: '#ffd591' }, P2: { label: 'P2', color: '#1890ff', bg: '#e6f7ff', border: '#91d5ff' }, } /** * 周期选项(筛选用) */ export const PERIOD_OPTIONS = [ { label: '全部', value: '' }, { label: 'Q1', value: 'Q1' }, { label: 'Q2', value: 'Q2' }, { label: 'Q3', value: 'Q3' }, { label: 'Q4', value: 'Q4' }, ] /** * 根据用户名称生成首字母头像颜色(固定映射,同一人颜色不变) */ const AVATAR_COLORS = ['#5B5BD6', '#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4', '#FF5722'] export function avatarColor(name = '') { let hash = 0 for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash) return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length] } /** * 取用户姓名首字(用于头像显示) */ export function avatarChar(name = '') { return name ? name[0] : '?' }