feat(日志管理): 开发日志管理功能。

fix(项目任务): 1、任务完成后需要依然能够修改工作日志,但是只能修改工作内容和上传附件。2、任务完成后,协办人的工作日志不应该能删除、所有任务里的成员不能新增工作日志,前端不显示新增、删除按钮。3、团队成员的面板,在成员排序时,让有下属的成员提前。4、在任务弹出框有个快速用执行的信息填充的icon。
This commit is contained in:
dk
2026-06-25 21:34:23 +08:00
parent ea6a816d58
commit 570f284230
35 changed files with 2434 additions and 72 deletions

View File

@@ -0,0 +1,137 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import BusinessFormSection from '@/components/custom/business-form-section.vue';
import DictText from '@/components/custom/dict-text.vue';
import { type LogDetailField, type LogDetailSection, formatDateTime, formatMultilineText, formatText } from '../shared';
defineOptions({ name: 'LogDetailDialog' });
type DetailRecord = { id: string };
const visible = defineModel<boolean>('visible', { default: false });
const props = defineProps<{
title: string;
rowData?: { id: string } | null;
sections: LogDetailSection[];
fetchDetail: (id: string) => Promise<{
error: unknown;
data: DetailRecord | null;
}>;
}>();
const loading = ref(false);
const detail = ref<DetailRecord | null>(null);
watch(
() => [visible.value, props.rowData?.id] as const,
([isVisible]) => {
if (!isVisible) return;
loadDetail();
}
);
async function loadDetail() {
if (!props.rowData?.id) {
detail.value = null;
return;
}
loading.value = true;
const result = await props.fetchDetail(props.rowData.id);
loading.value = false;
detail.value = !result.error && result.data ? result.data : null;
}
function getFieldValue(field: LogDetailField) {
if (!detail.value || !field.key) return undefined;
return (detail.value as Record<string, unknown>)[field.key];
}
function getDictFieldValue(field: LogDetailField): string | number | null | undefined {
const value = getFieldValue(field);
if (value === null || value === undefined) {
return value;
}
return typeof value === 'string' || typeof value === 'number' ? value : String(value);
}
function getFieldText(field: LogDetailField) {
if (field.formatter && detail.value) {
return field.formatter(detail.value);
}
const value = getFieldValue(field);
if (field.type === 'datetime') {
return formatDateTime(value as string | null | undefined);
}
if (field.type === 'multiline') {
return formatMultilineText(value);
}
return formatText(value);
}
function getFieldSpan(field: LogDetailField) {
return field.span || (field.type === 'multiline' ? 2 : 1);
}
</script>
<template>
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
<div v-if="detail" class="log-detail-dialog">
<BusinessFormSection v-for="section in sections" :key="section.title" :title="section.title">
<ElDescriptions class="log-detail-dialog__descriptions" :column="2" border size="small">
<ElDescriptionsItem
v-for="field in section.fields"
:key="`${section.title}-${field.label}`"
:label="field.label"
label-class-name="log-detail-dialog__label"
:span="getFieldSpan(field)"
>
<DictText v-if="field.type === 'dict'" :dict-code="field.dictCode!" :value="getDictFieldValue(field)" />
<div v-else-if="field.type === 'multiline'" class="log-detail-dialog__multiline">
{{ getFieldText(field) }}
</div>
<span v-else>{{ getFieldText(field) }}</span>
</ElDescriptionsItem>
</ElDescriptions>
</BusinessFormSection>
</div>
<ElEmpty v-else description="暂无日志详情" />
</BusinessFormDialog>
</template>
<style scoped>
.log-detail-dialog {
min-width: 0;
}
:deep(.log-detail-dialog__descriptions .el-descriptions__cell) {
line-height: 1.7;
vertical-align: middle;
}
:deep(.log-detail-dialog__label) {
width: 120px;
min-width: 120px;
white-space: nowrap;
vertical-align: middle;
}
.log-detail-dialog__multiline {
padding: 10px 12px;
border-radius: 6px;
background: var(--el-fill-color-light);
color: var(--el-text-color-regular);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>