From 610bfc5319bb304f4f9ff837883555ab5061c1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=E5=85=88=E7=94=9F?= <1211120248@qq.com> Date: Thu, 9 Jul 2026 14:10:17 +0800 Subject: [PATCH] chore: add screen dataset export script --- scripts/export-screen2-datasets.mjs | 493 ++++++++++++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 scripts/export-screen2-datasets.mjs diff --git a/scripts/export-screen2-datasets.mjs b/scripts/export-screen2-datasets.mjs new file mode 100644 index 0000000..9f56c47 --- /dev/null +++ b/scripts/export-screen2-datasets.mjs @@ -0,0 +1,493 @@ +#!/usr/bin/env node +import * as esbuild from "esbuild" +import fs from "node:fs" +import path from "node:path" +import vm from "node:vm" +import { fileURLToPath } from "node:url" +import { createRequire } from "node:module" + +const require = createRequire(import.meta.url) +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(__dirname, "..") + +const outputArgIndex = process.argv.findIndex((arg) => arg === "--out") +const outputPath = outputArgIndex >= 0 ? process.argv[outputArgIndex + 1] : "" + +const rawPlugin = { + name: "screen2-raw-loader", + setup(build) { + build.onResolve({ filter: /\?raw$/ }, (args) => { + const sourcePath = args.path.replace(/\?raw$/, "") + const absolutePath = sourcePath.startsWith("/public/") + ? path.join(rootDir, sourcePath.slice(1)) + : path.resolve(args.resolveDir, sourcePath) + return { path: absolutePath, namespace: "screen2-raw" } + }) + build.onLoad({ filter: /.*/, namespace: "screen2-raw" }, async (args) => { + const contents = await fs.promises.readFile(args.path, "utf8") + return { + contents: `export default ${JSON.stringify(contents)};`, + loader: "js", + } + }) + }, +} + +const aliasPlugin = { + name: "screen2-alias", + setup(build) { + build.onResolve({ filter: /^@\// }, (args) => ({ + path: resolveImportPath(path.join(rootDir, "src", args.path.slice(2))), + })) + build.onResolve({ filter: /^~@\// }, (args) => ({ + path: resolveImportPath(path.join(rootDir, "src", args.path.slice(3))), + })) + build.onResolve({ filter: /^\/public\// }, (args) => ({ + path: resolveImportPath(path.join(rootDir, args.path.slice(1))), + })) + }, +} + +function resolveImportPath(candidate) { + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate + for (const ext of [".js", ".json", ".vue"]) { + const withExt = `${candidate}${ext}` + if (fs.existsSync(withExt) && fs.statSync(withExt).isFile()) return withExt + } + const indexPath = path.join(candidate, "index.js") + if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) return indexPath + return candidate +} + +const entry = ` +import * as gd from "./src/views/gdMap/livestockData.js"; +import * as home from "./src/views/home/data.js"; +import * as pasture from "./src/views/pastureManagement/data.js"; +import * as yak from "./src/views/yakManagement/data.js"; +import * as balance from "./src/views/grassLivestockBalance/data.js"; +import * as industry from "./src/views/yakIndustryChain/data.js"; +import * as ecology from "./src/views/ecologicalProtection/data.js"; + +export default { gd, home, pasture, yak, balance, industry, ecology }; +` + +const buildResult = await esbuild.build({ + stdin: { + contents: entry, + resolveDir: rootDir, + sourcefile: "screen2-dataset-entry.js", + loader: "js", + }, + bundle: true, + write: false, + platform: "node", + format: "cjs", + target: "node18", + plugins: [rawPlugin, aliasPlugin], + logLevel: "silent", +}) + +const moduleStub = { exports: {} } +vm.runInNewContext(buildResult.outputFiles[0].text, { + module: moduleStub, + exports: moduleStub.exports, + require, + console, + process, + Buffer, + setTimeout, + clearTimeout, +}) + +const exported = moduleStub.exports.default || moduleStub.exports +const datasetDefinitions = [] +const configDefinitions = [] +const industrySubjects = [] + +const VALUE_KEYS = [ + "value", + "count", + "number", + "yakCount", + "stock", + "ratio", + "areaWanMu", + "areaRange", + "yakNumber", + "bearingCapacity", + "livestockCapacity", + "overloadRate", + "capacity", + "projectCount", + "subjectFeatureCount", + "lengthKm", + "peopleCount", + "householdCount", + "ecologyJobs", + "percent", +] + +function addRows(datasetKey, pageCode, title, rows, options = {}) { + if (!Array.isArray(rows)) return + datasetDefinitions.push({ + datasetKey, + pageCode, + moduleCode: options.moduleCode || pageCode, + datasetType: options.datasetType || "rows", + sourceType: options.sourceType || "frontend_seed", + title, + payload: options.payload || {}, + rows, + }) +} + +function addConfig(configKey, groupKey, title, payload) { + if (payload === undefined) return + configDefinitions.push({ configKey, groupKey, title, payload }) +} + +function objectToRows(source = {}, labels = {}) { + return Object.entries(source || {}).map(([key, value]) => ({ + key, + name: labels[key] || key, + value, + })) +} + +function uniqueRows(rows = [], keyBuilder) { + const seen = new Set() + return rows.filter((row, index) => { + const key = keyBuilder(row, index) + if (seen.has(key)) return false + seen.add(key) + return true + }) +} + +function collectDefinitions(data) { + const { gd, home, pasture, yak, balance, industry, ecology } = data + + addRows("gdMap.core", "gdMap", "gdMap 核心指标", objectToRows(gd.homeCore, { + herdsmanCount: "牧户数量", + grasslandArea: "草地面积", + wetArea: "湿地面积", + yakCount: "牦牛数量", + projectCount: "工程数量", + baseCount: "基地数量", + })) + addRows("gdMap.grasslandWet", "gdMap", "草地湿地乡镇分布", gd.grasslandWetRows) + addRows("gdMap.yakForecast", "gdMap", "牦牛月度预测", gd.yakForecastRows) + addRows("gdMap.townRecognition", "gdMap", "乡镇遥感识别", gd.townRecognitionRows) + addRows("gdMap.grasslandYak", "gdMap", "草地牦牛乡镇分布", gd.grasslandYakRows) + addRows("gdMap.herdStructure", "gdMap", "畜群结构", gd.herdStructureRows) + addRows("gdMap.tradeImpact", "gdMap", "交易影响", gd.tradeImpactRows) + addRows("gdMap.stockForecast", "gdMap", "存栏预测", gd.stockForecastRows) + addRows("gdMap.supportIndustry", "gdMap", "支撑产业", gd.supportIndustryRows) + addRows("gdMap.warning", "gdMap", "预警统计", gd.warningRows) + addConfig("gdMap.stockTotals", "gdMap", "牦牛存栏总量配置", { + YAK_STOCK_TOTAL: gd.YAK_STOCK_TOTAL, + YAK_DRONE_RECOGNITION_TOTAL: gd.YAK_DRONE_RECOGNITION_TOTAL, + YAK_EXPERT_CORRECTION: gd.YAK_EXPERT_CORRECTION, + recognitionTotal: gd.recognitionTotal, + expertCorrection: gd.expertCorrection, + tradeDeduction: gd.tradeDeduction, + estimatedStock: gd.estimatedStock, + }) + + addRows("home.topStats", "home", "首页顶部指标", home.homeTopStats) + addRows("home.heroStats", "home", "首页主视觉指标", home.homeHeroStats) + addRows("home.resourceComposition", "home", "首页资源构成", home.resourceCompositionRows) + addRows("home.businessMatrix", "home", "首页业务矩阵", home.businessMatrixRows) + addRows("home.townSynergy", "home", "首页乡镇协同", home.townSynergyRows) + addRows("home.yakTown", "home", "首页牦牛乡镇排行", home.yakTownRows) + addRows("home.trade", "home", "首页交易结构", home.tradeRows) + addRows("home.support", "home", "首页支撑产业", home.supportRows) + addRows("home.dataFlow", "home", "首页数据流", home.dataFlowRows) + addRows("home.centerOrbit", "home", "首页中心节点", home.centerOrbitNodes) + + addRows("pasture.core", "pastureManagement", "牧户核心指标", pasture.pastureCoreRows) + addRows("pasture.topStats", "pastureManagement", "牧户顶部指标", pasture.pastureTopStats) + addRows("pasture.town", "pastureManagement", "牧户乡镇分布", pasture.pastureTownRows) + addRows("pasture.online", "pastureManagement", "牧户在线率", pasture.pastureOnlineRows) + addRows("pasture.structure", "pastureManagement", "牧户年龄性别结构", pasture.pastureStructureRows) + addRows("pasture.age", "pastureManagement", "牧户年龄结构", pasture.pastureAgeRows) + addRows("pasture.gender", "pastureManagement", "牧户性别结构", pasture.pastureGenderRows) + addRows("pasture.list", "pastureManagement", "牧户示例列表", pasture.pastureListRows) + addRows("pasture.grassland", "pastureManagement", "牧户草场资源", pasture.pastureGrasslandRows) + addRows("pasture.season", "pastureManagement", "牧户季节草场", pasture.pastureSeasonRows) + addRows("pasture.map", "pastureManagement", "牧户地图统计", pasture.pastureMapRows) + + addRows("yak.town", "yakManagement", "牦牛乡镇分布", yak.townRows) + addRows("yak.structureBaseline", "yakManagement", "牦牛结构基准", yak.yakStructureBaselineRows) + addRows("yak.structureRatio", "yakManagement", "牦牛结构比例", yak.yakStructureRatioRows) + addRows("yak.structureRows", "yakManagement", "牦牛结构数量", yak.structureRows) + addRows("yak.tradeImpact", "yakManagement", "牦牛交易影响", yak.tradeImpactRows) + addRows("yak.genderAge", "yakManagement", "牦牛年龄性别", yak.genderAgeRows) + addRows("yak.forecastPeriods", "yakManagement", "牦牛预测周期", yak.forecastPeriods) + addRows("yak.forecastFactors", "yakManagement", "牦牛预测因素", yak.forecastFactorRows) + addRows("yak.forecastRows", "yakManagement", "牦牛月度预测", yak.yakForecastRows) + addConfig("yak.stockTotals", "yakManagement", "牦牛管理总量配置", { + YAK_STOCK_TOTAL: yak.YAK_STOCK_TOTAL, + recognitionTotal: yak.recognitionTotal, + expertCorrection: yak.expertCorrection, + tradeDeduction: yak.tradeDeduction, + estimatedStock: yak.estimatedStock, + }) + + addRows("balance.county", "grassLivestockBalance", "草畜平衡县级数据", [balance.countyBalanceRow]) + addRows("balance.town", "grassLivestockBalance", "草畜平衡乡镇数据", balance.balanceTownRows) + addRows("balance.fallbackRows", "grassLivestockBalance", "草畜平衡兜底数据", balance.balanceFallbackRows) + addConfig("balance.params", "grassLivestockBalance", "草畜平衡计算参数", balance.BALANCE_CALCULATION_PARAMS) + addConfig("balance.countyTarget", "grassLivestockBalance", "草畜平衡县级对象", balance.COUNTY_TARGET) + addConfig("balance.riskLevels", "grassLivestockBalance", "草畜平衡风险等级", balance.RISK_LEVELS) + + addRows("industry.topStats", "yakIndustryChain", "产业链顶部指标", industry.industryTopStats) + addRows("industry.overview", "yakIndustryChain", "产业链总览指标", industry.industryOverviewRows) + addRows("industry.stageComposition", "yakIndustryChain", "产业链阶段构成", industry.stageCompositionRows) + addRows("industry.stageRows", "yakIndustryChain", "产业链阶段数据", industry.industryStageRows) + addRows("industry.breedingLevel", "yakIndustryChain", "养殖水平", industry.breedingLevelRows) + addRows("industry.forageSecurity", "yakIndustryChain", "饲草保障", industry.forageSecurityRows) + addRows("industry.value", "yakIndustryChain", "产业价值", industry.industryValueRows) + addRows("industry.processingAbility", "yakIndustryChain", "加工能力", industry.processingAbilityRows) + addRows("industry.serviceCapability", "yakIndustryChain", "服务能力", industry.serviceCapabilityRows) + addRows("industry.financial", "yakIndustryChain", "金融服务", industry.industryFinancialRows) + addRows("industry.supportPoints", "yakIndustryChain", "饲草储备点位", industry.supportPointRows) + addRows("industry.processingPoints", "yakIndustryChain", "加工屠宰点位", industry.processingPointRows) + addRows("industry.points", "yakIndustryChain", "产业链点位", industry.industryPointRows) + addRows("industry.list", "yakIndustryChain", "产业链主体列表", industry.industryChainListRows) + addConfig("industry.stageDefs", "yakIndustryChain", "产业链阶段定义", industry.industryStageDefs) + addConfig("industry.datasetMeta", "yakIndustryChain", "产业链数据集信息", { + generatedAt: industry.industryDatasetGeneratedAt, + }) + + const subjectRows = uniqueRows(industry.industryChainListRows || [], (row, index) => + String(row.id || row.key || row.name || index), + ) + subjectRows.forEach((row, index) => industrySubjects.push({ row, index })) + + addRows("ecology.parkTown", "ecologicalProtection", "国家公园乡镇数据", ecology.parkTownRows) + addRows("ecology.parkCore", "ecologicalProtection", "国家公园核心指标", ecology.parkCoreRows) + addRows("ecology.parkTopStats", "ecologicalProtection", "国家公园顶部指标", ecology.parkTopStats) + addRows("ecology.restorationTopStats", "ecologicalProtection", "生态修复顶部指标", ecology.restorationTopStats) + addRows("ecology.engineeringAnalysisTopStats", "ecologicalProtection", "工程分析顶部指标", ecology.engineeringAnalysisTopStats) + addRows("ecology.engineeringProjectFeatures", "ecologicalProtection", "工程分析项目点", ecology.engineeringAnalysisProjectFeatures) + addConfig("ecology.menu", "ecologicalProtection", "生态保护菜单", ecology.ecologicalProtectionMenu) + addConfig("ecology.parkPanels", "ecologicalProtection", "国家公园面板", ecology.parkPanels) + addConfig("ecology.restorationPanels", "ecologicalProtection", "生态修复面板", ecology.restorationPanels) + addConfig("ecology.engineeringAnalysisPanels", "ecologicalProtection", "工程分析面板", ecology.engineeringAnalysisPanels) + addConfig("ecology.centerMapNotes", "ecologicalProtection", "生态保护地图说明", ecology.centerMapNotes) +} + +function pickValueKey(row = {}) { + for (const key of VALUE_KEYS) { + const value = row[key] + if (value !== undefined && value !== null && value !== "" && Number.isFinite(Number(value))) { + return key + } + } + const firstNumeric = Object.keys(row).find((key) => Number.isFinite(Number(row[key]))) + return firstNumeric || "" +} + +function normalizeRow(datasetKey, row, index) { + const valueKey = pickValueKey(row) + const value = valueKey ? row[valueKey] : undefined + const rowKey = row.key ?? row.id ?? row.code ?? row.areaCode ?? row.area_code ?? row.name ?? row.label ?? `${datasetKey}-${index + 1}` + return { + datasetKey, + rowKey: String(rowKey).slice(0, 255), + valueKey, + areaCode: row.areaCode ?? row.area_code ?? row.townCode ?? row.town_code ?? row.code ?? null, + areaName: row.areaName ?? row.area_name ?? row.townName ?? row.town ?? row.zoneName ?? null, + name: row.name ?? row.zh ?? row.label ?? row.areaName ?? row.area_name ?? row.zoneName ?? row.projectName ?? null, + valueNum: value !== undefined && value !== null && value !== "" && Number.isFinite(Number(value)) ? Number(value) : null, + valueText: value !== undefined && value !== null && value !== "" && !Number.isFinite(Number(value)) ? String(value) : null, + unit: row.unit ?? null, + percentNum: row.percent !== undefined && row.percent !== null && Number.isFinite(Number(row.percent)) ? Number(row.percent) : null, + extraText: row.extra ?? row.note ?? row.description ?? null, + period: row.year ?? row.month ?? row.period ?? row.label ?? null, + lng: row.lng ?? row.longitude ?? null, + lat: row.lat ?? row.latitude ?? null, + sortOrder: index, + attrs: row, + } +} + +function normalizeIndustrySubject(row, index) { + const subjectKey = String(row.id || row.key || row.name || `industry-subject-${index + 1}`).slice(0, 255) + const valueKey = pickValueKey(row) + return { + subjectKey, + stageKey: row.stageKey ?? row.stage_key ?? row.industryStage ?? null, + categoryCode: row.categoryCode ?? row.category_code ?? null, + categoryName: row.categoryName ?? row.category_name ?? null, + name: row.name ?? row.subjectName ?? subjectKey, + town: row.town ?? row.townName ?? row.areaName ?? null, + areaName: row.areaName ?? row.area_name ?? null, + address: row.address ?? row.location ?? row.detailAddress ?? null, + lng: row.lng ?? row.longitude ?? null, + lat: row.lat ?? row.latitude ?? null, + valueNum: valueKey && Number.isFinite(Number(row[valueKey])) ? Number(row[valueKey]) : null, + unit: row.unit ?? null, + sortOrder: index, + attrs: row, + sourceMeta: { + sourceType: "frontend_seed", + categoryCode: row.categoryCode ?? null, + generatedAt: exported.industry.industryDatasetGeneratedAt || null, + }, + } +} + +function sqlText(value) { + if (value === undefined || value === null) return "NULL" + return `'${String(value).replace(/'/g, "''")}'` +} + +function sqlNumber(value) { + if (value === undefined || value === null || value === "") return "NULL" + const number = Number(value) + return Number.isFinite(number) ? String(number) : "NULL" +} + +function sqlBoolean(value) { + return value ? "true" : "false" +} + +function sqlJson(value) { + return `${sqlText(JSON.stringify(value ?? {}))}::jsonb` +} + +function buildSql() { + collectDefinitions(exported) + const datasetKeys = datasetDefinitions.map((item) => item.datasetKey) + const configKeys = configDefinitions.map((item) => item.configKey) + + const lines = [ + "-- Generated by screenV2.0/scripts/export-screen2-datasets.mjs", + "-- Source: current Screen2.0 frontend static data modules.", + "", + "BEGIN;", + "", + ] + + if (datasetKeys.length) { + lines.push(`DELETE FROM public.screen2_dataset_row WHERE dataset_key IN (${datasetKeys.map(sqlText).join(", ")});`) + } + if (industrySubjects.length) { + lines.push("DELETE FROM public.screen2_industry_subject WHERE source_type = 'frontend_seed';") + } + if (configKeys.length) { + lines.push(`DELETE FROM public.screen2_config WHERE config_key IN (${configKeys.map(sqlText).join(", ")});`) + } + lines.push("") + + datasetDefinitions.forEach((dataset, datasetIndex) => { + lines.push(`INSERT INTO public.screen2_dataset ( + dataset_key, page_code, module_code, dataset_type, source_type, title, sort_order, enabled, payload, update_time +) VALUES ( + ${sqlText(dataset.datasetKey)}, ${sqlText(dataset.pageCode)}, ${sqlText(dataset.moduleCode)}, + ${sqlText(dataset.datasetType)}, ${sqlText(dataset.sourceType)}, ${sqlText(dataset.title)}, + ${datasetIndex}, true, ${sqlJson(dataset.payload)}, now() +) +ON CONFLICT (dataset_key) DO UPDATE SET + page_code = EXCLUDED.page_code, + module_code = EXCLUDED.module_code, + dataset_type = EXCLUDED.dataset_type, + source_type = EXCLUDED.source_type, + title = EXCLUDED.title, + sort_order = EXCLUDED.sort_order, + enabled = EXCLUDED.enabled, + payload = EXCLUDED.payload, + update_time = now();`) + + dataset.rows.map((row, index) => normalizeRow(dataset.datasetKey, row, index)).forEach((row) => { + lines.push(`INSERT INTO public.screen2_dataset_row ( + dataset_key, row_key, value_key, area_code, area_name, name, value_num, value_text, + unit, percent_num, extra_text, period, lng, lat, sort_order, enabled, attrs, update_time +) VALUES ( + ${sqlText(row.datasetKey)}, ${sqlText(row.rowKey)}, ${sqlText(row.valueKey || null)}, + ${sqlText(row.areaCode)}, ${sqlText(row.areaName)}, ${sqlText(row.name)}, ${sqlNumber(row.valueNum)}, + ${sqlText(row.valueText)}, ${sqlText(row.unit)}, ${sqlNumber(row.percentNum)}, ${sqlText(row.extraText)}, + ${sqlText(row.period)}, ${sqlNumber(row.lng)}, ${sqlNumber(row.lat)}, ${row.sortOrder}, + true, ${sqlJson(row.attrs)}, now() +) +ON CONFLICT (dataset_key, row_key) DO UPDATE SET + value_key = EXCLUDED.value_key, + area_code = EXCLUDED.area_code, + area_name = EXCLUDED.area_name, + name = EXCLUDED.name, + value_num = EXCLUDED.value_num, + value_text = EXCLUDED.value_text, + unit = EXCLUDED.unit, + percent_num = EXCLUDED.percent_num, + extra_text = EXCLUDED.extra_text, + period = EXCLUDED.period, + lng = EXCLUDED.lng, + lat = EXCLUDED.lat, + sort_order = EXCLUDED.sort_order, + enabled = EXCLUDED.enabled, + attrs = EXCLUDED.attrs, + update_time = now();`) + }) + }) + + configDefinitions.forEach((config) => { + lines.push(`INSERT INTO public.screen2_config ( + config_key, group_key, title, payload, enabled, update_time +) VALUES ( + ${sqlText(config.configKey)}, ${sqlText(config.groupKey)}, ${sqlText(config.title)}, + ${sqlJson(config.payload)}, true, now() +) +ON CONFLICT (config_key) DO UPDATE SET + group_key = EXCLUDED.group_key, + title = EXCLUDED.title, + payload = EXCLUDED.payload, + enabled = EXCLUDED.enabled, + update_time = now();`) + }) + + industrySubjects.map(({ row, index }) => normalizeIndustrySubject(row, index)).forEach((subject) => { + lines.push(`INSERT INTO public.screen2_industry_subject ( + subject_key, stage_key, category_code, category_name, name, town, area_name, address, + lng, lat, value_num, unit, sort_order, source_type, enabled, attrs, source_meta, update_time +) VALUES ( + ${sqlText(subject.subjectKey)}, ${sqlText(subject.stageKey)}, ${sqlText(subject.categoryCode)}, + ${sqlText(subject.categoryName)}, ${sqlText(subject.name)}, ${sqlText(subject.town)}, + ${sqlText(subject.areaName)}, ${sqlText(subject.address)}, ${sqlNumber(subject.lng)}, ${sqlNumber(subject.lat)}, + ${sqlNumber(subject.valueNum)}, ${sqlText(subject.unit)}, ${subject.sortOrder}, 'frontend_seed', + ${sqlBoolean(true)}, ${sqlJson(subject.attrs)}, ${sqlJson(subject.sourceMeta)}, now() +) +ON CONFLICT (subject_key) DO UPDATE SET + stage_key = EXCLUDED.stage_key, + category_code = EXCLUDED.category_code, + category_name = EXCLUDED.category_name, + name = EXCLUDED.name, + town = EXCLUDED.town, + area_name = EXCLUDED.area_name, + address = EXCLUDED.address, + lng = EXCLUDED.lng, + lat = EXCLUDED.lat, + value_num = EXCLUDED.value_num, + unit = EXCLUDED.unit, + sort_order = EXCLUDED.sort_order, + source_type = EXCLUDED.source_type, + enabled = EXCLUDED.enabled, + attrs = EXCLUDED.attrs, + source_meta = EXCLUDED.source_meta, + update_time = now();`) + }) + + lines.push("") + lines.push("COMMIT;") + lines.push("") + return lines.join("\n") +} + +const sql = buildSql() +if (outputPath) { + await fs.promises.mkdir(path.dirname(path.resolve(outputPath)), { recursive: true }) + await fs.promises.writeFile(path.resolve(outputPath), sql, "utf8") +} else { + process.stdout.write(sql) +}