Files
weihai_erp/src/data/okrMock.js
T
xiaojibeier f3726f0810 first
2026-07-06 03:36:46 +08:00

746 lines
31 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 复盘时间
*
* ── 接口约定(待服务端实现)──────────────────────────────────
* 所有接口统一 POSTtoken 放 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 CRUDmock,对接时替换为 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 }
}
// ── 步骤 CRUDmock,对接时替换为 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] : '?'
}