feat(personal-item): 个人事项
This commit is contained in:
@@ -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 {};
|
||||
Reference in New Issue
Block a user