Swanky 6 months ago
parent 7d815519b9
commit b39ecb74e5
  1. 1
      .gitignore
  2. 42
      DATA_CONFIG_GUIDE.md
  3. 103
      README.md
  4. 26
      index.html
  5. 2466
      package-lock.json
  6. 22
      package.json
  7. 1
      public/china.json
  8. 11
      public/config.json
  9. 160
      public/data-config.json
  10. 1
      public/datas/513233.json
  11. 471
      public/datas/supply-demand.json
  12. 1
      public/test-data.html
  13. 242
      public/yak-trading-data.json
  14. 438
      src/App.vue
  15. 70
      src/components/BaseCard.vue
  16. 137
      src/components/BeijingTrendChart.vue
  17. 1096
      src/components/ChinaMap.vue
  18. 271
      src/components/ComprehensiveSalesStats.vue
  19. 283
      src/components/ExchangeMonitor.vue
  20. 373
      src/components/MarketEnvironmentMonitor.vue
  21. 593
      src/components/MarketRealtimeMonitor.vue
  22. 223
      src/components/PurchaserAnalysis.vue
  23. 312
      src/components/RealTimeStats.vue
  24. 207
      src/components/SalesChannelStats.vue
  25. 205
      src/components/ScrollingAnnouncement.vue
  26. 1088
      src/components/SupplyDemandData.vue
  27. 159
      src/components/TradingTrendChart.vue
  28. 337
      src/components/TransactionDetails.vue
  29. 172
      src/components/YakPriceTrend.vue
  30. 296
      src/components/YakSalesTypeStats.vue
  31. 313
      src/components/YakTradingData.vue
  32. BIN
      src/images/大标题背景.png
  33. BIN
      src/images/数据底座.png
  34. 5
      src/main.js
  35. 166
      src/styles/index.scss
  36. 26
      src/styles/variables.scss
  37. 59
      src/utils/config.js
  38. 164
      src/utils/dataManager.js
  39. 1
      src/utils/dataStore.js
  40. 93
      src/utils/dataUpdater.js
  41. 23
      vite.config.js

1
.gitignore vendored

@ -8,4 +8,5 @@ docs/_book
# TODO: where does this rule come from?
test/
node_modules

@ -0,0 +1,42 @@
# 数据配置使用指南
## 概述
本系统支持通过JSON配置文件来管理所有模块的数据,方便在部署时进行数据修改而无需重新编译代码。
## 配置文件位置
- **开发环境**: `public/data-config.json`
- **生产环境**: 部署后的 `/data-config.json` 文件
## 主要数据模块
### 1. 实时交易统计 (realTimeStats)
- todayTransactions: 今日交易笔数
- totalAmount: 交易总额(万元)
- activeUsers: 活跃用户数
- systemStatus: 系统状态
### 2. 牦牛交易数据 (yakTradingData)
- totalCount: 总交易数量
- avgPrice: 平均价格
- monthlyGrowth: 月增长率(%)
- topRegion: 主要地区
## 部署时修改数据
### 方法一:直接修改JSON文件
1. 找到部署目录下的 `data-config.json` 文件
2. 使用文本编辑器打开文件
3. 修改对应的数据值
4. 保存文件
5. 刷新浏览器页面即可看到更新
### 方法二:替换整个配置文件
1. 准备新的 `data-config.json` 文件
2. 替换部署目录下的原文件
3. 刷新浏览器页面
## 注意事项
1. **JSON格式**: 确保修改后的文件符合JSON格式规范
2. **数据类型**: 注意保持数据类型一致(数字、字符串、数组等)
3. **编码格式**: 文件应保存为UTF-8编码
4. **备份**: 修改前建议备份原文件

@ -1,2 +1,103 @@
# livestock-trading
# 智慧活畜交易大数据中心
基于Vue 3 + ECharts的科技风数据大屏项目
## 特性
- 🖥 **固定尺寸设计**: 默认5120×1440像素,支持滚动条
- ⚙ **动态配置**: JSON配置文件管理屏幕尺寸
- 📊 **丰富图表**: 多种ECharts图表类型
- 🎨 **科技风格**: 深色主题,炫酷动效
- 📱 **响应式布局**: 适配不同屏幕尺寸
- 🔄 **实时数据**: 模拟数据自动更新
## 快速开始
### 安装依赖
```bash
npm install
```
### 开发模式
```bash
npm run dev
```
### 构建生产版本
```bash
npm run build
```
## 配置管理
### 修改屏幕尺寸
编辑 `public/config.json` 文件:
```json
{
"screen": {
"width": 5120, // 屏幕宽度
"height": 1440, // 屏幕高度
"title": "智慧活畜交易大数据中心"
},
"dashboard": {
"refreshInterval": 5000,
"animationDuration": 300
}
}
```
### 常用尺寸参考
| 分辨率 | 宽度 | 高度 | 用途 |
|--------|------|------|------|
| 5K超宽屏 | 5120px | 1440px | 专业显示器 |
| 4K | 3840px | 2160px | 4K显示器 |
| 2K | 2560px | 1440px | 2K显示器 |
| 1080p | 1920px | 1080px | 标准显示器 |
## 项目结构
```
├── public/
│ └── config.json # 配置文件
├── src/
│ ├── components/ # 组件目录
│ │ ├── RealTimeStats.vue # 实时交易统计
│ │ ├── CountySalesStats.vue # 本县销售统计
│ │ ├── TradingTrendChart.vue # 交易趋势图表
│ │ ├── BeijingTrendChart.vue # 转牛京津冀趋势
│ │ ├── ChinaMap.vue # 中国地图
│ │ ├── ExchangeMonitor.vue # 交易所实时监控
│ │ ├── SalesChannelStats.vue # 转牛销售渠道统计
│ │ ├── PurchaserAnalysis.vue # 采购商来源分析
│ │ ├── SupplyDemandData.vue # 供需实数据
│ │ └── TransactionDetails.vue # 交易明细统计
│ ├── styles/ # 样式文件
│ ├── utils/
│ │ └── config.js # 配置管理工具
│ └── App.vue # 主组件
└── package.json
```
## 部署说明
1. 构建项目:`npm run build`
2. 将 `dist` 目录部署到Web服务器
3. 根据实际屏幕修改 `config.json` 配置
4. 访问部署的URL
## 技术栈
- Vue 3.3.4
- ECharts 5.4.3
- Vite 4.4.9
- SCSS 1.66.1
## 开发说明
- 所有图表组件支持响应式
- 数据更新通过定时器模拟
- 支持深色科技风主题
- 使用CSS Grid进行布局

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>智慧活畜交易大数据一张图</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #0a0e1a;
overflow: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2466
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,22 @@
{
"name": "livestock-trading-dashboard",
"version": "1.0.0",
"description": "智慧活畜交易大数据一张图",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"echarts": "^5.4.3",
"axios": "^1.5.0",
"@vueuse/core": "^10.4.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"vite": "^4.4.9",
"sass": "^1.66.1"
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,11 @@
{
"screen": {
"width": 5120,
"height": 1440,
"title": "红原县智慧活畜交易大数据平台"
},
"dashboard": {
"refreshInterval": 5000,
"animationDuration": 300
}
}

@ -0,0 +1,160 @@
{
"realTimeStats": {
"yakTotalVolume": 15420,
"orderTotalVolume": 1248,
"sellerCount": 156,
"buyerCount": 89,
"systemStatus": "正常运行"
},
"yakTradingData": {
"totalCount": 15420,
"avgPrice": 8500,
"monthlyGrowth": 12.5,
"topRegion": "西藏"
},
"comprehensiveSalesStats": {
"localCountySales": [
{ "name": "A县", "value": 1240 },
{ "name": "B县", "value": 890 },
{ "name": "C县", "value": 560 },
{ "name": "D县", "value": 450 },
{ "name": "E县", "value": 320 },
{ "name": "F县", "value": 240 }
],
"overallSalesDistribution": [
{ "name": "A市", "value": 2340 },
{ "name": "B市", "value": 1890 },
{ "name": "C市", "value": 1456 },
{ "name": "D市", "value": 1234 },
{ "name": "E市", "value": 987 },
{ "name": "F市", "value": 289 }
],
"purchaseRegionDistribution": [
{ "name": "A省", "value": 1560 },
{ "name": "B省", "value": 1230 },
{ "name": "C省", "value": 980 },
{ "name": "D省", "value": 750 },
{ "name": "E省", "value": 620 },
{ "name": "F省", "value": 460 }
]
},
"yakPriceTrend": {
"months": ["1月", "2月", "3月", "4月", "5月", "6月"],
"prices": [7800, 8200, 8500, 8300, 8600, 8500],
"volumes": [1200, 1350, 1480, 1420, 1560, 1520]
},
"chinaMapData": [
{ "name": "西藏", "value": 8520 },
{ "name": "青海", "value": 3240 },
{ "name": "四川", "value": 2180 },
{ "name": "云南", "value": 1560 },
{ "name": "甘肃", "value": 890 }
],
"exchangeMonitor": {
"onlineUsers": 156,
"activeTransactions": 23,
"systemLoad": 68,
"networkStatus": "良好"
},
"yakSalesTypeStats": [
{ "name": "成年牦牛", "value": 45.2, "count": 6780 },
{ "name": "幼牛", "value": 28.6, "count": 4290 },
{ "name": "母牛", "value": 18.9, "count": 2835 },
{ "name": "种牛", "value": 7.3, "count": 1095 }
],
"purchaserAnalysis": [
{ "type": "个人买家", "count": 1245, "percentage": 62.3 },
{ "type": "企业采购", "count": 456, "percentage": 22.8 },
{ "type": "合作社", "count": 234, "percentage": 11.7 },
{ "type": "其他", "count": 65, "percentage": 3.2 }
],
"marketEnvironment": {
"temperature": "15°C",
"humidity": "45%",
"airQuality": "优",
"weather": "晴"
},
"marketRealtime": {
"currentPrice": 8500,
"priceChange": "+2.3%",
"volume": 1520,
"volumeChange": "-1.2%",
"lastUpdate": "2024-01-15 14:30:00"
},
"supplyDemandData": {
"supply": {
"total": 15420,
"available": 12340,
"reserved": 3080
},
"demand": {
"total": 18650,
"pending": 6230,
"matched": 12420
},
"ratio": 0.83
},
"transactionDetails": [
{
"id": "TX20240115001",
"buyer": "张三",
"seller": "李四",
"type": "成年牦牛",
"quantity": 5,
"price": 8500,
"total": 42500,
"status": "已完成",
"time": "14:25"
},
{
"id": "TX20240115002",
"buyer": "王五",
"seller": "赵六",
"type": "幼牛",
"quantity": 8,
"price": 6200,
"total": 49600,
"status": "进行中",
"time": "14:20"
},
{
"id": "TX20240115003",
"buyer": "陈七",
"seller": "刘八",
"type": "母牛",
"quantity": 3,
"price": 9200,
"total": 27600,
"status": "已完成",
"time": "14:15"
},
{
"id": "TX20240115004",
"buyer": "孙九",
"seller": "周十",
"type": "种牛",
"quantity": 2,
"price": 12000,
"total": 24000,
"status": "待确认",
"time": "14:10"
},
{
"id": "TX20240115005",
"buyer": "吴一",
"seller": "郑二",
"type": "成年牦牛",
"quantity": 6,
"price": 8300,
"total": 49800,
"status": "已完成",
"time": "14:05"
}
],
"announcements": [
"【重要通知】系统将于今晚22:00-23:00进行维护升级",
"【市场动态】本周牦牛价格稳中有升,建议关注市场变化",
"【政策解读】新的活畜交易规范将于下月实施",
"【技术支持】如遇交易问题请及时联系客服热线400-123-4567"
]
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,471 @@
[
{
"id": 1,
"name": "扎西多杰",
"licensePlate": "川A88888",
"yakCount": 25,
"contact": "138****5678",
"origin": "红原县安曲镇",
"quarantineNo": "HY2024001",
"entryTime": "2024-01-15 08: 30",
"progress": 80,
"tradedCount": 20,
"pendingCount": 5,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 1,
"buyer": "成都肉业公司",
"seller": "扎西多杰",
"tradeTime": "2024-01-15 10: 20",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 2,
"buyer": "重庆牧业",
"seller": "扎西多杰",
"tradeTime": "2024-01-15 14: 15",
"quantity": 8,
"weight": 1120,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 2,
"name": "达娃次仁",
"licensePlate": "川A99999",
"yakCount": 18,
"contact": "139****1234",
"origin": "红原县瓦切镇",
"quarantineNo": "HY2024002",
"entryTime": "2024-01-15 09: 15",
"progress": 50,
"tradedCount": 9,
"pendingCount": 9,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 3,
"buyer": "昆明食品厂",
"seller": "达娃次仁",
"tradeTime": "2024-01-15 11: 30",
"quantity": 9,
"weight": 1260,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 3,
"name": "洛桑平措",
"licensePlate": "川A77777",
"yakCount": 32,
"contact": "137****9876",
"origin": "红原县刷经寺镇",
"quarantineNo": "HY2024003",
"entryTime": "2024-01-15 07: 45",
"progress": 100,
"tradedCount": 32,
"pendingCount": 0,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 4,
"buyer": "西安牧业集团",
"seller": "洛桑平措",
"tradeTime": "2024-01-15 09: 00",
"quantity": 20,
"weight": 2800,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 5,
"buyer": "兰州畜牧公司",
"seller": "洛桑平措",
"tradeTime": "2024-01-15 13: 45",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 4,
"name": "白玛旺堆",
"licensePlate": "川A66666",
"yakCount": 15,
"contact": "136****5432",
"origin": "红原县江茸乡",
"quarantineNo": "HY2024004",
"entryTime": "2024-01-15 10: 20",
"progress": 30,
"tradedCount": 4,
"pendingCount": 11,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 6,
"buyer": "贵阳食品公司",
"tradeTime": "2024-01-15 12: 10",
"quantity": 4,
"weight": 560,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 1,
"name": "扎西多杰",
"licensePlate": "川A88888",
"yakCount": 25,
"contact": "138****5678",
"origin": "红原县安曲镇",
"quarantineNo": "HY2024001",
"entryTime": "2024-01-15 08: 30",
"progress": 80,
"tradedCount": 20,
"pendingCount": 5,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 1,
"buyer": "成都肉业公司",
"tradeTime": "2024-01-15 10: 20",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 2,
"buyer": "重庆牧业",
"tradeTime": "2024-01-15 14: 15",
"quantity": 8,
"weight": 1120,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 2,
"name": "达娃次仁",
"licensePlate": "川A99999",
"yakCount": 18,
"contact": "139****1234",
"origin": "红原县瓦切镇",
"quarantineNo": "HY2024002",
"entryTime": "2024-01-15 09: 15",
"progress": 50,
"tradedCount": 9,
"pendingCount": 9,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 3,
"buyer": "昆明食品厂",
"tradeTime": "2024-01-15 11: 30",
"quantity": 9,
"weight": 1260,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 3,
"name": "洛桑平措",
"licensePlate": "川A77777",
"yakCount": 32,
"contact": "137****9876",
"origin": "红原县刷经寺镇",
"quarantineNo": "HY2024003",
"entryTime": "2024-01-15 07: 45",
"progress": 100,
"tradedCount": 32,
"pendingCount": 0,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 4,
"buyer": "西安牧业集团",
"tradeTime": "2024-01-15 09: 00",
"quantity": 20,
"weight": 2800,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 5,
"buyer": "兰州畜牧公司",
"tradeTime": "2024-01-15 13: 45",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 4,
"name": "白玛旺堆",
"licensePlate": "川A66666",
"yakCount": 15,
"contact": "136****5432",
"origin": "红原县江茸乡",
"quarantineNo": "HY2024004",
"entryTime": "2024-01-15 10: 20",
"progress": 30,
"tradedCount": 4,
"pendingCount": 11,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 6,
"buyer": "贵阳食品公司",
"tradeTime": "2024-01-15 12: 10",
"quantity": 4,
"weight": 560,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 1,
"name": "扎西多杰",
"licensePlate": "川A88888",
"yakCount": 25,
"contact": "138****5678",
"origin": "红原县安曲镇",
"quarantineNo": "HY2024001",
"entryTime": "2024-01-15 08: 30",
"progress": 80,
"tradedCount": 20,
"pendingCount": 5,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 1,
"buyer": "成都肉业公司",
"tradeTime": "2024-01-15 10: 20",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 2,
"buyer": "重庆牧业",
"tradeTime": "2024-01-15 14: 15",
"quantity": 8,
"weight": 1120,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 2,
"name": "达娃次仁",
"licensePlate": "川A99999",
"yakCount": 18,
"contact": "139****1234",
"origin": "红原县瓦切镇",
"quarantineNo": "HY2024002",
"entryTime": "2024-01-15 09: 15",
"progress": 50,
"tradedCount": 9,
"pendingCount": 9,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 3,
"buyer": "昆明食品厂",
"tradeTime": "2024-01-15 11: 30",
"quantity": 9,
"weight": 1260,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 3,
"name": "洛桑平措",
"licensePlate": "川A77777",
"yakCount": 32,
"contact": "137****9876",
"origin": "红原县刷经寺镇",
"quarantineNo": "HY2024003",
"entryTime": "2024-01-15 07: 45",
"progress": 100,
"tradedCount": 32,
"pendingCount": 0,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 4,
"buyer": "西安牧业集团",
"tradeTime": "2024-01-15 09: 00",
"quantity": 20,
"weight": 2800,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 5,
"buyer": "兰州畜牧公司",
"tradeTime": "2024-01-15 13: 45",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 4,
"name": "白玛旺堆",
"licensePlate": "川A66666",
"yakCount": 15,
"contact": "136****5432",
"origin": "红原县江茸乡",
"quarantineNo": "HY2024004",
"entryTime": "2024-01-15 10: 20",
"progress": 30,
"tradedCount": 4,
"pendingCount": 11,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 6,
"buyer": "贵阳食品公司",
"tradeTime": "2024-01-15 12: 10",
"quantity": 4,
"weight": 560,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 1,
"name": "扎西多杰",
"licensePlate": "川A88888",
"yakCount": 25,
"contact": "138****5678",
"origin": "红原县安曲镇",
"quarantineNo": "HY2024001",
"entryTime": "2024-01-15 08: 30",
"progress": 80,
"tradedCount": 20,
"pendingCount": 5,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 1,
"buyer": "成都肉业公司",
"tradeTime": "2024-01-15 10: 20",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 2,
"buyer": "重庆牧业",
"tradeTime": "2024-01-15 14: 15",
"quantity": 8,
"weight": 1120,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 2,
"name": "达娃次仁",
"licensePlate": "川A99999",
"yakCount": 18,
"contact": "139****1234",
"origin": "红原县瓦切镇",
"quarantineNo": "HY2024002",
"entryTime": "2024-01-15 09: 15",
"progress": 50,
"tradedCount": 9,
"pendingCount": 9,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 3,
"buyer": "昆明食品厂",
"tradeTime": "2024-01-15 11: 30",
"quantity": 9,
"weight": 1260,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 3,
"name": "洛桑平措",
"licensePlate": "川A77777",
"yakCount": 32,
"contact": "137****9876",
"origin": "红原县刷经寺镇",
"quarantineNo": "HY2024003",
"entryTime": "2024-01-15 07: 45",
"progress": 100,
"tradedCount": 32,
"pendingCount": 0,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 4,
"buyer": "西安牧业集团",
"tradeTime": "2024-01-15 09: 00",
"quantity": 20,
"weight": 2800,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
},
{
"id": 5,
"buyer": "兰州畜牧公司",
"tradeTime": "2024-01-15 13: 45",
"quantity": 12,
"weight": 1680,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
},
{
"id": 4,
"name": "白玛旺堆",
"licensePlate": "川A66666",
"yakCount": 15,
"contact": "136****5432",
"origin": "红原县江茸乡",
"quarantineNo": "HY2024004",
"entryTime": "2024-01-15 10: 20",
"progress": 30,
"tradedCount": 4,
"pendingCount": 11,
"entryPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWFpeWcuueXp+eJhzwvdGV4dD48L3N2Zz4=",
"quarantineCert": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuajgOeWq+ivgTwvdGV4dD48L3N2Zz4=",
"orders": [
{
"id": 6,
"buyer": "贵阳食品公司",
"tradeTime": "2024-01-15 12: 10",
"quantity": 4,
"weight": 560,
"weightPhoto": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzJhMmEyYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7np7Dpho08L3RleHQ+PC9zdmc+"
}
]
}
]

@ -0,0 +1,242 @@
{
"centerCity": {
"name": "红原县",
"coordinates": [102.568685, 32.826358],
"description": "四川省阿坝州红原县,中国重要的牦牛养殖基地"
},
"geoCoordMap": {
"红原县": [102.568685, 32.826358],
"邛溪镇": [102.515, 32.7855],
"龙日镇": [102.484, 32.3605],
"麦洼乡": [102.918, 32.9156],
"阿木乡": [102.789, 32.9154],
"刷经寺镇": [102.661, 31.9177],
"瓦切镇": [102.669, 33.2578],
"查尔玛乡": [101.921, 32.5029],
"江茸乡": [102.412, 32.3022],
"安曲镇": [102.222, 32.6709],
"色地镇": [102.999, 32.865],
"阿坝县": [101.714684, 32.910406],
"壤塘县": [101.027085, 32.285498],
"炉霍县": [100.672937, 31.421821],
"甘孜县": [99.987638, 31.654244],
"若尔盖县": [102.96455, 33.547262],
"松潘县": [103.631452, 32.649103],
"红原县养殖基地": [102.550539, 32.797395],
"果洛藏族自治州": [98.871152, 34.255834],
"成都": [103.958004, 30.772708],
"玉树藏族自治州": [94.961726, 33.434535],
"甘孜藏族自治州": [101.226006, 30.470387],
"拉萨": [91.420246, 29.878931],
"西宁": [101.74113, 36.641678],
"兰州": [103.249708, 35.956623],
"甘南藏族自治州": [103.3049, 34.644479],
"昌都": [97.399368, 31.248109],
"重庆": [106.54, 29.59],
"西安": [108.95, 34.27],
"昆明": [102.73, 25.04],
"贵阳": [106.71, 26.57],
"北京": [116.46, 39.92],
"上海": [121.48, 31.22],
"广州": [113.23, 23.16],
"深圳": [114.07, 22.62],
"杭州": [120.19, 30.26],
"南京": [118.78, 32.04],
"武汉": [114.31, 30.52],
"长沙": [112.94, 28.19],
"郑州": [113.65, 34.76],
"济南": [117.00, 36.65],
"天津": [117.20, 39.13],
"石家庄": [114.48, 38.03],
"太原": [112.53, 37.87],
"呼和浩特": [111.65, 40.82],
"银川": [106.27, 38.47],
"乌鲁木齐": [87.68, 43.77]
},
"tradingModes": {
"outflow": {
"title": "销售网络分布图",
"description": "从红原县向各藏区及重要城市输出牦牛的流向",
"flows": [
{
"from": "红原县",
"to": "果洛藏族自治州",
"value": 1200,
"description": "青海果洛州主要销售市场"
},
{
"from": "红原县",
"to": "成都",
"value": 1100,
"description": "四川省会城市市场"
},
{
"from": "红原县",
"to": "玉树藏族自治州",
"value": 980,
"description": "青海玉树州销售市场"
},
{
"from": "红原县",
"to": "甘孜藏族自治州",
"value": 850,
"description": "四川甘孜州销售网络"
},
{
"from": "红原县",
"to": "拉萨",
"value": 800,
"description": "西藏自治区首府市场"
},
{
"from": "红原县",
"to": "西宁",
"value": 720,
"description": "青海省会城市市场"
},
{
"from": "红原县",
"to": "兰州",
"value": 680,
"description": "甘肃省会城市市场"
},
{
"from": "红原县",
"to": "甘南藏族自治州",
"value": 620,
"description": "甘肃甘南州销售市场"
},
{
"from": "红原县",
"to": "昌都",
"value": 550,
"description": "西藏昌都地区市场"
}
]
},
"inflow": {
"title": "源地供应分布图",
"description": "各牦牛养殖基地向红原县牦牛交易市场供应的流向",
"flows": [
{
"from": "若尔盖县",
"to": "红原县",
"value": 1200,
"description": "若尔盖县草原牦牛供应"
},
{
"from": "阿坝县",
"to": "红原县",
"value": 1000,
"description": "阿坝县高原牦牛供应"
},
{
"from": "松潘县",
"to": "红原县",
"value": 850,
"description": "松潘县牦牛养殖供应"
},
{
"from": "红原县养殖基地",
"to": "红原县",
"value": 800,
"description": "红原县本地养殖基地供应"
},
{
"from": "壤塘县",
"to": "红原县",
"value": 720,
"description": "壤塘县牦牛供应"
},
{
"from": "炉霍县",
"to": "红原县",
"value": 650,
"description": "炉霍县牦牛养殖供应"
},
{
"from": "甘孜县",
"to": "红原县",
"value": 580,
"description": "甘孜县牦牛供应"
}
]
},
"local": {
"title": "红原出栏分布图",
"description": "红原县各乡镇向县牦牛交易市场的出栏流向",
"flows": [
{
"from": "麦洼乡",
"to": "红原县",
"value": 1200,
"description": "麦洼乡牦牛出栏"
},
{
"from": "色地镇",
"to": "红原县",
"value": 1100,
"description": "色地镇牦牛出栏"
},
{
"from": "阿木乡",
"to": "红原县",
"value": 950,
"description": "阿木乡牦牛出栏"
},
{
"from": "邛溪镇",
"to": "红原县",
"value": 880,
"description": "邛溪镇牦牛出栏"
},
{
"from": "瓦切镇",
"to": "红原县",
"value": 820,
"description": "瓦切镇牦牛出栏"
},
{
"from": "刷经寺镇",
"to": "红原县",
"value": 750,
"description": "刷经寺镇牦牛出栏"
},
{
"from": "安曲镇",
"to": "红原县",
"value": 680,
"description": "安曲镇牦牛出栏"
},
{
"from": "查尔玛乡",
"to": "红原县",
"value": 620,
"description": "查尔玛乡牦牛出栏"
},
{
"from": "龙日镇",
"to": "红原县",
"value": 580,
"description": "龙日镇牦牛出栏"
},
{
"from": "江茸乡",
"to": "红原县",
"value": 520,
"description": "江茸乡牦牛出栏"
}
]
}
},
"cityCategories": {
"production": ["红原县", "拉萨", "西宁", "兰州", "银川", "呼和浩特", "乌鲁木齐"],
"consumption": ["成都", "重庆", "西安", "北京", "上海", "广州", "深圳", "杭州", "武汉", "昆明", "贵阳"],
"transit": ["西安", "兰州", "成都", "重庆"]
},
"flowLevels": {
"high": { "threshold": 600, "color": "#E6A23C", "description": "主要流向" },
"medium": { "threshold": 300, "color": "#409EFF", "description": "重要流向" },
"low": { "threshold": 0, "color": "#67C23A", "description": "一般流向" }
}
}

@ -0,0 +1,438 @@
<template>
<div class="dashboard-container">
<!-- 标题区域 -->
<div class="header-section">
<div class="header-info left">
<!-- <div class="info-item">
<span class="status-indicator" :class="isLayoutReady ? 'online' : 'loading'"></span>
<span>{{ isLayoutReady ? '系统运行正常' : '系统初始化中' }}</span>
</div> -->
</div>
<h1 class="main-title">{{ config?.screen?.title || '智慧活畜交易大数据中心' }}</h1>
<div class="header-info right">
<div class="info-item">
<span class="current-time">{{ currentTime || '--:--:--' }}</span>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div v-if="isLayoutReady" class="main-content">
<!-- 左侧区域 - 分为左和左中两列 -->
<div class="left-section">
<!-- 左列 -->
<div class="left-column">
<div class="left-top">
<RealTimeStats />
</div>
<div class="left-bottom">
<YakTradingData />
</div>
</div>
<!-- 左中列 -->
<div class="left-center-column">
<div class="left-center-top">
<ComprehensiveSalesStats />
</div>
<div class="left-center-bottom">
<YakPriceTrend />
</div>
</div>
</div>
<!-- 中央区域 -->
<div class="center-section">
<div class="map-container">
<ChinaMap />
<!-- <ScrollingAnnouncement /> -->
<!-- <div class="floating-table">
<TransactionDetails />
</div> -->
</div>
</div>
<!-- 右侧区域 - 分为右中和右两列 -->
<div class="right-section">
<!-- 当供应信息展开时整个右侧区域显示供应信息 -->
<div v-if="isSupplyExpanded" class="expanded-supply-container">
<SupplyDemandData :force-expanded="true" @expand-change="handleSupplyExpand" />
</div>
<!-- 正常状态下的右侧布局 -->
<template v-else>
<!-- 右中列 -->
<div class="right-center-column">
<div class="right-center-top">
<ExchangeMonitor />
</div>
<div class="right-center-middle">
<YakSalesTypeStats />
</div>
<div class="right-center-bottom">
<PurchaserAnalysis />
</div>
</div>
<!-- 右列 -->
<div class="right-column">
<div class="right-top">
<MarketEnvironmentMonitor />
</div>
<div class="right-middle">
<MarketRealtimeMonitor />
</div>
<div class="right-bottom">
<SupplyDemandData :force-expanded="false" @expand-change="handleSupplyExpand" />
</div>
</div>
</template>
</div>
</div>
<!-- 加载状态 -->
<div v-else class="loading-container">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text">正在初始化布局...</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { configManager } from './utils/config.js'
import { dataManager } from './utils/dataManager.js'
import { dataUpdater } from './utils/dataUpdater.js'
//
import RealTimeStats from './components/RealTimeStats.vue'
import YakTradingData from './components/YakTradingData.vue'
import ComprehensiveSalesStats from './components/ComprehensiveSalesStats.vue'
import YakPriceTrend from './components/YakPriceTrend.vue'
import ChinaMap from './components/ChinaMap.vue'
import ScrollingAnnouncement from './components/ScrollingAnnouncement.vue'
import TransactionDetails from './components/TransactionDetails.vue'
import ExchangeMonitor from './components/ExchangeMonitor.vue'
import YakSalesTypeStats from './components/YakSalesTypeStats.vue'
import PurchaserAnalysis from './components/PurchaserAnalysis.vue'
import MarketEnvironmentMonitor from './components/MarketEnvironmentMonitor.vue'
import MarketRealtimeMonitor from './components/MarketRealtimeMonitor.vue'
import SupplyDemandData from './components/SupplyDemandData.vue'
//
const config = ref(null)
const currentTime = ref('')
const isLayoutReady = ref(false) //
const isSupplyExpanded = ref(false) //
//
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// /
const handleSupplyExpand = (expanded) => {
isSupplyExpanded.value = expanded
}
//
let timeTimer = null
onMounted(async () => {
//
updateTime()
timeTimer = setInterval(updateTime, 1000)
try {
//
console.log('App.vue: 开始加载配置')
config.value = await configManager.loadConfig()
console.log('App.vue: 配置加载完成', config.value)
// CSS
console.log('App.vue: 设置CSS变量')
configManager.setCSSVariables()
// CSSDOM
await nextTick()
//
await new Promise(resolve => requestAnimationFrame(resolve))
console.log('App.vue: CSS变量已生效')
//
console.log('App.vue: 开始加载数据配置')
const loadedData = await dataManager.loadData()
console.log('App.vue: 数据配置加载完成', loadedData)
//
console.log('App.vue: 布局准备完成,开始渲染组件')
isLayoutReady.value = true
console.log('Dashboard 完全初始化完成')
} catch (error) {
console.error('Failed to initialize dashboard:', error)
// 使
config.value = configManager.getDefaultConfig()
configManager.setCSSVariables()
// DOM
await nextTick()
await new Promise(resolve => requestAnimationFrame(resolve))
isLayoutReady.value = true
}
})
onUnmounted(() => {
if (timeTimer) {
clearInterval(timeTimer)
}
})
</script>
<style scoped>
/* 标题区域 */
.header-section {
height: 84px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 40px;
background: url('./images/大标题背景.png') center/contain no-repeat;
background-size: auto 84px;
border: 1px solid rgba(64, 158, 255, 0.3);
border-radius: 12px;
backdrop-filter: blur(10px);
position: relative;
z-index: 10;
flex-shrink: 0;
background-size: 100% 84px;
}
.main-title {
font-size: 36px;
font-weight: 700;
background: linear-gradient(180deg, #fff 50%, #44c1ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
letter-spacing: 3px;
text-shadow: 0 0 20px rgba(64, 158, 255, 0.5);
flex: 2;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
}
.header-info {
display: flex;
align-items: center;
gap: 24px;
flex: 1;
&.left {
justify-content: flex-start;
}
&.right {
justify-content: flex-end;
}
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #a0a8b8;
}
.current-time {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #409EFF;
}
/* 主要内容区域 */
.main-content {
flex: 1;
display: flex;
gap: 16px;
min-height: 0;
padding-bottom: 20px;
}
/* 左侧区域布局 */
.left-section {
flex: 1;
display: flex;
gap: 12px;
min-width: 0;
}
.left-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.left-center-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.left-top,
.left-bottom,
.left-center-top,
.left-center-bottom {
flex: 1;
min-height: 0;
}
/* 中央区域布局 */
.center-section {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.map-container {
position: relative;
flex: 1;
min-height: 0;
}
.floating-table {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
height: 200px;
background: rgba(26, 31, 46, 0.95);
border: 1px solid rgba(64, 158, 255, 0.4);
border-radius: 8px;
backdrop-filter: blur(10px);
z-index: 100;
}
/* 右侧区域布局 */
.right-section {
flex: 1;
display: flex;
gap: 12px;
min-width: 0;
}
.right-center-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.right-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.right-center-top,
.right-center-middle,
.right-center-bottom,
.right-top,
.right-middle,
.right-bottom {
flex: 1;
min-height: 0;
}
/* 展开的供应信息容器 */
.expanded-supply-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 状态指示器动画 */
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
}
.status-indicator.online {
animation: pulse 2s infinite;
}
.status-indicator.loading {
background: #E6A23C;
box-shadow: 0 0 8px rgba(230, 162, 60, 0.6);
animation: pulse 1s infinite;
}
/* 加载状态样式 */
.loading-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.loading-content {
text-align: center;
color: #409EFF;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(64, 158, 255, 0.3);
border-top: 3px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
.loading-text {
font-size: 16px;
font-weight: 500;
color: #a0a8b8;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

@ -0,0 +1,70 @@
<template>
<div class="chart-card">
<div class="card-title">{{ title }}</div>
<div class="card-content">
<slot></slot>
</div>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
required: true
}
})
</script>
<style scoped>
.chart-card {
background: linear-gradient(135deg, #1a1f2e 0%, #16213e 100%);
border-radius: 8px;
padding: 16px;
border: 1px solid rgba(64, 158, 255, 0.3);
height: 100%;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.chart-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #409EFF, #67C23A, #E6A23C);
opacity: 0.8;
}
.card-title {
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, #409EFF 0%, #67C23A 50%, #E6A23C 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 12px;
text-align: left;
position: relative;
z-index: 1;
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
.chart-card:hover {
border-color: rgba(64, 158, 255, 0.6);
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
transform: translateY(-2px);
transition: all 0.3s ease;
}
</style>

@ -0,0 +1,137 @@
<template>
<BaseCard title="北京走势图">
<div ref="chartRef" class="chart"></div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
const chartRef = ref(null)
let chartInstance = null
//
const months = ['一月', '二月', '三月', '四月', '五月', '六月']
const data = [20, 40, 60, 80, 60, 100]
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#409EFF',
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: function(params) {
return `${params[0].name}<br/>转牛数量: ${params[0].value}`
}
},
grid: {
left: '8%',
right: '8%',
bottom: '15%',
top: '10%'
},
xAxis: {
type: 'category',
data: months,
axisLine: {
lineStyle: {
color: '#4a5568'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 11
}
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
color: '#2d3748',
type: 'dashed'
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 11
}
},
series: [{
name: '转牛数量',
type: 'bar',
data: data,
barWidth: '60%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#409EFF' },
{ offset: 1, color: '#1976D2' }
]),
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(64, 158, 255, 0.8)'
}
}
}]
}
chartInstance.setOption(option)
//
let currentIndex = 0
const timer = setInterval(() => {
chartInstance.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: currentIndex
})
currentIndex = (currentIndex + 1) % data.length
}, 3000)
chartInstance._autoTimer = timer
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
if (chartInstance._autoTimer) {
clearInterval(chartInstance._autoTimer)
}
chartInstance.dispose()
}
})
</script>
<style scoped>
.chart {
width: 100%;
height: 100%;
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,271 @@
<template>
<BaseCard title="综合销售统计">
<div class="stats-container">
<!-- 切换标签 -->
<div class="tab-container">
<div
v-for="(tab, index) in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: activeTab === tab.key }"
@click="switchTab(tab.key)"
>
{{ tab.name }}
</div>
</div>
<!-- 图表区域 -->
<div class="chart-container">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted, reactive, computed, watch } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
import { dataManager } from '../utils/dataManager.js'
const chartRef = ref(null)
let chartInstance = null
//
const activeTab = ref('localCountySales')
//
const tabs = [
{ key: 'localCountySales', name: '本县销售统计' },
{ key: 'overallSalesDistribution', name: '总体销售分布' },
{ key: 'purchaseRegionDistribution', name: '采购地区分布' }
]
//
const statsData = reactive({
localCountySales: [],
overallSalesDistribution: [],
purchaseRegionDistribution: []
})
//
const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#8B5CF6']
//
const currentChartData = computed(() => {
return statsData[activeTab.value] || []
})
//
const switchTab = (tabKey) => {
activeTab.value = tabKey
}
//
const loadData = async () => {
try {
await dataManager.loadData()
const data = dataManager.getData('comprehensiveSalesStats')
if (data) {
Object.assign(statsData, data)
console.log('综合销售统计数据加载成功:', data)
} else {
console.warn('未找到综合销售统计数据')
// 使
loadDefaultData()
}
} catch (error) {
console.error('加载综合销售统计数据失败:', error)
loadDefaultData()
}
}
//
const loadDefaultData = () => {
statsData.localCountySales = [
{ name: 'A县', value: 1240 },
{ name: 'B县', value: 890 },
{ name: 'C县', value: 560 },
{ name: 'D县', value: 450 }
]
statsData.overallSalesDistribution = [
{ name: 'A市', value: 2340 },
{ name: 'B市', value: 1890 },
{ name: 'C市', value: 1456 },
{ name: 'D市', value: 1234 }
]
statsData.purchaseRegionDistribution = [
{ name: 'A省', value: 1560 },
{ name: 'B省', value: 1230 },
{ name: 'C省', value: 980 },
{ name: 'D省', value: 750 }
]
}
//
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
updateChart()
}
//
const updateChart = () => {
if (!chartInstance) return
const data = currentChartData.value.map((item, index) => ({
name: item.name,
value: item.value,
itemStyle: {
color: colors[index % colors.length],
borderRadius: 4,
borderColor: '#1a1f2e',
borderWidth: 2
}
}))
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#409EFF',
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: '{b}: {c}头 ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: '5%',
left: 'center',
itemGap: 15,
textStyle: {
color: '#a0a8b8',
fontSize: 12
},
itemWidth: 12,
itemHeight: 12,
formatter: function(name) {
const item = currentChartData.value.find(d => d.name === name);
return `${name}: ${item ? item.value : 0}`;
}
},
series: [{
name: tabs.find(t => t.key === activeTab.value)?.name || '销售统计',
type: 'pie',
radius: ['35%', '60%'],
center: ['50%', '40%'],
data: data,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
labelLine: {
show: false
},
label: {
show: true,
position: 'inside',
formatter: '{d}%',
fontSize: 13,
color: '#fff',
fontWeight: 'bold'
},
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelay: function (idx) {
return Math.random() * 200;
}
}]
}
chartInstance.setOption(option, true)
}
// activeTab
watch(activeTab, () => {
updateChart()
})
onMounted(async () => {
await loadData()
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
//
window.addEventListener('dataReloaded', async () => {
console.log('收到数据重新加载事件,重新加载综合销售统计数据')
await loadData()
updateChart()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
</script>
<style scoped>
.stats-container {
display: flex;
flex-direction: column;
height: 100%;
}
.tab-container {
display: flex;
margin-bottom: 12px;
background: rgba(26, 31, 46, 0.5);
border-radius: 6px;
padding: 2px;
}
.tab-item {
flex: 1;
text-align: center;
padding: 6px 8px;
font-size: 13px;
color: #a0a8b8;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s ease;
user-select: none;
}
.tab-item:hover {
color: #409EFF;
background: rgba(64, 158, 255, 0.1);
}
.tab-item.active {
color: #ffffff;
background: #409EFF;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
}
.chart-container {
flex: 1;
min-height: 140px;
}
.chart {
width: 100%;
height: 100%;
}
</style>

@ -0,0 +1,283 @@
<template>
<BaseCard title="交易中心实时服务信息">
<div class="service-grid">
<div v-for="item in serviceData" :key="item.id" class="service-item">
<div class="service-left">
<div class="service-icon">
{{ item.icon }}
</div>
<div class="service-info">
<div class="service-label">{{ item.label }}</div>
<div class="service-status" :class="item.statusClass">
{{ item.status }}
</div>
</div>
</div>
<div class="service-value" :class="item.valueClass">
{{ item.value }}
<span class="service-unit">{{ item.unit }}</span>
</div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import BaseCard from './BaseCard.vue'
const serviceData = ref([
{
id: 1,
label: '牦牛供应总量',
value: 2850,
unit: '头',
icon: '🐄',
valueClass: 'total',
status: '充足',
statusClass: 'abundant'
},
{
id: 2,
label: '已售牦牛',
value: 1680,
unit: '头',
icon: '✅',
valueClass: 'sold',
status: '正常',
statusClass: 'normal'
},
{
id: 3,
label: '待售牦牛',
value: 1170,
unit: '头',
icon: '⏳',
valueClass: 'pending',
status: '充足',
statusClass: 'abundant'
},
{
id: 4,
label: '进场车辆',
value: 89,
unit: '辆',
icon: '🚛',
valueClass: 'vehicles',
status: '正常',
statusClass: 'normal'
},
{
id: 5,
label: '剩余车位',
value: 26,
unit: '个',
icon: '🅿',
valueClass: 'parking',
status: '紧张',
statusClass: 'tight'
},
{
id: 6,
label: '供应商数量',
value: 156,
unit: '家',
icon: '🏢',
valueClass: 'suppliers',
status: '活跃',
statusClass: 'active'
}
])
//
let updateTimer = null
const updateData = () => {
serviceData.value.forEach((item, index) => {
//
switch (item.id) {
case 1: //
const supplyChange = Math.floor((Math.random() - 0.5) * 100)
item.value = Math.floor(Math.max(2500, Math.min(3200, item.value + supplyChange)))
item.status = item.value > 2800 ? '充足' : item.value > 2600 ? '正常' : '紧张'
item.statusClass = item.value > 2800 ? 'abundant' : item.value > 2600 ? 'normal' : 'tight'
break
case 2: //
const soldChange = Math.floor(Math.random() * 50)
const maxSold = Math.floor(serviceData.value[0].value * 0.8)
item.value = Math.floor(Math.min(item.value + soldChange, maxSold))
break
case 3: //
item.value = Math.floor(serviceData.value[0].value - serviceData.value[1].value)
item.status = item.value > 1000 ? '充足' : item.value > 500 ? '正常' : '紧张'
item.statusClass = item.value > 1000 ? 'abundant' : item.value > 500 ? 'normal' : 'tight'
break
case 4: //
const vehicleChange = Math.floor((Math.random() - 0.5) * 10)
item.value = Math.floor(Math.max(60, Math.min(120, item.value + vehicleChange)))
break
case 5: //
const parkingChange = Math.floor((Math.random() - 0.5) * 8)
item.value = Math.floor(Math.max(5, Math.min(50, item.value + parkingChange)))
item.status = item.value > 30 ? '充足' : item.value > 15 ? '正常' : '紧张'
item.statusClass = item.value > 30 ? 'abundant' : item.value > 15 ? 'normal' : 'tight'
break
case 6: //
const supplierChange = Math.floor((Math.random() - 0.5) * 5)
item.value = Math.floor(Math.max(120, Math.min(200, item.value + supplierChange)))
item.status = item.value > 150 ? '活跃' : item.value > 130 ? '正常' : '较少'
item.statusClass = item.value > 150 ? 'active' : item.value > 130 ? 'normal' : 'low'
break
}
})
}
onMounted(() => {
updateTimer = setInterval(updateData, 8000) // 8
})
onUnmounted(() => {
if (updateTimer) {
clearInterval(updateTimer)
}
})
</script>
<style scoped>
.service-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: repeat(3, 1fr);
gap: 10px;
height: 100%;
padding: 8px;
}
.service-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.service-item:hover {
border-color: rgba(64, 158, 255, 0.4);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
transform: translateY(-1px);
}
.service-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.service-icon {
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: rgba(64, 158, 255, 0.1);
border-radius: 8px;
flex-shrink: 0;
}
.service-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.service-label {
font-size: 14px;
color: #a0a8b8;
font-weight: 500;
line-height: 1.2;
}
.service-value {
font-size: 20px;
font-weight: bold;
color: #ffffff;
line-height: 1.2;
display: flex;
align-items: baseline;
gap: 4px;
flex-shrink: 0;
text-align: right;
}
.service-value.total {
color: #409EFF;
}
.service-value.sold {
color: #67C23A;
}
.service-value.pending {
color: #E6A23C;
}
.service-value.vehicles {
color: #9C27B0;
}
.service-value.parking {
color: #F56C6C;
}
.service-value.suppliers {
color: #26C6DA;
}
.service-unit {
font-size: 11px;
color: #8cc8ff;
font-weight: normal;
}
.service-status {
font-size: 14px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
align-self: flex-start;
line-height: 1;
}
.service-status.abundant {
background: rgba(103, 194, 58, 0.2);
color: #67C23A;
}
.service-status.normal {
background: rgba(64, 158, 255, 0.2);
color: #409EFF;
}
.service-status.tight {
background: rgba(245, 108, 108, 0.2);
color: #F56C6C;
}
.service-status.active {
background: rgba(38, 198, 218, 0.2);
color: #26C6DA;
}
.service-status.low {
background: rgba(230, 162, 60, 0.2);
color: #E6A23C;
}
</style>

@ -0,0 +1,373 @@
<template>
<BaseCard title="市场环境监控">
<div class="monitor-content">
<div class="environment-grid">
<div v-for="item in environmentData" :key="item.id" class="env-item" :class="item.statusClass">
<div class="env-icon" :style="{ color: item.iconColor }">
{{ item.icon }}
</div>
<div class="env-info">
<div class="env-name">{{ item.name }}</div>
<div class="env-value" :style="{ color: item.valueColor }">
{{ item.value }}
<span class="env-unit">{{ item.unit }}</span>
</div>
<div class="env-status" :class="item.statusClass">
{{ item.statusText }}
</div>
</div>
</div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import BaseCard from './BaseCard.vue'
//
const temperature = ref(23)
const humidity = ref(58)
const airQuality = ref(45)
const pressure = ref(1013)
const uvIndex = ref(3)
const rainfall = ref(0.5)
const windDirection = ref(135)
const windLevel = ref(2)
//
const getStatus = (value, type) => {
switch (type) {
case 'temperature':
if (value >= 15 && value <= 25) return { class: 'excellent', text: '适宜', color: '#67C23A' }
if (value < 15 || value > 25) return { class: 'warning', text: '偏冷/热', color: '#E6A23C' }
return { class: 'danger', text: '异常', color: '#F56C6C' }
case 'humidity':
if (value >= 40 && value <= 70) return { class: 'excellent', text: '舒适', color: '#67C23A' }
if (value < 40 || value > 70) return { class: 'warning', text: '干燥/潮湿', color: '#E6A23C' }
return { class: 'danger', text: '异常', color: '#F56C6C' }
case 'airQuality':
if (value <= 50) return { class: 'excellent', text: '优良', color: '#67C23A' }
if (value <= 100) return { class: 'good', text: '良好', color: '#409EFF' }
if (value <= 150) return { class: 'warning', text: '轻度污染', color: '#E6A23C' }
return { class: 'danger', text: '重度污染', color: '#F56C6C' }
case 'pressure':
if (value >= 1000 && value <= 1020) return { class: 'excellent', text: '正常', color: '#67C23A' }
return { class: 'warning', text: '异常', color: '#E6A23C' }
case 'uvIndex':
if (value <= 2) return { class: 'excellent', text: '低', color: '#67C23A' }
if (value <= 5) return { class: 'good', text: '中等', color: '#409EFF' }
if (value <= 7) return { class: 'warning', text: '高', color: '#E6A23C' }
return { class: 'danger', text: '极高', color: '#F56C6C' }
case 'rainfall':
if (value < 1) return { class: 'excellent', text: '无雨', color: '#67C23A' }
if (value < 5) return { class: 'good', text: '小雨', color: '#409EFF' }
if (value < 15) return { class: 'warning', text: '中雨', color: '#E6A23C' }
return { class: 'danger', text: '大雨', color: '#F56C6C' }
case 'windLevel':
if (value <= 3) return { class: 'excellent', text: '微风', color: '#67C23A' }
if (value <= 5) return { class: 'good', text: '和风', color: '#409EFF' }
if (value <= 7) return { class: 'warning', text: '强风', color: '#E6A23C' }
return { class: 'danger', text: '大风', color: '#F56C6C' }
default:
return { class: 'normal', text: '正常', color: '#409EFF' }
}
}
//
const getWindDirection = (degree) => {
const directions = [
{ min: 0, max: 22.5, name: '北风' },
{ min: 22.5, max: 67.5, name: '东北风' },
{ min: 67.5, max: 112.5, name: '东风' },
{ min: 112.5, max: 157.5, name: '东南风' },
{ min: 157.5, max: 202.5, name: '南风' },
{ min: 202.5, max: 247.5, name: '西南风' },
{ min: 247.5, max: 292.5, name: '西风' },
{ min: 292.5, max: 337.5, name: '西北风' },
{ min: 337.5, max: 360, name: '北风' }
]
const direction = directions.find(d => degree >= d.min && degree < d.max)
return direction ? direction.name : '无风'
}
//
const environmentData = computed(() => {
const tempStatus = getStatus(temperature.value, 'temperature')
const humidityStatus = getStatus(humidity.value, 'humidity')
const airQualityStatus = getStatus(airQuality.value, 'airQuality')
const pressureStatus = getStatus(pressure.value, 'pressure')
const uvStatus = getStatus(uvIndex.value, 'uvIndex')
const rainfallStatus = getStatus(rainfall.value, 'rainfall')
const windLevelStatus = getStatus(windLevel.value, 'windLevel')
return [
{
id: 1,
name: '温度',
value: temperature.value,
unit: '°C',
icon: '🌡',
iconColor: tempStatus.color,
valueColor: tempStatus.color,
statusClass: tempStatus.class,
statusText: tempStatus.text
},
{
id: 2,
name: '湿度',
value: humidity.value,
unit: '%',
icon: '💧',
iconColor: humidityStatus.color,
valueColor: humidityStatus.color,
statusClass: humidityStatus.class,
statusText: humidityStatus.text
},
{
id: 3,
name: '空气质量',
value: airQuality.value,
unit: 'AQI',
icon: '🌬',
iconColor: airQualityStatus.color,
valueColor: airQualityStatus.color,
statusClass: airQualityStatus.class,
statusText: airQualityStatus.text
},
{
id: 4,
name: '大气压强',
value: pressure.value,
unit: 'hPa',
icon: '📊',
iconColor: pressureStatus.color,
valueColor: pressureStatus.color,
statusClass: pressureStatus.class,
statusText: pressureStatus.text
},
{
id: 5,
name: '紫外线',
value: uvIndex.value,
unit: '级',
icon: '☀',
iconColor: uvStatus.color,
valueColor: uvStatus.color,
statusClass: uvStatus.class,
statusText: uvStatus.text
},
{
id: 6,
name: '降雨量',
value: rainfall.value,
unit: 'mm',
icon: '🌧',
iconColor: rainfallStatus.color,
valueColor: rainfallStatus.color,
statusClass: rainfallStatus.class,
statusText: rainfallStatus.text
},
{
id: 7,
name: '风向',
value: getWindDirection(windDirection.value),
unit: '',
icon: '🧭',
iconColor: '#409EFF',
valueColor: '#409EFF',
statusClass: 'normal',
statusText: `${windDirection.value}°`
},
{
id: 8,
name: '风力',
value: windLevel.value,
unit: '级',
icon: '💨',
iconColor: windLevelStatus.color,
valueColor: windLevelStatus.color,
statusClass: windLevelStatus.class,
statusText: windLevelStatus.text
}
]
})
//
let updateTimer = null
const updateData = () => {
// 15-28
temperature.value = (15 + Math.random() * 13).toFixed(1)
// 湿 35-85%
humidity.value = Math.round(35 + Math.random() * 50)
// 20-120
airQuality.value = Math.round(20 + Math.random() * 100)
// 995-1025
pressure.value = Math.round(995 + Math.random() * 30)
// 线 0-8
uvIndex.value = Math.round(Math.random() * 8)
// 0-10mm
rainfall.value = (Math.random() * 10).toFixed(1)
// 0-360
windDirection.value = Math.round(Math.random() * 360)
// 0-8
windLevel.value = Math.round(Math.random() * 8)
}
onMounted(() => {
updateTimer = setInterval(updateData, 8000)
})
onUnmounted(() => {
if (updateTimer) {
clearInterval(updateTimer)
}
})
</script>
<style scoped>
.monitor-content {
height: 100%;
padding: 8px;
}
.environment-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
height: 100%;
}
.env-item {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
justify-content: center;
}
.env-item:hover {
border-color: rgba(64, 158, 255, 0.4);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(64, 158, 255, 0.15);
}
.env-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(64, 158, 255, 0.6), transparent);
transform: translateX(-100%);
transition: transform 0.6s ease;
}
.env-item:hover::before {
transform: translateX(100%);
}
.env-icon {
font-size: 24px;
margin-bottom: 4px;
transition: transform 0.3s ease;
}
.env-item:hover .env-icon {
transform: scale(1.1);
}
.env-info {
text-align: center;
width: 100%;
}
.env-name {
font-size: 14px;
color: #a0a8b8;
margin-bottom: 4px;
font-weight: 500;
}
.env-value {
font-size: 16px;
font-weight: bold;
margin-bottom: 4px;
transition: color 0.3s ease;
}
.env-unit {
font-size: 10px;
opacity: 0.8;
margin-left: 2px;
}
.env-status {
font-size: 12px;
padding: 2px 6px;
border-radius: 8px;
font-weight: 500;
background: rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.env-status.excellent {
background: rgba(103, 194, 58, 0.2);
color: #67C23A;
border: 1px solid rgba(103, 194, 58, 0.3);
}
.env-status.good {
background: rgba(64, 158, 255, 0.2);
color: #409EFF;
border: 1px solid rgba(64, 158, 255, 0.3);
}
.env-status.warning {
background: rgba(230, 162, 60, 0.2);
color: #E6A23C;
border: 1px solid rgba(230, 162, 60, 0.3);
}
.env-status.danger {
background: rgba(245, 108, 108, 0.2);
color: #F56C6C;
border: 1px solid rgba(245, 108, 108, 0.3);
}
.env-status.normal {
background: rgba(64, 158, 255, 0.15);
color: #409EFF;
border: 1px solid rgba(64, 158, 255, 0.25);
}
/* 响应式布局 */
@media (max-width: 1200px) {
.environment-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.environment-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

@ -0,0 +1,593 @@
<template>
<BaseCard title="市场实时监控">
<div class="video-monitor-content">
<div class="video-carousel">
<div class="carousel-header">
<div class="carousel-info">
<span class="carousel-title">监控画面 ({{ currentGroup + 1 }}/{{ totalGroups }})</span>
<div class="carousel-indicator">
<div v-for="(group, index) in cameraGroups" :key="index"
class="indicator-dot"
:class="{ active: index === currentGroup }"
@click="switchToGroup(index)">
</div>
</div>
</div>
<div class="carousel-controls">
<button class="carousel-btn" @click="prevGroup" title="上一组">
</button>
<button class="carousel-btn" @click="toggleAutoPlay" :class="{ active: isAutoPlay }" title="自动播放">
{{ isAutoPlay ? '⏸' : '▶' }}
</button>
<button class="carousel-btn" @click="nextGroup" title="下一组">
</button>
</div>
</div>
<div class="video-grid">
<div v-for="camera in currentCameras" :key="camera.id" class="video-item" :class="camera.status">
<div class="video-header">
<div class="camera-info">
<span class="camera-name">{{ camera.name }}</span>
<!-- <div class="status-indicator" :class="camera.status">
<span class="status-dot"></span>
<span class="status-text">{{ camera.statusText }}</span>
</div> -->
</div>
<div class="video-controls">
<button class="control-btn" @click="toggleFullscreen(camera.id)" title="全屏">
📺
</button>
<button class="control-btn" @click="toggleRecording(camera.id)" title="录制" :class="{ recording: camera.recording }">
{{ camera.recording ? '⏹' : '⏺' }}
</button>
</div>
</div>
<div class="video-screen">
<div class="video-placeholder" :style="{ backgroundImage: `url(${camera.preview})` }">
<div class="video-overlay">
<div class="live-indicator" v-if="camera.status === 'online'">
<span class="live-dot"></span>
LIVE
</div>
<div class="offline-message" v-if="camera.status === 'offline'">
📵 离线
</div>
<div class="error-message" v-if="camera.status === 'error'">
故障
</div>
</div>
</div>
</div>
<div class="video-info">
<div class="info-item">
<span class="info-label">分辨率:</span>
<span class="info-value">{{ camera.resolution }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import BaseCard from './BaseCard.vue'
//
const cameras = ref([
{
id: 1,
name: '交易大厅主区',
status: 'online',
statusText: '在线',
resolution: '1920×1080',
fps: 30,
recording: false,
preview: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuS6pOaYk+Wkp+WOhTwvdGV4dD48L3N2Zz4='
},
{
id: 2,
name: '牦牛展示区',
status: 'online',
statusText: '在线',
resolution: '1920×1080',
fps: 25,
recording: true,
preview: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPueJpueJm+WxleekuuWMujwvdGV4dD48L3N2Zz4='
},
{
id: 3,
name: '停车场入口',
status: 'online',
statusText: '在线',
resolution: '1280×720',
fps: 20,
recording: false,
preview: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWBnOi9puWcuuWFpeWPozwvdGV4dD48L3N2Zz4='
},
{
id: 4,
name: '安全出口',
status: 'offline',
statusText: '离线',
resolution: '1280×720',
fps: 0,
recording: false,
preview: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMWExYTFhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzU1NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWuieWFqOWHuuWPozwvdGV4dD48L3N2Zz4='
},
{
id: 5,
name: '办公区域',
status: 'error',
statusText: '故障',
resolution: '1920×1080',
fps: 0,
recording: false,
preview: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWKnuWFrOWMuuWfnzwvdGV4dD48L3N2Zz4='
},
{
id: 6,
name: '仓储区域',
status: 'online',
statusText: '在线',
resolution: '1280×720',
fps: 15,
recording: false,
preview: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMmEyYTJhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuS7k+WCqOWMuuWfnzwvdGV4dD48L3N2Zz4='
}
])
//
const currentGroup = ref(0)
const isAutoPlay = ref(true)
const carouselTimer = ref(null)
// 2
const cameraGroups = computed(() => {
const groups = []
for (let i = 0; i < cameras.value.length; i += 2) {
groups.push(cameras.value.slice(i, i + 2))
}
return groups
})
const totalGroups = computed(() => cameraGroups.value.length)
const currentCameras = computed(() => cameraGroups.value[currentGroup.value] || [])
//
const toggleFullscreen = (cameraId) => {
console.log(`全屏显示摄像头 ${cameraId}`)
//
}
const toggleRecording = (cameraId) => {
const camera = cameras.value.find(c => c.id === cameraId)
if (camera && camera.status === 'online') {
camera.recording = !camera.recording
}
}
//
const nextGroup = () => {
currentGroup.value = (currentGroup.value + 1) % totalGroups.value
}
const prevGroup = () => {
currentGroup.value = currentGroup.value === 0 ? totalGroups.value - 1 : currentGroup.value - 1
}
const switchToGroup = (index) => {
currentGroup.value = index
resetAutoPlayTimer()
}
const toggleAutoPlay = () => {
isAutoPlay.value = !isAutoPlay.value
if (isAutoPlay.value) {
startAutoPlay()
} else {
stopAutoPlay()
}
}
const startAutoPlay = () => {
if (carouselTimer.value) {
clearInterval(carouselTimer.value)
}
carouselTimer.value = setInterval(() => {
nextGroup()
}, 10000) // 10
}
const stopAutoPlay = () => {
if (carouselTimer.value) {
clearInterval(carouselTimer.value)
carouselTimer.value = null
}
}
const resetAutoPlayTimer = () => {
if (isAutoPlay.value) {
stopAutoPlay()
startAutoPlay()
}
}
//
let updateTimer = null
const updateData = () => {
// fps
cameras.value.forEach(camera => {
if (camera.status === 'online') {
const baseFps = camera.fps
camera.fps = Math.max(0, baseFps + Math.floor((Math.random() - 0.5) * 4))
}
})
}
onMounted(() => {
updateTimer = setInterval(updateData, 5000)
if (isAutoPlay.value) {
startAutoPlay()
}
})
onUnmounted(() => {
if (updateTimer) {
clearInterval(updateTimer)
}
stopAutoPlay()
})
</script>
<style scoped>
.video-monitor-content {
height: 100%;
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px;
}
.video-carousel {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.carousel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.carousel-info {
display: flex;
align-items: center;
gap: 12px;
}
.carousel-title {
font-size: 12px;
color: #409EFF;
font-weight: 600;
}
.carousel-indicator {
display: flex;
gap: 6px;
}
.indicator-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
cursor: pointer;
transition: all 0.3s ease;
}
.indicator-dot:hover {
background: rgba(64, 158, 255, 0.6);
transform: scale(1.2);
}
.indicator-dot.active {
background: #409EFF;
box-shadow: 0 0 10px rgba(64, 158, 255, 0.6);
}
.carousel-controls {
display: flex;
gap: 4px;
}
.carousel-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 6px 10px;
font-size: 12px;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
min-width: 32px;
}
.carousel-btn:hover {
background: rgba(64, 158, 255, 0.3);
border-color: rgba(64, 158, 255, 0.5);
transform: translateY(-1px);
}
.carousel-btn.active {
background: rgba(64, 158, 255, 0.4);
border-color: rgba(64, 158, 255, 0.6);
color: #409EFF;
}
.video-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
flex: 1;
min-height: 0;
}
.video-item {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.video-item:hover {
border-color: rgba(64, 158, 255, 0.4);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(64, 158, 255, 0.15);
}
.video-item.online {
border-color: rgba(103, 194, 58, 0.3);
}
.video-item.offline {
border-color: rgba(230, 162, 60, 0.3);
}
.video-item.error {
border-color: rgba(245, 108, 108, 0.3);
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background: rgba(0, 0, 0, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.camera-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.camera-name {
font-size: 14px;
font-weight: 600;
color: #fff;
}
.status-indicator {
display: flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-indicator.online .status-dot {
background: #67C23A;
box-shadow: 0 0 8px rgba(103, 194, 58, 0.6);
}
.status-indicator.offline .status-dot {
background: #E6A23C;
box-shadow: 0 0 8px rgba(230, 162, 60, 0.6);
}
.status-indicator.error .status-dot {
background: #F56C6C;
box-shadow: 0 0 8px rgba(245, 108, 108, 0.6);
}
.status-text {
font-size: 12px;
color: #a0a8b8;
}
.video-controls {
display: flex;
gap: 4px;
}
.control-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 4px 6px;
font-size: 10px;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
}
.control-btn:hover {
background: rgba(64, 158, 255, 0.3);
border-color: rgba(64, 158, 255, 0.5);
}
.control-btn.recording {
background: rgba(245, 108, 108, 0.3);
border-color: rgba(245, 108, 108, 0.5);
}
.video-screen {
flex: 1;
position: relative;
min-height: 120px;
}
.video-placeholder {
width: 100%;
height: 100%;
background-color: #1a1a1a;
background-size: cover;
background-position: center;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.video-overlay {
position: absolute;
top: 8px;
right: 8px;
}
.live-indicator {
background: rgba(245, 108, 108, 0.9);
color: #fff;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
display: flex;
align-items: center;
gap: 4px;
}
.live-dot {
width: 4px;
height: 4px;
background: #fff;
border-radius: 50%;
animation: blink 1s infinite;
}
.offline-message,
.error-message {
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.video-info {
padding: 6px 10px;
background: rgba(0, 0, 0, 0.2);
display: flex;
justify-content: space-between;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.info-item {
display: flex;
align-items: center;
gap: 4px;
}
.info-label {
font-size: 12px;
color: #a0a8b8;
}
.info-value {
font-size: 12px;
color: #409EFF;
font-weight: 600;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
}
@keyframes blink {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0.3;
}
}
/* 响应式布局 */
@media (max-width: 900px) {
.carousel-header {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.carousel-info {
justify-content: center;
}
.carousel-controls {
justify-content: center;
}
}
@media (max-width: 600px) {
.video-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.carousel-title {
font-size: 11px;
}
.carousel-btn {
padding: 4px 8px;
font-size: 11px;
min-width: 28px;
}
}
</style>

@ -0,0 +1,223 @@
<template>
<BaseCard title="采购商户来源分析">
<div ref="chartRef" class="chart"></div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
const chartRef = ref(null)
let chartInstance = null
// -
const regionsData = ref([
{ name: '果洛藏族自治州', value: 156, color: '#409EFF', description: '青海果洛州采购商户' },
{ name: '成都', value: 142, color: '#67C23A', description: '四川省会城市采购商户' },
{ name: '玉树藏族自治州', value: 128, color: '#E6A23C', description: '青海玉树州采购商户' },
{ name: '甘孜藏族自治州', value: 115, color: '#F56C6C', description: '四川甘孜州采购商户' },
{ name: '拉萨', value: 98, color: '#909399', description: '西藏自治区采购商户' },
{ name: '西宁', value: 89, color: '#00D4AA', description: '青海省会城市采购商户' },
{ name: '兰州', value: 76, color: '#8B5CF6', description: '甘肃省会城市采购商户' },
{ name: '甘南藏族自治州', value: 68, color: '#FF6B9D', description: '甘肃甘南州采购商户' },
{ name: '昌都', value: 54, color: '#FFB800', description: '西藏昌都地区采购商户' }
])
//
const updateData = () => {
regionsData.value.forEach(region => {
//
const variance = (Math.random() - 0.5) * 20 // ±10
region.value = Math.max(20, Math.floor(region.value + variance))
})
}
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const updateChart = () => {
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(15, 25, 45, 0.95)',
borderColor: '#00d4ff',
borderWidth: 1,
textStyle: {
color: '#fff',
fontSize: 15
},
formatter: function(params) {
const item = regionsData.value[params[0].dataIndex]
return `<div style="padding: 8px;">
<strong style="color: #00d4ff;">${params[0].name}</strong><br/>
<span style="color: #67c23a;">采购商户: ${params[0].value} </span><br/>
<span style="color: #8cc8ff;">${item.description}</span>
</div>`
}
},
grid: {
left: '8%',
right: '8%',
bottom: '20%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: regionsData.value.map(item => item.name),
axisLine: {
lineStyle: {
color: '#4a5568'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 12,
rotate: 30,
interval: 0,
formatter: function(value) {
//
if (value.length > 6) {
return value.substring(0, 4) + '\n' + value.substring(4)
}
return value
}
}
},
yAxis: {
type: 'value',
name: '采购商户数量(家)',
nameTextStyle: {
color: '#a0a8b8',
fontSize: 13
},
splitLine: {
lineStyle: {
color: '#2d3748',
type: 'dashed'
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 13
}
},
series: [{
name: '采购商户数量',
type: 'bar',
data: regionsData.value.map((item, index) => ({
value: item.value,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: item.color
}, {
offset: 1,
color: item.color + '40' //
}]
},
borderRadius: [6, 6, 0, 0],
borderWidth: 2,
borderColor: item.color,
shadowBlur: 8,
shadowColor: item.color + '30'
}
})),
barWidth: '60%',
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowColor: 'rgba(0, 212, 255, 0.6)',
borderColor: '#00d4ff',
borderWidth: 3
},
scaleSize: 5
},
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 13,
fontWeight: 'bold',
formatter: '{c}家'
},
animationDelay: function (idx) {
return idx * 100
}
}],
animationEasing: 'elasticOut',
animationDelayUpdate: function (idx) {
return idx * 50
}
}
chartInstance.setOption(option)
}
updateChart()
//
let currentIndex = 0
const timer = setInterval(() => {
chartInstance.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: currentIndex
})
currentIndex = (currentIndex + 1) % regionsData.value.length
}, 3000)
//
const dataTimer = setInterval(() => {
updateData()
updateChart()
}, 12000) // 12
chartInstance._autoTimer = timer
chartInstance._dataTimer = dataTimer
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
if (chartInstance._autoTimer) {
clearInterval(chartInstance._autoTimer)
}
if (chartInstance._dataTimer) {
clearInterval(chartInstance._dataTimer)
}
chartInstance.dispose()
}
})
</script>
<style scoped>
.chart {
width: 100%;
height: 100%;
}
</style>

@ -0,0 +1,312 @@
<template>
<BaseCard title="实时交易统计">
<!-- 时间维度切换按钮 -->
<div class="time-dimension-controls">
<button
v-for="dimension in timeDimensions"
:key="dimension.key"
@click="currentDimension = dimension.key"
:class="{ active: currentDimension === dimension.key }"
class="dimension-btn"
>
{{ dimension.label }}
</button>
</div>
<div class="stats-container">
<div class="stat-item">
<div class="stat-value">
<div>{{ formatNumber(statsData.yakTotalVolume) }}</div>
<div class="stat-unit">()</div>
</div>
<div class="stat-label">牦牛交易总量</div>
</div>
<div class="stat-item">
<div class="stat-value">
<div>{{ formatNumber(statsData.orderTotalVolume) }}</div>
<div class="stat-unit">()</div>
</div>
<div class="stat-label">订单交易总量</div>
</div>
<div class="stat-item">
<div class="stat-value">
<div>{{ formatNumber(statsData.sellerCount) }}</div>
<div class="stat-unit">()</div>
</div>
<div class="stat-label">销售商户数量</div>
</div>
<div class="stat-item">
<div class="stat-value">
<div>{{ formatNumber(statsData.buyerCount) }}</div>
<div class="stat-unit">()</div>
</div>
<div class="stat-label">采购商户数量</div>
</div>
<!-- 调试信息 -->
<div class="debug-info" v-if="showDebug">
<div style="font-size: 10px; color: #666; margin-top: 10px;">
调试: {{ statsData.systemStatus }}
</div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted, reactive, watch, computed } from 'vue'
import BaseCard from './BaseCard.vue'
import { dataManager } from '../utils/dataManager.js'
//
const timeDimensions = [
{ key: 'day', label: '日' },
{ key: 'week', label: '周' },
{ key: 'year', label: '年' }
]
//
const currentDimension = ref('day')
//
const statsData = reactive({
yakTotalVolume: 0,
orderTotalVolume: 0,
sellerCount: 0,
buyerCount: 0,
systemStatus: '加载中...'
})
//
const allDimensionData = reactive({
day: {
yakTotalVolume: 0,
orderTotalVolume: 0,
sellerCount: 0,
buyerCount: 0
},
week: {
yakTotalVolume: 0,
orderTotalVolume: 0,
sellerCount: 0,
buyerCount: 0
},
year: {
yakTotalVolume: 0,
orderTotalVolume: 0,
sellerCount: 0,
buyerCount: 0
}
})
//
const showDebug = ref(false)
//
const formatNumber = (num) => {
return dataManager.formatNumber(num)
}
//
const generateMockData = (dimension) => {
const baseData = {
yakTotalVolume: 8350,
orderTotalVolume: 1245,
sellerCount: 156,
buyerCount: 89
}
switch (dimension) {
case 'day':
return {
yakTotalVolume: baseData.yakTotalVolume,
orderTotalVolume: baseData.orderTotalVolume,
sellerCount: baseData.sellerCount,
buyerCount: baseData.buyerCount
}
case 'week':
return {
yakTotalVolume: Math.floor(baseData.yakTotalVolume * 6.8),
orderTotalVolume: Math.floor(baseData.orderTotalVolume * 6.5),
sellerCount: Math.floor(baseData.sellerCount * 1.2),
buyerCount: Math.floor(baseData.buyerCount * 1.15)
}
case 'year':
return {
yakTotalVolume: Math.floor(baseData.yakTotalVolume * 285),
orderTotalVolume: Math.floor(baseData.orderTotalVolume * 295),
sellerCount: Math.floor(baseData.sellerCount * 3.2),
buyerCount: Math.floor(baseData.buyerCount * 2.8)
}
default:
return baseData
}
}
//
const loadData = async () => {
try {
//
await dataManager.loadData()
//
timeDimensions.forEach(dimension => {
const data = generateMockData(dimension.key)
allDimensionData[dimension.key] = data
})
//
updateCurrentData()
statsData.systemStatus = '数据加载完成'
console.log('所有维度数据加载完成:', allDimensionData)
} catch (error) {
console.error('加载实时统计数据失败:', error)
statsData.systemStatus = '加载失败'
}
}
//
const updateCurrentData = () => {
const currentData = allDimensionData[currentDimension.value]
if (currentData) {
statsData.yakTotalVolume = currentData.yakTotalVolume
statsData.orderTotalVolume = currentData.orderTotalVolume
statsData.sellerCount = currentData.sellerCount
statsData.buyerCount = currentData.buyerCount
}
}
//
watch(currentDimension, () => {
updateCurrentData()
}, { immediate: false })
onMounted(async () => {
await loadData()
//
window.addEventListener('dataReloaded', async () => {
console.log('收到数据重新加载事件,重新加载实时统计数据')
await loadData()
})
})
onUnmounted(() => {
//
})
</script>
<style scoped>
.time-dimension-controls {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 12px;
padding: 8px;
}
.dimension-btn {
padding: 6px 16px;
border: 1px solid #2c5282;
background: rgba(15, 27, 46, 0.8);
color: #8cc8ff;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
min-width: 50px;
}
.dimension-btn:hover {
border-color: #00d4ff;
color: #00d4ff;
background: rgba(0, 212, 255, 0.1);
transform: translateY(-1px);
}
.dimension-btn.active {
border-color: #00d4ff;
background: linear-gradient(135deg, #00d4ff, #0099cc);
color: #fff;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
}
.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 12px;
height: calc(100% - 60px);
padding: 8px;
}
.stat-item {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
text-align: center;
padding: 20px 12px 16px 12px;
background: url('../images/数据底座.png') center/contain no-repeat;
background-size: 80% 70%;
border-radius: 8px;
transition: all 0.3s ease;
min-height: 90px;
overflow: hidden;
}
.stat-item:hover {
transform: translateY(-2px);
filter: brightness(1.1);
}
.stat-unit {
font-size: 16px;
margin-top: 12px;
}
.stat-value {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 22px;
font-weight: 900;
color: #ffffff;
text-shadow:
0 0 15px rgba(64, 158, 255, 1),
0 0 25px rgba(64, 158, 255, 0.8),
2px 2px 6px rgba(0, 0, 0, 0.9),
-1px -1px 2px rgba(0, 0, 0, 0.8);
line-height: 1;
margin: 4px 0;
letter-spacing: 1px;
}
.stat-label {
font-size: 18px;
font-weight: 700;
color: #409EFF;
/* text-shadow:
0 0 10px rgba(255, 255, 255, 0.8),
0 0 15px rgba(64, 158, 255, 0.6),
2px 2px 4px rgba(0, 0, 0, 0.9),
-1px -1px 2px rgba(0, 0, 0, 0.7); */
line-height: 1.2;
margin-top: auto;
letter-spacing: 0.5px;
}
.debug-info {
grid-column: 1 / -1;
text-align: center;
margin-top: 8px;
}
</style>

@ -0,0 +1,207 @@
<template>
<BaseCard title="销售渠道统计">
<div class="stats-container">
<div class="stats-row">
<div class="stat-item">
<div class="stat-value">1,234</div>
<div class="stat-label">线上销售</div>
</div>
<div class="stat-item">
<div class="stat-value">856</div>
<div class="stat-label">线下销售</div>
</div>
<div class="stat-item">
<div class="stat-value">2,090</div>
<div class="stat-label">总销售额</div>
</div>
</div>
<div class="chart-container">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
const chartRef = ref(null)
let chartInstance = null
//
const legendData = [
{ name: '直销', color: '#409EFF', percent: 50 },
{ name: '代销', color: '#67C23A', percent: 30 },
{ name: '其他', color: '#E6A23C', percent: 20 }
]
//
const data = [
{ name: '直销', value: 50 },
{ name: '代销', value: 30 },
{ name: '其他', value: 20 }
]
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#409EFF',
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
series: [{
name: '销售渠道',
type: 'pie',
radius: ['40%', '65%'],
center: ['50%', '50%'],
data: data.map((item, index) => ({
...item,
itemStyle: {
color: legendData[index].color,
borderRadius: 8,
borderColor: '#1a1f2e',
borderWidth: 2
}
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
labelLine: {
show: false
},
label: {
show: false
}
}]
}
chartInstance.setOption(option)
//
let currentIndex = 0
const timer = setInterval(() => {
chartInstance.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: currentIndex
})
currentIndex = (currentIndex + 1) % data.length
chartInstance.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: currentIndex
})
}, 2000)
chartInstance._autoTimer = timer
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
if (chartInstance._autoTimer) {
clearInterval(chartInstance._autoTimer)
}
chartInstance.dispose()
}
})
</script>
<style scoped>
.channel-container {
display: flex;
flex-direction: column;
height: 100%;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
.stat-item {
text-align: center;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
padding: 8px;
}
.stat-label {
font-size: 11px;
color: #a0a8b8;
margin-bottom: 4px;
}
.stat-value {
font-size: 13px;
font-weight: bold;
color: #409EFF;
}
.chart-section {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
}
.chart {
flex: 1;
height: 150px;
}
.chart-legend {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 80px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-text {
color: #a0a8b8;
flex: 1;
}
.legend-percent {
color: #409EFF;
font-weight: bold;
}
</style>

@ -0,0 +1,205 @@
<template>
<div class="scrolling-announcement">
<div class="announcement-header">
<div class="header-icon">📢</div>
<div class="header-title">最新公告</div>
</div>
<div class="announcement-content">
<div class="scrolling-container" ref="scrollingContainer">
<div
class="announcement-item current"
:key="'current-' + currentIndex"
>
{{ announcements[currentIndex]?.content }}
</div>
<div
class="announcement-item next"
:key="'next-' + nextIndex"
>
{{ announcements[nextIndex]?.content }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, reactive, computed } from 'vue'
import { dataManager } from '../utils/dataManager.js'
const scrollingContainer = ref(null)
const currentIndex = ref(0)
const isAnimating = ref(false)
let scrollTimer = null
const announcements = reactive([])
//
const nextIndex = computed(() => {
return announcements.length > 0 ? (currentIndex.value + 1) % announcements.length : 0
})
//
const loadAnnouncements = async () => {
try {
//
await dataManager.loadData()
const data = dataManager.getData('announcements')
if (data && Array.isArray(data)) {
announcements.splice(0, announcements.length, ...data.map(content => ({ content })))
console.log('公告数据加载成功:', data)
} else {
console.warn('未找到公告数据,使用默认数据')
loadDefaultAnnouncements()
}
} catch (error) {
console.error('加载公告数据失败:', error)
loadDefaultAnnouncements()
}
}
//
const loadDefaultAnnouncements = () => {
announcements.splice(0, announcements.length,
{ content: '【重要】今日牦牛交易价格上涨2.5%,请各位商户注意市场行情变化' },
{ content: '【通知】新增北京地区优质采购商,欢迎牧民朋友联系洽谈业务' },
{ content: '【提醒】本周末系统维护,交易时间调整为09:00-17:00' },
{ content: '【资讯】春季牧草丰茂,预计下月牦牛供应量将增加15%' },
{ content: '【市场】西藏地区牦牛肉品质检测合格率达98.5%' },
{ content: '【政策】政府出台新政策支持牦牛产业发展,提供资金扶持' }
)
}
//
const performScroll = () => {
if (isAnimating.value || announcements.length <= 1) return
isAnimating.value = true
const container = scrollingContainer.value
//
container.classList.add('scrolling')
//
setTimeout(() => {
currentIndex.value = nextIndex.value
container.classList.remove('scrolling')
isAnimating.value = false
}, 800) //
}
//
const startVerticalScroll = () => {
if (announcements.length <= 1) return
scrollTimer = setInterval(() => {
performScroll()
}, 5000) // 5
}
//
const stopVerticalScroll = () => {
if (scrollTimer) {
clearInterval(scrollTimer)
scrollTimer = null
}
}
onMounted(async () => {
await loadAnnouncements()
//
if (announcements.length > 1) {
startVerticalScroll()
}
})
onUnmounted(() => {
stopVerticalScroll()
})
</script>
<style scoped>
.scrolling-announcement {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
height: 50px;
background: rgba(26, 31, 46, 0.95);
border: 1px solid rgba(64, 158, 255, 0.4);
border-radius: 8px;
backdrop-filter: blur(10px);
z-index: 100;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.announcement-header {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100px;
background: rgba(64, 158, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-right: 1px solid rgba(64, 158, 255, 0.3);
}
.header-icon {
font-size: 16px;
}
.header-title {
font-size: 12px;
font-weight: bold;
color: #409EFF;
}
.announcement-content {
margin-left: 100px;
height: 100%;
overflow: hidden;
position: relative;
}
.scrolling-container {
position: relative;
height: 100%;
width: 100%;
}
.announcement-item {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
display: flex;
align-items: center;
font-size: 14px;
color: #fff;
padding: 0 20px;
white-space: nowrap;
transition: transform 0.8s ease-in-out;
}
.announcement-item.current {
transform: translateY(0);
}
.announcement-item.next {
transform: translateY(100%);
}
/* 滚动动画状态 */
.scrolling-container.scrolling .announcement-item.current {
transform: translateY(-100%);
}
.scrolling-container.scrolling .announcement-item.next {
transform: translateY(0);
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,159 @@
<template>
<BaseCard title="交易走势图">
<div ref="chartRef" class="chart"></div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
const chartRef = ref(null)
let chartInstance = null
//
const months = ['一月', '二月', '三月', '四月', '五月', '六月']
const salesData = [10, 20, 40, 60, 40, 80]
const targetData = [15, 25, 35, 50, 45, 75]
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#409EFF',
textStyle: {
color: '#fff',
fontSize: 12
}
},
grid: {
left: '8%',
right: '8%',
bottom: '15%',
top: '10%'
},
xAxis: {
type: 'category',
data: months,
axisLine: {
lineStyle: {
color: '#4a5568'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 11
}
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
color: '#2d3748',
type: 'dashed'
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 11
}
},
series: [
{
name: '实际交易额',
type: 'line',
data: salesData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#409EFF',
width: 3
},
itemStyle: {
color: '#409EFF',
borderColor: '#fff',
borderWidth: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
])
}
},
{
name: '目标额度',
type: 'line',
data: targetData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#67C23A',
width: 3
},
itemStyle: {
color: '#67C23A',
borderColor: '#fff',
borderWidth: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
{ offset: 1, color: 'rgba(103, 194, 58, 0.05)' }
])
}
}
]
}
chartInstance.setOption(option)
//
chartInstance.on('finished', () => {
setTimeout(() => {
chartInstance.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: salesData.length - 1
})
}, 1000)
})
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
</script>
<style scoped>
.chart {
width: 100%;
height: 100%;
}
</style>

@ -0,0 +1,337 @@
<template>
<BaseCard title="交易明细">
<div class="transaction-content">
<div class="transaction-header">
<div class="header-controls">
<select v-model="currentFilter" class="filter-select">
<option value="all">全部交易</option>
<option value="today">今日交易</option>
<option value="week">本周交易</option>
</select>
<div class="search-box">
<input v-model="searchKeyword" placeholder="搜索交易..." class="search-input">
</div>
</div>
<div class="summary-stats">
<div class="stat-item">
<span class="stat-label">总交易额</span>
<span class="stat-value">{{ totalAmount }} 万元</span>
</div>
<div class="stat-item">
<span class="stat-label">交易笔数</span>
<span class="stat-value">{{ totalCount }} </span>
</div>
</div>
</div>
<div class="transaction-table">
<div class="table-header">
<div class="th">时间</div>
<div class="th">品种</div>
<div class="th">数量</div>
<div class="th">单价</div>
<div class="th">金额</div>
<div class="th">状态</div>
</div>
<div class="table-body">
<div
v-for="transaction in filteredTransactions"
:key="transaction.id"
class="table-row"
:class="{ 'highlight': transaction.id === highlightId }"
>
<div class="td time">{{ transaction.time }}</div>
<div class="td category">{{ transaction.category }}</div>
<div class="td quantity">{{ transaction.quantity }}</div>
<div class="td price">{{ transaction.price }}</div>
<div class="td amount">{{ transaction.amount }}</div>
<div class="td status">
<span class="status-badge" :class="transaction.status">
{{ getStatusText(transaction.status) }}
</span>
</div>
</div>
</div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import BaseCard from './BaseCard.vue'
const highlightIndex = ref(0)
const currentIndex = ref(0)
const tableBody = ref(null)
//
const allTransactions = ref([
{
id: 'TXN001',
category: '肉牛',
quantity: 50,
unitPrice: 15000,
totalPrice: 75.0,
buyer: '北京牧场',
seller: '红原牧民',
region: '北京',
time: '2025/01/01 10:30',
status: 'completed'
},
{
id: 'TXN002',
category: '奶牛',
quantity: 30,
unitPrice: 18000,
totalPrice: 54.0,
buyer: '上海乳业',
seller: '高原农场',
region: '上海',
time: '2025/01/01 11:15',
status: 'processing'
},
{
id: 'TXN003',
category: '黄牛',
quantity: 80,
unitPrice: 12000,
totalPrice: 96.0,
buyer: '广州食品',
seller: '草原合作社',
region: '广州',
time: '2025/01/01 12:00',
status: 'completed'
},
{
id: 'TXN004',
category: '水牛',
quantity: 25,
unitPrice: 14000,
totalPrice: 35.0,
buyer: '成都餐饮',
seller: '藏区牧场',
region: '成都',
time: '2025/01/01 13:45',
status: 'pending'
},
{
id: 'TXN005',
category: '牦牛',
quantity: 40,
unitPrice: 20000,
totalPrice: 80.0,
buyer: '西安贸易',
seller: '高原牧业',
region: '西安',
time: '2025/01/01 14:20',
status: 'completed'
},
{
id: 'TXN006',
category: '肉牛',
quantity: 60,
unitPrice: 16000,
totalPrice: 96.0,
buyer: '深圳集团',
seller: '牧民联合社',
region: '深圳',
time: '2025/01/01 15:10',
status: 'processing'
},
{
id: 'TXN007',
category: '奶牛',
quantity: 35,
unitPrice: 17500,
totalPrice: 61.25,
buyer: '杭州乳制品',
seller: '草原农场',
region: '杭州',
time: '2025/01/01 16:30',
status: 'completed'
},
{
id: 'TXN008',
category: '黄牛',
quantity: 70,
unitPrice: 13000,
totalPrice: 91.0,
buyer: '重庆食品',
seller: '高原牧场',
region: '重庆',
time: '2025/01/01 17:15',
status: 'pending'
}
])
//
const visibleTransactions = computed(() => {
const itemsToShow = 6
const result = []
for (let i = 0; i < itemsToShow; i++) {
const index = (currentIndex.value + i) % allTransactions.value.length
result.push(allTransactions.value[index])
}
return result
})
const getStatusText = (status) => {
const statusMap = {
completed: '已完成',
processing: '处理中',
pending: '待处理'
}
return statusMap[status] || '未知'
}
//
let scrollTimer = null
let highlightTimer = null
onMounted(() => {
//
scrollTimer = setInterval(() => {
currentIndex.value = (currentIndex.value + 1) % allTransactions.value.length
}, 3000)
//
highlightTimer = setInterval(() => {
highlightIndex.value = (highlightIndex.value + 1) % 6
}, 1000)
})
onUnmounted(() => {
if (scrollTimer) {
clearInterval(scrollTimer)
}
if (highlightTimer) {
clearInterval(highlightTimer)
}
})
</script>
<style scoped>
.transaction-table {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 80px 80px 80px 80px 90px 100px 100px 80px 120px 80px;
background: rgba(64, 158, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.header-cell {
padding: 12px 8px;
font-size: 12px;
font-weight: bold;
color: #409EFF;
text-align: center;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.header-cell:last-child {
border-right: none;
}
.table-body {
flex: 1;
overflow: hidden;
}
.table-row {
display: grid;
grid-template-columns: 80px 80px 80px 80px 90px 100px 100px 80px 120px 80px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
}
.table-row:hover,
.table-row.highlight {
background: rgba(64, 158, 255, 0.1);
}
.table-cell {
padding: 10px 8px;
font-size: 11px;
color: #a0a8b8;
text-align: center;
border-right: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.table-cell:last-child {
border-right: none;
}
.table-cell.quantity {
color: #67C23A;
font-weight: bold;
}
.table-cell.price {
color: #409EFF;
font-weight: bold;
}
.table-cell.total {
color: #E6A23C;
font-weight: bold;
}
.table-cell.time {
color: #8B5CF6;
font-size: 10px;
}
.status {
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
}
.status.completed {
background: rgba(103, 194, 58, 0.2);
color: #67C23A;
}
.status.processing {
background: rgba(230, 162, 60, 0.2);
color: #E6A23C;
}
.status.pending {
background: rgba(245, 108, 108, 0.2);
color: #F56C6C;
}
/* 动画效果 */
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.table-row {
animation: slideUp 0.5s ease;
}
</style>

@ -0,0 +1,172 @@
<template>
<BaseCard title="活牛/鲜肉价格趋势">
<div ref="chartRef" class="chart"></div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
const chartRef = ref(null)
let chartInstance = null
// - 2024
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月']
const liveCattleData = [26.5, 27.2, 28.8, 28.3, 29.1, 30.2, 29.6, 30.8] // / (26-31)
const beefData = [62.8, 61.5, 60.2, 58.9, 59.8, 61.2, 63.5, 64.8] // / (58-65)
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#409EFF',
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: function(params) {
let result = `${params[0].axisValue}<br/>`
params.forEach(param => {
result += `${param.marker}${param.seriesName}: ${param.value}元/公斤<br/>`
})
return result
}
},
grid: {
left: '8%',
right: '8%',
bottom: '15%',
top: '15%'
},
legend: {
data: ['活牛价格', '牛肉价格'],
textStyle: {
color: '#a0a8b8',
fontSize: 11
},
top: '5%'
},
xAxis: {
type: 'category',
data: months,
axisLine: {
lineStyle: {
color: '#4a5568'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 10
}
},
yAxis: {
type: 'value',
name: '价格(元/公斤)',
splitLine: {
lineStyle: {
color: '#2d3748',
type: 'dashed'
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 10
},
nameTextStyle: {
color: '#a0a8b8',
fontSize: 10
}
},
series: [
{
name: '活牛价格',
type: 'line',
data: liveCattleData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#67C23A',
width: 3
},
itemStyle: {
color: '#67C23A',
borderColor: '#fff',
borderWidth: 2
}
},
{
name: '牛肉价格',
type: 'line',
data: beefData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#E6A23C',
width: 3
},
itemStyle: {
color: '#E6A23C',
borderColor: '#fff',
borderWidth: 2
}
}
]
}
chartInstance.setOption(option)
//
let currentIndex = 0
const timer = setInterval(() => {
chartInstance.dispatchAction({
type: 'showTip',
seriesIndex: [0, 1],
dataIndex: currentIndex
})
currentIndex = (currentIndex + 1) % months.length
}, 2500)
chartInstance._autoTimer = timer
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
if (chartInstance._autoTimer) {
clearInterval(chartInstance._autoTimer)
}
chartInstance.dispose()
}
})
</script>
<style scoped>
.chart {
width: 100%;
height: 100%;
}
</style>

@ -0,0 +1,296 @@
<template>
<BaseCard title="牦牛销售类型统计">
<div class="sales-container">
<div class="chart-wrapper">
<div ref="chartRef" class="chart"></div>
</div>
<div class="legend-container">
<div v-for="item in salesData" :key="item.name" class="legend-item">
<span class="legend-dot" :style="{ backgroundColor: item.color }"></span>
<div class="legend-info">
<span class="legend-text">{{ item.name }}</span>
<div class="legend-stats">
<span class="legend-count">{{ item.count }}</span>
<span class="legend-percent">{{ item.value }}%</span>
</div>
</div>
</div>
</div>
</div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
const chartRef = ref(null)
let chartInstance = null
//
const salesData = ref([
{
name: '屠宰用途',
value: 52,
count: 1456,
color: '#E6A23C',
description: '用于屠宰加工的牦牛'
},
{
name: '养殖用途',
value: 35,
count: 980,
color: '#67C23A',
description: '用于继续养殖的牦牛'
},
{
name: '其他用途',
value: 13,
count: 364,
color: '#409EFF',
description: '其他特殊用途的牦牛'
}
])
//
const updateData = () => {
salesData.value.forEach(item => {
// 100%
const variance = (Math.random() - 0.5) * 4 // ±2%
item.value = Math.max(5, Math.min(80, item.value + variance))
//
const totalCount = 2800 // 2800
item.count = Math.floor((item.value / 100) * totalCount)
})
// 100%
const totalPercent = salesData.value.reduce((sum, item) => sum + item.value, 0)
salesData.value.forEach(item => {
item.value = Math.round((item.value / totalPercent) * 100)
})
//
const totalCount = 2800
salesData.value.forEach(item => {
item.count = Math.floor((item.value / 100) * totalCount)
})
}
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const updateChart = () => {
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(15, 25, 45, 0.95)',
borderColor: '#00d4ff',
borderWidth: 1,
textStyle: {
color: '#fff',
fontSize: 13
},
formatter: function(params) {
const item = salesData.value.find(d => d.name === params.name)
return `<div style="padding: 8px;">
<strong style="color: #00d4ff;">${params.name}</strong><br/>
<span style="color: #67c23a;">数量: ${item.count} </span><br/>
<span style="color: #8cc8ff;">占比: ${params.percent}%</span><br/>
<span style="color: #a0a8b8;">${item.description}</span>
</div>`
}
},
series: [{
name: '销售类型',
type: 'pie',
radius: ['35%', '75%'],
center: ['50%', '50%'],
data: salesData.value.map(item => ({
name: item.name,
value: item.value,
itemStyle: {
color: item.color,
borderRadius: 8,
borderColor: '#1a1f2e',
borderWidth: 3,
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
})),
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 212, 255, 0.5)',
borderColor: '#00d4ff',
borderWidth: 3
},
scaleSize: 10
},
labelLine: {
show: false
},
label: {
show: true,
position: 'center',
fontSize: 14,
fontWeight: 'bold',
color: '#fff',
formatter: function(params) {
if (params.name === '屠宰用途') {
return `${params.percent}%\n${params.name}`
}
return ''
}
},
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelay: function (idx) {
return Math.random() * 200
}
}]
}
chartInstance.setOption(option)
}
updateChart()
//
let currentIndex = 0
const timer = setInterval(() => {
chartInstance.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: currentIndex
})
currentIndex = (currentIndex + 1) % salesData.value.length
chartInstance.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: currentIndex
})
}, 3000)
//
const dataTimer = setInterval(() => {
updateData()
updateChart()
}, 10000) // 10
chartInstance._autoTimer = timer
chartInstance._dataTimer = dataTimer
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
if (chartInstance._autoTimer) {
clearInterval(chartInstance._autoTimer)
}
if (chartInstance._dataTimer) {
clearInterval(chartInstance._dataTimer)
}
chartInstance.dispose()
}
})
</script>
<style scoped>
.sales-container {
display: flex;
align-items: center;
height: 100%;
gap: 20px;
padding: 10px;
}
.chart-wrapper {
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.chart {
width: 100%;
height: 100%;
min-height: 200px;
}
.legend-container {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 140px;
max-width: 160px;
}
.legend-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
}
.legend-item:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(64, 158, 255, 0.2);
transform: translateX(2px);
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 2px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.legend-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.legend-text {
color: #ffffff;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
}
.legend-stats {
display: flex;
flex-direction: column;
gap: 2px;
}
.legend-count {
color: #8cc8ff;
font-size: 11px;
font-weight: 600;
}
.legend-percent {
color: #00d4ff;
font-size: 14px;
font-weight: bold;
}
</style>

@ -0,0 +1,313 @@
<template>
<BaseCard title="交易所牦牛成交数据">
<div class="time-selector">
<div class="selector-buttons">
<button
v-for="period in timePeriods"
:key="period.value"
:class="['period-btn', { active: selectedPeriod === period.value }]"
@click="changePeriod(period.value)"
>
{{ period.label }}
</button>
</div>
</div>
<div ref="chartRef" class="chart"></div>
</BaseCard>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue'
const chartRef = ref(null)
let chartInstance = null
const selectedPeriod = ref('day')
const timePeriods = [
{ label: '日', value: 'day' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' }
]
//
const generateTimeLabels = (period) => {
const now = new Date()
const labels = []
if (period === 'day') {
// 6
for (let i = 5; i >= 0; i--) {
const date = new Date(now)
date.setDate(date.getDate() - i)
labels.push(`${date.getMonth() + 1}/${date.getDate()}`)
}
} else if (period === 'week') {
// 6
for (let i = 5; i >= 0; i--) {
const date = new Date(now)
date.setDate(date.getDate() - i * 7)
const weekStart = new Date(date)
const weekEnd = new Date(date)
weekEnd.setDate(weekEnd.getDate() + 6)
labels.push(`${weekStart.getMonth() + 1}/${weekStart.getDate()}-${weekEnd.getMonth() + 1}/${weekEnd.getDate()}`)
}
} else if (period === 'month') {
// 6
for (let i = 5; i >= 0; i--) {
const date = new Date(now)
date.setMonth(date.getMonth() - i)
labels.push(`${date.getFullYear()}/${date.getMonth() + 1}`)
}
}
return labels
}
//
const generateMockData = (period) => {
const baseTrading = period === 'day' ? 150 : period === 'week' ? 800 : 3500
const baseOrders = period === 'day' ? 45 : period === 'week' ? 280 : 1200
const tradingData = []
const orderData = []
for (let i = 0; i < 6; i++) {
const tradingVariation = Math.floor(Math.random() * 200) - 100
const orderVariation = Math.floor(Math.random() * 60) - 30
tradingData.push(baseTrading + tradingVariation)
orderData.push(baseOrders + orderVariation)
}
return { tradingData, orderData }
}
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
updateChart()
}
const updateChart = () => {
if (!chartInstance) return
const timeLabels = generateTimeLabels(selectedPeriod.value)
const { tradingData, orderData } = generateMockData(selectedPeriod.value)
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#409EFF',
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: function(params) {
let result = `${params[0].axisValue}<br/>`
params.forEach(param => {
const unit = param.seriesName === '牦牛交易数量' ? '头' : '单'
result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`
})
return result
}
},
grid: {
left: '8%',
right: '8%',
bottom: '15%',
top: '20%'
},
legend: {
data: ['牦牛交易数量', '成交订单数量'],
textStyle: {
color: '#a0a8b8',
fontSize: 11
},
top: '8%'
},
xAxis: {
type: 'category',
data: timeLabels,
axisLine: {
lineStyle: {
color: '#4a5568'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 10,
rotate: selectedPeriod.value === 'week' ? 45 : 0
}
},
yAxis: [
{
type: 'value',
name: '交易数量(头)',
position: 'left',
splitLine: {
lineStyle: {
color: '#2d3748',
type: 'dashed'
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 10
},
nameTextStyle: {
color: '#a0a8b8',
fontSize: 10
}
},
{
type: 'value',
name: '订单数量(单)',
position: 'right',
splitLine: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#a0a8b8',
fontSize: 10
},
nameTextStyle: {
color: '#a0a8b8',
fontSize: 10
}
}
],
series: [
{
name: '牦牛交易数量',
type: 'line',
yAxisIndex: 0,
data: tradingData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#409EFF',
width: 3
},
itemStyle: {
color: '#409EFF',
borderColor: '#fff',
borderWidth: 2
},
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// { offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
// { offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
// ])
// }
},
{
name: '成交订单数量',
type: 'line',
yAxisIndex: 1,
data: orderData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#67C23A',
width: 3
},
itemStyle: {
color: '#67C23A',
borderColor: '#fff',
borderWidth: 2
},
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// { offset: 0, color: 'rgba(103, 194, 58, 0.2)' },
// { offset: 1, color: 'rgba(103, 194, 58, 0.05)' }
// ])
// }
}
]
}
chartInstance.setOption(option, true)
}
const changePeriod = (period) => {
selectedPeriod.value = period
}
//
watch(selectedPeriod, () => {
updateChart()
})
onMounted(() => {
initChart()
window.addEventListener('resize', () => {
chartInstance?.resize()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
})
</script>
<style scoped>
.time-selector {
padding: 0 20px 15px;
}
.selector-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
.period-btn {
padding: 6px 16px;
border: 1px solid #4a5568;
background: transparent;
color: #a0a8b8;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.period-btn:hover {
border-color: #409EFF;
color: #409EFF;
}
.period-btn.active {
border-color: #409EFF;
background: #409EFF;
color: #fff;
}
.chart {
width: 100%;
height: calc(100% - 60px);
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './styles/index.scss'
createApp(App).mount('#app')

@ -0,0 +1,166 @@
// CSS变量定义
:root {
--screen-width: 5120px;
--screen-height: 1440px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100vh;
overflow: auto;
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
background: $bg-primary;
color: $text-primary;
user-select: none;
margin: 0;
padding: 0;
}
#app {
width: var(--screen-width);
height: var(--screen-height);
min-width: var(--screen-width);
min-height: var(--screen-height);
display: flex;
flex-direction: column;
position: relative;
}
// 固定尺寸布局 - 使用flex布局
.dashboard-container {
width: var(--screen-width);
height: var(--screen-height);
display: flex;
flex-direction: column;
padding: 16px;
background: radial-gradient(ellipse at center, rgba(64, 158, 255, 0.1) 0%, $bg-primary 70%);
overflow: hidden;
box-sizing: border-box;
gap: 16px;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(90deg, transparent 0%, rgba(64, 158, 255, 0.02) 50%, transparent 100%),
linear-gradient(0deg, transparent 0%, rgba(139, 92, 246, 0.02) 50%, transparent 100%);
pointer-events: none;
z-index: 1;
}
}
// 卡片样式现在由BaseCard组件统一管理
// 数据显示样式
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid $border-secondary;
&:last-child {
border-bottom: none;
}
.label {
color: $text-secondary;
font-size: 12px;
}
.value {
color: $text-primary;
font-size: 14px;
font-weight: 600;
}
}
// 标题样式
.main-title {
font-size: 24px;
font-weight: 700;
background: $gradient-primary;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
letter-spacing: 2px;
}
// 状态指示器
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
&.online {
background: $accent-green;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
}
&.offline {
background: $accent-red;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
}
&.warning {
background: $accent-orange;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.6);
}
}
// 动画效果
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.fade-in-up {
animation: fadeInUp 0.6s $animation-easing;
}
.pulse {
animation: pulse 2s infinite;
}
// 响应式适配 - 针对4:1宽高比优化
@media (max-width: 1919px) and (min-width: 1600px) {
.dashboard-container {
padding: 6px;
gap: 6px;
}
}
@media (max-width: 1599px) {
.dashboard-container {
padding: 5px;
gap: 5px;
}
}

@ -0,0 +1,26 @@
// 深色科技风格颜色变量
$bg-primary: #0a0e1a;
$bg-secondary: #1a1f2e;
$bg-card: rgba(26, 31, 46, 0.8);
$bg-glass: rgba(255, 255, 255, 0.05);
$text-primary: #ffffff;
$text-secondary: #a0a8b8;
$text-muted: #6b7280;
$border-primary: rgba(64, 158, 255, 0.3);
$border-secondary: rgba(255, 255, 255, 0.1);
$accent-blue: #409eff;
$accent-cyan: #00d4aa;
$accent-purple: #8b5cf6;
$accent-orange: #f59e0b;
$accent-red: #ef4444;
$accent-green: #10b981;
$gradient-primary: linear-gradient(135deg, rgba(64, 158, 255, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
$gradient-secondary: linear-gradient(135deg, rgba(0, 212, 170, 0.2) 0%, rgba(16, 185, 129, 0.2) 100%);
// 动画变量
$animation-duration: 0.3s;
$animation-easing: cubic-bezier(0.4, 0, 0.2, 1);

@ -0,0 +1,59 @@
// 配置管理工具
export class ConfigManager {
constructor() {
this.config = null;
}
// 异步加载配置文件
async loadConfig() {
try {
const response = await fetch('/config.json');
if (!response.ok) {
throw new Error('Failed to load config');
}
this.config = await response.json();
return this.config;
} catch (error) {
console.error('Error loading config:', error);
// 返回默认配置
return this.getDefaultConfig();
}
}
// 获取默认配置
getDefaultConfig() {
return {
screen: {
width: 5120,
height: 1440,
title: "智慧活畜交易大数据中心"
},
dashboard: {
refreshInterval: 5000,
animationDuration: 300
}
};
}
// 获取屏幕配置
getScreenConfig() {
return this.config?.screen || this.getDefaultConfig().screen;
}
// 获取仪表板配置
getDashboardConfig() {
return this.config?.dashboard || this.getDefaultConfig().dashboard;
}
// 设置CSS变量
setCSSVariables() {
const screenConfig = this.getScreenConfig();
const root = document.documentElement;
root.style.setProperty('--screen-width', `${screenConfig.width}px`);
root.style.setProperty('--screen-height', `${screenConfig.height}px`);
}
}
// 创建全局配置管理器实例
export const configManager = new ConfigManager();

@ -0,0 +1,164 @@
/**
* 数据管理器 - 用于加载和管理JSON配置数据
*/
class DataManager {
constructor() {
this.data = null
this.loadPromise = null
}
/**
* 加载数据配置文件
*/
async loadData() {
if (this.loadPromise) {
return this.loadPromise
}
this.loadPromise = this._fetchData()
return this.loadPromise
}
async _fetchData() {
try {
console.log('正在加载数据配置文件: /data-config.json')
const response = await fetch('/data-config.json')
console.log('响应状态:', response.status, response.statusText)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`)
}
const text = await response.text()
console.log('响应内容长度:', text.length)
this.data = JSON.parse(text)
console.log('数据配置加载成功:', this.data)
console.log('realTimeStats数据:', this.data.realTimeStats)
return this.data
} catch (error) {
console.error('加载数据配置失败:', error)
console.error('错误详情:', error.message)
console.log('使用默认数据')
// 返回默认数据
this.data = this.getDefaultData()
return this.data
}
}
/**
* 获取指定模块的数据
*/
getData(moduleName) {
if (!this.data) {
console.warn('数据尚未加载,请先调用 loadData()')
return null
}
return this.data[moduleName] || null
}
/**
* 获取所有数据
*/
getAllData() {
return this.data
}
/**
* 重新加载数据用于热更新
*/
async reloadData() {
this.loadPromise = null
this.data = null
return await this.loadData()
}
/**
* 默认数据配置当加载失败时使用
*/
getDefaultData() {
return {
realTimeStats: {
yakTotalVolume: 0,
orderTotalVolume: 0,
sellerCount: 0,
buyerCount: 0,
systemStatus: "系统异常"
},
yakTradingData: {
totalCount: 0,
avgPrice: 0,
monthlyGrowth: 0,
topRegion: "暂无数据"
},
countySalesStats: [],
yakPriceTrend: {
months: [],
prices: [],
volumes: []
},
chinaMapData: [],
exchangeMonitor: {
onlineUsers: 0,
activeTransactions: 0,
systemLoad: 0,
networkStatus: "未知"
},
yakSalesTypeStats: [],
purchaserAnalysis: [],
marketEnvironment: {
temperature: "--",
humidity: "--",
airQuality: "--",
weather: "--"
},
marketRealtime: {
currentPrice: 0,
priceChange: "0%",
volume: 0,
volumeChange: "0%",
lastUpdate: "--"
},
supplyDemandData: {
supply: { total: 0, available: 0, reserved: 0 },
demand: { total: 0, pending: 0, matched: 0 },
ratio: 0
},
transactionDetails: [],
announcements: ["暂无公告"]
}
}
/**
* 格式化数字显示
*/
formatNumber(num, decimals = 0) {
if (typeof num !== 'number') return num
return num.toLocaleString('zh-CN', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
}
/**
* 格式化百分比
*/
formatPercentage(num, decimals = 1) {
if (typeof num !== 'number') return num
return `${num.toFixed(decimals)}%`
}
/**
* 格式化金额
*/
formatCurrency(amount, unit = '万元') {
if (typeof amount !== 'number') return amount
return `${this.formatNumber(amount, 1)}${unit}`
}
}
// 创建单例实例
export const dataManager = new DataManager()
// 导出类供其他地方使用
export default DataManager

@ -0,0 +1,93 @@
/**
* 数据更新工具 - 用于在浏览器中手动更新数据
*/
import { dataManager } from './dataManager.js'
class DataUpdater {
constructor() {
// 将工具挂载到全局对象,方便在控制台使用
if (typeof window !== 'undefined') {
window.dataUpdater = this
window.dataManager = dataManager
}
}
/**
* 重新加载所有数据
*/
async reloadAllData() {
try {
console.log('正在重新加载数据...')
await dataManager.reloadData()
console.log('数据重新加载完成')
// 触发页面刷新事件
window.dispatchEvent(new CustomEvent('dataReloaded'))
return true
} catch (error) {
console.error('重新加载数据失败:', error)
return false
}
}
/**
* 获取当前数据
*/
getCurrentData() {
return dataManager.getAllData()
}
/**
* 获取指定模块数据
*/
getModuleData(moduleName) {
return dataManager.getData(moduleName)
}
/**
* 显示所有可用的数据模块
*/
showAvailableModules() {
const data = this.getCurrentData()
if (data) {
console.log('可用的数据模块:')
Object.keys(data).forEach(key => {
console.log(`- ${key}:`, typeof data[key])
})
} else {
console.log('暂无数据')
}
}
/**
* 显示使用帮助
*/
help() {
console.log(`
数据更新工具使用说明:
1. 重新加载数据:
dataUpdater.reloadAllData()
2. 查看当前数据:
dataUpdater.getCurrentData()
3. 查看指定模块数据:
dataUpdater.getModuleData('realTimeStats')
4. 显示所有模块:
dataUpdater.showAvailableModules()
5. 显示帮助:
dataUpdater.help()
注意: 修改 data-config.json 文件后使用 reloadAllData() 重新加载数据
`)
}
}
// 创建实例
export const dataUpdater = new DataUpdater()
export default DataUpdater

@ -0,0 +1,23 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
open: true
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
},
resolve: {
alias: {
'@': '/src'
}
}
})
Loading…
Cancel
Save