Просмотр исходного кода

feat(s8): add order execution overview filters

YY968XX 1 месяц назад
Родитель
Сommit
5e535b3b37

+ 89 - 0
Web/src/stores/orderExecution.ts

@@ -19,9 +19,38 @@ import type {
 	CustomerGroup,
 	OrderExecutionFilters,
 	OrderExecutionKpiAverages,
+	OrderNodeKey,
 	SalesOrderExecution,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 
+export interface OrderExecutionFilterOption<V = string> {
+	value: V;
+	label: string;
+}
+
+export interface OrderExecutionFilterOptions {
+	status: OrderExecutionFilterOption<'' | 'all' | 'in_progress' | 'completed'>[];
+	node: OrderExecutionFilterOption<'' | OrderNodeKey>[];
+	customer: OrderExecutionFilterOption[];
+	productLine: OrderExecutionFilterOption[];
+	region: OrderExecutionFilterOption[];
+	severity: OrderExecutionFilterOption<'' | 'green' | 'yellow' | 'red'>[];
+}
+
+const STATUS_LABELS: Record<'all' | 'in_progress' | 'completed', string> = {
+	all: '全部',
+	in_progress: '执行中',
+	completed: '已完成',
+};
+
+const SEVERITY_LABELS: Record<'green' | 'yellow' | 'red', string> = {
+	green: '正常',
+	yellow: '关注',
+	red: '严重',
+};
+
+const dedupe = <T>(values: Iterable<T>): T[] => Array.from(new Set(values));
+
 const NAV_STATE_KEY = 's8_order_chain_nav_state';
 
 interface OrderExecutionState {
@@ -51,6 +80,66 @@ export const useOrderExecutionStore = defineStore('orderExecution', {
 		customerGroups(state): CustomerGroup[] {
 			return state.customers;
 		},
+		filteredCustomerGroups(state): CustomerGroup[] {
+			const filtered = this.filteredOrders;
+			if (filtered.length === 0) return [];
+			const buckets = new Map<string, SalesOrderExecution[]>();
+			for (const order of filtered) {
+				const key = order.customerCode || order.customerName;
+				const bucket = buckets.get(key) ?? [];
+				bucket.push(order);
+				buckets.set(key, bucket);
+			}
+			return state.customers
+				.map((customer) => {
+					const sample = customer.orders[0];
+					const key = sample?.customerCode || sample?.customerName || customer.id;
+					const orders = buckets.get(key) ?? [];
+					return { id: customer.id, name: customer.name, type: customer.type, orders };
+				})
+				.filter((customer) => customer.orders.length > 0);
+		},
+		filterOptions(): OrderExecutionFilterOptions {
+			const orders = this.allOrders;
+			const nodePairs = new Map<OrderNodeKey, string>();
+			for (const order of orders) {
+				if (!nodePairs.has(order.currentNodeKey)) {
+					nodePairs.set(order.currentNodeKey, order.currentNode);
+				}
+			}
+			const customerNames = dedupe(orders.map((o) => o.customerName));
+			const productLines = dedupe(orders.map((o) => o.productLine)).filter(Boolean);
+			const regions = dedupe(orders.map((o) => o.region)).filter(Boolean);
+			const severities = dedupe(orders.map((o) => o.nodeStatus));
+			const statuses = dedupe(orders.map((o) => o.workflowStatus));
+
+			return {
+				status: [
+					{ value: '', label: '全部状态' },
+					...statuses.map((s) => ({ value: s, label: STATUS_LABELS[s] ?? s })),
+				],
+				node: [
+					{ value: '', label: '全部节点' },
+					...Array.from(nodePairs.entries()).map(([value, label]) => ({ value, label })),
+				],
+				customer: [
+					{ value: '', label: '全部客户' },
+					...customerNames.map((name) => ({ value: name, label: name })),
+				],
+				productLine: [
+					{ value: '', label: '全部产品线' },
+					...productLines.map((p) => ({ value: p, label: p })),
+				],
+				region: [
+					{ value: '', label: '全部区域' },
+					...regions.map((r) => ({ value: r, label: r })),
+				],
+				severity: [
+					{ value: '', label: '全部严重度' },
+					...severities.map((s) => ({ value: s, label: SEVERITY_LABELS[s] ?? s })),
+				],
+			};
+		},
 		currentObservationTime(): string {
 			return EXECUTION_OBSERVED_AT;
 		},

+ 58 - 10
Web/src/views/aidop/s8/monitoring/SoOrderExecutionDashboardPage.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts" name="aidopS8OrderExecution">
-// ORDER-FLOW-OVERVIEW-SHELL-1:SO 订单执行档案总览页壳。
-// 仅本轮:4 Tab + KPI 三卡 + 客户分组列表;不接后端、不做链路全景、不做筛选条。
+// ORDER-FLOW-OVERVIEW-SHELL-1 + ORDER-FLOW-OVERVIEW-FILTERS-1:
+// 总览页壳 + 7 维筛选条(status/node/keyword/customer/productLine/region/severity)。
+// KPI / 命中订单 / 客户分组随 store.filteredOrders 变化;空结果时 KPI 显示 --。
 import { computed, onMounted, ref } from 'vue';
 import { useOrderExecutionStore } from '/@/stores/orderExecution';
+import type { OrderExecutionFilters } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 import OrderExecutionKpiCards from './components/order-execution/OrderExecutionKpiCards.vue';
 import OrderExecutionCustomerGroups from './components/order-execution/OrderExecutionCustomerGroups.vue';
+import OrderExecutionFiltersBar from './components/order-execution/OrderExecutionFilters.vue';
 
 const store = useOrderExecutionStore();
 
@@ -12,13 +15,25 @@ onMounted(() => {
 	store.loadFixture();
 });
 
-const customers = computed(() => store.customerGroups);
-const orders = computed(() => store.allOrders);
+const filters = computed(() => store.filters);
+const filterOptions = computed(() => store.filterOptions);
+const filteredOrders = computed(() => store.filteredOrders);
+const filteredCustomers = computed(() => store.filteredCustomerGroups);
 const kpi = computed(() => store.kpiAverages);
 const observedAt = computed(() => store.currentObservationTime);
-const ready = computed(() => store.initialized && orders.value.length > 0);
-const orderCount = computed(() => orders.value.length);
-const customerCount = computed(() => customers.value.length);
+const ready = computed(() => store.initialized && filteredOrders.value.length > 0);
+const orderCount = computed(() => filteredOrders.value.length);
+const customerCount = computed(() => filteredCustomers.value.length);
+const totalOrderCount = computed(() => store.allOrders.length);
+const isEmptyResult = computed(() => store.initialized && filteredOrders.value.length === 0);
+
+function onFiltersUpdate(next: OrderExecutionFilters) {
+	store.setFilters(next);
+}
+
+function onFiltersClear() {
+	store.clearFilters();
+}
 
 const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>('overview');
 </script>
@@ -39,16 +54,24 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 		<el-tabs v-model="activeTab" class="oe-dashboard__tabs">
 			<el-tab-pane label="订单/客户总览" name="overview">
 				<section class="oe-dashboard__section">
+					<OrderExecutionFiltersBar
+						:model-value="filters"
+						:options="filterOptions"
+						@update:model-value="onFiltersUpdate"
+						@clear="onFiltersClear"
+					/>
+
 					<OrderExecutionKpiCards :kpi="kpi" :ready="ready" />
 
 					<div class="oe-dashboard__counts">
 						<div class="oe-dashboard__count">
 							<span class="oe-dashboard__count-label">当前命中订单</span>
-							<span class="oe-dashboard__count-value">{{ ready ? orderCount : '--' }}</span>
+							<span class="oe-dashboard__count-value">{{ orderCount }}</span>
+							<span class="oe-dashboard__count-total">/ {{ totalOrderCount }}</span>
 						</div>
 						<div class="oe-dashboard__count">
 							<span class="oe-dashboard__count-label">客户分组</span>
-							<span class="oe-dashboard__count-value">{{ ready ? customerCount : '--' }}</span>
+							<span class="oe-dashboard__count-value">{{ customerCount }}</span>
 						</div>
 					</div>
 
@@ -57,7 +80,14 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 						<h2 class="oe-dashboard__group-text">客户分组</h2>
 					</div>
 
-					<OrderExecutionCustomerGroups :customers="customers" :preview-limit="3" />
+					<OrderExecutionCustomerGroups
+						v-if="!isEmptyResult"
+						:customers="filteredCustomers"
+						:preview-limit="3"
+					/>
+					<div v-else class="oe-dashboard__empty">
+						当前筛选无匹配订单,可点击「重置」恢复全部。
+					</div>
 				</section>
 			</el-tab-pane>
 
@@ -193,6 +223,24 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 	color: var(--order-accent);
 }
 
+.oe-dashboard__count-total {
+	font-size: 12px;
+	color: var(--order-text-muted);
+}
+
+.oe-dashboard__empty {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	min-height: 160px;
+	border-radius: 14px;
+	background: var(--order-panel);
+	border: 1px dashed var(--order-border);
+	color: var(--order-text-secondary);
+	font-size: 13px;
+	letter-spacing: 0.04em;
+}
+
 .oe-dashboard__group-title {
 	display: flex;
 	align-items: center;

+ 164 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionFilters.vue

@@ -0,0 +1,164 @@
+<script setup lang="ts" name="OrderExecutionFilters">
+// ORDER-FLOW-OVERVIEW-FILTERS-1:订单执行档案筛选条。
+// 7 个维度:status / node / keyword / customer / productLine / region / severity。
+// severity 实际映射 SalesOrderExecution.nodeStatus(绿/黄/红),与 aggregation.filterOrders 现有口径一致。
+import type {
+	OrderExecutionFilters,
+	OrderNodeKey,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+import type { OrderExecutionFilterOptions } from '/@/stores/orderExecution';
+
+interface Props {
+	modelValue: OrderExecutionFilters;
+	options: OrderExecutionFilterOptions;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+	'update:modelValue': [value: OrderExecutionFilters];
+	clear: [];
+}>();
+
+function patch(partial: Partial<OrderExecutionFilters>) {
+	emit('update:modelValue', { ...props.modelValue, ...partial });
+}
+
+const onStatus = (v: OrderExecutionFilters['status']) => patch({ status: v });
+const onNode = (v: '' | OrderNodeKey) => patch({ node: v });
+const onCustomer = (v: string) => patch({ customer: v });
+const onProductLine = (v: string) => patch({ productLine: v });
+const onRegion = (v: string) => patch({ region: v });
+const onSeverity = (v: OrderExecutionFilters['severity']) => patch({ severity: v });
+const onKeyword = (v: string) => patch({ keyword: v });
+const onClear = () => emit('clear');
+</script>
+
+<template>
+	<div class="oe-filters">
+		<el-input
+			:model-value="modelValue.keyword"
+			class="oe-filters__keyword"
+			placeholder="搜索订单号 / 客户 / 产品"
+			clearable
+			@update:model-value="onKeyword"
+		/>
+		<el-select
+			:model-value="modelValue.status"
+			class="oe-filters__select"
+			placeholder="全部状态"
+			@update:model-value="onStatus"
+		>
+			<el-option
+				v-for="opt in options.status"
+				:key="`s-${opt.value}`"
+				:value="opt.value"
+				:label="opt.label"
+			/>
+		</el-select>
+		<el-select
+			:model-value="modelValue.node"
+			class="oe-filters__select"
+			placeholder="全部节点"
+			@update:model-value="onNode"
+		>
+			<el-option
+				v-for="opt in options.node"
+				:key="`n-${opt.value}`"
+				:value="opt.value"
+				:label="opt.label"
+			/>
+		</el-select>
+		<el-select
+			:model-value="modelValue.customer"
+			class="oe-filters__select"
+			placeholder="全部客户"
+			@update:model-value="onCustomer"
+		>
+			<el-option
+				v-for="opt in options.customer"
+				:key="`c-${opt.value}`"
+				:value="opt.value"
+				:label="opt.label"
+			/>
+		</el-select>
+		<el-select
+			:model-value="modelValue.productLine"
+			class="oe-filters__select"
+			placeholder="全部产品线"
+			@update:model-value="onProductLine"
+		>
+			<el-option
+				v-for="opt in options.productLine"
+				:key="`p-${opt.value}`"
+				:value="opt.value"
+				:label="opt.label"
+			/>
+		</el-select>
+		<el-select
+			:model-value="modelValue.region"
+			class="oe-filters__select"
+			placeholder="全部区域"
+			@update:model-value="onRegion"
+		>
+			<el-option
+				v-for="opt in options.region"
+				:key="`r-${opt.value}`"
+				:value="opt.value"
+				:label="opt.label"
+			/>
+		</el-select>
+		<el-select
+			:model-value="modelValue.severity"
+			class="oe-filters__select"
+			placeholder="全部严重度"
+			@update:model-value="onSeverity"
+		>
+			<el-option
+				v-for="opt in options.severity"
+				:key="`v-${opt.value}`"
+				:value="opt.value"
+				:label="opt.label"
+			/>
+		</el-select>
+		<el-button class="oe-filters__reset" @click="onClear">重置</el-button>
+	</div>
+</template>
+
+<style scoped>
+.oe-filters {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 10px;
+	padding: 12px 14px;
+	border-radius: 12px;
+	background: var(--order-panel, rgba(25, 28, 34, 0.72));
+	border: 1px solid var(--order-border, rgba(144, 144, 151, 0.18));
+}
+
+.oe-filters__keyword {
+	width: 240px;
+	flex: 0 0 240px;
+}
+
+.oe-filters__select {
+	width: 150px;
+	flex: 0 0 150px;
+}
+
+.oe-filters__reset {
+	margin-left: auto;
+}
+
+@media (max-width: 768px) {
+	.oe-filters__keyword,
+	.oe-filters__select {
+		width: 100%;
+		flex: 1 1 100%;
+	}
+
+	.oe-filters__reset {
+		margin-left: 0;
+	}
+}
+</style>