fix(产品管理、项目管理、登录密码校验、工作报告): 修复用户们提出的一系列问题。
This commit is contained in:
@@ -31,7 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const model = defineModel<Api.Project.AttachmentItem[]>({ default: () => [] });
|
||||
|
||||
/** 给用户看的简短分类(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)
|
||||
const ALLOWED_EXTENSIONS = new Set([
|
||||
@@ -55,7 +55,10 @@ const ALLOWED_EXTENSIONS = new Set([
|
||||
'rar',
|
||||
'7z',
|
||||
'mp4',
|
||||
'mp3'
|
||||
'mp3',
|
||||
'sql',
|
||||
'xml',
|
||||
'json'
|
||||
]);
|
||||
|
||||
const FORBIDDEN_EXTENSIONS = new Set([
|
||||
|
||||
@@ -661,7 +661,7 @@ const local: App.I18n.Schema = {
|
||||
},
|
||||
pwd: {
|
||||
required: '请输入密码',
|
||||
invalid: '密码格式不正确,6-18位字符,包含字母、数字、下划线'
|
||||
invalid: '密码格式不正确,4-30位字符,包含字母、数字、下划线'
|
||||
},
|
||||
confirmPwd: {
|
||||
required: '请输入确认密码',
|
||||
|
||||
@@ -18,6 +18,10 @@ type ProductResponse = Omit<Api.Product.Product, 'id' | 'managerUserId' | 'curre
|
||||
currentUserRoles?: Api.Common.CurrentUserRole[] | null;
|
||||
};
|
||||
|
||||
type ProductOptionResponse = Omit<Api.Product.ProductOption, 'id'> & {
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
type ProductPageResponse = Api.Product.PageResult<ProductResponse>;
|
||||
|
||||
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) {
|
||||
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'> & {
|
||||
/** 后端 overview-summary 升级(total/items)灰度期间可能缺省,适配层兜底 */
|
||||
total?: number | null;
|
||||
|
||||
12
src/typings/api/product.d.ts
vendored
12
src/typings/api/product.d.ts
vendored
@@ -92,6 +92,17 @@ declare namespace Api {
|
||||
lastStatusReason?: string | null;
|
||||
}
|
||||
|
||||
interface ProductOption {
|
||||
/** 产品 ID */
|
||||
id: string;
|
||||
/** 产品编码 */
|
||||
code: string;
|
||||
/** 产品名称 */
|
||||
name: string;
|
||||
/** 产品方向字典值 */
|
||||
directionCode: string;
|
||||
}
|
||||
|
||||
interface ProductLifecycleAction {
|
||||
actionCode: ProductStatusActionCode;
|
||||
actionName: string;
|
||||
@@ -216,6 +227,7 @@ declare namespace Api {
|
||||
interface DeleteProductParams {
|
||||
id: string;
|
||||
productName: string;
|
||||
confirmText: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -558,7 +558,9 @@ function resolveTaskMetrics(metricsText: string, fallback?: Partial<StructuredTa
|
||||
.filter(Boolean);
|
||||
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 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];
|
||||
|
||||
return {
|
||||
@@ -591,7 +593,7 @@ function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): Rev
|
||||
: escapeHtml(item.contentText || '').replace(/\n/g, '<br>') || EMPTY_TEXT,
|
||||
contentSections,
|
||||
reflection: item.reflectionText || '',
|
||||
removable: false,
|
||||
removable: true,
|
||||
source: item
|
||||
};
|
||||
}
|
||||
@@ -728,6 +730,13 @@ function removePlanItem(index: number) {
|
||||
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) {
|
||||
activeEditField.value = key;
|
||||
}
|
||||
@@ -736,9 +745,19 @@ function blurEditField(key: string) {
|
||||
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) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
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(
|
||||
target,
|
||||
item.contentSections || [],
|
||||
@@ -751,6 +770,11 @@ function syncRichContent(item: ReviewItem, event: Event) {
|
||||
function syncRichTarget(item: PlanItem, event: Event) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
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(
|
||||
target,
|
||||
item.targetSections || [],
|
||||
@@ -836,7 +860,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
confirm-button-text="删除"
|
||||
cancel-button-text="取消"
|
||||
width="220"
|
||||
@confirm="reviewItems.splice(index, 1)"
|
||||
@confirm="removeReviewItem(index)"
|
||||
>
|
||||
<template #reference>
|
||||
<button class="item-remove-btn" aria-label="删除回顾">×</button>
|
||||
|
||||
@@ -127,7 +127,7 @@ function toWorkItem(item: Api.WorkReport.Project.ProjectReportItem, index: numbe
|
||||
}
|
||||
|
||||
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[]>(() => {
|
||||
@@ -273,6 +273,11 @@ function removePlanItem(index: number) {
|
||||
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) {
|
||||
titleEditSnapshot.value[item.id] = item.title;
|
||||
}
|
||||
@@ -288,9 +293,9 @@ function notifyTitleSaved(item: WorkItem) {
|
||||
item.title = nextTitle;
|
||||
if (item.source) item.source.itemTitle = nextTitle;
|
||||
delete titleEditSnapshot.value[item.id];
|
||||
if (nextTitle !== previousTitle) {
|
||||
ElMessage.success('名称已成功修改');
|
||||
}
|
||||
// if (nextTitle !== previousTitle) {
|
||||
// ElMessage.success('名称已成功修改');
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -373,7 +378,7 @@ function notifyTitleSaved(item: WorkItem) {
|
||||
<div class="review-grid layout-row">
|
||||
<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 class="review-card-head">
|
||||
<div class="review-card-head" :class="{ 'no-remove': item.removable === false }">
|
||||
<span class="row-index">{{ index + 1 }}</span>
|
||||
<div class="review-title-readonly">
|
||||
<div class="work-title-line">
|
||||
@@ -390,6 +395,18 @@ function notifyTitleSaved(item: WorkItem) {
|
||||
<span class="meta-chip">共 {{ item.hours }}h</span>
|
||||
</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>
|
||||
@@ -727,10 +744,7 @@ function notifyTitleSaved(item: WorkItem) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.review-card-head {
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.review-card-head.no-remove,
|
||||
.plan-card-head.no-remove {
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
@@ -725,7 +725,7 @@ function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): Rev
|
||||
contentSections,
|
||||
contentTasks,
|
||||
reflection: item.reflectionText || '',
|
||||
removable: false,
|
||||
removable: true,
|
||||
source: item
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const DEFAULT_TOP_N = 5;
|
||||
const PAGE_SIZE = 10;
|
||||
/** 产品行多列数(名称/编码/经理/我的角色/状态/原因/更新),非产品行整行合并用 */
|
||||
const COLUMN_COUNT = 7;
|
||||
/** 产品描述副行最大展示字符数,超出截断并追加 …(完整内容走 title 悬浮) */
|
||||
/** 产品描述副行长度阈值:超过时展示「详情」入口 */
|
||||
const PRODUCT_DESC_MAX_LEN = 48;
|
||||
|
||||
interface DirectionGroup {
|
||||
@@ -274,11 +274,9 @@ function formatDate(value?: string | null) {
|
||||
return dayjs(value).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
/** 产品描述副行:控长截断,超出追加 …;完整内容由 title 悬浮展示 */
|
||||
/** 产品描述副行:不再手工截断字符,交给布局省略;完整内容由 title 悬浮展示 */
|
||||
function truncateDesc(text?: string | null) {
|
||||
const trimmed = (text ?? '').trim();
|
||||
|
||||
return trimmed.length > PRODUCT_DESC_MAX_LEN ? `${trimmed.slice(0, PRODUCT_DESC_MAX_LEN)}…` : trimmed;
|
||||
return (text ?? '').trim();
|
||||
}
|
||||
|
||||
/** 描述是否被截断(放不下);仅截断时才追加「详情」入口 */
|
||||
@@ -510,15 +508,17 @@ function isDescTruncated(text?: string | null) {
|
||||
|
||||
// 描述副行:控长截断 + 末尾「详情」链接,整行可点(进入对象域)
|
||||
.pg-prod-desc {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pg-prod-desc__text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -22,17 +22,21 @@ const visible = defineModel<boolean>('visible', {
|
||||
|
||||
const model = reactive({
|
||||
confirmName: '',
|
||||
confirmText: '',
|
||||
reason: ''
|
||||
});
|
||||
|
||||
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() {
|
||||
emit('submit', {
|
||||
id: props.productId,
|
||||
productName: model.confirmName.trim(),
|
||||
confirmText: model.confirmText.trim(),
|
||||
reason: model.reason.trim()
|
||||
});
|
||||
}
|
||||
@@ -45,6 +49,7 @@ watch(
|
||||
}
|
||||
|
||||
model.confirmName = '';
|
||||
model.confirmText = '';
|
||||
model.reason = '';
|
||||
}
|
||||
);
|
||||
@@ -60,7 +65,7 @@ watch(
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<ElAlert
|
||||
:title="`请输入当前产品名称 ${productName || '--'} 完成二次确认,删除后将退出当前对象上下文。`"
|
||||
:title="`请输入当前产品名称 ${productName || '--'} 和确认口令 DELETE,删除后将退出当前对象上下文。`"
|
||||
type="error"
|
||||
:closable="false"
|
||||
class="mb-16px"
|
||||
@@ -69,6 +74,9 @@ watch(
|
||||
<ElFormItem label="删除确认名称">
|
||||
<ElInput v-model="model.confirmName" placeholder="请输入当前产品名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="确认口令">
|
||||
<ElInput v-model="model.confirmText" placeholder="请输入 DELETE" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="删除原因">
|
||||
<ElInput
|
||||
v-model="model.reason"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
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 { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessUserPicker from '@/components/custom/business-user-picker.vue';
|
||||
@@ -26,7 +26,8 @@ export interface ProjectCreateBaseForm {
|
||||
interface ProductOption {
|
||||
id: string;
|
||||
name: string;
|
||||
directionCode: string;
|
||||
code?: string;
|
||||
directionCode?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -151,16 +152,17 @@ const rules = computed(
|
||||
);
|
||||
|
||||
async function loadProductOptions() {
|
||||
const { error, data } = await fetchGetProductPage({ pageNo: 1, pageSize: 200 });
|
||||
const { error, data } = await fetchGetProductOptions();
|
||||
|
||||
if (error || !data) {
|
||||
productOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
productOptions.value = data.list.map(item => ({
|
||||
productOptions.value = data.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || item.code || item.id,
|
||||
code: item.code || '',
|
||||
directionCode: item.directionCode || ''
|
||||
}));
|
||||
}
|
||||
@@ -175,8 +177,9 @@ watch([() => model.value.productId, productOptions], ([productId]) => {
|
||||
const product = productOptions.value.find(p => p.id === productId);
|
||||
|
||||
if (product) {
|
||||
if (model.value.directionCode !== product.directionCode) {
|
||||
model.value.directionCode = product.directionCode;
|
||||
const directionCode = product.directionCode || '';
|
||||
if (directionCode && model.value.directionCode !== directionCode) {
|
||||
model.value.directionCode = directionCode;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
fetchCreateProjectWithTeam,
|
||||
fetchGetProductPage,
|
||||
fetchGetProductOptions,
|
||||
fetchGetRoleSimpleList,
|
||||
fetchUpdateProject
|
||||
} from '@/service/api';
|
||||
@@ -66,7 +66,8 @@ const dialogTitle = computed(() => (isEditMode.value ? '编辑项目' : '新增
|
||||
interface ProductOption {
|
||||
id: string;
|
||||
name: string;
|
||||
directionCode: string;
|
||||
code?: string;
|
||||
directionCode?: string;
|
||||
}
|
||||
|
||||
const productOptions = ref<ProductOption[]>([]);
|
||||
@@ -209,16 +210,17 @@ function closeDialog() {
|
||||
}
|
||||
|
||||
async function loadProductOptions() {
|
||||
const { error, data } = await fetchGetProductPage({ pageNo: 1, pageSize: 200 });
|
||||
const { error, data } = await fetchGetProductOptions();
|
||||
|
||||
if (error || !data) {
|
||||
productOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
productOptions.value = data.list.map(item => ({
|
||||
productOptions.value = data.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || item.code || item.id,
|
||||
code: item.code || '',
|
||||
directionCode: item.directionCode || ''
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -508,6 +508,16 @@ watch(
|
||||
<span v-else>{{ formatWorklogPeriod(row.startDate, row.endDate).display }}</span>
|
||||
</template>
|
||||
</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">
|
||||
<template #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" />
|
||||
</template>
|
||||
</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">
|
||||
<template #default="{ row }">
|
||||
<div class="task-worklog-panel__actions" @click.stop>
|
||||
|
||||
@@ -11,6 +11,10 @@ export type WorkbenchTodoPriority = 'high' | 'mid' | 'low';
|
||||
export interface WorkbenchTodoItemSource {
|
||||
id: string;
|
||||
category: WorkbenchTodoCategory;
|
||||
/** 左侧分类 tag 文案;不传时按 category 默认映射 */
|
||||
categoryLabel?: string;
|
||||
/** 左侧分类 tag 色调;不传时按 category 默认映射 */
|
||||
categoryTone?: WorkbenchTodoItem['categoryTone'];
|
||||
title: string;
|
||||
/** 创建时间,ISO 字符串。列表默认按这个升序 */
|
||||
createdTime: string;
|
||||
@@ -126,8 +130,8 @@ export function buildWorkbenchTodoItems(source: readonly WorkbenchTodoItemSource
|
||||
...item,
|
||||
deadlineLabel: formatDeadline(item.deadline),
|
||||
remainingDays: getRemainingDays(item.deadline),
|
||||
categoryLabel: meta.label,
|
||||
categoryTone: meta.tone
|
||||
categoryLabel: item.categoryLabel || meta.label,
|
||||
categoryTone: item.categoryTone || meta.tone
|
||||
} satisfies WorkbenchTodoItem;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,6 +187,14 @@ const OVERTIME_APPROVAL_ACTION_ICONS = {
|
||||
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 = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
worklog: markRaw(IconMdiClipboardEditOutline),
|
||||
@@ -789,6 +797,7 @@ 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} 申请待审批`,
|
||||
createdTime: item.submitTime || item.createTime,
|
||||
deadline: item.submitTime || item.createTime,
|
||||
@@ -810,6 +819,7 @@ function buildWorkReportApprovalItems<T extends WorkReportRow>(
|
||||
rows.map(item => ({
|
||||
id: `${bizType}-${item.id}`,
|
||||
category: 'approval',
|
||||
categoryLabel: getApprovalCategoryLabel(bizType),
|
||||
title: `${reportTypeLabel} · ${bizType === 'weekly' ? formatWeeklyPeriodLabel(item) : formatPeriod(item)} 待审批`,
|
||||
createdTime: item.submitTime || item.createTime || '',
|
||||
deadline: item.submitTime || item.createTime || null,
|
||||
@@ -1439,7 +1449,7 @@ onActivated(refresh);
|
||||
|
||||
.workbench-todo__item {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr) auto;
|
||||
grid-template-columns: max-content minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
@@ -1460,17 +1470,20 @@ onActivated(refresh);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 72px;
|
||||
min-width: 0;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.workbench-todo__category {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.workbench-todo__category--sky {
|
||||
|
||||
Reference in New Issue
Block a user