327 lines
10 KiB
Vue
327 lines
10 KiB
Vue
<script setup lang="ts">
|
|
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 TaskWorklogPanel from '@/views/project/project/execution/modules/task-worklog-panel.vue';
|
|
import {
|
|
formatPersonalItemDate,
|
|
formatPersonalItemOwnerName,
|
|
formatPersonalItemProgress,
|
|
getPersonalItemStatusLabel,
|
|
resolvePersonalItemStatusTagType
|
|
} from './personal-item-shared';
|
|
|
|
defineOptions({ name: 'PersonalItemDetailDialog' });
|
|
|
|
type TabName = 'worklog';
|
|
|
|
interface Props {
|
|
rowData?: Api.PersonalItem.PersonalItem | null;
|
|
defaultTab?: TabName;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
rowData: null,
|
|
defaultTab: 'worklog'
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
changed: [item: Api.PersonalItem.PersonalItem];
|
|
}>();
|
|
|
|
const visible = defineModel<boolean>('visible', {
|
|
default: false
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
async function refreshDetail() {
|
|
if (!detailData.value?.id) {
|
|
return;
|
|
}
|
|
|
|
const { error, data } = await fetchGetPersonalItemDetail(detailData.value.id);
|
|
|
|
if (!error && data) {
|
|
detailData.value = data;
|
|
}
|
|
}
|
|
|
|
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 => {
|
|
if (value) {
|
|
activeTab.value = props.defaultTab;
|
|
syncDetailFromPageRow();
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.rowData,
|
|
() => {
|
|
if (visible.value) {
|
|
syncDetailFromPageRow();
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.defaultTab,
|
|
value => {
|
|
if (visible.value) {
|
|
activeTab.value = value;
|
|
}
|
|
}
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<BusinessFormDialog
|
|
v-model="visible"
|
|
title="工作日志"
|
|
width="1100px"
|
|
max-body-height="78vh"
|
|
:show-footer="false"
|
|
:scrollbar="false"
|
|
>
|
|
<ElTabs v-model="activeTab" class="personal-item-detail-dialog__tabs">
|
|
<ElTabPane label="工作日志" name="worklog" lazy>
|
|
<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>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.personal-item-detail-dialog__tabs {
|
|
--el-tabs-header-height: 40px;
|
|
}
|
|
|
|
.personal-item-detail-dialog__tabs :deep(.el-tabs__content),
|
|
.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>
|