feat(personal-center): 重构个人事项详情并复用任务工作日志组件
This commit is contained in:
@@ -1,8 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { fetchGetPersonalItemDetail } from '@/service/api';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
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 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' });
|
||||
|
||||
@@ -27,6 +43,49 @@ const visible = defineModel<boolean>('visible', {
|
||||
|
||||
const activeTab = ref<TabName>('worklog');
|
||||
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() {
|
||||
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() {
|
||||
await refreshDetail();
|
||||
await promptCompleteItemIfNeeded();
|
||||
|
||||
if (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(
|
||||
() => visible.value,
|
||||
value => {
|
||||
@@ -92,12 +201,66 @@ watch(
|
||||
>
|
||||
<ElTabs v-model="activeTab" class="personal-item-detail-dialog__tabs">
|
||||
<ElTabPane label="工作日志" name="worklog" lazy>
|
||||
<PersonalItemWorklogPanel
|
||||
v-if="detailData"
|
||||
:item="detailData"
|
||||
:active="activeTab === 'worklog' && visible"
|
||||
@changed="handleWorklogChanged"
|
||||
/>
|
||||
<div v-if="detailData" class="personal-item-worklog-content">
|
||||
<div class="personal-item-worklog-content__cards">
|
||||
<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"
|
||||
: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"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</BusinessFormDialog>
|
||||
@@ -112,4 +275,51 @@ watch(
|
||||
.personal-item-detail-dialog__tabs :deep(.el-tab-pane) {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user