feat(personal-item): 个人事项
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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.<routeName>",
|
||||
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
||||
},
|
||||
"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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* 需求允许删除的状态字典编码
|
||||
*
|
||||
|
||||
@@ -15,7 +15,8 @@ export type StatusDomain =
|
||||
| 'project'
|
||||
| 'product'
|
||||
| 'requirement'
|
||||
| 'workOrder';
|
||||
| 'workOrder'
|
||||
| 'personalItem';
|
||||
|
||||
const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>> = {
|
||||
// 项目-执行
|
||||
@@ -53,7 +54,14 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
||||
// 需求(待补全)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '我的绩效',
|
||||
|
||||
@@ -34,6 +34,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
"metrics_project-progress": () => 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"),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ServiceRequestResult<BackendUserInfoDTO>> | 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<BackendFileDTO>({
|
||||
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update-avatar`,
|
||||
method: 'put',
|
||||
data: formData
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<BackendFileDTO>, data => ({
|
||||
...data,
|
||||
id: normalizeStringId(data.id),
|
||||
configId: normalizeStringId(data.configId)
|
||||
}));
|
||||
}
|
||||
|
||||
export function fetchUpdateMyPassword(data: Api.Auth.UpdateMyPasswordParams) {
|
||||
return request<boolean>({
|
||||
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update-password`,
|
||||
|
||||
@@ -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';
|
||||
|
||||
872
src/service/api/personal-item.ts
Normal file
872
src/service/api/personal-item.ts
Normal file
@@ -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<T> = Promise<FlatResponseData<any, T>>;
|
||||
type StringIdResponse = string | number;
|
||||
type PersonalItemLocalDateValue = string | number[] | null;
|
||||
type AttachmentItemResponse = Omit<Api.Project.AttachmentItem, 'fileId'> & {
|
||||
fileId?: StringIdResponse;
|
||||
id?: StringIdResponse;
|
||||
};
|
||||
type PersonalItemLifecycleActionResponse = Omit<Api.PersonalItem.PersonalItemLifecycleAction, 'needReason'> & {
|
||||
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<Api.PersonalItem.PersonalItemPageResult, 'total' | 'list'> & {
|
||||
total: number | string;
|
||||
list: PersonalItemResponse[];
|
||||
};
|
||||
type PersonalItemWorklogPageResponse = Api.Project.PageResult<TaskWorklogResponse>;
|
||||
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<T>(data: T): PersonalItemResult<T> {
|
||||
return Promise.resolve({
|
||||
data,
|
||||
error: null,
|
||||
response: undefined
|
||||
} as unknown as FlatResponseData<any, T>);
|
||||
}
|
||||
|
||||
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: '<p>补齐今天会议纪要,沉淀成一页内部记录,便于后续同步。</p>',
|
||||
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: '<p>把已经废弃的测试环境、旧文档入口统一清理。</p>',
|
||||
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: '<p>为新同事入职说明补一版截图,后续发在群公告。</p>',
|
||||
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<Record<Api.PersonalItem.PersonalItemStatusCode, string>> = {
|
||||
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<PersonalItemPageResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${PERSONAL_ITEM_PREFIX}/page?${query}` : `${PERSONAL_ITEM_PREFIX}/page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PersonalItemPageResponse>, data => ({
|
||||
total: normalizePageTotal(data.total),
|
||||
list: data.list.map(normalizePersonalItem)
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchGetPersonalItemDetail(id: string) {
|
||||
const result = await request<PersonalItemResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PersonalItemResponse>, normalizePersonalItem);
|
||||
}
|
||||
|
||||
export async function fetchCreatePersonalItem(data: Api.PersonalItem.SavePersonalItemParams) {
|
||||
const result = await request<string | number>({
|
||||
...safeJsonRequestConfig,
|
||||
url: PERSONAL_ITEM_PREFIX,
|
||||
method: 'post',
|
||||
data: toPersonalItemSaveRequest(data)
|
||||
});
|
||||
|
||||
const mapped = mapServiceResult(result as ServiceRequestResult<string | number>, 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<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/${data.id}`,
|
||||
method: 'put',
|
||||
data: toPersonalItemSaveRequest(data)
|
||||
});
|
||||
|
||||
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, 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<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/${id}/change-status`,
|
||||
method: 'post',
|
||||
data: {
|
||||
actionCode: data.actionCode,
|
||||
reason: data.reason ?? undefined
|
||||
}
|
||||
});
|
||||
|
||||
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, 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<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/delete`,
|
||||
method: 'delete',
|
||||
params: { id }
|
||||
});
|
||||
|
||||
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, 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<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${PERSONAL_ITEM_PREFIX}/delete-list?${query}` : `${PERSONAL_ITEM_PREFIX}/delete-list`,
|
||||
method: 'delete'
|
||||
});
|
||||
|
||||
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, value => Boolean(value));
|
||||
|
||||
if (!mapped.error && mapped.data) {
|
||||
removeItemsByIds(payload.ids);
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export async function fetchGetPersonalItemExecutionOptions() {
|
||||
const result = await request<PersonalItemExecutionOptionResponse[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/owner/all-execution`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PersonalItemExecutionOptionResponse[]>, data =>
|
||||
data.map(normalizePersonalItemExecutionOption)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchBindPersonalItemsToExecution(payload: Api.PersonalItem.BindPersonalItemExecutionParams) {
|
||||
const query = createBindExecutionQuery(payload);
|
||||
const result = await request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${PERSONAL_ITEM_PREFIX}/relate-execution?${query}` : `${PERSONAL_ITEM_PREFIX}/relate-execution`,
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<boolean>, value => Boolean(value));
|
||||
}
|
||||
|
||||
export function fetchStartPersonalItem(id: string): PersonalItemResult<boolean> {
|
||||
return fetchChangePersonalItemStatus(id, { actionCode: 'start' }) as PersonalItemResult<boolean>;
|
||||
}
|
||||
|
||||
export function fetchCompletePersonalItem(id: string): PersonalItemResult<boolean> {
|
||||
return fetchChangePersonalItemStatus(id, { actionCode: 'complete' }) as PersonalItemResult<boolean>;
|
||||
}
|
||||
|
||||
export function fetchReopenPersonalItem(id: string): PersonalItemResult<boolean> {
|
||||
return fetchChangePersonalItemStatus(id, { actionCode: 'reopen' }) as PersonalItemResult<boolean>;
|
||||
}
|
||||
|
||||
export async function fetchGetPersonalItemWorklogPage(
|
||||
taskId: string,
|
||||
params: Api.PersonalItem.PersonalItemWorklogSearchParams = {}
|
||||
) {
|
||||
const result = await request<PersonalItemWorklogPageResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PersonalItemWorklogPageResponse>, data => ({
|
||||
...data,
|
||||
list: data.list.map(normalizeTaskWorklog)
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchCreatePersonalItemWorklog(
|
||||
taskId: string,
|
||||
data: Api.PersonalItem.SavePersonalItemWorklogParams
|
||||
) {
|
||||
const result = await request<string | number>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs`,
|
||||
method: 'post',
|
||||
data: toPersonalItemWorklogSaveRequest(data)
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||
}
|
||||
|
||||
export function fetchUpdatePersonalItemWorklog(
|
||||
taskId: string,
|
||||
payload: { worklogId: string; data: Api.PersonalItem.SavePersonalItemWorklogParams }
|
||||
): PersonalItemResult<boolean> {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs/${payload.worklogId}`,
|
||||
method: 'put',
|
||||
data: toPersonalItemWorklogSaveRequest(payload.data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDeletePersonalItemWorklog(taskId: string, worklogId: string): PersonalItemResult<boolean> {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs/${worklogId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// 工作台聚合接口尚未开通,当前页面使用 src/views/workbench/mock.ts 的本地假数据。
|
||||
// 接口契约确认后,在此处补:
|
||||
// - fetchGetWorkbenchSummary (Banner 摘要 + KPI)
|
||||
// - fetchGetWorkbenchTodos (我的待办)
|
||||
// - fetchGetWorkbenchActivity (最近动态)
|
||||
// - fetchGetWorkbenchProjects (我参与的项目)
|
||||
// 全部走 src/service/request/index.ts 的统一实例,并保持 ID 字符串口径。
|
||||
export {};
|
||||
97
src/typings/api/personal-item.d.ts
vendored
Normal file
97
src/typings/api/personal-item.d.ts
vendored
Normal file
@@ -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<PageParams, 'pageNo' | 'pageSize'> & {
|
||||
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;
|
||||
}
|
||||
}
|
||||
2
src/typings/api/project.d.ts
vendored
2
src/typings/api/project.d.ts
vendored
@@ -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;
|
||||
|
||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -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']
|
||||
|
||||
2
src/typings/elegant-router.d.ts
vendored
2
src/typings/elegant-router.d.ts
vendored
@@ -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"
|
||||
|
||||
667
src/views/personal-center/my-item/index.vue
Normal file
667
src/views/personal-center/my-item/index.vue
Normal file
@@ -0,0 +1,667 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, markRaw, nextTick, onActivated, reactive, ref } from 'vue';
|
||||
import type { TableInstance } from 'element-plus';
|
||||
import { ElButton, ElMessageBox, ElTag, ElTooltip } from 'element-plus';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import {
|
||||
fetchBatchDeletePersonalItems,
|
||||
fetchBindPersonalItemsToExecution,
|
||||
fetchChangePersonalItemStatus,
|
||||
fetchDeletePersonalItem,
|
||||
fetchGetPersonalItemDetail,
|
||||
fetchGetPersonalItemPage
|
||||
} from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import PersonalItemBindExecutionDialog from './modules/personal-item-bind-execution-dialog.vue';
|
||||
import PersonalItemDetailDialog from './modules/personal-item-detail-dialog.vue';
|
||||
import PersonalItemOperateDialog from './modules/personal-item-operate-dialog.vue';
|
||||
import PersonalItemSearch from './modules/personal-item-search.vue';
|
||||
import PersonalItemStatusActionDialog from './modules/personal-item-status-action-dialog.vue';
|
||||
import {
|
||||
formatPersonalItemDateRange,
|
||||
formatPersonalItemDateTime,
|
||||
formatPersonalItemOwnerName,
|
||||
formatPersonalItemProgress,
|
||||
getPersonalItemStatusLabel,
|
||||
resolvePersonalItemStatusTagType
|
||||
} from './modules/personal-item-shared';
|
||||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiPause from '~icons/mdi/pause';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
import IconMdiPlay from '~icons/mdi/play';
|
||||
import IconMdiRestart from '~icons/mdi/restart';
|
||||
import IconMdiSync from '~icons/mdi/sync';
|
||||
|
||||
defineOptions({ name: 'MyItem' });
|
||||
|
||||
type DetailTab = 'worklog';
|
||||
type PersonalItemOperateType = UI.TableOperateType | 'view';
|
||||
|
||||
interface PersonalItemRowAction {
|
||||
key: string;
|
||||
tooltip: string;
|
||||
icon: object;
|
||||
type: 'primary' | 'success' | 'warning' | 'danger';
|
||||
disabled?: boolean;
|
||||
onClick: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
const lifecycleActionIconMap: Record<string, object> = {
|
||||
start: markRaw(IconMdiPlay),
|
||||
pause: markRaw(IconMdiPause),
|
||||
resume: markRaw(IconMdiRestart),
|
||||
reopen: markRaw(IconMdiRestart),
|
||||
cancel: markRaw(IconMdiCloseCircleOutline),
|
||||
complete: markRaw(IconMdiCheckCircleOutline)
|
||||
};
|
||||
|
||||
const lifecycleActionTypeMap: Record<string, PersonalItemRowAction['type']> = {
|
||||
cancel: 'danger',
|
||||
pause: 'warning',
|
||||
complete: 'success',
|
||||
resume: 'primary',
|
||||
reopen: 'primary',
|
||||
start: 'primary'
|
||||
};
|
||||
|
||||
const lifecycleActionOrder: Record<string, number> = {
|
||||
pause: 1,
|
||||
cancel: 2,
|
||||
complete: 3,
|
||||
resume: 4,
|
||||
reopen: 5,
|
||||
start: 6
|
||||
};
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const currentUserId = computed(() => {
|
||||
const rawUserId = authStore.userInfo.userId;
|
||||
|
||||
return rawUserId ? String(rawUserId) : '';
|
||||
});
|
||||
|
||||
function getInitSearchParams(): Api.PersonalItem.PersonalItemSearchParams {
|
||||
return {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
keyword: undefined,
|
||||
ownerId: currentUserId.value || undefined,
|
||||
statusCode: undefined,
|
||||
updateTime: undefined
|
||||
};
|
||||
}
|
||||
|
||||
function transformPageResult(
|
||||
response: Awaited<ReturnType<typeof fetchGetPersonalItemPage>>,
|
||||
pageNo: number,
|
||||
pageSize: number
|
||||
) {
|
||||
if (!response.error) {
|
||||
return {
|
||||
data: response.data.list,
|
||||
pageNum: pageNo,
|
||||
pageSize,
|
||||
total: response.data.total
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: [],
|
||||
pageNum: pageNo,
|
||||
pageSize,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
const searchParams = reactive(getInitSearchParams());
|
||||
const tableRef = ref<TableInstance>();
|
||||
const checkedRowIds = ref<string[]>([]);
|
||||
const bindExecutionSubmitting = ref(false);
|
||||
|
||||
const selectedCount = computed(() => checkedRowIds.value.length);
|
||||
|
||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable({
|
||||
paginationProps: {
|
||||
currentPage: searchParams.pageNo,
|
||||
pageSize: searchParams.pageSize
|
||||
},
|
||||
api: () => fetchGetPersonalItemPage(searchParams),
|
||||
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
searchParams.pageSize = params.pageSize ?? 10;
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'selection', type: 'selection', width: 48 },
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{
|
||||
prop: 'taskTitle',
|
||||
label: '事项标题',
|
||||
minWidth: 260,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'ownerName',
|
||||
label: '负责人',
|
||||
minWidth: 140,
|
||||
formatter: row => formatPersonalItemOwnerName(row)
|
||||
},
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolvePersonalItemStatusTagType(row.statusCode)}>
|
||||
{getPersonalItemStatusLabel(row.statusCode)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'progressRate',
|
||||
label: '进度',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
formatter: row => formatPersonalItemProgress(row.progressRate)
|
||||
},
|
||||
{
|
||||
prop: 'plannedDateRange',
|
||||
label: '计划日期',
|
||||
minWidth: 220,
|
||||
formatter: row => formatPersonalItemDateRange(row.plannedStartDate, row.plannedEndDate)
|
||||
},
|
||||
{
|
||||
prop: 'actualDateRange',
|
||||
label: '实际日期',
|
||||
minWidth: 220,
|
||||
formatter: row => formatPersonalItemDateRange(row.actualStartDate, row.actualEndDate)
|
||||
},
|
||||
{
|
||||
prop: 'updateTime',
|
||||
label: '最近更新',
|
||||
minWidth: 180,
|
||||
formatter: row => formatPersonalItemDateTime(row.updateTime)
|
||||
},
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 240,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => renderRowActions(row)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { bool: operateVisible, setTrue: openOperateDialog, setFalse: closeOperateDialog } = useBoolean();
|
||||
const { bool: detailVisible, setTrue: openDetailDialog } = useBoolean();
|
||||
const {
|
||||
bool: bindExecutionVisible,
|
||||
setTrue: openBindExecutionDialog,
|
||||
setFalse: closeBindExecutionDialog
|
||||
} = useBoolean();
|
||||
const { bool: statusActionVisible, setTrue: openStatusActionDialog, setFalse: closeStatusActionDialog } = useBoolean();
|
||||
|
||||
const operateType = ref<PersonalItemOperateType>('add');
|
||||
const editingData = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||
const detailData = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||
const detailDefaultTab = ref<DetailTab>('worklog');
|
||||
const currentStatusAction = ref<Api.PersonalItem.PersonalItemLifecycleAction | null>(null);
|
||||
const currentStatusItem = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||
|
||||
async function openDetail(row: Api.PersonalItem.PersonalItem, defaultTab: DetailTab = 'worklog') {
|
||||
const { error, data: latestDetail } = await fetchGetPersonalItemDetail(row.id);
|
||||
|
||||
detailData.value = error || !latestDetail ? row : latestDetail;
|
||||
detailDefaultTab.value = defaultTab;
|
||||
openDetailDialog();
|
||||
}
|
||||
|
||||
function openView(row: Api.PersonalItem.PersonalItem) {
|
||||
operateType.value = 'view';
|
||||
editingData.value = row;
|
||||
openOperateDialog();
|
||||
}
|
||||
|
||||
// function createLifecycleAction(
|
||||
// fallback: {
|
||||
// key: string;
|
||||
// tooltip: string;
|
||||
// icon: object;
|
||||
// type: PersonalItemRowAction['type'];
|
||||
// actionCode: string;
|
||||
// },
|
||||
// action: Api.PersonalItem.PersonalItemLifecycleAction | null
|
||||
// ): PersonalItemRowAction {
|
||||
// return {
|
||||
// key: fallback.key,
|
||||
// tooltip: action?.actionName ?? fallback.tooltip,
|
||||
// icon: fallback.icon,
|
||||
// type: fallback.type,
|
||||
// disabled: !action,
|
||||
// onClick: async () =>
|
||||
// handleStatusAction(currentStatusItem.value!, {
|
||||
// actionCode: action?.actionCode ?? fallback.actionCode,
|
||||
// actionName: action?.actionName ?? fallback.tooltip,
|
||||
// needReason: action?.needReason ?? false
|
||||
// })
|
||||
// };
|
||||
// }
|
||||
|
||||
function buildRowActions(row: Api.PersonalItem.PersonalItem): PersonalItemRowAction[] {
|
||||
currentStatusItem.value = row;
|
||||
|
||||
const rawLifecycleActions = [...(row.availableActions ?? [])];
|
||||
const pauseAction = rawLifecycleActions.find(action => action.actionCode === 'pause') ?? null;
|
||||
const cancelAction = rawLifecycleActions.find(action => action.actionCode === 'cancel') ?? null;
|
||||
const completeAction = rawLifecycleActions.find(action => action.actionCode === 'complete') ?? null;
|
||||
|
||||
const lifecycleActions = rawLifecycleActions
|
||||
.filter(action => !['pause', 'cancel', 'complete'].includes(action.actionCode))
|
||||
.sort(
|
||||
(left, right) => (lifecycleActionOrder[left.actionCode] ?? 99) - (lifecycleActionOrder[right.actionCode] ?? 99)
|
||||
)
|
||||
.map(action => ({
|
||||
key: `status-${action.actionCode}`,
|
||||
tooltip: action.actionName,
|
||||
icon: markRaw(lifecycleActionIconMap[action.actionCode] ?? IconMdiSync),
|
||||
type: lifecycleActionTypeMap[action.actionCode] ?? 'primary',
|
||||
onClick: async () => handleStatusAction(row, action)
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'worklog',
|
||||
tooltip: '填报',
|
||||
icon: markRaw(IconMdiClipboardEditOutline),
|
||||
type: 'primary',
|
||||
onClick: async () => openDetail(row, 'worklog')
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
tooltip: '编辑',
|
||||
icon: markRaw(IconMdiPencilOutline),
|
||||
type: 'primary',
|
||||
onClick: async () => {
|
||||
operateType.value = 'edit';
|
||||
editingData.value = row;
|
||||
openOperateDialog();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'status-pause',
|
||||
tooltip: pauseAction?.actionName ?? '暂停',
|
||||
icon: markRaw(IconMdiPause),
|
||||
type: 'warning',
|
||||
disabled: !pauseAction,
|
||||
onClick: async () =>
|
||||
handleStatusAction(row, {
|
||||
actionCode: pauseAction?.actionCode ?? 'pause',
|
||||
actionName: pauseAction?.actionName ?? '暂停',
|
||||
needReason: pauseAction?.needReason ?? false
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'status-cancel',
|
||||
tooltip: cancelAction?.actionName ?? '取消',
|
||||
icon: markRaw(IconMdiCloseCircleOutline),
|
||||
type: 'danger',
|
||||
disabled: !cancelAction,
|
||||
onClick: async () =>
|
||||
handleStatusAction(row, {
|
||||
actionCode: cancelAction?.actionCode ?? 'cancel',
|
||||
actionName: cancelAction?.actionName ?? '取消',
|
||||
needReason: cancelAction?.needReason ?? false
|
||||
})
|
||||
},
|
||||
...lifecycleActions,
|
||||
{
|
||||
key: 'status-complete',
|
||||
tooltip: completeAction?.actionName ?? '完成',
|
||||
icon: markRaw(IconMdiCheckCircleOutline),
|
||||
type: 'success',
|
||||
disabled: !completeAction,
|
||||
onClick: async () =>
|
||||
handleStatusAction(row, {
|
||||
actionCode: completeAction?.actionCode ?? 'complete',
|
||||
actionName: completeAction?.actionName ?? '完成',
|
||||
needReason: completeAction?.needReason ?? false
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
tooltip: '删除',
|
||||
icon: markRaw(IconMdiDeleteOutline),
|
||||
type: 'danger',
|
||||
onClick: async () => handleDelete(row)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function renderRowActions(row: Api.PersonalItem.PersonalItem) {
|
||||
return (
|
||||
<div class="personal-item-row-actions" onClick={event => event.stopPropagation()}>
|
||||
{buildRowActions(row).map(action => {
|
||||
const Icon = action.icon as any;
|
||||
|
||||
return (
|
||||
<ElTooltip key={action.key} content={action.tooltip}>
|
||||
<span class="inline-flex">
|
||||
<ElButton
|
||||
link
|
||||
type={action.type}
|
||||
class="personal-item-row-action-btn"
|
||||
disabled={action.disabled}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
if (action.disabled) {
|
||||
return;
|
||||
}
|
||||
action.onClick();
|
||||
}}
|
||||
>
|
||||
<Icon class="text-15px" />
|
||||
</ElButton>
|
||||
</span>
|
||||
</ElTooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
operateType.value = 'add';
|
||||
editingData.value = null;
|
||||
openOperateDialog();
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: Api.PersonalItem.PersonalItem[]) {
|
||||
checkedRowIds.value = rows.map(item => item.id);
|
||||
}
|
||||
|
||||
function resolveReloadPageAfterRemove() {
|
||||
const currentPage = searchParams.pageNo ?? 1;
|
||||
|
||||
if (currentPage > 1 && data.value.length > 0 && checkedRowIds.value.length >= data.value.length) {
|
||||
return currentPage - 1;
|
||||
}
|
||||
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
async function reloadTable(page = searchParams.pageNo ?? 1) {
|
||||
checkedRowIds.value = [];
|
||||
await getDataByPage(page);
|
||||
await nextTick();
|
||||
tableRef.value?.clearSelection();
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
Object.assign(searchParams, getInitSearchParams());
|
||||
reloadTable(1);
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
reloadTable(1);
|
||||
}
|
||||
|
||||
function handleSubmitted() {
|
||||
closeOperateDialog();
|
||||
reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
function handleDetailChanged(latestItem: Api.PersonalItem.PersonalItem) {
|
||||
detailData.value = latestItem;
|
||||
|
||||
const targetIndex = data.value.findIndex(item => item.id === latestItem.id);
|
||||
|
||||
if (targetIndex >= 0) {
|
||||
data.value.splice(targetIndex, 1, latestItem);
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusAction(row: Api.PersonalItem.PersonalItem, action: Api.PersonalItem.PersonalItemLifecycleAction) {
|
||||
currentStatusItem.value = row;
|
||||
currentStatusAction.value = action;
|
||||
openStatusActionDialog();
|
||||
}
|
||||
|
||||
async function handleStatusActionSubmit(reason: string | null) {
|
||||
if (!currentStatusItem.value || !currentStatusAction.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await fetchChangePersonalItemStatus(currentStatusItem.value.id, {
|
||||
actionCode: currentStatusAction.value.actionCode,
|
||||
reason
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeStatusActionDialog();
|
||||
window.$message?.success(`${currentStatusAction.value.actionName}成功`);
|
||||
await reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.PersonalItem.PersonalItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除个人事项“${row.taskTitle}”吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await fetchDeletePersonalItem(row.id);
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('删除成功');
|
||||
await reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
if (!checkedRowIds.value.length) {
|
||||
window.$message?.warning('请先选择个人事项');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${selectedCount.value} 条个人事项吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetPage = resolveReloadPageAfterRemove();
|
||||
const { error } = await fetchBatchDeletePersonalItems({ ids: [...checkedRowIds.value] });
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('批量删除成功');
|
||||
await reloadTable(targetPage);
|
||||
}
|
||||
|
||||
function handleOpenBindExecution() {
|
||||
if (!checkedRowIds.value.length) {
|
||||
window.$message?.warning('请先选择个人事项');
|
||||
return;
|
||||
}
|
||||
|
||||
openBindExecutionDialog();
|
||||
}
|
||||
|
||||
async function handleBindExecutionSubmit(payload: { executionId: string }) {
|
||||
bindExecutionSubmitting.value = true;
|
||||
|
||||
const targetPage = resolveReloadPageAfterRemove();
|
||||
const { error } = await fetchBindPersonalItemsToExecution({
|
||||
ids: [...checkedRowIds.value],
|
||||
executionId: payload.executionId
|
||||
});
|
||||
|
||||
bindExecutionSubmitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeBindExecutionDialog();
|
||||
window.$message?.success('批量关联执行成功');
|
||||
await reloadTable(targetPage);
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
searchParams.ownerId = currentUserId.value || undefined;
|
||||
reloadTable(searchParams.pageNo ?? 1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<PersonalItemSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-12px">
|
||||
<div class="flex items-center gap-10px">
|
||||
<p>个人事项</p>
|
||||
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
||||
</div>
|
||||
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadTable">
|
||||
<template #default>
|
||||
<ElButton plain type="danger" :disabled="selectedCount === 0" @click="handleBatchDelete">
|
||||
<template #icon>
|
||||
<icon-ic-round-delete class="text-icon" />
|
||||
</template>
|
||||
批量删除
|
||||
</ElButton>
|
||||
<ElButton plain :disabled="selectedCount === 0" @click="handleOpenBindExecution">
|
||||
<template #icon>
|
||||
<icon-mdi-link-variant class="text-icon" />
|
||||
</template>
|
||||
批量关联执行
|
||||
</ElButton>
|
||||
<ElButton plain type="primary" @click="openAdd">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex-1">
|
||||
<ElTable
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
height="100%"
|
||||
border
|
||||
row-key="id"
|
||||
:data="data"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template v-for="col in columns" :key="String(col.prop)">
|
||||
<ElTableColumn v-if="col.prop === 'taskTitle'" v-bind="col">
|
||||
<template #default="{ row }">
|
||||
<ElButton link type="primary" class="personal-item-title-link" @click.stop="openView(row)">
|
||||
{{ row.taskTitle || '--' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn v-else v-bind="col" />
|
||||
</template>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<div class="mt-20px flex justify-end">
|
||||
<ElPagination
|
||||
v-if="mobilePagination.total"
|
||||
layout="total,prev,pager,next,sizes"
|
||||
v-bind="mobilePagination"
|
||||
@current-change="mobilePagination['current-change']"
|
||||
@size-change="mobilePagination['size-change']"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<PersonalItemOperateDialog
|
||||
v-model:visible="operateVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
|
||||
<PersonalItemDetailDialog
|
||||
v-model:visible="detailVisible"
|
||||
:row-data="detailData"
|
||||
:default-tab="detailDefaultTab"
|
||||
@changed="handleDetailChanged"
|
||||
/>
|
||||
|
||||
<PersonalItemBindExecutionDialog
|
||||
v-model:visible="bindExecutionVisible"
|
||||
:selected-count="selectedCount"
|
||||
:submit-loading="bindExecutionSubmitting"
|
||||
@submit="handleBindExecutionSubmit"
|
||||
/>
|
||||
|
||||
<PersonalItemStatusActionDialog
|
||||
v-model:visible="statusActionVisible"
|
||||
:action="currentStatusAction"
|
||||
@submit="handleStatusActionSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.personal-item-row-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.personal-item-row-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
:deep(.personal-item-row-action-btn) {
|
||||
padding: 3px;
|
||||
min-width: auto;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:deep(.personal-item-title-link) {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
:deep(.personal-item-title-link > span) {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { fetchGetPersonalItemExecutionOptions } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'PersonalItemBindExecutionDialog' });
|
||||
|
||||
interface Props {
|
||||
selectedCount: number;
|
||||
submitLoading?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: { executionId: string }): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
submitLoading: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const loading = ref(false);
|
||||
const executionOptions = ref<Api.PersonalItem.PersonalItemExecutionOption[]>([]);
|
||||
|
||||
const model = reactive({
|
||||
executionId: ''
|
||||
});
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
executionId: [createRequiredRule('请选择执行')]
|
||||
}) satisfies Record<string, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
function getExecutionOptionLabel(option: Api.PersonalItem.PersonalItemExecutionOption) {
|
||||
if (option.projectName?.trim()) {
|
||||
return `${option.projectName} / ${option.executionName}`;
|
||||
}
|
||||
|
||||
return option.executionName;
|
||||
}
|
||||
|
||||
async function loadExecutionOptions() {
|
||||
loading.value = true;
|
||||
const { error, data } = await fetchGetPersonalItemExecutionOptions();
|
||||
loading.value = false;
|
||||
|
||||
if (error || !data) {
|
||||
executionOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
executionOptions.value = data.map(item => ({ ...item }));
|
||||
}
|
||||
|
||||
async function initDialog() {
|
||||
model.executionId = '';
|
||||
await loadExecutionOptions();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
await validate();
|
||||
|
||||
emit('submit', {
|
||||
executionId: model.executionId
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
initDialog();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="批量关联执行"
|
||||
preset="sm"
|
||||
:loading="loading"
|
||||
:confirm-loading="props.submitLoading"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<ElAlert
|
||||
:title="`已选中 ${props.selectedCount} 条个人事项,关联成功后这些事项会从当前列表移除。`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-16px"
|
||||
/>
|
||||
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<ElFormItem label="执行" prop="executionId">
|
||||
<ElSelect
|
||||
v-model="model.executionId"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择执行"
|
||||
class="w-full"
|
||||
:loading="loading"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in executionOptions"
|
||||
:key="option.executionId"
|
||||
:label="getExecutionOptionLabel(option)"
|
||||
:value="option.executionId"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { fetchGetPersonalItemDetail } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import PersonalItemWorklogPanel from './personal-item-worklog-panel.vue';
|
||||
|
||||
defineOptions({ name: 'PersonalItemDetailDialog' });
|
||||
|
||||
type TabName = 'worklog';
|
||||
|
||||
interface Props {
|
||||
rowData?: Api.PersonalItem.PersonalItem | null;
|
||||
defaultTab?: TabName;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
defaultTab: 'worklog'
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
changed: [item: Api.PersonalItem.PersonalItem];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const activeTab = ref<TabName>('worklog');
|
||||
const detailData = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||
|
||||
function syncDetailFromPageRow() {
|
||||
detailData.value = props.rowData ?? null;
|
||||
}
|
||||
|
||||
async function refreshDetail() {
|
||||
if (!detailData.value?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, data } = await fetchGetPersonalItemDetail(detailData.value.id);
|
||||
|
||||
if (!error && data) {
|
||||
detailData.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWorklogChanged() {
|
||||
await refreshDetail();
|
||||
|
||||
if (detailData.value) {
|
||||
emit('changed', detailData.value);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
activeTab.value = props.defaultTab;
|
||||
syncDetailFromPageRow();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.rowData,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
syncDetailFromPageRow();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.defaultTab,
|
||||
value => {
|
||||
if (visible.value) {
|
||||
activeTab.value = value;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="工作日志"
|
||||
width="1100px"
|
||||
max-body-height="78vh"
|
||||
:show-footer="false"
|
||||
:scrollbar="false"
|
||||
>
|
||||
<ElTabs v-model="activeTab" class="personal-item-detail-dialog__tabs">
|
||||
<ElTabPane label="工作日志" name="worklog" lazy>
|
||||
<PersonalItemWorklogPanel
|
||||
v-if="detailData"
|
||||
:item="detailData"
|
||||
:active="activeTab === 'worklog' && visible"
|
||||
@changed="handleWorklogChanged"
|
||||
/>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.personal-item-detail-dialog__tabs {
|
||||
--el-tabs-header-height: 40px;
|
||||
}
|
||||
|
||||
.personal-item-detail-dialog__tabs :deep(.el-tabs__content),
|
||||
.personal-item-detail-dialog__tabs :deep(.el-tab-pane) {
|
||||
min-height: 640px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,325 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchCreatePersonalItem, fetchGetPersonalItemDetail, fetchUpdatePersonalItem } from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||
import { isEmptyRichText } from './personal-item-shared';
|
||||
|
||||
defineOptions({ name: 'PersonalItemOperateDialog' });
|
||||
|
||||
type PersonalItemOperateType = UI.TableOperateType | 'view';
|
||||
|
||||
interface Props {
|
||||
operateType: PersonalItemOperateType;
|
||||
rowData?: Api.PersonalItem.PersonalItem | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submitted: [];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const currentUserId = computed(() => authStore.userInfo.userId || 'current-user');
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
||||
const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null>(null);
|
||||
const leftColRef = ref<HTMLElement>();
|
||||
const editorHeight = ref<string>('45vh');
|
||||
|
||||
const ATTACHMENT_SECTION_RESERVE_PX = 140;
|
||||
|
||||
useResizeObserver(leftColRef, entries => {
|
||||
const h = entries[0]?.contentRect.height;
|
||||
if (h && h > 120) {
|
||||
editorHeight.value = `${Math.max(h - 60 - ATTACHMENT_SECTION_RESERVE_PX, 200)}px`;
|
||||
}
|
||||
});
|
||||
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
const isView = computed(() => props.operateType === 'view');
|
||||
const detailLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
interface Model {
|
||||
taskTitle: string;
|
||||
plannedStartDate: string | null;
|
||||
plannedEndDate: string | null;
|
||||
taskDesc: string | null;
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
}
|
||||
|
||||
const model = reactive<Model>(createDefaultModel());
|
||||
|
||||
const title = computed(() => {
|
||||
if (isView.value) {
|
||||
return '个人事项详情';
|
||||
}
|
||||
|
||||
return isEdit.value ? '编辑个人事项' : '新增个人事项';
|
||||
});
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
taskTitle: '',
|
||||
plannedStartDate: null,
|
||||
plannedEndDate: null,
|
||||
taskDesc: null,
|
||||
attachments: []
|
||||
};
|
||||
}
|
||||
|
||||
function isPlannedDateRangeValid(startDate: string | null, endDate: string | null) {
|
||||
if (!startDate || !endDate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !dayjs(endDate).isBefore(dayjs(startDate), 'day');
|
||||
}
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
taskTitle: [
|
||||
createRequiredRule('请输入事项标题'),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error('请输入事项标题'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
plannedStartDate: [createRequiredRule('请选择计划开始日期')],
|
||||
plannedEndDate: [
|
||||
createRequiredRule('请选择计划结束日期'),
|
||||
{
|
||||
validator: (_rule, value: string | null, callback) => {
|
||||
if (!isPlannedDateRangeValid(model.plannedStartDate, value)) {
|
||||
callback(new Error('计划结束日期不能早于计划开始日期'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}) satisfies Record<string, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
async function initModel() {
|
||||
detailLoading.value = true;
|
||||
|
||||
Object.assign(model, createDefaultModel());
|
||||
|
||||
if ((isEdit.value || isView.value) && props.rowData) {
|
||||
const { error, data } = await fetchGetPersonalItemDetail(props.rowData.id);
|
||||
|
||||
if (!error && data) {
|
||||
model.taskTitle = data.taskTitle;
|
||||
model.plannedStartDate = data.plannedStartDate;
|
||||
model.plannedEndDate = data.plannedEndDate;
|
||||
model.taskDesc = data.taskDesc;
|
||||
model.attachments = data.attachments ? [...data.attachments] : [];
|
||||
}
|
||||
}
|
||||
|
||||
detailLoading.value = false;
|
||||
|
||||
await nextTick();
|
||||
attachmentUploaderRef.value?.initSession();
|
||||
richTextEditorRef.value?.initSession();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (isView.value) {
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await validate();
|
||||
|
||||
if (attachmentUploaderRef.value?.hasUploading) {
|
||||
window.$message?.warning('附件正在上传中,请稍候');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Api.PersonalItem.SavePersonalItemParams = {
|
||||
taskTitle: model.taskTitle.trim(),
|
||||
ownerId: currentUserId.value,
|
||||
plannedStartDate: model.plannedStartDate,
|
||||
plannedEndDate: model.plannedEndDate,
|
||||
taskDesc: isEmptyRichText(model.taskDesc) ? null : (model.taskDesc ?? null),
|
||||
attachments: [...model.attachments]
|
||||
};
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const result =
|
||||
isEdit.value && props.rowData
|
||||
? await fetchUpdatePersonalItem({ id: props.rowData.id, ...payload })
|
||||
: await fetchCreatePersonalItem(payload);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]);
|
||||
|
||||
window.$message?.success(isEdit.value ? '个人事项修改成功' : '个人事项创建成功');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
initModel();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="1100px"
|
||||
:loading="detailLoading"
|
||||
:confirm-loading="submitting"
|
||||
:show-footer="!isView"
|
||||
max-body-height="78vh"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
:validate-on-rule-change="false"
|
||||
class="personal-item-operate-dialog__form"
|
||||
>
|
||||
<div class="personal-item-operate-dialog__grid">
|
||||
<div ref="leftColRef" class="personal-item-operate-dialog__col-left">
|
||||
<BusinessFormSection title="事项信息">
|
||||
<ElFormItem label="事项标题" prop="taskTitle">
|
||||
<ElInput
|
||||
v-model="model.taskTitle"
|
||||
:clearable="!isView"
|
||||
:disabled="isView"
|
||||
maxlength="300"
|
||||
placeholder="请输入事项标题"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="计划开始日期" prop="plannedStartDate">
|
||||
<ElDatePicker
|
||||
v-model="model.plannedStartDate"
|
||||
:disabled="isView"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择计划开始日期"
|
||||
class="personal-item-operate-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="计划结束日期" prop="plannedEndDate">
|
||||
<ElDatePicker
|
||||
v-model="model.plannedEndDate"
|
||||
:disabled="isView"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择计划结束日期"
|
||||
class="personal-item-operate-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
|
||||
<div class="personal-item-operate-dialog__col-right">
|
||||
<BusinessFormSection title="事项说明">
|
||||
<ElFormItem class="personal-item-operate-dialog__desc-item" prop="taskDesc">
|
||||
<BusinessRichTextEditor
|
||||
ref="richTextEditorRef"
|
||||
v-model="model.taskDesc"
|
||||
:height="editorHeight"
|
||||
:disabled="isView"
|
||||
upload-directory="personal-item"
|
||||
placeholder="请输入事项说明"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="附件">
|
||||
<ElFormItem class="personal-item-operate-dialog__attachment-item">
|
||||
<BusinessAttachmentUploader
|
||||
ref="attachmentUploaderRef"
|
||||
v-model="model.attachments"
|
||||
directory="personal-item"
|
||||
:disabled="isView"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.personal-item-operate-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.personal-item-operate-dialog__col-left,
|
||||
.personal-item-operate-dialog__col-right {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.personal-item-operate-dialog__col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.personal-item-operate-dialog__desc-item,
|
||||
.personal-item-operate-dialog__attachment-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (width <= 1024px) {
|
||||
.personal-item-operate-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.personal-item-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { fetchGetObjectStatusModelPage } from '@/service/api';
|
||||
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
import { personalItemStatusOptions } from './personal-item-shared';
|
||||
|
||||
defineOptions({ name: 'PersonalItemSearch' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
reset: [];
|
||||
search: [];
|
||||
}>();
|
||||
|
||||
const model = defineModel<Api.PersonalItem.PersonalItemSearchParams>('model', {
|
||||
required: true
|
||||
});
|
||||
|
||||
const searchModel = reactive<{
|
||||
keyword: string;
|
||||
statusCode?: Api.PersonalItem.PersonalItemStatusCode;
|
||||
}>({
|
||||
keyword: '',
|
||||
statusCode: undefined
|
||||
});
|
||||
|
||||
let syncingFromSource = false;
|
||||
const statusOptions = ref<Array<{ label: string; value: string }>>([...personalItemStatusOptions]);
|
||||
|
||||
watch(
|
||||
() => [model.value.keyword, model.value.statusCode] as const,
|
||||
([keyword, statusCode]) => {
|
||||
syncingFromSource = true;
|
||||
searchModel.keyword = keyword ?? '';
|
||||
searchModel.statusCode = statusCode;
|
||||
syncingFromSource = false;
|
||||
},
|
||||
{ immediate: true, flush: 'sync' }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [searchModel.keyword, searchModel.statusCode] as const,
|
||||
([keyword, statusCode]) => {
|
||||
if (syncingFromSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value.keyword = keyword.trim() || undefined;
|
||||
model.value.statusCode = statusCode;
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
);
|
||||
|
||||
const fields = computed<SearchField[]>(() => [
|
||||
{
|
||||
key: 'keyword',
|
||||
label: '关键字',
|
||||
type: 'input',
|
||||
placeholder: '请输入标题或说明'
|
||||
},
|
||||
{
|
||||
key: 'statusCode',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: statusOptions.value
|
||||
}
|
||||
]);
|
||||
|
||||
async function loadStatusOptions() {
|
||||
const { error, data } = await fetchGetObjectStatusModelPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
objectType: 'task',
|
||||
status: 0,
|
||||
initialFlag: undefined,
|
||||
terminalFlag: undefined,
|
||||
keyword: undefined
|
||||
});
|
||||
|
||||
if (error || !data?.list?.length) {
|
||||
statusOptions.value = [...personalItemStatusOptions];
|
||||
return;
|
||||
}
|
||||
|
||||
statusOptions.value = data.list
|
||||
.slice()
|
||||
.sort((left, right) => left.sort - right.sort)
|
||||
.map(item => ({
|
||||
label: item.statusName,
|
||||
value: item.statusCode
|
||||
}));
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
emit('search');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStatusOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchFields v-model="searchModel" :fields="fields" :columns="3" @reset="handleReset" @search="handleSearch" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -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<Api.PersonalItem.PersonalItemStatusCode, string> = {
|
||||
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<Api.PersonalItem.PersonalItem, 'ownerNickname' | 'ownerName' | 'ownerId'>
|
||||
) {
|
||||
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 !/<img\b/i.test(html);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
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<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const model = reactive({
|
||||
reason: ''
|
||||
});
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
reason: props.action?.needReason ? [createRequiredRule('请输入动作原因')] : []
|
||||
}) satisfies Record<string, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="action?.actionName || '状态变更'" preset="sm" @confirm="handleConfirm">
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<ElFormItem label="动作原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="model.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:placeholder="action?.needReason ? '请输入动作原因' : '可选填写动作原因'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,409 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { RDMS_WORKLOG_DIFFICULTY_DICT_CODE } from '@/constants/dict';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'PersonalItemWorklogFormDialog' });
|
||||
|
||||
type Mode = 'create' | 'edit' | 'view';
|
||||
type Granularity = 'day' | 'week';
|
||||
|
||||
interface Props {
|
||||
mode: Mode;
|
||||
rowData: Api.PersonalItem.PersonalItemWorklog | null;
|
||||
itemStatusCode: Api.PersonalItem.PersonalItemStatusCode;
|
||||
defaultProgressRate?: number;
|
||||
confirmLoading?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: Api.PersonalItem.SavePersonalItemWorklogParams): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
defaultProgressRate: 0,
|
||||
confirmLoading: false
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const isView = computed(() => props.mode === 'view');
|
||||
const isProgressReadonly = computed(() => isView.value || props.itemStatusCode === 'completed');
|
||||
|
||||
interface FormModel {
|
||||
granularity: Granularity;
|
||||
workDate: string | null;
|
||||
weekDate: Date | null;
|
||||
durationHours: number | null;
|
||||
progressRate: number;
|
||||
difficulty: string;
|
||||
workContent: string | null;
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
}
|
||||
|
||||
const model = reactive<FormModel>({
|
||||
granularity: 'day',
|
||||
workDate: null,
|
||||
weekDate: null,
|
||||
durationHours: null,
|
||||
progressRate: 0,
|
||||
difficulty: '2',
|
||||
workContent: null,
|
||||
attachments: []
|
||||
});
|
||||
|
||||
const granularityOptions = [
|
||||
{ label: '按天', value: 'day' as const },
|
||||
{ label: '按周', value: 'week' as const }
|
||||
];
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.mode === 'create') return '填写工作日志';
|
||||
if (props.mode === 'view') return '查看工作日志';
|
||||
return '编辑工作日志';
|
||||
});
|
||||
const dateFieldLabel = computed(() => (model.granularity === 'day' ? '工作日期' : '工作周次'));
|
||||
|
||||
const workDateShortcuts = [
|
||||
{ text: '今天', value: () => new Date() },
|
||||
{ text: '昨天', value: () => dayjs().subtract(1, 'day').toDate() },
|
||||
{ text: '前天', value: () => dayjs().subtract(2, 'day').toDate() }
|
||||
];
|
||||
|
||||
const weekDateShortcuts = [
|
||||
{ text: '本周', value: () => dayjs().startOf('isoWeek').toDate() },
|
||||
{ text: '上周', value: () => dayjs().subtract(1, 'week').startOf('isoWeek').toDate() }
|
||||
];
|
||||
|
||||
const weekRangeTooltip = computed(() => {
|
||||
if (!model.weekDate) return '';
|
||||
const start = dayjs(model.weekDate);
|
||||
if (!start.isValid()) return '';
|
||||
return `${start.format('YYYY-MM-DD')} ~ ${start.add(6, 'day').format('YYYY-MM-DD')}`;
|
||||
});
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
granularity: [createRequiredRule('请选择填报粒度')],
|
||||
workDate: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value: string | null, callback) => {
|
||||
if (model.granularity !== 'day') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择工作日期'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
weekDate: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value: Date | null, callback) => {
|
||||
if (model.granularity !== 'week') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择工作周次'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
durationHours: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value: number | null, callback) => {
|
||||
if (value === null || value === undefined) {
|
||||
callback(new Error('请输入工时'));
|
||||
return;
|
||||
}
|
||||
if (value <= 0) {
|
||||
callback(new Error('工时必须大于 0'));
|
||||
return;
|
||||
}
|
||||
if (Math.round(value * 10) % 5 !== 0) {
|
||||
callback(new Error('工时必须是 0.5 小时的整数倍'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
progressRate: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value: number, callback) => {
|
||||
if (value < 0 || value > 100) {
|
||||
callback(new Error('进度需在 0 到 100 之间'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
difficulty: [createRequiredRule('请选择完成难度')],
|
||||
workContent: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value: string | null, callback) => {
|
||||
if (!value || !value.trim()) {
|
||||
callback(new Error('请输入工作内容'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}) satisfies Record<string, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
function detectGranularityFromRow(row: Api.PersonalItem.PersonalItemWorklog): Granularity {
|
||||
if (row.startDate === row.endDate) {
|
||||
return 'day';
|
||||
}
|
||||
|
||||
const start = dayjs(row.startDate);
|
||||
const end = dayjs(row.endDate);
|
||||
|
||||
if (start.isoWeekday() === 1 && end.isoWeekday() === 7 && end.diff(start, 'day') === 6) {
|
||||
return 'week';
|
||||
}
|
||||
|
||||
return 'day';
|
||||
}
|
||||
|
||||
function getStartEndFromModel(): { startDate: string; endDate: string } {
|
||||
if (model.granularity === 'day') {
|
||||
return {
|
||||
startDate: model.workDate!,
|
||||
endDate: model.workDate!
|
||||
};
|
||||
}
|
||||
|
||||
const weekStart = dayjs(model.weekDate!).startOf('isoWeek');
|
||||
return {
|
||||
startDate: weekStart.format('YYYY-MM-DD'),
|
||||
endDate: weekStart.add(6, 'day').format('YYYY-MM-DD')
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => model.granularity,
|
||||
() => {
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
|
||||
async function handleConfirm() {
|
||||
if (isView.value) {
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await validate();
|
||||
|
||||
if (attachmentUploaderRef.value?.hasUploading) {
|
||||
window.$message?.warning('附件正在上传中,请稍候');
|
||||
return;
|
||||
}
|
||||
|
||||
const { startDate, endDate } = getStartEndFromModel();
|
||||
|
||||
const payload: Api.PersonalItem.SavePersonalItemWorklogParams = {
|
||||
startDate,
|
||||
endDate,
|
||||
durationHours: Number(model.durationHours!.toFixed(1)),
|
||||
progressRate: Number(model.progressRate.toFixed(2)),
|
||||
difficulty: model.difficulty,
|
||||
workContent: model.workContent?.trim() || null,
|
||||
attachments: [...model.attachments]
|
||||
};
|
||||
|
||||
emit('submit', payload);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = props.rowData;
|
||||
if (row) {
|
||||
const granularity = detectGranularityFromRow(row);
|
||||
model.granularity = granularity;
|
||||
model.workDate = granularity === 'day' ? row.startDate : null;
|
||||
model.weekDate = granularity === 'week' ? dayjs(row.startDate).toDate() : null;
|
||||
model.durationHours = row.durationHours;
|
||||
model.progressRate = row.progressRate;
|
||||
model.difficulty = row.difficulty || '2';
|
||||
model.workContent = row.workContent || null;
|
||||
model.attachments = row.attachments ? [...row.attachments] : [];
|
||||
} else {
|
||||
model.granularity = 'day';
|
||||
model.workDate = dayjs().format('YYYY-MM-DD');
|
||||
model.weekDate = null;
|
||||
model.durationHours = null;
|
||||
model.progressRate = props.defaultProgressRate;
|
||||
model.difficulty = '2';
|
||||
model.workContent = null;
|
||||
model.attachments = [];
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
attachmentUploaderRef.value?.initSession();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
async commit() {
|
||||
await attachmentUploaderRef.value?.commit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
preset="md"
|
||||
:confirm-loading="props.confirmLoading"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="填报粒度" prop="granularity">
|
||||
<ElSegmented v-model="model.granularity" :options="granularityOptions" :disabled="isView" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="dateFieldLabel" :prop="model.granularity === 'day' ? 'workDate' : 'weekDate'">
|
||||
<ElDatePicker
|
||||
v-if="model.granularity === 'day'"
|
||||
v-model="model.workDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择工作日期"
|
||||
:shortcuts="isView ? undefined : workDateShortcuts"
|
||||
:disabled="isView"
|
||||
class="personal-item-worklog-form-dialog__date-picker"
|
||||
/>
|
||||
<ElTooltip v-else :content="weekRangeTooltip" :disabled="!weekRangeTooltip" placement="top">
|
||||
<span class="personal-item-worklog-form-dialog__week-wrapper">
|
||||
<ElDatePicker
|
||||
v-model="model.weekDate"
|
||||
type="week"
|
||||
format="YYYY[年第]ww[周]"
|
||||
placeholder="选择工作周次"
|
||||
:shortcuts="isView ? undefined : weekDateShortcuts"
|
||||
:disabled="isView"
|
||||
class="personal-item-worklog-form-dialog__date-picker"
|
||||
/>
|
||||
</span>
|
||||
</ElTooltip>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="工时(小时)" prop="durationHours">
|
||||
<ElInputNumber
|
||||
v-model="model.durationHours"
|
||||
:min="0.5"
|
||||
:step="0.5"
|
||||
:precision="1"
|
||||
:disabled="isView"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="进度(%)" prop="progressRate">
|
||||
<ElInputNumber
|
||||
v-model="model.progressRate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="2"
|
||||
:disabled="isProgressReadonly"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="完成难度" prop="difficulty">
|
||||
<DictSelect
|
||||
v-model="model.difficulty"
|
||||
:dict-code="RDMS_WORKLOG_DIFFICULTY_DICT_CODE"
|
||||
:disabled="isView"
|
||||
:clearable="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="工作内容" prop="workContent">
|
||||
<ElInput
|
||||
v-model="model.workContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
:maxlength="isView ? undefined : 2000"
|
||||
:show-word-limit="!isView"
|
||||
:disabled="isView"
|
||||
placeholder="简述本次填报的工作内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="附件">
|
||||
<BusinessAttachmentUploader
|
||||
ref="attachmentUploaderRef"
|
||||
v-model="model.attachments"
|
||||
:disabled="isView"
|
||||
directory="personal-item-worklog"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
|
||||
<template v-if="isView" #footer="{ close }">
|
||||
<ElButton type="primary" @click="close">关闭</ElButton>
|
||||
</template>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.personal-item-worklog-form-dialog__date-picker.el-date-editor.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.personal-item-worklog-form-dialog__week-wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,656 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ElPopconfirm, ElTag, ElTooltip } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { RDMS_WORKLOG_DIFFICULTY_DICT_CODE } from '@/constants/dict';
|
||||
import {
|
||||
fetchCreatePersonalItemWorklog,
|
||||
fetchDeletePersonalItemWorklog,
|
||||
fetchGetPersonalItemWorklogPage,
|
||||
fetchUpdatePersonalItemWorklog
|
||||
} from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||
import {
|
||||
formatPersonalItemDate,
|
||||
formatPersonalItemOwnerName,
|
||||
formatPersonalItemProgress,
|
||||
getPersonalItemStatusLabel,
|
||||
resolvePersonalItemStatusTagType
|
||||
} from './personal-item-shared';
|
||||
import PersonalItemWorklogFormDialog from './personal-item-worklog-form-dialog.vue';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiPaperclip from '~icons/mdi/paperclip';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
|
||||
defineOptions({ name: 'PersonalItemWorklogPanel' });
|
||||
|
||||
type WorklogGranularity = 'day' | 'week';
|
||||
|
||||
interface Props {
|
||||
item: Api.PersonalItem.PersonalItem;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
active: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
changed: [];
|
||||
}>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const { getLabel: getDifficultyLabel } = useDict(RDMS_WORKLOG_DIFFICULTY_DICT_CODE);
|
||||
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||
const currentUserName = computed(
|
||||
() => authStore.userInfo.nickname?.trim() || authStore.userInfo.userName?.trim() || ''
|
||||
);
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const TABLE_HEIGHT = 390;
|
||||
|
||||
const pageNo = ref(1);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const records = ref<Api.PersonalItem.PersonalItemWorklog[]>([]);
|
||||
|
||||
const formVisible = ref(false);
|
||||
const formMode = ref<'create' | 'edit' | 'view'>('create');
|
||||
const submitting = ref(false);
|
||||
const editingWorklog = ref<Api.PersonalItem.PersonalItemWorklog | null>(null);
|
||||
const worklogFormDialogRef = ref<InstanceType<typeof PersonalItemWorklogFormDialog> | null>(null);
|
||||
|
||||
const totalHours = computed(() => {
|
||||
if (typeof props.item.totalSpentHours === 'number' && Number.isFinite(props.item.totalSpentHours)) {
|
||||
return props.item.totalSpentHours;
|
||||
}
|
||||
|
||||
return records.value.reduce((sum, item) => sum + (item.durationHours ?? 0), 0);
|
||||
});
|
||||
const totalHoursText = computed(() => `${totalHours.value.toFixed(1)}h`);
|
||||
const ownerName = computed(() => {
|
||||
const displayName = formatPersonalItemOwnerName(props.item);
|
||||
|
||||
if (displayName !== props.item.ownerId) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return props.item.ownerId === currentUserId.value && currentUserName.value ? currentUserName.value : displayName;
|
||||
});
|
||||
const statusName = computed(() => getPersonalItemStatusLabel(props.item.statusCode));
|
||||
const progressText = computed(() => formatPersonalItemProgress(props.item.progressRate));
|
||||
const plannedStartText = computed(() => formatPersonalItemDate(props.item.plannedStartDate));
|
||||
const plannedEndText = computed(() => formatPersonalItemDate(props.item.plannedEndDate));
|
||||
const actualStartText = computed(() => formatPersonalItemDate(props.item.actualStartDate));
|
||||
const actualEndText = computed(() => formatPersonalItemDate(props.item.actualEndDate));
|
||||
|
||||
const list = computed(() => records.value);
|
||||
|
||||
const canCreate = computed(() =>
|
||||
Boolean(
|
||||
props.item.id &&
|
||||
(props.item.statusCode === 'pending' ||
|
||||
props.item.statusCode === 'active' ||
|
||||
props.item.statusCode === 'completed')
|
||||
)
|
||||
);
|
||||
|
||||
const isWorklogMutableStatus = computed(
|
||||
() => props.item.statusCode === 'active' || props.item.statusCode === 'completed'
|
||||
);
|
||||
|
||||
function getRowIndex(index: number) {
|
||||
return (pageNo.value - 1) * PAGE_SIZE + index + 1;
|
||||
}
|
||||
|
||||
function formatHours(hours: number | null | undefined) {
|
||||
if (typeof hours !== 'number' || !Number.isFinite(hours)) {
|
||||
return '0h';
|
||||
}
|
||||
|
||||
return `${hours.toFixed(1)}h`;
|
||||
}
|
||||
|
||||
function formatWorklogPeriod(startDate: string | null | undefined, endDate: string | null | undefined) {
|
||||
if (!startDate || !endDate) {
|
||||
return {
|
||||
granularity: null as WorklogGranularity | null,
|
||||
display: '--',
|
||||
tooltip: null as string | null
|
||||
};
|
||||
}
|
||||
|
||||
const startKey = formatPersonalItemDate(startDate);
|
||||
const endKey = formatPersonalItemDate(endDate);
|
||||
|
||||
if (startKey === endKey) {
|
||||
const current = dayjs(startDate);
|
||||
const weekSuffix = current.isValid() ? `(第${current.isoWeek()}周)` : '';
|
||||
|
||||
return {
|
||||
granularity: 'day' as const,
|
||||
display: `${startKey}${weekSuffix}`,
|
||||
tooltip: null
|
||||
};
|
||||
}
|
||||
|
||||
const start = dayjs(startDate);
|
||||
|
||||
return {
|
||||
granularity: 'week' as const,
|
||||
display: start.isValid() ? `${start.isoWeekYear()}年第${start.isoWeek()}周` : `${startKey} ~ ${endKey}`,
|
||||
tooltip: `${startKey} ~ ${endKey}`
|
||||
};
|
||||
}
|
||||
|
||||
function getWorklogGranularityName(granularity: WorklogGranularity | null) {
|
||||
if (granularity === 'day') {
|
||||
return '日';
|
||||
}
|
||||
|
||||
if (granularity === 'week') {
|
||||
return '周';
|
||||
}
|
||||
|
||||
return '--';
|
||||
}
|
||||
|
||||
function canEditRow(row: Api.PersonalItem.PersonalItemWorklog) {
|
||||
return Boolean(isWorklogMutableStatus.value && currentUserId.value && row.userId === currentUserId.value);
|
||||
}
|
||||
|
||||
function canDeleteRow(row: Api.PersonalItem.PersonalItemWorklog) {
|
||||
return Boolean(isWorklogMutableStatus.value && currentUserId.value && row.userId === currentUserId.value);
|
||||
}
|
||||
|
||||
async function loadRecords() {
|
||||
if (!props.item.id || !props.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { error, data } = await fetchGetPersonalItemWorklogPage(props.item.id, {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: PAGE_SIZE
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
if (error || !data) {
|
||||
records.value = [];
|
||||
total.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
records.value = data.list;
|
||||
total.value = data.total;
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pageNo.value = page;
|
||||
loadRecords();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
formMode.value = 'create';
|
||||
editingWorklog.value = null;
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
function openView(row: Api.PersonalItem.PersonalItemWorklog) {
|
||||
formMode.value = 'view';
|
||||
editingWorklog.value = row;
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: Api.PersonalItem.PersonalItemWorklog) {
|
||||
formMode.value = 'edit';
|
||||
editingWorklog.value = row;
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.PersonalItem.PersonalItemWorklog) {
|
||||
const shouldStepBack = records.value.length === 1 && pageNo.value > 1;
|
||||
const { error } = await fetchDeletePersonalItemWorklog(props.item.id, row.id);
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldStepBack) {
|
||||
pageNo.value -= 1;
|
||||
}
|
||||
|
||||
window.$message?.success('工作日志删除成功');
|
||||
await loadRecords();
|
||||
emit('changed');
|
||||
}
|
||||
|
||||
async function handleSubmit(payload: Api.PersonalItem.SavePersonalItemWorklogParams) {
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result =
|
||||
formMode.value === 'edit' && editingWorklog.value
|
||||
? await fetchUpdatePersonalItemWorklog(props.item.id, {
|
||||
worklogId: editingWorklog.value.id,
|
||||
data: payload
|
||||
})
|
||||
: await fetchCreatePersonalItemWorklog(props.item.id, payload);
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
await worklogFormDialogRef.value?.commit();
|
||||
|
||||
window.$message?.success(formMode.value === 'edit' ? '工作日志修改成功' : '工作日志新增成功');
|
||||
formVisible.value = false;
|
||||
await loadRecords();
|
||||
emit('changed');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(total, value => {
|
||||
const maxPage = Math.max(1, Math.ceil(value / PAGE_SIZE));
|
||||
if (pageNo.value > maxPage) {
|
||||
pageNo.value = maxPage;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.item.id, () => props.active],
|
||||
([itemId, active]) => {
|
||||
if (!itemId) {
|
||||
records.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
pageNo.value = 1;
|
||||
|
||||
if (active) {
|
||||
loadRecords();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="personal-item-worklog-panel">
|
||||
<div class="personal-item-worklog-panel__cards">
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">负责人</span>
|
||||
<span class="personal-item-worklog-panel__card-value" :title="ownerName">{{ ownerName }}</span>
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">当前状态</span>
|
||||
<ElTag
|
||||
:type="resolvePersonalItemStatusTagType(props.item.statusCode)"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="personal-item-worklog-panel__card-tag"
|
||||
>
|
||||
{{ statusName }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">计划开始</span>
|
||||
<span class="personal-item-worklog-panel__card-value">{{ plannedStartText }}</span>
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">计划结束</span>
|
||||
<span class="personal-item-worklog-panel__card-value">{{ plannedEndText }}</span>
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">当前进度</span>
|
||||
<span class="personal-item-worklog-panel__card-value">{{ progressText }}</span>
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">累计工时</span>
|
||||
<span class="personal-item-worklog-panel__card-value personal-item-worklog-panel__card-value--accent">
|
||||
{{ totalHoursText }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">实际开始</span>
|
||||
<span class="personal-item-worklog-panel__card-value">{{ actualStartText }}</span>
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__card">
|
||||
<span class="personal-item-worklog-panel__card-label">实际结束</span>
|
||||
<span class="personal-item-worklog-panel__card-value">{{ actualEndText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header v-if="canCreate" class="personal-item-worklog-panel__header">
|
||||
<ElButton type="primary" size="small" :icon="Plus" @click="openCreate">填报</ElButton>
|
||||
</header>
|
||||
|
||||
<ElTable
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:height="TABLE_HEIGHT"
|
||||
border
|
||||
empty-text="暂无工作日志"
|
||||
class="personal-item-worklog-panel__table"
|
||||
>
|
||||
<ElTableColumn type="index" :index="getRowIndex" label="序号" width="60" align="center" />
|
||||
<ElTableColumn label="粒度" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag
|
||||
:type="formatWorklogPeriod(row.startDate, row.endDate).granularity === 'week' ? 'warning' : 'info'"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ getWorklogGranularityName(formatWorklogPeriod(row.startDate, row.endDate).granularity) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="日期" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTooltip
|
||||
v-if="formatWorklogPeriod(row.startDate, row.endDate).tooltip"
|
||||
:content="formatWorklogPeriod(row.startDate, row.endDate).tooltip ?? ''"
|
||||
placement="top"
|
||||
>
|
||||
<span>{{ formatWorklogPeriod(row.startDate, row.endDate).display }}</span>
|
||||
</ElTooltip>
|
||||
<span v-else>{{ formatWorklogPeriod(row.startDate, row.endDate).display }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="工作内容" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<ElPopover
|
||||
v-if="row.workContent || (row.attachments && row.attachments.length)"
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
:width="360"
|
||||
:show-after="200"
|
||||
popper-class="personal-item-worklog-panel__content-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<span class="personal-item-worklog-panel__content-cell">
|
||||
{{ row.workContent || `附件 ${row.attachments?.length ?? 0} 个` }}
|
||||
</span>
|
||||
</template>
|
||||
<div class="personal-item-worklog-panel__content-card">
|
||||
<div class="personal-item-worklog-panel__content-card-header">
|
||||
<span>{{ formatWorklogPeriod(row.startDate, row.endDate).display }}</span>
|
||||
<span class="personal-item-worklog-panel__content-card-meta">
|
||||
{{ formatHours(row.durationHours) }} / {{ formatPersonalItemProgress(row.progressRate) }} /
|
||||
{{ getDifficultyLabel(row.difficulty, { fallback: '--' }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="row.workContent" class="personal-item-worklog-panel__content-card-body">
|
||||
{{ row.workContent }}
|
||||
</div>
|
||||
<div class="personal-item-worklog-panel__content-card-attachments">
|
||||
<div class="personal-item-worklog-panel__content-card-section-title">
|
||||
<ElIcon><IconMdiPaperclip /></ElIcon>
|
||||
<span v-if="row.attachments && row.attachments.length">附件({{ row.attachments.length }})</span>
|
||||
<span v-else class="personal-item-worklog-panel__content-card-attachment-empty">无附件</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="row.attachments && row.attachments.length"
|
||||
class="personal-item-worklog-panel__content-card-attachments-scroll"
|
||||
>
|
||||
<BusinessAttachmentUploader :model-value="row.attachments" disabled flat />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<span v-else class="personal-item-worklog-panel__content-cell-empty">--</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="时长" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="personal-item-worklog-panel__duration">{{ formatHours(row.durationHours) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="进度" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="personal-item-worklog-panel__progress">{{ formatPersonalItemProgress(row.progressRate) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="personal-item-worklog-panel__actions" @click.stop>
|
||||
<ElTooltip content="查看">
|
||||
<ElButton link type="primary" class="personal-item-worklog-panel__action-btn" @click="openView(row)">
|
||||
<IconMdiEyeOutline class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip :content="canEditRow(row) ? '编辑' : '仅可编辑本人填报'">
|
||||
<span class="inline-flex">
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
class="personal-item-worklog-panel__action-btn"
|
||||
:disabled="!canEditRow(row)"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
<IconMdiPencilOutline class="text-15px" />
|
||||
</ElButton>
|
||||
</span>
|
||||
</ElTooltip>
|
||||
<ElPopconfirm
|
||||
v-if="canDeleteRow(row)"
|
||||
title="确认删除该条工作日志?"
|
||||
confirm-button-text="删除"
|
||||
cancel-button-text="取消"
|
||||
confirm-button-type="danger"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<template #reference>
|
||||
<span class="inline-flex">
|
||||
<ElTooltip content="删除">
|
||||
<ElButton link type="danger" class="personal-item-worklog-panel__action-btn">
|
||||
<IconMdiDeleteOutline class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</span>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
<ElTooltip v-else content="仅可删除本人填报">
|
||||
<span class="inline-flex">
|
||||
<ElButton link type="danger" class="personal-item-worklog-panel__action-btn" disabled>
|
||||
<IconMdiDeleteOutline class="text-15px" />
|
||||
</ElButton>
|
||||
</span>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="personal-item-worklog-panel__pagination">
|
||||
<ElPagination
|
||||
v-if="total > 0"
|
||||
small
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:current-page="pageNo"
|
||||
:page-size="PAGE_SIZE"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PersonalItemWorklogFormDialog
|
||||
ref="worklogFormDialogRef"
|
||||
v-model:visible="formVisible"
|
||||
:mode="formMode"
|
||||
:row-data="editingWorklog"
|
||||
:item-status-code="props.item.statusCode"
|
||||
:default-progress-rate="props.item.progressRate"
|
||||
:confirm-loading="submitting"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.personal-item-worklog-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 12px 14px;
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__card-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__card-value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__card-value--accent {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__card-tag {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__duration {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__progress {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
:deep(.personal-item-worklog-panel__action-btn) {
|
||||
padding: 3px;
|
||||
min-width: auto;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-cell {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-cell-empty {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card-meta {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card-body {
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card-attachments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card-attachments-scroll {
|
||||
max-height: 144px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card-section-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.personal-item-worklog-panel__content-card-attachment-empty {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue';
|
||||
import { userGenderRecord } from '@/constants/business';
|
||||
import { fetchGetMyProfileDetail, fetchUpdateMyProfile } from '@/service/api';
|
||||
import { buildFileProxyUrl, uploadFile } from '@/service/api/file';
|
||||
import { fetchGetMyProfileDetail, fetchUpdateMyAvatar } from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { $t } from '@/locales';
|
||||
import ProfileInfoDialog from './modules/profile-info-dialog.vue';
|
||||
import ProfilePasswordDialog from './modules/profile-password-dialog.vue';
|
||||
import { buildProfileUpdatePayload, formatProfileDateTime, resolveProfileRoleLabels } from './modules/profile-model';
|
||||
import { formatProfileDateTime, resolveProfileRoleLabels } from './modules/profile-model';
|
||||
|
||||
defineOptions({ name: 'MyProfile' });
|
||||
|
||||
@@ -22,6 +21,8 @@ const profileInfoVisible = ref(false);
|
||||
const passwordVisible = ref(false);
|
||||
const avatarInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const MAX_AVATAR_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
const descriptionColumns = computed(() => (appStore.isMobile ? 1 : 2));
|
||||
const displayName = computed(() => profile.value?.nickname?.trim() || profile.value?.username || '--');
|
||||
const displayUsername = computed(() => profile.value?.username?.trim() || '--');
|
||||
@@ -41,6 +42,7 @@ const genderText = computed(() => {
|
||||
|
||||
return $t(userGenderRecord[value]);
|
||||
});
|
||||
|
||||
const roleLabels = computed(() => {
|
||||
const roles = profile.value?.roles ?? [];
|
||||
|
||||
@@ -57,16 +59,6 @@ function getAvatarText() {
|
||||
return name === '--' ? 'CN' : name.slice(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
function getEditableProfileValues(current: Api.Auth.MyProfileDetail): Api.Auth.UpdateMyProfileParams {
|
||||
return {
|
||||
nickname: current.nickname ?? '',
|
||||
email: current.email ?? '',
|
||||
mobile: current.mobile ?? '',
|
||||
sex: current.sex ?? 1,
|
||||
avatar: current.avatar ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
const userId = authStore.userInfo.userId;
|
||||
|
||||
@@ -75,9 +67,7 @@ async function loadProfile() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await fetchGetMyProfileDetail({
|
||||
userId
|
||||
});
|
||||
const { data, error } = await fetchGetMyProfileDetail({ userId });
|
||||
|
||||
if (!error) {
|
||||
profile.value = data;
|
||||
@@ -115,21 +105,14 @@ async function handleAvatarChange(event: Event) {
|
||||
return;
|
||||
}
|
||||
|
||||
avatarSubmitting.value = true;
|
||||
|
||||
const uploadResult = await uploadFile(file, 'avatar');
|
||||
|
||||
if (uploadResult.error) {
|
||||
avatarSubmitting.value = false;
|
||||
if (file.size > MAX_AVATAR_SIZE) {
|
||||
window.$message?.error('头像图片大小不能超过 5MB');
|
||||
return;
|
||||
}
|
||||
|
||||
const avatar = buildFileProxyUrl(uploadResult.data.configId, uploadResult.data.path);
|
||||
const payload = buildProfileUpdatePayload({
|
||||
...getEditableProfileValues(profile.value),
|
||||
avatar
|
||||
});
|
||||
const updateResult = await fetchUpdateMyProfile(payload);
|
||||
avatarSubmitting.value = true;
|
||||
|
||||
const updateResult = await fetchUpdateMyAvatar(file);
|
||||
|
||||
avatarSubmitting.value = false;
|
||||
|
||||
@@ -212,7 +195,7 @@ onActivated(() => {
|
||||
|
||||
<ElDescriptions :column="descriptionColumns" border>
|
||||
<ElDescriptionsItem label="用户名">{{ displayUsername }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="昵称">{{ displayName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="名称">{{ displayName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手机号">{{ mobileText }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="邮箱">{{ emailText }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="性别">{{ genderText }}</ElDescriptionsItem>
|
||||
|
||||
@@ -321,7 +321,7 @@ onMounted(async () => {
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
新建
|
||||
新增
|
||||
</ElButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
|
||||
Reference in New Issue
Block a user