refactor(frontend): 按需加载页面并清理未使用代码
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

- 将业务页面路由统一改为动态导入,拆分首屏入口与各功能模块构建产物。

- 将网页端和桌面端布局改为异步组件,避免两套平台布局同时进入初始包。

- 新增 Element Plus 按需安装入口,并通过全局配置组件统一注入中文语言包。

- 提取 API 运行时钩子,在应用启动时注入项目清理、令牌续期和认证失效退出能力。

- 将权限监控面板及地图资源改为延迟加载,补充地图加载状态、失败提示和切换竞态保护。

- 删除已被现有工作流替代的项目成员、接口权限、中心绑定、培训表单及旧项目首页等页面。

- 清理废弃的快捷操作、项目选择、用户选择、FAQ 表单、风险占位组件和旧地图辅助模块。

- 移除未使用的 API 方法、类型、字典、状态机、展示工具、样式和项目详情编辑逻辑。

- 开启 TypeScript 未使用变量与参数检查,并同步收紧相关测试和组件暴露类型。

- 移除未使用的 updater、date-fns 和 Sass 前端依赖,更新锁文件并删除旧 CSS 清洗插件。

- 更新路由、Axios、ETMF、通知、权限监控和桌面布局测试以覆盖重构后的边界。
This commit is contained in:
Cheng Zhou
2026-07-16 14:58:47 +08:00
parent 1d26646a96
commit 6b55c18610
65 changed files with 345 additions and 3639 deletions
@@ -56,16 +56,20 @@
</div>
</div>
<div class="source-map-wrap">
<v-chart
v-if="!fullscreenVisible"
ref="sourceChartRef"
:key="mapView"
:option="sourceMapOption"
:init-options="chartInitOptions"
:update-options="chartUpdateOptions"
:autoresize="chartAutoresize"
class="source-map"
/>
<template v-if="!fullscreenVisible">
<v-chart
v-if="mapReady"
ref="sourceChartRef"
:key="mapView"
:option="sourceMapOption"
:init-options="chartInitOptions"
:update-options="chartUpdateOptions"
:autoresize="chartAutoresize"
class="source-map"
/>
<el-empty v-else-if="mapLoadError" :description="mapLoadError" :image-size="72" />
<div v-else v-loading="true" class="source-map" aria-label="地图加载中" />
</template>
<div class="flow-legend map-legend" aria-label="访问链路图例">
<span v-if="serverLocation" class="legend-item"><i class="legend-dot server"></i>服务器</span>
<template v-if="mapMetric === 'traffic'">
@@ -125,7 +129,7 @@
</template>
<div class="fullscreen-map-wrap">
<v-chart
v-if="fullscreenChartMounted"
v-if="fullscreenChartMounted && mapReady"
ref="fullscreenChartRef"
:key="fullscreenMapKey"
:option="fullscreenSourceMapOption"
@@ -152,21 +156,26 @@
</template>
<script setup lang="ts">
import { computed, h, nextTick, onMounted, ref } from "vue";
import { computed, h, nextTick, onMounted, ref, watch } from "vue";
import VChart from "vue-echarts";
import { registerMap, use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { EffectScatterChart, LinesChart, ScatterChart } from "echarts/charts";
import { GeoComponent, TooltipComponent } from "echarts/components";
import chinaMapGeoJson from "../assets/china.json";
import worldMapGeoJson from "../assets/world.json";
import { fetchIpLocations } from "../api/projectPermissions";
import type { IpLocationsResponse, IpLocationStatItem } from "../types/api";
import TimeRangeSelector from "./TimeRangeSelector.vue";
use([CanvasRenderer, LinesChart, ScatterChart, EffectScatterChart, GeoComponent, TooltipComponent]);
registerMap("ctms-china", chinaMapGeoJson as any);
registerMap("ctms-world", worldMapGeoJson as any);
type MapView = "china" | "flow";
const mapDefinitions: Record<MapView, { name: string; load: () => Promise<{ default: unknown }> }> = {
china: { name: "ctms-china", load: () => import("../assets/china.json") },
flow: { name: "ctms-world", load: () => import("../assets/world.json") },
};
const registeredMaps = new Set<string>();
const IconGlobe = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [
h("circle", { cx: "12", cy: "12", r: "10" }),
@@ -192,12 +201,6 @@ const IconShield = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke:
const loading = ref(false);
type TimeRangePreset = "all" | "24h" | "7d" | "30d";
const timeRangePreset = ref<TimeRangePreset>("30d");
const timeRangeOptions = [
{ value: "all", label: "全部" },
{ value: "24h", label: "24小时" },
{ value: "7d", label: "7天" },
{ value: "30d", label: "30天" },
] as const;
const getIpLocationsDays = (preset: TimeRangePreset): number | undefined => {
const map: Record<TimeRangePreset, number | undefined> = {
@@ -208,7 +211,7 @@ const getIpLocationsDays = (preset: TimeRangePreset): number | undefined => {
};
return map[preset];
};
const mapView = ref<"china" | "flow">("flow");
const mapView = ref<MapView>("flow");
const mapMetric = ref<"traffic" | "risk">("traffic");
const items = ref<IpLocationStatItem[]>([]);
const summary = ref<IpLocationsResponse["summary"] | null>(null);
@@ -216,6 +219,8 @@ const periodMetadata = ref<IpLocationsResponse["period"] | null>(null);
const dataQuality = ref<IpLocationsResponse["data_quality"] | null>(null);
const serverLocation = ref<IpLocationsResponse["server_location"]>(null);
const loadError = ref("");
const mapLoadError = ref("");
const mapReady = ref(false);
const lastUpdatedAt = ref<Date | null>(null);
const getDevicePixelRatio = () => (typeof window === "undefined" ? 1 : window.devicePixelRatio || 1);
const chartInitOptions = { devicePixelRatio: Math.min(getDevicePixelRatio(), 2), useDirtyRect: true };
@@ -270,6 +275,31 @@ const resizeSourceCharts = async () => {
sourceChartRef.value?.resize?.();
};
let mapLoadSequence = 0;
const loadMap = async (view: MapView) => {
const sequence = ++mapLoadSequence;
const definition = mapDefinitions[view];
mapReady.value = false;
mapLoadError.value = "";
try {
if (!registeredMaps.has(definition.name)) {
const geoJson = await definition.load();
registerMap(definition.name, geoJson.default as any);
registeredMaps.add(definition.name);
}
if (sequence !== mapLoadSequence) return;
mapReady.value = true;
await resizeSourceCharts();
} catch {
if (sequence !== mapLoadSequence) return;
mapLoadError.value = "地图资源加载失败,请刷新后重试";
}
};
watch(mapView, (view) => void loadMap(view), { immediate: true });
const formatNumber = (value: number) => new Intl.NumberFormat("zh-CN").format(value || 0);
const formatLastUpdated = (value: Date) => {
return `${value.getFullYear()}/${value.getMonth() + 1}/${value.getDate()} ${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}:${String(value.getSeconds()).padStart(2, "0")}`;