fix(产品管理、项目管理、登录密码校验、工作报告): 修复用户们提出的一系列问题。

This commit is contained in:
dk
2026-06-23 17:59:42 +08:00
parent 10418fea0a
commit b26a9c8a39
14 changed files with 157 additions and 50 deletions

View File

@@ -31,7 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
const model = defineModel<Api.Project.AttachmentItem[]>({ default: () => [] }); const model = defineModel<Api.Project.AttachmentItem[]>({ default: () => [] });
/** 给用户看的简短分类hint 行展示) */ /** 给用户看的简短分类hint 行展示) */
const ALLOWED_EXTENSIONS_HINT = '支持 PDF、Word、Excel、PPT、TXT/MD/CSV、图片、ZIP/RAR/7Z、MP3/MP4'; const ALLOWED_EXTENSIONS_HINT = '支持 PDF、Word、Excel、PPT、TXT/MD/CSV、图片、ZIP/RAR/7Z、MP3/MP4、SQL/JSON/XML';
// 与后端 AttachmentValidator 白/黑名单保持一致5.16 // 与后端 AttachmentValidator 白/黑名单保持一致5.16
const ALLOWED_EXTENSIONS = new Set([ const ALLOWED_EXTENSIONS = new Set([
@@ -55,7 +55,10 @@ const ALLOWED_EXTENSIONS = new Set([
'rar', 'rar',
'7z', '7z',
'mp4', 'mp4',
'mp3' 'mp3',
'sql',
'xml',
'json'
]); ]);
const FORBIDDEN_EXTENSIONS = new Set([ const FORBIDDEN_EXTENSIONS = new Set([

View File

@@ -661,7 +661,7 @@ const local: App.I18n.Schema = {
}, },
pwd: { pwd: {
required: '请输入密码', required: '请输入密码',
invalid: '密码格式不正确,6-18位字符,包含字母、数字、下划线' invalid: '密码格式不正确,4-30位字符,包含字母、数字、下划线'
}, },
confirmPwd: { confirmPwd: {
required: '请输入确认密码', required: '请输入确认密码',

View File

@@ -18,6 +18,10 @@ type ProductResponse = Omit<Api.Product.Product, 'id' | 'managerUserId' | 'curre
currentUserRoles?: Api.Common.CurrentUserRole[] | null; currentUserRoles?: Api.Common.CurrentUserRole[] | null;
}; };
type ProductOptionResponse = Omit<Api.Product.ProductOption, 'id'> & {
id: string | number;
};
type ProductPageResponse = Api.Product.PageResult<ProductResponse>; type ProductPageResponse = Api.Product.PageResult<ProductResponse>;
type ProductActivityTimelineItemResponse = Omit< type ProductActivityTimelineItemResponse = Omit<
@@ -46,6 +50,13 @@ function normalizeProduct(product: ProductResponse): Api.Product.Product {
}; };
} }
function normalizeProductOption(option: ProductOptionResponse): Api.Product.ProductOption {
return {
...option,
id: normalizeStringId(option.id)
};
}
function normalizeOccurredAt(occurredAt: number | string) { function normalizeOccurredAt(occurredAt: number | string) {
const value = Number(occurredAt); const value = Number(occurredAt);
@@ -109,6 +120,19 @@ export async function fetchGetProductPage(params?: Api.Product.ProductSearchPara
})); }));
} }
/** 获取可绑定产品下拉选项 */
export async function fetchGetProductOptions() {
const result = await request<ProductOptionResponse[]>({
...safeJsonRequestConfig,
url: `${PRODUCT_PREFIX}/options`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<ProductOptionResponse[]>, data =>
(data ?? []).map(normalizeProductOption)
);
}
type ProductOverviewSummaryResponse = Omit<Api.Product.ProductOverviewSummary, 'total' | 'items'> & { type ProductOverviewSummaryResponse = Omit<Api.Product.ProductOverviewSummary, 'total' | 'items'> & {
/** 后端 overview-summary 升级total/items灰度期间可能缺省适配层兜底 */ /** 后端 overview-summary 升级total/items灰度期间可能缺省适配层兜底 */
total?: number | null; total?: number | null;

View File

@@ -92,6 +92,17 @@ declare namespace Api {
lastStatusReason?: string | null; lastStatusReason?: string | null;
} }
interface ProductOption {
/** 产品 ID */
id: string;
/** 产品编码 */
code: string;
/** 产品名称 */
name: string;
/** 产品方向字典值 */
directionCode: string;
}
interface ProductLifecycleAction { interface ProductLifecycleAction {
actionCode: ProductStatusActionCode; actionCode: ProductStatusActionCode;
actionName: string; actionName: string;
@@ -216,6 +227,7 @@ declare namespace Api {
interface DeleteProductParams { interface DeleteProductParams {
id: string; id: string;
productName: string; productName: string;
confirmText: string;
reason: string; reason: string;
} }

View File

@@ -558,7 +558,9 @@ function resolveTaskMetrics(metricsText: string, fallback?: Partial<StructuredTa
.filter(Boolean); .filter(Boolean);
const priorityText = metricsText.match(/\bP\d+\b/iu)?.[0] || metricParts.find(item => /^P?\d+$/iu.test(item)); const priorityText = metricsText.match(/\bP\d+\b/iu)?.[0] || metricParts.find(item => /^P?\d+$/iu.test(item));
const priority = normalizePriorityCode(priorityText || (useFallback ? fallback?.priority : undefined)); const priority = normalizePriorityCode(priorityText || (useFallback ? fallback?.priority : undefined));
const progressText = metricsText.match(/进度\s*(\d+(?:\.\d+)?)%/u)?.[1]; const progressText =
metricsText.match(/进度\s*(\d+(?:\.\d+)?)%/u)?.[1] ||
metricParts.find(item => /^\d+(?:\.\d+)?%$/u.test(item))?.replace(/%$/u, '');
const hoursText = metricsText.match(/(\d+(?:\.\d+)?)h/u)?.[1]; const hoursText = metricsText.match(/(\d+(?:\.\d+)?)h/u)?.[1];
return { return {
@@ -591,7 +593,7 @@ function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): Rev
: escapeHtml(item.contentText || '').replace(/\n/g, '<br>') || EMPTY_TEXT, : escapeHtml(item.contentText || '').replace(/\n/g, '<br>') || EMPTY_TEXT,
contentSections, contentSections,
reflection: item.reflectionText || '', reflection: item.reflectionText || '',
removable: false, removable: true,
source: item source: item
}; };
} }
@@ -728,6 +730,13 @@ function removePlanItem(index: number) {
if (item?.sourceIndex !== undefined) props.model.planItems.splice(item.sourceIndex, 1); if (item?.sourceIndex !== undefined) props.model.planItems.splice(item.sourceIndex, 1);
} }
function removeReviewItem(index: number) {
const item = reviewItems.value[index];
if (!item?.source) return;
const sourceIndex = props.model.reviewItems.indexOf(item.source);
if (sourceIndex >= 0) props.model.reviewItems.splice(sourceIndex, 1);
}
function focusEditField(key: string) { function focusEditField(key: string) {
activeEditField.value = key; activeEditField.value = key;
} }
@@ -736,9 +745,19 @@ function blurEditField(key: string) {
if (activeEditField.value === key) activeEditField.value = ''; if (activeEditField.value === key) activeEditField.value = '';
} }
function isStructuredEditorUnchanged(text: string, sections: StructuredSection[] | undefined, showHours = false) {
if (!sections?.length) return false;
return normalizeEditorText(text) === createStructuredTextV2(sections, showHours);
}
function syncRichContent(item: ReviewItem, event: Event) { function syncRichContent(item: ReviewItem, event: Event) {
const target = event.currentTarget as HTMLElement; const target = event.currentTarget as HTMLElement;
if (!item.source) return; if (!item.source) return;
if (isStructuredEditorUnchanged(target.innerText, item.contentSections, true)) {
item.source.contentJson = createSectionsJson(item.contentSections || []);
item.source.contentText = createStructuredTextV2(item.contentSections || [], true);
return;
}
const sections = parseStructuredSectionsFromEditorV2( const sections = parseStructuredSectionsFromEditorV2(
target, target,
item.contentSections || [], item.contentSections || [],
@@ -751,6 +770,11 @@ function syncRichContent(item: ReviewItem, event: Event) {
function syncRichTarget(item: PlanItem, event: Event) { function syncRichTarget(item: PlanItem, event: Event) {
const target = event.currentTarget as HTMLElement; const target = event.currentTarget as HTMLElement;
if (!item.source) return; if (!item.source) return;
if (isStructuredEditorUnchanged(target.innerText, item.targetSections)) {
item.source.targetJson = createSectionsJson(item.targetSections || []);
item.source.targetText = createStructuredTextV2(item.targetSections || []);
return;
}
const sections = parseStructuredSectionsFromEditorV2( const sections = parseStructuredSectionsFromEditorV2(
target, target,
item.targetSections || [], item.targetSections || [],
@@ -836,7 +860,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
confirm-button-text="删除" confirm-button-text="删除"
cancel-button-text="取消" cancel-button-text="取消"
width="220" width="220"
@confirm="reviewItems.splice(index, 1)" @confirm="removeReviewItem(index)"
> >
<template #reference> <template #reference>
<button class="item-remove-btn" aria-label="删除回顾">×</button> <button class="item-remove-btn" aria-label="删除回顾">×</button>

View File

@@ -127,7 +127,7 @@ function toWorkItem(item: Api.WorkReport.Project.ProjectReportItem, index: numbe
} }
const currentWorks = computed<WorkItem[]>(() => { const currentWorks = computed<WorkItem[]>(() => {
return (props.model.currentItems || []).map((item, index) => toWorkItem(item, index, false)); return (props.model.currentItems || []).map((item, index) => toWorkItem(item, index, true));
}); });
const nextPlans = computed<WorkItem[]>(() => { const nextPlans = computed<WorkItem[]>(() => {
@@ -273,6 +273,11 @@ function removePlanItem(index: number) {
if (item?.sourceIndex !== undefined) props.model.nextItems.splice(item.sourceIndex, 1); if (item?.sourceIndex !== undefined) props.model.nextItems.splice(item.sourceIndex, 1);
} }
function removeCurrentWorkItem(index: number) {
const item = currentWorks.value[index];
if (item?.sourceIndex !== undefined) props.model.currentItems.splice(item.sourceIndex, 1);
}
function startTitleEdit(item: WorkItem) { function startTitleEdit(item: WorkItem) {
titleEditSnapshot.value[item.id] = item.title; titleEditSnapshot.value[item.id] = item.title;
} }
@@ -288,9 +293,9 @@ function notifyTitleSaved(item: WorkItem) {
item.title = nextTitle; item.title = nextTitle;
if (item.source) item.source.itemTitle = nextTitle; if (item.source) item.source.itemTitle = nextTitle;
delete titleEditSnapshot.value[item.id]; delete titleEditSnapshot.value[item.id];
if (nextTitle !== previousTitle) { // if (nextTitle !== previousTitle) {
ElMessage.success('名称已成功修改'); // ElMessage.success('名称已成功修改');
} // }
} }
</script> </script>
@@ -373,7 +378,7 @@ function notifyTitleSaved(item: WorkItem) {
<div class="review-grid layout-row"> <div class="review-grid layout-row">
<div v-if="!currentWorks.length">{{ EMPTY_TEXT }}</div> <div v-if="!currentWorks.length">{{ EMPTY_TEXT }}</div>
<div v-for="(item, index) in currentWorks" :key="item.id" class="review-card compact-work-card"> <div v-for="(item, index) in currentWorks" :key="item.id" class="review-card compact-work-card">
<div class="review-card-head"> <div class="review-card-head" :class="{ 'no-remove': item.removable === false }">
<span class="row-index">{{ index + 1 }}</span> <span class="row-index">{{ index + 1 }}</span>
<div class="review-title-readonly"> <div class="review-title-readonly">
<div class="work-title-line"> <div class="work-title-line">
@@ -390,6 +395,18 @@ function notifyTitleSaved(item: WorkItem) {
<span class="meta-chip"> {{ item.hours }}h</span> <span class="meta-chip"> {{ item.hours }}h</span>
</div> </div>
</div> </div>
<ElPopconfirm
v-if="item.removable !== false && !isReadonly"
title="确认删除这条工作内容吗?"
confirm-button-text="删除"
cancel-button-text="取消"
width="220"
@confirm="removeCurrentWorkItem(index)"
>
<template #reference>
<button class="item-remove-btn" aria-label="删除工作内容">×</button>
</template>
</ElPopconfirm>
</div> </div>
</div> </div>
</div> </div>
@@ -727,10 +744,7 @@ function notifyTitleSaved(item: WorkItem) {
align-items: center; align-items: center;
} }
.review-card-head { .review-card-head.no-remove,
grid-template-columns: 28px minmax(0, 1fr);
}
.plan-card-head.no-remove { .plan-card-head.no-remove {
grid-template-columns: 28px minmax(0, 1fr); grid-template-columns: 28px minmax(0, 1fr);
} }

View File

@@ -725,7 +725,7 @@ function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): Rev
contentSections, contentSections,
contentTasks, contentTasks,
reflection: item.reflectionText || '', reflection: item.reflectionText || '',
removable: false, removable: true,
source: item source: item
}; };
} }

View File

@@ -38,7 +38,7 @@ const DEFAULT_TOP_N = 5;
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
/** 产品行多列数(名称/编码/经理/我的角色/状态/原因/更新),非产品行整行合并用 */ /** 产品行多列数(名称/编码/经理/我的角色/状态/原因/更新),非产品行整行合并用 */
const COLUMN_COUNT = 7; const COLUMN_COUNT = 7;
/** 产品描述副行最大展示字符数,超出截断并追加 …(完整内容走 title 悬浮) */ /** 产品描述副行长度阈值:超过时展示「详情」入口 */
const PRODUCT_DESC_MAX_LEN = 48; const PRODUCT_DESC_MAX_LEN = 48;
interface DirectionGroup { interface DirectionGroup {
@@ -274,11 +274,9 @@ function formatDate(value?: string | null) {
return dayjs(value).format('YYYY-MM-DD'); return dayjs(value).format('YYYY-MM-DD');
} }
/** 产品描述副行:控长截断,超出追加 …;完整内容由 title 悬浮展示 */ /** 产品描述副行:不再手工截断字符,交给布局省略;完整内容由 title 悬浮展示 */
function truncateDesc(text?: string | null) { function truncateDesc(text?: string | null) {
const trimmed = (text ?? '').trim(); return (text ?? '').trim();
return trimmed.length > PRODUCT_DESC_MAX_LEN ? `${trimmed.slice(0, PRODUCT_DESC_MAX_LEN)}` : trimmed;
} }
/** 描述是否被截断(放不下);仅截断时才追加「详情」入口 */ /** 描述是否被截断(放不下);仅截断时才追加「详情」入口 */
@@ -510,15 +508,17 @@ function isDescTruncated(text?: string | null) {
// 描述副行:控长截断 + 末尾「详情」链接,整行可点(进入对象域) // 描述副行:控长截断 + 末尾「详情」链接,整行可点(进入对象域)
.pg-prod-desc { .pg-prod-desc {
display: inline-flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
max-width: 100%; width: 100%;
min-width: 0; min-width: 0;
cursor: pointer; cursor: pointer;
} }
.pg-prod-desc__text { .pg-prod-desc__text {
flex: 1 1 auto;
min-width: 0;
overflow: hidden; overflow: hidden;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
font-size: 12px; font-size: 12px;

View File

@@ -22,17 +22,21 @@ const visible = defineModel<boolean>('visible', {
const model = reactive({ const model = reactive({
confirmName: '', confirmName: '',
confirmText: '',
reason: '' reason: ''
}); });
const confirmDisabled = computed(() => { const confirmDisabled = computed(() => {
return !model.reason.trim() || model.confirmName.trim() !== props.productName; return (
!model.reason.trim() || model.confirmName.trim() !== props.productName || model.confirmText.trim() !== 'DELETE'
);
}); });
function handleConfirm() { function handleConfirm() {
emit('submit', { emit('submit', {
id: props.productId, id: props.productId,
productName: model.confirmName.trim(), productName: model.confirmName.trim(),
confirmText: model.confirmText.trim(),
reason: model.reason.trim() reason: model.reason.trim()
}); });
} }
@@ -45,6 +49,7 @@ watch(
} }
model.confirmName = ''; model.confirmName = '';
model.confirmText = '';
model.reason = ''; model.reason = '';
} }
); );
@@ -60,7 +65,7 @@ watch(
@confirm="handleConfirm" @confirm="handleConfirm"
> >
<ElAlert <ElAlert
:title="`请输入当前产品名称 ${productName || '--'} 完成二次确认,删除后将退出当前对象上下文。`" :title="`请输入当前产品名称 ${productName || '--'} 和确认口令 DELETE,删除后将退出当前对象上下文。`"
type="error" type="error"
:closable="false" :closable="false"
class="mb-16px" class="mb-16px"
@@ -69,6 +74,9 @@ watch(
<ElFormItem label="删除确认名称"> <ElFormItem label="删除确认名称">
<ElInput v-model="model.confirmName" placeholder="请输入当前产品名称" /> <ElInput v-model="model.confirmName" placeholder="请输入当前产品名称" />
</ElFormItem> </ElFormItem>
<ElFormItem label="确认口令">
<ElInput v-model="model.confirmText" placeholder="请输入 DELETE" />
</ElFormItem>
<ElFormItem label="删除原因"> <ElFormItem label="删除原因">
<ElInput <ElInput
v-model="model.reason" v-model="model.reason"

View File

@@ -3,7 +3,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus'; import { ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict'; import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchGetProductPage } from '@/service/api'; import { fetchGetProductOptions } from '@/service/api';
import { useDict } from '@/hooks/business/dict'; import { useDict } from '@/hooks/business/dict';
import { useForm, useFormRules } from '@/hooks/common/form'; import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessUserPicker from '@/components/custom/business-user-picker.vue'; import BusinessUserPicker from '@/components/custom/business-user-picker.vue';
@@ -26,7 +26,8 @@ export interface ProjectCreateBaseForm {
interface ProductOption { interface ProductOption {
id: string; id: string;
name: string; name: string;
directionCode: string; code?: string;
directionCode?: string;
} }
interface Props { interface Props {
@@ -151,16 +152,17 @@ const rules = computed(
); );
async function loadProductOptions() { async function loadProductOptions() {
const { error, data } = await fetchGetProductPage({ pageNo: 1, pageSize: 200 }); const { error, data } = await fetchGetProductOptions();
if (error || !data) { if (error || !data) {
productOptions.value = []; productOptions.value = [];
return; return;
} }
productOptions.value = data.list.map(item => ({ productOptions.value = data.map(item => ({
id: item.id, id: item.id,
name: item.name || item.code || item.id, name: item.name || item.code || item.id,
code: item.code || '',
directionCode: item.directionCode || '' directionCode: item.directionCode || ''
})); }));
} }
@@ -175,8 +177,9 @@ watch([() => model.value.productId, productOptions], ([productId]) => {
const product = productOptions.value.find(p => p.id === productId); const product = productOptions.value.find(p => p.id === productId);
if (product) { if (product) {
if (model.value.directionCode !== product.directionCode) { const directionCode = product.directionCode || '';
model.value.directionCode = product.directionCode; if (directionCode && model.value.directionCode !== directionCode) {
model.value.directionCode = directionCode;
} }
return; return;
} }

View File

@@ -6,7 +6,7 @@ import { PROJECT_MANAGER_ROLE_CODE } from '@/constants/business';
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict'; import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict';
import { import {
fetchCreateProjectWithTeam, fetchCreateProjectWithTeam,
fetchGetProductPage, fetchGetProductOptions,
fetchGetRoleSimpleList, fetchGetRoleSimpleList,
fetchUpdateProject fetchUpdateProject
} from '@/service/api'; } from '@/service/api';
@@ -66,7 +66,8 @@ const dialogTitle = computed(() => (isEditMode.value ? '编辑项目' : '新增
interface ProductOption { interface ProductOption {
id: string; id: string;
name: string; name: string;
directionCode: string; code?: string;
directionCode?: string;
} }
const productOptions = ref<ProductOption[]>([]); const productOptions = ref<ProductOption[]>([]);
@@ -209,16 +210,17 @@ function closeDialog() {
} }
async function loadProductOptions() { async function loadProductOptions() {
const { error, data } = await fetchGetProductPage({ pageNo: 1, pageSize: 200 }); const { error, data } = await fetchGetProductOptions();
if (error || !data) { if (error || !data) {
productOptions.value = []; productOptions.value = [];
return; return;
} }
productOptions.value = data.list.map(item => ({ productOptions.value = data.map(item => ({
id: item.id, id: item.id,
name: item.name || item.code || item.id, name: item.name || item.code || item.id,
code: item.code || '',
directionCode: item.directionCode || '' directionCode: item.directionCode || ''
})); }));
} }

View File

@@ -508,6 +508,16 @@ watch(
<span v-else>{{ formatWorklogPeriod(row.startDate, row.endDate).display }}</span> <span v-else>{{ formatWorklogPeriod(row.startDate, row.endDate).display }}</span>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="时长" width="100" align="center">
<template #default="{ row }">
<span class="task-worklog-panel__duration">{{ formatHours(row.durationHours) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="进度" width="100" align="center">
<template #default="{ row }">
<span class="task-worklog-panel__progress">{{ formatProgress(row.progressRate) }}</span>
</template>
</ElTableColumn>
<ElTableColumn v-if="showAssigneeColumn" label="填报人" width="120" align="center"> <ElTableColumn v-if="showAssigneeColumn" label="填报人" width="120" align="center">
<template #header> <template #header>
<div class="task-worklog-panel__user-header"> <div class="task-worklog-panel__user-header">
@@ -675,16 +685,6 @@ watch(
<DictTag :dict-code="RDMS_WORKLOG_DIFFICULTY_DICT_CODE" :value="row.difficulty" size="small" effect="light" /> <DictTag :dict-code="RDMS_WORKLOG_DIFFICULTY_DICT_CODE" :value="row.difficulty" size="small" effect="light" />
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="时长" width="100" align="center">
<template #default="{ row }">
<span class="task-worklog-panel__duration">{{ formatHours(row.durationHours) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="进度" width="100" align="center">
<template #default="{ row }">
<span class="task-worklog-panel__progress">{{ formatProgress(row.progressRate) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="120" align="center" fixed="right"> <ElTableColumn label="操作" width="120" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="task-worklog-panel__actions" @click.stop> <div class="task-worklog-panel__actions" @click.stop>

View File

@@ -11,6 +11,10 @@ export type WorkbenchTodoPriority = 'high' | 'mid' | 'low';
export interface WorkbenchTodoItemSource { export interface WorkbenchTodoItemSource {
id: string; id: string;
category: WorkbenchTodoCategory; category: WorkbenchTodoCategory;
/** 左侧分类 tag 文案;不传时按 category 默认映射 */
categoryLabel?: string;
/** 左侧分类 tag 色调;不传时按 category 默认映射 */
categoryTone?: WorkbenchTodoItem['categoryTone'];
title: string; title: string;
/** 创建时间ISO 字符串。列表默认按这个升序 */ /** 创建时间ISO 字符串。列表默认按这个升序 */
createdTime: string; createdTime: string;
@@ -126,8 +130,8 @@ export function buildWorkbenchTodoItems(source: readonly WorkbenchTodoItemSource
...item, ...item,
deadlineLabel: formatDeadline(item.deadline), deadlineLabel: formatDeadline(item.deadline),
remainingDays: getRemainingDays(item.deadline), remainingDays: getRemainingDays(item.deadline),
categoryLabel: meta.label, categoryLabel: item.categoryLabel || meta.label,
categoryTone: meta.tone categoryTone: item.categoryTone || meta.tone
} satisfies WorkbenchTodoItem; } satisfies WorkbenchTodoItem;
}); });
} }

View File

@@ -187,6 +187,14 @@ const OVERTIME_APPROVAL_ACTION_ICONS = {
detail: markRaw(IconMdiEyeOutline) detail: markRaw(IconMdiEyeOutline)
}; };
function getApprovalCategoryLabel(bizType: ApprovalBizType) {
if (bizType === 'weekly') return '周报';
if (bizType === 'monthly') return '月报';
if (bizType === 'project') return '项目半月报';
if (bizType === 'overtime_application') return '加班申请';
return '待审批';
}
const PERSONAL_ACTION_ICONS = { const PERSONAL_ACTION_ICONS = {
detail: markRaw(IconMdiEyeOutline), detail: markRaw(IconMdiEyeOutline),
worklog: markRaw(IconMdiClipboardEditOutline), worklog: markRaw(IconMdiClipboardEditOutline),
@@ -789,6 +797,7 @@ async function loadOvertimeApprovalItems() {
data.list.map(item => ({ data.list.map(item => ({
id: `overtime-application-${item.id}`, id: `overtime-application-${item.id}`,
category: 'approval', category: 'approval',
categoryLabel: '加班申请',
title: `${item.applicantName} · ${item.overtimeDate.slice(5, 7)} 月加班 ${item.overtimeDuration} 申请待审批`, title: `${item.applicantName} · ${item.overtimeDate.slice(5, 7)} 月加班 ${item.overtimeDuration} 申请待审批`,
createdTime: item.submitTime || item.createTime, createdTime: item.submitTime || item.createTime,
deadline: item.submitTime || item.createTime, deadline: item.submitTime || item.createTime,
@@ -810,6 +819,7 @@ function buildWorkReportApprovalItems<T extends WorkReportRow>(
rows.map(item => ({ rows.map(item => ({
id: `${bizType}-${item.id}`, id: `${bizType}-${item.id}`,
category: 'approval', category: 'approval',
categoryLabel: getApprovalCategoryLabel(bizType),
title: `${reportTypeLabel} · ${bizType === 'weekly' ? formatWeeklyPeriodLabel(item) : formatPeriod(item)} 待审批`, title: `${reportTypeLabel} · ${bizType === 'weekly' ? formatWeeklyPeriodLabel(item) : formatPeriod(item)} 待审批`,
createdTime: item.submitTime || item.createTime || '', createdTime: item.submitTime || item.createTime || '',
deadline: item.submitTime || item.createTime || null, deadline: item.submitTime || item.createTime || null,
@@ -1439,7 +1449,7 @@ onActivated(refresh);
.workbench-todo__item { .workbench-todo__item {
display: grid; display: grid;
grid-template-columns: 72px minmax(0, 1fr) auto; grid-template-columns: max-content minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
padding: 14px 16px; padding: 14px 16px;
@@ -1460,17 +1470,20 @@ onActivated(refresh);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
width: 72px; min-width: max-content;
min-width: 0;
} }
.workbench-todo__category { .workbench-todo__category {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
padding: 3px 10px; padding: 3px 10px;
border-radius: 999px; border-radius: 999px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
line-height: 1;
white-space: nowrap;
flex: 0 0 auto;
} }
.workbench-todo__category--sky { .workbench-todo__category--sky {