feat(personal-center): 将加班申请功能重构为业务学习功能。

fix(week-report): 修复周报中工作日志的显示问题。
feat(work-report): 点击编辑时,自动刷新出最新的内容。
This commit is contained in:
dk
2026-07-19 14:04:51 +08:00
parent f6cf03a34a
commit 7f31bed6f6
14 changed files with 106 additions and 64 deletions

View File

@@ -408,7 +408,7 @@
"name": "personal-center_overtime-application",
"path": "/personal-center/overtime-application",
"component": "view.personal-center_overtime-application",
"title": "加班申请",
"title": "业务学习",
"routeTitle": "personal-center_overtime-application",
"i18nKey": "route.personal-center_overtime-application",
"icon": "mdi:clock-plus-outline",
@@ -422,7 +422,7 @@
"redirect": null,
"props": null,
"meta": {
"title": "加班申请",
"title": "业务学习",
"i18nKey": "route.personal-center_overtime-application",
"icon": "mdi:clock-plus-outline",
"localIcon": null,

View File

@@ -113,9 +113,9 @@ export const RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE = 'rdms_req_can_delete_status'
export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_task_item_worklog_difficulty';
/**
* 加班时长快捷选项字典编码
* 学习时长快捷选项字典编码
*
* 对应业务字段:加班申请中的 overtimeDuration
* 对应业务字段:业务学习中的 overtimeDuration
* 来源口径:`overtime-application-design.md` 明确时长下拉字典为 rdms_overtime_duration
*/
export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';

View File

@@ -104,7 +104,7 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
completed: 'success',
cancelled: 'danger'
},
// 加班申请
// 业务学习
overtimeApplication: {
pending: 'warning',
approved: 'success',

View File

@@ -175,7 +175,7 @@ const local: App.I18n.Schema = {
'personal-center_work-report_project': '项目半月报',
'personal-center_my-performance': '我的绩效',
'personal-center_my-application': '我的申请',
'personal-center_overtime-application': '加班申请',
'personal-center_overtime-application': '业务学习',
'personal-center_pending-approval': '待我审批',
feedback: '意见反馈',
infra: '基础设施',

View File

@@ -180,7 +180,7 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination, r
{ prop: 'index', type: 'index', label: '序号', width: 64 },
{
prop: 'overtimeDate',
label: '加班日期',
label: '学习日期',
width: 120,
formatter: row => formatOvertimeDate(row.overtimeDate)
}
@@ -191,17 +191,17 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination, r
}
cols.push(
{ prop: 'overtimeDuration', label: '加班时长', width: 110, showOverflowTooltip: true },
{ prop: 'overtimeDuration', label: '学习时长', width: 110, showOverflowTooltip: true },
{
prop: 'overtimeReason',
label: '加班原因',
label: '学习原因',
minWidth: 180,
className: 'overtime-application__cell-ellipsis',
formatter: row => formatEmptyText(row.overtimeReason)
},
{
prop: 'overtimeContent',
label: '加班内容',
label: '学习内容',
minWidth: 200,
className: 'overtime-application__cell-ellipsis',
formatter: row => formatEmptyText(row.overtimeContent)
@@ -357,7 +357,7 @@ async function handleExport() {
return;
}
downloadBlob(blob, `加班申请_${dayjs().format('YYYY-MM-DD')}.xls`);
downloadBlob(blob, `业务学习_${dayjs().format('YYYY-MM-DD')}.xls`);
}
async function loadSubordinateTree() {
@@ -485,7 +485,7 @@ watch(
<template #header>
<div class="flex flex-wrap items-center justify-between gap-12px">
<div class="flex items-center gap-10px">
<p class="text-16px font-600">加班申请</p>
<p class="text-16px font-600">业务学习</p>
<ElTag effect="plain">{{ totalCount }}</ElTag>
</div>
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadTable">
@@ -592,7 +592,7 @@ watch(
white-space: nowrap;
}
/* 加班原因/加班内容:单元格内容溢出时仅显示省略号,不弹出 tooltip */
/* 学习原因/学习内容:单元格内容溢出时仅显示省略号,不弹出 tooltip */
:deep(.overtime-application__cell-ellipsis .cell) {
display: block;
overflow: hidden;

View File

@@ -49,7 +49,7 @@ watch(
<template>
<BusinessFormDialog
v-model="visible"
title="加班申请审批记录"
title="业务学习审批记录"
width="820px"
:loading="loading"
:show-footer="false"

View File

@@ -12,9 +12,9 @@ defineOptions({ name: 'OvertimeApplicationBatchDetailDialog' });
type ActionType = 'approve' | 'reject';
interface Props {
/** 选中的加班申请 id 列表(原始 id */
/** 选中的业务学习 id 列表(原始 id */
selectedIds: string[];
/** 全部加班申请行数据,用于通过 id 查找 */
/** 全部业务学习行数据,用于通过 id 查找 */
rows: Api.OvertimeApplication.OvertimeApplication[];
actionLoading?: boolean;
}
@@ -163,23 +163,23 @@ watch(
<ElDescriptionsItem label="申请人" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.applicantName }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班日期" label-class-name="overtime-application-detail-dialog__label--compact">
<ElDescriptionsItem label="学习日期" label-class-name="overtime-application-detail-dialog__label--compact">
{{ formatOvertimeDate(detailData.overtimeDate) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班时长" label-class-name="overtime-application-detail-dialog__label">
<ElDescriptionsItem label="学习时长" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeDuration }}
</ElDescriptionsItem>
<ElDescriptionsItem label="提交时间" label-class-name="overtime-application-detail-dialog__label--compact">
{{ formatOvertimeDateTime(detailData.submitTime) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
<ElDescriptionsItem label="学习原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeReason }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
<ElDescriptionsItem label="学习内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeContent }}
</ElDescriptionsItem>
</ElDescriptions>
<ElEmpty v-else description="未获取到加班申请详情" />
<ElEmpty v-else description="未获取到业务学习详情" />
<div class="batch-detail__approval-form">
<div class="audit-field">

View File

@@ -114,28 +114,28 @@ watch(
</script>
<template>
<BusinessFormDialog v-model="visible" title="加班申请详情" preset="md" :loading="loading">
<BusinessFormDialog v-model="visible" title="业务学习详情" preset="md" :loading="loading">
<ElDescriptions v-if="detailData" class="overtime-application-detail-dialog__descriptions" :column="2" border>
<ElDescriptionsItem label="申请人" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.applicantName }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班日期" label-class-name="overtime-application-detail-dialog__label--compact">
<ElDescriptionsItem label="学习日期" label-class-name="overtime-application-detail-dialog__label--compact">
{{ formatOvertimeDate(detailData.overtimeDate) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班时长" label-class-name="overtime-application-detail-dialog__label">
<ElDescriptionsItem label="学习时长" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeDuration }}
</ElDescriptionsItem>
<ElDescriptionsItem label="提交时间" label-class-name="overtime-application-detail-dialog__label--compact">
{{ formatOvertimeDateTime(detailData.submitTime) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
<ElDescriptionsItem label="学习原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeReason }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
<ElDescriptionsItem label="学习内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeContent }}
</ElDescriptionsItem>
</ElDescriptions>
<ElEmpty v-else description="未获取到加班申请详情" />
<ElEmpty v-else description="未获取到业务学习详情" />
<div v-if="props.showApprovalActions" class="overtime-application-detail-dialog__approval-form">
<div class="audit-field">

View File

@@ -43,21 +43,21 @@ const approverName = ref('');
const currentUserName = computed(() => authStore.userInfo.nickname || authStore.userInfo.userName || '--');
const isEdit = computed(() => props.operateType === 'edit');
const title = computed(() => (isEdit.value ? '修改并重新提交' : '新增加班申请'));
const title = computed(() => (isEdit.value ? '修改并重新提交' : '新增业务学习'));
const model = reactive<Api.OvertimeApplication.SaveOvertimeApplicationParams>(createDefaultModel());
const rules = computed(
() =>
({
overtimeDate: [createRequiredRule('请选择加班日期')],
overtimeDuration: [createRequiredRule('请选择加班时长')],
overtimeDate: [createRequiredRule('请选择学习日期')],
overtimeDuration: [createRequiredRule('请选择学习时长')],
overtimeReason: [
createRequiredRule('请输入加班原因'),
createRequiredRule('请输入学习原因'),
{
validator: (_rule, value: string, callback) => {
if (!value?.trim()) {
callback(new Error('请输入加班原因'));
callback(new Error('请输入学习原因'));
return;
}
@@ -67,11 +67,11 @@ const rules = computed(
}
],
overtimeContent: [
createRequiredRule('请输入加班内容'),
createRequiredRule('请输入学习内容'),
{
validator: (_rule, value: string, callback) => {
if (!value?.trim()) {
callback(new Error('请输入加班内容'));
callback(new Error('请输入学习内容'));
return;
}
@@ -152,7 +152,7 @@ async function handleSubmit() {
return;
}
window.$message?.success(isEdit.value ? '加班申请已重新提交' : '加班申请已提交');
window.$message?.success(isEdit.value ? '业务学习已重新提交' : '业务学习已提交');
visible.value = false;
emit('submitted');
}
@@ -199,52 +199,52 @@ watch(
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="加班日期" prop="overtimeDate" style="width: 100%">
<ElFormItem label="学习日期" prop="overtimeDate" style="width: 100%">
<ElDatePicker
v-model="model.overtimeDate"
class="w-full"
type="date"
value-format="YYYY-MM-DD"
style="width: 100%"
placeholder="请选择加班日期"
placeholder="请选择学习日期"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="加班时长" prop="overtimeDuration">
<ElFormItem label="学习时长" prop="overtimeDuration">
<DictSelect
v-model="model.overtimeDuration"
:dict-code="RDMS_OVERTIME_DURATION_DICT_CODE"
placeholder="请选择加班时长"
placeholder="请选择学习时长"
/>
</ElFormItem>
</ElCol>
</ElRow>
</BusinessFormSection>
<BusinessFormSection title="加班说明">
<BusinessFormSection title="学习说明">
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem label="加班原因" prop="overtimeReason">
<ElFormItem label="学习原因" prop="overtimeReason">
<ElInput
v-model="model.overtimeReason"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入加班原因"
placeholder="请输入学习原因"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="加班内容" prop="overtimeContent">
<ElFormItem label="学习内容" prop="overtimeContent">
<ElInput
v-model="model.overtimeContent"
type="textarea"
:rows="4"
maxlength="1000"
show-word-limit
placeholder="请输入加班内容"
placeholder="请输入学习内容"
/>
</ElFormItem>
</ElCol>

View File

@@ -105,9 +105,9 @@ const fields = computed<SearchField[]>(() => {
: []),
{
key: 'overtimeDate',
label: '加班日期',
label: '学习日期',
type: 'dateRange',
placeholder: '请选择加班日期'
placeholder: '请选择学习日期'
},
{
key: 'statusCode',

View File

@@ -388,7 +388,7 @@ async function pullDefaultDraft(confirmOverwrite = false) {
}
}
async function refreshDraft() {
async function refreshDraft(showSuccess = true) {
if (props.mode !== 'edit') {
await pullDefaultDraft(true);
return;
@@ -433,7 +433,9 @@ async function refreshDraft() {
applyProjectEditableFields(result.data as Api.WorkReport.Project.ProjectReport);
}
window.$message?.success('最新数据已刷新');
if (showSuccess) {
window.$message?.success('最新数据已刷新');
}
}
async function loadInitData() {
@@ -479,6 +481,9 @@ watch(visible, async isVisible => {
if (props.rowData?.id) {
await loadDetail(props.rowData.id);
if (props.mode === 'edit') {
await refreshDraft(false);
}
await fillMonthlyPerformanceResult();
}
});

View File

@@ -100,7 +100,7 @@ const activeEditField = ref('');
const travelDialogVisible = ref(false);
const isReadonly = computed(() => props.mode === 'detail' || props.scene === 'approval');
const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
const { getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE);
const { dictData: taskItemTypeDictData, getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE);
const DEFAULT_SECTION_CATEGORY = '工作内容';
const TRAVEL_SECTION_CATEGORY = '差旅';
@@ -613,6 +613,41 @@ function createStructuredSectionsFromTextV2(
const legacyTaskLineRe =
/^(?!(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、.]))\s*)(.+?)\s*[-]\s*(.+)$/u;
const isKnownSectionCategory = (category: string) => {
const normalizedCategory = category.trim();
if (!normalizedCategory) return false;
if ([DEFAULT_SECTION_CATEGORY, TRAVEL_SECTION_CATEGORY, MY_AFFAIRS_TITLE].includes(normalizedCategory)) return true;
return taskItemTypeDictData.value.some(item => item.label === normalizedCategory);
};
const isLegacyStructuredTaskLine = (line: string, strict = false) => {
const legacyMatch = line.match(legacyTaskLineRe);
if (!legacyMatch) return false;
const [, rawCategory, rawTaskText] = legacyMatch;
const category = rawCategory.trim();
const normalizedTaskText = rawTaskText.trim();
if (!category || !normalizedTaskText) return false;
const { rawTitle, metricsText, detail } = extractStructuredTaskParts(normalizedTaskText);
const title = stripStructuredTaskSuffix(rawTitle);
if (!title) return false;
const hasTaskStructureHint =
Boolean(detail) ||
isStructuredMetricsText(metricsText) ||
normalizedTaskText.includes('') ||
normalizedTaskText.includes('(') ||
normalizedTaskText.includes('') ||
normalizedTaskText.includes(':');
if (isKnownSectionCategory(category) || hasTaskStructureHint) return true;
if (strict) return false;
return category.length <= 12;
};
const isExplicitStructuredTaskLine = (line: string) => {
if (!STRUCTURED_TASK_PREFIX_RE.test(line)) return false;
const normalizedLine = stripStructuredTaskPrefix(line);
@@ -623,7 +658,7 @@ function createStructuredSectionsFromTextV2(
const shouldAppendToPreviousTaskDetail = (line: string) => {
if (!previousTask?.detail) return false;
if (line.startsWith('#')) return false;
if (legacyTaskLineRe.test(line)) return false;
if (isLegacyStructuredTaskLine(line, true)) return false;
if (isExplicitStructuredTaskLine(line)) return false;
return true;
};
@@ -641,7 +676,9 @@ function createStructuredSectionsFromTextV2(
// 仅当行首不是结构化任务前缀(如 "3、")时,才按旧式 "<分类> - <事项>" 解析;
// 否则会把 "2026-06-12 - 2026-06-19" 这种含 " - " 的出差行误判为分类。
const legacyMatch = trimmedLine.match(legacyTaskLineRe);
const legacyMatch = isLegacyStructuredTaskLine(trimmedLine, Boolean(previousTask?.detail))
? trimmedLine.match(legacyTaskLineRe)
: null;
if (legacyMatch) {
const [, rawCategory, rawTaskText] = legacyMatch;
const category = rawCategory.trim();

View File

@@ -255,11 +255,11 @@ function buildTeamBarOption(): ECOption {
if (!params.length) return '';
const name = params[0].axisValue as string;
const totalHours = Number(params.find((p: any) => p.seriesName === '总工时')?.value ?? 0);
const overtimeHours = Number(params.find((p: any) => p.seriesName === '加班工时')?.value ?? 0);
const overtimeHours = Number(params.find((p: any) => p.seriesName === '学习工时')?.value ?? 0);
return [
`<div style="font-weight:600;margin-bottom:4px">${name}</div>`,
`总工时:${totalHours}h`,
`加班工时:${overtimeHours}h`
`学习工时:${overtimeHours}h`
].join('<br/>');
}
},
@@ -288,7 +288,7 @@ function buildTeamBarOption(): ECOption {
}
},
{
name: '加班工时',
name: '学习工时',
type: 'bar',
barWidth: 16,
data: v.members.map(member => member.overtimeHours),

View File

@@ -145,7 +145,7 @@ const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
{ key: 'monthly', label: '月报' },
{ key: 'project', label: '项目半月报' },
{ key: 'performance', label: '我的绩效' },
{ key: 'overtime_application', label: '加班申请' }
{ key: 'overtime_application', label: '业务学习' }
];
const hasWorkReportApprovePermission = computed(() => hasButtonPermission('project:work-report:approve'));
@@ -224,7 +224,7 @@ const performanceActionVisible = ref(false);
const currentPerformanceSheet = ref<Api.Performance.Sheet.Sheet | null>(null);
const currentPerformanceActionType = ref<PerformanceApprovalActionType>('confirm');
// 批量审批选中状态(存原始加班申请 id避免映射转换
// 批量审批选中状态(存原始业务学习 id避免映射转换
const selectedOvertimeIds = ref<Set<string>>(new Set());
const APPROVAL_ENTRY_ICON = markRaw(IconMdiClipboardCheckOutline);
@@ -234,7 +234,7 @@ function getApprovalCategoryLabel(bizType: ApprovalBizType) {
if (bizType === 'monthly') return '月报';
if (bizType === 'project') return '项目半月报';
if (bizType === 'performance') return '我的绩效';
if (bizType === 'overtime_application') return '加班申请';
if (bizType === 'overtime_application') return '业务学习';
return '待审批';
}
@@ -786,7 +786,7 @@ async function handleOvertimeActionSubmit(payload: { actionType: OvertimeApprova
}
overtimeDetailVisible.value = false;
window.$message?.success(payload.actionType === 'approve' ? '加班申请已通过' : '加班申请已退回');
window.$message?.success(payload.actionType === 'approve' ? '业务学习已通过' : '业务学习已退回');
await loadOvertimeApprovalItems();
}
@@ -902,7 +902,7 @@ watch(activeApprovalBizType, () => {
clearOvertimeSelection();
});
// 当前页加班申请是否全部选中(用于全选复选框状态)
// 当前页业务学习是否全部选中(用于全选复选框状态)
const allOvertimeItemsSelected = computed(() => {
const items = overtimeApprovalItems.value;
if (items.length === 0) return false;
@@ -980,11 +980,11 @@ async function loadOvertimeApprovalItems() {
data.list.map(item => ({
id: `overtime-application-${item.id}`,
category: 'approval',
categoryLabel: '加班申请',
title: `${item.applicantName} · ${item.overtimeDate.slice(5, 7)}加班 ${item.overtimeDuration} 申请待审批`,
categoryLabel: '业务学习',
title: `${item.applicantName} · ${item.overtimeDate.slice(5, 7)}学习 ${item.overtimeDuration} 申请待审批`,
createdTime: item.submitTime || item.createTime,
deadline: item.submitTime || item.createTime,
source: `加班申请 · ${item.applicantName}`,
source: `业务学习 · ${item.applicantName}`,
priority: 'mid',
approvalBizType: 'overtime_application',
approvalBizId: item.id
@@ -1198,7 +1198,7 @@ onActivated(refresh);
</div>
<div class="workbench-todo__content">
<!-- 批量操作栏加班申请待审批时显示 -->
<!-- 批量操作栏业务学习待审批时显示 -->
<div
v-if="
activeTab === 'approval' &&
@@ -1239,7 +1239,7 @@ onActivated(refresh);
}"
>
<div class="workbench-todo__leading">
<!-- 加班申请待审批时显示复选框 -->
<!-- 业务学习待审批时显示复选框 -->
<ElCheckbox
v-if="
activeTab === 'approval' &&