commit b710247ba7770aaa64232ae5b084b5a70cc8a4cc Author: Zhongwei Li Date: Sun Nov 30 08:55:46 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..72a7fab --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e97236b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# coder-web-plugin + +Web development plugin with frontend agents diff --git a/agents/frontend-ant.md b/agents/frontend-ant.md new file mode 100644 index 0000000..74f79e7 --- /dev/null +++ b/agents/frontend-ant.md @@ -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('/api/endpoint', { + method: 'GET', + params: { id: 123 } +}); + +// POST 请求(带认证) +const result = await request('/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 + { + const data = await fetchData(params); + return { data: data.list, total: data.total }; + }} + rowKey="id" +/> + +// ProForm + { await submitForm(values); }}> + + +``` + +## 【工作流程】 + +1. **需求分析**:理解用户需求,确定页面和功能 +2. **开发页面**:在 `src/pages/` 创建页面,`src/services/` 添加 API +3. **配置路由**:新页面需在 `config/routes.ts` 添加路由 +4. **执行构建**:**必须**调用 `@web-build` 执行构建 + +## 【注意事项】 + +- 遵循 React 和 TypeScript 最佳实践 +- 所有 API 调用需有错误处理 +- 使用 message、notification 提供用户反馈 +- **构建成功即完成**,无需访问页面或测试接口 diff --git a/agents/frontend-h5.md b/agents/frontend-h5.md new file mode 100644 index 0000000..5fe630e --- /dev/null +++ b/agents/frontend-h5.md @@ -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` 的 `
` 区域 +**示例**: +```html + +
+ +
+ +
+
+``` + +### 场景 3:修改工单数据获取 +**位置**: `services/business.js` 的 `getWorkOrderList` 方法 +**模式**: +1. 找到方法(line 28) +2. 修改 `requestBody` 参数 +3. 修改 `processWorkOrderListResponse` 的字段映射(line 85) + +### 场景 4:修改物料信息显示 +**位置**: `index.html` 的 `
` 区域 +**模式**: +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 + diff --git a/agents/frontend-html-back.md b/agents/frontend-html-back.md new file mode 100644 index 0000000..6ccb4f8 --- /dev/null +++ b/agents/frontend-html-back.md @@ -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. **快速交付**:页面可直接访问,无需构建,**无需检查代码,无需测试** diff --git a/agents/frontend-html.md b/agents/frontend-html.md new file mode 100644 index 0000000..9f1b915 --- /dev/null +++ b/agents/frontend-html.md @@ -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` 的 `
` 区域 +**示例**: +```html + +
+ +
+ +
+
+``` + +### 场景 3:修改工单数据获取 +**位置**: `services/business.js` 的 `getWorkOrderList` 方法 +**模式**: +1. 找到方法(line 28) +2. 修改 `requestBody` 参数 +3. 修改 `processWorkOrderListResponse` 的字段映射(line 85) + +### 场景 4:修改物料信息显示 +**位置**: `index.html` 的 `
` 区域 +**模式**: +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 diff --git a/agents/frontend-react.md b/agents/frontend-react.md new file mode 100644 index 0000000..4b13de1 --- /dev/null +++ b/agents/frontend-react.md @@ -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(); +``` + +### 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
加载中...
; + + return ( +
+

React 应用

+
    + {data.map(item =>
  • {item.name}
  • )} +
+
+ ); +} + +export default App; +``` + +## 【工作流程】 + +1. **需求分析**:理解用户需求,确定组件结构 +2. **创建文件**:在 `src/` 目录下创建 main.jsx 和 App.jsx +3. **组件开发**:使用函数组件和 Hooks 实现功能 +4. **样式设计**:使用内联样式实现响应式布局 + +## 【注意事项】 + +- **函数组件优先**:使用函数组件和 Hooks +- **组件职责单一**:合理拆分组件 +- **错误处理**:添加适当的错误处理 +- **性能优化**:合理使用 useMemo、useCallback diff --git a/agents/frontend-vue.md b/agents/frontend-vue.md new file mode 100644 index 0000000..646cc26 --- /dev/null +++ b/agents/frontend-vue.md @@ -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 + + + + + +``` + +## 【工作流程】 + +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 +- **构建成功即完成**,无需访问页面或测试接口 diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..51fce58 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/skills/web-build/SKILL.md b/skills/web-build/SKILL.md new file mode 100644 index 0000000..dff2575 --- /dev/null +++ b/skills/web-build/SKILL.md @@ -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}` diff --git a/skills/web-build/templates/mes-dashboard/index.html b/skills/web-build/templates/mes-dashboard/index.html new file mode 100644 index 0000000..6967e53 --- /dev/null +++ b/skills/web-build/templates/mes-dashboard/index.html @@ -0,0 +1,651 @@ + + + + + + 智能工厂生产实时监控中心 + + + + +
+ +
智能制造MES生产看板
+
00:00:00
+
+ +
+ +
+
+
设备综合效率 (OEE)
+
+
+
+
+
+
85%
+
OEE
+
+
+
+
+
+
+
42
+
运行中
+
+
+
3
+
待机
+
+
+
1
+
故障
+
+
+
+
+ +
+
+
车间产出进度
+
+
+
+
A线 - 电子组装92%
+
+
+
+
B线 - 注塑成型78%
+
+
+
+
C线 - 包装质检45%
+
+
+
+
D线 - 激光焊接88%
+
+
+
+
+
+ + +
+
+
+
24,592
+
今日总产量 (PCS)
+
+
+
99.2%
+
良品率
+
+
+
4.2s
+
平均节拍
+
+
+ +
+
+
+
+
+
+
+ ⚙️ +
+
+
+
主轴转速: 1200 RPM
+
核心温度: 45.2 °C
+
+
+
+ SYSTEM: ONLINE +
+
+
+
+ + +
+
+
实时产量趋势 (24H)
+
+ +
+
+ +
+
+
实时告警信息
+
+
+ +
+ 10:42 + 警告 + B线 3号机台 温度偏高 +
+
+ 09:15 + 故障 + C线 缺料停机报警 +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/skills/web-build/templates/report-app/README.md b/skills/web-build/templates/report-app/README.md new file mode 100644 index 0000000..151a154 --- /dev/null +++ b/skills/web-build/templates/report-app/README.md @@ -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行) +- **模式**: 复制相邻的 `
` 结构 + +#### 场景 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+)** +- **无需构建**,直接作为静态资源访问 diff --git a/skills/web-build/templates/report-app/api_doc/工单列表_BLACKLAKE-1686655055663532.json b/skills/web-build/templates/report-app/api_doc/工单列表_BLACKLAKE-1686655055663532.json new file mode 100644 index 0000000..ed8e57e --- /dev/null +++ b/skills/web-build/templates/report-app/api_doc/工单列表_BLACKLAKE-1686655055663532.json @@ -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 方法" +} + diff --git a/skills/web-build/templates/report-app/api_doc/批量报工_BLACKLAKE-1681109889053798.json b/skills/web-build/templates/report-app/api_doc/批量报工_BLACKLAKE-1681109889053798.json new file mode 100644 index 0000000..a9b7b0e --- /dev/null +++ b/skills/web-build/templates/report-app/api_doc/批量报工_BLACKLAKE-1681109889053798.json @@ -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 方法" +} + diff --git a/skills/web-build/templates/report-app/api_doc/批量报工_简化版.json b/skills/web-build/templates/report-app/api_doc/批量报工_简化版.json new file mode 100644 index 0000000..f5b1d23 --- /dev/null +++ b/skills/web-build/templates/report-app/api_doc/批量报工_简化版.json @@ -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 方法" +} + diff --git a/skills/web-build/templates/report-app/api_doc/报工物料列表_BLACKLAKE-1681369551143844.json b/skills/web-build/templates/report-app/api_doc/报工物料列表_BLACKLAKE-1681369551143844.json new file mode 100644 index 0000000..f968e19 --- /dev/null +++ b/skills/web-build/templates/report-app/api_doc/报工物料列表_BLACKLAKE-1681369551143844.json @@ -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 - 可报工方式列表", + "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 方法" +} + diff --git a/skills/web-build/templates/report-app/api_doc/报工记录列表_BLACKLAKE-1681109889053794.json b/skills/web-build/templates/report-app/api_doc/报工记录列表_BLACKLAKE-1681109889053794.json new file mode 100644 index 0000000..86bfe7b --- /dev/null +++ b/skills/web-build/templates/report-app/api_doc/报工记录列表_BLACKLAKE-1681109889053794.json @@ -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 方法" +} + diff --git a/skills/web-build/templates/report-app/api_doc/接口说明.txt b/skills/web-build/templates/report-app/api_doc/接口说明.txt new file mode 100755 index 0000000..7f33c92 --- /dev/null +++ b/skills/web-build/templates/report-app/api_doc/接口说明.txt @@ -0,0 +1,28 @@ +# 接口文档 + +## 📋 概述 + +本文档详细说明了本项目涉及的所有接口调用情况。 + +## 🔄 接口调用流程 + +### 1. 页面初始化流程 +``` +页面加载 → url获取 code → 换取 access_token提供后续open接口调用 +``` + +### 2. 选择工单 +``` +用户选择特定工单 → 选择后回显工单上的物料信息 +``` + +### 3. 报工提交流程 +``` +用户填写报工数据 → 点击提交 → 构建批量报工请求参数 → 调用批量报工接口 → 清空表单 +``` + +### 4. 查看报工记录(可选) +``` +根据用户报工时使用的生产 taskId 或者 workOrderId → 构建报工记录的请求参数 → 调用报工记录的接口 → 渲染数据 +``` + diff --git a/skills/web-build/templates/report-app/api_doc/生产任务列表_BLACKLAKE-1681109889053785.json b/skills/web-build/templates/report-app/api_doc/生产任务列表_BLACKLAKE-1681109889053785.json new file mode 100644 index 0000000..e9ffdec --- /dev/null +++ b/skills/web-build/templates/report-app/api_doc/生产任务列表_BLACKLAKE-1681109889053785.json @@ -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 方法中的任务查询部分" +} diff --git a/skills/web-build/templates/report-app/config.json b/skills/web-build/templates/report-app/config.json new file mode 100644 index 0000000..4b98848 --- /dev/null +++ b/skills/web-build/templates/report-app/config.json @@ -0,0 +1,3 @@ +{ + "welcome_message": "右侧是当前生产任务执行(报工页面)的样式。

您可以:
  • 通过对话描述需要修改的部分
  • 直接上传手绘稿告诉我您期望的样式
我将通过AI魔法帮您心想事成。" +} diff --git a/skills/web-build/templates/report-app/css/custom.css b/skills/web-build/templates/report-app/css/custom.css new file mode 100644 index 0000000..41dd6e9 --- /dev/null +++ b/skills/web-build/templates/report-app/css/custom.css @@ -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; +} diff --git a/skills/web-build/templates/report-app/index.html b/skills/web-build/templates/report-app/index.html new file mode 100644 index 0000000..b415b45 --- /dev/null +++ b/skills/web-build/templates/report-app/index.html @@ -0,0 +1,257 @@ + + + + + + 报工 - 工单管理系统 + + + + + + + +
+ +
+ + + + +

标准报工页面

+ + +
+
+ + +
+ +
+
+
+ +
+ + + + +
+ + +
+ 工单 + 请选择 +
+
+ +
+ + + + + + + +
+
+
+ + + + + +
+ +
+ + + +
+

先选择工单

+
+ + + + + + +
+ + + +
+ + + + + + + + \ No newline at end of file diff --git a/skills/web-build/templates/report-app/js/common.js b/skills/web-build/templates/report-app/js/common.js new file mode 100644 index 0000000..1ad24d8 --- /dev/null +++ b/skills/web-build/templates/report-app/js/common.js @@ -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 = ` + + ${workOrder.id} + `; + + // 点击事件 + 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 = ` +
+
+
+
加载中...
+
+ `; + + // 添加样式 + 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'); + } + } +}; \ No newline at end of file diff --git a/skills/web-build/templates/report-app/js/main.js b/skills/web-build/templates/report-app/js/main.js new file mode 100644 index 0000000..707eee1 --- /dev/null +++ b/skills/web-build/templates/report-app/js/main.js @@ -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('开发模式:服务已暴露到全局'); + } +}); diff --git a/skills/web-build/templates/report-app/services/business.js b/skills/web-build/templates/report-app/services/business.js new file mode 100644 index 0000000..d96f7b4 --- /dev/null +++ b/skills/web-build/templates/report-app/services/business.js @@ -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} params.workOrderStatusList - 工单业务状态列表 + * @param {number} params.pauseFlag - 是否暂停 0未暂停 1已暂停 + * @returns {Promise} + */ + 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} 返回必填参数对象 + */ + 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} 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} reportData.executorIds - 执行人ID列表 + * @param {number} reportData.reportUnitId - 报工单位ID + * @param {number} reportData.auxUnitId1 - 辅助单位1 ID + * @returns {Promise} + */ + 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} params.taskId - 生产任务ID或ID列表(可选) + * @param {number|Array} 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} params.processIdList - 工序ID列表(可选) + * @param {Array} params.executorIdList - 可执行人ID列表(可选) + * @param {Array} params.qcStatusList - 质量状态列表(可选) + * @param {Array} params.sorter - 排序条件列表(可选),列表顺序表示排序顺序 + * @param {string} params.sorter[].field - 排序字段,如 "reportTime", "taskCode" 等 + * @param {string} params.sorter[].order - 排序规律,默认 "asc","asc" 升序 "desc" 降序 + * @returns {Promise} 返回报工记录列表(分页响应) + * @returns {boolean} returns.success - 是否成功 + * @returns {Array} 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} 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} returns.records[].equipments - 设备列表(可选),数组元素包含 id、code、name + * @returns {Array} returns.records[].executors - 计划执行人(可选),数组元素包含 id、code、name、username、avatarUrl + * @returns {Array} returns.records[].produceDepartments - 生产部门列表(可选),数组元素包含 id、code、name + * @returns {Array} returns.records[].customFields - 自定义字段(可选),数组元素包含 fieldCode、fieldName、fieldValue 等 + * @returns {Array} returns.records[].qcDefectReasons - 不良原因(可选),数组元素包含 id、name + * @returns {Array} returns.records[].salesOrderCode - 订单编号(可选) + * @returns {Array} 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(); + diff --git a/skills/web-build/templates/report-app/services/core.js b/skills/web-build/templates/report-app/services/core.js new file mode 100644 index 0000000..8d922f7 --- /dev/null +++ b/skills/web-build/templates/report-app/services/core.js @@ -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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} 返回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(); + diff --git a/skills/web-build/templates/report-h5/index.html b/skills/web-build/templates/report-h5/index.html new file mode 100644 index 0000000..2e813ff --- /dev/null +++ b/skills/web-build/templates/report-h5/index.html @@ -0,0 +1,419 @@ + + + + + + + 智筑报工页面 + + + + + +
+

智筑报工页面

+
+ +
+ + +
+ +
+
+ 生产工单 + + 请选择生产工单 +
+ +
+ + +
+
+ 加工物料名称: + -- +
+
+ 加工物料编号: + -- +
+ + +
+
+ + + + + +
+

报工记录

+ +
+ +
+
+
报工数量
+
报工时间
+
+
+
10 个
+
2025.11.25 16:27
+
+
+ + +
+
+
报工数量
+
报工时间
+
+
+
10 个
+
2025.11.25 15:20
+
+
+ + +
+
+
报工数量
+
报工时间
+
+
+
10 个
+
2025.11.25 14:11
+
+
+
+ + +
+ - 暂无更多记录 - +
+
+ +
+ + + + + +
+ +
+ +
+ 报工成功 +
+ + + + \ No newline at end of file