From b5099e5c4a301476e470fa5e2fa34ac11e52795e Mon Sep 17 00:00:00 2001 From: Swanky <413564165@qq.com> Date: Sat, 27 Dec 2025 22:45:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E7=BA=A2?= =?UTF-8?q?=E5=8E=9F=E5=8E=BF=E6=99=BA=E6=85=A7=E6=B4=BB=E7=95=9C=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E5=A4=A7=E6=95=B0=E6=8D=AE=E5=B9=B3=E5=8F=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 35 + PROJECT_HISTORY.md | 535 +++++++++ index.html | 13 + package-lock.json | 1802 ++++++++++++++++++++++++++++ package.json | 21 + public/data/china.json | 1 + public/data/mockData.json | 116 ++ src/App.vue | 477 ++++++++ src/components/FlowMonitorMap.vue | 458 +++++++ src/components/MarketAnalysis.vue | 466 +++++++ src/components/SceneManagement.vue | 1315 ++++++++++++++++++++ src/main.js | 9 + src/style.css | 449 +++++++ vite.config.js | 7 + 14 files changed, 5704 insertions(+) create mode 100644 .gitignore create mode 100644 PROJECT_HISTORY.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/data/china.json create mode 100644 public/data/mockData.json create mode 100644 src/App.vue create mode 100644 src/components/FlowMonitorMap.vue create mode 100644 src/components/MarketAnalysis.vue create mode 100644 src/components/SceneManagement.vue create mode 100644 src/main.js create mode 100644 src/style.css create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44eb70a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Environment files +.env +.env.local +.env.*.local + +# Coverage directory +coverage + +# Misc +*.local diff --git a/PROJECT_HISTORY.md b/PROJECT_HISTORY.md new file mode 100644 index 0000000..3259ff3 --- /dev/null +++ b/PROJECT_HISTORY.md @@ -0,0 +1,535 @@ +# 项目开发历史记录 + +## 一、项目概述 + +本项目是一个牦牛产业数据可视化平台,采用 Vue.js 框架开发,包含多个功能模块:交易流向监测、市场环境监测、多维度市场分析、牦牛供应展示等。平台集成了 Element UI 组件库和 ECharts 数据可视化库,实现了响应式布局和交互式数据展示功能。 + +**技术栈:** +- 前端框架:Vue.js 3.x(Composition API) +- UI 组件库:Element Plus +- 图表库:ECharts +- 地图数据:GeoJSON(china.json) +- 样式方案:CSS Scoped + CSS Flexbox/Grid +- 构建工具:Vite + +--- + +## 二、组件架构演变 + +### 2.1 初始阶段:单文件 App.vue + +项目最初是一个单文件的 `App.vue`,包含了所有的业务逻辑和 UI 代码。随着功能增加,代码变得臃肿,难以维护。 + +### 2.2 组件拆分阶段 + +按照单一职责原则,将 monolithic App.vue 拆分为以下独立组件: + +``` +src/components/ +├── AppHeader.vue # 顶部导航栏 +├── TransactionStats.vue # 实时交易统计 +├── SalesChart.vue # 销售趋势图表 +├── YakPopulationChart.vue # 牦牛存栏图表 +├── FlowMonitorMap.vue # 交易流向地图 +├── SceneManagement.vue # 场景管理(监控+环境) +└── MarketAnalysis.vue # 多维度市场分析 +``` + +--- + +## 三、主题风格演变 + +### 3.1 初始深绿色主题 + +最初尝试使用偏绿色的深色风格,模拟草原风格: + +```css +--primary-color: #22c55e; +--primary-dark: #15803d; +--background-dark: #0f172a; +--card-dark: #1e293b; +``` + +### 3.2 朴素灰色白色主题 + +用户反馈绿色不太合适,要求改回朴实一点的深色主题: + +```css +--background-color: #f8fafc; +--card-background: #ffffff; +--text-primary: #1e293b; +--text-secondary: #64748b; +--border-color: #e2e8f0; +--primary-color: #22c55e; /* 保留绿色作为强调色 */ +``` + +### 3.3 顶部导航栏样式 + +将顶部导航栏从白色改为深蓝色,增强视觉层次: + +```css +.app-header { + background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); + color: white; +} +``` + +--- + +## 四、核心功能模块实现 + +### 4.1 场景管理组件(SceneManagement.vue) + +场景管理组件是平台的核心模块之一,负责展示监控视频和环境监测数据。 + +**布局演变:** + +1. **初始布局**:简单的垂直排列 +2. **卡片式布局**:引入 Element UI 的 el-card 组件 +3. **双栏布局**:左侧展示环境指标,右侧展示监控视频 + +**最终布局结构:** + +```html + + + +
+
+ +
+
+ +
+
+
+ + + +
+
+
+
+ + +
+
+
+
+``` + +**环境监测状态样式:** + +```css +.env-card.normal { + background: #ffffff; + border: 1px solid #e2e8f0; +} + +.env-card.warning { + background: #fef2f2; + border: 1px solid #fecaca; +} + +.env-card.critical { + background: #fef2f2; + border: 1px solid #ef4444; +} +``` + +### 4.2 交易流向监测组件(FlowMonitorMap.vue) + +使用 ECharts 实现地理数据可视化,展示牦牛销售网络、源地供应和红原出栏分布。 + +**实现方案:** + +```javascript + +``` + +### 4.3 多维度市场分析组件(MarketAnalysis.vue) + +包含三个分析维度:销售结构分析、价格趋势分析、客户来源分析。 + +**布局结构:** + +```html + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + +``` + +--- + +## 五、数据架构演变 + +### 5.1 初始阶段:组件内嵌 mock 数据 + +各组件内部定义模拟数据,导致数据冗余和重复请求。 + +```javascript +const mockData = { + salesByPriceRange: [...], + priceTrend: [...], + // ... +}; +``` + +### 5.2 JSON 文件阶段 + +将 mock 数据移到静态 JSON 文件: + +```json +{ + "marketAnalysis": { + "salesByPriceRange": [...], + "salesByRegion": [...], + "priceTrend": [...], + "customerSource": [...] + } +} +``` + +### 5.3 共享数据架构阶段(最终方案) + +在 App.vue 中集中获取数据,通过 props 传递给子组件: + +```javascript +// App.vue + + + +``` + +**优势:** +- 减少网络请求次数(只请求一次) +- 保证数据一致性 +- 便于数据管理和维护 + +--- + +## 六、关键文件结构 + +``` +zhhxjy/ +├── public/ +│ └── data/ +│ ├── mockData.json # 共享模拟数据 +│ └── china.json # 中国地图地理数据 +├── src/ +│ ├── components/ +│ │ ├── AppHeader.vue # 顶部导航 +│ │ ├── TransactionStats.vue +│ │ ├── SalesChart.vue +│ │ ├── YakPopulationChart.vue +│ │ ├── FlowMonitorMap.vue # 交易流向地图 +│ │ ├── SceneManagement.vue # 场景管理 +│ │ └── MarketAnalysis.vue # 市场分析 +│ ├── App.vue # 主应用组件 +│ ├── main.js # 应用入口 +│ └── style.css # 全局样式 +├── index.html +├── package.json +└── vite.config.js +``` + +--- + +## 七、遇到的问题及解决方案 + +### 7.1 地图数据加载失败 + +**问题**:从外部 URL 下载地图数据失败(404 错误) + +**解决方案**: +- 使用本地 china.json 文件 +- 放置在 public/data/ 目录下 +- 通过 `/data/china.json` 路径访问 + +### 7.2 视频容器高度不匹配 + +**问题**:增加视频宽度后,容器高度计算错误,导致内容溢出或留白 + +**解决方案**: +```css +.video-wrapper { + height: 100%; + display: flex; + flex-direction: column; +} + +.video-content { + flex: 1; + height: 100%; +} + +.video-info-main { + flex: 1; + display: flex; + flex-direction: column; +} +``` + +### 7.3 右侧导航按钮消失 + +**问题**:视频宽度增加后,右侧切换按钮被隐藏 + +**解决方案**: +```css +.monitor-right { + width: 800px; /* 增加容器宽度 */ +} + +.video-content { + width: 320px; /* 固定视频宽度 */ +} +``` + +### 7.4 预警颜色不一致 + +**问题**:环境监测的预警状态使用了黄色主题,与整体风格不符 + +**解决方案**: +```css +.env-card.warning { + background: #fef2f2; + border: 1px solid #fecaca; +} + +.env-card.critical { + background: #fef2f2; + border: 1px solid #ef4444; +} + +.warning-badge { + background: #ef4444; + color: white; +} +``` + +### 7.5 Mock 数据位置错误 + +**问题**:mockData.json 放在 src/data 目录,无法通过 HTTP 请求访问 + +**解决方案**: +- 创建 public/data/ 目录 +- 将 mockData.json 移动到新目录 +- 通过 `/data/mockData.json` 路径访问 + +--- + +## 八、开发里程碑 + +### 里程碑 1:组件拆分 +- 完成 App.vue 到独立组件的拆分 +- 建立规范的目录结构 + +### 里程碑 2:主题定制 +- 实现灰色白色主题 +- 保留绿色作为强调色 +- 优化顶部导航栏样式 + +### 里程碑 3:布局优化 +- 实现卡片式布局 +- 优化监控视频布局 +- 修复各种溢出和高度问题 + +### 里程碑 4:数据架构 +- 实现共享数据获取 +- 减少冗余请求 +- 统一数据管理 + +### 里程碑 5:地图集成 +- 实现本地地图数据加载 +- 完成交易流向可视化 +- 添加交互控制功能 + +--- + +## 九、后续优化建议 + +1. **性能优化**:对 ECharts 图表实现懒加载 +2. **响应式设计**:增加移动端适配 +3. **数据可视化**:添加更多交互功能(缩放、筛选、导出) +4. **错误处理**:增加请求失败的重试机制和错误提示 +5. **代码规范**:添加 TypeScript 类型支持 +6. **测试覆盖**:增加单元测试和 E2E 测试 + +--- + +## 十、技术要点总结 + +| 功能 | 技术方案 | 关键代码 | +|------|----------|----------| +| 组件通信 | Props + Events | `defineProps()`, `defineEmits()` | +| 状态管理 | Ref + Watch | `ref()`, `watch()` | +| 图表渲染 | ECharts | `echarts.init()`, `chart.setOption()` | +| 地图渲染 | ECharts Geo | `registerMap()`, `geo` 配置 | +| 卡片布局 | Element UI | `el-card`, `el-row`, `el-col` | +| 数据获取 | Fetch API | `fetch('/data/xxx.json')` | +| 样式隔离 | Scoped CSS | ` diff --git a/src/components/FlowMonitorMap.vue b/src/components/FlowMonitorMap.vue new file mode 100644 index 0000000..fba4c92 --- /dev/null +++ b/src/components/FlowMonitorMap.vue @@ -0,0 +1,458 @@ + + + + + diff --git a/src/components/MarketAnalysis.vue b/src/components/MarketAnalysis.vue new file mode 100644 index 0000000..f3cf647 --- /dev/null +++ b/src/components/MarketAnalysis.vue @@ -0,0 +1,466 @@ + + + + + diff --git a/src/components/SceneManagement.vue b/src/components/SceneManagement.vue new file mode 100644 index 0000000..32f88e4 --- /dev/null +++ b/src/components/SceneManagement.vue @@ -0,0 +1,1315 @@ + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..e4f3782 --- /dev/null +++ b/src/main.js @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import './style.css' +import App from './App.vue' + +const app = createApp(App) +app.use(ElementPlus) +app.mount('#app') diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..3038112 --- /dev/null +++ b/src/style.css @@ -0,0 +1,449 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: Arial, sans-serif; + background-color: #f5f7fa; + color: #333333; +} + +#app { + width: 100vw; + height: 100vh; + overflow: hidden; +} + +.header { + height: 80px; + background-color: #1e40af; + color: white; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.header-title { + font-size: 24px; + font-weight: bold; +} + +.tabs { + display: flex; +} + +.tab-item { + padding: 12px 24px; + margin-left: 10px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 4px; + cursor: pointer; + transition: all 0.3s; + color: rgba(255, 255, 255, 0.9); + font-size: 16px; +} + +.tab-item.active { + background-color: #22c55e; + color: white; +} + +.tab-item:hover { + background-color: rgba(255, 255, 255, 0.15); + color: white; +} + +.tab-item.active:hover { + background-color: #22c55e; +} + +.content { + height: calc(100vh - 80px); + padding: 0; + background-color: #f5f7fa; + overflow: hidden; +} + +.tab-content { + background-color: transparent; + padding: 0; + border-radius: 0; + box-shadow: none; + border: none; + height: 100%; + overflow: hidden; +} + +.section { + margin-bottom: 30px; +} + +.section-title { + font-size: 18px; + font-weight: bold; + margin-bottom: 20px; + color: #333333; +} + +.section-subtitle { + font-size: 16px; + font-weight: bold; + margin: 15px 0 10px 0; + color: #666666; +} + +/* 卡片式布局 */ +.card-section { + margin-bottom: 25px; + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + overflow: hidden; + border: 1px solid #e0e0e0; +} + +.card-title { + font-size: 16px; + font-weight: bold; + padding: 15px 20px; + margin: 0; + background-color: #f8f9fa; + color: #333333; + border-bottom: 1px solid #e0e0e0; +} + +.card-content { + padding: 20px; +} + +/* 图表行布局 */ +.charts-row { + display: flex; + gap: 20px; + margin-bottom: 25px; + flex-wrap: wrap; +} + +/* 图表卡片 */ +.chart-card { + flex: 1; + min-width: 45%; + margin-bottom: 0; +} + +/* 调整图表容器高度 */ +.chart-card .chart-container { + height: 350px; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.stat-card { + background-color: #ffffff; + padding: 20px; + border-radius: 8px; + text-align: center; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.stat-value { + font-size: 32px; + font-weight: bold; + color: #22c55e; + margin-bottom: 8px; +} + +.stat-label { + font-size: 18px; + color: #666666; +} + +.chart-container { + height: 450px; + margin: 20px 0; +} + +.chart-container > div { + background-color: #ffffff !important; + color: #333333 !important; +} + +.map-container { + width: 100%; + height: 100%; + background-color: #ffffff; + border-radius: 0; + border: none; + overflow: hidden; + display: block; +} + +#map { + width: 100%; + height: 100%; +} + +/* 交易流向监测布局 */ +.flow-monitor-container { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.map-switcher { + position: absolute; + top: 20px; + left: 20px; + display: flex; + gap: 10px; + z-index: 1000; + padding: 0; + margin: 0; +} + +.map-switch-btn { + padding: 12px 24px; + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + color: #333333; + cursor: pointer; + transition: all 0.3s; + font-size: 14px; + backdrop-filter: blur(5px); +} + +.map-switch-btn:hover { + background-color: rgba(248, 249, 250, 0.9); + border-color: rgba(0, 0, 0, 0.3); +} + +.map-switch-btn.active { + background-color: rgba(34, 197, 94, 0.9); + color: white; + border-color: rgba(34, 197, 94, 0.9); +} + +.table-container { + overflow-x: auto; + margin: 20px 0; +} + +.data-table { + width: 100%; + border-collapse: collapse; + background-color: #ffffff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.data-table th, +.data-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #e0e0e0; + color: #333333; +} + +.data-table th { + background-color: #f8f9fa; + font-weight: bold; + color: #333333; +} + +.data-table tr:hover { + background-color: rgba(0, 0, 0, 0.02); +} + +.period-selector { + margin-bottom: 20px; +} + +.period-btn { + padding: 8px 16px; + margin-right: 10px; + background-color: #f8f9fa; + border: 1px solid #e0e0e0; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s; + color: #333333; +} + +.period-btn:hover { + background-color: #e9ecef; +} + +.period-btn.active { + background-color: #22c55e; + color: white; + border-color: #22c55e; +} + +.environment-monitor { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + margin: 20px 0; +} + +.env-item { + background-color: #ffffff; + padding: 15px; + border-radius: 8px; + text-align: center; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.env-value { + font-size: 20px; + font-weight: bold; + color: #333333; + margin-bottom: 5px; +} + +.env-label { + font-size: 14px; + color: #666666; +} + +.env-item.warning { + border-color: #faad14; + background-color: rgba(250, 173, 20, 0.05); + color: #faad14; +} + +.env-item.error { + border-color: #f5222d; + background-color: rgba(245, 34, 45, 0.05); + color: #f5222d; +} + +.env-item.warning .env-value, +.env-item.warning .env-label { + color: #faad14; +} + +.env-item.error .env-value, +.env-item.error .env-label { + color: #f5222d; +} + +.video-monitors { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin: 20px 0; +} + +.video-monitor { + background-color: #000; + height: 200px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 14px; + border: 1px solid #e0e0e0; +} + +.supply-list { + margin: 20px 0; + background-color: #ffffff; + border-radius: 8px; + overflow: hidden; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.supply-item { + padding: 15px; + border-bottom: 1px solid #e0e0e0; + display: flex; + justify-content: space-between; + align-items: center; + color: #333333; +} + +.supply-item:hover { + background-color: rgba(0, 0, 0, 0.02); +} + +.supply-item:last-child { + border-bottom: none; +} + +.supply-info { + display: flex; + gap: 20px; +} + +.year-selector { + display: flex; + align-items: center; + gap: 10px; +} + +.year-selector label { + color: #666666; + font-size: 14px; +} + +.year-select { + padding: 8px 12px; + background-color: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 4px; + color: #333333; + font-size: 14px; + cursor: pointer; + outline: none; + transition: all 0.3s; +} + +.year-select:hover { + border-color: #22c55e; +} + +.year-select:focus { + border-color: #22c55e; + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2); +} + +.year-select option { + background-color: #ffffff; + color: #333333; + padding: 8px; +} + +.card-title { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 16px; + font-weight: bold; + padding: 15px 20px; + margin: 0; + background-color: #f8f9fa; + color: #333333; + border-bottom: 1px solid #e0e0e0; +} + +.card-title .year-selector { + margin-bottom: 0; +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..00552e8 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], +}) \ No newline at end of file