feat(projects): 1、站内信、通知功能完善;2、项目列表按会议需求重新开发

This commit is contained in:
2026-06-11 14:02:26 +08:00
parent d53a8dfae5
commit 0652a24c5e
26 changed files with 2064 additions and 768 deletions

View File

@@ -3,7 +3,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
import type { Component } from 'vue';
import { ElButton, ElTag } from 'element-plus';
import dayjs from 'dayjs';
import { Box, DeleteFilled, VideoPause, VideoPlay } from '@element-plus/icons-vue';
import { Box, DeleteFilled, Document, Menu, VideoPause, VideoPlay } from '@element-plus/icons-vue';
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
@@ -16,11 +16,10 @@ import ProductSearch from './modules/product-search.vue';
defineOptions({ name: 'ProductList' });
interface StatusNavMeta {
key: Api.Product.ProductStatusCode;
label: string;
description: string;
tone: 'teal' | 'slate' | 'amber' | 'rose';
type StatusNavTone = 'sky' | 'teal' | 'slate' | 'amber' | 'rose';
interface StatusVisualMeta {
tone: StatusNavTone;
icon: Component;
}
@@ -70,39 +69,20 @@ function formatDate(value?: string | null) {
return dayjs(value).format('YYYY-MM-DD');
}
const statusNavMetas: StatusNavMeta[] = [
{
key: 'active',
label: '启用产品',
description: '当前正常服务中的产品',
tone: 'teal',
icon: VideoPlay
},
{
key: 'archived',
label: '归档产品',
description: '已完成阶段目标的产品',
tone: 'slate',
icon: Box
},
{
key: 'paused',
label: '暂停产品',
description: '阶段性暂停投入的产品',
tone: 'amber',
icon: VideoPause
},
{
key: 'abandoned',
label: '废弃产品',
description: '已明确停止建设的产品',
tone: 'rose',
icon: DeleteFilled
}
];
/** 状态视觉资产icon/tone是前端本地映射状态名直接渲染后端 statusName不做本地名称映射 */
const STATUS_VISUALS: Record<string, StatusVisualMeta> = {
active: { tone: 'teal', icon: VideoPlay },
archived: { tone: 'slate', icon: Box },
paused: { tone: 'amber', icon: VideoPause },
abandoned: { tone: 'rose', icon: DeleteFilled }
};
/** 状态机新增状态未配置视觉资产时的默认兜底:通用图标 + 中性色 */
const DEFAULT_STATUS_VISUAL: StatusVisualMeta = { tone: 'slate', icon: Document };
const searchParams = reactive(getInitSearchParams());
const selectedStatus = ref<Api.Product.ProductStatusCode>('active');
/** 当前选中导航键:状态编码(状态机动态下发)或 'all'(全部视图,分页接口不传 statusCode */
const selectedStatus = ref<string>('active');
const managerFilterOptions = ref<Api.SystemManage.UserSimple[]>([]);
const managerUserOptions = ref<Api.SystemManage.UserSimple[]>([]);
const operateVisible = ref(false);
@@ -111,23 +91,29 @@ const { routerPush } = useRouterPush();
const { getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
const statusCounts = ref<Record<string, number>>({
active: 0,
archived: 0,
paused: 0,
abandoned: 0
});
/** 状态看板项overview-summary items状态机动态下发已按 sort 升序) */
const statusBoardItems = ref<Api.Product.OverviewStatusItem[]>([]);
/** "全部"口径总数(后端 total前端不自行求和 */
const statusBoardTotal = ref(0);
const managerLabelMap = computed(() => {
return new Map(managerUserOptions.value.map(item => [String(item.id), item.nickname]));
});
const statusItems = computed(() =>
statusNavMetas.map(item => ({
...item,
count: statusCounts.value[item.key] ?? 0
}))
);
const statusItems = computed(() => [
{ key: 'all', label: '全部产品', count: statusBoardTotal.value, tone: 'sky' as StatusNavTone, icon: Menu },
...statusBoardItems.value.map(item => {
const visual = STATUS_VISUALS[item.statusCode] ?? DEFAULT_STATUS_VISUAL;
return {
key: item.statusCode,
label: item.statusName,
count: item.count,
tone: visual.tone,
icon: visual.icon
};
})
]);
function getDirectionLabel(directionCode?: string | null) {
return getDirectionDictLabel(directionCode, '--');
@@ -145,7 +131,7 @@ function createRequestParams(): Api.Product.ProductSearchParams {
return {
...searchParams,
keyword: searchParams.keyword?.trim() || undefined,
statusCode: selectedStatus.value
statusCode: selectedStatus.value === 'all' ? undefined : selectedStatus.value
};
}
@@ -233,12 +219,8 @@ async function loadManagerOptions() {
async function loadOverviewData() {
const { error, data: overviewSummary } = await fetchGetProductOverviewSummary();
if (error || !overviewSummary) {
statusCounts.value = {};
return;
}
statusCounts.value = overviewSummary.statusCounts || {};
statusBoardItems.value = error || !overviewSummary ? [] : overviewSummary.items;
statusBoardTotal.value = error || !overviewSummary ? 0 : overviewSummary.total;
}
async function reloadProductTable(page = searchParams.pageNo ?? 1) {
@@ -263,7 +245,7 @@ async function handleResetSearch() {
await reloadProductTable(1);
}
async function handleStatusChange(status: Api.Product.ProductStatusCode) {
async function handleStatusChange(status: string) {
selectedStatus.value = status;
await Promise.all([loadOverviewData(), reloadProductTable(1)]);
}
@@ -302,7 +284,7 @@ onMounted(async () => {
class="min-h-560px gap-16px overflow-hidden xl:grid xl:grid-cols-[396px_minmax(0,1fr)] lt-xl:flex lt-xl:flex-col lt-xl:overflow-auto"
>
<div class="flex-col-stretch gap-16px xl:min-h-0">
<ElCard class="product-overview-card card-wrapper">
<ElCard class="product-overview-card card-wrapper xl:flex-1">
<div class="product-status-panel__list">
<button
v-for="item in statusItems"
@@ -323,7 +305,6 @@ onMounted(async () => {
<strong>{{ item.label }}</strong>
<em>{{ item.count }}</em>
</div>
<p class="product-status-item__desc">{{ item.description }}</p>
</div>
</button>
</div>
@@ -462,7 +443,6 @@ onMounted(async () => {
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.product-status-item__top strong {
@@ -478,10 +458,9 @@ onMounted(async () => {
font-weight: 700;
}
.product-status-item__desc {
color: rgb(100 116 139 / 94%);
font-size: 13px;
line-height: 1.6;
.product-status-item--sky .product-status-item__icon {
background-color: rgb(240 249 255 / 96%);
color: rgb(2 132 199 / 96%);
}
.product-status-item--teal .product-status-item__icon {

View File

@@ -16,19 +16,21 @@ export const productStatusActionRecord: Record<Api.Product.ProductStatusActionCo
abandon: '废弃产品'
};
export function getProductStatusLabel(status: Api.Product.ProductStatusCode) {
return productStatusRecord[status];
/** 状态编码来自状态机动态下发,未配置的新状态回退编码本身(展示名优先用后端 statusName */
export function getProductStatusLabel(status: string) {
return (productStatusRecord as Record<string, string>)[status] ?? status;
}
export function getProductStatusTagType(status: Api.Product.ProductStatusCode): UI.ThemeColor {
const statusTagTypeMap: Record<Api.Product.ProductStatusCode, UI.ThemeColor> = {
/** 根据产品状态返回对应的 Tag 类型;未配置的新状态回退 info */
export function getProductStatusTagType(status: string): UI.ThemeColor {
const statusTagTypeMap: Record<string, UI.ThemeColor> = {
active: 'success',
paused: 'warning',
archived: 'info',
abandoned: 'danger'
};
return statusTagTypeMap[status];
return statusTagTypeMap[status] ?? 'info';
}
export function isProductEditable(status: Api.Product.ProductStatusCode) {