feat(projects): 1、站内信、通知功能完善;2、项目列表按会议需求重新开发
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user