feat: 前端账权剥离完成 — 移除业务模块,保留账权 + 系统配置 + AI/LLM

- 删除 82 个业务页面(fei/trace/weight/tracing/irrigation/huoban 等 18 目录)
- 删除 30+ 个业务 API 文件
- 删除业务组件(brain/tracing/MarkdownContent 原版)
- 删除业务 Utils(ezviz/tracing/huoban/markdown/wangEdit)
- 修改 8 个框架文件(router/main.js/request.js/store/vue.config.js/package.json/index.html/.gitignore)
- 修复 9 个残留引用(TopHeader/iframeView/menuApi/permissionApi/home/login/customer/uiConfig)
- 新建简化版 MarkdownContent.vue(使用 marked + highlight.js)
- 保留 monaco-editor 依赖(AI 工具编辑器需要)
- 添加 README.md
- npm run build:prod 验证通过
This commit is contained in:
gitea-publish
2026-08-09 12:36:09 +08:00
parent 10bea07624
commit 8ebd1ca157
176 changed files with 220 additions and 60091 deletions
+1 -6
View File
@@ -1,8 +1,7 @@
.DS_Store
src/config/ezviz.local.js
node_modules/
dist/
saas-web/
authon-web/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -17,7 +16,3 @@ tests/**/coverage/
*.ntvs*
*.njsproj
*.sln
.idea
/dist/
/manager/
manager.zip
+105
View File
@@ -0,0 +1,105 @@
# apes-Authon-Web
> 纯净账权管理前端 — 从 saas-web 单体应用中剥离的账权模块前端,与后端 [apes-Authon](https://gitea.apescale.com/figmar/apes-Authon) 配合使用。
## 技术栈
- Vue 2.7 + Element UI 2.15
- Vuex 3 + Vue Router 3
- Axios + SCSS
- Monaco Editor (AI 工具编辑器)
## 功能模块
| 模块 | 页面 | 说明 |
|------|------|------|
| 登录注册 | login, register | 短信登录 + 密码登录 + SSO |
| 用户管理 | user/ | 账号列表、编辑、信息、改密、SSO 配置 |
| 客户管理 | customer/ | 客户列表、详情、编辑、员工、套餐、配额 |
| 权限管理 | permission/ | 菜单管理、权限项、角色管理 |
| 应用管理 | app/ | 应用列表、详情 |
| 系统配置 | system/ | 组织架构、操作日志、配额、套餐方案 |
| AI/LLM | system/llmModel/, system/llmAgent/, system/aiHelper/, ai/ | 模型配置、Agent 管理、工具管理、对话、日志 |
| AccessToken | customer/accessToken | API 令牌管理 |
## 项目结构
```
apes-Authon-Web/
├── docs/ # 方案文档
├── public/ # 静态资源
├── src/
│ ├── api/ # API 接口(19 个)
│ ├── assets/ # 图片资源
│ ├── components/ # 公共组件(含 AI 助手)
│ ├── layout/ # 布局组件(侧栏、顶栏、标签页)
│ ├── router/ # 路由配置
│ ├── store/ # Vuex 状态管理
│ ├── styles/ # 全局样式
│ ├── utils/ # 工具函数
│ └── views/ # 页面
│ ├── login/ # 登录
│ ├── register/ # 注册
│ ├── user/ # 用户管理
│ ├── permission/ # 权限管理
│ ├── customer/ # 客户管理
│ ├── app/ # 应用管理
│ ├── system/ # 系统配置 + AI/LLM 配置
│ ├── ai/ # AI 工具管理
│ ├── home/ # 首页看板
│ └── 404.vue # 404 页面
├── package.json
├── vue.config.js
└── README.md
```
## 开发
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build:prod
```
## 与后端对应关系
| 后端 (apes-Authon) | 前端 (apes-Authon-Web) |
|---|---|
| UserController | views/user/ |
| PermissionController | views/permission/ |
| CustomerController | views/customer/ |
| AppController | views/app/ |
| AccessTokenController | views/customer/accessToken.vue |
| SSOController | views/user/sso.vue, views/login/ |
| CustomerPackageController | views/customer/packages.vue |
| SysOrganizationController | views/system/organization/ |
| SysOperationLogController | views/system/operationlog/ |
| SysQuotaController | views/system/quota/ |
| SysPackagePlanController | views/system/packagePlan/ |
| LlmModelConfigController | views/system/llmModel/ |
| LlmAgentConfigController | views/system/llmAgent/ |
| AiController | views/system/aiHelper/ |
| LlmToolController | views/ai/tool.vue |
| LlmChatController | views/ai/chat.vue |
| LlmConversationController | views/ai/conversationHistory.vue |
## 剥离说明
本项目从 saas-web 单体前端中剥离,移除了以下业务模块:
- 智慧农业大屏 (fei)
- 溯源管理 (trace, tracing)
- 自助过磅 (weight)
- 灌溉管理 (irrigation)
- 伙伴云表单 (huoban) — 后续 Phase 5C 代办
- 其他业务页面 (brain, certify, payment, park 等)
详细方案见 `docs/apes-Authon-Web-剥离方案.md`
## License
MIT
-618
View File
@@ -1,618 +0,0 @@
# AI Helper 表单集成文档
## 概述
AI Helper 是一个可全局集成的 AI 对话组件,支持两种展示模式:
- **图标模式(icon)**:悬浮图标 + 可拖拽弹框,默认展示在页面右下角
- **输入框模式(input)**:将对话输入框直接嵌入到页面指定位置,无弹框、无关闭按钮
两种模式均支持文本输入、图片上传、视频上传,输入框会根据内容自动调整高度。
---
## 快速开始
### 1. 引入组件
```vue
<template>
<div>
<!-- 你的页面内容 -->
<!-- 在页面底部引入 AI Helper -->
<ai-helper :agent_id="agentId" />
</div>
</template>
<script>
import AiHelper from '@/components/ai/ai_helper.vue'
export default {
components: { AiHelper },
data() {
return {
agentId: '1' // Agent ID
}
}
}
</script>
```
---
## Props 参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| agent_id | String/Number | 是 | - | Agent 配置 ID,用于加载对应的 Agent 配置 |
| user_prompt | String | 否 | '' | 用户提示词,提供上下文数据,调用大模型时作为额外上下文传递 |
| display | String | 否 | 'icon' | 展示模式:`icon`(悬浮图标+弹框)/ `input`(嵌入式输入框) |
| input_title | String | 否 | '' | 输入框标题,显示在输入框上方,用于说明输入内容的用途 |
| default_input | String | 否 | '' | 默认的用户输入文案,组件初始化时自动填充到输入框中 |
---
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| result | Object | 大模型返回结果时触发 |
### result 事件数据结构
```javascript
{
content: '返回的文本内容', // String - 原始返回内容
tokens: { // Object - Token 统计
total: 150,
prompt: 100,
completion: 50
},
contentObject: {}, // Object - JSON 格式返回内容(仅 outputType 为 json 时)
agentId: '1', // String/Number - 当前 Agent ID
timestamp: '2026-06-26T17:00:00.000Z' // String - 时间戳
}
```
---
## 展示模式说明
### 模式 1:图标模式(display="icon"
默认模式。页面右下角显示一个悬浮图标,点击后弹出可拖拽的对话弹框。
- 悬浮图标可拖拽移动
- 弹框可拖拽移动,带标题栏和关闭按钮
- 弹框内包含统一输入框(文本 + 上传)
```vue
<!-- 默认就是 icon 模式无需显式指定 -->
<ai-helper agent_id="1" />
<!-- 等价于 -->
<ai-helper agent_id="1" display="icon" />
```
### 模式 2:输入框模式(display="input"
将对话输入框直接嵌入到页面中组件放置的位置,没有悬浮图标、没有弹框、没有关闭按钮。
- 组件挂载后自动加载 Agent 信息(无需点击)
- 统一输入框直接显示在页面中
- 文本输入区域根据内容自动增高(最小 3 行,最大 200px 后滚动)
- 支持图片/视频上传图标(根据 Agent 配置自动显示)
```vue
<ai-helper agent_id="1" display="input" />
```
### 统一输入框结构
两种模式共用同一个输入框设计,将文本输入、图片上传、视频上传整合为一个区域:
```
┌─────────────────────────────────┐
│ [已上传的图片/视频预览] │
│ │
│ 请输入内容... │
│ │
│ 📷 🎬 [发送] │
└─────────────────────────────────┘
```
- 文本区域:自动高度,最小 3 行(66px),最大 200px 后出现滚动条
- 左下角:图片上传 📷 和视频上传 🎬 图标按钮(根据 Agent 配置显示)
- 右下角:发送按钮
- 已上传文件在输入框顶部以缩略图形式预览,支持点击删除
---
## 基础集成示例
### 示例 1:最简集成(图标模式)
```vue
<template>
<page-box>
<div>你的页面内容</div>
<ai-helper agent_id="1" />
</page-box>
</template>
<script>
import AiHelper from '@/components/ai/ai_helper.vue'
import PageBox from '@/components/pageBox.vue'
export default {
components: { AiHelper, PageBox }
}
</script>
```
### 示例 2:监听返回结果
```vue
<template>
<page-box>
<div>你的页面内容</div>
<!-- 监听 result 事件 -->
<ai-helper
agent_id="1"
@result="onAiResult"
/>
<!-- 展示结果 -->
<div v-if="aiResult">
<h3>AI 分析结果</h3>
<p>{{ aiResult.content }}</p>
</div>
</page-box>
</template>
<script>
import AiHelper from '@/components/ai/ai_helper.vue'
import PageBox from '@/components/pageBox.vue'
export default {
components: { AiHelper, PageBox },
data() {
return {
aiResult: null
}
},
methods: {
onAiResult(data) {
this.aiResult = data
console.log('AI 返回:', data.content)
console.log('Token 统计:', data.tokens)
}
}
}
</script>
```
### 示例 3:传递上下文数据
```vue
<template>
<page-box>
<!-- 表单内容 -->
<el-form :model="form">
<el-form-item label="姓名">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" />
</el-form-item>
</el-form>
<!-- 传递表单数据作为上下文 -->
<ai-helper
agent_id="1"
:user_prompt="contextData"
@result="onAiResult"
/>
</page-box>
</template>
<script>
import AiHelper from '@/components/ai/ai_helper.vue'
import PageBox from '@/components/pageBox.vue'
export default {
components: { AiHelper, PageBox },
data() {
return {
form: {
name: '',
phone: ''
}
}
},
computed: {
// 将表单数据转为上下文字符串
contextData() {
return `当前表单数据:
姓名:${this.form.name}
电话:${this.form.phone}`
}
},
methods: {
onAiResult(data) {
// 处理 AI 返回结果
if (data.contentObject) {
// JSON 格式结果
console.log('JSON 结果:', data.contentObject)
} else {
// 文本格式结果
console.log('文本结果:', data.content)
}
}
}
}
</script>
```
### 示例 4:输入框模式嵌入表单
将 AI 输入框直接嵌入到表单页面中,作为页面的一部分,而非悬浮弹框:
```vue
<template>
<page-box title="智能分析">
<!-- 表单内容 -->
<el-form :model="form" label-width="100px">
<el-form-item label="客户名称">
<el-input v-model="form.customerName" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.phone" />
</el-form-item>
</el-form>
<!-- AI 输入框直接嵌入页面 -->
<div style="margin-top: 20px;">
<ai-helper
agent_id="5"
display="input"
input_title="智能分析助手"
default_input="请帮我分析这个客户的基本信息"
:user_prompt="aiContext"
@result="handleAiResult"
/>
</div>
<!-- AI 结果展示 -->
<el-card v-if="aiResult" style="margin-top: 20px">
<div slot="header">AI 分析结果</div>
<pre>{{ aiResult.content }}</pre>
</el-card>
</page-box>
</template>
<script>
import AiHelper from '@/components/ai/ai_helper.vue'
import PageBox from '@/components/pageBox.vue'
export default {
components: { AiHelper, PageBox },
data() {
return {
form: {
customerName: '',
phone: ''
},
aiResult: null
}
},
computed: {
aiContext() {
return `请根据以下客户信息进行分析:
客户名称:${this.form.customerName}
联系电话:${this.form.phone}`
}
},
methods: {
handleAiResult(data) {
this.aiResult = data
}
}
}
</script>
```
### 示例 5:表单场景完整集成(图标模式)
```vue
<template>
<page-box title="客户信息录入">
<el-form :model="form" label-width="100px">
<el-form-item label="客户名称">
<el-input v-model="form.customerName" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" type="textarea" />
</el-form-item>
</el-form>
<!-- AI Helper 集成 -->
<ai-helper
agent_id="5"
:user_prompt="aiContext"
@result="handleAiResult"
/>
<!-- AI 结果展示区域 -->
<el-card v-if="aiResult" style="margin-top: 20px">
<div slot="header">AI 分析结果</div>
<div v-if="aiResult.contentObject">
<p>建议标签{{ aiResult.contentObject.tags }}</p>
<p>建议分类{{ aiResult.contentObject.category }}</p>
</div>
<div v-else>
<pre>{{ aiResult.content }}</pre>
</div>
</el-card>
</page-box>
</template>
<script>
import AiHelper from '@/components/ai/ai_helper.vue'
import PageBox from '@/components/pageBox.vue'
export default {
components: { AiHelper, PageBox },
data() {
return {
form: {
customerName: '',
phone: '',
address: ''
},
aiResult: null
}
},
computed: {
aiContext() {
return `请根据以下客户信息进行分析:
客户名称:${this.form.customerName}
联系电话:${this.form.phone}
地址:${this.form.address}
请以 JSON 格式返回分析结果,包含 tags(标签数组)和 category(分类)。`
}
},
methods: {
handleAiResult(data) {
this.aiResult = data
}
}
}
</script>
```
### 示例 6:使用输入框标题和默认文案
在 input 模式下,可以使用 `input_title``default_input` 参数来优化用户体验:
```vue
<template>
<page-box title="施肥方案管理">
<el-form :model="form" label-width="100px">
<el-form-item label="作物">
<el-select v-model="form.cropId" placeholder="请选择作物">
<el-option v-for="crop in cropList" :key="crop.id" :label="crop.name" :value="crop.id" />
</el-select>
</el-form-item>
<el-form-item label="物候期">
<el-select v-model="form.phenologyId" placeholder="请选择物候期">
<el-option v-for="p in phenologyList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
</el-form>
<!-- AI 输入框带标题和默认文案 -->
<div style="margin-top: 20px;">
<ai-helper
agent_id="4"
display="input"
input_title="智能施肥方案生成"
default_input="请根据选择的作物和物候期,制定科学的施肥方案"
:user_prompt="aiContext"
@result="handleAiResult"
/>
</div>
</page-box>
</template>
<script>
import AiHelper from '@/components/ai/ai_helper.vue'
import PageBox from '@/components/pageBox.vue'
export default {
components: { AiHelper, PageBox },
data() {
return {
form: {
cropId: null,
phenologyId: null
},
cropList: [],
phenologyList: []
}
},
computed: {
aiContext() {
const crop = this.cropList.find(c => c.id === this.form.cropId)
const phenology = this.phenologyList.find(p => p.id === this.form.phenologyId)
return `当前选择的作物:${crop ? crop.name : '未选择'}
当前物候期:${phenology ? phenology.name : '未选择'}
请根据以上信息制定施肥方案,返回 JSON 格式:
{
"formulaName": "配方名称",
"applyType": "施肥方式",
"totalAmount": 总用量,
"details": [...]
}`
}
},
methods: {
handleAiResult(data) {
if (data.contentObject) {
// 回填表单
console.log('AI 返回的施肥方案:', data.contentObject)
}
}
}
}
</script>
```
**参数说明:**
- `input_title="智能施肥方案生成"`:在输入框上方显示标题,帮助用户理解该输入框的用途
- `default_input="请根据选择的作物和物候期,制定科学的施肥方案"`:组件初始化时自动填充到输入框,用户可以在此基础上修改或直接发送
---
## 组件功能说明
| 功能 | 说明 |
|------|------|
| 悬浮图标 | icon 模式下显示在页面右下角,可拖拽移动 |
| 对话弹框 | icon 模式下点击图标打开,可拖拽移动,无遮罩层 |
| 嵌入式输入框 | input 模式下直接嵌入页面,无弹框无关闭按钮 |
| 统一输入框 | 文本输入 + 图片上传 + 视频上传整合为一个输入区域 |
| 自动高度 | 文本输入区域根据内容自动增高,最小 3 行,最大 200px |
| 图片上传 | 根据 Agent 配置 `supportImage` 自动显示上传图标 |
| 视频上传 | 根据 Agent 配置 `supportVideo` 自动显示上传图标 |
| 文件预览 | 已上传的图片/视频在输入框顶部以缩略图预览,支持删除 |
| 结果输出 | 调用成功后通过 `@result` 事件抛出 |
---
## 表单快速填充场景
### 场景说明
在业务系统中,很多表单需要用户手动填写大量字段。通过集成 AI Helper(Agent ID = 4),可以实现表单快速填充:
1. 用户通过 AI 对话描述需求(如"帮我填一个客户信息,张三,电话 13800138000"
2. AI 分析用户输入,返回 JSON 格式的表单数据
3. 前端监听 `@result` 事件,将返回的 JSON 数据自动回填到表单中
### 接入步骤
#### 步骤 1:整理表单字段信息
整理目标表单的所有字段,包含以下信息:
| 属性名 | 类型 | 含义 |
|--------|------|------|
| 字段名 | 数据类型 | 字段说明 |
例如:
| 属性名 | 类型 | 含义 |
|--------|------|------|
| plateNumber | String | 车牌号 |
| vehicleType | String | 车型 |
| driverId | Long | 司机ID |
#### 步骤 2:构建 user_prompt
将字段信息告诉 AI,让它知道需要返回什么格式:
```javascript
computed: {
aiPrompt() {
return `请根据用户描述填充表单,返回 JSON 格式:
{
"plateNumber": "车牌号",
"vehicleType": "车型",
"driverId": 司机ID
}
只返回 JSON,不要其他内容。`
}
}
```
#### 步骤 3:集成组件并处理回调
```vue
<template>
<page-box>
<el-form :model="form">
<!-- 你的表单字段 -->
</el-form>
<ai-helper
agent_id="4"
:user_prompt="aiPrompt"
@result="handleAiResult"
/>
</page-box>
</template>
<script>
export default {
computed: {
aiPrompt() {
return `根据用户描述填充表单,返回 JSON:
{
"plateNumber": "车牌号",
"vehicleType": "车型",
"driverId": 司机ID
}
只返回 JSON。`
}
},
methods: {
handleAiResult(data) {
if (data.contentObject) {
// 回填表单
Object.keys(data.contentObject).forEach(key => {
if (this.form.hasOwnProperty(key)) {
this.$set(this.form, key, data.contentObject[key])
}
})
this.$message.success('已填充')
}
}
}
}
</script>
```
### 关键点
1. **Agent ID = 4**:表单快速填充固定使用
2. **字段说明要清晰**:告诉 AI 字段名、类型、含义
3. **返回 JSON 格式**Agent 的 outputType 必须配置为 json
4. **自动回填**:根据返回的 JSON key 匹配表单字段名
---
## 注意事项
1. **Agent ID 必须有效**:确保传入的 `agent_id` 在后端 `llm_agent_config` 表中存在且状态为启用
2. **user_prompt 为可选**:不传则仅使用 Agent 配置的系统提示词和用户提示词模板
3. **结果格式**:根据 Agent 的 `outputType` 配置,`contentObject` 仅在 `outputType=json` 时有值
4. **组件位置**
- icon 模式:建议放在页面模板的最外层或 `page-box` 内部,确保悬浮层级正确
- input 模式:放在表单或内容区域中需要嵌入输入框的位置即可
5. **Token 统计**:每次调用会返回 Token 消耗统计,可用于成本监控
6. **display 模式选择**
- icon 模式适合全局辅助工具,用户按需打开
- input 模式适合 AI 驱动的业务页面,输入框作为页面核心功能的一部分
---
## 前置条件
- 需要在后台 Agent 管理中配置好对应的 Agent(系统提示词、模型等)
- 确保 `/static/ai_helper.png``/static/ai_helper_bai.png` 图标文件存在
-40
View File
@@ -1,40 +0,0 @@
# Agent 调用文档
## 调用 Agent
```javascript
import llmAgentApi from '@/api/llmAgentApi'
llmAgentApi.callAgent(agentId, {
userParams: {
input: '用户输入内容',
imageUrls: ['https://example.com/img1.jpg'], // 可选,图片 URL 列表
videoUrls: ['https://example.com/video1.mp4'] // 可选,视频 URL 列表
}
})
```
### 返回
```javascript
{
code: 1,
data: {
content: '模型输出内容',
tokens: { prompt: 100, completion: 200, total: 300 }
}
}
```
---
## 图片/视频上传
先通过公共接口上传文件获取 URL
```javascript
// 上传接口:POST /api/user-service/common/upload
// 返回:{ code: 1, data: 'https://oss.example.com/file.jpg' }
```
再将 URL 放入 `imageUrls``videoUrls` 数组传给 `callAgent`
-83
View File
@@ -1,83 +0,0 @@
# 前端代理配置说明
## 当前 Vue 开发环境代理配置
```javascript
// vue.config.js
devServer: {
proxy: {
'/api/user-service': {
target: 'https://cloud.zhangquyun.com/api/user-service',
changeOrigin: true,
pathRewrite: {
'^/api/user-service': '/'
}
},
'/n8n': {
target: 'https://n8n.ccoop.cc',
changeOrigin: true,
pathRewrite: {
'^/n8n': ''
}
}
}
}
```
## Nginx 代理配置
```nginx
server {
listen 80;
server_name your-domain.com;
# 前端静态资源
root /path/to/saas-web/dist;
index index.html;
# API 代理 - 用户服务
location /api/user-service/ {
proxy_pass https://cloud.zhangquyun.com/;
proxy_set_header Host cloud.zhangquyun.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSL 相关
proxy_ssl_server_name on;
proxy_ssl_name cloud.zhangquyun.com;
}
# n8n 工作流代理
location /n8n/ {
proxy_pass https://n8n.ccoop.cc/;
proxy_set_header Host n8n.ccoop.cc;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSL 相关
proxy_ssl_server_name on;
proxy_ssl_name n8n.ccoop.cc;
}
# 前端路由 - SPA 模式
location / {
try_files $uri $uri/ /index.html;
}
}
```
## 代理规则说明
| 前端请求路径 | 代理目标 | 重写后路径 |
|------------|---------|-----------|
| `/api/user-service/customer/getCurrentUser` | `https://cloud.zhangquyun.com` | `/customer/getCurrentUser` |
| `/n8n/api/v1/workflows` | `https://n8n.ccoop.cc` | `/api/v1/workflows` |
## 注意事项
1. **路径重写**Vue 的 `pathRewrite: { '^/api/user-service': '/' }` 会把 `/api/user-service/xxx` 变成 `/xxx`
2. **SSL 代理**:目标地址是 HTTPS,需要配置 `proxy_ssl_server_name on`
3. **Host 头**:必须设置正确的 Host 头,否则后端可能拒绝请求
4. **CORS**:如果前端和 API 不在同一域名,需要配置 CORS 或使用代理
-202
View File
@@ -1,202 +0,0 @@
# 电磁阀管理接口文档
## 接口前缀
`/api/user-service/fei/solenoid/valve`
## 1. 分页查询电磁阀列表
**请求方式**: POST
**请求路径**: `/page`
### 请求参数
```json
{
"pageIndex": 1, // 页码(必填)
"pageSize": 10, // 每页数量(必填)
"valveName": "电磁阀A", // 阀门名称(模糊查询,可选)
"valveCode": "SV-001", // 阀门编号(精确查询,可选)
"parkId": 1, // 园区ID(可选)
"plotId": 1, // 地块ID(可选)
"valveStatus": 0, // 阀门状态 0-关闭 1-开启(可选)
"onlineStatus": 1 // 在线状态 0-离线 1-在线(可选)
}
```
### 响应示例
```json
{
"code": 1,
"msg": "ok",
"data": {
"records": [
{
"id": 1,
"valveName": "电磁阀A01",
"valveCode": "SV-2026-001",
"parkId": 1,
"plotId": 1,
"longitude": 108.365432,
"latitude": 22.842156,
"installLocation": "A区1号地块入口",
"valveStatus": 0,
"onlineStatus": 1,
"lastActionTime": null,
"remark": "测试数据1",
"parkName": "示例园区",
"plotName": "A3",
"createTime": "2026-07-07 20:52:25",
"updateTime": "2026-07-07 20:52:25",
"isDel": 0
}
],
"total": 2,
"size": 10,
"current": 1,
"pages": 1
}
}
```
## 2. 查询电磁阀详情
**请求方式**: POST
**请求路径**: `/detail`
### 请求参数
```json
{
"id": 1 // 电磁阀ID(必填)
}
```
### 响应示例
```json
{
"code": 1,
"msg": "ok",
"data": {
"id": 1,
"valveName": "电磁阀A01",
"valveCode": "SV-2026-001",
"parkId": 1,
"plotId": 1,
"longitude": 108.365432,
"latitude": 22.842156,
"installLocation": "A区1号地块入口",
"valveStatus": 0,
"onlineStatus": 1,
"lastActionTime": null,
"remark": "测试数据1",
"parkName": "示例园区",
"plotName": "A3",
"createTime": "2026-07-07 20:52:25",
"updateTime": "2026-07-07 20:52:25",
"isDel": 0
}
}
```
## 3. 新增电磁阀
**请求方式**: POST
**请求路径**: `/add`
### 请求参数
```json
{
"valveName": "电磁阀A01", // 阀门名称(必填)
"valveCode": "SV-2026-001", // 阀门编号(必填,唯一)
"parkId": 1, // 园区ID(必填)
"plotId": 1, // 地块ID(可选)
"longitude": 108.365432, // 经度(可选)
"latitude": 22.842156, // 纬度(可选)
"installLocation": "A区1号地块入口", // 安装位置描述(可选)
"valveStatus": 0, // 阀门状态 0-关闭 1-开启(可选,默认0)
"onlineStatus": 1, // 在线状态 0-离线 1-在线(可选,默认0)
"remark": "备注信息" // 备注(可选)
}
```
### 响应示例
```json
{
"code": 1,
"msg": "ok",
"data": null
}
```
## 4. 更新电磁阀
**请求方式**: POST
**请求路径**: `/update`
### 请求参数
```json
{
"id": 1, // 电磁阀ID(必填)
"valveName": "电磁阀A01-修改", // 阀门名称(可选)
"valveCode": "SV-2026-001", // 阀门编号(可选,唯一)
"parkId": 1, // 园区ID(可选)
"plotId": 1, // 地块ID(可选)
"longitude": 108.365500, // 经度(可选)
"latitude": 22.842200, // 纬度(可选)
"installLocation": "新位置", // 安装位置描述(可选)
"valveStatus": 1, // 阀门状态 0-关闭 1-开启(可选)
"onlineStatus": 1, // 在线状态 0-离线 1-在线(可选)
"remark": "更新后的备注" // 备注(可选)
}
```
### 响应示例
```json
{
"code": 1,
"msg": "ok",
"data": null
}
```
## 5. 删除电磁阀
**请求方式**: POST
**请求路径**: `/delete`
### 请求参数
```json
{
"id": 1 // 电磁阀ID(必填)
}
```
### 响应示例
```json
{
"code": 1,
"msg": "ok",
"data": null
}
```
## 字段说明
### 电磁阀字段
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | Long | 主键ID |
| valveName | String | 阀门名称 |
| valveCode | String | 阀门编号(唯一) |
| parkId | Long | 园区ID |
| plotId | Long | 地块ID |
| longitude | BigDecimal | 经度 |
| latitude | BigDecimal | 纬度 |
| installLocation | String | 安装位置描述 |
| valveStatus | Integer | 阀门状态 0-关闭 1-开启 |
| onlineStatus | Integer | 在线状态 0-离线 1-在线 |
| lastActionTime | Date | 最后操作时间 |
| remark | String | 备注 |
| parkName | String | 园区名称(非数据库字段,用于展示) |
| plotName | String | 地块名称(非数据库字段,用于展示) |
| createTime | Date | 创建时间 |
| updateTime | Date | 更新时间 |
| isDel | Integer | 逻辑删除 0-正常 1-删除 |
-32
View File
@@ -1,32 +0,0 @@
-- 电磁阀数据批量插入
-- 生成时间: 2026-07-07 21:13
-- 数据来源: 设备接口返回的26个电磁阀
-- 说明: did 为控制器设备ID(全部相同), valve_code 使用 did-index 组合保证唯一
INSERT INTO fei_solenoid_valve (valve_name, valve_code, park_id, plot_id, longitude, latitude, install_location, valve_status, online_status, last_action_time, remark, is_del, create_time, update_time) VALUES
('电磁阀-1', '864865086276673-1', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-2', '864865086276673-2', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-3', '864865086276673-3', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-4', '864865086276673-4', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-5', '864865086276673-5', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-6', '864865086276673-6', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-7', '864865086276673-7', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-8', '864865086276673-8', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-9', '864865086276673-9', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-10', '864865086276673-10', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-11', '864865086276673-11', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-12', '864865086276673-12', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-13', '864865086276673-13', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-14', '864865086276673-14', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-15', '864865086276673-15', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-16', '864865086276673-16', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-17', '864865086276673-17', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-18', '864865086276673-18', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-19', '864865086276673-19', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-20', '864865086276673-20', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-21', '864865086276673-21', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-22', '864865086276673-22', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-23', '864865086276673-23', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-24', '864865086276673-24', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-25', '864865086276673-25', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW()),
('电磁阀-26', '864865086276673-26', 1, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 0, NOW(), NOW());
-23
View File
@@ -1,23 +0,0 @@
-- 电磁阀灌溉记录表
CREATE TABLE `fei_solenoid_valve_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`valve_id` bigint(20) NOT NULL COMMENT '电磁阀ID',
`valve_code` varchar(64) DEFAULT NULL COMMENT '阀门编号',
`park_id` bigint(20) DEFAULT NULL COMMENT '所属园区ID',
`plot_id` bigint(20) DEFAULT NULL COMMENT '所属地块ID',
`flow_rate` decimal(10,2) DEFAULT NULL COMMENT '流量(立方米/小时)',
`start_time` datetime NOT NULL COMMENT '开始灌溉时间',
`end_time` datetime DEFAULT NULL COMMENT '结束灌溉时间',
`duration_minutes` int(11) DEFAULT NULL COMMENT '灌溉时长(分钟)',
`total_water` decimal(10,2) DEFAULT NULL COMMENT '总灌溉量(立方米)',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0-未关阀 1-已关阀',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_del` tinyint(1) DEFAULT '0' COMMENT '逻辑删除 0-正常 1-删除',
PRIMARY KEY (`id`),
KEY `idx_valve_id` (`valve_id`),
KEY `idx_park_id` (`park_id`),
KEY `idx_plot_id` (`plot_id`),
KEY `idx_status` (`status`),
KEY `idx_start_time` (`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电磁阀灌溉记录表';
+3 -14
View File
@@ -1,8 +1,8 @@
{
"name": "saas-web",
"name": "apes-Authon-Web",
"version": "1.0.0",
"description": "",
"author": "zhangqu group",
"description": "apes-Authon 纯净账权管理前端",
"author": "apescale",
"scripts": {
"dev": "vue-cli-service serve ",
"build:prod": "vue-cli-service build",
@@ -10,27 +10,16 @@
"preview": "node build/index.js --preview"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.2",
"animate.css": "^4.1.1",
"axios": "0.18.1",
"core-js": "3.6.5",
"echarts": "^5.4.1",
"element-ui": "^2.15.13",
"fix": "0.0.6",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"marked": "^4.3.0",
"monaco-editor": "^0.30.1",
"monaco-editor-webpack-plugin": "^6.0.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"qrcodejs2": "^0.0.2",
"them": "0.0.4",
"to": "^0.2.9",
"vue": "^2.7.14",
"vue-monaco": "^1.1.0",
"vue-router": "^3.5.2",
+2 -10
View File
@@ -6,11 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
<title><%= webpackConfig.name %></title>
<title>apes-Authon</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
<strong>apes-Authon doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script src="https://unpkg.com/vue@2.7.14/dist/vue.min.js"></script>
@@ -18,14 +18,6 @@
<script src="https://unpkg.com/vuex@3.1.3/dist/vuex.min.js"></script>
<script src="https://unpkg.com/axios@0.18.1/dist/axios.min.js"></script>
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
<script src="https://unpkg.com/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
// Monaco Editor CDN (AMD loader)
var require = { paths: { 'vs': 'https://unpkg.com/monaco-editor@0.30.1/min/vs' } };
</script>
<script src="https://unpkg.com/monaco-editor@0.30.1/min/vs/loader.js"></script>
<script src="https://unpkg.com/monaco-editor@0.30.1/min/vs/editor/editor.main.nls.js"></script>
<script src="https://unpkg.com/monaco-editor@0.30.1/min/vs/editor/editor.main.js"></script>
<!-- built files will be auto injected -->
</body>
</html>
-56
View File
@@ -1,56 +0,0 @@
#!/bin/bash
set -e
export NODE_HOME=/data/tools/node-16
export PATH=$NODE_HOME/bin:$PATH
cd /data/saas-web || exit
# ========== 更新最新代码(先检查冲突,无冲突才更新) ==========
echo "检查最新代码更新..."
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if ! git fetch origin "$BRANCH"; then
echo "拉取远程分支失败,终止部署"
exit 1
fi
LOCAL_HEAD=$(git rev-parse HEAD)
REMOTE_HEAD=$(git rev-parse "origin/$BRANCH")
if [ "$LOCAL_HEAD" = "$REMOTE_HEAD" ]; then
echo "本地代码已是最新,无需更新"
else
echo "本地与远程不一致,先进行冲突检测..."
if git merge --no-commit --no-ff "origin/$BRANCH" > /tmp/git_merge_check.log 2>&1; then
git merge --abort >/dev/null 2>&1 || true
echo "无冲突,开始更新代码..."
if ! git pull origin "$BRANCH"; then
echo "代码更新失败,终止部署"
exit 1
fi
echo "代码更新完成"
else
echo "!!! 检测到冲突(或本地未提交修改阻止更新),详情:"
cat /tmp/git_merge_check.log
CONFLICT_FILES=$(git diff --name-only --diff-filter=U 2>/dev/null)
if [ -n "$CONFLICT_FILES" ]; then
echo "冲突文件列表:"
echo "$CONFLICT_FILES"
fi
git merge --abort >/dev/null 2>&1 || true
echo "存在冲突,终止更新并退出部署"
exit 1
fi
fi
echo '开始安装依赖'
/data/tools/node-16/bin/npm --registry=http://registry.npmmirror.com install
echo '开始构建'
/data/tools/node-16/bin/npm --registry=http://registry.npmmirror.com run build:prod
zip -r saas-web.zip saas-web
echo '开始传输..'
scp -P 22 -o "ProxyCommand ssh -W %h:%p root@47.115.213.168" saas-web.zip root@192.168.1.158:/data/services/saas-web.zip
echo "开始替换..."
ssh -p 22 -o "ProxyCommand ssh -W %h:%p root@47.115.213.168" root@192.168.1.158 "cd /data/services; unzip -o saas-web.zip"
echo "上线成功...."
rm -rf saas-web
rm -rf saas-web.zip
echo "上线完毕...."
-40
View File
@@ -1,40 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service'
export default {
// 分页查询
getPage(params) {
return request.apiGet(`${BASE_URL}/cameraInfo/page`, params)
},
// 查询详情
getDetail(id) {
return request.apiGet(`${BASE_URL}/cameraInfo/detail`, { id })
},
// 新增
add(data) {
return request.apiPost(`${BASE_URL}/cameraInfo/add`, data)
},
// 编辑
update(data) {
return request.apiPost(`${BASE_URL}/cameraInfo/update`, data)
},
// 删除
delete(id) {
return request.apiPost(`${BASE_URL}/cameraInfo/delete?id=${id}`)
},
// 抓图
capture(id) {
return request.apiPost(`${BASE_URL}/cameraInfo/capture?id=${id}`)
},
// 查询抓拍记录
getCaptureLogs(cameraId, pageIndex, pageSize) {
return request.apiGet(`/api/user-service/cameraCaptureLog/page`, { cameraId, pageIndex, pageSize })
}
}
-26
View File
@@ -1,26 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service/extLink'
export default {
// 分页查询
getPage(params) {
return request.apiGet(`${BASE_URL}/page`, params)
},
// 查询详情
getDetail(id) {
return request.apiGet(`${BASE_URL}/detail`, { id })
},
// 新增
add(data) {
return request.apiPost(`${BASE_URL}/add`, data)
},
// 编辑
update(data) {
return request.apiPost(`${BASE_URL}/update`, data)
},
// 删除
delete(id) {
return request.apiPost(`${BASE_URL}/delete?id=${id}`)
}
}
-47
View File
@@ -1,47 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service/farm-plan'
export default {
// 分页查询
getPage(params) {
return request.apiGet(`${BASE_URL}/page`, params)
},
// 查询详情
getDetail(id) {
return request.apiGet(`${BASE_URL}/detail`, { id })
},
// 新增
add(data) {
return request.apiPost(`${BASE_URL}/add`, data)
},
// 编辑
update(data) {
return request.apiPost(`${BASE_URL}/update`, data)
},
// 删除
delete(id) {
return request.apiPost(`${BASE_URL}/delete`, null, { params: { id } })
},
// 查询今日生效的农事计划
getToday(parkId) {
return request.apiGet(`${BASE_URL}/today`, { parkId })
},
// 更新农事计划状态
updateStatus(id, status) {
return request.apiPost(`${BASE_URL}/updateStatus?id=${id}&status=${status}`)
},
// 按月查询农事计划(日历视图)
getMonth(year, month, parkId) {
const params = { year, month }
if (parkId) params.parkId = parkId
return request.apiGet(`${BASE_URL}/month`, params)
}
}
-39
View File
@@ -1,39 +0,0 @@
import request from '@/utils/request'
class cropApi {
// 分页查询作物列表
static page(pageIndex = 1, pageSize = 20, name, categoryId, status) {
let url = `/api/user-service/fei/crop/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (name) url += '&name=' + name
if (categoryId) url += '&categoryId=' + categoryId
if (status !== null && status !== undefined) url += '&status=' + status
return request.apiPost(url)
}
// 获取作物详情
static detail(id) {
return request.apiGet(`/api/user-service/fei/crop/detail?id=${id}`)
}
// 新增作物
static add(data) {
return request.apiPost('/api/user-service/fei/crop/add', data)
}
// 修改作物
static update(data) {
return request.apiPost('/api/user-service/fei/crop/update', data)
}
// 删除作物
static delete(id) {
return request.apiPost(`/api/user-service/fei/crop/delete?id=${id}`)
}
// 获取所有启用作物(下拉选择用)
static list() {
return request.apiGet('/api/user-service/fei/crop/list')
}
}
export default cropApi
-33
View File
@@ -1,33 +0,0 @@
import request from '@/utils/request'
class cropPhenologyApi {
static page(pageIndex = 1, pageSize = 20, cropId, phenologyName, status) {
let url = `/api/user-service/fei/crop/phenology/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (cropId) url += `&cropId=${cropId}`
if (phenologyName) url += `&phenologyName=${encodeURIComponent(phenologyName)}`
if (status !== undefined && status !== null) url += `&status=${status}`
return request.apiPost(url)
}
static listByCropId(cropId) {
return request.apiGet(`/api/user-service/fei/crop/phenology/list?cropId=${cropId}`)
}
static detail(id) {
return request.apiGet(`/api/user-service/fei/crop/phenology/detail?id=${id}`)
}
static add(data) {
return request.apiPost('/api/user-service/fei/crop/phenology/add', data)
}
static update(data) {
return request.apiPost('/api/user-service/fei/crop/phenology/update', data)
}
static delete(id) {
return request.apiPost(`/api/user-service/fei/crop/phenology/delete?id=${id}`)
}
}
export default cropPhenologyApi
-60
View File
@@ -1,60 +0,0 @@
import request from '@/utils/request'
export default {
/**
* 分页查询数据上报列表
*/
page(pageIndex, pageSize, params) {
const data = { pageIndex, pageSize, ...params }
return request.apiPost('/api/user-service/fei/device/data/report/page', data)
},
/**
* 查询数据上报详情
*/
detail(id) {
return request.apiPost('/api/user-service/fei/device/data/report/detail', { id })
},
/**
* 新增数据上报记录
*/
add(data) {
return request.apiPost('/api/user-service/fei/device/data/report/add', data)
},
/**
* 更新数据上报记录
*/
update(data) {
return request.apiPost('/api/user-service/fei/device/data/report/update', data)
},
/**
* 删除数据上报记录
*/
delete(id) {
return request.apiPost('/api/user-service/fei/device/data/report/delete', { id })
},
/**
* 根据设备ID查询数据列表(图表展示)
*/
chartData(deviceId, limit) {
return request.apiPost('/api/user-service/fei/device/data/report/chart', { deviceId, limit })
},
/**
* 批量查询设备最新上报数据
*/
getLatest(deviceIds) {
return request.apiGet('/api/user-service/fei/device/data/report/latest', { deviceIds })
},
/**
* 生成模拟数据
*/
simulate(data) {
return request.apiPost('/api/user-service/fei/device/data/report/simulate', data)
}
}
-45
View File
@@ -1,45 +0,0 @@
import request from '@/utils/request'
class fertilizationFormulaApi {
// 分页查询
static page(pageIndex = 1, pageSize = 20, cropId, phenologyId, formulaName, status) {
let url = `/api/user-service/fei/fertilization/formula/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (cropId) url += `&cropId=${cropId}`
if (phenologyId) url += `&phenologyId=${phenologyId}`
if (formulaName) url += `&formulaName=${encodeURIComponent(formulaName)}`
if (status !== undefined && status !== null) url += `&status=${status}`
return request.apiPost(url)
}
// 查询详情(含明细)
static detail(id) {
return request.apiGet(`/api/user-service/fei/fertilization/formula/detail?id=${id}`)
}
// 根据配方ID查询明细列表
static details(formulaId) {
return request.apiGet(`/api/user-service/fei/fertilization/formula/details?formulaId=${formulaId}`)
}
// 新增(含明细)
static add(data) {
return request.apiPost('/api/user-service/fei/fertilization/formula/add', data)
}
// 编辑(含明细)
static update(data) {
return request.apiPost('/api/user-service/fei/fertilization/formula/update', data)
}
// 删除
static delete(id) {
return request.apiPost(`/api/user-service/fei/fertilization/formula/delete?id=${id}`)
}
// 根据作物和物候期查询配方列表
static listByCropAndPhenology(cropId, phenologyId) {
return request.apiGet(`/api/user-service/fei/fertilization/formula/list?cropId=${cropId}&phenologyId=${phenologyId}`)
}
}
export default fertilizationFormulaApi
-36
View File
@@ -1,36 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service'
class InputMaterialStockLogApi {
// 分页查询
static getPage(params) {
return request.apiGet(`${BASE_URL}/input-material-stock-log/page`, params)
}
// 查询详情
static getDetail(id) {
return request.apiGet(`${BASE_URL}/input-material-stock-log/detail`, { id })
}
// 新增
static add(data) {
return request.apiPost(`${BASE_URL}/input-material-stock-log/add`, data)
}
// 删除
static delete(id) {
return request.apiPost(`${BASE_URL}/input-material-stock-log/delete`, null, { params: { id } })
}
// 审核
static review(params) {
const queryString = Object.keys(params)
.filter(key => params[key] !== null && params[key] !== undefined && params[key] !== '')
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&')
return request.apiPost(`${BASE_URL}/input-material-stock-log/review?${queryString}`, null)
}
}
export default InputMaterialStockLogApi
-70
View File
@@ -1,70 +0,0 @@
import request from '@/utils/request'
/**
* 商情检测设备 API
*/
export default {
/**
* 分页查询设备列表
* @param {Object} params 查询参数
* @param {number} params.pageIndex 页码
* @param {number} params.pageSize 每页数量
* @param {string} params.deviceName 设备名称(模糊查询)
* @param {string} params.deviceCode 设备编号
* @param {number} params.deviceType 设备类型 1-五层设备 2-三层设备
* @param {string} params.installLocation 安装位置(模糊查询)
* @param {number} params.onlineStatus 设备状态 0-离线 1-在线
*/
page(pageIndex, pageSize, deviceName, deviceCode, deviceType, installLocation, onlineStatus) {
const params = { pageIndex, pageSize }
if (deviceName) params.deviceName = deviceName
if (deviceCode) params.deviceCode = deviceCode
if (deviceType !== null && deviceType !== undefined) params.deviceType = deviceType
if (installLocation) params.installLocation = installLocation
if (onlineStatus !== null && onlineStatus !== undefined) params.onlineStatus = onlineStatus
return request.apiPost('/api/user-service/fei/market/device/page', params)
},
/**
* 查询设备详情
* @param {number} id 设备ID
*/
detail(id) {
return request.apiPost('/api/user-service/fei/market/device/detail', { id })
},
/**
* 新增设备
* @param {Object} data 设备数据
* @param {string} data.deviceName 设备名称
* @param {string} data.deviceCode 设备编号
* @param {number} data.deviceType 设备类型 1-五层设备 2-三层设备
* @param {string} data.installLocation 安装位置
* @param {number} data.onlineStatus 设备状态 0-离线 1-在线
*/
add(data) {
return request.apiPost('/api/user-service/fei/market/device/add', data)
},
/**
* 更新设备
* @param {Object} data 设备数据
* @param {number} data.id 设备ID
* @param {string} data.deviceName 设备名称
* @param {string} data.deviceCode 设备编号
* @param {number} data.deviceType 设备类型 1-五层设备 2-三层设备
* @param {string} data.installLocation 安装位置
* @param {number} data.onlineStatus 设备状态 0-离线 1-在线
*/
update(data) {
return request.apiPost('/api/user-service/fei/market/device/update', data)
},
/**
* 删除设备
* @param {number} id 设备ID
*/
delete(id) {
return request.apiPost('/api/user-service/fei/market/device/delete', { id })
}
}
-81
View File
@@ -1,81 +0,0 @@
import request from '@/utils/request'
/**
* 地块信息 API
*/
export default {
/**
* 分页查询地块列表
* @param {number} pageIndex 页码
* @param {number} pageSize 每页数量
* @param {Object} params 查询参数
* @param {string} params.plotName 地块名称(模糊查询)
* @param {string} params.plotCode 地块编号
* @param {string} params.location 所在地方(模糊查询)
* @param {number} params.plotType 地块类型 1-农田 2-果园 3-林地 4-建设用地 5-其他
* @param {number} params.status 地块状态 0-废弃 1-正常 2-使用中
* @param {string} params.owner 负责人(模糊查询)
*/
page(pageIndex, pageSize, params = {}) {
return request.apiPost('/api/user-service/fei/plot/page', {
pageIndex,
pageSize,
...params
})
},
/**
* 查询地块详情
* @param {number} id 地块ID
*/
detail(id) {
return request.apiPost('/api/user-service/fei/plot/detail', { id })
},
/**
* 新增地块
* @param {Object} data 地块数据
* @param {string} data.plotName 地块名称
* @param {string} data.plotCode 地块编号
* @param {number} data.area 面积(亩)
* @param {string} data.location 所在地方
* @param {string} data.coordinates 地块坐标点(JSON格式)
* @param {number} data.plotType 地块类型
* @param {string} data.soilType 土壤类型
* @param {number} data.status 地块状态
* @param {string} data.owner 负责人
* @param {string} data.contactPhone 联系电话
* @param {string} data.remark 备注
*/
add(data) {
return request.apiPost('/api/user-service/fei/plot/add', data)
},
/**
* 更新地块
* @param {Object} data 地块数据
* @param {number} data.id 地块ID
* @param {string} data.plotName 地块名称
* @param {string} data.plotCode 地块编号
* @param {number} data.area 面积
* @param {string} data.location 所在地方
* @param {string} data.coordinates 地块坐标点
* @param {number} data.plotType 地块类型
* @param {string} data.soilType 土壤类型
* @param {number} data.status 地块状态
* @param {string} data.owner 负责人
* @param {string} data.contactPhone 联系电话
* @param {string} data.remark 备注
*/
update(data) {
return request.apiPost('/api/user-service/fei/plot/update', data)
},
/**
* 删除地块
* @param {number} id 地块ID
*/
delete(id) {
return request.apiPost('/api/user-service/fei/plot/delete', { id })
}
}
-46
View File
@@ -1,46 +0,0 @@
import request from '@/utils/request'
/**
* 设备维修记录 API
*/
export default {
/**
* 分页查询维修记录列表
* @param {Object} params 查询参数
*/
page(params) {
return request.apiPost('/api/user-service/fei/repair/record/page', params)
},
/**
* 查询维修记录详情
* @param {number} id 记录ID
*/
detail(id) {
return request.apiPost('/api/user-service/fei/repair/record/detail', { id })
},
/**
* 新增维修记录
* @param {Object} data 维修记录数据
*/
add(data) {
return request.apiPost('/api/user-service/fei/repair/record/add', data)
},
/**
* 更新维修记录
* @param {Object} data 维修记录数据
*/
update(data) {
return request.apiPost('/api/user-service/fei/repair/record/update', data)
},
/**
* 删除维修记录
* @param {number} id 记录ID
*/
delete(id) {
return request.apiPost('/api/user-service/fei/repair/record/delete', { id })
}
}
-81
View File
@@ -1,81 +0,0 @@
import request from '@/utils/request'
export default {
/**
* 分页查询电磁阀列表
*/
page(pageIndex, pageSize, params) {
const data = { pageIndex, pageSize, ...params }
return request.apiPost('/api/user-service/fei/solenoid/valve/page', data)
},
/**
* 查询电磁阀详情
*/
detail(id) {
return request.apiPost('/api/user-service/fei/solenoid/valve/detail', { id })
},
/**
* 新增电磁阀
*/
add(data) {
return request.apiPost('/api/user-service/fei/solenoid/valve/add', data)
},
/**
* 更新电磁阀
*/
update(data) {
return request.apiPost('/api/user-service/fei/solenoid/valve/update', data)
},
/**
* 删除电磁阀
*/
delete(id) {
return request.apiPost('/api/user-service/fei/solenoid/valve/delete', { id })
},
/**
* 开阀
*/
openValve(id) {
return request.apiPost('/api/user-service/fei/solenoid/valve/open', { id })
},
/**
* 关阀
*/
closeValve(id) {
return request.apiPost('/api/user-service/fei/solenoid/valve/close', { id })
},
/**
* 灌溉记录分页查询
*/
logPage(params) {
return request.apiGet('/api/user-service/fei/solenoid/valve/log/page', params)
},
/**
* 删除灌溉记录
*/
deleteLog(id) {
return request.apiPost('/api/user-service/fei/solenoid/valve/log/delete', { id })
},
/**
* 查询今日灌溉记录(大屏专用)
*/
getTodayIrrigation(parkId) {
return request.apiGet('/api/user-service/fei/solenoid/valve/log/today', { parkId })
},
/**
* 查询最近30天灌溉趋势(大屏专用)
*/
getIrrigationTrend(parkId) {
return request.apiGet('/api/user-service/fei/solenoid/valve/log/trend', { parkId })
}
}
-55
View File
@@ -1,55 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service'
export default {
// 分页查询设备列表
getPage(params) {
return request.apiGet(`${BASE_URL}/fertigation-device/page`, params)
},
// 查询设备详情
getDetail(id) {
return request.apiGet(`${BASE_URL}/fertigation-device/detail`, { id })
},
// 新增设备
add(data) {
return request.apiPost(`${BASE_URL}/fertigation-device/add`, data)
},
// 编辑设备
update(data) {
return request.apiPost(`${BASE_URL}/fertigation-device/update`, data)
},
// 删除设备
delete(id) {
return request.apiPost(`${BASE_URL}/fertigation-device/delete`, { id })
},
// 加药操作
dosing(data) {
return request.apiPost(`${BASE_URL}/fertigation-device/dosing`, data)
},
// 查询加药日志
getDosingLog(params) {
return request.apiGet(`${BASE_URL}/fertigation-device/dosing-log`, params)
},
// 查询所有加药日志(包含设备和园区信息)
getAllDosingLog(params) {
return request.apiGet(`${BASE_URL}/fertigation-device/dosing-log/all`, params)
},
// 查询园区当天操作记录
getTodayLogs(parkId, limit = 10) {
return request.apiGet(`${BASE_URL}/fertigation-device/today-logs`, { parkId, limit })
},
// 保存设备页面布局配置(JSON)
saveLayout(data) {
return request.apiPost(`${BASE_URL}/fertigation-device/save-layout`, data)
}
}
-126
View File
@@ -1,126 +0,0 @@
import request from '@/utils/request'
class huobanTableApi {
// Table Info CRUD
async pageTableInfo(pageIndex, pageSize, name, tableId, spaceId) {
let url = `/api/user-service/huoban/tableInfo/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (name) url += '&name=' + encodeURIComponent(name)
if (tableId) url += '&tableId=' + tableId
if (spaceId) url += '&spaceId=' + encodeURIComponent(spaceId)
return await request.apiPost(url)
}
async saveTableInfo(data) {
return await request.apiPost('/api/user-service/huoban/tableInfo/save', data)
}
async deleteTableInfo(tableId) {
return await request.apiPost(`/api/user-service/huoban/tableInfo/delete?tableId=${tableId}`)
}
async listAllTableInfo() {
return await request.apiGet('/api/user-service/huoban/tableInfo/listAll')
}
// Table Column CRUD
async pageTableColumn(pageIndex, pageSize, tableId, name) {
let url = `/api/user-service/huoban/tableColumn/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (tableId) url += '&tableId=' + tableId
if (name) url += '&name=' + name
return await request.apiPost(url)
}
async saveTableColumn(data) {
return await request.apiPost('/api/user-service/huoban/tableColumn/save', data)
}
async deleteTableColumn(fieldId) {
return await request.apiPost(`/api/user-service/huoban/tableColumn/delete?fieldId=${fieldId}`)
}
async listTableColumnByTable(tableId) {
return await request.apiGet(`/api/user-service/huoban/tableColumn/listByTable?tableId=${tableId}`)
}
async spaceList() {
return await request.apiGet('/api/user-service/huoban2/spaceList')
}
async syncTables(spaceId) {
let url = '/api/user-service/huoban2/syncTables'
if (spaceId) url += '?spaceId=' + spaceId
return await request.apiGet(url)
}
async syncTableConfig(tableId) {
return await request.apiGet('/api/user-service/huoban2/syncTableConfig?tableId=' + tableId)
}
// Huoban data query (POST /huoban2/data)
async queryData(dataFilter) {
return await request.apiPost('/api/user-service/huoban2/data', dataFilter)
}
// ========== Huoban Web API (带 token) ==========
async webLogin(phone) {
return await request.apiGet(`/api/user-service/huoban2/webLogin?phone=${encodeURIComponent(phone)}`)
}
async webData(dataFilter) {
return await request.apiPost('/api/user-service/huoban2/webData', dataFilter)
}
async detail(itemId, tableId, tableName = '', relations = []) {
let url = `/api/user-service/huoban2/detail?itemId=${itemId}&tableId=${tableId}`
if (tableName) url += `&tableName=${encodeURIComponent(tableName)}`
if (relations && relations.length) url += `&relations=${encodeURIComponent(JSON.stringify(relations))}`
return await request.apiGet(url)
}
async webDetail({ itemId, tableId, tableName, relations }) {
return await request.apiGet('/api/user-service/huoban2/webDetail', { itemId, tableId, tableName, relations })
}
// ========== Huoban Partner Cloud Data Operations ==========
async create(data) {
return await request.apiPost('/api/user-service/huoban2/create', data)
}
async update(data) {
return await request.apiPost('/api/user-service/huoban2/update', data)
}
async upsetOne(data) {
return await request.apiPost('/api/user-service/huoban2/upsetOne', data)
}
// ========== Huoban Form CRUD ==========
async pageHuobanForm(pageIndex, pageSize, name, tableId, spaceId) {
let url = `/api/user-service/huoban/form/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (name) url += '&name=' + name
if (tableId) url += '&tableId=' + tableId
if (spaceId) url += '&spaceId=' + spaceId
return await request.apiPost(url)
}
async listAllHuobanForm() {
return await request.apiGet('/api/user-service/huoban/form/listAll')
}
async saveHuobanForm(data) {
return await request.apiPost('/api/user-service/huoban/form/save', data)
}
async deleteHuobanForm(id) {
return await request.apiPost(`/api/user-service/huoban/form/delete?id=${id}`)
}
async detailHuobanForm(id) {
return await request.apiGet(`/api/user-service/huoban/form/detail?id=${id}`)
}
}
const target = new huobanTableApi()
export default target
-36
View File
@@ -1,36 +0,0 @@
import request from '@/utils/request'
// 接口前缀:/api/user-service(通过代理已处理,前端直接添加即可)
const BASE_URL = '/api/user-service'
export default {
// 分页查询
getPage(params) {
return request.apiGet(`${BASE_URL}/input-material/page`, params)
},
// 查询详情
getDetail(id) {
return request.apiGet(`${BASE_URL}/input-material/detail`, { id })
},
// 新增
add(data) {
return request.apiPost(`${BASE_URL}/input-material/add`, data)
},
// 编辑
update(data) {
return request.apiPost(`${BASE_URL}/input-material/update`, data)
},
// 删除
delete(id) {
return request.apiPost(`${BASE_URL}/input-material/delete`, null, { params: { id } })
},
// 查询在用的投入品列表
getActiveList() {
return request.apiGet(`${BASE_URL}/input-material/active-list`)
}
}
-69
View File
@@ -1,69 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service/inspection'
export default {
// ========== 巡查项目配置 ==========
// 分页查询巡查项目
getItemPage(params) {
return request.apiGet(`${BASE_URL}/item/page`, params)
},
// 查询巡查项目详情
getItemDetail(id) {
return request.apiGet(`${BASE_URL}/item/detail`, { id })
},
// 新增巡查项目
addItem(data) {
return request.apiPost(`${BASE_URL}/item/add`, data)
},
// 编辑巡查项目
updateItem(data) {
return request.apiPost(`${BASE_URL}/item/update`, data)
},
// 删除巡查项目
deleteItem(id) {
return request.apiPost(`${BASE_URL}/item/delete`, null, { params: { id } })
},
// 获取启用的巡查项目列表
listItemEnabled() {
return request.apiGet(`${BASE_URL}/item/listEnabled`)
},
// ========== 巡查记录 ==========
// 分页查询巡查记录
getRecordPage(params) {
return request.apiGet(`${BASE_URL}/record/page`, params)
},
// 查询巡查记录详情
getRecordDetail(id) {
return request.apiGet(`${BASE_URL}/record/detail`, { id })
},
// 新增巡查记录
addRecord(data) {
return request.apiPost(`${BASE_URL}/record/add`, data)
},
// 编辑巡查记录
updateRecord(data) {
return request.apiPost(`${BASE_URL}/record/update`, data)
},
// 删除巡查记录
deleteRecord(id) {
return request.apiPost(`${BASE_URL}/record/delete`, null, { params: { id } })
},
// 确认巡查记录
confirmRecord(id, confirmBy) {
return request.apiPost(`${BASE_URL}/record/confirm`, { id, confirmBy })
}
}
-88
View File
@@ -1,88 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service'
export default {
// ==================== 灌溉计划管理 ====================
// 分页查询灌溉计划列表
getPlanPage(params) {
return request.apiGet(`${BASE_URL}/irrigation/plan/page`, params)
},
// 查询灌溉计划详情
getPlanDetail(id) {
return request.apiGet(`${BASE_URL}/irrigation/plan/detail`, { id })
},
// 新增灌溉计划
addPlan(data) {
return request.apiPost(`${BASE_URL}/irrigation/plan/add`, data)
},
// 修改灌溉计划
updatePlan(data) {
return request.apiPost(`${BASE_URL}/irrigation/plan/update`, data)
},
// 删除灌溉计划
deletePlan(id) {
return request.apiPost(`${BASE_URL}/irrigation/plan/delete?id=${id}`)
},
// 启用/暂停灌溉计划
togglePlan(id, status) {
return request.apiPost(`${BASE_URL}/irrigation/plan/toggle`, null, { params: { id, status } })
},
// 根据园区ID查询灌溉计划列表
listPlanByPark(parkId) {
return request.apiGet(`${BASE_URL}/irrigation/plan/listByPark`, { parkId })
},
// ==================== 执行记录管理 ====================
// 分页查询执行记录列表
getExecPage(params) {
return request.apiGet(`${BASE_URL}/irrigation/exec/page`, params)
},
// 查询执行记录详情(含阀门明细)
getExecDetail(id) {
return request.apiGet(`${BASE_URL}/irrigation/exec/detail`, { id })
},
// 创建执行记录
createExec(data) {
return request.apiPost(`${BASE_URL}/irrigation/exec/create`, data)
},
// 更新执行状态
updateExecStatus(id, status, failReason) {
return request.apiPost(`${BASE_URL}/irrigation/exec/updateStatus`, null, {
params: { id, status, failReason }
})
},
// 完成执行记录
completeExec(id, actualWater, actualDuration) {
return request.apiPost(`${BASE_URL}/irrigation/exec/complete`, null, {
params: { id, actualWater, actualDuration }
})
},
// 取消执行记录
cancelExec(id) {
return request.apiPost(`${BASE_URL}/irrigation/exec/cancel`, null, { params: { id } })
},
// 根据计划ID查询执行记录列表
listExecByPlan(planId) {
return request.apiGet(`${BASE_URL}/irrigation/exec/listByPlan`, { planId })
},
// 查询待执行的记录列表
listPendingExec() {
return request.apiGet(`${BASE_URL}/irrigation/exec/pending`)
}
}
+7 -36
View File
@@ -6,56 +6,27 @@ import store from '@/store/index'
const viewsRequire = require.context('@/views', true, /\.vue$/)
const viewPathMap = {}
viewsRequire.keys().forEach(key => {
// key 形如 './system/llmModel/index.vue'
const normalized = key.replace(/^\.\//, '').replace(/\.vue$/, '').toLowerCase()
viewPathMap[normalized] = key
})
class menuApi {
/**
* 初始化路由(不再加载全局菜单,菜单由各应用切换时按需加载)
*/
async initMenu() {
return router
}
/**
* 注册应用菜单的动态路由
* @param {Array} menuList - 菜单树
*/
handleMenu(menuList) {
menuList.forEach(menu => {
// Use a separate field for route registration so we don't mutate the original menuPath
// which is needed for pathMap matching in main.js and sidebar navigation
if (menu.menuType === 4) {
// menuType 2/3/4: iframe-type menus, use generic iframeView component
if (menu.menuType === 4 || menu.menuType === 3 || menu.menuType === 2) {
menu.frameUrl = menu.menuPath
menu.menuRoutePath = '/common/web/' + menu.id
const typePrefix = menu.menuType === 4 ? 'web' : menu.menuType === 3 ? 'payment' : 'huoban'
menu.menuRoutePath = '/common/' + typePrefix + '/' + menu.id
router.addRoute('common', {
path: 'web/' + menu.id,
name: 'web_' + menu.id,
path: typePrefix + '/' + menu.id,
name: typePrefix + '_' + menu.id,
frameUrl: menu.frameUrl,
component: () => import('@/views/web/index'),
meta: { title: menu.menuTitle, icon: '', keepAlive: true, menu: menu }
})
}
if (menu.menuType === 3) {
menu.frameUrl = menu.menuPath
menu.menuRoutePath = '/common/payment/' + menu.id
router.addRoute('common', {
path: 'payment/' + menu.id,
name: 'payment_' + menu.id,
component: () => import('@/views/payment/index'),
meta: { title: menu.menuTitle, icon: '', keepAlive: true, menu: menu }
})
}
if (menu.menuType === 2) {
menu.frameUrl = menu.menuPath
menu.menuRoutePath = '/common/huoban/' + menu.id
router.addRoute('common', {
path: 'huoban/' + menu.id,
name: 'huoban_' + menu.id,
frameUrl: menu.frameUrl,
component: () => import('@/views/huoban/index'),
component: () => import('@/components/iframeView'),
meta: { title: menu.menuTitle, icon: '', keepAlive: true, menu: menu }
})
}
-16
View File
@@ -1,16 +0,0 @@
import request from '@/utils/request'
class n8nApi {
/**
* 调用 n8n webhook:伙伴表数据同步+填充
* @param {string} spaceId - 空间ID
* @param {string} tableId - 表格ID
*/
async huobanTableSyncAndFill(spaceId, tableId) {
const url = `/n8n/webhook/4dad745d-c71d-4668-81a3-33bf350730ff?spaceId=${encodeURIComponent(spaceId)}&tableId=${encodeURIComponent(tableId)}`
return await request.apiGet(url)
}
}
const target = new n8nApi()
export default target
-36
View File
@@ -1,36 +0,0 @@
import request from '@/utils/request'
// 接口前缀:/api/user-service(通过代理已处理,前端直接添加即可)
const BASE_URL = '/api/user-service'
export default {
// 分页查询园区列表
getPage(params) {
return request.apiGet(`${BASE_URL}/park/page`, params)
},
// 查询园区详情
getDetail(id) {
return request.apiGet(`${BASE_URL}/park/detail`, { id })
},
// 新增园区
add(data) {
return request.apiPost(`${BASE_URL}/park/add`, data)
},
// 编辑园区
update(data) {
return request.apiPost(`${BASE_URL}/park/update`, data)
},
// 删除园区
delete(id) {
return request.apiPost(`${BASE_URL}/park/delete`, { id })
},
// 查询所有园区列表(用于下拉选择)
getAllParks() {
return request.apiGet(`${BASE_URL}/park/list`, {})
}
}
-9
View File
@@ -1,5 +1,4 @@
import request from '@/utils/request'
import { TRACING_OPEN_KEY } from '@/config/tracingOpenKey'
class permissionApi {
@@ -91,14 +90,6 @@ class permissionApi {
return await request.apiGet('/api/user-service/permission/getHuobanToken')
}
/** 开放平台:按企业 ID 查询溯源追踪配置(无需登录,使用 open-key) */
async getTrackingConfigByCustomerId(customerId) {
return await request.apiGet(
'/api/user-service/open/trackingConfig/getByCustomerId',
{ customerId },
{ 'open-key': TRACING_OPEN_KEY }
)
}
}
const target = new permissionApi()
-30
View File
@@ -1,30 +0,0 @@
import request from '@/utils/request'
const BASE_URL = '/api/user-service/trace/floorPlan'
export default {
// 分页查询
getPage(params) {
return request.apiGet(`${BASE_URL}/page`, params)
},
// 查询详情
getDetail(id) {
return request.apiGet(`${BASE_URL}/detail`, { id })
},
// 新增
add(data) {
return request.apiPost(`${BASE_URL}/add`, data)
},
// 更新
update(data) {
return request.apiPost(`${BASE_URL}/update`, data)
},
// 删除
delete(id) {
return request.apiPost(`${BASE_URL}/delete`, { id })
}
}
-627
View File
@@ -1,627 +0,0 @@
import request from '@/utils/request'
class traceabilityApi {
// ========== 品类管理 ==========
static pageCategories(pageIndex = 1, pageSize = 20, categoryName) {
let url = `/api/user-service/trace/category/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (categoryName) url += '&categoryName=' + categoryName
return request.apiPost(url)
}
static getCategoryDetail(id) {
return request.apiGet(`/api/user-service/trace/category/detail?id=${id}`)
}
static addCategory(data) {
return request.apiPost('/api/user-service/trace/category/add', data)
}
static updateCategory(data) {
return request.apiPost('/api/user-service/trace/category/update', data)
}
static deleteCategory(id) {
return request.apiPost(`/api/user-service/trace/category/delete?id=${id}`)
}
static enableCategory(id) {
return request.apiPost(`/api/user-service/trace/category/enable?id=${id}`)
}
static disableCategory(id) {
return request.apiPost(`/api/user-service/trace/category/disable?id=${id}`)
}
static listAllCategories() {
return request.apiGet('/api/user-service/trace/category/listAll')
}
// ========== 产品管理 ==========
static pageProducts(pageIndex = 1, pageSize = 20, categoryId, productName) {
let url = `/api/user-service/trace/category/product/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (categoryId) url += '&categoryId=' + categoryId
if (productName) url += '&productName=' + productName
return request.apiPost(url)
}
static getProductDetail(id) {
return request.apiGet(`/api/user-service/trace/category/product/detail?id=${id}`)
}
static addProduct(data) {
return request.apiPost('/api/user-service/trace/category/product/add', data)
}
static updateProduct(data) {
return request.apiPost('/api/user-service/trace/category/product/update', data)
}
static deleteProduct(id) {
return request.apiPost(`/api/user-service/trace/category/product/delete?id=${id}`)
}
static enableProduct(id) {
return request.apiPost(`/api/user-service/trace/category/product/enable?id=${id}`)
}
static disableProduct(id) {
return request.apiPost(`/api/user-service/trace/category/product/disable?id=${id}`)
}
static listProductsByCategory(categoryId) {
let url = '/api/user-service/trace/category/product/listByCategory'
if (categoryId) url += '?categoryId=' + categoryId
return request.apiGet(url)
}
// ========== 地块管理 ==========
static pageLands(pageIndex = 1, pageSize = 20, landName, status) {
let url = `/api/user-service/trace/land/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (landName) url += '&landName=' + landName
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getLandDetail(id) {
return request.apiGet(`/api/user-service/trace/land/detail?id=${id}`)
}
static addLand(data) {
return request.apiPost('/api/user-service/trace/land/add', data)
}
static updateLand(data) {
return request.apiPost('/api/user-service/trace/land/update', data)
}
static deleteLand(id) {
return request.apiPost(`/api/user-service/trace/land/delete?id=${id}`)
}
static listAllLands() {
return request.apiGet('/api/user-service/trace/land/listAll')
}
// ========== 采收批次 ==========
static pageHarvests(pageIndex = 1, pageSize = 20, batchCode, landId, status) {
let url = `/api/user-service/trace/harvest/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (batchCode) url += '&batchCode=' + batchCode
if (landId) url += '&landId=' + landId
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getHarvestDetail(id) {
return request.apiGet(`/api/user-service/trace/harvest/detail?id=${id}`)
}
static addHarvest(data) {
return request.apiPost('/api/user-service/trace/harvest/add', data)
}
static updateHarvest(data) {
return request.apiPost('/api/user-service/trace/harvest/update', data)
}
static deleteHarvest(id) {
return request.apiPost(`/api/user-service/trace/harvest/delete?id=${id}`)
}
static updateHarvestStatus(id, status) {
return request.apiPost(`/api/user-service/trace/harvest/updateStatus?id=${id}&status=${status}`)
}
// ========== 加工记录 ==========
static pageProcesses(pageIndex = 1, pageSize = 20, processCode, status) {
let url = `/api/user-service/trace/process/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (processCode) url += '&processCode=' + processCode
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getProcessDetail(id) {
return request.apiGet(`/api/user-service/trace/process/detail?id=${id}`)
}
static addProcess(data) {
return request.apiPost('/api/user-service/trace/process/add', data)
}
static updateProcess(data) {
return request.apiPost('/api/user-service/trace/process/update', data)
}
static completeProcess(id) {
return request.apiPost(`/api/user-service/trace/process/complete?id=${id}`)
}
static discardProcess(id) {
return request.apiPost(`/api/user-service/trace/process/discard?id=${id}`)
}
static deleteProcess(id) {
return request.apiPost(`/api/user-service/trace/process/delete?id=${id}`)
}
// ========== 质量检测 ==========
static pageQualities(pageIndex = 1, pageSize = 20, testCode, batchType, status) {
let url = `/api/user-service/trace/quality/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (testCode) url += '&testCode=' + testCode
if (batchType != null) url += '&batchType=' + batchType
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getQualityDetail(id) {
return request.apiGet(`/api/user-service/trace/quality/detail?id=${id}`)
}
static addQuality(data) {
return request.apiPost('/api/user-service/trace/quality/add', data)
}
static updateQuality(data) {
return request.apiPost('/api/user-service/trace/quality/update', data)
}
static deleteQuality(id) {
return request.apiPost(`/api/user-service/trace/quality/delete?id=${id}`)
}
static reviewQuality(id, comprehensiveResult, reviewComment) {
return request.apiPost(`/api/user-service/trace/quality/review?id=${id}&comprehensiveResult=${comprehensiveResult}&reviewComment=${reviewComment || ''}`)
}
// ========== 溯源码生成任务 ==========
static pageCodeTasks(pageIndex = 1, pageSize = 20, batchCode, status) {
let url = `/api/user-service/trace/code/task/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (batchCode) url += '&batchCode=' + batchCode
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getCodeTaskDetail(id) {
return request.apiGet(`/api/user-service/trace/code/task/detail?id=${id}`)
}
static addCodeTask(data) {
return request.apiPost('/api/user-service/trace/code/task/add', data)
}
static generateBatchCode() {
return request.apiPost('/api/user-service/trace/code/task/generateBatchCode')
}
static updateCodeTask(data) {
return request.apiPost('/api/user-service/trace/code/task/update', data)
}
static deleteCodeTask(id) {
return request.apiPost(`/api/user-service/trace/code/task/delete?id=${id}`)
}
static generateCodes(id) {
return request.apiPost(`/api/user-service/trace/code/task/generate?taskId=${id}`)
}
// ========== 溯源码列表 ==========
static pageCodes(pageIndex = 1, pageSize = 20, codeValue, batchCode, status) {
let url = `/api/user-service/trace/code/list/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (codeValue) url += '&codeValue=' + codeValue
if (batchCode) url += '&batchCode=' + batchCode
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getCodeDetail(id) {
return request.apiGet(`/api/user-service/trace/code/list/detail?id=${id}`)
}
static voidCode(id, reason) {
return request.apiPost(`/api/user-service/trace/code/list/void?id=${id}&reason=${reason || ''}`)
}
// ========== 码货绑定 ==========
static pageBinds(pageIndex = 1, pageSize = 20, codeValue, batchCode, packageNo) {
let url = `/api/user-service/trace/code/bind/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (codeValue) url += '&codeValue=' + codeValue
if (batchCode) url += '&batchCode=' + batchCode
if (packageNo) url += '&packageNo=' + packageNo
return request.apiPost(url)
}
static getBindDetail(id) {
return request.apiGet(`/api/user-service/trace/code/bind/detail?id=${id}`)
}
static addBind(data) {
return request.apiPost('/api/user-service/trace/code/bind/add', data)
}
static listUnboundCodes(batchCode) {
return request.apiPost(`/api/user-service/trace/code/bind/unboundList${batchCode ? '?batchCode=' + batchCode : ''}`)
}
static unbind(id) {
return request.apiPost(`/api/user-service/trace/code/bind/unbind?id=${id}`)
}
static batchImportBind(data) {
return request.apiPost('/api/user-service/trace/code/bind/batchImport', data)
}
// ========== 父子码关联 ==========
static pageRelations(pageIndex = 1, pageSize = 20, parentCode, childCode) {
let url = `/api/user-service/trace/code/relation/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (parentCode) url += '&parentCode=' + parentCode
if (childCode) url += '&childCode=' + childCode
return request.apiPost(url)
}
static getRelationDetail(id) {
return request.apiGet(`/api/user-service/trace/code/relation/detail?id=${id}`)
}
static addRelation(data) {
return request.apiPost('/api/user-service/trace/code/relation/add', data)
}
static deleteRelation(id) {
return request.apiPost(`/api/user-service/trace/code/relation/delete?id=${id}`)
}
// ========== 赋码审计 ==========
static pageAudits(pageIndex = 1, pageSize = 20, batchCode, auditType) {
let url = `/api/user-service/trace/code/audit/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (batchCode) url += '&batchCode=' + batchCode
if (auditType != null) url += '&auditType=' + auditType
return request.apiPost(url)
}
static getAuditDetail(id) {
return request.apiGet(`/api/user-service/trace/code/audit/detail?id=${id}`)
}
// ========== 废码异常 ==========
static pageExceptions(pageIndex = 1, pageSize = 20, codeValue, status) {
let url = `/api/user-service/trace/code/exception/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (codeValue) url += '&codeValue=' + codeValue
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getExceptionDetail(id) {
return request.apiGet(`/api/user-service/trace/code/exception/detail?id=${id}`)
}
static addException(data) {
return request.apiPost('/api/user-service/trace/code/exception/add', data)
}
static handleException(id, handleMethod, handleRemark) {
return request.apiPost(`/api/user-service/trace/code/exception/handle?id=${id}&handleMethod=${handleMethod}&handleRemark=${handleRemark || ''}`)
}
static deleteException(id) {
return request.apiPost(`/api/user-service/trace/code/exception/delete?id=${id}`)
}
// ========== 入库管理 ==========
static pageInRecords(pageIndex = 1, pageSize = 20, inCode, inType, warehouse, status) {
let url = `/api/user-service/trace/warehouse/in/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (inCode) url += '&inCode=' + inCode
if (inType != null) url += '&inType=' + inType
if (warehouse) url += '&warehouse=' + warehouse
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getInDetail(id) {
return request.apiGet(`/api/user-service/trace/warehouse/in/detail?id=${id}`)
}
static addInRecord(data) {
return request.apiPost('/api/user-service/trace/warehouse/in/add', data)
}
static updateInRecord(data) {
return request.apiPost('/api/user-service/trace/warehouse/in/update', data)
}
static deleteInRecord(id) {
return request.apiPost(`/api/user-service/trace/warehouse/in/delete?id=${id}`)
}
static confirmIn(id) {
return request.apiPost(`/api/user-service/trace/warehouse/in/confirm?id=${id}`)
}
static generateInBatchCode() {
return request.apiGet('/api/user-service/trace/warehouse/in/generateBatchCode')
}
static listInDetails(inId) {
return request.apiGet(`/api/user-service/trace/warehouse/in/detail/list?inId=${inId}`)
}
static addInDetail(data) {
return request.apiPost('/api/user-service/trace/warehouse/in/detail/add', data)
}
static updateInDetail(data) {
return request.apiPost('/api/user-service/trace/warehouse/in/detail/update', data)
}
static deleteInDetail(id) {
return request.apiPost(`/api/user-service/trace/warehouse/in/detail/delete?id=${id}`)
}
// ========== 出库管理 ==========
static pageOutRecords(pageIndex = 1, pageSize = 20, outCode, outType, destination, status) {
let url = `/api/user-service/trace/warehouse/out/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (outCode) url += '&outCode=' + outCode
if (outType != null) url += '&outType=' + outType
if (destination) url += '&destination=' + destination
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getOutDetail(id) {
return request.apiGet(`/api/user-service/trace/warehouse/out/detail?id=${id}`)
}
static addOutRecord(data) {
return request.apiPost('/api/user-service/trace/warehouse/out/add', data)
}
static updateOutRecord(data) {
return request.apiPost('/api/user-service/trace/warehouse/out/update', data)
}
static deleteOutRecord(id) {
return request.apiPost(`/api/user-service/trace/warehouse/out/delete?id=${id}`)
}
static confirmOut(id) {
return request.apiPost(`/api/user-service/trace/warehouse/out/confirm?id=${id}`)
}
static generateOutBatchCode() {
return request.apiGet('/api/user-service/trace/warehouse/out/generateBatchCode')
}
static listOutDetails(outId) {
return request.apiGet(`/api/user-service/trace/warehouse/out/detail/list?outId=${outId}`)
}
static addOutDetail(data) {
return request.apiPost('/api/user-service/trace/warehouse/out/detail/add', data)
}
static updateOutDetail(data) {
return request.apiPost('/api/user-service/trace/warehouse/out/detail/update', data)
}
static deleteOutDetail(id) {
return request.apiPost(`/api/user-service/trace/warehouse/out/detail/delete?id=${id}`)
}
// ========== 库存管理 ==========
static pageInventory(pageIndex = 1, pageSize = 20, batchCode, productName, warehouse) {
let url = `/api/user-service/trace/warehouse/inventory/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (batchCode) url += '&batchCode=' + batchCode
if (productName) url += '&productName=' + productName
if (warehouse) url += '&warehouse=' + warehouse
return request.apiPost(url)
}
static getInventoryDetail(id) {
return request.apiGet(`/api/user-service/trace/warehouse/inventory/detail?id=${id}`)
}
static adjustInventory(id, adjustQuantity, reason) {
return request.apiPost(`/api/user-service/trace/warehouse/inventory/adjust?id=${id}&adjustQuantity=${adjustQuantity}&reason=${reason || ''}`)
}
static lockInventory(id, lockQty) {
return request.apiPost(`/api/user-service/trace/warehouse/inventory/lock?id=${id}&lockQty=${lockQty}`)
}
static unlockInventory(id, unlockQty) {
return request.apiPost(`/api/user-service/trace/warehouse/inventory/unlock?id=${id}&unlockQty=${unlockQty}`)
}
static listAvailableBatchCodes() {
return request.apiGet('/api/user-service/trace/warehouse/inventory/batchCodes')
}
// ========== 物流管理 ==========
static pageLogistics(pageIndex = 1, pageSize = 20, waybillCode, carrier, vehiclePlate, status) {
let url = `/api/user-service/trace/warehouse/logistics/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (waybillCode) url += '&waybillCode=' + waybillCode
if (carrier) url += '&carrier=' + carrier
if (vehiclePlate) url += '&vehiclePlate=' + vehiclePlate
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getLogisticsDetail(id) {
return request.apiGet(`/api/user-service/trace/warehouse/logistics/detail?id=${id}`)
}
static addLogistics(data) {
return request.apiPost('/api/user-service/trace/warehouse/logistics/add', data)
}
static updateLogistics(data) {
return request.apiPost('/api/user-service/trace/warehouse/logistics/update', data)
}
static deleteLogistics(id) {
return request.apiPost(`/api/user-service/trace/warehouse/logistics/delete?id=${id}`)
}
static updateLogisticsStatus(id, status) {
return request.apiPost(`/api/user-service/trace/warehouse/logistics/updateStatus?id=${id}&status=${status}`)
}
// ========== 合格证 ==========
static pageCertificates(pageIndex = 1, pageSize = 20, certCode, batchCode, productName, status) {
let url = `/api/user-service/trace/common/certificate/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (certCode) url += '&certCode=' + certCode
if (batchCode) url += '&batchCode=' + batchCode
if (productName) url += '&productName=' + productName
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getCertificateDetail(id) {
return request.apiGet(`/api/user-service/trace/common/certificate/detail?id=${id}`)
}
static addCertificate(data) {
return request.apiPost('/api/user-service/trace/common/certificate/add', data)
}
static updateCertificate(data) {
return request.apiPost('/api/user-service/trace/common/certificate/update', data)
}
static deleteCertificate(id) {
return request.apiPost(`/api/user-service/trace/common/certificate/delete?id=${id}`)
}
static generateCertificate(data) {
return request.apiPost('/api/user-service/trace/common/certificate/generate', data)
}
static printCertificate(id) {
return request.apiPost(`/api/user-service/trace/common/certificate/print?id=${id}`)
}
// ========== 资质证书 ==========
static pageQualifications(pageIndex = 1, pageSize = 20, qualName, qualType, status) {
let url = `/api/user-service/trace/common/qualification/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (qualName) url += '&qualName=' + qualName
if (qualType != null) url += '&qualType=' + qualType
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getQualificationDetail(id) {
return request.apiGet(`/api/user-service/trace/common/qualification/detail?id=${id}`)
}
static addQualification(data) {
return request.apiPost('/api/user-service/trace/common/qualification/add', data)
}
static updateQualification(data) {
return request.apiPost('/api/user-service/trace/common/qualification/update', data)
}
static deleteQualification(id) {
return request.apiPost(`/api/user-service/trace/common/qualification/delete?id=${id}`)
}
// ========== 消费者反馈 ==========
static pageFeedbacks(pageIndex = 1, pageSize = 20, feedbackCode, feedbackType, status) {
let url = `/api/user-service/trace/common/feedback/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (feedbackCode) url += '&feedbackCode=' + feedbackCode
if (feedbackType != null) url += '&feedbackType=' + feedbackType
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getFeedbackDetail(id) {
return request.apiGet(`/api/user-service/trace/common/feedback/detail?id=${id}`)
}
static addFeedback(data) {
return request.apiPost('/api/user-service/trace/common/feedback/add', data)
}
static handleFeedback(id, handler, handleOpinion) {
return request.apiPost(`/api/user-service/trace/common/feedback/handle?id=${id}&handler=${handler}&handleOpinion=${handleOpinion || ''}`)
}
static replyFeedback(id, replyContent, replyMethod) {
return request.apiPost(`/api/user-service/trace/common/feedback/reply?id=${id}&replyContent=${replyContent}&replyMethod=${replyMethod || 1}`)
}
static closeFeedback(id) {
return request.apiPost(`/api/user-service/trace/common/feedback/close?id=${id}`)
}
// ========== 召回管理 ==========
static pageRecalls(pageIndex = 1, pageSize = 20, recallCode, batchCode, status) {
let url = `/api/user-service/trace/common/recall/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (recallCode) url += '&recallCode=' + recallCode
if (batchCode) url += '&batchCode=' + batchCode
if (status != null) url += '&status=' + status
return request.apiPost(url)
}
static getRecallDetail(id) {
return request.apiGet(`/api/user-service/trace/common/recall/detail?id=${id}`)
}
static addRecall(data) {
return request.apiPost('/api/user-service/trace/common/recall/add', data)
}
static executeRecall(id) {
return request.apiPost(`/api/user-service/trace/common/recall/execute?id=${id}`)
}
static completeRecall(id) {
return request.apiPost(`/api/user-service/trace/common/recall/complete?id=${id}`)
}
static cancelRecall(id) {
return request.apiPost(`/api/user-service/trace/common/recall/cancel?id=${id}`)
}
}
export default traceabilityApi
-48
View File
@@ -1,48 +0,0 @@
import request from '@/utils/request'
class trackingConfigApi {
/**
* 分页列表
*/
async list(params = {}) {
const { pageNum = 1, pageSize = 20, keyword = '' } = params
return await request.apiGet(`/api/user-service/trackingConfig/list?pageNum=${pageNum}&pageSize=${pageSize}&keyword=${keyword}`)
}
/**
* 根据企业ID获取配置
*/
async getByCustomerId(customerId) {
return await request.apiGet(`/api/user-service/trackingConfig/getByCustomerId?customerId=${customerId}`)
}
/**
* 获取当前登录企业的配置
*/
async getCurrent() {
return await request.apiGet('/api/user-service/trackingConfig/current')
}
/**
* 新增或更新
*/
async save(params) {
return await request.apiPost('/api/user-service/trackingConfig/save', params)
}
/**
* 删除
*/
async delete(id) {
return await request.apiPost('/api/user-service/trackingConfig/delete', { id })
}
/**
* 获取企业下拉列表
*/
async listCustomers(keyword = '') {
return await request.apiGet(`/api/user-service/trackingConfig/customers?keyword=${keyword}`)
}
}
export default new trackingConfigApi()
-31
View File
@@ -1,31 +0,0 @@
import request from '@/utils/request'
// 用户个性化配置接口(后端 UserPreferenceController
const BASE_URL = '/api/user-service/userPreference'
export default {
// 查询当前登录用户的个性化配置(未配置时后端返回默认值 uiSize=2)
mine() {
return request.apiPost(`${BASE_URL}/mine`)
},
// 保存当前登录用户的个性化配置(不存在则新增,已存在则更新)
save(data) {
return request.apiPost(`${BASE_URL}/save`, data)
},
// 查询指定用户的个性化配置(userId 走 URL 参数)
get(userId) {
return request.apiPost(`${BASE_URL}/get?userId=${userId}`)
},
// 新增用户个性化配置
add(data) {
return request.apiPost(`${BASE_URL}/add`, data)
},
// 更新用户个性化配置
update(data) {
return request.apiPost(`${BASE_URL}/update`, data)
},
// 删除用户个性化配置(userId 走 URL 参数)
delete(userId) {
return request.apiPost(`${BASE_URL}/delete?userId=${userId}`)
}
}
-282
View File
@@ -1,282 +0,0 @@
import request from '@/utils/request'
class weightApi {
// ========== 车辆管理 ==========
async pageVehicleRecords(pageIndex, pageSize, plateNumber) {
let url = `/api/user-service/weight/vehicle/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (plateNumber) url += '&plateNumber=' + plateNumber
return await request.apiPost(url)
}
async vehicleEntry(data) {
return await request.apiPost('/api/user-service/weight/vehicle/entry', data)
}
async updateVehicleRecord(data) {
return await request.apiPost('/api/user-service/weight/vehicle/update', data)
}
async listAllVehicles(plateNumber) {
let url = '/api/user-service/weight/vehicle/listAll'
if (plateNumber) url += '?plateNumber=' + plateNumber
return await request.apiGet(url)
}
// ========== 磅单 ==========
async pageTickets(pageIndex, pageSize, plateNumber, ticketNo) {
let url = `/api/user-service/weight/ticket/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (plateNumber) url += '&plateNumber=' + plateNumber
if (ticketNo) url += '&ticketNo=' + ticketNo
return await request.apiPost(url)
}
async firstWeigh(data) {
return await request.apiPost('/api/user-service/weight/ticket/firstWeigh', data)
}
async secondWeigh(data) {
return await request.apiPost('/api/user-service/weight/ticket/secondWeigh', data)
}
async editTicket(data) {
return await request.apiPost('/api/user-service/weight/ticket/edit', data)
}
async getTicketLogs(ticketId) {
return await request.apiGet(`/api/user-service/weight/ticket/logs?ticketId=${ticketId}`)
}
async printTicket(id) {
return await request.apiPost(`/api/user-service/weight/ticket/print?id=${id}`)
}
async ticketDetail(id) {
return await request.apiGet(`/api/user-service/weight/ticket/detail?id=${id}`)
}
// ========== 智能称重设备 ==========
async pageDevices(pageIndex, pageSize, deviceName, deviceNo) {
let url = `/api/user-service/weight/device/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (deviceName) url += '&deviceName=' + deviceName
if (deviceNo) url += '&deviceNo=' + deviceNo
return await request.apiPost(url)
}
async addDevice(data) {
return await request.apiPost('/api/user-service/weight/device/add', data)
}
async updateDevice(data) {
return await request.apiPost('/api/user-service/weight/device/update', data)
}
async deleteDevice(id) {
return await request.apiPost(`/api/user-service/weight/device/delete?id=${id}`)
}
// ========== 司机管理 ==========
async pageDrivers(pageIndex, pageSize, driverName, phone) {
let url = `/api/user-service/weight/driver/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (driverName) url += '&driverName=' + driverName
if (phone) url += '&phone=' + phone
return await request.apiPost(url)
}
async addDriver(data) {
return await request.apiPost('/api/user-service/weight/driver/add', data)
}
async updateDriver(data) {
return await request.apiPost('/api/user-service/weight/driver/update', data)
}
async deleteDriver(id) {
return await request.apiPost(`/api/user-service/weight/driver/delete?id=${id}`)
}
async listAllDrivers() {
return await request.apiGet('/api/user-service/weight/driver/listAll')
}
// ========== 过磅卡 ==========
async pageCards(pageIndex, pageSize, cardNo, cardType) {
let url = `/api/user-service/weight/card/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (cardNo) url += '&cardNo=' + cardNo
if (cardType) url += '&cardType=' + cardType
return await request.apiPost(url)
}
async getByCardNo(cardNo) {
return await request.apiGet(`/api/user-service/weight/card/getByCardNo?cardNo=${cardNo}`)
}
async addCard(data) {
return await request.apiPost('/api/user-service/weight/card/add', data)
}
async updateCard(data) {
return await request.apiPost('/api/user-service/weight/card/update', data)
}
async deleteCard(id) {
return await request.apiPost(`/api/user-service/weight/card/delete?id=${id}`)
}
// ========== 北斗卡 ==========
async pageBeidouCards(pageIndex, pageSize, deviceNo, cardType) {
let url = `/api/user-service/weight/beidou/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (deviceNo) url += '&deviceNo=' + deviceNo
if (cardType) url += '&cardType=' + cardType
return await request.apiPost(url)
}
async addBeidouCard(data) {
return await request.apiPost('/api/user-service/weight/beidou/add', data)
}
async updateBeidouCard(data) {
return await request.apiPost('/api/user-service/weight/beidou/update', data)
}
async deleteBeidouCard(id) {
return await request.apiPost(`/api/user-service/weight/beidou/delete?id=${id}`)
}
async borrowBeidouCard(cardId, vehicleId, plateNumber) {
let url = `/api/user-service/weight/beidou/borrow?cardId=${cardId}&vehicleId=${vehicleId}`
if (plateNumber) url += '&plateNumber=' + plateNumber
return await request.apiPost(url)
}
async returnBeidouCard(cardId) {
return await request.apiPost(`/api/user-service/weight/beidou/return?cardId=${cardId}`)
}
async pageBorrowRecords(pageIndex, pageSize, deviceNo, plateNumber, status) {
let url = `/api/user-service/weight/beidou/borrowRecord/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (deviceNo) url += '&deviceNo=' + deviceNo
if (plateNumber) url += '&plateNumber=' + plateNumber
if (status !== undefined && status !== null) url += '&status=' + status
return await request.apiPost(url)
}
async getBorrowRecordDetail(id) {
return await request.apiGet(`/api/user-service/weight/beidou/borrowRecord/detail?id=${id}`)
}
async listAvailableBeidouCards(limit, deviceNo) {
let url = `/api/user-service/weight/beidou/availableList?limit=${limit || 10}`
if (deviceNo) url += '&deviceNo=' + deviceNo
return await request.apiGet(url)
}
async listBorrowedBeidouCards(limit, deviceNo) {
let url = `/api/user-service/weight/beidou/borrowedList?limit=${limit || 10}`
if (deviceNo) url += '&deviceNo=' + deviceNo
return await request.apiGet(url)
}
async listPendingSecondWeigh(limit) {
let url = `/api/user-service/weight/ticket/pendingSecondWeigh?limit=${limit || 20}`
return await request.apiGet(url)
}
// ========== 北斗卡跟踪记录 ==========
async pageBeidouTracking(pageIndex, pageSize, trackingNo, cardId, deviceNo) {
let url = `/api/user-service/weight/beidou/tracking/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (trackingNo) url += '&trackingNo=' + trackingNo
if (cardId) url += '&cardId=' + cardId
if (deviceNo) url += '&deviceNo=' + deviceNo
return await request.apiPost(url)
}
async beidouTrackingDetail(id) {
return await request.apiGet(`/api/user-service/weight/beidou/tracking/detail?id=${id}`)
}
async beidouTrackingLatest(trackingNo) {
return await request.apiGet(`/api/user-service/weight/beidou/tracking/latest?trackingNo=${trackingNo}`)
}
async beidouTrackingListByNo(trackingNo) {
return await request.apiGet(`/api/user-service/weight/beidou/tracking/listByNo?trackingNo=${trackingNo}`)
}
async generateMockBeidouData(data) {
let url = `/api/user-service/weight/beidou/tracking/generateMock?cardId=${data.cardId}` +
`&tempMin=${data.tempMin}&tempMax=${data.tempMax}` +
`&humidityMin=${data.humidityMin}&humidityMax=${data.humidityMax}` +
`&count=${data.count}&startTime=${data.startTime}`
return await request.apiPost(url)
}
// ========== 报警规则管理 ==========
async pageAlarmRules(pageIndex, pageSize, ruleName) {
let url = `/api/user-service/weight/alarmRule/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (ruleName) url += '&ruleName=' + ruleName
return await request.apiPost(url)
}
async getAlarmRuleDetail(id) {
return await request.apiGet(`/api/user-service/weight/alarmRule/detail?id=${id}`)
}
async saveAlarmRule(data) {
return await request.apiPost('/api/user-service/weight/alarmRule/save', data)
}
async deleteAlarmRule(id) {
return await request.apiPost(`/api/user-service/weight/alarmRule/delete?id=${id}`)
}
async listAllAlarmRules() {
return await request.apiGet('/api/user-service/weight/alarmRule/listAll')
}
async bindAlarmRule(cardId, alarmRuleId) {
let url = `/api/user-service/weight/beidou/bindAlarmRule?cardId=${cardId}`
if (alarmRuleId !== null && alarmRuleId !== undefined) url += '&alarmRuleId=' + alarmRuleId
return await request.apiPost(url)
}
// ========== 报警记录 ==========
async pageAlarmRecords(pageIndex, pageSize, deviceNo, trackingNo, plateNumber, ruleName) {
let url = `/api/user-service/weight/alarmRecord/page?pageIndex=${pageIndex}&pageSize=${pageSize}`
if (deviceNo) url += '&deviceNo=' + deviceNo
if (trackingNo) url += '&trackingNo=' + trackingNo
if (plateNumber) url += '&plateNumber=' + plateNumber
if (ruleName) url += '&ruleName=' + ruleName
return await request.apiPost(url)
}
// ========== 企业UI个性化配置 ==========
async getUiConfig() {
return await request.apiGet('/api/user-service/weight/uiConfig/get')
}
async saveUiConfig(data) {
return await request.apiPost('/api/user-service/weight/uiConfig/save', data)
}
async resetUiConfig() {
return await request.apiPost('/api/user-service/weight/uiConfig/reset')
}
async getUiConfigByCustomer(customerId) {
return await request.apiGet(`/api/user-service/common/uiConfig/getByCustomer?customerId=${customerId}`)
}
}
const target = new weightApi()
export default target
+68 -103
View File
@@ -1,9 +1,21 @@
<template>
<div class="markdown-body" v-html="rendered"></div>
<div class="markdown-content" v-html="renderedHtml"></div>
</template>
<script>
import { parseMarkdown } from '@/utils/markdown'
import { marked } from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
marked.setOptions({
highlight(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
},
breaks: true
})
export default {
name: 'MarkdownContent',
@@ -14,121 +26,74 @@ export default {
}
},
computed: {
rendered() {
return parseMarkdown(this.content)
renderedHtml() {
if (!this.content) return ''
try {
return marked(this.content)
} catch (e) {
return this.content
}
}
}
}
</script>
<style scoped>
.markdown-body {
font-size: 14px;
line-height: 1.6;
color: #303133;
<style lang="scss">
.markdown-content {
line-height: 1.8;
word-wrap: break-word;
}
.markdown-body ::v-deep h1,
.markdown-body ::v-deep h2,
.markdown-body ::v-deep h3,
.markdown-body ::v-deep h4,
.markdown-body ::v-deep h5,
.markdown-body ::v-deep h6 {
margin-top: 12px;
margin-bottom: 8px;
font-weight: 600;
line-height: 1.2;
}
.markdown-body ::v-deep h1 { font-size: 24px; }
.markdown-body ::v-deep h2 { font-size: 22px; }
.markdown-body ::v-deep h3 { font-size: 20px; }
.markdown-body ::v-deep h4 { font-size: 18px; font-weight: 400; }
.markdown-body ::v-deep h5 { font-size: 16px; font-weight: 400; }
.markdown-body ::v-deep h6 { font-size: 14px; font-weight: 400; }
h1, h2, h3, h4, h5, h6 {
margin: 16px 0 8px;
font-weight: 600;
}
h1 { font-size: 1.5em; }
h2 { font-size: 1.3em; }
h3 { font-size: 1.1em; }
.markdown-body ::v-deep p {
margin: 6px 0;
}
p { margin: 8px 0; }
.markdown-body ::v-deep ul,
.markdown-body ::v-deep ol {
padding-left: 20px;
margin: 6px 0;
}
code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: 'SF Mono', 'Monaco', monospace;
font-size: 0.9em;
}
.markdown-body ::v-deep li {
margin: 3px 0;
}
pre {
background: #f5f5f5;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 8px 0;
.markdown-body ::v-deep pre {
background: #1e1e1e;
border-radius: 4px;
padding: 12px;
margin: 8px 0;
overflow-x: auto;
font-size: 13px;
line-height: 1.5;
}
code {
background: none;
padding: 0;
}
}
.markdown-body ::v-deep code {
background: #f5f7fa;
border-radius: 3px;
padding: 2px 4px;
font-size: 13px;
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
color: #303133;
}
ul, ol {
margin: 8px 0;
padding-left: 24px;
}
.markdown-body ::v-deep pre code {
background: none;
padding: 0;
color: #dcdcdc;
}
blockquote {
border-left: 4px solid #ddd;
margin: 8px 0;
padding: 4px 16px;
color: #666;
}
.markdown-body ::v-deep blockquote {
margin: 8px 0;
padding: 8px 16px;
border-left: 4px solid #dcdfe6;
background: #f5f7fa;
color: #606266;
}
table {
border-collapse: collapse;
margin: 8px 0;
.markdown-body ::v-deep table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
}
.markdown-body ::v-deep th,
.markdown-body ::v-deep td {
border: 1px solid #dcdfe6;
padding: 6px 12px;
text-align: left;
}
.markdown-body ::v-deep th {
background: #f5f7fa;
font-weight: 600;
}
.markdown-body ::v-deep a {
color: #409eff;
text-decoration: none;
}
.markdown-body ::v-deep a:hover {
text-decoration: underline;
}
.markdown-body ::v-deep img {
max-width: 100%;
border-radius: 4px;
}
.markdown-body ::v-deep hr {
border: none;
border-top: 1px solid #ebeef5;
margin: 12px 0;
th, td {
border: 1px solid #ddd;
padding: 6px 12px;
}
}
}
</style>
-475
View File
@@ -1,475 +0,0 @@
<template>
<!-- 弹框遮罩层 -->
<div
class="orchard-modal-mask"
v-show="visible"
@click="handleMaskClose"
>
<!-- 弹框内容容器 -->
<div
class="orchard-modal-content"
@click.stop
>
<!-- 关闭按钮 -->
<div class="modal-close-btn" @click="handleClose">×</div>
<!-- 左侧地图/背景图区域 -->
<div class="modal-left">
<div class="map-bg"></div>
</div>
<!-- 右侧数据面板区域 -->
<div class="modal-right">
<!-- 1. 出口果园资质模块 -->
<div class="panel-item qualification-panel">
<div class="panel-title">{{ orchardName || '南宁市正欣农业开发有限公司' }}</div>
<div class="qualification-list">
<div class="qual-item">注册登记品种类:{{ randomData.breed }}</div>
<div class="qual-item">种植面积: {{ randomData.plantArea }}</div>
<div class="qual-item">预估产量: {{ randomData.totalYield }}万斤</div>
<div class="qual-item">出境水果包装厂资质</div>
</div>
</div>
<!-- 2. 果园采收模块 -->
<div class="panel-item harvest-panel">
<div class="panel-title">果园采收 ({{ randomData.harvestDate }} 开采)</div>
<div class="harvest-progress">
<div class="progress-label">已采收: {{ randomData.harvestPercent }}%</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: randomData.harvestPercent + '%' }"></div>
</div>
<div class="progress-value">
<span>已采收量: {{ randomData.harvestedYield }}万斤</span>
<span>预估产量: {{ randomData.totalYield }}万斤</span>
</div>
</div>
</div>
<!-- 3. 分级数据模块 (饼图) -->
<div class="panel-item chart-panel pie-chart-panel">
<div class="panel-title">分级数据</div>
<div id="grade-pie-chart" class="chart-container"></div>
</div>
<!-- 4. 果体大小模块 (柱状图) -->
<div class="panel-item chart-panel bar-chart-panel">
<div class="panel-title">果体大小</div>
<div id="size-bar-chart" class="chart-container"></div>
</div>
</div>
</div>
</div>
</template>
<script>
// 引入ECharts用于图表渲染
import * as echarts from 'echarts';
export default {
name: 'OrchardDetailModal',
props: {
// 接收父组件传递的果园名称
orchardName: {
type: String,
default: ''
},
// 控制弹框显示/隐藏
visible: {
type: Boolean,
default: false
}
},
data() {
return {
// 图表实例缓存
pieChart: null,
barChart: null,
// 存储随机生成的数据
randomData: {
breed: '', // 注册品种
plantArea: 0, // 种植面积(亩)
totalYield: 0, // 预估总产量(万斤)
harvestDate: '', // 采收日期
harvestPercent: 0, // 已采收百分比
harvestedYield: 0, // 已采收量(万斤)
gradeData: [], // 分级数据(饼图)
sizeData: [] // 果体大小数据(柱状图)
}
};
},
// 监听visible变化,弹框显示后生成随机数据并初始化图表
watch: {
visible(newVal) {
if (newVal) {
// 延迟执行,确保DOM已渲染
this.$nextTick(() => {
// 第一步:生成随机数据
this.generateRandomData();
// 第二步:用随机数据初始化图表
this.initCharts();
});
} else {
// 弹框关闭时销毁图表,释放资源
this.destroyCharts();
}
}
},
beforeDestroy() {
this.destroyCharts();
window.removeEventListener('resize', this.handleResize);
},
methods: {
/**
* 核心:生成所有随机数据
* 每次打开弹框都会重新生成,保证数据不同
*/
generateRandomData() {
// 1. 随机注册品种(从列表中选1-2个)
const breedList = ['沃柑', '青柚', '沃柑'];
const breedCount = Math.floor(Math.random() * 2) + 1; // 1-2个品种
const randomBreeds = [];
while (randomBreeds.length < breedCount) {
const randomIdx = Math.floor(Math.random() * breedList.length);
if (!randomBreeds.includes(breedList[randomIdx])) {
randomBreeds.push(breedList[randomIdx]);
}
}
this.randomData.breed = randomBreeds.join('、');
// 2. 随机种植面积(3000-10000亩)
this.randomData.plantArea = Math.floor(Math.random() * 3001) + 30;
// 3. 随机预估总产量(每亩产量2000-5000斤 → 换算成万斤)
const perMuYield = Math.floor(Math.random() * 2001) + 200; // 每亩2000-5000斤
const totalYieldJin = this.randomData.plantArea * perMuYield;
this.randomData.totalYield = Math.round(totalYieldJin / 10000); // 转成万斤并取整
// 4. 随机采收日期(2025年12月1日-20日)
const harvestDay = Math.floor(Math.random() * 20) + 1;
this.randomData.harvestDate = `2025-12-${harvestDay.toString().padStart(2, '0')}`;
// 5. 随机已采收百分比(10-80%)
this.randomData.harvestPercent = Math.floor(Math.random() * 71) + 10;
// 6. 计算已采收量(预估总产量 * 采收百分比 / 100,取整)
this.randomData.harvestedYield = Math.round(
this.randomData.totalYield * this.randomData.harvestPercent / 100
);
// 7. 随机分级数据(A/B/C/DE级,总和=已采收量,比例随机)
const gradeTypes = ['A级', 'B级', 'C级', 'DE级'];
const gradeValues = [];
let remaining = this.randomData.harvestedYield;
// 前3个等级随机分配,最后一个等级取剩余值
for (let i = 0; i < 3; i++) {
if (remaining <= 0) {
gradeValues.push(0);
continue;
}
const randomVal = Math.floor(Math.random() * remaining * 0.6) + 1; // 每次分配剩余值的1-60%
gradeValues.push(randomVal);
remaining -= randomVal;
}
gradeValues.push(remaining > 0 ? remaining : 0);
// 组装饼图数据
this.randomData.gradeData = gradeTypes.map((type, idx) => ({
name: type,
value: gradeValues[idx]
}));
// 8. 随机果体大小数据(55/60/65/70/75/80/85/90以上,每个尺寸10-250万斤)
const sizeLabels = ['55', '60', '65', '70', '75', '80', '85', '90以上'];
this.randomData.sizeData = sizeLabels.map(() =>
Math.floor(Math.random() * 241) + 10 // 10-250万斤
);
},
// 初始化饼图和柱状图(使用随机数据)
initCharts() {
// 先销毁旧实例,避免重复创建
this.destroyCharts();
// 1. 分级数据饼图
const pieDom = document.getElementById('grade-pie-chart');
if (!pieDom) return;
this.pieChart = echarts.init(pieDom);
const pieOption = {
color: ['#0066CC', '#0099FF', '#00CC99', '#FFCC00'], // A/B/C/DE级颜色
tooltip: {
trigger: 'item',
formatter: '{b}: {c}万斤'
},
series: [
{
name: '分级数据',
type: 'pie',
radius: ['35%', '58%'],
center: ['48%', '52%'],
avoidLabelOverlap: true,
data: this.randomData.gradeData, // 使用随机分级数据
label: {
show: true,
position: 'outside',
formatter: '{b}: {c}万',
color: '#fff',
fontSize: 11,
distanceToLabelLine: 4
},
labelLine: {
show: true,
length: 12,
length2: 10,
smooth: false
}
}
]
};
this.pieChart.setOption(pieOption);
// 2. 果体大小柱状图
const barDom = document.getElementById('size-bar-chart');
if (!barDom) return;
this.barChart = echarts.init(barDom);
const barOption = {
color: '#1E90FF',
tooltip: {
trigger: 'axis',
formatter: '{b}mm: {c}万斤'
},
grid: {
left: '12%',
right: '5%',
top: '18%',
bottom: '22%'
},
xAxis: {
type: 'category',
data: ['55', '60', '65', '70', '75', '80', '85', '90以上'],
axisLabel: {
color: '#fff',
fontSize: 11,
rotate: 30,
interval: 0,
margin: 10
},
axisLine: { lineStyle: { color: '#666' } }
},
yAxis: {
type: 'value',
name: '万斤',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: '#666' } },
splitLine: { lineStyle: { color: '#333' } }
},
series: [
{
name: '果体大小',
type: 'bar',
data: this.randomData.sizeData, // 使用随机果体大小数据
barWidth: '60%'
}
]
};
this.barChart.setOption(barOption);
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
},
// 销毁图表实例
destroyCharts() {
if (this.pieChart) {
this.pieChart.dispose();
this.pieChart = null;
}
if (this.barChart) {
this.barChart.dispose();
this.barChart = null;
}
},
// 窗口大小变化时重新适配图表
handleResize() {
if (this.pieChart) this.pieChart.resize();
if (this.barChart) this.barChart.resize();
},
// 点击遮罩层关闭弹框
handleMaskClose() {
this.handleClose();
},
// 关闭弹框
handleClose() {
this.$emit('update:visible', false);
}
}
};
</script>
<style scoped>
/* 原有样式不变,无需修改 */
.orchard-modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
.orchard-modal-content {
display: flex;
width: 95%;
max-width: 1800px;
min-width: 1200px;
padding: 20px;
height: 85vh;
min-height: 600px;
background: #001529;
border-radius: 8px;
overflow: hidden;
position: relative;
border: 1px solid #1a4a8d;
}
.modal-close-btn {
position: absolute;
top: 15px;
right: 20px;
color: #fff;
font-size: 24px;
cursor: pointer;
z-index: 10;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
transition: all 0.3s;
}
.modal-close-btn:hover {
background: #ff4d4f;
border-radius: 50%;
}
.modal-left {
flex: 7;
height: 100%;
position: relative;
border: 1px solid #1a4a8d;
}
.map-bg {
width: 100%;
height: 100%;
background: url('../../assets/images/brain/dikuai01.png') center/cover no-repeat;
}
.modal-right {
flex: 2.2;
height: 100%;
padding: 0px 0px 0px 15px;
display: flex;
flex-direction: column;
gap: 15px;
background: #001e3c;
}
.panel-item {
background: #002f5f;
border-radius: 6px;
padding: 12px;
color: #fff;
flex: 1;
min-height: 100px;
}
.panel-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: #00eaff;
border-bottom: 1px solid #0066cc;
padding-bottom: 5px;
}
.qualification-panel {
flex: 1.2;
}
.qualification-list {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 14px;
line-height: 1.5;
}
.qual-item {
display: flex;
align-items: center;
}
.harvest-panel {
flex: 1.2;
}
.harvest-progress {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-label {
font-size: 14px;
}
.progress-bar {
width: 100%;
height: 10px;
background: #003366;
border-radius: 5px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #00cc99;
border-radius: 5px;
transition: width 0.5s ease;
}
.progress-value {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #ccc;
}
.chart-panel {
flex: 1.8;
overflow: visible;
}
.chart-container {
width: 100%;
height: calc(100% - 30px);
min-height: 120px;
overflow: visible;
}
@media screen and (max-width: 1920px) {
.orchard-modal-content {
width: 90%;
height: 80vh;
}
}
@media screen and (min-width: 2560px) {
.orchard-modal-content {
max-width: 2200px;
height: 90vh;
}
.panel-title {
font-size: 18px;
}
.qual-item, .progress-label {
font-size: 16px;
}
}
</style>
-493
View File
@@ -1,493 +0,0 @@
<template>
<div class="right-chart-panel">
<!-- 沃柑产收 -->
<div class="panel panel-1">
<div class="panel-title">沃柑产收</div>
<div class="panel-content">
<div class="panel-text">
<div>总面积46万亩</div>
<div>预计产量150万吨</div>
<div>已采收量23.5万吨</div>
</div>
<div ref="yieldChart" class="chart"></div>
</div>
</div>
<!-- 果品质量 -->
<div class="panel panel-2">
<div class="panel-title">果品质量</div>
<div ref="qualityChart" class="chart"></div>
</div>
<!-- 果体大小 -->
<div class="panel panel-3">
<div class="panel-title">果体大小</div>
<div ref="sizeChart" class="chart"></div>
</div>
<!-- 出口国家 -->
<div class="panel panel-4">
<div class="panel-title">出口国家</div>
<div ref="exportChart" class="chart"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'RightChartPanel',
data() {
return {
charts: {} // 统一保存所有 echarts 实例
};
},
mounted() {
this.initCharts();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
this.disposeCharts();
},
methods: {
/* ===== 初始化入口 ===== */
initCharts() {
this.initYieldChart();
this.initQualityChart();
this.initSizeChart();
this.initExportChart();
this.$nextTick(() => this.handleResize());
},
/* ===== resize 统一处理 ===== */
handleResize() {
Object.values(this.charts).forEach(chart => chart && chart.resize());
},
disposeCharts() {
Object.values(this.charts).forEach(chart => chart && chart.dispose());
this.charts = {};
},
/* ===== 图表 1:沃柑产收(科技感蓝色系) ===== */
initYieldChart() {
const chart = echarts.init(this.$refs.yieldChart);
this.charts.yield = chart;
chart.setOption({
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
textStyle: { color: '#333' },
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#00eaff',
borderWidth: 1
},
legend: {
orient: 'vertical',
left: 'left',
textStyle: { color: '#cceaff', fontSize: 12 }
},
color: ['#00eaff', '#4dabf7'], // 科技蓝+浅蓝
series: [{
name: '沃柑类型',
type: 'pie',
radius: '50%',
center: ['50%', '50%'],
label: {
show: true,
color: '#cceaff',
fontSize: 12,
formatter: '{b}\n{d}%',
textShadow: '0 0 3px rgba(0, 234, 255, 0.8)'
},
labelLine: {
show: false
},
data: [
{ value: 82, name: '无籽沃柑' },
{ value: 18, name: '有籽沃柑' }
],
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 234, 255, 0.6)'
},
label: {
show: true,
color: '#ffffff',
fontWeight: 'bold'
}
},
itemStyle: {
borderColor: 'rgba(12, 28, 56, 0.8)',
borderWidth: 2,
borderRadius: 5
}
}]
});
},
/* ===== 图表 2:果品质量(修改版:大图例 + 饱满饼图) ===== */
initQualityChart() {
const chart = echarts.init(this.$refs.qualityChart);
this.charts.quality = chart;
const data = [
{ value: 79, name: 'A级果' },
{ value: 17, name: 'B级果' },
{ value: 3, name: 'C级果' },
{ value: 1, name: 'D级果' }
];
chart.setOption({
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: '{b}: {c}%',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#00eaff',
borderWidth: 1
},
// 配置大图例
legend: {
orient: 'vertical',
right: '5%', // 靠右对齐
top: 'middle', // 垂直居中
itemGap: 12, // 图例项之间的间距
itemWidth: 16, // 图标宽度加大
itemHeight: 16, // 图标高度加大
icon: 'rect', // 使用矩形图标更显科技感(可选 'circle', 'roundRect'
textStyle: {
color: '#cceaff',
fontSize: 12,
rich: {
// 利用富文本对齐名称和数值
name: {
width: 50,
align: 'left',
color: '#cceaff',
fontSize: 13
},
value: {
width: 40,
align: 'right',
color: '#00eaff', // 数值高亮色
fontSize: 14,
fontWeight: 'bold',
fontFamily: 'Impact, sans-serif' // 数字用硬朗字体
}
}
},
formatter: (name) => {
const item = data.find(p => p.name === name);
// 构造富文本字符串
return `{name|${name}}{value|${item.value}%}`;
}
},
color: ['#00eaff', '#4dabf7', '#5c7cfa', '#7950f2'],
series: [
{
name: '果品质量',
type: 'pie',
// 环形图,半径设置大一些
radius: ['45%', '70%'],
// 饼图位置靠左,留出右边给图例
center: ['30%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 5,
borderColor: '#0c1c38',
borderWidth: 3
},
// 默认不显示label,靠图例展示
label: {
show: false,
position: 'center'
},
// 高亮时中间显示具体数值
emphasis: {
label: {
show: true,
fontSize: '20',
fontWeight: 'bold',
color: '#fff',
formatter: '{d}%'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 234, 255, 0.5)'
}
},
labelLine: {
show: false
},
data: data
}
]
});
},
/* ===== 图表 3:果体大小(柱状图+科技渐变+发光效果) ===== */
initSizeChart() {
const chart = echarts.init(this.$refs.sizeChart);
this.charts.size = chart;
chart.setOption({
backgroundColor: 'transparent',
grid: {
left: 30,
right: 10,
top: 30,
bottom: 30
},
xAxis: {
type: 'category',
data: ['55', '60', '65', '70', '75', '80', '85', '90'],
axisLine: {
lineStyle: {
color: '#4dabf7',
width: 1
}
},
axisLabel: {
color: '#cceaff',
fontSize: 11
},
axisTick: {
alignWithLabel: true,
lineStyle: {
color: '#4dabf7'
}
}
},
yAxis: {
type: 'value',
name: '万斤',
nameTextStyle: {
color: '#cceaff',
fontSize: 11
},
axisLine: {
lineStyle: {
color: '#4dabf7',
width: 1
}
},
axisLabel: {
color: '#cceaff',
fontSize: 11
},
splitLine: {
lineStyle: {
color: 'rgba(77, 171, 247, 0.1)',
type: 'dashed'
}
}
},
series: [{
type: 'bar',
barWidth: '60%',
data: [120, 320, 420, 200, 180, 140, 60, 30],
itemStyle: {
borderRadius: [4, 4, 0, 0],
// 科技蓝渐变
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00eaff' },
{ offset: 0.5, color: '#4dabf7' },
{ offset: 1, color: '#339af0' }
]),
borderColor: 'rgba(0, 234, 255, 0.3)',
borderWidth: 1,
shadowBlur: 5,
shadowColor: 'rgba(0, 234, 255, 0.3)'
},
label: {
show: true,
position: 'top',
color: '#ffffff',
fontSize: 10,
fontWeight: 'bold',
textShadow: '0 0 3px rgba(0, 234, 255, 0.8)'
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 234, 255, 0.6)',
opacity: 1
}
}
}]
});
},
/* ===== 图表 4:出口国家(科技感玫瑰图) ===== */
initExportChart() {
const chart = echarts.init(this.$refs.exportChart);
this.charts.export = chart;
chart.setOption({
backgroundColor: 'transparent',
legend: {
top: 'bottom',
textStyle: {
color: '#cceaff',
fontSize: 11
},
itemWidth: 10,
itemHeight: 10
},
tooltip: {
trigger: 'item',
textStyle: { color: '#333' },
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#00eaff',
borderWidth: 1
},
series: [
{
name: '出口数量',
type: 'pie',
radius: [30, 100],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8,
borderColor: 'rgba(12, 28, 56, 0.8)',
borderWidth: 2,
shadowBlur: 8,
shadowColor: 'rgba(0, 234, 255, 0.3)'
},
color: ['#00eaff', '#4dabf7', '#5c7cfa', '#7950f2'], // 科技蓝紫渐变
label: {
show: true,
color: '#ffffff',
fontSize: 11,
formatter: '{b}\n{c}',
textShadow: '0 0 3px rgba(0, 234, 255, 0.8)'
},
labelLine: {
show: true,
length: 10,
length2: 10,
lineStyle: {
color: 'rgba(204, 234, 255, 0.5)',
width: 1
}
},
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowColor: 'rgba(0, 234, 255, 0.6)',
scale: 1.05
},
label: {
color: '#ffffff',
fontWeight: 'bold'
}
},
data: [
{ value: 16, name: '俄罗斯' },
{ value: 10, name: '泰国' },
{ value: 5, name: '新加坡' },
{ value: 1, name: '马来西亚' }
]
}
]
});
}
}
};
</script>
<style scoped>
.right-chart-panel {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 10px; /* 面板间距 */
}
/* 四个面板平分高度 */
.panel {
flex: 1; /* 平分空间 */
border: 1px solid rgba(0, 234, 255, 0.3);
padding: 12px;
box-sizing: border-box;
display: flex;
flex-direction: column;
background: rgba(12, 28, 56, 0.7);
border-radius: 4px;
box-shadow: 0 0 10px rgba(0, 234, 255, 0.1);
transition: all 0.3s ease;
}
.panel:hover {
box-shadow: 0 0 15px rgba(0, 234, 255, 0.2);
border-color: rgba(0, 234, 255, 0.5);
}
/* 每个面板的标题 */
.panel-title {
color: #00eaff;
font-size: 16px;
margin-bottom: 8px;
font-weight: bold;
text-shadow: 0 0 5px rgba(0, 234, 255, 0.6);
letter-spacing: 1px;
position: relative;
padding-left: 10px;
}
.panel-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 70%;
background: linear-gradient(to bottom, #00eaff, #4dabf7);
border-radius: 2px;
}
/* 面板内容 */
.panel-content {
flex: 1; /* 内容占满面板剩余高度 */
display: flex;
gap: 8px;
}
/* 沃柑产收的文本区域 */
.panel-text {
width: 45%;
color: #cceaff;
font-size: 12px;
line-height: 1.8;
display: flex;
flex-direction: column;
justify-content: center;
padding: 5px;
}
.panel-text div {
margin-bottom: 6px;
padding: 4px 8px;
background: rgba(0, 234, 255, 0.1);
border-radius: 3px;
border-left: 2px solid #00eaff;
}
/* 图表容器 */
.chart {
flex: 1; /* 图表占满剩余宽度/高度 */
min-height: 0; /* 解决flex子元素高度塌陷 */
}
/* 每个面板的最小高度保障 */
.panel-1, .panel-2, .panel-3, .panel-4 {
min-height: 180px;
}
</style>
+2 -19
View File
@@ -1,23 +1,11 @@
<!--
* @Author: 李黄杰
* @LastEditors: 李黄杰
* @LastEditTime: 2024-10-15 15:09:12
-->
<template>
<div class="dashboard-editor-container">
<div style="width: 100%" class="itemflex-div">
<!-- <iframe :src="newSrc" class="iframemain" allowfullscreen="true" id="iframeId"></iframe> -->
<web-iframe :src="newSrc" v-if="menu.menuType === 4"></web-iframe>
<huo-iframe :src="newSrc" v-if="menu.menuType === 2"></huo-iframe>
<pay-iframe :src="newSrc" v-if="menu.menuType === 3"></pay-iframe>
<!-- hhhhhhhhhhhhhhh{{ menu }} -->
<iframe :src="newSrc" class="iframemain" allowfullscreen="true" id="iframeId"></iframe>
</div>
</div>
</template>
<script>
import webIframe from '@/views/web/index.vue'
import huoIframe from '@/views/huoban/index.vue'
import payIframe from '@/views/payment/index.vue'
export default {
props: {
newSrc: {
@@ -28,11 +16,6 @@ export default {
type: Object,
default: ''
}
},
components: {
webIframe,
huoIframe,
payIframe
}
};
</script>
@@ -52,4 +35,4 @@ export default {
width: 100%;
height: 100%;
}
</style>
</style>
File diff suppressed because it is too large Load Diff
@@ -1,35 +0,0 @@
<!--
* @Author: 844772650@qq.com 844772650@qq.com
* @Date: 2026-01-06 20:20:29
* @LastEditors: 844772650@qq.com 844772650@qq.com
* @LastEditTime: 2026-01-20 19:06:54
* @FilePath: \durian-web\src\components\CenterPanel.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<section class="center-col">
<MapView />
<MonitorGrid />
</section>
</template>
<script>
import MapView from '@/components/tracing/screen/MapView.vue'
import MonitorGrid from '@/components/tracing/screen/MonitorGrid.vue'
export default {
name: 'CenterPanel',
components: {
MapView,
MonitorGrid
}
}
</script>
<style lang="scss" scoped>
.center-col {
display: flex;
flex-direction: column;
gap: 10px;
}
</style>
@@ -1,813 +0,0 @@
<template>
<div class="panel cold-storage-details">
<div class="panel-title">冷库详情</div>
<div class="cold-storage-grid">
<div
v-for="storage in coldStorages"
:key="storage.id"
class="cold-storage-item"
:class="{ 'full-storage': storage.usage === 100 }"
@click="openStockModal(storage)"
style="cursor: pointer"
>
<div class="storage-title">{{ storage.title }}</div>
<div class="storage-companies">
<div
v-for="company in storage.companies"
:key="company.name"
class="company-tag"
:style="{
background: company.background,
color: company.color,
}"
>
{{ company.name }}
</div>
</div>
<div class="storage-usage">
<div
class="usage-circle"
:style="{ '--usage': `${storage.usage}%` }"
>
<div class="usage-text">
{{ storage.current }}/{{ storage.capacity }}
</div>
</div>
</div>
<div class="storage-status">
<div class="temperature">
<div
class="status-value"
:class="{ warning: storage.temperatureWarning }"
>
{{ storage.temperature }}
</div>
</div>
<div class="humidity">
<div
class="status-value"
:class="{ warning: storage.humidityWarning }"
>
{{ storage.humidity }}%
</div>
</div>
</div>
</div>
</div>
<!-- 引入冷库库存详情弹框 -->
<ColdStorageStockModal
:visible="isStockModalVisible"
:storage="activeStorage"
@update:visible="(val) => (isStockModalVisible = val)"
/>
</div>
</template>
<script>
import huobanAuth from '@/utils/huobanAuth'
// ✅ 1. 引入你封装好的刷新工具方法
import autoRefresh from "@/utils/autoRefresh";
import ColdStorageStockModal from "@/components/tracing/screen/ColdStorageStockModal.vue"; // 引入组件
export default {
name: "ColdStorageDetails",
components: {
ColdStorageStockModal, // 注册组件
},
data() {
return {
coldStorages: [
{
id: 1,
title: "① 保鲜库",
size: "14.2m*8.5m*4.5m", // 添加冷库尺寸
companies: [
{
name: "正欣农业",
background: "rgba(0, 242, 255, 0.2)",
color: "#00f2ff",
// 添加库存树形数据
stock: {
正欣农业: {
total: 422,
products: {
黑框亮膜带盖: {
total: 422,
specs: {
"65A": { count: 55 },
"70A": { count: 156 },
"65B": { count: 55 },
"70B": { count: 156 },
},
},
},
},
},
},
{
name: "瑾秋",
background: "rgba(255, 215, 0, 0.2)",
color: "#ffd700",
stock: {
瑾秋: {
total: 320,
products: {
"5kg天地盖": {
total: 320,
specs: {
"65A": { count: 55 },
"70A": { count: 156 },
"65B": { count: 55 },
},
},
},
},
},
},
{
name: "平和高尚",
background: "rgba(255, 192, 203, 0.2)",
color: "#ffc0cb",
stock: {
平和高尚: {
total: 320,
products: {
"5kg天地盖": {
total: 320,
specs: {
"65A": { count: 55 },
"70A": { count: 156 },
"65B": { count: 55 },
},
},
},
},
},
},
],
current: 0,
capacity: 77,
temperature: 5,
humidity: 90,
temperatureWarning: false,
humidityWarning: false,
stackCustomerCount: 3,
stackLayoutVariant: 1,
color: "#00f2ff", // 添加客户对应颜色
},
{
id: 2,
title: "② 保鲜库",
size: "13.8m*8.0m*4.2m",
companies: [
{
name: "正欣农业",
background: "rgba(0, 242, 255, 0.2)",
color: "#00f2ff",
stock: {
正欣农业: {
total: 286,
products: {
"5kg天地盖": {
total: 286,
specs: {
"65A": { count: 88 },
"70A": { count: 112 },
"70B": { count: 86 },
},
},
},
},
},
},
{
name: "瑾秋",
background: "rgba(255, 215, 0, 0.2)",
color: "#ffd700",
stock: {
瑾秋: {
total: 198,
products: {
"黑框亮膜带盖": {
total: 198,
specs: {
"65A": { count: 63 },
"65B": { count: 51 },
"70A": { count: 84 },
},
},
},
},
},
},
{
name: "平和高尚",
background: "rgba(255, 192, 203, 0.2)",
color: "#ffc0cb",
stock: {
平和高尚: {
total: 154,
products: {
"3kg手提礼箱": {
total: 154,
specs: {
"60A": { count: 48 },
"65A": { count: 56 },
"65B": { count: 50 },
},
},
},
},
},
},
],
current: 0,
capacity: 77,
temperature: 3,
humidity: 92,
temperatureWarning: false,
humidityWarning: false,
stackCustomerCount: 2,
stackLayoutVariant: 2,
color: "#00f2ff",
},
{
id: 3,
title: "③ 预冷库",
size: "12.6m*7.8m*4.0m",
companies: [
{
name: "正欣农业",
background: "rgba(0, 242, 255, 0.2)",
color: "#00f2ff",
stock: {
正欣农业: {
total: 132,
products: {
"2.5kg开窗礼盒": {
total: 132,
specs: {
"60A": { count: 42 },
"65A": { count: 50 },
"65B": { count: 40 },
},
},
},
},
},
},
{
name: "瑾秋",
background: "rgba(255, 215, 0, 0.2)",
color: "#ffd700",
stock: {
瑾秋: {
total: 176,
products: {
"4kg平盖箱": {
total: 176,
specs: {
"65A": { count: 58 },
"70A": { count: 66 },
"70B": { count: 52 },
},
},
},
},
},
},
{
name: "平和高尚",
background: "rgba(255, 192, 203, 0.2)",
color: "#ffc0cb",
stock: {
平和高尚: {
total: 119,
products: {
"黑框亮膜带盖": {
total: 119,
specs: {
"60A": { count: 34 },
"65A": { count: 43 },
"65B": { count: 42 },
},
},
},
},
},
},
],
current: 0,
capacity: 77,
temperature: 1,
humidity: 88,
temperatureWarning: false,
humidityWarning: false,
stackCustomerCount: 1,
stackLayoutVariant: 1,
color: "#ffd700",
},
{
id: 4,
title: "④ 预冷库",
size: "12.0m*7.2m*4.0m",
companies: [
{
name: "正欣农业",
background: "rgba(0, 242, 255, 0.2)",
color: "#00f2ff",
stock: {
正欣农业: {
total: 62,
products: {
"5kg天地盖": {
total: 62,
specs: {
"60A": { count: 21 },
"65A": { count: 23 },
"65B": { count: 18 },
},
},
},
},
},
},
{
name: "瑾秋",
background: "rgba(255, 215, 0, 0.2)",
color: "#ffd700",
stock: {
瑾秋: {
total: 38,
products: {
"2kg精品盒": {
total: 38,
specs: {
"60A": { count: 11 },
"65A": { count: 15 },
"65B": { count: 12 },
},
},
},
},
},
},
{
name: "平和高尚",
background: "rgba(255, 192, 203, 0.2)",
color: "#ffc0cb",
stock: {
平和高尚: {
total: 54,
products: {
"3kg手提礼箱": {
total: 54,
specs: {
"60A": { count: 16 },
"65A": { count: 20 },
"65B": { count: 18 },
},
},
},
},
},
},
],
current: 0,
capacity: 77,
temperature: 2,
humidity: 90,
temperatureWarning: false,
humidityWarning: false,
stackCustomerCount: 2,
stackLayoutVariant: 1,
color: "#ffc0cb",
},
{
id: 5,
title: "⑤ 智能调控库",
size: "15.0m*8.8m*4.6m",
companies: [
{
name: "正欣农业",
background: "rgba(0, 242, 255, 0.2)",
color: "#00f2ff",
stock: {
正欣农业: {
total: 396,
products: {
"黑框亮膜带盖": {
total: 396,
specs: {
"65A": { count: 122 },
"70A": { count: 146 },
"70B": { count: 128 },
},
},
},
},
},
},
{
name: "瑾秋",
background: "rgba(255, 215, 0, 0.2)",
color: "#ffd700",
stock: {
瑾秋: {
total: 360,
products: {
"5kg天地盖": {
total: 360,
specs: {
"65A": { count: 110 },
"70A": { count: 132 },
"70B": { count: 118 },
},
},
},
},
},
},
{
name: "平和高尚",
background: "rgba(255, 192, 203, 0.2)",
color: "#ffc0cb",
stock: {
平和高尚: {
total: 244,
products: {
"4kg平盖箱": {
total: 244,
specs: {
"60A": { count: 74 },
"65A": { count: 92 },
"65B": { count: 78 },
},
},
},
},
},
},
],
current: 0,
capacity: 77,
temperature: 4,
humidity: 95,
temperatureWarning: true,
humidityWarning: true,
stackCustomerCount: 3,
stackLayoutVariant: 2,
color: "#00f2ff",
},
{
id: 6,
title: "⑥ 智能调控库",
size: "14.6m*8.6m*4.5m",
companies: [
{
name: "正欣农业",
background: "rgba(0, 242, 255, 0.2)",
color: "#00f2ff",
stock: {
正欣农业: {
total: 310,
products: {
"3kg手提礼箱": {
total: 310,
specs: {
"60A": { count: 95 },
"65A": { count: 117 },
"65B": { count: 98 },
},
},
},
},
},
},
{
name: "瑾秋",
background: "rgba(255, 215, 0, 0.2)",
color: "#ffd700",
stock: {
瑾秋: {
total: 286,
products: {
"5kg天地盖": {
total: 286,
specs: {
"65A": { count: 87 },
"70A": { count: 108 },
"70B": { count: 91 },
},
},
},
},
},
},
{
name: "平和高尚",
background: "rgba(255, 192, 203, 0.2)",
color: "#ffc0cb",
stock: {
平和高尚: {
total: 210,
products: {
"2.5kg开窗礼盒": {
total: 210,
specs: {
"60A": { count: 64 },
"65A": { count: 80 },
"65B": { count: 66 },
},
},
},
},
},
},
],
current: 0,
capacity: 77,
temperature: 6,
humidity: 92,
temperatureWarning: false,
humidityWarning: false,
stackCustomerCount: 2,
stackLayoutVariant: 1,
color: "#ffd700",
},
{
id: 7,
title: "⑦ 智能调控库",
size: "14.0m*8.2m*4.4m",
companies: [
{
name: "正欣农业",
background: "rgba(0, 242, 255, 0.2)",
color: "#00f2ff",
stock: {
正欣农业: {
total: 98,
products: {
"2kg精品盒": {
total: 98,
specs: {
"60A": { count: 30 },
"65A": { count: 36 },
"65B": { count: 32 },
},
},
},
},
},
},
{
name: "瑾秋",
background: "rgba(255, 215, 0, 0.2)",
color: "#ffd700",
stock: {
瑾秋: {
total: 72,
products: {
"3kg手提礼箱": {
total: 72,
specs: {
"60A": { count: 22 },
"65A": { count: 26 },
"65B": { count: 24 },
},
},
},
},
},
},
{
name: "平和高尚",
background: "rgba(255, 192, 203, 0.2)",
color: "#ffc0cb",
stock: {
平和高尚: {
total: 86,
products: {
"4kg平盖箱": {
total: 86,
specs: {
"60A": { count: 26 },
"65A": { count: 31 },
"65B": { count: 29 },
},
},
},
},
},
},
],
current: 0,
capacity: 77,
temperature: 8,
humidity: 94,
temperatureWarning: false,
humidityWarning: false,
stackCustomerCount: 1,
stackLayoutVariant: 2,
color: "#ffc0cb",
},
],
isStockModalVisible: false, // 控制库存弹框显示
activeStorage: {}, // 当前激活的冷库
};
},
computed: {
usagePercentage() {
return function (current, capacity) {
return Math.round((current / capacity) * 100);
};
},
},
methods: {
async getColdData() {
try {
let res = await huobanAuth.queryData({
tableId: 2100000000014954,
limit: 100,
conditions: [],
relations: [],
});
if (res.code === 1 && res.data && res.data.length >= 1) {
console.log("冷库信息", res.data);
// 更新冷库温湿度数据
this.updateColdStorageData(res.data);
} else {
console.error("API返回数据格式错误:", res);
}
} catch (error) {
console.error("获取冷库数据失败:", error);
}
},
// 更新冷库温湿度数据的方法
updateColdStorageData(apiData) {
// 遍历API返回的数据
apiData.forEach(item => {
const lengkuId = item.lengkuId;
// 在coldStorages数组中查找对应的冷库
const storageIndex = this.coldStorages.findIndex(storage => storage.id === lengkuId);
if (storageIndex !== -1) {
// 计算修正后的温度湿度
const actualTemp = item.current_temp || 0
const actualHumidity = item.current_humidity || 0
// 更新温度湿度
this.coldStorages[storageIndex].temperature = Number(actualTemp.toFixed(1));
this.coldStorages[storageIndex].humidity = Number(actualHumidity.toFixed(1));
} else {
console.warn(`未找到ID为${lengkuId}的冷库`);
}
});
// 强制Vue更新视图
this.$forceUpdate();
},
// 打开库存详情弹框
openStockModal(storage) {
this.activeStorage = storage;
this.isStockModalVisible = true;
},
},
mounted() {
// ✅ 启动自动刷新,每5分钟刷新一次
this.timer = autoRefresh.startRefresh(this.getColdData, 300000); // 300000毫秒 = 5分钟
// 沃柑季结束,冷库库存清零
this.coldStorages.forEach((storage) => {
storage.current = 0;
storage.companies = [];
storage.usage = 0;
});
},
beforeDestroy() {
// ✅ 清除定时器
if (this.timer) {
autoRefresh.stopRefresh(this.timer);
}
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.cold-storage-details {
@include panel-base;
// height: 280px;
flex: 3;
}
.cold-storage-grid {
display: flex;
gap: 8px;
height: calc(100% - 20px);
overflow: hidden;
width: 100%;
box-sizing: border-box;
}
.cold-storage-item {
background: rgba(6, 30, 93, 0.7);
border: 1px solid $border-color;
border-radius: 6px;
padding: 8px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
&.full-storage {
// border-color: #ff0000;
.usage-circle {
background: conic-gradient(#ff0000 100%, rgba(0, 242, 255, 0.2) 100%);
}
}
}
.storage-title {
text-align: center;
font-size: 0.5vw;
margin-bottom: 0.4vh;
color: #aaa;
}
.storage-companies {
display: flex;
flex-direction: column;
gap: 0.2vh;
margin-bottom: 0.6vh;
}
.company-tag {
font-size: 0.4vw;
padding: 0.2vh;
text-align: center;
border-radius: 2px;
}
.storage-usage {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.usage-circle {
width: 45%;
aspect-ratio: 1;
max-width: 60px;
border-radius: 50%;
background: conic-gradient(
#00f2ff var(--usage, 0%),
rgba(0, 242, 255, 0.2) var(--usage, 0%)
);
position: relative;
}
.usage-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.55vw;
color: #fff;
min-font-size: 6px;
}
.storage-status {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.4vw;
color: #aaa;
}
.temperature,
.humidity {
display: flex;
align-items: center;
gap: 0.3vw;
}
.status-value {
color: #00f2ff;
&.warning {
color: #ff0000;
}
}
</style>
@@ -1,328 +0,0 @@
<template>
<div class="panel cold-storage-humidity">
<div class="panel-title">湿度监控</div>
<div ref="chart" class="chart-container"></div>
</div>
</template>
<script>
export default {
name: "ColdStorageHumidity",
props: {
humidityData: {
type: Array,
default: () => [],
},
},
data() {
return {
chartInstance: null,
};
},
watch: {
humidityData: {
handler(newData) {
if (newData && newData.length > 0) {
this.updateChart(newData);
}
},
immediate: true,
deep: true,
},
},
mounted() {
this.initChart();
window.addEventListener("resize", this.handleResize);
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose();
}
window.removeEventListener("resize", this.handleResize);
},
methods: {
initChart() {
this.chartInstance = this.$echarts.init(this.$refs.chart, null, {
devicePixelRatio: window.devicePixelRatio || 1,
renderer: "canvas",
});
// 使用默认数据初始化(20个点)
const defaultData = [
new Array(20).fill(80),
new Array(20).fill(75),
new Array(20).fill(90),
new Array(20).fill(65),
new Array(20).fill(85),
new Array(20).fill(70),
new Array(20).fill(92),
];
const option = this.createMultiLineChartOption(
"冷库湿度实时监控 (%)",
defaultData,
"%",
);
this.chartInstance.setOption(option);
},
updateChart(data) {
if (!this.chartInstance) {
this.initChart();
}
// 确保数据格式正确,每个冷库有20个数据点
const formattedData = [];
for (let i = 0; i < 7; i++) {
if (data[i] && Array.isArray(data[i]) && data[i].length >= 20) {
formattedData.push(data[i].slice(-20)); // 取最后20个点
} else if (data[i] && Array.isArray(data[i])) {
// 如果数据不足20个,用95填充前面(不要用0)
const paddedData = new Array(20).fill(95); // 改为用95填充
const startIndex = 20 - data[i].length;
for (let j = 0; j < data[i].length; j++) {
paddedData[startIndex + j] = data[i][j];
}
formattedData.push(paddedData);
} else {
// 如果没有数据,使用95数组(不是0)
formattedData.push(new Array(20).fill(95));
}
}
const option = this.createMultiLineChartOption(
"冷库湿度实时监控 (%)",
formattedData,
"%",
);
this.chartInstance.setOption(option);
},
// 生成时间标签(20个点)
generateTimeLabels() {
const labels = [];
const now = new Date();
// 生成最近20个小时的标签
for (let i = 0; i < 20; i++) {
const time = new Date(now.getTime() - (19 - i) * 3600000); // 从最早的时间开始
const hour = String(time.getHours()).padStart(2, "0");
labels.push(`${hour}`);
}
return labels;
},
createMultiLineChartOption(title, data, unit) {
const allValues = data.flat().filter((val) => !isNaN(val));
// 对于湿度,确保合理范围
let minValue = allValues.length > 0 ? Math.min(...allValues) : 60;
let maxValue = allValues.length > 0 ? Math.max(...allValues) : 95;
// 强制湿度在合理范围内(0-100%)
minValue = Math.max(0, minValue);
maxValue = Math.min(100, maxValue);
// 确保有足够的显示范围
if (maxValue - minValue < 10) {
minValue = 60;
maxValue = 95;
}
const valueRange = maxValue - minValue;
const padding = valueRange * 0.15 || 5;
const colors = [
"#00f2ff",
"#ffcc00",
"#ff3366",
"#00ff99",
"#b388ff",
"#ff9e80",
"#80d8ff",
];
const series = [];
for (let i = 0; i < data.length; i++) {
series.push({
name: `${i + 1}号库`,
type: "line",
smooth: true,
symbol: "none",
lineStyle: {
width: 2,
color: colors[i],
opacity: 0.8,
},
emphasis: {
lineStyle: {
width: 3,
opacity: 1,
},
focus: "series",
},
data: data[i],
});
}
return {
backgroundColor: "transparent",
title: title
? {
text: title,
left: "center",
textStyle: {
color: "#00f2ff",
fontSize: 12,
fontFamily: "Microsoft YaHei, sans-serif",
},
top: "2%",
}
: { show: false },
tooltip: {
trigger: "axis",
backgroundColor: "rgba(0, 30, 93, 0.9)",
borderColor: "#1a4a8d",
borderWidth: 1,
textStyle: {
color: "#fff",
fontSize: 10,
fontFamily: "Microsoft YaHei, sans-serif",
},
axisPointer: {
type: "cross",
label: {
backgroundColor: "rgba(0, 30, 93, 0.9)",
color: "#fff",
fontSize: 9,
},
},
},
legend: {
show: true,
bottom: "5%",
left: "center",
type: "scroll",
orient: "horizontal",
textStyle: {
color: "#ccc",
fontSize: 6,
fontFamily: "Microsoft YaHei, sans-serif",
},
itemWidth: 4,
itemHeight: 2,
itemGap: 8,
pageIconSize: 6,
pageTextStyle: {
color: "#999",
fontSize: 5,
},
padding: [0, 0, 0, 0],
data: ["1号库", "2号库", "3号库", "4号库", "5号库", "6号库", "7号库"],
},
grid: {
top: title ? "15%" : "10%",
left: "1%",
right: "1%",
bottom: "20%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: this.generateTimeLabels(),
axisLine: {
lineStyle: {
color: "#1a4a8d",
},
},
axisLabel: {
color: "#aaa",
fontSize: 6,
rotate: 0,
margin: 2,
fontFamily: "Microsoft YaHei, sans-serif",
interval: (index) => index % 2 === 0, // 修正这里:每隔4个显示一个标签
},
axisTick: {
show: true,
length: 2,
lineStyle: {
color: "#1a4a8d",
},
},
},
yAxis: {
type: "value",
// 关键修正:确保y轴最小值不会小于0
min: Math.max(0, minValue - padding),
max: maxValue + padding,
splitLine: {
lineStyle: {
color: "rgba(26, 74, 141, 0.3)",
},
},
axisLine: {
show: true,
lineStyle: {
color: "#1a4a8d",
},
},
axisLabel: {
color: "#aaa",
fontSize: 7,
margin: 3,
fontFamily: "Microsoft YaHei, sans-serif",
formatter: (value) => {
// 确保只显示整数,并且不会显示负数
return value < 0 ? "0" : Math.round(value) + unit;
},
},
axisTick: {
show: true,
length: 2,
lineStyle: {
color: "#1a4a8d",
},
},
},
series,
};
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize();
}
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.cold-storage-humidity {
@include panel-base;
flex: 1;
}
.chart-container {
width: 100%;
height: 100%;
}
</style>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.cold-storage-humidity {
@include panel-base;
flex: 1;
}
.chart-container {
width: 100%;
height: 100%;
}
</style>
@@ -1,573 +0,0 @@
<template>
<!-- 修复只保留一个根元素div解决"exactly one root element"错误 -->
<div>
<!-- 冷库库存详情弹框 -->
<div v-if="visible" class="cold-stock-modal-mask" @click="closeModal">
<div class="cold-stock-modal-container" @click.stop>
<!-- 弹框头部 -->
<div class="modal-header">
<div class="modal-title">{{ storage.title }}库存情况</div>
<button class="close-btn" @click="closeModal">×</button>
</div>
<!-- 弹框内容 -->
<div class="modal-content">
<!-- 左侧信息区按客户颜色区分 -->
<div class="left-info-panel">
<!-- 冷库基本信息 -->
<div class="storage-basic">
<div class="size-info">冷库尺寸: {{ storage.size }}</div>
<div class="env-status">
<span class="temp-status">
<i class="icon">🌡</i> {{ storage.temperature }}
</span>
<span class="humidity-status">
<i class="icon">💧</i> {{ storage.humidity }}%
</span>
</div>
</div>
<!-- 客户Tab按客户颜色区分背景 -->
<div class="customer-tabs">
<button
v-for="customer in displayCompanies"
:key="customer.name"
class="tab-btn"
:class="{ active: activeCustomer === customer.name }"
:style="{
backgroundColor: customer.background,
color: customer.color,
borderColor: activeCustomer === customer.name ? customer.color : '#1a4a8d'
}"
@click="activeCustomer = customer.name"
>
{{ customer.name }}
</button>
</div>
<!-- 产品规格统计按客户颜色区分背景 -->
<div class="stock-stat-table" :style="{ backgroundColor: getActiveCustomer().background }">
<div
v-for="(item, index) in getStockStatList()"
:key="index"
class="stat-row"
>
<div class="stat-label">{{ item.label }}</div>
<div class="stat-value">{{ item.value }}</div>
</div>
</div>
</div>
<!-- 右侧库存布局区7行11列正方形格子 -->
<div class="right-layout-panel">
<!-- 7行11列正方形格子 -->
<div class="stock-grid square-grid">
<div
v-for="(row, rowIndex) in 7"
:key="rowIndex"
class="grid-row"
>
<div
v-for="(col, colIndex) in 11"
:key="colIndex"
class="grid-cell square-cell"
:style="{
backgroundColor: getCellBgColor(allGridData[rowIndex][colIndex]),
borderColor: getCellBorderColor(allGridData[rowIndex][colIndex])
}"
@mouseenter="showCellTooltip(allGridData[rowIndex][colIndex], $event)"
@mouseleave="hideCellTooltip()"
>
<!-- 格子内容 -->
<div v-if="allGridData[rowIndex][colIndex].product" class="cell-content">
<div class="product-type">{{ allGridData[rowIndex][colIndex].product.type }}</div>
<div class="product-spec">{{ allGridData[rowIndex][colIndex].product.spec }}</div>
<div class="product-date">{{ allGridData[rowIndex][colIndex].product.date }}</div>
</div>
<!-- 底部蓝条 -->
<div class="cell-bottom-bar"></div>
<!-- 右侧件数柱状图匹配当前格子客户颜色 -->
<div class="cell-quantity-bar" :style="{
height: `${(allGridData[rowIndex][colIndex].quantity / 30) * 100}%`,
backgroundColor: getCellBorderColor(allGridData[rowIndex][colIndex])
}"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 悬浮提示框 -->
<div
v-if="tooltipVisible"
class="cell-tooltip"
:style="{ top: tooltipTop + 'px', left: tooltipLeft + 'px' }"
>
<div class="tooltip-title">库存详情</div>
<div class="tooltip-item">客户: {{ tooltipData.customer }}</div>
<div class="tooltip-item">产品: {{ tooltipData.product.type }}</div>
<div class="tooltip-item">规格: {{ tooltipData.product.spec }}</div>
<div class="tooltip-item">数量: {{ tooltipData.quantity }}</div>
<div class="tooltip-item">日期: {{ tooltipData.product.date }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ColdStorageStockModal',
props: {
visible: { type: Boolean, default: false },
storage: { type: Object, default: () => ({}) }
},
data() {
return {
activeCustomer: '',
allGridData: [], // 全局存放【所有客户】的7行11列格子数据,永久不变
tooltipVisible: false,
tooltipData: {},
tooltipTop: 0,
tooltipLeft: 0
}
},
computed: {
displayCompanies() {
const companies = this.storage?.companies || [];
const count = Number(this.storage?.stackCustomerCount || companies.length);
return companies.slice(0, Math.max(0, count));
}
},
watch: {
visible(newVal) {
if (newVal && this.displayCompanies.length > 0) {
this.activeCustomer = this.displayCompanies[0].name;
this.generateFixedAreaGridData(); // 弹窗打开时生成【固定分区】的库存数据,只执行一次
}
}
},
methods: {
closeModal() {
this.$emit('update:visible', false);
},
// 获取当前激活的客户
getActiveCustomer() {
return this.displayCompanies.find(c => c.name === this.activeCustomer) || {};
},
// 生成产品规格统计列表(匹配图片格式)
getStockStatList() {
const customer = this.getActiveCustomer();
const stock = customer.stock || {};
const groupKey = Object.keys(stock)[0];
const products = stock[groupKey]?.products || {};
const list = [{ label: groupKey, value: stock[groupKey]?.total || 0 }];
// 展示该客户下全部产品与规格,不再只取第一项
Object.entries(products).forEach(([productName, productInfo]) => {
list.push({ label: `> ${productName}`, value: productInfo?.total || 0 });
Object.entries(productInfo?.specs || {}).forEach(([specName, specInfo]) => {
list.push({ label: `>> ${specName}`, value: specInfo?.count || 0 });
});
});
return list;
},
// ✅ 核心:生成【固定分区】库存数据(左上/右上/中下 三家客户,中间隔2行空白)
generateFixedAreaGridData() {
const allData = [];
const companies = this.displayCompanies;
const totalRow = 7, totalCol = 11;
// 1. 初始化7行11列全部为空白格子
for (let row = 0; row < totalRow; row++) {
allData[row] = [];
for (let col = 0; col < totalCol; col++) {
allData[row][col] = { customer: '', product: null, quantity: 0 };
}
}
const areas = this.getLayoutAreas(companies.length, Number(this.storage?.stackLayoutVariant || 1));
companies.forEach((customer, index) => {
const area = areas[index];
if (!area) return;
this.fillCustomerArea(
allData,
customer,
area.startRow,
area.endRow,
area.startCol,
area.endCol
);
});
this.allGridData = allData;
},
getLayoutAreas(companyCount, variant = 1) {
if (companyCount <= 1) {
return variant % 2 === 0
? [{ startRow: 0, endRow: 4, startCol: 1, endCol: 9 }]
: [{ startRow: 1, endRow: 5, startCol: 2, endCol: 8 }];
}
if (companyCount === 2) {
return variant % 2 === 0
? [
{ startRow: 0, endRow: 3, startCol: 6, endCol: 10 },
{ startRow: 2, endRow: 6, startCol: 0, endCol: 4 },
]
: [
{ startRow: 0, endRow: 3, startCol: 0, endCol: 4 },
{ startRow: 5, endRow: 6, startCol: 0, endCol: 10 },
];
}
return variant % 2 === 0
? [
{ startRow: 0, endRow: 2, startCol: 6, endCol: 10 },
{ startRow: 2, endRow: 4, startCol: 0, endCol: 3 },
{ startRow: 5, endRow: 6, startCol: 6, endCol: 10 },
]
: [
{ startRow: 0, endRow: 3, startCol: 0, endCol: 4 },
{ startRow: 0, endRow: 2, startCol: 7, endCol: 10 },
{ startRow: 5, endRow: 5, startCol: 0, endCol: 10 },
];
},
// ✅ 工具方法:为指定客户填充【指定行+列范围】的库存数据
fillCustomerArea(gridData, customer, startRow, endRow, startCol, endCol) {
if (!customer || !customer.stock) return;
const stockItems = this.getCustomerStockItems(customer);
if (stockItems.length === 0) return;
const datePool = ['12.05', '12.06', '12.07', '12.08', '12.09', '12.10'];
let itemIndex = 0;
// 遍历指定行列范围,填充客户库存数据
for (let row = startRow; row <= endRow; row++) {
for (let col = startCol; col <= endCol; col++) {
const pick = stockItems[itemIndex % stockItems.length];
const date = datePool[(row + col + itemIndex) % datePool.length];
const quantity = Math.max(1, Math.round((pick.count || 20) / 8)) + ((row + col) % 3);
gridData[row][col] = {
customer: customer.name,
product: { type: pick.type, spec: pick.spec, date },
quantity
};
itemIndex++;
}
}
},
// 从客户库存中提取所有「产品 + 规格」用于单元格铺货
getCustomerStockItems(customer) {
const stock = customer?.stock || {};
const groupKey = Object.keys(stock)[0];
const products = stock[groupKey]?.products || {};
const items = [];
Object.entries(products).forEach(([productName, productInfo]) => {
Object.entries(productInfo?.specs || {}).forEach(([specName, specInfo]) => {
items.push({
type: productName,
spec: specName,
count: Number(specInfo?.count || 0),
});
});
});
// 按数量降序,主规格更容易出现,但仍会轮播到其他规格
items.sort((a, b) => b.count - a.count);
return items;
},
// 格子背景色用客户的background
getCellBgColor(cell) {
if (!cell.customer) return 'rgba(6, 30, 93, 0.3)'; // 空格子默认背景
const customer = this.displayCompanies.find(item => item.name === cell.customer)
|| this.storage.companies?.find(item => item.name === cell.customer);
return customer?.background || 'rgba(0, 242, 255, 0.2)';
},
// 格子边框色用客户的color
getCellBorderColor(cell) {
if (!cell.customer) return '#1a4a8d'; // 空格子默认边框
const customer = this.displayCompanies.find(item => item.name === cell.customer)
|| this.storage.companies?.find(item => item.name === cell.customer);
return customer?.color || '#00f2ff';
},
// 显示格子悬浮提示
showCellTooltip(cell, e) {
if (!cell.product) return;
this.tooltipData = cell;
this.tooltipTop = e.clientY + 10;
this.tooltipLeft = e.clientX + 10;
this.tooltipVisible = true;
},
// 隐藏格子悬浮提示
hideCellTooltip() {
this.tooltipVisible = false;
}
}
}
</script>
<style lang="scss" scoped>
/* 遮罩层 */
.cold-stock-modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 13, 33, 0.95);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
}
/* 弹框容器 */
.cold-stock-modal-container {
width: 95vw;
height: 95vh;
max-width: 3072px;
max-height: 2304px;
background: #030d21;
border: 2px solid #1a4a8d;
border-radius: 8px;
padding: 20px;
padding-bottom: 40px;
box-sizing: border-box;
color: #fff;
}
/* 弹框头部 */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
.modal-title {
font-size: 22px;
font-weight: bold;
color: #00f2ff;
}
.close-btn {
background: transparent;
border: none;
color: #aaa;
font-size: 32px;
cursor: pointer;
&:hover {
color: #00f2ff;
}
}
}
/* 弹框内容 */
.modal-content {
display: flex;
gap: 30px;
height: calc(100% - 30px);
}
/* 左侧信息区 */
.left-info-panel {
width: 320px;
display: flex;
flex-direction: column;
gap: 20px;
/* 冷库基本信息 */
.storage-basic {
background: rgba(26, 74, 141, 0.3);
border: 1px solid #1a4a8d;
border-radius: 6px;
padding: 15px;
.size-info {
font-size: 18px;
color: #aaa;
margin-bottom: 12px;
}
.env-status {
display: flex;
gap: 15px;
font-size: 18px;
.icon {
margin-right: 6px;
}
.temp-status {
color: #00f2ff;
}
.humidity-status {
color: #66a3ff;
}
}
}
/* 客户Tab:按客户颜色区分 */
.customer-tabs {
display: flex;
gap: 10px;
flex-wrap: wrap;
.tab-btn {
border: 1px solid #1a4a8d;
border-radius: 4px;
padding: 8px 15px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
&.active {
font-weight: bold;
box-shadow: 0 0 8px rgba(0, 242, 255, 0.4);
}
}
}
/* 产品规格统计表:按客户颜色区分背景 */
.stock-stat-table {
flex: 1;
border: 1px solid #1a4a8d;
border-radius: 6px;
overflow: hidden;
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
font-size: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
&:last-child {
border-bottom: none;
}
.stat-label {
color: #fff;
}
.stat-value {
color: #00f2ff;
font-weight: bold;
}
}
}
}
/* 右侧库存布局区:7行11列正方形格子 */
.right-layout-panel {
flex: 1;
background: rgba(26, 74, 141, 0.2);
border: 1px solid #1a4a8d;
border-radius: 6px;
padding: 20px;
overflow: auto;
// 隐藏滚动条(兼容各浏览器)
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
/* 正方形格子容器 */
.stock-grid.square-grid {
display: flex;
flex-direction: column;
gap: 1px;
height: 100%;
/* 行容器:控制每行高度 */
.grid-row {
display: flex;
gap: 1px;
flex: 1; /* 每行高度均分,确保格子是正方形 */
/* 正方形格子:宽高比1:1 */
.grid-cell.square-cell {
flex: 1;
position: relative;
overflow: hidden;
border: 1px solid #1a4a8d;
border-radius: 4px;
/* 强制正方形:宽高比1:1 */
aspect-ratio: 1 / 1;
min-width: 0;
/* 格子内容 */
.cell-content {
padding: 8px;
font-size: 14px;
color: #fff;
line-height: 1.4;
}
/* 底部蓝条 */
.cell-bottom-bar {
position: absolute;
bottom: 6px;
left: 5%;
width: 90%;
height: 3px;
background: #00f2ff;
}
/* 右侧件数柱状图:按客户颜色区分 */
.cell-quantity-bar {
position: absolute;
top: 0;
right: 0;
width: 6px;
}
}
}
}
}
/* 悬浮提示框 */
.cell-tooltip {
position: fixed;
background: rgba(0, 30, 93, 0.95);
border: 1px solid #00f2ff;
border-radius: 6px;
padding: 15px;
z-index: 10000;
min-width: 240px;
box-shadow: 0 0 15px rgba(0, 242, 255, 0.3);
.tooltip-title {
font-size: 18px;
font-weight: bold;
color: #00f2ff;
margin-bottom: 10px;
border-bottom: 1px solid #1a4a8d;
padding-bottom: 6px;
}
.tooltip-item {
font-size: 16px;
color: #aaa;
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
}
}
</style>
@@ -1,298 +0,0 @@
<template>
<div class="panel cold-storage-temperature">
<div class="panel-title">温度监控</div>
<div ref="chart" class="chart-container"></div>
</div>
</template>
<script>
export default {
name: 'ColdStorageTemperature',
props: {
temperatureData: {
type: Array,
default: () => []
}
},
data() {
return {
chartInstance: null,
// 定义默认填充温度值
defaultTempValue: 4.8
}
},
watch: {
temperatureData: {
handler(newData) {
if (newData && newData.length > 0) {
this.updateChart(newData);
}
},
immediate: true,
deep: true
}
},
mounted() {
this.initChart();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose();
}
window.removeEventListener('resize', this.handleResize);
},
methods: {
initChart() {
this.chartInstance = this.$echarts.init(this.$refs.chart, null, {
devicePixelRatio: window.devicePixelRatio || 1,
renderer: 'canvas'
});
// 使用默认数据初始化(20个点),替换0为4.8
const defaultData = [
new Array(20).fill(this.defaultTempValue),
new Array(20).fill(this.defaultTempValue),
new Array(20).fill(this.defaultTempValue),
new Array(20).fill(this.defaultTempValue),
new Array(20).fill(this.defaultTempValue),
new Array(20).fill(this.defaultTempValue),
new Array(20).fill(this.defaultTempValue)
];
const option = this.createMultiLineChartOption('冷库库温实时监控 (°C)', defaultData, '°C');
this.chartInstance.setOption(option);
},
updateChart(data) {
if (!this.chartInstance) {
this.initChart();
}
// 确保数据格式正确,每个冷库有20个数据点
const formattedData = [];
for (let i = 0; i < 7; i++) {
if (data[i] && Array.isArray(data[i]) && data[i].length >= 20) {
formattedData.push(data[i].slice(-20)); // 取最后20个点
} else if (data[i] && Array.isArray(data[i])) {
// 如果数据不足20个,用4.8填充前面(替换0)
const paddedData = new Array(20).fill(this.defaultTempValue);
const startIndex = 20 - data[i].length;
for (let j = 0; j < data[i].length; j++) {
paddedData[startIndex + j] = data[i][j];
}
formattedData.push(paddedData);
} else {
// 如果没有数据,使用4.8数组(替换0)
formattedData.push(new Array(20).fill(this.defaultTempValue));
}
}
const option = this.createMultiLineChartOption('冷库库温实时监控 (°C)', formattedData, '°C');
this.chartInstance.setOption(option);
},
// 生成时间标签(20个点)
generateTimeLabels() {
const labels = [];
const now = new Date();
// 生成最近20个小时的标签
for (let i = 0; i < 20; i++) {
const time = new Date(now.getTime() - (19 - i) * 3600000); // 从最早的时间开始
const hour = String(time.getHours()).padStart(2, '0');
labels.push(`${hour}`);
}
return labels;
},
createMultiLineChartOption(title, data, unit) {
const allValues = data.flat().filter(val => !isNaN(val));
const minValue = allValues.length > 0 ? Math.min(...allValues) : 0;
const maxValue = allValues.length > 0 ? Math.max(...allValues) : 10;
const valueRange = maxValue - minValue;
const padding = valueRange * 0.15 || 3;
const colors = ['#00f2ff', '#ffcc00', '#ff3366', '#00ff99', '#b388ff', '#ff9e80', '#80d8ff'];
const series = [];
for (let i = 0; i < data.length; i++) {
series.push({
name: `${i + 1}号库`,
type: 'line',
smooth: true,
symbol: 'none',
lineStyle: {
width: 2,
color: colors[i],
opacity: 0.8
},
emphasis: {
lineStyle: {
width: 3,
opacity: 1
},
focus: 'series'
},
data: data[i]
});
}
return {
backgroundColor: 'transparent',
title: title ? {
text: title,
left: 'center',
textStyle: {
color: '#00f2ff',
fontSize: 12,
fontFamily: 'Microsoft YaHei, sans-serif'
},
top: '2%'
} : { show: false },
// 核心修改:完全按照湿度图表的tooltip配置
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 30, 93, 0.9)',
borderColor: '#1a4a8d',
borderWidth: 1,
textStyle: {
color: '#fff',
fontSize: 10,
fontFamily: 'Microsoft YaHei, sans-serif'
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: 'rgba(0, 30, 93, 0.9)',
color: '#fff',
fontSize: 9
}
},
// 移除自定义formatter,使用ECharts默认格式(和湿度图表一致)
// 仅保留数值格式化,确保小数显示规范
formatter: (params) => {
let content = params[0].axisValueLabel || params[0].axisValue;
params.forEach((item) => {
const formattedValue = Number(item.value).toFixed(1);
content += `<br/>${item.seriesName}${formattedValue}${unit}`;
});
return content;
}
},
legend: {
show: true,
bottom: '5%',
left: 'center',
type: 'scroll',
orient: 'horizontal',
textStyle: {
color: '#ccc',
fontSize: 6,
fontFamily: 'Microsoft YaHei, sans-serif'
},
itemWidth: 4,
itemHeight: 2,
itemGap: 8,
pageIconSize: 6,
pageTextStyle: {
color: '#999',
fontSize: 5
},
padding: [0, 0, 0, 0],
data: ['1号库', '2号库', '3号库', '4号库', '5号库', '6号库', '7号库']
},
grid: {
top: title ? '15%' : '10%',
left: '1%',
right: '1%',
bottom: '20%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: this.generateTimeLabels(),
axisLine: {
lineStyle: {
color: '#1a4a8d'
}
},
axisLabel: {
color: '#aaa',
fontSize: 6,
rotate: 0,
margin: 2,
fontFamily: 'Microsoft YaHei, sans-serif',
interval: (index) => index % 2 === 0
},
axisTick: {
show: true,
length: 2,
lineStyle: {
color: '#1a4a8d',
},
},
},
yAxis: {
type: 'value',
min: minValue - padding,
max: maxValue + padding,
splitLine: {
lineStyle: {
color: 'rgba(26, 74, 141, 0.3)'
}
},
axisLine: {
show: true,
lineStyle: {
color: '#1a4a8d',
},
},
axisLabel: {
color: '#aaa',
fontSize: 7,
margin: 3,
fontFamily: 'Microsoft YaHei, sans-serif',
// 保持数值格式化,和湿度图表逻辑一致
formatter: (value) => {
return Number(value).toFixed(1) + unit;
}
},
axisTick: {
show: true,
length: 2,
lineStyle: {
color: '#1a4a8d',
},
},
// 移除强制interval,和湿度图表保持一致
// interval: 1
},
series
}
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize();
}
}
}
}
</script>
<style lang="scss" scoped>
@import '@/styles/tracing-screen.scss';
.cold-storage-temperature {
@include panel-base;
flex: 1;
}
.chart-container {
width: 100%;
height: 100%;
}
</style>
@@ -1,134 +0,0 @@
<template>
<div class="panel cold-storage-utilization">
<div class="panel-title">7个冷库</div>
<div ref="chart" class="chart-container"></div>
</div>
</template>
<script>
export default {
name: 'ColdStorageUtilization',
data() {
return {
chartInstance: null
}
},
mounted() {
this.initChart()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose()
}
window.removeEventListener('resize', this.handleResize)
},
methods: {
initChart() {
this.chartInstance = this.$echarts.init(this.$refs.chart, null, {
devicePixelRatio: window.devicePixelRatio || 1,
renderer: 'canvas'
})
const option = {
backgroundColor: 'transparent',
title: {
text: '利用率',
left: 'center',
bottom: '20%',
textStyle: {
color: '#00f2ff',
fontSize: 12,
fontFamily: 'Microsoft YaHei, sans-serif'
}
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c}%',
backgroundColor: 'rgba(0, 30, 93, 0.8)',
borderColor: '#1a4a8d',
borderWidth: 1,
textStyle: {
color: '#fff',
fontFamily: 'Microsoft YaHei, sans-serif'
}
},
series: [{
name: '冷库利用率',
type: 'pie',
radius: ['65%', '85%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 0,
borderColor: 'transparent',
borderWidth: 0
},
label: {
show: true,
position: 'center',
formatter: '0%',
textStyle: {
fontSize: 24,
fontWeight: 'bold',
color: '#00f2ff',
fontFamily: 'Microsoft YaHei, sans-serif'
}
},
emphasis: {
label: {
show: true,
fontSize: 24,
fontWeight: 'bold',
color: '#00f2ff',
fontFamily: 'Microsoft YaHei, sans-serif',
formatter: '0%'
}
},
labelLine: {
show: false
},
data: [
{
value: 0,
name: '已使用',
itemStyle: {
color: '#00f2ff'
}
},
{
value: 100,
name: '未使用',
itemStyle: {
color: '#fff'
}
}
]
}]
}
this.chartInstance.setOption(option)
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize()
}
}
}
}
</script>
<style lang="scss" scoped>
@import '@/styles/tracing-screen.scss';
.cold-storage-utilization {
@include panel-base;
flex: 1;
}
.chart-container {
width: 100%;
height: 100%;
}
</style>
@@ -1,232 +0,0 @@
<template>
<div class="panel export-orchard">
<div class="panel-title">出口果园资质</div>
<div class="orchard-content">
<div class="registered-products">注册登记产品: {{ products }}</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">种植面积</div>
<div class="stat-value">{{ area }}<span class="stat-unit"></span></div>
</div>
<div class="stat-item">
<div class="stat-label">预估产量</div>
<div class="stat-value">{{ EstimatedOutput }}<span class="stat-unit">万斤</span></div>
</div>
<div class="export-info">
<div class="export-icon">
<div class="export-circle">
<div class="export-inner">
<span>出口</span>
<div class="leaf leaf-1"></div>
<div class="leaf leaf-2"></div>
</div>
</div>
</div>
<div class="export-text">56</div>
<div class="export-text">出口果园</div>
</div>
</div>
<div class="packing-factory">出口水果包装厂资质</div>
</div>
</div>
</template>
<script>
import huobanAuth from '@/utils/huobanAuth'
// ✅ 1. 引入你封装好的刷新工具方法
import autoRefresh from "@/utils/autoRefresh";
export default {
name: "ExportOrchard",
props: {},
data() {
return {
goodsList: [],
products: null,
area: null,
EstimatedOutput: null,
timer: null, // ✅ 只需要定义定时器变量接收返回值
};
},
mounted() {
// ✅ 2. 一行代码启动自动刷新,完美适配你的工具方法
// 参数1:你的请求方法 this.getData
// 参数2:刷新时间 5000 = 5秒,可自定义修改,比如3000=3秒,60000=1分钟
this.timer = autoRefresh.startRefresh(this.getData, 60000000);
},
beforeDestroy() {
// ✅ 3. 一行代码清除定时器,调用工具方法
autoRefresh.stopRefresh(this.timer);
},
watch: {},
methods: {
async getData() {
// 对齐 web-zx:系统 token 的 /huoban2/data(非 webData
let res = await huobanAuth.queryData({
tableId: 2100000000016030,
limit: 100,
conditions: [],
relations: [],
});
if (res.code === 1 && res.data && res.data.length >= 1) {
const row = res.data[0];
console.log("果园信息", res);
this.products = this.formatField(row.registeredProductTypes);
this.area = this.formatField(row.plantingArea);
this.EstimatedOutput = this.formatField(row.estimatedYield);
} else {
console.error("出口果园资质 API 错误:", res);
}
},
formatField(val) {
if (val == null || val === '') return '';
if (Array.isArray(val)) {
return val
.map((item) => {
if (item == null) return '';
if (typeof item === 'object') {
return item.title || item.name || item.text || item.value || '';
}
return String(item);
})
.filter(Boolean)
.join('、');
}
if (typeof val === 'object') {
return val.title || val.name || val.text || val.value || '';
}
return val;
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.export-orchard {
@include panel-base;
flex: 2;
}
.orchard-content {
display: flex;
flex-direction: column;
height: calc(100% - 20px);
gap: 10px;
}
.registered-products {
font-size: 14px;
color: #aaa;
margin-bottom: 5px;
}
.stats-grid {
display: flex;
align-items: center;
gap: 15px;
justify-content: space-between;
.stat-item {
flex: 1;
.stat-label {
font-size: 10px;
color: #aaa;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
color: $highlight;
font-weight: bold;
}
.stat-unit {
font-size: 10px;
color: #66a3ff;
margin-left: 2px;
}
}
}
.export-info {
display: flex;
flex-direction: column;
align-items: center;
.export-icon {
width: 50px;
height: 50px;
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 5px;
background-image: linear-gradient(
135deg,
rgba(76, 175, 80, 0.2),
rgba(76, 175, 80, 0.1)
);
}
.export-circle {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #4caf50, #8bc34a);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
span {
font-size: 12px;
color: white;
font-weight: bold;
z-index: 1;
}
}
.leaf {
position: absolute;
z-index: 0;
&.leaf-1 {
top: 5px;
left: 5px;
width: 12px;
height: 12px;
background: #8bc34a;
border-radius: 50% 50% 0 50%;
transform: rotate(45deg);
}
&.leaf-2 {
top: 8px;
left: 10px;
width: 10px;
height: 10px;
background: #4caf50;
border-radius: 50% 50% 50% 0;
transform: rotate(135deg);
}
}
.export-text {
font-size: 14px;
color: #aaa;
}
}
.packing-factory {
font-size: 14px;
color: #aaa;
}
</style>
@@ -1,426 +0,0 @@
<template>
<div class="panel export-orders">
<div class="panel-title">出口订单情况</div>
<div class="export-content">
<!-- 左侧统计信息 -->
<div class="export-stats">
<div class="stat-item export-stat-item">
<div class="stat-label">出口批次</div>
<div class="stat-value">
<span class="value">80</span>
<span class="unit"></span>
</div>
</div>
<div class="stat-item export-stat-item">
<div class="stat-label">累计出口</div>
<div class="stat-value">
<span class="value">588</span>
<span class="unit">万斤</span>
</div>
</div>
</div>
<!-- 中间环形图 -->
<div class="export-chart">
<div ref="pieChart" class="chart"></div>
</div>
<!-- 右侧柱状图 -->
<div class="export-countries">
<div ref="legendChart" class="legend-chart"></div>
<div ref="barChart" class="bar-chart"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ExportOrders",
data() {
return {
pieChartInstance: null,
barChartInstance: null,
legendChartInstance: null,
pieData: [
{ value: 21, name: "泰国", itemStyle: { color: "#00f2ff" } },
{ value: 16, name: "越南", itemStyle: { color: "#4caf50" } },
{ value: 8, name: "新加坡", itemStyle: { color: "#ff9800" } },
{ value: 4, name: "印尼", itemStyle: { color: "#9c27b0" } },
],
};
},
mounted() {
this.initPieChart();
this.initBarChart();
this.initLegendChart();
window.addEventListener("resize", this.handleResize);
},
beforeDestroy() {
if (this.pieChartInstance) this.pieChartInstance.dispose();
if (this.barChartInstance) this.barChartInstance.dispose();
if (this.legendChartInstance) this.legendChartInstance.dispose();
window.removeEventListener("resize", this.handleResize);
},
methods: {
initPieChart() {
this.pieChartInstance = this.$echarts.init(this.$refs.pieChart);
const option = {
backgroundColor: "transparent",
tooltip: {
trigger: "item",
formatter: "{b}: {c}万斤 ({d}%)",
backgroundColor: "rgba(255, 255, 255, 0.95)",
borderColor: "#00f2ff",
borderWidth: 1,
textStyle: {
color: "#333",
},
},
legend: {
show: true,
top: "5%",
left: "center",
type: "scroll",
textStyle: {
color: "#fff",
fontSize: 12,
},
itemWidth: 12,
itemHeight: 12,
itemGap: 10,
},
series: [
{
name: "出口国家",
type: "pie",
radius: ["35%", "60%"],
center: ["50%", "55%"],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 5,
borderColor: "rgba(3, 13, 33, 0.5)",
borderWidth: 1,
},
label: {
show: true,
color: "#fff",
fontSize: 11,
formatter: "{c}万斤",
},
labelLine: {
show: true,
length: 8,
length2: 5,
lineStyle: {
color: "#00f2ff",
width: 1,
},
},
emphasis: {
label: {
show: true,
fontSize: "12",
fontWeight: "bold",
color: "#fff",
},
},
data: this.pieData.sort(() => Math.random() - 0.5),
},
],
};
this.pieChartInstance.setOption(option);
},
initBarChart() {
this.barChartInstance = this.$echarts.init(this.$refs.barChart);
const option = {
backgroundColor: "transparent",
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
backgroundColor: "rgba(255, 255, 255, 0.95)",
borderColor: "#00f2ff",
borderWidth: 1,
textStyle: {
color: "#333",
},
},
grid: {
left: "8%",
right: "8%",
bottom: "15%",
top: "20%",
containLabel: true,
},
xAxis: {
type: "category",
data: ["泰国", "越南", "俄罗斯", "新加坡", "印尼"],
axisLine: {
lineStyle: {
color: "rgba(26, 74, 141, 0.5)",
},
},
axisLabel: {
color: "#aaa",
fontSize: 9,
rotate: 0,
},
},
yAxis: {
type: "value",
name: "(万斤)",
nameTextStyle: {
color: "#aaa",
fontSize: 9,
},
min: 0,
axisLine: {
lineStyle: {
color: "rgba(26, 74, 141, 0.5)",
},
},
axisLabel: {
color: "#aaa",
fontSize: 9,
},
splitLine: {
lineStyle: {
color: "rgba(26, 74, 141, 0.2)",
},
},
},
series: [
{
name: "直接出口",
type: "bar",
barWidth: "30%",
data: [22, 20, 18, 15, 10],
itemStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#00f2ff" },
{ offset: 1, color: "#1a4a8d" },
]),
},
emphasis: {
itemStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#00f2ff" },
{ offset: 1, color: "#2d6cb5" },
]),
},
},
// 关键修改:显示直接出口的数值标签
label: {
show: true,
position: "top",
color: "#00f2ff",
fontSize: 9,
fontWeight: "bold",
distance: 2
},
},
{
name: "代加工",
type: "bar",
barWidth: "30%",
data: [18, 15, 13, 10, 7],
itemStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#66a3ff" },
{ offset: 1, color: "#1a4a8d" },
]),
},
emphasis: {
itemStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#66a3ff" },
{ offset: 1, color: "#2d6cb5" },
]),
},
},
// 关键修改:显示代加工的数值标签
label: {
show: true,
position: "top",
color: "#66a3ff",
fontSize: 9,
fontWeight: "bold",
distance: 2
},
},
],
};
this.barChartInstance.setOption(option);
},
initLegendChart() {
this.legendChartInstance = this.$echarts.init(this.$refs.legendChart);
const option = {
backgroundColor: "transparent",
legend: {
show: true,
data: ["直接出口", "代加工"],
orient: "horizontal",
top: "center",
left: "center",
textStyle: {
color: "#aaa",
fontSize: 10,
},
itemWidth: 10,
itemHeight: 6,
selectedMode: true,
},
};
this.legendChartInstance.setOption(option);
// 图例交互与主图表同步
this.legendChartInstance.on("legendselectchanged", (params) => {
this.barChartInstance.dispatchAction({
type: "legendToggleSelect",
name: params.name,
});
});
this.barChartInstance.on("legendselectchanged", (params) => {
this.legendChartInstance.dispatchAction({
type: "legendToggleSelect",
name: params.name,
});
});
},
handleResize() {
if (this.pieChartInstance) this.pieChartInstance.resize();
if (this.barChartInstance) this.barChartInstance.resize();
if (this.legendChartInstance) this.legendChartInstance.resize();
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.export-orders {
@include panel-base;
flex: 1;
}
.export-content {
display: flex;
height: calc(100% - 30px);
gap: 12px;
padding: 5px;
}
.export-stats {
flex: 0.3;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 8px;
gap: 8px;
border: 1px solid rgba(26, 74, 141, 0.5);
border-radius: 6px;
}
.export-stat-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
background: linear-gradient(
135deg,
rgba(6, 30, 93, 0.8),
rgba(6, 30, 93, 0.6)
);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 8px;
padding: 12px 8px;
box-shadow: 0 2px 10px rgba(0, 242, 255, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
.stat-label {
font-size: 10px;
color: #99ccff;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.3px;
min-width: 60px;
text-align: center;
}
.stat-value {
display: flex;
flex-direction: column;
align-items: center;
.value {
font-size: 20px;
color: $highlight;
font-weight: bold;
text-shadow: 0 0 6px rgba(0, 242, 255, 0.5);
}
.unit {
font-size: 11px;
color: #99ccff;
margin-top: 2px;
}
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.2);
}
}
.export-chart {
flex: 2;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(26, 74, 141, 0.5);
border-radius: 6px;
padding: 5px;
.chart {
width: 100%;
height: 100%;
}
}
.export-countries {
flex: 2;
display: flex;
flex-direction: column;
border: 1px solid rgba(26, 74, 141, 0.5);
border-radius: 6px;
padding: 5px;
}
.legend-chart {
height: 30px;
width: 100%;
margin-bottom: 5px;
}
.bar-chart {
width: 100%;
height: calc(100% - 35px);
}
</style>
@@ -1,338 +0,0 @@
<template>
<div class="panel fruit-size-chart">
<div class="panel-title">果体大小</div>
<div class="chart-container">
<div ref="chart" class="chart"></div>
</div>
</div>
</template>
<script>
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from "@/utils/autoRefresh";
export default {
name: "FruitSizeChart",
data() {
return {
chartInstance: null,
loading: false, // 请求锁 防止重复请求/请求堆积
timer: null, // 定时器变量 用于自动刷新
// 果径统计数据-万斤,和x轴的55/60/65/70/75/80/85mm一一对应
sizeWeightData: [0, 0, 0, 0, 0, 0, 0],
inventoryCacheAt: 0,
inventoryCacheTTL: 10 * 60 * 1000, // 缓存10分钟,减少高频全表扫描
};
},
mounted() {
this.initChart();
// ✅ 开启5分钟自动刷新,复用公共刷新方法
this.timer = autoRefresh.startRefresh(this.getSizeWeightData, 5 * 60 * 1000);
window.addEventListener("resize", this.handleResize);
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose();
}
window.removeEventListener("resize", this.handleResize);
// ✅ 清除定时器 防止内存泄漏
autoRefresh.stopRefresh(this.timer);
},
methods: {
// ✅ 分页拉取全部库存数据 - 和分级图表完全一致的请求逻辑
async getSizeWeightData() {
if (Date.now() - this.inventoryCacheAt < this.inventoryCacheTTL) {
return;
}
if (this.loading) return;
this.loading = true;
let allInventoryData = [];
let offset = 0;
const limit = 100;
let hasMoreData = true;
try {
while (hasMoreData) {
let res = await huobanAuth.queryData({
tableId: 2100000000014911,
limit: limit,
offset: offset,
conditions: [
{ fieldName: "chanPinLeiXing", value: [3], operator: "eq" },
{ fieldName: "kuCunZongZhongLiangJin", value: 0, operator: "gt" },
],
relations: [],
});
if (res.code === 1) {
console.log('====================,',res.data);
if (res.data && res.data.length > 0) {
allInventoryData = allInventoryData.concat(res.data);
hasMoreData = res.data.length === limit;
offset += limit;
} else {
hasMoreData = false;
}
} else {
console.error("果体大小数据API错误:", res);
hasMoreData = false;
}
await new Promise((r) => setTimeout(r, 100));
}
// 解析规格+统计重量+转换单位
this.processSizeWeightData(allInventoryData);
this.inventoryCacheAt = Date.now();
} catch (err) {
console.error("获取果体大小数据出错:", err);
} finally {
this.loading = false;
}
},
// ✅ 核心:解析Size规格 + 按果径累加重量 + 斤转万斤 + 保底数据逻辑
processSizeWeightData(allData) {
// console.log("果体大小全部分页数据", allData);
// 初始化7个果径的重量(斤),顺序:55、60、65、70、75、80、85mm
let weight55 = 0,
weight60 = 0,
weight65 = 0;
let weight70 = 0,
weight75 = 0,
weight80 = 0,
weight85 = 0;
allData.forEach((item) => {
const size = item.customerAbbreviation || "";
const weight = Number(item.kuCunZongZhongLiangJin) || 0;
// 过滤E果,不参与统计
if (size === "E果") return;
// ✅ 精准匹配所有规格,按规则解析果径并累加重量
if (size.includes("55")) {
weight55 += weight;
} else if (size.includes("60")) {
weight60 += weight;
} else if (size.includes("65")) {
weight65 += weight;
} else if (size.includes("70")) {
weight70 += weight;
} else if (size.includes("75")) {
weight75 += weight;
} else if (size.includes("80")) {
weight80 += weight;
} else if (size.includes("85")) {
weight85 += weight;
}
});
// 计算总重量(斤)
const totalWeightJin = weight55 + weight60 + weight65 + weight70 + weight75 + weight80 + weight85;
// 保底数据逻辑:如果总重量大于0,但 A, B, C 三个主要等级中有任意两个为0,则用保底数据覆盖
// 在此场景下,我们可以认为 A, B, C 对应的是中间的主流果径,例如 65, 70, 75mm
if (totalWeightJin > 0) {
// 检查 A, B, C (65, 70, 75) 中有多少个为0
const zeroCount = [weight65, weight70, weight75].filter(w => w === 0).length;
if (zeroCount >= 2) {
// 生成保底数据,将总重量按一个合理的比例分配给7个果径
// 例如,按 1:2:3:4:3:2:1 的比例模拟一个常见的正态分布
const ratios = [1, 2, 3, 4, 3, 2, 1]; // 65, 70, 75 是峰值
const totalRatio = ratios.reduce((sum, r) => sum + r, 0);
weight55 = (totalWeightJin * ratios[0]) / totalRatio;
weight60 = (totalWeightJin * ratios[1]) / totalRatio;
weight65 = (totalWeightJin * ratios[2]) / totalRatio;
weight70 = (totalWeightJin * ratios[3]) / totalRatio;
weight75 = (totalWeightJin * ratios[4]) / totalRatio;
weight80 = (totalWeightJin * ratios[5]) / totalRatio;
weight85 = (totalWeightJin * ratios[6]) / totalRatio;
}
} else if (totalWeightJin === 0) {
// 如果总重量也为0,则生成随机的保底总重量,再按比例分配
const randomTotalWeightJin = Math.floor(Math.random() * 31 + 30) * 10000; // 随机生成30-60万斤
const ratios = [1, 2, 3, 4, 3, 2, 1];
const totalRatio = ratios.reduce((sum, r) => sum + r, 0);
weight55 = (randomTotalWeightJin * ratios[0]) / totalRatio;
weight60 = (randomTotalWeightJin * ratios[1]) / totalRatio;
weight65 = (randomTotalWeightJin * ratios[2]) / totalRatio;
weight70 = (randomTotalWeightJin * ratios[3]) / totalRatio;
weight75 = (randomTotalWeightJin * ratios[4]) / totalRatio;
weight80 = (randomTotalWeightJin * ratios[5]) / totalRatio;
weight85 = (randomTotalWeightJin * ratios[6]) / totalRatio;
}
// ✅ 核心转换:斤 转 万斤,保留2位小数,统一单位
this.sizeWeightData = [
(weight55 / 10000).toFixed(2) * 1,
(weight60 / 10000).toFixed(2) * 1,
(weight65 / 10000).toFixed(2) * 1,
(weight70 / 10000).toFixed(2) * 1,
(weight75 / 10000).toFixed(2) * 1,
(weight80 / 10000).toFixed(2) * 1,
(weight85 / 10000).toFixed(2) * 1,
];
// 正确写法:数字开头的属性名用引号包裹即可
// console.log("果体大小统计完成(万斤)", {
// "55mm": this.sizeWeightData[0],
// "60mm": this.sizeWeightData[1],
// "65mm": this.sizeWeightData[2],
// "70mm": this.sizeWeightData[3],
// "75mm": this.sizeWeightData[4],
// "80mm": this.sizeWeightData[5],
// "85mm": this.sizeWeightData[6],
// });
// 重新渲染图表
this.updateChart();
},
initChart() {
this.chartInstance = this.$echarts.init(this.$refs.chart, null, {
devicePixelRatio: window.devicePixelRatio || 1,
renderer: "canvas",
});
this.updateChart();
},
// ✅ 抽离图表更新方法,数据变化后重新渲染
updateChart() {
const option = {
backgroundColor: "transparent",
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
backgroundColor: "rgba(255, 255, 255, 0.95)",
borderColor: "#00f2ff",
borderWidth: 1,
textStyle: {
color: "#333",
fontFamily: "Microsoft YaHei, sans-serif",
},
formatter: "{b}: {c} 万斤", // ✅ 优化tooltip,显示 果径+万斤数
},
legend: {
show: false,
},
grid: {
left: "5%",
right: "5%",
bottom: "10%",
top: "10%",
containLabel: true,
},
xAxis: [
{
type: "category",
data: ["55mm", "60mm", "65mm", "70mm", "75mm", "80mm", "85mm"],
axisLine: {
lineStyle: {
color: "rgba(26, 74, 141, 0.5)",
},
},
axisLabel: {
color: "#aaa",
fontSize: 9,
interval: 0, // 强制显示所有标签
rotate: 25, // 如果标签文字过长或较多导致重叠,可以设置旋转角度,例如 30
fontFamily: "Microsoft YaHei, sans-serif",
},
axisTick: {
show: false,
},
},
],
yAxis: [
{
type: "value",
name: "(万斤)",
nameTextStyle: {
color: "#aaa",
fontSize: 9,
fontFamily: "Microsoft YaHei, sans-serif",
},
axisLine: {
lineStyle: {
color: "rgba(26, 74, 141, 0.5)",
},
},
axisLabel: {
color: "#aaa",
fontSize: 9,
fontFamily: "Microsoft YaHei, sans-serif",
},
splitLine: {
lineStyle: {
color: "rgba(26, 74, 141, 0.2)",
},
},
},
],
series: [
{
name: "果体大小",
type: "bar",
barWidth: "60%",
data: this.sizeWeightData, // ✅ 赋值动态统计的重量数据
itemStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#00f2ff" },
{ offset: 1, color: "#1a4a8d" },
]),
},
emphasis: {
itemStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#00f2ff" },
{ offset: 1, color: "#2d6cb5" },
]),
},
},
label: {
show: true, // 是否显示数值标签
position: "top", // 标签位置:top(柱顶)、bottom(柱底)、inside(柱内)
distance: 5, // 数值和柱顶的间距,5px刚好不拥挤
color: "#00f2ff", // 数值文字颜色,和柱子渐变浅色调一致,美观统一
fontSize: 9, // 字号和坐标轴文字一致,协调
fontFamily: "Microsoft YaHei, sans-serif", // 字体统一
formatter: "{c}", // 显示内容:{c}代表当前柱子的数值(已转好的 万斤 单位)
},
},
],
};
this.chartInstance.setOption(option, true);
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize();
}
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.fruit-size-chart {
@include panel-base;
flex: 3;
}
.chart-container {
height: calc(100% - 20px);
display: flex;
align-items: center;
justify-content: center;
}
.chart {
width: 100%;
height: 100%;
}
</style>
@@ -1,471 +0,0 @@
<template>
<div class="panel grading-chart">
<div class="panel-title">分级数据</div>
<div class="chart-container">
<div ref="chart" class="chart-main"></div>
<div class="chart-legend">
<div
style="font-size: 10px; color: var(--highlight); margin-bottom: 10px"
>
(万斤)
</div>
<div class="legend-items">
<div
v-for="item in legendData"
:key="item.name"
class="legend-item"
:data-level="item.name"
@click="toggleLegend(item)"
:style="{
opacity: item.visible ? 1 : 0.5,
filter: item.visible ? 'none' : 'grayscale(100%)',
}"
>
<div
class="legend-color"
:style="{ backgroundColor: item.color }"
></div>
<div class="legend-text">{{ item.name }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import * as echarts from "echarts";
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from "@/utils/autoRefresh";
export default {
name: "GradingChart",
data() {
return {
InventoryData: 0, // 总库存(万斤)
loading: false,
chartInstance: null,
originalData: [], // 改为动态赋值,不再写死数据
currentData: [],
legendData: [
{ name: "A级", color: "#00f2ff", visible: true },
{ name: "B级", color: "#4caf50", visible: true },
{ name: "C级", color: "#ffeb3b", visible: true },
{ name: "DE级", color: "#ff9800", visible: true },
], // 固定图例配色和名称
timer: null, // 定时器变量
};
},
mounted() {
this.initChart();
this.initLegendData();
// ✅ 开启30秒自动刷新 核心方法
this.timer = autoRefresh.startRefresh(this.getInventoryData, 1800000);
window.addEventListener("resize", this.handleResize);
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose();
}
window.removeEventListener("resize", this.handleResize);
// ✅ 清除定时器 防止内存泄漏
autoRefresh.stopRefresh(this.timer);
},
methods: {
// ✅ 请求库存数据【完整版:分页拉取全部数据 + 分级统计 + 单位转换 + 饼图赋值】
async getInventoryData() {
if (this.loading) return;
this.loading = true;
let allInventoryData = [];
let offset = 0;
const limit = 100;
let hasMoreData = true;
try {
while (hasMoreData) {
let res = await huobanAuth.queryData({
tableId: 2100000000014911,
limit: limit,
offset: offset,
conditions: [
{ fieldName: "productType", value: [3], operator: "eq" },
{ fieldName: "Totalweight", value: 0, operator: "gt" },
],
relations: [],
});
if (res.code === 1) {
if (res.data && res.data.length > 0) {
allInventoryData = allInventoryData.concat(res.data);
hasMoreData = res.data.length === limit;
offset += limit;
} else {
hasMoreData = false;
}
} else {
console.error("库存数据API错误:", res);
hasMoreData = false;
}
await new Promise((r) => setTimeout(r, 100));
}
// 所有数据拉取完成后 处理分级+统计+饼图赋值
this.processInventoryData(allInventoryData);
} catch (err) {
console.error("获取库存数据出错:", err);
} finally {
this.loading = false;
}
},
// ✅ 核心:处理库存数据+精准分级统计+单位转换+饼图数据组装 【所有核心逻辑都在这里】
// processInventoryData(allData) {
// console.log("库存全部分页数据", allData);
// // 1. 初始化各等级重量 (单位:斤) 初始值为0
// let weightA = 0;
// let weightB = 0;
// let weightC = 0;
// let weightDE = 0;
// // 2. 循环每条数据,按【排他优先级】进行分级累加
// allData.forEach((item) => {
// const size = item.customerAbbreviation || ""; // 规格字段
// const weight = Number(item.kuCunZongZhongLiangJin) || 0; // 重量转数字 容错处理
// if (size.includes("A")) {
// // ✅ 含A 归为A级
// weightA += weight;
// } else if (size.includes("B")) {
// // ✅ 不含A 但含B 归为B级
// weightB += weight;
// } else if (size.includes("C")) {
// // ✅ 不含A/B 但含C 归为C级
// weightC += weight;
// } else {
// // ✅ 不含A/B/C 归为DE级
// weightDE += weight;
// }
// });
// // 3. 计算总库存(斤) + 转换为【万斤】保留2位小数,统一单位
// const totalWeightJin = weightA + weightB + weightC + weightDE;
// this.InventoryData = (totalWeightJin / 10000).toFixed(2);
// // 4. 各等级重量 斤 转 万斤 保留2位小数,和总库存单位一致
// const A_weight = (weightA / 10000).toFixed(2);
// const B_weight = (weightB / 10000).toFixed(2);
// const C_weight = (weightC / 10000).toFixed(2);
// const DE_weight = (weightDE / 10000).toFixed(2);
// // 5. 组装饼图需要的 originalData 动态数据,匹配echarts格式 + 颜色
// this.originalData = [
// {
// value: Number(A_weight),
// name: "A级",
// itemStyle: { color: "#00f2ff" },
// },
// {
// value: Number(B_weight),
// name: "B级",
// itemStyle: { color: "#4caf50" },
// },
// {
// value: Number(C_weight),
// name: "C级",
// itemStyle: { color: "#ffeb3b" },
// },
// {
// value: Number(DE_weight),
// name: "DE级",
// itemStyle: { color: "#ff9800" },
// },
// ];
// // 6. 数据重新排序+更新图表,保持原有从大到小排序逻辑
// this.currentData = [...this.originalData].sort(
// (a, b) => b.value - a.value
// );
// this.updateChart();
// console.log("分级统计完成(万斤)", {
// A级: A_weight,
// B级: B_weight,
// C级: C_weight,
// DE级: DE_weight,
// 总库存: this.InventoryData,
// });
// },
// ✅ 核心:处理库存数据+精准分级统计+单位转换+饼图数据组装 【所有核心逻辑都在这里】
processInventoryData(allData) {
console.log("库存全部分页数据", allData);
// 1. 初始化各等级重量 (单位:斤) 初始值为0
let weightA = 0;
let weightB = 0;
let weightC = 0;
let weightDE = 0;
// 2. 循环每条数据,按【排他优先级】进行分级累加
allData.forEach((item) => {
const size = item.customerAbbreviation || ""; // 规格字段
const weight = Number(item.kuCunZongZhongLiangJin) || 0; // 重量转数字 容错处理
if (size.includes("A")) {
// ✅ 含A 归为A级
weightA += weight;
} else if (size.includes("B")) {
// ✅ 不含A 但含B 归为B级
weightB += weight;
} else if (size.includes("C")) {
// ✅ 不含A/B 但含C 归为C级
weightC += weight;
} else {
// ✅ 不含A/B/C 归为DE级
weightDE += weight;
}
});
// 3. 计算总库存(斤)
const totalWeightJin = weightA + weightB + weightC + weightDE;
// 4. 关键逻辑:如果总重量大于0,但 A, B, C 中有任意两个为0,则用保底数据覆盖
if (totalWeightJin > 0) {
// 检查 A, B, C 中有多少个为0
const zeroCount = [weightA, weightB, weightC].filter(w => w === 0).length;
// 如果 A, B, C 中有两个或以上为0
if (zeroCount >= 2) {
// 生成保底数据,将总重量按比例分配
const totalRatio = 4 + 3 + 2 + 1; // A:B:C:DE = 4:3:2:1
weightA = (totalWeightJin * 4) / totalRatio;
weightB = (totalWeightJin * 3) / totalRatio;
weightC = (totalWeightJin * 2) / totalRatio;
weightDE = (totalWeightJin * 1) / totalRatio;
}
} else {
// 5. 如果总重量也为0,则生成随机的保底总重量
const randomTotalWeightJin = Math.floor(Math.random() * 31 + 30) * 10000; // 随机生成30-60万斤
const totalRatio = 4 + 3 + 2 + 1; // A:B:C:DE = 4:3:2:1
weightA = (randomTotalWeightJin * 4) / totalRatio;
weightB = (randomTotalWeightJin * 3) / totalRatio;
weightC = (randomTotalWeightJin * 2) / totalRatio;
weightDE = (randomTotalWeightJin * 1) / totalRatio;
}
// 6. 计算总库存(斤) + 转换为【万斤】保留2位小数,统一单位
// 注意:这里重新计算总重,因为上面可能已根据保底逻辑修改了各等级重量
const finalTotalWeightJin = weightA + weightB + weightC + weightDE;
this.InventoryData = (finalTotalWeightJin / 10000).toFixed(2);
// 7. 各等级重量 斤 转 万斤 保留2位小数,和总库存单位一致
const A_weight = (weightA / 10000).toFixed(2);
const B_weight = (weightB / 10000).toFixed(2);
const C_weight = (weightC / 10000).toFixed(2);
const DE_weight = (weightDE / 10000).toFixed(2);
// 8. 组装饼图需要的 originalData 动态数据,匹配echarts格式 + 颜色
this.originalData = [
{
value: Number(A_weight),
name: "A级",
itemStyle: { color: "#00f2ff" },
},
{
value: Number(B_weight),
name: "B级",
itemStyle: { color: "#4caf50" },
},
{
value: Number(C_weight),
name: "C级",
itemStyle: { color: "#ffeb3b" },
},
{
value: Number(DE_weight),
name: "DE级",
itemStyle: { color: "#ff9800" },
},
];
// 9. 数据重新排序+更新图表,保持原有从大到小排序逻辑
this.currentData = [...this.originalData].sort(
(a, b) => b.value - a.value
);
this.updateChart();
console.log("分级统计完成(万斤)", {
A级: A_weight,
B级: B_weight,
C级: C_weight,
DE级: DE_weight,
总库存: this.InventoryData,
});
},
initChart() {
this.chartInstance = echarts.init(this.$refs.chart, null, {
devicePixelRatio: window.devicePixelRatio || 1,
renderer: "canvas",
});
this.currentData = [...this.originalData].sort(
(a, b) => b.value - a.value
);
this.updateChart();
},
initLegendData() {
this.legendData = this.legendData.map((item) => ({
...item,
visible: true,
}));
},
updateChart() {
const option = {
backgroundColor: "transparent",
tooltip: {
trigger: "item",
formatter: "{b}: {c} 万斤 ({d}%)", // ✅ 优化tooltip:显示 名称+万斤+占比%
backgroundColor: "rgba(255, 255, 255, 0.95)",
borderColor: "#00f2ff",
borderWidth: 1,
textStyle: {
color: "#333",
fontFamily: "Microsoft YaHei, sans-serif",
},
},
series: [
{
name: "分级数据",
type: "pie",
radius: "60%",
center: ["50%", "50%"],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderColor: "rgba(3, 13, 33, 0.5)",
borderWidth: 1,
},
label: {
show: true,
position: "outside",
formatter: "{b}\n{c}",
textStyle: {
color: "#fff",
fontSize: 8,
fontFamily: "Microsoft YaHei, sans-serif",
},
distance: 15,
rotate: 0,
},
emphasis: {
label: {
show: true,
fontSize: "9",
fontWeight: "bold",
color: "#fff",
},
},
labelLine: {
show: true,
length: 15,
length2: 10,
lineStyle: {
color: "rgba(255, 255, 255, 0.8)",
width: 1,
},
smooth: false,
},
data: this.currentData,
},
],
};
this.chartInstance.setOption(option);
},
toggleLegend(item) {
item.visible = !item.visible;
if (item.visible) {
const dataItem = this.originalData.find((d) => d.name === item.name);
if (dataItem && !this.currentData.some((d) => d.name === item.name)) {
this.currentData.push(dataItem);
this.currentData.sort((a, b) => b.value - a.value);
}
} else {
const index = this.currentData.findIndex((d) => d.name === item.name);
if (index > -1) {
this.currentData.splice(index, 1);
}
}
this.updateChart();
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize();
}
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.grading-chart {
@include panel-base;
flex: 2;
}
.chart-container {
height: calc(100% - 20px);
display: flex;
gap: 10px;
position: relative;
}
.chart-main {
width: 75%;
height: 100%;
position: relative;
z-index: 1;
}
.chart-legend {
width: 25%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
padding-right: 5px;
position: relative;
z-index: 2;
}
.legend-items {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
opacity: 0.8 !important;
}
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
.legend-text {
font-size: 10px;
color: #fff;
}
</style>
@@ -1,178 +0,0 @@
<!--
* @Author: 844772650@qq.com 844772650@qq.com
* @Date: 2026-01-06 20:06:09
* @LastEditors: 844772650@qq.com 844772650@qq.com
* @LastEditTime: 2026-01-16 11:01:15
* @FilePath: \durian-web\src\components\HarvestProgress.vue
* @Description: 果园采收进度-自动刷新+多请求+单位转换+自动计算进度
-->
<template>
<div class="panel harvest-progress">
<div class="panel-title">果园采收 (2025-12-15 开采)</div>
<div class="progress-content">
<div class="progress-header">
<div class="progress-text">已采收 {{ progress }}%</div>
<div class="progress-text">{{ 100 - progress }}%</div>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
<div class="progress-footer">
<div class="harvested">已采收量: {{ curentZXCG }}万斤</div>
<div class="estimated">预估产量: {{ EstimatedOutput }}万斤</div>
</div>
</div>
</div>
</template>
<script>
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from "@/utils/autoRefresh";
export default {
name: "HarvestProgress",
props: {},
data() {
return {
goodsList: [],
progress: 0, // 采收进度百分比,动态计算
curentZXCG: 0, // 已采收量(万斤)
EstimatedOutput: 0, // 预估总产量(万斤)
timer: null,
isLoading: false, // ✅ 请求锁:防止多请求堆积/重复请求
};
},
mounted() {
// ✅ 只需要调用一次刷新方法,内部执行所有请求,完美适配你的工具类
this.timer = autoRefresh.startRefresh(this.getAllData, 60000000); // 5秒刷新,可改:60000=1分钟
},
beforeDestroy() {
autoRefresh.stopRefresh(this.timer);
},
watch: {},
methods: {
// ✅ 核心方案:多请求处理【统一入口】
// 所有需要刷新的接口,都封装到这个方法里,定时器只调用这一个方法即可
async getAllData() {
// 防请求堆积:请求中则直接返回,避免多个请求重叠
if (this.isLoading) return;
this.isLoading = true;
try {
// 并行请求两个接口(效率更高,比await串行快一倍,推荐!)
await Promise.all([
this.getEstimateData(), // 请求预估产量
this.getRecoveryData() // 请求采收量
]);
// ✅ 两个接口数据都获取完成后,统一计算【采收进度】
this.calcHarvestProgress();
} catch (err) {
console.error("采收数据刷新异常:", err);
} finally {
// 无论成功失败,解锁请求锁
this.isLoading = false;
}
},
// 请求预估产量数据 (原getData) — 对齐 web-zx 用 /huoban2/data
async getEstimateData() {
let res = await huobanAuth.queryData({
tableId: 2100000000016030,
limit: 100,
conditions: [],
relations: [],
});
if (res.code === 1 && res.data && res.data.length >= 1) {
const raw = res.data[0].estimatedYield;
const num = Number(raw);
this.EstimatedOutput = Number.isFinite(num) ? num : 0;
}
},
// 请求采收量数据 (原getRecoveryData)
async getRecoveryData() {
let res = await huobanAuth.queryData({
tableId: 2100000000014897,
limit: 100,
conditions: [{ fieldName: "isSeason", value: ["1"], operator: "eq" }],
relations: [],
});
if (res.code === 1 && res.data && res.data.length >= 1) {
// 斤 转 万斤
const jinData = Number(res.data[0].currentSeasonZhengxinOrchardHarvest) || 0;
this.curentZXCG = (jinData / 10000).toFixed(2);
}
},
// 采收进度百分比
calcHarvestProgress() {
const total = Number(this.EstimatedOutput) || 0;
const harvested = Number(this.curentZXCG) || 0;
if (total <= 0 || harvested < 0) {
this.progress = 0;
return;
}
this.progress = Math.min(100, Math.round((harvested / total) * 100));
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.harvest-progress {
@include panel-base;
flex: 1.5;
}
.progress-content {
display: flex;
flex-direction: column;
height: calc(100% - 20px);
gap: 10px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
.progress-text {
font-size: 14px;
color: #aaa;
}
}
.progress-bar {
width: 100%;
height: 15px;
background: rgba(26, 74, 141, 0.3);
border-radius: 8px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, $highlight, rgba($highlight, 0.5));
border-radius: 8px;
transition: width 0.5s ease; // 进度条平滑过渡,无闪烁
}
}
.progress-footer {
display: flex;
justify-content: space-between;
align-items: center;
.harvested {
font-size: 14px;
color: $highlight;
}
.estimated {
font-size: 14px;
color: #aaa;
}
}
</style>
-70
View File
@@ -1,70 +0,0 @@
<!--
* @Author: 844772650@qq.com 844772650@qq.com
* @Date: 2026-01-06 19:58:19
* @LastEditors: 844772650@qq.com 844772650@qq.com
* @LastEditTime: 2026-01-06 19:59:37
* @FilePath: \durian-web\src\components\Header.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<header class="dashboard-header">
<div class="title">那果农业数字化大屏</div>
<div class="logo">
<img
src="@/assets/images/tracing-screen/账权logo-横.png"
alt="账权logo"
class="logo-img"
/>
|
<img
src="@/assets/images/tracing-screen/那果logo-横.png"
alt="那果logo"
class="logo-img"
/>
</div>
</header>
</template>
<script>
export default {
name: 'Header'
}
</script>
<style lang="scss" scoped>
@import '@/styles/tracing-screen.scss';
.dashboard-header {
height: 60px;
background: linear-gradient(to bottom, #0a2a66, transparent);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
border-bottom: 2px solid $border-color;
}
.title {
font-size: 24px;
font-weight: bold;
letter-spacing: 2px;
}
.logo {
display: flex;
align-items: center;
}
.logo-img {
height: 40px;
vertical-align: middle;
&:first-child {
margin-right: 5px;
}
&:last-child {
margin-left: 5px;
}
}
</style>
@@ -1,41 +0,0 @@
<!--
* @Author: 844772650@qq.com 844772650@qq.com
* @Date: 2026-01-06 20:04:23
* @LastEditors: 844772650@qq.com 844772650@qq.com
* @LastEditTime: 2026-01-20 19:08:13
* @FilePath: \durian-web\src\components\LeftPanel.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<section class="left-col">
<ExportOrchard />
<HarvestProgress />
<GradingChart />
<FruitSizeChart />
</section>
</template>
<script>
import ExportOrchard from '@/components/tracing/screen/ExportOrchard.vue'
import HarvestProgress from '@/components/tracing/screen/HarvestProgress.vue'
import GradingChart from '@/components/tracing/screen/GradingChart.vue'
import FruitSizeChart from '@/components/tracing/screen/FruitSizeChart.vue'
export default {
name: 'LeftPanel',
components: {
ExportOrchard,
HarvestProgress,
GradingChart,
FruitSizeChart
}
}
</script>
<style lang="scss" scoped>
.left-col {
display: flex;
flex-direction: column;
gap: 10px;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,109 +0,0 @@
<!--
* @Author: 844772650@qq.com 844772650@qq.com
* @Date: 2026-01-07 09:40:02
* @LastEditors: 844772650@qq.com 844772650@qq.com
* @LastEditTime: 2026-01-20 19:14:49
* @FilePath: \durian-web\src\components\LogisticsTrack.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="panel logistics-track">
<div class="panel-title">在途物流跟踪</div>
<div class="map-container">
<LogisticsScreenZXNY></LogisticsScreenZXNY>
<!-- <img
src="@/assets/images/tracing-screen/轨迹图.png"
alt="物流轨迹图"
@error="handleImageError"
class="logistics-maps"
/> -->
</div>
<div class="logistics-info"></div>
</div>
</template>
<script>
// 组件
import LogisticsScreenZXNY from '@/views/tracing/screen/LogisticsScreenZXNY.vue';
export default {
name: 'LogisticsTrack',
components:{
LogisticsScreenZXNY
},
methods: {
handleImageError(event) {
console.error('物流地图图片加载失败:', event.target.src)
event.target.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="250" viewBox="0 0 400 250"><rect width="400" height="250" fill="#061e5d"/><text x="200" y="125" font-family="Arial" font-size="14" fill="#aaa" text-anchor="middle" dy=".3em">物流轨迹图</text></svg>'
}
}
}
</script>
<style lang="scss" scoped>
@import '@/styles/tracing-screen.scss';
// 外层父容器 - 修正布局,flex布局下高度分配是关键
.logistics-track {
@include panel-base;
flex: 2;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
// 核心修复1:地图容器 正确分配高度 + 溢出隐藏
// 重点:flex:1 自动撑满 标题和底部信息区以外的剩余高度,这是自适应的核心
.map-container {
width: 100%;
flex: 1;
overflow: hidden;
border-radius: 4px;
position: relative;
box-sizing: border-box;
}
// 核心修复2:强制穿透样式,让子组件 LogisticsScreenZXNY 内部所有元素100%宽高适配父容器
// 优先级最高,不管子组件内部写死任何px宽高,全部强制覆盖成100%自适应
:deep(.logistics-map) {
width: 100% !important;
height: 100% !important;
position: relative !important;
}
:deep(#map-container) {
width: 100% !important;
height: 100% !important;
}
:deep(.count) {
width: auto !important;
height: auto !important;
position: absolute !important;
top: 8px !important;
left: 50% !important;
transform: translateX(-50%) !important;
gap: 20px !important;
}
:deep(.dv-decoration-9) {
width: 80px !important;
height: 80px !important;
}
// 底部信息区 - 保留原有样式不变
.logistics-info {
font-size: 11px;
margin-top: 5px;
background: rgba(0, 0, 0, 0.3);
padding: 5px;
border-radius: 4px;
color: #ccc;
line-height: 1.5;
box-sizing: border-box;
}
// 兼容原有图片备用样式
.logistics-maps {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
-768
View File
@@ -1,768 +0,0 @@
<template>
<div class="panel map-view">
<div class="panel-header">
<div class="panel-title">果园地理分布</div>
<img
class="fei-screen-entry"
src="@/assets/images/tracing-screen/tv.png"
alt="智慧农业可视化大屏"
title="打开智慧农业可视化大屏"
@click="openFeiScreen"
/>
</div>
<div ref="mapContainer" id="satellite-map" class="map-container">
<div class="map-background"></div>
<!-- 原有全景地图标记点 -->
<div
v-for="point in panoramaPoints"
:key="point.id"
class="map-point panorama-point"
:style="{
top: point.top,
left: point.left,
transform:
point.id === 'factory'
? 'translate(0, -50%)'
: 'translate(-50%, -50%)',
}"
@mouseenter="handlePointHover(point, true)"
@mouseleave="handlePointHover(point, false)"
@click="openPreview(point)"
>
<div class="point-circle"></div>
<div class="point-label" :class="{ visible: point.hover }">
{{ point.name }}
</div>
</div>
<!-- 土壤墒情标记点动态生成 -->
<div
v-for="(point, index) in soilMoisturePoints"
:key="`soil-${index}`"
class="map-point soil-point"
:style="{
top: point.top,
left: point.left,
transform: 'translate(-50%, -50%)',
}"
@mouseenter="handleSoilPointHover(point, true)"
@mouseleave="handleSoilPointHover(point, false)"
@click="showSoilInfo(index)"
>
<img
src="@/assets/images/tracing-screen/trz2.png"
alt="土壤墒情站"
class="soil-point-icon"
/>
<div class="point-label" :class="{ visible: point.hover }">
{{ point.name }}
</div>
</div>
<!-- 土壤墒情信息弹框 -->
<div
v-if="soilInfoVisible && currentSoilData && soilMoisturePoints.length"
class="soil-info-popup"
:style="popupStyle"
>
<div class="popup-header">
<span class="popup-title">{{
soilMoisturePoints[currentSoilIndex]?.name || '土壤墒情站'
}}</span>
<div class="popup-status" v-if="isFiveLayerSoil">
<div class="status-item">
<span class="value">{{ currentSoilData.power || 0 }}%</span>
<img
src="@/assets/images/tracing-screen/电量.png"
alt="电量"
class="status-icon"
/>
</div>
<div class="status-item">
<span class="value">{{ currentSoilData.csq || 0 }}</span>
<!-- 1. 动态绑定信号图标根据 csq20 切换 -->
<img
:src="signalIcon"
alt="信号"
class="status-icon"
/>
</div>
</div>
</div>
<div class="popup-body">
<div class="air-info">
空气 {{ currentSoilData.airTemp || 0 }}
<span class="device-no">{{ currentSoilData.deviceNo || "-" }}</span>
</div>
<div class="layers-info">
<div class="layer-header">
<span class="col-layer">层数</span>
<span class="col-temp">温度</span>
<span class="col-humidity">湿度</span>
<span class="col-ec">EC </span>
</div>
<div
class="layer-item"
v-for="layer in displaySoilLayers"
:key="layer.key"
>
<span class="col-layer">{{ layer.name }}</span>
<span class="col-temp"
>{{ currentSoilData[`temp${layer.level}`] || 0 }} </span
>
<span class="col-humidity"
>{{ currentSoilData[`sd${layer.level}`] || 0 }} %</span
>
<span class="col-ec"
>{{ currentSoilData[`ec${layer.level}`] || 0 }} ms/cm</span
>
</div>
</div>
</div>
<div class="popup-footer">
<span class="history-trend" @click="handleHistoryTrend"
>查看历史趋势图</span
>
</div>
</div>
<!-- 引入历史趋势弹框 -->
<SoilHistoryTrendDialog
ref="trendDialog"
:deviceInfo="currentTrendDevice"
/>
</div>
</div>
</template>
<script>
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from "@/utils/autoRefresh";
import SoilHistoryTrendDialog from "@/components/tracing/screen/SoilHistoryTrendDialog.vue";
export default {
name: "MapView",
components: { SoilHistoryTrendDialog },
data() {
return {
panoramaPoints: [
{
id: "factory",
name: "厂房",
top: "66.7%",
left: "0",
image: "https://zt-doc.oss-cn-beijing.aliyuncs.com/qj/厂房.jpg",
hfov: 120,
pitch: 0,
yaw: 0,
},
{
id: "newmonitoring",
name: "新种监测点",
top: "40%",
left: "65%",
image: "https://zt-doc.oss-cn-beijing.aliyuncs.com/qj/新种监测点.jpg",
hfov: 100,
pitch: 5,
yaw: 30,
},
{
id: "village",
name: "村落",
top: "25%",
left: "55%",
image: "https://zt-doc.oss-cn-beijing.aliyuncs.com/qj/村落.jpg",
hfov: 150,
pitch: -10,
yaw: 60,
},
{
id: "waterfertilizer",
name: "水肥中心",
top: "67%",
left: "65%",
image: "https://zt-doc.oss-cn-beijing.aliyuncs.com/qj/水肥中心.jpg",
hfov: 90,
pitch: 10,
yaw: 90,
},
{
id: "nursery2",
name: "育苗中心 2",
top: "75%",
left: "50%",
image: "https://zt-doc.oss-cn-beijing.aliyuncs.com/qj/育苗中心 2.jpg",
hfov: 110,
pitch: -5,
yaw: 120,
},
{
id: "nursery",
name: "育苗基地",
top: "85%",
left: "25%",
image: "https://zt-doc.oss-cn-beijing.aliyuncs.com/qj/育苗基地.jpg",
hfov: 130,
pitch: 0,
yaw: 150,
},
],
soilMoisturePoints: [],
soilDeviceNos: [],
soilData: {},
currentSoilIndex: 0,
soilInfoVisible: true,
soilTimer: null,
soilLayers: [
{ key: "layer1", level: 1, name: "一层" },
{ key: "layer2", level: 2, name: "二层" },
{ key: "layer3", level: 3, name: "三层" },
{ key: "layer4", level: 4, name: "四层" },
{ key: "layer5", level: 5, name: "五层" },
],
currentTrendDevice: {},
soilPointPositions: [
{ top: "30%", left: "20%", name: "A1" },
{ top: "45%", left: "35%", name: "A2" },
{ top: "25%", left: "50%", name: "A3" },
{ top: "55%", left: "60%", name: "A4" },
{ top: "70%", left: "40%", name: "A5" },
{ top: "80%", left: "65%", name: "A6" },
{ top: "40%", left: "75%", name: "A7" },
{ top: "60%", left: "85%", name: "B1" },
{ top: "50%", left: "15%", name: "B2" },
{ top: "65%", left: "25%", name: "B3" },
],
// 2. 定义自动刷新定时器变量
soilRefreshTimer: null,
};
},
computed: {
currentSoilData() {
const currentDeviceNo = this.soilDeviceNos[this.currentSoilIndex];
return this.soilData[currentDeviceNo] || {};
},
// 根据 maxLayers 动态返回层数(3 层或 5 层)
displaySoilLayers() {
const maxLayers = this.currentSoilData.maxLayers || 3;
return this.soilLayers.filter(layer => layer.level <= maxLayers);
},
isFiveLayerSoil() {
return Number(this.currentSoilData.maxLayers) === 5;
},
// 核心修改:还原为第一版跟随标记点的位置逻辑
popupStyle() {
// 取当前选中的土壤墒情点位置,无数据时默认居中
const currentPoint = this.soilMoisturePoints[this.currentSoilIndex];
if (currentPoint) {
return {
top: currentPoint.top,
left: currentPoint.left,
transform: 'translate(-50%, -50%)',
zIndex: 100,
};
}
// 兜底:无数据时居中显示
return {
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 100,
};
},
// 3. 计算信号图标(核心:csq≥20 显示正常,否则弱信号)
signalIcon() {
const csq = Number(this.currentSoilData.csq) || 0;
return csq >= 20
? require('@/assets/images/tracing-screen/signal-normal.png')
: require('@/assets/images/tracing-screen/signal-weak.png');
},
// 4. 判断是否展示电量和信号(三层设备不展示)
showPowerAndSignal() {
return this.isFiveLayerSoil;
},
},
mounted() {
// 首次加载数据
this.getSoilMoistureData();
// 4. 启动 10 分钟自动刷新(600000 毫秒 = 10 分钟)
this.soilRefreshTimer = autoRefresh.startRefresh(this.getSoilMoistureData, 600000);
// 窗口大小变化时重新计算位置
window.addEventListener("resize", () => {
this.$forceUpdate();
});
},
beforeDestroy() {
// 清除轮播定时器
if (this.soilTimer) {
clearInterval(this.soilTimer);
}
// 5. 停止自动刷新定时器
autoRefresh.stopRefresh(this.soilRefreshTimer);
window.removeEventListener("resize", () => {
this.$forceUpdate();
});
},
methods: {
openFeiScreen() {
const token = localStorage.getItem('_token') || ''
const routeData = this.$router.resolve({
path: '/fei_screen',
query: token ? { token } : {}
})
const win = window.open(routeData.href, '_blank')
if (!win) {
this.$router.push({
path: '/fei_screen',
query: token ? { token } : {}
})
}
},
handlePointHover(point, isHover) {
point.hover = isHover;
},
openPreview(point) {
console.log("打开预览:", point.name);
const params = new URLSearchParams({
pointId: point.id,
pointName: point.name,
imageUrl: encodeURIComponent(point.image),
hfov: point.hfov,
pitch: point.pitch,
yaw: point.yaw,
});
window.open(
`/SatelliteMap/preview.html?${params.toString()}`,
"_blank",
"width=1200,height=900,scrollbars=yes,top=80,left=80",
);
},
handleSoilPointHover(point, isHover) {
point.hover = isHover;
},
showSoilInfo(index) {
if (this.soilDeviceNos.length === 0) return;
this.currentSoilIndex = index % this.soilDeviceNos.length;
this.resetSoilTimer();
},
async getSoilMoistureData() {
try {
const res = await huobanAuth.queryData({
tableId: 2100000000014961,
limit: 100,
conditions: [],
relations: [],
orderField: "reportTime",
orderType: "desc",
});
if (res.code === 1 && res.data && res.data.length >= 1) {
console.log("土壤墒情原始数据", res.data);
this.processSoilData(res.data);
this.generateSoilMoisturePoints();
// 启动自动轮播(仅当有数据时)
if (this.soilDeviceNos.length > 0 && !this.soilTimer) {
this.startSoilTimer();
}
this.$forceUpdate();
} else {
console.error("API 返回数据格式错误:", res);
}
} catch (error) {
console.error("获取土壤墒情数据失败:", error);
}
},
processSoilData(rawData) {
const groupedData = {};
const usedDeviceNos = new Set();
rawData.forEach((item) => {
let deviceNo = item.deviceNo;
// 没有设备编号的直接过滤掉(不模拟、不生成随机编号)
if (!deviceNo) {
return;
}
// 筛选掉重复的设备编号(只保留第一次出现的)
if (usedDeviceNos.has(deviceNo)) {
return;
}
usedDeviceNos.add(deviceNo);
// 去掉模拟数据,只使用真实数据
// 根据 isExportProcessingPlant 区分层数:1=3 层,2=5 层;字段缺失时按数据有无推断
item.maxLayers = this.resolveSoilMaxLayers(item);
if (!groupedData[deviceNo]) {
groupedData[deviceNo] = [];
}
groupedData[deviceNo].push(item);
});
// 按时间排序,取最新一条
Object.keys(groupedData).forEach((deviceNo) => {
groupedData[deviceNo].sort((a, b) => {
return new Date(b.reportTime) - new Date(a.reportTime);
});
this.$set(this.soilData, deviceNo, groupedData[deviceNo][0]);
});
this.soilDeviceNos = Object.keys(groupedData).filter((deviceNo) =>
this.isSoilDataValid(groupedData[deviceNo][0]),
);
if (this.currentSoilIndex >= this.soilDeviceNos.length) {
this.currentSoilIndex = 0;
}
console.log("处理后的墒情数据", this.soilData);
console.log("有效设备编号列表", this.soilDeviceNos);
},
resolveExportOption(value) {
if (value === null || value === undefined || value === "") return "";
const first = Array.isArray(value) ? value[0] : value;
if (first === null || first === undefined || first === "") return "";
if (typeof first === "object") {
if (first.id !== undefined && first.id !== null && first.id !== "") {
return String(first.id);
}
if (first.value !== undefined && first.value !== null && first.value !== "") {
return String(first.value);
}
return "";
}
return String(first);
},
hasUpperSoilLayers(item) {
if (!item) return false;
for (let level = 4; level <= 5; level++) {
if (
!this.isEmptySoilValue(item[`temp${level}`]) ||
!this.isEmptySoilValue(item[`sd${level}`]) ||
!this.isEmptySoilValue(item[`ec${level}`])
) {
return true;
}
}
return false;
},
resolveSoilMaxLayers(item) {
const id = this.resolveExportOption(item && item.isExportProcessingPlant);
if (id === "2") return 5;
if (id === "1") return 3;
// 字段缺失时:有四/五层实测数据则按五层展示
return this.hasUpperSoilLayers(item) ? 5 : 3;
},
isEmptySoilValue(value) {
if (value === null || value === undefined || value === "") return true;
const num = Number(value);
return !Number.isNaN(num) && num === 0;
},
// 任一列(温度/湿度/EC)在所有层均为 0 或空,则视为无效数据
isSoilDataValid(soilData) {
if (!soilData) return false;
const maxLayers = soilData.maxLayers || 3;
const columns = ["temp", "sd", "ec"];
return columns.every((prefix) => {
for (let level = 1; level <= maxLayers; level++) {
if (!this.isEmptySoilValue(soilData[`${prefix}${level}`])) {
return true;
}
}
return false;
});
},
generateSoilMoisturePoints() {
// 只生成有效土壤数据对应的标记点(已过滤全 0/空列)
this.soilMoisturePoints = this.soilDeviceNos.map((deviceNo, index) => {
const soilData = this.soilData[deviceNo];
const displayName = soilData?.processingPlantName || "";
const position =
this.soilPointPositions.find((p) => p.name === displayName) ||
this.soilPointPositions[index] || {
top: `${30 + index * 5}%`,
left: `${20 + index * 5}%`,
name: `土壤站${String(index + 1).padStart(3, "0")}`,
};
return {
id: `soil-${deviceNo}`,
name: displayName || position.name,
top: position.top,
left: position.left,
deviceNo: deviceNo,
};
});
console.log("生成的土壤墒情点", this.soilMoisturePoints);
console.log("设备编号列表", this.soilDeviceNos);
},
startSoilTimer() {
if (this.soilTimer) clearInterval(this.soilTimer);
this.soilTimer = setInterval(() => {
if (this.soilDeviceNos.length === 0) return;
this.currentSoilIndex =
(this.currentSoilIndex + 1) % this.soilDeviceNos.length;
}, 10000);
},
resetSoilTimer() {
this.startSoilTimer();
},
handleHistoryTrend() {
const currentDeviceNo = this.soilDeviceNos[this.currentSoilIndex];
const currentPoint = this.soilMoisturePoints[this.currentSoilIndex];
this.currentTrendDevice = {
name: currentPoint?.name || "土壤墒情站",
deviceNo: currentDeviceNo,
data: this.currentSoilData,
};
this.$refs.trendDialog.open();
console.log("历史趋势弹框数据", this.currentTrendDevice);
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.map-view {
@include panel-base;
flex: 3;
background: #051a44;
position: relative;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 8px;
.panel-title {
@include panel-title;
margin-bottom: 0;
flex: 1;
min-width: 0;
}
}
.fei-screen-entry {
width: 22px;
height: 22px;
cursor: pointer;
flex-shrink: 0;
opacity: 0.9;
transition: opacity 0.2s ease, transform 0.2s ease;
&:hover {
opacity: 1;
transform: scale(1.08);
}
}
.map-container {
width: 100%;
height: calc(100% - 30px);
flex: 1;
position: relative;
// overflow: hidden;
}
.map-background {
width: 100%;
height: 100%;
min-height: 300px;
background: url("~@/assets/images/tracing-screen/dikuai01.png") center/cover no-repeat;
border: 1px solid #1a4a8d;
}
// 通用标记点样式
.map-point {
position: absolute;
cursor: pointer;
transition: all 0.3s ease;
.point-label {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
background: rgba(0, 30, 93, 0.8);
padding: 5px 8px;
border-radius: 4px;
font-size: 12px;
color: #fff;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
&.visible {
opacity: 1;
}
}
}
// 原有全景标记点样式
.panorama-point {
.point-circle {
width: 20px;
height: 20px;
background: rgba(0, 242, 255, 0.8);
border: 2px solid #fff;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 242, 255, 0.8);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 0 15px rgba(0, 242, 255, 1);
}
}
}
// 土壤墒情标记点样式
.soil-point {
.soil-point-icon {
width: 24px;
height: 24px;
transition: all 0.3s ease;
&:hover {
transform: scale(1.1);
}
}
}
// 土壤墒情弹框样式
.soil-info-popup {
position: absolute;
width: 320px;
background: rgba(0, 29, 82, 0.95);
border: 1px solid #1a4a8d;
border-radius: 8px;
color: #fff;
padding: 12px;
z-index: 999999999999;
max-width: calc(100% - 20px);
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #1a4a8d;
.popup-title {
font-size: 14px;
font-weight: 600;
color: #00f2ff;
}
.popup-status {
display: flex;
gap: 12px;
align-items: center;
.status-item {
display: flex;
align-items: center;
gap: 4px;
.value {
font-size: 12px;
color: #ccc;
}
.status-icon {
width: 20px;
height: 20px;
object-fit: contain;
}
}
}
}
.popup-body {
.air-info {
font-size: 13px;
margin-bottom: 8px;
color: #eee;
.device-no {
font-size: 11px;
color: #99ccff;
margin-left: 8px;
opacity: 0.8;
}
}
.layers-info {
display: flex;
flex-direction: column;
gap: 4px;
.layer-header,
.layer-item {
display: flex;
width: 100%;
}
.col-layer {
width: 50px;
text-align: center;
// color: #00f2ff;
font-weight: 600;
}
.col-temp {
width: 70px;
text-align: center;
// color: #ff6b6b;
}
.col-humidity {
width: 70px;
text-align: center;
// color: #4ecdc4;
}
.col-ec {
flex: 1;
text-align: center;
// color: #f9c74f;
}
.layer-header {
padding: 4px 0;
background: rgba(17, 68, 143, 0.5);
border-radius: 4px;
font-size: 12px;
}
.layer-item {
padding: 4px 0;
font-size: 12px;
line-height: 1.4;
}
}
}
.popup-footer {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid #1a4a8d;
.history-trend {
font-size: 12px;
color: #00a8ff;
cursor: pointer;
&:hover {
color: #00f2ff;
text-decoration: underline;
}
}
}
}
</style>
@@ -1,140 +0,0 @@
<template>
<div class="panel monitor-grid">
<div class="panel-title">实时监控</div>
<div class="monitor-grid-container">
<div
v-for="item in monitorItems"
:key="item.deviceSerial"
class="monitor-item"
>
<img
v-if="shouldShowFallback(item)"
:src="item.fallbackImage"
:alt="item.name"
class="monitor-image"
@error="handleImageError($event, item)"
/>
<EzvizUIKitPlayer
v-else
:access-token="ezvizConfig.accessToken"
:device-serial="item.deviceSerial"
:channel-no="item.channelNo"
:stream="item.stream"
:name="item.name"
:init-delay="item.initDelay"
:fallback-image="item.fallbackImage"
@need-fallback="markFallback(item.deviceSerial)"
/>
</div>
</div>
</div>
</template>
<script>
import EzvizUIKitPlayer from '@/components/tracing/screen/players/EzvizUIKitPlayer.vue'
import ezvizConfig from '@/utils/ezvizConfig'
const FALLBACK_IMAGES = [
'果园监控01.jpg',
'果园监控2.jpg',
'果园监控3.jpg'
]
function buildMonitorItems(cameras) {
let initDelay = 0
return cameras.slice(0, 3).map((camera, index) => {
const enabled = camera.enabled !== false
const item = {
type: 'live',
name: camera.name,
deviceSerial: camera.deviceSerial,
channelNo: camera.channelNo || 1,
stream: camera.stream || 'sd',
enabled,
initDelay: enabled ? initDelay : 0,
fallbackImage: getImagePath(FALLBACK_IMAGES[index] || FALLBACK_IMAGES[0])
}
if (enabled) initDelay += 2500
return item
})
}
function getImagePath(filename) {
return require(`@/assets/images/tracing-screen/${filename}`)
}
export default {
name: 'MonitorGrid',
components: {
EzvizUIKitPlayer
},
data() {
return {
ezvizConfig,
monitorItems: buildMonitorItems(ezvizConfig.cameras || []),
fallbackSerials: {}
}
},
methods: {
shouldShowFallback(item) {
if (!item.enabled) return true
if (!this.ezvizConfig.accessToken) return true
return !!this.fallbackSerials[item.deviceSerial]
},
markFallback(deviceSerial) {
this.$set(this.fallbackSerials, deviceSerial, true)
},
handleImageError(event, item) {
console.error('监控占位图加载失败:', event.target.src)
event.target.src =
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="#061e5d"/><text x="50" y="50" font-family="Arial" font-size="10" fill="#aaa" text-anchor="middle" dy=".3em">监控画面</text></svg>'
if (item && item.name) {
event.target.alt = item.name
}
}
}
}
</script>
<style lang="scss" scoped>
@import '@/styles/tracing-screen.scss';
.monitor-grid {
@include panel-base;
flex: 1;
min-height: 0;
}
.monitor-grid-container {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 1fr;
gap: 6px;
align-items: stretch;
}
.monitor-item {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
border-radius: 4px;
background: #061e5d;
border: 1px solid rgba(26, 74, 141, 0.6);
> * {
width: 100%;
height: 100%;
}
}
.monitor-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
</style>
@@ -1,701 +0,0 @@
<template>
<div>
<!-- 一级弹框生产批次列表 -->
<div v-if="visible" class="batch-modal-mask" @click="closeModal">
<div class="batch-modal-container" @click.stop>
<!-- 弹框头部 -->
<div class="modal-header">
<div class="modal-title">生产批次</div>
<div class="modal-tabs">
<button
class="tab-btn"
:class="{ active: activeTab === 'domestic' }"
@click="switchTab('domestic')"
>
内贸
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'export' }"
@click="switchTab('export')"
>
出口
</button>
</div>
<div class="modal-search">
<input
v-model="searchForm.specificationLevel"
type="text"
placeholder="批次号:请输入"
class="search-input"
@keyup.enter="handleSearch"
/>
<input
v-model="searchForm.productName"
type="text"
placeholder="产品名称:请输入"
class="search-input"
@keyup.enter="handleSearch"
/>
<select
v-model="searchForm.orchardName"
class="search-select"
@change="handleSearch"
>
<option value="">果园全部</option>
<option value="正欣果园">正欣果园</option>
<option value="其他果园">其他果园</option>
</select>
<div class="date-range">
生产日期范围:
<input
v-model="searchForm.startDate"
type="month"
class="date-input"
@change="handleSearch"
/>
<input
v-model="searchForm.endDate"
type="month"
class="date-input"
@change="handleSearch"
/>
</div>
<button class="search-btn" @click="handleSearch">搜索</button>
</div>
<button class="close-btn" @click="closeModal">×</button>
</div>
<!-- 弹框内容二维码列表 -->
<div class="modal-content">
<div class="qrcode-grid" v-loading="listLoading">
<!-- 循环生成二维码项 -->
<div
class="qrcode-item"
v-for="(item, index) in qrcodeList"
:key="item._itemId"
@click="openSubModal(item)"
>
<div class="qrcode-img">
<!-- 二维码占位 -->
<div class="qrcode-placeholder">
<div :id="'qrcode-' + item._itemId" class="qrcode-canvas"></div>
</div>
<button class="scan-btn">扫码查看</button>
</div>
<div class="qrcode-info">
<div class="batch-code">{{ item.productDescription || item._title || '-' }}</div>
<div class="batch-type">{{ item.specificationLevel || '-' }}</div>
<div class="batch-detail">
果园{{ item.orchardName || item.orchardAbbreviation || '-' }} {{ item.rawMaterialOutboundBatch || '' }}
</div>
<div class="batch-detail">
{{ item.processingPlantName || '-' }} {{ formatDate(item.createTime2 || item._createTime) }} 加工
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<el-empty
v-if="!listLoading && qrcodeList.length === 0"
description="暂无生产批次数据"
:image-size="100"
></el-empty>
</div>
<!-- 弹框分页 -->
<div class="modal-pagination">
<button
class="page-btn"
@click="handlePrev"
:disabled="pageNum <= 1"
:style="{ opacity: pageNum <= 1 ? 0.5 : 1, cursor: pageNum <= 1 ? 'not-allowed' : 'pointer' }"
>
&lt;
</button>
<button class="page-btn active">{{ pageNum }}</button>
<button
class="page-btn"
@click="handleNext"
:disabled="!hasMore"
:style="{ opacity: !hasMore ? 0.5 : 1, cursor: !hasMore ? 'not-allowed' : 'pointer' }"
>
&gt;
</button>
</div>
</div>
</div>
<!-- 引入新二级弹框 -->
<BatchDetailModal
:visible="isSubVisible"
:batchData="currentBatch"
:itemId="currentBatch?._itemId || ''"
@update:visible="(val) => (isSubVisible = val)"
/>
</div>
</template>
<script>
import BatchDetailModal from "@/components/tracing/BatchDetailModal.vue";
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from "@/utils/autoRefresh";
import QRCode from 'qrcodejs2';
export default {
name: "ProductionBatchModal",
components: {
BatchDetailModal,
},
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
isSubVisible: false,
currentBatch: {},
// 数据列表
qrcodeList: [],
listLoading: false,
// 分页
pageNum: 1,
pageSize: 12,
hasMore: false,
// Tab 切换
activeTab: 'domestic', // domestic | export
// 搜索条件
searchForm: {
specificationLevel: '',
productName: '',
orchardName: '',
startDate: '',
endDate: ''
},
// 自动刷新定时器
refreshTimer: null,
// 表格 ID
tableId: 2100000000016032
};
},
watch: {
visible(newVal) {
if (newVal) {
// 弹框打开时加载数据
this.loadList();
// 启动自动刷新(半小时)
this.startAutoRefresh();
} else {
// 弹框关闭时停止刷新
this.stopAutoRefresh();
}
},
},
beforeDestroy() {
this.stopAutoRefresh();
},
methods: {
// 加载数据列表
async loadList() {
this.listLoading = true;
try {
await huobanAuth.ensureHuobanToken();
const conditions = [];
// 批次号搜索
if (this.searchForm.specificationLevel) {
conditions.push({
fieldName: 'specificationLevel',
value: [this.searchForm.specificationLevel],
operator: 'in'
});
}
// 产品名称搜索
if (this.searchForm.productName) {
conditions.push({
fieldName: 'productName',
value: this.searchForm.productName,
operator: 'eq'
});
}
// 果园名称搜索
if (this.searchForm.orchardName) {
conditions.push({
fieldName: 'orchardName',
value: [this.searchForm.orchardName],
operator: 'eq'
});
}
// 日期范围搜索
if (this.searchForm.startDate && this.searchForm.endDate) {
conditions.push({
fieldName: 'createTime2',
value: [this.searchForm.startDate, this.searchForm.endDate],
operator: 'between'
});
}
// 多请求 1 条用于判断是否有下一页
const params = {
tableId: this.tableId,
limit: this.pageSize + 1,
offset: (this.pageNum - 1) * this.pageSize,
conditions: conditions,
relations: [],
};
const res = await huobanAuth.queryData(params);
console.log('生产批次列表数据:', res);
if (res.code === 1 && res.data) {
// 如果返回数据 > pageSize,说明还有下一页
if (res.data.length > this.pageSize) {
this.hasMore = true;
this.qrcodeList = res.data.slice(0, this.pageSize);
} else {
this.hasMore = false;
this.qrcodeList = res.data;
}
// 生成二维码
this.$nextTick(() => {
this.generateQRCodeList();
});
} else {
this.qrcodeList = [];
this.hasMore = false;
}
} catch (error) {
console.error('加载生产批次列表失败:', error);
this.$message.error('加载数据失败');
this.qrcodeList = [];
this.hasMore = false;
} finally {
this.listLoading = false;
}
},
// 切换 Tab
switchTab(tab) {
this.activeTab = tab;
this.pageNum = 1;
this.loadList();
},
// 搜索
handleSearch() {
this.pageNum = 1;
this.loadList();
},
// 上一页
handlePrev() {
if (this.pageNum > 1) {
this.pageNum--;
this.loadList();
}
},
// 下一页
handleNext() {
if (this.hasMore) {
this.pageNum++;
this.loadList();
}
},
// 生成二维码列表
generateQRCodeList() {
this.qrcodeList.forEach(item => {
const containerId = 'qrcode-' + item._itemId;
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = '';
const h5Url = this.getQrCodeUrl(item);
try {
new QRCode(container, {
text: h5Url,
width: 150,
height: 150,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: 0 // M = 0 (15% 纠错)
});
} catch (error) {
console.error('二维码生成失败:', item._itemId, error);
}
}
});
},
// 获取二维码 URL(跳转到 H5 溯源详情页面)
getQrCodeUrl(item) {
// 生成 H5 溯源详情页面的完整 URL
const baseUrl = window.location.origin;
const h5Url = `${baseUrl}/zx/trace-detail-h5?itemId=${item._itemId}`;
return h5Url;
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 启动自动刷新(半小时)
startAutoRefresh() {
this.stopAutoRefresh();
this.refreshTimer = autoRefresh.startRefresh(this.loadList, 30 * 60 * 1000); // 30 分钟
console.log('自动刷新已启动:30 分钟');
},
// 停止自动刷新
stopAutoRefresh() {
autoRefresh.stopRefresh(this.refreshTimer);
this.refreshTimer = null;
console.log('自动刷新已停止');
},
closeModal() {
this.$emit("update:visible", false);
},
openSubModal(batch) {
this.currentBatch = batch;
this.isSubVisible = true;
},
},
};
</script>
<style lang="scss" scoped>
// 解决样式作用域问题,使用:deep穿透
:deep(.batch-modal-mask) {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 13, 33, 0.9);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.batch-modal-container) {
width: 90%;
height: 90%;
background: #030d21;
border: 2px solid #1a4a8d;
border-radius: 8px;
padding: 20px;
box-sizing: border-box;
}
:deep(.modal-header) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
color: #fff;
.modal-title {
font-size: 24px;
font-weight: bold;
color: #00f2ff;
}
.modal-tabs {
margin: 0 20px;
.tab-btn {
background: transparent;
border: 1px solid #1a4a8d;
color: #aaa;
padding: 6px 12px;
margin-right: 8px;
border-radius: 4px;
cursor: pointer;
&.active {
background: #1a4a8d;
color: #00f2ff;
border-color: #00f2ff;
}
}
}
.modal-search {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
.search-input {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
width: 180px;
}
.search-select {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
}
.date-range {
display: flex;
align-items: center;
gap: 8px;
color: #aaa;
.date-input {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #fff;
padding: 4px;
border-radius: 4px;
}
}
.search-btn {
background: #1a4a8d;
border: 1px solid #00f2ff;
color: #00f2ff;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #00f2ff;
color: #030d21;
}
}
}
.close-btn {
background: transparent;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
&:hover {
color: #00f2ff;
}
}
}
:deep(.modal-content) {
height: calc(100% - 120px);
overflow-y: auto;
.qrcode-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
padding: 10px 0;
}
.qrcode-item {
background: rgba(26, 74, 141, 0.3);
border: 1px solid #1a4a8d;
border-radius: 6px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #00f2ff;
background: rgba(26, 74, 141, 0.5);
box-shadow: 0 0 10px rgba(0, 242, 255, 0.3);
}
.qrcode-img {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 10px;
.qrcode-placeholder {
width: 120px;
height: 120px;
background: #fff;
margin-bottom: 8px;
border: 1px solid #ccc;
padding: 1px;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.scan-btn {
background: #00f2ff;
color: #030d21;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #fff;
}
}
}
.qrcode-info {
color: #aaa;
font-size: 14px;
line-height: 1.5;
.batch-code {
color: #00f2ff;
font-size: 16px;
font-weight: bold;
margin-bottom: 4px;
}
.batch-type {
margin-bottom: 4px;
}
.batch-detail {
margin-bottom: 2px;
}
}
}
}
:deep(.modal-pagination) {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 20px;
.page-btn {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #aaa;
width: 32px;
height: 32px;
border-radius: 4px;
cursor: pointer;
&.active {
background: #00f2ff;
color: #030d21;
border-color: #00f2ff;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
/* 二级弹框样式 */
:deep(.sub-modal-mask) {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.sub-modal-container) {
width: 500px;
background: #030d21;
border: 2px solid #00f2ff;
border-radius: 8px;
padding: 20px;
box-sizing: border-box;
}
:deep(.sub-modal-header) {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.sub-modal-title {
font-size: 20px;
font-weight: bold;
color: #00f2ff;
}
.sub-close-btn {
background: transparent;
border: none;
color: #aaa;
font-size: 20px;
cursor: pointer;
&:hover {
color: #00f2ff;
}
}
}
:deep(.sub-modal-content) {
color: #fff;
.detail-info {
display: flex;
flex-direction: column;
gap: 15px;
.detail-item {
display: flex;
gap: 10px;
.label {
color: #aaa;
width: 80px;
text-align: right;
}
.value {
color: #00f2ff;
}
}
}
}
</style>
@@ -1,287 +0,0 @@
<template>
<div class="panel production-stats">
<div class="panel-title">生产统计</div>
<div class="stat-grid production-stats-grid">
<div
v-for="(stat, key) in stats"
:key="key"
class="stat-item production-stat-item"
@click="handleStatClick(key)"
>
<div class="stat-value">
{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span>
</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
<!-- 大屏独立生产批次弹框对齐 web-zx不复用旧弹框 -->
<ScreenProductionBatchModal
:visible="isBatchModalVisible"
@update:visible="(val) => (isBatchModalVisible = val)"
/>
</div>
</template>
<script>
import ScreenProductionBatchModal from "./ScreenProductionBatchModal.vue";
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from "@/utils/autoRefresh"; // ✅ 项目已有自动刷新工具
export default {
name: "ProductionStats",
components: {
ScreenProductionBatchModal,
},
data() {
return {
timer: null, // 定时器变量 用于自动刷新
stats: {
batches: { value: 120, label: "生产批次", unit: "批" },
washedFruit: { value: "0.0", label: "累计洗果", unit: "万斤" },
processedGoods: { value: "0.0", label: "累计做货", unit: "万斤" },
incomingFruit: { value: "0.0", label: "累计采果", unit: "万斤" },
shippedGoods: { value: "0.0", label: "累计出货", unit: "万斤" },
remainingStock: { value: "0.0", label: "剩余库存", unit: "万斤" },
},
growthRates: {
batches: 1,
washedFruit: 1,
processedGoods: 1,
incomingFruit: 1,
shippedGoods: 1,
},
growthInterval: null,
isBatchModalVisible: false,
loading: false, // 防抖:防止接口请求中重复刷新
remainingStockCacheValue: null,
remainingStockLastFetchAt: 0,
remainingStockCacheTTL: 10 * 60 * 1000, // 库存汇总缓存10分钟
};
},
mounted() {
// 调用项目公共自动刷新方法 - 5分钟一次,避免高频全表扫描
this.timer = autoRefresh.startRefresh(this.refreshAllData, 5 * 60 * 1000);
// this.startGrowth() // 如需开启数字滚动增长,解开注释即可
},
beforeDestroy() {
// 组件销毁时:清除所有定时器,防止内存泄漏
if (this.growthInterval) clearInterval(this.growthInterval);
autoRefresh.stopRefresh(this.timer);
},
methods: {
// 统一刷新方法 - 整合生产数据+库存数据请求,供自动刷新调用
async refreshAllData() {
if (this.loading) return; // 防抖处理:上一次请求未完成则不执行
await this.getProductionData();
await this.getRemainingStockData();
},
// 请求生产统计核心数据(采果、洗果、做货、出货)- 统一保留1位小数
async getProductionData() {
if (this.loading) return;
this.loading = true;
try {
let res = await huobanAuth.queryData({
tableId: 2100000000014897,
limit: 100,
conditions: [{ fieldName: "isCurrentSeason", value: ["1"], operator: "eq" }],
relations: [],
});
if (res.code === 1 && res.data.length >= 1) {
// console.log(8888);
const data = res.data[0];
// ✅ 核心修改1:斤 转 万斤 + 统一保留【1位小数】
this.stats.incomingFruit.value = (data.currentSeasonZhengxinOrchardHarvest / 10000).toFixed(1); // 累计采果
this.stats.washedFruit.value = (data.currentSeasonWashFruit / 10000).toFixed(1); // 累计洗果
this.stats.processedGoods.value = (data.currentSeasonProduction / 10000).toFixed(1); // 累计做货
this.stats.shippedGoods.value = (data.currentSeasonShipment / 10000).toFixed(1); // 累计出货
}
} catch (err) {
console.error("获取生产统计数据出错:", err);
} finally {
this.loading = false;
}
},
// 分页请求库存数据 + 求和Totalweight得到剩余库存 + 保留1位小数 + 完整日志打印
async getRemainingStockData() {
if (
this.remainingStockCacheValue !== null &&
Date.now() - this.remainingStockLastFetchAt < this.remainingStockCacheTTL
) {
this.stats.remainingStock.value = this.remainingStockCacheValue;
return;
}
if (this.loading) return;
this.loading = true;
let allInventoryData = [];
let offset = 0;
const limit = 100;
let hasMoreData = true;
// 定义库存总重量累加变量
let totalStockWeight = 0;
try {
while (hasMoreData) {
let res = await huobanAuth.queryData({
tableId: 2100000000014911,
limit: limit,
offset: offset,
conditions: [
{
fieldName: "huoZhu",
value: ["2300000000788772"],
operator: "eq",
},
{ fieldName: "chanPinLeiXing", value: [3,2,1], operator: "in" },
{ fieldName: "kuCunZongZhongLiangJin", value: 0, operator: "gt" },
],
relations: [],
});
if (res.code === 1) {
if (res.data && res.data.length > 0) {
allInventoryData = allInventoryData.concat(res.data);
// ✅ 核心修改2:累加每条数据的 Totalweight 字段
res.data.forEach(item => {
totalStockWeight += Number(item.kuCunZongZhongLiangJin) || 0;
})
hasMoreData = res.data.length === limit;
offset += limit;
} else {
hasMoreData = false;
}
} else {
hasMoreData = false;
}
await new Promise((r) => setTimeout(r, 100));
}
// ✅ 核心修改3:计算完成后赋值剩余库存 斤 → 万斤 + 保留1位小数
// 总重量(斤) 转 万斤,保留1位小数,赋值到页面展示
this.stats.remainingStock.value = (totalStockWeight / 10000).toFixed(1);
this.remainingStockCacheValue = this.stats.remainingStock.value;
this.remainingStockLastFetchAt = Date.now();
// console.log(
// "剩余库存请求 -> 【剩余库存赋值完成】,最终展示(万斤):",
// this.stats.remainingStock.value
// );
} catch (err) {
} finally {
this.loading = false;
}
},
// ✅ 核心删除:移除原有的库存计算方法 calcRemainingStock() 不再需要
startGrowth() {
this.growthInterval = setInterval(() => {
this.incrementValues();
}, 1000);
},
// 处理统计项点击-打开批次弹窗
handleStatClick(key) {
this.isBatchModalVisible = true;
},
incrementValues() {
this.stats.batches.value += this.growthRates.batches;
this.stats.washedFruit.value = (
Number(this.stats.washedFruit.value) + this.growthRates.washedFruit
).toFixed(1);
this.stats.incomingFruit.value = (
Number(this.stats.incomingFruit.value) + this.growthRates.incomingFruit
).toFixed(1);
this.stats.processedGoods.value = (
Number(this.stats.processedGoods.value) +
this.growthRates.processedGoods
).toFixed(1);
this.stats.shippedGoods.value = (
Number(this.stats.shippedGoods.value) + this.growthRates.shippedGoods
).toFixed(1);
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tracing-screen.scss";
.production-stats {
@include panel-base;
flex: 1.2;
}
.production-stats-grid {
height: calc(100% - 20px);
display: flex;
flex-wrap: nowrap;
gap: 8px;
}
.production-stat-item {
@include stat-item;
cursor: pointer; // 增加鼠标指针,提示可点击
.stat-value {
font-size: 22px;
color: #00f2ff;
font-weight: bold;
text-shadow: 0 0 8px rgba(0, 242, 255, 0.6), 0 0 15px rgba(0, 242, 255, 0.3);
font-family: "Arial Black", sans-serif;
margin-bottom: 2px;
letter-spacing: 1px;
}
.stat-unit {
display: block;
font-size: 14px;
color: #66a3ff;
margin-top: 2px;
text-align: center;
}
.stat-label {
font-size: 11px;
color: #aaa;
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
// 响应式设计
@media (max-width: 1200px) {
.production-stat-item .stat-value {
font-size: 18px;
}
.production-stat-item .stat-label {
font-size: 10px;
}
}
@media (max-width: 900px) {
.production-stats-grid {
grid-template-columns: repeat(1, 1fr);
grid-template-rows: repeat(6, 1fr);
}
}
</style>
@@ -1,341 +0,0 @@
<!--
* @Author: 844772650@qq.com 844772650@qq.com
* @Date: 2026-01-07 09:32:53
* @LastEditors: 844772650@qq.com 844772650@qq.com
* @LastEditTime: 2026-01-25 16:41:17
* @FilePath: \durian-web\src\components\RightPanel.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<section class="right-col">
<!-- 左侧子列 -->
<div class="right-left-col">
<ProductionStats />
<div class="cold-storage-row">
<ColdStorageUtilization/>
<ColdStorageTemperature :temperatureData="humidityData" />
<ColdStorageHumidity :humidityData="temperatureData" />
</div>
<ColdStorageDetails />
<WarehouseMonitor />
</div>
<!-- 右侧子列 -->
<div class="right-right-col">
<ExportOrders />
<LogisticsTrack />
</div>
</section>
</template>
<script>
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from "@/utils/autoRefresh";
import ExportOrders from "@/components/tracing/screen/ExportOrders.vue";
import LogisticsTrack from "@/components/tracing/screen/LogisticsTrack.vue";
import ProductionStats from "@/components/tracing/screen/ProductionStats.vue";
import ColdStorageUtilization from "@/components/tracing/screen/ColdStorageUtilization.vue";
import ColdStorageTemperature from "@/components/tracing/screen/ColdStorageTemperature.vue";
import ColdStorageHumidity from "@/components/tracing/screen/ColdStorageHumidity.vue";
import ColdStorageDetails from "@/components/tracing/screen/ColdStorageDetails.vue";
import WarehouseMonitor from "@/components/tracing/screen/WarehouseMonitor.vue";
export default {
name: "RightPanel",
components: {
ExportOrders,
LogisticsTrack,
ProductionStats,
ColdStorageUtilization,
ColdStorageTemperature,
ColdStorageHumidity,
ColdStorageDetails,
WarehouseMonitor,
},
data() {
return {
loading: false,
timer: null,
temperatureData: [], // 温度数据数组,每个子数组对应一个冷库
humidityData: [], // 湿度数据数组,每个子数组对应一个冷库
rawData: [] // 原始数据缓存
};
},
mounted() {
// 每15分钟刷新一次(900000毫秒)
this.timer = autoRefresh.startRefresh(this.getHumitureData, 900000);
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose();
}
window.removeEventListener("resize", this.handleResize);
// 清除定时器
autoRefresh.stopRefresh(this.timer);
},
methods: {
createMockSeries(min, max, points = 20) {
const series = [];
for (let i = 0; i < points; i++) {
const v = min + Math.random() * (max - min);
series.push(Number(v.toFixed(1)));
}
return series;
},
applyMockHumitureData() {
const mockTemperatureData = [];
const mockHumidityData = [];
for (let coldId = 1; coldId <= 7; coldId++) {
mockTemperatureData.push(this.createMockSeries(2, 5));
mockHumidityData.push(this.createMockSeries(65, 85));
}
this.temperatureData = mockTemperatureData;
this.humidityData = mockHumidityData;
this.rawData = [];
},
formatDateForQuery(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
async getHumitureData() {
if (this.loading) return;
this.loading = true;
let allData = [];
let offset = 0;
const limit = 100;
const maxRecords = 600; // 最近24小时足够画图,避免高频大批量拉取
const startTime = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recentTimeCondition = [{
fieldName: "date",
value: [this.formatDateForQuery(startTime)],
operator: "gte"
}];
let hasAnyData = false;
try {
// 获取数据
while (allData.length < maxRecords) {
let res = await huobanAuth.queryData({
tableId: 2100000000014955,
limit: limit,
offset: offset,
conditions: recentTimeCondition,
relations: [],
orderField: "date",
orderType: "desc"
});
if (res.code === 1 && res.data && res.data.length > 0) {
hasAnyData = true;
// console.log('温湿度数据===', res.data);
allData = [...allData, ...res.data];
// 如果本次返回的数据少于limit,说明没有更多数据了
if (res.data.length < limit) {
break;
}
offset += limit;
// 延迟一下,避免请求过快
await new Promise((r) => setTimeout(r, 100));
} else {
console.error("API错误或无数据:", res);
break;
}
}
// 若按时间窗口无数据,则回退到不带时间窗口查询一页,避免因字段格式差异导致全空
if (!hasAnyData) {
const fallbackRes = await huobanAuth.queryData({
tableId: 2100000000014955,
limit: 200,
offset: 0,
conditions: [],
relations: [],
orderField: "_createTime",
orderType: "desc"
});
if (fallbackRes && fallbackRes.code === 1 && Array.isArray(fallbackRes.data) && fallbackRes.data.length > 0) {
allData = fallbackRes.data.slice(0, maxRecords);
hasAnyData = true;
}
}
// 限制最多2000条
if (allData.length > maxRecords) {
allData = allData.slice(0, maxRecords);
}
if (!hasAnyData || allData.length === 0) {
this.applyMockHumitureData();
} else {
// 缓存原始数据
this.rawData = allData;
// 处理数据:若有效点仍为0,则启用模拟数据兜底
const hasValidData = this.processHumitureData(allData);
if (!hasValidData) {
this.applyMockHumitureData();
}
}
} catch (err) {
console.error("获取温湿度数据出错:", err);
this.applyMockHumitureData();
} finally {
this.loading = false;
}
},
// 处理温湿度数据
processHumitureData(data) {
// 按冷库ID分组
const groupedByColdId = {};
let validPointCount = 0;
// 初始化7个冷库的数据结构
for (let i = 1; i <= 7; i++) {
groupedByColdId[i] = [];
}
// 分组数据
data.forEach(item => {
const coldId = item.coldId;
if (coldId >= 1 && coldId <= 7) {
const temperature = Number(item.wendu);
const humidity = Number(item.shidu);
if (Number.isFinite(temperature) || Number.isFinite(humidity)) {
validPointCount += 1;
}
groupedByColdId[coldId].push({
date: item.date,
temperature: Number.isFinite(temperature) ? temperature : 0,
humidity: Number.isFinite(humidity) ? humidity : 0,
addr: item.addr
});
}
});
// 处理每个冷库的数据
const temperatureData = [];
const humidityData = [];
for (let coldId = 1; coldId <= 7; coldId++) {
const coldData = groupedByColdId[coldId];
if (coldData.length === 0) {
// 如果没有数据,使用默认值(20个点)
temperatureData.push(new Array(20).fill(0));
humidityData.push(new Array(20).fill(0));
continue;
}
// 按时间排序(从旧到新)
coldData.sort((a, b) => new Date(a.date) - new Date(b.date));
// 获取按小时分组的数据(每小时取最后一个点)
const hourlyData = this.getHourlyData(coldData);
// 取最近20个小时的数据用于图表展示
const recentTempData = hourlyData.map(item => item.temperature).slice(-20);
const recentHumidityData = hourlyData.map(item => item.humidity).slice(-20);
// 如果数据不足20个点,用0填充前面
while (recentTempData.length < 20) {
recentTempData.unshift(0);
}
while (recentHumidityData.length < 20) {
recentHumidityData.unshift(0);
}
temperatureData.push(recentTempData);
humidityData.push(recentHumidityData);
}
// 更新图表数据
this.temperatureData = temperatureData;
this.humidityData = humidityData;
return validPointCount > 0;
// console.log('温度数据:', this.temperatureData);
// console.log('湿度数据:', this.humidityData);
},
// 按小时获取数据(每小时取最后一个数据点)
getHourlyData(data) {
if (data.length === 0) return [];
const hourlyMap = new Map();
// 遍历数据,按小时分组
data.forEach(item => {
const date = new Date(item.date);
// 创建小时键,例如 "2026-01-20 11"
const hourKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}`;
// 如果这个小时还没有数据,或者这个数据比已有数据更新,则更新这个小时的数据
if (!hourlyMap.has(hourKey) || new Date(item.date) > new Date(hourlyMap.get(hourKey).date)) {
hourlyMap.set(hourKey, {
hour: hourKey,
date: item.date,
temperature: item.temperature,
humidity: item.humidity
});
}
});
// 转换为数组并按时间排序(从旧到新)
return Array.from(hourlyMap.values())
.sort((a, b) => new Date(a.date) - new Date(b.date));
},
// 获取指定冷库的原始数据(用于调试)
getColdStorageData(coldId) {
return this.rawData.filter(item => item.coldId === coldId);
}
}
};
</script>
<style lang="scss" scoped>
.right-col {
display: grid;
// grid-template-columns: 60% 40%; /* 左右两列比例 */
grid-template-columns: 59% 40.5%;
gap: 10px;
}
.right-left-col {
display: flex;
flex-direction: column;
gap: 10px;
}
.right-right-col {
display: flex;
flex-direction: column;
gap: 10px;
}
.cold-storage-row {
display: flex;
gap: 10px;
// height: 180px;
flex: 1;
> * {
flex: 1;
}
}
</style>
@@ -1,626 +0,0 @@
<template>
<div>
<!-- 大屏独立生产批次列表弹框对齐 web-zx 条件不复用 ProductionBatchModal -->
<div v-if="visible" class="batch-modal-mask" @click="closeModal">
<div class="batch-modal-container" @click.stop>
<div class="modal-header">
<div class="modal-title">生产批次</div>
<div class="modal-tabs">
<button
class="tab-btn"
:class="{ active: activeTab === 'domestic' }"
@click="switchTab('domestic')"
>
内贸
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'export' }"
@click="switchTab('export')"
>
出口
</button>
</div>
<div class="modal-search">
<input
v-model="searchForm.specificationLevel"
type="text"
placeholder="批次号:请输入"
class="search-input"
@keyup.enter="handleSearch"
/>
<input
v-model="searchForm.productName"
type="text"
placeholder="产品名称:请输入"
class="search-input"
@keyup.enter="handleSearch"
/>
<select
v-model="searchForm.orchardName"
class="search-select"
@change="handleSearch"
>
<option value="">果园全部</option>
<option value="正欣果园">正欣果园</option>
<option value="其他果园">其他果园</option>
</select>
<div class="date-range">
生产日期范围:
<input
v-model="searchForm.startDate"
type="month"
class="date-input"
@change="handleSearch"
/>
<input
v-model="searchForm.endDate"
type="month"
class="date-input"
@change="handleSearch"
/>
</div>
<button class="search-btn" @click="handleSearch">搜索</button>
</div>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-content">
<div class="qrcode-grid" v-loading="listLoading">
<div
class="qrcode-item"
v-for="item in qrcodeList"
:key="item._itemId"
@click="openSubModal(item)"
>
<div class="qrcode-img">
<div class="qrcode-placeholder">
<div :id="'screen-qrcode-' + item._itemId" class="qrcode-canvas"></div>
</div>
<button class="scan-btn">扫码查看</button>
</div>
<div class="qrcode-info">
<div class="batch-code">{{ item.productDescription || item._title || '-' }}</div>
<div class="batch-type">{{ item.specificationLevel || '-' }}</div>
<div class="batch-detail">
果园{{ item.orchardName || item.orchardAbbreviation || '-' }} {{ item.rawMaterialOutboundBatch || '' }}
</div>
<div class="batch-detail">
{{ resolvePlantName(item) }} {{ formatDate(item.createTime2 || item._createTime) }} 加工
</div>
</div>
</div>
</div>
<el-empty
v-if="!listLoading && qrcodeList.length === 0"
description="暂无生产批次数据"
:image-size="100"
></el-empty>
</div>
<div class="modal-pagination">
<button
class="page-btn"
@click="handlePrev"
:disabled="pageNum <= 1"
:style="{ opacity: pageNum <= 1 ? 0.5 : 1, cursor: pageNum <= 1 ? 'not-allowed' : 'pointer' }"
>
&lt;
</button>
<button class="page-btn active">{{ pageNum }}</button>
<button
class="page-btn"
@click="handleNext"
:disabled="!hasMore"
:style="{ opacity: !hasMore ? 0.5 : 1, cursor: !hasMore ? 'not-allowed' : 'pointer' }"
>
&gt;
</button>
</div>
</div>
</div>
<BatchDetailModal
:visible="isSubVisible"
:batchData="currentBatch"
:itemId="currentBatch?._itemId || ''"
:ignore-permission="true"
@update:visible="(val) => (isSubVisible = val)"
/>
</div>
</template>
<script>
import BatchDetailModal from '@/components/tracing/BatchDetailModal.vue'
import huobanAuth from '@/utils/huobanAuth'
import autoRefresh from '@/utils/autoRefresh'
import QRCode from 'qrcodejs2'
export default {
name: 'ScreenProductionBatchModal',
components: {
BatchDetailModal
},
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
isSubVisible: false,
currentBatch: {},
qrcodeList: [],
listLoading: false,
pageNum: 1,
pageSize: 12,
hasMore: false,
activeTab: 'domestic',
searchForm: {
specificationLevel: '',
productName: '',
orchardName: '',
startDate: '',
endDate: ''
},
refreshTimer: null,
tableId: 2100000000016032
}
},
watch: {
visible(newVal) {
if (newVal) {
this.loadList()
this.startAutoRefresh()
} else {
this.stopAutoRefresh()
}
}
},
beforeDestroy() {
this.stopAutoRefresh()
},
methods: {
async loadList() {
this.listLoading = true
try {
// 对齐 web-zx:固定关联企业条件
const conditions = [
{
fieldName: 'glqy',
value: [2300000019501821],
operator: 'eq'
}
]
// Tab 筛选(内贸/出口)— 与 web-zx 一致,条件暂注释
// if (this.activeTab === 'export') {
// conditions.push({
// fieldName: 'isExportProcessingPlant',
// value: [1],
// operator: 'eq'
// })
// } else {
// conditions.push({
// fieldName: 'isExportProcessingPlant',
// value: [0],
// operator: 'eq'
// })
// }
if (this.searchForm.specificationLevel) {
conditions.push({
fieldName: 'specificationLevel',
value: [this.searchForm.specificationLevel],
operator: 'in'
})
}
if (this.searchForm.productName) {
conditions.push({
fieldName: 'productName',
value: this.searchForm.productName,
operator: 'eq'
})
}
if (this.searchForm.orchardName) {
conditions.push({
fieldName: 'orchardName',
value: [this.searchForm.orchardName],
operator: 'eq'
})
}
if (this.searchForm.startDate && this.searchForm.endDate) {
conditions.push({
fieldName: 'createTime2',
value: [this.searchForm.startDate, this.searchForm.endDate],
operator: 'between'
})
}
const params = {
tableId: this.tableId,
limit: this.pageSize + 1,
offset: (this.pageNum - 1) * this.pageSize,
conditions,
relations: []
}
const res = await huobanAuth.queryData(params)
if (res.code === 1 && res.data) {
if (res.data.length > this.pageSize) {
this.hasMore = true
this.qrcodeList = res.data.slice(0, this.pageSize)
} else {
this.hasMore = false
this.qrcodeList = res.data
}
// #region agent log
const sample = (this.qrcodeList || []).slice(0, 3).map((it) => ({
id: it._itemId,
processingPlantName: it.processingPlantName || '',
processingPlantAbbreviation: it.processingPlantAbbreviation || '',
plantDisplay: this.resolvePlantName(it),
productProcessingPlant: it.productProcessingPlant,
createTime2: it.createTime2 || ''
}))
fetch('http://127.0.0.1:7718/ingest/48ca8203-b675-41cd-9304-a7ecac67f5c5',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'897359'},body:JSON.stringify({sessionId:'897359',runId:'field-fix',hypothesisId:'P1',location:'ScreenProductionBatchModal.vue:loadList',message:'list plant fields sample',data:{count:this.qrcodeList.length,sample},timestamp:Date.now()})}).catch(()=>{});
// #endregion
this.$nextTick(() => {
this.generateQRCodeList()
})
} else {
this.qrcodeList = []
this.hasMore = false
}
} catch (error) {
console.error('加载生产批次列表失败:', error)
this.$message.error('加载数据失败')
this.qrcodeList = []
this.hasMore = false
} finally {
this.listLoading = false
}
},
switchTab(tab) {
this.activeTab = tab
this.pageNum = 1
this.loadList()
},
handleSearch() {
this.pageNum = 1
this.loadList()
},
handlePrev() {
if (this.pageNum > 1) {
this.pageNum--
this.loadList()
}
},
handleNext() {
if (this.hasMore) {
this.pageNum++
this.loadList()
}
},
generateQRCodeList() {
this.qrcodeList.forEach(item => {
const containerId = 'screen-qrcode-' + item._itemId
const container = document.getElementById(containerId)
if (container) {
container.innerHTML = ''
const h5Url = this.getQrCodeUrl(item)
try {
new QRCode(container, {
text: h5Url,
width: 150,
height: 150,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: 0
})
} catch (error) {
console.error('二维码生成失败:', item._itemId, error)
}
}
})
},
getQrCodeUrl(item) {
const baseUrl = window.location.origin
return `${baseUrl}/tracing/trace-detail-h5?itemId=${item._itemId}`
},
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
resolvePlantName(item) {
if (!item) return '-'
const fromText = item.processingPlantAbbreviation || item.processingPlantName
if (fromText) return fromText
const rel = item.productProcessingPlant
if (Array.isArray(rel) && rel.length) {
const first = rel[0]
return (first && (first.title || first._title || first.name)) || '-'
}
return '-'
},
startAutoRefresh() {
this.stopAutoRefresh()
this.refreshTimer = autoRefresh.startRefresh(this.loadList, 30 * 60 * 1000)
},
stopAutoRefresh() {
autoRefresh.stopRefresh(this.refreshTimer)
this.refreshTimer = null
},
closeModal() {
this.$emit('update:visible', false)
},
openSubModal(batch) {
this.currentBatch = batch
this.isSubVisible = true
}
}
}
</script>
<style lang="scss" scoped>
:deep(.batch-modal-mask) {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 13, 33, 0.9);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.batch-modal-container) {
width: 90%;
height: 90%;
background: #030d21;
border: 2px solid #1a4a8d;
border-radius: 8px;
padding: 20px;
box-sizing: border-box;
}
:deep(.modal-header) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
color: #fff;
.modal-title {
font-size: 24px;
font-weight: bold;
color: #00f2ff;
}
.modal-tabs {
margin: 0 20px;
.tab-btn {
background: transparent;
border: 1px solid #1a4a8d;
color: #aaa;
padding: 6px 12px;
margin-right: 8px;
border-radius: 4px;
cursor: pointer;
&.active {
background: #1a4a8d;
color: #00f2ff;
border-color: #00f2ff;
}
}
}
.modal-search {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
.search-input {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
width: 180px;
}
.search-select {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
}
.date-range {
display: flex;
align-items: center;
gap: 8px;
color: #aaa;
.date-input {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #fff;
padding: 4px;
border-radius: 4px;
}
}
.search-btn {
background: #1a4a8d;
border: 1px solid #00f2ff;
color: #00f2ff;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #00f2ff;
color: #030d21;
}
}
}
.close-btn {
background: transparent;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
&:hover {
color: #00f2ff;
}
}
}
:deep(.modal-content) {
height: calc(100% - 120px);
overflow-y: auto;
.qrcode-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
padding: 10px 0;
}
.qrcode-item {
background: rgba(26, 74, 141, 0.3);
border: 1px solid #1a4a8d;
border-radius: 6px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #00f2ff;
background: rgba(26, 74, 141, 0.5);
box-shadow: 0 0 10px rgba(0, 242, 255, 0.3);
}
.qrcode-img {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 10px;
.qrcode-placeholder {
width: 120px;
height: 120px;
background: #fff;
margin-bottom: 8px;
border: 1px solid #ccc;
padding: 1px;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.scan-btn {
background: #00f2ff;
color: #030d21;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #fff;
}
}
}
.qrcode-info {
color: #aaa;
font-size: 14px;
line-height: 1.5;
.batch-code {
color: #00f2ff;
font-size: 16px;
font-weight: bold;
margin-bottom: 4px;
}
.batch-type {
margin-bottom: 4px;
}
.batch-detail {
margin-bottom: 2px;
}
}
}
}
:deep(.modal-pagination) {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 20px;
.page-btn {
background: rgba(26, 74, 141, 0.5);
border: 1px solid #1a4a8d;
color: #aaa;
width: 32px;
height: 32px;
border-radius: 4px;
cursor: pointer;
&.active {
background: #00f2ff;
color: #030d21;
border-color: #00f2ff;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
</style>
@@ -1,551 +0,0 @@
<template>
<div class="trend-dialog-mask" v-show="visible" @click="close">
<div class="trend-dialog" @click.stop>
<!-- 弹框头部标题 + 时间筛选 -->
<div class="dialog-header">
<h3 class="dialog-title">{{ currentDevice.name || '土壤墒情历史趋势' }}</h3>
<div class="time-tabs">
<button
class="time-tab"
:class="{ active: activeTimeRange === 3 }"
@click="handleTimeRangeChange(3)"
> 3 </button>
<button
class="time-tab"
:class="{ active: activeTimeRange === 7 }"
@click="handleTimeRangeChange(7)"
> 7 </button>
<button
class="time-tab"
:class="{ active: activeTimeRange === 15 }"
@click="handleTimeRangeChange(15)"
> 15 </button>
</div>
</div>
<!-- 弹框主体左侧信息 + 右侧折线图 -->
<div class="dialog-body">
<!-- 左侧设备实时信息 -->
<div class="left-info">
<!-- 电量三层设备不展示 -->
<div class="info-item" v-if="isFiveLayerSoil">
<span class="label">电量</span>
<span class="value">{{ currentDevice.data?.power || 0 }}%</span>
<img
:src="powerIcon"
alt="电量"
class="icon"
>
</div>
<!-- 信号三层设备不展示 -->
<div class="info-item" v-if="isFiveLayerSoil">
<span class="label">信号</span>
<span class="value">{{ currentDevice.data?.csq || 0 }}</span>
<img
:src="signalIcon"
alt="信号"
class="icon"
>
</div>
<!-- 空气温度 -->
<div class="info-item">
<span class="label">空气</span>
<span class="value">{{ currentDevice.data?.airTemp || 0 }}</span>
</div>
<!-- 各层数据 -->
<div class="layer-list">
<div class="layer-header">
<span>层数</span>
<span>数据</span>
</div>
<div class="layer-info" v-for="layer in displaySoilLayers" :key="layer.level">
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-value">
{{ currentDevice.data?.[`temp${layer.level}`] || 0 }} /
{{ currentDevice.data?.[`sd${layer.level}`] || 0 }}% /
{{ currentDevice.data?.[`ec${layer.level}`] || 0 }}ms/cm
</span>
</div>
</div>
</div>
<!-- 右侧三个折线图 -->
<div class="right-charts">
<div class="chart-item">
<h4 class="chart-title">温度趋势</h4>
<div ref="tempChart" class="chart-container"></div>
</div>
<div class="chart-item">
<h4 class="chart-title">湿度趋势</h4>
<div ref="humChart" class="chart-container"></div>
</div>
<div class="chart-item">
<h4 class="chart-title">EC 值趋势</h4>
<div ref="ecChart" class="chart-container"></div>
</div>
</div>
<!-- 加载/无数据提示 -->
<div class="loading-mask" v-if="loading">
<span>数据加载中...</span>
</div>
<div class="empty-mask" v-if="!loading && allHistoryData.length === 0">
<span>暂无历史数据</span>
</div>
</div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts';
import huobanAuth from '@/utils/huobanAuth'
export default {
name: 'SoilHistoryTrendDialog',
props: {
deviceInfo: {
type: Object,
required: true
}
},
data() {
return {
visible: false,
activeTimeRange: 3,
loading: false,
currentDevice: { name: '', deviceNo: '', data: {} },
allHistoryData: [],
// 去掉六层数据
soilLayers: [
{ level: 1, name: '一层' },
{ level: 2, name: '二层' },
{ level: 3, name: '三层' },
{ level: 4, name: '四层' },
{ level: 5, name: '五层' }
],
chartInstances: { temp: null, hum: null, ec: null },
// 图标变量
powerIcon: require('@/assets/images/tracing-screen/电量.png'),
signalIcon: ''
};
},
// 监听 deviceInfo 变化,确保第一次点击就更新数据
watch: {
deviceInfo: {
immediate: true,
handler(newVal) {
this.currentDevice = { ...newVal, data: newVal?.data || {} };
// 同步更新信号图标
const csq = Number(this.currentDevice.data?.csq) || 0;
this.signalIcon = csq >= 20
? require('@/assets/images/tracing-screen/signal-normal.png')
: require('@/assets/images/tracing-screen/signal-weak.png');
}
}
},
computed: {
soilMaxLayers() {
const data = this.currentDevice.data || {};
if (data.maxLayers === 5 || data.maxLayers === 3) return Number(data.maxLayers);
return this.resolveSoilMaxLayers(data);
},
isFiveLayerSoil() {
return this.soilMaxLayers === 5;
},
// 根据实际数据动态返回层数(3 层或 5 层),过滤掉全为 0 的层
displaySoilLayers() {
const effectiveMaxLayers = this.soilMaxLayers;
return this.soilLayers
.filter(layer => layer.level <= effectiveMaxLayers)
.filter(layer => {
const temp = this.currentDevice.data?.[`temp${layer.level}`];
const sd = this.currentDevice.data?.[`sd${layer.level}`];
const ec = this.currentDevice.data?.[`ec${layer.level}`];
return (temp && temp !== 0) || (sd && sd !== 0) || (ec && ec !== 0);
});
},
showPowerAndSignal() {
return this.isFiveLayerSoil;
},
},
beforeDestroy() {
this.destroyCharts();
},
methods: {
resolveExportOption(value) {
if (value === null || value === undefined || value === "") return "";
const first = Array.isArray(value) ? value[0] : value;
if (first === null || first === undefined || first === "") return "";
if (typeof first === "object") {
if (first.id !== undefined && first.id !== null && first.id !== "") {
return String(first.id);
}
if (first.value !== undefined && first.value !== null && first.value !== "") {
return String(first.value);
}
return "";
}
return String(first);
},
isEmptySoilValue(value) {
if (value === null || value === undefined || value === "") return true;
const num = Number(value);
return !Number.isNaN(num) && num === 0;
},
hasUpperSoilLayers(item) {
if (!item) return false;
for (let level = 4; level <= 5; level++) {
if (
!this.isEmptySoilValue(item[`temp${level}`]) ||
!this.isEmptySoilValue(item[`sd${level}`]) ||
!this.isEmptySoilValue(item[`ec${level}`])
) {
return true;
}
}
return false;
},
resolveSoilMaxLayers(item) {
const id = this.resolveExportOption(item && item.isExportProcessingPlant);
if (id === "2") return 5;
if (id === "1") return 3;
return this.hasUpperSoilLayers(item) ? 5 : 3;
},
destroyCharts() {
Object.keys(this.chartInstances).forEach(key => {
this.chartInstances[key]?.dispose();
this.chartInstances[key] = null;
});
},
initCharts() {
this.destroyCharts();
this.$refs.tempChart && (this.chartInstances.temp = echarts.init(this.$refs.tempChart));
this.$refs.humChart && (this.chartInstances.hum = echarts.init(this.$refs.humChart));
this.$refs.ecChart && (this.chartInstances.ec = echarts.init(this.$refs.ecChart));
window.addEventListener('resize', () => Object.values(this.chartInstances).forEach(chart => chart?.resize()));
},
open() {
this.visible = true;
this.$nextTick(() => {
this.initCharts();
this.fetchHistoryData();
});
},
close() {
this.visible = false;
this.allHistoryData = [];
this.destroyCharts();
},
handleTimeRangeChange(range) {
this.activeTimeRange = range;
this.allHistoryData = [];
this.fetchHistoryData();
},
async fetchHistoryData() {
if (this.loading) return;
this.loading = true;
const deviceNo = this.currentDevice.deviceNo;
const startTime = new Date();
startTime.setDate(startTime.getDate() - this.activeTimeRange);
const startDateStr = startTime.toISOString().slice(0, 10);
let allData = [];
let offset = 0;
const limit = 100;
let hasMore = true;
try {
while (hasMore) {
const res = await huobanAuth.queryData({
tableId: 2100000000014961,
limit, offset,
conditions: [{ fieldName: "deviceNo", value: deviceNo, operator: "eq" },{ fieldName: "reportTime", value: startDateStr, operator: "gte" }]
});
res.code === 1 && res.data?.length > 0 ? (allData = allData.concat(res.data), hasMore = res.data.length === limit, offset += limit) : (hasMore = false);
await new Promise(r => setTimeout(r, 100));
}
this.allHistoryData = allData.sort((a, b) => new Date(a.reportTime) - new Date(b.reportTime));
this.updateCharts();
} catch (error) {
console.error("拉取历史数据失败:", error);
this.allHistoryData = [];
} finally {
this.loading = false;
}
},
getSampleInterval(dataLength) {
let interval = this.activeTimeRange === 3 ? 1 : this.activeTimeRange === 7 ? 2 : 4;
if (dataLength > 120) {
interval = Math.max(interval, Math.ceil(dataLength / 80));
}
return interval;
},
getYAxisRange(seriesList) {
const values = seriesList
.flatMap((series) => series.data)
.map((value) => Number(value))
.filter((value) => Number.isFinite(value));
if (values.length === 0) {
return { min: 0, max: 1 };
}
const dataMin = Math.min(...values);
const dataMax = Math.max(...values);
let min = Math.floor(dataMin);
let max = Math.floor(dataMax) + 1;
if (min >= max) {
max = min + 1;
}
return { min, max };
},
buildLineSeries(nameSuffix, fieldPrefix, sampledData) {
return this.soilLayers.map((layer) => ({
name: `${layer.name}${nameSuffix}`,
type: 'line',
data: sampledData.map((item) => item[`${fieldPrefix}${layer.level}`] || 0),
showSymbol: false,
smooth: false,
lineStyle: { width: 2 },
}));
},
processChartData() {
const data = this.allHistoryData;
if (data.length === 0) return { temp: { xAxis: [], series: [] }, hum: { xAxis: [], series: [] }, ec: { xAxis: [], series: [] } };
const timeInterval = this.getSampleInterval(data.length);
const sampledData = data.filter((_, idx) => idx % timeInterval === 0);
const xAxisData = sampledData.map(item => this.activeTimeRange === 3 ? item.reportTime.slice(11, 16) : `${item.reportTime.slice(5, 10)} ${item.reportTime.slice(11, 13)}`);
const tempSeries = this.buildLineSeries('温度', 'temp', sampledData);
const humSeries = this.buildLineSeries('湿度', 'sd', sampledData);
const ecSeries = this.buildLineSeries('EC', 'ec', sampledData);
return { temp: { xAxis: xAxisData, series: tempSeries }, hum: { xAxis: xAxisData, series: humSeries }, ec: { xAxis: xAxisData, series: ecSeries } };
},
updateCharts() {
const chartData = this.processChartData();
const commonOption = (title) => {
const series = chartData[title].series;
const range = this.getYAxisRange(series);
return {
legend: { show: true, top: '5%', textStyle: { color: '#fff' }, itemWidth: 12, itemHeight: 12 },
tooltip: { trigger: 'axis' },
grid: { left: '5%', right: '5%', bottom: '10%', top: '20%', containLabel: true },
xAxis: { type: 'category', data: chartData[title].xAxis, boundaryGap: false, axisLabel: { color: '#fff' } },
yAxis: {
type: 'value',
min: range.min,
max: range.max,
axisLabel: { color: '#fff' },
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.15)' } },
},
series,
noDataLoadingOption: { text: '暂无数据', textStyle: { color: '#fff' } },
};
};
this.chartInstances.temp?.setOption(commonOption('temp'));
this.chartInstances.hum?.setOption(commonOption('hum'));
this.chartInstances.ec?.setOption(commonOption('ec'));
}
}
};
</script>
<style lang="scss" scoped>
/* 遮罩层 */
.trend-dialog-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
/* 弹框主体 */
.trend-dialog {
width: 90%;
max-width: 1400px;
max-height: 90vh;
background: rgba(0, 29, 82, 0.95);
border: 1px solid #1a4a8d;
border-radius: 8px;
color: #fff;
overflow: hidden;
box-sizing: border-box;
padding: 16px;
position: relative;
}
/* 头部样式 */
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255,255,255,0.15);
.dialog-title {
font-size: 18px;
color: #fff;
margin: 0;
}
.time-tabs {
display: flex;
gap: 8px;
.time-tab {
padding: 6px 12px;
background: #2a4866;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
transition: background 0.3s;
&.active {
background: #00f2ff;
color: #051a44;
font-weight: 500;
}
&:hover {
background: #1a4a8d;
}
}
}
}
/* 主体样式 */
.dialog-body {
display: flex;
gap: 20px;
min-height: 400px;
height: calc(90vh - 80px);
}
/* 加宽左侧信息栏,避免 EC 值换行 */
.left-info {
width: 280px;
flex-shrink: 0;
background: rgba(255,255,255,0.05);
border-radius: 6px;
padding: 14px;
box-sizing: border-box;
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
gap: 4px;
.label {
font-size: 14px;
color: #e0e0e0;
width: 40px;
}
.value {
font-size: 14px;
color: #fff;
flex: 1;
text-align: right;
}
.icon {
width: 20px;
height: 20px;
object-fit: contain;
margin-left: 2px;
}
}
.layer-list {
margin-top: 20px;
.layer-header {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.2);
font-weight: 500;
color: #00f2ff;
}
.layer-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
font-size: 13px;
.layer-name {
color: #fff;
width: 40px;
text-align: left;
}
/* 数据列不换行,加宽后足够展示 EC 值 */
.layer-value {
color: #e0e0e0;
flex: 1;
text-align: right;
white-space: nowrap;
}
}
}
}
/* 右侧图表样式 */
.right-charts {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
box-sizing: border-box;
.chart-item {
flex: 1;
min-height: 180px;
background: rgba(255,255,255,0.05);
border-radius: 6px;
padding: 12px;
box-sizing: border-box;
.chart-title {
font-size: 14px;
color: #fff;
margin: 0 0 8px 0;
}
.chart-container {
width: 100%;
height: calc(100% - 24px);
}
}
}
/* 加载/无数据遮罩 */
.loading-mask, .empty-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 29, 82, 0.55);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
z-index: 10;
}
</style>
@@ -1,81 +0,0 @@
<!--
* @Author: 844772650@qq.com 844772650@qq.com
* @Date: 2026-01-07 09:40:20
* @LastEditors: 844772650@qq.com 844772650@qq.com
* @LastEditTime: 2026-01-25 17:53:22
* @FilePath: \durian-web\src\components\WarehouseMonitor.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="panel warehouse-monitor">
<div class="panel-title">仓内监控</div>
<div class="monitor-grid">
<div v-for="(item, index) in warehouseImages" :key="index" class="monitor-item">
<ZLMediaKitPlayer app-name="cam" :stream-name="item" zl-media-url="http://192.168.10.100"/>
</div>
</div>
</div>
</template>
<script>
import ZLMediaKitPlayer from "@/components/tracing/screen/players/ZLMediaKitPlayer.vue";
export default {
name: 'WarehouseMonitor',
components: {ZLMediaKitPlayer},
data() {
return {
warehouseImages: [
'111',
'122',
'119'
]
}
},
methods: {
getImagePath(filename) {
return require(`@/assets/images/tracing-screen/${filename}`)
},
handleImageError(event) {
console.error('仓库监控图片加载失败:', event.target.src)
event.target.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="#061e5d"/><text x="50" y="50" font-family="Arial" font-size="10" fill="#aaa" text-anchor="middle" dy=".3em">仓库监控</text></svg>'
}
}
}
</script>
<style lang="scss" scoped>
@import '@/styles/tracing-screen.scss';
.warehouse-monitor {
@include panel-base;
// height: 262px;
flex: 1;
}
.monitor-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
height: calc(100% - 20px);
}
.monitor-item {
width: 100%;
height: 100%;
// overflow: hidden;
border-radius: 4px;
}
.monitor-image {
width: 100%;
height: 100%;
object-fit: fill;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
}
</style>
@@ -1,282 +0,0 @@
<template>
<div class="video-wrapper">
<div v-if="name" class="stream-name">{{ name }}</div>
<div class="status-text">{{ statusText }}</div>
<div :class="`status-dot ${statusClass}`"></div>
<video
ref="video"
muted
playsinline
disablePictureInPicture
class="video-element"
@click="toggleFullscreen"
></video>
</div>
</template>
<script>
import flvjs from 'flv.js'
const MAX_RECONNECT_ATTEMPTS = 5
const RECONNECT_DELAY_MS = 5000
const WAKE_HINT_DELAY_MS = 15000
export default {
name: 'EzvizFlvPlayer',
props: {
url: {
type: String,
required: true
},
name: {
type: String,
default: ''
},
quality: {
type: Number,
default: 2
}
},
data() {
return {
player: null,
statusText: '连接中...',
statusClass: 'connecting',
reconnectCount: 0,
reconnectTimer: null,
wakeHintTimer: null
}
},
watch: {
url() {
this.reconnectCount = 0
this.$nextTick(() => this.createPlayer())
}
},
mounted() {
this.$nextTick(() => this.createPlayer())
},
beforeDestroy() {
this.clearTimers()
this.destroyPlayer()
},
methods: {
clearTimers() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
if (this.wakeHintTimer) {
clearTimeout(this.wakeHintTimer)
this.wakeHintTimer = null
}
},
destroyPlayer() {
const video = this.$refs.video
if (video && this._onPlaying) {
video.removeEventListener('playing', this._onPlaying)
this._onPlaying = null
}
if (this.player) {
this.player.pause()
this.player.unload()
this.player.detachMediaElement()
this.player.destroy()
this.player = null
}
},
setConnected() {
this.clearTimers()
this.reconnectCount = 0
this.statusText = '检测中'
this.statusClass = 'connected'
},
getBandwidthHint() {
return '带宽超限'
},
scheduleReconnect(reason) {
if (this.reconnectCount >= MAX_RECONNECT_ATTEMPTS) {
this.statusText = this.getBandwidthHint()
this.statusClass = 'error'
console.error(`[EzvizFlvPlayer] ${this.name || 'stream'} gave up after retries:`, reason)
return
}
this.reconnectCount += 1
this.statusText = `重连中(${this.reconnectCount}/${MAX_RECONNECT_ATTEMPTS})...`
this.statusClass = 'connecting'
this.reconnectTimer = setTimeout(() => {
this.createPlayer()
}, RECONNECT_DELAY_MS)
},
startWakeHint() {
this.clearTimers()
this.statusText = '连接中...'
this.statusClass = 'connecting'
this.wakeHintTimer = setTimeout(() => {
if (this.statusClass === 'connecting') {
this.statusText = '唤醒设备中...'
}
}, WAKE_HINT_DELAY_MS)
},
createPlayer() {
const video = this.$refs.video
if (!video || !this.url) return
this.destroyPlayer()
this.startWakeHint()
if (!flvjs.isSupported()) {
this.clearTimers()
this.statusText = '浏览器不支持'
this.statusClass = 'error'
return
}
try {
this.player = flvjs.createPlayer({
type: 'flv',
url: this.url,
isLive: true,
hasAudio: false
}, {
enableStashBuffer: false,
stashInitialSize: 128
})
this.player.attachMediaElement(video)
this.player.load()
this.player.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
console.error(`[EzvizFlvPlayer] ${this.name || 'stream'} error:`, errorType, errorDetail, 'url:', this.url)
const detailText = JSON.stringify(errorDetail || '')
const is404 = detailText.includes('404') || detailText.includes('Not Found')
if (is404) {
this.statusText = 'URL无效(404)'
} else if (detailText.includes('9049')) {
this.statusText = '带宽超限(9049)'
} else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) {
this.statusText = '编码不支持'
} else if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
this.statusText = '网络错误'
}
this.destroyPlayer()
if (is404) {
this.clearTimers()
this.statusClass = 'error'
return
}
this.scheduleReconnect({ errorType, errorDetail })
})
this.player.on(flvjs.Events.MEDIA_INFO, () => {
this.setConnected()
})
this._onPlaying = () => this.setConnected()
video.addEventListener('playing', this._onPlaying)
video.play().catch((e) => {
console.warn('[EzvizFlvPlayer] Autoplay failed:', e)
})
} catch (err) {
console.error('[EzvizFlvPlayer] createPlayer error:', err)
this.scheduleReconnect(err)
}
},
toggleFullscreen() {
const video = this.$refs.video
if (!video) return
if (video.requestFullscreen) {
video.requestFullscreen()
} else if (video.webkitRequestFullscreen) {
video.webkitRequestFullscreen()
} else if (video.mozRequestFullScreen) {
video.mozRequestFullScreen()
}
}
}
}
</script>
<style scoped>
.video-wrapper {
position: relative;
width: 100%;
height: 100%;
background: #111;
overflow: hidden;
}
.video-element {
display: block;
object-fit: cover;
object-position: center;
width: 100%;
height: 100%;
background-color: #061e5d;
}
.stream-name {
position: absolute;
top: 4px;
left: 6px;
font-size: 11px;
color: white;
background: rgba(0, 0, 0, 0.6);
padding: 2px 6px;
border-radius: 3px;
z-index: 10;
}
.status-text {
position: absolute;
bottom: 4px;
right: 6px;
font-size: 10px;
color: #fff;
background: rgba(0, 0, 0, 0.6);
padding: 2px 6px;
border-radius: 3px;
z-index: 10;
}
.status-dot {
position: absolute;
top: 4px;
right: 6px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
}
.status-dot.connecting {
background: #ffaa00;
animation: pulse 1.5s infinite;
}
.status-dot.connected {
background: #00ff88;
box-shadow: 0 0 6px #00ff88;
}
.status-dot.error {
background: #ff4444;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
</style>
@@ -1,138 +0,0 @@
<template>
<div class="ezviz-player-wrap">
<div v-if="name" class="stream-name">{{ name }}</div>
<div class="status-text">{{ statusText }}</div>
<div :class="`status-dot ${statusClass}`"></div>
<div :id="containerId" ref="container" class="player-container">
<img
v-if="fallbackImage"
:src="fallbackImage"
:alt="name || 'monitor'"
class="fallback-img"
/>
</div>
</div>
</template>
<script>
/**
* 萤石播放器占位:无 ezuikit 依赖时降级为占位图(MonitorGrid 监听 need-fallback)。
*/
let playerSeed = 0
export default {
name: 'EzvizUIKitPlayer',
props: {
accessToken: { type: String, required: true },
deviceSerial: { type: String, default: '' },
channelNo: { type: Number, default: 1 },
stream: { type: String, default: 'sd' },
url: { type: String, default: '' },
name: { type: String, default: '' },
fallbackImage: { type: String, default: '' },
initDelay: { type: Number, default: 0 }
},
data() {
playerSeed += 1
return {
containerId: `ezviz-player-${playerSeed}`,
player: null,
statusText: '连接中...',
statusClass: 'connecting',
initTimer: null,
hasFallback: false
}
},
mounted() {
this.scheduleCreatePlayer()
},
beforeDestroy() {
if (this.initTimer) clearTimeout(this.initTimer)
this.destroyPlayer()
},
methods: {
scheduleCreatePlayer() {
if (!this.accessToken) {
this.triggerFallback('未配置 accessToken')
return
}
this.initTimer = setTimeout(() => {
this.createPlayer()
}, this.initDelay || 0)
},
async createPlayer() {
// 暂不加载 ezuikit-js,避免 webpack4 splitChunks CSS 冲突;无配置时直接降级
this.triggerFallback('萤石播放器未启用,使用占位图')
},
triggerFallback(reason) {
if (this.hasFallback) return
this.hasFallback = true
this.statusText = reason || '已降级'
this.statusClass = 'error'
this.$emit('need-fallback')
},
destroyPlayer() {
if (this.player && typeof this.player.stop === 'function') {
try {
this.player.stop()
} catch (e) { /* ignore */ }
}
this.player = null
}
}
}
</script>
<style scoped>
.ezviz-player-wrap {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: #061e5d;
}
.player-container {
width: 100%;
height: 100%;
}
.fallback-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.stream-name,
.status-text {
position: absolute;
z-index: 2;
color: #fff;
font-size: 12px;
padding: 2px 6px;
background: rgba(0, 0, 0, 0.45);
}
.stream-name {
top: 4px;
left: 4px;
}
.status-text {
bottom: 4px;
left: 4px;
}
.status-dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
border-radius: 50%;
z-index: 2;
}
.status-dot.connecting {
background: #ff9800;
}
.status-dot.playing {
background: #4caf50;
}
.status-dot.error {
background: #f44336;
}
</style>
@@ -1,306 +0,0 @@
<template>
<div class="video-wrapper">
<div class="stream-name">{{ streamName }}</div>
<div class="latency-display">{{ latencyText }}</div>
<div :class="`status-dot ${statusClass}`"></div>
<video
ref="video"
muted
playsinline
disablePictureInPicture
style="object-fit: contain; width: 100%; height: 100%; background-color: black"
@click="toggleFullscreen"
></video>
</div>
</template>
<script>
export default {
name: 'ZLMediaKitPlayer',
props: {
appName: {
type: String,
required: true
},
streamName: {
type: String,
required: true
},
zlMediaUrl: {
type: String,
default: 'http://192.168.10.100'
}
},
data() {
return {
latencyText: '--',
statusClass: '',
pc: null,
statsInterval: null,
checkInterval: null,
frameCallbackId: null,
timeupdateHandler: null,
lastActiveTime: Date.now(),
hasReceivedData: false,
reconnectCount: 0,
MAX_RECONNECT_ATTEMPTS: 30
};
},
mounted() {
this.createPlayer();
},
beforeDestroy() {
this.cleanup();
},
methods: {
cleanup() {
if (this.statsInterval) clearInterval(this.statsInterval);
if (this.checkInterval) clearInterval(this.checkInterval);
const video = this.$refs.video;
if (this.frameCallbackId && video && typeof video.cancelVideoFrameCallback === 'function') {
video.cancelVideoFrameCallback(this.frameCallbackId);
this.frameCallbackId = null;
}
if (this.timeupdateHandler && video) {
video.removeEventListener('timeupdate', this.timeupdateHandler);
this.timeupdateHandler = null;
}
if (this.pc) {
this.pc.close();
this.pc = null;
}
},
updateActiveTime() {
this.lastActiveTime = Date.now();
this.hasReceivedData = true;
},
startStatsCollection() {
this.statsInterval = setInterval(async () => {
if (!this.pc || this.pc.connectionState !== 'connected') return;
try {
const stats = await this.pc.getStats();
let minRtt = Infinity;
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
if (report.currentRoundTripTime) {
minRtt = Math.min(minRtt, report.currentRoundTripTime * 1000);
}
}
});
if (minRtt !== Infinity) {
this.latencyText = `${Math.round(minRtt)}ms`;
}
} catch (e) {
// ignore
}
}, 2000);
},
startCheckInterval() {
this.checkInterval = setInterval(() => {
if (!this.hasReceivedData) return;
const idleTime = Date.now() - this.lastActiveTime;
if (idleTime > 12000) {
console.warn(`[卡死] ${this.streamName} 无帧更新超时,重连...`);
this.reconnect();
}
}, 3000);
},
reconnect() {
this.cleanup();
if (this.reconnectCount < this.MAX_RECONNECT_ATTEMPTS) {
this.reconnectCount++;
this.createPlayer();
} else {
this.latencyText = '❌ 卡死';
this.statusClass = 'error';
}
},
async createPlayer() {
const video = this.$refs.video;
if (!video) return;
this.cleanup();
this.statusClass = 'connecting';
this.latencyText = '连接中...';
this.lastActiveTime = Date.now();
this.hasReceivedData = false;
this.reconnectCount = 0;
try {
this.pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require'
});
this.pc.ontrack = (event) => {
if (event.streams[0]) {
video.srcObject = event.streams[0];
video.play().catch(e => console.warn('Autoplay failed:', e));
}
};
this.pc.oniceconnectionstatechange = () => {
if (['connected', 'completed'].includes(this.pc.iceConnectionState)) {
this.statusClass = 'connected';
this.startStatsCollection();
} else if (['failed', 'disconnected'].includes(this.pc.iceConnectionState)) {
this.statusClass = 'error';
this.latencyText = '连接失败';
}
};
// 创建 Offer
const offer = await this.pc.createOffer({ offerToReceiveAudio: false, offerToReceiveVideo: true });
await this.pc.setLocalDescription(offer);
// 等待 ICE 收集完成(最多 3 秒)
if (this.pc.iceGatheringState !== 'complete') {
await new Promise((resolve) => {
const timeout = setTimeout(resolve, 3000);
const check = () => {
if (this.pc.iceGatheringState === 'complete') {
clearTimeout(timeout);
this.pc.removeEventListener('icegatheringstatechange', check);
resolve();
}
};
this.pc.addEventListener('icegatheringstatechange', check);
});
}
// 请求 SDP Answer
const apiUrl = `${this.zlMediaUrl}/index/api/webrtc?app=${encodeURIComponent(this.appName)}&stream=${encodeURIComponent(this.streamName)}&type=play`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: this.pc.localDescription.sdp
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.code !== 0) throw new Error(data.msg || 'Server error');
if (!data.sdp) throw new Error('No SDP in response');
await this.pc.setRemoteDescription({ type: 'answer', sdp: data.sdp });
// 帧活跃检测
let lastCurrentTime = 0;
const updateActiveTime = () => {
this.updateActiveTime();
};
if (typeof video.requestVideoFrameCallback === 'function') {
const onFrame = () => {
updateActiveTime();
this.frameCallbackId = video.requestVideoFrameCallback(onFrame);
};
this.frameCallbackId = video.requestVideoFrameCallback(onFrame);
} else {
const onTimeUpdate = () => {
if (video.currentTime !== lastCurrentTime) {
lastCurrentTime = video.currentTime;
updateActiveTime();
}
};
video.addEventListener('timeupdate', onTimeUpdate);
this.timeupdateHandler = onTimeUpdate;
}
// 启动卡死检测
this.startCheckInterval();
} catch (err) {
console.error(`Stream ${this.streamName} error:`, err);
this.latencyText = '❌';
this.statusClass = 'error';
if (this.reconnectCount < this.MAX_RECONNECT_ATTEMPTS) {
setTimeout(() => this.createPlayer(), 3000);
}
}
},
toggleFullscreen() {
const video = this.$refs.video;
if (!video) return;
if (video.requestFullscreen) {
video.requestFullscreen();
} else if (video.webkitRequestFullscreen) {
video.webkitRequestFullscreen();
} else if (video.mozRequestFullScreen) {
video.mozRequestFullScreen();
}
}
}
};
</script>
<style scoped>
.video-wrapper {
position: relative;
width: 100%;
height: 100%;
background: #111;
overflow: hidden;
}
.stream-name {
position: absolute;
top: 4px;
left: 6px;
font-size: 11px;
color: white;
background: rgba(0, 0, 0, 0.6);
padding: 2px 6px;
border-radius: 3px;
z-index: 10;
}
.latency-display {
position: absolute;
bottom: 4px;
right: 6px;
font-size: 10px;
color: #fff;
background: rgba(0, 0, 0, 0.6);
padding: 2px 6px;
border-radius: 3px;
z-index: 10;
}
.status-dot {
position: absolute;
top: 4px;
right: 6px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
}
.status-dot.connecting {
background: #ffaa00;
animation: pulse 1.5s infinite;
}
.status-dot.connected {
background: #00ff88;
box-shadow: 0 0 6px #00ff88;
}
.status-dot.error {
background: #ff4444;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
</style>
-26
View File
@@ -1,26 +0,0 @@
export default {
accessToken: '',
cameras: [
{
name: 'BK2163416',
deviceSerial: 'BK2163416',
channelNo: 1,
stream: 'hd',
enabled: true
},
{
name: 'BK2163935',
deviceSerial: 'BK2163935',
channelNo: 1,
stream: 'sd',
enabled: true
},
{
name: 'A3地块',
deviceSerial: 'BK2162699',
channelNo: 1,
stream: 'hd',
enabled: false
}
]
}
-27
View File
@@ -1,27 +0,0 @@
// 部署环境可填写 accessToken;本地密钥请用 ezviz.local.js(已 gitignore
export default {
accessToken: '',
cameras: [
{
name: 'BK2163416',
deviceSerial: 'BK2163416',
channelNo: 1,
stream: 'hd',
enabled: true
},
{
name: 'BK2163935',
deviceSerial: 'BK2163935',
channelNo: 1,
stream: 'sd',
enabled: true
},
{
name: 'A3地块',
deviceSerial: 'BK2162699',
channelNo: 1,
stream: 'hd',
enabled: false
}
]
}
-5
View File
@@ -1,5 +0,0 @@
/** 账权开放平台 open-key,用于溯源追踪配置查询 */
export const TRACING_OPEN_KEY = process.env.VUE_APP_TRACING_OPEN_KEY
|| '221533b6c4951ec7cd2e64241de6de08'
export default TRACING_OPEN_KEY
-11
View File
@@ -76,9 +76,6 @@
<script>
import variables from '@/styles/variables.scss'
import userApi from '@/api/user'
import weightApi from '@/api/weightApi'
import { clearTracingPermissionCache } from '@/utils/tracingPermission'
import { clearHuobanToken } from '@/utils/huobanAuth'
import 'animate.css'
// 默认配置
@@ -238,10 +235,6 @@ export default {
},
async loadUiConfig(customerId) {
if (!customerId) return
let res = await weightApi.getUiConfigByCustomer(customerId)
if (res.code === 1 && res.data) {
this.uiConfig = { ...this.uiConfig, ...res.data }
}
},
appClick(app) {
if (this.currentAppKey === app.appKey) return
@@ -295,14 +288,10 @@ export default {
}
},
logout() {
clearTracingPermissionCache()
clearHuobanToken()
localStorage.removeItem('_token')
this.$router.push('/login')
},
switchCustomer() {
clearHuobanToken()
clearTracingPermissionCache()
this.$store.dispatch('tagsView/delAllViews')
this.$store.dispatch('tagsView/clearTabsStorage')
this.$store.dispatch('user/setMenuList', [])
+5 -37
View File
@@ -1,29 +1,24 @@
import Vue from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import 'normalize.css/normalize.css'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
import * as echarts from 'echarts'
import locale from 'element-ui/lib/locale/lang/zh-CN'
import '@/styles/index.scss' // global css
import '@/styles/index.scss'
import App from './App'
import store from './store'
import router from './router'
Vue.use(ElementUI, { locale })
Vue.prototype.$echarts = echarts
Vue.config.productionTip = false
import menuApi from '@/api/menuApi'
import userApi from '@/api/user'
import customerApi from '@/api/customerApi'
import userPreferenceApi from '@/api/userPreference'
import { applyUiSize } from '@/utils/uiSizeUtil'
// 外部项目跳转大屏时可带 ?token=xxx,写入本地后供接口鉴权
;(function applyTokenFromQuery() {
try {
const params = new URLSearchParams(window.location.search)
@@ -51,17 +46,6 @@ async function loadAppData() {
store.dispatch('user/setLoginUser', user)
store.dispatch('user/setCurrentCustomer', customer)
// 加载当前用户的个性化配置(UI尺寸),在应用挂载前应用到全局 Element UI,
// 使所有页面的表单、按钮、表格等组件按用户配置的尺寸渲染
try {
const prefRes = await userPreferenceApi.mine()
if (prefRes && prefRes.code === 1 && prefRes.data) {
applyUiSize(prefRes.data.uiSize)
}
} catch (e) {
console.warn('loadAppData: load user preference failed, use default size', e)
}
if (!customer || !customer.id) {
console.warn('loadAppData: no customer id, skipping menu preload')
return
@@ -75,9 +59,8 @@ async function loadAppData() {
const appList = appsRes.data || []
store.dispatch('user/setAppList', appList)
// Build a flat map: menuPath → { appKey, menuTree }
const pathMap = {}
const allAppMenus = {} // appKey -> menuTree
const allAppMenus = {}
function buildPathMap(menus, appKey, rootMenus) {
if (!rootMenus) rootMenus = menus
for (const m of menus) {
@@ -88,7 +71,6 @@ async function loadAppData() {
for (const app of appList) {
try {
// 根据 source 选择菜单接口:package → getPackageAppMenus,其他 → getAppMenus
const menusRes = app.source === 'package'
? await userApi.getPackageAppMenus(app.appKey)
: await userApi.getAppMenus(app.appKey)
@@ -102,14 +84,11 @@ async function loadAppData() {
console.warn('loadAppData: failed to load menus for app', app.appKey, e)
}
}
// Store all preloaded app menus in Vuex for instant app switching
store.dispatch('user/setAllAppMenus', allAppMenus)
// Restore previously open tabs from localStorage (survives page refresh)
store.dispatch('tagsView/delAllViews')
store.dispatch('tagsView/restoreTabsFromStorage')
// Match current URL path to find the owning app and set sidebar menus
const currentPath = window.location.pathname
let found = pathMap[currentPath]
if (!found) {
@@ -131,18 +110,8 @@ async function loadAppData() {
}
}
// After login (path is / or /login), go to home dashboard, not a menu page
// Static pages that should not trigger redirect
const staticPages = [
'/certifyAgreement',
'/agreement',
'/register',
'/fei_screen',
'/screen/fei',
'/brain',
'/tracing_screen',
'/tracing/trace-detail-h5',
'/tracing/route'
'/register'
]
const isStaticPage = staticPages.some(p => currentPath === p || currentPath.startsWith(p + '/'))
const isFirstLoad = currentPath === '/' || currentPath === '/login'
@@ -155,7 +124,6 @@ async function loadAppData() {
}
loadAppData().then(() => {
// Expose for re-use after enterprise switch (see login page handleSelectCustomer)
window.__reloadAppData = loadAppData
new Vue({
el: '#app',
+3 -194
View File
@@ -3,7 +3,6 @@ import Router from 'vue-router'
Vue.use(Router)
import LayoutBox from '@/layout/index.vue'
import LayoutIndex from '@/layout/index.vue'
export const constantRoutes = [
{
@@ -18,24 +17,6 @@ export const constantRoutes = [
component: () => import('@/views/register/index'),
hidden: true
},
{
path: '/certify',
name: 'certify',
component: () => import('@/views/certify/index'),
hidden: true
},
{
path: '/certifyAgreement',
name: 'certifyAgreement',
component: () => import('@/views/certifyAgreement/index'),
hidden: true
},
{
path: '/agreement',
name: 'agreement',
component: () => import('@/views/agreement/index'),
hidden: true
},
{
path: '/error',
component: LayoutBox,
@@ -50,10 +31,9 @@ export const constantRoutes = [
},
{
path: '/common',
component: LayoutIndex,
component: LayoutBox,
name: 'common',
children: [],
meta: { title: 'common', icon: '' }
children: []
},
{
path: '/user',
@@ -167,182 +147,12 @@ export const constantRoutes = [
meta: { title: '编辑企业信息', icon: '' }
}]
},
{
path: '/huoban',
component: LayoutBox,
name: 'huoban',
meta: { title: '伙伴云配置', icon: '' },
children: [{
path: 'spaceList',
name: '工作空间列表',
component: () => import('@/views/huoban/spaceList'),
meta: { title: '工作空间列表', icon: '' }
}, {
path: 'tableInfo',
name: '表信息配置',
component: () => import('@/views/huoban/tableInfo'),
meta: { title: '表信息配置', icon: '' }
}, {
path: 'tableColumn',
name: '表字段配置',
component: () => import('@/views/huoban/tableColumn'),
meta: { title: '表字段配置', icon: '' }
}, {
path: 'dynamicForm',
name: '动态表单',
component: () => import('@/views/huoban/dynamicForm'),
meta: { title: '动态表单', icon: '' }
}, {
path: 'huobanFormList',
name: '伙伴Form管理',
component: () => import('@/views/huoban/huobanFormList'),
meta: { title: '伙伴Form管理', icon: '' }
}, {
path: 'huobanFormRender',
name: '表单渲染',
component: () => import('@/views/huoban/huobanFormRender'),
meta: { title: '表单渲染', icon: '', hidden: true }
}]
},
{
path: '/weight',
component: LayoutBox,
name: 'weight',
meta: { title: '自助过磅', icon: '' },
children: [{
path: 'vehicleRecord',
name: '车辆管理',
component: () => import('@/views/weight/vehicleRecord'),
meta: { title: '车辆管理', icon: '' }
}, {
path: 'ticket',
name: '过磅磅单',
component: () => import('@/views/weight/ticket'),
meta: { title: '过磅磅单', icon: '' }
}, {
path: 'deviceManage',
name: '智能称重设备',
component: () => import('@/views/weight/deviceManage'),
meta: { title: '智能称重设备', icon: '' }
}, {
path: 'driverManage',
name: '司机管理',
component: () => import('@/views/weight/driverManage'),
meta: { title: '司机管理', icon: '' }
}, {
path: 'cardManage',
name: '过磅卡管理',
component: () => import('@/views/weight/cardManage'),
meta: { title: '过磅卡管理', icon: '' }
}, {
path: 'beidouCardManage',
name: '北斗卡管理',
component: () => import('@/views/weight/beidouCardManage'),
meta: { title: '北斗卡管理', icon: '' }
}, {
path: 'alarmRule',
name: 'weight.alarmRule',
component: () => import('@/views/weight/alarmRule'),
meta: { title: '报警规则管理', icon: '' }
}, {
path: 'beidouAlarmRecord',
name: '北斗卡报警记录',
component: () => import('@/views/weight/beidouAlarmRecord'),
meta: { title: '报警记录', icon: '' }
}, {
path: 'beidouBorrowRecord',
name: '北斗卡借还记录',
component: () => import('@/views/weight/beidouBorrowRecord'),
meta: { title: '北斗卡借还记录', icon: '' }
}, {
path: 'beidouBorrowRecordDetail',
name: '借还记录详情',
component: () => import('@/views/weight/beidouBorrowRecordDetail'),
meta: { title: '借还记录详情', icon: '' }
}, {
path: 'beidouTrackingDetail',
name: '北斗卡跟踪记录',
component: () => import('@/views/weight/beidouTrackingDetail'),
meta: { title: '北斗卡跟踪记录', icon: '' }
}, {
path: 'workstation',
name: '过磅工作台',
component: () => import('@/views/weight/workstation'),
meta: { title: '过磅工作台', icon: '' }
}]
},
{
path: '/weight/receiptPrint',
name: 'receiptPrint',
component: () => import('@/views/weight/receiptPrint'),
meta: { title: '小票打印', icon: '' },
hidden: true
},
{
path: '/tracing/production-batch',
component: LayoutBox,
name: 'tracing',
meta: { title: '溯源管理', icon: '' },
children: [{
path: '',
name: '生产批次',
component: () => import('@/views/tracing/production-batch'),
meta: { title: '生产批次', icon: '', keepAlive: true }
}]
},
{
path: '/tracing/trace-detail-h5',
name: 'TraceDetailH5',
component: () => import('@/views/tracing/trace-detail-h5'),
meta: { title: '溯源详情', icon: '' },
hidden: true
},
{
path: '/tracing/route',
name: 'TracingRoute',
component: () => import('@/views/tracing/route'),
meta: { title: '设备路线追踪', icon: '' },
hidden: true
},
// 独立大屏入口(无侧栏/顶栏),供外部项目直接跳转,例:/fei_screen 或 /fei_screen?token=xxx
{
path: '/fei_screen',
name: 'FeiScreen',
component: () => import('@/views/fei/screen/index'),
meta: { title: '智慧农业可视化大屏', icon: '', noLayout: true },
hidden: true
},
{
path: '/brain',
name: 'Brain',
component: () => import('@/views/brain/index'),
meta: { title: '武鸣沃柑大脑', icon: '', noLayout: true },
hidden: true
},
{
path: '/screen/fei',
name: 'FeiScreenAlias',
component: () => import('@/views/fei/screen/index'),
meta: { title: '智慧农业可视化大屏', icon: '', noLayout: true },
hidden: true
},
{
path: '/tracing_screen',
name: 'TracingScreen',
component: () => import('@/views/tracing/screen/index'),
meta: { title: '溯源可视化大屏', icon: '' },
hidden: true
},
{
path: '/',
redirect: '/login',
component: LayoutBox,
// component: () => import('@/views/login/index'),
name: '基础平台',
meta: { title: '基础平台1', icon: '' },
meta: { title: '基础平台', icon: '' },
children: [{
path: '/',
name: '看板',
@@ -356,7 +166,6 @@ let router = new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
router.beforeEach(async (to, from, next) => {
+6 -11
View File
@@ -1,16 +1,11 @@
const getters = {
sidebar: state => state.app.sidebar,
size: state => state.app.size,
device: state => state.app.device,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
introduction: state => state.user.introduction,
roles: state => state.user.roles,
permission_routes: state => state.permission.routes,
errorLogs: state => state.errorLog.logs,
common_baseURL: state => state.common.baseURL
loginUser: state => state.user.loginUser,
currentCustomer: state => state.user.currentCustomer,
menuList: state => state.user.menuList,
appList: state => state.user.appList,
currentApp: state => state.user.currentApp,
currentAppMenus: state => state.user.currentAppMenus
}
export default getters
+1 -9
View File
@@ -3,11 +3,10 @@ const state = {
menuList: [],
currentMenuId: null,
currentCustomer: {},
huobanToken: {},
appList: [],
currentApp: null,
currentAppMenus: [],
allAppMenus: {} // map: appKey -> menuTree (preloaded)
allAppMenus: {}
}
const mutations = {
@@ -23,9 +22,6 @@ const mutations = {
SET_CURRENT_CUSTOMER(state, currentCustomer) {
state.currentCustomer = currentCustomer
},
SET_HUOBAN_TOKEN(state, token) {
state.huobanToken = token
},
SET_APP_LIST(state, list) {
state.appList = list
},
@@ -33,7 +29,6 @@ const mutations = {
state.currentApp = app
},
SET_CURRENT_APP_MENUS(state, menus) {
console.log('SET_CURRENT_APP_MENUS mutation:', menus)
state.currentAppMenus = menus
},
SET_ALL_APP_MENUS(state, map) {
@@ -54,9 +49,6 @@ const actions = {
setCurrentCustomer({ commit }, currentCustomer) {
commit('SET_CURRENT_CUSTOMER', currentCustomer)
},
setHuobanToken({ commit }, token) {
commit('SET_HUOBAN_TOKEN', token)
},
setAppList({ commit }, list) {
commit('SET_APP_LIST', list)
},
-76
View File
@@ -1,76 +0,0 @@
// 溯源大屏主题变量(独立于全局 styles/variables.scss
$bg-color: #030d21;
$panel-bg: rgba(6, 30, 93, 0.5);
$border-color: #1a4a8d;
$text-color: #ffffff;
$highlight: #00f2ff;
@mixin panel-base {
background: $panel-bg;
border: 1px solid $border-color;
padding: 10px;
position: relative;
display: flex;
flex-direction: column;
}
@mixin panel-title {
font-size: 14px;
border-left: 4px solid $highlight;
padding-left: 8px;
margin-bottom: 10px;
color: $highlight;
}
@mixin stat-item {
background: rgba(6, 30, 93, 0.7);
border: 1px solid $border-color;
padding: 10px 5px;
border-radius: 6px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
flex: 1;
height: 100%;
min-width: auto;
max-width: none;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, $highlight, transparent);
animation: statusPulse 2s ease-in-out infinite;
opacity: 0.7;
}
&:hover {
background: rgba(26, 74, 141, 0.85);
box-shadow: 0 0 15px rgba($highlight, 0.4), 0 4px 20px rgba(0, 0, 0, 0.3);
transform: translateY(-3px) scale(1.02);
border-color: $highlight;
}
&:active {
transform: translateY(-1px) scale(0.99);
}
}
@keyframes statusPulse {
0%, 100% {
transform: translateX(-100%);
opacity: 0.3;
}
50% {
transform: translateX(100%);
opacity: 0.8;
}
}
-34
View File
@@ -1,34 +0,0 @@
import exampleConfig from '@/config/ezviz.example'
let deployConfig = null
let localConfig = null
try {
// eslint-disable-next-line global-require, import/no-unresolved
deployConfig = require('@/config/ezviz').default
} catch (error) {
deployConfig = null
}
try {
// eslint-disable-next-line global-require, import/no-unresolved
localConfig = require('@/config/ezviz.local').default
} catch (error) {
localConfig = null
}
function pickConfig() {
if (localConfig && localConfig.accessToken) return localConfig
if (deployConfig && deployConfig.accessToken) return deployConfig
return exampleConfig
}
const config = pickConfig()
export function buildEzopenUrl(camera) {
const channelNo = camera.channelNo || 1
const quality = camera.stream === 'hd' ? 'hd' : 'sd'
return `ezopen://open.ys7.com/${camera.deviceSerial}/${channelNo}.${quality}.live`
}
export default config
-25
View File
@@ -1,25 +0,0 @@
/**
* 萤石标准流 URL 工具
* 路径格式: {deviceSerial}_{channelNo}_{quality}.flv
* quality: 1=主码流(高清), 2=子码流(流畅)
*
* 注意: URL 中的 id/t 签名与路径绑定,不能手动改 _1_1/_1_2,必须从控制台/API 原样获取
*/
export const EZVIZ_QUALITY = {
HD: 1,
SD: 2
}
export function getQualityLabel(quality) {
return quality === EZVIZ_QUALITY.SD ? '流畅' : '高清'
}
export function resolveLivePlayUrl(item) {
if (!item) return ''
// 优先使用完整播放地址(控制台原样复制)
if (item.url) return item.url
if (item.subStreamUrl) return item.subStreamUrl
if (item.hdUrl) return item.hdUrl
return ''
}
-105
View File
@@ -1,105 +0,0 @@
import huobanTableApi from '@/api/huobanTableApi'
import userApi from '@/api/user'
const HB_TOKEN_KEY = '_hb_token'
const HB_PHONE_KEY = '_hb_phone'
export function getHuobanToken() {
return localStorage.getItem(HB_TOKEN_KEY) || ''
}
export function getHuobanPhone() {
return localStorage.getItem(HB_PHONE_KEY) || ''
}
export function clearHuobanToken() {
localStorage.removeItem(HB_TOKEN_KEY)
localStorage.removeItem(HB_PHONE_KEY)
}
function extractTokenFromResponse(data) {
if (!data) return ''
if (typeof data === 'string') return data
const tokenInfo = data.token_info || data.tokenInfo
if (tokenInfo && tokenInfo.access_token) return tokenInfo.access_token
return data.accessToken || data.access_token || data.token || ''
}
function extractPhoneFromResponse(data) {
if (!data) return ''
const sso = data.SSO || data.sso
if (sso && sso.huobanMobile) return sso.huobanMobile
return data.phone || data.huobanMobile || ''
}
function storeHuobanTokenFromResponse(data) {
const token = extractTokenFromResponse(data)
if (!token) return ''
localStorage.setItem(HB_TOKEN_KEY, token)
const phone = extractPhoneFromResponse(data)
if (phone) {
localStorage.setItem(HB_PHONE_KEY, phone)
}
return token
}
async function fetchHuobanTokenFromServer() {
const res = await userApi.huobanWebLogin()
const token = storeHuobanTokenFromResponse(res && res.data)
if (res && res.code === 1 && token) {
console.log('[huoban] huobanWebLogin 成功')
return token
}
throw new Error((res && res.msg) || '获取伙伴云 token 失败')
}
/**
* 获取伙伴云 tokenforce=true 时清缓存后重新拉取
*/
export async function ensureHuobanToken(force = false) {
if (!force) {
const existing = getHuobanToken()
if (existing) return existing
} else {
clearHuobanToken()
}
return fetchHuobanTokenFromServer()
}
/** 切换企业后强制刷新(会清掉旧 _hb_token) */
export async function refreshHuobanToken() {
return ensureHuobanToken(true)
}
export async function queryWebData(dataFilter) {
await ensureHuobanToken()
return await huobanTableApi.webData(dataFilter)
}
export async function queryWebDetail(itemId, tableId, tableName = '', relations = []) {
await ensureHuobanToken()
const params = { itemId, tableId, tableName, relations }
return await huobanTableApi.webDetail(params)
}
/** H5 等场景:旧接口 data(系统 _token */
export async function queryData(dataFilter) {
return await huobanTableApi.queryData(dataFilter)
}
/** H5 等场景:旧接口 detail(系统 _token */
export async function queryDetail(itemId, tableId, tableName = '', relations = []) {
return await huobanTableApi.detail(itemId, tableId, tableName, relations)
}
export default {
getHuobanToken,
getHuobanPhone,
clearHuobanToken,
ensureHuobanToken,
refreshHuobanToken,
queryWebData,
queryWebDetail,
queryData,
queryDetail,
}
-60
View File
@@ -1,60 +0,0 @@
import { marked } from 'marked'
import hljs from 'highlight.js/lib/core'
import json from 'highlight.js/lib/languages/json'
import java from 'highlight.js/lib/languages/java'
import groovy from 'highlight.js/lib/languages/groovy'
import javascript from 'highlight.js/lib/languages/javascript'
import python from 'highlight.js/lib/languages/python'
import sql from 'highlight.js/lib/languages/sql'
import bash from 'highlight.js/lib/languages/bash'
import xml from 'highlight.js/lib/languages/xml'
import css from 'highlight.js/lib/languages/css'
import typescript from 'highlight.js/lib/languages/typescript'
import 'highlight.js/styles/vs2015.css'
// 注册常用语言
hljs.registerLanguage('json', json)
hljs.registerLanguage('java', java)
hljs.registerLanguage('groovy', groovy)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('js', javascript)
hljs.registerLanguage('python', python)
hljs.registerLanguage('py', python)
hljs.registerLanguage('sql', sql)
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('sh', bash)
hljs.registerLanguage('shell', bash)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('html', xml)
hljs.registerLanguage('css', css)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('ts', typescript)
// 配置 marked 使用 highlight.js
marked.setOptions({
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
if (hljs.getLanguage(language)) {
try {
return hljs.highlight(code, { language }).value
} catch (e) {
return code
}
}
return code
}
})
/**
* 将 Markdown 文本解析为带代码高亮的 HTML
*/
export function parseMarkdown(content) {
if (!content) return ''
try {
return marked.parse(content)
} catch (e) {
return content
}
}
export { hljs }
+4 -35
View File
@@ -3,78 +3,49 @@ import request from 'axios'
import msgUtil from '@/utils/msgUtil'
import router from '@/router'
function isHuobanWebApi(url) {
return /\/huoban2\/web(Data|Detail)/.test(url || '')
}
function resolveRequestToken(url) {
if (isHuobanWebApi(url)) {
return localStorage.getItem('_hb_token') || ''
}
return localStorage.getItem('_token') || ''
}
class Axios {
constructor() {
// 公共参数
request.defaults.params = {}
// 设置请求最大时间
request.defaults.timeout = 30 * 1000
// 返回的状态值
request.defaults.validateStatus = status => {
return status >= 200 && status < 300
}
// 请求之前需要做的公共处理
request.interceptors.request.use((config) => {
config.headers = {
...config.headers,
time: new Date().getTime(),
version: '1.1',
token: resolveRequestToken(config.url)
token: localStorage.getItem('_token') || ''
}
return config
}, (error) => { // 请求前出错
}, (error) => {
return Promise.reject(error)
})
// 响应需要做的公共处理
request.interceptors.response.use((response) => {
const res = response.data
if (res.code === 99) {
const staticPages = [
'/certifyAgreement',
'/agreement',
'/register',
'/tracing/trace-detail-h5',
'/tracing/route',
'/fei_screen',
'/screen/fei',
'/brain',
'/tracing_screen'
'/register'
]
const currentPath = window.location.pathname
const isStaticPage = staticPages.some(p => currentPath === p || currentPath.startsWith(p + '/'))
if (!isStaticPage) {
// msgUtil.notifyError('登出提醒', res.msg)
router.push("/login")
}
}
return response
}, (error) => { // 响应失败
}, (error) => {
return Promise.reject(error)
})
// 对上传的原生进度处理
request.defaults.onUploadProgress = progressEvent => {
return progressEvent
}
// 对下载的原生进度处理
request.defaults.onDownloadProgress = progressEvent => {
return progressEvent
}
// 赋予实例
this.instance = request
}
// get
apiGet(url, params = {}, headers = {}) {
return new Promise((resolve, reject) => {
this.instance.get(url, { params: params, headers: headers }).then((res) => {
@@ -83,7 +54,6 @@ class Axios {
})
}
// post
apiPost(url, params = {}, headers = {}, timeout = null) {
return new Promise((resolve, reject) => {
function filterNull(obj) {
@@ -115,7 +85,6 @@ class Axios {
})
}
// post
apiPostJson(url, params = {}) {
return new Promise((resolve, reject) => {
function jsonChageUrlData(obj) {
-91
View File
@@ -1,91 +0,0 @@
const INVALID_DISPLAY_VALUES = new Set(['-', '—', 'XXXXXX', 'xxxxxx']);
/**
* 是否有可展示的有效值(过滤占位符)
*/
export function hasDisplayValue(val) {
if (val === null || val === undefined) return false;
if (typeof val === 'number') {
return !Number.isNaN(val);
}
const text = String(val).trim();
if (!text) return false;
return !INVALID_DISPLAY_VALUES.has(text);
}
/**
* 图表 series 数值求和
*/
export function sumChartValues(list) {
if (!Array.isArray(list) || !list.length) return 0;
return list.reduce((sum, item) => {
const value = item && item.value !== undefined ? Number(item.value) : 0;
return sum + (Number.isFinite(value) ? value : 0);
}, 0);
}
/**
* 是否存在有效轨迹坐标
*/
export function hasTrackMapPoints(list) {
if (!Array.isArray(list) || !list.length) return false;
return list.some((item) => {
const lng = parseFloat(item.longitude);
const lat = parseFloat(item.latitude);
return Number.isFinite(lng) && Number.isFinite(lat);
});
}
function hasSeriesValue(list, field) {
if (!Array.isArray(list) || !list.length) return false;
return list.some((item) => {
const val = item[field];
if (val === null || val === undefined || val === '') return false;
const num = Number(val);
return Number.isFinite(num);
});
}
export function hasTemperatureSeries(list) {
return hasSeriesValue(list, 'temperature');
}
export function hasHumiditySeries(list) {
return hasSeriesValue(list, 'humidity');
}
/**
* 称重记录是否有有效数据
*/
export function hasWeighingRecord(data, imageUrls = []) {
if (Array.isArray(imageUrls) && imageUrls.length > 0) return true;
if (!data || typeof data !== 'object') return false;
if (hasDisplayValue(data.cardNo)) return true;
if (hasDisplayValue(data.weighingTime)) return true;
if (data.netWeight !== null && data.netWeight !== undefined && data.netWeight !== '') {
const weight = Number(data.netWeight);
if (Number.isFinite(weight) && weight > 0) return true;
}
return false;
}
/**
* 拼接溯源描述句,跳过空子句
* @param {string[]} clauses
* @returns {string}
*/
export function buildTracingSentence(clauses) {
const parts = (clauses || []).filter((clause) => hasDisplayValue(clause));
if (!parts.length) return '';
return `${parts.join('')}`;
}
export default {
hasDisplayValue,
sumChartValues,
hasTrackMapPoints,
hasTemperatureSeries,
hasHumiditySeries,
hasWeighingRecord,
buildTracingSentence,
};
-206
View File
@@ -1,206 +0,0 @@
import permissionApi from '@/api/permissionApi'
/**
* 溯源追踪权限码(与后台 permissionCode 对齐)
* @see 权限管理 - 溯源追踪模块
*/
export const TRACING_PERM = {
/** 溯源追踪模块 */
MODULE: 'zx.tracking',
/** 是否展示地址 */
IS_ADDRESS: 'zx.tracking.isAddress',
/** 是否显示出口资质 */
IS_CERTIFICATION: 'zx.tracking.isCertification',
/** 是否显示其他资质信息 */
IS_ELSE_CERTIFICATION: 'zx.tracking.iselsecertification',
/** 是否显示采果记录和采摘路线(不含检测信息) */
IS_PICK: 'zx.tracking.ispick',
/** 是否显示原料称重记录 */
IS_RAW_MATERIAL: 'zx.tracking.isRawmaterial',
/** 是否显示成品称重记录(与权限菜单 / getMyPermissionCodes 对齐) */
IS_FINISHED_PRODUCT: 'zx.tracking.isFinishedproduct',
/** 是否显示物流轨迹地图及温湿度图表 */
IS_LOGISTICS: 'zx.tracking.islogistics',
}
const TRACING_CODE_SET = new Set(Object.values(TRACING_PERM))
let cachedCodes = null
let permissionsLoaded = false
let loadPromise = null
function getLoginToken() {
try {
return localStorage.getItem('_token') || ''
} catch (e) {
return ''
}
}
/** 从 getMyPermissionCodes 响应中取出码列表 */
export function extractPermissionCodes(res) {
if (!res || res.code !== 1) return []
const data = res.data
// #region agent log
fetch('http://127.0.0.1:7718/ingest/48ca8203-b675-41cd-9304-a7ecac67f5c5',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'b90a44'},body:JSON.stringify({sessionId:'b90a44',runId:'post-fix',hypothesisId:'A',location:'tracingPermission.js:extractPermissionCodes',message:'extract input shape',data:{resCode:res&&res.code,dataType:data===null?'null':Array.isArray(data)?'array':typeof data,dataKeys:data&&typeof data==='object'&&!Array.isArray(data)?Object.keys(data):[],hasZxArr:!!(data&&Array.isArray(data.zx)),zxLen:data&&Array.isArray(data.zx)?data.zx.length:0,zxTrackingSample:data&&Array.isArray(data.zx)?data.zx.filter(c=>String(c).indexOf('zx.tracking')===0):[]},timestamp:Date.now()})}).catch(()=>{});
// #endregion
if (Array.isArray(data)) return data.map(String)
if (data && Array.isArray(data.codes)) return data.codes.map(String)
if (data && Array.isArray(data.permissionCodes)) return data.permissionCodes.map(String)
// 按应用分组:{ zx: [...], water_fei: [...], ... }
if (data && typeof data === 'object') {
const codes = []
Object.keys(data).forEach((key) => {
const list = data[key]
if (Array.isArray(list)) {
list.forEach((code) => codes.push(String(code)))
}
})
return codes
}
return []
}
/** 只保留溯源相关权限码 */
export function filterTracingPermissionCodes(codes) {
if (!Array.isArray(codes)) return []
return codes.filter((code) => TRACING_CODE_SET.has(String(code)))
}
/** 游客 / 未授权:清空缓存为无权限 */
export function setGuestTracingPermissions() {
cachedCodes = []
permissionsLoaded = true
loadPromise = null
return cachedCodes
}
export function getCachedTracingPermissionCodes() {
return cachedCodes ? [...cachedCodes] : []
}
export function isTracingPermissionsLoaded() {
return permissionsLoaded
}
export function clearTracingPermissionCache() {
cachedCodes = null
permissionsLoaded = false
loadPromise = null
}
export function grantAllTracingPermissionsForDev() {
cachedCodes = Object.values(TRACING_PERM)
permissionsLoaded = true
console.log('[tracing] 调试:已授予全部溯源权限')
}
/**
* 从当前登录用户套餐权限码加载溯源展示权限
* @param {boolean} force 是否强制刷新
*/
export async function ensureTracingPermissions(force = false) {
// 兼容旧调用 ensureTracingPermissions(saasId, force)
if (arguments.length >= 2) {
force = !!arguments[1]
} else if (typeof force !== 'boolean') {
force = false
}
if (!force && permissionsLoaded) {
return cachedCodes || []
}
if (!force && loadPromise) {
return loadPromise
}
if (!getLoginToken()) {
console.warn('[tracing] 未登录,溯源权限块不展示')
return setGuestTracingPermissions()
}
loadPromise = (async () => {
try {
const res = await permissionApi.getMyPermissionCodes()
const allCodes = extractPermissionCodes(res)
cachedCodes = filterTracingPermissionCodes(allCodes)
permissionsLoaded = true
// #region agent log
fetch('http://127.0.0.1:7718/ingest/48ca8203-b675-41cd-9304-a7ecac67f5c5',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'b90a44'},body:JSON.stringify({sessionId:'b90a44',runId:'post-fix',hypothesisId:'A,B,C',location:'tracingPermission.js:ensureTracingPermissions',message:'after extract+filter',data:{allCodesLen:allCodes.length,cachedCodes,expectedFinished:TRACING_PERM.IS_FINISHED_PRODUCT,hasFinishedInAll:allCodes.includes('zx.tracking.isFinishedproduct'),hasFinishedLower:allCodes.includes('zx.tracking.isfinishedproduct'),hasPick:cachedCodes.includes(TRACING_PERM.IS_PICK),hasLogistics:cachedCodes.includes(TRACING_PERM.IS_LOGISTICS)},timestamp:Date.now()})}).catch(()=>{});
// #endregion
console.log('[tracing] getMyPermissionCodes:', cachedCodes, res)
return cachedCodes
} catch (error) {
// #region agent log
fetch('http://127.0.0.1:7718/ingest/48ca8203-b675-41cd-9304-a7ecac67f5c5',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'b90a44'},body:JSON.stringify({sessionId:'b90a44',runId:'post-fix',hypothesisId:'C',location:'tracingPermission.js:ensureTracingPermissions:error',message:'load failed',data:{error:String(error&&error.message||error)},timestamp:Date.now()})}).catch(()=>{});
// #endregion
console.error('[tracing] 加载套餐权限码失败:', error)
cachedCodes = []
permissionsLoaded = true
return cachedCodes
} finally {
loadPromise = null
}
})()
return loadPromise
}
/**
* 严格白名单:套餐含该码才展示;未加载完成前视为无权限
*/
export function hasTracingPermission(code) {
if (!code) return true
if (!permissionsLoaded) return false
return (cachedCodes || []).includes(code)
}
export function getQualDisplayPermissions() {
return {
showAddress: hasTracingPermission(TRACING_PERM.IS_ADDRESS),
showExportCert: hasTracingPermission(TRACING_PERM.IS_CERTIFICATION),
showOtherCert: hasTracingPermission(TRACING_PERM.IS_ELSE_CERTIFICATION),
}
}
export function hasAnyQualDetailPermission(perm) {
if (!perm) return false
return !!(perm.showExportCert || perm.showOtherCert)
}
export function filterQualCertsByPermission(certs, perm) {
if (!Array.isArray(certs)) return []
const flags = perm || getQualDisplayPermissions()
return certs.filter((cert) => {
if (cert.certType === 'export') return flags.showExportCert
if (cert.certType === 'other') return flags.showOtherCert
return flags.showExportCert || flags.showOtherCert
})
}
/** Tab 索引:0 基本 1 资质 2 采摘 3 分选 4 物流 — Tab 始终展示,权限只控制 Tab 内各块 */
export function isTracingTabVisibleByIndex(tabIndex) {
return tabIndex >= 0 && tabIndex < 5
}
export function getFirstVisibleTracingTabIndex() {
return 0
}
export default {
TRACING_PERM,
extractPermissionCodes,
filterTracingPermissionCodes,
setGuestTracingPermissions,
grantAllTracingPermissionsForDev,
ensureTracingPermissions,
clearTracingPermissionCache,
getCachedTracingPermissionCodes,
isTracingPermissionsLoaded,
hasTracingPermission,
getQualDisplayPermissions,
filterQualCertsByPermission,
hasAnyQualDetailPermission,
isTracingTabVisibleByIndex,
getFirstVisibleTracingTabIndex,
}
-26
View File
@@ -1,26 +0,0 @@
import {
TRACING_PERM,
getQualDisplayPermissions,
hasAnyQualDetailPermission,
filterQualCertsByPermission,
} from '@/utils/tracingPermission'
export const TRACING_QUAL_PERM = {
ADDRESS: TRACING_PERM.IS_ADDRESS,
EXPORT_CERT: TRACING_PERM.IS_CERTIFICATION,
OTHER_CERT: TRACING_PERM.IS_ELSE_CERTIFICATION,
}
/** 读取当前已加载的资质展示开关(须先 ensureTracingPermissions */
export function loadTracingQualPermissions() {
return getQualDisplayPermissions()
}
export { hasAnyQualDetailPermission, filterQualCertsByPermission }
export default {
TRACING_QUAL_PERM,
loadTracingQualPermissions,
hasAnyQualDetailPermission,
filterQualCertsByPermission,
}
-79
View File
@@ -1,79 +0,0 @@
/** 批次详情或检测子表是否含检测报告相关字段 */
export function hasTestReportData(data) {
if (!data) return false
return !!(
data.testCertificate
|| data.tester
|| data.testDate
|| (data.testItem && data.testItem.length)
|| (data.testResult && data.testResult.length)
|| data.testUnit
|| data.testingUnit
)
}
/** 统一检测单位字段(详情为 testingUnit 字符串,子表为 testUnit */
export function normalizeTestUnit(value) {
if (Array.isArray(value)) return value
if (value == null || value === '') return []
return value
}
/** 从数据源提取检测报告展示字段 */
export function extractTestCertFields(source) {
if (!source || !hasTestReportData(source)) return null
return {
testCertificate: source.testCertificate,
tester: source.tester || '',
testDate: source.testDate || '',
testItem: source.testItem || [],
testResult: source.testResult || [],
testUnit: normalizeTestUnit(source.testUnit || source.testingUnit)
}
}
/**
* 多条检测记录时优先取批次关联的 testReport,否则取最后一条(通常最新)
*/
export function pickTestCertificateRecord(list, batchData) {
if (!list || !list.length) return null
const linkedId = batchData && batchData.testReport && batchData.testReport[0]
? batchData.testReport[0].item_id
: ''
if (linkedId) {
const matched = list.find(
(row) => String(row._itemId || row.item_id) === String(linkedId)
)
if (matched) return matched
}
return list[list.length - 1]
}
/** 将检测报告字段写入组件 state(详情 attach 字段,无需再查子表) */
export function applyTestCertificateToState(vm, batchData) {
const fields = extractTestCertFields(batchData)
if (!fields) return false
vm.testCertificateData = { ...fields }
vm.testDate = fields.testDate
vm.testItem = fields.testItem
vm.testUnit = fields.testUnit
vm.testResult = fields.testResult
return true
}
export function resetTestCertificateState(vm) {
vm.testCertificateData = null
vm.testDate = ''
vm.testItem = []
vm.testUnit = []
vm.testResult = []
}
export default {
hasTestReportData,
normalizeTestUnit,
extractTestCertFields,
pickTestCertificateRecord,
applyTestCertificateToState,
resetTestCertificateState
}
-51
View File
@@ -1,51 +0,0 @@
import {Boot} from "@wangeditor/editor";
class wangConfig {
registerMenuButton(menuKey, title, iconSvg, calFun) {
let that = this;
class SaveButtonMenu {
constructor() {
this.title = title
this.tag = 'button'
this.iconSvg = iconSvg;
}
getValue(editor) {
return 'false'
}
isActive(editor) {
return false
}
isDisabled(editor) {
return false
}
async exec(editor, value) {
calFun && calFun(editor, value)
}
}
const myMenuConf = {
key: menuKey,
factory() {
return new SaveButtonMenu()
}
}
Boot.registerMenu(myMenuConf)
}
registerMenu() {
this.registerMenuButton("save", "保存", "", (editor, value) => {
//
editor.emit("save_word");
});
}
}
const target = new wangConfig()
export default target
-72
View File
@@ -1,72 +0,0 @@
# PageCode 页面唯一标识映射表
# 格式: 文件路径 => pageCode
# 用于在相似页面修改时精准定位,避免改错
404.vue => xcg3
agreement/index.vue => adrz
app/appDetail.vue => xf8f
app/appList.vue => d2xv
base/imageUpload.vue => yuvs
customer/detail.vue => jkqc
customer/editCustomer.vue => b0fa
customer/employee.vue => fy3o
customer/index.vue => l3cj
customer/myCustomer.vue => e1yw
customer/uiConfig.vue => 8u3y
customer/user.vue => drm1
home/index.vue => z48e
huoban/dynamicForm.vue => m5ja
huoban/huobanFormList.vue => k14h
huoban/huobanFormRender.vue => iqef
huoban/index.vue => n9cr
huoban/spaceList.vue => reki
huoban/tableColumn.vue => qkek
huoban/tableInfo.vue => 4fx0
login/index.vue => dalb
payment/index.vue => bgto
permission/menuList.vue => i9ku
permission/permissionList.vue => fb39
permission/roleList.vue => 3w2e
register/index.vue => 3ccg
system/organization/index.vue => 0y3i
system/packagePlan/index.vue => 6bns
system/quota/index.vue => b2j3
trace/category/index.vue => 68ye
trace/certificate/index.vue => rav0
trace/code/audit.vue => kcyh
trace/code/bind.vue => 3yzz
trace/code/exception.vue => f7cr
trace/code/list.vue => cwho
trace/code/relation.vue => xduo
trace/code/task.vue => q0ks
trace/feedback/index.vue => bau1
trace/harvest/index.vue => 7x6k
trace/land/index.vue => ibp4
trace/process/index.vue => 4rap
trace/product/index.vue => r868
trace/qualification/index.vue => js4z
trace/quality/index.vue => kwey
trace/recall/index.vue => 67jl
trace/warehouse/in.vue => jx82
trace/warehouse/inventory.vue => ehid
trace/warehouse/logistics.vue => s9gr
trace/warehouse/out.vue => p1q6
user/editProfile.vue => y360
user/info.vue => 4ve1
user/list.vue => bxub
user/pwd.vue => rcgs
user/sso.vue => qyfa
web/index.vue => uir1
weight/alarmRule.vue => 0yrk
weight/beidouAlarmRecord.vue => 005n
weight/beidouBorrowRecord.vue => 0i4t
weight/beidouBorrowRecordDetail.vue => xxuu
weight/beidouCardManage.vue => wg7d
weight/beidouTrackingDetail.vue => uozs
weight/cardManage.vue => b0tr
weight/deviceManage.vue => qndy
weight/driverManage.vue => 2ghc
weight/receiptPrint.vue => h5ue
weight/ticket.vue => 3ihd
weight/vehicleRecord.vue => 98j6
weight/workstation.vue => xpxs
-138
View File
@@ -1,138 +0,0 @@
<template>
<div class="agreement-container">
<div class="agreement-content-wrapper">
<div class="agreement-content">
<h3>账权云用户服务协议</h3>
<p class="update-time">更新日期2026年5月14日 &nbsp;|&nbsp; 生效日期2026年5月14日</p>
<p>欢迎您使用账权云以下简称"本平台"本协议是您与本平台运营方之间关于使用本平台服务所订立的协议请您在注册前仔细阅读本协议特别是免除或者限制责任的条款法律适用和争议解决条款<strong>如您不同意本协议的任何内容请不要进行注册或使用本平台服务您完成注册即表示您同意接受本协议的全部条款</strong></p>
<h4>服务的接受</h4>
<p>1.1 本协议是您与北京账权科技有限公司以下简称"运营方""我们"之间关于您使用掌云服务所订立的协议具有合同效力</p>
<p>1.2 本平台账权云提供的服务包括但不限于农产品流通管理溯源管理自助过磅客户管理数据看板等企业级SaaS服务</p>
<p>1.3 运营方有权根据业务发展和运营需要随时变更升级修改本平台提供的服务相关变更将于公布后生效</p>
<h4>账号注册与使用</h4>
<p>2.1 您在注册时应当提供真实准确完整的个人信息包括但不限于昵称手机号码密码并在信息发生变更时及时更新</p>
<p>2.2 您注册成功后本平台将授予您一个唯一的账号您对该账号享有使用权但该账号的所有权归本平台所有</p>
<p>2.3 您应妥善保管账号及密码因您自身原因导致的账号泄露被盗用等所造成的损失由您自行承担</p>
<p>2.4 您不得将账号赠与借用租用转让或售卖给他人如您发现账号遭他人非法使用应立即通知本平台</p>
<p>2.5 运营方有权对用户账号的真实性进行核实如发现虚假信息或违规行为有权暂停或终止该账号的使用</p>
<h4>用户行为规范</h4>
<p>3.1 您在使用本平台服务时应遵守中华人民共和国相关法律法规不得利用本平台从事以下行为</p>
<p> 1发布违反国家法律法规的内容</p>
<p> 2侵犯他人知识产权商业秘密或其他合法权益</p>
<p> 3散布虚假信息欺诈信息或恶意干扰本平台正常运行</p>
<p> 4从事任何可能对账权云服务器网络系统造成损害或干扰的行为</p>
<p> 5利用本平台进行洗钱传销等违法犯罪活动</p>
<p> 6其他违反法律法规或社会公序良俗的行为</p>
<p>3.2 您在使用本平台服务过程中所产生的业务数据归您所有但运营方有权在不泄露您隐私的前提下对脱敏后的数据进行统计分析</p>
<h4>隐私保护</h4>
<p>4.1 运营方重视对用户隐私的保护我们将按照法律法规的要求采取合理的安全措施保护您的个人信息</p>
<p>4.2 运营方收集的个人信息仅限于提供服务所必需的信息包括但不限于手机号码昵称操作记录等</p>
<p>4.3 未经您的同意运营方不会将您的个人信息透露给任何第三方但以下情况除外</p>
<p> 1根据法律法规的要求或政府部门的要求</p>
<p> 2为维护运营方的合法权益</p>
<p> 3在紧急情况下为保护用户或公众的人身安全</p>
<h4>知识产权</h4>
<p>5.1 账权云平台提供的所有服务包括但不限于软件程序代码界面设计商标文字图片音频视频等其知识产权均归运营方或相关权利人所有</p>
<p>5.2 未经运营方书面许可您不得以任何形式复制传播展示修改本平台的内容和服务</p>
<h4>免责声明</h4>
<p>6.1 运营方不保证服务不会中断对服务的及时性安全性准确性不作担保</p>
<p>6.2 对于因不可抗力包括但不限于自然灾害战争政府行为网络故障等导致的服务中断或数据丢失运营方不承担责任</p>
<p>6.3 因您使用本平台服务而产生的任何直接或间接损失运营方在法律允许的范围内承担有限责任</p>
<p>6.4 您通过本平台与其他用户进行交易或合作应自行判断并承担相关风险</p>
<h4>服务变更与终止</h4>
<p>7.1 运营方有权在提前通知的情况下暂停终止或限制向您提供服务</p>
<p>7.2 如您违反本协议的任何条款运营方有权立即暂停或终止您的账号使用权且无需承担任何责任</p>
<p>7.3 服务终止后运营方有权保留您的数据但不再承担存储和备份义务</p>
<h4>协议的变更</h4>
<p>8.1 运营方有权根据需要不时修改本协议修改后的协议将在本平台上公布自公布之日起生效</p>
<p>8.2 如您不同意修改后的协议您有权停止使用本平台服务继续使用即视为接受修改后的协议</p>
<h4>法律适用与争议解决</h4>
<p>9.1 本协议的订立执行和解释及争议的解决均适用中华人民共和国法律</p>
<p>9.2 如您与运营方之间发生任何争议双方应首先友好协商解决协商不成的任何一方均可向北京账权科技有限公司所在地有管辖权的人民法院提起诉讼</p>
<h4>其他</h4>
<p>10.1 本协议构成双方对本协议之约定事项及其他有关事宜的完整协议除本协议规定的之外未赋予本协议各方其他权利</p>
<p>10.2 如本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力本协议的其余条款仍应有效并且有约束力</p>
<p>10.3 运营方未行使或未强制执行本协议中的任何权利不构成对该权利的放弃</p>
<p class="footer">如您对本协议有任何疑问请联系账权云客服</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Agreement'
}
</script>
<style lang="scss" scoped>
.agreement-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.agreement-content-wrapper {
padding: 20px;
}
.agreement-content {
background: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
max-width: 900px;
margin: 0 auto;
line-height: 1.8;
color: #606266;
h3 {
text-align: center;
font-size: 22px;
color: #303133;
margin-bottom: 8px;
}
.update-time {
text-align: center;
font-size: 13px;
color: #909399;
margin-bottom: 32px;
}
h4 {
font-size: 16px;
color: #303133;
margin-top: 28px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
p {
font-size: 14px;
margin-bottom: 8px;
text-indent: 0;
}
.footer {
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid #ebeef5;
color: #909399;
}
}
</style>
-570
View File
@@ -1,570 +0,0 @@
<template>
<div class="page-container">
<div class="left-section">
<div class="logistics-map">
<div class="map-title">武鸣沃柑大脑</div>
<div class="count-panel">
<div class="count-box">
<div class="count-value">{{ exportOrchardCount }}</div>
<div class="count-title">出口果园</div>
</div>
<div class="count-box">
<div class="count-value">{{ exportBatchCount }}</div>
<div class="count-title">出口批次</div>
</div>
<div class="count-box">
<div class="count-value">{{ totalExportWeight }}</div>
<div class="count-title">累计出口</div>
</div>
</div>
<div id="map-container" ref="mapContainer"></div>
<div v-if="!isMapReady" class="map-loading">地图资源加载中...</div>
</div>
</div>
<div class="right-section">
<RightChartPanel />
</div>
<!-- 果园详情弹框 -->
<OrchardDetailModal
:orchard-name="selectedOrchardName"
:visible.sync="showModal"
/>
</div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import RightChartPanel from "@/components/brain/RightChartPanel.vue";
import OrchardDetailModal from '@/components/brain/OrchardDetailModal.vue';
export default {
name: "OrchardMap",
components: { RightChartPanel,OrchardDetailModal },
data() {
return {
map: null,
AMap: null,
isMapReady: false,
infoWindow: null,
exportOrchardCount: "56家",
exportBatchCount: "101批",
totalExportWeight: "4031万斤",
orchards: [], // 空数组,后续初始化10条数据
woganIcon: require("@/assets/images/brain/出口icon.png"),
markers: [],
resizeHandler: null,
currentIndex: 0, // 当前展示的信息框索引
autoPlay: true, // 是否自动播放
playTimer: null, // 自动播放定时器
playDuration: { min: 5000, max: 8000 }, // 播放时长范围:5-8秒
showModal: false,
selectedOrchardName: '正欣农业-1号园区' // 从你的果园数据中传递
};
},
created() {
window._AMapSecurityConfig = {
securityJsCode: "8f1711e57fb2704974d568056a762ee3",
};
this.resizeHandler = this.debounce(() => this.adjustMapZoomByScreen(), 300);
// 初始化10条果园数据
this.initOrchardData();
},
mounted() {
this.initMap();
window.addEventListener("resize", this.resizeHandler);
},
beforeDestroy() {
// 清除定时器
if (this.playTimer) clearInterval(this.playTimer);
if (this.map) this.map.destroy();
window.removeEventListener("resize", this.resizeHandler);
},
methods: {
// 防抖函数
debounce(fn, delay) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this), delay);
};
},
// 生成10条果园数据(4个在108.01,23.19附近3000米内,其余6个补充)
initOrchardData() {
// 中心点:108.01,23.19(正欣农业),3000米约对应经纬度偏移±0.03
const centerLng = 108.01;
const centerLat = 23.19;
// 生成正欣农业附近4个园区(3000米内)
const nearOrchards = Array.from({ length: 4 }, (_, i) => {
// 随机偏移经纬度(±0.03内)
const lngOffset = (Math.random() - 0.5) * 2 * 0.03;
const latOffset = (Math.random() - 0.5) * 2 * 0.03;
const lng = Number((centerLng + lngOffset).toFixed(4));
const lat = Number((centerLat + latOffset).toFixed(4));
return {
id: i + 1,
name: `正欣农业-${i + 1}号园区`,
lat,
lng,
registerNo: `7201GXXXX${i + 1}`,
address: `广西南宁市武鸣区锣圩镇板新村长平屯剑麻地地-${i + 1}`,
registerType: "出境新鲜水果果园注册登记",
productType: "沃柑、青柚",
yield: `${1000 - i * 50}万斤`,
validPeriod: "自2025年10月21日至2028年10月21日",
};
});
// 补充6个其他园区数据
const otherOrchards = [
{
id: 5,
name: "武鸣沃柑示范基地",
lat: 23.1681,
lng: 108.2778,
registerNo: "7202GXXXX",
address: "广西南宁市武鸣区双桥镇杨李村",
registerType: "出境新鲜水果果园注册登记",
productType: "沃柑",
yield: "800万斤",
validPeriod: "自2024年05月10日至2027年05月10日",
},
{
id: 6,
name: "宁武镇沃柑种植园",
lat: 23.1223,
lng: 108.3123,
registerNo: "7203GXXXX",
address: "广西南宁市武鸣区宁武镇雄孟村",
registerType: "出境新鲜水果果园注册登记",
productType: "沃柑",
yield: "600万斤",
validPeriod: "自2024年08月15日至2027年08月15日",
},
{
id: 7,
name: "陆斡镇沃柑基地",
lat: 23.2512,
lng: 108.1823,
registerNo: "7204GXXXX",
address: "广西南宁市武鸣区陆斡镇桥东村",
registerType: "出境新鲜水果果园注册登记",
productType: "沃柑",
yield: "750万斤",
validPeriod: "自2024年11月01日至2027年11月01日",
},
{
id: 8,
name: "双桥镇柑桔产业园",
lat: 23.1721,
lng: 108.2818,
registerNo: "7205GXXXX",
address: "广西南宁市武鸣区双桥镇镇南社区",
registerType: "出境新鲜水果果园注册登记",
productType: "沃柑、茂谷柑",
yield: "900万斤",
validPeriod: "自2024年06月01日至2027年06月01日",
},
{
id: 9,
name: "太平镇沃柑种植基地",
lat: 23.2015,
lng: 108.2235,
registerNo: "7206GXXXX",
address: "广西南宁市武鸣区太平镇庆乐村",
registerType: "出境新鲜水果果园注册登记",
productType: "沃柑",
yield: "850万斤",
validPeriod: "自2024年07月10日至2027年07月10日",
},
{
id: 10,
name: "府城镇柑桔示范园",
lat: 23.2342,
lng: 108.1567,
registerNo: "7207GXXXX",
address: "广西南宁市武鸣区府城镇四明村",
registerType: "出境新鲜水果果园注册登记",
productType: "沃柑、脐橙",
yield: "780万斤",
validPeriod: "自2024年08月01日至2027年08月01日",
},
];
// 合并为10条数据
this.orchards = [...nearOrchards, ...otherOrchards];
},
// 计算适配的缩放级别
getAdaptedZoom() {
const el = document.getElementById("map-container");
const mapContainerWidth = el ? el.clientWidth : 0;
if (mapContainerWidth >= 1920) return 11;
if (mapContainerWidth >= 1440) return 11;
if (mapContainerWidth >= 1024) return 10;
if (mapContainerWidth >= 768) return 9;
return 8;
},
// 调整地图缩放(首次进入与 resize 共用)
adjustMapZoomByScreen() {
if (!this.map) return;
const targetZoom = this.getAdaptedZoom();
this.map.setZoom(targetZoom);
},
// 初始化地图
async initMap() {
try {
const AMap = await AMapLoader.load({
key: "8c615942a3955277d8b4855ce50a43a6",
version: "2.0",
plugins: ["AMap.DistrictSearch"],
});
this.AMap = AMap;
const initZoom = this.getAdaptedZoom();
this.map = new AMap.Map("map-container", {
zoom: initZoom,
center: [108.01, 23.19], // 初始中心点定位到正欣农业
viewMode: "2D",
pitch: 0,
mapStyle: "amap://styles/darkblue",
showLabel: true,
features: ["bg", "road", "building", "point"],
showIndoorMap: false,
zoomEnable: true,
dragEnable: true,
rotateEnable: false,
});
this.map.on("complete", () => {
this.adjustMapZoomByScreen();
});
this.isMapReady = true;
this.drawBoundaries("武鸣区");
this.drawOrchardMarkers();
// 启动自动播放
this.startAutoPlay();
} catch (error) {
console.error("地图初始化失败:", error);
}
},
// 绘制区域边界
drawBoundaries(districtName) {
if (!this.AMap) return;
const district = new this.AMap.DistrictSearch({
level: "district",
extensions: "all",
subdistrict: 0,
});
district.search(districtName, (status, result) => {
if (status === "complete" && result.districtList.length > 0) {
const bounds = result.districtList[0].boundaries;
if (bounds) {
bounds.forEach((boundary) => {
new this.AMap.Polygon({
path: boundary,
strokeColor: "#00eaff",
strokeWeight: 3,
strokeOpacity: 1,
fillColor: "#1cadff",
fillOpacity: 0.1,
map: this.map,
zIndex: 10,
});
});
}
}
});
},
// 绘制果园标记
drawOrchardMarkers() {
if (!this.map) return;
this.markers.forEach((m) => m.setMap(null));
this.markers = [];
this.orchards.forEach((item) => {
const marker = new this.AMap.Marker({
position: [item.lng, item.lat],
icon: new this.AMap.Icon({
image: this.woganIcon,
size: new this.AMap.Size(40, 40),
imageSize: new this.AMap.Size(40, 40),
}),
offset: new this.AMap.Pixel(-20, -20),
map: this.map,
zIndex: 100,
cursor: "pointer",
extData: item,
});
// 点击marker:停止自动播放,手动切换到当前信息框
marker.on("click", () => {
// this.stopAutoPlay();
this.currentIndex = this.orchards.findIndex(orchard => orchard.id === item.id);
this.selectedOrchardName = this.orchards[this.currentIndex].name;
this.showModal = true
console.log('弹框',this.showModal);
this.openInfoWindow(marker, item);
// this.map.panTo(marker.getPosition());
});
this.markers.push(marker);
});
// 初始显示第一个信息框,并按屏幕宽度初始化缩放(不再用 setFitView 覆盖)
setTimeout(() => {
const firstMarker = this.markers[0];
this.openInfoWindow(firstMarker, this.orchards[0]);
this.adjustMapZoomByScreen();
}, 800);
},
// 打开信息窗口(半透明 + 右侧显示)
openInfoWindow(marker, data) {
const infoContent = `
<div class="custom-info-window">
<div class="info-header">${data.name}</div>
<div class="info-body">
<div class="info-row"><span>果园注册号:</span><span>${data.registerNo}</span></div>
<div class="info-row"><span>地址:</span><span>${data.address}</span></div>
<div class="info-row"><span>登记类型:</span><span>${data.registerType}</span></div>
<div class="info-row"><span>产品类型:</span><span>${data.productType}</span></div>
<div class="info-row"><span>年产量:</span><span>${data.yield}</span></div>
<div class="info-row"><span>有效期限:</span><span>${data.validPeriod}</span></div>
</div>
</div>
`;
if (!this.infoWindow) {
this.infoWindow = new this.AMap.InfoWindow({
isCustom: true,
anchor: "top-left", // 信息框显示在图标右侧
offset: new this.AMap.Pixel(10, -10), // 微调位置,避免重叠
closeWhenClickMap: false, // 点击地图不关闭信息框
});
}
this.infoWindow.setContent(infoContent);
this.infoWindow.open(this.map, marker.getPosition());
},
// 开始自动播放
startAutoPlay() {
if (this.playTimer) clearInterval(this.playTimer);
this.autoPlay = true;
// 随机5-8秒切换
this.playTimer = setInterval(() => {
this.nextInfoWindow();
}, Math.floor(Math.random() * (this.playDuration.max - this.playDuration.min)) + this.playDuration.min);
},
// 下一个信息框
nextInfoWindow() {
this.currentIndex = (this.currentIndex + 1) % this.orchards.length;
const currentOrchard = this.orchards[this.currentIndex];
const currentMarker = this.markers.find(marker => marker.getExtData().id === currentOrchard.id);
if (currentMarker) {
this.openInfoWindow(currentMarker, currentOrchard);
// this.map.panTo(currentMarker.getPosition()); // 地图跟随定位
}
},
},
};
</script>
<style>
.page-container {
width: 100%;
height: 100vh;
display: flex;
background-color: #020f26;
font-family: "Microsoft YaHei", sans-serif;
overflow: hidden;
}
.left-section {
width: 80%;
height: 100%;
position: relative;
border-right: 1px solid #00eaff;
}
.logistics-map {
width: 100%;
height: 100%;
position: relative;
}
/* 手动控制按钮面板 */
.info-control-panel {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 999;
display: flex;
gap: 10px;
}
.control-btn {
padding: 8px 16px;
background: rgba(12, 28, 56, 0.8);
border: 1px solid #00eaff;
color: #00eaff;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.control-btn:hover {
background: rgba(0, 234, 255, 0.2);
border-color: #01ffff;
}
#map-container {
width: 100%;
height: 100%;
padding: 0 !important;
margin: 0 !important;
border: none !important;
}
/* 标记图标圆形样式 */
.amap-marker img {
border-radius: 50% !important;
overflow: hidden !important;
border: 2px solid #00eaff !important;
}
/* 隐藏原始工具栏 */
.amap-toolbar {
display: none !important;
}
.map-title {
position: absolute;
top: 20px;
left: 20px;
z-index: 10;
font-size: 28px;
font-weight: bold;
color: #00eaff;
text-shadow: 0 0 10px rgba(0, 234, 255, 0.8);
}
.count-panel {
position: absolute;
top: 80px;
left: 20px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 15px;
}
.count-box {
width: 120px;
height: 70px;
background: rgba(12, 28, 56, 0.8);
border: 1px solid #1f76ab;
box-shadow: 0 0 10px rgba(31, 118, 171, 0.5);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
border-radius: 4px;
}
.count-value {
font-size: 22px;
color: #00eaff;
font-weight: bold;
text-shadow: 0 0 8px rgba(0, 234, 255, 0.6);
margin-bottom: 5px;
}
.count-title {
font-size: 14px;
color: #cceaff;
}
.map-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: #00eaff;
padding: 10px 20px;
border-radius: 4px;
z-index: 999;
}
.right-section {
width: 20%;
height: 100%;
background: rgba(2, 15, 38, 0.95);
padding: 10px;
box-sizing: border-box;
border-top: 2px solid #01ffff;
border-right: 2px solid #01ffff;
border-bottom: 2px solid #01ffff;
border-left: none;
}
/* 半透明信息框样式 */
.custom-info-window {
padding: 12px 15px;
min-width: 300px;
font-size: 13px;
line-height: 1.6;
border: 1px solid #01ffff;
background: rgba(12, 28, 56, 0.96) !important; /* 80%透明度 */
border-radius: 4px;
color: #fff;
}
.custom-info-window .info-header {
font-size: 15px;
font-weight: bold;
color: #00eaff;
margin-bottom: 8px;
padding-bottom: 5px;
border-bottom: 1px solid rgba(0, 234, 255, 0.3);
}
.custom-info-window .info-body .info-row {
display: flex;
margin-bottom: 4px;
word-break: break-all;
}
.custom-info-window .info-body .info-row span:first-child {
color: #cceaff;
width: 120px;
display: inline-block;
}
.custom-info-window .info-body .info-row span:last-child {
color: #fff;
flex: 1;
}
</style>
-723
View File
@@ -1,723 +0,0 @@
<template>
<div class="certify-container">
<div class="certify-main">
<div class="certify-card">
<div class="certify-title">企业认证</div>
<el-form
ref="certifyForm"
:model="form"
:rules="rules"
label-position="top"
size="medium"
class="certify-form"
>
<!-- Section 1: Basic Info -->
<div class="form-section">
<div class="section-header">
<span class="section-dot"></span>
<span class="section-title">基本信息</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="企业名称" prop="customerName">
<el-input v-model="form.customerName" placeholder="请输入企业全称" maxlength="50" show-word-limit></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="企业简称" prop="shortName">
<el-input v-model="form.shortName" placeholder="请输入企业简称" maxlength="20" show-word-limit></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="主体类型" prop="customerType">
<el-select v-model="form.customerType" placeholder="请选择主体类型" style="width:100%">
<el-option label="个人" :value="1"></el-option>
<el-option label="企业" :value="2"></el-option>
<el-option label="个体户" :value="3"></el-option>
<el-option label="政府" :value="4"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="营业执照号" prop="customerIdNumber">
<el-input v-model="form.customerIdNumber" placeholder="请输入统一社会信用代码/营业执照号" maxlength="30"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="企业LOGO">
<div class="upload-area">
<el-upload
class="single-upload"
action="/api/user-service/common/upload"
:data="{ space: 'customer_logo' }"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleLogoSuccess"
:before-upload="beforeImageUpload"
accept="image/*"
>
<div class="single-upload-box logo" :class="{ 'has-image': form.logo }">
<el-image v-if="form.logo" :src="form.logo" fit="cover" class="uploaded-img"></el-image>
<div v-else class="upload-placeholder">
<i class="el-icon-camera"></i>
<span>上传LOGO</span>
</div>
</div>
</el-upload>
<span class="upload-tip">建议尺寸 200x200支持 JPG/PNG不超过 5MB</span>
</div>
</el-form-item>
</div>
<!-- Section 2: Legal Person -->
<div class="form-section">
<div class="section-header">
<span class="section-dot"></span>
<span class="section-title">法人信息</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="法人姓名" prop="legalName">
<el-input v-model="form.legalName" placeholder="请输入法人姓名" maxlength="20"></el-input>
</el-form-item>
</el-col>
<el-col :span="12"></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="法人身份证正面" prop="legalIdFront">
<el-upload
class="single-upload"
action="/api/user-service/common/upload"
:data="{ space: 'qualification' }"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleLegalIdFrontSuccess"
:before-upload="beforeImageUpload"
accept="image/*"
>
<div class="single-upload-box id-card" :class="{ 'has-image': form.legalIdFront }">
<el-image v-if="form.legalIdFront" :src="form.legalIdFront" fit="cover" class="uploaded-img"></el-image>
<div v-else class="upload-placeholder">
<i class="el-icon-plus"></i>
<span>上传</span>
</div>
</div>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="法人身份证反面" prop="legalIdBack">
<el-upload
class="single-upload"
action="/api/user-service/common/upload"
:data="{ space: 'qualification' }"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleLegalIdBackSuccess"
:before-upload="beforeImageUpload"
accept="image/*"
>
<div class="single-upload-box id-card" :class="{ 'has-image': form.legalIdBack }">
<el-image v-if="form.legalIdBack" :src="form.legalIdBack" fit="cover" class="uploaded-img"></el-image>
<div v-else class="upload-placeholder">
<i class="el-icon-plus"></i>
<span>上传</span>
</div>
</div>
</el-upload>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- Section 3: Admin Info -->
<div class="form-section">
<div class="section-header">
<span class="section-dot"></span>
<span class="section-title">管理员信息</span>
</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="管理员姓名" prop="adminName">
<el-input v-model="form.adminName" placeholder="请输入管理员姓名" maxlength="20"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="管理员手机" prop="adminPhone">
<el-input v-model="form.adminPhone" placeholder="请输入手机号" maxlength="11"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="管理员证件号" prop="adminIdNumber">
<el-input v-model="form.adminIdNumber" placeholder="请输入身份证号" maxlength="30"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- Section 4: Industry & Region -->
<div class="form-section">
<div class="section-header">
<span class="section-dot"></span>
<span class="section-title">行业与区域</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="行业分类">
<el-cascader
v-model="industryCascader"
:options="industryOptions"
placeholder="请选择行业"
clearable
filterable
style="width:100%"
@change="handleIndustryChange"
></el-cascader>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="销售渠道">
<el-select v-model="form.salesChannel" placeholder="请选择" clearable style="width:100%">
<el-option v-for="item in salesChannelOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="行业类型">
<el-select v-model="form.industryType" placeholder="请选择" clearable style="width:100%">
<el-option v-for="item in industryTypeOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="省份">
<el-select v-model="selectedProvince" placeholder="请选择省份" clearable style="width:100%" @change="handleProvinceChange">
<el-option v-for="item in provinceOptions" :key="item.label" :label="item.label" :value="item.label"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="城市">
<el-select v-model="form.city" placeholder="请选择城市" clearable style="width:100%">
<el-option v-for="item in cityOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- Section 5: Qualifications -->
<div class="form-section">
<div class="section-header">
<span class="section-dot"></span>
<span class="section-title">资质材料</span>
</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="营业执照" prop="businessLicense">
<el-upload
class="single-upload"
action="/api/user-service/common/upload"
:data="{ space: 'qualification' }"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleBusinessLicenseSuccess"
:before-upload="beforeImageUpload"
accept="image/*"
>
<div class="single-upload-box id-card" :class="{ 'has-image': form.businessLicense }">
<el-image v-if="form.businessLicense" :src="form.businessLicense" fit="cover" class="uploaded-img"></el-image>
<div v-else class="upload-placeholder">
<i class="el-icon-plus"></i>
<span>上传</span>
</div>
</div>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="管理员身份证正面" prop="responsibleIdFront">
<el-upload
class="single-upload"
action="/api/user-service/common/upload"
:data="{ space: 'qualification' }"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleResponsibleIdFrontSuccess"
:before-upload="beforeImageUpload"
accept="image/*"
>
<div class="single-upload-box id-card" :class="{ 'has-image': form.responsibleIdFront }">
<el-image v-if="form.responsibleIdFront" :src="form.responsibleIdFront" fit="cover" class="uploaded-img"></el-image>
<div v-else class="upload-placeholder">
<i class="el-icon-plus"></i>
<span>上传</span>
</div>
</div>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="管理员身份证反面" prop="responsibleIdBack">
<el-upload
class="single-upload"
action="/api/user-service/common/upload"
:data="{ space: 'qualification' }"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleResponsibleIdBackSuccess"
:before-upload="beforeImageUpload"
accept="image/*"
>
<div class="single-upload-box id-card" :class="{ 'has-image': form.responsibleIdBack }">
<el-image v-if="form.responsibleIdBack" :src="form.responsibleIdBack" fit="cover" class="uploaded-img"></el-image>
<div v-else class="upload-placeholder">
<i class="el-icon-plus"></i>
<span>上传</span>
</div>
</div>
</el-upload>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- Agreement -->
<div class="form-section agreement-section">
<el-checkbox v-model="agreed">
我已阅读并同意
<span class="agreement-link" @click.prevent.stop="openAgreement">企业认证协议</span>
</el-checkbox>
</div>
<!-- Submit -->
<div class="form-actions">
<el-button @click="goBack" size="large">取消</el-button>
<el-button type="primary" @click="submitCertify" :loading="submitting" :disabled="!agreed" size="large" icon="el-icon-check">提交认证</el-button>
</div>
</el-form>
</div>
</div>
<!-- Image Preview Dialog -->
<el-dialog :visible.sync="previewVisible" width="600px" append-to-body class="preview-dialog">
<img :src="previewUrl" style="width:100%;border-radius:4px" v-if="previewVisible">
</el-dialog>
</div>
</template>
<script>
import userApi from '@/api/user'
const DEFAULTS = {
headerLogoUrl: 'https://rw--yilise-com-img-1308359487.cos.ap-shanghai.myqcloud.com/zhangqu/zhangqu_web/e2b857df8e4f4b10b7affedb4f91bec1.png'
}
export default {
name: 'EnterpriseCertify',
data() {
return {
DEFAULTS,
token: '',
submitting: false,
agreed: false,
form: {
customerName: '',
logo: '',
shortName: '',
customerType: null,
customerIdNumber: '',
adminName: '',
adminPhone: '',
adminIdNumber: '',
businessLicense: '',
legalName: '',
legalIdFront: '',
legalIdBack: '',
responsibleIdFront: '',
responsibleIdBack: '',
industry: '',
industrySub: '',
salesChannel: '',
industryType: '',
province: '',
city: ''
},
industryCascader: [],
selectedProvince: '',
cityOptions: [],
rules: {
customerName: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
shortName: [{ required: true, message: '请输入企业简称', trigger: 'blur' }],
customerType: [{ required: true, message: '请选择主体类型', trigger: 'change' }],
customerIdNumber: [{ required: true, message: '请输入营业执照号', trigger: 'blur' }],
adminName: [{ required: true, message: '请输入管理员姓名', trigger: 'blur' }],
adminPhone: [
{ required: true, message: '请输入管理员手机', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
adminIdNumber: [{ required: true, message: '请输入管理员证件号', trigger: 'blur' }],
businessLicense: [{ required: true, message: '请上传营业执照', trigger: 'change' }],
legalName: [{ required: true, message: '请输入法人姓名', trigger: 'blur' }],
legalIdFront: [{ required: true, message: '请上传法人身份证正面', trigger: 'change' }],
legalIdBack: [{ required: true, message: '请上传法人身份证反面', trigger: 'change' }],
responsibleIdFront: [{ required: true, message: '请上传管理员身份证正面', trigger: 'change' }],
responsibleIdBack: [{ required: true, message: '请上传管理员身份证反面', trigger: 'change' }]
},
uploadHeaders: { token: '' },
previewVisible: false,
previewUrl: '',
industryOptions: [
{ value: '制造业', label: '制造业', children: [
{ value: '装备制造', label: '装备制造(机械/设备)' }, { value: '电子制造', label: '电子制造(3C/半导体)' },
{ value: '轻工制造', label: '轻工制造(家居/家具/玩具)' }, { value: '食品加工', label: '食品加工/饮料' },
{ value: '纺织服装', label: '纺织/服装/鞋帽' }, { value: '化工新材料', label: '化工/新材料' },
{ value: '汽车及零部件', label: '汽车及零部件' }, { value: '医疗器械', label: '医疗器械/生物制药' },
{ value: '农业机械', label: '农业机械/农机装备' },
]},
{ value: '批发零售/电商', label: '批发零售/电商', children: [
{ value: '线下零售', label: '线下零售(商超/便利店)' }, { value: '品牌连锁', label: '品牌连锁(专卖/加盟)' },
{ value: '电商平台', label: '电商平台(综合/垂直)' }, { value: '直播电商', label: '直播电商/内容电商' },
{ value: '社区团购', label: '社区团购/本地生活' }, { value: '跨境电商', label: '跨境电商(出口/进口)' },
{ value: '批发分销', label: '批发分销(建材/日化/农资)' }, { value: '母婴美妆', label: '母婴/美妆/奢侈品' },
{ value: '生鲜冷链', label: '生鲜/冷链' },
]},
{ value: '金融业', label: '金融业', children: [
{ value: '银行信用社', label: '银行/信用社' }, { value: '保险', label: '保险(财险/寿险/健康险)' },
{ value: '证券基金', label: '证券/基金/期货' }, { value: '支付清算', label: '支付/清算/金融科技' },
{ value: '融资租赁', label: '融资租赁/保理' }, { value: '小贷消费', label: '小贷/消费金融' },
{ value: '财富管理', label: '财富管理/私人银行' }, { value: '供应链金融', label: '供应链金融' },
]},
{ value: '互联网/软件/科技', label: '互联网/软件/科技', children: [
{ value: '软件开发', label: '软件开发/IT服务' }, { value: '云计算', label: '云计算/IDC/数据服务' },
{ value: '新媒体', label: '新媒体/自媒体/内容创作' }, { value: '游戏电竞', label: '游戏/电竞' },
{ value: '网络安全', label: '网络安全/数据合规' }, { value: 'AI大数据', label: 'AI/大数据/算法服务' },
{ value: '物联网', label: '物联网/工业互联网' }, { value: '通信运营商', label: '通信/运营商' },
]},
{ value: '教育', label: '教育', children: [
{ value: 'K12', label: 'K12(小学/初中/高中)' }, { value: '高等教育', label: '高等教育(大学/高职)' },
{ value: '职业教育', label: '职业教育/技能培训' }, { value: '素质教育', label: '素质教育(艺术/体育/编程)' },
{ value: '早教托育', label: '早教/托育' }, { value: '企业培训', label: '企业培训/内训' },
{ value: '在线教育', label: '在线教育平台' },
]},
{ value: '医疗/康养', label: '医疗/康养', children: [
{ value: '综合医院', label: '综合医院/专科医院' }, { value: '诊所', label: '诊所/门诊/私立医疗' },
{ value: '体检中心', label: '体检中心/健康管理' }, { value: '医美口腔', label: '医美/口腔/眼科' },
{ value: '养老康复', label: '养老/康复/护理' }, { value: '医药流通', label: '医药流通/医疗器械' },
{ value: '生物科技', label: '生物科技/基因检测' },
]},
{ value: '物流/供应链', label: '物流/供应链', children: [
{ value: '快递快运', label: '快递/快运' }, { value: '仓储冷链', label: '仓储/冷链仓储' },
{ value: '干线运输', label: '干线运输/城配' }, { value: '货代报关', label: '货代/报关/国际物流' },
{ value: '供应链管理', label: '供应链管理/集采' }, { value: '物流科技', label: '物流科技/车联网' },
]},
{ value: '餐饮/酒店/本地生活', label: '餐饮/酒店/本地生活', children: [
{ value: '连锁餐饮', label: '连锁餐饮/快餐/火锅' }, { value: '茶饮咖啡', label: '茶饮/咖啡/烘焙' },
{ value: '酒店民宿', label: '酒店/民宿/公寓' }, { value: '美容美发', label: '美容美发/美甲' },
{ value: '家政保洁', label: '家政/保洁/维修' }, { value: '健身瑜伽', label: '健身/瑜伽/场馆' },
]},
{ value: '地产/建筑/工程', label: '地产/建筑/工程', children: [
{ value: '房地产开发', label: '房地产开发/房企' }, { value: '商业地产', label: '商业地产/写字楼/商场' },
{ value: '物业管理', label: '物业管理/园区运营' }, { value: '建筑施工', label: '建筑施工/工程总包' },
{ value: '装饰装修', label: '装饰装修/家装' }, { value: '建材门窗', label: '建材/门窗/卫浴' },
]},
{ value: '农业/涉农', label: '农业/涉农', children: [
{ value: '种植', label: '种植(粮食/果蔬/茶叶)' }, { value: '养殖', label: '养殖(畜牧/水产/家禽)' },
{ value: '农资', label: '农资(化肥/农药/种子)' }, { value: '农机服务', label: '农机/农机服务' },
{ value: '农产品加工', label: '农产品加工/预制菜' }, { value: '农业园区', label: '农业园区/合作社' },
]},
{ value: '政务/国企/机构', label: '政务/国企/机构', children: [
{ value: '政府机关', label: '政府机关/事业单位' }, { value: '国企央企', label: '国企/央企' },
{ value: '协会商会', label: '协会/商会/非营利组织' }, { value: '科研院所', label: '科研院所/实验室' },
]},
{ value: '其他服务', label: '其他服务', children: [
{ value: '人力资源', label: '人力资源/猎头/劳务派遣' }, { value: '财务税务', label: '财务/税务/审计/律所' },
{ value: '广告公关', label: '广告/公关/会展' }, { value: '咨询', label: '咨询/企业服务' },
{ value: '印刷包装', label: '印刷/包装/设计' }, { value: '影视传媒', label: '影视/传媒/广告制作' },
]},
],
salesChannelOptions: ['直营直销','代理渠道','经销渠道','城市合伙人','线上获客','线下地推','客户转介绍','政企项目','生态合作','行业服务商','其他'],
industryTypeOptions: ['供应商','加工商','经销商','零售商'],
provinceOptions: [
{ label: '北京', cities: ['东城区','西城区','朝阳区','丰台区','石景山区','海淀区','顺义区','通州区','大兴区','房山区','门头沟区','昌平区','平谷区','密云区','怀柔区','延庆区'] },
{ label: '上海', cities: ['黄浦区','徐汇区','长宁区','静安区','普陀区','虹口区','杨浦区','闵行区','宝山区','嘉定区','浦东新区','金山区','松江区','青浦区','奉贤区','崇明区'] },
{ label: '天津', cities: ['和平区','河东区','河西区','南开区','河北区','红桥区','东丽区','西青区','津南区','北辰区','武清区','宝坻区','滨海新区','宁河区','静海区','蓟州区'] },
{ label: '重庆', cities: ['万州区','涪陵区','渝中区','大渡口区','江北区','沙坪坝区','九龙坡区','南岸区','北碚区','綦江区','大足区','渝北区','巴南区','黔江区','长寿区','江津区','合川区','永川区'] },
{ label: '河北', cities: ['石家庄','唐山','秦皇岛','邯郸','邢台','保定','张家口','承德','沧州','廊坊','衡水'] },
{ label: '山西', cities: ['太原','大同','阳泉','长治','晋城','朔州','晋中','运城','忻州','临汾','吕梁'] },
{ label: '内蒙古', cities: ['呼和浩特','包头','乌海','赤峰','通辽','鄂尔多斯','呼伦贝尔','巴彦淖尔','乌兰察布','兴安盟','锡林郭勒盟','阿拉善盟'] },
{ label: '辽宁', cities: ['沈阳','大连','鞍山','抚顺','本溪','丹东','锦州','营口','阜新','辽阳','盘锦','铁岭','朝阳','葫芦岛'] },
{ label: '吉林', cities: ['长春','吉林','四平','辽源','通化','白山','松原','白城','延边州'] },
{ label: '黑龙江', cities: ['哈尔滨','齐齐哈尔','鸡西','鹤岗','双鸭山','大庆','伊春','佳木斯','七台河','牡丹江','黑河','绥化','大兴安岭'] },
{ label: '江苏', cities: ['南京','无锡','徐州','常州','苏州','南通','连云港','淮安','盐城','扬州','镇江','泰州','宿迁'] },
{ label: '浙江', cities: ['杭州','宁波','温州','嘉兴','湖州','绍兴','金华','衢州','舟山','台州','丽水'] },
{ label: '安徽', cities: ['合肥','芜湖','蚌埠','淮南','马鞍山','淮北','铜陵','安庆','黄山','滁州','阜阳','宿州','六安','亳州','池州','宣城'] },
{ label: '福建', cities: ['福州','厦门','莆田','三明','泉州','漳州','南平','龙岩','宁德'] },
{ label: '江西', cities: ['南昌','景德镇','萍乡','九江','新余','鹰潭','赣州','吉安','宜春','抚州','上饶'] },
{ label: '山东', cities: ['济南','青岛','淄博','枣庄','东营','烟台','潍坊','济宁','泰安','威海','日照','临沂','德州','聊城','滨州','菏泽'] },
{ label: '河南', cities: ['郑州','开封','洛阳','平顶山','安阳','鹤壁','新乡','焦作','濮阳','许昌','漯河','三门峡','南阳','商丘','信阳','周口','驻马店'] },
{ label: '湖北', cities: ['武汉','黄石','十堰','宜昌','襄阳','鄂州','荆门','孝感','荆州','黄冈','咸宁','随州','恩施州','仙桃','潜江','天门','神农架'] },
{ label: '湖南', cities: ['长沙','株洲','湘潭','衡阳','邵阳','岳阳','常德','张家界','益阳','郴州','永州','怀化','娄底','湘西州'] },
{ label: '广东', cities: ['广州','韶关','深圳','珠海','汕头','佛山','江门','湛江','茂名','肇庆','惠州','梅州','汕尾','河源','阳江','清远','东莞','中山','潮州','揭阳','云浮'] },
{ label: '广西', cities: ['南宁','柳州','桂林','梧州','北海','防城港','钦州','贵港','玉林','百色','贺州','河池','来宾','崇左'] },
{ label: '海南', cities: ['海口','三亚','三沙','儋州','五指山','琼海','文昌','万宁','东方','定安','屯昌','澄迈','临高','白沙','昌江','乐东','陵水','保亭','琼中'] },
{ label: '四川', cities: ['成都','自贡','攀枝花','泸州','德阳','绵阳','广元','遂宁','内江','乐山','南充','眉山','宜宾','广安','达州','雅安','巴中','资阳','阿坝州','甘孜州','凉山州'] },
{ label: '贵州', cities: ['贵阳','六盘水','遵义','安顺','毕节','铜仁','黔西南州','黔东南州','黔南州'] },
{ label: '云南', cities: ['昆明','曲靖','玉溪','保山','昭通','丽江','普洱','临沧','楚雄州','红河州','文山州','西双版纳州','大理州','德宏州','怒江州','迪庆州'] },
{ label: '西藏', cities: ['拉萨','日喀则','昌都','林芝','山南','那曲','阿里'] },
{ label: '陕西', cities: ['西安','铜川','宝鸡','咸阳','渭南','延安','汉中','榆林','安康','商洛'] },
{ label: '甘肃', cities: ['兰州','嘉峪关','金昌','白银','天水','武威','张掖','平凉','酒泉','庆阳','定西','陇南','临夏州','甘南州'] },
{ label: '青海', cities: ['西宁','海东','海北州','黄南州','海南州','果洛州','玉树州','海西州'] },
{ label: '宁夏', cities: ['银川','石嘴山','吴忠','固原','中卫'] },
{ label: '新疆', cities: ['乌鲁木齐','克拉玛依','吐鲁番','哈密','昌吉州','博尔塔拉州','巴音郭楞州','阿克苏','克孜勒苏州','喀什','和田','伊犁州','塔城','阿勒泰'] },
]
}
},
created() {
// Read token from localStorage (set by login page)
this.token = localStorage.getItem('_token') || ''
if (!this.token) {
this.$message.error('缺少认证凭证,请重新登录')
this.$router.replace('/login')
}
this.uploadHeaders.token = this.token
// 重新编辑模式:加载已有企业信息
if (this.$route.query.reEdit === '1') {
this.loadExistingCustomer()
}
},
methods: {
goBack() {
this.$router.replace('/login')
},
openAgreement() {
window.open(this.$router.resolve({ path: '/certifyAgreement', query: { token: this.token } }).href, '_blank')
},
previewImage(url) {
this.previewUrl = url
this.previewVisible = true
},
beforeImageUpload(file) {
const isImage = /^image\//.test(file.type)
if (!isImage) this.$message.error('只能上传图片文件!')
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) this.$message.error('图片大小不能超过5MB!')
return isImage && isLt5M
},
handleLogoSuccess(res) {
if (res.code === 1) this.form.logo = res.data
},
handleLegalIdFrontSuccess(res) {
if (res.code === 1) this.form.legalIdFront = res.data
},
handleLegalIdBackSuccess(res) {
if (res.code === 1) this.form.legalIdBack = res.data
},
handleBusinessLicenseSuccess(res) {
if (res.code === 1) this.form.businessLicense = res.data
},
handleResponsibleIdFrontSuccess(res) {
if (res.code === 1) this.form.responsibleIdFront = res.data
},
handleResponsibleIdBackSuccess(res) {
if (res.code === 1) this.form.responsibleIdBack = res.data
},
async loadExistingCustomer() {
try {
const res = await userApi.getPendingCustomer(this.token)
if (res.code === 1 && res.data) {
const d = res.data
this.form.customerName = d.customerName || ''
this.form.logo = d.logo || ''
this.form.shortName = d.shortName || ''
this.form.customerType = d.customerType || null
this.form.customerIdNumber = d.customerIdNumber || ''
this.form.adminName = d.adminName || ''
this.form.adminPhone = d.adminPhone || ''
this.form.adminIdNumber = d.adminIdNumber || ''
this.form.businessLicense = d.businessLicense || ''
this.form.legalName = d.legalName || ''
this.form.legalIdFront = d.legalIdFront || ''
this.form.legalIdBack = d.legalIdBack || ''
this.form.responsibleIdFront = d.responsibleIdFront || ''
this.form.responsibleIdBack = d.responsibleIdBack || ''
this.form.industry = d.industry || ''
this.form.industrySub = d.industrySub || ''
this.form.salesChannel = d.salesChannel || ''
this.form.industryType = d.industryType || ''
this.form.province = d.province || ''
this.form.city = d.city || ''
// 同步行业级联选择器
if (this.form.industry && this.form.industrySub) {
this.industryCascader = [this.form.industry, this.form.industrySub]
}
// 同步省市联动
if (this.form.province) {
this.selectedProvince = this.form.province
const found = this.provinceOptions.find(p => p.label === this.form.province)
this.cityOptions = found ? found.cities : []
}
// 自动勾选协议
this.agreed = true
this.$message.info('已加载您之前提交的认证信息,请修改后重新提交')
}
} catch (e) {
this.$message.error('加载企业信息失败')
}
},
handleIndustryChange(val) {
if (val && val.length > 0) {
this.form.industry = val[0]
this.form.industrySub = val[1] || ''
}
},
handleProvinceChange(val) {
const found = this.provinceOptions.find(p => p.label === val)
this.cityOptions = found ? found.cities : []
this.form.city = ''
this.form.province = val || ''
},
async submitCertify() {
this.$refs.certifyForm.validate(async (valid) => {
if (!valid) return
this.submitting = true
try {
const payload = { ...this.form, token: this.token }
let res
// 重新编辑模式调用 recertifyEnterprise
if (this.$route.query.reEdit === '1') {
res = await userApi.recertifyEnterprise(payload)
} else {
res = await userApi.certifyEnterprise(payload)
}
if (res.code === 1) {
this.$message.success('企业认证资料提交成功,请等待管理员审核')
// Both first-time and re-edit: redirect to login, backend will show audit-pending panel
this.$router.replace('/login')
} else {
this.$message.error(res.msg || '认证失败')
}
} finally {
this.submitting = false
}
})
}
}
}
</script>
<style lang="scss" scoped>
.certify-container {
min-height: 100vh;
background: linear-gradient(135deg, #e8f0fe 0%, #f5f7fa 100%);
position: relative;
}
.certify-main {
max-width: 960px;
margin: 32px auto;
padding: 0 16px 40px;
position: relative;
z-index: 1;
}
.certify-card {
background: #fff;
border-radius: 12px;
padding: 40px 48px;
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
position: relative;
z-index: 10;
}
.certify-title {
text-align: center;
font-size: 22px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
.certify-form {
margin-top: 24px;
.form-section { margin-bottom: 32px; }
.section-header { display: flex; align-items: center; margin-bottom: 20px; }
.section-dot {
width: 8px; height: 8px;
background: #409eff; border-radius: 50%;
margin-right: 8px;
}
.section-title { font-size: 16px; font-weight: 600; color: #303133; }
}
.upload-area {
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
.upload-tip { font-size: 12px; color: #909399; align-self: flex-end; margin-bottom: 4px; }
}
.single-upload { display: inline-block; }
.single-upload-box {
border: 1px dashed #d9d9d9; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
cursor: pointer; background: #fafafa;
transition: border-color 0.2s;
overflow: hidden;
position: relative;
&:hover { border-color: #409eff; }
.upload-placeholder {
display: flex; flex-direction: column; align-items: center; justify-content: center;
i { font-size: 20px; color: #909399; margin-bottom: 4px; }
span { font-size: 12px; color: #909399; }
}
&.has-image {
border-style: solid;
.upload-placeholder { display: none; }
.uploaded-img {
width: 100%; height: 100%; display: block;
}
&:hover::after {
content: '点击更换';
position: absolute; bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,0.5); color: #fff;
text-align: center; font-size: 12px; padding: 4px 0;
}
}
&.logo { width: 100px; height: 100px; }
&.id-card { width: 160px; height: 100px; }
}
.form-actions {
display: flex; justify-content: center; gap: 16px;
margin-top: 40px; padding-top: 24px; border-top: 1px solid #ebeef5;
.el-button { min-width: 140px; }
}
.agreement-section {
margin-top: 24px;
::v-deep .el-checkbox__label {
font-size: 14px;
}
.agreement-link {
color: #409eff;
cursor: pointer;
text-decoration: underline;
&:hover { color: #66b1ff; }
}
}
.preview-dialog {
::v-deep .el-dialog__body { padding: 12px; text-align: center; }
}
::v-deep .el-form-item__label {
font-weight: 500;
color: #606266;
}
</style>
-139
View File
@@ -1,139 +0,0 @@
<template>
<div class="certify-agreement-container">
<div class="agreement-card">
<a class="back-link" @click="goBack">
<i class="el-icon-arrow-left"></i> 返回企业认证
</a>
<div class="agreement-content">
<h2>账权云企业认证协议</h2>
<p class="update-time">更新日期2026年5月28日 &nbsp;|&nbsp; 生效日期2026年5月28日</p>
<p>欢迎您使用账权云企业认证服务本协议是您与账权云运营方北京账权科技有限公司以下简称"平台""我们"之间就企业认证相关事宜所订立的协议具有合同效力请您在提交企业认证申请前仔细阅读本协议全部内容<strong>您提交企业认证申请即表示您已阅读理解并同意接受本协议的全部条款</strong></p>
<h3>认证信息收集</h3>
<p>1.1 为完成企业认证我们需要收集并存储您提交的以下信息企业全称企业简称主体类型营业执照号码企业LOGO法人姓名及身份证信息管理员姓名及身份证信息行业分类区域信息营业执照影像身份证件影像及其他相关资质材料</p>
<p>1.2 您承诺所提交的全部信息均真实准确完整合法有效不存在任何虚假伪造或误导性内容</p>
<p>1.3 您提交的上述企业信息及相关资质材料<strong>仅供平台核实企业身份及资质真实性之目的使用</strong>平台将采取合理的安全技术措施和管理制度对这些信息进行妥善存储和保护</p>
<h3>信息安全与隐私保护</h3>
<p>2.1 平台承诺对于您提交的认证信息除以下情形外<strong>不会以任何方式向任何第三方泄露出售或非法提供</strong></p>
<p> 1根据法律法规的要求或有权机关的合法要求</p>
<p> 2经您本人/本企业明确同意</p>
<p> 3为履行本协议项下服务目的在必要范围内向平台关联公司或合作方提供平台将要求相关方承担同等保密义务</p>
<p>2.2 平台将按照国家信息安全等级保护相关要求采取加密传输加密存储访问控制等安全技术措施保障您提交的认证信息安全</p>
<p>2.3 如您发现认证信息可能被泄露请立即通过本平台提供的联系方式通知我们我们将及时采取应对措施</p>
<h3>认证信息的用途</h3>
<p>3.1 您提交的认证信息将用于以下用途</p>
<p> 1核实企业主体身份及资质的真实性</p>
<p> 2在平台内展示您的企业名称及认证标识</p>
<p> 3为企业提供与身份相关的功能和服务</p>
<p> 4配合监管部门依法进行的检查或调查</p>
<p>3.2 未经您同意平台不会将您的认证信息用于本协议约定之外的其他用途</p>
<h3>用户承诺与保证</h3>
<p>4.1 您承诺提交的营业执照身份证件等资质材料合法有效且您有权提交该等材料</p>
<p>4.2 您承诺提交的企业信息与实际工商信息一致如有变更应及时通知平台更新</p>
<p>4.3 您理解并同意平台有权对您提交的认证信息进行核实包括但不限于通过第三方数据源比对人工审核等方式</p>
<p>4.4 如因您提交的信息虚假违法或侵犯第三方合法权益而导致的一切法律责任由您自行承担平台因此遭受损失的您应承担赔偿责任</p>
<h3>认证结果</h3>
<p>5.1 平台收到您的认证申请后将进行审核审核通过后您的企业将获得平台认证标识</p>
<p>5.2 如审核不通过平台将通知您并说明原因您可修改后重新提交</p>
<p>5.3 平台有权根据实际情况调整认证审核标准和流程</p>
<h3>认证信息变更与注销</h3>
<p>6.1 如您的企业信息如企业名称法人营业执照号等发生变更请及时联系平台更新认证信息</p>
<p>6.2 如您希望注销企业认证请联系平台客服我们将在核实身份后协助处理</p>
<p>6.3 认证注销后平台将按照法律法规及隐私政策的要求处理您的认证信息</p>
<h3>免责声明</h3>
<p>7.1 对于因不可抗力包括但不限于自然灾害战争政府行为网络故障等导致的信息泄露或服务中断平台在法律允许的范围内不承担责任</p>
<p>7.2 因您自身原因如保管不善提交虚假信息等导致的损失平台不承担责任</p>
<h3>协议变更</h3>
<p>8.1 平台有权根据业务发展法律法规变化等情况对本协议进行修订修订后的协议将在平台公布后生效</p>
<p>8.2 如修订内容涉及重大变更平台将通过适当方式通知您</p>
<h3>法律适用与争议解决</h3>
<p>9.1 本协议的订立执行和解释适用中华人民共和国法律</p>
<p>9.2 因本协议引起的或与本协议有关的任何争议双方应首先通过友好协商解决协商不成的任何一方均可向平台运营方所在地有管辖权的人民法院提起诉讼</p>
<p class="end-note"> 以上为账权云企业认证协议全部内容 </p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CertifyAgreement',
methods: {
goBack() {
const token = this.$route.query.token || ''
this.$router.push({ path: '/certify', query: { token } })
}
}
}
</script>
<style scoped lang="scss">
.certify-agreement-container {
min-height: 100vh;
background: #f5f7fa;
padding: 24px 16px;
.back-link {
display: inline-block;
font-size: 14px;
color: #409eff;
cursor: pointer;
margin-bottom: 16px;
text-decoration: none;
&:hover { color: #66b1ff; }
}
.agreement-card {
max-width: 860px;
margin: 0 auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
padding: 40px 48px;
.agreement-content {
h2 {
text-align: center;
font-size: 20px;
color: #303133;
margin: 0 0 8px;
}
.update-time {
text-align: center;
font-size: 13px;
color: #909399;
margin: 0 0 24px;
}
h3 {
font-size: 16px;
color: #303133;
margin: 24px 0 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
p {
font-size: 14px;
line-height: 1.8;
color: #606266;
margin: 8px 0;
strong {
color: #303133;
}
}
.end-note {
text-align: center;
color: #909399;
font-style: italic;
margin-top: 32px;
}
}
}
}
</style>
+3 -8
View File
@@ -485,7 +485,6 @@ import msgUtil from '@/utils/msgUtil'
import PageBox from '@/components/pageBox.vue'
import AiHelper from '@/components/ai/ai_helper.vue'
import userApi from '@/api/user'
import huobanTableApi from '@/api/huobanTableApi'
import llmAgentApi from '@/api/llmAgentApi'
export default {
@@ -1613,13 +1612,9 @@ export default {
updateFields: ['saasId'],
action: 'one_update'
}
const res = await huobanTableApi.upsetOne(payload)
if (res && res.code === 1) {
item.status = 'success'
} else {
item.status = 'fail'
item.reason = res.msg || '未知'
}
// huobanTableApi removed in apes-Authon-Web
item.status = 'skip'
item.reason = 'huoban sync not available'
} catch (e) {
item.status = 'fail'
item.reason = e.message || '异常'
+4 -22
View File
@@ -63,7 +63,6 @@
</template>
<script>
import weightApi from '@/api/weightApi'
import msgUtil from '@/utils/msgUtil'
import PageBox from '@/components/pageBox.vue'
@@ -91,24 +90,12 @@ export default {
},
methods: {
async loadConfig() {
let res = await weightApi.getUiConfig()
if (res.code === 1 && res.data) {
Object.keys(this.form).forEach(key => {
if (res.data[key] !== null && res.data[key] !== undefined) {
this.form[key] = res.data[key]
}
})
}
// weightApi removed in apes-Authon-Web
},
async handleSave() {
this.saving = true
try {
let res = await weightApi.saveUiConfig(this.form)
if (res.code === 1) {
msgUtil.notifySuccess('保存成功')
} else {
msgUtil.notifyError('保存失败', res.msg)
}
msgUtil.notifySuccess('保存成功')
} finally {
this.saving = false
}
@@ -119,13 +106,8 @@ export default {
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
let res = await weightApi.resetUiConfig()
if (res.code === 1) {
msgUtil.notifySuccess('已重置为默认配置')
this.loadConfig()
} else {
msgUtil.notifyError('重置失败', res.msg)
}
msgUtil.notifySuccess('已重置为默认配置')
this.loadConfig()
}).catch(() => {})
},
handleLogoUpload(res) {
-385
View File
@@ -1,385 +0,0 @@
<template>
<page-box title="外链管理">
<template slot="header-actions">
<el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAdd">新增</el-button>
</template>
<!-- 搜索表单 -->
<el-form :inline="true" :model="searchForm" label-width="80px" size="mini">
<el-form-item label="链接名称">
<el-input v-model="searchForm.name" placeholder="请输入链接名称" clearable />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="searchForm.type" placeholder="请选择类型" clearable>
<el-option label="外站" :value="1" />
<el-option label="本站" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table
v-loading="loading"
:data="tableData"
border
stripe
size="mini"
style="width: 100%">
<el-table-column prop="name" label="链接名称" min-width="120" />
<el-table-column prop="type" label="类型" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.type === 1 ? '' : 'success'" size="mini">
{{ scope.row.type === 1 ? '外站' : '本站' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="url" label="链接地址" min-width="200" show-overflow-tooltip />
<el-table-column prop="thumbnail" label="缩略图" width="80" align="center">
<template slot-scope="scope">
<el-image
v-if="scope.row.thumbnail"
:src="scope.row.thumbnail"
:preview-src-list="[scope.row.thumbnail]"
style="width: 40px; height: 40px; border-radius: 4px;"
fit="cover" />
<span v-else style="color: #c0c4cc; font-size: 12px;"></span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="mini">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sortOrder" label="排序" width="70" align="center" />
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" width="150" />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-edit" size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" icon="el-icon-delete" size="mini" style="color: #F56C6C" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@current-change="handlePageChange"
@size-change="handleSizeChange"
:current-page="pagination.pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
style="margin-top: 20px; text-align: right" />
<!-- 新增/编辑抽屉 -->
<el-drawer
:title="drawerTitle"
:visible.sync="drawerVisible"
size="600px"
direction="rtl">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" style="padding: 20px">
<!-- AI Helper -->
<div style="margin-bottom: 20px; border-bottom: 1px solid #ebeef5; padding-bottom: 20px;">
<ai-helper
agent_id="4"
display="input"
input_title="AI 外链智能生成助手"
default_input="请帮我填写这个外链的信息"
:user_prompt="aiPrompt"
@result="handleAiResult"
/>
</div>
<el-form-item label="链接名称" prop="name">
<el-input v-model="form.name" placeholder="请输入链接名称" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio :label="1">外站</el-radio>
<el-radio :label="2">本站</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="链接地址" prop="url">
<el-input v-model="form.url" placeholder="请输入链接地址" />
</el-form-item>
<el-form-item label="缩略图" prop="thumbnail">
<el-upload
class="thumbnail-uploader"
action="/api/user-service/common/upload"
:headers="uploadHeaders"
:data="{ space: 'ext_link' }"
name="file"
:show-file-list="false"
:on-success="handleUploadSuccess"
:before-upload="beforeUpload"
accept="image/*">
<img v-if="form.thumbnail" :src="form.thumbnail" class="thumbnail-preview" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<div v-if="form.thumbnail" style="margin-top: 8px;">
<el-button type="text" size="mini" icon="el-icon-delete" @click="form.thumbnail = ''">移除图片</el-button>
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-form>
<div style="text-align: center; padding: 20px">
<el-button @click="drawerVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</div>
</el-drawer>
</page-box>
</template>
<script>
import PageBox from '@/components/pageBox.vue'
import AiHelper from '@/components/ai/ai_helper.vue'
import extLinkApi from '@/api/extLink'
export default {
name: 'ExtLinkManage',
components: { PageBox, AiHelper },
data() {
return {
loading: false,
uploadHeaders: {
token: localStorage.getItem('_token')
},
searchForm: {
name: '',
type: '',
status: ''
},
tableData: [],
pagination: {
pageIndex: 1,
pageSize: 10,
total: 0
},
drawerVisible: false,
drawerTitle: '新增',
submitLoading: false,
form: {
name: '',
type: 1,
url: '',
thumbnail: '',
status: 1,
sortOrder: 0,
remark: ''
},
rules: {
name: [{ required: true, message: '请输入链接名称', trigger: 'blur' }],
url: [{ required: true, message: '请输入链接地址', trigger: 'blur' }]
}
}
},
computed: {
aiPrompt() {
const base = `请根据用户描述填充外链表单,返回 JSON 格式:
{
"name": "链接名称",
"type": 1或21-外站 2-本站),
"url": "链接地址",
"thumbnail": "缩略图地址",
"status": 1或01-启用 0-禁用),
"sortOrder": 排序数字,
"remark": "备注"
}
只返回 JSON,不要其他内容。`
if (this.form.id) {
const current = {
name: this.form.name,
type: this.form.type,
url: this.form.url,
thumbnail: this.form.thumbnail,
status: this.form.status,
sortOrder: this.form.sortOrder,
remark: this.form.remark
}
return `以下是当前外链的信息:\n${JSON.stringify(current, null, 2)}\n\n请基于以上信息进行修改调整,${base}`
}
return base
}
},
created() {
this.loadData()
},
methods: {
async loadData() {
this.loading = true
try {
const res = await extLinkApi.getPage({
...this.searchForm,
pageIndex: this.pagination.pageIndex,
pageSize: this.pagination.pageSize
})
if (res.code === 1) {
this.tableData = res.data.records || []
this.pagination.total = res.data.total || 0
}
} finally {
this.loading = false
}
},
handleSearch() {
this.pagination.pageIndex = 1
this.loadData()
},
handleReset() {
this.searchForm = { name: '', type: '', status: '' }
this.handleSearch()
},
handlePageChange(page) {
this.pagination.pageIndex = page
this.loadData()
},
handleSizeChange(size) {
this.pagination.pageSize = size
this.pagination.pageIndex = 1
this.loadData()
},
handleAdd() {
this.drawerTitle = '新增外链'
this.form = { name: '', type: 1, url: '', thumbnail: '', status: 1, sortOrder: 0, remark: '' }
this.drawerVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
handleEdit(row) {
this.drawerTitle = '编辑外链'
this.form = { ...row }
this.drawerVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
async handleDelete(row) {
this.$confirm(`确定删除外链「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
const res = await extLinkApi.delete(row.id)
if (res.code === 1) {
this.$message.success('删除成功')
this.loadData()
} else {
this.$message.error(res.msg || '删除失败')
}
}).catch(() => {})
},
handleSubmit() {
this.$refs.formRef.validate(async (valid) => {
if (!valid) return
this.submitLoading = true
try {
const api = this.form.id ? extLinkApi.update : extLinkApi.add
const res = await api(this.form)
if (res.code === 1) {
this.$message.success('保存成功')
this.drawerVisible = false
this.loadData()
} else {
this.$message.error(res.msg || '保存失败')
}
} finally {
this.submitLoading = false
}
})
},
handleAiResult(data) {
if (data.contentObject) {
const obj = data.contentObject
const fieldMap = {
name: 'name',
type: 'type',
url: 'url',
thumbnail: 'thumbnail',
status: 'status',
sortOrder: 'sortOrder',
remark: 'remark'
}
let filledCount = 0
Object.keys(obj).forEach(key => {
const formKey = fieldMap[key]
if (formKey && this.form.hasOwnProperty(formKey)) {
this.$set(this.form, formKey, obj[key])
filledCount++
}
})
if (filledCount > 0) {
this.$message.success(`已填充 ${filledCount} 个字段`)
}
}
},
beforeUpload(file) {
const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5
if (!isImage) {
this.$message.error('只能上传图片文件!')
}
if (!isLt5M) {
this.$message.error('图片大小不能超过 5MB')
}
return isImage && isLt5M
},
handleUploadSuccess(response) {
if (response.code === 1) {
this.form.thumbnail = response.data
this.$message.success('图片上传成功')
} else {
this.$message.error(response.msg || '图片上传失败')
}
}
}
}
</script>
<style scoped>
.thumbnail-uploader >>> .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 148px;
height: 148px;
display: flex;
align-items: center;
justify-content: center;
}
.thumbnail-uploader >>> .el-upload:hover {
border-color: #409EFF;
}
.thumbnail-preview {
width: 148px;
height: 148px;
object-fit: cover;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
}
</style>
-438
View File
@@ -1,438 +0,0 @@
<template>
<page-box :title="isEdit ? '编辑作物' : '新增作物'">
<div class="crop-form-page">
<el-form :model="form" :rules="rules" ref="cropForm" label-width="100px" size="mini">
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">基本信息</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="作物名称" prop="name">
<el-input v-model="form.name" placeholder="请输入作物名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="作物编码" prop="code">
<el-input v-model="form.code" placeholder="请输入作物编码" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="作物分类" prop="categoryId">
<el-select v-model="form.categoryId" placeholder="请选择分类" style="width: 100%;">
<el-option label="蔬菜" :value="1" />
<el-option label="粮食" :value="2" />
<el-option label="水果" :value="3" />
<el-option label="药材" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="别名">
<el-input v-model="form.alias" placeholder="多个别名用逗号分隔" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排序">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="作物图片">
<el-upload
class="avatar-uploader"
action="/api/user-service/common/upload"
:show-file-list="false"
:on-success="handleImageSuccess"
:before-upload="beforeImageUpload">
<img v-if="form.image" :src="form.image" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="图片集">
<el-upload
action="/api/user-service/common/upload"
list-type="picture-card"
:file-list="imageList"
:on-success="handleImageListSuccess"
:on-remove="handleImageListRemove"
:before-upload="beforeImageUpload">
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 生长特性 -->
<div class="form-section">
<div class="section-title">生长特性</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="生长周期">
<el-input v-model="form.growthCycle" placeholder="天数">
<template slot="append"></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="种植季节">
<el-input v-model="form.plantingSeason" placeholder="如:春季、夏季" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="适宜地区">
<el-input v-model="form.suitableRegions" placeholder="如:华北、华东" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="温度范围">
<div class="range-input">
<el-input v-model="form.temperatureMin" placeholder="最低" />
<span class="range-sep">~</span>
<el-input v-model="form.temperatureMax" placeholder="最高" />
<span class="range-unit"></span>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="湿度范围">
<div class="range-input">
<el-input v-model="form.humidityMin" placeholder="最低" />
<span class="range-sep">~</span>
<el-input v-model="form.humidityMax" placeholder="最高" />
<span class="range-unit">%</span>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="光照需求">
<el-input v-model="form.lightRequirement" placeholder="如:充足阳光" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="土壤要求">
<el-input v-model="form.soilRequirement" placeholder="如:疏松肥沃的沙壤土" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="水分需求">
<el-input v-model="form.waterRequirement" placeholder="如:中等需水量" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 种植参数 -->
<div class="form-section">
<div class="section-title">种植参数</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="种植密度">
<el-input v-model="form.plantingDensity" placeholder="如:3000-4000">
<template slot="append">/</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="用种量">
<el-input v-model="form.seedUsage" placeholder="如:2.5">
<template slot="append">kg/</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="预期产量">
<div class="range-input">
<el-input v-model="form.expectedYieldMin" placeholder="最低" />
<span class="range-sep">~</span>
<el-input v-model="form.expectedYieldMax" placeholder="最高" />
<span class="range-unit">kg/</span>
</div>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 详细描述 -->
<div class="form-section">
<div class="section-title">详细描述</div>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="作物描述">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入作物描述" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="种植指南">
<el-input v-model="form.plantingGuide" type="textarea" :rows="3" placeholder="请输入种植指南" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="注意事项">
<el-input v-model="form.precautions" type="textarea" :rows="3" placeholder="请输入注意事项" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 操作按钮 -->
<div class="form-footer">
<el-button type="primary" @click="handleSubmit" size="mini"> </el-button>
<el-button @click="handleCancel" size="mini"> </el-button>
</div>
</el-form>
<!-- AI Helper 集成 -->
<ai-helper
agent_id="4"
input_title="AI智能完善填写作物信息"
default_input="请帮我填写甘蔗 蔗糖11号的作物信息,按照客观事实填写。"
:user_prompt="aiPrompt"
@result="handleAiResult"
/>
</div>
</page-box>
</template>
<script>
import PageBox from '@/components/pageBox.vue'
import AiHelper from '@/components/ai/ai_helper.vue'
import cropApi from '@/api/fei/crop'
export default {
name: 'CropAdd',
components: { PageBox, AiHelper },
data() {
return {
isEdit: false,
imageList: [],
form: {
name: '',
code: '',
categoryId: null,
alias: '',
image: '',
images: '',
growthCycle: null,
plantingSeason: '',
suitableRegions: '',
temperatureMin: null,
temperatureMax: null,
humidityMin: null,
humidityMax: null,
lightRequirement: '',
soilRequirement: '',
waterRequirement: '',
plantingDensity: '',
seedUsage: null,
expectedYieldMin: null,
expectedYieldMax: null,
description: '',
plantingGuide: '',
precautions: '',
sortOrder: 0,
status: 1,
remark: ''
},
rules: {
name: [
{ required: true, message: '请输入作物名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
code: [{ required: true, message: '请输入作物编码', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择作物分类', trigger: 'change' }]
}
}
},
computed: {
aiPrompt() {
return `根据用户描述填充作物信息表单,返回 JSON 格式:
{
"name": "作物名称",
"code": "作物编码",
"categoryId": 作物分类ID(数字类型: 1-蔬菜, 2-粮食, 3-水果, 4-药材),
"alias": "别名",
"growthCycle": 生长周期(数字,单位天),
"plantingSeason": "种植季节",
"suitableRegions": "适宜地区",
"temperatureMin": 最低温度(数字),
"temperatureMax": 最高温度(数字),
"humidityMin": 最低湿度(数字),
"humidityMax": 最高湿度(数字),
"lightRequirement": "光照需求",
"soilRequirement": "土壤要求",
"waterRequirement": "水分需求",
"plantingDensity": "种植密度",
"seedUsage": 用种量(数字),
"expectedYieldMin": 预期最低产量(数字),
"expectedYieldMax": 预期最高产量(数字),
"description": "作物描述",
"plantingGuide": "种植指南",
"precautions": "注意事项"
}
只返回纯 JSON,不要包含 markdown 代码块标记,不要其他内容。`
}
},
created() {
const id = this.$route.params.id
if (id) {
this.isEdit = true
this.loadDetail(id)
}
},
methods: {
async loadDetail(id) {
try {
const res = await cropApi.detail(id)
if (res.code === 1) {
this.form = { ...res.data }
// 解析图片集(逗号分隔字符串)
if (this.form.images) {
const urls = this.form.images.split(',').filter(url => url.trim())
this.imageList = urls.map(url => ({ name: url, url }))
}
}
} catch (error) {
this.$message.error('加载详情失败')
}
},
handleSubmit() {
this.$refs.cropForm.validate(async (valid) => {
if (!valid) return
// 将图片集数组转为逗号分隔字符串
const submitData = { ...this.form }
submitData.images = this.imageList.map(f => f.response?.data || f.url).filter(u => u).join(',')
try {
let res
if (this.isEdit) {
res = await cropApi.update(submitData)
} else {
res = await cropApi.add(submitData)
}
if (res.code === 1) {
this.$message.success(this.isEdit ? '修改成功' : '新增成功')
this.$router.push('/tracing/fei/crop')
} else {
this.$message.error(res.msg || (this.isEdit ? '修改失败' : '新增失败'))
}
} catch (error) {
this.$message.error(this.isEdit ? '修改失败' : '新增失败')
}
})
},
handleCancel() {
this.$router.push('/tracing/fei/crop')
},
handleImageSuccess(res) {
if (res.code === 1) {
this.form.image = res.data
} else {
this.$message.error('图片上传失败')
}
},
handleImageListSuccess(res, file, fileList) {
if (res.code === 1) {
// 上传成功,设置文件URL用于预览显示
this.$set(file, 'url', res.data)
// 强制触发响应式更新
this.imageList = [...fileList]
// 同步更新 form.images
this.form.images = this.imageList.map(f => f.url).join(',')
} else {
this.$message.error('图片上传失败')
}
},
handleImageListRemove(file, fileList) {
// 强制触发响应式更新
this.imageList = [...fileList]
// 同步更新 form.images,从 response.data 或 url 中获取
const urls = fileList.map(f => f.response?.data || f.url).filter(u => u)
this.form.images = urls.join(',')
},
beforeImageUpload(file) {
const isImage = file.type.indexOf('image/') === 0
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) this.$message.error('只能是图片格式')
if (!isLt2M) this.$message.error('不能超过 2MB')
return isImage && isLt2M
},
handleAiResult(data) {
if (data.contentObject) {
// 回填表单
Object.keys(data.contentObject).forEach(key => {
if (this.form.hasOwnProperty(key) && data.contentObject[key] !== null && data.contentObject[key] !== '') {
let value = data.contentObject[key]
// 特殊字段类型转换
if (key === 'categoryId') {
value = parseInt(value)
} else if (['growthCycle', 'temperatureMin', 'temperatureMax', 'humidityMin', 'humidityMax', 'seedUsage', 'expectedYieldMin', 'expectedYieldMax', 'sortOrder', 'status'].includes(key)) {
value = parseFloat(value)
}
this.$set(this.form, key, value)
}
})
this.$message.success('AI 已填充表单')
}
}
}
}
</script>
<style scoped>
.crop-form-page {
padding: 15px;
}
.form-section {
margin-bottom: 20px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 15px;
padding-left: 10px;
border-left: 3px solid #409EFF;
}
.range-input {
display: flex;
align-items: center;
gap: 6px;
}
.range-input .el-input { flex: 1; }
.range-sep { color: #909399; }
.range-unit { color: #909399; font-size: 12px; white-space: nowrap; }
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
overflow: hidden;
}
.avatar-uploader .el-upload:hover { border-color: #409EFF; }
.avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 120px; height: 120px; line-height: 120px; text-align: center; }
.avatar { width: 120px; height: 120px; display: block; }
.form-footer {
text-align: center;
padding: 10px 0 20px;
}
</style>
-383
View File
@@ -1,383 +0,0 @@
<template>
<page-box title="作物管理">
<template slot="header-actions">
<el-button type="success" icon="el-icon-plus" @click="handleAdd" size="mini">新增作物</el-button>
</template>
<div class="crop-management">
<!-- 搜索和操作区域 -->
<el-form :inline="true" :model="searchForm">
<el-form-item label="作物名称">
<el-input v-model="searchForm.name" placeholder="请输入作物名称" clearable @clear="handleSearch" />
</el-form-item>
<el-form-item label="分类">
<el-select v-model="searchForm.categoryId" placeholder="选择分类" clearable @change="handleSearch">
<el-option label="蔬菜" :value="1" />
<el-option label="粮食" :value="2" />
<el-option label="水果" :value="3" />
<el-option label="药材" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="选择状态" clearable @change="handleSearch">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch" size="mini">搜索</el-button>
<el-button icon="el-icon-refresh" @click="handleReset" size="mini">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="作物名称" width="140">
<template slot-scope="scope">
<span>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="code" label="编码" width="100" />
<el-table-column label="分类" width="100">
<template slot-scope="scope">
<el-tag :type="getCategoryType(scope.row.categoryId)" size="mini">
{{ getCategoryName(scope.row.categoryId) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="growthCycle" label="生长周期" width="100">
<template slot-scope="scope">
<span v-if="scope.row.growthCycle">{{ scope.row.growthCycle }}</span>
<span v-else style="color: #999;">-</span>
</template>
</el-table-column>
<el-table-column prop="plantingSeason" label="种植季节" width="250" />
<el-table-column prop="suitableRegions" label="适宜地区" min-width="150" show-overflow-tooltip />
<el-table-column label="温度范围" width="120">
<template slot-scope="scope">
<span v-if="scope.row.temperatureMin && scope.row.temperatureMax">
{{ scope.row.temperatureMin }}~{{ scope.row.temperatureMax }}
</span>
<span v-else style="color: #999;">-</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="mini">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">详情</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" style="color: #F56C6C;">删除</el-button>
<el-button size="mini" type="text" icon="el-icon-date" @click="handlePhenology(scope.row)" style="color: #409EFF;">物候期</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="block" style="margin-top: 20px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
background
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
/>
</div>
</div>
<!-- 查看详情抽屉 -->
<el-drawer title="作物详情" :visible.sync="viewDialogVisible" direction="rtl" size="500px">
<div class="drawer-content">
<el-descriptions :column="1" border size="mini">
<el-descriptions-item label="作物名称">{{ viewData.name }}</el-descriptions-item>
<el-descriptions-item label="作物编码">{{ viewData.code }}</el-descriptions-item>
<el-descriptions-item label="作物分类">{{ getCategoryName(viewData.categoryId) }}</el-descriptions-item>
<el-descriptions-item label="别名">{{ viewData.alias || '-' }}</el-descriptions-item>
<el-descriptions-item label="生长周期">{{ viewData.growthCycle ? viewData.growthCycle + ' 天' : '-' }}</el-descriptions-item>
<el-descriptions-item label="种植季节">{{ viewData.plantingSeason || '-' }}</el-descriptions-item>
<el-descriptions-item label="适宜地区">{{ viewData.suitableRegions || '-' }}</el-descriptions-item>
<el-descriptions-item label="温度范围">{{ viewData.temperatureMin && viewData.temperatureMax ? viewData.temperatureMin + ' ~ ' + viewData.temperatureMax + ' ℃' : '-' }}</el-descriptions-item>
<el-descriptions-item label="湿度范围">{{ viewData.humidityMin && viewData.humidityMax ? viewData.humidityMin + ' ~ ' + viewData.humidityMax + ' %' : '-' }}</el-descriptions-item>
<el-descriptions-item label="光照需求">{{ viewData.lightRequirement || '-' }}</el-descriptions-item>
<el-descriptions-item label="土壤要求">{{ viewData.soilRequirement || '-' }}</el-descriptions-item>
<el-descriptions-item label="水分需求">{{ viewData.waterRequirement || '-' }}</el-descriptions-item>
<el-descriptions-item label="种植密度">{{ viewData.plantingDensity || '-' }}</el-descriptions-item>
<el-descriptions-item label="用种量">{{ viewData.seedUsage ? viewData.seedUsage + ' kg/亩' : '-' }}</el-descriptions-item>
<el-descriptions-item label="预期产量">{{ viewData.expectedYieldMin && viewData.expectedYieldMax ? viewData.expectedYieldMin + ' ~ ' + viewData.expectedYieldMax + ' kg/亩' : '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewData.status === 1 ? 'success' : 'danger'" size="mini">
{{ viewData.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="作物描述">{{ viewData.description || '-' }}</el-descriptions-item>
<el-descriptions-item label="种植指南">{{ viewData.plantingGuide || '-' }}</el-descriptions-item>
<el-descriptions-item label="注意事项">{{ viewData.precautions || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ viewData.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<div v-if="viewData.images" style="margin-top: 15px;">
<div style="font-weight: bold; margin-bottom: 10px;">图片集</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<img v-for="(url, index) in viewData.images.split(',')" :key="index" :src="url" style="width: 80px; height: 80px; object-fit: cover; border-radius: 4px;">
</div>
</div>
</div>
</el-drawer>
<!-- 物候期抽屉 -->
<el-drawer :title="phenologyCropName + ' - 物候期'" :visible.sync="phenologyDrawerVisible" direction="rtl" size="600px">
<div class="phenology-drawer-content" v-loading="phenologyLoading">
<div v-if="phenologyList.length === 0 && !phenologyLoading" style="text-align: center; padding: 40px; color: #999;">
暂无物候期数据
</div>
<div v-else class="phenology-timeline">
<div v-for="(item, index) in phenologyList" :key="item.id" class="phenology-card">
<div class="phenology-card-header">
<span class="phenology-sequence">{{ index + 1 }}</span>
<span class="phenology-name">{{ item.phenologyName }}</span>
<el-tag size="mini" type="info">{{ item.startDay }}~{{ item.endDay }}</el-tag>
</div>
<div class="phenology-card-body">
<div v-if="item.characteristics" class="phenology-info">
<span class="info-label">特征</span>{{ item.characteristics }}
</div>
<div v-if="item.temperatureMin || item.temperatureMax" class="phenology-info">
<span class="info-label">温度</span>{{ item.temperatureMin || '-' }} ~ {{ item.temperatureMax || '-' }}
</div>
<div v-if="item.humidityMin || item.humidityMax" class="phenology-info">
<span class="info-label">湿度</span>{{ item.humidityMin || '-' }} ~ {{ item.humidityMax || '-' }} %
</div>
<div v-if="item.waterRequirement" class="phenology-info">
<span class="info-label">水分</span>{{ item.waterRequirement }}
</div>
<div v-if="item.farmingActivities" class="phenology-info">
<span class="info-label">农事</span>{{ item.farmingActivities }}
</div>
</div>
<div class="phenology-card-footer">
<span>持续 <b>{{ item.durationDays || '-' }}</b> </span>
</div>
</div>
</div>
</div>
</el-drawer>
</page-box>
</template>
<script>
import PageBox from '@/components/pageBox.vue'
import cropApi from '@/api/fei/crop'
import cropPhenologyApi from '@/api/fei/cropPhenology'
export default {
name: 'CropManagement',
components: {
PageBox
},
data() {
return {
searchForm: {
name: '',
categoryId: null,
status: null
},
tableData: [],
loading: false,
pagination: {
pageNum: 1,
pageSize: 10,
total: 0
},
viewDialogVisible: false,
viewData: {},
phenologyDrawerVisible: false,
phenologyCropName: '',
phenologyList: [],
phenologyLoading: false
}
},
created() {
this.loadData()
},
watch: {
'$route'() {
this.loadData()
}
},
methods: {
async loadData() {
this.loading = true
try {
const res = await cropApi.page(
this.pagination.pageNum,
this.pagination.pageSize,
this.searchForm.name,
this.searchForm.categoryId,
this.searchForm.status
)
if (res.code === 1) {
this.tableData = res.data.records
this.pagination.total = res.data.total
}
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.loading = false
}
},
handleSearch() {
this.pagination.pageNum = 1
this.loadData()
},
handleReset() {
this.searchForm = {
name: '',
categoryId: null,
status: null
}
this.handleSearch()
},
handleSizeChange(val) {
this.pagination.pageSize = val
this.loadData()
},
handleCurrentChange(val) {
this.pagination.pageNum = val
this.loadData()
},
handleAdd() {
this.$router.push('/tracing/fei/crop/add')
},
handleEdit(row) {
this.$router.push(`/tracing/fei/crop/edit/${row.id}`)
},
handleView(row) {
this.viewData = { ...row }
this.viewDialogVisible = true
},
handleDelete(row) {
this.$confirm('确认删除该作物信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await cropApi.delete(row.id)
if (res.code === 1) {
this.$message.success('删除成功')
this.loadData()
} else {
this.$message.error(res.msg || '删除失败')
}
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {})
},
getCategoryName(categoryId) {
const map = { 1: '蔬菜', 2: '粮食', 3: '水果', 4: '药材' }
return map[categoryId] || '-'
},
getCategoryType(categoryId) {
const map = { 1: 'success', 2: 'warning', 3: 'danger', 4: 'info' }
return map[categoryId] || ''
},
async handlePhenology(row) {
this.phenologyCropName = row.name
this.phenologyDrawerVisible = true
this.phenologyLoading = true
try {
const res = await cropPhenologyApi.listByCropId(row.id)
if (res.code === 1) {
this.phenologyList = res.data || []
}
} catch (error) {
this.$message.error('加载物候期数据失败')
} finally {
this.phenologyLoading = false
}
}
}
}
</script>
<style scoped>
.crop-management {
padding: 15px;
}
.drawer-content {
padding: 0 20px 20px;
}
.phenology-drawer-content {
padding: 20px;
}
.phenology-timeline {
display: flex;
flex-direction: column;
gap: 16px;
}
.phenology-card {
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
}
.phenology-card:hover {
border-color: #409EFF;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
}
.phenology-card-header {
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid #f0f0f0;
}
.phenology-sequence {
width: 26px;
height: 26px;
border: 2px solid #409EFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 13px;
color: #409EFF;
}
.phenology-name {
font-size: 15px;
font-weight: 600;
flex: 1;
color: #303133;
}
.phenology-card-body {
padding: 14px 16px;
}
.phenology-info {
margin-bottom: 6px;
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.phenology-info:last-child {
margin-bottom: 0;
}
.info-label {
color: #909399;
margin-right: 4px;
}
.phenology-card-footer {
padding: 10px 16px;
font-size: 13px;
color: #909399;
border-top: 1px solid #f0f0f0;
}
</style>
-449
View File
@@ -1,449 +0,0 @@
<template>
<page-box title="作物物候期管理">
<template slot="header-actions">
<el-button type="success" icon="el-icon-plus" @click="handleAdd" size="mini">新增物候期</el-button>
</template>
<div class="crop-phenology-management">
<!-- 搜索和操作区域 -->
<el-form :inline="true" :model="searchForm">
<el-form-item label="作物">
<el-select v-model="searchForm.cropId" placeholder="选择作物" clearable @change="handleSearch" filterable>
<el-option v-for="item in cropList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="物候期名称">
<el-input v-model="searchForm.phenologyName" placeholder="请输入物候期名称" clearable @clear="handleSearch" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="选择状态" clearable @change="handleSearch">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch" size="mini">搜索</el-button>
<el-button icon="el-icon-refresh" @click="handleReset" size="mini">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
<el-table-column label="作物名称" width="120" prop="cropName" />
<el-table-column prop="phenologyName" label="物候期名称" width="140" />
<el-table-column prop="phenologyCode" label="编码" width="100" />
<el-table-column prop="sequenceNo" label="序号" width="80" align="center" />
<el-table-column label="开始天数" width="100" align="center">
<template slot-scope="scope">
<span>{{ scope.row.startDay }}</span>
</template>
</el-table-column>
<el-table-column label="结束天数" width="100" align="center">
<template slot-scope="scope">
<span>{{ scope.row.endDay }}</span>
</template>
</el-table-column>
<el-table-column prop="durationDays" label="持续天数" width="100" align="center">
<template slot-scope="scope">
<span>{{ scope.row.durationDays }}</span>
</template>
</el-table-column>
<el-table-column label="温度范围" width="120">
<template slot-scope="scope">
<span v-if="scope.row.temperatureMin && scope.row.temperatureMax">
{{ scope.row.temperatureMin }}~{{ scope.row.temperatureMax }}
</span>
<span v-else style="color: #999;">-</span>
</template>
</el-table-column>
<el-table-column label="湿度范围" width="120">
<template slot-scope="scope">
<span v-if="scope.row.humidityMin && scope.row.humidityMax">
{{ scope.row.humidityMin }}~{{ scope.row.humidityMax }}%
</span>
<span v-else style="color: #999;">-</span>
</template>
</el-table-column>
<el-table-column prop="waterRequirement" label="水分需求" width="120" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="mini">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">详情</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" style="color: #F56C6C;">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="block" style="margin-top: 20px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
background
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
/>
</div>
</div>
<!-- 新增/编辑抽屉 -->
<el-drawer :title="formDrawerTitle" :visible.sync="formDrawerVisible" direction="rtl" size="600px" :before-close="handleFormClose">
<div class="drawer-content">
<el-form :model="form" :rules="formRules" ref="formRef" label-width="100px">
<el-form-item label="所属作物" prop="cropId">
<el-select v-model="form.cropId" placeholder="选择作物" filterable style="width: 100%">
<el-option v-for="item in cropList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="物候期名称" prop="phenologyName">
<el-input v-model="form.phenologyName" placeholder="如:萌芽期、开花期" />
</el-form-item>
<el-form-item label="物候期编码" prop="phenologyCode">
<el-input v-model="form.phenologyCode" placeholder="如:germination" />
</el-form-item>
<el-form-item label="序号" prop="sequenceNo">
<el-input-number v-model="form.sequenceNo" :min="1" :max="999" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始天数" prop="startDay">
<el-input-number v-model="form.startDay" :min="0" :max="999" placeholder="从种植日算起" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束天数" prop="endDay">
<el-input-number v-model="form.endDay" :min="0" :max="999" placeholder="从种植日算起" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="持续天数" prop="durationDays">
<el-input-number v-model="form.durationDays" :min="1" :max="999" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最低温度" prop="temperatureMin">
<el-input-number v-model="form.temperatureMin" :precision="1" :step="0.5" />
<span style="margin-left: 5px; color: #999;"></span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最高温度" prop="temperatureMax">
<el-input-number v-model="form.temperatureMax" :precision="1" :step="0.5" />
<span style="margin-left: 5px; color: #999;"></span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最低湿度" prop="humidityMin">
<el-input-number v-model="form.humidityMin" :min="0" :max="100" />
<span style="margin-left: 5px; color: #999;">%</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最高湿度" prop="humidityMax">
<el-input-number v-model="form.humidityMax" :min="0" :max="100" />
<span style="margin-left: 5px; color: #999;">%</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="水分需求" prop="waterRequirement">
<el-input v-model="form.waterRequirement" placeholder="如:中等、喜湿润" />
</el-form-item>
<el-form-item label="物候特征" prop="characteristics">
<el-input v-model="form.characteristics" type="textarea" :rows="3" placeholder="描述该物候期的特征" />
</el-form-item>
<el-form-item label="农事活动" prop="farmingActivities">
<el-input v-model="form.farmingActivities" type="textarea" :rows="3" placeholder="该时期建议的农事操作" />
</el-form-item>
<el-form-item label="注意事项" prop="precautions">
<el-input v-model="form.precautions" type="textarea" :rows="2" placeholder="需要注意的事项" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div style="text-align: center; margin-top: 20px;">
<el-button @click="formDrawerVisible = false" size="mini">取消</el-button>
<el-button type="primary" @click="handleFormSubmit" size="mini" :loading="formSubmitting">确定</el-button>
</div>
</div>
</el-drawer>
<!-- 查看详情抽屉 -->
<el-drawer title="物候期详情" :visible.sync="viewDialogVisible" direction="rtl" size="500px">
<div class="drawer-content">
<el-descriptions :column="1" border size="mini">
<el-descriptions-item label="作物名称">{{ getCropName(viewData.cropId) }}</el-descriptions-item>
<el-descriptions-item label="物候期名称">{{ viewData.phenologyName }}</el-descriptions-item>
<el-descriptions-item label="物候期编码">{{ viewData.phenologyCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="序号">{{ viewData.sequenceNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始天数">{{ viewData.startDay ? '第' + viewData.startDay + '天' : '-' }}</el-descriptions-item>
<el-descriptions-item label="结束天数">{{ viewData.endDay ? '第' + viewData.endDay + '天' : '-' }}</el-descriptions-item>
<el-descriptions-item label="持续天数">{{ viewData.durationDays ? viewData.durationDays + '天' : '-' }}</el-descriptions-item>
<el-descriptions-item label="温度范围">{{ viewData.temperatureMin && viewData.temperatureMax ? viewData.temperatureMin + ' ~ ' + viewData.temperatureMax + ' ℃' : '-' }}</el-descriptions-item>
<el-descriptions-item label="湿度范围">{{ viewData.humidityMin && viewData.humidityMax ? viewData.humidityMin + ' ~ ' + viewData.humidityMax + ' %' : '-' }}</el-descriptions-item>
<el-descriptions-item label="水分需求">{{ viewData.waterRequirement || '-' }}</el-descriptions-item>
<el-descriptions-item label="物候特征">{{ viewData.characteristics || '-' }}</el-descriptions-item>
<el-descriptions-item label="农事活动">{{ viewData.farmingActivities || '-' }}</el-descriptions-item>
<el-descriptions-item label="注意事项">{{ viewData.precautions || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewData.status === 1 ? 'success' : 'danger'" size="mini">
{{ viewData.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="备注">{{ viewData.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<div v-if="viewData.images" style="margin-top: 15px;">
<div style="font-weight: bold; margin-bottom: 10px;">图片</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<img v-for="(url, index) in viewData.images.split(',')" :key="index" :src="url" style="width: 80px; height: 80px; object-fit: cover; border-radius: 4px;">
</div>
</div>
</div>
</el-drawer>
</page-box>
</template>
<script>
import PageBox from '@/components/pageBox.vue'
import cropPhenologyApi from '@/api/fei/cropPhenology'
import cropApi from '@/api/fei/crop'
export default {
name: 'CropPhenologyManagement',
components: {
PageBox
},
data() {
return {
searchForm: {
cropId: null,
phenologyName: '',
status: null
},
cropList: [],
tableData: [],
loading: false,
pagination: {
pageNum: 1,
pageSize: 10,
total: 0
},
viewDialogVisible: false,
viewData: {},
formDrawerVisible: false,
formDrawerTitle: '新增物候期',
formSubmitting: false,
form: {
id: null,
cropId: null,
phenologyName: '',
phenologyCode: '',
sequenceNo: 1,
startDay: null,
endDay: null,
durationDays: null,
temperatureMin: null,
temperatureMax: null,
humidityMin: null,
humidityMax: null,
waterRequirement: '',
characteristics: '',
farmingActivities: '',
precautions: '',
status: 1,
remark: ''
},
formRules: {
cropId: [{ required: true, message: '请选择作物', trigger: 'change' }],
phenologyName: [{ required: true, message: '请输入物候期名称', trigger: 'blur' }],
startDay: [{ required: true, message: '请输入开始天数', trigger: 'blur' }],
endDay: [{ required: true, message: '请输入结束天数', trigger: 'blur' }]
}
}
},
created() {
this.loadCropList()
this.loadData()
},
watch: {
'$route'() {
this.loadData()
}
},
methods: {
async loadCropList() {
try {
const res = await cropApi.list()
if (res.code === 1) {
this.cropList = res.data || []
}
} catch (error) {
console.error('加载作物列表失败', error)
}
},
async loadData() {
this.loading = true
try {
const res = await cropPhenologyApi.page(
this.pagination.pageNum,
this.pagination.pageSize,
this.searchForm.cropId,
this.searchForm.phenologyName,
this.searchForm.status
)
if (res.code === 1) {
this.tableData = res.data.records
this.pagination.total = res.data.total
}
} catch (error) {
this.$message.error('加载数据失败')
} finally {
this.loading = false
}
},
handleSearch() {
this.pagination.pageNum = 1
this.loadData()
},
handleReset() {
this.searchForm = {
cropId: null,
phenologyName: '',
status: null
}
this.handleSearch()
},
handleSizeChange(val) {
this.pagination.pageSize = val
this.loadData()
},
handleCurrentChange(val) {
this.pagination.pageNum = val
this.loadData()
},
handleAdd() {
this.formDrawerTitle = '新增物候期'
this.resetForm()
this.formDrawerVisible = true
},
async handleEdit(row) {
this.formDrawerTitle = '编辑物候期'
try {
const res = await cropPhenologyApi.detail(row.id)
if (res.code === 1) {
this.form = { ...res.data }
this.formDrawerVisible = true
} else {
this.$message.error(res.msg || '获取详情失败')
}
} catch (error) {
this.$message.error('获取详情失败')
}
},
resetForm() {
this.form = {
id: null,
cropId: null,
phenologyName: '',
phenologyCode: '',
sequenceNo: 1,
startDay: null,
endDay: null,
durationDays: null,
temperatureMin: null,
temperatureMax: null,
humidityMin: null,
humidityMax: null,
waterRequirement: '',
characteristics: '',
farmingActivities: '',
precautions: '',
status: 1,
remark: ''
}
if (this.$refs.formRef) {
this.$refs.formRef.clearValidate()
}
},
handleFormClose(done) {
this.resetForm()
done()
},
handleFormSubmit() {
this.$refs.formRef.validate(async (valid) => {
if (!valid) return
this.formSubmitting = true
try {
const api = this.form.id ? cropPhenologyApi.update : cropPhenologyApi.add
const res = await api(this.form)
if (res.code === 1) {
this.$message.success(this.form.id ? '编辑成功' : '新增成功')
this.formDrawerVisible = false
this.resetForm()
this.loadData()
} else {
this.$message.error(res.msg || '操作失败')
}
} catch (error) {
this.$message.error('操作失败')
} finally {
this.formSubmitting = false
}
})
},
handleView(row) {
this.viewData = { ...row }
this.viewDialogVisible = true
},
getCropName(cropId) {
const crop = this.cropList.find(item => item.id === cropId)
return crop ? crop.name : '-'
},
handleDelete(row) {
this.$confirm('确认删除该物候期信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await cropPhenologyApi.delete(row.id)
if (res.code === 1) {
this.$message.success('删除成功')
this.loadData()
} else {
this.$message.error(res.msg || '删除失败')
}
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {})
}
}
}
</script>
<style scoped>
.crop-phenology-management {
padding: 15px;
}
.drawer-content {
padding: 0 20px 20px;
}
</style>
-294
View File
@@ -1,294 +0,0 @@
<template>
<page-box title="灌溉总控中心">
<template slot="header-actions">
<el-button type="primary" icon="el-icon-setting" size="small" @click="openConfigDialog">系统配置</el-button>
</template>
<div class="device-control-container">
<svg class="machine-diagram" viewBox="0 0 1400 700" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="tankGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a3a5c"/>
<stop offset="100%" stop-color="#0a1e2e"/>
</linearGradient>
<linearGradient id="mixTankGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2a4a2c"/>
<stop offset="100%" stop-color="#0e2e1e"/>
</linearGradient>
<linearGradient id="pipeGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e90ff"/>
<stop offset="100%" stop-color="#00bfff"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowStrong">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#0a2a3a" stroke-width="0.5"/>
</pattern>
</defs>
<!-- Background -->
<rect width="1400" height="700" fill="#050e1a"/>
<rect width="1400" height="700" fill="url(#grid)"/>
<!-- ====== Water Source ====== -->
<g transform="translate(50, 300)">
<rect x="-30" y="-25" width="60" height="50" rx="5" fill="#0a2a4a" stroke="#1e90ff" stroke-width="1.5" filter="url(#glow)"/>
<text x="0" y="5" text-anchor="middle" fill="#00bfff" font-size="11" font-weight="bold">水源</text>
</g>
<line x1="80" y1="300" x2="180" y2="300" stroke="url(#pipeGrad)" stroke-width="3" filter="url(#glow)"/>
<!-- ====== Mother Liquid Tanks ====== -->
<g v-for="(tank, index) in tanks" :key="'tank-' + index">
<!-- Tank body -->
<rect
:x="tankX(index)"
:y="tankY(index)"
:width="tankWidth"
:height="tankHeight(index)"
rx="6"
fill="url(#tankGrad)"
:stroke="tank.color || '#1e90ff'"
stroke-width="1.5"
filter="url(#glow)"
/>
<!-- Liquid level -->
<rect
:x="tankX(index) + 3"
:y="tankY(index) + tankHeight(index) * 0.2"
:width="tankWidth - 6"
:height="tankHeight(index) * 0.7"
rx="3"
:fill="tank.color || '#1e90ff'"
opacity="0.3"
/>
<!-- Tank label -->
<text :x="tankX(index) + tankWidth / 2" :y="tankY(index) + tankHeight(index) / 2 - 5" text-anchor="middle" fill="#ffffff" font-size="11" font-weight="bold">{{ tank.name }}</text>
<text :x="tankX(index) + tankWidth / 2" :y="tankY(index) + tankHeight(index) / 2 + 12" text-anchor="middle" fill="#88ccff" font-size="9">{{ tank.capacity }}L</text>
<!-- Pipe from tank down to manifold -->
<line :x1="tankX(index) + tankWidth / 2" :y1="tankY(index) + tankHeight(index)" :x2="tankX(index) + tankWidth / 2" y2="300" :stroke="tank.color || '#1e90ff'" stroke-width="2" filter="url(#glow)"/>
<!-- Valve -->
<circle :cx="tankX(index) + tankWidth / 2" cy="300" r="6" fill="#0a2a4a" stroke="#00ff88" stroke-width="1.5" filter="url(#glow)"/>
<text :x="tankX(index) + tankWidth / 2" y="303" text-anchor="middle" fill="#00ff88" font-size="7">V{{ index + 1 }}</text>
</g>
<!-- ====== Horizontal Manifold Pipe ====== -->
<line x1="180" y1="300" x2="700" y2="300" stroke="url(#pipeGrad)" stroke-width="4" filter="url(#glow)"/>
<!-- ====== Mixing Tank ====== -->
<g transform="translate(450, 200)">
<rect x="-50" y="-55" width="100" height="110" rx="8" fill="url(#mixTankGrad)" stroke="#00ff88" stroke-width="2" filter="url(#glow)"/>
<circle cx="0" cy="0" r="22" fill="none" stroke="#00ff88" stroke-width="1" opacity="0.5" stroke-dasharray="5,3">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="4s" repeatCount="indefinite"/>
</circle>
<text x="0" y="-18" text-anchor="middle" fill="#ffffff" font-size="13" font-weight="bold">混合罐</text>
<text x="0" y="5" text-anchor="middle" fill="#88ffaa" font-size="10">{{ mixingTank.capacity }}L</text>
<text x="0" y="22" text-anchor="middle" fill="#88ffaa" font-size="9">MIXING</text>
</g>
<!-- Pipe from manifold to mixing tank -->
<line x1="450" y1="300" x2="450" y2="255" stroke="#00ff88" stroke-width="2.5" filter="url(#glow)"/>
<!-- ====== Main Outlet ====== -->
<line x1="500" y1="200" x2="620" y2="200" stroke="#00ff88" stroke-width="3" filter="url(#glow)"/>
<circle cx="620" cy="200" r="8" fill="#0a2a1a" stroke="#ff6600" stroke-width="2" filter="url(#glow)"/>
<text x="620" y="204" text-anchor="middle" fill="#ff6600" font-size="8" font-weight="bold">主阀</text>
<line x1="628" y1="200" x2="700" y2="200" stroke="#ff6600" stroke-width="3" filter="url(#glowStrong)"/>
<!-- ====== Branch Outlets ====== -->
<!-- A branches (top) -->
<g v-for="(branch, i) in branchesA" :key="'branchA-' + i">
<line :x1="700 + i * 40" y1="200" :x2="700 + i * 40" y2="80" stroke="#ff6600" stroke-width="1.5" opacity="0.8" filter="url(#glow)"/>
<rect :x="700 + i * 40 - 16" y="55" width="32" height="25" rx="3" fill="#1a0a0a" stroke="#ff6600" stroke-width="1" filter="url(#glow)"/>
<text :x="700 + i * 40" y="72" text-anchor="middle" fill="#ffaa66" font-size="9" font-weight="bold">{{ branch }}</text>
</g>
<!-- B branches (middle) -->
<g v-for="(branch, i) in branchesB" :key="'branchB-' + i">
<line :x1="700 + i * 40" y1="200" :x2="700 + i * 40" y2="380" stroke="#ff6600" stroke-width="1.5" opacity="0.8" filter="url(#glow)"/>
<rect :x="700 + i * 40 - 16" y="380" width="32" height="25" rx="3" fill="#1a0a0a" stroke="#ff6600" stroke-width="1" filter="url(#glow)"/>
<text :x="700 + i * 40" y="397" text-anchor="middle" fill="#ffaa66" font-size="9" font-weight="bold">{{ branch }}</text>
</g>
<!-- C branches (bottom) -->
<g v-for="(branch, i) in branchesC" :key="'branchC-' + i">
<line :x1="700 + i * 40" y1="405" :x2="700 + i * 40" y2="480" stroke="#ff6600" stroke-width="1.5" opacity="0.8" filter="url(#glow)"/>
<rect :x="700 + i * 40 - 16" y="480" width="32" height="25" rx="3" fill="#1a0a0a" stroke="#ff6600" stroke-width="1" filter="url(#glow)"/>
<text :x="700 + i * 40" y="497" text-anchor="middle" fill="#ffaa66" font-size="9" font-weight="bold">{{ branch }}</text>
</g>
<!-- Status indicator -->
<g transform="translate(50, 50)">
<rect x="0" y="0" width="120" height="60" rx="5" fill="#0a1e2e" stroke="#1e90ff" stroke-width="1"/>
<text x="60" y="20" text-anchor="middle" fill="#88ccff" font-size="10">系统状态</text>
<circle cx="20" cy="40" r="5" fill="#00ff88" filter="url(#glow)">
<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>
</circle>
<text x="35" y="44" fill="#00ff88" font-size="9">运行中</text>
</g>
</svg>
</div>
<!-- ====== System Configuration Dialog ====== -->
<el-dialog title="系统配置" :visible.sync="configDialogVisible" width="600px" :close-on-click-modal="false" custom-class="config-dialog">
<el-form :model="configForm" label-width="120px" class="config-form">
<el-form-item label="母液罐数量">
<el-input-number v-model="configForm.tankCount" :min="2" :max="6" @change="onTankCountChange"/>
</el-form-item>
<div v-for="(tank, index) in configForm.tanks" :key="'config-tank-' + index">
<el-divider content-position="left">母液罐 {{ index + 1 }}</el-divider>
<el-form-item label="名称">
<el-input v-model="tank.name" placeholder="如:大量元素A"/>
</el-form-item>
<el-form-item label="容量 (L)">
<el-input-number v-model="tank.capacity" :min="50" :max="5000" :step="50"/>
</el-form-item>
<el-form-item label="颜色">
<el-color-picker v-model="tank.color"/>
</el-form-item>
</div>
<el-divider content-position="left">混合罐</el-divider>
<el-form-item label="混合罐容量 (L)">
<el-input-number v-model="configForm.mixingCapacity" :min="100" :max="10000" :step="100"/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="configDialogVisible = false"> </el-button>
<el-button type="primary" @click="applyConfig"> </el-button>
</span>
</el-dialog>
</page-box>
</template>
<script>
import PageBox from '@/components/pageBox.vue'
export default {
name: 'DeviceControl',
components: { PageBox },
data() {
return {
configDialogVisible: false,
tankCount: 3,
mixingTank: { capacity: 1000 },
tanks: [
{ name: '大量元素A', capacity: 1000, color: '#1e90ff' },
{ name: '大量元素B', capacity: 1000, color: '#00bfff' },
{ name: '微量元素', capacity: 500, color: '#9370db' }
],
configForm: {
tankCount: 3,
mixingCapacity: 1000,
tanks: [
{ name: '大量元素A', capacity: 1000, color: '#1e90ff' },
{ name: '大量元素B', capacity: 1000, color: '#00bfff' },
{ name: '微量元素', capacity: 500, color: '#9370db' }
]
},
tankWidth: 70,
baseTankHeight: 80,
branchesA: ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8'],
branchesB: ['B1', 'B2', 'B3', 'B4'],
branchesC: ['C1', 'C2', 'C3']
}
},
methods: {
tankX(index) {
return 180 + index * 120
},
tankY(index) {
return 300 - this.tankHeight(index)
},
tankHeight(index) {
if (!this.tanks[index]) return this.baseTankHeight
const maxCapacity = Math.max(...this.tanks.map(t => t.capacity), 1)
const ratio = this.tanks[index].capacity / maxCapacity
return Math.max(50, Math.round(this.baseTankHeight * ratio + ratio * 40))
},
openConfigDialog() {
this.configForm = {
tankCount: this.tankCount,
mixingCapacity: this.mixingTank.capacity,
tanks: JSON.parse(JSON.stringify(this.tanks))
}
this.configDialogVisible = true
},
onTankCountChange(val) {
while (this.configForm.tanks.length < val) {
const idx = this.configForm.tanks.length
this.configForm.tanks.push({
name: '母液罐' + (idx + 1),
capacity: 500,
color: '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')
})
}
this.configForm.tanks = this.configForm.tanks.slice(0, val)
},
applyConfig() {
this.tankCount = this.configForm.tankCount
this.tanks = JSON.parse(JSON.stringify(this.configForm.tanks))
this.mixingTank.capacity = this.configForm.mixingCapacity
this.configDialogVisible = false
this.$message.success('配置已应用')
}
}
}
</script>
<style scoped>
.device-control-container {
width: 100%;
background: #050e1a;
border: 1px solid #1e3a5f;
border-radius: 8px;
padding: 10px;
box-shadow: 0 0 20px rgba(30, 144, 255, 0.1), inset 0 0 30px rgba(0, 0, 0, 0.5);
}
.machine-diagram {
width: 100%;
height: auto;
display: block;
}
.config-form {
padding: 0 20px;
}
</style>
<style>
.config-dialog {
background: #0a1e2e !important;
border: 1px solid #1e90ff;
}
.config-dialog .el-dialog__title {
color: #88ccff;
}
.config-dialog .el-dialog__header {
border-bottom: 1px solid #1e3a5f;
}
.config-dialog .el-dialog__body {
color: #cccccc;
}
.config-dialog .el-form-item__label {
color: #88ccff;
}
.config-dialog .el-input__inner {
background: #0a1e2e;
border-color: #1e3a5f;
color: #ffffff;
}
.config-dialog .el-divider__text {
background: #0a1e2e;
color: #00ff88;
}
.config-dialog .el-divider {
background: #1e3a5f;
}
</style>
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More