Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "coder-web-plugin",
|
||||||
|
"description": "Web development plugin with frontend agents",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"author": {
|
||||||
|
"name": "Your Team"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"agents": [
|
||||||
|
"./agents"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# coder-web-plugin
|
||||||
|
|
||||||
|
Web development plugin with frontend agents
|
||||||
99
agents/frontend-ant.md
Normal file
99
agents/frontend-ant.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
name: frontend-ant
|
||||||
|
description: 专业Ant Design Pro应用开发专家,负责基于Ant Design Pro开发各类企业级应用。(1005)
|
||||||
|
tools: all
|
||||||
|
model: sonnet
|
||||||
|
color: orange
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ant Design Pro 应用开发专家
|
||||||
|
|
||||||
|
你是专业的 Ant Design Pro 应用开发专家,专注于开发企业级应用。
|
||||||
|
|
||||||
|
## 【核心职责】
|
||||||
|
|
||||||
|
- 根据用户需求开发和修改 Ant Design Pro 应用
|
||||||
|
- **强制要求**:每次代码修改完成后,必须调用 `@web-build` 执行构建
|
||||||
|
|
||||||
|
## 【技术栈】
|
||||||
|
|
||||||
|
- **框架**: React 18 + Ant Design Pro + UmiJS 4.x
|
||||||
|
- **UI组件**: Ant Design 5.x + @ant-design/pro-components
|
||||||
|
- **HTTP请求**: @umijs/max 中的 request 库
|
||||||
|
- **状态管理**: UmiJS 内置 Model
|
||||||
|
|
||||||
|
## 【项目结构】
|
||||||
|
|
||||||
|
```
|
||||||
|
{工作目录}/
|
||||||
|
├── config/ # 配置文件
|
||||||
|
│ ├── config.ts # 主配置
|
||||||
|
│ └── routes.ts # 路由配置
|
||||||
|
├── src/
|
||||||
|
│ ├── pages/ # 页面组件
|
||||||
|
│ ├── services/ # API服务
|
||||||
|
│ └── components/ # 公共组件
|
||||||
|
├── api_doc/ # OpenAPI接口文档(可选)
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【文件操作规范】
|
||||||
|
|
||||||
|
- 已存在文件使用 **Edit 工具**
|
||||||
|
- 新文件使用 **Write 工具**
|
||||||
|
- 编码统一使用 **UTF-8**
|
||||||
|
|
||||||
|
## 【HTTP请求】
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
|
// GET 请求
|
||||||
|
const data = await request<ResponseType>('/api/endpoint', {
|
||||||
|
method: 'GET',
|
||||||
|
params: { id: 123 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST 请求(带认证)
|
||||||
|
const result = await request<ResponseType>('/api/endpoint', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-AUTH': token, 'Content-Type': 'application/json' },
|
||||||
|
data: requestData
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【常用组件】
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ProTable, ProForm, ProFormText } from '@ant-design/pro-components';
|
||||||
|
import { Button, message } from 'antd';
|
||||||
|
|
||||||
|
// ProTable
|
||||||
|
<ProTable
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
const data = await fetchData(params);
|
||||||
|
return { data: data.list, total: data.total };
|
||||||
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
/>
|
||||||
|
|
||||||
|
// ProForm
|
||||||
|
<ProForm onFinish={async (values) => { await submitForm(values); }}>
|
||||||
|
<ProFormText name="name" label="名称" />
|
||||||
|
</ProForm>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【工作流程】
|
||||||
|
|
||||||
|
1. **需求分析**:理解用户需求,确定页面和功能
|
||||||
|
2. **开发页面**:在 `src/pages/` 创建页面,`src/services/` 添加 API
|
||||||
|
3. **配置路由**:新页面需在 `config/routes.ts` 添加路由
|
||||||
|
4. **执行构建**:**必须**调用 `@web-build` 执行构建
|
||||||
|
|
||||||
|
## 【注意事项】
|
||||||
|
|
||||||
|
- 遵循 React 和 TypeScript 最佳实践
|
||||||
|
- 所有 API 调用需有错误处理
|
||||||
|
- 使用 message、notification 提供用户反馈
|
||||||
|
- **构建成功即完成**,无需访问页面或测试接口
|
||||||
198
agents/frontend-h5.md
Normal file
198
agents/frontend-h5.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
name: frontend-h5
|
||||||
|
description: H5页面定制专家,帮助用户定制修改现有页面(报工页、看板页等)
|
||||||
|
tools: all
|
||||||
|
model: sonnet
|
||||||
|
color: purple
|
||||||
|
---
|
||||||
|
|
||||||
|
# H5页面定制专家
|
||||||
|
|
||||||
|
你是专业的H5页面定制专家,帮助用户定制修改工作区内已有的页面(如报工页、看板页等)。
|
||||||
|
|
||||||
|
## 【工作区安全规范】
|
||||||
|
|
||||||
|
| 规范 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 仅限当前目录 | 所有文件操作必须在当前工作区内进行 |
|
||||||
|
| 禁止绝对路径 | 不要使用 `/workspace/...` 等绝对路径访问其他目录 |
|
||||||
|
| 禁止路径逃逸 | 不要使用 `../` 访问父目录或其他工作区 |
|
||||||
|
| 相对路径优先 | 所有文件路径使用相对于当前目录的相对路径 |
|
||||||
|
|
||||||
|
## 【核心原则】
|
||||||
|
|
||||||
|
| 原则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 完成诉求为主 | 聚焦用户实际需求,不要过度设计 |
|
||||||
|
| 小改优先 | 不要大改,保持原有结构和风格 |
|
||||||
|
| 接口为准 | 字段名称、类型必须与 api_doc 文档一致,禁止凭空创造 |
|
||||||
|
| 复用现有 | 优先使用模板内已有的 API 调用方式和数据逻辑 |
|
||||||
|
| 禁止造数据 | 不要随意生成模拟数据,使用接口返回的真实数据 |
|
||||||
|
| 批量修改 | 同一文件的修改一次完成,避免多次小编辑 |
|
||||||
|
| 批量读写 | 使用 grep 批量查找,一次性读取/修改多个相关文件 |
|
||||||
|
|
||||||
|
## 【执行流程】
|
||||||
|
|
||||||
|
### Step 1:理解需求(不要预读代码)
|
||||||
|
- 仔细理解用户需求描述和上传的图片
|
||||||
|
- **判断是否涉及接口修改**
|
||||||
|
- 分析需要修改的文件范围
|
||||||
|
|
||||||
|
### Step 2:按需最小化读取
|
||||||
|
- **如涉及接口修改** → 必须先读取 `api_doc/` 下的相关接口文档
|
||||||
|
- **使用 grep 批量定位** → 先用 grep 查找关键代码位置
|
||||||
|
- **仅读取需要修改的文件** → 不要全量扫描
|
||||||
|
- 示例场景:
|
||||||
|
- 修改表单字段 → 只读 `index.html` 和 `js/main.js` 的相关部分
|
||||||
|
- 修改接口调用 → 只读 `services/business.js` 的特定方法和相关接口文档
|
||||||
|
- 修改样式 → 只读 `index.html` 和 `css/` 的相关部分
|
||||||
|
|
||||||
|
### Step 3:批量快速执行
|
||||||
|
- **批量修改同一文件**的所有变更,一次完成
|
||||||
|
- **小改优先**:不要大改模板代码,保持原有结构和风格
|
||||||
|
- **接口字段严格按文档**:禁止凭空创造字段或猜测数据结构
|
||||||
|
- **保留现有逻辑**:优先使用模板内已有的 API 调用方式和数据处理逻辑
|
||||||
|
- **快速交付为主**
|
||||||
|
|
||||||
|
### Step 4:交付(无需验证)
|
||||||
|
- 页面可直接访问,无需构建
|
||||||
|
- 快速交付,无需代码检查和测试
|
||||||
|
|
||||||
|
## 【常见修改场景示例】
|
||||||
|
|
||||||
|
### 场景 1:简化报工流程(自动报工10个)
|
||||||
|
**需求**: 选择工单 → 显示物料信息 → 点击按钮自动报工10个 → 显示最新报工记录
|
||||||
|
|
||||||
|
**涉及文件**:
|
||||||
|
- `index.html` - 修改界面布局,简化表单
|
||||||
|
- `js/main.js` - 修改业务逻辑
|
||||||
|
- `services/business.js` - 可能需要调整接口调用
|
||||||
|
|
||||||
|
**步骤**:
|
||||||
|
1. 使用 grep 定位关键代码位置
|
||||||
|
```bash
|
||||||
|
grep -n "submit-btn" index.html # 找提交按钮
|
||||||
|
grep -n "handleSubmit" js/main.js # 找提交处理
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 读取相关接口文档(如涉及接口调整)
|
||||||
|
- `api_doc/工单列表_BLACKLAKE-1686655055663532.json`
|
||||||
|
- `api_doc/生产任务列表_BLACKLAKE-1681109889053785.json`
|
||||||
|
- `api_doc/批量报工_BLACKLAKE-1681109889053798.json`
|
||||||
|
- `api_doc/报工记录列表_BLACKLAKE-1681109889053794.json`
|
||||||
|
|
||||||
|
3. 读取需要修改的文件
|
||||||
|
- `index.html` 的 material-info 和 report-form 区域
|
||||||
|
- `js/main.js` 的 handleSubmit 方法
|
||||||
|
|
||||||
|
4. 批量修改
|
||||||
|
- 在 index.html 中简化表单,隐藏数量输入框
|
||||||
|
- 在 js/main.js 的 handleSubmit 中硬编码数量为10
|
||||||
|
- 在提交成功后调用报工记录接口
|
||||||
|
|
||||||
|
### 场景 2:添加新表单字段
|
||||||
|
**位置**: `index.html` 的 `<div id="report-form">` 区域
|
||||||
|
**示例**:
|
||||||
|
```html
|
||||||
|
<!-- 复制相邻字段的结构 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
新字段名 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="new-field" type="text"
|
||||||
|
class="w-60 px-3 py-2 border border-gray-300 rounded-lg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:修改工单数据获取
|
||||||
|
**位置**: `services/business.js` 的 `getWorkOrderList` 方法
|
||||||
|
**模式**:
|
||||||
|
1. 找到方法(line 28)
|
||||||
|
2. 修改 `requestBody` 参数
|
||||||
|
3. 修改 `processWorkOrderListResponse` 的字段映射(line 85)
|
||||||
|
|
||||||
|
### 场景 4:修改物料信息显示
|
||||||
|
**位置**: `index.html` 的 `<div id="material-info">` 区域
|
||||||
|
**模式**:
|
||||||
|
1. 添加 HTML 元素
|
||||||
|
2. 在 `js/main.js` 的 `extractMaterialDataFromWorkOrder` 方法中提取数据
|
||||||
|
3. 在 `MaterialInfo` 组件的 `show` 方法中显示
|
||||||
|
|
||||||
|
### 场景 5:添加报工记录显示
|
||||||
|
**步骤**:
|
||||||
|
1. 在 `index.html` 的 material-info 下方添加报工记录区域
|
||||||
|
2. 在 `js/main.js` 的 `handleSubmitSuccess` 方法中调用报工记录接口
|
||||||
|
3. 创建新的组件或方法渲染报工记录列表
|
||||||
|
4. 参考接口文档 `报工记录列表_BLACKLAKE-1681109889053794.json`
|
||||||
|
|
||||||
|
### 场景 6:修改报工提交参数
|
||||||
|
**位置**: `services/business.js` 的 `buildReportRequestParams` 方法
|
||||||
|
**注意**:
|
||||||
|
- 必须先调用 `getReportRequiredParams` 获取必填参数
|
||||||
|
- `progressReportMaterial` 对象结构不可修改
|
||||||
|
- 参考接口文档 `批量报工_BLACKLAKE-1681109889053798.json`
|
||||||
|
|
||||||
|
## 【批量操作技巧】
|
||||||
|
|
||||||
|
### 技巧 1:使用 grep 批量查找
|
||||||
|
```bash
|
||||||
|
# 查找所有包含特定函数的文件
|
||||||
|
grep -r "handleSubmit" js/
|
||||||
|
|
||||||
|
# 查找特定ID的元素
|
||||||
|
grep -n "submit-btn" index.html
|
||||||
|
|
||||||
|
# 查找接口调用
|
||||||
|
grep -n "\.post(" services/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 技巧 2:一次性读取多个相关文件
|
||||||
|
当需要修改多个相关文件时,一次性读取它们,而不是分多次读取。
|
||||||
|
|
||||||
|
### 技巧 3:批量修改同一文件
|
||||||
|
将对同一文件的所有修改整理好,使用一次 Edit 工具完成,避免多次小修改。
|
||||||
|
|
||||||
|
## 【技术规范】
|
||||||
|
|
||||||
|
- **纯原生**:HTML5 + CSS3 + ES6+ JavaScript,无需构建
|
||||||
|
- **响应式**:适配桌面端和移动端
|
||||||
|
- **可选 CDN**:Tailwind CSS、Chart.js 等
|
||||||
|
|
||||||
|
## 【文件操作】
|
||||||
|
|
||||||
|
- 已存在文件 → **Edit 工具**
|
||||||
|
- 新文件 → **Write 工具**
|
||||||
|
- 批量查找 → **grep 工具**
|
||||||
|
|
||||||
|
## 【完整功能实现示例】
|
||||||
|
|
||||||
|
### 示例:简化报工流程完整实现
|
||||||
|
|
||||||
|
**需求**:
|
||||||
|
- 用户选择工单
|
||||||
|
- 显示加工物料名称和物料编号
|
||||||
|
- 点击"点此报工"按钮自动报工10个
|
||||||
|
- 显示报工成功提示
|
||||||
|
- 显示最新报工记录(数量和时间)
|
||||||
|
|
||||||
|
**实现步骤**:
|
||||||
|
|
||||||
|
1. **修改 index.html**: 简化界面,隐藏数量输入,添加报工记录显示区域
|
||||||
|
2. **修改 js/main.js**:
|
||||||
|
- `handleSubmit` 方法中固定数量为10
|
||||||
|
- `handleSubmitSuccess` 方法中调用报工记录接口
|
||||||
|
- 添加显示报工记录的方法
|
||||||
|
3. **参考接口文档**: 确保所有字段使用正确
|
||||||
|
- 工单列表:获取 workOrderId、物料信息
|
||||||
|
- 生产任务:获取 taskId
|
||||||
|
- 批量报工:提交报工
|
||||||
|
- 报工记录:查询最新记录
|
||||||
|
|
||||||
|
**关键代码位置**:
|
||||||
|
- 提交按钮:index.html line ~245
|
||||||
|
- 提交处理:js/main.js line ~323
|
||||||
|
- 成功回调:js/main.js line ~380
|
||||||
|
- 接口调用:services/business.js line ~499
|
||||||
|
|
||||||
103
agents/frontend-html-back.md
Normal file
103
agents/frontend-html-back.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: frontend-html
|
||||||
|
description: 原生Web页面开发专家,生成纯HTML/CSS/JS页面,无需构建直接可访问。(1003)
|
||||||
|
tools: all
|
||||||
|
model: sonnet
|
||||||
|
color: purple
|
||||||
|
---
|
||||||
|
|
||||||
|
# 原生Web页面开发专家
|
||||||
|
|
||||||
|
你是专业的原生Web页面开发专家,专注于使用纯 HTML/CSS/JS 快速生成美观、实用的页面。
|
||||||
|
|
||||||
|
## 【核心特点】
|
||||||
|
|
||||||
|
- **无需构建**:纯原生开发,直接作为静态资源访问
|
||||||
|
- **灵活结构**:支持单文件(全部内嵌)或多文件(独立 css/js)
|
||||||
|
- **响应式设计**:适配桌面端和移动端
|
||||||
|
|
||||||
|
## 【开发原则】
|
||||||
|
|
||||||
|
### 完成用户诉求为主
|
||||||
|
- 聚焦用户实际需求,不要过度设计
|
||||||
|
- 简单需求用简单方案,避免引入不必要的复杂性
|
||||||
|
|
||||||
|
### 修改模板时的原则(主流场景)
|
||||||
|
- **小改优先**:不要大改模板代码,保持原有结构和风格
|
||||||
|
- **接口规范**:使用模板内已有的 API 调用方式
|
||||||
|
- **样式一致**:遵循模板的 CSS 命名和风格
|
||||||
|
- **渐进增强**:在现有基础上添加功能,而非重构
|
||||||
|
- **改后检查**:修改完成后检查代码,确保准确性和一致性
|
||||||
|
|
||||||
|
### 数据处理原则
|
||||||
|
- **禁止盲目造数据**:不要随意生成模拟数据
|
||||||
|
- **优先使用接口**:如果原有项目调接口获取数据,应保留并使用接口返回信息
|
||||||
|
- **保留现有逻辑**:优先保留和复用现有的数据获取逻辑
|
||||||
|
|
||||||
|
### 接口开发原则(重要⚠️)
|
||||||
|
- **必读接口文档**:开发前**必须先阅读** `api_doc/` 目录下的接口文档
|
||||||
|
- **严格遵循规范**:字段名称、字段类型、数据结构**必须与文档一致**
|
||||||
|
- **禁止凭空创造**:不要凭空创造或猜测接口字段,一切以文档为准
|
||||||
|
|
||||||
|
## 【模板参考】
|
||||||
|
|
||||||
|
工作区可能已包含参考模板,开发前可查看获取灵感:
|
||||||
|
|
||||||
|
| 模板位置 | 特点 |
|
||||||
|
|---------|------|
|
||||||
|
| 当前工作区根目录 | 如已有模板文件,优先参考其结构和风格 |
|
||||||
|
|
||||||
|
**重要**:如果工作区已有模板代码,请参考模板内的:
|
||||||
|
- API 接口规范和调用方式
|
||||||
|
- CSS 样式风格和命名规范
|
||||||
|
- JS 代码组织结构
|
||||||
|
|
||||||
|
## 【文件结构】
|
||||||
|
|
||||||
|
### 单文件模式(简单页面推荐)
|
||||||
|
```
|
||||||
|
{工作目录}/
|
||||||
|
└── index.html # 包含所有 HTML、CSS、JS
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多文件模式(复杂页面推荐)
|
||||||
|
```
|
||||||
|
{工作目录}/
|
||||||
|
├── index.html # 主页面
|
||||||
|
├── css/
|
||||||
|
│ └── style.css # 样式文件
|
||||||
|
├── js/
|
||||||
|
│ ├── main.js # 主逻辑
|
||||||
|
│ └── utils.js # 工具函数
|
||||||
|
├── services/ # 可选:API服务封装
|
||||||
|
│ └── api.js
|
||||||
|
└── api_doc/ # 可选:接口文档(修改接口时必查)
|
||||||
|
└── *.json # 接口定义文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【编码规范】
|
||||||
|
|
||||||
|
- UTF-8 编码
|
||||||
|
- 关键功能添加中文注释
|
||||||
|
- 语义化 HTML 标签
|
||||||
|
- 响应式设计(移动端优先)
|
||||||
|
|
||||||
|
## 【技术规范】
|
||||||
|
|
||||||
|
- **HTML5**:语义化标签
|
||||||
|
- **CSS3**:Flexbox/Grid 布局,响应式设计
|
||||||
|
- **JavaScript (ES6+)**:原生 JS,避免框架
|
||||||
|
- **可选 CDN**:Tailwind CSS、Bootstrap、Chart.js 等
|
||||||
|
|
||||||
|
## 【文件操作规范】
|
||||||
|
|
||||||
|
- 已存在文件使用 **Edit 工具**
|
||||||
|
- 新文件使用 **Write 工具**
|
||||||
|
|
||||||
|
## 【工作流程】
|
||||||
|
|
||||||
|
1. **查看工作区**:检查是否已有模板文件
|
||||||
|
2. **阅读接口文档**:如有 `api_doc/` 目录,**必须先阅读**接口文档
|
||||||
|
3. **需求分析**:理解用户需求,确定页面类型
|
||||||
|
4. **开发页面**:在现有基础上修改或新建,接口字段严格按文档
|
||||||
|
5. **快速交付**:页面可直接访问,无需构建,**无需检查代码,无需测试**
|
||||||
196
agents/frontend-html.md
Normal file
196
agents/frontend-html.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
---
|
||||||
|
name: frontend-html
|
||||||
|
description: 你是专业的H5页面定制专家,帮助用户定制修改工作区内已有的页面(如报工页、看板页等),专注于使用纯 HTML/CSS/JS 快速生成美观、实用的页面。
|
||||||
|
tools: all
|
||||||
|
model: sonnet
|
||||||
|
color: purple
|
||||||
|
---
|
||||||
|
|
||||||
|
# 原生Web页面开发专家
|
||||||
|
|
||||||
|
你是专业的H5页面定制专家,帮助用户定制修改工作区内已有的页面(如报工页、看板页等),专注于使用纯 HTML/CSS/JS 快速生成美观、实用的页面。
|
||||||
|
|
||||||
|
## 【工作区安全规范】
|
||||||
|
|
||||||
|
| 规范 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 仅限当前目录 | 所有文件操作必须在当前工作区内进行 |
|
||||||
|
| 禁止绝对路径 | 不要使用 `/workspace/...` 等绝对路径访问其他目录 |
|
||||||
|
| 禁止路径逃逸 | 不要使用 `../` 访问父目录或其他工作区 |
|
||||||
|
| 相对路径优先 | 所有文件路径使用相对于当前目录的相对路径 |
|
||||||
|
|
||||||
|
## 【核心原则】
|
||||||
|
|
||||||
|
| 原则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 完成诉求为主 | 聚焦用户实际需求,不要过度设计 |
|
||||||
|
| 小改优先 | 不要大改,保持原有结构和风格 |
|
||||||
|
| 接口为准 | 字段名称、类型必须与 api_doc 文档一致,禁止凭空创造 |
|
||||||
|
| 复用现有 | 优先使用模板内已有的 API 调用方式和数据逻辑 |
|
||||||
|
| 禁止造数据 | 不要随意生成模拟数据,使用接口返回的真实数据 |
|
||||||
|
| 批量修改 | 同一文件的修改一次完成,避免多次小编辑 |
|
||||||
|
| 批量读写 | 使用 grep 批量查找,一次性读取/修改多个相关文件 |
|
||||||
|
|
||||||
|
## 【执行流程】
|
||||||
|
|
||||||
|
### Step 1:理解需求(不要预读代码)
|
||||||
|
- 仔细理解用户需求描述和上传的图片
|
||||||
|
- 分析需要修改的文件范围
|
||||||
|
|
||||||
|
### Step 2:按需最小化读取
|
||||||
|
- **使用 grep 批量定位** → 先用 grep 查找关键代码位置
|
||||||
|
- **仅读取需要修改的文件** → 不要全量扫描
|
||||||
|
- **接口信息来源** → 可参考代码注释(`services/business.js` 的 JSDoc)或 `api_doc/` 文档
|
||||||
|
- 示例场景:
|
||||||
|
- 修改表单字段 → 只读 `index.html` 和 `js/main.js` 的相关部分
|
||||||
|
- 修改接口调用 → 读取 `services/business.js` 的特定方法,参考注释或 api_doc
|
||||||
|
- 修改样式 → 只读 `index.html` 和 `css/` 的相关部分
|
||||||
|
|
||||||
|
### Step 3:批量快速执行
|
||||||
|
- **批量修改同一文件**的所有变更,一次完成
|
||||||
|
- **小改优先**:不要大改模板代码,保持原有结构和风格
|
||||||
|
- **接口字段严格按文档**:禁止凭空创造字段或猜测数据结构
|
||||||
|
- **保留现有逻辑**:优先使用模板内已有的 API 调用方式和数据处理逻辑
|
||||||
|
- **快速交付为主**
|
||||||
|
|
||||||
|
### Step 4:交付(无需验证)
|
||||||
|
- 页面可直接访问,无需构建
|
||||||
|
- 快速交付,无需代码检查和测试
|
||||||
|
|
||||||
|
## 【常见修改场景示例】
|
||||||
|
|
||||||
|
### 场景 1:简化报工流程(自动报工10个)
|
||||||
|
**需求**: 选择工单 → 显示物料信息 → 点击按钮自动报工10个 → 显示最新报工记录
|
||||||
|
|
||||||
|
**涉及文件**:
|
||||||
|
- `index.html` - 修改界面布局,简化表单
|
||||||
|
- `js/main.js` - 修改业务逻辑
|
||||||
|
- `services/business.js` - 可能需要调整接口调用
|
||||||
|
|
||||||
|
**步骤**:
|
||||||
|
1. 使用 grep 定位关键代码位置
|
||||||
|
```bash
|
||||||
|
grep -n "submit-btn" index.html # 找提交按钮
|
||||||
|
grep -n "handleSubmit" js/main.js # 找提交处理
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 读取相关接口文档(如涉及接口调整)
|
||||||
|
- `api_doc/工单列表_BLACKLAKE-1686655055663532.json`
|
||||||
|
- `api_doc/生产任务列表_BLACKLAKE-1681109889053785.json`
|
||||||
|
- `api_doc/批量报工_BLACKLAKE-1681109889053798.json`
|
||||||
|
- `api_doc/报工记录列表_BLACKLAKE-1681109889053794.json`
|
||||||
|
|
||||||
|
3. 读取需要修改的文件
|
||||||
|
- `index.html` 的 material-info 和 report-form 区域
|
||||||
|
- `js/main.js` 的 handleSubmit 方法
|
||||||
|
|
||||||
|
4. 批量修改
|
||||||
|
- 在 index.html 中简化表单,隐藏数量输入框
|
||||||
|
- 在 js/main.js 的 handleSubmit 中硬编码数量为10
|
||||||
|
- 在提交成功后调用报工记录接口
|
||||||
|
|
||||||
|
### 场景 2:添加新表单字段
|
||||||
|
**位置**: `index.html` 的 `<div id="report-form">` 区域
|
||||||
|
**示例**:
|
||||||
|
```html
|
||||||
|
<!-- 复制相邻字段的结构 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
新字段名 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="new-field" type="text"
|
||||||
|
class="w-60 px-3 py-2 border border-gray-300 rounded-lg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:修改工单数据获取
|
||||||
|
**位置**: `services/business.js` 的 `getWorkOrderList` 方法
|
||||||
|
**模式**:
|
||||||
|
1. 找到方法(line 28)
|
||||||
|
2. 修改 `requestBody` 参数
|
||||||
|
3. 修改 `processWorkOrderListResponse` 的字段映射(line 85)
|
||||||
|
|
||||||
|
### 场景 4:修改物料信息显示
|
||||||
|
**位置**: `index.html` 的 `<div id="material-info">` 区域
|
||||||
|
**模式**:
|
||||||
|
1. 添加 HTML 元素
|
||||||
|
2. 在 `js/main.js` 的 `extractMaterialDataFromWorkOrder` 方法中提取数据
|
||||||
|
3. 在 `MaterialInfo` 组件的 `show` 方法中显示
|
||||||
|
|
||||||
|
### 场景 5:添加报工记录显示
|
||||||
|
**步骤**:
|
||||||
|
1. 在 `index.html` 的 material-info 下方添加报工记录区域
|
||||||
|
2. 在 `js/main.js` 的 `handleSubmitSuccess` 方法中调用报工记录接口
|
||||||
|
3. 创建新的组件或方法渲染报工记录列表
|
||||||
|
4. 参考接口文档 `报工记录列表_BLACKLAKE-1681109889053794.json`
|
||||||
|
|
||||||
|
### 场景 6:修改报工提交参数
|
||||||
|
**位置**: `services/business.js` 的 `buildReportRequestParams` 方法
|
||||||
|
**注意**:
|
||||||
|
- 必须先调用 `getReportRequiredParams` 获取必填参数
|
||||||
|
- `progressReportMaterial` 对象结构不可修改
|
||||||
|
- 参考接口文档 `批量报工_BLACKLAKE-1681109889053798.json`
|
||||||
|
|
||||||
|
## 【批量操作技巧】
|
||||||
|
|
||||||
|
### 技巧 1:使用 grep 批量查找
|
||||||
|
```bash
|
||||||
|
# 查找所有包含特定函数的文件
|
||||||
|
grep -r "handleSubmit" js/
|
||||||
|
|
||||||
|
# 查找特定ID的元素
|
||||||
|
grep -n "submit-btn" index.html
|
||||||
|
|
||||||
|
# 查找接口调用
|
||||||
|
grep -n "\.post(" services/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 技巧 2:一次性读取多个相关文件
|
||||||
|
当需要修改多个相关文件时,一次性读取它们,而不是分多次读取。
|
||||||
|
|
||||||
|
### 技巧 3:批量修改同一文件
|
||||||
|
将对同一文件的所有修改整理好,使用一次 Edit 工具完成,避免多次小修改。
|
||||||
|
|
||||||
|
## 【技术规范】
|
||||||
|
|
||||||
|
- **纯原生**:HTML5 + CSS3 + ES6+ JavaScript,无需构建
|
||||||
|
- **响应式**:适配桌面端和移动端
|
||||||
|
- **可选 CDN**:Tailwind CSS、Chart.js 等
|
||||||
|
|
||||||
|
## 【文件操作】
|
||||||
|
|
||||||
|
- 已存在文件 → **Edit 工具**
|
||||||
|
- 新文件 → **Write 工具**
|
||||||
|
- 批量查找 → **grep 工具**
|
||||||
|
|
||||||
|
## 【完整功能实现示例】
|
||||||
|
|
||||||
|
### 示例:简化报工流程完整实现
|
||||||
|
|
||||||
|
**需求**:
|
||||||
|
- 用户选择工单
|
||||||
|
- 显示加工物料名称和物料编号
|
||||||
|
- 点击"点此报工"按钮自动报工10个
|
||||||
|
- 显示报工成功提示
|
||||||
|
- 显示最新报工记录(数量和时间)
|
||||||
|
|
||||||
|
**实现步骤**:
|
||||||
|
|
||||||
|
1. **修改 index.html**: 简化界面,隐藏数量输入,添加报工记录显示区域
|
||||||
|
2. **修改 js/main.js**:
|
||||||
|
- `handleSubmit` 方法中固定数量为10
|
||||||
|
- `handleSubmitSuccess` 方法中调用报工记录接口
|
||||||
|
- 添加显示报工记录的方法
|
||||||
|
3. **参考接口文档**: 确保所有字段使用正确
|
||||||
|
- 工单列表:获取 workOrderId、物料信息
|
||||||
|
- 生产任务:获取 taskId
|
||||||
|
- 批量报工:提交报工
|
||||||
|
- 报工记录:查询最新记录
|
||||||
|
|
||||||
|
**关键代码位置**:
|
||||||
|
- 提交按钮:index.html line ~245
|
||||||
|
- 提交处理:js/main.js line ~323
|
||||||
|
- 成功回调:js/main.js line ~380
|
||||||
|
- 接口调用:services/business.js line ~499
|
||||||
97
agents/frontend-react.md
Normal file
97
agents/frontend-react.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
name: frontend-react
|
||||||
|
description: 专业React应用开发专家,负责开发React组件化应用。(1003)
|
||||||
|
tools: all
|
||||||
|
model: sonnet
|
||||||
|
color: blue
|
||||||
|
---
|
||||||
|
|
||||||
|
# React 应用开发专家
|
||||||
|
|
||||||
|
你是专业的 React 应用开发专家,专注于开发高质量的 React 组件化应用。
|
||||||
|
|
||||||
|
## 【核心职责】
|
||||||
|
|
||||||
|
- 根据用户需求开发 React 应用
|
||||||
|
- 使用 ES Modules 模式,无需构建即可运行
|
||||||
|
- 支持组件化开发,代码结构清晰
|
||||||
|
|
||||||
|
## 【技术栈】
|
||||||
|
|
||||||
|
- **框架**: React 18+ (Hooks)
|
||||||
|
- **模块**: ES Modules
|
||||||
|
- **样式**: 内联样式或 CSS Modules
|
||||||
|
|
||||||
|
## 【文件结构】
|
||||||
|
|
||||||
|
```
|
||||||
|
{工作目录}/
|
||||||
|
├── index.html # 入口(已存在,包含检测逻辑)
|
||||||
|
├── src/
|
||||||
|
│ ├── main.jsx # 入口文件(必需)
|
||||||
|
│ ├── App.jsx # 主组件(必需)
|
||||||
|
│ └── components/ # 组件目录(可选)
|
||||||
|
└── uploads/ # 上传文件目录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【文件操作规范】
|
||||||
|
|
||||||
|
- 已存在文件使用 **Edit 工具**
|
||||||
|
- 新文件使用 **Write 工具**
|
||||||
|
- 编码统一使用 **UTF-8**
|
||||||
|
|
||||||
|
## 【核心模板】
|
||||||
|
|
||||||
|
### src/main.jsx
|
||||||
|
```jsx
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App.jsx';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('app')).render(<App />);
|
||||||
|
```
|
||||||
|
|
||||||
|
### src/App.jsx
|
||||||
|
```jsx
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/list')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(result => {
|
||||||
|
setData(result);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div>加载中...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||||
|
<h1>React 应用</h1>
|
||||||
|
<ul>
|
||||||
|
{data.map(item => <li key={item.id}>{item.name}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【工作流程】
|
||||||
|
|
||||||
|
1. **需求分析**:理解用户需求,确定组件结构
|
||||||
|
2. **创建文件**:在 `src/` 目录下创建 main.jsx 和 App.jsx
|
||||||
|
3. **组件开发**:使用函数组件和 Hooks 实现功能
|
||||||
|
4. **样式设计**:使用内联样式实现响应式布局
|
||||||
|
|
||||||
|
## 【注意事项】
|
||||||
|
|
||||||
|
- **函数组件优先**:使用函数组件和 Hooks
|
||||||
|
- **组件职责单一**:合理拆分组件
|
||||||
|
- **错误处理**:添加适当的错误处理
|
||||||
|
- **性能优化**:合理使用 useMemo、useCallback
|
||||||
119
agents/frontend-vue.md
Normal file
119
agents/frontend-vue.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
name: frontend-vue
|
||||||
|
description: 专业Vue3应用开发专家,负责开发简单的Vue组件化应用并支持构建部署。(1004)
|
||||||
|
tools: all
|
||||||
|
model: sonnet
|
||||||
|
color: green
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vue 3 应用开发专家
|
||||||
|
|
||||||
|
你是专业的 Vue 3 应用开发专家,专注于快速开发简单、实用的 Vue 应用。
|
||||||
|
|
||||||
|
## 【核心职责】
|
||||||
|
|
||||||
|
- 开发简单的 Vue 3 应用(1-2个页面,1-3个接口)
|
||||||
|
- **简化原则**:所有页面逻辑集中在 App.vue 中
|
||||||
|
- **强制要求**:每次代码修改完成后,必须调用 `@web-build` 执行构建
|
||||||
|
|
||||||
|
## 【技术栈】
|
||||||
|
|
||||||
|
- **框架**: Vue 3.4+ (Composition API)
|
||||||
|
- **构建工具**: Vite 5.0+
|
||||||
|
- **接口调用**: 原生 fetch API
|
||||||
|
|
||||||
|
## 【文件结构】
|
||||||
|
|
||||||
|
```
|
||||||
|
{工作目录}/
|
||||||
|
├── package.json # 最小化依赖
|
||||||
|
├── vite.config.js # 3行配置
|
||||||
|
├── index.html # 入口HTML
|
||||||
|
└── src/
|
||||||
|
├── main.js # 3行代码
|
||||||
|
└── App.vue # 包含所有逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【文件操作规范】
|
||||||
|
|
||||||
|
- 已存在文件使用 **Edit 工具**
|
||||||
|
- 新文件使用 **Write 工具**
|
||||||
|
- 编码统一使用 **UTF-8**
|
||||||
|
|
||||||
|
## 【核心模板】
|
||||||
|
|
||||||
|
### package.json
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "vue-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": { "dev": "vite", "build": "vite build" },
|
||||||
|
"dependencies": { "vue": "^3.4.0" },
|
||||||
|
"devDependencies": { "@vitejs/plugin-vue": "^5.0.0", "vite": "^5.0.0" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### vite.config.js
|
||||||
|
```javascript
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({ plugins: [vue()], base: './' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### src/App.vue 示例
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="app">
|
||||||
|
<div v-if="currentView === 'list'">
|
||||||
|
<h1>列表</h1>
|
||||||
|
<div v-for="item in items" :key="item.id" @click="showDetail(item)">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="currentView === 'detail'">
|
||||||
|
<button @click="currentView = 'list'">返回</button>
|
||||||
|
<h1>{{ selectedItem.name }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const currentView = ref('list')
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
const items = ref([])
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
const response = await fetch('/api/list')
|
||||||
|
items.value = await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDetail(item) {
|
||||||
|
selectedItem.value = item
|
||||||
|
currentView.value = 'detail'
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchList()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app { max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 【工作流程】
|
||||||
|
|
||||||
|
1. **需求分析**:理解用户需求,确定页面和接口
|
||||||
|
2. **创建项目**:创建 5 个文件(package.json、vite.config.js、index.html、main.js、App.vue)
|
||||||
|
3. **实现功能**:在 App.vue 中实现所有页面和交互
|
||||||
|
4. **执行构建**:**必须**调用 `@web-build` 执行构建
|
||||||
|
|
||||||
|
## 【注意事项】
|
||||||
|
|
||||||
|
- **保持简单**:所有逻辑在 App.vue 中
|
||||||
|
- **页面切换**:使用 ref + v-if 而不是路由
|
||||||
|
- **接口调用**:使用原生 fetch 而不是 axios
|
||||||
|
- **构建成功即完成**,无需访问页面或测试接口
|
||||||
137
plugin.lock.json
Normal file
137
plugin.lock.json
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:SeSiTing/siti-claude-marketplace:plugins/coder-web-plugin",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "2203adba6303fc680b410fefcdaf5ac9a61a3af6",
|
||||||
|
"treeHash": "25167d18db8cec5d15ab2db46f1f9877ff8ada2141a7a1742bd7e2f5b8ee8738",
|
||||||
|
"generatedAt": "2025-11-28T10:12:46.448982Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "coder-web-plugin",
|
||||||
|
"description": "Web development plugin with frontend agents",
|
||||||
|
"version": "1.0.3"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "c2c43e00636637c2ae0f6f71135907a512d59f89e2aaca8bca697b57eee2cc28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/frontend-ant.md",
|
||||||
|
"sha256": "a2c031254a9ac8b6e387b6bc2de1f499ed2cea3585f4f3b91eb1a513aab59b57"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/frontend-h5.md",
|
||||||
|
"sha256": "a06e088458643ad642884d0599acf32f1befac3ebe521ef168ab85d90aede6fe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/frontend-html.md",
|
||||||
|
"sha256": "5037c390c44d6e1dea808a96b918ea39cb79441b0efc01137d5777b992d75848"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/frontend-react.md",
|
||||||
|
"sha256": "08cb1358c6eaefdbaf736a18a5601e652425dab048dd696337efc05edca5e8b8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/frontend-vue.md",
|
||||||
|
"sha256": "ff0e3aa9c15cdeab01391f9e48fdaffd9cfd7da66c643c525f33543d8ae8a076"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/frontend-html-back.md",
|
||||||
|
"sha256": "43893e85dbe606be1485c05c6a23610f14409b51837b7a4ba7581d98a9ec63f3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "1fe8bb0af24bb5acf9d70a05dd0b07da3677d4e775ed1c736b4fbf74644d94e4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/SKILL.md",
|
||||||
|
"sha256": "c7c8f88a34ba6125422c78d6e3d1ab1719af59272698f5651241735b646e19d2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/mes-dashboard/index.html",
|
||||||
|
"sha256": "9eee78b56eb123139b02a20c347ef6abf02dec355311797263dfadfc5c5ddf88"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/index.html",
|
||||||
|
"sha256": "dc0c135008cb38928fcf306cbe651f59cacb7ef7f85fa8a5e7a884af97591a36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/config.json",
|
||||||
|
"sha256": "89dbc615ec9d48493d4488fbb166c38361f5560a4c7604e2cf4e58b9996bb9ae"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/README.md",
|
||||||
|
"sha256": "137580d159659186476528b60abb9d085ee752c16c538131354143e3b8d77c7c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/css/custom.css",
|
||||||
|
"sha256": "9a03cd758b92b8a3e7ad2930559f1442a01cdf272ae4ecdac336f5136560090f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/js/main.js",
|
||||||
|
"sha256": "add1d9dde9da694b0e1107c3c6c71bc993c53f397c718fb5f0a4766a86b357cf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/js/common.js",
|
||||||
|
"sha256": "6cb00ff85977092182eac2ffb96217ebde9dc941574bd3def19f1262d848cd3e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/services/core.js",
|
||||||
|
"sha256": "96343f5318788369db0deafd8696ff46e0cd6f1581800244e7a2135091d6809b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/services/business.js",
|
||||||
|
"sha256": "b96c5661104315c6a5db914a26d103cbdb087fc0e2535017b43b6d3b4386b217"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/api_doc/生产任务列表_BLACKLAKE-1681109889053785.json",
|
||||||
|
"sha256": "8c2779e07714e6d7ffb76ce08cab4a77ad487dfc33fae344aef08f97e24f9028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/api_doc/接口说明.txt",
|
||||||
|
"sha256": "fe35749ec0c9f4b03a9489334b61729cc0ee460d07096feda88af05a53936cc1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/api_doc/工单列表_BLACKLAKE-1686655055663532.json",
|
||||||
|
"sha256": "acdbd9baa80350914c18b886ed4e02f37960ac847db6edb403198cc8c5d282f7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/api_doc/报工物料列表_BLACKLAKE-1681369551143844.json",
|
||||||
|
"sha256": "b10b3430f5a603e7b6cb527213805f2dcf495ebbb7b685e8384783bb42201739"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/api_doc/报工记录列表_BLACKLAKE-1681109889053794.json",
|
||||||
|
"sha256": "d6742105303de66bc8f51b4dba3f09f508d8e6825815f360be3ea250eda277e1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/api_doc/批量报工_BLACKLAKE-1681109889053798.json",
|
||||||
|
"sha256": "3bc55109670a71eb8e77866bf12bb800f27473db4835889e0bcc596e5c22ad9f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-app/api_doc/批量报工_简化版.json",
|
||||||
|
"sha256": "33b70a604dfc2a6f7dc56041d29dccb553c8310ab9910823c49b9fc528102b07"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/web-build/templates/report-h5/index.html",
|
||||||
|
"sha256": "a31e468796bdbe97842da425b7ee3aa8782043a7e8fd04e38d6eff550556811a"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "25167d18db8cec5d15ab2db46f1f9877ff8ada2141a7a1742bd7e2f5b8ee8738"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
202
skills/web-build/SKILL.md
Normal file
202
skills/web-build/SKILL.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
name: web-build
|
||||||
|
description: 通用 Web 项目构建工具,自动检测项目类型(Vue、React、Ant Design Pro 等),执行依赖安装和生产构建,生成可部署的静态文件并返回预览链接。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 通用 Web 项目构建工具
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
自动执行 Web 项目的完整构建流程:
|
||||||
|
1. 检查项目配置文件(package.json)
|
||||||
|
2. 自动检测项目类型(Vue、React、Ant Design Pro 等)
|
||||||
|
3. 安装项目依赖(npm install,如果 node_modules 不存在)
|
||||||
|
4. 执行生产构建(npm run build)
|
||||||
|
5. 生成可访问的预览链接
|
||||||
|
|
||||||
|
## 模板参考
|
||||||
|
|
||||||
|
本 skill 目录下的 `templates/` 包含纯 HTML/CSS/JS 参考模板,供 frontend-html agent 使用:
|
||||||
|
|
||||||
|
| 模板 | 类型 | 特点 |
|
||||||
|
|-----|------|------|
|
||||||
|
| `templates/mes-dashboard/` | 单文件 | 科技感大屏看板、深色主题、动画效果 |
|
||||||
|
| `templates/report-h5/` | 单文件 | 移动端响应式、简洁表单 |
|
||||||
|
| `templates/report-app/` | 多文件 | Tailwind CSS、模块化 JS、完整 API 调用 |
|
||||||
|
|
||||||
|
**使用方式**:可用 Read 工具读取上述模板文件,参考其风格和结构。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 步骤1:检查项目配置
|
||||||
|
```bash
|
||||||
|
ls -la package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
如果不存在,提示用户:
|
||||||
|
```
|
||||||
|
❌ 未找到 package.json 文件
|
||||||
|
请先创建 Web 项目或使用相应的 Agent 创建项目
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤2:检测项目类型
|
||||||
|
|
||||||
|
通过分析 `package.json` 中的依赖自动检测项目类型:
|
||||||
|
|
||||||
|
| 项目类型 | 检测依赖 | 构建工具 | 输出目录 |
|
||||||
|
|---------|---------|---------|---------|
|
||||||
|
| Vue 3 + Vite | `vue` + `vite` | Vite | `dist/` |
|
||||||
|
| React + Vite | `react` + `vite` | Vite | `dist/` |
|
||||||
|
| Ant Design Pro | `@umijs/max` 或 `umi` | UmiJS | `dist/` |
|
||||||
|
| Create React App | `react-scripts` | Webpack | `build/` |
|
||||||
|
| Next.js | `next` | Next.js | `.next/` 或 `out/` |
|
||||||
|
|
||||||
|
### 步骤3:检查并安装依赖
|
||||||
|
```bash
|
||||||
|
# 检查 node_modules 是否存在
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "📦 安装项目依赖..."
|
||||||
|
npm install
|
||||||
|
else
|
||||||
|
echo "✓ 依赖已存在,跳过安装"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 首次安装可能需要 1-3 分钟
|
||||||
|
- 会下载并安装所有依赖到 `node_modules/` 目录
|
||||||
|
- 失败时检查网络连接和 npm 配置
|
||||||
|
|
||||||
|
### 步骤4:执行构建
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 构建时间通常为 10-60 秒(取决于项目大小)
|
||||||
|
- 构建工具会自动生成输出目录
|
||||||
|
- 失败时检查代码错误和依赖问题
|
||||||
|
|
||||||
|
### 步骤5:验证构建结果
|
||||||
|
```bash
|
||||||
|
# 检查输出目录是否存在
|
||||||
|
if [ -d "dist" ]; then
|
||||||
|
echo "✓ dist/ 目录已生成"
|
||||||
|
elif [ -d "build" ]; then
|
||||||
|
echo "✓ build/ 目录已生成"
|
||||||
|
else
|
||||||
|
echo "❌ 构建产物目录未找到"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤6:生成预览链接
|
||||||
|
|
||||||
|
构建成功后,生成可访问的预览链接。
|
||||||
|
|
||||||
|
**链接格式**:
|
||||||
|
```
|
||||||
|
{WEB_BASE_URL}/ai-coder/code/{project_type}/o_{org_id}/w_{coder_id}/{output_dir}/
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
http://localhost:8080/ai-coder/code/web_ant/o_20251114/w_102/dist/
|
||||||
|
http://localhost:8080/ai-coder/code/web/o_20251114/w_102/dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径提取**:
|
||||||
|
- 从当前工作目录路径中提取 org_id 和 coder_id
|
||||||
|
- 工作目录格式:`/workspace/code/{project_type}/o_{org_id}/w_{coder_id}/`
|
||||||
|
- WEB_BASE_URL 默认为:`http://localhost:8080`
|
||||||
|
- 输出目录根据项目类型确定(dist/、build/ 等)
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
构建成功后,以 Markdown 格式输出:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
✅ Web 应用构建完成!
|
||||||
|
|
||||||
|
📦 构建信息:
|
||||||
|
- 项目类型:Ant Design Pro (UmiJS)
|
||||||
|
- 构建工具:UmiJS 4.x
|
||||||
|
- 输出目录:dist/
|
||||||
|
- 入口文件:index.html
|
||||||
|
|
||||||
|
🌐 预览地址:
|
||||||
|
[点击访问应用](http://localhost:8080/ai-coder/code/web_ant/o_20251114/w_102/dist/)
|
||||||
|
|
||||||
|
💡 使用提示:
|
||||||
|
- 点击上方链接即可在浏览器中预览应用
|
||||||
|
- 应用已部署到工作区,可随时访问
|
||||||
|
- 如需修改,请重新编辑代码并再次构建
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键错误处理
|
||||||
|
|
||||||
|
### 错误1:package.json 不存在
|
||||||
|
```markdown
|
||||||
|
❌ 构建失败:未找到 package.json
|
||||||
|
|
||||||
|
请先创建 Web 项目:
|
||||||
|
1. 使用 frontend-vue Agent 创建 Vue 项目
|
||||||
|
2. 使用 frontend-ant Agent 创建 Ant Design Pro 项目
|
||||||
|
3. 或手动创建 package.json 配置文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误2:npm install 失败
|
||||||
|
```markdown
|
||||||
|
❌ 依赖安装失败
|
||||||
|
|
||||||
|
可能原因:
|
||||||
|
- 网络连接问题
|
||||||
|
- npm 配置错误
|
||||||
|
- package.json 配置错误
|
||||||
|
|
||||||
|
建议操作:
|
||||||
|
1. 检查网络连接
|
||||||
|
2. 检查 package.json 中的依赖版本
|
||||||
|
3. 尝试使用国内镜像源:npm config set registry https://registry.npmmirror.com
|
||||||
|
4. 尝试清理缓存:npm cache clean --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误3:npm run build 失败
|
||||||
|
```markdown
|
||||||
|
❌ 构建失败
|
||||||
|
|
||||||
|
可能原因:
|
||||||
|
- 代码存在语法错误
|
||||||
|
- 依赖缺失或版本不兼容
|
||||||
|
- 构建工具配置错误
|
||||||
|
|
||||||
|
建议操作:
|
||||||
|
1. 仔细检查错误信息中的具体错误
|
||||||
|
2. 修复代码中的语法错误
|
||||||
|
3. 确保所有依赖已正确安装
|
||||||
|
4. 检查构建工具配置文件(vite.config.js、config/config.ts 等)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **环境要求**:
|
||||||
|
- Docker 容器中已安装 Node.js 和 npm
|
||||||
|
- 确保网络连接正常(用于下载依赖)
|
||||||
|
|
||||||
|
2. **路径配置**:
|
||||||
|
- Vue/Vite 项目:确保 `vite.config.js` 中 `base` 设置为 `'./'`(相对路径)
|
||||||
|
- Ant Design Pro:确保 `config/config.ts` 中 `publicPath` 设置为 `'./'`
|
||||||
|
- 这样可以适配任意部署路径
|
||||||
|
|
||||||
|
3. **构建时间**:
|
||||||
|
- 首次构建较慢(需要下载依赖,1-3 分钟)
|
||||||
|
- 后续构建会利用缓存加速(10-60 秒)
|
||||||
|
|
||||||
|
4. **预览链接**:
|
||||||
|
- 链接格式由环境变量 `WEB_BASE_URL` 决定
|
||||||
|
- 默认为 `http://localhost:8080`
|
||||||
|
- 可在 `.env` 文件中修改
|
||||||
|
|
||||||
|
5. **路径映射**:
|
||||||
|
- URL路径:`/ai-coder/code/{path}`
|
||||||
|
- 文件系统路径:`/workspace/code/{path}`
|
||||||
|
- 后端路由:`/ai-coder/code/{path:path}` → 映射到 `/workspace/code/{path}`
|
||||||
651
skills/web-build/templates/mes-dashboard/index.html
Normal file
651
skills/web-build/templates/mes-dashboard/index.html
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>智能工厂生产实时监控中心</title>
|
||||||
|
<style>
|
||||||
|
/* --- 全局重置与基础样式 --- */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Microsoft YaHei", Arial, sans-serif;
|
||||||
|
background-color: #050a15;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 50% 50%, #0d1a35 0%, #050a15 100%),
|
||||||
|
linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 100% 100%, 30px 30px, 30px 30px;
|
||||||
|
color: #fff;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden; /* 防止滚动,全屏展示 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 颜色变量 --- */
|
||||||
|
:root {
|
||||||
|
--primary-color: #00f0ff;
|
||||||
|
--secondary-color: #0088ff;
|
||||||
|
--warn-color: #ffcc00;
|
||||||
|
--danger-color: #ff4d4d;
|
||||||
|
--bg-panel: rgba(10, 25, 50, 0.6);
|
||||||
|
--border-panel: rgba(0, 240, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 顶部 Header --- */
|
||||||
|
header {
|
||||||
|
height: 8vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: linear-gradient(to bottom, rgba(0,20,40,0.9), transparent);
|
||||||
|
border-bottom: 2px solid var(--border-panel);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--primary-color), transparent);
|
||||||
|
box-shadow: 0 0 10px var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-shadow: 0 0 10px var(--secondary-color);
|
||||||
|
background: linear-gradient(to bottom, #fff, #aaddff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-time {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 主体布局 --- */
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28% 44% 28%; /* 左 中 右 布局 */
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 通用面板样式 --- */
|
||||||
|
.panel {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-panel);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 面板四角装饰 */
|
||||||
|
.panel::before, .panel::after,
|
||||||
|
.panel-corner::before, .panel-corner::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
border-style: solid;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.panel::before { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
|
||||||
|
.panel::after { top: -1px; right: -1px; border-width: 2px 2px 0 0; }
|
||||||
|
.panel-corner { position: absolute; bottom: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
||||||
|
.panel-corner::before { bottom: -1px; left: -1px; border-width: 0 0 2px 2px; }
|
||||||
|
.panel-corner::after { bottom: -1px; right: -1px; border-width: 0 2px 2px 0; }
|
||||||
|
|
||||||
|
.panel:hover {
|
||||||
|
box-shadow: 0 0 15px rgba(0, 240, 255, 0.1);
|
||||||
|
border-color: rgba(0, 240, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #fff;
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 左侧内容 --- */
|
||||||
|
.chart-box {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义进度条列表 */
|
||||||
|
.progress-list {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.progress-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.p-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.p-bar-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.p-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--secondary-color), var(--primary-color));
|
||||||
|
width: 0%; /* 动画用 JS 控制 */
|
||||||
|
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.p-bar-fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 2px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 5px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 环形图 (CSS实现) */
|
||||||
|
.donut-chart {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(var(--primary-color) 0% calc(var(--val) * 1%), rgba(255,255,255,0.1) calc(var(--val) * 1%) 100%);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 240, 255, 0.2);
|
||||||
|
}
|
||||||
|
.donut-chart::before {
|
||||||
|
content: "";
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
background: #0d1a35;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.donut-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.donut-value { font-size: 1.4rem; font-weight: bold; color: #fff; }
|
||||||
|
.donut-label { font-size: 0.8rem; color: #aaa; }
|
||||||
|
|
||||||
|
.device-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.stat-item { text-align: center; }
|
||||||
|
.stat-val { font-size: 1.2rem; font-weight: bold; }
|
||||||
|
.stat-val.online { color: #00ff00; }
|
||||||
|
.stat-val.offline { color: #888; }
|
||||||
|
.stat-val.warning { color: var(--warn-color); }
|
||||||
|
.stat-desc { font-size: 0.8rem; color: #aaa; margin-top: 2px; }
|
||||||
|
|
||||||
|
|
||||||
|
/* --- 中间内容 --- */
|
||||||
|
.center-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-board {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
height: 15vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(0, 136, 255, 0.1);
|
||||||
|
border: 1px solid rgba(0, 136, 255, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.kpi-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(0,240,255,0.1) 0%, transparent 70%);
|
||||||
|
animation: rotate 10s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
.kpi-num {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
z-index: 1;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
text-shadow: 0 0 10px var(--primary-color);
|
||||||
|
}
|
||||||
|
.kpi-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #ccc;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid var(--border-panel);
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模拟 3D 地图/雷达效果 */
|
||||||
|
.radar-grid {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(0, 240, 255, 0.1) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 240, 255, 0.1) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
perspective: 500px;
|
||||||
|
transform: perspective(1000px) rotateX(60deg) scale(1.5);
|
||||||
|
animation: gridMove 20s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes gridMove { from { background-position: 0 0; } to { background-position: 0 50px; } }
|
||||||
|
|
||||||
|
.factory-model {
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
/* 简单的 CSS 旋转动画模拟中心产线 */
|
||||||
|
.circle-ring {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
box-shadow: 0 0 15px var(--primary-color);
|
||||||
|
}
|
||||||
|
.r1 { width: 100px; height: 100px; border-style: dashed; animation: spin 10s linear infinite; }
|
||||||
|
.r2 { width: 180px; height: 180px; border-color: rgba(0,240,255,0.3); border-width: 1px; animation: spinReverse 15s linear infinite; }
|
||||||
|
.r3 { width: 60px; height: 60px; background: rgba(0,240,255,0.1); display: flex; align-items: center; justify-content: center;}
|
||||||
|
|
||||||
|
.factory-icon { font-size: 2rem; }
|
||||||
|
|
||||||
|
@keyframes spin { 100% { transform: translate(-50%, -50%) rotate(360deg); } }
|
||||||
|
@keyframes spinReverse { 100% { transform: translate(-50%, -50%) rotate(-360deg); } }
|
||||||
|
|
||||||
|
/* --- 右侧内容 --- */
|
||||||
|
.alert-list {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.alert-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
animation: fadeIn 0.5s ease;
|
||||||
|
}
|
||||||
|
.alert-time { color: #888; margin-right: 10px; font-size: 0.8rem;}
|
||||||
|
.alert-content { flex: 1; }
|
||||||
|
.alert-level {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.level-high { background: rgba(255, 77, 77, 0.2); color: var(--danger-color); border: 1px solid var(--danger-color); }
|
||||||
|
.level-warn { background: rgba(255, 204, 0, 0.2); color: var(--warn-color); border: 1px solid var(--warn-color); }
|
||||||
|
|
||||||
|
/* SVG 折线图容器 */
|
||||||
|
.svg-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.chart-line {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--primary-color);
|
||||||
|
stroke-width: 2;
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
|
.chart-area {
|
||||||
|
fill: rgba(0, 240, 255, 0.1);
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
.chart-grid line {
|
||||||
|
stroke: rgba(255,255,255,0.1);
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
.chart-axis text {
|
||||||
|
fill: #888;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
main { grid-template-columns: 30% 70%; grid-template-rows: auto auto; }
|
||||||
|
.center-column { grid-row: 1 / 2; grid-column: 2 / 3; }
|
||||||
|
.right-column { grid-row: 2 / 3; grid-column: 1 / 3; display: flex; gap: 15px; height: 30vh;}
|
||||||
|
.left-column { grid-row: 1 / 2; grid-column: 1 / 2; }
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
main { display: flex; flex-direction: column; overflow-y: auto; }
|
||||||
|
body { overflow: auto; height: auto; }
|
||||||
|
.kpi-board { flex-wrap: wrap; height: auto; }
|
||||||
|
.kpi-card { width: 48%; margin-bottom: 10px; height: 100px; }
|
||||||
|
.map-container { height: 300px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="header-logo">🏭 SMART FACTORY</div>
|
||||||
|
<div class="header-title">智能制造MES生产看板</div>
|
||||||
|
<div class="header-time" id="clock">00:00:00</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- 左列:设备与效率 -->
|
||||||
|
<section class="panel left-column">
|
||||||
|
<div class="panel-corner"></div>
|
||||||
|
<div class="panel-title">设备综合效率 (OEE)</div>
|
||||||
|
<div class="chart-box" style="flex-direction: column; justify-content: space-around;">
|
||||||
|
<div class="device-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="donut-chart" style="--val:85">
|
||||||
|
<div class="donut-text">
|
||||||
|
<div class="donut-value">85%</div>
|
||||||
|
<div class="donut-label">OEE</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="device-stats" style="margin-top: 20px;">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-val online">42</div>
|
||||||
|
<div class="stat-desc">运行中</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-val warning">3</div>
|
||||||
|
<div class="stat-desc">待机</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-val offline">1</div>
|
||||||
|
<div class="stat-desc">故障</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel left-column">
|
||||||
|
<div class="panel-corner"></div>
|
||||||
|
<div class="panel-title">车间产出进度</div>
|
||||||
|
<div class="chart-box">
|
||||||
|
<div class="progress-list">
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="p-label"><span>A线 - 电子组装</span><span>92%</span></div>
|
||||||
|
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 92%;"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="p-label"><span>B线 - 注塑成型</span><span>78%</span></div>
|
||||||
|
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 78%; background: linear-gradient(90deg, #ffcc00, #ff9900);"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="p-label"><span>C线 - 包装质检</span><span>45%</span></div>
|
||||||
|
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 45%;"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="p-label"><span>D线 - 激光焊接</span><span>88%</span></div>
|
||||||
|
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 88%;"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 中列:核心监控 -->
|
||||||
|
<section class="center-column">
|
||||||
|
<div class="kpi-board">
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-num" id="kpi-total">24,592</div>
|
||||||
|
<div class="kpi-title">今日总产量 (PCS)</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-num" style="color: #00ff00;">99.2%</div>
|
||||||
|
<div class="kpi-title">良品率</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-num" style="color: #ffcc00;" id="kpi-cycle">4.2s</div>
|
||||||
|
<div class="kpi-title">平均节拍</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel map-container" style="flex:1;">
|
||||||
|
<div class="panel-corner"></div>
|
||||||
|
<div class="radar-grid"></div>
|
||||||
|
<div class="factory-model">
|
||||||
|
<div class="circle-ring r1"></div>
|
||||||
|
<div class="circle-ring r2"></div>
|
||||||
|
<div class="circle-ring r3">
|
||||||
|
<span class="factory-icon">⚙️</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="position:absolute; bottom:20px; left:20px; font-size:0.9rem; color:#aaa;">
|
||||||
|
<div>主轴转速: <span style="color:#fff;" id="motor-speed">1200</span> RPM</div>
|
||||||
|
<div>核心温度: <span style="color:#fff;" id="motor-temp">45.2</span> °C</div>
|
||||||
|
</div>
|
||||||
|
<div style="position:absolute; top:20px; right:20px; text-align:right;">
|
||||||
|
<div style="color:var(--primary-color); border:1px solid var(--primary-color); padding: 5px 10px; font-size:0.8rem;">
|
||||||
|
SYSTEM: ONLINE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 右列:趋势与告警 -->
|
||||||
|
<section class="panel right-column">
|
||||||
|
<div class="panel-corner"></div>
|
||||||
|
<div class="panel-title">实时产量趋势 (24H)</div>
|
||||||
|
<div class="chart-box" id="line-chart-box">
|
||||||
|
<!-- SVG Chart will be injected here by JS -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel right-column">
|
||||||
|
<div class="panel-corner"></div>
|
||||||
|
<div class="panel-title">实时告警信息</div>
|
||||||
|
<div class="chart-box" style="align-items: flex-start;">
|
||||||
|
<div class="alert-list" id="alert-container">
|
||||||
|
<!-- Alerts injected by JS -->
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-time">10:42</span>
|
||||||
|
<span class="alert-level level-warn">警告</span>
|
||||||
|
<span class="alert-content">B线 3号机台 温度偏高</span>
|
||||||
|
</div>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-time">09:15</span>
|
||||||
|
<span class="alert-level level-high">故障</span>
|
||||||
|
<span class="alert-content">C线 缺料停机报警</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- 1. 时钟功能 ---
|
||||||
|
function updateClock() {
|
||||||
|
const now = new Date();
|
||||||
|
const timeString = now.toLocaleTimeString('zh-CN', { hour12: false });
|
||||||
|
const dateString = now.toLocaleDateString('zh-CN');
|
||||||
|
document.getElementById('clock').innerHTML = `${dateString} ${timeString}`;
|
||||||
|
}
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
updateClock();
|
||||||
|
|
||||||
|
// --- 2. 模拟数据动态跳动 ---
|
||||||
|
function fluctuateData() {
|
||||||
|
// 更新产量
|
||||||
|
const totalEl = document.getElementById('kpi-total');
|
||||||
|
let currentTotal = parseInt(totalEl.innerText.replace(/,/g, ''));
|
||||||
|
currentTotal += Math.floor(Math.random() * 5); // 随机增加
|
||||||
|
totalEl.innerText = currentTotal.toLocaleString();
|
||||||
|
|
||||||
|
// 更新节拍
|
||||||
|
const cycleEl = document.getElementById('kpi-cycle');
|
||||||
|
cycleEl.innerText = (4.0 + Math.random() * 0.5).toFixed(1) + 's';
|
||||||
|
|
||||||
|
// 更新机器参数
|
||||||
|
document.getElementById('motor-speed').innerText = 1200 + Math.floor(Math.random() * 50 - 25);
|
||||||
|
document.getElementById('motor-temp').innerText = (45 + Math.random() * 2).toFixed(1);
|
||||||
|
}
|
||||||
|
setInterval(fluctuateData, 2000);
|
||||||
|
|
||||||
|
// --- 3. 绘制 SVG 折线图 (无需 Canvas 库) ---
|
||||||
|
function drawLineChart() {
|
||||||
|
const container = document.getElementById('line-chart-box');
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
|
||||||
|
// 生成模拟数据 (24小时)
|
||||||
|
const dataPoints = [];
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
dataPoints.push(50 + Math.random() * 40); // 50-90之间的数值
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxVal = 100;
|
||||||
|
const stepX = width / (dataPoints.length - 1);
|
||||||
|
|
||||||
|
// 构建路径点
|
||||||
|
let pointsStr = "";
|
||||||
|
let areaPointsStr = `0,${height} `; // 闭合区域起点
|
||||||
|
|
||||||
|
dataPoints.forEach((val, index) => {
|
||||||
|
const x = index * stepX;
|
||||||
|
const y = height - (val / maxVal * height); // 坐标翻转
|
||||||
|
pointsStr += `${x},${y} `;
|
||||||
|
areaPointsStr += `${x},${y} `;
|
||||||
|
});
|
||||||
|
|
||||||
|
areaPointsStr += `${width},${height}`; // 闭合区域终点
|
||||||
|
|
||||||
|
const svgContent = `
|
||||||
|
<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
|
||||||
|
<!-- 网格线 -->
|
||||||
|
<g class="chart-grid">
|
||||||
|
<line x1="0" y1="${height * 0.25}" x2="${width}" y2="${height * 0.25}" />
|
||||||
|
<line x1="0" y1="${height * 0.5}" x2="${width}" y2="${height * 0.5}" />
|
||||||
|
<line x1="0" y1="${height * 0.75}" x2="${width}" y2="${height * 0.75}" />
|
||||||
|
</g>
|
||||||
|
<!-- 面积 -->
|
||||||
|
<polygon points="${areaPointsStr}" class="chart-area" />
|
||||||
|
<!-- 折线 -->
|
||||||
|
<polyline points="${pointsStr}" class="chart-line" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
container.innerHTML = svgContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口大小改变时重绘图表
|
||||||
|
window.addEventListener('resize', drawLineChart);
|
||||||
|
setTimeout(drawLineChart, 100); // 初始化绘制
|
||||||
|
|
||||||
|
// --- 4. 模拟告警滚动 ---
|
||||||
|
const alerts = [
|
||||||
|
{ level: 'warn', text: 'A线 气压波动异常' },
|
||||||
|
{ level: 'high', text: 'AGV 小车受阻 #04' },
|
||||||
|
{ level: 'warn', text: '物料库存预警: 螺丝M4' },
|
||||||
|
{ level: 'normal', text: '班次产量已达标' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function addRandomAlert() {
|
||||||
|
const container = document.getElementById('alert-container');
|
||||||
|
const randomAlert = alerts[Math.floor(Math.random() * alerts.length)];
|
||||||
|
const now = new Date();
|
||||||
|
const timeStr = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'alert-item';
|
||||||
|
|
||||||
|
let levelClass = '';
|
||||||
|
let levelText = '信息';
|
||||||
|
if (randomAlert.level === 'warn') { levelClass = 'level-warn'; levelText = '警告'; }
|
||||||
|
if (randomAlert.level === 'high') { levelClass = 'level-high'; levelText = '故障'; }
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="alert-time">${timeStr}</span>
|
||||||
|
<span class="alert-level ${levelClass}">${levelText}</span>
|
||||||
|
<span class="alert-content">${randomAlert.text}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.insertBefore(div, container.firstChild);
|
||||||
|
|
||||||
|
// 保持列表长度
|
||||||
|
if (container.children.length > 6) {
|
||||||
|
container.removeChild(container.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(addRandomAlert, 5000);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
184
skills/web-build/templates/report-app/README.md
Normal file
184
skills/web-build/templates/report-app/README.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# 报工页面模板
|
||||||
|
|
||||||
|
移动端报工 H5 页面,支持工单选择、物料信息展示和报工提交。
|
||||||
|
|
||||||
|
## 📁 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
report-app/
|
||||||
|
├── index.html # 主页面
|
||||||
|
├── css/custom.css # 自定义样式
|
||||||
|
├── js/
|
||||||
|
│ ├── main.js # 主应用逻辑(MainApp 类)
|
||||||
|
│ └── common.js # 工具函数 + UI组件
|
||||||
|
├── services/
|
||||||
|
│ ├── core.js # API服务 + 认证服务
|
||||||
|
│ └── business.js # 工单服务 + 报工服务
|
||||||
|
├── api_doc/ # API 接口文档(精简版)
|
||||||
|
│ ├── 工单列表_BLACKLAKE-1686655055663532.json
|
||||||
|
│ ├── 生产任务列表_BLACKLAKE-1681109889053785.json
|
||||||
|
│ ├── 报工物料列表_BLACKLAKE-1681369551143844.json
|
||||||
|
│ ├── 批量报工_BLACKLAKE-1681109889053798.json
|
||||||
|
│ └── 报工记录列表_BLACKLAKE-1681109889053794.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 核心接口调用流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 工单列表 → 获取 workOrderId、物料信息
|
||||||
|
↓
|
||||||
|
2. 生产任务列表 → 获取 taskId、executorIds
|
||||||
|
↓
|
||||||
|
3. 报工物料列表 → 获取 progressReportKey、reportUnitId
|
||||||
|
↓
|
||||||
|
4. 批量报工 → 提交报工
|
||||||
|
↓
|
||||||
|
5. 报工记录列表 → 查询最新报工记录
|
||||||
|
```
|
||||||
|
|
||||||
|
### 接口与代码位置对照
|
||||||
|
|
||||||
|
| 接口 | 文档 | 代码位置 | 方法 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| 工单列表 | `api_doc/工单列表_*.json` | `services/business.js:28` | `getWorkOrderList` |
|
||||||
|
| 生产任务列表 | `api_doc/生产任务列表_*.json` | `services/business.js:303` | `getReportRequiredParams` |
|
||||||
|
| 报工物料列表 | `api_doc/报工物料列表_*.json` | `services/business.js:324` | `getReportRequiredParams` |
|
||||||
|
| 批量报工 | `api_doc/批量报工_*.json` | `services/business.js:499` | `submitReport` |
|
||||||
|
| 报工记录列表 | `api_doc/报工记录列表_*.json` | `services/business.js:643` | `getReportRecordsByTask` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 关键代码位置
|
||||||
|
|
||||||
|
### index.html 关键区域
|
||||||
|
|
||||||
|
| 区域 | 元素ID | 行号 | 用途 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| 工单选择 | `#work-order-selector` | ~34 | 工单选择按钮 |
|
||||||
|
| 工单下拉 | `#work-order-dropdown` | ~69 | 工单列表弹窗 |
|
||||||
|
| 空状态 | `#empty-state` | ~111 | 未选工单时显示 |
|
||||||
|
| 物料信息 | `#material-info` | ~122 | 显示物料名称、编号 |
|
||||||
|
| 报工表单 | `#report-form` | ~167 | 数量输入等表单 |
|
||||||
|
| 提交按钮 | `#submit-btn` | ~245 | 提交报工 |
|
||||||
|
|
||||||
|
### js/main.js 关键方法
|
||||||
|
|
||||||
|
| 方法 | 行号 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `onWorkOrderSelected` | ~169 | 工单选择后的处理 |
|
||||||
|
| `extractMaterialDataFromWorkOrder` | ~204 | 从工单提取物料信息 |
|
||||||
|
| `switchToMaterialView` | ~252 | 显示物料信息区域 |
|
||||||
|
| `handleSubmit` | ~323 | 提交报工处理 |
|
||||||
|
| `handleSubmitSuccess` | ~380 | 报工成功后处理 |
|
||||||
|
|
||||||
|
### services/business.js 关键方法
|
||||||
|
|
||||||
|
| 方法 | 行号 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getWorkOrderList` | ~28 | 获取工单列表 |
|
||||||
|
| `processWorkOrderListResponse` | ~85 | 处理工单响应 |
|
||||||
|
| `getReportRequiredParams` | ~287 | 获取报工必填参数 |
|
||||||
|
| `buildReportRequestParams` | ~406 | 构建报工请求 |
|
||||||
|
| `submitReport` | ~499 | 提交报工 |
|
||||||
|
| `getReportRecordsByTask` | ~643 | 查询报工记录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ AI 修改指南
|
||||||
|
|
||||||
|
### 常见修改场景
|
||||||
|
|
||||||
|
#### 场景 1:修改表单字段
|
||||||
|
- **位置**: `index.html` 的 `#report-form` 区域 (~167行)
|
||||||
|
- **模式**: 复制相邻的 `<div class="flex items-center justify-between">` 结构
|
||||||
|
|
||||||
|
#### 场景 2:修改物料信息显示
|
||||||
|
- **位置**:
|
||||||
|
- HTML: `index.html` 的 `#material-info` 区域 (~122行)
|
||||||
|
- JS: `js/main.js` 的 `extractMaterialDataFromWorkOrder` 方法 (~204行)
|
||||||
|
|
||||||
|
#### 场景 3:修改报工提交逻辑
|
||||||
|
- **位置**: `js/main.js` 的 `handleSubmit` 方法 (~323行)
|
||||||
|
- **数据来源**: `this.state.selectedWorkOrder` 和 `this.state.materialData`
|
||||||
|
|
||||||
|
#### 场景 4:修改接口参数
|
||||||
|
- **位置**: `services/business.js` 的对应方法
|
||||||
|
- **必读**: `api_doc/` 下的接口文档
|
||||||
|
|
||||||
|
#### 场景 5:添加报工记录显示
|
||||||
|
- **步骤**:
|
||||||
|
1. 在 `index.html` 添加显示区域
|
||||||
|
2. 在 `js/main.js` 的 `handleSubmitSuccess` 中调用 `reportService.getReportRecordsByTask`
|
||||||
|
3. 渲染报工记录列表
|
||||||
|
|
||||||
|
### 修改原则
|
||||||
|
|
||||||
|
1. **接口字段必须与 api_doc 一致** - 禁止凭空创造字段
|
||||||
|
2. **优先使用现有方法** - 不要重写已有逻辑
|
||||||
|
3. **保持代码风格一致** - 参考相邻代码
|
||||||
|
4. **批量修改** - 同一文件的修改一次完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 关键数据结构
|
||||||
|
|
||||||
|
### 工单数据 (workOrder)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
workOrderId: 12345, // 工单ID(用于报工)
|
||||||
|
workOrderCode: "WO-001", // 工单编号
|
||||||
|
materialInfo: { // 物料信息
|
||||||
|
baseInfo: {
|
||||||
|
id: 100, // 物料ID
|
||||||
|
name: "产品A", // 物料名称(加工物料名称)
|
||||||
|
code: "M001" // 物料编码(物料编号)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
qualifiedHoldAmount: { // 合格数量
|
||||||
|
amount: 100,
|
||||||
|
amountDisplay: "100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 报工必填参数 (requiredParams)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
taskId: 67890, // 生产任务ID(必填)
|
||||||
|
progressReportMaterial: { // 报工物料(必填,不可修改结构)
|
||||||
|
lineId: 111,
|
||||||
|
materialId: 100,
|
||||||
|
reportProcessId: 222
|
||||||
|
},
|
||||||
|
outputMaterialUnit: {
|
||||||
|
id: 333 // 报工单位ID(reportUnitId)
|
||||||
|
},
|
||||||
|
executorIds: [1, 2, 3] // 执行人ID列表
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 样式配置
|
||||||
|
|
||||||
|
主题色配置在 `css/custom.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary-color: #02b980; /* 主色 */
|
||||||
|
--primary-hover: #029968; /* 悬停色 */
|
||||||
|
--primary-light: #e6f7f1; /* 浅色背景 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ 技术栈
|
||||||
|
|
||||||
|
- **HTML5** + **Tailwind CSS** + **原生 JavaScript (ES6+)**
|
||||||
|
- **无需构建**,直接作为静态资源访问
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"接口名称": "工单列表",
|
||||||
|
"接口路径": "/openapi/domain/web/v1/route/med/open/v2/work_order/base/_list",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"代码位置": "services/business.js line 59",
|
||||||
|
|
||||||
|
"请求参数": {
|
||||||
|
"page": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 1,
|
||||||
|
"说明": "页码"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 50,
|
||||||
|
"说明": "每页数量"
|
||||||
|
},
|
||||||
|
"workOrderCode": {
|
||||||
|
"类型": "string",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工单编号(模糊查询)"
|
||||||
|
},
|
||||||
|
"exactWorkOrderCode": {
|
||||||
|
"类型": "string",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工单编号(精确查询)"
|
||||||
|
},
|
||||||
|
"workOrderStatusList": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工单业务状态列表"
|
||||||
|
},
|
||||||
|
"pauseFlag": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "是否暂停 0未暂停 1已暂停"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"响应结构": {
|
||||||
|
"code": "200 表示成功",
|
||||||
|
"message": "返回信息",
|
||||||
|
"data": {
|
||||||
|
"list": "工单数组",
|
||||||
|
"page": "当前页",
|
||||||
|
"total": "总条数"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"关键响应字段": {
|
||||||
|
"data.list[].workOrderId": "number - 工单ID(必填,用于查询任务和报工)",
|
||||||
|
"data.list[].workOrderCode": "string - 工单编号(显示用)",
|
||||||
|
"data.list[].materialInfo": "object - 完整物料信息对象,包含 baseInfo、attribute、conversions、unit 等",
|
||||||
|
"data.list[].materialInfo.baseInfo.id": "number - 物料ID(主产出物料ID)",
|
||||||
|
"data.list[].materialInfo.baseInfo.name": "string - 物料名称(显示:加工物料名称)",
|
||||||
|
"data.list[].materialInfo.baseInfo.code": "string - 物料编码(显示:物料编号)",
|
||||||
|
"data.list[].materialInfo.baseInfo.specification": "string - 规格",
|
||||||
|
"data.list[].materialInfo.conversions[]": "array - 单位转换关系数组",
|
||||||
|
"data.list[].materialInfo.conversions[].fromUnitId": "number - 基本单位ID",
|
||||||
|
"data.list[].materialInfo.conversions[].fromUnitName": "string - 基本单位名称",
|
||||||
|
"data.list[].materialInfo.conversions[].toUnitId": "number - 转换单位ID",
|
||||||
|
"data.list[].materialInfo.conversions[].toUnitName": "string - 转换单位名称",
|
||||||
|
"data.list[].materialInfo.unit": "object - 物料单位信息",
|
||||||
|
"data.list[].materialInfo.unit.id": "number - 单位ID",
|
||||||
|
"data.list[].materialInfo.unit.code": "string - 单位编码",
|
||||||
|
"data.list[].materialInfo.unit.name": "string - 单位名称",
|
||||||
|
"data.list[].qualifiedHoldAmount": "BaseAmountDisplay对象 - 合格报工数(显示用)",
|
||||||
|
"data.list[].qualifiedHoldAmount.amount": "number - 合格数量",
|
||||||
|
"data.list[].qualifiedHoldAmount.amountDisplay": "string - 合格数量显示",
|
||||||
|
"data.list[].disqualifiedHoldAmount": "BaseAmountDisplay对象 - 不良报工数(显示用)",
|
||||||
|
"data.list[].disqualifiedHoldAmount.amount": "number - 不良数量",
|
||||||
|
"data.list[].totalHoldAmount": "BaseAmountDisplay对象 - 总报工数(显示用)",
|
||||||
|
"data.list[].totalHoldAmount.amount": "number - 总数量",
|
||||||
|
"data.list[].expectedAmount": "BaseAmountDisplay对象 - 预计生产数",
|
||||||
|
"data.list[].plannedAmount": "BaseAmountDisplay对象 - 计划生产数",
|
||||||
|
"data.list[].createdAt": "number - 创建时间戳",
|
||||||
|
"data.list[].updatedAt": "number - 更新时间戳",
|
||||||
|
"data.list[].plannedStartTime": "number - 计划开始时间戳",
|
||||||
|
"data.list[].plannedEndTime": "number - 计划完工时间戳",
|
||||||
|
"data.list[].pauseFlag": "number - 暂停状态 1:是 0:否",
|
||||||
|
"data.list[].workOrderStatus": "object - 工单状态",
|
||||||
|
"data.list[].workOrderStatus.code": "number - 状态码",
|
||||||
|
"data.list[].workOrderStatus.message": "string - 状态描述",
|
||||||
|
"data.list[].workOrderType": "object - 工单类型",
|
||||||
|
"data.list[].processRoute": "object - 工艺路线信息",
|
||||||
|
"data.list[].processRoute.id": "number - 工艺路线ID",
|
||||||
|
"data.list[].processRoute.code": "string - 工艺路线编码",
|
||||||
|
"data.list[].processRoute.name": "string - 工艺路线名称"
|
||||||
|
},
|
||||||
|
|
||||||
|
"BaseAmountDisplay 结构": {
|
||||||
|
"amount": "number - 数量",
|
||||||
|
"amountDisplay": "string - 显示文本(无科学计数法)",
|
||||||
|
"unitId": "number - 单位ID",
|
||||||
|
"unitCode": "string - 单位编码",
|
||||||
|
"unitName": "string - 单位名称"
|
||||||
|
},
|
||||||
|
|
||||||
|
"使用示例": "参见 services/business.js:28-141 的 getWorkOrderList 方法和 processWorkOrderListResponse 方法"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
"接口名称": "批量报工",
|
||||||
|
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"代码位置": "services/business.js line 516",
|
||||||
|
|
||||||
|
"请求参数": {
|
||||||
|
"taskId": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "生产任务ID(从生产任务列表获取)"
|
||||||
|
},
|
||||||
|
"progressReportMaterial": {
|
||||||
|
"类型": "object",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "报工物料对象(从报工物料列表获取)",
|
||||||
|
"结构": {
|
||||||
|
"lineId": "number - 物料行ID",
|
||||||
|
"materialId": "number - 物料ID",
|
||||||
|
"reportProcessId": "number - 报工工序ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qcStatus": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 1,
|
||||||
|
"说明": "质量状态(1:合格 2:让步合格 3:代检 4:不合格)"
|
||||||
|
},
|
||||||
|
"reportType": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 2,
|
||||||
|
"说明": "报工方式(1:扫码报工-合格 2:记录报工-合格 3:扫码报工-不合格 4:记录报工-不合格 5:打码报工-合格 6:打码报工-不合格)"
|
||||||
|
},
|
||||||
|
"progressReportItems": {
|
||||||
|
"类型": "array",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "报工详情数组",
|
||||||
|
"结构": [{
|
||||||
|
"executorIds": "number[] - 执行人ID列表",
|
||||||
|
"progressReportMaterialItems": "array - 物料报工项",
|
||||||
|
"progressReportMaterialItems[].reportAmount": "number - 报工数量(必填)",
|
||||||
|
"progressReportMaterialItems[].reportUnitId": "number - 报工单位ID(必填,从 outputMaterialUnit.id 获取)",
|
||||||
|
"progressReportMaterialItems[].remark": "string - 备注(可选)"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"storageLocationId": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "仓位ID,当前预置为 1716848012872791"
|
||||||
|
},
|
||||||
|
"reportStartTime": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "报工开始时间(毫秒时间戳)"
|
||||||
|
},
|
||||||
|
"reportEndTime": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "报工结束时间(毫秒时间戳)"
|
||||||
|
},
|
||||||
|
"actualExecutorIds": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "实际执行人ID列表"
|
||||||
|
},
|
||||||
|
"actualEquipmentIds": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "设备ID列表"
|
||||||
|
},
|
||||||
|
"workHour": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工时"
|
||||||
|
},
|
||||||
|
"workHourUnit": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工时单位"
|
||||||
|
},
|
||||||
|
"qcDefectReasonIds": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "不良原因ID列表(质量状态为不合格时)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"响应结构": {
|
||||||
|
"code": "200 表示成功",
|
||||||
|
"message": "返回信息",
|
||||||
|
"data": {
|
||||||
|
"messageTraceId": "string - 消息追踪ID",
|
||||||
|
"progressReportRecordIds": "number[] - 报工记录ID列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"关键说明": [
|
||||||
|
"必须先调用 getReportRequiredParams 获取 taskId、progressReportMaterial、reportUnitId 等必填参数",
|
||||||
|
"progressReportMaterial 对象的结构不可修改,必须从报工物料列表接口获取",
|
||||||
|
"reportUnitId 必须从报工物料列表的 outputMaterialUnit.id 获取",
|
||||||
|
"executorIds 必须从生产任务列表的 executorList[].id 获取"
|
||||||
|
],
|
||||||
|
|
||||||
|
"完整调用流程示例": {
|
||||||
|
"步骤1": "查询工单列表 -> 获取 workOrderId 和物料信息",
|
||||||
|
"步骤2": "查询生产任务 -> 获取 taskId 和 executorIds",
|
||||||
|
"步骤3": "查询报工物料 -> 获取 progressReportKey 和 reportUnitId",
|
||||||
|
"步骤4": "构建报工参数 -> 调用批量报工接口",
|
||||||
|
"步骤5": "查询报工记录 -> 显示最新报工信息"
|
||||||
|
},
|
||||||
|
|
||||||
|
"使用示例": "参见 services/business.js:406-482 的 buildReportRequestParams 方法和 submitReport 方法"
|
||||||
|
}
|
||||||
|
|
||||||
106
skills/web-build/templates/report-app/api_doc/批量报工_简化版.json
Normal file
106
skills/web-build/templates/report-app/api_doc/批量报工_简化版.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"接口名称": "批量报工",
|
||||||
|
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"代码位置": "services/business.js line 516",
|
||||||
|
|
||||||
|
"请求参数": {
|
||||||
|
"taskId": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "生产任务ID(从生产任务列表获取)"
|
||||||
|
},
|
||||||
|
"progressReportMaterial": {
|
||||||
|
"类型": "object",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "报工物料对象(从报工物料列表获取)",
|
||||||
|
"结构": {
|
||||||
|
"lineId": "number - 物料行ID",
|
||||||
|
"materialId": "number - 物料ID",
|
||||||
|
"reportProcessId": "number - 报工工序ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qcStatus": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 1,
|
||||||
|
"说明": "质量状态(1:合格 2:让步合格 3:代检 4:不合格)"
|
||||||
|
},
|
||||||
|
"reportType": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 2,
|
||||||
|
"说明": "报工方式(1:扫码报工-合格 2:记录报工-合格 3:扫码报工-不合格 4:记录报工-不合格 5:打码报工-合格 6:打码报工-不合格)"
|
||||||
|
},
|
||||||
|
"progressReportItems": {
|
||||||
|
"类型": "array",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "报工详情数组",
|
||||||
|
"结构": [{
|
||||||
|
"executorIds": "number[] - 执行人ID列表",
|
||||||
|
"progressReportMaterialItems": "array - 物料报工项",
|
||||||
|
"progressReportMaterialItems[].reportAmount": "number - 报工数量(必填)",
|
||||||
|
"progressReportMaterialItems[].reportUnitId": "number - 报工单位ID(必填,从 outputMaterialUnit.id 获取)",
|
||||||
|
"progressReportMaterialItems[].remark": "string - 备注(可选)"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"storageLocationId": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "仓位ID,当前预置为 1716848012872791"
|
||||||
|
},
|
||||||
|
"reportStartTime": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "报工开始时间(毫秒时间戳)"
|
||||||
|
},
|
||||||
|
"reportEndTime": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "报工结束时间(毫秒时间戳)"
|
||||||
|
},
|
||||||
|
"actualExecutorIds": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "实际执行人ID列表"
|
||||||
|
},
|
||||||
|
"actualEquipmentIds": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "设备ID列表"
|
||||||
|
},
|
||||||
|
"workHour": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工时"
|
||||||
|
},
|
||||||
|
"workHourUnit": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工时单位"
|
||||||
|
},
|
||||||
|
"qcDefectReasonIds": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "不良原因ID列表(质量状态为不合格时)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"响应结构": {
|
||||||
|
"code": "200 表示成功",
|
||||||
|
"message": "返回信息",
|
||||||
|
"data": {
|
||||||
|
"messageTraceId": "string - 消息追踪ID",
|
||||||
|
"progressReportRecordIds": "number[] - 报工记录ID列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"关键说明": [
|
||||||
|
"必须先调用 getReportRequiredParams 获取 taskId、progressReportMaterial、reportUnitId 等必填参数",
|
||||||
|
"progressReportMaterial 对象的结构不可修改,必须从报工物料列表接口获取",
|
||||||
|
"reportUnitId 必须从报工物料列表的 outputMaterialUnit.id 获取"
|
||||||
|
],
|
||||||
|
|
||||||
|
"使用示例": "参见 services/business.js:406-482 的 buildReportRequestParams 方法和 submitReport 方法"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"接口名称": "报工物料列表",
|
||||||
|
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list_progress_report_materials",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"代码位置": "services/business.js line 324",
|
||||||
|
|
||||||
|
"请求参数": {
|
||||||
|
"taskId": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "生产任务ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"响应结构": {
|
||||||
|
"code": "200 表示成功",
|
||||||
|
"message": "返回信息",
|
||||||
|
"data": {
|
||||||
|
"outputMaterials": "产出物料数组",
|
||||||
|
"inputMaterials": "投入物料数组"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"关键响应字段 - outputMaterials[]": {
|
||||||
|
"materialInfo": "object - 物料信息,包含 baseInfo(id、name、code 等)",
|
||||||
|
"materialInfo.baseInfo.id": "number - 物料ID(主产出物料ID)",
|
||||||
|
"materialInfo.baseInfo.name": "string - 物料名称(显示:加工物料名称)",
|
||||||
|
"materialInfo.baseInfo.code": "string - 物料编码(显示:物料编号)",
|
||||||
|
"materialInfo.baseInfo.specification": "string - 规格",
|
||||||
|
"outputMaterialUnit": "object - 报工单位信息(必填,用于报工)",
|
||||||
|
"outputMaterialUnit.id": "number - 单位ID(报工时必需:reportUnitId)",
|
||||||
|
"outputMaterialUnit.code": "string - 单位编码",
|
||||||
|
"outputMaterialUnit.name": "string - 单位名称(如:个、kg)",
|
||||||
|
"mainFlag": "boolean - 是否为主产出物料(优先选择 mainFlag=true 的物料)",
|
||||||
|
"reportType": "array<number> - 可报工方式列表",
|
||||||
|
"warehousingFlag": "boolean - 是否入库",
|
||||||
|
"autoWarehousingFlag": "boolean - 是否自动入库",
|
||||||
|
"progressReportKey": "object - 报工关系信息(必填,用于构建报工参数)",
|
||||||
|
"progressReportKey.lineId": "number - 物料行ID(报工时必需)",
|
||||||
|
"progressReportKey.materialId": "number - 物料ID(报工时必需)",
|
||||||
|
"progressReportKey.reportProcessId": "number - 报工工序ID(报工时必需)",
|
||||||
|
"outputMaterialAmount": "object - 产出物料数量信息",
|
||||||
|
"outputMaterialAmount.amount": "number - 数量",
|
||||||
|
"outputMaterialAmount.amountDisplay": "string - 数量显示"
|
||||||
|
},
|
||||||
|
|
||||||
|
"使用场景": "用于获取报工必填参数,特别是 progressReportKey 和 outputMaterialUnit",
|
||||||
|
"使用示例": "参见 services/business.js:318-389 的 getReportRequiredParams 方法"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"接口名称": "报工记录列表",
|
||||||
|
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"代码位置": "services/business.js line 718",
|
||||||
|
|
||||||
|
"请求参数": {
|
||||||
|
"page": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 1,
|
||||||
|
"说明": "页码"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 200,
|
||||||
|
"说明": "每页数量"
|
||||||
|
},
|
||||||
|
"taskIds": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "生产任务ID列表(与 workOrderIdList 至少提供一个)"
|
||||||
|
},
|
||||||
|
"workOrderIdList": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工单ID列表(与 taskIds 至少提供一个)"
|
||||||
|
},
|
||||||
|
"reportTimeFrom": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "报工时间From(闭区间),毫秒时间戳"
|
||||||
|
},
|
||||||
|
"reportTimeTo": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "报工时间To(开区间),毫秒时间戳"
|
||||||
|
},
|
||||||
|
"sorter": {
|
||||||
|
"类型": "array",
|
||||||
|
"必填": false,
|
||||||
|
"默认": [{"field": "reportTime", "order": "desc"}],
|
||||||
|
"说明": "排序条件列表",
|
||||||
|
"结构": [{
|
||||||
|
"field": "string - 排序字段(如 reportTime、taskCode)",
|
||||||
|
"order": "string - 排序规律(asc 升序 / desc 降序,默认 asc)"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"processIdList": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "工序ID列表"
|
||||||
|
},
|
||||||
|
"executorIdList": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "可执行人ID列表"
|
||||||
|
},
|
||||||
|
"qcStatusList": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": false,
|
||||||
|
"说明": "质量状态列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"响应结构": {
|
||||||
|
"code": "200 表示成功",
|
||||||
|
"message": "返回信息",
|
||||||
|
"data": {
|
||||||
|
"list": "报工记录数组",
|
||||||
|
"page": "当前页",
|
||||||
|
"total": "总条数"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"关键响应字段 - list[]": {
|
||||||
|
"id": "number - 报工记录详情id",
|
||||||
|
"lineId": "number - 物料行id",
|
||||||
|
"reportRecordId": "number - 报工记录id",
|
||||||
|
"taskId": "number - 生产任务Id",
|
||||||
|
"taskCode": "string - 生产任务编号",
|
||||||
|
"workOrderId": "number - 工单Id",
|
||||||
|
"workOrderCode": "string - 工单编号(显示用)",
|
||||||
|
"processId": "number - 工序Id",
|
||||||
|
"processCode": "string - 工序编号",
|
||||||
|
"processName": "string - 工序名称",
|
||||||
|
"mainMaterialId": "number - 工单物料id",
|
||||||
|
"mainMaterialCode": "string - 工单物料编号",
|
||||||
|
"mainMaterialName": "string - 工单物料名称",
|
||||||
|
"materialInfo": "object - 报工物料信息",
|
||||||
|
"materialInfo.baseInfo.id": "number - 物料ID",
|
||||||
|
"materialInfo.baseInfo.name": "string - 物料名称",
|
||||||
|
"materialInfo.baseInfo.code": "string - 物料编码",
|
||||||
|
"reportBaseAmount": "object - 报工数量对象",
|
||||||
|
"reportBaseAmount.amount": "number - 报工数量(显示:报工数量 10个)",
|
||||||
|
"reportBaseAmount.unit": "object - 单位信息",
|
||||||
|
"reportBaseAmount.unit.name": "string - 单位名称",
|
||||||
|
"reportBaseAmountDisplay": "object - 报工数量显示对象",
|
||||||
|
"reportBaseAmountDisplay.amount": "number - 报工数量",
|
||||||
|
"reportBaseAmountDisplay.amountDisplay": "string - 报工数量显示文本",
|
||||||
|
"reportBaseAmountDisplay.unitCode": "string - 单位编码",
|
||||||
|
"reportBaseAmountDisplay.unitId": "number - 单位ID",
|
||||||
|
"reportBaseAmountDisplay.unitName": "string - 单位名称",
|
||||||
|
"reportTime": "number - 报工时间,毫秒时间戳(显示:报工时间 2024-11-26 14:30)",
|
||||||
|
"reporter": "object - 报工人员",
|
||||||
|
"reporter.id": "number - 报工人ID",
|
||||||
|
"reporter.name": "string - 报工人姓名",
|
||||||
|
"reporter.username": "string - 报工人用户名",
|
||||||
|
"producers": "array - 生产人员数组",
|
||||||
|
"producers[].id": "number - 生产人ID",
|
||||||
|
"producers[].name": "string - 生产人姓名",
|
||||||
|
"qcStatus": "object - 质量状态",
|
||||||
|
"qcStatus.code": "number - 质量状态码(1:合格 4:不合格)",
|
||||||
|
"qcStatus.message": "string - 质量状态描述",
|
||||||
|
"workHourUnit": "object - 工时单位",
|
||||||
|
"startTime": "number - 报工开始时间,毫秒时间戳(可选)",
|
||||||
|
"endTime": "number - 报工结束时间,毫秒时间戳(可选)",
|
||||||
|
"workHour": "number - 工时(可选)",
|
||||||
|
"remark": "string - 备注(可选)",
|
||||||
|
"reportRecordCode": "string - 报工记录编号(可选)",
|
||||||
|
"createdAt": "number - 创建时间戳",
|
||||||
|
"updatedAt": "number - 更新时间戳"
|
||||||
|
},
|
||||||
|
|
||||||
|
"使用示例": "参见 services/business.js:643-752 的 getReportRecordsByTask 方法"
|
||||||
|
}
|
||||||
|
|
||||||
28
skills/web-build/templates/report-app/api_doc/接口说明.txt
Executable file
28
skills/web-build/templates/report-app/api_doc/接口说明.txt
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
# 接口文档
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
本文档详细说明了本项目涉及的所有接口调用情况。
|
||||||
|
|
||||||
|
## 🔄 接口调用流程
|
||||||
|
|
||||||
|
### 1. 页面初始化流程
|
||||||
|
```
|
||||||
|
页面加载 → url获取 code → 换取 access_token提供后续open接口调用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 选择工单
|
||||||
|
```
|
||||||
|
用户选择特定工单 → 选择后回显工单上的物料信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 报工提交流程
|
||||||
|
```
|
||||||
|
用户填写报工数据 → 点击提交 → 构建批量报工请求参数 → 调用批量报工接口 → 清空表单
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 查看报工记录(可选)
|
||||||
|
```
|
||||||
|
根据用户报工时使用的生产 taskId 或者 workOrderId → 构建报工记录的请求参数 → 调用报工记录的接口 → 渲染数据
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"接口名称": "生产任务列表",
|
||||||
|
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/produce_task/_list",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"代码位置": "services/business.js line 303",
|
||||||
|
|
||||||
|
"请求参数": {
|
||||||
|
"page": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 1,
|
||||||
|
"说明": "页码"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"类型": "number",
|
||||||
|
"必填": false,
|
||||||
|
"默认": 10,
|
||||||
|
"说明": "每页数量"
|
||||||
|
},
|
||||||
|
"workOrderIdList": {
|
||||||
|
"类型": "number[]",
|
||||||
|
"必填": true,
|
||||||
|
"说明": "工单ID列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"响应结构": {
|
||||||
|
"code": "200 表示成功",
|
||||||
|
"message": "返回信息",
|
||||||
|
"data": {
|
||||||
|
"list": "生产任务数组",
|
||||||
|
"page": "当前页",
|
||||||
|
"total": "总条数"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"关键响应字段": {
|
||||||
|
"data.list[].taskId": "number - 生产任务ID(必填,用于报工)",
|
||||||
|
"data.list[].taskCode": "string - 生产任务编号",
|
||||||
|
"data.list[].workOrderId": "number - 工单ID",
|
||||||
|
"data.list[].workOrderCode": "string - 工单编号",
|
||||||
|
"data.list[].executorList": "array - 执行人列表(报工时需要)",
|
||||||
|
"data.list[].executorList[].id": "number - 执行人ID",
|
||||||
|
"data.list[].executorList[].code": "string - 执行人编号",
|
||||||
|
"data.list[].executorList[].name": "string - 执行人姓名",
|
||||||
|
"data.list[].executorList[].username": "string - 用户名",
|
||||||
|
"data.list[].processId": "number - 工序ID",
|
||||||
|
"data.list[].processCode": "string - 工序编号",
|
||||||
|
"data.list[].processName": "string - 工序名称",
|
||||||
|
"data.list[].taskStatus": "object - 任务状态",
|
||||||
|
"data.list[].taskStatus.code": "number - 状态码",
|
||||||
|
"data.list[].taskStatus.message": "string - 状态描述",
|
||||||
|
"data.list[].materialInfo": "object - 主产出物料信息",
|
||||||
|
"data.list[].materialInfo.baseInfo.id": "number - 物料ID",
|
||||||
|
"data.list[].materialInfo.baseInfo.name": "string - 物料名称",
|
||||||
|
"data.list[].materialInfo.baseInfo.code": "string - 物料编码"
|
||||||
|
},
|
||||||
|
|
||||||
|
"使用场景": "用于获取工单下的生产任务信息,特别是获取 taskId 用于后续报工操作",
|
||||||
|
"使用示例": "参见 services/business.js:287-317 的 getReportRequiredParams 方法中的任务查询部分"
|
||||||
|
}
|
||||||
3
skills/web-build/templates/report-app/config.json
Normal file
3
skills/web-build/templates/report-app/config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"welcome_message": "右侧是当前<strong>生产任务执行(报工页面)</strong>的样式。<br><br>您可以:<ul><li>通过对话描述需要修改的部分</li><li>直接上传手绘稿告诉我您期望的样式</li></ul>我将通过AI魔法帮您心想事成。"
|
||||||
|
}
|
||||||
391
skills/web-build/templates/report-app/css/custom.css
Normal file
391
skills/web-build/templates/report-app/css/custom.css
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
/**
|
||||||
|
* 报工页面自定义样式
|
||||||
|
* 补充Tailwind CSS的样式定义
|
||||||
|
* 主题色: #02B980
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 全局样式 */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题色变量定义 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #02B980;
|
||||||
|
--primary-hover: #029968;
|
||||||
|
--primary-light: #E6F7F1;
|
||||||
|
--primary-dark: #016B4A;
|
||||||
|
--primary-shadow: rgba(2, 185, 128, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题色相关的通用样式 */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background-color: #9CA3AF;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-primary {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-light {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-primary {
|
||||||
|
--tw-ring-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉菜单动画 */
|
||||||
|
#work-order-dropdown {
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-dropdown.show {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-dropdown .bg-white {
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-dropdown.show .bg-white {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉箭头旋转动画 */
|
||||||
|
#dropdown-arrow.rotate {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工单列表项样式 */
|
||||||
|
.work-order-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-order-item:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-order-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-order-item.selected {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框焦点样式增强 */
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(2, 185, 128, 0.1);
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮点击效果 */
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题色按钮悬停效果 */
|
||||||
|
button[style*="background-color: var(--primary-color)"]:hover {
|
||||||
|
background-color: var(--primary-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[style*="border-color: var(--primary-color)"]:hover {
|
||||||
|
background-color: var(--primary-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 复选框和单选框主题色 */
|
||||||
|
input[type="checkbox"]:checked,
|
||||||
|
input[type="radio"]:checked {
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:focus,
|
||||||
|
input[type="radio"]:focus {
|
||||||
|
box-shadow: 0 0 0 3px var(--primary-shadow) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义CSS类 */
|
||||||
|
.hover-primary-bg:hover {
|
||||||
|
background-color: var(--primary-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-primary-light-bg:hover {
|
||||||
|
background-color: var(--primary-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-primary-ring:focus {
|
||||||
|
--tw-ring-color: var(--primary-color) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-primary:checked {
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-primary:focus {
|
||||||
|
box-shadow: 0 0 0 3px var(--primary-shadow) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态标签动画 */
|
||||||
|
.status-tag {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
/* 移动端下拉菜单全屏显示 */
|
||||||
|
#work-order-dropdown .bg-white {
|
||||||
|
min-height: 80vh;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端表单字段间距调整 */
|
||||||
|
#report-form .space-y-4 > * + * {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端工单选择区域优化 */
|
||||||
|
#work-order-selector {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端物料信息卡片优化 */
|
||||||
|
#material-info .bg-white {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端状态标签优化 */
|
||||||
|
.status-tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端输入框优化 */
|
||||||
|
input[type="number"], input[type="text"] {
|
||||||
|
font-size: 16px; /* 防止iOS缩放 */
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端按钮优化 */
|
||||||
|
#submit-btn {
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板设备优化 */
|
||||||
|
@media (min-width: 641px) and (max-width: 1024px) {
|
||||||
|
/* 平板端下拉菜单适配 */
|
||||||
|
#work-order-dropdown .bg-white {
|
||||||
|
min-height: 60vh;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端表单布局优化 */
|
||||||
|
#report-form .bg-white {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏设备优化 */
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
/* 桌面端容器最大宽度 */
|
||||||
|
#app {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端下拉菜单 */
|
||||||
|
#work-order-dropdown .bg-white {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 5vh;
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 横屏适配 */
|
||||||
|
@media (orientation: landscape) and (max-height: 600px) {
|
||||||
|
/* 横屏时减少垂直间距 */
|
||||||
|
#header-nav {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-dropdown .bg-white {
|
||||||
|
min-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#empty-state {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#empty-state .w-48 {
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高分辨率屏幕优化 */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
/* 高分辨率屏幕下的图标和边框优化 */
|
||||||
|
.border {
|
||||||
|
border-width: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
shape-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式支持 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
/* 可以添加深色模式样式 */
|
||||||
|
/* 目前保持浅色主题 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 减少动画偏好 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-dropdown,
|
||||||
|
#work-order-dropdown .bg-white,
|
||||||
|
#dropdown-arrow {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 触摸设备优化 */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
/* 增大触摸目标 */
|
||||||
|
button, .cursor-pointer {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工单列表项触摸优化 */
|
||||||
|
.work-order-item {
|
||||||
|
padding: 1rem;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框触摸优化 */
|
||||||
|
input, select, textarea {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无障碍优化 */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
/* 高对比度模式 */
|
||||||
|
.border-gray-300 {
|
||||||
|
border-color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray-50 {
|
||||||
|
background-color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态样式 */
|
||||||
|
.loading {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
#work-order-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-list::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#work-order-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
257
skills/web-build/templates/report-app/index.html
Normal file
257
skills/web-build/templates/report-app/index.html
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>报工 - 工单管理系统</title>
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- 自定义样式 -->
|
||||||
|
<link rel="stylesheet" href="css/custom.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 min-h-screen">
|
||||||
|
<!-- 应用容器 -->
|
||||||
|
<div id="app" class="min-h-screen flex flex-col">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<header id="header-nav" class="px-4 py-3 flex items-center justify-between" style="background-color: var(--primary-color)">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<button id="back-btn" class="p-2 rounded-lg transition-colors hover-primary-bg">
|
||||||
|
<svg class="w-6 h-6 text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: white;">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<h1 class="text-lg font-semibold text-gray-800" style="color: white;">标准报工页面</h1>
|
||||||
|
|
||||||
|
<!-- 占位元素保持居中 -->
|
||||||
|
<div class="w-10"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main class="flex-1 px-4 py-6">
|
||||||
|
<!-- 工单选择区域 -->
|
||||||
|
<div id="work-order-selector" class="bg-white rounded-lg shadow-sm p-4 mb-6 cursor-pointer hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<!-- 工单图标占位符 -->
|
||||||
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: var(--primary-light)">
|
||||||
|
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
|
||||||
|
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 001 1h6a1 1 0 001-1V3a2 2 0 012 2v6.5a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 014 11.5V5z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工单文字 -->
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-800 font-medium">工单</span>
|
||||||
|
<span id="selected-work-order-text" class="text-gray-500 ml-2">请选择</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<!-- 扫码按钮 -->
|
||||||
|
<button id="scan-btn" class="p-2 border-2 rounded-lg transition-colors border-primary hover-primary-light-bg">
|
||||||
|
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h2M4 4h5v5H4V4zm11 0h5v5h-5V4zM4 15h5v5H4v-5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 下拉箭头 -->
|
||||||
|
<svg id="dropdown-arrow" class="w-5 h-5 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工单下拉菜单 -->
|
||||||
|
<div id="work-order-dropdown" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
|
||||||
|
<div class="bg-white rounded-t-lg mt-auto min-h-[70vh] flex flex-col">
|
||||||
|
<!-- 下拉菜单头部 -->
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800">工单编号</h2>
|
||||||
|
<button id="close-dropdown" class="p-2 hover:bg-gray-100 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="p-4 border-b">
|
||||||
|
<div class="relative">
|
||||||
|
<input id="search-input" type="text" placeholder="搜索工单编号"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工单列表 -->
|
||||||
|
<div id="work-order-list" class="flex-1 overflow-y-auto">
|
||||||
|
<!-- 工单列表项将通过JavaScript动态生成 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作区 -->
|
||||||
|
<div class="p-4 border-t bg-gray-50 flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input id="select-all" type="checkbox" class="rounded border-gray-300 checkbox-primary">
|
||||||
|
<span class="text-sm text-gray-600">全选</span>
|
||||||
|
</label>
|
||||||
|
<span id="selected-count" class="text-sm text-gray-600">已选择0个</span>
|
||||||
|
</div>
|
||||||
|
<button id="confirm-selection" class="btn-primary text-white px-6 py-2 rounded-lg transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed" disabled>
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态区域 -->
|
||||||
|
<div id="empty-state" class="text-center py-12">
|
||||||
|
<!-- 插图占位符 -->
|
||||||
|
<div class="w-48 h-48 mx-auto mb-6 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||||
|
<svg class="w-24 h-24 text-gray-300" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm8 2a1 1 0 100 2h2a1 1 0 100-2H11z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 text-lg">先选择工单</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物料信息区域 -->
|
||||||
|
<div id="material-info" class="hidden">
|
||||||
|
<!-- 工单信息卡片 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-4">
|
||||||
|
<div class="flex items-center justify-between mb-3" style="display: none;">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: var(--primary-light)">
|
||||||
|
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
|
||||||
|
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 001 1h6a1 1 0 001-1V3a2 2 0 012 2v6.5a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 014 11.5V5z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-800 font-medium">工单</span>
|
||||||
|
<span id="selected-work-order-id" class="text-gray-800 font-semibold ml-2"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="p-2 border-2 rounded-lg transition-colors border-primary hover-primary-light-bg">
|
||||||
|
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h2M4 4h5v5H4V4zm11 0h5v5h-5V4zM4 15h5v5H4v-5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物料信息 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 id="material-name" class="text-lg font-semibold text-gray-800"></h3>
|
||||||
|
<p id="material-code" class="text-gray-600"></p>
|
||||||
|
|
||||||
|
<!-- 状态标签 -->
|
||||||
|
<div class="flex space-x-2 mt-3">
|
||||||
|
<span class="px-3 py-1 rounded-full text-sm font-medium" style="background-color: var(--primary-light); color: var(--primary-dark)">
|
||||||
|
合格 <span id="qualified-count">0</span>
|
||||||
|
</span>
|
||||||
|
<span class="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
|
||||||
|
不良 <span id="defective-count">0</span>
|
||||||
|
</span>
|
||||||
|
<span class="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-sm font-medium">
|
||||||
|
待产 <span id="pending-count">0</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 报工表单区域 -->
|
||||||
|
<div id="report-form" class="hidden">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">报工</h2>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-sm p-4 space-y-4">
|
||||||
|
<!-- 数量输入 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
数量 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="quantity" type="number" value="1" min="0"
|
||||||
|
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||||
|
<span class="text-gray-500"> 个</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 辅助数量输入 -->
|
||||||
|
<div class="flex items-center justify-between" style="display: none;">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
辅助数量 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="auxiliary-quantity" type="number" value="1" min="0"
|
||||||
|
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||||
|
<span id="auxName" class="text-gray-500">公斤</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工时输入 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
工时
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="working-hours" type="number" value="1" min="0"
|
||||||
|
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||||
|
<span class="text-gray-500">小时</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 报工开始时间 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
报工开始时间
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="report-start-time" type="datetime-local" value="1" min="0"
|
||||||
|
class="w-50 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 报工结束时间 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
报工结束时间
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="report-end-time" type="datetime-local" value="1" min="0"
|
||||||
|
class="w-50 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 备注输入 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-gray-700 font-medium">
|
||||||
|
备注
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input id="remark" type="text" value="" placeholder="请输入备注"
|
||||||
|
class="w-60 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<div id="submit-section" class="hidden p-4">
|
||||||
|
<button id="submit-btn" class="w-full btn-primary text-white py-3 rounded-lg font-semibold transition-colors">
|
||||||
|
提交
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript文件 -->
|
||||||
|
<script src="js/common.js"></script>
|
||||||
|
<script src="services/core.js"></script>
|
||||||
|
<script src="services/business.js"></script>
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
856
skills/web-build/templates/report-app/js/common.js
Normal file
856
skills/web-build/templates/report-app/js/common.js
Normal file
@@ -0,0 +1,856 @@
|
|||||||
|
/**
|
||||||
|
* 通用代码
|
||||||
|
* 包含工具函数和UI组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从URL查询参数中获取指定参数的值
|
||||||
|
* @param {string} name - 参数名称
|
||||||
|
* @returns {string|null} 参数值或null
|
||||||
|
*/
|
||||||
|
function getUrlParameter(name) {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
* @param {Function} func - 要防抖的函数
|
||||||
|
* @param {number} wait - 等待时间(毫秒)
|
||||||
|
* @returns {Function} 防抖后的函数
|
||||||
|
*/
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示Toast消息
|
||||||
|
* @param {string} message - 消息内容
|
||||||
|
* @param {string} type - 消息类型 ('success', 'error', 'warning', 'info')
|
||||||
|
* @param {number} duration - 显示时长(毫秒),默认3000
|
||||||
|
*/
|
||||||
|
function showToast(message, type = 'info', duration = 3000) {
|
||||||
|
// 创建toast容器(如果不存在)
|
||||||
|
let toastContainer = document.getElementById('toast-container');
|
||||||
|
if (!toastContainer) {
|
||||||
|
toastContainer = document.createElement('div');
|
||||||
|
toastContainer.id = 'toast-container';
|
||||||
|
toastContainer.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 space-y-2';
|
||||||
|
document.body.appendChild(toastContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建toast元素
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
const bgColor = {
|
||||||
|
success: 'bg-green-500',
|
||||||
|
error: 'bg-red-500',
|
||||||
|
warning: 'bg-yellow-500',
|
||||||
|
info: 'bg-blue-500'
|
||||||
|
}[type] || 'bg-blue-500';
|
||||||
|
|
||||||
|
toast.className = `${bgColor} text-white px-4 py-2 rounded-lg shadow-lg transform transition-all duration-300 translate-x-full opacity-0`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
// 添加到容器
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// 显示动画
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-x-full', 'opacity-0');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 自动隐藏
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('translate-x-full', 'opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) {
|
||||||
|
toast.parentNode.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证表单字段
|
||||||
|
* @param {Object} data - 表单数据
|
||||||
|
* @param {Object} rules - 验证规则
|
||||||
|
* @returns {Object} 验证结果 {isValid: boolean, errors: Object}
|
||||||
|
*/
|
||||||
|
function validateForm(data, rules) {
|
||||||
|
const errors = {};
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
for (const field in rules) {
|
||||||
|
const value = data[field];
|
||||||
|
const rule = rules[field];
|
||||||
|
|
||||||
|
// 必填验证
|
||||||
|
if (rule.required && (!value || value.toString().trim() === '')) {
|
||||||
|
errors[field] = rule.message || `${field}是必填项`;
|
||||||
|
isValid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字验证
|
||||||
|
if (rule.type === 'number' && value !== undefined && value !== '') {
|
||||||
|
const num = Number(value);
|
||||||
|
if (isNaN(num)) {
|
||||||
|
errors[field] = `${field}必须是数字`;
|
||||||
|
isValid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.min !== undefined && num < rule.min) {
|
||||||
|
errors[field] = `${field}不能小于${rule.min}`;
|
||||||
|
isValid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.max !== undefined && num > rule.max) {
|
||||||
|
errors[field] = `${field}不能大于${rule.max}`;
|
||||||
|
isValid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符串长度验证
|
||||||
|
if (rule.type === 'string' && value) {
|
||||||
|
if (rule.minLength && value.length < rule.minLength) {
|
||||||
|
errors[field] = `${field}长度不能少于${rule.minLength}个字符`;
|
||||||
|
isValid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.maxLength && value.length > rule.maxLength) {
|
||||||
|
errors[field] = `${field}长度不能超过${rule.maxLength}个字符`;
|
||||||
|
isValid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝对象
|
||||||
|
* @param {any} obj - 要拷贝的对象
|
||||||
|
* @returns {any} 拷贝后的对象
|
||||||
|
*/
|
||||||
|
function deepClone(obj) {
|
||||||
|
if (obj === null || typeof obj !== 'object') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Date) {
|
||||||
|
return new Date(obj.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
return obj.map(item => deepClone(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const cloned = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
cloned[key] = deepClone(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UI组件 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部导航栏组件
|
||||||
|
*/
|
||||||
|
class HeaderNav {
|
||||||
|
constructor() {
|
||||||
|
this.element = document.getElementById('header-nav');
|
||||||
|
this.backBtn = document.getElementById('back-btn');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化导航栏事件
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.backBtn.addEventListener('click', () => {
|
||||||
|
// 返回上一页或关闭页面
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单选择器组件
|
||||||
|
*/
|
||||||
|
class WorkOrderSelector {
|
||||||
|
constructor(onSelect) {
|
||||||
|
this.element = document.getElementById('work-order-selector');
|
||||||
|
this.selectedText = document.getElementById('selected-work-order-text');
|
||||||
|
this.dropdownArrow = document.getElementById('dropdown-arrow');
|
||||||
|
this.scanBtn = document.getElementById('scan-btn');
|
||||||
|
this.onSelect = onSelect;
|
||||||
|
this.selectedWorkOrder = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化选择器事件
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// 点击选择器展开下拉菜单
|
||||||
|
this.element.addEventListener('click', (e) => {
|
||||||
|
// 如果点击的是扫码按钮,不展开下拉菜单
|
||||||
|
if (e.target.closest('#scan-btn')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onSelect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 扫码按钮事件
|
||||||
|
this.scanBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.handleScan();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理扫码功能
|
||||||
|
*/
|
||||||
|
handleScan() {
|
||||||
|
showToast('扫码功能开发中...', 'info');
|
||||||
|
// TODO: 实现扫码功能
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新选中的工单显示
|
||||||
|
* @param {Object} workOrder - 工单对象
|
||||||
|
*/
|
||||||
|
updateSelected(workOrder) {
|
||||||
|
this.selectedWorkOrder = workOrder;
|
||||||
|
if (workOrder) {
|
||||||
|
this.selectedText.textContent = workOrder.id;
|
||||||
|
this.selectedText.className = 'text-gray-800 ml-2 font-medium';
|
||||||
|
} else {
|
||||||
|
this.selectedText.textContent = '请选择';
|
||||||
|
this.selectedText.className = 'text-gray-500 ml-2';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换下拉箭头状态
|
||||||
|
* @param {boolean} expanded - 是否展开
|
||||||
|
*/
|
||||||
|
toggleArrow(expanded) {
|
||||||
|
if (expanded) {
|
||||||
|
this.dropdownArrow.classList.add('rotate');
|
||||||
|
} else {
|
||||||
|
this.dropdownArrow.classList.remove('rotate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单下拉菜单组件
|
||||||
|
*/
|
||||||
|
class WorkOrderDropdown {
|
||||||
|
constructor(onConfirm, onClose) {
|
||||||
|
this.element = document.getElementById('work-order-dropdown');
|
||||||
|
this.closeBtn = document.getElementById('close-dropdown');
|
||||||
|
this.searchInput = document.getElementById('search-input');
|
||||||
|
this.workOrderList = document.getElementById('work-order-list');
|
||||||
|
this.selectAllCheckbox = document.getElementById('select-all');
|
||||||
|
this.selectedCount = document.getElementById('selected-count');
|
||||||
|
this.confirmBtn = document.getElementById('confirm-selection');
|
||||||
|
|
||||||
|
this.onConfirm = onConfirm;
|
||||||
|
this.onClose = onClose;
|
||||||
|
this.workOrders = [];
|
||||||
|
this.filteredWorkOrders = [];
|
||||||
|
this.selectedWorkOrders = new Set();
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化下拉菜单事件
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// 关闭按钮事件
|
||||||
|
this.closeBtn.addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击背景关闭
|
||||||
|
this.element.addEventListener('click', (e) => {
|
||||||
|
if (e.target === this.element) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 搜索功能
|
||||||
|
this.searchInput.addEventListener('input', debounce((e) => {
|
||||||
|
this.filterWorkOrders(e.target.value);
|
||||||
|
}, 300));
|
||||||
|
|
||||||
|
// 全选功能
|
||||||
|
this.selectAllCheckbox.addEventListener('change', (e) => {
|
||||||
|
this.toggleSelectAll(e.target.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确定按钮事件
|
||||||
|
this.confirmBtn.addEventListener('click', () => {
|
||||||
|
this.confirmSelection();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESC键关闭
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && this.isVisible()) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示下拉菜单
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
this.element.classList.remove('hidden');
|
||||||
|
this.element.classList.add('show');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// 聚焦搜索框
|
||||||
|
setTimeout(() => {
|
||||||
|
this.searchInput.focus();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏下拉菜单
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.element.classList.remove('show');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.element.classList.add('hidden');
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
this.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可见
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isVisible() {
|
||||||
|
return !this.element.classList.contains('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工单数据
|
||||||
|
* @param {Array} workOrders - 工单数组
|
||||||
|
*/
|
||||||
|
setWorkOrders(workOrders) {
|
||||||
|
this.workOrders = workOrders;
|
||||||
|
this.filteredWorkOrders = [...workOrders];
|
||||||
|
this.renderWorkOrderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤工单
|
||||||
|
* @param {string} searchTerm - 搜索关键词
|
||||||
|
*/
|
||||||
|
filterWorkOrders(searchTerm) {
|
||||||
|
const term = searchTerm.toLowerCase().trim();
|
||||||
|
if (!term) {
|
||||||
|
this.filteredWorkOrders = [...this.workOrders];
|
||||||
|
} else {
|
||||||
|
this.filteredWorkOrders = this.workOrders.filter(wo =>
|
||||||
|
wo.id.toLowerCase().includes(term) ||
|
||||||
|
(wo.name && wo.name.toLowerCase().includes(term))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.renderWorkOrderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染工单列表
|
||||||
|
*/
|
||||||
|
renderWorkOrderList() {
|
||||||
|
this.workOrderList.innerHTML = '';
|
||||||
|
|
||||||
|
if (this.filteredWorkOrders.length === 0) {
|
||||||
|
const emptyDiv = document.createElement('div');
|
||||||
|
emptyDiv.className = 'text-center py-8 text-gray-500';
|
||||||
|
emptyDiv.textContent = '没有找到匹配的工单';
|
||||||
|
this.workOrderList.appendChild(emptyDiv);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filteredWorkOrders.forEach(workOrder => {
|
||||||
|
const item = this.createWorkOrderItem(workOrder);
|
||||||
|
this.workOrderList.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateSelectAllState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建工单列表项
|
||||||
|
* @param {Object} workOrder - 工单对象
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
createWorkOrderItem(workOrder) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'work-order-item flex items-center space-x-3';
|
||||||
|
item.dataset.workOrderId = workOrder.id;
|
||||||
|
|
||||||
|
const isSelected = this.selectedWorkOrders.has(workOrder.id);
|
||||||
|
if (isSelected) {
|
||||||
|
item.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<input type="radio" name="work-order" value="${workOrder.id}"
|
||||||
|
class="checkbox-primary focus:ring-2" ${isSelected ? 'checked' : ''}>
|
||||||
|
<span class="flex-1 text-gray-800">${workOrder.id}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 点击事件
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
this.selectWorkOrder(workOrder.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择工单
|
||||||
|
* @param {string} workOrderId - 工单ID
|
||||||
|
*/
|
||||||
|
selectWorkOrder(workOrderId) {
|
||||||
|
// 单选模式:清除之前的选择
|
||||||
|
this.selectedWorkOrders.clear();
|
||||||
|
this.selectedWorkOrders.add(workOrderId);
|
||||||
|
|
||||||
|
// 更新UI
|
||||||
|
this.workOrderList.querySelectorAll('.work-order-item').forEach(item => {
|
||||||
|
const radio = item.querySelector('input[type="radio"]');
|
||||||
|
const isSelected = item.dataset.workOrderId === workOrderId;
|
||||||
|
|
||||||
|
item.classList.toggle('selected', isSelected);
|
||||||
|
radio.checked = isSelected;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateSelectedCount();
|
||||||
|
this.updateConfirmButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换全选状态
|
||||||
|
* @param {boolean} selectAll - 是否全选
|
||||||
|
*/
|
||||||
|
toggleSelectAll(selectAll) {
|
||||||
|
if (selectAll) {
|
||||||
|
this.filteredWorkOrders.forEach(wo => {
|
||||||
|
this.selectedWorkOrders.add(wo.id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.selectedWorkOrders.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderWorkOrderList();
|
||||||
|
this.updateSelectedCount();
|
||||||
|
this.updateConfirmButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新全选状态
|
||||||
|
*/
|
||||||
|
updateSelectAllState() {
|
||||||
|
const visibleCount = this.filteredWorkOrders.length;
|
||||||
|
const selectedVisibleCount = this.filteredWorkOrders.filter(wo =>
|
||||||
|
this.selectedWorkOrders.has(wo.id)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
this.selectAllCheckbox.checked = visibleCount > 0 && selectedVisibleCount === visibleCount;
|
||||||
|
this.selectAllCheckbox.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新选中数量显示
|
||||||
|
*/
|
||||||
|
updateSelectedCount() {
|
||||||
|
this.selectedCount.textContent = `已选择${this.selectedWorkOrders.size}个`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新确定按钮状态
|
||||||
|
*/
|
||||||
|
updateConfirmButton() {
|
||||||
|
this.confirmBtn.disabled = this.selectedWorkOrders.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认选择
|
||||||
|
*/
|
||||||
|
confirmSelection() {
|
||||||
|
if (this.selectedWorkOrders.size === 0) {
|
||||||
|
showToast('请选择工单', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds = Array.from(this.selectedWorkOrders);
|
||||||
|
const selectedWorkOrder = this.workOrders.find(wo => wo.id === selectedIds[0]);
|
||||||
|
|
||||||
|
this.onConfirm(selectedWorkOrder);
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置选择状态
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.selectedWorkOrders.clear();
|
||||||
|
this.searchInput.value = '';
|
||||||
|
this.filteredWorkOrders = [...this.workOrders];
|
||||||
|
this.renderWorkOrderList();
|
||||||
|
this.updateSelectedCount();
|
||||||
|
this.updateConfirmButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物料信息组件
|
||||||
|
*/
|
||||||
|
class MaterialInfo {
|
||||||
|
constructor() {
|
||||||
|
this.element = document.getElementById('material-info');
|
||||||
|
this.workOrderId = document.getElementById('selected-work-order-id');
|
||||||
|
this.materialName = document.getElementById('material-name');
|
||||||
|
this.materialCode = document.getElementById('material-code');
|
||||||
|
this.qualifiedCount = document.getElementById('qualified-count');
|
||||||
|
this.defectiveCount = document.getElementById('defective-count');
|
||||||
|
this.pendingCount = document.getElementById('pending-count');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示物料信息
|
||||||
|
* @param {Object} data - 物料数据
|
||||||
|
*/
|
||||||
|
show(data) {
|
||||||
|
this.workOrderId.textContent = data.workOrderId;
|
||||||
|
this.materialName.textContent = data.materialName;
|
||||||
|
this.materialCode.textContent = data.materialCode;
|
||||||
|
this.qualifiedCount.textContent = data.qualifiedCount || 0;
|
||||||
|
this.defectiveCount.textContent = data.defectiveCount || 0;
|
||||||
|
this.pendingCount.textContent = data.pendingCount || 0;
|
||||||
|
|
||||||
|
this.element.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏物料信息
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.element.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报工表单组件
|
||||||
|
*/
|
||||||
|
class ReportForm {
|
||||||
|
constructor() {
|
||||||
|
this.element = document.getElementById('report-form');
|
||||||
|
this.quantityInput = document.getElementById('quantity');
|
||||||
|
this.auxiliaryQuantityInput = document.getElementById('auxiliary-quantity');
|
||||||
|
this.remarkInput = document.getElementById('remark');
|
||||||
|
this.workingHoursInput = document.getElementById('working-hours');
|
||||||
|
this.reportStartTimeInput = document.getElementById('report-start-time');
|
||||||
|
this.reportEndTimeInput = document.getElementById('report-end-time');
|
||||||
|
this.auxName = document.getElementById('auxName');
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化表单事件
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// 数字输入验证
|
||||||
|
[this.quantityInput, this.auxiliaryQuantityInput].forEach(input => {
|
||||||
|
if (input) {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
this.validateNumberInput(e.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单字段变化时的验证
|
||||||
|
this.getAllInputs().forEach(input => {
|
||||||
|
if (input) {
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
this.validateField(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证数字输入
|
||||||
|
* @param {HTMLInputElement} input - 输入元素
|
||||||
|
*/
|
||||||
|
validateNumberInput(input) {
|
||||||
|
if (!input) return;
|
||||||
|
const value = parseFloat(input.value);
|
||||||
|
if (isNaN(value) || value < 0) {
|
||||||
|
input.classList.add('border-red-500');
|
||||||
|
this.showFieldError(input, '请输入有效的数字');
|
||||||
|
} else {
|
||||||
|
input.classList.remove('border-red-500');
|
||||||
|
this.hideFieldError(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证单个字段
|
||||||
|
* @param {HTMLInputElement} input - 输入元素
|
||||||
|
*/
|
||||||
|
validateField(input) {
|
||||||
|
if (!input) return false;
|
||||||
|
const value = input.value.trim();
|
||||||
|
const isRequired = input.previousElementSibling?.querySelector('.text-red-500');
|
||||||
|
|
||||||
|
if (isRequired && !value) {
|
||||||
|
input.classList.add('border-red-500');
|
||||||
|
this.showFieldError(input, '此字段为必填项');
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
input.classList.remove('border-red-500');
|
||||||
|
this.hideFieldError(input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示字段错误
|
||||||
|
* @param {HTMLInputElement} input - 输入元素
|
||||||
|
* @param {string} message - 错误消息
|
||||||
|
*/
|
||||||
|
showFieldError(input, message) {
|
||||||
|
let errorElement = input.parentNode.querySelector('.field-error');
|
||||||
|
if (!errorElement) {
|
||||||
|
errorElement = document.createElement('div');
|
||||||
|
// errorElement.className = 'field-error text-red-500 text-sm mt-1';
|
||||||
|
errorElement.className = 'field-error text-red-500 text-sm mt-1 w-full';
|
||||||
|
input.parentNode.appendChild(errorElement);
|
||||||
|
}
|
||||||
|
errorElement.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏字段错误
|
||||||
|
* @param {HTMLInputElement} input - 输入元素
|
||||||
|
*/
|
||||||
|
hideFieldError(input) {
|
||||||
|
const errorElement = input.parentNode.querySelector('.field-error');
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有输入元素
|
||||||
|
* @returns {HTMLInputElement[]}
|
||||||
|
*/
|
||||||
|
getAllInputs() {
|
||||||
|
return [
|
||||||
|
this.quantityInput,
|
||||||
|
this.auxiliaryQuantityInput,
|
||||||
|
this.remarkInput,
|
||||||
|
this.workingHoursInput,
|
||||||
|
this.reportStartTimeInput,
|
||||||
|
this.reportEndTimeInput,
|
||||||
|
].filter(input => input !== null && input !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表单数据
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
return {
|
||||||
|
quantity: parseFloat(this.quantityInput?.value) || 0,
|
||||||
|
auxiliaryQuantity: parseFloat(this.auxiliaryQuantityInput?.value) || 0,
|
||||||
|
remark: this.remarkInput?.value?.trim() || '',
|
||||||
|
workingHours: parseFloat(this.workingHoursInput?.value) || 0,
|
||||||
|
reportStartTime: this.reportStartTimeInput?.value?.trim() || '',
|
||||||
|
reportEndTimeInput: this.reportEndTimeInput?.value?.trim() || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置表单数据
|
||||||
|
* @param {Object} data - 表单数据
|
||||||
|
*/
|
||||||
|
setData(data) {
|
||||||
|
if (this.quantityInput) {
|
||||||
|
this.quantityInput.value = data.quantity || '';
|
||||||
|
}
|
||||||
|
if (this.auxiliaryQuantityInput) {
|
||||||
|
this.auxiliaryQuantityInput.value = data.auxiliaryQuantity || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuxName(data) {
|
||||||
|
if (this.auxName) {
|
||||||
|
this.auxName.innerText = data.auxName || '公斤';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证整个表单
|
||||||
|
* @returns {Object} 验证结果
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
const data = this.getData();
|
||||||
|
const rules = {
|
||||||
|
quantity: { required: true, type: 'number', min: 0 },
|
||||||
|
auxiliaryQuantity: { required: true, type: 'number', min: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return validateForm(data, rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示表单
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
this.element.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏表单
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.element.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置表单
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.getAllInputs().forEach(input => {
|
||||||
|
input.value = '';
|
||||||
|
input.classList.remove('border-red-500');
|
||||||
|
this.hideFieldError(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const GlobalLoading = {
|
||||||
|
element: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.element) return;
|
||||||
|
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.element.id = 'global-loading';
|
||||||
|
this.element.innerHTML = `
|
||||||
|
<div class="loading-backdrop"></div>
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div class="loading-text">加载中...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加样式
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
#global-loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 9999;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.loading-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.loading-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #007bff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
}
|
||||||
|
.loading-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
body.loading-active {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
document.body.appendChild(this.element);
|
||||||
|
},
|
||||||
|
|
||||||
|
show(text = '加载中...') {
|
||||||
|
this.init();
|
||||||
|
const textElement = this.element.querySelector('.loading-text');
|
||||||
|
if (textElement) {
|
||||||
|
textElement.textContent = text;
|
||||||
|
}
|
||||||
|
this.element.style.display = 'block';
|
||||||
|
document.body.classList.add('loading-active');
|
||||||
|
},
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
if (this.element) {
|
||||||
|
this.element.style.display = 'none';
|
||||||
|
document.body.classList.remove('loading-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
457
skills/web-build/templates/report-app/js/main.js
Normal file
457
skills/web-build/templates/report-app/js/main.js
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
/**
|
||||||
|
* 主应用逻辑
|
||||||
|
* 管理页面状态、组件交互和业务流程
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MainApp {
|
||||||
|
constructor() {
|
||||||
|
// 应用状态
|
||||||
|
this.state = {
|
||||||
|
currentView: 'empty', // empty, material, form
|
||||||
|
selectedWorkOrder: null,
|
||||||
|
materialData: null,
|
||||||
|
formData: null,
|
||||||
|
isLoading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件实例
|
||||||
|
this.components = {};
|
||||||
|
|
||||||
|
// 初始化应用
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化应用
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
console.log('初始化报工应用...');
|
||||||
|
|
||||||
|
// 等待认证服务初始化完成
|
||||||
|
if (authService && !authService.isAuthenticated()) {
|
||||||
|
await this.waitForAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化组件
|
||||||
|
this.initComponents();
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
this.bindEvents();
|
||||||
|
|
||||||
|
// 预加载数据
|
||||||
|
this.preloadData();
|
||||||
|
|
||||||
|
console.log('报工应用初始化完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('应用初始化失败:', error);
|
||||||
|
showToast('应用初始化失败,请刷新页面重试', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待认证完成
|
||||||
|
*/
|
||||||
|
async waitForAuth() {
|
||||||
|
let retries = 0;
|
||||||
|
const maxRetries = 10;
|
||||||
|
|
||||||
|
while (!authService.isAuthenticated() && retries < maxRetries) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authService.isAuthenticated()) {
|
||||||
|
console.warn('认证超时,继续使用离线模式');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化组件
|
||||||
|
*/
|
||||||
|
initComponents() {
|
||||||
|
// 初始化顶部导航栏
|
||||||
|
this.components.headerNav = new HeaderNav();
|
||||||
|
|
||||||
|
// 初始化工单选择器
|
||||||
|
this.components.workOrderSelector = new WorkOrderSelector(() => {
|
||||||
|
this.showWorkOrderDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化工单下拉菜单
|
||||||
|
this.components.workOrderDropdown = new WorkOrderDropdown(
|
||||||
|
(workOrder) => this.onWorkOrderSelected(workOrder),
|
||||||
|
() => this.onWorkOrderDropdownClosed()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化物料信息组件
|
||||||
|
this.components.materialInfo = new MaterialInfo();
|
||||||
|
|
||||||
|
// 初始化报工表单组件
|
||||||
|
this.components.reportForm = new ReportForm();
|
||||||
|
|
||||||
|
console.log('组件初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
bindEvents() {
|
||||||
|
// 提交按钮事件
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.addEventListener('click', () => {
|
||||||
|
this.handleSubmit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络状态变化事件
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
console.log('网络已连接');
|
||||||
|
showToast('网络已连接', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
console.log('网络已断开');
|
||||||
|
showToast('网络已断开,将使用离线模式', 'warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('事件绑定完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载数据
|
||||||
|
*/
|
||||||
|
async preloadData() {
|
||||||
|
try {
|
||||||
|
// 预加载工单数据
|
||||||
|
if (workOrderService) {
|
||||||
|
await workOrderService.preloadWorkOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('预加载数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示工单下拉菜单
|
||||||
|
*/
|
||||||
|
async showWorkOrderDropdown() {
|
||||||
|
try {
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
// 获取工单列表
|
||||||
|
const workOrderData = await workOrderService.getWorkOrderList();
|
||||||
|
|
||||||
|
// 设置工单数据到下拉菜单
|
||||||
|
this.components.workOrderDropdown.setWorkOrders(workOrderData.workOrders);
|
||||||
|
|
||||||
|
// 显示下拉菜单
|
||||||
|
this.components.workOrderDropdown.show();
|
||||||
|
|
||||||
|
// 更新选择器箭头状态
|
||||||
|
this.components.workOrderSelector.toggleArrow(true);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载工单列表失败:', error);
|
||||||
|
showToast('加载工单列表失败', 'error');
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单选择事件处理
|
||||||
|
* @param {Object} workOrder - 选中的工单
|
||||||
|
*/
|
||||||
|
async onWorkOrderSelected(workOrder) {
|
||||||
|
try {
|
||||||
|
console.log('选择工单:', workOrder);
|
||||||
|
|
||||||
|
this.setLoading(true);
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
this.state.selectedWorkOrder = workOrder;
|
||||||
|
|
||||||
|
// 更新工单选择器显示
|
||||||
|
this.components.workOrderSelector.updateSelected(workOrder);
|
||||||
|
|
||||||
|
// 从工单数据中提取物料信息(工单数据中已包含materialInfo)
|
||||||
|
const materialData = this.extractMaterialDataFromWorkOrder(workOrder);
|
||||||
|
this.state.materialData = materialData;
|
||||||
|
|
||||||
|
// 切换到物料和表单视图
|
||||||
|
this.switchToMaterialView();
|
||||||
|
|
||||||
|
// 初始化表单数据
|
||||||
|
this.initializeFormData();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理工单选择失败:', error);
|
||||||
|
showToast('处理工单选择失败', 'error');
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从工单数据中提取物料信息
|
||||||
|
* @param {Object} workOrder - 工单对象
|
||||||
|
* @returns {Object} 物料数据
|
||||||
|
*/
|
||||||
|
extractMaterialDataFromWorkOrder(workOrder) {
|
||||||
|
// 从materialInfo中提取详细信息
|
||||||
|
const materialInfo = workOrder.materialInfo || {};
|
||||||
|
const materialBaseInfo = materialInfo.baseInfo || {};
|
||||||
|
|
||||||
|
// 提取单位转换关系
|
||||||
|
const conversions = materialInfo.conversions || [];
|
||||||
|
let unit = '个';
|
||||||
|
let auxiliaryUnit = '公斤';
|
||||||
|
|
||||||
|
if (conversions.length > 0) {
|
||||||
|
const firstConversion = conversions[0];
|
||||||
|
unit = firstConversion.fromUnitName || '个';
|
||||||
|
auxiliaryUnit = firstConversion.toUnitName || '公斤';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workOrderId: workOrder.workOrderId || workOrder.id,
|
||||||
|
workOrderCode: workOrder.workOrderCode || workOrder.id,
|
||||||
|
materialId: workOrder.materialId || materialBaseInfo.id,
|
||||||
|
materialName: workOrder.materialName || materialBaseInfo.name || '',
|
||||||
|
materialCode: workOrder.materialCode || materialBaseInfo.code || '',
|
||||||
|
materialSpec: materialBaseInfo.specification || '',
|
||||||
|
unit: unit,
|
||||||
|
auxiliaryUnit: auxiliaryUnit,
|
||||||
|
conversions: conversions,
|
||||||
|
// 数量信息
|
||||||
|
qualifiedCount: workOrder.qualifiedQuantity || 0,
|
||||||
|
qualifiedCountDisplay: workOrder.qualifiedQuantityDisplay || '0',
|
||||||
|
defectiveCount: workOrder.disqualifiedQuantity || 0,
|
||||||
|
defectiveCountDisplay: workOrder.disqualifiedQuantityDisplay || '0',
|
||||||
|
pendingCount: Math.max(0, (workOrder.totalQuantity || 0) - (workOrder.qualifiedQuantity || 0) - (workOrder.disqualifiedQuantity || 0)),
|
||||||
|
totalCount: workOrder.totalQuantity,
|
||||||
|
unitId: materialInfo?.unit?.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单下拉菜单关闭事件处理
|
||||||
|
*/
|
||||||
|
onWorkOrderDropdownClosed() {
|
||||||
|
// 更新选择器箭头状态
|
||||||
|
this.components.workOrderSelector.toggleArrow(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到物料视图
|
||||||
|
*/
|
||||||
|
switchToMaterialView() {
|
||||||
|
// 隐藏空状态
|
||||||
|
const emptyState = document.getElementById('empty-state');
|
||||||
|
if (emptyState) {
|
||||||
|
emptyState.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示物料信息
|
||||||
|
this.components.materialInfo.show(this.state.materialData);
|
||||||
|
|
||||||
|
// 显示报工表单
|
||||||
|
this.components.reportForm.show();
|
||||||
|
|
||||||
|
// 显示提交按钮
|
||||||
|
const submitSection = document.getElementById('submit-section');
|
||||||
|
if (submitSection) {
|
||||||
|
submitSection.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
this.state.currentView = 'material';
|
||||||
|
|
||||||
|
this.components.reportForm.setAuxName({ auxName: this.state.materialData.unit });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到空状态视图
|
||||||
|
*/
|
||||||
|
switchToEmptyView() {
|
||||||
|
// 显示空状态
|
||||||
|
const emptyState = document.getElementById('empty-state');
|
||||||
|
if (emptyState) {
|
||||||
|
emptyState.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏物料信息
|
||||||
|
this.components.materialInfo.hide();
|
||||||
|
|
||||||
|
// 隐藏报工表单
|
||||||
|
this.components.reportForm.hide();
|
||||||
|
|
||||||
|
// 隐藏提交按钮
|
||||||
|
const submitSection = document.getElementById('submit-section');
|
||||||
|
if (submitSection) {
|
||||||
|
submitSection.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
this.state.currentView = 'empty';
|
||||||
|
this.state.selectedWorkOrder = null;
|
||||||
|
this.state.materialData = null;
|
||||||
|
this.state.formData = null;
|
||||||
|
|
||||||
|
// 重置工单选择器
|
||||||
|
this.components.workOrderSelector.updateSelected(null);
|
||||||
|
|
||||||
|
console.log('切换到空状态视图');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化表单数据
|
||||||
|
*/
|
||||||
|
initializeFormData() {
|
||||||
|
// 直接从 HTML 读取值,不设置默认值
|
||||||
|
const formData = this.components.reportForm.getData();
|
||||||
|
this.state.formData = formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理提交事件
|
||||||
|
*/
|
||||||
|
async handleSubmit() {
|
||||||
|
try {
|
||||||
|
// 验证表单
|
||||||
|
const validation = this.components.reportForm.validate();
|
||||||
|
if (!validation.isValid) {
|
||||||
|
const errorMessage = Object.values(validation.errors)[0];
|
||||||
|
showToast(errorMessage, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取表单数据
|
||||||
|
const formData = this.components.reportForm.getData();
|
||||||
|
console.log('表单数据:', formData);
|
||||||
|
// 构建报工数据
|
||||||
|
const reportData = {
|
||||||
|
workOrderId: this.state.selectedWorkOrder.workOrderId, // 使用数字类型的工单ID
|
||||||
|
workOrderCode: this.state.selectedWorkOrder.workOrderCode, // 同时传递工单编号作为备用
|
||||||
|
materialId: this.state.materialData.materialId,
|
||||||
|
quantity: formData.quantity,
|
||||||
|
auxiliaryQuantity: formData.auxiliaryQuantity,
|
||||||
|
remark: formData.remark,
|
||||||
|
workHour: formData.workingHours,
|
||||||
|
workHourUnit: 2,
|
||||||
|
auxUnitId1: this.state.materialData.unitId,
|
||||||
|
reportStartTime: formData.reportStartTime,
|
||||||
|
reportEndTime: formData.reportEndTimeInput
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('提交报工数据:', reportData);
|
||||||
|
|
||||||
|
// this.setLoading(true);
|
||||||
|
GlobalLoading.show('提交中...');
|
||||||
|
|
||||||
|
// 提交报工
|
||||||
|
const result = await reportService.submitReport(reportData);
|
||||||
|
|
||||||
|
console.log('报工提交结果:', result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 提交成功,重置表单
|
||||||
|
this.handleSubmitSuccess(result);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '提交失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交报工失败:', error);
|
||||||
|
showToast(error.message || '提交失败', 'error');
|
||||||
|
} finally {
|
||||||
|
// this.setLoading(false);
|
||||||
|
GlobalLoading.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理提交成功
|
||||||
|
* @param {Object} result - 提交结果
|
||||||
|
*/
|
||||||
|
handleSubmitSuccess(result) {
|
||||||
|
// 显示成功消息
|
||||||
|
showToast('报工提交成功', 'success');
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
this.components.reportForm.reset();
|
||||||
|
|
||||||
|
// 重新初始化表单数据
|
||||||
|
this.initializeFormData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置加载状态
|
||||||
|
* @param {boolean} loading - 是否加载中
|
||||||
|
*/
|
||||||
|
setLoading(loading) {
|
||||||
|
this.state.isLoading = loading;
|
||||||
|
|
||||||
|
// 更新提交按钮状态
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = loading;
|
||||||
|
submitBtn.textContent = loading ? '提交中...' : '提交';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以添加全局加载指示器
|
||||||
|
if (loading) {
|
||||||
|
document.body.style.cursor = 'wait';
|
||||||
|
} else {
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置应用状态
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.switchToEmptyView();
|
||||||
|
this.components.workOrderDropdown.reset();
|
||||||
|
console.log('应用状态已重置');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用状态
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
getState() {
|
||||||
|
return deepClone(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁应用
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
// 清理事件监听器
|
||||||
|
// 清理组件
|
||||||
|
// 清理定时器等
|
||||||
|
console.log('应用已销毁');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化应用
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('DOM加载完成,初始化应用...');
|
||||||
|
|
||||||
|
// 创建全局应用实例
|
||||||
|
window.MainApp = new MainApp();
|
||||||
|
|
||||||
|
// 开发模式下暴露服务到全局
|
||||||
|
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||||
|
window.authService = authService;
|
||||||
|
window.apiService = apiService;
|
||||||
|
window.workOrderService = workOrderService;
|
||||||
|
window.reportService = reportService;
|
||||||
|
console.log('开发模式:服务已暴露到全局');
|
||||||
|
}
|
||||||
|
});
|
||||||
758
skills/web-build/templates/report-app/services/business.js
Normal file
758
skills/web-build/templates/report-app/services/business.js
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
/**
|
||||||
|
* 业务服务
|
||||||
|
* 包含工单服务和报工服务
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 工单服务 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单服务
|
||||||
|
* 处理工单相关的API请求
|
||||||
|
*/
|
||||||
|
class WorkOrderService {
|
||||||
|
constructor() {
|
||||||
|
this.api = apiService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工单列表
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @param {string} params.search - 搜索关键词(工单编号模糊查询)
|
||||||
|
* @param {string} params.exactWorkOrderCode - 工单编号精确查询
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.pageSize - 每页数量
|
||||||
|
* @param {Array<number>} params.workOrderStatusList - 工单业务状态列表
|
||||||
|
* @param {number} params.pauseFlag - 是否暂停 0未暂停 1已暂停
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
async getWorkOrderList(params = {}) {
|
||||||
|
try {
|
||||||
|
console.log('从服务器获取工单列表', params);
|
||||||
|
|
||||||
|
// 构建请求体,根据API文档格式
|
||||||
|
const requestBody = {
|
||||||
|
page: params.page || 1,
|
||||||
|
size: params.pageSize || 50
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果有搜索关键词,使用模糊查询
|
||||||
|
if (params.search) {
|
||||||
|
requestBody.workOrderCode = params.search.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有精确查询
|
||||||
|
if (params.exactWorkOrderCode) {
|
||||||
|
requestBody.exactWorkOrderCode = params.exactWorkOrderCode.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工单状态筛选
|
||||||
|
if (params.workOrderStatusList && Array.isArray(params.workOrderStatusList)) {
|
||||||
|
requestBody.workOrderStatusList = params.workOrderStatusList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂停状态筛选
|
||||||
|
if (params.pauseFlag !== undefined) {
|
||||||
|
requestBody.pauseFlag = params.pauseFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API接口(POST方法)
|
||||||
|
const response = await this.api.post('/openapi/domain/web/v1/route/med/open/v2/work_order/base/_list', requestBody);
|
||||||
|
|
||||||
|
// 处理响应数据
|
||||||
|
const result = this.processWorkOrderListResponse(response);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取工单列表失败:', error);
|
||||||
|
|
||||||
|
// 如果是网络错误,返回模拟数据
|
||||||
|
if (error.name === 'TypeError' || error.message.includes('Failed to fetch')) {
|
||||||
|
console.log('网络错误,返回模拟工单数据');
|
||||||
|
return this.getMockWorkOrderList(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理工单列表响应数据
|
||||||
|
* @param {Object} response - API响应
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
processWorkOrderListResponse(response) {
|
||||||
|
// 根据API文档,响应格式:{ code, message, data: { list, page, total } }
|
||||||
|
if (response.code === 200 || response.code === 0) {
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (!data || !data.list) {
|
||||||
|
throw new Error('响应数据格式错误:缺少data或list字段');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workOrders: data.list.map(item => {
|
||||||
|
// 提取物料信息
|
||||||
|
const materialInfo = item.materialInfo || {};
|
||||||
|
const materialBaseInfo = materialInfo.baseInfo || {};
|
||||||
|
|
||||||
|
// 提取数量信息(BaseAmountDisplay对象)
|
||||||
|
const qualifiedAmount = item.qualifiedHoldAmount || {};
|
||||||
|
const disqualifiedAmount = item.disqualifiedHoldAmount || {};
|
||||||
|
const totalAmount = item.totalHoldAmount || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.workOrderCode || item.workOrderId?.toString() || '',
|
||||||
|
workOrderId: item.workOrderId,
|
||||||
|
workOrderCode: item.workOrderCode,
|
||||||
|
name: materialBaseInfo.name || materialInfo.name || item.workOrderCode,
|
||||||
|
status: item.workOrderStatus?.code || 'active',
|
||||||
|
// 物料信息(保留完整结构)
|
||||||
|
materialInfo: item.materialInfo, // 保留完整的materialInfo对象
|
||||||
|
materialId: materialBaseInfo.id || materialInfo.id,
|
||||||
|
materialName: materialBaseInfo.name || materialInfo.name,
|
||||||
|
materialCode: materialBaseInfo.code || materialInfo.code,
|
||||||
|
// 数量信息
|
||||||
|
qualifiedQuantity: qualifiedAmount.amount || 0,
|
||||||
|
qualifiedQuantityDisplay: qualifiedAmount.amountDisplay || '0',
|
||||||
|
disqualifiedQuantity: disqualifiedAmount.amount || 0,
|
||||||
|
disqualifiedQuantityDisplay: disqualifiedAmount.amountDisplay || '0',
|
||||||
|
totalQuantity: totalAmount.amount || 0,
|
||||||
|
totalQuantityDisplay: totalAmount.amountDisplay || '0',
|
||||||
|
// 时间信息
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
plannedStartTime: item.plannedStartTime,
|
||||||
|
plannedEndTime: item.plannedEndTime,
|
||||||
|
// 其他信息
|
||||||
|
pauseFlag: item.pauseFlag,
|
||||||
|
workOrderStatus: item.workOrderStatus,
|
||||||
|
workOrderType: item.workOrderType
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
total: data.total || 0,
|
||||||
|
page: data.page || 1,
|
||||||
|
pageSize: data.size || 50
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || `获取工单列表失败 (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模拟工单列表(用于离线模式或测试)
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
getMockWorkOrderList(params = {}) {
|
||||||
|
const mockData = [
|
||||||
|
{
|
||||||
|
id: 'cyy-SOP',
|
||||||
|
name: '测试工单-SOP',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M001',
|
||||||
|
materialName: '物料1-工序a-10',
|
||||||
|
materialCode: 'wuliao1-gongxua-10'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cyy-chengliangrenwu',
|
||||||
|
name: '测试工单-成量任务',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M002',
|
||||||
|
materialName: '物料2-工序b-20',
|
||||||
|
materialCode: 'wuliao2-gongxub-20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2510300131-',
|
||||||
|
name: '工单2510300131',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M003',
|
||||||
|
materialName: '物料3-工序c-30',
|
||||||
|
materialCode: 'wuliao3-gongxuc-30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2510270130-',
|
||||||
|
name: '工单2510270130',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M004',
|
||||||
|
materialName: '物料4-工序d-40',
|
||||||
|
materialCode: 'wuliao4-gongxud-40'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2510210129-',
|
||||||
|
name: '工单2510210129',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M005',
|
||||||
|
materialName: '物料5-工序e-50',
|
||||||
|
materialCode: 'wuliao5-gongxue-50'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2509020128-',
|
||||||
|
name: '工单2509020128',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M006',
|
||||||
|
materialName: '物料6-工序f-60',
|
||||||
|
materialCode: 'wuliao6-gongxuf-60'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2509010126-',
|
||||||
|
name: '工单2509010126',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M007',
|
||||||
|
materialName: '物料7-工序g-70',
|
||||||
|
materialCode: 'wuliao7-gongxug-70'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2509010125-',
|
||||||
|
name: '工单2509010125',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M008',
|
||||||
|
materialName: '物料8-工序h-80',
|
||||||
|
materialCode: 'wuliao8-gongxuh-80'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2508260124-',
|
||||||
|
name: '工单2508260124',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M009',
|
||||||
|
materialName: '物料9-工序i-90',
|
||||||
|
materialCode: 'wuliao9-gongxui-90'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gd2507210123-',
|
||||||
|
name: '工单2507210123',
|
||||||
|
status: 'active',
|
||||||
|
materialId: 'M010',
|
||||||
|
materialName: '物料10-工序j-100',
|
||||||
|
materialCode: 'wuliao10-gongxuj-100'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 应用搜索过滤
|
||||||
|
let filteredData = mockData;
|
||||||
|
if (params.search) {
|
||||||
|
const searchTerm = params.search.toLowerCase();
|
||||||
|
filteredData = mockData.filter(item =>
|
||||||
|
item.id.toLowerCase().includes(searchTerm) ||
|
||||||
|
item.name.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workOrders: filteredData,
|
||||||
|
total: filteredData.length,
|
||||||
|
page: params.page || 1,
|
||||||
|
pageSize: params.pageSize || 50
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载工单数据
|
||||||
|
*/
|
||||||
|
async preloadWorkOrders() {
|
||||||
|
try {
|
||||||
|
console.log('预加载工单数据...');
|
||||||
|
await this.getWorkOrderList({ pageSize: 100 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('预加载工单数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局工单服务实例
|
||||||
|
const workOrderService = new WorkOrderService();
|
||||||
|
|
||||||
|
// ==================== 报工服务 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报工服务
|
||||||
|
* 处理报工相关的API请求
|
||||||
|
*/
|
||||||
|
class ReportService {
|
||||||
|
constructor() {
|
||||||
|
this.api = apiService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取报工必填参数
|
||||||
|
* 从接口获取报工所需的必填参数(如taskId、progressReportMaterial等)
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @param {string|number} params.workOrderId - 工单ID
|
||||||
|
* @param {string|number} params.materialId - 物料ID
|
||||||
|
* @returns {Promise<Object>} 返回必填参数对象
|
||||||
|
*/
|
||||||
|
async getReportRequiredParams(params) {
|
||||||
|
try {
|
||||||
|
console.log('获取报工必填参数:', params);
|
||||||
|
|
||||||
|
const { workOrderId, materialId } = params;
|
||||||
|
|
||||||
|
if (!workOrderId ) {
|
||||||
|
throw new Error('工单ID获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskListRequest = {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
workOrderIdList: [workOrderId]
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/produce_task/_list', taskListRequest);
|
||||||
|
|
||||||
|
if (taskListResponse.code !== 200 || !taskListResponse.data || !taskListResponse.data.list || taskListResponse.data.list.length === 0) {
|
||||||
|
throw new Error('未找到对应的生产任务');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取第一个任务(通常一个工单对应一个主任务)
|
||||||
|
const task = taskListResponse.data.list[0];
|
||||||
|
const taskId = task.taskId;
|
||||||
|
const executorIds = (task.executorList || []).map(item => item.id); // 只取执行人id数组
|
||||||
|
|
||||||
|
if (!taskId) {
|
||||||
|
throw new Error('生产任务中未找到任务ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 通过生产任务ID获取报工物料列表
|
||||||
|
console.log('获取报工物料列表...');
|
||||||
|
const materialListRequest = {
|
||||||
|
taskId: taskId
|
||||||
|
};
|
||||||
|
|
||||||
|
const materialListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list_progress_report_materials', materialListRequest);
|
||||||
|
|
||||||
|
if (materialListResponse.code !== 200 || !materialListResponse.data || !materialListResponse.data.outputMaterials || materialListResponse.data.outputMaterials.length === 0) {
|
||||||
|
throw new Error('未找到报工物料信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取报工物料列表
|
||||||
|
const outputMaterials = materialListResponse.data.outputMaterials;
|
||||||
|
|
||||||
|
// 优先选择主产出物料(mainFlag为true),否则选择第一个
|
||||||
|
let selectedMaterial = outputMaterials.find(m => m.mainFlag === true) || outputMaterials[0];
|
||||||
|
|
||||||
|
if (!selectedMaterial) {
|
||||||
|
throw new Error('未找到可报工的物料');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从报工物料中提取报工关系信息
|
||||||
|
const progressReportKey = selectedMaterial.progressReportKey;
|
||||||
|
|
||||||
|
if (!progressReportKey) {
|
||||||
|
throw new Error('报工物料中未找到报工关系信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 构建progressReportMaterial对象
|
||||||
|
const progressReportMaterial = {
|
||||||
|
lineId: progressReportKey.lineId,
|
||||||
|
materialId: progressReportKey.materialId || selectedMaterial.materialInfo?.baseInfo?.id || materialId,
|
||||||
|
reportProcessId: progressReportKey.reportProcessId
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if (!progressReportMaterial.lineId) {
|
||||||
|
throw new Error('无法获取物料行ID');
|
||||||
|
}
|
||||||
|
if (!progressReportMaterial.materialId) {
|
||||||
|
throw new Error('无法获取物料ID');
|
||||||
|
}
|
||||||
|
if (!progressReportMaterial.reportProcessId) {
|
||||||
|
throw new Error('无法获取报工工序ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 返回必填参数
|
||||||
|
const requiredParams = {
|
||||||
|
taskId: taskId,
|
||||||
|
progressReportMaterial: progressReportMaterial,
|
||||||
|
// 额外信息,可能用于构建报工参数
|
||||||
|
materialInfo: selectedMaterial.materialInfo,
|
||||||
|
outputMaterialUnit: selectedMaterial.outputMaterialUnit, // 报工单位信息
|
||||||
|
lineId: progressReportMaterial.lineId,
|
||||||
|
reportProcessId: progressReportMaterial.reportProcessId,
|
||||||
|
executorIds: executorIds, // 执行人列表
|
||||||
|
// 报工物料的其他信息
|
||||||
|
reportType: selectedMaterial.reportType || [], // 可报工方式
|
||||||
|
mainFlag: selectedMaterial.mainFlag, // 是否为主产出
|
||||||
|
warehousingFlag: selectedMaterial.warehousingFlag, // 是否入库
|
||||||
|
autoWarehousingFlag: selectedMaterial.autoWarehousingFlag // 是否自动入库
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('成功获取报工必填参数:', requiredParams);
|
||||||
|
return requiredParams;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取报工必填参数失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建报工请求参数
|
||||||
|
* 将必填参数和表单数据合并,构建完整的报工请求参数
|
||||||
|
* @param {Object} requiredParams - 从接口获取的必填参数
|
||||||
|
* @param {Object} formData - 表单数据
|
||||||
|
* @param {Object} formData.workOrderId - 工单ID
|
||||||
|
* @param {Object} formData.materialId - 物料ID
|
||||||
|
* @param {number} formData.quantity - 报工数量
|
||||||
|
* @param {number} formData.auxiliaryQuantity - 辅助数量
|
||||||
|
* @param {number} formData.qcStatus - 质量状态(1:合格 2:让步合格 3:代检 4:不合格)
|
||||||
|
* @param {number} formData.reportType - 报工方式(1:扫码报工-合格 2:记录报工-合格 3:扫码报工-不合格 4:记录报工-不合格 5:打码报工-合格 6:打码报工-不合格)
|
||||||
|
* @param {Array<number>} formData.executorIds - 执行人ID列表
|
||||||
|
* @returns {Object} 完整的报工请求参数
|
||||||
|
* @note storageLocationId 已预置为 1716848012872791
|
||||||
|
*/
|
||||||
|
buildReportRequestParams(requiredParams, formData) {
|
||||||
|
// 从必填参数中获取基础信息
|
||||||
|
const {
|
||||||
|
taskId, // 生产任务ID(必填)
|
||||||
|
progressReportMaterial, // 报工物料对象(必填)
|
||||||
|
reportProcessId, // 报工工序ID
|
||||||
|
lineId, // 物料行ID
|
||||||
|
outputMaterialUnit, // 报工单位信息
|
||||||
|
executorIds // 执行人ID列表
|
||||||
|
} = requiredParams;
|
||||||
|
|
||||||
|
// 获取报工单位ID(从outputMaterialUnit中获取)
|
||||||
|
const reportUnitId = outputMaterialUnit?.id;
|
||||||
|
if (!reportUnitId) {
|
||||||
|
throw new Error('无法获取报工单位ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建报工请求参数
|
||||||
|
const requestParams = {
|
||||||
|
// 必填字段
|
||||||
|
taskId: taskId,
|
||||||
|
progressReportMaterial: progressReportMaterial || {
|
||||||
|
materialId: formData.materialId,
|
||||||
|
lineId: lineId,
|
||||||
|
reportProcessId: reportProcessId
|
||||||
|
},
|
||||||
|
qcStatus: formData.qcStatus || 1, // 默认合格
|
||||||
|
reportType: formData.reportType || 2, // 默认记录报工-合格
|
||||||
|
|
||||||
|
// 报工详情(必填)
|
||||||
|
progressReportItems: [{
|
||||||
|
executorIds: executorIds ,
|
||||||
|
progressReportMaterialItems: [{
|
||||||
|
reportAmount: formData.quantity,
|
||||||
|
reportUnitId: reportUnitId, // 必填:报工单位ID
|
||||||
|
remark: formData.remark || undefined,
|
||||||
|
// auxAmount1: formData.auxiliaryQuantity || undefined,
|
||||||
|
// auxUnitId1: formData.auxUnitId1 || undefined
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
|
||||||
|
// 可选字段
|
||||||
|
storageLocationId: 1716848012872791,// 预置仓位 Id,不可修改
|
||||||
|
reportStartTime: formData.reportStartTime ? new Date(formData.reportStartTime).getTime() : undefined,
|
||||||
|
reportEndTime: formData.reportEndTime ? new Date(formData.reportEndTime).getTime() : undefined,
|
||||||
|
actualExecutorIds: executorIds || [],
|
||||||
|
actualEquipmentIds: formData.equipmentIds || [],
|
||||||
|
workHour: formData.workHour,
|
||||||
|
workHourUnit: formData.workHourUnit ,
|
||||||
|
qcDefectReasonIds: formData.qcDefectReasonIds || [] // 不良原因(质量状态为不合格时)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除undefined字段
|
||||||
|
Object.keys(requestParams).forEach(key => {
|
||||||
|
if (requestParams[key] === undefined) {
|
||||||
|
delete requestParams[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 递归移除嵌套对象中的undefined字段
|
||||||
|
const cleanUndefined = (obj) => {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(item => cleanUndefined(item));
|
||||||
|
} else if (obj && typeof obj === 'object') {
|
||||||
|
const cleaned = {};
|
||||||
|
Object.keys(obj).forEach(key => {
|
||||||
|
if (obj[key] !== undefined) {
|
||||||
|
cleaned[key] = cleanUndefined(obj[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cleanUndefined(requestParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交报工
|
||||||
|
* @param {Object} reportData - 报工数据
|
||||||
|
* @param {string|number} reportData.workOrderId - 工单ID
|
||||||
|
* @param {string|number} reportData.workOrderCode - 工单编号
|
||||||
|
* @param {string|number} reportData.materialId - 物料ID
|
||||||
|
* @param {number} reportData.quantity - 数量
|
||||||
|
* @param {number} reportData.auxiliaryQuantity - 辅助数量
|
||||||
|
* @param {number} reportData.qcStatus - 质量状态(1:合格 2:让步合格 3:代检 4:不合格)
|
||||||
|
* @param {number} reportData.reportType - 报工方式(1-6)
|
||||||
|
* @param {Array<number>} reportData.executorIds - 执行人ID列表
|
||||||
|
* @param {number} reportData.reportUnitId - 报工单位ID
|
||||||
|
* @param {number} reportData.auxUnitId1 - 辅助单位1 ID
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
async submitReport(reportData) {
|
||||||
|
try {
|
||||||
|
// 验证报工数据
|
||||||
|
this.validateReportData(reportData);
|
||||||
|
|
||||||
|
// 1. 获取报工必填参数
|
||||||
|
const requiredParams = await this.getReportRequiredParams({
|
||||||
|
workOrderId: reportData.workOrderId,
|
||||||
|
materialId: reportData.materialId
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 构建完整的报工请求参数
|
||||||
|
const requestParams = this.buildReportRequestParams(requiredParams, reportData);
|
||||||
|
|
||||||
|
console.log('提交报工请求参数:', requestParams);
|
||||||
|
|
||||||
|
// 3. 调用批量报工接口
|
||||||
|
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report', requestParams);
|
||||||
|
|
||||||
|
// 4. 处理响应
|
||||||
|
const result = this.processReportResponse(response);
|
||||||
|
|
||||||
|
console.log('报工提交成功:', result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('报工提交失败:', error);
|
||||||
|
showToast(error.message || '报工提交失败', 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证报工数据
|
||||||
|
* @param {Object} data - 报工数据
|
||||||
|
*/
|
||||||
|
validateReportData(data) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!data.workOrderId) {
|
||||||
|
errors.push('工单ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.quantity || data.quantity <= 0) {
|
||||||
|
errors.push('数量必须大于0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.auxiliaryQuantity || data.auxiliaryQuantity <= 0) {
|
||||||
|
errors.push('辅助数量必须大于0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(errors.join(', '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理报工响应
|
||||||
|
* 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
|
||||||
|
* @param {Object} response - API响应
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
processReportResponse(response) {
|
||||||
|
// 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
|
||||||
|
if (response.code === 200 || response.code === 0) {
|
||||||
|
const data = response.data || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messageTraceId: data.messageTraceId,
|
||||||
|
progressReportRecordIds: data.progressReportRecordIds || [],
|
||||||
|
message: response.message || '报工提交成功',
|
||||||
|
reportTime: new Date().toISOString()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || `报工提交失败 (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据生产任务查询报工记录列表
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @param {number|Array<number>} params.taskId - 生产任务ID或ID列表(可选)
|
||||||
|
* @param {number|Array<number>} params.workOrderId - 工单ID或ID列表(可选)
|
||||||
|
* @param {number} params.reportTimeFrom - 报工时间From(闭区间),时间戳(可选)
|
||||||
|
* @param {number} params.reportTimeTo - 报工时间To(开区间),时间戳(可选)
|
||||||
|
* @param {number} params.page - 请求页,默认1(可选)
|
||||||
|
* @param {number} params.size - 每页大小,默认200(可选)
|
||||||
|
* @param {Array<number>} params.processIdList - 工序ID列表(可选)
|
||||||
|
* @param {Array<number>} params.executorIdList - 可执行人ID列表(可选)
|
||||||
|
* @param {Array<number>} params.qcStatusList - 质量状态列表(可选)
|
||||||
|
* @param {Array<Object>} params.sorter - 排序条件列表(可选),列表顺序表示排序顺序
|
||||||
|
* @param {string} params.sorter[].field - 排序字段,如 "reportTime", "taskCode" 等
|
||||||
|
* @param {string} params.sorter[].order - 排序规律,默认 "asc","asc" 升序 "desc" 降序
|
||||||
|
* @returns {Promise<Object>} 返回报工记录列表(分页响应)
|
||||||
|
* @returns {boolean} returns.success - 是否成功
|
||||||
|
* @returns {Array<Object>} returns.records - 报工记录数组,每个记录包含以下字段:
|
||||||
|
* @returns {number} returns.records[].id - 报工记录详情id(必填)
|
||||||
|
* @returns {number} returns.records[].lineId - 物料行id(必填)
|
||||||
|
* @returns {number} returns.records[].reportRecordId - 报工记录id(必填)
|
||||||
|
* @returns {number} returns.records[].taskId - 生产任务Id(必填)
|
||||||
|
* @returns {string} returns.records[].taskCode - 生产任务编号(必填)
|
||||||
|
* @returns {number} returns.records[].workOrderId - 工单Id(必填)
|
||||||
|
* @returns {string} returns.records[].workOrderCode - 工单编号(必填)
|
||||||
|
* @returns {number} returns.records[].processId - 工序Id(必填)
|
||||||
|
* @returns {string} returns.records[].processCode - 工序编号(必填)
|
||||||
|
* @returns {string} returns.records[].processName - 工序名称(必填)
|
||||||
|
* @returns {number} returns.records[].mainMaterialId - 工单物料id(必填)
|
||||||
|
* @returns {string} returns.records[].mainMaterialCode - 工单物料编号(必填)
|
||||||
|
* @returns {string} returns.records[].mainMaterialName - 工单物料名称(必填)
|
||||||
|
* @returns {Object} returns.records[].materialInfo - 报工物料信息(必填),包含 baseInfo(物料基本信息)、attribute(物料属性信息)等
|
||||||
|
* @returns {Object} returns.records[].reportBaseAmount - 报工数量1对应数量对象(必填),包含 amount(数量)和 unit(单位信息)
|
||||||
|
* @returns {Object} returns.records[].reportBaseAmountDisplay - 报工数量1对应数量对象-无科学技术法(必填),包含 amount、amountDisplay、unitCode、unitId、unitName
|
||||||
|
* @returns {number} returns.records[].reportTime - 报工时间,时间戳(必填)
|
||||||
|
* @returns {Object} returns.records[].reporter - 报工人员(必填),包含 id、code、name、username、avatarUrl
|
||||||
|
* @returns {Array<Object>} returns.records[].producers - 生产人员(必填),数组元素包含 id、code、name、username、avatarUrl
|
||||||
|
* @returns {Object} returns.records[].qcStatus - 质量状态(必填),包含 code(枚举code)和 message(枚举信息)
|
||||||
|
* @returns {Object} returns.records[].workHourUnit - 工时单位(必填),包含 code 和 message
|
||||||
|
* @returns {number} returns.records[].startTime - 报工开始时间,时间戳(可选)
|
||||||
|
* @returns {number} returns.records[].endTime - 报工结束时间,时间戳(可选)
|
||||||
|
* @returns {number} returns.records[].workHour - 工时(可选)
|
||||||
|
* @returns {string} returns.records[].batchNo - 批次号(可选)
|
||||||
|
* @returns {number} returns.records[].batchNoId - 批次号Id(可选)
|
||||||
|
* @returns {string} returns.records[].flowCardCode - 流转卡编号(可选)
|
||||||
|
* @returns {string} returns.records[].qrCode - 标示码(可选)
|
||||||
|
* @returns {string} returns.records[].reportRecordCode - 报工记录编号(可选)
|
||||||
|
* @returns {string} returns.records[].remark - 备注(可选)
|
||||||
|
* @returns {number} returns.records[].autoWarehousingFlag - 是否自动入库标识 0-否 1-是(可选)
|
||||||
|
* @returns {Array<Object>} returns.records[].equipments - 设备列表(可选),数组元素包含 id、code、name
|
||||||
|
* @returns {Array<Object>} returns.records[].executors - 计划执行人(可选),数组元素包含 id、code、name、username、avatarUrl
|
||||||
|
* @returns {Array<Object>} returns.records[].produceDepartments - 生产部门列表(可选),数组元素包含 id、code、name
|
||||||
|
* @returns {Array<Object>} returns.records[].customFields - 自定义字段(可选),数组元素包含 fieldCode、fieldName、fieldValue 等
|
||||||
|
* @returns {Array<Object>} returns.records[].qcDefectReasons - 不良原因(可选),数组元素包含 id、name
|
||||||
|
* @returns {Array<string>} returns.records[].salesOrderCode - 订单编号(可选)
|
||||||
|
* @returns {Array<number>} returns.records[].pictureIds - 图片ids(可选)
|
||||||
|
* @returns {Object} returns.records[].reportAuxBaseAmount1 - 报工数量2对应数量对象(可选),包含 amount 和 unit
|
||||||
|
* @returns {number} returns.records[].updatedAt - 更新时间,时间戳(可选)
|
||||||
|
* @returns {number} returns.total - 总条数
|
||||||
|
* @returns {number} returns.page - 当前页
|
||||||
|
* @returns {number} returns.size - 每页大小
|
||||||
|
* @returns {string} returns.message - 返回消息
|
||||||
|
*/
|
||||||
|
async getReportRecordsByTask(params) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
taskId,
|
||||||
|
workOrderId,
|
||||||
|
reportTimeFrom,
|
||||||
|
reportTimeTo,
|
||||||
|
page = 1,
|
||||||
|
size = 200,
|
||||||
|
processIdList,
|
||||||
|
executorIdList,
|
||||||
|
qcStatusList,
|
||||||
|
sorter
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
// 构建请求参数
|
||||||
|
const requestParams = {
|
||||||
|
page: parseInt(page),
|
||||||
|
size: parseInt(size)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加排序参数(如果未提供,默认按报工时间降序)
|
||||||
|
if (sorter && Array.isArray(sorter) && sorter.length > 0) {
|
||||||
|
// 使用用户提供的排序条件
|
||||||
|
requestParams.sorter = sorter.map(item => ({
|
||||||
|
field: item.field,
|
||||||
|
order: item.order
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// 默认按报工时间降序排序
|
||||||
|
requestParams.sorter = [{
|
||||||
|
field: 'reportTime',
|
||||||
|
order: 'desc'
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加任务ID列表(支持单个或数组)
|
||||||
|
if (taskId) {
|
||||||
|
requestParams.taskIds = Array.isArray(taskId) ? taskId.map(id => parseInt(id)) : [parseInt(taskId)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加工单ID列表(支持单个或数组)
|
||||||
|
if (workOrderId) {
|
||||||
|
requestParams.workOrderIdList = Array.isArray(workOrderId)
|
||||||
|
? workOrderId.map(id => parseInt(id))
|
||||||
|
: [parseInt(workOrderId)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加报工时间范围
|
||||||
|
if (reportTimeFrom) {
|
||||||
|
requestParams.reportTimeFrom = parseInt(reportTimeFrom);
|
||||||
|
}
|
||||||
|
if (reportTimeTo) {
|
||||||
|
requestParams.reportTimeTo = parseInt(reportTimeTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加其他可选参数
|
||||||
|
if (processIdList && processIdList.length > 0) {
|
||||||
|
requestParams.processIdList = processIdList.map(id => parseInt(id));
|
||||||
|
}
|
||||||
|
if (executorIdList && executorIdList.length > 0) {
|
||||||
|
requestParams.executorIdList = executorIdList.map(id => parseInt(id));
|
||||||
|
}
|
||||||
|
if (qcStatusList && qcStatusList.length > 0) {
|
||||||
|
requestParams.qcStatusList = qcStatusList.map(status => parseInt(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证至少有一个查询条件
|
||||||
|
if (!requestParams.taskIds && !requestParams.workOrderIdList) {
|
||||||
|
throw new Error('必须提供 taskId 或 workOrderId 中的至少一个');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('查询报工记录,参数:', requestParams);
|
||||||
|
|
||||||
|
// 调用报工记录列表接口
|
||||||
|
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list', requestParams);
|
||||||
|
|
||||||
|
// 处理响应
|
||||||
|
if (response.code !== 200 && response.code !== 0) {
|
||||||
|
throw new Error(response.message || `查询报工记录失败 (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应格式:{ code, message, data: { list, page, total } }
|
||||||
|
// data.list 是报工记录数组,每个记录包含以下主要字段:
|
||||||
|
// - 必填字段:id, lineId, reportRecordId, taskId, taskCode, workOrderId, workOrderCode,
|
||||||
|
// processId, processCode, processName, mainMaterialId, mainMaterialCode, mainMaterialName,
|
||||||
|
// materialInfo, reportBaseAmount, reportBaseAmountDisplay, reportTime, reporter, producers, qcStatus, workHourUnit
|
||||||
|
// - 可选字段:startTime, endTime, workHour, batchNo, flowCardCode, qrCode, remark,
|
||||||
|
// equipments, executors, customFields, qcDefectReasons 等
|
||||||
|
const data = response.data || {};
|
||||||
|
const reportRecords = data.list || [];
|
||||||
|
const total = data.total || 0;
|
||||||
|
const currentPage = data.page || page;
|
||||||
|
|
||||||
|
console.log(`查询到 ${reportRecords.length} 条报工记录,共 ${total} 条`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
records: reportRecords, // 报工记录数组,详细字段说明见函数 JSDoc 注释
|
||||||
|
total: total,
|
||||||
|
page: currentPage,
|
||||||
|
size: size,
|
||||||
|
message: response.message || '查询成功'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询报工记录失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局报工服务实例
|
||||||
|
const reportService = new ReportService();
|
||||||
|
|
||||||
781
skills/web-build/templates/report-app/services/core.js
Normal file
781
skills/web-build/templates/report-app/services/core.js
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
/**
|
||||||
|
* 核心服务
|
||||||
|
* 包含API服务和认证服务
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== API服务 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API服务基础类
|
||||||
|
* 封装HTTP请求,自动处理认证和错误
|
||||||
|
*/
|
||||||
|
class ApiService {
|
||||||
|
constructor() {
|
||||||
|
this.baseURL = this.getBaseURL();
|
||||||
|
this.defaultHeaders = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取API基础URL
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getBaseURL() {
|
||||||
|
// 根据环境自动判断API地址
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
// 开发环境,使用黑湖API服务器
|
||||||
|
return 'https://v3-feature.blacklake.cn/api';
|
||||||
|
} else {
|
||||||
|
// 生产环境,使用黑湖API服务器
|
||||||
|
return 'https://v3-feature.blacklake.cn/api';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建完整的请求URL
|
||||||
|
* @param {string} endpoint - API端点
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
buildURL(endpoint) {
|
||||||
|
// 如果endpoint已经是完整URL,直接返回
|
||||||
|
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保endpoint以/开头
|
||||||
|
if (!endpoint.startsWith('/')) {
|
||||||
|
endpoint = '/' + endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.baseURL + endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头
|
||||||
|
* @param {Object} customHeaders - 自定义请求头
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
getHeaders(customHeaders = {}) {
|
||||||
|
const headers = { ...this.defaultHeaders, ...customHeaders };
|
||||||
|
|
||||||
|
// 添加认证头
|
||||||
|
if (authService && authService.isAuthenticated()) {
|
||||||
|
Object.assign(headers, authService.getAuthHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理响应
|
||||||
|
* @param {Response} response - fetch响应对象
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
async handleResponse(response) {
|
||||||
|
// 解析响应数据
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
data = await response.json();
|
||||||
|
} else {
|
||||||
|
data = await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理HTTP状态码错误
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `请求失败: ${response.status}`;
|
||||||
|
|
||||||
|
if (typeof data === 'object' && data.message) {
|
||||||
|
errorMessage = data.message;
|
||||||
|
} else if (typeof data === 'string') {
|
||||||
|
errorMessage = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理认证错误
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
if (authService) {
|
||||||
|
const canRetry = await authService.handleAuthError(response);
|
||||||
|
if (canRetry) {
|
||||||
|
throw new Error('AUTH_RETRY'); // 特殊错误,表示可以重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查业务错误码(即使HTTP状态码是200,业务也可能返回错误)
|
||||||
|
if (typeof data === 'object' && data.code !== undefined) {
|
||||||
|
// 业务错误码:非200且非0表示错误
|
||||||
|
if (data.code !== 200 && data.code !== 0) {
|
||||||
|
// 处理认证相关的业务错误
|
||||||
|
if (data.subCode && (
|
||||||
|
data.subCode.includes('TOKEN') ||
|
||||||
|
data.subCode.includes('AUTH') ||
|
||||||
|
data.subCode.includes('APP_KEY') ||
|
||||||
|
data.subCode.includes('NOT_EXIST')
|
||||||
|
)) {
|
||||||
|
console.warn('业务认证错误:', data.subCode, data.message);
|
||||||
|
if (authService) {
|
||||||
|
const canRetry = await authService.handleAuthError(response);
|
||||||
|
if (canRetry) {
|
||||||
|
throw new Error('AUTH_RETRY');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.message || `业务错误 (code: ${data.code}, subCode: ${data.subCode})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送HTTP请求
|
||||||
|
* @param {string} endpoint - API端点
|
||||||
|
* @param {Object} options - 请求选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
async request(endpoint, options = {}) {
|
||||||
|
const url = this.buildURL(endpoint);
|
||||||
|
const config = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.getHeaders(options.headers),
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果有body且不是FormData,转换为JSON字符串
|
||||||
|
if (config.body && !(config.body instanceof FormData)) {
|
||||||
|
if (typeof config.body === 'object') {
|
||||||
|
config.body = JSON.stringify(config.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = 2;
|
||||||
|
|
||||||
|
while (retryCount <= maxRetries) {
|
||||||
|
try {
|
||||||
|
console.log(`API请求: ${config.method} ${url}`, config.body ? JSON.parse(config.body) : '');
|
||||||
|
|
||||||
|
const response = await fetch(url, config);
|
||||||
|
const result = await this.handleResponse(response);
|
||||||
|
|
||||||
|
console.log(`API响应: ${config.method} ${url}`, result);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`API错误: ${config.method} ${url}`, error);
|
||||||
|
|
||||||
|
// 如果是认证错误且可以重试
|
||||||
|
if (error.message === 'AUTH_RETRY' && retryCount < maxRetries) {
|
||||||
|
retryCount++;
|
||||||
|
// 更新请求头中的认证信息
|
||||||
|
config.headers = this.getHeaders(options.headers);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络错误重试
|
||||||
|
if (error.name === 'TypeError' && error.message.includes('Failed to fetch') && retryCount < maxRetries) {
|
||||||
|
retryCount++;
|
||||||
|
await this.delay(1000 * retryCount); // 延迟重试
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET请求
|
||||||
|
* @param {string} endpoint - API端点
|
||||||
|
* @param {Object} params - 查询参数
|
||||||
|
* @param {Object} options - 请求选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
async get(endpoint, params = {}, options = {}) {
|
||||||
|
// 构建查询字符串
|
||||||
|
const queryString = new URLSearchParams(params).toString();
|
||||||
|
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
||||||
|
|
||||||
|
return this.request(url, {
|
||||||
|
method: 'GET',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST请求
|
||||||
|
* @param {string} endpoint - API端点
|
||||||
|
* @param {any} data - 请求数据
|
||||||
|
* @param {Object} options - 请求选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
async post(endpoint, data = null, options = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT请求
|
||||||
|
* @param {string} endpoint - API端点
|
||||||
|
* @param {any} data - 请求数据
|
||||||
|
* @param {Object} options - 请求选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
async put(endpoint, data = null, options = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE请求
|
||||||
|
* @param {string} endpoint - API端点
|
||||||
|
* @param {Object} options - 请求选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
async delete(endpoint, options = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'DELETE',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
* @param {string} endpoint - API端点
|
||||||
|
* @param {File|FileList} files - 文件对象
|
||||||
|
* @param {Object} additionalData - 额外数据
|
||||||
|
* @param {Function} onProgress - 进度回调
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
async upload(endpoint, files, additionalData = {}, onProgress = null) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// 添加文件
|
||||||
|
if (files instanceof FileList) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
formData.append('files', files[i]);
|
||||||
|
}
|
||||||
|
} else if (files instanceof File) {
|
||||||
|
formData.append('file', files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加额外数据
|
||||||
|
for (const key in additionalData) {
|
||||||
|
formData.append(key, additionalData[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {} // 不设置Content-Type,让浏览器自动设置
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果需要进度回调,使用XMLHttpRequest
|
||||||
|
if (onProgress) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentComplete = (e.loaded / e.total) * 100;
|
||||||
|
onProgress(percentComplete);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(xhr.responseText);
|
||||||
|
resolve(result);
|
||||||
|
} catch (e) {
|
||||||
|
resolve(xhr.responseText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`上传失败: ${xhr.status}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
reject(new Error('上传失败'));
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open('POST', this.buildURL(endpoint));
|
||||||
|
|
||||||
|
// 添加认证头
|
||||||
|
const headers = this.getHeaders();
|
||||||
|
for (const key in headers) {
|
||||||
|
if (key !== 'Content-Type') { // 不设置Content-Type
|
||||||
|
xhr.setRequestHeader(key, headers[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request(endpoint, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟函数
|
||||||
|
* @param {number} ms - 延迟毫秒数
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量请求
|
||||||
|
* @param {Array} requests - 请求数组,每个元素包含 {method, endpoint, data}
|
||||||
|
* @returns {Promise<Array>}
|
||||||
|
*/
|
||||||
|
async batch(requests) {
|
||||||
|
const promises = requests.map(req => {
|
||||||
|
const { method = 'GET', endpoint, data, options = {} } = req;
|
||||||
|
|
||||||
|
switch (method.toUpperCase()) {
|
||||||
|
case 'GET':
|
||||||
|
return this.get(endpoint, data, options);
|
||||||
|
case 'POST':
|
||||||
|
return this.post(endpoint, data, options);
|
||||||
|
case 'PUT':
|
||||||
|
return this.put(endpoint, data, options);
|
||||||
|
case 'DELETE':
|
||||||
|
return this.delete(endpoint, options);
|
||||||
|
default:
|
||||||
|
return Promise.reject(new Error(`不支持的请求方法: ${method}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.allSettled(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置基础URL
|
||||||
|
* @param {string} baseURL - 新的基础URL
|
||||||
|
*/
|
||||||
|
setBaseURL(baseURL) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认请求头
|
||||||
|
* @param {Object} headers - 请求头对象
|
||||||
|
*/
|
||||||
|
setDefaultHeaders(headers) {
|
||||||
|
this.defaultHeaders = { ...this.defaultHeaders, ...headers };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局API服务实例
|
||||||
|
const apiService = new ApiService();
|
||||||
|
|
||||||
|
// ==================== 认证服务 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认配置
|
||||||
|
* 修改登录账号时,需要同时更新 appKey
|
||||||
|
*/
|
||||||
|
const AUTH_CONFIG = {
|
||||||
|
// 登录信息
|
||||||
|
login: {
|
||||||
|
type: 1,
|
||||||
|
username: "cyy",
|
||||||
|
code: "67768820",
|
||||||
|
password: "794db7135639d5b59cd7e53b325f36a8f24fb906e95e403c49e89b99046654fae36a940c1e8496b75b9e69d4f79022c9123aa0d8cd665e2ea9cf584242a702664e77fdd2399c452bd03a174a3edbb41b86a406851b4da8b11b8faa7044925e3e9bffd4fd5afb14c70f592a2114ce5f45cf567e2e1f0d8688ef345ca28c2687c5"
|
||||||
|
},
|
||||||
|
// AppKey(用于获取user-access token)
|
||||||
|
appKey: "cli_1764100796489835"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证服务
|
||||||
|
* 处理token获取、存储和管理
|
||||||
|
*/
|
||||||
|
class AuthService {
|
||||||
|
constructor() {
|
||||||
|
this.token = null; // 最终用于业务接口的user-access token
|
||||||
|
this.tokenKey = 'work_report_user_access_token';
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化认证服务
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// 1. 先查看URL里是否存在code
|
||||||
|
const code = getUrlParameter('code');
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
// 如果存在code,直接使用code获取user-access token
|
||||||
|
console.log('从URL参数获取到code:', code);
|
||||||
|
await this.getUserAccessTokenByCode(code);
|
||||||
|
} else {
|
||||||
|
// 如果不存在code,走登录流程
|
||||||
|
console.log('URL参数中没有code,走登录流程...');
|
||||||
|
|
||||||
|
// 2.1 先登录获取token
|
||||||
|
console.log('开始登录...');
|
||||||
|
const loginResult = await this.loginAndGetToken();
|
||||||
|
|
||||||
|
if (!loginResult || !loginResult.token) {
|
||||||
|
throw new Error('登录失败,未获取到token');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('登录成功,获取到登录token');
|
||||||
|
|
||||||
|
// 2.2 使用登录token获取code
|
||||||
|
console.log('使用登录token获取code...');
|
||||||
|
const codeFromApi = await this.getCodeByLoginToken(loginResult.token);
|
||||||
|
|
||||||
|
if (!codeFromApi) {
|
||||||
|
throw new Error('获取code失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('成功获取code:', codeFromApi);
|
||||||
|
|
||||||
|
// 2.3 使用code获取user-access token
|
||||||
|
await this.getUserAccessTokenByCode(codeFromApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('认证初始化完成,user-access token已获取');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('认证初始化失败:', error);
|
||||||
|
showToast('认证初始化失败,请刷新页面重试', 'error');
|
||||||
|
throw error; // 抛出错误,让调用方知道初始化失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用code获取user-access token
|
||||||
|
* @param {string} code - code参数
|
||||||
|
*/
|
||||||
|
async getUserAccessTokenByCode(code) {
|
||||||
|
try {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('code参数不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('使用code获取user-access token...');
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
code: code,
|
||||||
|
appKey: AUTH_CONFIG.appKey
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('https://v3-feature.blacklake.cn/api/openapi/domain/api/v1/access_token/_get_user_token_for_customized', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`获取user-access token失败: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('获取user-access token响应:', data);
|
||||||
|
|
||||||
|
// 根据实际响应格式提取token
|
||||||
|
if (data.code === 200 || data.code === 0) {
|
||||||
|
const tokenData = data.data || data.result || data;
|
||||||
|
const userAccessToken = tokenData.token ||
|
||||||
|
tokenData.userAccessToken ||
|
||||||
|
tokenData.accessToken ||
|
||||||
|
data.token ||
|
||||||
|
data.userAccessToken ||
|
||||||
|
data.accessToken;
|
||||||
|
|
||||||
|
if (userAccessToken) {
|
||||||
|
this.token = userAccessToken;
|
||||||
|
this.saveToken(userAccessToken);
|
||||||
|
console.log('成功获取user-access token');
|
||||||
|
} else {
|
||||||
|
throw new Error('响应中没有找到user-access token');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || `获取user-access token失败 (code: ${data.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取user-access token失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用登录token获取code
|
||||||
|
* @param {string} loginToken - 登录获取的token
|
||||||
|
* @returns {Promise<string>} 返回code
|
||||||
|
*/
|
||||||
|
async getCodeByLoginToken(loginToken) {
|
||||||
|
try {
|
||||||
|
if (!loginToken) {
|
||||||
|
throw new Error('登录token不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('使用登录token获取code...');
|
||||||
|
|
||||||
|
const response = await fetch('https://v3-feature.blacklake.cn/api/openapiadmin/domain/web/v1/access_token/_code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-AUTH': loginToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`获取code失败: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('获取code响应:', data);
|
||||||
|
|
||||||
|
// 根据实际响应格式提取code
|
||||||
|
if (data.code === 200 || data.code === 0) {
|
||||||
|
const codeData = data.data || data.result || data;
|
||||||
|
const code = codeData.code ||
|
||||||
|
codeData.accessToken ||
|
||||||
|
data.code ||
|
||||||
|
data.accessToken;
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
console.log('成功获取code');
|
||||||
|
return code;
|
||||||
|
} else {
|
||||||
|
throw new Error('响应中没有找到code');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || `获取code失败 (code: ${data.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取code失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录并获取token
|
||||||
|
*/
|
||||||
|
async loginAndGetToken() {
|
||||||
|
try {
|
||||||
|
const loginData = {
|
||||||
|
type: AUTH_CONFIG.login.type,
|
||||||
|
username: AUTH_CONFIG.login.username,
|
||||||
|
code: AUTH_CONFIG.login.code,
|
||||||
|
password: AUTH_CONFIG.login.password
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('尝试登录获取token...');
|
||||||
|
const response = await fetch('https://v3-feature.blacklake.cn/api/user/domain/web/v1/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(loginData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`登录失败: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('登录响应完整数据:', JSON.stringify(data, null, 2));
|
||||||
|
console.log('登录响应状态码:', response.status);
|
||||||
|
console.log('登录响应headers:', Object.fromEntries(response.headers.entries()));
|
||||||
|
|
||||||
|
// 检查响应是否成功(支持多种响应格式)
|
||||||
|
const isSuccess = data.success === true ||
|
||||||
|
data.code === 0 ||
|
||||||
|
data.code === 200 ||
|
||||||
|
(data.code === undefined && response.ok);
|
||||||
|
|
||||||
|
console.log('登录响应是否成功:', isSuccess, 'code:', data.code, 'success:', data.success);
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
// 尝试从不同位置提取token(这是登录token,用于后续获取code)
|
||||||
|
const tokenData = data.data || data.result || data;
|
||||||
|
const loginToken = tokenData?.token ||
|
||||||
|
tokenData?.access_token ||
|
||||||
|
tokenData?.accessToken ||
|
||||||
|
data.token ||
|
||||||
|
data.access_token ||
|
||||||
|
data.accessToken;
|
||||||
|
|
||||||
|
if (loginToken) {
|
||||||
|
console.log('登录成功,获取到登录token');
|
||||||
|
return { token: loginToken };
|
||||||
|
} else {
|
||||||
|
const errorMsg = '登录响应中没有找到token';
|
||||||
|
console.error(errorMsg, data);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 登录失败,抛出错误
|
||||||
|
const errorMsg = data.message || data.msg || `登录失败 (code: ${data.code})`;
|
||||||
|
console.error('登录失败:', errorMsg, data);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录接口调用失败:', error);
|
||||||
|
throw error; // 重新抛出错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前token
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
getToken() {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已认证
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isAuthenticated() {
|
||||||
|
return !!this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存token到本地存储
|
||||||
|
* @param {string} token - 要保存的token
|
||||||
|
*/
|
||||||
|
saveToken(token) {
|
||||||
|
try {
|
||||||
|
const tokenData = {
|
||||||
|
token: token,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24小时后过期
|
||||||
|
};
|
||||||
|
localStorage.setItem(this.tokenKey, JSON.stringify(tokenData));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存token失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地存储获取token
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
getStoredToken() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(this.tokenKey);
|
||||||
|
if (!stored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = JSON.parse(stored);
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (tokenData.expiresAt && Date.now() > tokenData.expiresAt) {
|
||||||
|
console.log('存储的token已过期');
|
||||||
|
this.clearToken();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenData.token;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取存储token失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除token
|
||||||
|
*/
|
||||||
|
clearToken() {
|
||||||
|
this.token = null;
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(this.tokenKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除token失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新token(重新执行完整的认证流程)
|
||||||
|
*/
|
||||||
|
async refreshToken() {
|
||||||
|
console.log('刷新token,重新执行认证流程...');
|
||||||
|
this.clearToken();
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取认证头部
|
||||||
|
* 根据接口文档,所有业务接口都需要使用 X-AUTH 头部,值为 user-access token
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
getAuthHeaders() {
|
||||||
|
const headers = {};
|
||||||
|
if (this.token) {
|
||||||
|
// 使用最终的user-access token
|
||||||
|
headers['X-AUTH'] = this.token;
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理认证错误
|
||||||
|
* @param {Response} response - HTTP响应对象
|
||||||
|
*/
|
||||||
|
async handleAuthError(response) {
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
console.log('认证失败,尝试刷新token');
|
||||||
|
showToast('认证已过期,正在重新获取...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.refreshToken();
|
||||||
|
return true; // 表示可以重试请求
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新token失败:', error);
|
||||||
|
showToast('认证失败,请刷新页面', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出
|
||||||
|
*/
|
||||||
|
logout() {
|
||||||
|
this.clearToken();
|
||||||
|
showToast('已退出登录', 'info');
|
||||||
|
// 可以重定向到登录页面或刷新页面
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局认证服务实例
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
419
skills/web-build/templates/report-h5/index.html
Normal file
419
skills/web-build/templates/report-h5/index.html
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<!-- 智筑报工页面 -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>智筑报工页面</title>
|
||||||
|
<style>
|
||||||
|
/* CSS Reset & Base */
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.flex { display: flex; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.items-start { align-items: flex-start; }
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
.justify-center { justify-content: center; }
|
||||||
|
.flex-col { flex-direction: column; }
|
||||||
|
.flex-shrink-0 { flex-shrink: 0; }
|
||||||
|
.space-x-2 > * + * { margin-left: 0.5rem; }
|
||||||
|
.space-y-1 > * + * { margin-top: 0.25rem; }
|
||||||
|
.space-y-2 > * + * { margin-top: 0.5rem; }
|
||||||
|
.space-y-3 > * + * { margin-top: 0.75rem; }
|
||||||
|
.hidden { display: none; }
|
||||||
|
.cursor-pointer { cursor: pointer; }
|
||||||
|
.w-full { width: 100%; }
|
||||||
|
.max-w-md { max-width: 28rem; margin-left: auto; margin-right: auto; }
|
||||||
|
.max-h-60 { max-height: 15rem; }
|
||||||
|
.overflow-y-auto { overflow-y: auto; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
.p-1 { padding: 0.25rem; }
|
||||||
|
.p-3 { padding: 0.75rem; }
|
||||||
|
.p-4 { padding: 1rem; }
|
||||||
|
.p-5 { padding: 1.25rem; }
|
||||||
|
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||||
|
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||||
|
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
||||||
|
.py-3\.5 { padding-top: 0.875rem; padding-bottom: 0.875rem; }
|
||||||
|
.pb-4 { padding-bottom: 1rem; }
|
||||||
|
.pb-10 { padding-bottom: 2.5rem; }
|
||||||
|
.pl-1 { padding-left: 0.25rem; }
|
||||||
|
.mb-1 { margin-bottom: 0.25rem; }
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
.mb-6 { margin-bottom: 1.5rem; }
|
||||||
|
.mb-8 { margin-bottom: 2rem; }
|
||||||
|
.mt-1 { margin-top: 0.25rem; }
|
||||||
|
.mt-6 { margin-top: 1.5rem; }
|
||||||
|
.mt-0\.5 { margin-top: 0.125rem; }
|
||||||
|
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
.text-xs { font-size: 0.75rem; }
|
||||||
|
.text-sm { font-size: 0.875rem; }
|
||||||
|
.text-base { font-size: 1rem; }
|
||||||
|
.text-lg { font-size: 1.125rem; }
|
||||||
|
.font-medium { font-weight: 500; }
|
||||||
|
.font-bold { font-weight: 700; }
|
||||||
|
.font-mono { font-family: monospace; }
|
||||||
|
.leading-none { line-height: 1; }
|
||||||
|
|
||||||
|
/* Colors */
|
||||||
|
.bg-white { background-color: #ffffff; }
|
||||||
|
.bg-blue-600 { background-color: #2563eb; }
|
||||||
|
.bg-gray-50 { background-color: #f9fafb; }
|
||||||
|
.bg-gray-800 { background-color: #1f2937; }
|
||||||
|
.bg-black { background-color: rgba(0, 0, 0, 0.5); }
|
||||||
|
.text-white { color: #ffffff; }
|
||||||
|
.text-gray-400 { color: #9ca3af; }
|
||||||
|
.text-gray-500 { color: #6b7280; }
|
||||||
|
.text-gray-600 { color: #4b5563; }
|
||||||
|
.text-gray-800 { color: #1f2937; }
|
||||||
|
.text-blue-600 { color: #2563eb; }
|
||||||
|
.text-green-400 { color: #4ade80; }
|
||||||
|
.text-red-400 { color: #f87171; }
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
.border { border-width: 1px; }
|
||||||
|
.border-b { border-bottom-width: 1px; }
|
||||||
|
.border-l-4 { border-left-width: 4px; }
|
||||||
|
.border-gray-100 { border-color: #f3f4f6; }
|
||||||
|
.border-gray-200 { border-color: #e5e7eb; }
|
||||||
|
.border-blue-300 { border-color: #93c5fd; }
|
||||||
|
.border-blue-600 { border-color: #2563eb; }
|
||||||
|
.border-transparent { border-color: transparent; }
|
||||||
|
.rounded-lg { border-radius: 0.5rem; }
|
||||||
|
.rounded-xl { border-radius: 0.75rem; }
|
||||||
|
.rounded-2xl { border-radius: 1rem; }
|
||||||
|
.rounded-t-2xl { border-top-left-radius: 1rem; border-top-right-radius: 1rem; }
|
||||||
|
.rounded-full { border-radius: 9999px; }
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
.card-shadow { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); }
|
||||||
|
.shadow-md { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
|
||||||
|
.shadow-lg { box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); }
|
||||||
|
|
||||||
|
/* Positioning */
|
||||||
|
.fixed { position: fixed; }
|
||||||
|
.sticky { position: sticky; }
|
||||||
|
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
|
||||||
|
.top-0 { top: 0; }
|
||||||
|
.bottom-10 { bottom: 2.5rem; }
|
||||||
|
.left-1\/2 { left: 50%; }
|
||||||
|
.z-10 { z-index: 10; }
|
||||||
|
.z-50 { z-index: 50; }
|
||||||
|
|
||||||
|
/* Transforms */
|
||||||
|
.transform { transform: translate(var(--tw-translate-x, 0), var(--tw-translate-y, 0)); }
|
||||||
|
.-translate-x-1\/2 { transform: translateX(-50%); }
|
||||||
|
.translate-y-full { transform: translateY(100%); }
|
||||||
|
.translate-y-0 { transform: translateY(0); }
|
||||||
|
.translate-y-10 { transform: translateY(2.5rem); }
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
.transition-all { transition: all 0.3s; }
|
||||||
|
.transition-opacity { transition: opacity 0.3s; }
|
||||||
|
.transition-transform { transition: transform 0.3s; }
|
||||||
|
.duration-300 { transition-duration: 300ms; }
|
||||||
|
|
||||||
|
/* Opacity */
|
||||||
|
.opacity-0 { opacity: 0; }
|
||||||
|
.pointer-events-none { pointer-events: none; }
|
||||||
|
|
||||||
|
/* Hover & Active */
|
||||||
|
.bg-blue-600:hover { background-color: #1d4ed8; }
|
||||||
|
.bg-gray-50:active, .bg-gray-50.active { background-color: #eff6ff; }
|
||||||
|
.border-transparent:hover { border-color: #93c5fd; }
|
||||||
|
.btn-active:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons (simple SVG replacements) */
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.icon-chevron-right::after { content: '›'; font-size: 1.5rem; }
|
||||||
|
.icon-check::after { content: '✓'; }
|
||||||
|
.icon-x::after { content: '×'; font-size: 1.5rem; }
|
||||||
|
.icon-clipboard::after { content: '📋'; }
|
||||||
|
.icon-alert::after { content: '⚠'; }
|
||||||
|
|
||||||
|
/* Responsive - Mobile First */
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:items-center { align-items: center; }
|
||||||
|
.sm\:rounded-xl { border-radius: 0.75rem; }
|
||||||
|
.sm\:translate-y-0 { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="pb-10">
|
||||||
|
|
||||||
|
<!-- 顶部导航/标题 -->
|
||||||
|
<header class="bg-white sticky top-0 z-10 border-b border-gray-200 px-4 py-3 text-center">
|
||||||
|
<h1 class="text-lg font-bold text-gray-800">智筑报工页面</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="p-4 max-w-md mx-auto">
|
||||||
|
|
||||||
|
<!-- 上部:工单信息区域 -->
|
||||||
|
<div class="bg-white rounded-xl card-shadow p-5 mb-6 border border-gray-200" style="border-width: 2px;">
|
||||||
|
<!-- 生产工单选择行 -->
|
||||||
|
<div onclick="openOrderSelector()" class="flex items-center justify-between border-b border-gray-100 pb-4 mb-4 cursor-pointer btn-active">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-gray-800 font-bold text-base mb-1">生产工单</span>
|
||||||
|
<!-- 模拟未选择或已选择的状态 -->
|
||||||
|
<span id="selectedOrderText" class="text-gray-400 text-sm">请选择生产工单</span>
|
||||||
|
</div>
|
||||||
|
<span class="icon icon-chevron-right text-gray-400"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物料信息 -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="text-gray-500 text-sm w-24 flex-shrink-0">加工物料名称:</span>
|
||||||
|
<span id="materialName" class="text-gray-800 text-sm font-medium">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="text-gray-500 text-sm w-24 flex-shrink-0">加工物料编号:</span>
|
||||||
|
<span id="materialCode" class="text-gray-800 text-sm font-medium">--</span>
|
||||||
|
</div>
|
||||||
|
<!-- 隐藏的任务ID字段,模拟查询到的任务 -->
|
||||||
|
<div id="taskIdContainer" class="hidden flex items-start">
|
||||||
|
<span class="text-gray-500 text-sm w-24 flex-shrink-0">关联任务ID:</span>
|
||||||
|
<span id="taskIdDisplay" class="text-gray-500 text-xs font-mono mt-0.5">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中部:报工按钮 -->
|
||||||
|
<button onclick="handleAutoReport()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3.5 px-4 rounded-xl shadow-md btn-active flex items-center justify-center space-x-2 mb-8" style="border: 2px solid #1d4ed8;">
|
||||||
|
<span class="icon icon-clipboard"></span>
|
||||||
|
<span>点 此 报 工 (自动 10 个)</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 下部:报工记录 -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold text-gray-800 mb-4 pl-1 border-l-4 border-blue-600 leading-none">报工记录</h2>
|
||||||
|
|
||||||
|
<div id="recordList" class="space-y-3">
|
||||||
|
<!-- 记录项 1 (草图数据) -->
|
||||||
|
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center" style="border-width: 2px;">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-gray-500 text-sm">报工数量</div>
|
||||||
|
<div class="text-gray-500 text-sm">报工时间</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right space-y-1">
|
||||||
|
<div class="text-gray-800 font-bold text-lg">10 个</div>
|
||||||
|
<div class="text-gray-600 text-sm">2025.11.25 16:27</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 记录项 2 (草图数据) -->
|
||||||
|
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center" style="border-width: 2px;">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-gray-500 text-sm">报工数量</div>
|
||||||
|
<div class="text-gray-500 text-sm">报工时间</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right space-y-1">
|
||||||
|
<div class="text-gray-800 font-bold text-lg">10 个</div>
|
||||||
|
<div class="text-gray-600 text-sm">2025.11.25 15:20</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 记录项 3 (草图数据) -->
|
||||||
|
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center" style="border-width: 2px;">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-gray-500 text-sm">报工数量</div>
|
||||||
|
<div class="text-gray-500 text-sm">报工时间</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right space-y-1">
|
||||||
|
<div class="text-gray-800 font-bold text-lg">10 个</div>
|
||||||
|
<div class="text-gray-600 text-sm">2025.11.25 14:11</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 到底提示 -->
|
||||||
|
<div class="text-center text-gray-400 text-xs mt-6">
|
||||||
|
- 暂无更多记录 -
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 模拟弹窗:选择工单 -->
|
||||||
|
<div id="orderModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-end sm:items-center justify-center transition-opacity duration-300">
|
||||||
|
<div class="bg-white w-full max-w-md rounded-t-2xl sm:rounded-xl p-5 transform transition-transform duration-300 translate-y-full sm:translate-y-0 shadow-lg" id="orderModalContent" style="border: 2px solid #e5e7eb;">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-bold">选择生产工单</h3>
|
||||||
|
<button onclick="closeOrderModal()" class="text-gray-500 p-1"><span class="icon icon-x"></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||||
|
<div onclick="selectOrder('MO-20251125-001', '高强度螺栓 M24', 'MT-8823-A', 'TASK-001-A')" class="p-3 bg-gray-50 rounded-lg active:bg-blue-50 cursor-pointer border border-transparent hover:border-blue-300" style="border-width: 2px;">
|
||||||
|
<div class="font-bold text-gray-800">MO-20251125-001</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">物料:高强度螺栓 M24</div>
|
||||||
|
</div>
|
||||||
|
<div onclick="selectOrder('MO-20251125-002', '精密齿轮 Z-50', 'MT-5501-B', 'TASK-002-B')" class="p-3 bg-gray-50 rounded-lg active:bg-blue-50 cursor-pointer border border-transparent hover:border-blue-300" style="border-width: 2px;">
|
||||||
|
<div class="font-bold text-gray-800">MO-20251125-002</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">物料:精密齿轮 Z-50</div>
|
||||||
|
</div>
|
||||||
|
<div onclick="selectOrder('MO-20251125-003', '传动轴承 AX-99', 'MT-1209-C', 'TASK-003-C')" class="p-3 bg-gray-50 rounded-lg active:bg-blue-50 cursor-pointer border border-transparent hover:border-blue-300" style="border-width: 2px;">
|
||||||
|
<div class="font-bold text-gray-800">MO-20251125-003</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">物料:传动轴承 AX-99</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast 提示组件 (修改了结构以支持动态图标) -->
|
||||||
|
<div id="toast" class="fixed bottom-10 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-6 py-3 rounded-full shadow-lg z-50 flex items-center space-x-2 transition-all duration-300 opacity-0 pointer-events-none translate-y-10" style="border: 2px solid #374151;">
|
||||||
|
<!-- 图标容器 -->
|
||||||
|
<div id="toast-icon-container">
|
||||||
|
<span class="icon icon-check text-green-400"></span>
|
||||||
|
</div>
|
||||||
|
<span id="toast-text" class="font-medium text-sm">报工成功</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 状态变量
|
||||||
|
let currentOrder = null;
|
||||||
|
|
||||||
|
// --- 工单选择逻辑 ---
|
||||||
|
const orderModal = document.getElementById('orderModal');
|
||||||
|
const orderModalContent = document.getElementById('orderModalContent');
|
||||||
|
|
||||||
|
function openOrderSelector() {
|
||||||
|
orderModal.classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
orderModalContent.classList.remove('translate-y-full');
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOrderModal() {
|
||||||
|
orderModalContent.classList.add('translate-y-full');
|
||||||
|
setTimeout(() => {
|
||||||
|
orderModal.classList.add('hidden');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择工单后,模拟查询到任务并展示物料信息
|
||||||
|
function selectOrder(orderNo, materialName, materialCode, taskId) {
|
||||||
|
currentOrder = { orderNo, materialName, materialCode, taskId };
|
||||||
|
|
||||||
|
// 更新 UI
|
||||||
|
document.getElementById('selectedOrderText').innerText = orderNo;
|
||||||
|
document.getElementById('selectedOrderText').classList.replace('text-gray-400', 'text-blue-600');
|
||||||
|
document.getElementById('selectedOrderText').classList.add('font-bold');
|
||||||
|
|
||||||
|
document.getElementById('materialName').innerText = materialName;
|
||||||
|
document.getElementById('materialCode').innerText = materialCode;
|
||||||
|
|
||||||
|
// 显示关联的任务ID
|
||||||
|
const taskContainer = document.getElementById('taskIdContainer');
|
||||||
|
const taskDisplay = document.getElementById('taskIdDisplay');
|
||||||
|
taskContainer.classList.remove('hidden');
|
||||||
|
taskDisplay.innerText = taskId;
|
||||||
|
|
||||||
|
closeOrderModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 自动报工逻辑 ---
|
||||||
|
function handleAutoReport() {
|
||||||
|
// 1. 校验是否选择了工单
|
||||||
|
if (!currentOrder) {
|
||||||
|
showToast("请先选择生产工单", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 模拟自动报工过程
|
||||||
|
const autoAmount = 10;
|
||||||
|
const now = new Date();
|
||||||
|
const timeString = `${now.getFullYear()}.${(now.getMonth()+1).toString().padStart(2, '0')}.${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
// 3. 创建记录 DOM
|
||||||
|
const newRecordHtml = `
|
||||||
|
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center animate-pulse-once">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-gray-500 text-sm">报工数量</div>
|
||||||
|
<div class="text-gray-500 text-sm">报工时间</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right space-y-1">
|
||||||
|
<div class="text-gray-800 font-bold text-lg text-blue-600">${autoAmount} 个</div>
|
||||||
|
<div class="text-gray-600 text-sm">${timeString}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 4. 插入到列表最前面
|
||||||
|
const list = document.getElementById('recordList');
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = newRecordHtml;
|
||||||
|
const newElement = tempDiv.firstElementChild;
|
||||||
|
|
||||||
|
newElement.style.transition = "all 0.5s ease";
|
||||||
|
newElement.style.opacity = "0";
|
||||||
|
newElement.style.transform = "translateY(-10px)";
|
||||||
|
|
||||||
|
list.insertBefore(newElement, list.firstChild);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
newElement.style.opacity = "1";
|
||||||
|
newElement.style.transform = "translateY(0)";
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// 5. 显示成功提示
|
||||||
|
showToast("报工成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Toast 提示逻辑 (修复了 Null 报错问题) ---
|
||||||
|
let toastTimeout;
|
||||||
|
function showToast(message, type = 'success') {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
const toastIconContainer = document.getElementById('toast-icon-container'); // 使用容器获取
|
||||||
|
const toastText = document.getElementById('toast-text');
|
||||||
|
|
||||||
|
// 设置内容
|
||||||
|
toastText.innerText = message;
|
||||||
|
|
||||||
|
// 根据类型重置图标 HTML
|
||||||
|
if (type === 'error') {
|
||||||
|
toastIconContainer.innerHTML = '<span class="icon icon-alert text-red-400"></span>';
|
||||||
|
} else {
|
||||||
|
toastIconContainer.innerHTML = '<span class="icon icon-check text-green-400"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示
|
||||||
|
toast.classList.remove('opacity-0', 'translate-y-10');
|
||||||
|
|
||||||
|
// 清除上一次的定时器
|
||||||
|
if (toastTimeout) clearTimeout(toastTimeout);
|
||||||
|
|
||||||
|
// 2秒后自动隐藏
|
||||||
|
toastTimeout = setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-10');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target == orderModal) {
|
||||||
|
closeOrderModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user