Преглед на файлове

feat(s8): unify dashboard period filtering

- Add S8PeriodHelper backend cases: last_24h / last_7d / last_30d
- Extract useS8PeriodFilter composable + S8PeriodFilter shared component
- Extend period selector to 6 options: today/this_week/this_month/last_24h/last_7d/last_30d
- Top-bar period controls all data components on each S8 monitoring page
- Remove right-panel 24h/7d local window; trend title syncs with period
- S8DashboardPage and all 3 monitoring domain pages wired to period
YY968XX преди 1 седмица
родител
ревизия
e32ca76f1a
променени са 20 файла, в които са добавени 360 реда и са изтрити 171 реда
  1. 2 2
      Web/src/views/aidop/s8/api/s8DashboardApi.ts
  2. 5 5
      Web/src/views/aidop/s8/api/s8DeliveryMonitoringApi.ts
  3. 11 4
      Web/src/views/aidop/s8/api/s8MonitoringApi.ts
  4. 5 5
      Web/src/views/aidop/s8/api/s8ProductionMonitoringApi.ts
  5. 5 5
      Web/src/views/aidop/s8/api/s8SupplyMonitoringApi.ts
  6. 13 4
      Web/src/views/aidop/s8/dashboard/S8DashboardPage.vue
  7. 31 32
      Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue
  8. 15 2
      Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue
  9. 30 24
      Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue
  10. 30 24
      Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue
  11. 7 4
      Web/src/views/aidop/s8/monitoring/components/S8DeliveryTrendChart.vue
  12. 63 0
      Web/src/views/aidop/s8/monitoring/components/S8PeriodFilter.vue
  13. 8 5
      Web/src/views/aidop/s8/monitoring/components/S8ProductionTrendChart.vue
  14. 8 5
      Web/src/views/aidop/s8/monitoring/components/S8SupplyTrendChart.vue
  15. 8 0
      Web/src/views/aidop/s8/monitoring/useS8PeriodFilter.ts
  16. 5 4
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8DashboardController.cs
  17. 9 9
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8MonitoringController.cs
  18. 6 3
      server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8PeriodHelper.cs
  19. 31 8
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DashboardService.cs
  20. 68 26
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8MonitoringService.cs

+ 2 - 2
Web/src/views/aidop/s8/api/s8DashboardApi.ts

@@ -95,7 +95,7 @@ export const s8DashboardApi = {
 	overview: (params?: { tenantId?: number; factoryId?: number; beginTime?: string; endTime?: string; period?: S8Period }) =>
 		service.get<S8OverviewData>('/api/aidop/s8/dashboard/overview', { params }).then(unwrap),
 
-	trends: (params?: { tenantId?: number; factoryId?: number; days?: number }) =>
+	trends: (params?: { tenantId?: number; factoryId?: number; days?: number; period?: S8Period }) =>
 		service.get<{ date: string; count: number }[]>('/api/aidop/s8/dashboard/trends', { params }).then(unwrap),
 
 	distributions: (params?: { tenantId?: number; factoryId?: number; period?: S8Period }) =>
@@ -107,7 +107,7 @@ export const s8DashboardApi = {
 	deptBacklog: (params?: { tenantId?: number; factoryId?: number; period?: S8Period }) =>
 		service.get<S8DeptBacklogItem[]>('/api/aidop/s8/dashboard/dept-backlog', { params }).then(unwrap),
 
-	dimTrends: (params?: { tenantId?: number; factoryId?: number; dim?: string; days?: number }) =>
+	dimTrends: (params?: { tenantId?: number; factoryId?: number; dim?: string; days?: number; period?: S8Period }) =>
 		service.get<{ dates: string[]; series: { name: string; data: number[] }[] }>(
 			'/api/aidop/s8/dashboard/dim-trends', { params }
 		).then(unwrap),

+ 5 - 5
Web/src/views/aidop/s8/api/s8DeliveryMonitoringApi.ts

@@ -1,4 +1,4 @@
-import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary } from './s8MonitoringApi';
+import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8Period } from './s8MonitoringApi';
 import { loadConfiguredAnomalyTypes } from './s8MonitoringCellApi';
 
 export interface DeliveryAnomalyType {
@@ -40,16 +40,16 @@ const EMPTY_SUMMARY: S8MonitoringSummary = { total: 0, red: 0, yellow: 0, green:
 
 export const s8DeliveryMonitoringApi = {
 	/** 获取交付域汇总摘要;moduleCodes 由 page-config 派生(fallback 走 STAGE_META_FALLBACK)。空数组时直接返回空态,不退化为后端全量。 */
-	summary: (moduleCodes: string[]): Promise<S8MonitoringSummary> =>
+	summary: (moduleCodes: string[], period?: S8Period): Promise<S8MonitoringSummary> =>
 		moduleCodes.length === 0
 			? Promise.resolve({ ...EMPTY_SUMMARY, byModule: [] })
-			: s8MonitoringApi.summary({ moduleCode: moduleCodes.join(',') }),
+			: s8MonitoringApi.summary({ moduleCode: moduleCodes.join(','), period }),
 
 	/** 获取交付域各阶段订单统计;按 moduleCodes 过滤 byModule 全集。空数组时不调接口,直接返回空数组。 */
-	modules: (moduleCodes: string[]): Promise<S8ModuleOrderSummary[]> => {
+	modules: (moduleCodes: string[], period?: S8Period): Promise<S8ModuleOrderSummary[]> => {
 		if (moduleCodes.length === 0) return Promise.resolve([]);
 		const set = new Set(moduleCodes);
-		return s8MonitoringApi.orderGrid().then((data) =>
+		return s8MonitoringApi.orderGrid({ period }).then((data) =>
 			data.modules.filter((m) => set.has(m.moduleCode)),
 		);
 	},

+ 11 - 4
Web/src/views/aidop/s8/api/s8MonitoringApi.ts

@@ -25,7 +25,13 @@ export interface S8MonitoringSummary {
 	byModule: S8ModuleSummaryItem[];
 }
 
-export type S8Period = 'today' | 'this_week' | 'this_month';
+export type S8Period =
+	| 'today'
+	| 'this_week'
+	| 'this_month'
+	| 'last_24h'
+	| 'last_7d'
+	| 'last_30d';
 
 export interface S8MonitoringSummaryQuery {
 	tenantId?: number;
@@ -158,17 +164,17 @@ export const s8MonitoringApi = {
 			.get<S8OrderGridData>('/api/aidop/s8/monitoring/order-grid', { params })
 			.then(unwrap),
 
-	deliveryTrend: (params: { tenantId?: number; factoryId?: number; days?: number } = {}) =>
+	deliveryTrend: (params: { tenantId?: number; factoryId?: number; days?: number; period?: S8Period } = {}) =>
 		service
 			.get<S8DeliveryTrendData>('/api/aidop/s8/monitoring/delivery-trend', { params })
 			.then(unwrap),
 
-	productionTrend: (params: { tenantId?: number; factoryId?: number; days?: number } = {}) =>
+	productionTrend: (params: { tenantId?: number; factoryId?: number; days?: number; period?: S8Period } = {}) =>
 		service
 			.get<S8ProductionTrendData>('/api/aidop/s8/monitoring/production-trend', { params })
 			.then(unwrap),
 
-	supplyTrend: (params: { tenantId?: number; factoryId?: number; days?: number } = {}) =>
+	supplyTrend: (params: { tenantId?: number; factoryId?: number; days?: number; period?: S8Period } = {}) =>
 		service
 			.get<S8SupplyTrendData>('/api/aidop/s8/monitoring/supply-trend', { params })
 			.then(unwrap),
@@ -176,6 +182,7 @@ export const s8MonitoringApi = {
 	domainTypeMetrics: (params: {
 		domain: 'DELIVERY' | 'PRODUCTION' | 'SUPPLY';
 		window?: S8DomainTypeWindow;
+		period?: S8Period;
 		tenantId?: number;
 		factoryId?: number;
 	}) =>

+ 5 - 5
Web/src/views/aidop/s8/api/s8ProductionMonitoringApi.ts

@@ -1,4 +1,4 @@
-import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary } from './s8MonitoringApi';
+import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8Period } from './s8MonitoringApi';
 import { loadConfiguredAnomalyTypes } from './s8MonitoringCellApi';
 
 export interface ProductionAnomalyType {
@@ -40,16 +40,16 @@ const EMPTY_SUMMARY: S8MonitoringSummary = { total: 0, red: 0, yellow: 0, green:
 
 export const s8ProductionMonitoringApi = {
 	/** 获取生产域汇总摘要;moduleCodes 由 page-config 派生(fallback 走 STAGE_META_FALLBACK)。空数组时直接返回空态,不退化为后端全量。 */
-	summary: (moduleCodes: string[]): Promise<S8MonitoringSummary> =>
+	summary: (moduleCodes: string[], period?: S8Period): Promise<S8MonitoringSummary> =>
 		moduleCodes.length === 0
 			? Promise.resolve({ ...EMPTY_SUMMARY, byModule: [] })
-			: s8MonitoringApi.summary({ moduleCode: moduleCodes.join(',') }),
+			: s8MonitoringApi.summary({ moduleCode: moduleCodes.join(','), period }),
 
 	/** 获取生产域各阶段订单统计;按 moduleCodes 过滤 byModule 全集。空数组时不调接口,直接返回空数组。 */
-	modules: (moduleCodes: string[]): Promise<S8ModuleOrderSummary[]> => {
+	modules: (moduleCodes: string[], period?: S8Period): Promise<S8ModuleOrderSummary[]> => {
 		if (moduleCodes.length === 0) return Promise.resolve([]);
 		const set = new Set(moduleCodes);
-		return s8MonitoringApi.orderGrid().then((data) =>
+		return s8MonitoringApi.orderGrid({ period }).then((data) =>
 			data.modules.filter((m) => set.has(m.moduleCode)),
 		);
 	},

+ 5 - 5
Web/src/views/aidop/s8/api/s8SupplyMonitoringApi.ts

@@ -1,4 +1,4 @@
-import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary } from './s8MonitoringApi';
+import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8Period } from './s8MonitoringApi';
 import { loadConfiguredAnomalyTypes } from './s8MonitoringCellApi';
 
 export interface SupplyAnomalyType {
@@ -64,16 +64,16 @@ const EMPTY_SUMMARY: S8MonitoringSummary = { total: 0, red: 0, yellow: 0, green:
 
 export const s8SupplyMonitoringApi = {
 	/** 获取供应域汇总摘要;moduleCodes 由 page-config 派生(fallback 走 STAGE_META_FALLBACK)。空数组时直接返回空态,不退化为后端全量。 */
-	summary: (moduleCodes: string[]): Promise<S8MonitoringSummary> =>
+	summary: (moduleCodes: string[], period?: S8Period): Promise<S8MonitoringSummary> =>
 		moduleCodes.length === 0
 			? Promise.resolve({ ...EMPTY_SUMMARY, byModule: [] })
-			: s8MonitoringApi.summary({ moduleCode: moduleCodes.join(',') }),
+			: s8MonitoringApi.summary({ moduleCode: moduleCodes.join(','), period }),
 
 	/** 获取供应域各阶段订单统计;按 moduleCodes 过滤 byModule 全集。空数组时不调接口,直接返回空数组。 */
-	modules: (moduleCodes: string[]): Promise<S8ModuleOrderSummary[]> => {
+	modules: (moduleCodes: string[], period?: S8Period): Promise<S8ModuleOrderSummary[]> => {
 		if (moduleCodes.length === 0) return Promise.resolve([]);
 		const set = new Set(moduleCodes);
-		return s8MonitoringApi.orderGrid().then((data) =>
+		return s8MonitoringApi.orderGrid({ period }).then((data) =>
 			data.modules.filter((m) => set.has(m.moduleCode)),
 		);
 	},

+ 13 - 4
Web/src/views/aidop/s8/dashboard/S8DashboardPage.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts" name="aidopS8Dashboard">
 import * as echarts from 'echarts';
-import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
+import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
@@ -11,6 +11,8 @@ import {
 	type S8Distributions,
 	type S8OverviewData,
 } from '../api/s8DashboardApi';
+import type { S8Period } from '../api/s8MonitoringApi';
+import S8PeriodFilter from '../monitoring/components/S8PeriodFilter.vue';
 import service from '/@/utils/request';
 import { Local } from '/@/utils/storage';
 
@@ -143,6 +145,9 @@ function savePreferences(config: DashboardPreferences) {
 
 const preferences = reactive<DashboardPreferences>(normalizePreferences(Local.get(DASHBOARD_PREFERENCE_KEY)));
 
+// ─── 时间筛选 ─────────────────────────────────────────────────
+const dashboardPeriod = shallowRef<S8Period | undefined>('this_month');
+
 // ─── 状态 ────────────────────────────────────────────────────
 const loading = ref(false);
 const lastRefreshedAt = ref('');
@@ -397,14 +402,15 @@ async function loadOverview() {
 	const data = await s8DashboardApi.overview({
 		beginTime: toLocalDateTimeString(filter.dateStart, false),
 		endTime: toLocalDateTimeString(filter.dateEnd, true),
+		period: dashboardPeriod.value,
 	});
 	Object.assign(overview, data);
 }
 
 async function loadChartData() {
 	const [dist, backlog] = await Promise.all([
-		s8DashboardApi.distributions(),
-		s8DashboardApi.deptBacklog(),
+		s8DashboardApi.distributions({ period: dashboardPeriod.value }),
+		s8DashboardApi.deptBacklog({ period: dashboardPeriod.value }),
 	]);
 	distributions.value = dist;
 	deptBacklog.value   = backlog;
@@ -413,7 +419,7 @@ async function loadChartData() {
 async function loadDimTrends() {
 	dimTrendsData.value = await s8DashboardApi.dimTrends({
 		dim: panelToDim(activePanel.value),
-		days: preferences.trendDays,
+		period: dashboardPeriod.value,
 	});
 }
 
@@ -561,6 +567,8 @@ function onDetailPageChange(page: number) {
 	void loadDetail();
 }
 
+watch(dashboardPeriod, () => { void loadOverview(); void loadChartData(); void loadDimTrends(); });
+
 onMounted(() => void loadAll());
 onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
 </script>
@@ -575,6 +583,7 @@ onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
 					<span class="s8-toolbar__hint">支持按面板维护筛选条件</span>
 				</div>
 				<div class="s8-toolbar__actions">
+					<S8PeriodFilter v-model="dashboardPeriod" />
 					<div v-if="preferences.sections.showRefreshBar" class="s8-refresh-bar">
 						<span class="s8-refresh-time">最近刷新:{{ lastRefreshedAt || '—' }}</span>
 						<el-button size="small" plain @click="onQuery">刷新</el-button>

+ 31 - 32
Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue

@@ -3,6 +3,7 @@
 		<button v-if="!editMode" class="anomaly-monitor__btn anomaly-monitor__btn--edit" type="button" @click="enterEdit">编辑布局</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--config" type="button" @click="configDrawerVisible = true">卡片配置</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<S8PeriodFilter class="anomaly-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
 		<div v-if="loadState === 'error'" class="anomaly-monitor__error-banner" role="alert">
@@ -33,22 +34,7 @@
 					<div class="section-title">
 						<div class="section-title__bar" />
 						<h2 class="section-title__text">交付异常类型</h2>
-						<!-- S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡支持 近24h / 近7日 切换;同窗口同分母。 -->
-						<div class="anomaly-monitor__window-toggle" role="group" aria-label="时间窗口">
-							<button
-								type="button"
-								class="anomaly-monitor__window-btn"
-								:class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_24H' }"
-								@click="setSidebarWindow('LAST_24H')"
-							>近24h</button>
-							<button
-								type="button"
-								class="anomaly-monitor__window-btn"
-								:class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_7D' }"
-								@click="setSidebarWindow('LAST_7D')"
-							>近7日</button>
 						</div>
-					</div>
 
 					<div class="anomaly-monitor__type-list">
 						<article
@@ -86,7 +72,7 @@
 				<section class="anomaly-monitor__analysis-panel glass-panel">
 					<!-- S8-DELIVERY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:原「交付异常多维分析」三连卡与右侧「交付异常类型」重复,
 					     替换为近 7 日交付异常趋势图(接口 /monitoring/delivery-trend)。 -->
-					<S8DeliveryTrendChart :data="deliveryTrendData" />
+					<S8DeliveryTrendChart :data="deliveryTrendData" :title="trendTitle('交付')" />
 				</section>
 			</template>
 		</S8MonitoringResizableShell>
@@ -106,12 +92,12 @@
 </template>
 
 <script setup lang="ts" name="aidopS8MonitoringDelivery">
-import { computed, onMounted, reactive, shallowRef, type CSSProperties, type Component } from 'vue';
+import { computed, onMounted, reactive, shallowRef, watch, type CSSProperties, type Component } from 'vue';
 import { Checked, Van } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
 import { s8DeliveryMonitoringApi } from '../api/s8DeliveryMonitoringApi';
-import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8DeliveryTrendData, type S8DomainTypeMetrics, type S8DomainTypeWindow } from '../api/s8MonitoringApi';
+import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8DeliveryTrendData, type S8DomainTypeMetrics } from '../api/s8MonitoringApi';
 import S8MonitoringResizableShell from './components/S8MonitoringResizableShell.vue';
 import S8MonitoringModulesGrid from './components/S8MonitoringModulesGrid.vue';
 import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
@@ -121,6 +107,8 @@ import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8PageConfigDriver } from './useS8PageConfigDriver';
+import { useS8PeriodFilter } from './useS8PeriodFilter';
+import S8PeriodFilter from './components/S8PeriodFilter.vue';
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 const DELIVERY_DEMO_LAYOUT: S8LayoutSchema = {
@@ -233,9 +221,8 @@ const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
 
 const effectiveStageMeta = computed(() => pageConfig.effectiveStageMeta(STAGE_META_FALLBACK));
 
-// S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡走新统一接口 /monitoring/domain-type-metrics,
-// 同窗口同分母。废弃旧 loadConfiguredAnomalyTypes(混用 OPEN_COUNT/24H + AVG_DURATION/CLOSE_RATE/7D)。
-const sidebarWindow = shallowRef<S8DomainTypeWindow>('LAST_24H');
+// S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡走新统一接口 /monitoring/domain-type-metrics。
+// 顶部 period 统一控制时间口径,sidebarWindow 已移除。
 const sidebarMetrics = shallowRef<S8DomainTypeMetrics | null>(null);
 const anomalyTypes = computed(() => sidebarMetrics.value?.items ?? ANOMALY_TYPES_FALLBACK.map((d) => ({
 	key: d.key,
@@ -252,7 +239,7 @@ async function loadSidebarMetrics() {
 	try {
 		sidebarMetrics.value = await s8MonitoringApi.domainTypeMetrics({
 			domain: 'DELIVERY',
-			window: sidebarWindow.value,
+			period: period.value,
 			tenantId: 1,
 			factoryId: 1,
 		});
@@ -262,12 +249,6 @@ async function loadSidebarMetrics() {
 	}
 }
 
-function setSidebarWindow(w: S8DomainTypeWindow) {
-	if (sidebarWindow.value === w) return;
-	sidebarWindow.value = w;
-	void loadSidebarMetrics();
-}
-
 async function loadPageConfig() {
 	await pageConfig.load();
 }
@@ -334,11 +315,11 @@ const stageKeys = computed(() => stageCards.value.map((c) => c.code));
 // S8-DELIVERY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:原 categoryCards / categoryKeys 仅服务被替换的多维分析区,已移除。
 const totalAnomalies = computed(() => sidebarMetrics.value?.total ?? 0);
 
-// S8-DELIVERY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:近 7 日交付异常趋势数据。
+// S8-DELIVERY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:交付异常趋势数据,跟随顶部 period
 const deliveryTrendData = shallowRef<S8DeliveryTrendData | null>(null);
 async function loadDeliveryTrend() {
 	try {
-		deliveryTrendData.value = await s8MonitoringApi.deliveryTrend({ tenantId: 1, factoryId: 1, days: 7 });
+		deliveryTrendData.value = await s8MonitoringApi.deliveryTrend({ tenantId: 1, factoryId: 1, period: period.value });
 	} catch (err) {
 		console.error('[S8MonitoringDeliveryPage] loadDeliveryTrend failed:', err);
 		deliveryTrendData.value = null;
@@ -373,7 +354,16 @@ function anomalyToneClass(closeRate: number | null | undefined) {
 	return `anomaly-type-card--${resolveTone(closeRate)}`;
 }
 
+// ─── 工具(趋势标题)──────────────────────────────────────────────────────
+const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
+function trendTitle(domain: string) {
+	const label = PERIOD_LABEL[period.value ?? ''] ?? '本月';
+	return `${label}${domain}异常趋势`;
+}
+
 // ─── 数据加载 ──────────────────────────────────────────────────────────────
+const { period } = useS8PeriodFilter();
+
 const loadState = shallowRef<'idle' | 'loading' | 'ok' | 'error'>('idle');
 const loadError = shallowRef<string>('');
 
@@ -384,8 +374,8 @@ async function loadData() {
 		// moduleCodes 由 page-config 派生;fallback 时 effectiveStageMeta 退到 STAGE_META_FALLBACK,行为与历史硬编码一致。
 		const moduleCodes = effectiveStageMeta.value.map((m) => m.code);
 		const [summaryData, modulesData] = await Promise.all([
-			s8DeliveryMonitoringApi.summary(moduleCodes),
-			s8DeliveryMonitoringApi.modules(moduleCodes),
+			s8DeliveryMonitoringApi.summary(moduleCodes, period.value),
+			s8DeliveryMonitoringApi.modules(moduleCodes, period.value),
 		]);
 		Object.assign(summary, summaryData);
 		moduleData.list = modulesData;
@@ -401,6 +391,8 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState([]); }
 
+watch(period, () => { void loadData(); void loadDeliveryTrend(); void loadSidebarMetrics(); });
+
 onMounted(async () => {
 	// 1. 先拉 page-config(决定卡片结构)
 	await loadPageConfig();
@@ -453,6 +445,13 @@ onMounted(async () => {
 	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 18px 36px rgba(5, 10, 18, 0.24);
 }
 
+.anomaly-monitor__period-filter {
+	position: absolute;
+	top: 10px;
+	left: 12px;
+	z-index: 12;
+}
+
 .anomaly-monitor__btn {
 	position: absolute;
 	top: 10px;

+ 15 - 2
Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue

@@ -3,6 +3,7 @@
 		<button v-if="!editMode" class="s8-monitor__edit-btn" type="button" @click="enterEdit">编辑布局</button>
 		<button class="s8-monitor__config-btn" type="button" @click="configDrawerVisible = true">卡片配置</button>
 		<button v-if="!editMode" class="s8-monitor__restore-btn" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<S8PeriodFilter class="s8-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
 		<div v-if="loadState === 'error'" class="s8-monitor__error-banner" role="alert">
@@ -84,6 +85,8 @@ import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS9DeptConfig } from './useS9DeptConfig';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8PageConfigDriver } from './useS8PageConfigDriver';
+import { useS8PeriodFilter } from './useS8PeriodFilter';
+import S8PeriodFilter from './components/S8PeriodFilter.vue';
 
 interface StageMeta {
 	code: string;
@@ -578,6 +581,8 @@ function formatPercent(value: number, decimals = 0) {
 	return `${clampPercent(value).toFixed(decimals)}%`;
 }
 
+const { period } = useS8PeriodFilter();
+
 const loadState = shallowRef<'idle' | 'loading' | 'ok' | 'error'>('idle');
 const loadError = shallowRef<string>('');
 
@@ -586,8 +591,8 @@ async function loadData() {
 	loadError.value = '';
 	try {
 		const [summaryData, gridRaw] = await Promise.all([
-			s8MonitoringApi.summary({ tenantId: 1, factoryId: 1 }),
-			s8MonitoringApi.orderGrid(),
+			s8MonitoringApi.summary({ tenantId: 1, factoryId: 1, period: period.value }),
+			s8MonitoringApi.orderGrid({ period: period.value }),
 		]);
 		Object.assign(summary, summaryData);
 		Object.assign(gridData, {
@@ -653,6 +658,7 @@ watch(
 	},
 );
 
+watch(period, () => void loadData());
 watch(categoryCards, (cats) => { initializeFromCategories(cats); });
 watch(deptCards, (depts) => {
 	initializeFromDepts(depts);
@@ -725,6 +731,13 @@ watch(deptCards, (depts) => {
 	pointer-events: none;
 }
 
+.s8-monitor__period-filter {
+	position: absolute;
+	top: 10px;
+	left: 12px;
+	z-index: 12;
+}
+
 .s8-monitor__edit-btn {
 	position: absolute;
 	top: 10px;

+ 30 - 24
Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue

@@ -3,6 +3,7 @@
 		<button v-if="!editMode" class="anomaly-monitor__btn anomaly-monitor__btn--edit" type="button" @click="enterEdit">编辑布局</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--config" type="button" @click="configDrawerVisible = true">卡片配置</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<S8PeriodFilter class="anomaly-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
 		<div v-if="loadState === 'error'" class="anomaly-monitor__error-banner" role="alert">
@@ -33,16 +34,7 @@
 					<div class="section-title">
 						<div class="section-title__bar" />
 						<h2 class="section-title__text">生产异常类型</h2>
-						<!-- S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡支持 近24h / 近7日 切换;同窗口同分母。 -->
-						<div class="anomaly-monitor__window-toggle" role="group" aria-label="时间窗口">
-							<button type="button" class="anomaly-monitor__window-btn"
-								:class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_24H' }"
-								@click="setSidebarWindow('LAST_24H')">近24h</button>
-							<button type="button" class="anomaly-monitor__window-btn"
-								:class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_7D' }"
-								@click="setSidebarWindow('LAST_7D')">近7日</button>
 						</div>
-					</div>
 
 					<div class="anomaly-monitor__type-list">
 						<article
@@ -80,7 +72,7 @@
 				<section class="anomaly-monitor__analysis-panel glass-panel">
 					<!-- S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:原「生产异常多维分析」三连卡与右侧「生产异常类型」重复,
 					     替换为近 7 日生产异常趋势图(接口 /monitoring/production-trend)。 -->
-					<S8ProductionTrendChart :data="productionTrendData" />
+					<S8ProductionTrendChart :data="productionTrendData" :title="trendTitle('生产')" />
 				</section>
 			</template>
 		</S8MonitoringResizableShell>
@@ -100,12 +92,12 @@
 </template>
 
 <script setup lang="ts" name="aidopS8MonitoringProduction">
-import { computed, onMounted, reactive, shallowRef, type CSSProperties, type Component } from 'vue';
+import { computed, onMounted, reactive, shallowRef, watch, type CSSProperties, type Component } from 'vue';
 import { TrendCharts, Box } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
 import { s8ProductionMonitoringApi } from '../api/s8ProductionMonitoringApi';
-import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8ProductionTrendData, type S8DomainTypeMetrics, type S8DomainTypeWindow } from '../api/s8MonitoringApi';
+import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8ProductionTrendData, type S8DomainTypeMetrics } from '../api/s8MonitoringApi';
 import S8MonitoringResizableShell from './components/S8MonitoringResizableShell.vue';
 import S8MonitoringModulesGrid from './components/S8MonitoringModulesGrid.vue';
 import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
@@ -115,6 +107,8 @@ import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8PageConfigDriver } from './useS8PageConfigDriver';
+import { useS8PeriodFilter } from './useS8PeriodFilter';
+import S8PeriodFilter from './components/S8PeriodFilter.vue';
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 const PRODUCTION_DEMO_LAYOUT: S8LayoutSchema = {
@@ -225,7 +219,7 @@ const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
 const effectiveStageMeta = computed(() => pageConfig.effectiveStageMeta(STAGE_META_FALLBACK));
 
 // S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡走新统一接口 /monitoring/domain-type-metrics。
-const sidebarWindow = shallowRef<S8DomainTypeWindow>('LAST_24H');
+// 顶部 period 统一控制时间口径,sidebarWindow 已移除。
 const sidebarMetrics = shallowRef<S8DomainTypeMetrics | null>(null);
 const anomalyTypes = computed(() => sidebarMetrics.value?.items ?? ANOMALY_TYPES_FALLBACK.map((d) => ({
 	key: d.key,
@@ -242,7 +236,7 @@ async function loadSidebarMetrics() {
 	try {
 		sidebarMetrics.value = await s8MonitoringApi.domainTypeMetrics({
 			domain: 'PRODUCTION',
-			window: sidebarWindow.value,
+			period: period.value,
 			tenantId: 1,
 			factoryId: 1,
 		});
@@ -252,12 +246,6 @@ async function loadSidebarMetrics() {
 	}
 }
 
-function setSidebarWindow(w: S8DomainTypeWindow) {
-	if (sidebarWindow.value === w) return;
-	sidebarWindow.value = w;
-	void loadSidebarMetrics();
-}
-
 async function loadPageConfig() {
 	await pageConfig.load();
 }
@@ -320,11 +308,11 @@ const stageKeys = computed(() => stageCards.value.map((c) => c.code));
 // S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:原 categoryCards / categoryKeys 仅服务被替换的多维分析区,已移除。
 const totalAnomalies = computed(() => sidebarMetrics.value?.total ?? 0);
 
-// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:近 7 日生产异常趋势数据。
+// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:生产异常趋势数据,跟随顶部 period
 const productionTrendData = shallowRef<S8ProductionTrendData | null>(null);
 async function loadProductionTrend() {
 	try {
-		productionTrendData.value = await s8MonitoringApi.productionTrend({ tenantId: 1, factoryId: 1, days: 7 });
+		productionTrendData.value = await s8MonitoringApi.productionTrend({ tenantId: 1, factoryId: 1, period: period.value });
 	} catch (err) {
 		console.error('[S8MonitoringProductionPage] loadProductionTrend failed:', err);
 		productionTrendData.value = null;
@@ -360,7 +348,16 @@ function anomalyToneClass(closeRate: number | null | undefined) {
 	return `anomaly-type-card--${resolveTone(closeRate)}`;
 }
 
+// ─── 工具(趋势标题)──────────────────────────────────────────────────────
+const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
+function trendTitle(domain: string) {
+	const label = PERIOD_LABEL[period.value ?? ''] ?? '本月';
+	return `${label}${domain}异常趋势`;
+}
+
 // ─── 数据加载 ──────────────────────────────────────────────────────────────
+const { period } = useS8PeriodFilter();
+
 const loadState = shallowRef<'idle' | 'loading' | 'ok' | 'error'>('idle');
 const loadError = shallowRef<string>('');
 
@@ -371,8 +368,8 @@ async function loadData() {
 		// moduleCodes 由 page-config 派生;fallback 时 effectiveStageMeta 退到 STAGE_META_FALLBACK,行为与历史硬编码一致。
 		const moduleCodes = effectiveStageMeta.value.map((m) => m.code);
 		const [summaryData, modulesData] = await Promise.all([
-			s8ProductionMonitoringApi.summary(moduleCodes),
-			s8ProductionMonitoringApi.modules(moduleCodes),
+			s8ProductionMonitoringApi.summary(moduleCodes, period.value),
+			s8ProductionMonitoringApi.modules(moduleCodes, period.value),
 		]);
 		Object.assign(summary, summaryData);
 		moduleData.list = modulesData;
@@ -388,6 +385,8 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState([]); }
 
+watch(period, () => { void loadData(); void loadProductionTrend(); void loadSidebarMetrics(); });
+
 onMounted(async () => {
 	await loadPageConfig();
 	initializeFromCards(stageCards.value);
@@ -437,6 +436,13 @@ onMounted(async () => {
 	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 18px 36px rgba(5, 10, 18, 0.24);
 }
 
+.anomaly-monitor__period-filter {
+	position: absolute;
+	top: 10px;
+	left: 12px;
+	z-index: 12;
+}
+
 .anomaly-monitor__btn {
 	position: absolute;
 	top: 10px;

+ 30 - 24
Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue

@@ -3,6 +3,7 @@
 		<button v-if="!editMode" class="anomaly-monitor__btn anomaly-monitor__btn--edit" type="button" @click="enterEdit">编辑布局</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--config" type="button" @click="configDrawerVisible = true">卡片配置</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<S8PeriodFilter class="anomaly-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
 		<div v-if="loadState === 'error'" class="anomaly-monitor__error-banner" role="alert">
@@ -33,16 +34,7 @@
 					<div class="section-title">
 						<div class="section-title__bar" />
 						<h2 class="section-title__text">供应异常类型</h2>
-						<!-- S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡支持 近24h / 近7日 切换;同窗口同分母。 -->
-						<div class="anomaly-monitor__window-toggle" role="group" aria-label="时间窗口">
-							<button type="button" class="anomaly-monitor__window-btn"
-								:class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_24H' }"
-								@click="setSidebarWindow('LAST_24H')">近24h</button>
-							<button type="button" class="anomaly-monitor__window-btn"
-								:class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_7D' }"
-								@click="setSidebarWindow('LAST_7D')">近7日</button>
 						</div>
-					</div>
 
 					<div class="anomaly-monitor__type-list">
 						<article
@@ -80,7 +72,7 @@
 				<section class="anomaly-monitor__analysis-panel glass-panel">
 					<!-- S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:原「供应异常多维分析」7 张卡与右侧「供应异常类型」重复,
 					     替换为近 7 日供应异常趋势图(接口 /monitoring/supply-trend,前端按近7日总量选 Top5 非零类型展示)。 -->
-					<S8SupplyTrendChart :data="supplyTrendData" />
+					<S8SupplyTrendChart :data="supplyTrendData" :title="trendTitle('供应')" />
 				</section>
 			</template>
 		</S8MonitoringResizableShell>
@@ -100,12 +92,12 @@
 </template>
 
 <script setup lang="ts" name="aidopS8MonitoringSupply">
-import { computed, onMounted, reactive, shallowRef, type CSSProperties, type Component } from 'vue';
+import { computed, onMounted, reactive, shallowRef, watch, type CSSProperties, type Component } from 'vue';
 import { ShoppingBag, Tools, DataAnalysis } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
 import { s8SupplyMonitoringApi } from '../api/s8SupplyMonitoringApi';
-import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8SupplyTrendData, type S8DomainTypeMetrics, type S8DomainTypeWindow } from '../api/s8MonitoringApi';
+import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8SupplyTrendData, type S8DomainTypeMetrics } from '../api/s8MonitoringApi';
 import S8MonitoringResizableShell from './components/S8MonitoringResizableShell.vue';
 import S8MonitoringModulesGrid from './components/S8MonitoringModulesGrid.vue';
 import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
@@ -115,6 +107,8 @@ import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8PageConfigDriver } from './useS8PageConfigDriver';
+import { useS8PeriodFilter } from './useS8PeriodFilter';
+import S8PeriodFilter from './components/S8PeriodFilter.vue';
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 // 供应异常 analysis 7 张卡,3+3+1 三行布局(col-num=12,每张 w=4;CategoryGrid minW=3 允许窄屏下缩到 25%)
@@ -236,7 +230,7 @@ const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
 const effectiveStageMeta = computed(() => pageConfig.effectiveStageMeta(STAGE_META_FALLBACK));
 
 // S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡走新统一接口 /monitoring/domain-type-metrics。
-const sidebarWindow = shallowRef<S8DomainTypeWindow>('LAST_24H');
+// 顶部 period 统一控制时间口径,sidebarWindow 已移除。
 const sidebarMetrics = shallowRef<S8DomainTypeMetrics | null>(null);
 const anomalyTypes = computed(() => sidebarMetrics.value?.items ?? ANOMALY_TYPES_FALLBACK.map((d) => ({
 	key: d.key,
@@ -253,7 +247,7 @@ async function loadSidebarMetrics() {
 	try {
 		sidebarMetrics.value = await s8MonitoringApi.domainTypeMetrics({
 			domain: 'SUPPLY',
-			window: sidebarWindow.value,
+			period: period.value,
 			tenantId: 1,
 			factoryId: 1,
 		});
@@ -263,12 +257,6 @@ async function loadSidebarMetrics() {
 	}
 }
 
-function setSidebarWindow(w: S8DomainTypeWindow) {
-	if (sidebarWindow.value === w) return;
-	sidebarWindow.value = w;
-	void loadSidebarMetrics();
-}
-
 async function loadPageConfig() {
 	await pageConfig.load();
 }
@@ -332,11 +320,11 @@ const stageKeys = computed(() => stageCards.value.map((c) => c.code));
 // S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:原 categoryCards / categoryKeys 仅服务被替换的多维分析区,已移除。
 const totalAnomalies = computed(() => sidebarMetrics.value?.total ?? 0);
 
-// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:近 7 日供应异常趋势数据。
+// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:供应异常趋势数据,跟随顶部 period
 const supplyTrendData = shallowRef<S8SupplyTrendData | null>(null);
 async function loadSupplyTrend() {
 	try {
-		supplyTrendData.value = await s8MonitoringApi.supplyTrend({ tenantId: 1, factoryId: 1, days: 7 });
+		supplyTrendData.value = await s8MonitoringApi.supplyTrend({ tenantId: 1, factoryId: 1, period: period.value });
 	} catch (err) {
 		console.error('[S8MonitoringSupplyPage] loadSupplyTrend failed:', err);
 		supplyTrendData.value = null;
@@ -371,7 +359,16 @@ function anomalyToneClass(closeRate: number | null | undefined) {
 	return `anomaly-type-card--${resolveTone(closeRate)}`;
 }
 
+// ─── 工具(趋势标题)──────────────────────────────────────────────────────
+const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
+function trendTitle(domain: string) {
+	const label = PERIOD_LABEL[period.value ?? ''] ?? '本月';
+	return `${label}${domain}异常趋势`;
+}
+
 // ─── 数据加载 ──────────────────────────────────────────────────────────────
+const { period } = useS8PeriodFilter();
+
 const loadState = shallowRef<'idle' | 'loading' | 'ok' | 'error'>('idle');
 const loadError = shallowRef<string>('');
 
@@ -382,8 +379,8 @@ async function loadData() {
 		// moduleCodes 由 page-config 派生;fallback 时 effectiveStageMeta 退到 STAGE_META_FALLBACK,行为与历史硬编码一致。
 		const moduleCodes = effectiveStageMeta.value.map((m) => m.code);
 		const [summaryData, modulesData] = await Promise.all([
-			s8SupplyMonitoringApi.summary(moduleCodes),
-			s8SupplyMonitoringApi.modules(moduleCodes),
+			s8SupplyMonitoringApi.summary(moduleCodes, period.value),
+			s8SupplyMonitoringApi.modules(moduleCodes, period.value),
 		]);
 		Object.assign(summary, summaryData);
 		moduleData.list = modulesData;
@@ -399,6 +396,8 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState([]); }
 
+watch(period, () => { void loadData(); void loadSupplyTrend(); void loadSidebarMetrics(); });
+
 onMounted(async () => {
 	await loadPageConfig();
 	initializeFromCards(stageCards.value);
@@ -448,6 +447,13 @@ onMounted(async () => {
 	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 18px 36px rgba(5, 10, 18, 0.24);
 }
 
+.anomaly-monitor__period-filter {
+	position: absolute;
+	top: 10px;
+	left: 12px;
+	z-index: 12;
+}
+
 .anomaly-monitor__btn {
 	position: absolute;
 	top: 10px;

+ 7 - 4
Web/src/views/aidop/s8/monitoring/components/S8DeliveryTrendChart.vue

@@ -1,12 +1,12 @@
 <template>
 	<div class="s8-delivery-trend">
 		<div class="s8-delivery-trend__header">
-			<span class="s8-delivery-trend__title">近7日交付异常趋势</span>
+			<span class="s8-delivery-trend__title">{{ title }}</span>
 		</div>
 
 		<div v-if="isEmpty" class="s8-delivery-trend__empty">
 			<div class="s8-delivery-trend__empty-title">暂无趋势数据</div>
-			<div class="s8-delivery-trend__empty-desc">最近7天未统计到交付异常记录</div>
+			<div class="s8-delivery-trend__empty-desc">当前时段未统计到交付异常记录</div>
 		</div>
 		<scEcharts
 			v-else
@@ -18,11 +18,11 @@
 
 		<div class="s8-delivery-trend__summary">
 			<div class="s8-delivery-trend__summary-card">
-				<div class="s8-delivery-trend__summary-label">7日峰值</div>
+				<div class="s8-delivery-trend__summary-label">峰值</div>
 				<div class="s8-delivery-trend__summary-value">{{ peakText }}</div>
 			</div>
 			<div class="s8-delivery-trend__summary-card">
-				<div class="s8-delivery-trend__summary-label">7日均值</div>
+				<div class="s8-delivery-trend__summary-label">均值</div>
 				<div class="s8-delivery-trend__summary-value">{{ avgText }}</div>
 			</div>
 			<div class="s8-delivery-trend__summary-card">
@@ -40,8 +40,11 @@ import type { S8DeliveryTrendData } from '../../api/s8MonitoringApi';
 
 const props = defineProps<{
 	data: S8DeliveryTrendData | null;
+	title?: string;
 }>();
 
+const title = computed(() => props.title ?? '本月交付异常趋势');
+
 const chartRef = ref();
 
 const days = computed(() => props.data?.days ?? []);

+ 63 - 0
Web/src/views/aidop/s8/monitoring/components/S8PeriodFilter.vue

@@ -0,0 +1,63 @@
+<template>
+	<div class="s8-period-filter" role="group" aria-label="时间范围">
+		<button
+			v-for="opt in OPTIONS"
+			:key="opt.value"
+			type="button"
+			class="s8-period-filter__btn"
+			:class="{ 's8-period-filter__btn--active': modelValue === opt.value }"
+			@click="emit('update:modelValue', opt.value)"
+		>{{ opt.label }}</button>
+	</div>
+</template>
+
+<script setup lang="ts">
+import type { S8Period } from '../../api/s8MonitoringApi';
+
+defineProps<{ modelValue?: S8Period }>();
+const emit = defineEmits<{ (e: 'update:modelValue', v: S8Period): void }>();
+
+const OPTIONS = [
+	{ value: 'today' as S8Period, label: '本日' },
+	{ value: 'this_week' as S8Period, label: '本周' },
+	{ value: 'this_month' as S8Period, label: '本月' },
+	{ value: 'last_24h' as S8Period, label: '过去24h' },
+	{ value: 'last_7d' as S8Period, label: '过去7天' },
+	{ value: 'last_30d' as S8Period, label: '过去30天' },
+] as const;
+</script>
+
+<style scoped>
+.s8-period-filter {
+	display: inline-flex;
+	align-items: center;
+	padding: 2px;
+	background: rgba(11, 14, 20, 0.55);
+	border: 1px solid rgba(123, 208, 255, 0.18);
+	border-radius: 999px;
+}
+
+.s8-period-filter__btn {
+	height: 26px;
+	padding: 0 8px;
+	font-size: 12px;
+	font-weight: 500;
+	color: #c6c6cd;
+	background: transparent;
+	border: none;
+	border-radius: 999px;
+	cursor: pointer;
+	letter-spacing: 0.04em;
+	transition: background 0.18s ease, color 0.18s ease;
+}
+
+.s8-period-filter__btn:hover {
+	color: #e1e6ee;
+}
+
+.s8-period-filter__btn--active {
+	color: #0b0e14;
+	background: linear-gradient(90deg, #7bd0ff 0%, #6de039 100%);
+	font-weight: 600;
+}
+</style>

+ 8 - 5
Web/src/views/aidop/s8/monitoring/components/S8ProductionTrendChart.vue

@@ -1,13 +1,13 @@
 <template>
 	<div class="s8-prod-trend">
 		<div class="s8-prod-trend__header">
-			<span class="s8-prod-trend__title">近7日生产异常趋势</span>
-			<span class="s8-prod-trend__subtitle">统计近7天生产相关异常数量变化,辅助识别生产过程风险波动。</span>
+			<span class="s8-prod-trend__title">{{ title }}</span>
+			<span class="s8-prod-trend__subtitle">统计当前时段生产相关异常数量变化,辅助识别生产过程风险波动。</span>
 		</div>
 
 		<div v-if="isEmpty" class="s8-prod-trend__empty">
 			<div class="s8-prod-trend__empty-title">暂无趋势数据</div>
-			<div class="s8-prod-trend__empty-desc">最近7天未统计到生产异常记录</div>
+			<div class="s8-prod-trend__empty-desc">当前时段未统计到生产异常记录</div>
 		</div>
 		<scEcharts
 			v-else
@@ -19,11 +19,11 @@
 
 		<div class="s8-prod-trend__summary">
 			<div class="s8-prod-trend__summary-card">
-				<div class="s8-prod-trend__summary-label">7日峰值</div>
+				<div class="s8-prod-trend__summary-label">峰值</div>
 				<div class="s8-prod-trend__summary-value">{{ peakText }}</div>
 			</div>
 			<div class="s8-prod-trend__summary-card">
-				<div class="s8-prod-trend__summary-label">7日均值</div>
+				<div class="s8-prod-trend__summary-label">均值</div>
 				<div class="s8-prod-trend__summary-value">{{ avgText }}</div>
 			</div>
 			<div class="s8-prod-trend__summary-card">
@@ -41,8 +41,11 @@ import type { S8ProductionTrendData } from '../../api/s8MonitoringApi';
 
 const props = defineProps<{
 	data: S8ProductionTrendData | null;
+	title?: string;
 }>();
 
+const title = computed(() => props.title ?? '本月生产异常趋势');
+
 const chartRef = ref();
 
 const days = computed(() => props.data?.days ?? []);

+ 8 - 5
Web/src/views/aidop/s8/monitoring/components/S8SupplyTrendChart.vue

@@ -1,13 +1,13 @@
 <template>
 	<div class="s8-supply-trend">
 		<div class="s8-supply-trend__header">
-			<span class="s8-supply-trend__title">近7日供应异常趋势</span>
-			<span class="s8-supply-trend__subtitle">统计近7天供应相关异常数量变化,辅助识别供应风险波动。</span>
+			<span class="s8-supply-trend__title">{{ title }}</span>
+			<span class="s8-supply-trend__subtitle">统计当前时段供应相关异常数量变化,辅助识别供应风险波动。</span>
 		</div>
 
 		<div v-if="isEmpty" class="s8-supply-trend__empty">
 			<div class="s8-supply-trend__empty-title">暂无趋势数据</div>
-			<div class="s8-supply-trend__empty-desc">最近7天未统计到供应异常记录</div>
+			<div class="s8-supply-trend__empty-desc">当前时段未统计到供应异常记录</div>
 		</div>
 		<scEcharts
 			v-else
@@ -19,11 +19,11 @@
 
 		<div class="s8-supply-trend__summary">
 			<div class="s8-supply-trend__summary-card">
-				<div class="s8-supply-trend__summary-label">7日峰值</div>
+				<div class="s8-supply-trend__summary-label">峰值</div>
 				<div class="s8-supply-trend__summary-value">{{ peakText }}</div>
 			</div>
 			<div class="s8-supply-trend__summary-card">
-				<div class="s8-supply-trend__summary-label">7日均值</div>
+				<div class="s8-supply-trend__summary-label">均值</div>
 				<div class="s8-supply-trend__summary-value">{{ avgText }}</div>
 			</div>
 			<div class="s8-supply-trend__summary-card">
@@ -41,8 +41,11 @@ import type { S8SupplyTrendData, S8SupplyTrendDay } from '../../api/s8Monitoring
 
 const props = defineProps<{
 	data: S8SupplyTrendData | null;
+	title?: string;
 }>();
 
+const title = computed(() => props.title ?? '本月供应异常趋势');
+
 const chartRef = ref();
 
 const days = computed<S8SupplyTrendDay[]>(() => props.data?.days ?? []);

+ 8 - 0
Web/src/views/aidop/s8/monitoring/useS8PeriodFilter.ts

@@ -0,0 +1,8 @@
+import { shallowRef } from 'vue';
+import type { S8Period } from '../api/s8MonitoringApi';
+
+export function useS8PeriodFilter() {
+	const period = shallowRef<S8Period | undefined>('this_month');
+	const setPeriod = (next: S8Period) => { period.value = next; };
+	return { period, setPeriod };
+}

+ 5 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8DashboardController.cs

@@ -34,8 +34,8 @@ public class AdoS8DashboardController : ControllerBase
 
     [HttpGet("trends")]
     public async Task<IActionResult> TrendsAsync([FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
-        [FromQuery] int days = 14) =>
-        Ok(await _svc.GetTrendsAsync(tenantId, factoryId, days));
+        [FromQuery] int days = 14, [FromQuery] string? period = null) =>
+        Ok(await _svc.GetTrendsAsync(tenantId, factoryId, days, period));
 
     [HttpGet("distributions")]
     public async Task<IActionResult> DistributionsAsync([FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1, [FromQuery] string? period = null) =>
@@ -55,8 +55,9 @@ public class AdoS8DashboardController : ControllerBase
         [FromQuery] long tenantId = 1,
         [FromQuery] long factoryId = 1,
         [FromQuery] string dim = "object",
-        [FromQuery] int days = 14) =>
-        Ok(await _svc.GetDimTrendsAsync(tenantId, factoryId, dim, days));
+        [FromQuery] int days = 14,
+        [FromQuery] string? period = null) =>
+        Ok(await _svc.GetDimTrendsAsync(tenantId, factoryId, dim, days, period));
 
     /// <summary>
     /// 按配置获取单个大屏卡片的数据。配置来源 ado_s8_dashboard_cell_config。

+ 9 - 9
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8MonitoringController.cs

@@ -35,30 +35,30 @@ public class AdoS8MonitoringController : ControllerBase
     /// 口径 module_code IN (S1,S7) AND exception_type_code IN (ORDER_CHANGE / DELIVERY_DELAY / PENDING_SHIPMENT)。
     /// </summary>
     [HttpGet("delivery-trend")]
-    public async Task<IActionResult> GetDeliveryTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
-        => Ok(await _svc.GetDeliveryTrendAsync(tenantId, factoryId, days));
+    public async Task<IActionResult> GetDeliveryTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7, string? period = null)
+        => Ok(await _svc.GetDeliveryTrendAsync(tenantId, factoryId, days, period));
 
     /// <summary>
     /// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:Production 页近 N 日生产异常趋势。
     /// 口径 module_code IN (S2,S6) AND exception_type_code IN (EQUIP_FAULT / MFG_MATERIAL_ABNORMAL / MFG_QUALITY_ABNORMAL)。
     /// </summary>
     [HttpGet("production-trend")]
-    public async Task<IActionResult> GetProductionTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
-        => Ok(await _svc.GetProductionTrendAsync(tenantId, factoryId, days));
+    public async Task<IActionResult> GetProductionTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7, string? period = null)
+        => Ok(await _svc.GetProductionTrendAsync(tenantId, factoryId, days, period));
 
     /// <summary>
     /// S8-PROD-SUPPLY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:Supply 页近 N 日供应异常趋势。
     /// 口径 module_code IN (S3,S4,S5) AND exception_type_code IN 7 类供应异常。
     /// </summary>
     [HttpGet("supply-trend")]
-    public async Task<IActionResult> GetSupplyTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
-        => Ok(await _svc.GetSupplyTrendAsync(tenantId, factoryId, days));
+    public async Task<IActionResult> GetSupplyTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7, string? period = null)
+        => Ok(await _svc.GetSupplyTrendAsync(tenantId, factoryId, days, period));
 
     /// <summary>
     /// S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:专题页右侧异常类型卡同窗口同分母聚合。
-    /// domain ∈ {DELIVERY, PRODUCTION, SUPPLY};window ∈ {LAST_24H(默认), LAST_7D}。
+    /// domain ∈ {DELIVERY, PRODUCTION, SUPPLY};window ∈ {LAST_24H(默认), LAST_7D};period 优先于 window
     /// </summary>
     [HttpGet("domain-type-metrics")]
-    public async Task<IActionResult> GetDomainTypeMetricsAsync(string domain, string window = "LAST_24H", long tenantId = 1, long factoryId = 1)
-        => Ok(await _svc.GetDomainTypeMetricsAsync(domain, window, tenantId, factoryId));
+    public async Task<IActionResult> GetDomainTypeMetricsAsync(string domain, string window = "LAST_24H", string? period = null, long tenantId = 1, long factoryId = 1)
+        => Ok(await _svc.GetDomainTypeMetricsAsync(domain, window, tenantId, factoryId, period));
 }

+ 6 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8PeriodHelper.cs

@@ -17,10 +17,13 @@ public static class S8PeriodHelper
         var today = DateTime.Today;
         return period.Trim().ToLowerInvariant() switch
         {
-            "today" => (today, today.AddDays(1)),
-            "this_week" => ResolveThisWeek(today),
+            "today"      => (today, today.AddDays(1)),
+            "this_week"  => ResolveThisWeek(today),
             "this_month" => (new DateTime(today.Year, today.Month, 1), new DateTime(today.Year, today.Month, 1).AddMonths(1)),
-            _ => (null, null),
+            "last_24h"   => (DateTime.Now.AddHours(-24), DateTime.Now),
+            "last_7d"    => (today.AddDays(-6), today.AddDays(1)),
+            "last_30d"   => (today.AddDays(-29), today.AddDays(1)),
+            _            => (null, null),
         };
     }
 

+ 31 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DashboardService.cs

@@ -76,11 +76,22 @@ public class S8DashboardService : ITransient
         return new { total, pending, inProgress, timeout, closed, todayNew, critical, closureRate, avgCycleHours };
     }
 
-    public async Task<object> GetTrendsAsync(long tenantId, long factoryId, int days)
+    public async Task<object> GetTrendsAsync(long tenantId, long factoryId, int days, string? period = null)
     {
-        var from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90));
+        var (periodFrom, periodTo) = S8PeriodHelper.Resolve(period);
+        DateTime from, toExclusive;
+        if (periodFrom.HasValue && periodTo.HasValue)
+        {
+            from = periodFrom.Value;
+            toExclusive = periodTo.Value;
+        }
+        else
+        {
+            from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90));
+            toExclusive = DateTime.Today.AddDays(1);
+        }
         var rows = await _rep.AsQueryable()
-            .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from)
+            .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from && x.CreatedAt < toExclusive)
             .Where(x => S8ModuleCode.All.Contains(x.ModuleCode))
             .Select(x => new { x.CreatedAt })
             .ToListAsync();
@@ -230,12 +241,23 @@ public class S8DashboardService : ITransient
     /// 按维度返回多系列日趋势数据。dim: object | process | occDept | respDept
     /// 返回: { dates, series: [{ name, data[] }] }
     /// </summary>
-    public async Task<object> GetDimTrendsAsync(long tenantId, long factoryId, string dim, int days)
+    public async Task<object> GetDimTrendsAsync(long tenantId, long factoryId, string dim, int days, string? period = null)
     {
-        var from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90));
+        var (periodFrom, periodTo) = S8PeriodHelper.Resolve(period);
+        DateTime from, toExclusive;
+        if (periodFrom.HasValue && periodTo.HasValue)
+        {
+            from = periodFrom.Value;
+            toExclusive = periodTo.Value;
+        }
+        else
+        {
+            from = DateTime.Today.AddDays(-Math.Clamp(days, 1, 90));
+            toExclusive = DateTime.Today.AddDays(1);
+        }
 
         var rows = await _rep.AsQueryable()
-            .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from)
+            .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.CreatedAt >= from && x.CreatedAt < toExclusive)
             .Where(x => S8ModuleCode.All.Contains(x.ModuleCode))
             .Select(x => new
             {
@@ -248,8 +270,9 @@ public class S8DashboardService : ITransient
             })
             .ToListAsync();
 
-        // 生成连续日期序列
-        var dates = Enumerable.Range(0, (DateTime.Today - from).Days + 1)
+        // 生成连续日期序列(含首尾,endDate 取 toExclusive-1 与 Today 较小值避免未来日期)
+        var endDate = new[] { toExclusive.AddDays(-1).Date, DateTime.Today }.Min();
+        var dates = Enumerable.Range(0, (endDate - from.Date).Days + 1)
             .Select(i => from.AddDays(i).Date)
             .ToList();
         var dateLabels = dates.Select(d => d.ToString("yyyy-MM-dd")).ToList();

+ 68 - 26
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8MonitoringService.cs

@@ -279,12 +279,24 @@ public class S8MonitoringService : ITransient
     /// 口径:module_code IN (S1,S7) AND exception_type_code IN (ORDER_CHANGE / DELIVERY_DELAY / PENDING_SHIPMENT);
     /// 时间字段 created_at;不排除 CLOSED;按日聚合,缺失日期补 0;days 限 1-30。
     /// </summary>
-    public async Task<AdoS8DeliveryTrendDto> GetDeliveryTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
+    public async Task<AdoS8DeliveryTrendDto> GetDeliveryTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7, string? period = null)
     {
-        days = Math.Clamp(days, 1, 30);
-        var today = DateTime.Today;
-        var from = today.AddDays(-(days - 1));
-        var toExclusive = today.AddDays(1);
+        DateTime from, toExclusive;
+        int effectiveDays;
+        var (periodFrom, periodTo) = S8PeriodHelper.Resolve(period);
+        if (periodFrom.HasValue && periodTo.HasValue)
+        {
+            from = periodFrom.Value;
+            toExclusive = periodTo.Value;
+            effectiveDays = Math.Max(1, Math.Min(90, (int)(toExclusive - from).TotalDays));
+        }
+        else
+        {
+            effectiveDays = Math.Clamp(days, 1, 30);
+            var today = DateTime.Today;
+            from = today.AddDays(-(effectiveDays - 1));
+            toExclusive = today.AddDays(1);
+        }
 
         var deliveryModules = new[] { "S1", "S7" };
         var deliveryTypes = new[] { "ORDER_CHANGE", "DELIVERY_DELAY", "PENDING_SHIPMENT" };
@@ -301,8 +313,8 @@ public class S8MonitoringService : ITransient
             .GroupBy(r => r.CreatedAt.Date)
             .ToDictionary(g => g.Key, g => g.ToList());
 
-        var dayList = new List<AdoS8DeliveryTrendDayDto>(days);
-        for (var i = 0; i < days; i++)
+        var dayList = new List<AdoS8DeliveryTrendDayDto>(effectiveDays);
+        for (var i = 0; i < effectiveDays; i++)
         {
             var d = from.AddDays(i);
             var bucket = byDate.TryGetValue(d, out var list) ? list : new();
@@ -332,7 +344,7 @@ public class S8MonitoringService : ITransient
         {
             PeakValue       = peak?.Total ?? 0,
             PeakDate        = (peak is null || peak.Total == 0) ? null : peak.RawDate,
-            AvgValue        = Math.Round(totalSum / (double)days, 2),
+            AvgValue        = Math.Round(totalSum / (double)effectiveDays, 2),
             TodayValue      = todayDay.Total,
             TodayChangeRate = changeRate,
         };
@@ -345,12 +357,24 @@ public class S8MonitoringService : ITransient
     /// 口径:module_code IN (S2,S6) AND exception_type_code IN (EQUIP_FAULT / MFG_MATERIAL_ABNORMAL / MFG_QUALITY_ABNORMAL);
     /// 时间字段 created_at;不排除 CLOSED;按日聚合,缺失日期补 0;days 限 1-30。
     /// </summary>
-    public async Task<AdoS8ProductionTrendDto> GetProductionTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
+    public async Task<AdoS8ProductionTrendDto> GetProductionTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7, string? period = null)
     {
-        days = Math.Clamp(days, 1, 30);
-        var today = DateTime.Today;
-        var from = today.AddDays(-(days - 1));
-        var toExclusive = today.AddDays(1);
+        DateTime from, toExclusive;
+        int effectiveDays;
+        var (periodFrom, periodTo) = S8PeriodHelper.Resolve(period);
+        if (periodFrom.HasValue && periodTo.HasValue)
+        {
+            from = periodFrom.Value;
+            toExclusive = periodTo.Value;
+            effectiveDays = Math.Max(1, Math.Min(90, (int)(toExclusive - from).TotalDays));
+        }
+        else
+        {
+            effectiveDays = Math.Clamp(days, 1, 30);
+            var today = DateTime.Today;
+            from = today.AddDays(-(effectiveDays - 1));
+            toExclusive = today.AddDays(1);
+        }
 
         var prodModules = new[] { "S2", "S6" };
         var prodTypes = new[] { "EQUIP_FAULT", "MFG_MATERIAL_ABNORMAL", "MFG_QUALITY_ABNORMAL" };
@@ -365,8 +389,8 @@ public class S8MonitoringService : ITransient
 
         var byDate = rows.GroupBy(r => r.CreatedAt.Date).ToDictionary(g => g.Key, g => g.ToList());
 
-        var dayList = new List<AdoS8ProductionTrendDayDto>(days);
-        for (var i = 0; i < days; i++)
+        var dayList = new List<AdoS8ProductionTrendDayDto>(effectiveDays);
+        for (var i = 0; i < effectiveDays; i++)
         {
             var d = from.AddDays(i);
             var bucket = byDate.TryGetValue(d, out var list) ? list : new();
@@ -396,7 +420,7 @@ public class S8MonitoringService : ITransient
         {
             PeakValue       = peak?.Total ?? 0,
             PeakDate        = (peak is null || peak.Total == 0) ? null : peak.RawDate,
-            AvgValue        = Math.Round(totalSum / (double)days, 2),
+            AvgValue        = Math.Round(totalSum / (double)effectiveDays, 2),
             TodayValue      = todayDay.Total,
             TodayChangeRate = changeRate,
         };
@@ -410,12 +434,24 @@ public class S8MonitoringService : ITransient
     /// / WAREHOUSE_RECEIPT_ABNORMAL / IQC_ISSUE / WH_PUTAWAY_ISSUE / WORK_ORDER_KITTING_ABNORMAL / WORK_ORDER_ISSUE_ABNORMAL);
     /// 时间字段 created_at;不排除 CLOSED;按日聚合,缺失日期补 0;days 限 1-30。
     /// </summary>
-    public async Task<AdoS8SupplyTrendDto> GetSupplyTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7)
+    public async Task<AdoS8SupplyTrendDto> GetSupplyTrendAsync(long tenantId = 1, long factoryId = 1, int days = 7, string? period = null)
     {
-        days = Math.Clamp(days, 1, 30);
-        var today = DateTime.Today;
-        var from = today.AddDays(-(days - 1));
-        var toExclusive = today.AddDays(1);
+        DateTime from, toExclusive;
+        int effectiveDays;
+        var (periodFrom, periodTo) = S8PeriodHelper.Resolve(period);
+        if (periodFrom.HasValue && periodTo.HasValue)
+        {
+            from = periodFrom.Value;
+            toExclusive = periodTo.Value;
+            effectiveDays = Math.Max(1, Math.Min(90, (int)(toExclusive - from).TotalDays));
+        }
+        else
+        {
+            effectiveDays = Math.Clamp(days, 1, 30);
+            var today = DateTime.Today;
+            from = today.AddDays(-(effectiveDays - 1));
+            toExclusive = today.AddDays(1);
+        }
 
         var supplyModules = new[] { "S3", "S4", "S5" };
         var supplyTypes = new[]
@@ -434,8 +470,8 @@ public class S8MonitoringService : ITransient
 
         var byDate = rows.GroupBy(r => r.CreatedAt.Date).ToDictionary(g => g.Key, g => g.ToList());
 
-        var dayList = new List<AdoS8SupplyTrendDayDto>(days);
-        for (var i = 0; i < days; i++)
+        var dayList = new List<AdoS8SupplyTrendDayDto>(effectiveDays);
+        for (var i = 0; i < effectiveDays; i++)
         {
             var d = from.AddDays(i);
             var bucket = byDate.TryGetValue(d, out var list) ? list : new();
@@ -473,7 +509,7 @@ public class S8MonitoringService : ITransient
         {
             PeakValue       = peak?.Total ?? 0,
             PeakDate        = (peak is null || peak.Total == 0) ? null : peak.RawDate,
-            AvgValue        = Math.Round(totalSum / (double)days, 2),
+            AvgValue        = Math.Round(totalSum / (double)effectiveDays, 2),
             TodayValue      = todayDay.Total,
             TodayChangeRate = changeRate,
         };
@@ -486,7 +522,7 @@ public class S8MonitoringService : ITransient
     /// 单一时间窗口下 total / open / closed / avgProcessHours / closeRate 共用同一分母。
     /// 公共过滤:tenant/factory + is_deleted=0 + module_code IN S1-S7 + exception_type_code IN domain 类型集 + created_at IN window。
     /// </summary>
-    public async Task<AdoS8DomainTypeMetricsDto> GetDomainTypeMetricsAsync(string domain, string window, long tenantId = 1, long factoryId = 1)
+    public async Task<AdoS8DomainTypeMetricsDto> GetDomainTypeMetricsAsync(string domain, string window, long tenantId = 1, long factoryId = 1, string? period = null)
     {
         var d = (domain ?? string.Empty).Trim().ToUpperInvariant();
         var w = (window ?? string.Empty).Trim().ToUpperInvariant();
@@ -526,7 +562,13 @@ public class S8MonitoringService : ITransient
         }
 
         DateTime from, to;
-        if (w == "LAST_7D")
+        var (periodFrom, periodTo) = S8PeriodHelper.Resolve(period);
+        if (periodFrom.HasValue && periodTo.HasValue)
+        {
+            from = periodFrom.Value;
+            to   = periodTo.Value;
+        }
+        else if (w == "LAST_7D")
         {
             // 与 trend 接口保持一致:[today-6, today+1)
             var today = DateTime.Today;