diff --git a/web/.env.development b/web/.env.development index dcf5a27..1585102 100644 --- a/web/.env.development +++ b/web/.env.development @@ -3,7 +3,7 @@ VITE_CLI_PORT = 8080 VITE_SERVER_PORT = 8000 VITE_BASE_API = /api VITE_FILE_API = /api -VITE_BASE_PATH = http://127.0.0.1 +VITE_BASE_PATH = http://sw2.fuhua91.com VITE_POSITION = open VITE_EDITOR = code // VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm diff --git a/web/.gitignore b/web/.gitignore index a2163d9..0eaf983 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -6,3 +6,10 @@ bun.lockb config.yaml # 渲染产物:Jenkins 从 .docker-compose/nginx/conf.d/my.conf 生成 /nginx.k8s.conf + +# Cursor Codegraph 本地索引 +.codegraph/ + +# Vite 插件自动生成的类型声明 +src/types/auto-imports.d.ts +src/types/components.d.ts diff --git a/web/README.md b/web/README.md index eb575d4..c30e71e 100644 --- a/web/README.md +++ b/web/README.md @@ -1,16 +1,18 @@ # Kra 管理后台 -Kra 的 Vue 3 管理端,与仓库中的 Kratos + Gin 后端配套使用。 +Kra 的 Vue 3 商务管理端,本仓库使用 `kra` 分支。 + +Go 后端单独维护于 [java/sw](http://gitlab.fuhua91.com/java/sw.git) 的 `dev` 分支,本仓库不包含后端源代码。 ```bash -npm install -npm run dev +pnpm install --frozen-lockfile +pnpm dev ``` 生产构建: ```bash -npm run build +pnpm build ``` -开发环境默认把 `/api` 代理到 `http://127.0.0.1:8000`。 +开发环境沿用当前分支配置,将 `/api` 代理到 `http://sw2.fuhua91.com/api`。生产构建使用同域 `/api`;本地独立后端默认监听 `8000`,切换本地调试时需同步调整 `.env.development` 与 Vite 代理的前缀处理。 diff --git a/web/package.json b/web/package.json index f548406..a8e57dd 100644 --- a/web/package.json +++ b/web/package.json @@ -27,6 +27,7 @@ "axios": "1.8.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "echarts": "^6.1.0", "element-plus": "^2.13.6", "highlight.js": "^11.10.0", "mitt": "^3.0.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 431ed3c..a89d78f 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + echarts: + specifier: ^6.1.0 + version: 6.1.0 element-plus: specifier: ^2.13.6 version: 2.14.4(vue@3.5.41) @@ -1592,6 +1595,9 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + echarts@6.1.0: + resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==} + electron-to-chromium@1.5.405: resolution: {integrity: sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==} @@ -1788,12 +1794,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -2458,6 +2464,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2662,6 +2671,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zrender@6.1.0: + resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==} + snapshots: '@babel/code-frame@7.29.7': @@ -4244,6 +4256,11 @@ snapshots: duplexer@0.1.2: {} + echarts@6.1.0: + dependencies: + tslib: 2.3.0 + zrender: 6.1.0 + electron-to-chromium@1.5.405: {} element-plus@2.14.4(vue@3.5.41): @@ -5069,6 +5086,8 @@ snapshots: totalist@3.0.1: {} + tslib@2.3.0: {} + tslib@2.8.1: {} type-check@0.4.0: @@ -5255,3 +5274,7 @@ snapshots: yallist@3.1.1: {} yocto-queue@0.1.0: {} + + zrender@6.1.0: + dependencies: + tslib: 2.3.0 diff --git a/web/src/view/dashboard/components/BusinessDashboard.vue b/web/src/view/dashboard/components/BusinessDashboard.vue index 5707922..19480a8 100644 --- a/web/src/view/dashboard/components/BusinessDashboard.vue +++ b/web/src/view/dashboard/components/BusinessDashboard.vue @@ -1,5 +1,5 @@ - diff --git a/web/src/view/dashboard/components/dashboard-metrics.js b/web/src/view/dashboard/components/dashboard-metrics.js new file mode 100644 index 0000000..1577bed --- /dev/null +++ b/web/src/view/dashboard/components/dashboard-metrics.js @@ -0,0 +1,32 @@ +import { channelCount, channelMoney } from '../../sw/channel-display.js' + +const definitions = [ + { key: 'todayRegister', label: '玩家注册' }, + { key: 'todayActive', label: '活跃玩家' }, + { key: 'todayPay', label: '充值金额', money: true }, + { key: 'effectivePromote', label: '有效端' }, + { key: 'activePromote', label: '活跃渠道数' }, + { key: 'newPromote', label: '新增渠道数' }, + { key: 'lockedPromote', label: '锁定渠道', event: true }, + { key: 'terminatedPromote', label: '解除合作渠道', event: true } +] + +export const overviewCards = (data) => definitions.map(definition => { + const metric = data?.cards?.[definition.key] + const known = data?.available !== false && metric?.value != null + const changeRate = metric?.changeRate == null ? null : Number(metric.changeRate) + const label = metric?.compareLabel || '较上一同期' + const change = !known ? metric?.compareLabel || '—' : changeRate == null ? `${label}:无可比数据` : `${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}% ${label}` + return { + ...definition, + value: known ? (definition.money ? channelMoney(metric.value) : channelCount(String(metric.value).split('.')[0])) : !data ? '—' : data.available === false || definition.event ? '未接入' : '未获取', + change, + up: known && changeRate > 0, + down: known && changeRate < 0, + tip: definition.key === 'effectivePromote' + ? `渠道注册后 ${data?.settings?.days ?? 30} 天内充值满 ¥${data?.settings?.amount ?? 3000},按达标日期统计。` + : definition.event ? '按所选区间内的状态变更事件统计。' : '所选时间区间内的数据,与上一等长周期比较。' + } +}) + +export const escapeChartText = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])) diff --git a/web/src/view/dashboard/components/dashboard-metrics.test.js b/web/src/view/dashboard/components/dashboard-metrics.test.js new file mode 100644 index 0000000..cc1ff4c --- /dev/null +++ b/web/src/view/dashboard/components/dashboard-metrics.test.js @@ -0,0 +1,24 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { overviewCards, escapeChartText } from './dashboard-metrics.js' + +test('overview preserves exact money, eight cards, and unavailable event data', () => { + const cards = overviewCards({ available: true, settings: { days: 15, amount: 500 }, cards: { + todayPay: { value: '9007199254740993.01', changeRate: null }, + todayRegister: { value: '0.00', changeRate: '0' }, + todayActive: { value: '5998.00', changeRate: '2.1' }, + lockedPromote: { value: null, compareLabel: '事件数据未接入' } + } }) + assert.equal(cards.length, 8) + assert.equal(cards.find(c => c.key === 'todayPay').value, '¥9,007,199,254,740,993.01') + assert.equal(cards.find(c => c.key === 'todayRegister').value, '0') + assert.equal(cards.find(c => c.key === 'todayActive').value, '5,998') + assert.equal(cards.find(c => c.key === 'lockedPromote').value, '未接入') + assert.match(cards.find(c => c.key === 'effectivePromote').tip, /15 天内充值满 ¥500/) + assert.ok(overviewCards(null).every(c => c.value === '—')) + assert.ok(overviewCards({ available: false }).every(c => c.value === '未接入')) +}) + +test('chart HTML tooltips escape provider-controlled names', () => { + assert.equal(escapeChartText('&'), '<img src=x onerror="alert(1)">&') +}) diff --git a/web/src/view/dashboard/index.vue b/web/src/view/dashboard/index.vue index 96837a4..5ef8a8d 100644 --- a/web/src/view/dashboard/index.vue +++ b/web/src/view/dashboard/index.vue @@ -1,23 +1,12 @@ diff --git a/web/vite.config.js b/web/vite.config.js index 4420bb9..1219ad8 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -119,11 +119,9 @@ export default ({ mode }) => { // 把key的路径代理到target位置 // detail: https://cli.vuejs.org/config/#devserver-proxy [env.VITE_BASE_API]: { - // 需要代理的路径 例如 '/api' - target: `${env.VITE_BASE_PATH}:${env.VITE_SERVER_PORT}/`, // 代理到 目标路径 - changeOrigin: true, - rewrite: (path) => - path.replace(new RegExp('^' + env.VITE_BASE_API), '') + // 线上接口形如 http://sw2.fuhua91.com/api/sw/promote/list,保留 /api,不追加 8000 端口 + target: env.VITE_BASE_PATH, + changeOrigin: true } } },