diff --git a/package-lock.json b/package-lock.json
index f20bfbb..12a6fc6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"ezuikit-js": "^0.7.2",
"gsap": "^3.12.2",
"normalize.css": "^8.0.1",
+ "ol": "^10.9.0",
"point-in-polygon": "^1.1.0",
"shpjs": "^6.2.0",
"three": "0.161.0",
@@ -768,6 +769,18 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/@petamoriken/float16": {
+ "version": "3.9.3",
+ "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz",
+ "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/rbush": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz",
+ "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==",
+ "license": "MIT"
+ },
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz",
@@ -915,6 +928,16 @@
"integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
"license": "MIT"
},
+ "node_modules/@zarrita/storage": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.2.0.tgz",
+ "integrity": "sha512-855ZXqtnds7spnT8vNvD+MXa3QExP1m2GqShe8yt7uZXHnQLgJHgkpVwFjE1B0KDDRO0ki09hmk6OboTaIfPsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "reference-spec-reader": "^0.2.0",
+ "unzipit": "2.0.0"
+ }
+ },
"node_modules/autofit.js": {
"version": "3.2.8",
"resolved": "https://registry.npmmirror.com/autofit.js/-/autofit.js-3.2.8.tgz",
@@ -993,6 +1016,12 @@
"node": ">=8"
}
},
+ "node_modules/earcut": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz",
+ "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==",
+ "license": "ISC"
+ },
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
@@ -1087,6 +1116,25 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/geotiff": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-3.0.5.tgz",
+ "integrity": "sha512-OWcL9S9+yDZ6iAlXMt32T1iwUApJM8UiD47xbm6ZP1h33d10fqkPs14EG/ttT5EnefpZSx3G15iDFC5FxUNUwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@petamoriken/float16": "^3.9.3",
+ "lerc": "^3.0.0",
+ "pako": "^2.0.4",
+ "parse-headers": "^2.0.2",
+ "quick-lru": "^6.1.1",
+ "web-worker": "^1.5.0",
+ "xml-utils": "^1.10.2",
+ "zstddec": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=10.19"
+ }
+ },
"node_modules/gsap": {
"version": "3.14.2",
"resolved": "https://registry.npmmirror.com/gsap/-/gsap-3.14.2.tgz",
@@ -1134,6 +1182,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/lerc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz",
+ "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==",
+ "license": "Apache-2.0"
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -1188,12 +1242,79 @@
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==",
"license": "MIT"
},
+ "node_modules/numcodecs": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/numcodecs/-/numcodecs-0.3.2.tgz",
+ "integrity": "sha512-6YSPnmZgg0P87jnNhi3s+FVLOcIn3y+1CTIgUulA3IdASzK9fJM87sUFkpyA+be9GibGRaST2wCgkD+6U+fWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "fflate": "^0.8.0"
+ }
+ },
+ "node_modules/numcodecs/node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "license": "MIT"
+ },
+ "node_modules/ol": {
+ "version": "10.9.0",
+ "resolved": "https://registry.npmjs.org/ol/-/ol-10.9.0.tgz",
+ "integrity": "sha512-svbbgVQUmEHaKpLQ8kRySojs59Brvgl2zYIrqG9eQNXGfsbi55rQasZIDpwpQzDL6OlzrUb0H4hQaiX9wDoGmA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/rbush": "4.0.0",
+ "earcut": "^3.0.0",
+ "geotiff": "^3.0.5 || ^3.1.0-beta.0",
+ "pbf": "4.0.1",
+ "rbush": "^4.0.0",
+ "zarrita": "^0.7.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/openlayers"
+ }
+ },
+ "node_modules/pako": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
+ "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/parse-headers": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
+ "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
+ "license": "MIT"
+ },
"node_modules/parsedbf": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-2.0.0.tgz",
"integrity": "sha512-WNjKn/cwgGBkXqQLif+2VMEahcRHkBRU0/RfBWZ7Vj7snRNNW63yW1mVuuHRDyXTRxuGCzAHHBcr/Fn+U/bXjQ==",
"license": "MIT"
},
+ "node_modules/pbf": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
+ "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "resolve-protobuf-schema": "^2.1.0"
+ },
+ "bin": {
+ "pbf": "bin/pbf"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@@ -1269,6 +1390,39 @@
}
}
},
+ "node_modules/protocol-buffers-schema": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
+ "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
+ "license": "MIT"
+ },
+ "node_modules/quick-lru": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
+ "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/quickselect": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
+ "license": "ISC"
+ },
+ "node_modules/rbush": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz",
+ "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "quickselect": "^3.0.0"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
@@ -1283,12 +1437,27 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/reference-spec-reader": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/reference-spec-reader/-/reference-spec-reader-0.2.0.tgz",
+ "integrity": "sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==",
+ "license": "MIT"
+ },
"node_modules/resize-detector": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/resize-detector/-/resize-detector-0.3.0.tgz",
"integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==",
"license": "MIT"
},
+ "node_modules/resolve-protobuf-schema": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "protocol-buffers-schema": "^3.3.1"
+ }
+ },
"node_modules/robust-predicates": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz",
@@ -1380,6 +1549,15 @@
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
+ "node_modules/unzipit": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unzipit/-/unzipit-2.0.0.tgz",
+ "integrity": "sha512-DVeVIWUZCAQPNzm5sB0hpsG1GygTTdBnzNtYYEpInkttx5evkyqRgZi6rTczoySqp8hO5jHVKzrH0f23X8FZLg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz",
@@ -1536,6 +1714,12 @@
"vue": "^3.5.0"
}
},
+ "node_modules/web-worker": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
+ "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
+ "license": "Apache-2.0"
+ },
"node_modules/wkt-parser": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.5.tgz",
@@ -1545,6 +1729,22 @@
"url": "https://github.com/sponsors/ahocevar"
}
},
+ "node_modules/xml-utils": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
+ "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/zarrita": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.7.3.tgz",
+ "integrity": "sha512-wChTQ1Ox75INoQCzKAfLWAfB70JJ4KjdW8Sz5x4ZWrFB4Dw+YZdnxHTL0xSdsrB9EmKSeK7fS1Y+I2ibhfGbkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zarrita/storage": "^0.2.0",
+ "numcodecs": "^0.3.2"
+ }
+ },
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz",
@@ -1553,6 +1753,12 @@
"dependencies": {
"tslib": "2.3.0"
}
+ },
+ "node_modules/zstddec": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0.tgz",
+ "integrity": "sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==",
+ "license": "MIT AND BSD-3-Clause"
}
}
}
diff --git a/package.json b/package.json
index 6e4b326..64f70d4 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"ezuikit-js": "^0.7.2",
"gsap": "^3.12.2",
"normalize.css": "^8.0.1",
+ "ol": "^10.9.0",
"point-in-polygon": "^1.1.0",
"shpjs": "^6.2.0",
"three": "0.161.0",
diff --git a/src/router/index.js b/src/router/index.js
index f36c0ef..25c4c8d 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -3,6 +3,7 @@ import { fetchCurrentUser, getAuthorizationToken, getLoginPath } from "@/service
const platform = () => import("@/views/platform/index.vue")
const login = () => import("@/views/login/index.vue")
+const chartStyleGuide = () => import("@/views/chartStyleGuide/index.vue")
const router = createRouter({
history: createWebHashHistory(),
routes: [
@@ -23,6 +24,10 @@ const router = createRouter({
path: "/forest-grass-wet",
component: platform,
},
+ {
+ path: "/chart-style-guide",
+ component: chartStyleGuide,
+ },
{
path: "/:pathMatch(.*)",
diff --git a/src/services/ndviTimeline.js b/src/services/ndviTimeline.js
new file mode 100644
index 0000000..daaa008
--- /dev/null
+++ b/src/services/ndviTimeline.js
@@ -0,0 +1,72 @@
+const NDVI_TIMELINE_URL = "/jsyApi/api/map/ndvi_timeline"
+
+export async function getGrasslandNdviTimeline() {
+ const response = await fetch(NDVI_TIMELINE_URL, {
+ credentials: "include",
+ headers: {
+ Accept: "application/json",
+ },
+ })
+ if (!response.ok) {
+ const error = new Error(`HTTP ${response.status}`)
+ error.status = response.status
+ throw error
+ }
+ const data = await response.json().catch(() => ({}))
+ if (data?.code === 401 || data?.code === "401") {
+ const error = new Error(data?.msg || data?.message || "NDVI 时间轴未授权")
+ error.status = 401
+ throw error
+ }
+ const rows = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []
+ return normalizeNdviTimelineRows(rows)
+}
+
+function normalizeNdviTimelineRows(rows = []) {
+ const seen = new Set()
+ return rows
+ .map((row, index) => {
+ const layerName = normalizeTimelineLayerName(pickValue(row, ["url", "layerName", "layer_name", "layer", "name"]))
+ if (!layerName) return null
+ const rawDate = pickValue(row, ["date", "month", "time", "label"]) || layerName
+ const label = formatTimelineLabel(rawDate)
+ const key = `${layerName}-${rawDate || index}`
+ return {
+ key,
+ label,
+ date: String(rawDate),
+ layerName,
+ source: row,
+ }
+ })
+ .filter(Boolean)
+ .filter((item) => {
+ if (seen.has(item.key)) return false
+ seen.add(item.key)
+ return true
+ })
+}
+
+function pickValue(item, keys) {
+ for (const key of keys) {
+ if (item?.[key] !== undefined && item?.[key] !== null && item?.[key] !== "") {
+ return item[key]
+ }
+ }
+ return undefined
+}
+
+function normalizeTimelineLayerName(value) {
+ const text = String(value || "").trim()
+ if (!text) return ""
+ if (text.includes(":") || /^https?:\/\//i.test(text)) return text
+ return `ne:${text}`
+}
+
+function formatTimelineLabel(value) {
+ const text = String(value || "").trim()
+ if (!text) return ""
+ const match = text.match(/(\d{4})[-_/年. ]?(0?[1-9]|1[0-2])(?:月)?/)
+ if (match) return `${match[1]}.${String(Number(match[2])).padStart(2, "0")}`
+ return text
+}
diff --git a/src/utils/chartTheme.js b/src/utils/chartTheme.js
index 4249669..8c19889 100644
--- a/src/utils/chartTheme.js
+++ b/src/utils/chartTheme.js
@@ -124,11 +124,238 @@ export function withChartDefaults(option = {}) {
return normalized
}
+export function createSystemBarOption({
+ categories = [],
+ rows = [],
+ unit = "",
+ name = "指标",
+ orientation = "vertical",
+ colorIndex = 0,
+ grid = {},
+ showLabel = true,
+} = {}) {
+ const color = chartPalette[colorIndex % chartPalette.length]
+ const horizontal = orientation === "horizontal"
+ const data = rows.map((item, index) => ({
+ name: item.name || categories[index],
+ value: Number(item.value) || 0,
+ itemStyle: {
+ borderRadius: horizontal ? [0, 8, 8, 0] : [8, 8, 0, 0],
+ color: horizontal ? horizontalGradient(item.color || color, "rgba(48, 220, 255, 0.14)", item.color || color) : verticalGradient(item.color || color),
+ },
+ }))
+
+ return withChartDefaults({
+ color: chartPalette,
+ grid: chartGrid(horizontal ? { left: 72, right: showLabel ? 58 : 26, top: 24, bottom: 22, ...grid } : { left: 46, right: 24, top: 28, bottom: 42, ...grid }),
+ tooltip: chartTooltip((params) => `${params.marker}${params.name}:${formatChartValue(params.value)}${unit}`),
+ xAxis: horizontal ? valueAxis() : categoryAxis(categories, { axisLabel: { rotate: categories.length > 6 ? 24 : 0 } }),
+ yAxis: horizontal ? categoryAxis(categories, { inverse: true, axisLabel: { color: "rgba(225, 249, 255, 0.82)" } }) : valueAxis(),
+ series: [
+ {
+ name,
+ type: "bar",
+ barWidth: horizontal ? 12 : categories.length > 7 ? 14 : 20,
+ data,
+ label: {
+ show: showLabel,
+ position: horizontal ? "right" : "top",
+ color: "#f7feff",
+ fontSize: 10,
+ formatter: ({ value }) => formatChartValue(value),
+ },
+ },
+ ],
+ })
+}
+
+export function createSystemGroupedBarOption({ categories = [], series = [], unit = "", grid = {} } = {}) {
+ return withChartDefaults({
+ color: chartPalette,
+ grid: chartGrid({ left: 46, right: 20, top: 42, bottom: 42, ...grid }),
+ tooltip: chartTooltip((params) => formatAxisTooltip(params, unit), { trigger: "axis", axisPointer: { type: "shadow" } }),
+ legend: chartLegend({ top: 0, right: 4 }),
+ xAxis: categoryAxis(categories),
+ yAxis: valueAxis(),
+ series: series.map((item, index) => ({
+ name: item.name,
+ type: "bar",
+ barWidth: 10,
+ data: item.values || [],
+ itemStyle: {
+ borderRadius: [6, 6, 0, 0],
+ color: verticalGradient(chartPalette[index % chartPalette.length]),
+ },
+ })),
+ })
+}
+
+export function createSystemStackedBarOption({ categories = [], series = [], unit = "", grid = {} } = {}) {
+ return withChartDefaults({
+ color: chartPalette,
+ grid: chartGrid({ left: 48, right: 22, top: 42, bottom: 42, ...grid }),
+ tooltip: chartTooltip((params) => formatAxisTooltip(params, unit), { trigger: "axis", axisPointer: { type: "shadow" } }),
+ legend: chartLegend({ top: 0, right: 4 }),
+ xAxis: categoryAxis(categories),
+ yAxis: valueAxis(),
+ series: series.map((item, index) => ({
+ name: item.name,
+ type: "bar",
+ stack: "total",
+ barWidth: 18,
+ data: item.values || [],
+ itemStyle: {
+ color: verticalGradient(chartPalette[index % chartPalette.length]),
+ },
+ })),
+ })
+}
+
+export function createSystemLineOption({ categories = [], series = [], unit = "", area = false, grid = {} } = {}) {
+ return withChartDefaults({
+ color: chartPalette,
+ grid: chartGrid({ left: 46, right: 24, top: 42, bottom: 38, ...grid }),
+ tooltip: chartTooltip((params) => formatAxisTooltip(params, unit), {
+ trigger: "axis",
+ axisPointer: {
+ type: "line",
+ lineStyle: { color: "rgba(255, 224, 107, 0.54)" },
+ },
+ }),
+ legend: chartLegend({ top: 0, right: 4 }),
+ xAxis: categoryAxis(categories),
+ yAxis: valueAxis(),
+ series: series.map((item, index) => {
+ const color = item.color || chartPalette[index % chartPalette.length]
+ return {
+ name: item.name,
+ type: "line",
+ smooth: true,
+ symbol: "circle",
+ symbolSize: 7,
+ data: item.values || [],
+ lineStyle: {
+ width: 3,
+ color,
+ shadowColor: `${color}55`,
+ shadowBlur: 10,
+ },
+ itemStyle: {
+ color: "#041522",
+ borderColor: color,
+ borderWidth: 2,
+ },
+ areaStyle: area
+ ? {
+ color: verticalGradient(color, `${color}08`),
+ }
+ : undefined,
+ }
+ }),
+ })
+}
+
+export function createSystemPieOption({ rows = [], unit = "", name = "结构占比", rose = false, donut = true } = {}) {
+ return withChartDefaults({
+ color: chartPalette,
+ tooltip: chartTooltip((params) => `${params.marker}${params.name}:${formatChartValue(params.value)}${unit}
占比:${formatChartValue(params.percent)}%`),
+ legend: chartLegend({
+ orient: "vertical",
+ right: 6,
+ top: "middle",
+ itemWidth: 9,
+ itemHeight: 9,
+ }),
+ series: [
+ {
+ name,
+ type: "pie",
+ radius: donut ? ["45%", rose ? "74%" : "66%"] : rose ? [12, "72%"] : "64%",
+ center: ["36%", "52%"],
+ roseType: rose ? "radius" : undefined,
+ avoidLabelOverlap: true,
+ itemStyle: {
+ borderColor: "rgba(3, 18, 31, 0.94)",
+ borderWidth: 3,
+ },
+ label: {
+ color: "#f7feff",
+ fontSize: 10,
+ formatter: "{b}\n{d}%",
+ },
+ labelLine: {
+ length: 8,
+ length2: 8,
+ lineStyle: { color: "rgba(223, 250, 255, 0.42)" },
+ },
+ data: rows.map((row, index) => ({
+ name: row.name,
+ value: Number(row.value) || 0,
+ itemStyle: { color: row.color || chartPalette[index % chartPalette.length] },
+ })),
+ },
+ ],
+ })
+}
+
+export function createSystemRadarOption({ indicators = [], rows = [], name = "综合评价" } = {}) {
+ return withChartDefaults({
+ color: chartPalette,
+ tooltip: chartTooltip(),
+ legend: chartLegend({ top: 0, right: 4 }),
+ radar: {
+ center: ["50%", "58%"],
+ radius: "62%",
+ indicator: indicators,
+ splitNumber: 4,
+ axisName: {
+ color: "rgba(223, 250, 255, 0.78)",
+ fontSize: 10,
+ },
+ axisLine: { lineStyle: { color: "rgba(48, 220, 255, 0.18)" } },
+ splitLine: { lineStyle: { color: "rgba(48, 220, 255, 0.16)" } },
+ splitArea: {
+ areaStyle: {
+ color: ["rgba(48, 220, 255, 0.04)", "rgba(36, 246, 196, 0.02)"],
+ },
+ },
+ },
+ series: [
+ {
+ name,
+ type: "radar",
+ data: rows.map((row, index) => ({
+ name: row.name,
+ value: row.values || [],
+ areaStyle: { color: `${chartPalette[index % chartPalette.length]}24` },
+ lineStyle: { color: chartPalette[index % chartPalette.length], width: 2 },
+ itemStyle: { color: chartPalette[index % chartPalette.length] },
+ })),
+ },
+ ],
+ })
+}
+
+export function formatChartValue(value) {
+ const number = Number(value)
+ if (!Number.isFinite(number)) return "--"
+ return Number.isInteger(number) ? number.toLocaleString("zh-CN") : number.toLocaleString("zh-CN", { maximumFractionDigits: 2 })
+}
+
function stripAxisOverrides(overrides = {}) {
const { axisLabel, axisLine, axisTick, splitLine, ...rest } = overrides
return rest
}
+function formatAxisTooltip(params, unit = "") {
+ const rows = Array.isArray(params) ? params : [params]
+ const title = rows[0]?.axisValueLabel || rows[0]?.name || ""
+ const body = rows
+ .map((item) => `${item.marker}${item.seriesName || item.name}:${formatChartValue(item.value)}${unit}`)
+ .join("
")
+ return `${title}
${body}`
+}
+
function normalizeChartTypography(target) {
if (!target || typeof target !== "object") return
const styleKeys = ["textStyle", "axisLabel", "nameTextStyle", "label", "axisName"]
diff --git a/src/views/chartStyleGuide/index.vue b/src/views/chartStyleGuide/index.vue
new file mode 100644
index 0000000..5d053c0
--- /dev/null
+++ b/src/views/chartStyleGuide/index.vue
@@ -0,0 +1,894 @@
+
+ 图表样式规范
+