fix(personal-item): 个人事项&任务添加type类型字段

This commit is contained in:
caozehui
2026-05-21 14:06:05 +08:00
parent fe29fde564
commit 28d597d91e
11 changed files with 151 additions and 15 deletions

View File

@@ -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';
/**
* 需求允许删除的状态字典编码

View File

@@ -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,

View File

@@ -133,10 +133,14 @@ export type ProjectTaskResponse = Omit<
totalSpentHours?: number | null;
};
export type TaskWorklogResponse = Omit<Api.Project.TaskWorklog, 'id' | 'taskId' | 'userId' | 'attachments'> & {
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
};

View File

@@ -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;

View File

@@ -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~100scale=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~100scale=2必填 */
progressRate: number;
difficulty?: string | null;
/** 难度,来自字典 rdms_task&item_worklog_difficulty */
difficulty: string;
workContent?: string | null;
/** 编辑语义null 保留原值 / [] 清空 / [...] 替换 */
attachments?: AttachmentItem[] | null;

View File

@@ -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(
/>
</ElFormItem>
<ElFormItem label="事项类型" prop="type">
<DictSelect
v-model="model.type"
:dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE"
:clearable="!isView"
:disabled="isView"
placeholder="请选择事项类型"
/>
</ElFormItem>
<ElFormItem label="计划开始日期" prop="plannedStartDate">
<ElDatePicker
v-model="model.plannedStartDate"

View File

@@ -162,7 +162,7 @@ const rules = computed(
trigger: 'change'
}
],
difficulty: [createRequiredRule('请选择完成难度')],
difficulty: [createRequiredRule('请选择难度')],
workContent: [
{
required: true,
@@ -356,7 +356,7 @@ defineExpose({
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="完成难度" prop="difficulty">
<ElFormItem label="难度" prop="difficulty">
<DictSelect
v-model="model.difficulty"
:dict-code="RDMS_WORKLOG_DIFFICULTY_DICT_CODE"

View File

@@ -1,12 +1,14 @@
<script setup lang="ts">
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;
}

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue';
import { RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
import BusinessFormSection from '@/components/custom/business-form-section.vue';
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
import DictSelect from '@/components/custom/dict-select.vue';
defineOptions({ name: 'ProjectExecutionTaskInfoReadonly' });
@@ -19,6 +21,7 @@ const props = withDefaults(defineProps<Props>(), {
});
const taskTitle = computed(() => props.task?.taskTitle ?? '');
const taskType = computed(() => props.task?.type ?? '');
const taskDesc = computed(() => props.task?.taskDesc ?? '');
const ownerId = computed(() => props.task?.ownerId ?? null);
const parentTaskId = computed(() => props.task?.parentTaskId ?? null);
@@ -46,6 +49,9 @@ const parentTaskOptions = computed(() => {
<ElFormItem label="任务名称">
<ElInput :model-value="taskTitle" readonly placeholder="--" />
</ElFormItem>
<ElFormItem label="任务类型">
<DictSelect :model-value="taskType" :dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE" disabled placeholder="--" />
</ElFormItem>
<ElFormItem label="父任务">
<ElSelect :model-value="parentTaskId" disabled clearable filterable class="w-full" placeholder="无">
<ElOption v-for="item in parentTaskOptions" :key="item.id" :label="item.taskTitle" :value="item.id" />

View File

@@ -2,12 +2,14 @@
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 { 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 BusinessUserSelect from '@/components/custom/business-user-select.vue';
import DictSelect from '@/components/custom/dict-select.vue';
defineOptions({ name: 'ProjectExecutionTaskOperateDialog' });
type OperateMode = 'create' | 'edit';
@@ -58,6 +60,7 @@ const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null
interface FormModel {
parentTaskId: string | null;
taskTitle: string;
type: string;
ownerId: string | null;
plannedStartDate: string | null;
plannedEndDate: string | null;
@@ -69,6 +72,7 @@ interface FormModel {
const model = reactive<FormModel>({
parentTaskId: null,
taskTitle: '',
type: '',
ownerId: null,
plannedStartDate: null,
plannedEndDate: null,
@@ -120,6 +124,7 @@ const rules = computed(
() =>
({
taskTitle: [createRequiredRule('请输入任务名称')],
type: [createRequiredRule('请选择任务类型')],
ownerId: model.parentTaskId ? [] : [createRequiredRule('请选择负责人')],
plannedStartDate: [createRequiredRule('请选择计划开始日期')],
plannedEndDate: [
@@ -227,6 +232,7 @@ async function handleConfirm() {
const payload: Api.Project.SaveProjectTaskParams = {
parentTaskId: model.parentTaskId || null,
taskTitle: model.taskTitle.trim(),
type: model.type,
ownerId: model.ownerId || null,
plannedStartDate: model.plannedStartDate,
plannedEndDate: model.plannedEndDate,
@@ -254,6 +260,7 @@ function applyRowDataToModel() {
model.parentTaskId =
props.mode === 'create' ? (props.defaultParentTaskId ?? null) : props.rowData?.parentTaskId || null;
model.taskTitle = props.rowData?.taskTitle || '';
model.type = props.rowData?.type || '';
model.ownerId = props.rowData?.ownerId || null;
model.plannedStartDate = props.rowData?.plannedStartDate || null;
model.plannedEndDate = props.rowData?.plannedEndDate || null;
@@ -318,6 +325,14 @@ defineExpose({
<ElInput v-model="model.taskTitle" maxlength="200" placeholder="请输入任务名称" />
</ElFormItem>
<ElFormItem label="任务类型" prop="type">
<DictSelect
v-model="model.type"
:dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE"
placeholder="请选择任务类型"
/>
</ElFormItem>
<ElFormItem label="父任务">
<ElSelect v-model="model.parentTaskId" clearable filterable class="w-full" placeholder="请选择父任务">
<ElOption

View File

@@ -1,11 +1,13 @@
<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 { fetchGetProjectTaskWorklogPage } from '@/service/api/project';
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 DictSelect from '@/components/custom/dict-select.vue';
defineOptions({ name: 'ProjectExecutionTaskWorklogFormDialog' });
@@ -60,6 +62,7 @@ interface FormModel {
/** 0.5 颗粒小时数 */
durationHours: number | null;
progressRate: number;
difficulty: string;
workContent: string | null;
attachments: Api.Project.AttachmentItem[];
}
@@ -75,6 +78,7 @@ const model = reactive<FormModel>({
weekDate: null,
durationHours: null,
progressRate: 0,
difficulty: '2',
workContent: null,
attachments: []
});
@@ -180,6 +184,7 @@ const rules = computed(
trigger: 'change'
}
],
difficulty: [createRequiredRule('请选择难度')],
workContent: [
{
required: true,
@@ -282,6 +287,7 @@ async function handleConfirm() {
endDate,
durationHours: Number(model.durationHours!.toFixed(1)),
progressRate: Number(model.progressRate.toFixed(2)),
difficulty: model.difficulty,
workContent: model.workContent?.trim() || null,
attachments: [...model.attachments]
};
@@ -309,6 +315,7 @@ watch(
model.weekDate = null;
}
model.durationHours = typeof row.durationHours === 'number' ? row.durationHours : null;
model.difficulty = row.difficulty || '2';
model.workContent = row.workContent || null;
model.attachments = row.attachments ? [...row.attachments] : [];
} else {
@@ -316,6 +323,7 @@ watch(
model.workDate = dayjs().format('YYYY-MM-DD');
model.weekDate = null;
model.durationHours = null;
model.difficulty = '2';
model.workContent = null;
model.attachments = [];
}
@@ -409,6 +417,16 @@ defineExpose({
/>
</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