feat(performance、notify、weekly): 添加绩效考核功能、通知跳转链接、优化工作日志分割逻辑。

This commit is contained in:
dk
2026-07-07 16:51:34 +08:00
parent c1f710fbd8
commit a0dcd39c23
6 changed files with 330 additions and 34 deletions

View File

@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useDebounceFn, useInfiniteScroll } from '@vueuse/core'; import { useDebounceFn, useInfiniteScroll } from '@vueuse/core';
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
import { NOTIFY_MESSAGE_LEVEL_DICT_CODE } from '@/constants/dict'; import { NOTIFY_MESSAGE_LEVEL_DICT_CODE } from '@/constants/dict';
import { import {
fetchGetMyNotifyMessagePage, fetchGetMyNotifyMessagePage,
@@ -14,6 +16,7 @@ import { formatDateTime, formatRelativeTime } from '@/utils/datetime';
defineOptions({ name: 'NotificationBell' }); defineOptions({ name: 'NotificationBell' });
const dictStore = useDictStore(); const dictStore = useDictStore();
const router = useRouter();
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
const UNREAD_COUNT_POLL_INTERVAL = 15 * 1000; const UNREAD_COUNT_POLL_INTERVAL = 15 * 1000;
@@ -50,6 +53,12 @@ const searchKeyword = ref('');
const detailVisible = ref(false); const detailVisible = ref(false);
const detailMessage = ref<Api.NotifyMessage.NotifyMessage | null>(null); const detailMessage = ref<Api.NotifyMessage.NotifyMessage | null>(null);
interface NotifyRenderParts {
prefix: string;
clickable: string;
suffix: string;
}
function keywordParam() { function keywordParam() {
return searchKeyword.value.trim() || undefined; return searchKeyword.value.trim() || undefined;
} }
@@ -202,6 +211,89 @@ function openDetail(row: Api.NotifyMessage.NotifyMessage) {
} }
} }
function getClickableText(message: Api.NotifyMessage.NotifyMessage) {
const params = message.templateParams;
switch (message.templateCode) {
case 'execution_assigned':
return params?.executionName?.trim() || '';
case 'task_assigned':
return params?.taskName?.trim() || '';
case 'project_created':
return params?.projectName?.trim() || '';
default:
return '';
}
}
function canJump(message: Api.NotifyMessage.NotifyMessage) {
const params = message.templateParams;
return Boolean(params?.jumpType && params.projectId && getClickableText(message));
}
function renderNotifyParts(message: Api.NotifyMessage.NotifyMessage): NotifyRenderParts {
const content = message.templateContent || '';
const clickable = getClickableText(message);
if (!content || !clickable) {
return { prefix: content, clickable: '', suffix: '' };
}
const startIndex = content.indexOf(clickable);
if (startIndex < 0) {
return { prefix: content, clickable: '', suffix: '' };
}
return {
prefix: content.slice(0, startIndex),
clickable,
suffix: content.slice(startIndex + clickable.length)
};
}
async function handleNotifyJump(message: Api.NotifyMessage.NotifyMessage) {
const params = message.templateParams;
if (!params?.jumpType || !params.projectId) return;
const query: Record<string, string> = {
[OBJECT_CONTEXT_QUERY_KEY]: params.projectId
};
let path = '';
switch (params.jumpType) {
case 'project':
path = '/project/project/overview';
break;
case 'execution':
path = '/project/project/execution';
if (params.executionName) {
query.executionName = params.executionName;
}
break;
case 'task':
path = '/project/project/execution';
if (params.taskName) {
query.taskName = params.taskName;
}
break;
default:
return;
}
detailVisible.value = false;
drawerOpen.value = false;
await router.push({ path, query });
}
async function onNotifyLinkClick(row: Api.NotifyMessage.NotifyMessage) {
if (!canJump(row)) return;
if (!row.readStatus) {
await markRead(row);
}
await handleNotifyJump(row);
}
async function markAllRead() { async function markAllRead() {
const { error } = await fetchUpdateAllNotifyMessageRead(); const { error } = await fetchUpdateAllNotifyMessageRead();
if (error) return; if (error) return;
@@ -281,7 +373,25 @@ onBeforeUnmount(() => {
> >
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" /> <span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
<div class="notification-bell__row-body"> <div class="notification-bell__row-body">
<div class="notification-bell__row-title">{{ row.templateContent }}</div> <div class="notification-bell__row-title">
<template v-if="canJump(row)">
<template v-for="(part, index) in [renderNotifyParts(row)]" :key="`${row.id}-${index}`">
<span>{{ part.prefix }}</span>
<button
v-if="part.clickable"
type="button"
class="notification-bell__inline-link"
@click.stop="onNotifyLinkClick(row)"
>
{{ part.clickable }}
</button>
<span>{{ part.suffix }}</span>
</template>
</template>
<template v-else>
{{ row.templateContent }}
</template>
</div>
<div class="notification-bell__row-meta"> <div class="notification-bell__row-meta">
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round /> <DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span> <span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
@@ -312,7 +422,25 @@ onBeforeUnmount(() => {
> >
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" /> <span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
<div class="notification-bell__row-body"> <div class="notification-bell__row-body">
<div class="notification-bell__row-title">{{ row.templateContent }}</div> <div class="notification-bell__row-title">
<template v-if="canJump(row)">
<template v-for="(part, index) in [renderNotifyParts(row)]" :key="`${row.id}-${index}`">
<span>{{ part.prefix }}</span>
<button
v-if="part.clickable"
type="button"
class="notification-bell__inline-link"
@click.stop="onNotifyLinkClick(row)"
>
{{ part.clickable }}
</button>
<span>{{ part.suffix }}</span>
</template>
</template>
<template v-else>
{{ row.templateContent }}
</template>
</div>
<div class="notification-bell__row-meta"> <div class="notification-bell__row-meta">
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round /> <DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span> <span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
@@ -350,7 +478,25 @@ onBeforeUnmount(() => {
</div> </div>
</template> </template>
<div v-if="detailMessage" class="notification-bell__detail-body"> <div v-if="detailMessage" class="notification-bell__detail-body">
<div class="notification-bell__detail-content">{{ detailMessage.templateContent }}</div> <div class="notification-bell__detail-content">
<template v-if="canJump(detailMessage)">
<template v-for="(part, index) in [renderNotifyParts(detailMessage)]" :key="`detail-${index}`">
<span>{{ part.prefix }}</span>
<button
v-if="part.clickable"
type="button"
class="notification-bell__inline-link"
@click.stop="onNotifyLinkClick(detailMessage)"
>
{{ part.clickable }}
</button>
<span>{{ part.suffix }}</span>
</template>
</template>
<template v-else>
{{ detailMessage.templateContent }}
</template>
</div>
<div class="notification-bell__detail-time">收到于 {{ formatDateTime(detailMessage.createTime) }}</div> <div class="notification-bell__detail-time">收到于 {{ formatDateTime(detailMessage.createTime) }}</div>
</div> </div>
<template #footer> <template #footer>
@@ -578,6 +724,19 @@ onBeforeUnmount(() => {
font-weight: 500; font-weight: 500;
} }
.notification-bell__inline-link {
padding: 0;
border: 0;
background: transparent;
color: var(--el-color-primary);
cursor: pointer;
font: inherit;
}
.notification-bell__inline-link:hover {
text-decoration: underline;
}
.notification-bell__row-meta { .notification-bell__row-meta {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -5,6 +5,18 @@ declare namespace Api {
* backend api module: "notify-message"(站内信 · 我的收件箱) * backend api module: "notify-message"(站内信 · 我的收件箱)
*/ */
namespace NotifyMessage { namespace NotifyMessage {
type NotifyJumpType = 'project' | 'execution' | 'task';
interface NotifyMessageTemplateParams {
jumpType?: NotifyJumpType;
projectId?: string;
projectName?: string;
executionName?: string;
taskName?: string;
roleName?: string;
[key: string]: unknown;
}
interface PageParams { interface PageParams {
pageNo: number; pageNo: number;
pageSize: number; pageSize: number;
@@ -19,6 +31,8 @@ declare namespace Api {
interface NotifyMessage { interface NotifyMessage {
/** 站内信编号(雪花 Long按 string 接收) */ /** 站内信编号(雪花 Long按 string 接收) */
id: string; id: string;
/** 模板编码 */
templateCode: string;
/** 发送人名称(模板配置的发件人显示名) */ /** 发送人名称(模板配置的发件人显示名) */
templateNickname: string; templateNickname: string;
/** 最终消息正文(占位符已渲染,直接展示) */ /** 最终消息正文(占位符已渲染,直接展示) */
@@ -33,6 +47,8 @@ declare namespace Api {
readTime: string | number | null; readTime: string | number | null;
/** 收到时间 */ /** 收到时间 */
createTime: string | number; createTime: string | number;
/** 模板参数(部分模板支持跳转) */
templateParams?: NotifyMessageTemplateParams;
} }
/** 我的站内信分页查询参数 */ /** 我的站内信分页查询参数 */

View File

@@ -27,19 +27,25 @@ interface Props {
mode: 'view' | 'edit' | 'create'; mode: 'view' | 'edit' | 'create';
subordinateOptions?: SubordinateOption[]; subordinateOptions?: SubordinateOption[];
showApprovalFooter?: boolean; showApprovalFooter?: boolean;
initialEmployeeId?: string;
initialPeriodMonth?: string;
hideDraftAction?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
rowData: null, rowData: null,
subordinateOptions: () => [], subordinateOptions: () => [],
showApprovalFooter: false showApprovalFooter: false,
initialEmployeeId: '',
initialPeriodMonth: '',
hideDraftAction: false
}); });
const visible = defineModel<boolean>('visible', { default: false }); const visible = defineModel<boolean>('visible', { default: false });
const emit = defineEmits<{ const emit = defineEmits<{
saved: []; saved: [payload: PerformanceSheetSavePayload];
savedAndSent: []; savedAndSent: [payload: PerformanceSheetSavePayload];
startApproval: []; startApproval: [];
}>(); }>();
@@ -60,6 +66,13 @@ const createForm = reactive({
employeeId: '' employeeId: ''
}); });
interface PerformanceSheetSavePayload {
sheet: Api.Performance.Sheet.Sheet;
actualScoreTotal: string;
baseScoreTotal: string;
extraScoreTotal: string;
}
const createFormRules = computed<FormRules>(() => ({ const createFormRules = computed<FormRules>(() => ({
periodMonth: [createRequiredRule('请选择绩效月份')], periodMonth: [createRequiredRule('请选择绩效月份')],
employeeId: [createRequiredRule('请选择下属')] employeeId: [createRequiredRule('请选择下属')]
@@ -526,7 +539,16 @@ async function ensureCreatedSheet() {
return sheetResult.data; return sheetResult.data;
} }
async function executeSave(): Promise<Api.Performance.Sheet.Sheet | null> { function createInitialPeriodMonth() {
return props.initialPeriodMonth || createDefaultPeriodMonth();
}
function initializeCreateForm() {
createForm.periodMonth = createInitialPeriodMonth();
createForm.employeeId = props.initialEmployeeId || '';
}
async function executeSave(): Promise<PerformanceSheetSavePayload | null> {
const workbook = getActiveWorkbook(); const workbook = getActiveWorkbook();
if (!workbook) return null; if (!workbook) return null;
@@ -573,18 +595,23 @@ async function executeSave(): Promise<Api.Performance.Sheet.Sheet | null> {
return null; return null;
} }
return sheet; return {
sheet,
actualScoreTotal: scores.actualScoreTotal,
baseScoreTotal: scores.baseScoreTotal,
extraScoreTotal: scores.extraScoreTotal
};
} }
async function handleSaveDraft() { async function handleSaveDraft() {
saving.value = true; saving.value = true;
try { try {
const sheet = await executeSave(); const saveResult = await executeSave();
if (!sheet) return; if (!saveResult) return;
window.$message?.success(isCreateMode.value ? '绩效表已保存为草稿' : '绩效 Excel 已保存'); window.$message?.success(isCreateMode.value ? '绩效表已保存为草稿' : '绩效 Excel 已保存');
visible.value = false; visible.value = false;
emit('saved'); emit('saved', saveResult);
} catch (error) { } catch (error) {
window.$message?.error(error instanceof Error ? error.message : '绩效 Excel 保存失败'); window.$message?.error(error instanceof Error ? error.message : '绩效 Excel 保存失败');
} finally { } finally {
@@ -595,15 +622,15 @@ async function handleSaveDraft() {
async function handleSaveAndSend() { async function handleSaveAndSend() {
sending.value = true; sending.value = true;
try { try {
const sheet = await executeSave(); const saveResult = await executeSave();
if (!sheet) return; if (!saveResult) return;
const sendResult = await sendPerformanceSheet(sheet.id); const sendResult = await sendPerformanceSheet(saveResult.sheet.id);
if (sendResult.error) return; if (sendResult.error) return;
window.$message?.success('绩效表已保存并发送'); window.$message?.success('绩效表已保存并发送');
visible.value = false; visible.value = false;
emit('savedAndSent'); emit('savedAndSent', saveResult);
} catch (error) { } catch (error) {
window.$message?.error(error instanceof Error ? error.message : '绩效表发送失败'); window.$message?.error(error instanceof Error ? error.message : '绩效表发送失败');
} finally { } finally {
@@ -616,12 +643,14 @@ watch(visible, async isVisible => {
disposeUniver(); disposeUniver();
currentSheet.value = null; currentSheet.value = null;
currentTemplate.value = null; currentTemplate.value = null;
// 重置创建表单 initializeCreateForm();
createForm.periodMonth = createDefaultPeriodMonth();
createForm.employeeId = '';
return; return;
} }
if (isCreateMode.value) {
initializeCreateForm();
}
await nextTick(); await nextTick();
await loadWorkbook(); await loadWorkbook();
}); });
@@ -698,7 +727,9 @@ onMounted(() => {
<template v-if="canSave || showApprovalFooter" #footer> <template v-if="canSave || showApprovalFooter" #footer>
<div class="performance-excel-editor__footer"> <div class="performance-excel-editor__footer">
<template v-if="canSave"> <template v-if="canSave">
<ElButton :loading="saving" :disabled="sending" @click="handleSaveDraft">保存草稿</ElButton> <ElButton v-if="!props.hideDraftAction" :loading="saving" :disabled="sending" @click="handleSaveDraft">
保存草稿
</ElButton>
<ElButton type="primary" :loading="sending" :disabled="saving" @click="handleSaveAndSend">发送绩效</ElButton> <ElButton type="primary" :loading="sending" :disabled="saving" @click="handleSaveAndSend">发送绩效</ElButton>
</template> </template>
<template v-else-if="showApprovalFooter"> <template v-else-if="showApprovalFooter">

View File

@@ -4,9 +4,11 @@ import { computed, nextTick, reactive, ref, watch } from 'vue';
import type { FormRules } from 'element-plus'; import type { FormRules } from 'element-plus';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict'; import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchPerformanceSheetPage } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form'; import { useForm, useFormRules } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict'; import { useDict } from '@/hooks/business/dict';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue'; import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import PerformanceExcelEditorDrawer from '../../../my-performance/modules/performance-excel-editor-drawer.vue';
import { import {
type WorkReportStructuredSection, type WorkReportStructuredSection,
type WorkReportStructuredTask, type WorkReportStructuredTask,
@@ -73,6 +75,10 @@ const EMPTY_TEXT = '';
const { getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE); const { getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE);
const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE); const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
const todayText = computed(() => dayjs().format('YYYY-MM-DD')); const todayText = computed(() => dayjs().format('YYYY-MM-DD'));
const performanceDrawerVisible = ref(false);
const performanceDrawerLoading = ref(false);
const performanceDrawerMode = ref<'create' | 'edit'>('create');
const performanceDrawerRow = ref<Api.Performance.Sheet.Sheet | null>(null);
const auditDialogVisible = ref(false); const auditDialogVisible = ref(false);
const auditForm = reactive({ const auditForm = reactive({
@@ -90,7 +96,6 @@ const pageValidationModel = reactive({
} }
}); });
const pageRules: FormRules = { const pageRules: FormRules = {
meetingDate: [createRequiredRule('请选择面谈时间')],
performanceResult: [ performanceResult: [
createRequiredRule('请输入绩效考核结果'), createRequiredRule('请输入绩效考核结果'),
{ {
@@ -177,6 +182,24 @@ const performanceForm = reactive({
} }
}); });
const performanceTargetOption = computed(() => {
const reporterId = props.baseInfo?.reporterId || '';
const reporterName = props.baseInfo?.reporterName || '';
if (!reporterId || !reporterName) return [];
return [{ label: reporterName, value: reporterId }];
});
const performanceInitialEmployeeId = computed(() => props.baseInfo?.reporterId || '');
const performanceInitialPeriodMonth = computed(() => {
const periodDate = props.baseInfo?.periodStartDate || props.baseInfo?.periodEndDate || '';
if (!periodDate) return '';
const targetMonth = dayjs(periodDate);
return targetMonth.isValid() ? targetMonth.format('YYYY-MM') : '';
});
/** 绩效分数输入限制0-100最多一位小数 */ /** 绩效分数输入限制0-100最多一位小数 */
function handlePerformanceScoreInput(value: string) { function handlePerformanceScoreInput(value: string) {
// 只允许数字和一个小数点 // 只允许数字和一个小数点
@@ -563,6 +586,37 @@ function openAuditDialog() {
}); });
} }
async function openPerformanceDrawer() {
const reporterId = props.baseInfo?.reporterId || '';
const periodMonth = performanceInitialPeriodMonth.value;
performanceDrawerMode.value = 'create';
performanceDrawerRow.value = null;
if (reporterId && periodMonth) {
performanceDrawerLoading.value = true;
const { error, data } = await fetchPerformanceSheetPage({
pageNo: 1,
pageSize: 1,
employeeId: reporterId,
periodMonthRange: [periodMonth, periodMonth]
});
performanceDrawerLoading.value = false;
const existingSheet = !error && data?.list?.length ? data.list[0] : null;
if (existingSheet) {
performanceDrawerMode.value = 'edit';
performanceDrawerRow.value = existingSheet;
}
}
performanceDrawerVisible.value = true;
}
function handlePerformanceSaved(payload: { actualScoreTotal: string }) {
patchApproval('performanceResult', payload.actualScoreTotal);
}
watch(rejectOpinionRequired, async () => { watch(rejectOpinionRequired, async () => {
if (!auditDialogVisible.value) return; if (!auditDialogVisible.value) return;
@@ -614,10 +668,7 @@ watch(
<ElInput v-model="mainForm.supervisor" disabled /> <ElInput v-model="mainForm.supervisor" disabled />
</div> </div>
<div class="field field-form-item"> <div class="field field-form-item">
<label> <label>面谈时间</label>
面谈时间
<span class="field-required-mark">*</span>
</label>
<ElFormItem class="field-inline-form-item" prop="meetingDate"> <ElFormItem class="field-inline-form-item" prop="meetingDate">
<ElDatePicker v-model="mainForm.meetingDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" /> <ElDatePicker v-model="mainForm.meetingDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
</ElFormItem> </ElFormItem>
@@ -730,12 +781,25 @@ watch(
<span class="field-required-mark">*</span> <span class="field-required-mark">*</span>
</div> </div>
<ElFormItem class="performance-inline-form-item" prop="performanceResult"> <ElFormItem class="performance-inline-form-item" prop="performanceResult">
<ElInput <div class="performance-input-group">
v-model="performanceForm.score" <ElInput
class="performance-input" v-model="performanceForm.score"
placeholder="请输入考核分数" class="performance-input"
@input="handlePerformanceScoreInput" placeholder="请输入考核分数"
/> readonly
@input="handlePerformanceScoreInput"
/>
<ElButton
plain
size="small"
type="primary"
:loading="performanceDrawerLoading"
:disabled="!performanceTargetOption.length"
@click="openPerformanceDrawer"
>
填写绩效
</ElButton>
</div>
</ElFormItem> </ElFormItem>
</div> </div>
</div> </div>
@@ -871,6 +935,18 @@ watch(
</ElForm> </ElForm>
</div> </div>
</BusinessFormDialog> </BusinessFormDialog>
<PerformanceExcelEditorDrawer
v-model:visible="performanceDrawerVisible"
:row-data="performanceDrawerRow"
:mode="performanceDrawerMode"
:subordinate-options="performanceTargetOption"
:initial-employee-id="performanceInitialEmployeeId"
:initial-period-month="performanceInitialPeriodMonth"
hide-draft-action
@saved="handlePerformanceSaved"
@saved-and-sent="handlePerformanceSaved"
/>
</div> </div>
</template> </template>
@@ -1794,13 +1870,19 @@ watch(
flex: 0 0 auto; flex: 0 0 auto;
} }
.performance-input-group {
display: inline-flex;
align-items: center;
gap: 12px;
}
.performance-input { .performance-input {
width: 200px; width: 200px;
} }
.performance-input :deep(.el-input__wrapper) { .performance-input :deep(.el-input__wrapper) {
box-shadow: none !important; box-shadow: none !important;
background: transparent; background: #f8fafc;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
border-radius: 0; border-radius: 0;
padding: 0 4px; padding: 0 4px;
@@ -1812,6 +1894,10 @@ watch(
text-align: center; text-align: center;
} }
.performance-input :deep(.el-input__inner[readonly]) {
cursor: default;
}
.performance-input :deep(.el-input__inner::placeholder) { .performance-input :deep(.el-input__inner::placeholder) {
color: #cbd5e1; color: #cbd5e1;
font-weight: 400; font-weight: 400;

View File

@@ -359,6 +359,8 @@ function resolveTaskItemTypeLabel(value?: string | null) {
} }
const STRUCTURED_TASK_PREFIX_RE = /^(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、.]))\s*/u; const STRUCTURED_TASK_PREFIX_RE = /^(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、.]))\s*/u;
const WORKLOG_DAY_SEPARATOR = '[[WR_DAY_SPLIT]]';
const LEGACY_WORKLOG_DAY_SEPARATOR = '';
function stripStructuredTaskPrefixV2(value: string) { function stripStructuredTaskPrefixV2(value: string) {
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, ''); return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
@@ -385,9 +387,10 @@ function formatStructuredTaskDisplayLine(task: StructuredTask, index: number, sh
function getWorkLogEntries(detail: string): string[] { function getWorkLogEntries(detail: string): string[] {
if (!detail) return []; if (!detail) return [];
// 仅按中文分号切分,避免误伤文本中的其他标点;每段作为单独一天的工作日志展示 // 新默认稿优先按显式分隔标记切分;历史数据继续兼容中文分号
const separator = detail.includes(WORKLOG_DAY_SEPARATOR) ? WORKLOG_DAY_SEPARATOR : LEGACY_WORKLOG_DAY_SEPARATOR;
return detail return detail
.split('') .split(separator)
.map(item => item.trim()) .map(item => item.trim())
.filter(Boolean); .filter(Boolean);
} }

View File

@@ -825,7 +825,8 @@ watch(
() => props.execution?.id, () => props.execution?.id,
async () => { async () => {
await loadExecutionAssigneeOptions(); await loadExecutionAssigneeOptions();
} },
{ immediate: true }
); );
// 范围维度变化(scopedExecutionIds / scopedExecutionInvolveUserId / scopedExecutionStatusCodes 引用变: // 范围维度变化(scopedExecutionIds / scopedExecutionInvolveUserId / scopedExecutionStatusCodes 引用变: