fix(产品管理、项目管理、登录密码校验、工作报告): 修复用户们提出的一系列问题。
This commit is contained in:
@@ -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([
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
pwd: {
|
pwd: {
|
||||||
required: '请输入密码',
|
required: '请输入密码',
|
||||||
invalid: '密码格式不正确,6-18位字符,包含字母、数字、下划线'
|
invalid: '密码格式不正确,4-30位字符,包含字母、数字、下划线'
|
||||||
},
|
},
|
||||||
confirmPwd: {
|
confirmPwd: {
|
||||||
required: '请输入确认密码',
|
required: '请输入确认密码',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 || ''
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user