diff --git a/build/plugins/router.ts b/build/plugins/router.ts index a1dd133..cae40b4 100644 --- a/build/plugins/router.ts +++ b/build/plugins/router.ts @@ -126,29 +126,34 @@ export function setupElegantRouter() { order: 0, keepAlive: true }, + 'personal-center_my-item': { + icon: 'mdi:checkbox-multiple-blank-circle-outline', + order: 1, + keepAlive: true + }, 'personal-center_my-weekly': { icon: 'mdi:calendar-week-outline', - order: 1, + order: 2, keepAlive: true }, 'personal-center_my-monthly': { icon: 'mdi:calendar-month-outline', - order: 2, + order: 3, keepAlive: true }, 'personal-center_my-performance': { icon: 'mdi:trophy-outline', - order: 3, + order: 4, keepAlive: true }, 'personal-center_my-application': { icon: 'mdi:file-document-outline', - order: 4, + order: 5, keepAlive: true }, 'personal-center_pending-approval': { icon: 'mdi:check-decagram-outline', - order: 5, + order: 6, keepAlive: true }, system: { diff --git a/docs/frontend-page-resource-manifest.json b/docs/frontend-page-resource-manifest.json index e531a4b..bb5db84 100644 --- a/docs/frontend-page-resource-manifest.json +++ b/docs/frontend-page-resource-manifest.json @@ -1,46 +1,13 @@ { - "generatedAt": "2026-05-13T10:54:08.684Z", + "generatedAt": "2026-05-19T07:08:28.081Z", "description": "Frontend visible page resource whitelist for backend route/menu configuration.", "rules": { "directoryComponent": "layout.base", "pageComponentPattern": "view.", "singlePageComponentPattern": "layout.$view." }, - "total": 21, + "total": 22, "items": [ - { - "name": "workbench", - "path": "/workbench", - "component": "layout.base$view.workbench", - "title": "workbench", - "routeTitle": "workbench", - "i18nKey": "route.workbench", - "icon": "mdi:view-dashboard-outline", - "localIcon": null, - "order": 1, - "hideInMenu": false, - "keepAlive": true, - "activeMenu": null, - "multiTab": false, - "fixedIndexInTab": null, - "redirect": null, - "props": null, - "meta": { - "title": "workbench", - "i18nKey": "route.workbench", - "icon": "mdi:view-dashboard-outline", - "localIcon": null, - "order": 1, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": false, - "fixedIndexInTab": null - }, - "parentName": null, - "pageType": "single", - "source": "generated" - }, { "name": "product_list", "path": "/product/list", @@ -111,7 +78,7 @@ "name": "ticket_my-submitted", "path": "/ticket/my-submitted", "component": "view.ticket_my-submitted", - "title": "ticket_my-submitted", + "title": "我提交的工单", "routeTitle": "ticket_my-submitted", "i18nKey": "route.ticket_my-submitted", "icon": "mdi:upload-outline", @@ -125,7 +92,7 @@ "redirect": null, "props": null, "meta": { - "title": "ticket_my-submitted", + "title": "我提交的工单", "i18nKey": "route.ticket_my-submitted", "icon": "mdi:upload-outline", "localIcon": null, @@ -144,7 +111,7 @@ "name": "ticket_my-pending", "path": "/ticket/my-pending", "component": "view.ticket_my-pending", - "title": "ticket_my-pending", + "title": "待我处理的工单", "routeTitle": "ticket_my-pending", "i18nKey": "route.ticket_my-pending", "icon": "mdi:inbox-arrow-down-outline", @@ -158,7 +125,7 @@ "redirect": null, "props": null, "meta": { - "title": "ticket_my-pending", + "title": "待我处理的工单", "i18nKey": "route.ticket_my-pending", "icon": "mdi:inbox-arrow-down-outline", "localIcon": null, @@ -177,7 +144,7 @@ "name": "metrics_project-progress", "path": "/metrics/project-progress", "component": "view.metrics_project-progress", - "title": "metrics_project-progress", + "title": "项目进度", "routeTitle": "metrics_project-progress", "i18nKey": "route.metrics_project-progress", "icon": "mdi:progress-clock", @@ -191,7 +158,7 @@ "redirect": null, "props": null, "meta": { - "title": "metrics_project-progress", + "title": "项目进度", "i18nKey": "route.metrics_project-progress", "icon": "mdi:progress-clock", "localIcon": null, @@ -210,7 +177,7 @@ "name": "metrics_member-efficiency", "path": "/metrics/member-efficiency", "component": "view.metrics_member-efficiency", - "title": "metrics_member-efficiency", + "title": "员工能效", "routeTitle": "metrics_member-efficiency", "i18nKey": "route.metrics_member-efficiency", "icon": "mdi:account-multiple-check-outline", @@ -224,7 +191,7 @@ "redirect": null, "props": null, "meta": { - "title": "metrics_member-efficiency", + "title": "员工能效", "i18nKey": "route.metrics_member-efficiency", "icon": "mdi:account-multiple-check-outline", "localIcon": null, @@ -243,7 +210,7 @@ "name": "metrics_worktime", "path": "/metrics/worktime", "component": "view.metrics_worktime", - "title": "metrics_worktime", + "title": "工时统计", "routeTitle": "metrics_worktime", "i18nKey": "route.metrics_worktime", "icon": "mdi:clock-time-five-outline", @@ -257,7 +224,7 @@ "redirect": null, "props": null, "meta": { - "title": "metrics_worktime", + "title": "工时统计", "i18nKey": "route.metrics_worktime", "icon": "mdi:clock-time-five-outline", "localIcon": null, @@ -272,11 +239,77 @@ "pageType": "leaf", "source": "generated" }, + { + "name": "personal-center_my-profile", + "path": "/personal-center/my-profile", + "component": "view.personal-center_my-profile", + "title": "个人信息", + "routeTitle": "personal-center_my-profile", + "i18nKey": "route.personal-center_my-profile", + "icon": "mdi:account-box-outline", + "localIcon": null, + "order": 0, + "hideInMenu": false, + "keepAlive": true, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "个人信息", + "i18nKey": "route.personal-center_my-profile", + "icon": "mdi:account-box-outline", + "localIcon": null, + "order": 0, + "keepAlive": true, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "personal-center", + "pageType": "leaf", + "source": "generated" + }, + { + "name": "personal-center_my-item", + "path": "/personal-center/my-item", + "component": "view.personal-center_my-item", + "title": "我的事项", + "routeTitle": "personal-center_my-item", + "i18nKey": "route.personal-center_my-item", + "icon": "mdi:checkbox-multiple-blank-circle-outline", + "localIcon": null, + "order": 1, + "hideInMenu": false, + "keepAlive": true, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "我的事项", + "i18nKey": "route.personal-center_my-item", + "icon": "mdi:checkbox-multiple-blank-circle-outline", + "localIcon": null, + "order": 1, + "keepAlive": true, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "personal-center", + "pageType": "leaf", + "source": "generated" + }, { "name": "personal-center_my-weekly", "path": "/personal-center/my-weekly", "component": "view.personal-center_my-weekly", - "title": "personal-center_my-weekly", + "title": "我的周报", "routeTitle": "personal-center_my-weekly", "i18nKey": "route.personal-center_my-weekly", "icon": "mdi:calendar-week-outline", @@ -290,7 +323,7 @@ "redirect": null, "props": null, "meta": { - "title": "personal-center_my-weekly", + "title": "我的周报", "i18nKey": "route.personal-center_my-weekly", "icon": "mdi:calendar-week-outline", "localIcon": null, @@ -309,7 +342,7 @@ "name": "personal-center_my-monthly", "path": "/personal-center/my-monthly", "component": "view.personal-center_my-monthly", - "title": "personal-center_my-monthly", + "title": "我的月报", "routeTitle": "personal-center_my-monthly", "i18nKey": "route.personal-center_my-monthly", "icon": "mdi:calendar-month-outline", @@ -323,7 +356,7 @@ "redirect": null, "props": null, "meta": { - "title": "personal-center_my-monthly", + "title": "我的月报", "i18nKey": "route.personal-center_my-monthly", "icon": "mdi:calendar-month-outline", "localIcon": null, @@ -342,7 +375,7 @@ "name": "personal-center_my-performance", "path": "/personal-center/my-performance", "component": "view.personal-center_my-performance", - "title": "personal-center_my-performance", + "title": "我的绩效", "routeTitle": "personal-center_my-performance", "i18nKey": "route.personal-center_my-performance", "icon": "mdi:trophy-outline", @@ -356,7 +389,7 @@ "redirect": null, "props": null, "meta": { - "title": "personal-center_my-performance", + "title": "我的绩效", "i18nKey": "route.personal-center_my-performance", "icon": "mdi:trophy-outline", "localIcon": null, @@ -375,7 +408,7 @@ "name": "personal-center_my-application", "path": "/personal-center/my-application", "component": "view.personal-center_my-application", - "title": "personal-center_my-application", + "title": "我的申请", "routeTitle": "personal-center_my-application", "i18nKey": "route.personal-center_my-application", "icon": "mdi:file-document-outline", @@ -389,7 +422,7 @@ "redirect": null, "props": null, "meta": { - "title": "personal-center_my-application", + "title": "我的申请", "i18nKey": "route.personal-center_my-application", "icon": "mdi:file-document-outline", "localIcon": null, @@ -408,7 +441,7 @@ "name": "personal-center_pending-approval", "path": "/personal-center/pending-approval", "component": "view.personal-center_pending-approval", - "title": "personal-center_pending-approval", + "title": "待我审批", "routeTitle": "personal-center_pending-approval", "i18nKey": "route.personal-center_pending-approval", "icon": "mdi:check-decagram-outline", @@ -422,7 +455,7 @@ "redirect": null, "props": null, "meta": { - "title": "personal-center_pending-approval", + "title": "待我审批", "i18nKey": "route.personal-center_pending-approval", "icon": "mdi:check-decagram-outline", "localIcon": null, @@ -639,7 +672,7 @@ "name": "infra_state-machine", "path": "/infra/state-machine", "component": "view.infra_state-machine", - "title": "infra_state-machine", + "title": "状态机管理", "routeTitle": "infra_state-machine", "i18nKey": "route.infra_state-machine", "icon": "mdi:state-machine", @@ -653,7 +686,7 @@ "redirect": null, "props": null, "meta": { - "title": "infra_state-machine", + "title": "状态机管理", "i18nKey": "route.infra_state-machine", "icon": "mdi:state-machine", "localIcon": null, @@ -672,7 +705,7 @@ "name": "infra_rd-code", "path": "/infra/rd-code", "component": "view.infra_rd-code", - "title": "infra_rd-code", + "title": "研发令号", "routeTitle": "infra_rd-code", "i18nKey": "route.infra_rd-code", "icon": "mdi:identifier", @@ -686,7 +719,7 @@ "redirect": null, "props": null, "meta": { - "title": "infra_rd-code", + "title": "研发令号", "i18nKey": "route.infra_rd-code", "icon": "mdi:identifier", "localIcon": null, diff --git a/src/constants/dict.ts b/src/constants/dict.ts index fff7e59..e745da5 100644 --- a/src/constants/dict.ts +++ b/src/constants/dict.ts @@ -84,6 +84,14 @@ export const RDMS_PROJECT_EXECUTION_TYPE_DICT_CODE = 'rdms_project_execution_typ */ export const OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE = 'object_status_model_object_type'; +/** + * 工作日志完成难度字典编码 + * + * 对应业务字段:任务/个人事项工作日志中的 difficulty + * 来源口径:后端工作日志表 `rdms_task_worklog.difficulty` 字段注释明确使用字典 `rdms_worklog_difficulty` + */ +export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_worklog_difficulty'; + /** * 需求允许删除的状态字典编码 * diff --git a/src/constants/status-tag.ts b/src/constants/status-tag.ts index 78e9e9f..7a21663 100644 --- a/src/constants/status-tag.ts +++ b/src/constants/status-tag.ts @@ -15,7 +15,8 @@ export type StatusDomain = | 'project' | 'product' | 'requirement' - | 'workOrder'; + | 'workOrder' + | 'personalItem'; const statusTagTypeRegistry: Record> = { // 项目-执行 @@ -53,7 +54,14 @@ const statusTagTypeRegistry: Record> // 需求(待补全) requirement: {}, // 工单(待补全) - workOrder: {} + workOrder: {}, + // 个人事项 + personalItem: { + pending: 'info', + active: 'primary', + completed: 'success', + cancelled: 'danger' + } }; export function getStatusTagType(domain: StatusDomain, statusCode: string | null | undefined): StatusTagType { @@ -63,3 +71,7 @@ export function getStatusTagType(domain: StatusDomain, statusCode: string | null return statusTagTypeRegistry[domain][statusCode] || 'info'; } + +export function getPersonalItemStatusTagType(statusCode: string | null | undefined) { + return getStatusTagType('personalItem', statusCode); +} diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index 64ccf6d..7283392 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -168,6 +168,7 @@ const local: App.I18n.Schema = { metrics_worktime: 'Worktime', 'personal-center': 'Personal Center', 'personal-center_my-profile': 'My Profile', + 'personal-center_my-item': 'My Items', 'personal-center_my-weekly': 'My Weekly Report', 'personal-center_my-monthly': 'My Monthly Report', 'personal-center_my-performance': 'My Performance', diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index 19c9cee..71d4828 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -168,6 +168,7 @@ const local: App.I18n.Schema = { metrics_worktime: '工时统计', 'personal-center': '个人中心', 'personal-center_my-profile': '个人信息', + 'personal-center_my-item': '我的事项', 'personal-center_my-weekly': '我的周报', 'personal-center_my-monthly': '我的月报', 'personal-center_my-performance': '我的绩效', diff --git a/src/router/elegant/imports.ts b/src/router/elegant/imports.ts index 754dc61..277c561 100644 --- a/src/router/elegant/imports.ts +++ b/src/router/elegant/imports.ts @@ -34,6 +34,7 @@ export const views: Record Promise import("@/views/metrics/project-progress/index.vue"), metrics_worktime: () => import("@/views/metrics/worktime/index.vue"), "personal-center_my-application": () => import("@/views/personal-center/my-application/index.vue"), + "personal-center_my-item": () => import("@/views/personal-center/my-item/index.vue"), "personal-center_my-monthly": () => import("@/views/personal-center/my-monthly/index.vue"), "personal-center_my-performance": () => import("@/views/personal-center/my-performance/index.vue"), "personal-center_my-profile": () => import("@/views/personal-center/my-profile/index.vue"), diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index 5f1a871..95d6452 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -291,6 +291,18 @@ export const generatedRoutes: GeneratedRoute[] = [ keepAlive: true } }, + { + name: 'personal-center_my-item', + path: '/personal-center/my-item', + component: 'view.personal-center_my-item', + meta: { + title: 'personal-center_my-item', + i18nKey: 'route.personal-center_my-item', + icon: 'mdi:checkbox-multiple-blank-circle-outline', + order: 1, + keepAlive: true + } + }, { name: 'personal-center_my-monthly', path: '/personal-center/my-monthly', diff --git a/src/router/elegant/transform.ts b/src/router/elegant/transform.ts index ee360d8..08bbf39 100644 --- a/src/router/elegant/transform.ts +++ b/src/router/elegant/transform.ts @@ -191,6 +191,7 @@ const routeMap: RouteMap = { "metrics_worktime": "/metrics/worktime", "personal-center": "/personal-center", "personal-center_my-application": "/personal-center/my-application", + "personal-center_my-item": "/personal-center/my-item", "personal-center_my-monthly": "/personal-center/my-monthly", "personal-center_my-performance": "/personal-center/my-performance", "personal-center_my-profile": "/personal-center/my-profile", diff --git a/src/service/api/auth.ts b/src/service/api/auth.ts index 21a47a2..0abeb83 100644 --- a/src/service/api/auth.ts +++ b/src/service/api/auth.ts @@ -1,7 +1,7 @@ import { SYSTEM_SERVICE_PREFIX } from '@/constants/service'; import { request } from '../request'; import { clearUserRouteCache } from './route'; -import type { ServiceRequestResult } from './shared'; +import { type ServiceRequestResult, mapServiceResult, normalizeStringId } from './shared'; /** 后端登录返回 */ interface BackendLoginToken { @@ -38,6 +38,14 @@ interface BackendMyProfileDetailDTO { position?: Api.SystemManage.PostSimple | null; } +interface BackendFileDTO { + id: string | number; + configId: string | number; + name?: string | null; + path: string; + url: string; +} + let userInfoPromise: Promise> | null = null; /** 将后端 token 结构转换成前端现有结构 */ @@ -187,6 +195,23 @@ export function fetchUpdateMyProfile(data: Api.Auth.UpdateMyProfileParams) { } /** 修改当前登录人密码 */ +export async function fetchUpdateMyAvatar(file: File) { + const formData = new FormData(); + formData.append('file', file); + + const result = await request({ + url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update-avatar`, + method: 'put', + data: formData + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + id: normalizeStringId(data.id), + configId: normalizeStringId(data.configId) + })); +} + export function fetchUpdateMyPassword(data: Api.Auth.UpdateMyPasswordParams) { return request({ url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update-password`, diff --git a/src/service/api/index.ts b/src/service/api/index.ts index 308d494..89f7f5d 100644 --- a/src/service/api/index.ts +++ b/src/service/api/index.ts @@ -3,6 +3,7 @@ export * from './dict'; export * from './file'; export * from './infra'; export * from './object-context'; +export * from './personal-item'; export * from './product'; export * from './project'; export * from './project-shared'; diff --git a/src/service/api/personal-item.ts b/src/service/api/personal-item.ts new file mode 100644 index 0000000..5ce14b4 --- /dev/null +++ b/src/service/api/personal-item.ts @@ -0,0 +1,872 @@ +import dayjs from 'dayjs'; +import type { ConfigType } from 'dayjs'; +import type { FlatResponseData } from '@sa/axios'; +import { WEB_SERVICE_PREFIX } from '@/constants/service'; +import { request } from '../request'; +import { + type ProjectExecutionResponse, + type TaskWorklogResponse, + normalizeProjectLocalDate, + normalizeTaskWorklog +} from './project-shared'; +import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared'; + +type PersonalItemRecord = Api.PersonalItem.PersonalItem; +type PersonalItemWorklogRecord = Api.Project.TaskWorklog; +type PersonalItemResult = Promise>; +type StringIdResponse = string | number; +type PersonalItemLocalDateValue = string | number[] | null; +type AttachmentItemResponse = Omit & { + fileId?: StringIdResponse; + id?: StringIdResponse; +}; +type PersonalItemLifecycleActionResponse = Omit & { + needReason?: boolean | number | string | null; +}; +type PersonalItemResponse = Omit< + Api.PersonalItem.PersonalItem, + | 'id' + | 'ownerId' + | 'terminal' + | 'allowEdit' + | 'availableActions' + | 'plannedStartDate' + | 'plannedEndDate' + | 'actualStartDate' + | 'actualEndDate' + | 'attachments' + | 'totalSpentHours' +> & { + id: StringIdResponse; + ownerId: StringIdResponse; + terminal?: boolean | number | string | null; + allowEdit?: boolean | number | string | null; + availableActions?: PersonalItemLifecycleActionResponse[] | null; + plannedStartDate?: PersonalItemLocalDateValue; + plannedEndDate?: PersonalItemLocalDateValue; + actualStartDate?: PersonalItemLocalDateValue; + actualEndDate?: PersonalItemLocalDateValue; + attachments?: AttachmentItemResponse[] | null; + progressRate?: number | null; + totalSpentHours?: number | string | null; +}; +type PersonalItemPageResponse = Omit & { + total: number | string; + list: PersonalItemResponse[]; +}; +type PersonalItemWorklogPageResponse = Api.Project.PageResult; +type PersonalItemExecutionOptionResponse = ProjectExecutionResponse & { + projectName?: string | null; +}; +type PersonalItemSaveRequest = { + executionId?: string; + taskTitle: string; + progressRate?: number; + plannedStartDate?: string; + plannedEndDate?: string; + taskDesc?: string; + attachments?: Array<{ + id?: string; + url: string; + name: string; + size?: number; + contentType?: string; + }>; +}; +type PersonalItemWorklogSaveRequest = { + startDate: string; + endDate: string; + durationHours: number; + progressRate: number; + workContent?: string; + attachments?: Array<{ + id?: string; + url: string; + name: string; + size?: number; + contentType?: string; + }>; + difficulty?: string; +}; + +const PERSONAL_ITEM_PREFIX = `${WEB_SERVICE_PREFIX}/project/personal-items`; + +const CURRENT_USER_ID = 'current-user'; +const CURRENT_USER_NAME = '当前用户'; + +const personalItems: PersonalItemRecord[] = createSeedItems(); +const personalItemWorklogs: PersonalItemWorklogRecord[] = createSeedWorklogs(); +const executionOptions: Api.PersonalItem.PersonalItemExecutionOption[] = createExecutionOptions(); + +function createSuccessResult(data: T): PersonalItemResult { + return Promise.resolve({ + data, + error: null, + response: undefined + } as unknown as FlatResponseData); +} + +function normalizePageTotal(total: number | string) { + const value = Number(total); + + return Number.isFinite(value) ? Math.max(0, value) : 0; +} + +function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null { + if (!list) { + return null; + } + + return list.map(item => { + const rawId = item.fileId ?? item.id; + + return { + ...item, + fileId: rawId === null || rawId === undefined ? '' : String(rawId) + }; + }); +} + +function normalizeBooleanFlag(value: boolean | number | string | null | undefined) { + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'number') { + return value === 1; + } + + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + + if (!normalized || normalized === '0' || normalized === 'false' || normalized === 'n') { + return false; + } + + return true; + } + + return false; +} + +function normalizeLifecycleActions( + actions?: PersonalItemLifecycleActionResponse[] | null +): Api.PersonalItem.PersonalItemLifecycleAction[] { + return (actions ?? []).map(action => ({ + actionCode: action.actionCode, + actionName: action.actionName ?? '', + needReason: normalizeBooleanFlag(action.needReason) + })); +} + +function normalizePersonalItem(response: PersonalItemResponse): Api.PersonalItem.PersonalItem { + return { + id: normalizeStringId(response.id), + taskTitle: response.taskTitle ?? '', + ownerId: normalizeStringId(response.ownerId), + statusCode: response.statusCode, + terminal: normalizeBooleanFlag(response.terminal), + allowEdit: normalizeBooleanFlag(response.allowEdit), + availableActions: normalizeLifecycleActions(response.availableActions), + progressRate: + typeof response.progressRate === 'number' ? response.progressRate : Number(response.progressRate ?? 0), + totalSpentHours: (() => { + if (typeof response.totalSpentHours === 'number') { + return response.totalSpentHours; + } + + if (response.totalSpentHours === null || response.totalSpentHours === undefined) { + return null; + } + + return Number(response.totalSpentHours); + })(), + plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate), + plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), + actualStartDate: normalizeProjectLocalDate(response.actualStartDate), + actualEndDate: normalizeProjectLocalDate(response.actualEndDate), + taskDesc: response.taskDesc ?? null, + lastStatusReason: response.lastStatusReason ?? null, + attachments: normalizeAttachments(response.attachments), + creator: response.creator ?? '', + createTime: response.createTime ?? '', + updater: response.updater ?? '', + updateTime: response.updateTime ?? '', + deleted: Boolean(response.deleted), + ownerName: response.ownerName ?? null, + ownerNickname: response.ownerNickname ?? null, + statusName: response.statusName ?? null + }; +} + +function normalizePersonalItemExecutionOption( + response: PersonalItemExecutionOptionResponse +): Api.PersonalItem.PersonalItemExecutionOption { + return { + executionId: normalizeStringId(response.id), + executionName: response.executionName ?? '', + projectId: normalizeStringId(response.projectId), + projectName: response.projectName ?? null + }; +} + +function toPersonalItemSaveRequest(data: Api.PersonalItem.SavePersonalItemParams): PersonalItemSaveRequest { + return { + executionId: data.executionId ?? undefined, + taskTitle: data.taskTitle.trim(), + progressRate: typeof data.progressRate === 'number' ? data.progressRate : undefined, + plannedStartDate: data.plannedStartDate ?? undefined, + plannedEndDate: data.plannedEndDate ?? undefined, + taskDesc: data.taskDesc ?? undefined, + attachments: + data.attachments?.map(item => ({ + id: item.fileId || undefined, + url: item.url, + name: item.name, + size: item.size, + contentType: item.contentType + })) ?? undefined + }; +} + +function toPersonalItemWorklogSaveRequest( + data: Api.PersonalItem.SavePersonalItemWorklogParams +): PersonalItemWorklogSaveRequest { + return { + startDate: data.startDate, + endDate: data.endDate, + durationHours: Number(data.durationHours.toFixed(1)), + progressRate: Number(data.progressRate.toFixed(2)), + workContent: data.workContent ?? undefined, + attachments: + data.attachments?.map(item => ({ + id: item.fileId || undefined, + url: item.url, + name: item.name, + size: item.size, + contentType: item.contentType + })) ?? undefined, + difficulty: data.difficulty ?? undefined + }; +} + +function createPersonalItemPageQuery(params: Api.PersonalItem.PersonalItemSearchParams = {}) { + const query = new URLSearchParams(); + + query.append('pageNo', String(params.pageNo ?? 1)); + query.append('pageSize', String(params.pageSize ?? 10)); + + if (params.keyword) { + query.append('keyword', params.keyword); + } + + if (params.ownerId) { + query.append('ownerId', params.ownerId); + } + + if (params.statusCode) { + query.append('statusCode', params.statusCode); + } + + params.updateTime?.forEach(item => { + if (item) { + query.append('updateTime', item); + } + }); + + return query.toString(); +} + +function createIdsQuery(ids: string[]) { + const query = new URLSearchParams(); + + ids.forEach(id => { + if (id) { + query.append('ids', id); + } + }); + + return query.toString(); +} + +function createBindExecutionQuery(payload: Api.PersonalItem.BindPersonalItemExecutionParams) { + const query = new URLSearchParams(); + + payload.ids.forEach(id => { + if (id) { + query.append('itemIds', id); + } + }); + query.append('executionId', payload.executionId); + + return query.toString(); +} + +function cloneAttachment(item: Api.Project.AttachmentItem): Api.Project.AttachmentItem { + return { ...item }; +} + +function cloneItem(item: PersonalItemRecord): PersonalItemRecord { + return { + ...item, + attachments: item.attachments?.map(cloneAttachment) ?? null + }; +} + +function cloneWorklog(item: PersonalItemWorklogRecord): PersonalItemWorklogRecord { + return { + ...item, + attachments: item.attachments?.map(cloneAttachment) ?? null + }; +} + +function normalizeDateTime(value?: ConfigType | null) { + const target = value ? dayjs(value) : dayjs(); + return target.isValid() ? target.format('YYYY-MM-DD HH:mm:ss') : dayjs().format('YYYY-MM-DD HH:mm:ss'); +} + +function normalizeDate(value?: ConfigType | null) { + if (!value) { + return null; + } + + const target = dayjs(value); + return target.isValid() ? target.format('YYYY-MM-DD') : null; +} + +function createSeedItems(): PersonalItemRecord[] { + const now = dayjs(); + + return [ + { + id: 'personal-item-1', + taskTitle: '整理供应商沟通纪要', + ownerId: CURRENT_USER_ID, + statusCode: 'active', + progressRate: 45, + plannedStartDate: normalizeDate(now.subtract(3, 'day')), + plannedEndDate: normalizeDate(now.add(2, 'day')), + actualStartDate: normalizeDate(now.subtract(2, 'day')), + actualEndDate: null, + taskDesc: '

补齐今天会议纪要,沉淀成一页内部记录,便于后续同步。

', + lastStatusReason: null, + attachments: null, + creator: CURRENT_USER_NAME, + createTime: normalizeDateTime(now.subtract(3, 'day').hour(9).minute(20).second(0)), + updater: CURRENT_USER_NAME, + updateTime: normalizeDateTime(now.subtract(2, 'hour')), + deleted: false, + ownerName: CURRENT_USER_NAME, + statusName: '进行中' + }, + { + id: 'personal-item-2', + taskTitle: '清理浏览器收藏夹里的项目入口', + ownerId: CURRENT_USER_ID, + statusCode: 'pending', + progressRate: 0, + plannedStartDate: normalizeDate(now.add(1, 'day')), + plannedEndDate: normalizeDate(now.add(4, 'day')), + actualStartDate: null, + actualEndDate: null, + taskDesc: '

把已经废弃的测试环境、旧文档入口统一清理。

', + lastStatusReason: null, + attachments: null, + creator: CURRENT_USER_NAME, + createTime: normalizeDateTime(now.subtract(2, 'day').hour(14).minute(10).second(0)), + updater: CURRENT_USER_NAME, + updateTime: normalizeDateTime(now.subtract(5, 'hour')), + deleted: false, + ownerName: CURRENT_USER_NAME, + statusName: '待处理' + }, + { + id: 'personal-item-3', + taskTitle: '补充账号开通说明截图', + ownerId: CURRENT_USER_ID, + statusCode: 'completed', + progressRate: 100, + plannedStartDate: normalizeDate(now.subtract(5, 'day')), + plannedEndDate: normalizeDate(now.subtract(2, 'day')), + actualStartDate: normalizeDate(now.subtract(5, 'day')), + actualEndDate: normalizeDate(now.subtract(1, 'day')), + taskDesc: '

为新同事入职说明补一版截图,后续发在群公告。

', + lastStatusReason: '已完成并同步团队', + attachments: null, + creator: CURRENT_USER_NAME, + createTime: normalizeDateTime(now.subtract(5, 'day').hour(11).minute(0).second(0)), + updater: CURRENT_USER_NAME, + updateTime: normalizeDateTime(now.subtract(1, 'day').hour(18).minute(30).second(0)), + deleted: false, + ownerName: CURRENT_USER_NAME, + statusName: '已完成' + } + ]; +} + +function createSeedWorklogs(): PersonalItemWorklogRecord[] { + const now = dayjs(); + + return [ + { + id: 'worklog-1', + taskId: 'personal-item-1', + userId: CURRENT_USER_ID, + userNickname: CURRENT_USER_NAME, + startDate: normalizeDate(now.subtract(2, 'day'))!, + endDate: normalizeDate(now.subtract(2, 'day'))!, + durationHours: 2.5, + progressRate: 30, + difficulty: '2', + workContent: '整理会议录音和重点结论,先输出初版纪要。', + attachments: null, + createTime: normalizeDateTime(now.subtract(2, 'day').hour(19)), + updateTime: normalizeDateTime(now.subtract(2, 'day').hour(19)) + }, + { + id: 'worklog-2', + taskId: 'personal-item-1', + userId: CURRENT_USER_ID, + userNickname: CURRENT_USER_NAME, + startDate: normalizeDate(now.subtract(1, 'day'))!, + endDate: normalizeDate(now.subtract(1, 'day'))!, + durationHours: 1.5, + progressRate: 45, + difficulty: '2', + workContent: '补全供应商待确认项并整理后续跟进人。', + attachments: null, + createTime: normalizeDateTime(now.subtract(1, 'day').hour(18)), + updateTime: normalizeDateTime(now.subtract(1, 'day').hour(18)) + }, + { + id: 'worklog-3', + taskId: 'personal-item-3', + userId: CURRENT_USER_ID, + userNickname: CURRENT_USER_NAME, + startDate: normalizeDate(now.subtract(5, 'day'))!, + endDate: normalizeDate(now.subtract(5, 'day'))!, + durationHours: 1, + progressRate: 60, + difficulty: '1', + workContent: '补拍账号开通流程截图。', + attachments: null, + createTime: normalizeDateTime(now.subtract(5, 'day').hour(15)), + updateTime: normalizeDateTime(now.subtract(5, 'day').hour(15)) + }, + { + id: 'worklog-4', + taskId: 'personal-item-3', + userId: CURRENT_USER_ID, + userNickname: CURRENT_USER_NAME, + startDate: normalizeDate(now.subtract(1, 'day'))!, + endDate: normalizeDate(now.subtract(1, 'day'))!, + durationHours: 0.5, + progressRate: 100, + difficulty: '1', + workContent: '校对文案并发到群公告。', + attachments: null, + createTime: normalizeDateTime(now.subtract(1, 'day').hour(18).minute(20)), + updateTime: normalizeDateTime(now.subtract(1, 'day').hour(18).minute(20)) + } + ]; +} + +function createExecutionOptions(): Api.PersonalItem.PersonalItemExecutionOption[] { + return [ + { + executionId: 'execution-1001', + executionName: '2026Q2 运营提效', + projectId: 'project-1001', + projectName: '运营中台优化' + }, + { + executionId: 'execution-1002', + executionName: '2026Q2 用户支持专项', + projectId: 'project-1002', + projectName: '基础平台升级' + }, + { + executionId: 'execution-1003', + executionName: '2026Q3 数据治理', + projectId: 'project-1003', + projectName: '数据资产规范化' + } + ]; +} + +function findItemIndex(id: string) { + return personalItems.findIndex(item => item.id === id); +} + +function getItemOrThrow(id: string) { + const item = personalItems.find(current => current.id === id && !current.deleted); + + if (!item) { + throw new Error(`personal item not found: ${id}`); + } + + return item; +} + +function sortItems(list: PersonalItemRecord[]) { + return [...list].sort((left, right) => dayjs(right.updateTime).valueOf() - dayjs(left.updateTime).valueOf()); +} + +function sortWorklogs(list: PersonalItemWorklogRecord[]) { + return [...list].sort((left, right) => { + const endDiff = dayjs(right.endDate).valueOf() - dayjs(left.endDate).valueOf(); + if (endDiff !== 0) { + return endDiff; + } + return dayjs(right.updateTime).valueOf() - dayjs(left.updateTime).valueOf(); + }); +} + +function getPersonalItemStatusName(statusCode: Api.PersonalItem.PersonalItemStatusCode) { + const statusNameMap: Partial> = { + pending: '待处理', + active: '进行中', + completed: '已完成' + }; + + return statusNameMap[statusCode] || statusCode; +} + +function removeItemsByIds(ids: string[]) { + const idSet = new Set(ids); + + for (let i = personalItems.length - 1; i >= 0; i -= 1) { + if (idSet.has(personalItems[i].id)) { + personalItems.splice(i, 1); + } + } + + for (let i = personalItemWorklogs.length - 1; i >= 0; i -= 1) { + if (idSet.has(personalItemWorklogs[i].taskId)) { + personalItemWorklogs.splice(i, 1); + } + } +} + +function sumWorklogHours(logs: PersonalItemWorklogRecord[]) { + return logs.reduce((sum, log) => sum + (log.durationHours ?? 0), 0); +} + +function syncItemFromWorklogs(itemId: string) { + const item = getItemOrThrow(itemId); + const logs = sortWorklogs(personalItemWorklogs.filter(log => log.taskId === itemId)); + + item.statusName = getPersonalItemStatusName(item.statusCode); + item.totalSpentHours = sumWorklogHours(logs); + + if (logs.length === 0) { + if (item.statusCode !== 'completed') { + item.progressRate = 0; + item.actualStartDate = null; + item.actualEndDate = null; + } + return; + } + + const latestLog = logs[0]; + const chronologicalLogs = [...logs].sort( + (left, right) => dayjs(left.startDate).valueOf() - dayjs(right.startDate).valueOf() + ); + + item.progressRate = latestLog.progressRate ?? item.progressRate; + item.actualStartDate = chronologicalLogs[0]?.startDate ?? item.actualStartDate; + item.actualEndDate = latestLog.endDate ?? item.actualEndDate; + item.updateTime = latestLog.updateTime; + item.updater = CURRENT_USER_NAME; + + if (item.statusCode === 'pending') { + item.statusCode = 'active'; + item.statusName = getPersonalItemStatusName(item.statusCode); + } +} + +function applySaveFields(target: PersonalItemRecord, payload: Api.PersonalItem.SavePersonalItemParams) { + target.taskTitle = payload.taskTitle.trim(); + target.ownerId = payload.ownerId || target.ownerId; + target.ownerName = CURRENT_USER_NAME; + target.plannedStartDate = payload.plannedStartDate; + target.plannedEndDate = payload.plannedEndDate; + target.taskDesc = payload.taskDesc ?? null; + target.attachments = payload.attachments?.map(cloneAttachment) ?? null; + target.updater = CURRENT_USER_NAME; + target.updateTime = normalizeDateTime(); +} + +function filterWorklogs(taskId: string, params?: Api.PersonalItem.PersonalItemWorklogSearchParams) { + return sortWorklogs( + personalItemWorklogs.filter(item => { + if (item.taskId !== taskId) { + return false; + } + + if (params?.userId && item.userId !== params.userId) { + return false; + } + + if (params?.startDate && dayjs(item.endDate).isBefore(dayjs(params.startDate), 'day')) { + return false; + } + + if (params?.endDate && dayjs(item.startDate).isAfter(dayjs(params.endDate), 'day')) { + return false; + } + + return true; + }) + ); +} + +export async function fetchGetPersonalItemPage(params: Api.PersonalItem.PersonalItemSearchParams = {}) { + const query = createPersonalItemPageQuery(params); + + const result = await request({ + ...safeJsonRequestConfig, + url: query ? `${PERSONAL_ITEM_PREFIX}/page?${query}` : `${PERSONAL_ITEM_PREFIX}/page`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + total: normalizePageTotal(data.total), + list: data.list.map(normalizePersonalItem) + })); +} + +export async function fetchGetPersonalItemDetail(id: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/${id}`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, normalizePersonalItem); +} + +export async function fetchCreatePersonalItem(data: Api.PersonalItem.SavePersonalItemParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: PERSONAL_ITEM_PREFIX, + method: 'post', + data: toPersonalItemSaveRequest(data) + }); + + const mapped = mapServiceResult(result as ServiceRequestResult, normalizeStringId); + + if (!mapped.error && mapped.data) { + const now = normalizeDateTime(); + const createdItem: PersonalItemRecord = { + id: mapped.data, + taskTitle: data.taskTitle.trim(), + ownerId: data.ownerId || CURRENT_USER_ID, + statusCode: 'pending', + progressRate: typeof data.progressRate === 'number' ? data.progressRate : 0, + plannedStartDate: data.plannedStartDate, + plannedEndDate: data.plannedEndDate, + actualStartDate: null, + actualEndDate: null, + taskDesc: data.taskDesc ?? null, + lastStatusReason: null, + attachments: data.attachments?.map(cloneAttachment) ?? null, + creator: CURRENT_USER_NAME, + createTime: now, + updater: CURRENT_USER_NAME, + updateTime: now, + deleted: false, + ownerName: CURRENT_USER_NAME, + statusName: getPersonalItemStatusName('pending') + }; + + personalItems.unshift(createdItem); + } + + return mapped; +} + +export async function fetchUpdatePersonalItem(data: Api.PersonalItem.UpdatePersonalItemParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/${data.id}`, + method: 'put', + data: toPersonalItemSaveRequest(data) + }); + + const mapped = mapServiceResult(result as ServiceRequestResult, value => Boolean(value)); + + if (!mapped.error && mapped.data) { + const targetIndex = findItemIndex(data.id); + + if (targetIndex >= 0) { + applySaveFields(personalItems[targetIndex], data); + } + } + + return mapped; +} + +export async function fetchChangePersonalItemStatus(id: string, data: Api.PersonalItem.ChangePersonalItemStatusParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/${id}/change-status`, + method: 'post', + data: { + actionCode: data.actionCode, + reason: data.reason ?? undefined + } + }); + + const mapped = mapServiceResult(result as ServiceRequestResult, value => Boolean(value)); + + if (!mapped.error && mapped.data) { + const target = personalItems.find(item => item.id === id); + + if (target) { + target.lastStatusReason = data.reason ?? null; + target.updater = CURRENT_USER_NAME; + target.updateTime = normalizeDateTime(); + + if (data.actionCode === 'start') { + target.statusCode = 'active'; + target.statusName = getPersonalItemStatusName('active'); + target.actualStartDate ??= normalizeDate(dayjs()); + target.actualEndDate = null; + } else if (data.actionCode === 'complete') { + target.statusCode = 'completed'; + target.statusName = getPersonalItemStatusName('completed'); + target.progressRate = 100; + target.actualStartDate ??= normalizeDate(dayjs()); + target.actualEndDate = normalizeDate(dayjs()); + } else if (data.actionCode === 'reopen') { + target.statusCode = 'active'; + target.statusName = getPersonalItemStatusName('active'); + target.actualStartDate ??= normalizeDate(dayjs()); + target.actualEndDate = null; + } + } + } + + return mapped; +} + +export async function fetchDeletePersonalItem(id: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/delete`, + method: 'delete', + params: { id } + }); + + const mapped = mapServiceResult(result as ServiceRequestResult, value => Boolean(value)); + + if (!mapped.error && mapped.data) { + removeItemsByIds([id]); + } + + return mapped; +} + +export async function fetchBatchDeletePersonalItems(payload: Api.PersonalItem.BatchDeletePersonalItemParams) { + const query = createIdsQuery(payload.ids); + const result = await request({ + ...safeJsonRequestConfig, + url: query ? `${PERSONAL_ITEM_PREFIX}/delete-list?${query}` : `${PERSONAL_ITEM_PREFIX}/delete-list`, + method: 'delete' + }); + + const mapped = mapServiceResult(result as ServiceRequestResult, value => Boolean(value)); + + if (!mapped.error && mapped.data) { + removeItemsByIds(payload.ids); + } + + return mapped; +} + +export async function fetchGetPersonalItemExecutionOptions() { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/owner/all-execution`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, data => + data.map(normalizePersonalItemExecutionOption) + ); +} + +export async function fetchBindPersonalItemsToExecution(payload: Api.PersonalItem.BindPersonalItemExecutionParams) { + const query = createBindExecutionQuery(payload); + const result = await request({ + ...safeJsonRequestConfig, + url: query ? `${PERSONAL_ITEM_PREFIX}/relate-execution?${query}` : `${PERSONAL_ITEM_PREFIX}/relate-execution`, + method: 'post' + }); + + return mapServiceResult(result as ServiceRequestResult, value => Boolean(value)); +} + +export function fetchStartPersonalItem(id: string): PersonalItemResult { + return fetchChangePersonalItemStatus(id, { actionCode: 'start' }) as PersonalItemResult; +} + +export function fetchCompletePersonalItem(id: string): PersonalItemResult { + return fetchChangePersonalItemStatus(id, { actionCode: 'complete' }) as PersonalItemResult; +} + +export function fetchReopenPersonalItem(id: string): PersonalItemResult { + return fetchChangePersonalItemStatus(id, { actionCode: 'reopen' }) as PersonalItemResult; +} + +export async function fetchGetPersonalItemWorklogPage( + taskId: string, + params: Api.PersonalItem.PersonalItemWorklogSearchParams = {} +) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeTaskWorklog) + })); +} + +export async function fetchCreatePersonalItemWorklog( + taskId: string, + data: Api.PersonalItem.SavePersonalItemWorklogParams +) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs`, + method: 'post', + data: toPersonalItemWorklogSaveRequest(data) + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +export function fetchUpdatePersonalItemWorklog( + taskId: string, + payload: { worklogId: string; data: Api.PersonalItem.SavePersonalItemWorklogParams } +): PersonalItemResult { + return request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs/${payload.worklogId}`, + method: 'put', + data: toPersonalItemWorklogSaveRequest(payload.data) + }); +} + +export function fetchDeletePersonalItemWorklog(taskId: string, worklogId: string): PersonalItemResult { + return request({ + ...safeJsonRequestConfig, + url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs/${worklogId}`, + method: 'delete' + }); +} diff --git a/src/service/api/workbench.ts b/src/service/api/workbench.ts deleted file mode 100644 index a987a0a..0000000 --- a/src/service/api/workbench.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 工作台聚合接口尚未开通,当前页面使用 src/views/workbench/mock.ts 的本地假数据。 -// 接口契约确认后,在此处补: -// - fetchGetWorkbenchSummary (Banner 摘要 + KPI) -// - fetchGetWorkbenchTodos (我的待办) -// - fetchGetWorkbenchActivity (最近动态) -// - fetchGetWorkbenchProjects (我参与的项目) -// 全部走 src/service/request/index.ts 的统一实例,并保持 ID 字符串口径。 -export {}; diff --git a/src/typings/api/personal-item.d.ts b/src/typings/api/personal-item.d.ts new file mode 100644 index 0000000..40c0387 --- /dev/null +++ b/src/typings/api/personal-item.d.ts @@ -0,0 +1,97 @@ +declare namespace Api { + namespace PersonalItem { + interface PageParams { + pageNo: number; + pageSize: number; + } + + type PersonalItemStatusCode = 'pending' | 'active' | 'paused' | 'completed' | 'cancelled'; + + interface PersonalItemLifecycleAction { + actionCode: string; + actionName: string; + needReason: boolean; + } + + interface PersonalItem { + id: string; + taskTitle: string; + ownerId: string; + statusCode: PersonalItemStatusCode; + terminal?: boolean; + allowEdit?: boolean; + availableActions?: PersonalItemLifecycleAction[] | null; + progressRate: number; + totalSpentHours?: number | null; + plannedStartDate: string | null; + plannedEndDate: string | null; + actualStartDate: string | null; + actualEndDate: string | null; + taskDesc: string | null; + lastStatusReason: string | null; + attachments: Api.Project.AttachmentItem[] | null; + creator: string; + createTime: string; + updater: string; + updateTime: string; + deleted: boolean; + ownerName?: string | null; + ownerNickname?: string | null; + statusName?: string | null; + } + + type PersonalItemSearchParams = CommonType.RecordNullable< + Pick & { + keyword: string; + ownerId: string; + statusCode: PersonalItemStatusCode; + updateTime: string[]; + } + >; + + interface PersonalItemPageResult { + total: number; + list: PersonalItem[]; + } + + interface SavePersonalItemParams { + taskTitle: string; + ownerId?: string; + executionId?: string | null; + progressRate?: number | null; + plannedStartDate: string | null; + plannedEndDate: string | null; + taskDesc: string | null; + attachments: Api.Project.AttachmentItem[] | null; + } + + interface UpdatePersonalItemParams extends SavePersonalItemParams { + id: string; + } + + interface ChangePersonalItemStatusParams { + actionCode: string; + reason?: string | null; + } + + interface PersonalItemExecutionOption { + executionId: string; + executionName: string; + projectId?: string | null; + projectName?: string | null; + } + + interface BatchDeletePersonalItemParams { + ids: string[]; + } + + interface BindPersonalItemExecutionParams { + ids: string[]; + executionId: string; + } + + type PersonalItemWorklog = Api.Project.TaskWorklog; + type PersonalItemWorklogSearchParams = Api.Project.TaskWorklogSearchParams; + type SavePersonalItemWorklogParams = Api.Project.SaveTaskWorklogParams; + } +} diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index ea7bd72..8813ba8 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -380,6 +380,7 @@ declare namespace Api { durationHours: number; /** 本次填报进度(0~100,scale=2) */ progressRate: number; + difficulty?: string | null; workContent: string | null; attachments?: AttachmentItem[] | null; createTime: string; @@ -403,6 +404,7 @@ declare namespace Api { durationHours: number; /** 本次填报进度(0~100,scale=2,必填) */ progressRate: number; + difficulty?: string | null; workContent?: string | null; /** 编辑语义:null 保留原值 / [] 清空 / [...] 替换 */ attachments?: AttachmentItem[] | null; diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 3497532..9af0628 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -151,6 +151,7 @@ declare module 'vue' { IconMdiFolderPlusOutline: typeof import('~icons/mdi/folder-plus-outline')['default'] IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] + IconMdiLinkVariant: typeof import('~icons/mdi/link-variant')['default'] IconMdiMenuDown: typeof import('~icons/mdi/menu-down')['default'] IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline')['default'] IconMdiPlus: typeof import('~icons/mdi/plus')['default'] diff --git a/src/typings/elegant-router.d.ts b/src/typings/elegant-router.d.ts index 6089926..d423099 100644 --- a/src/typings/elegant-router.d.ts +++ b/src/typings/elegant-router.d.ts @@ -45,6 +45,7 @@ declare module "@elegant-router/types" { "metrics_worktime": "/metrics/worktime"; "personal-center": "/personal-center"; "personal-center_my-application": "/personal-center/my-application"; + "personal-center_my-item": "/personal-center/my-item"; "personal-center_my-monthly": "/personal-center/my-monthly"; "personal-center_my-performance": "/personal-center/my-performance"; "personal-center_my-profile": "/personal-center/my-profile"; @@ -181,6 +182,7 @@ declare module "@elegant-router/types" { | "metrics_project-progress" | "metrics_worktime" | "personal-center_my-application" + | "personal-center_my-item" | "personal-center_my-monthly" | "personal-center_my-performance" | "personal-center_my-profile" diff --git a/src/views/personal-center/my-item/index.vue b/src/views/personal-center/my-item/index.vue new file mode 100644 index 0000000..fb034e2 --- /dev/null +++ b/src/views/personal-center/my-item/index.vue @@ -0,0 +1,667 @@ + + + + + diff --git a/src/views/personal-center/my-item/modules/personal-item-bind-execution-dialog.vue b/src/views/personal-center/my-item/modules/personal-item-bind-execution-dialog.vue new file mode 100644 index 0000000..14ff411 --- /dev/null +++ b/src/views/personal-center/my-item/modules/personal-item-bind-execution-dialog.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/views/personal-center/my-item/modules/personal-item-detail-dialog.vue b/src/views/personal-center/my-item/modules/personal-item-detail-dialog.vue new file mode 100644 index 0000000..688bd89 --- /dev/null +++ b/src/views/personal-center/my-item/modules/personal-item-detail-dialog.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue b/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue new file mode 100644 index 0000000..262f80e --- /dev/null +++ b/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue @@ -0,0 +1,325 @@ + + + + + diff --git a/src/views/personal-center/my-item/modules/personal-item-search.vue b/src/views/personal-center/my-item/modules/personal-item-search.vue new file mode 100644 index 0000000..a682d7d --- /dev/null +++ b/src/views/personal-center/my-item/modules/personal-item-search.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/views/personal-center/my-item/modules/personal-item-shared.ts b/src/views/personal-center/my-item/modules/personal-item-shared.ts new file mode 100644 index 0000000..94998d8 --- /dev/null +++ b/src/views/personal-center/my-item/modules/personal-item-shared.ts @@ -0,0 +1,105 @@ +import dayjs from 'dayjs'; +import { getPersonalItemStatusTagType } from '@/constants/status-tag'; + +export const personalItemStatusOptions = [ + { label: '待处理', value: 'pending' as const }, + { label: '进行中', value: 'active' as const }, + { label: '已完成', value: 'completed' as const } +]; + +const personalItemStatusLabelMap: Record = { + pending: '待开始', + active: '进行中', + paused: '已暂停', + completed: '已完成', + cancelled: '已取消' +}; + +export function getPersonalItemStatusLabel(statusCode: Api.PersonalItem.PersonalItemStatusCode | null | undefined) { + if (!statusCode) { + return '--'; + } + + return personalItemStatusLabelMap[statusCode] || '--'; +} + +export function resolvePersonalItemStatusTagType( + statusCode: Api.PersonalItem.PersonalItemStatusCode | null | undefined +) { + return getPersonalItemStatusTagType(statusCode); +} + +export function formatPersonalItemDate(value: string | null | undefined) { + if (!value) { + return '--'; + } + + const target = dayjs(value); + + if (!target.isValid()) { + return '--'; + } + + return target.format('YYYY-MM-DD'); +} + +export function formatPersonalItemDateTime(value: string | null | undefined) { + if (!value) { + return '--'; + } + + const target = dayjs(value); + + if (!target.isValid()) { + return '--'; + } + + return target.format('YYYY-MM-DD HH:mm:ss'); +} + +export function formatPersonalItemProgress(value: number | null | undefined) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return '0%'; + } + + const normalized = Math.round(Math.min(100, Math.max(0, value)) * 100) / 100; + return `${normalized}%`; +} + +export function formatPersonalItemName(value: string | null | undefined) { + return value?.trim() || '--'; +} + +export function formatPersonalItemOwnerName( + item: Pick +) { + return item.ownerNickname?.trim() || item.ownerName?.trim() || item.ownerId || '--'; +} + +export function formatPersonalItemDateRange(start: string | null | undefined, end: string | null | undefined) { + const startText = formatPersonalItemDate(start); + const endText = formatPersonalItemDate(end); + + if (startText === '--' && endText === '--') { + return '--'; + } + + return `${startText} ~ ${endText}`; +} + +export function isEmptyRichText(html: string | null | undefined) { + if (!html) { + return true; + } + + const text = html + .replace(/<[^>]+>/g, '') + .replace(/ /g, '') + .trim(); + + if (text) { + return false; + } + + return !/ +import { computed, nextTick, reactive, watch } from 'vue'; +import { useForm, useFormRules } from '@/hooks/common/form'; +import BusinessFormDialog from '@/components/custom/business-form-dialog.vue'; + +defineOptions({ name: 'PersonalItemStatusActionDialog' }); + +interface Props { + action: Api.PersonalItem.PersonalItemLifecycleAction | null; +} + +interface Emits { + (e: 'submit', reason: string | null): void; +} + +const props = defineProps(); +const emit = defineEmits(); + +const visible = defineModel('visible', { + default: false +}); + +const { formRef, validate } = useForm(); +const { createRequiredRule } = useFormRules(); + +const model = reactive({ + reason: '' +}); + +const rules = computed( + () => + ({ + reason: props.action?.needReason ? [createRequiredRule('请输入动作原因')] : [] + }) satisfies Record +); + +async function handleConfirm() { + await validate(); + emit('submit', model.reason.trim() || null); +} + +watch( + () => visible.value, + async value => { + if (!value) { + return; + } + + model.reason = ''; + await nextTick(); + formRef.value?.clearValidate(); + } +); + + + + + diff --git a/src/views/personal-center/my-item/modules/personal-item-worklog-form-dialog.vue b/src/views/personal-center/my-item/modules/personal-item-worklog-form-dialog.vue new file mode 100644 index 0000000..eaaedd3 --- /dev/null +++ b/src/views/personal-center/my-item/modules/personal-item-worklog-form-dialog.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/src/views/personal-center/my-item/modules/personal-item-worklog-panel.vue b/src/views/personal-center/my-item/modules/personal-item-worklog-panel.vue new file mode 100644 index 0000000..7f57aba --- /dev/null +++ b/src/views/personal-center/my-item/modules/personal-item-worklog-panel.vue @@ -0,0 +1,656 @@ + + + + + diff --git a/src/views/personal-center/my-profile/index.vue b/src/views/personal-center/my-profile/index.vue index a3aeb8e..c017a7d 100644 --- a/src/views/personal-center/my-profile/index.vue +++ b/src/views/personal-center/my-profile/index.vue @@ -1,14 +1,13 @@