1.优化地区csv加载机制 2.修复数据库监控无法访问问题 3.增加了登录状态保存 4.针对session存储机制做适配 5.文件扫描机制优化,并行扫描速度更快
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 地区数据模块级单例缓存。
|
||||
* 首次调用时 fetch /region.csv 并构建 idMap 和 parentMap,
|
||||
* 后续调用直接返回缓存,SPA 生命周期内只加载一次。
|
||||
*/
|
||||
|
||||
interface RegionItem {
|
||||
id: number;
|
||||
parentId: number;
|
||||
level: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface RegionCache {
|
||||
/** id → RegionItem,用于表格列的 id→name 转换 */
|
||||
idMap: Map<number, RegionItem>;
|
||||
/** parentId → children[],用于省/市/区下拉联动 O(1) 查找 */
|
||||
parentMap: Map<number, RegionItem[]>;
|
||||
/** 所有地区平铺数组 */
|
||||
list: RegionItem[];
|
||||
}
|
||||
|
||||
let _cache: RegionCache | null = null;
|
||||
|
||||
function parseCSV(text: string): RegionItem[] {
|
||||
const lines = text.split("\n").filter((l) => l.trim());
|
||||
const headers = lines[0].split(",").map((h) => h.trim());
|
||||
return lines.slice(1).map((line) => {
|
||||
const cols = line.split(",").map((c) => c.trim());
|
||||
const obj: any = {};
|
||||
headers.forEach((h, i) => {
|
||||
obj[h] = h === "name" ? cols[i] : Number(cols[i]);
|
||||
});
|
||||
return obj as RegionItem;
|
||||
});
|
||||
}
|
||||
|
||||
export async function useRegionCache(): Promise<RegionCache> {
|
||||
if (_cache) return _cache;
|
||||
|
||||
const res = await fetch("/region.csv");
|
||||
if (!res.ok) throw new Error("地区数据加载失败");
|
||||
const text = await res.text();
|
||||
const list = parseCSV(text);
|
||||
|
||||
const idMap = new Map<number, RegionItem>();
|
||||
const parentMap = new Map<number, RegionItem[]>();
|
||||
|
||||
for (const item of list) {
|
||||
idMap.set(item.id, item);
|
||||
if (!parentMap.has(item.parentId)) {
|
||||
parentMap.set(item.parentId, []);
|
||||
}
|
||||
parentMap.get(item.parentId)!.push(item);
|
||||
}
|
||||
|
||||
_cache = { idMap, parentMap, list };
|
||||
return _cache;
|
||||
}
|
||||
@@ -91,6 +91,14 @@ export function useWeatherChart(
|
||||
async function renderChart() {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
// 确保 DOM 已渲染且有尺寸,避免 ECharts "invalid dom" 报错
|
||||
const { width, height } = chartRef.value.getBoundingClientRect();
|
||||
if (width === 0 || height === 0) {
|
||||
// DOM 未就绪,延迟重试
|
||||
scheduleRender();
|
||||
return;
|
||||
}
|
||||
|
||||
// 动态按需加载 ECharts
|
||||
const echarts = await import("echarts");
|
||||
const mod = (echarts as any).default || echarts;
|
||||
|
||||
@@ -67,7 +67,7 @@ export const useAppStore = defineStore("useAppStore", {
|
||||
},
|
||||
//退出
|
||||
logout() {
|
||||
removeCache(CacheToken, true);
|
||||
removeCache(CacheToken);
|
||||
this.updateState({
|
||||
appIsLogin: false,
|
||||
permissions: [],
|
||||
|
||||
@@ -67,5 +67,5 @@ export const removeCache = (key: string, isSessionStorage?: boolean): void => {
|
||||
};
|
||||
|
||||
export const getToken = (): string => {
|
||||
return getCache(CacheToken, { isSessionStorage: true }, {})["token"];
|
||||
return getCache(CacheToken, {}, {})["token"];
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ const onLogin = () => {
|
||||
.then((res) => {
|
||||
state.loading = false;
|
||||
if (res.code === 0) {
|
||||
setCache(CacheToken, res.data, true);
|
||||
setCache(CacheToken, res.data);
|
||||
ElMessage.success("登录成功");
|
||||
router.push("/");
|
||||
} else {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRegionCache } from '@/composables/useRegionCache';
|
||||
|
||||
const rawList = ref<any[]>([]);
|
||||
const treeData = ref<any[]>([]);
|
||||
@@ -35,61 +36,32 @@ const loadingStatus = ref('正在解析数据...');
|
||||
|
||||
const rawListCount = computed(() => rawList.value.length);
|
||||
|
||||
// 优化后的构建树函数
|
||||
const buildTreeMap = (list: any[]) => {
|
||||
const map = new Map<number, any[]>();
|
||||
|
||||
// 构建 parentId -> children 的 Map
|
||||
list.forEach(item => {
|
||||
const pid = item.parentid;
|
||||
if (!map.has(pid)) map.set(pid, []);
|
||||
map.get(pid)!.push({ ...item });
|
||||
// 从 parentMap 递归构建树
|
||||
const buildTreeFromCache = (parentMap: Map<number, any[]>, pId: number): any[] => {
|
||||
const children = parentMap.get(pId) || [];
|
||||
return children.map((child) => {
|
||||
const node = { ...child };
|
||||
const sub = buildTreeFromCache(parentMap, child.id);
|
||||
if (sub.length > 0) node.children = sub;
|
||||
return node;
|
||||
});
|
||||
|
||||
// 递归构建树
|
||||
const createTree = (pId: number): any[] => {
|
||||
const children = map.get(pId) || [];
|
||||
children.forEach(child => {
|
||||
const sub = createTree(child.id);
|
||||
if (sub.length > 0) child.children = sub;
|
||||
});
|
||||
return children;
|
||||
};
|
||||
|
||||
// 自动探测根节点
|
||||
const rootParentId = Math.min(...list.map(r => r.parentid));
|
||||
return createTree(rootParentId);
|
||||
};
|
||||
|
||||
const initRegionTree = async () => {
|
||||
try {
|
||||
const res = await fetch("/region.csv");
|
||||
if (!res.ok) throw new Error("无法读取数据,请检查文件");
|
||||
const cache = await useRegionCache();
|
||||
rawList.value = cache.list;
|
||||
|
||||
const text = await res.text();
|
||||
const lines = text.split("\n").map(l => l.trim()).filter(l => l);
|
||||
if (lines.length < 2) throw new Error("内容为空或格式错误");
|
||||
// 自动探测根节点(最小 parentId)
|
||||
const rootParentId = Math.min(...cache.list.map((r) => r.parentId));
|
||||
const tree = buildTreeFromCache(cache.parentMap, rootParentId);
|
||||
|
||||
const headers = lines[0].split(",").map(h => h.trim().toLowerCase());
|
||||
|
||||
rawList.value = lines.slice(1).map(line => {
|
||||
const cols = line.split(",").map(c => c.trim());
|
||||
const obj: any = {};
|
||||
headers.forEach((h, i) => {
|
||||
obj[h] = h === 'name' ? cols[i] : Number(cols[i]);
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
// 构建树
|
||||
const tree = buildTreeMap(rawList.value);
|
||||
if (tree.length === 0) {
|
||||
loadingStatus.value = '构建失败:根节点下无数据';
|
||||
} else {
|
||||
treeData.value = tree;
|
||||
loadingStatus.value = '加载完成';
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
loadingStatus.value = `错误: ${error.message}`;
|
||||
console.error(error);
|
||||
|
||||
@@ -21,12 +21,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="测站级别" prop="stationLevel">
|
||||
<el-select v-model="dataForm.stationLevel" placeholder="选择级别">
|
||||
<el-option label="国家基准气候站(11)" :value="11"/>
|
||||
<el-option label="国家基本气象站(12)" :value="12"/>
|
||||
<el-option label="国家一般气象站(13)" :value="13"/>
|
||||
<el-option label="区域气象站(14)" :value="14"/>
|
||||
<el-option label="其他气象站(15)" :value="15"/>
|
||||
<el-option label="国家站(16)" :value="16"/>
|
||||
<el-option v-for="item in stationLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -135,11 +130,15 @@
|
||||
import { reactive, ref, computed, nextTick } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useAppStore } from "@/store";
|
||||
import { getDictDataList } from "@/utils/utils";
|
||||
|
||||
// 接收父页面传来的 CSV 原始数组和 Map 索引
|
||||
const store = useAppStore();
|
||||
|
||||
// 接收父页面传来的地区数据
|
||||
const props = defineProps<{
|
||||
regionsData: any[]
|
||||
regionMap: Map<number, any>
|
||||
parentMap: Map<number, any[]>
|
||||
deptData: any[]
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
@@ -170,14 +169,19 @@ const rules = {
|
||||
stationLevel: [{ required: true, message: "请选择测站级别", trigger: "change" }]
|
||||
};
|
||||
|
||||
// ----------------- 下拉框联动逻辑 -----------------
|
||||
// ----------------- 字典数据 ----------------
|
||||
|
||||
// 1. 获取省级列表 (根据你说的:parentId 为 1 是省级)
|
||||
const provinceOptions = computed(() => {
|
||||
const list = props.regionsData.filter(r => Number(r.parentId) === 1);
|
||||
console.log("省级加载检查,匹配数量:", list.length);
|
||||
return list;
|
||||
});
|
||||
const stationLevelOptions = computed(() =>
|
||||
getDictDataList(store.state.dicts, "station_level").map((item: any) => ({
|
||||
label: `${item.dictLabel}(${item.dictValue})`,
|
||||
value: Number(item.dictValue)
|
||||
}))
|
||||
);
|
||||
|
||||
// ----------------- 下拉框联动逻辑(O(1) 查找) -----------------
|
||||
|
||||
// 省级:parentId = 1
|
||||
const provinceOptions = computed(() => props.parentMap.get(1) || []);
|
||||
|
||||
// 市、区列表:动态变化
|
||||
const cityOptions = ref<any[]>([]);
|
||||
@@ -189,9 +193,8 @@ const onProvinceChange = (val: string) => {
|
||||
dataForm.countyName = "";
|
||||
cityOptions.value = [];
|
||||
countyOptions.value = [];
|
||||
|
||||
if (val) {
|
||||
cityOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(val));
|
||||
cityOptions.value = props.parentMap.get(Number(val)) || [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -199,18 +202,11 @@ const onProvinceChange = (val: string) => {
|
||||
const onCityChange = (val: string) => {
|
||||
dataForm.countyName = "";
|
||||
countyOptions.value = [];
|
||||
|
||||
if (val) {
|
||||
countyOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(val));
|
||||
countyOptions.value = props.parentMap.get(Number(val)) || [];
|
||||
}
|
||||
};
|
||||
|
||||
function getDeptList() {
|
||||
return baseService.get("/sys/dept/list").then((res) => {
|
||||
deptList.value = res.data || [];
|
||||
});
|
||||
}
|
||||
|
||||
function onDeptClick(data: any) {
|
||||
dataForm.deptId = data.id;
|
||||
}
|
||||
@@ -220,9 +216,11 @@ function onDeptClick(data: any) {
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
|
||||
// 重置表单和选项
|
||||
// 重置选项
|
||||
cityOptions.value = [];
|
||||
countyOptions.value = [];
|
||||
// 复用父页面的部门数据,无需重复请求
|
||||
deptList.value = props.deptData || [];
|
||||
|
||||
nextTick(() => {
|
||||
if (dataFormRef.value) dataFormRef.value.resetFields();
|
||||
@@ -234,12 +232,9 @@ const init = (id?: number) => {
|
||||
townName: "",
|
||||
deptId: ""
|
||||
});
|
||||
|
||||
getDeptList().then(() => {
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
});
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -251,12 +246,12 @@ const getInfo = async (id: number) => {
|
||||
// 1. 基础数据赋值
|
||||
Object.assign(dataForm, data);
|
||||
|
||||
// 2. 关键:手动触发联动列表加载,否则 Select 只有 ID 没有数据源,无法显示名字
|
||||
// 2. 手动加载省市区联动列表数据源
|
||||
if (data.provinceName) {
|
||||
cityOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(data.provinceName));
|
||||
cityOptions.value = props.parentMap.get(Number(data.provinceName)) || [];
|
||||
}
|
||||
if (data.cityName) {
|
||||
countyOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(data.cityName));
|
||||
countyOptions.value = props.parentMap.get(Number(data.cityName)) || [];
|
||||
}
|
||||
|
||||
// 3. 回显选择的管理部门
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" :regions-data="regionsData" :region-map="regionMap" @refreshDataList="state.getDataList"> </add-or-update>
|
||||
<add-or-update ref="addOrUpdateRef" :parent-map="parentMap" :dept-data="deptData" @refreshDataList="state.getDataList"> </add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -68,6 +68,7 @@ import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs, onMounted } from "vue";
|
||||
import AddOrUpdate from "./weatherstation-add-or-update.vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { useRegionCache } from "@/composables/useRegionCache";
|
||||
|
||||
// 表格 state
|
||||
const view = reactive({
|
||||
@@ -97,22 +98,25 @@ const addOrUpdateHandle = (id?: number) => {
|
||||
//
|
||||
// }
|
||||
|
||||
// ---------------- CSV 数据加载 ----------------
|
||||
const regionsData = ref<any[]>([]);
|
||||
// ---------------- 地区数据(模块级缓存) ----------------
|
||||
const parentMap = reactive(new Map<number, any[]>());
|
||||
const regionMap = reactive(new Map<number, any>());
|
||||
|
||||
// ---------------- 部门数据加载 ----------------
|
||||
// ---------------- 部门数据 ----------------
|
||||
const deptMap = reactive(new Map<number, string>());
|
||||
const deptData = ref<any[]>([]);
|
||||
|
||||
async function loadDepts() {
|
||||
try {
|
||||
const res = await baseService.get("/sys/dept/list");
|
||||
deptData.value = res.data || [];
|
||||
const flat = (list: any[]) => {
|
||||
list.forEach((d) => {
|
||||
list.forEach((d: any) => {
|
||||
deptMap.set(Number(d.id), d.name);
|
||||
if (d.children) flat(d.children);
|
||||
});
|
||||
};
|
||||
flat(res.data || []);
|
||||
flat(deptData.value);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -120,26 +124,8 @@ async function loadDepts() {
|
||||
|
||||
onMounted(async () => {
|
||||
loadDepts();
|
||||
const res = await fetch("/region.csv");
|
||||
const text = await res.text();
|
||||
const parseCSV = (csvText: string) => {
|
||||
const lines = csvText.split("\n").filter((l) => l.trim());
|
||||
const headers = lines[0].split(",");
|
||||
return lines.slice(1).map((line) => {
|
||||
const cols = line.split(",");
|
||||
const obj: any = {};
|
||||
headers.forEach((h, i) => (obj[h.trim()] = cols[i].trim()));
|
||||
return obj;
|
||||
});
|
||||
};
|
||||
const data = parseCSV(text);
|
||||
data.forEach((r) => {
|
||||
const id = Number(r.id);
|
||||
const parentId = Number(r.parentId);
|
||||
const level = Number(r.level);
|
||||
const name = r.name;
|
||||
regionMap.set(id, { id, parentId, level, name });
|
||||
regionsData.value.push({ id, parentId, level, name });
|
||||
});
|
||||
const cache = await useRegionCache();
|
||||
cache.idMap.forEach((item, id) => regionMap.set(id, item));
|
||||
cache.parentMap.forEach((children, pid) => parentMap.set(pid, children));
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -74,9 +74,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import app from "@/constants/app";
|
||||
import { CacheToken } from "@/constants/cacheKey";
|
||||
import { getToken } from "@/utils/cache";
|
||||
import baseService from "@/service/baseService";
|
||||
import { getCache } from "@/utils/cache";
|
||||
import { ElMessage } from "element-plus";
|
||||
import axios from "axios";
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
@@ -134,7 +133,7 @@ const loadBlobUrl = (fileId: string): Promise<string> => {
|
||||
|
||||
const base = (app.api || "").replace(/\/$/, "");
|
||||
const url = `${base}/filescan/file/display/${fileId}`;
|
||||
const token = getCache(CacheToken, { isSessionStorage: true }, {})?.token;
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers.token = token;
|
||||
|
||||
|
||||
@@ -75,9 +75,9 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import app from "@/constants/app";
|
||||
import { CacheToken } from "@/constants/cacheKey";
|
||||
|
||||
import baseService from "@/service/baseService";
|
||||
import { getCache } from "@/utils/cache";
|
||||
import { getToken } from "@/utils/cache";
|
||||
import { copyToClipboard } from "@/utils/utils";
|
||||
import { ElMessage } from "element-plus";
|
||||
import axios from "axios";
|
||||
@@ -137,7 +137,7 @@ const loadBlobUrl = (fileId: string): Promise<string> => {
|
||||
|
||||
const base = (app.api || "").replace(/\/$/, "");
|
||||
const url = `${base}/filescan/file/display/${fileId}`;
|
||||
const token = getCache(CacheToken, { isSessionStorage: true }, {})?.token;
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers.token = token;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user