From 28d597d91e6ab49ab6bd2b4c8c0f539e5b0cbe5e Mon Sep 17 00:00:00 2001 From: caozehui <2427765068@qq.com> Date: Thu, 21 May 2026 14:06:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(personal-item):=20=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=BA=8B=E9=A1=B9&=E4=BB=BB=E5=8A=A1=E6=B7=BB=E5=8A=A0type?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/dict.ts | 14 ++++- src/service/api/personal-item.ts | 12 +++- src/service/api/project-shared.ts | 8 ++- src/typings/api/personal-item.d.ts | 2 + src/typings/api/project.d.ts | 8 ++- .../modules/personal-item-operate-dialog.vue | 17 +++++ .../personal-item-worklog-form-dialog.vue | 4 +- .../modules/personal-item-worklog-panel.vue | 62 +++++++++++++++++-- .../execution/modules/task-info-readonly.vue | 6 ++ .../execution/modules/task-operate-dialog.vue | 15 +++++ .../modules/task-worklog-form-dialog.vue | 18 ++++++ 11 files changed, 151 insertions(+), 15 deletions(-) diff --git a/src/constants/dict.ts b/src/constants/dict.ts index e745da5..91c24f3 100644 --- a/src/constants/dict.ts +++ b/src/constants/dict.ts @@ -85,12 +85,20 @@ 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` + * 来源口径:用户明确指定任务/个人事项工作日志难度下拉来自运行时字典 rdms_task&item_worklog_difficulty */ -export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_worklog_difficulty'; +export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_task&item_worklog_difficulty'; + +/** + * 任务/个人事项类型字典编码 + * + * 对应业务字段:任务、个人事项中的 type + * 来源口径:用户明确指定任务/个人事项类型下拉来自运行时字典 rdms_task&item_type + */ +export const RDMS_TASK_ITEM_TYPE_DICT_CODE = 'rdms_task&item_type'; /** * 需求允许删除的状态字典编码 diff --git a/src/service/api/personal-item.ts b/src/service/api/personal-item.ts index 5ce14b4..8f8639f 100644 --- a/src/service/api/personal-item.ts +++ b/src/service/api/personal-item.ts @@ -61,6 +61,7 @@ type PersonalItemExecutionOptionResponse = ProjectExecutionResponse & { type PersonalItemSaveRequest = { executionId?: string; taskTitle: string; + type: string; progressRate?: number; plannedStartDate?: string; plannedEndDate?: string; @@ -86,7 +87,7 @@ type PersonalItemWorklogSaveRequest = { size?: number; contentType?: string; }>; - difficulty?: string; + difficulty: string; }; const PERSONAL_ITEM_PREFIX = `${WEB_SERVICE_PREFIX}/project/personal-items`; @@ -163,6 +164,7 @@ function normalizePersonalItem(response: PersonalItemResponse): Api.PersonalItem return { id: normalizeStringId(response.id), taskTitle: response.taskTitle ?? '', + type: response.type ?? '', ownerId: normalizeStringId(response.ownerId), statusCode: response.statusCode, terminal: normalizeBooleanFlag(response.terminal), @@ -214,6 +216,7 @@ function toPersonalItemSaveRequest(data: Api.PersonalItem.SavePersonalItemParams return { executionId: data.executionId ?? undefined, taskTitle: data.taskTitle.trim(), + type: data.type, progressRate: typeof data.progressRate === 'number' ? data.progressRate : undefined, plannedStartDate: data.plannedStartDate ?? undefined, plannedEndDate: data.plannedEndDate ?? undefined, @@ -246,7 +249,7 @@ function toPersonalItemWorklogSaveRequest( size: item.size, contentType: item.contentType })) ?? undefined, - difficulty: data.difficulty ?? undefined + difficulty: data.difficulty }; } @@ -341,6 +344,7 @@ function createSeedItems(): PersonalItemRecord[] { { id: 'personal-item-1', taskTitle: '整理供应商沟通纪要', + type: 'daily', ownerId: CURRENT_USER_ID, statusCode: 'active', progressRate: 45, @@ -362,6 +366,7 @@ function createSeedItems(): PersonalItemRecord[] { { id: 'personal-item-2', taskTitle: '清理浏览器收藏夹里的项目入口', + type: 'daily', ownerId: CURRENT_USER_ID, statusCode: 'pending', progressRate: 0, @@ -383,6 +388,7 @@ function createSeedItems(): PersonalItemRecord[] { { id: 'personal-item-3', taskTitle: '补充账号开通说明截图', + type: 'support', ownerId: CURRENT_USER_ID, statusCode: 'completed', progressRate: 100, @@ -587,6 +593,7 @@ function syncItemFromWorklogs(itemId: string) { function applySaveFields(target: PersonalItemRecord, payload: Api.PersonalItem.SavePersonalItemParams) { target.taskTitle = payload.taskTitle.trim(); + target.type = payload.type; target.ownerId = payload.ownerId || target.ownerId; target.ownerName = CURRENT_USER_NAME; target.plannedStartDate = payload.plannedStartDate; @@ -661,6 +668,7 @@ export async function fetchCreatePersonalItem(data: Api.PersonalItem.SavePersona const createdItem: PersonalItemRecord = { id: mapped.data, taskTitle: data.taskTitle.trim(), + type: data.type, ownerId: data.ownerId || CURRENT_USER_ID, statusCode: 'pending', progressRate: typeof data.progressRate === 'number' ? data.progressRate : 0, diff --git a/src/service/api/project-shared.ts b/src/service/api/project-shared.ts index b709815..e7d33f0 100644 --- a/src/service/api/project-shared.ts +++ b/src/service/api/project-shared.ts @@ -133,10 +133,14 @@ export type ProjectTaskResponse = Omit< totalSpentHours?: number | null; }; -export type TaskWorklogResponse = Omit & { +export type TaskWorklogResponse = Omit< + Api.Project.TaskWorklog, + 'id' | 'taskId' | 'userId' | 'difficulty' | 'attachments' +> & { id: StringIdResponse; taskId: StringIdResponse; userId: StringIdResponse; + difficulty?: string | null; attachments?: AttachmentItemResponse[] | null; }; @@ -290,6 +294,7 @@ export function normalizeProjectTask(response: ProjectTaskResponse): Api.Project projectId: normalizeStringId(response.projectId), executionId: normalizeStringId(response.executionId), parentTaskId: normalizeNullableStringId(response.parentTaskId), + type: response.type ?? '', ownerId: normalizeStringId(response.ownerId), ownerNickname: response.ownerNickname ?? null, statusName: response.statusName ?? null, @@ -322,6 +327,7 @@ export function normalizeTaskWorklog(response: TaskWorklogResponse): Api.Project userId: normalizeStringId(response.userId), userNickname: response.userNickname ?? null, workContent: response.workContent ?? null, + difficulty: response.difficulty ?? '', attachments: normalizeAttachments(response.attachments), progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0 }; diff --git a/src/typings/api/personal-item.d.ts b/src/typings/api/personal-item.d.ts index 40c0387..05c3a25 100644 --- a/src/typings/api/personal-item.d.ts +++ b/src/typings/api/personal-item.d.ts @@ -16,6 +16,7 @@ declare namespace Api { interface PersonalItem { id: string; taskTitle: string; + type: string; ownerId: string; statusCode: PersonalItemStatusCode; terminal?: boolean; @@ -56,6 +57,7 @@ declare namespace Api { interface SavePersonalItemParams { taskTitle: string; + type: string; ownerId?: string; executionId?: string | null; progressRate?: number | null; diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index 8813ba8..a8483ca 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -214,6 +214,7 @@ declare namespace Api { executionId: string; parentTaskId: string | null; taskTitle: string; + type: string; ownerId: string; ownerNickname?: string | null; /** 所属执行的负责人 userId(按钮可见度公式用) */ @@ -350,6 +351,7 @@ declare namespace Api { interface SaveProjectTaskParams { parentTaskId: string | null; taskTitle: string; + type: string; ownerId: string | null; progressRate?: number; plannedStartDate: string | null; @@ -380,7 +382,8 @@ declare namespace Api { durationHours: number; /** 本次填报进度(0~100,scale=2) */ progressRate: number; - difficulty?: string | null; + /** 难度,来自字典 rdms_task&item_worklog_difficulty */ + difficulty: string; workContent: string | null; attachments?: AttachmentItem[] | null; createTime: string; @@ -404,7 +407,8 @@ declare namespace Api { durationHours: number; /** 本次填报进度(0~100,scale=2,必填) */ progressRate: number; - difficulty?: string | null; + /** 难度,来自字典 rdms_task&item_worklog_difficulty */ + difficulty: string; workContent?: string | null; /** 编辑语义:null 保留原值 / [] 清空 / [...] 替换 */ attachments?: AttachmentItem[] | null; diff --git a/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue b/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue index 262f80e..2e690e2 100644 --- a/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue +++ b/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue @@ -2,6 +2,7 @@ import { computed, nextTick, reactive, ref, watch } from 'vue'; import { useResizeObserver } from '@vueuse/core'; import dayjs from 'dayjs'; +import { RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict'; import { fetchCreatePersonalItem, fetchGetPersonalItemDetail, fetchUpdatePersonalItem } from '@/service/api'; import { useAuthStore } from '@/store/modules/auth'; import { useForm, useFormRules } from '@/hooks/common/form'; @@ -9,6 +10,7 @@ import BusinessAttachmentUploader from '@/components/custom/business-attachment- 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 DictSelect from '@/components/custom/dict-select.vue'; import { isEmptyRichText } from './personal-item-shared'; defineOptions({ name: 'PersonalItemOperateDialog' }); @@ -57,6 +59,7 @@ const submitting = ref(false); interface Model { taskTitle: string; + type: string; plannedStartDate: string | null; plannedEndDate: string | null; taskDesc: string | null; @@ -76,6 +79,7 @@ const title = computed(() => { function createDefaultModel(): Model { return { taskTitle: '', + type: '', plannedStartDate: null, plannedEndDate: null, taskDesc: null, @@ -108,6 +112,7 @@ const rules = computed( trigger: 'blur' } ], + type: [createRequiredRule('请选择事项类型')], plannedStartDate: [createRequiredRule('请选择计划开始日期')], plannedEndDate: [ createRequiredRule('请选择计划结束日期'), @@ -136,6 +141,7 @@ async function initModel() { if (!error && data) { model.taskTitle = data.taskTitle; + model.type = data.type; model.plannedStartDate = data.plannedStartDate; model.plannedEndDate = data.plannedEndDate; model.taskDesc = data.taskDesc; @@ -166,6 +172,7 @@ async function handleSubmit() { const payload: Api.PersonalItem.SavePersonalItemParams = { taskTitle: model.taskTitle.trim(), + type: model.type, ownerId: currentUserId.value, plannedStartDate: model.plannedStartDate, plannedEndDate: model.plannedEndDate, @@ -235,6 +242,16 @@ watch( /> + + + + - + import { computed, ref, watch } from 'vue'; -import { ElPopconfirm, ElTag, ElTooltip } from 'element-plus'; +import { ElMessageBox, 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 { + fetchCompletePersonalItem, fetchCreatePersonalItemWorklog, fetchDeletePersonalItemWorklog, + fetchGetPersonalItemDetail, fetchGetPersonalItemWorklogPage, fetchUpdatePersonalItemWorklog } from '@/service/api'; @@ -52,6 +54,8 @@ const currentUserName = computed( const PAGE_SIZE = 10; const TABLE_HEIGHT = 390; +const COMPLETED_STATUS_CODE: Api.PersonalItem.PersonalItemStatusCode = 'completed'; +const COMPLETE_ACTION_CODE = 'complete'; const pageNo = ref(1); const total = ref(0); @@ -189,6 +193,56 @@ async function loadRecords() { total.value = data.total; } +function canPromptCompleteItem(item: Api.PersonalItem.PersonalItem) { + if (item.statusCode === COMPLETED_STATUS_CODE || item.terminal) { + return false; + } + + return ( + item.progressRate >= 100 && (item.availableActions ?? []).some(action => action.actionCode === COMPLETE_ACTION_CODE) + ); +} + +async function fetchLatestItem() { + const { error, data } = await fetchGetPersonalItemDetail(props.item.id); + + if (error || !data) { + return null; + } + + return data; +} + +async function promptCompleteItemIfNeeded() { + const latestItem = await fetchLatestItem(); + + if (!latestItem || !canPromptCompleteItem(latestItem)) { + return; + } + + try { + await ElMessageBox.confirm('事项进度已达 100%,是否完成当前事项?', '完成确认', { + confirmButtonText: '完成事项', + cancelButtonText: '仅保留工时', + type: 'info' + }); + } catch { + return; + } + + const { error } = await fetchCompletePersonalItem(latestItem.id); + + if (!error) { + window.$message?.success('个人事项已完成'); + } +} + +async function reloadAfterWorklogChanged() { + await loadRecords(); + await promptCompleteItemIfNeeded(); + emit('changed'); +} + function handlePageChange(page: number) { pageNo.value = page; loadRecords(); @@ -225,8 +279,7 @@ async function handleDelete(row: Api.PersonalItem.PersonalItemWorklog) { } window.$message?.success('工作日志删除成功'); - await loadRecords(); - emit('changed'); + await reloadAfterWorklogChanged(); } async function handleSubmit(payload: Api.PersonalItem.SavePersonalItemWorklogParams) { @@ -249,8 +302,7 @@ async function handleSubmit(payload: Api.PersonalItem.SavePersonalItemWorklogPar window.$message?.success(formMode.value === 'edit' ? '工作日志修改成功' : '工作日志新增成功'); formVisible.value = false; - await loadRecords(); - emit('changed'); + await reloadAfterWorklogChanged(); } finally { submitting.value = false; } diff --git a/src/views/project/project/execution/modules/task-info-readonly.vue b/src/views/project/project/execution/modules/task-info-readonly.vue index a10ca1a..4f1fb17 100644 --- a/src/views/project/project/execution/modules/task-info-readonly.vue +++ b/src/views/project/project/execution/modules/task-info-readonly.vue @@ -1,9 +1,11 @@