feat(personal-center): 重构个人事项详情并复用任务工作日志组件
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, h, ref } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
import type { Component, PropType } from 'vue';
|
||||||
import { ElButton, ElPopover } from 'element-plus';
|
import { ElButton, ElPopover, ElTooltip } from 'element-plus';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
export type BusinessTableAction = {
|
export type BusinessTableAction = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
icon?: Component;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick: () => void | Promise<void>;
|
onClick: () => void | Promise<void>;
|
||||||
};
|
};
|
||||||
@@ -17,12 +18,20 @@ export default defineComponent({
|
|||||||
actions: {
|
actions: {
|
||||||
type: Array as PropType<BusinessTableAction[]>,
|
type: Array as PropType<BusinessTableAction[]>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String as PropType<'button' | 'icon'>,
|
||||||
|
default: 'button'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const popoverVisible = ref(false);
|
const popoverVisible = ref(false);
|
||||||
|
|
||||||
const directActions = computed(() => {
|
const directActions = computed(() => {
|
||||||
|
if (props.variant === 'icon') {
|
||||||
|
return props.actions;
|
||||||
|
}
|
||||||
|
|
||||||
if (props.actions.length <= 2) {
|
if (props.actions.length <= 2) {
|
||||||
return props.actions;
|
return props.actions;
|
||||||
}
|
}
|
||||||
@@ -31,6 +40,10 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const moreActions = computed(() => {
|
const moreActions = computed(() => {
|
||||||
|
if (props.variant === 'icon') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (props.actions.length <= 2) {
|
if (props.actions.length <= 2) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -47,9 +60,14 @@ export default defineComponent({
|
|||||||
await action.onClick();
|
await action.onClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => (
|
function renderIcon(action: BusinessTableAction) {
|
||||||
<div class="business-table-action-cell" onClick={event => event.stopPropagation()}>
|
if (!action.icon) return null;
|
||||||
{directActions.value.map(action => (
|
|
||||||
|
return h(action.icon, { class: 'business-table-action-icon' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderButtonAction(action: BusinessTableAction) {
|
||||||
|
return (
|
||||||
<ElButton
|
<ElButton
|
||||||
key={action.key}
|
key={action.key}
|
||||||
plain
|
plain
|
||||||
@@ -61,7 +79,67 @@ export default defineComponent({
|
|||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
))}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIconAction(action: BusinessTableAction) {
|
||||||
|
return (
|
||||||
|
<ElTooltip key={action.key} content={action.label} placement="top">
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
type={action.buttonType}
|
||||||
|
disabled={action.disabled}
|
||||||
|
class="business-table-action-icon-button"
|
||||||
|
aria-label={action.label}
|
||||||
|
onClick={() => handleAction(action)}
|
||||||
|
>
|
||||||
|
{renderIcon(action)}
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMenuButton(action: BusinessTableAction) {
|
||||||
|
if (props.variant === 'icon') {
|
||||||
|
return (
|
||||||
|
<ElButton
|
||||||
|
key={action.key}
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
type={action.buttonType}
|
||||||
|
disabled={action.disabled}
|
||||||
|
class="business-table-action-menu__link"
|
||||||
|
onClick={() => handleAction(action)}
|
||||||
|
>
|
||||||
|
<span class="business-table-action-menu__item">
|
||||||
|
{renderIcon(action)}
|
||||||
|
<span>{action.label}</span>
|
||||||
|
</span>
|
||||||
|
</ElButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ElButton
|
||||||
|
key={action.key}
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
type={action.buttonType}
|
||||||
|
disabled={action.disabled}
|
||||||
|
class="business-table-action-menu__button"
|
||||||
|
onClick={() => handleAction(action)}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</ElButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class="business-table-action-cell" onClick={event => event.stopPropagation()}>
|
||||||
|
{directActions.value.map(action =>
|
||||||
|
props.variant === 'icon' ? renderIconAction(action) : renderButtonAction(action)
|
||||||
|
)}
|
||||||
|
|
||||||
{moreActions.value.length > 0 && (
|
{moreActions.value.length > 0 && (
|
||||||
<ElPopover
|
<ElPopover
|
||||||
@@ -74,32 +152,28 @@ export default defineComponent({
|
|||||||
{{
|
{{
|
||||||
reference: () => (
|
reference: () => (
|
||||||
<ElButton
|
<ElButton
|
||||||
plain
|
link={props.variant === 'icon'}
|
||||||
|
plain={props.variant !== 'icon'}
|
||||||
size="small"
|
size="small"
|
||||||
class="business-table-action-button"
|
class={
|
||||||
|
props.variant === 'icon' ? 'business-table-action-icon-button' : 'business-table-action-button'
|
||||||
|
}
|
||||||
|
aria-label={$t('common.more')}
|
||||||
onClick={event => event.stopPropagation()}
|
onClick={event => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
{props.variant === 'icon' ? (
|
||||||
|
<icon-mdi-dots-horizontal class="business-table-action-icon" />
|
||||||
|
) : (
|
||||||
<span class="inline-flex items-center gap-4px">
|
<span class="inline-flex items-center gap-4px">
|
||||||
{$t('common.more')}
|
{$t('common.more')}
|
||||||
<icon-mdi-chevron-down class="text-14px" />
|
<icon-mdi-chevron-down class="text-14px" />
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
),
|
),
|
||||||
default: () => (
|
default: () => (
|
||||||
<div class="business-table-action-menu">
|
<div class="business-table-action-menu">
|
||||||
{moreActions.value.map(action => (
|
{moreActions.value.map(action => renderMenuButton(action))}
|
||||||
<ElButton
|
|
||||||
key={action.key}
|
|
||||||
plain
|
|
||||||
size="small"
|
|
||||||
type={action.buttonType}
|
|
||||||
disabled={action.disabled}
|
|
||||||
class="business-table-action-menu__button"
|
|
||||||
onClick={() => handleAction(action)}
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
</ElButton>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
|
|
||||||
defineOptions({ name: 'DictSelect' });
|
defineOptions({ name: 'DictSelect' });
|
||||||
|
|
||||||
|
const ensuredEmptyDictCodes = new Set<string>();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dictCode: string;
|
dictCode: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -34,6 +37,7 @@ const model = defineModel<string | number | Array<string | number> | null | unde
|
|||||||
default: undefined
|
default: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dictStore = useDictStore();
|
||||||
const { enabledDictData, dictData } = useDict(() => props.dictCode);
|
const { enabledDictData, dictData } = useDict(() => props.dictCode);
|
||||||
|
|
||||||
const dictOptions = computed(() => {
|
const dictOptions = computed(() => {
|
||||||
@@ -53,6 +57,19 @@ const selectedColorType = computed<string | null>(() => {
|
|||||||
if (value === null || value === undefined || value === '') return null;
|
if (value === null || value === undefined || value === '') return null;
|
||||||
return dictOptions.value.find(opt => opt.value === value)?.colorType ?? null;
|
return dictOptions.value.find(opt => opt.value === value)?.colorType ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.dictCode, dictOptions.value.length, dictStore.initialized, dictStore.loading] as const,
|
||||||
|
async ([dictCode, optionCount, initialized, loading]) => {
|
||||||
|
if (!dictCode || optionCount > 0 || !initialized || loading || ensuredEmptyDictCodes.has(dictCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensuredEmptyDictCodes.add(dictCode);
|
||||||
|
await dictStore.ensureDictData(dictCode, true);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ export const OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE = 'object_status_model_ob
|
|||||||
* 任务/个人事项类型字典编码
|
* 任务/个人事项类型字典编码
|
||||||
*
|
*
|
||||||
* 对应业务字段:任务、个人事项中的 type
|
* 对应业务字段:任务、个人事项中的 type
|
||||||
* 来源口径:用户明确指定任务/个人事项类型下拉来自运行时字典 rdms_task&item_type
|
* 来源口径:用户明确指定任务/个人事项类型下拉来自运行时字典 rdms_task_item_type
|
||||||
*/
|
*/
|
||||||
export const RDMS_TASK_ITEM_TYPE_DICT_CODE = 'rdms_task&item_type';
|
export const RDMS_TASK_ITEM_TYPE_DICT_CODE = 'rdms_task_item_type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 需求允许删除的状态字典编码
|
* 需求允许删除的状态字典编码
|
||||||
@@ -108,6 +108,6 @@ export const RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE = 'rdms_req_can_delete_status'
|
|||||||
* 工作日志难度字典编码
|
* 工作日志难度字典编码
|
||||||
*
|
*
|
||||||
* 对应业务字段:任务/个人事项工作日志中的 difficulty
|
* 对应业务字段:任务/个人事项工作日志中的 difficulty
|
||||||
* 来源口径:用户明确指定任务/个人事项工作日志难度下拉来自运行时字典 rdms_task&item_worklog_difficulty
|
* 来源口径:用户明确指定任务/个人事项工作日志难度下拉来自运行时字典 rdms_task_item_worklog_difficulty
|
||||||
*/
|
*/
|
||||||
export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_task&item_worklog_difficulty';
|
export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_task_item_worklog_difficulty';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE } from '@/constants/dict';
|
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE } from '@/constants/dict';
|
||||||
import { fetchGetFrontendDictCache } from '@/service/api';
|
import { fetchGetDictDataByCode, fetchGetFrontendDictCache } from '@/service/api';
|
||||||
import { SetupStoreId } from '@/enum';
|
import { SetupStoreId } from '@/enum';
|
||||||
|
|
||||||
type DictValue = string | number | null | undefined;
|
type DictValue = string | number | null | undefined;
|
||||||
@@ -48,6 +48,17 @@ function normalizeFrontendDictData(
|
|||||||
return sortDictData(normalizedList);
|
return sortDictData(normalizedList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDictDataItem(item: Api.Dict.DictData, dictType: string): Api.Dict.DictData {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: String(item.value),
|
||||||
|
dictType: item.dictType || dictType,
|
||||||
|
status: item.status ?? 0,
|
||||||
|
colorType: normalizeColorType(item.colorType),
|
||||||
|
remark: item.remark ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFrontendDictCache(cache: Api.Dict.FrontendDictCache) {
|
function normalizeFrontendDictCache(cache: Api.Dict.FrontendDictCache) {
|
||||||
const entries = Object.entries(cache);
|
const entries = Object.entries(cache);
|
||||||
|
|
||||||
@@ -99,6 +110,7 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
const loadedAt = ref<number | null>(null);
|
const loadedAt = ref<number | null>(null);
|
||||||
|
|
||||||
let initPromise: Promise<boolean> | null = null;
|
let initPromise: Promise<boolean> | null = null;
|
||||||
|
const dictDataLoadPromises = new Map<string, Promise<boolean>>();
|
||||||
|
|
||||||
function resetDictCache() {
|
function resetDictCache() {
|
||||||
dictTypes.value = [];
|
dictTypes.value = [];
|
||||||
@@ -106,6 +118,7 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
loadedAt.value = null;
|
loadedAt.value = null;
|
||||||
initialized.value = false;
|
initialized.value = false;
|
||||||
initPromise = null;
|
initPromise = null;
|
||||||
|
dictDataLoadPromises.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initDictCache(force = false) {
|
async function initDictCache(force = false) {
|
||||||
@@ -147,6 +160,51 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
return initPromise;
|
return initPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureDictData(dictType: string, force = false) {
|
||||||
|
if (!dictType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialized.value) {
|
||||||
|
await initDictCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && getDictData(dictType).length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = dictDataLoadPromises.get(dictType);
|
||||||
|
if (pending && !force) {
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const result = await fetchGetDictDataByCode(dictType);
|
||||||
|
|
||||||
|
if (result.error || !result.data?.list?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dictDataMap.value = {
|
||||||
|
...dictDataMap.value,
|
||||||
|
[dictType]: sortDictData(result.data.list.map(item => normalizeDictDataItem(item, dictType)))
|
||||||
|
};
|
||||||
|
dictTypes.value = createRuntimeDictTypes(dictDataMap.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
dictDataLoadPromises.set(dictType, promise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} finally {
|
||||||
|
if (dictDataLoadPromises.get(dictType) === promise) {
|
||||||
|
dictDataLoadPromises.delete(dictType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getDictData(dictType: string, onlyEnabled = false) {
|
function getDictData(dictType: string, onlyEnabled = false) {
|
||||||
if (!dictType) {
|
if (!dictType) {
|
||||||
return [];
|
return [];
|
||||||
@@ -209,6 +267,7 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
dictDataMap,
|
dictDataMap,
|
||||||
loadedAt,
|
loadedAt,
|
||||||
initDictCache,
|
initDictCache,
|
||||||
|
ensureDictData,
|
||||||
resetDictCache,
|
resetDictCache,
|
||||||
getDictData,
|
getDictData,
|
||||||
getDictOptions,
|
getDictOptions,
|
||||||
|
|||||||
@@ -416,6 +416,20 @@ html .el-collapse {
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-table-action-icon-button {
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.el-button + .el-button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-table-action-icon {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.business-table-action-menu {
|
.business-table-action-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -428,6 +442,19 @@ html .el-collapse {
|
|||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-table-action-menu__link {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-table-action-menu__item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.business-table-card-body {
|
.business-table-card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100% - 56px);
|
height: calc(100% - 56px);
|
||||||
|
|||||||
4
src/typings/api/project.d.ts
vendored
4
src/typings/api/project.d.ts
vendored
@@ -412,7 +412,7 @@ declare namespace Api {
|
|||||||
durationHours: number;
|
durationHours: number;
|
||||||
/** 本次填报进度(0~100,scale=2) */
|
/** 本次填报进度(0~100,scale=2) */
|
||||||
progressRate: number;
|
progressRate: number;
|
||||||
/** 难度,来自字典 rdms_task&item_worklog_difficulty */
|
/** 难度,来自字典 rdms_task_item_worklog_difficulty */
|
||||||
difficulty: string;
|
difficulty: string;
|
||||||
/** 后端预留字段,目前始终为 null,前端按 difficulty + 字典 cache 自译 */
|
/** 后端预留字段,目前始终为 null,前端按 difficulty + 字典 cache 自译 */
|
||||||
difficultyName?: string | null;
|
difficultyName?: string | null;
|
||||||
@@ -441,7 +441,7 @@ declare namespace Api {
|
|||||||
durationHours: number;
|
durationHours: number;
|
||||||
/** 本次填报进度(0~100,scale=2,必填) */
|
/** 本次填报进度(0~100,scale=2,必填) */
|
||||||
progressRate: number;
|
progressRate: number;
|
||||||
/** 难度,来自字典 rdms_task&item_worklog_difficulty */
|
/** 难度,来自字典 rdms_task_item_worklog_difficulty */
|
||||||
difficulty: string;
|
difficulty: string;
|
||||||
workContent?: string | null;
|
workContent?: string | null;
|
||||||
/** 编辑语义:null 保留原值 / [] 清空 / [...] 替换 */
|
/** 编辑语义:null 保留原值 / [] 清空 / [...] 替换 */
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import StateMachineOperateDialog from './modules/state-machine-operate-dialog.vu
|
|||||||
import StateMachineSearch from './modules/state-machine-search.vue';
|
import StateMachineSearch from './modules/state-machine-search.vue';
|
||||||
import StateTransitionDialog from './modules/state-transition-dialog.vue';
|
import StateTransitionDialog from './modules/state-transition-dialog.vue';
|
||||||
import { formatDateTime, getBooleanLabel, getBooleanTagType, getStatusLabel, getStatusTagType } from './shared';
|
import { formatDateTime, getBooleanLabel, getBooleanTagType, getStatusLabel, getStatusTagType } from './shared';
|
||||||
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
|
import IconMdiSourceBranch from '~icons/mdi/source-branch';
|
||||||
|
|
||||||
defineOptions({ name: 'StateMachineManage' });
|
defineOptions({ name: 'StateMachineManage' });
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ function getStatusModelActions(row: Api.Infra.ObjectStatusModel): BusinessTableA
|
|||||||
actions.push({
|
actions.push({
|
||||||
key: 'transition',
|
key: 'transition',
|
||||||
label: '状态流转',
|
label: '状态流转',
|
||||||
|
icon: IconMdiSourceBranch,
|
||||||
buttonType: 'primary',
|
buttonType: 'primary',
|
||||||
onClick: () => openTransitionDialog(row)
|
onClick: () => openTransitionDialog(row)
|
||||||
});
|
});
|
||||||
@@ -80,6 +84,7 @@ function getStatusModelActions(row: Api.Infra.ObjectStatusModel): BusinessTableA
|
|||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
|
icon: IconMdiPencilOutline,
|
||||||
buttonType: 'primary',
|
buttonType: 'primary',
|
||||||
onClick: () => openEdit(row)
|
onClick: () => openEdit(row)
|
||||||
});
|
});
|
||||||
@@ -89,6 +94,7 @@ function getStatusModelActions(row: Api.Infra.ObjectStatusModel): BusinessTableA
|
|||||||
actions.push({
|
actions.push({
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: '删除',
|
label: '删除',
|
||||||
|
icon: IconMdiDeleteOutline,
|
||||||
buttonType: 'danger',
|
buttonType: 'danger',
|
||||||
onClick: () => handleDeleteAction(row)
|
onClick: () => handleDeleteAction(row)
|
||||||
});
|
});
|
||||||
@@ -203,7 +209,7 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
|||||||
return <span>--</span>;
|
return <span>--</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BusinessTableActionCell actions={actions} />;
|
return <BusinessTableActionCell actions={actions} variant="icon" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -292,6 +292,13 @@ function buildRowActions(row: Api.PersonalItem.PersonalItem): PersonalItemRowAct
|
|||||||
openOperateDialog();
|
openOperateDialog();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
tooltip: '删除',
|
||||||
|
icon: markRaw(IconMdiDeleteOutline),
|
||||||
|
type: 'danger',
|
||||||
|
onClick: async () => handleDelete(row)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'status-pause',
|
key: 'status-pause',
|
||||||
tooltip: pauseAction?.actionName ?? '暂停',
|
tooltip: pauseAction?.actionName ?? '暂停',
|
||||||
@@ -305,19 +312,19 @@ function buildRowActions(row: Api.PersonalItem.PersonalItem): PersonalItemRowAct
|
|||||||
needReason: pauseAction?.needReason ?? false
|
needReason: pauseAction?.needReason ?? false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
key: 'status-cancel',
|
// key: 'status-cancel',
|
||||||
tooltip: cancelAction?.actionName ?? '取消',
|
// tooltip: cancelAction?.actionName ?? '取消',
|
||||||
icon: markRaw(IconMdiCloseCircleOutline),
|
// icon: markRaw(IconMdiCloseCircleOutline),
|
||||||
type: 'danger',
|
// type: 'danger',
|
||||||
disabled: !cancelAction,
|
// disabled: !cancelAction,
|
||||||
onClick: async () =>
|
// onClick: async () =>
|
||||||
handleStatusAction(row, {
|
// handleStatusAction(row, {
|
||||||
actionCode: cancelAction?.actionCode ?? 'cancel',
|
// actionCode: cancelAction?.actionCode ?? 'cancel',
|
||||||
actionName: cancelAction?.actionName ?? '取消',
|
// actionName: cancelAction?.actionName ?? '取消',
|
||||||
needReason: cancelAction?.needReason ?? false
|
// needReason: cancelAction?.needReason ?? false
|
||||||
})
|
// })
|
||||||
},
|
// },
|
||||||
...lifecycleActions,
|
...lifecycleActions,
|
||||||
{
|
{
|
||||||
key: 'status-complete',
|
key: 'status-complete',
|
||||||
@@ -331,13 +338,6 @@ function buildRowActions(row: Api.PersonalItem.PersonalItem): PersonalItemRowAct
|
|||||||
actionName: completeAction?.actionName ?? '完成',
|
actionName: completeAction?.actionName ?? '完成',
|
||||||
needReason: completeAction?.needReason ?? false
|
needReason: completeAction?.needReason ?? false
|
||||||
})
|
})
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
tooltip: '删除',
|
|
||||||
icon: markRaw(IconMdiDeleteOutline),
|
|
||||||
type: 'danger',
|
|
||||||
onClick: async () => handleDelete(row)
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { fetchGetPersonalItemDetail } from '@/service/api';
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import {
|
||||||
|
fetchCompletePersonalItem,
|
||||||
|
fetchCreatePersonalItemWorklog,
|
||||||
|
fetchDeletePersonalItemWorklog,
|
||||||
|
fetchGetPersonalItemDetail,
|
||||||
|
fetchGetPersonalItemWorklogPage,
|
||||||
|
fetchUpdatePersonalItemWorklog
|
||||||
|
} from '@/service/api';
|
||||||
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
import PersonalItemWorklogPanel from './personal-item-worklog-panel.vue';
|
import TaskWorklogPanel from '@/views/project/project/execution/modules/task-worklog-panel.vue';
|
||||||
|
import {
|
||||||
|
formatPersonalItemDate,
|
||||||
|
formatPersonalItemOwnerName,
|
||||||
|
formatPersonalItemProgress,
|
||||||
|
getPersonalItemStatusLabel,
|
||||||
|
resolvePersonalItemStatusTagType
|
||||||
|
} from './personal-item-shared';
|
||||||
|
|
||||||
defineOptions({ name: 'PersonalItemDetailDialog' });
|
defineOptions({ name: 'PersonalItemDetailDialog' });
|
||||||
|
|
||||||
@@ -27,6 +43,49 @@ const visible = defineModel<boolean>('visible', {
|
|||||||
|
|
||||||
const activeTab = ref<TabName>('worklog');
|
const activeTab = ref<TabName>('worklog');
|
||||||
const detailData = ref<Api.PersonalItem.PersonalItem | null>(null);
|
const detailData = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||||
|
const currentUserName = computed(
|
||||||
|
() => authStore.userInfo.nickname?.trim() || authStore.userInfo.userName?.trim() || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const COMPLETED_STATUS_CODE: Api.PersonalItem.PersonalItemStatusCode = 'completed';
|
||||||
|
const COMPLETE_ACTION_CODE = 'complete';
|
||||||
|
|
||||||
|
const ownerName = computed(() => {
|
||||||
|
if (!detailData.value) return '--';
|
||||||
|
|
||||||
|
const displayName = formatPersonalItemOwnerName(detailData.value);
|
||||||
|
|
||||||
|
if (displayName !== detailData.value.ownerId) {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return detailData.value.ownerId === currentUserId.value && currentUserName.value
|
||||||
|
? currentUserName.value
|
||||||
|
: displayName;
|
||||||
|
});
|
||||||
|
const statusName = computed(() => (detailData.value ? getPersonalItemStatusLabel(detailData.value.statusCode) : '--'));
|
||||||
|
const statusTagType = computed(() =>
|
||||||
|
detailData.value ? resolvePersonalItemStatusTagType(detailData.value.statusCode) : 'info'
|
||||||
|
);
|
||||||
|
const progressText = computed(() => formatPersonalItemProgress(detailData.value?.progressRate));
|
||||||
|
const plannedStartText = computed(() => formatPersonalItemDate(detailData.value?.plannedStartDate));
|
||||||
|
const plannedEndText = computed(() => formatPersonalItemDate(detailData.value?.plannedEndDate));
|
||||||
|
const actualStartText = computed(() => formatPersonalItemDate(detailData.value?.actualStartDate));
|
||||||
|
const actualEndText = computed(() => formatPersonalItemDate(detailData.value?.actualEndDate));
|
||||||
|
const totalHoursText = computed(() => {
|
||||||
|
const total = detailData.value?.totalSpentHours;
|
||||||
|
return `${typeof total === 'number' && Number.isFinite(total) ? total.toFixed(1) : '0.0'}h`;
|
||||||
|
});
|
||||||
|
const canSubmitWorklog = computed(() =>
|
||||||
|
Boolean(
|
||||||
|
detailData.value?.id &&
|
||||||
|
(detailData.value.statusCode === 'pending' ||
|
||||||
|
detailData.value.statusCode === 'active' ||
|
||||||
|
detailData.value.statusCode === 'completed')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
function syncDetailFromPageRow() {
|
function syncDetailFromPageRow() {
|
||||||
detailData.value = props.rowData ?? null;
|
detailData.value = props.rowData ?? null;
|
||||||
@@ -44,14 +103,64 @@ async function refreshDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 promptCompleteItemIfNeeded() {
|
||||||
|
if (!detailData.value || !canPromptCompleteItem(detailData.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('事项进度已达 100%,是否完成当前事项?', '完成确认', {
|
||||||
|
confirmButtonText: '完成事项',
|
||||||
|
cancelButtonText: '仅保留工时',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await fetchCompletePersonalItem(detailData.value.id);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
window.$message?.success('个人事项已完成');
|
||||||
|
await refreshDetail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleWorklogChanged() {
|
async function handleWorklogChanged() {
|
||||||
await refreshDetail();
|
await refreshDetail();
|
||||||
|
await promptCompleteItemIfNeeded();
|
||||||
|
|
||||||
if (detailData.value) {
|
if (detailData.value) {
|
||||||
emit('changed', detailData.value);
|
emit('changed', detailData.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchPersonalWorklogPage(params: Api.Project.TaskWorklogSearchParams) {
|
||||||
|
return fetchGetPersonalItemWorklogPage(detailData.value!.id, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPersonalWorklog(data: Api.Project.SaveTaskWorklogParams) {
|
||||||
|
return fetchCreatePersonalItemWorklog(detailData.value!.id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePersonalWorklog(payload: { worklogId: string; data: Api.Project.SaveTaskWorklogParams }) {
|
||||||
|
return fetchUpdatePersonalItemWorklog(detailData.value!.id, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePersonalWorklog(worklogId: string) {
|
||||||
|
return fetchDeletePersonalItemWorklog(detailData.value!.id, worklogId);
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => visible.value,
|
() => visible.value,
|
||||||
value => {
|
value => {
|
||||||
@@ -92,12 +201,66 @@ watch(
|
|||||||
>
|
>
|
||||||
<ElTabs v-model="activeTab" class="personal-item-detail-dialog__tabs">
|
<ElTabs v-model="activeTab" class="personal-item-detail-dialog__tabs">
|
||||||
<ElTabPane label="工作日志" name="worklog" lazy>
|
<ElTabPane label="工作日志" name="worklog" lazy>
|
||||||
<PersonalItemWorklogPanel
|
<div v-if="detailData" class="personal-item-worklog-content">
|
||||||
v-if="detailData"
|
<div class="personal-item-worklog-content__cards">
|
||||||
:item="detailData"
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">负责人</span>
|
||||||
|
<span class="personal-item-worklog-content__card-value" :title="ownerName">{{ ownerName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">当前状态</span>
|
||||||
|
<ElTag :type="statusTagType" size="small" effect="light" class="personal-item-worklog-content__card-tag">
|
||||||
|
{{ statusName }}
|
||||||
|
</ElTag>
|
||||||
|
</div>
|
||||||
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">计划开始</span>
|
||||||
|
<span class="personal-item-worklog-content__card-value">{{ plannedStartText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">计划结束</span>
|
||||||
|
<span class="personal-item-worklog-content__card-value">{{ plannedEndText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">当前进度</span>
|
||||||
|
<span class="personal-item-worklog-content__card-value">{{ progressText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">累计工时</span>
|
||||||
|
<span class="personal-item-worklog-content__card-value personal-item-worklog-content__card-value--accent">
|
||||||
|
{{ totalHoursText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">实际开始</span>
|
||||||
|
<span class="personal-item-worklog-content__card-value">{{ actualStartText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="personal-item-worklog-content__card">
|
||||||
|
<span class="personal-item-worklog-content__card-label">实际结束</span>
|
||||||
|
<span class="personal-item-worklog-content__card-value">{{ actualEndText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TaskWorklogPanel
|
||||||
|
project-id=""
|
||||||
|
execution-id=""
|
||||||
|
:task-id="detailData.id"
|
||||||
|
:task-owner-id="currentUserId"
|
||||||
|
:task-status-code="detailData.statusCode"
|
||||||
|
:task-progress-rate="detailData.progressRate"
|
||||||
|
:can-submit="canSubmitWorklog"
|
||||||
:active="activeTab === 'worklog' && visible"
|
:active="activeTab === 'worklog' && visible"
|
||||||
|
:fetch-worklog-page="fetchPersonalWorklogPage"
|
||||||
|
:create-worklog="createPersonalWorklog"
|
||||||
|
:update-worklog="updatePersonalWorklog"
|
||||||
|
:delete-worklog="deletePersonalWorklog"
|
||||||
|
attachment-directory="personal-item-worklog"
|
||||||
|
create-success-message="工作日志新增成功"
|
||||||
|
update-success-message="工作日志修改成功"
|
||||||
|
delete-success-message="工作日志删除成功"
|
||||||
@changed="handleWorklogChanged"
|
@changed="handleWorklogChanged"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
</ElTabs>
|
</ElTabs>
|
||||||
</BusinessFormDialog>
|
</BusinessFormDialog>
|
||||||
@@ -112,4 +275,51 @@ watch(
|
|||||||
.personal-item-detail-dialog__tabs :deep(.el-tab-pane) {
|
.personal-item-detail-dialog__tabs :deep(.el-tab-pane) {
|
||||||
min-height: 640px;
|
min-height: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.personal-item-worklog-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item-worklog-content__cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item-worklog-content__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-content__card-label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item-worklog-content__card-value {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item-worklog-content__card-value--accent {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item-worklog-content__card-tag {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,409 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,708 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
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';
|
|
||||||
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 COMPLETED_STATUS_CODE: Api.PersonalItem.PersonalItemStatusCode = 'completed';
|
|
||||||
const COMPLETE_ACTION_CODE = 'complete';
|
|
||||||
|
|
||||||
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 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 reloadAfterWorklogChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 reloadAfterWorklogChanged();
|
|
||||||
} 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>
|
|
||||||
@@ -25,6 +25,8 @@ interface Props {
|
|||||||
taskStatusCode: string;
|
taskStatusCode: string;
|
||||||
/** 创建模式下的进度兜底默认值(owner 路径会传 task.progressRate) */
|
/** 创建模式下的进度兜底默认值(owner 路径会传 task.progressRate) */
|
||||||
defaultOwnerProgressRate?: number;
|
defaultOwnerProgressRate?: number;
|
||||||
|
/** 附件上传目录;任务默认 task-worklog,复用方可按业务域覆盖 */
|
||||||
|
attachmentDirectory?: string;
|
||||||
/** 提交中(HTTP 进行中),由父组件控制 */
|
/** 提交中(HTTP 进行中),由父组件控制 */
|
||||||
confirmLoading?: boolean;
|
confirmLoading?: boolean;
|
||||||
}
|
}
|
||||||
@@ -35,6 +37,7 @@ interface Emits {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
defaultOwnerProgressRate: 0,
|
defaultOwnerProgressRate: 0,
|
||||||
|
attachmentDirectory: 'task-worklog',
|
||||||
confirmLoading: false
|
confirmLoading: false
|
||||||
});
|
});
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
@@ -62,7 +65,7 @@ interface FormModel {
|
|||||||
/** 0.5 颗粒小时数 */
|
/** 0.5 颗粒小时数 */
|
||||||
durationHours: number | null;
|
durationHours: number | null;
|
||||||
progressRate: number;
|
progressRate: number;
|
||||||
/** 完成难度,字典 rdms_worklog_difficulty 的 value,默认 "2";用户清空后为 null,由 required 校验拦截 */
|
/** 完成难度,字典 rdms_task_item_worklog_difficulty 的 value,默认 "2";用户清空后为 null,由 required 校验拦截 */
|
||||||
difficulty: string | null;
|
difficulty: string | null;
|
||||||
workContent: string | null;
|
workContent: string | null;
|
||||||
attachments: Api.Project.AttachmentItem[];
|
attachments: Api.Project.AttachmentItem[];
|
||||||
@@ -458,7 +461,7 @@ defineExpose({
|
|||||||
ref="attachmentUploaderRef"
|
ref="attachmentUploaderRef"
|
||||||
v-model="model.attachments"
|
v-model="model.attachments"
|
||||||
:disabled="isView"
|
:disabled="isView"
|
||||||
directory="task-worklog"
|
:directory="props.attachmentDirectory"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
fetchGetProjectTaskWorklogPage,
|
fetchGetProjectTaskWorklogPage,
|
||||||
fetchUpdateProjectTaskWorklog
|
fetchUpdateProjectTaskWorklog
|
||||||
} from '@/service/api/project';
|
} from '@/service/api/project';
|
||||||
|
import type { ServiceRequestResult } from '@/service/api/shared';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||||
@@ -23,6 +24,15 @@ import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
|||||||
|
|
||||||
defineOptions({ name: 'ProjectExecutionTaskWorklogPanel' });
|
defineOptions({ name: 'ProjectExecutionTaskWorklogPanel' });
|
||||||
|
|
||||||
|
type WorklogPageResult = {
|
||||||
|
error: unknown;
|
||||||
|
data?: Api.Project.PageResult<Api.Project.TaskWorklog> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorklogMutationResult = {
|
||||||
|
error: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
executionId: string;
|
executionId: string;
|
||||||
@@ -42,6 +52,24 @@ interface Props {
|
|||||||
ownerNickname?: string | null;
|
ownerNickname?: string | null;
|
||||||
/** 是否展示「填报人」列与列头筛选;只在 owner 视角下展示 */
|
/** 是否展示「填报人」列与列头筛选;只在 owner 视角下展示 */
|
||||||
showAssigneeColumn?: boolean;
|
showAssigneeColumn?: boolean;
|
||||||
|
/** 是否激活;非激活时不主动加载内部数据 */
|
||||||
|
active?: boolean;
|
||||||
|
/** 自定义分页查询,个人事项等复用方通过它接入自己的接口 */
|
||||||
|
fetchWorklogPage?: (params: Api.Project.TaskWorklogSearchParams) => Promise<WorklogPageResult>;
|
||||||
|
/** 自定义新增接口 */
|
||||||
|
createWorklog?: (data: Api.Project.SaveTaskWorklogParams) => Promise<WorklogMutationResult>;
|
||||||
|
/** 自定义编辑接口 */
|
||||||
|
updateWorklog?: (payload: {
|
||||||
|
worklogId: string;
|
||||||
|
data: Api.Project.SaveTaskWorklogParams;
|
||||||
|
}) => Promise<WorklogMutationResult>;
|
||||||
|
/** 自定义删除接口 */
|
||||||
|
deleteWorklog?: (worklogId: string) => Promise<WorklogMutationResult>;
|
||||||
|
/** 附件上传目录 */
|
||||||
|
attachmentDirectory?: string;
|
||||||
|
createSuccessMessage?: string;
|
||||||
|
updateSuccessMessage?: string;
|
||||||
|
deleteSuccessMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -50,7 +78,12 @@ interface Emits {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
taskProgressRate: 0,
|
taskProgressRate: 0,
|
||||||
showAssigneeColumn: false
|
showAssigneeColumn: false,
|
||||||
|
active: true,
|
||||||
|
attachmentDirectory: 'task-worklog',
|
||||||
|
createSuccessMessage: '填报成功',
|
||||||
|
updateSuccessMessage: '填报已更新',
|
||||||
|
deleteSuccessMessage: '工时已删除'
|
||||||
});
|
});
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
@@ -249,7 +282,19 @@ async function loadList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!props.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!props.projectId || !props.executionId || !props.taskId) {
|
if (!props.projectId || !props.executionId || !props.taskId) {
|
||||||
|
if (!props.fetchWorklogPage) {
|
||||||
|
internalList.value = [];
|
||||||
|
internalTotal.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.taskId) {
|
||||||
internalList.value = [];
|
internalList.value = [];
|
||||||
internalTotal.value = 0;
|
internalTotal.value = 0;
|
||||||
return;
|
return;
|
||||||
@@ -270,12 +315,9 @@ async function loadList() {
|
|||||||
params.difficulty = difficultyFilter.value;
|
params.difficulty = difficultyFilter.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error, data } = await fetchGetProjectTaskWorklogPage(
|
const { error, data } = props.fetchWorklogPage
|
||||||
props.projectId,
|
? await props.fetchWorklogPage(params)
|
||||||
props.executionId,
|
: await fetchGetProjectTaskWorklogPage(props.projectId, props.executionId, props.taskId, params);
|
||||||
props.taskId,
|
|
||||||
params
|
|
||||||
);
|
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
@@ -318,19 +360,33 @@ function handleView(row: Api.Project.TaskWorklog) {
|
|||||||
async function handleSubmit(payload: Api.Project.SaveTaskWorklogParams) {
|
async function handleSubmit(payload: Api.Project.SaveTaskWorklogParams) {
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const result =
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
formMode.value === 'create'
|
let result: any;
|
||||||
? await fetchCreateProjectTaskWorklog(props.projectId, props.executionId, props.taskId, payload)
|
|
||||||
: await fetchUpdateProjectTaskWorklog(props.projectId, props.executionId, props.taskId, {
|
if (formMode.value === 'create') {
|
||||||
|
if (props.createWorklog) {
|
||||||
|
result = await props.createWorklog(payload);
|
||||||
|
} else {
|
||||||
|
result = await fetchCreateProjectTaskWorklog(props.projectId, props.executionId, props.taskId, payload);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const updatePayload = {
|
||||||
worklogId: editingWorklog.value!.id,
|
worklogId: editingWorklog.value!.id,
|
||||||
data: payload
|
data: payload
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (props.updateWorklog) {
|
||||||
|
result = await props.updateWorklog(updatePayload);
|
||||||
|
} else {
|
||||||
|
result = await fetchUpdateProjectTaskWorklog(props.projectId, props.executionId, props.taskId, updatePayload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$message?.success(formMode.value === 'create' ? '填报成功' : '填报已更新');
|
window.$message?.success(formMode.value === 'create' ? props.createSuccessMessage : props.updateSuccessMessage);
|
||||||
// 业务保存成功才 commit:删除用户在弹层里标记删除的附件
|
// 业务保存成功才 commit:删除用户在弹层里标记删除的附件
|
||||||
await worklogFormDialogRef.value?.commit();
|
await worklogFormDialogRef.value?.commit();
|
||||||
formVisible.value = false;
|
formVisible.value = false;
|
||||||
@@ -351,12 +407,14 @@ async function handleSubmit(payload: Api.Project.SaveTaskWorklogParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(row: Api.Project.TaskWorklog) {
|
async function handleDelete(row: Api.Project.TaskWorklog) {
|
||||||
const { error } = await fetchDeleteProjectTaskWorklog(props.projectId, props.executionId, props.taskId, row.id);
|
const { error } = props.deleteWorklog
|
||||||
|
? await props.deleteWorklog(row.id)
|
||||||
|
: await fetchDeleteProjectTaskWorklog(props.projectId, props.executionId, props.taskId, row.id);
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$message?.success('工时已删除');
|
window.$message?.success(props.deleteSuccessMessage);
|
||||||
if (!usingExternal.value) {
|
if (!usingExternal.value) {
|
||||||
// 删完最后一页若空了,回退一页(仅 server-side 路径手动处理;外部模式下由 watch(total) 兜底)
|
// 删完最后一页若空了,回退一页(仅 server-side 路径手动处理;外部模式下由 watch(total) 兜底)
|
||||||
if (list.value.length === 1 && pageNo.value > 1) {
|
if (list.value.length === 1 && pageNo.value > 1) {
|
||||||
@@ -391,14 +449,16 @@ watch(difficultyFilter, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.taskId,
|
() => [props.active, props.taskId] as const,
|
||||||
() => {
|
([active]) => {
|
||||||
pageNo.value = 1;
|
pageNo.value = 1;
|
||||||
userFilter.value = [];
|
userFilter.value = [];
|
||||||
userFilterPopoverVisible.value = false;
|
userFilterPopoverVisible.value = false;
|
||||||
difficultyFilter.value = '';
|
difficultyFilter.value = '';
|
||||||
difficultyFilterPopoverVisible.value = false;
|
difficultyFilterPopoverVisible.value = false;
|
||||||
|
if (active) {
|
||||||
loadList();
|
loadList();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
@@ -694,6 +754,7 @@ watch(
|
|||||||
:task-owner-id="taskOwnerId"
|
:task-owner-id="taskOwnerId"
|
||||||
:task-status-code="taskStatusCode"
|
:task-status-code="taskStatusCode"
|
||||||
:default-owner-progress-rate="taskProgressRate"
|
:default-owner-progress-rate="taskProgressRate"
|
||||||
|
:attachment-directory="props.attachmentDirectory"
|
||||||
:confirm-loading="submitting"
|
:confirm-loading="submitting"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user