refactor(frontend): 按需加载页面并清理未使用代码
- 将业务页面路由统一改为动态导入,拆分首屏入口与各功能模块构建产物。 - 将网页端和桌面端布局改为异步组件,避免两套平台布局同时进入初始包。 - 新增 Element Plus 按需安装入口,并通过全局配置组件统一注入中文语言包。 - 提取 API 运行时钩子,在应用启动时注入项目清理、令牌续期和认证失效退出能力。 - 将权限监控面板及地图资源改为延迟加载,补充地图加载状态、失败提示和切换竞态保护。 - 删除已被现有工作流替代的项目成员、接口权限、中心绑定、培训表单及旧项目首页等页面。 - 清理废弃的快捷操作、项目选择、用户选择、FAQ 表单、风险占位组件和旧地图辅助模块。 - 移除未使用的 API 方法、类型、字典、状态机、展示工具、样式和项目详情编辑逻辑。 - 开启 TypeScript 未使用变量与参数检查,并同步收紧相关测试和组件暴露类型。 - 移除未使用的 updater、date-fns 和 Sass 前端依赖,更新锁文件并删除旧 CSS 清洗插件。 - 更新路由、Axios、ETMF、通知、权限监控和桌面布局测试以覆盖重构后的边界。
This commit is contained in:
@@ -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")}`;
|
||||
|
||||
Reference in New Issue
Block a user