Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:55:46 +08:00
commit b710247ba7
27 changed files with 6516 additions and 0 deletions

202
skills/web-build/SKILL.md Normal file
View File

@@ -0,0 +1,202 @@
---
name: web-build
description: 通用 Web 项目构建工具自动检测项目类型Vue、React、Ant Design Pro 等),执行依赖安装和生产构建,生成可部署的静态文件并返回预览链接。
---
# 通用 Web 项目构建工具
## 功能说明
自动执行 Web 项目的完整构建流程:
1. 检查项目配置文件package.json
2. 自动检测项目类型Vue、React、Ant Design Pro 等)
3. 安装项目依赖npm install如果 node_modules 不存在)
4. 执行生产构建npm run build
5. 生成可访问的预览链接
## 模板参考
本 skill 目录下的 `templates/` 包含纯 HTML/CSS/JS 参考模板,供 frontend-html agent 使用:
| 模板 | 类型 | 特点 |
|-----|------|------|
| `templates/mes-dashboard/` | 单文件 | 科技感大屏看板、深色主题、动画效果 |
| `templates/report-h5/` | 单文件 | 移动端响应式、简洁表单 |
| `templates/report-app/` | 多文件 | Tailwind CSS、模块化 JS、完整 API 调用 |
**使用方式**:可用 Read 工具读取上述模板文件,参考其风格和结构。
## 执行流程
### 步骤1检查项目配置
```bash
ls -la package.json
```
如果不存在,提示用户:
```
❌ 未找到 package.json 文件
请先创建 Web 项目或使用相应的 Agent 创建项目
```
### 步骤2检测项目类型
通过分析 `package.json` 中的依赖自动检测项目类型:
| 项目类型 | 检测依赖 | 构建工具 | 输出目录 |
|---------|---------|---------|---------|
| Vue 3 + Vite | `vue` + `vite` | Vite | `dist/` |
| React + Vite | `react` + `vite` | Vite | `dist/` |
| Ant Design Pro | `@umijs/max``umi` | UmiJS | `dist/` |
| Create React App | `react-scripts` | Webpack | `build/` |
| Next.js | `next` | Next.js | `.next/``out/` |
### 步骤3检查并安装依赖
```bash
# 检查 node_modules 是否存在
if [ ! -d "node_modules" ]; then
echo "📦 安装项目依赖..."
npm install
else
echo "✓ 依赖已存在,跳过安装"
fi
```
**说明**
- 首次安装可能需要 1-3 分钟
- 会下载并安装所有依赖到 `node_modules/` 目录
- 失败时检查网络连接和 npm 配置
### 步骤4执行构建
```bash
npm run build
```
**说明**
- 构建时间通常为 10-60 秒(取决于项目大小)
- 构建工具会自动生成输出目录
- 失败时检查代码错误和依赖问题
### 步骤5验证构建结果
```bash
# 检查输出目录是否存在
if [ -d "dist" ]; then
echo "✓ dist/ 目录已生成"
elif [ -d "build" ]; then
echo "✓ build/ 目录已生成"
else
echo "❌ 构建产物目录未找到"
fi
```
### 步骤6生成预览链接
构建成功后,生成可访问的预览链接。
**链接格式**
```
{WEB_BASE_URL}/ai-coder/code/{project_type}/o_{org_id}/w_{coder_id}/{output_dir}/
```
**示例**
```
http://localhost:8080/ai-coder/code/web_ant/o_20251114/w_102/dist/
http://localhost:8080/ai-coder/code/web/o_20251114/w_102/dist/
```
**路径提取**
- 从当前工作目录路径中提取 org_id 和 coder_id
- 工作目录格式:`/workspace/code/{project_type}/o_{org_id}/w_{coder_id}/`
- WEB_BASE_URL 默认为:`http://localhost:8080`
- 输出目录根据项目类型确定dist/、build/ 等)
## 输出格式
构建成功后,以 Markdown 格式输出:
```markdown
✅ Web 应用构建完成!
📦 构建信息:
- 项目类型Ant Design Pro (UmiJS)
- 构建工具UmiJS 4.x
- 输出目录dist/
- 入口文件index.html
🌐 预览地址:
[点击访问应用](http://localhost:8080/ai-coder/code/web_ant/o_20251114/w_102/dist/)
💡 使用提示:
- 点击上方链接即可在浏览器中预览应用
- 应用已部署到工作区,可随时访问
- 如需修改,请重新编辑代码并再次构建
```
## 关键错误处理
### 错误1package.json 不存在
```markdown
❌ 构建失败:未找到 package.json
请先创建 Web 项目:
1. 使用 frontend-vue Agent 创建 Vue 项目
2. 使用 frontend-ant Agent 创建 Ant Design Pro 项目
3. 或手动创建 package.json 配置文件
```
### 错误2npm install 失败
```markdown
❌ 依赖安装失败
可能原因:
- 网络连接问题
- npm 配置错误
- package.json 配置错误
建议操作:
1. 检查网络连接
2. 检查 package.json 中的依赖版本
3. 尝试使用国内镜像源npm config set registry https://registry.npmmirror.com
4. 尝试清理缓存npm cache clean --force
```
### 错误3npm 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}`

View File

@@ -0,0 +1,651 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能工厂生产实时监控中心</title>
<style>
/* --- 全局重置与基础样式 --- */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background-color: #050a15;
background-image:
radial-gradient(circle at 50% 50%, #0d1a35 0%, #050a15 100%),
linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
background-size: 100% 100%, 30px 30px, 30px 30px;
color: #fff;
height: 100vh;
overflow: hidden; /* 防止滚动,全屏展示 */
display: flex;
flex-direction: column;
}
/* --- 颜色变量 --- */
:root {
--primary-color: #00f0ff;
--secondary-color: #0088ff;
--warn-color: #ffcc00;
--danger-color: #ff4d4d;
--bg-panel: rgba(10, 25, 50, 0.6);
--border-panel: rgba(0, 240, 255, 0.2);
}
/* --- 顶部 Header --- */
header {
height: 8vh;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
background: linear-gradient(to bottom, rgba(0,20,40,0.9), transparent);
border-bottom: 2px solid var(--border-panel);
position: relative;
}
header::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--primary-color), transparent);
box-shadow: 0 0 10px var(--primary-color);
}
.header-title {
font-size: 1.8rem;
font-weight: bold;
letter-spacing: 2px;
text-shadow: 0 0 10px var(--secondary-color);
background: linear-gradient(to bottom, #fff, #aaddff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-time {
font-family: 'Courier New', monospace;
font-size: 1.2rem;
color: var(--primary-color);
}
/* --- 主体布局 --- */
main {
flex: 1;
display: grid;
grid-template-columns: 28% 44% 28%; /* 左 中 右 布局 */
gap: 15px;
padding: 15px;
}
/* --- 通用面板样式 --- */
.panel {
background: var(--bg-panel);
border: 1px solid var(--border-panel);
border-radius: 4px;
padding: 15px;
display: flex;
flex-direction: column;
position: relative;
backdrop-filter: blur(5px);
}
/* 面板四角装饰 */
.panel::before, .panel::after,
.panel-corner::before, .panel-corner::after {
content: "";
position: absolute;
width: 10px;
height: 10px;
border-color: var(--primary-color);
border-style: solid;
transition: all 0.3s ease;
}
.panel::before { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
.panel::after { top: -1px; right: -1px; border-width: 2px 2px 0 0; }
.panel-corner { position: absolute; bottom: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
.panel-corner::before { bottom: -1px; left: -1px; border-width: 0 0 2px 2px; }
.panel-corner::after { bottom: -1px; right: -1px; border-width: 0 2px 2px 0; }
.panel:hover {
box-shadow: 0 0 15px rgba(0, 240, 255, 0.1);
border-color: rgba(0, 240, 255, 0.5);
}
.panel-title {
font-size: 1.1rem;
color: #fff;
border-left: 4px solid var(--primary-color);
padding-left: 10px;
margin-bottom: 15px;
font-weight: 600;
}
/* --- 左侧内容 --- */
.chart-box {
flex: 1;
position: relative;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
/* 自定义进度条列表 */
.progress-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
}
.progress-item {
width: 100%;
}
.p-label {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
margin-bottom: 5px;
color: #ccc;
}
.p-bar-bg {
width: 100%;
height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
}
.p-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--secondary-color), var(--primary-color));
width: 0%; /* 动画用 JS 控制 */
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.p-bar-fill::after {
content: '';
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 2px;
background: #fff;
box-shadow: 0 0 5px #fff;
}
/* 环形图 (CSS实现) */
.donut-chart {
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(var(--primary-color) 0% calc(var(--val) * 1%), rgba(255,255,255,0.1) calc(var(--val) * 1%) 100%);
position: relative;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 0 20px rgba(0, 240, 255, 0.2);
}
.donut-chart::before {
content: "";
width: 80%;
height: 80%;
background: #0d1a35;
border-radius: 50%;
position: absolute;
}
.donut-text {
position: relative;
z-index: 2;
text-align: center;
}
.donut-value { font-size: 1.4rem; font-weight: bold; color: #fff; }
.donut-label { font-size: 0.8rem; color: #aaa; }
.device-stats {
display: flex;
justify-content: space-around;
width: 100%;
margin-top: 10px;
}
.stat-item { text-align: center; }
.stat-val { font-size: 1.2rem; font-weight: bold; }
.stat-val.online { color: #00ff00; }
.stat-val.offline { color: #888; }
.stat-val.warning { color: var(--warn-color); }
.stat-desc { font-size: 0.8rem; color: #aaa; margin-top: 2px; }
/* --- 中间内容 --- */
.center-column {
display: flex;
flex-direction: column;
gap: 15px;
}
.kpi-board {
display: flex;
justify-content: space-between;
gap: 10px;
height: 15vh;
}
.kpi-card {
flex: 1;
background: rgba(0, 136, 255, 0.1);
border: 1px solid rgba(0, 136, 255, 0.3);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.kpi-card::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(0,240,255,0.1) 0%, transparent 70%);
animation: rotate 10s linear infinite;
}
@keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.kpi-num {
font-size: 2.5rem;
font-weight: bold;
color: var(--primary-color);
z-index: 1;
font-family: 'Courier New', monospace;
text-shadow: 0 0 10px var(--primary-color);
}
.kpi-title {
font-size: 1rem;
color: #ccc;
z-index: 1;
margin-top: 5px;
}
.map-container {
flex: 1;
border: 1px solid var(--border-panel);
background: rgba(0,0,0,0.2);
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
/* 模拟 3D 地图/雷达效果 */
.radar-grid {
width: 100%;
height: 100%;
position: absolute;
background:
linear-gradient(rgba(0, 240, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 240, 255, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
perspective: 500px;
transform: perspective(1000px) rotateX(60deg) scale(1.5);
animation: gridMove 20s linear infinite;
}
@keyframes gridMove { from { background-position: 0 0; } to { background-position: 0 50px; } }
.factory-model {
position: relative;
width: 300px;
height: 300px;
z-index: 10;
}
/* 简单的 CSS 旋转动画模拟中心产线 */
.circle-ring {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 2px solid var(--primary-color);
box-shadow: 0 0 15px var(--primary-color);
}
.r1 { width: 100px; height: 100px; border-style: dashed; animation: spin 10s linear infinite; }
.r2 { width: 180px; height: 180px; border-color: rgba(0,240,255,0.3); border-width: 1px; animation: spinReverse 15s linear infinite; }
.r3 { width: 60px; height: 60px; background: rgba(0,240,255,0.1); display: flex; align-items: center; justify-content: center;}
.factory-icon { font-size: 2rem; }
@keyframes spin { 100% { transform: translate(-50%, -50%) rotate(360deg); } }
@keyframes spinReverse { 100% { transform: translate(-50%, -50%) rotate(-360deg); } }
/* --- 右侧内容 --- */
.alert-list {
width: 100%;
overflow: hidden;
flex: 1;
}
.alert-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid rgba(255,255,255,0.05);
font-size: 0.9rem;
animation: fadeIn 0.5s ease;
}
.alert-time { color: #888; margin-right: 10px; font-size: 0.8rem;}
.alert-content { flex: 1; }
.alert-level {
padding: 2px 6px;
border-radius: 2px;
font-size: 0.75rem;
margin-right: 8px;
}
.level-high { background: rgba(255, 77, 77, 0.2); color: var(--danger-color); border: 1px solid var(--danger-color); }
.level-warn { background: rgba(255, 204, 0, 0.2); color: var(--warn-color); border: 1px solid var(--warn-color); }
/* SVG 折线图容器 */
.svg-chart-container {
width: 100%;
height: 100%;
position: relative;
}
svg {
width: 100%;
height: 100%;
overflow: visible;
}
.chart-line {
fill: none;
stroke: var(--primary-color);
stroke-width: 2;
vector-effect: non-scaling-stroke;
}
.chart-area {
fill: rgba(0, 240, 255, 0.1);
stroke: none;
}
.chart-grid line {
stroke: rgba(255,255,255,0.1);
stroke-width: 1;
}
.chart-axis text {
fill: #888;
font-size: 10px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
main { grid-template-columns: 30% 70%; grid-template-rows: auto auto; }
.center-column { grid-row: 1 / 2; grid-column: 2 / 3; }
.right-column { grid-row: 2 / 3; grid-column: 1 / 3; display: flex; gap: 15px; height: 30vh;}
.left-column { grid-row: 1 / 2; grid-column: 1 / 2; }
}
@media (max-width: 768px) {
main { display: flex; flex-direction: column; overflow-y: auto; }
body { overflow: auto; height: auto; }
.kpi-board { flex-wrap: wrap; height: auto; }
.kpi-card { width: 48%; margin-bottom: 10px; height: 100px; }
.map-container { height: 300px; }
}
</style>
</head>
<body>
<header>
<div class="header-logo">🏭 SMART FACTORY</div>
<div class="header-title">智能制造MES生产看板</div>
<div class="header-time" id="clock">00:00:00</div>
</header>
<main>
<!-- 左列:设备与效率 -->
<section class="panel left-column">
<div class="panel-corner"></div>
<div class="panel-title">设备综合效率 (OEE)</div>
<div class="chart-box" style="flex-direction: column; justify-content: space-around;">
<div class="device-stats">
<div class="stat-item">
<div class="donut-chart" style="--val:85">
<div class="donut-text">
<div class="donut-value">85%</div>
<div class="donut-label">OEE</div>
</div>
</div>
</div>
</div>
<div class="device-stats" style="margin-top: 20px;">
<div class="stat-item">
<div class="stat-val online">42</div>
<div class="stat-desc">运行中</div>
</div>
<div class="stat-item">
<div class="stat-val warning">3</div>
<div class="stat-desc">待机</div>
</div>
<div class="stat-item">
<div class="stat-val offline">1</div>
<div class="stat-desc">故障</div>
</div>
</div>
</div>
</section>
<section class="panel left-column">
<div class="panel-corner"></div>
<div class="panel-title">车间产出进度</div>
<div class="chart-box">
<div class="progress-list">
<div class="progress-item">
<div class="p-label"><span>A线 - 电子组装</span><span>92%</span></div>
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 92%;"></div></div>
</div>
<div class="progress-item">
<div class="p-label"><span>B线 - 注塑成型</span><span>78%</span></div>
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 78%; background: linear-gradient(90deg, #ffcc00, #ff9900);"></div></div>
</div>
<div class="progress-item">
<div class="p-label"><span>C线 - 包装质检</span><span>45%</span></div>
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 45%;"></div></div>
</div>
<div class="progress-item">
<div class="p-label"><span>D线 - 激光焊接</span><span>88%</span></div>
<div class="p-bar-bg"><div class="p-bar-fill" style="width: 88%;"></div></div>
</div>
</div>
</div>
</section>
<!-- 中列:核心监控 -->
<section class="center-column">
<div class="kpi-board">
<div class="kpi-card">
<div class="kpi-num" id="kpi-total">24,592</div>
<div class="kpi-title">今日总产量 (PCS)</div>
</div>
<div class="kpi-card">
<div class="kpi-num" style="color: #00ff00;">99.2%</div>
<div class="kpi-title">良品率</div>
</div>
<div class="kpi-card">
<div class="kpi-num" style="color: #ffcc00;" id="kpi-cycle">4.2s</div>
<div class="kpi-title">平均节拍</div>
</div>
</div>
<div class="panel map-container" style="flex:1;">
<div class="panel-corner"></div>
<div class="radar-grid"></div>
<div class="factory-model">
<div class="circle-ring r1"></div>
<div class="circle-ring r2"></div>
<div class="circle-ring r3">
<span class="factory-icon">⚙️</span>
</div>
</div>
<div style="position:absolute; bottom:20px; left:20px; font-size:0.9rem; color:#aaa;">
<div>主轴转速: <span style="color:#fff;" id="motor-speed">1200</span> RPM</div>
<div>核心温度: <span style="color:#fff;" id="motor-temp">45.2</span> °C</div>
</div>
<div style="position:absolute; top:20px; right:20px; text-align:right;">
<div style="color:var(--primary-color); border:1px solid var(--primary-color); padding: 5px 10px; font-size:0.8rem;">
SYSTEM: ONLINE
</div>
</div>
</div>
</section>
<!-- 右列:趋势与告警 -->
<section class="panel right-column">
<div class="panel-corner"></div>
<div class="panel-title">实时产量趋势 (24H)</div>
<div class="chart-box" id="line-chart-box">
<!-- SVG Chart will be injected here by JS -->
</div>
</section>
<section class="panel right-column">
<div class="panel-corner"></div>
<div class="panel-title">实时告警信息</div>
<div class="chart-box" style="align-items: flex-start;">
<div class="alert-list" id="alert-container">
<!-- Alerts injected by JS -->
<div class="alert-item">
<span class="alert-time">10:42</span>
<span class="alert-level level-warn">警告</span>
<span class="alert-content">B线 3号机台 温度偏高</span>
</div>
<div class="alert-item">
<span class="alert-time">09:15</span>
<span class="alert-level level-high">故障</span>
<span class="alert-content">C线 缺料停机报警</span>
</div>
</div>
</div>
</section>
</main>
<script>
// --- 1. 时钟功能 ---
function updateClock() {
const now = new Date();
const timeString = now.toLocaleTimeString('zh-CN', { hour12: false });
const dateString = now.toLocaleDateString('zh-CN');
document.getElementById('clock').innerHTML = `${dateString} ${timeString}`;
}
setInterval(updateClock, 1000);
updateClock();
// --- 2. 模拟数据动态跳动 ---
function fluctuateData() {
// 更新产量
const totalEl = document.getElementById('kpi-total');
let currentTotal = parseInt(totalEl.innerText.replace(/,/g, ''));
currentTotal += Math.floor(Math.random() * 5); // 随机增加
totalEl.innerText = currentTotal.toLocaleString();
// 更新节拍
const cycleEl = document.getElementById('kpi-cycle');
cycleEl.innerText = (4.0 + Math.random() * 0.5).toFixed(1) + 's';
// 更新机器参数
document.getElementById('motor-speed').innerText = 1200 + Math.floor(Math.random() * 50 - 25);
document.getElementById('motor-temp').innerText = (45 + Math.random() * 2).toFixed(1);
}
setInterval(fluctuateData, 2000);
// --- 3. 绘制 SVG 折线图 (无需 Canvas 库) ---
function drawLineChart() {
const container = document.getElementById('line-chart-box');
const width = container.clientWidth;
const height = container.clientHeight;
// 生成模拟数据 (24小时)
const dataPoints = [];
for (let i = 0; i < 24; i++) {
dataPoints.push(50 + Math.random() * 40); // 50-90之间的数值
}
const maxVal = 100;
const stepX = width / (dataPoints.length - 1);
// 构建路径点
let pointsStr = "";
let areaPointsStr = `0,${height} `; // 闭合区域起点
dataPoints.forEach((val, index) => {
const x = index * stepX;
const y = height - (val / maxVal * height); // 坐标翻转
pointsStr += `${x},${y} `;
areaPointsStr += `${x},${y} `;
});
areaPointsStr += `${width},${height}`; // 闭合区域终点
const svgContent = `
<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
<!-- 网格线 -->
<g class="chart-grid">
<line x1="0" y1="${height * 0.25}" x2="${width}" y2="${height * 0.25}" />
<line x1="0" y1="${height * 0.5}" x2="${width}" y2="${height * 0.5}" />
<line x1="0" y1="${height * 0.75}" x2="${width}" y2="${height * 0.75}" />
</g>
<!-- 面积 -->
<polygon points="${areaPointsStr}" class="chart-area" />
<!-- 折线 -->
<polyline points="${pointsStr}" class="chart-line" />
</svg>
`;
container.innerHTML = svgContent;
}
// 窗口大小改变时重绘图表
window.addEventListener('resize', drawLineChart);
setTimeout(drawLineChart, 100); // 初始化绘制
// --- 4. 模拟告警滚动 ---
const alerts = [
{ level: 'warn', text: 'A线 气压波动异常' },
{ level: 'high', text: 'AGV 小车受阻 #04' },
{ level: 'warn', text: '物料库存预警: 螺丝M4' },
{ level: 'normal', text: '班次产量已达标' }
];
function addRandomAlert() {
const container = document.getElementById('alert-container');
const randomAlert = alerts[Math.floor(Math.random() * alerts.length)];
const now = new Date();
const timeStr = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
const div = document.createElement('div');
div.className = 'alert-item';
let levelClass = '';
let levelText = '信息';
if (randomAlert.level === 'warn') { levelClass = 'level-warn'; levelText = '警告'; }
if (randomAlert.level === 'high') { levelClass = 'level-high'; levelText = '故障'; }
div.innerHTML = `
<span class="alert-time">${timeStr}</span>
<span class="alert-level ${levelClass}">${levelText}</span>
<span class="alert-content">${randomAlert.text}</span>
`;
container.insertBefore(div, container.firstChild);
// 保持列表长度
if (container.children.length > 6) {
container.removeChild(container.lastChild);
}
}
setInterval(addRandomAlert, 5000);
</script>
</body>
</html>

View File

@@ -0,0 +1,184 @@
# 报工页面模板
移动端报工 H5 页面,支持工单选择、物料信息展示和报工提交。
## 📁 文件结构
```
report-app/
├── index.html # 主页面
├── css/custom.css # 自定义样式
├── js/
│ ├── main.js # 主应用逻辑MainApp 类)
│ └── common.js # 工具函数 + UI组件
├── services/
│ ├── core.js # API服务 + 认证服务
│ └── business.js # 工单服务 + 报工服务
├── api_doc/ # API 接口文档(精简版)
│ ├── 工单列表_BLACKLAKE-1686655055663532.json
│ ├── 生产任务列表_BLACKLAKE-1681109889053785.json
│ ├── 报工物料列表_BLACKLAKE-1681369551143844.json
│ ├── 批量报工_BLACKLAKE-1681109889053798.json
│ └── 报工记录列表_BLACKLAKE-1681109889053794.json
└── README.md
```
---
## 🔗 核心接口调用流程
```
1. 工单列表 → 获取 workOrderId、物料信息
2. 生产任务列表 → 获取 taskId、executorIds
3. 报工物料列表 → 获取 progressReportKey、reportUnitId
4. 批量报工 → 提交报工
5. 报工记录列表 → 查询最新报工记录
```
### 接口与代码位置对照
| 接口 | 文档 | 代码位置 | 方法 |
|------|------|---------|------|
| 工单列表 | `api_doc/工单列表_*.json` | `services/business.js:28` | `getWorkOrderList` |
| 生产任务列表 | `api_doc/生产任务列表_*.json` | `services/business.js:303` | `getReportRequiredParams` |
| 报工物料列表 | `api_doc/报工物料列表_*.json` | `services/business.js:324` | `getReportRequiredParams` |
| 批量报工 | `api_doc/批量报工_*.json` | `services/business.js:499` | `submitReport` |
| 报工记录列表 | `api_doc/报工记录列表_*.json` | `services/business.js:643` | `getReportRecordsByTask` |
---
## 🎯 关键代码位置
### index.html 关键区域
| 区域 | 元素ID | 行号 | 用途 |
|------|--------|------|------|
| 工单选择 | `#work-order-selector` | ~34 | 工单选择按钮 |
| 工单下拉 | `#work-order-dropdown` | ~69 | 工单列表弹窗 |
| 空状态 | `#empty-state` | ~111 | 未选工单时显示 |
| 物料信息 | `#material-info` | ~122 | 显示物料名称、编号 |
| 报工表单 | `#report-form` | ~167 | 数量输入等表单 |
| 提交按钮 | `#submit-btn` | ~245 | 提交报工 |
### js/main.js 关键方法
| 方法 | 行号 | 用途 |
|------|------|------|
| `onWorkOrderSelected` | ~169 | 工单选择后的处理 |
| `extractMaterialDataFromWorkOrder` | ~204 | 从工单提取物料信息 |
| `switchToMaterialView` | ~252 | 显示物料信息区域 |
| `handleSubmit` | ~323 | 提交报工处理 |
| `handleSubmitSuccess` | ~380 | 报工成功后处理 |
### services/business.js 关键方法
| 方法 | 行号 | 用途 |
|------|------|------|
| `getWorkOrderList` | ~28 | 获取工单列表 |
| `processWorkOrderListResponse` | ~85 | 处理工单响应 |
| `getReportRequiredParams` | ~287 | 获取报工必填参数 |
| `buildReportRequestParams` | ~406 | 构建报工请求 |
| `submitReport` | ~499 | 提交报工 |
| `getReportRecordsByTask` | ~643 | 查询报工记录 |
---
## 🛠️ AI 修改指南
### 常见修改场景
#### 场景 1修改表单字段
- **位置**: `index.html``#report-form` 区域 (~167行)
- **模式**: 复制相邻的 `<div class="flex items-center justify-between">` 结构
#### 场景 2修改物料信息显示
- **位置**:
- HTML: `index.html``#material-info` 区域 (~122行)
- JS: `js/main.js``extractMaterialDataFromWorkOrder` 方法 (~204行)
#### 场景 3修改报工提交逻辑
- **位置**: `js/main.js``handleSubmit` 方法 (~323行)
- **数据来源**: `this.state.selectedWorkOrder``this.state.materialData`
#### 场景 4修改接口参数
- **位置**: `services/business.js` 的对应方法
- **必读**: `api_doc/` 下的接口文档
#### 场景 5添加报工记录显示
- **步骤**:
1.`index.html` 添加显示区域
2.`js/main.js``handleSubmitSuccess` 中调用 `reportService.getReportRecordsByTask`
3. 渲染报工记录列表
### 修改原则
1. **接口字段必须与 api_doc 一致** - 禁止凭空创造字段
2. **优先使用现有方法** - 不要重写已有逻辑
3. **保持代码风格一致** - 参考相邻代码
4. **批量修改** - 同一文件的修改一次完成
---
## 📝 关键数据结构
### 工单数据 (workOrder)
```javascript
{
workOrderId: 12345, // 工单ID用于报工
workOrderCode: "WO-001", // 工单编号
materialInfo: { // 物料信息
baseInfo: {
id: 100, // 物料ID
name: "产品A", // 物料名称(加工物料名称)
code: "M001" // 物料编码(物料编号)
}
},
qualifiedHoldAmount: { // 合格数量
amount: 100,
amountDisplay: "100"
}
}
```
### 报工必填参数 (requiredParams)
```javascript
{
taskId: 67890, // 生产任务ID必填
progressReportMaterial: { // 报工物料(必填,不可修改结构)
lineId: 111,
materialId: 100,
reportProcessId: 222
},
outputMaterialUnit: {
id: 333 // 报工单位IDreportUnitId
},
executorIds: [1, 2, 3] // 执行人ID列表
}
```
---
## 🎨 样式配置
主题色配置在 `css/custom.css`
```css
:root {
--primary-color: #02b980; /* 主色 */
--primary-hover: #029968; /* 悬停色 */
--primary-light: #e6f7f1; /* 浅色背景 */
}
```
---
## ⚡ 技术栈
- **HTML5** + **Tailwind CSS** + **原生 JavaScript (ES6+)**
- **无需构建**,直接作为静态资源访问

View File

@@ -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 方法"
}

View File

@@ -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 方法"
}

View File

@@ -0,0 +1,106 @@
{
"接口名称": "批量报工",
"接口路径": "/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report",
"请求方法": "POST",
"代码位置": "services/business.js line 516",
"请求参数": {
"taskId": {
"类型": "number",
"必填": true,
"说明": "生产任务ID从生产任务列表获取"
},
"progressReportMaterial": {
"类型": "object",
"必填": true,
"说明": "报工物料对象(从报工物料列表获取)",
"结构": {
"lineId": "number - 物料行ID",
"materialId": "number - 物料ID",
"reportProcessId": "number - 报工工序ID"
}
},
"qcStatus": {
"类型": "number",
"必填": false,
"默认": 1,
"说明": "质量状态1合格 2让步合格 3代检 4不合格"
},
"reportType": {
"类型": "number",
"必填": false,
"默认": 2,
"说明": "报工方式1扫码报工-合格 2记录报工-合格 3扫码报工-不合格 4记录报工-不合格 5打码报工-合格 6打码报工-不合格)"
},
"progressReportItems": {
"类型": "array",
"必填": true,
"说明": "报工详情数组",
"结构": [{
"executorIds": "number[] - 执行人ID列表",
"progressReportMaterialItems": "array - 物料报工项",
"progressReportMaterialItems[].reportAmount": "number - 报工数量(必填)",
"progressReportMaterialItems[].reportUnitId": "number - 报工单位ID必填从 outputMaterialUnit.id 获取)",
"progressReportMaterialItems[].remark": "string - 备注(可选)"
}]
},
"storageLocationId": {
"类型": "number",
"必填": false,
"说明": "仓位ID当前预置为 1716848012872791"
},
"reportStartTime": {
"类型": "number",
"必填": false,
"说明": "报工开始时间(毫秒时间戳)"
},
"reportEndTime": {
"类型": "number",
"必填": false,
"说明": "报工结束时间(毫秒时间戳)"
},
"actualExecutorIds": {
"类型": "number[]",
"必填": false,
"说明": "实际执行人ID列表"
},
"actualEquipmentIds": {
"类型": "number[]",
"必填": false,
"说明": "设备ID列表"
},
"workHour": {
"类型": "number",
"必填": false,
"说明": "工时"
},
"workHourUnit": {
"类型": "number",
"必填": false,
"说明": "工时单位"
},
"qcDefectReasonIds": {
"类型": "number[]",
"必填": false,
"说明": "不良原因ID列表质量状态为不合格时"
}
},
"响应结构": {
"code": "200 表示成功",
"message": "返回信息",
"data": {
"messageTraceId": "string - 消息追踪ID",
"progressReportRecordIds": "number[] - 报工记录ID列表"
}
},
"关键说明": [
"必须先调用 getReportRequiredParams 获取 taskId、progressReportMaterial、reportUnitId 等必填参数",
"progressReportMaterial 对象的结构不可修改,必须从报工物料列表接口获取",
"reportUnitId 必须从报工物料列表的 outputMaterialUnit.id 获取"
],
"使用示例": "参见 services/business.js:406-482 的 buildReportRequestParams 方法和 submitReport 方法"
}

View File

@@ -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 - 物料信息,包含 baseInfoid、name、code 等)",
"materialInfo.baseInfo.id": "number - 物料ID主产出物料ID",
"materialInfo.baseInfo.name": "string - 物料名称(显示:加工物料名称)",
"materialInfo.baseInfo.code": "string - 物料编码(显示:物料编号)",
"materialInfo.baseInfo.specification": "string - 规格",
"outputMaterialUnit": "object - 报工单位信息(必填,用于报工)",
"outputMaterialUnit.id": "number - 单位ID报工时必需reportUnitId",
"outputMaterialUnit.code": "string - 单位编码",
"outputMaterialUnit.name": "string - 单位名称个、kg",
"mainFlag": "boolean - 是否为主产出物料(优先选择 mainFlag=true 的物料)",
"reportType": "array<number> - 可报工方式列表",
"warehousingFlag": "boolean - 是否入库",
"autoWarehousingFlag": "boolean - 是否自动入库",
"progressReportKey": "object - 报工关系信息(必填,用于构建报工参数)",
"progressReportKey.lineId": "number - 物料行ID报工时必需",
"progressReportKey.materialId": "number - 物料ID报工时必需",
"progressReportKey.reportProcessId": "number - 报工工序ID报工时必需",
"outputMaterialAmount": "object - 产出物料数量信息",
"outputMaterialAmount.amount": "number - 数量",
"outputMaterialAmount.amountDisplay": "string - 数量显示"
},
"使用场景": "用于获取报工必填参数,特别是 progressReportKey 和 outputMaterialUnit",
"使用示例": "参见 services/business.js:318-389 的 getReportRequiredParams 方法"
}

View File

@@ -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 方法"
}

View File

@@ -0,0 +1,28 @@
# 接口文档
## 📋 概述
本文档详细说明了本项目涉及的所有接口调用情况。
## 🔄 接口调用流程
### 1. 页面初始化流程
```
页面加载 → url获取 code → 换取 access_token提供后续open接口调用
```
### 2. 选择工单
```
用户选择特定工单 → 选择后回显工单上的物料信息
```
### 3. 报工提交流程
```
用户填写报工数据 → 点击提交 → 构建批量报工请求参数 → 调用批量报工接口 → 清空表单
```
### 4. 查看报工记录(可选)
```
根据用户报工时使用的生产 taskId 或者 workOrderId → 构建报工记录的请求参数 → 调用报工记录的接口 → 渲染数据
```

View File

@@ -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 方法中的任务查询部分"
}

View File

@@ -0,0 +1,3 @@
{
"welcome_message": "右侧是当前<strong>生产任务执行(报工页面)</strong>的样式。<br><br>您可以:<ul><li>通过对话描述需要修改的部分</li><li>直接上传手绘稿告诉我您期望的样式</li></ul>我将通过AI魔法帮您心想事成。"
}

View File

@@ -0,0 +1,391 @@
/**
* 报工页面自定义样式
* 补充Tailwind CSS的样式定义
* 主题色: #02B980
*/
/* 全局样式 */
* {
box-sizing: border-box;
}
/* 主题色变量定义 */
:root {
--primary-color: #02B980;
--primary-hover: #029968;
--primary-light: #E6F7F1;
--primary-dark: #016B4A;
--primary-shadow: rgba(2, 185, 128, 0.1);
}
/* 主题色相关的通用样式 */
.btn-primary {
background-color: var(--primary-color);
color: white;
transition: background-color 0.2s ease;
}
.btn-primary:hover {
background-color: var(--primary-hover);
}
.btn-primary:disabled {
background-color: #9CA3AF;
cursor: not-allowed;
}
.border-primary {
border-color: var(--primary-color);
}
.text-primary {
color: var(--primary-color);
}
.bg-primary {
background-color: var(--primary-color);
}
.bg-primary-light {
background-color: var(--primary-light);
}
.ring-primary {
--tw-ring-color: var(--primary-color);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* 下拉菜单动画 */
#work-order-dropdown {
transition: opacity 0.3s ease-in-out;
}
#work-order-dropdown.show {
display: flex !important;
flex-direction: column;
justify-content: flex-end;
}
#work-order-dropdown .bg-white {
transform: translateY(100%);
transition: transform 0.3s ease-out;
}
#work-order-dropdown.show .bg-white {
transform: translateY(0);
}
/* 下拉箭头旋转动画 */
#dropdown-arrow.rotate {
transform: rotate(90deg);
}
/* 工单列表项样式 */
.work-order-item {
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
cursor: pointer;
transition: background-color 0.2s ease;
}
.work-order-item:hover {
background-color: #f9fafb;
}
.work-order-item:last-child {
border-bottom: none;
}
.work-order-item.selected {
background-color: var(--primary-light);
border-left: 4px solid var(--primary-color);
}
/* 输入框焦点样式增强 */
input:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(2, 185, 128, 0.1);
border-color: var(--primary-color) !important;
}
/* 按钮点击效果 */
button:active {
transform: scale(0.98);
}
/* 主题色按钮悬停效果 */
button[style*="background-color: var(--primary-color)"]:hover {
background-color: var(--primary-hover) !important;
}
button[style*="border-color: var(--primary-color)"]:hover {
background-color: var(--primary-light) !important;
}
/* 复选框和单选框主题色 */
input[type="checkbox"]:checked,
input[type="radio"]:checked {
background-color: var(--primary-color) !important;
border-color: var(--primary-color) !important;
}
input[type="checkbox"]:focus,
input[type="radio"]:focus {
box-shadow: 0 0 0 3px var(--primary-shadow) !important;
border-color: var(--primary-color) !important;
}
/* 自定义CSS类 */
.hover-primary-bg:hover {
background-color: var(--primary-hover) !important;
}
.hover-primary-light-bg:hover {
background-color: var(--primary-light) !important;
}
.focus-primary-ring:focus {
--tw-ring-color: var(--primary-color) !important;
border-color: var(--primary-color) !important;
}
.checkbox-primary:checked {
background-color: var(--primary-color) !important;
border-color: var(--primary-color) !important;
}
.checkbox-primary:focus {
box-shadow: 0 0 0 3px var(--primary-shadow) !important;
border-color: var(--primary-color) !important;
}
/* 状态标签动画 */
.status-tag {
transition: transform 0.2s ease;
}
.status-tag:hover {
transform: scale(1.05);
}
/* 响应式调整 */
@media (max-width: 640px) {
/* 移动端下拉菜单全屏显示 */
#work-order-dropdown .bg-white {
min-height: 80vh;
border-radius: 16px 16px 0 0;
}
/* 移动端表单字段间距调整 */
#report-form .space-y-4 > * + * {
margin-top: 1.5rem;
}
/* 移动端工单选择区域优化 */
#work-order-selector {
margin-bottom: 1rem;
}
/* 移动端物料信息卡片优化 */
#material-info .bg-white {
padding: 1rem;
margin-bottom: 1rem;
}
/* 移动端状态标签优化 */
.status-tag {
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
}
/* 移动端输入框优化 */
input[type="number"], input[type="text"] {
font-size: 16px; /* 防止iOS缩放 */
padding: 0.75rem;
}
/* 移动端按钮优化 */
#submit-btn {
padding: 1rem;
font-size: 1.125rem;
font-weight: 600;
}
}
/* 平板设备优化 */
@media (min-width: 641px) and (max-width: 1024px) {
/* 平板端下拉菜单适配 */
#work-order-dropdown .bg-white {
min-height: 60vh;
max-width: 600px;
margin: 0 auto;
border-radius: 12px;
margin-top: 10vh;
}
/* 平板端表单布局优化 */
#report-form .bg-white {
max-width: 500px;
margin: 0 auto;
}
}
/* 大屏设备优化 */
@media (min-width: 1025px) {
/* 桌面端容器最大宽度 */
#app {
max-width: 480px;
margin: 0 auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
/* 桌面端下拉菜单 */
#work-order-dropdown .bg-white {
max-width: 480px;
margin: 0 auto;
border-radius: 12px;
margin-top: 5vh;
min-height: 50vh;
}
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 600px) {
/* 横屏时减少垂直间距 */
#header-nav {
padding: 0.5rem 1rem;
}
main {
padding: 1rem;
}
#work-order-dropdown .bg-white {
min-height: 70vh;
}
#empty-state {
padding: 2rem 0;
}
#empty-state .w-48 {
width: 8rem;
height: 8rem;
}
}
/* 高分辨率屏幕优化 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
/* 高分辨率屏幕下的图标和边框优化 */
.border {
border-width: 0.5px;
}
svg {
shape-rendering: geometricPrecision;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
/* 可以添加深色模式样式 */
/* 目前保持浅色主题 */
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
#work-order-dropdown,
#work-order-dropdown .bg-white,
#dropdown-arrow {
transition: none !important;
}
}
/* 触摸设备优化 */
@media (pointer: coarse) {
/* 增大触摸目标 */
button, .cursor-pointer {
min-height: 44px;
min-width: 44px;
}
/* 工单列表项触摸优化 */
.work-order-item {
padding: 1rem;
min-height: 60px;
}
/* 输入框触摸优化 */
input, select, textarea {
min-height: 44px;
}
}
/* 无障碍优化 */
@media (prefers-contrast: high) {
/* 高对比度模式 */
.border-gray-300 {
border-color: #000 !important;
}
.text-gray-500 {
color: #333 !important;
}
.bg-gray-50 {
background-color: #fff !important;
}
}
/* 加载状态样式 */
.loading {
position: relative;
overflow: hidden;
}
.loading::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* 滚动条样式 */
#work-order-list::-webkit-scrollbar {
width: 4px;
}
#work-order-list::-webkit-scrollbar-track {
background: #f1f5f9;
}
#work-order-list::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 2px;
}
#work-order-list::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}

View File

@@ -0,0 +1,257 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>报工 - 工单管理系统</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 自定义样式 -->
<link rel="stylesheet" href="css/custom.css">
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 应用容器 -->
<div id="app" class="min-h-screen flex flex-col">
<!-- 顶部导航栏 -->
<header id="header-nav" class="px-4 py-3 flex items-center justify-between" style="background-color: var(--primary-color)">
<!-- 返回按钮 -->
<button id="back-btn" class="p-2 rounded-lg transition-colors hover-primary-bg">
<svg class="w-6 h-6 text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: white;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<!-- 标题 -->
<h1 class="text-lg font-semibold text-gray-800" style="color: white;">标准报工页面</h1>
<!-- 占位元素保持居中 -->
<div class="w-10"></div>
</header>
<!-- 主要内容区域 -->
<main class="flex-1 px-4 py-6">
<!-- 工单选择区域 -->
<div id="work-order-selector" class="bg-white rounded-lg shadow-sm p-4 mb-6 cursor-pointer hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<!-- 工单图标占位符 -->
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: var(--primary-light)">
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 001 1h6a1 1 0 001-1V3a2 2 0 012 2v6.5a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 014 11.5V5z" clip-rule="evenodd"></path>
</svg>
</div>
<!-- 工单文字 -->
<div>
<span class="text-gray-800 font-medium">工单</span>
<span id="selected-work-order-text" class="text-gray-500 ml-2">请选择</span>
</div>
</div>
<div class="flex items-center space-x-2">
<!-- 扫码按钮 -->
<button id="scan-btn" class="p-2 border-2 rounded-lg transition-colors border-primary hover-primary-light-bg">
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h2M4 4h5v5H4V4zm11 0h5v5h-5V4zM4 15h5v5H4v-5z"></path>
</svg>
</button>
<!-- 下拉箭头 -->
<svg id="dropdown-arrow" class="w-5 h-5 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
</div>
<!-- 工单下拉菜单 -->
<div id="work-order-dropdown" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
<div class="bg-white rounded-t-lg mt-auto min-h-[70vh] flex flex-col">
<!-- 下拉菜单头部 -->
<div class="flex items-center justify-between p-4 border-b">
<h2 class="text-lg font-semibold text-gray-800">工单编号</h2>
<button id="close-dropdown" class="p-2 hover:bg-gray-100 rounded-lg">
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- 搜索框 -->
<div class="p-4 border-b">
<div class="relative">
<input id="search-input" type="text" placeholder="搜索工单编号"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent focus-primary-ring">
</div>
</div>
<!-- 工单列表 -->
<div id="work-order-list" class="flex-1 overflow-y-auto">
<!-- 工单列表项将通过JavaScript动态生成 -->
</div>
<!-- 底部操作区 -->
<div class="p-4 border-t bg-gray-50 flex items-center justify-between">
<div class="flex items-center space-x-4">
<label class="flex items-center space-x-2 cursor-pointer">
<input id="select-all" type="checkbox" class="rounded border-gray-300 checkbox-primary">
<span class="text-sm text-gray-600">全选</span>
</label>
<span id="selected-count" class="text-sm text-gray-600">已选择0个</span>
</div>
<button id="confirm-selection" class="btn-primary text-white px-6 py-2 rounded-lg transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed" disabled>
确定
</button>
</div>
</div>
</div>
<!-- 空状态区域 -->
<div id="empty-state" class="text-center py-12">
<!-- 插图占位符 -->
<div class="w-48 h-48 mx-auto mb-6 bg-gray-100 rounded-lg flex items-center justify-center">
<svg class="w-24 h-24 text-gray-300" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm8 2a1 1 0 100 2h2a1 1 0 100-2H11z" clip-rule="evenodd"></path>
</svg>
</div>
<p class="text-gray-500 text-lg">先选择工单</p>
</div>
<!-- 物料信息区域 -->
<div id="material-info" class="hidden">
<!-- 工单信息卡片 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-4">
<div class="flex items-center justify-between mb-3" style="display: none;">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: var(--primary-light)">
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 001 1h6a1 1 0 001-1V3a2 2 0 012 2v6.5a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 014 11.5V5z" clip-rule="evenodd"></path>
</svg>
</div>
<div>
<span class="text-gray-800 font-medium">工单</span>
<span id="selected-work-order-id" class="text-gray-800 font-semibold ml-2"></span>
</div>
</div>
<button class="p-2 border-2 rounded-lg transition-colors border-primary hover-primary-light-bg">
<svg class="w-5 h-5" style="color: var(--primary-color)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h2M4 4h5v5H4V4zm11 0h5v5h-5V4zM4 15h5v5H4v-5z"></path>
</svg>
</button>
</div>
<!-- 物料信息 -->
<div class="space-y-2">
<h3 id="material-name" class="text-lg font-semibold text-gray-800"></h3>
<p id="material-code" class="text-gray-600"></p>
<!-- 状态标签 -->
<div class="flex space-x-2 mt-3">
<span class="px-3 py-1 rounded-full text-sm font-medium" style="background-color: var(--primary-light); color: var(--primary-dark)">
合格 <span id="qualified-count">0</span>
</span>
<span class="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
不良 <span id="defective-count">0</span>
</span>
<span class="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-sm font-medium">
待产 <span id="pending-count">0</span>
</span>
</div>
</div>
</div>
</div>
<!-- 报工表单区域 -->
<div id="report-form" class="hidden">
<h2 class="text-lg font-semibold text-gray-800 mb-4">报工</h2>
<div class="bg-white rounded-lg shadow-sm p-4 space-y-4">
<!-- 数量输入 -->
<div class="flex items-center justify-between">
<label class="text-gray-700 font-medium">
数量 <span class="text-red-500">*</span>
</label>
<div class="flex items-center space-x-2">
<input id="quantity" type="number" value="1" min="0"
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
<span class="text-gray-500">&nbsp;&nbsp;&nbsp;&nbsp;</span>
</div>
</div>
<!-- 辅助数量输入 -->
<div class="flex items-center justify-between" style="display: none;">
<label class="text-gray-700 font-medium">
辅助数量 <span class="text-red-500">*</span>
</label>
<div class="flex items-center space-x-2">
<input id="auxiliary-quantity" type="number" value="1" min="0"
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
<span id="auxName" class="text-gray-500">公斤</span>
</div>
</div>
<!-- 工时输入 -->
<div class="flex items-center justify-between">
<label class="text-gray-700 font-medium">
工时
</label>
<div class="flex items-center space-x-2">
<input id="working-hours" type="number" value="1" min="0"
class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
<span class="text-gray-500">小时</span>
</div>
</div>
<!-- 报工开始时间 -->
<div class="flex items-center justify-between">
<label class="text-gray-700 font-medium">
报工开始时间
</label>
<div class="flex items-center space-x-2">
<input id="report-start-time" type="datetime-local" value="1" min="0"
class="w-50 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
</div>
</div>
<!-- 报工结束时间 -->
<div class="flex items-center justify-between">
<label class="text-gray-700 font-medium">
报工结束时间
</label>
<div class="flex items-center space-x-2">
<input id="report-end-time" type="datetime-local" value="1" min="0"
class="w-50 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
</div>
</div>
<!-- 备注输入 -->
<div class="flex items-center justify-between">
<label class="text-gray-700 font-medium">
备注
</label>
<div class="flex items-center space-x-2">
<input id="remark" type="text" value="" placeholder="请输入备注"
class="w-60 px-3 py-2 border border-gray-300 rounded-lg text-right focus:ring-2 focus:border-transparent focus-primary-ring">
</div>
</div>
</div>
</div>
</main>
<!-- 提交按钮 -->
<div id="submit-section" class="hidden p-4">
<button id="submit-btn" class="w-full btn-primary text-white py-3 rounded-lg font-semibold transition-colors">
提交
</button>
</div>
</div>
<!-- JavaScript文件 -->
<script src="js/common.js"></script>
<script src="services/core.js"></script>
<script src="services/business.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,856 @@
/**
* 通用代码
* 包含工具函数和UI组件
*/
// ==================== 工具函数 ====================
/**
* 从URL查询参数中获取指定参数的值
* @param {string} name - 参数名称
* @returns {string|null} 参数值或null
*/
function getUrlParameter(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
/**
* 防抖函数
* @param {Function} func - 要防抖的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* 显示Toast消息
* @param {string} message - 消息内容
* @param {string} type - 消息类型 ('success', 'error', 'warning', 'info')
* @param {number} duration - 显示时长毫秒默认3000
*/
function showToast(message, type = 'info', duration = 3000) {
// 创建toast容器如果不存在
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 space-y-2';
document.body.appendChild(toastContainer);
}
// 创建toast元素
const toast = document.createElement('div');
const bgColor = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-yellow-500',
info: 'bg-blue-500'
}[type] || 'bg-blue-500';
toast.className = `${bgColor} text-white px-4 py-2 rounded-lg shadow-lg transform transition-all duration-300 translate-x-full opacity-0`;
toast.textContent = message;
// 添加到容器
toastContainer.appendChild(toast);
// 显示动画
setTimeout(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
}, 100);
// 自动隐藏
setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, duration);
}
/**
* 验证表单字段
* @param {Object} data - 表单数据
* @param {Object} rules - 验证规则
* @returns {Object} 验证结果 {isValid: boolean, errors: Object}
*/
function validateForm(data, rules) {
const errors = {};
let isValid = true;
for (const field in rules) {
const value = data[field];
const rule = rules[field];
// 必填验证
if (rule.required && (!value || value.toString().trim() === '')) {
errors[field] = rule.message || `${field}是必填项`;
isValid = false;
continue;
}
// 数字验证
if (rule.type === 'number' && value !== undefined && value !== '') {
const num = Number(value);
if (isNaN(num)) {
errors[field] = `${field}必须是数字`;
isValid = false;
continue;
}
if (rule.min !== undefined && num < rule.min) {
errors[field] = `${field}不能小于${rule.min}`;
isValid = false;
continue;
}
if (rule.max !== undefined && num > rule.max) {
errors[field] = `${field}不能大于${rule.max}`;
isValid = false;
continue;
}
}
// 字符串长度验证
if (rule.type === 'string' && value) {
if (rule.minLength && value.length < rule.minLength) {
errors[field] = `${field}长度不能少于${rule.minLength}个字符`;
isValid = false;
continue;
}
if (rule.maxLength && value.length > rule.maxLength) {
errors[field] = `${field}长度不能超过${rule.maxLength}个字符`;
isValid = false;
continue;
}
}
}
return { isValid, errors };
}
/**
* 深拷贝对象
* @param {any} obj - 要拷贝的对象
* @returns {any} 拷贝后的对象
*/
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item));
}
if (typeof obj === 'object') {
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
return obj;
}
// ==================== UI组件 ====================
/**
* 顶部导航栏组件
*/
class HeaderNav {
constructor() {
this.element = document.getElementById('header-nav');
this.backBtn = document.getElementById('back-btn');
this.init();
}
/**
* 初始化导航栏事件
*/
init() {
this.backBtn.addEventListener('click', () => {
// 返回上一页或关闭页面
if (window.history.length > 1) {
window.history.back();
} else {
window.close();
}
});
}
}
/**
* 工单选择器组件
*/
class WorkOrderSelector {
constructor(onSelect) {
this.element = document.getElementById('work-order-selector');
this.selectedText = document.getElementById('selected-work-order-text');
this.dropdownArrow = document.getElementById('dropdown-arrow');
this.scanBtn = document.getElementById('scan-btn');
this.onSelect = onSelect;
this.selectedWorkOrder = null;
this.init();
}
/**
* 初始化选择器事件
*/
init() {
// 点击选择器展开下拉菜单
this.element.addEventListener('click', (e) => {
// 如果点击的是扫码按钮,不展开下拉菜单
if (e.target.closest('#scan-btn')) {
return;
}
this.onSelect();
});
// 扫码按钮事件
this.scanBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.handleScan();
});
}
/**
* 处理扫码功能
*/
handleScan() {
showToast('扫码功能开发中...', 'info');
// TODO: 实现扫码功能
}
/**
* 更新选中的工单显示
* @param {Object} workOrder - 工单对象
*/
updateSelected(workOrder) {
this.selectedWorkOrder = workOrder;
if (workOrder) {
this.selectedText.textContent = workOrder.id;
this.selectedText.className = 'text-gray-800 ml-2 font-medium';
} else {
this.selectedText.textContent = '请选择';
this.selectedText.className = 'text-gray-500 ml-2';
}
}
/**
* 切换下拉箭头状态
* @param {boolean} expanded - 是否展开
*/
toggleArrow(expanded) {
if (expanded) {
this.dropdownArrow.classList.add('rotate');
} else {
this.dropdownArrow.classList.remove('rotate');
}
}
}
/**
* 工单下拉菜单组件
*/
class WorkOrderDropdown {
constructor(onConfirm, onClose) {
this.element = document.getElementById('work-order-dropdown');
this.closeBtn = document.getElementById('close-dropdown');
this.searchInput = document.getElementById('search-input');
this.workOrderList = document.getElementById('work-order-list');
this.selectAllCheckbox = document.getElementById('select-all');
this.selectedCount = document.getElementById('selected-count');
this.confirmBtn = document.getElementById('confirm-selection');
this.onConfirm = onConfirm;
this.onClose = onClose;
this.workOrders = [];
this.filteredWorkOrders = [];
this.selectedWorkOrders = new Set();
this.init();
}
/**
* 初始化下拉菜单事件
*/
init() {
// 关闭按钮事件
this.closeBtn.addEventListener('click', () => {
this.hide();
});
// 点击背景关闭
this.element.addEventListener('click', (e) => {
if (e.target === this.element) {
this.hide();
}
});
// 搜索功能
this.searchInput.addEventListener('input', debounce((e) => {
this.filterWorkOrders(e.target.value);
}, 300));
// 全选功能
this.selectAllCheckbox.addEventListener('change', (e) => {
this.toggleSelectAll(e.target.checked);
});
// 确定按钮事件
this.confirmBtn.addEventListener('click', () => {
this.confirmSelection();
});
// ESC键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isVisible()) {
this.hide();
}
});
}
/**
* 显示下拉菜单
*/
show() {
this.element.classList.remove('hidden');
this.element.classList.add('show');
document.body.style.overflow = 'hidden';
// 聚焦搜索框
setTimeout(() => {
this.searchInput.focus();
}, 300);
}
/**
* 隐藏下拉菜单
*/
hide() {
this.element.classList.remove('show');
document.body.style.overflow = '';
setTimeout(() => {
this.element.classList.add('hidden');
}, 300);
this.onClose();
}
/**
* 检查是否可见
* @returns {boolean}
*/
isVisible() {
return !this.element.classList.contains('hidden');
}
/**
* 设置工单数据
* @param {Array} workOrders - 工单数组
*/
setWorkOrders(workOrders) {
this.workOrders = workOrders;
this.filteredWorkOrders = [...workOrders];
this.renderWorkOrderList();
}
/**
* 过滤工单
* @param {string} searchTerm - 搜索关键词
*/
filterWorkOrders(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (!term) {
this.filteredWorkOrders = [...this.workOrders];
} else {
this.filteredWorkOrders = this.workOrders.filter(wo =>
wo.id.toLowerCase().includes(term) ||
(wo.name && wo.name.toLowerCase().includes(term))
);
}
this.renderWorkOrderList();
}
/**
* 渲染工单列表
*/
renderWorkOrderList() {
this.workOrderList.innerHTML = '';
if (this.filteredWorkOrders.length === 0) {
const emptyDiv = document.createElement('div');
emptyDiv.className = 'text-center py-8 text-gray-500';
emptyDiv.textContent = '没有找到匹配的工单';
this.workOrderList.appendChild(emptyDiv);
return;
}
this.filteredWorkOrders.forEach(workOrder => {
const item = this.createWorkOrderItem(workOrder);
this.workOrderList.appendChild(item);
});
this.updateSelectAllState();
}
/**
* 创建工单列表项
* @param {Object} workOrder - 工单对象
* @returns {HTMLElement}
*/
createWorkOrderItem(workOrder) {
const item = document.createElement('div');
item.className = 'work-order-item flex items-center space-x-3';
item.dataset.workOrderId = workOrder.id;
const isSelected = this.selectedWorkOrders.has(workOrder.id);
if (isSelected) {
item.classList.add('selected');
}
item.innerHTML = `
<input type="radio" name="work-order" value="${workOrder.id}"
class="checkbox-primary focus:ring-2" ${isSelected ? 'checked' : ''}>
<span class="flex-1 text-gray-800">${workOrder.id}</span>
`;
// 点击事件
item.addEventListener('click', () => {
this.selectWorkOrder(workOrder.id);
});
return item;
}
/**
* 选择工单
* @param {string} workOrderId - 工单ID
*/
selectWorkOrder(workOrderId) {
// 单选模式:清除之前的选择
this.selectedWorkOrders.clear();
this.selectedWorkOrders.add(workOrderId);
// 更新UI
this.workOrderList.querySelectorAll('.work-order-item').forEach(item => {
const radio = item.querySelector('input[type="radio"]');
const isSelected = item.dataset.workOrderId === workOrderId;
item.classList.toggle('selected', isSelected);
radio.checked = isSelected;
});
this.updateSelectedCount();
this.updateConfirmButton();
}
/**
* 切换全选状态
* @param {boolean} selectAll - 是否全选
*/
toggleSelectAll(selectAll) {
if (selectAll) {
this.filteredWorkOrders.forEach(wo => {
this.selectedWorkOrders.add(wo.id);
});
} else {
this.selectedWorkOrders.clear();
}
this.renderWorkOrderList();
this.updateSelectedCount();
this.updateConfirmButton();
}
/**
* 更新全选状态
*/
updateSelectAllState() {
const visibleCount = this.filteredWorkOrders.length;
const selectedVisibleCount = this.filteredWorkOrders.filter(wo =>
this.selectedWorkOrders.has(wo.id)
).length;
this.selectAllCheckbox.checked = visibleCount > 0 && selectedVisibleCount === visibleCount;
this.selectAllCheckbox.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleCount;
}
/**
* 更新选中数量显示
*/
updateSelectedCount() {
this.selectedCount.textContent = `已选择${this.selectedWorkOrders.size}`;
}
/**
* 更新确定按钮状态
*/
updateConfirmButton() {
this.confirmBtn.disabled = this.selectedWorkOrders.size === 0;
}
/**
* 确认选择
*/
confirmSelection() {
if (this.selectedWorkOrders.size === 0) {
showToast('请选择工单', 'warning');
return;
}
const selectedIds = Array.from(this.selectedWorkOrders);
const selectedWorkOrder = this.workOrders.find(wo => wo.id === selectedIds[0]);
this.onConfirm(selectedWorkOrder);
this.hide();
}
/**
* 重置选择状态
*/
reset() {
this.selectedWorkOrders.clear();
this.searchInput.value = '';
this.filteredWorkOrders = [...this.workOrders];
this.renderWorkOrderList();
this.updateSelectedCount();
this.updateConfirmButton();
}
}
/**
* 物料信息组件
*/
class MaterialInfo {
constructor() {
this.element = document.getElementById('material-info');
this.workOrderId = document.getElementById('selected-work-order-id');
this.materialName = document.getElementById('material-name');
this.materialCode = document.getElementById('material-code');
this.qualifiedCount = document.getElementById('qualified-count');
this.defectiveCount = document.getElementById('defective-count');
this.pendingCount = document.getElementById('pending-count');
}
/**
* 显示物料信息
* @param {Object} data - 物料数据
*/
show(data) {
this.workOrderId.textContent = data.workOrderId;
this.materialName.textContent = data.materialName;
this.materialCode.textContent = data.materialCode;
this.qualifiedCount.textContent = data.qualifiedCount || 0;
this.defectiveCount.textContent = data.defectiveCount || 0;
this.pendingCount.textContent = data.pendingCount || 0;
this.element.classList.remove('hidden');
}
/**
* 隐藏物料信息
*/
hide() {
this.element.classList.add('hidden');
}
}
/**
* 报工表单组件
*/
class ReportForm {
constructor() {
this.element = document.getElementById('report-form');
this.quantityInput = document.getElementById('quantity');
this.auxiliaryQuantityInput = document.getElementById('auxiliary-quantity');
this.remarkInput = document.getElementById('remark');
this.workingHoursInput = document.getElementById('working-hours');
this.reportStartTimeInput = document.getElementById('report-start-time');
this.reportEndTimeInput = document.getElementById('report-end-time');
this.auxName = document.getElementById('auxName');
this.init();
}
/**
* 初始化表单事件
*/
init() {
// 数字输入验证
[this.quantityInput, this.auxiliaryQuantityInput].forEach(input => {
if (input) {
input.addEventListener('input', (e) => {
this.validateNumberInput(e.target);
});
}
});
// 表单字段变化时的验证
this.getAllInputs().forEach(input => {
if (input) {
input.addEventListener('blur', () => {
this.validateField(input);
});
}
});
}
/**
* 验证数字输入
* @param {HTMLInputElement} input - 输入元素
*/
validateNumberInput(input) {
if (!input) return;
const value = parseFloat(input.value);
if (isNaN(value) || value < 0) {
input.classList.add('border-red-500');
this.showFieldError(input, '请输入有效的数字');
} else {
input.classList.remove('border-red-500');
this.hideFieldError(input);
}
}
/**
* 验证单个字段
* @param {HTMLInputElement} input - 输入元素
*/
validateField(input) {
if (!input) return false;
const value = input.value.trim();
const isRequired = input.previousElementSibling?.querySelector('.text-red-500');
if (isRequired && !value) {
input.classList.add('border-red-500');
this.showFieldError(input, '此字段为必填项');
return false;
} else {
input.classList.remove('border-red-500');
this.hideFieldError(input);
return true;
}
}
/**
* 显示字段错误
* @param {HTMLInputElement} input - 输入元素
* @param {string} message - 错误消息
*/
showFieldError(input, message) {
let errorElement = input.parentNode.querySelector('.field-error');
if (!errorElement) {
errorElement = document.createElement('div');
// errorElement.className = 'field-error text-red-500 text-sm mt-1';
errorElement.className = 'field-error text-red-500 text-sm mt-1 w-full';
input.parentNode.appendChild(errorElement);
}
errorElement.textContent = message;
}
/**
* 隐藏字段错误
* @param {HTMLInputElement} input - 输入元素
*/
hideFieldError(input) {
const errorElement = input.parentNode.querySelector('.field-error');
if (errorElement) {
errorElement.remove();
}
}
/**
* 获取所有输入元素
* @returns {HTMLInputElement[]}
*/
getAllInputs() {
return [
this.quantityInput,
this.auxiliaryQuantityInput,
this.remarkInput,
this.workingHoursInput,
this.reportStartTimeInput,
this.reportEndTimeInput,
].filter(input => input !== null && input !== undefined);
}
/**
* 获取表单数据
* @returns {Object}
*/
getData() {
return {
quantity: parseFloat(this.quantityInput?.value) || 0,
auxiliaryQuantity: parseFloat(this.auxiliaryQuantityInput?.value) || 0,
remark: this.remarkInput?.value?.trim() || '',
workingHours: parseFloat(this.workingHoursInput?.value) || 0,
reportStartTime: this.reportStartTimeInput?.value?.trim() || '',
reportEndTimeInput: this.reportEndTimeInput?.value?.trim() || '',
};
}
/**
* 设置表单数据
* @param {Object} data - 表单数据
*/
setData(data) {
if (this.quantityInput) {
this.quantityInput.value = data.quantity || '';
}
if (this.auxiliaryQuantityInput) {
this.auxiliaryQuantityInput.value = data.auxiliaryQuantity || '';
}
}
setAuxName(data) {
if (this.auxName) {
this.auxName.innerText = data.auxName || '公斤';
}
}
/**
* 验证整个表单
* @returns {Object} 验证结果
*/
validate() {
const data = this.getData();
const rules = {
quantity: { required: true, type: 'number', min: 0 },
auxiliaryQuantity: { required: true, type: 'number', min: 0 }
};
return validateForm(data, rules);
}
/**
* 显示表单
*/
show() {
this.element.classList.remove('hidden');
}
/**
* 隐藏表单
*/
hide() {
this.element.classList.add('hidden');
}
/**
* 重置表单
*/
reset() {
this.getAllInputs().forEach(input => {
input.value = '';
input.classList.remove('border-red-500');
this.hideFieldError(input);
});
}
}
const GlobalLoading = {
element: null,
init() {
if (this.element) return;
this.element = document.createElement('div');
this.element.id = 'global-loading';
this.element.innerHTML = `
<div class="loading-backdrop"></div>
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
`;
// 添加样式
const style = document.createElement('style');
style.textContent = `
#global-loading {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
display: none;
}
.loading-backdrop {
position: absolute;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
.loading-text {
color: white;
font-size: 14px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
body.loading-active {
overflow: hidden;
}
`;
document.head.appendChild(style);
document.body.appendChild(this.element);
},
show(text = '加载中...') {
this.init();
const textElement = this.element.querySelector('.loading-text');
if (textElement) {
textElement.textContent = text;
}
this.element.style.display = 'block';
document.body.classList.add('loading-active');
},
hide() {
if (this.element) {
this.element.style.display = 'none';
document.body.classList.remove('loading-active');
}
}
};

View File

@@ -0,0 +1,457 @@
/**
* 主应用逻辑
* 管理页面状态、组件交互和业务流程
*/
class MainApp {
constructor() {
// 应用状态
this.state = {
currentView: 'empty', // empty, material, form
selectedWorkOrder: null,
materialData: null,
formData: null,
isLoading: false
};
// 组件实例
this.components = {};
// 初始化应用
this.init();
}
/**
* 初始化应用
*/
async init() {
try {
console.log('初始化报工应用...');
// 等待认证服务初始化完成
if (authService && !authService.isAuthenticated()) {
await this.waitForAuth();
}
// 初始化组件
this.initComponents();
// 绑定事件
this.bindEvents();
// 预加载数据
this.preloadData();
console.log('报工应用初始化完成');
} catch (error) {
console.error('应用初始化失败:', error);
showToast('应用初始化失败,请刷新页面重试', 'error');
}
}
/**
* 等待认证完成
*/
async waitForAuth() {
let retries = 0;
const maxRetries = 10;
while (!authService.isAuthenticated() && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 500));
retries++;
}
if (!authService.isAuthenticated()) {
console.warn('认证超时,继续使用离线模式');
}
}
/**
* 初始化组件
*/
initComponents() {
// 初始化顶部导航栏
this.components.headerNav = new HeaderNav();
// 初始化工单选择器
this.components.workOrderSelector = new WorkOrderSelector(() => {
this.showWorkOrderDropdown();
});
// 初始化工单下拉菜单
this.components.workOrderDropdown = new WorkOrderDropdown(
(workOrder) => this.onWorkOrderSelected(workOrder),
() => this.onWorkOrderDropdownClosed()
);
// 初始化物料信息组件
this.components.materialInfo = new MaterialInfo();
// 初始化报工表单组件
this.components.reportForm = new ReportForm();
console.log('组件初始化完成');
}
/**
* 绑定事件
*/
bindEvents() {
// 提交按钮事件
const submitBtn = document.getElementById('submit-btn');
if (submitBtn) {
submitBtn.addEventListener('click', () => {
this.handleSubmit();
});
}
// 网络状态变化事件
window.addEventListener('online', () => {
console.log('网络已连接');
showToast('网络已连接', 'success');
});
window.addEventListener('offline', () => {
console.log('网络已断开');
showToast('网络已断开,将使用离线模式', 'warning');
});
console.log('事件绑定完成');
}
/**
* 预加载数据
*/
async preloadData() {
try {
// 预加载工单数据
if (workOrderService) {
await workOrderService.preloadWorkOrders();
}
} catch (error) {
console.error('预加载数据失败:', error);
}
}
/**
* 显示工单下拉菜单
*/
async showWorkOrderDropdown() {
try {
this.setLoading(true);
// 获取工单列表
const workOrderData = await workOrderService.getWorkOrderList();
// 设置工单数据到下拉菜单
this.components.workOrderDropdown.setWorkOrders(workOrderData.workOrders);
// 显示下拉菜单
this.components.workOrderDropdown.show();
// 更新选择器箭头状态
this.components.workOrderSelector.toggleArrow(true);
} catch (error) {
console.error('加载工单列表失败:', error);
showToast('加载工单列表失败', 'error');
} finally {
this.setLoading(false);
}
}
/**
* 工单选择事件处理
* @param {Object} workOrder - 选中的工单
*/
async onWorkOrderSelected(workOrder) {
try {
console.log('选择工单:', workOrder);
this.setLoading(true);
// 更新状态
this.state.selectedWorkOrder = workOrder;
// 更新工单选择器显示
this.components.workOrderSelector.updateSelected(workOrder);
// 从工单数据中提取物料信息工单数据中已包含materialInfo
const materialData = this.extractMaterialDataFromWorkOrder(workOrder);
this.state.materialData = materialData;
// 切换到物料和表单视图
this.switchToMaterialView();
// 初始化表单数据
this.initializeFormData();
} catch (error) {
console.error('处理工单选择失败:', error);
showToast('处理工单选择失败', 'error');
} finally {
this.setLoading(false);
}
}
/**
* 从工单数据中提取物料信息
* @param {Object} workOrder - 工单对象
* @returns {Object} 物料数据
*/
extractMaterialDataFromWorkOrder(workOrder) {
// 从materialInfo中提取详细信息
const materialInfo = workOrder.materialInfo || {};
const materialBaseInfo = materialInfo.baseInfo || {};
// 提取单位转换关系
const conversions = materialInfo.conversions || [];
let unit = '个';
let auxiliaryUnit = '公斤';
if (conversions.length > 0) {
const firstConversion = conversions[0];
unit = firstConversion.fromUnitName || '个';
auxiliaryUnit = firstConversion.toUnitName || '公斤';
}
return {
workOrderId: workOrder.workOrderId || workOrder.id,
workOrderCode: workOrder.workOrderCode || workOrder.id,
materialId: workOrder.materialId || materialBaseInfo.id,
materialName: workOrder.materialName || materialBaseInfo.name || '',
materialCode: workOrder.materialCode || materialBaseInfo.code || '',
materialSpec: materialBaseInfo.specification || '',
unit: unit,
auxiliaryUnit: auxiliaryUnit,
conversions: conversions,
// 数量信息
qualifiedCount: workOrder.qualifiedQuantity || 0,
qualifiedCountDisplay: workOrder.qualifiedQuantityDisplay || '0',
defectiveCount: workOrder.disqualifiedQuantity || 0,
defectiveCountDisplay: workOrder.disqualifiedQuantityDisplay || '0',
pendingCount: Math.max(0, (workOrder.totalQuantity || 0) - (workOrder.qualifiedQuantity || 0) - (workOrder.disqualifiedQuantity || 0)),
totalCount: workOrder.totalQuantity,
unitId: materialInfo?.unit?.id
};
}
/**
* 工单下拉菜单关闭事件处理
*/
onWorkOrderDropdownClosed() {
// 更新选择器箭头状态
this.components.workOrderSelector.toggleArrow(false);
}
/**
* 切换到物料视图
*/
switchToMaterialView() {
// 隐藏空状态
const emptyState = document.getElementById('empty-state');
if (emptyState) {
emptyState.classList.add('hidden');
}
// 显示物料信息
this.components.materialInfo.show(this.state.materialData);
// 显示报工表单
this.components.reportForm.show();
// 显示提交按钮
const submitSection = document.getElementById('submit-section');
if (submitSection) {
submitSection.classList.remove('hidden');
}
// 更新状态
this.state.currentView = 'material';
this.components.reportForm.setAuxName({ auxName: this.state.materialData.unit });
}
/**
* 切换到空状态视图
*/
switchToEmptyView() {
// 显示空状态
const emptyState = document.getElementById('empty-state');
if (emptyState) {
emptyState.classList.remove('hidden');
}
// 隐藏物料信息
this.components.materialInfo.hide();
// 隐藏报工表单
this.components.reportForm.hide();
// 隐藏提交按钮
const submitSection = document.getElementById('submit-section');
if (submitSection) {
submitSection.classList.add('hidden');
}
// 重置状态
this.state.currentView = 'empty';
this.state.selectedWorkOrder = null;
this.state.materialData = null;
this.state.formData = null;
// 重置工单选择器
this.components.workOrderSelector.updateSelected(null);
console.log('切换到空状态视图');
}
/**
* 初始化表单数据
*/
initializeFormData() {
// 直接从 HTML 读取值,不设置默认值
const formData = this.components.reportForm.getData();
this.state.formData = formData;
}
/**
* 处理提交事件
*/
async handleSubmit() {
try {
// 验证表单
const validation = this.components.reportForm.validate();
if (!validation.isValid) {
const errorMessage = Object.values(validation.errors)[0];
showToast(errorMessage, 'error');
return;
}
// 获取表单数据
const formData = this.components.reportForm.getData();
console.log('表单数据:', formData);
// 构建报工数据
const reportData = {
workOrderId: this.state.selectedWorkOrder.workOrderId, // 使用数字类型的工单ID
workOrderCode: this.state.selectedWorkOrder.workOrderCode, // 同时传递工单编号作为备用
materialId: this.state.materialData.materialId,
quantity: formData.quantity,
auxiliaryQuantity: formData.auxiliaryQuantity,
remark: formData.remark,
workHour: formData.workingHours,
workHourUnit: 2,
auxUnitId1: this.state.materialData.unitId,
reportStartTime: formData.reportStartTime,
reportEndTime: formData.reportEndTimeInput
};
console.log('提交报工数据:', reportData);
// this.setLoading(true);
GlobalLoading.show('提交中...');
// 提交报工
const result = await reportService.submitReport(reportData);
console.log('报工提交结果:', result);
if (result.success) {
// 提交成功,重置表单
this.handleSubmitSuccess(result);
} else {
throw new Error(result.message || '提交失败');
}
} catch (error) {
console.error('提交报工失败:', error);
showToast(error.message || '提交失败', 'error');
} finally {
// this.setLoading(false);
GlobalLoading.hide();
}
}
/**
* 处理提交成功
* @param {Object} result - 提交结果
*/
handleSubmitSuccess(result) {
// 显示成功消息
showToast('报工提交成功', 'success');
// 重置表单
this.components.reportForm.reset();
// 重新初始化表单数据
this.initializeFormData();
}
/**
* 设置加载状态
* @param {boolean} loading - 是否加载中
*/
setLoading(loading) {
this.state.isLoading = loading;
// 更新提交按钮状态
const submitBtn = document.getElementById('submit-btn');
if (submitBtn) {
submitBtn.disabled = loading;
submitBtn.textContent = loading ? '提交中...' : '提交';
}
// 可以添加全局加载指示器
if (loading) {
document.body.style.cursor = 'wait';
} else {
document.body.style.cursor = '';
}
}
/**
* 重置应用状态
*/
reset() {
this.switchToEmptyView();
this.components.workOrderDropdown.reset();
console.log('应用状态已重置');
}
/**
* 获取应用状态
* @returns {Object}
*/
getState() {
return deepClone(this.state);
}
/**
* 销毁应用
*/
destroy() {
// 清理事件监听器
// 清理组件
// 清理定时器等
console.log('应用已销毁');
}
}
// 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM加载完成初始化应用...');
// 创建全局应用实例
window.MainApp = new MainApp();
// 开发模式下暴露服务到全局
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
window.authService = authService;
window.apiService = apiService;
window.workOrderService = workOrderService;
window.reportService = reportService;
console.log('开发模式:服务已暴露到全局');
}
});

View File

@@ -0,0 +1,758 @@
/**
* 业务服务
* 包含工单服务和报工服务
*/
// ==================== 工单服务 ====================
/**
* 工单服务
* 处理工单相关的API请求
*/
class WorkOrderService {
constructor() {
this.api = apiService;
}
/**
* 获取工单列表
* @param {Object} params - 查询参数
* @param {string} params.search - 搜索关键词(工单编号模糊查询)
* @param {string} params.exactWorkOrderCode - 工单编号精确查询
* @param {number} params.page - 页码
* @param {number} params.pageSize - 每页数量
* @param {Array<number>} params.workOrderStatusList - 工单业务状态列表
* @param {number} params.pauseFlag - 是否暂停 0未暂停 1已暂停
* @returns {Promise<Object>}
*/
async getWorkOrderList(params = {}) {
try {
console.log('从服务器获取工单列表', params);
// 构建请求体根据API文档格式
const requestBody = {
page: params.page || 1,
size: params.pageSize || 50
};
// 如果有搜索关键词,使用模糊查询
if (params.search) {
requestBody.workOrderCode = params.search.trim();
}
// 如果有精确查询
if (params.exactWorkOrderCode) {
requestBody.exactWorkOrderCode = params.exactWorkOrderCode.trim();
}
// 工单状态筛选
if (params.workOrderStatusList && Array.isArray(params.workOrderStatusList)) {
requestBody.workOrderStatusList = params.workOrderStatusList;
}
// 暂停状态筛选
if (params.pauseFlag !== undefined) {
requestBody.pauseFlag = params.pauseFlag;
}
// 调用API接口POST方法
const response = await this.api.post('/openapi/domain/web/v1/route/med/open/v2/work_order/base/_list', requestBody);
// 处理响应数据
const result = this.processWorkOrderListResponse(response);
return result;
} catch (error) {
console.error('获取工单列表失败:', error);
// 如果是网络错误,返回模拟数据
if (error.name === 'TypeError' || error.message.includes('Failed to fetch')) {
console.log('网络错误,返回模拟工单数据');
return this.getMockWorkOrderList(params);
}
throw error;
}
}
/**
* 处理工单列表响应数据
* @param {Object} response - API响应
* @returns {Object}
*/
processWorkOrderListResponse(response) {
// 根据API文档响应格式{ code, message, data: { list, page, total } }
if (response.code === 200 || response.code === 0) {
const data = response.data;
if (!data || !data.list) {
throw new Error('响应数据格式错误缺少data或list字段');
}
return {
workOrders: data.list.map(item => {
// 提取物料信息
const materialInfo = item.materialInfo || {};
const materialBaseInfo = materialInfo.baseInfo || {};
// 提取数量信息BaseAmountDisplay对象
const qualifiedAmount = item.qualifiedHoldAmount || {};
const disqualifiedAmount = item.disqualifiedHoldAmount || {};
const totalAmount = item.totalHoldAmount || {};
return {
id: item.workOrderCode || item.workOrderId?.toString() || '',
workOrderId: item.workOrderId,
workOrderCode: item.workOrderCode,
name: materialBaseInfo.name || materialInfo.name || item.workOrderCode,
status: item.workOrderStatus?.code || 'active',
// 物料信息(保留完整结构)
materialInfo: item.materialInfo, // 保留完整的materialInfo对象
materialId: materialBaseInfo.id || materialInfo.id,
materialName: materialBaseInfo.name || materialInfo.name,
materialCode: materialBaseInfo.code || materialInfo.code,
// 数量信息
qualifiedQuantity: qualifiedAmount.amount || 0,
qualifiedQuantityDisplay: qualifiedAmount.amountDisplay || '0',
disqualifiedQuantity: disqualifiedAmount.amount || 0,
disqualifiedQuantityDisplay: disqualifiedAmount.amountDisplay || '0',
totalQuantity: totalAmount.amount || 0,
totalQuantityDisplay: totalAmount.amountDisplay || '0',
// 时间信息
createdAt: item.createdAt,
updatedAt: item.updatedAt,
plannedStartTime: item.plannedStartTime,
plannedEndTime: item.plannedEndTime,
// 其他信息
pauseFlag: item.pauseFlag,
workOrderStatus: item.workOrderStatus,
workOrderType: item.workOrderType
};
}),
total: data.total || 0,
page: data.page || 1,
pageSize: data.size || 50
};
} else {
throw new Error(response.message || `获取工单列表失败 (code: ${response.code})`);
}
}
/**
* 获取模拟工单列表(用于离线模式或测试)
* @param {Object} params - 查询参数
* @returns {Object}
*/
getMockWorkOrderList(params = {}) {
const mockData = [
{
id: 'cyy-SOP',
name: '测试工单-SOP',
status: 'active',
materialId: 'M001',
materialName: '物料1-工序a-10',
materialCode: 'wuliao1-gongxua-10'
},
{
id: 'cyy-chengliangrenwu',
name: '测试工单-成量任务',
status: 'active',
materialId: 'M002',
materialName: '物料2-工序b-20',
materialCode: 'wuliao2-gongxub-20'
},
{
id: 'gd2510300131-',
name: '工单2510300131',
status: 'active',
materialId: 'M003',
materialName: '物料3-工序c-30',
materialCode: 'wuliao3-gongxuc-30'
},
{
id: 'gd2510270130-',
name: '工单2510270130',
status: 'active',
materialId: 'M004',
materialName: '物料4-工序d-40',
materialCode: 'wuliao4-gongxud-40'
},
{
id: 'gd2510210129-',
name: '工单2510210129',
status: 'active',
materialId: 'M005',
materialName: '物料5-工序e-50',
materialCode: 'wuliao5-gongxue-50'
},
{
id: 'gd2509020128-',
name: '工单2509020128',
status: 'active',
materialId: 'M006',
materialName: '物料6-工序f-60',
materialCode: 'wuliao6-gongxuf-60'
},
{
id: 'gd2509010126-',
name: '工单2509010126',
status: 'active',
materialId: 'M007',
materialName: '物料7-工序g-70',
materialCode: 'wuliao7-gongxug-70'
},
{
id: 'gd2509010125-',
name: '工单2509010125',
status: 'active',
materialId: 'M008',
materialName: '物料8-工序h-80',
materialCode: 'wuliao8-gongxuh-80'
},
{
id: 'gd2508260124-',
name: '工单2508260124',
status: 'active',
materialId: 'M009',
materialName: '物料9-工序i-90',
materialCode: 'wuliao9-gongxui-90'
},
{
id: 'gd2507210123-',
name: '工单2507210123',
status: 'active',
materialId: 'M010',
materialName: '物料10-工序j-100',
materialCode: 'wuliao10-gongxuj-100'
}
];
// 应用搜索过滤
let filteredData = mockData;
if (params.search) {
const searchTerm = params.search.toLowerCase();
filteredData = mockData.filter(item =>
item.id.toLowerCase().includes(searchTerm) ||
item.name.toLowerCase().includes(searchTerm)
);
}
return {
workOrders: filteredData,
total: filteredData.length,
page: params.page || 1,
pageSize: params.pageSize || 50
};
}
/**
* 预加载工单数据
*/
async preloadWorkOrders() {
try {
console.log('预加载工单数据...');
await this.getWorkOrderList({ pageSize: 100 });
} catch (error) {
console.error('预加载工单数据失败:', error);
}
}
}
// 创建全局工单服务实例
const workOrderService = new WorkOrderService();
// ==================== 报工服务 ====================
/**
* 报工服务
* 处理报工相关的API请求
*/
class ReportService {
constructor() {
this.api = apiService;
}
/**
* 获取报工必填参数
* 从接口获取报工所需的必填参数如taskId、progressReportMaterial等
* @param {Object} params - 查询参数
* @param {string|number} params.workOrderId - 工单ID
* @param {string|number} params.materialId - 物料ID
* @returns {Promise<Object>} 返回必填参数对象
*/
async getReportRequiredParams(params) {
try {
console.log('获取报工必填参数:', params);
const { workOrderId, materialId } = params;
if (!workOrderId ) {
throw new Error('工单ID获取失败');
}
const taskListRequest = {
page: 1,
size: 10,
workOrderIdList: [workOrderId]
};
const taskListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/produce_task/_list', taskListRequest);
if (taskListResponse.code !== 200 || !taskListResponse.data || !taskListResponse.data.list || taskListResponse.data.list.length === 0) {
throw new Error('未找到对应的生产任务');
}
// 取第一个任务(通常一个工单对应一个主任务)
const task = taskListResponse.data.list[0];
const taskId = task.taskId;
const executorIds = (task.executorList || []).map(item => item.id); // 只取执行人id数组
if (!taskId) {
throw new Error('生产任务中未找到任务ID');
}
// 2. 通过生产任务ID获取报工物料列表
console.log('获取报工物料列表...');
const materialListRequest = {
taskId: taskId
};
const materialListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list_progress_report_materials', materialListRequest);
if (materialListResponse.code !== 200 || !materialListResponse.data || !materialListResponse.data.outputMaterials || materialListResponse.data.outputMaterials.length === 0) {
throw new Error('未找到报工物料信息');
}
// 获取报工物料列表
const outputMaterials = materialListResponse.data.outputMaterials;
// 优先选择主产出物料mainFlag为true否则选择第一个
let selectedMaterial = outputMaterials.find(m => m.mainFlag === true) || outputMaterials[0];
if (!selectedMaterial) {
throw new Error('未找到可报工的物料');
}
// 从报工物料中提取报工关系信息
const progressReportKey = selectedMaterial.progressReportKey;
if (!progressReportKey) {
throw new Error('报工物料中未找到报工关系信息');
}
// 3. 构建progressReportMaterial对象
const progressReportMaterial = {
lineId: progressReportKey.lineId,
materialId: progressReportKey.materialId || selectedMaterial.materialInfo?.baseInfo?.id || materialId,
reportProcessId: progressReportKey.reportProcessId
};
// 验证必填字段
if (!progressReportMaterial.lineId) {
throw new Error('无法获取物料行ID');
}
if (!progressReportMaterial.materialId) {
throw new Error('无法获取物料ID');
}
if (!progressReportMaterial.reportProcessId) {
throw new Error('无法获取报工工序ID');
}
// 4. 返回必填参数
const requiredParams = {
taskId: taskId,
progressReportMaterial: progressReportMaterial,
// 额外信息,可能用于构建报工参数
materialInfo: selectedMaterial.materialInfo,
outputMaterialUnit: selectedMaterial.outputMaterialUnit, // 报工单位信息
lineId: progressReportMaterial.lineId,
reportProcessId: progressReportMaterial.reportProcessId,
executorIds: executorIds, // 执行人列表
// 报工物料的其他信息
reportType: selectedMaterial.reportType || [], // 可报工方式
mainFlag: selectedMaterial.mainFlag, // 是否为主产出
warehousingFlag: selectedMaterial.warehousingFlag, // 是否入库
autoWarehousingFlag: selectedMaterial.autoWarehousingFlag // 是否自动入库
};
console.log('成功获取报工必填参数:', requiredParams);
return requiredParams;
} catch (error) {
console.error('获取报工必填参数失败:', error);
throw error;
}
}
/**
* 构建报工请求参数
* 将必填参数和表单数据合并,构建完整的报工请求参数
* @param {Object} requiredParams - 从接口获取的必填参数
* @param {Object} formData - 表单数据
* @param {Object} formData.workOrderId - 工单ID
* @param {Object} formData.materialId - 物料ID
* @param {number} formData.quantity - 报工数量
* @param {number} formData.auxiliaryQuantity - 辅助数量
* @param {number} formData.qcStatus - 质量状态1合格 2让步合格 3代检 4不合格
* @param {number} formData.reportType - 报工方式1扫码报工-合格 2记录报工-合格 3扫码报工-不合格 4记录报工-不合格 5打码报工-合格 6打码报工-不合格)
* @param {Array<number>} formData.executorIds - 执行人ID列表
* @returns {Object} 完整的报工请求参数
* @note storageLocationId 已预置为 1716848012872791
*/
buildReportRequestParams(requiredParams, formData) {
// 从必填参数中获取基础信息
const {
taskId, // 生产任务ID必填
progressReportMaterial, // 报工物料对象(必填)
reportProcessId, // 报工工序ID
lineId, // 物料行ID
outputMaterialUnit, // 报工单位信息
executorIds // 执行人ID列表
} = requiredParams;
// 获取报工单位ID从outputMaterialUnit中获取
const reportUnitId = outputMaterialUnit?.id;
if (!reportUnitId) {
throw new Error('无法获取报工单位ID');
}
// 构建报工请求参数
const requestParams = {
// 必填字段
taskId: taskId,
progressReportMaterial: progressReportMaterial || {
materialId: formData.materialId,
lineId: lineId,
reportProcessId: reportProcessId
},
qcStatus: formData.qcStatus || 1, // 默认合格
reportType: formData.reportType || 2, // 默认记录报工-合格
// 报工详情(必填)
progressReportItems: [{
executorIds: executorIds ,
progressReportMaterialItems: [{
reportAmount: formData.quantity,
reportUnitId: reportUnitId, // 必填报工单位ID
remark: formData.remark || undefined,
// auxAmount1: formData.auxiliaryQuantity || undefined,
// auxUnitId1: formData.auxUnitId1 || undefined
}]
}],
// 可选字段
storageLocationId: 1716848012872791,// 预置仓位 Id不可修改
reportStartTime: formData.reportStartTime ? new Date(formData.reportStartTime).getTime() : undefined,
reportEndTime: formData.reportEndTime ? new Date(formData.reportEndTime).getTime() : undefined,
actualExecutorIds: executorIds || [],
actualEquipmentIds: formData.equipmentIds || [],
workHour: formData.workHour,
workHourUnit: formData.workHourUnit ,
qcDefectReasonIds: formData.qcDefectReasonIds || [] // 不良原因(质量状态为不合格时)
};
// 移除undefined字段
Object.keys(requestParams).forEach(key => {
if (requestParams[key] === undefined) {
delete requestParams[key];
}
});
// 递归移除嵌套对象中的undefined字段
const cleanUndefined = (obj) => {
if (Array.isArray(obj)) {
return obj.map(item => cleanUndefined(item));
} else if (obj && typeof obj === 'object') {
const cleaned = {};
Object.keys(obj).forEach(key => {
if (obj[key] !== undefined) {
cleaned[key] = cleanUndefined(obj[key]);
}
});
return cleaned;
}
return obj;
};
return cleanUndefined(requestParams);
}
/**
* 提交报工
* @param {Object} reportData - 报工数据
* @param {string|number} reportData.workOrderId - 工单ID
* @param {string|number} reportData.workOrderCode - 工单编号
* @param {string|number} reportData.materialId - 物料ID
* @param {number} reportData.quantity - 数量
* @param {number} reportData.auxiliaryQuantity - 辅助数量
* @param {number} reportData.qcStatus - 质量状态1合格 2让步合格 3代检 4不合格
* @param {number} reportData.reportType - 报工方式1-6
* @param {Array<number>} reportData.executorIds - 执行人ID列表
* @param {number} reportData.reportUnitId - 报工单位ID
* @param {number} reportData.auxUnitId1 - 辅助单位1 ID
* @returns {Promise<Object>}
*/
async submitReport(reportData) {
try {
// 验证报工数据
this.validateReportData(reportData);
// 1. 获取报工必填参数
const requiredParams = await this.getReportRequiredParams({
workOrderId: reportData.workOrderId,
materialId: reportData.materialId
});
// 2. 构建完整的报工请求参数
const requestParams = this.buildReportRequestParams(requiredParams, reportData);
console.log('提交报工请求参数:', requestParams);
// 3. 调用批量报工接口
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report', requestParams);
// 4. 处理响应
const result = this.processReportResponse(response);
console.log('报工提交成功:', result);
return result;
} catch (error) {
console.error('报工提交失败:', error);
showToast(error.message || '报工提交失败', 'error');
throw error;
}
}
/**
* 验证报工数据
* @param {Object} data - 报工数据
*/
validateReportData(data) {
const errors = [];
if (!data.workOrderId) {
errors.push('工单ID不能为空');
}
if (!data.quantity || data.quantity <= 0) {
errors.push('数量必须大于0');
}
if (!data.auxiliaryQuantity || data.auxiliaryQuantity <= 0) {
errors.push('辅助数量必须大于0');
}
if (errors.length > 0) {
throw new Error(errors.join(', '));
}
}
/**
* 处理报工响应
* 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
* @param {Object} response - API响应
* @returns {Object}
*/
processReportResponse(response) {
// 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
if (response.code === 200 || response.code === 0) {
const data = response.data || {};
return {
success: true,
messageTraceId: data.messageTraceId,
progressReportRecordIds: data.progressReportRecordIds || [],
message: response.message || '报工提交成功',
reportTime: new Date().toISOString()
};
} else {
throw new Error(response.message || `报工提交失败 (code: ${response.code})`);
}
}
/**
* 根据生产任务查询报工记录列表
* @param {Object} params - 查询参数
* @param {number|Array<number>} params.taskId - 生产任务ID或ID列表可选
* @param {number|Array<number>} params.workOrderId - 工单ID或ID列表可选
* @param {number} params.reportTimeFrom - 报工时间From(闭区间),时间戳(可选)
* @param {number} params.reportTimeTo - 报工时间To(开区间),时间戳(可选)
* @param {number} params.page - 请求页默认1可选
* @param {number} params.size - 每页大小默认200可选
* @param {Array<number>} params.processIdList - 工序ID列表可选
* @param {Array<number>} params.executorIdList - 可执行人ID列表可选
* @param {Array<number>} params.qcStatusList - 质量状态列表(可选)
* @param {Array<Object>} params.sorter - 排序条件列表(可选),列表顺序表示排序顺序
* @param {string} params.sorter[].field - 排序字段,如 "reportTime", "taskCode" 等
* @param {string} params.sorter[].order - 排序规律,默认 "asc""asc" 升序 "desc" 降序
* @returns {Promise<Object>} 返回报工记录列表(分页响应)
* @returns {boolean} returns.success - 是否成功
* @returns {Array<Object>} returns.records - 报工记录数组,每个记录包含以下字段:
* @returns {number} returns.records[].id - 报工记录详情id必填
* @returns {number} returns.records[].lineId - 物料行id必填
* @returns {number} returns.records[].reportRecordId - 报工记录id必填
* @returns {number} returns.records[].taskId - 生产任务Id必填
* @returns {string} returns.records[].taskCode - 生产任务编号(必填)
* @returns {number} returns.records[].workOrderId - 工单Id必填
* @returns {string} returns.records[].workOrderCode - 工单编号(必填)
* @returns {number} returns.records[].processId - 工序Id必填
* @returns {string} returns.records[].processCode - 工序编号(必填)
* @returns {string} returns.records[].processName - 工序名称(必填)
* @returns {number} returns.records[].mainMaterialId - 工单物料id必填
* @returns {string} returns.records[].mainMaterialCode - 工单物料编号(必填)
* @returns {string} returns.records[].mainMaterialName - 工单物料名称(必填)
* @returns {Object} returns.records[].materialInfo - 报工物料信息(必填),包含 baseInfo物料基本信息、attribute物料属性信息
* @returns {Object} returns.records[].reportBaseAmount - 报工数量1对应数量对象必填包含 amount数量和 unit单位信息
* @returns {Object} returns.records[].reportBaseAmountDisplay - 报工数量1对应数量对象-无科学技术法(必填),包含 amount、amountDisplay、unitCode、unitId、unitName
* @returns {number} returns.records[].reportTime - 报工时间,时间戳(必填)
* @returns {Object} returns.records[].reporter - 报工人员(必填),包含 id、code、name、username、avatarUrl
* @returns {Array<Object>} returns.records[].producers - 生产人员(必填),数组元素包含 id、code、name、username、avatarUrl
* @returns {Object} returns.records[].qcStatus - 质量状态(必填),包含 code枚举code和 message枚举信息
* @returns {Object} returns.records[].workHourUnit - 工时单位(必填),包含 code 和 message
* @returns {number} returns.records[].startTime - 报工开始时间,时间戳(可选)
* @returns {number} returns.records[].endTime - 报工结束时间,时间戳(可选)
* @returns {number} returns.records[].workHour - 工时(可选)
* @returns {string} returns.records[].batchNo - 批次号(可选)
* @returns {number} returns.records[].batchNoId - 批次号Id可选
* @returns {string} returns.records[].flowCardCode - 流转卡编号(可选)
* @returns {string} returns.records[].qrCode - 标示码(可选)
* @returns {string} returns.records[].reportRecordCode - 报工记录编号(可选)
* @returns {string} returns.records[].remark - 备注(可选)
* @returns {number} returns.records[].autoWarehousingFlag - 是否自动入库标识 0-否 1-是(可选)
* @returns {Array<Object>} returns.records[].equipments - 设备列表(可选),数组元素包含 id、code、name
* @returns {Array<Object>} returns.records[].executors - 计划执行人(可选),数组元素包含 id、code、name、username、avatarUrl
* @returns {Array<Object>} returns.records[].produceDepartments - 生产部门列表(可选),数组元素包含 id、code、name
* @returns {Array<Object>} returns.records[].customFields - 自定义字段(可选),数组元素包含 fieldCode、fieldName、fieldValue 等
* @returns {Array<Object>} returns.records[].qcDefectReasons - 不良原因(可选),数组元素包含 id、name
* @returns {Array<string>} returns.records[].salesOrderCode - 订单编号(可选)
* @returns {Array<number>} returns.records[].pictureIds - 图片ids可选
* @returns {Object} returns.records[].reportAuxBaseAmount1 - 报工数量2对应数量对象可选包含 amount 和 unit
* @returns {number} returns.records[].updatedAt - 更新时间,时间戳(可选)
* @returns {number} returns.total - 总条数
* @returns {number} returns.page - 当前页
* @returns {number} returns.size - 每页大小
* @returns {string} returns.message - 返回消息
*/
async getReportRecordsByTask(params) {
try {
const {
taskId,
workOrderId,
reportTimeFrom,
reportTimeTo,
page = 1,
size = 200,
processIdList,
executorIdList,
qcStatusList,
sorter
} = params;
// 构建请求参数
const requestParams = {
page: parseInt(page),
size: parseInt(size)
};
// 添加排序参数(如果未提供,默认按报工时间降序)
if (sorter && Array.isArray(sorter) && sorter.length > 0) {
// 使用用户提供的排序条件
requestParams.sorter = sorter.map(item => ({
field: item.field,
order: item.order
}));
} else {
// 默认按报工时间降序排序
requestParams.sorter = [{
field: 'reportTime',
order: 'desc'
}];
}
// 添加任务ID列表支持单个或数组
if (taskId) {
requestParams.taskIds = Array.isArray(taskId) ? taskId.map(id => parseInt(id)) : [parseInt(taskId)];
}
// 添加工单ID列表支持单个或数组
if (workOrderId) {
requestParams.workOrderIdList = Array.isArray(workOrderId)
? workOrderId.map(id => parseInt(id))
: [parseInt(workOrderId)];
}
// 添加报工时间范围
if (reportTimeFrom) {
requestParams.reportTimeFrom = parseInt(reportTimeFrom);
}
if (reportTimeTo) {
requestParams.reportTimeTo = parseInt(reportTimeTo);
}
// 添加其他可选参数
if (processIdList && processIdList.length > 0) {
requestParams.processIdList = processIdList.map(id => parseInt(id));
}
if (executorIdList && executorIdList.length > 0) {
requestParams.executorIdList = executorIdList.map(id => parseInt(id));
}
if (qcStatusList && qcStatusList.length > 0) {
requestParams.qcStatusList = qcStatusList.map(status => parseInt(status));
}
// 验证至少有一个查询条件
if (!requestParams.taskIds && !requestParams.workOrderIdList) {
throw new Error('必须提供 taskId 或 workOrderId 中的至少一个');
}
console.log('查询报工记录,参数:', requestParams);
// 调用报工记录列表接口
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list', requestParams);
// 处理响应
if (response.code !== 200 && response.code !== 0) {
throw new Error(response.message || `查询报工记录失败 (code: ${response.code})`);
}
// 响应格式:{ code, message, data: { list, page, total } }
// data.list 是报工记录数组,每个记录包含以下主要字段:
// - 必填字段id, lineId, reportRecordId, taskId, taskCode, workOrderId, workOrderCode,
// processId, processCode, processName, mainMaterialId, mainMaterialCode, mainMaterialName,
// materialInfo, reportBaseAmount, reportBaseAmountDisplay, reportTime, reporter, producers, qcStatus, workHourUnit
// - 可选字段startTime, endTime, workHour, batchNo, flowCardCode, qrCode, remark,
// equipments, executors, customFields, qcDefectReasons 等
const data = response.data || {};
const reportRecords = data.list || [];
const total = data.total || 0;
const currentPage = data.page || page;
console.log(`查询到 ${reportRecords.length} 条报工记录,共 ${total}`);
return {
success: true,
records: reportRecords, // 报工记录数组,详细字段说明见函数 JSDoc 注释
total: total,
page: currentPage,
size: size,
message: response.message || '查询成功'
};
} catch (error) {
console.error('查询报工记录失败:', error);
throw error;
}
}
}
// 创建全局报工服务实例
const reportService = new ReportService();

View File

@@ -0,0 +1,781 @@
/**
* 核心服务
* 包含API服务和认证服务
*/
// ==================== API服务 ====================
/**
* API服务基础类
* 封装HTTP请求自动处理认证和错误
*/
class ApiService {
constructor() {
this.baseURL = this.getBaseURL();
this.defaultHeaders = {
'Content-Type': 'application/json'
};
}
/**
* 获取API基础URL
* @returns {string}
*/
getBaseURL() {
// 根据环境自动判断API地址
const hostname = window.location.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1') {
// 开发环境使用黑湖API服务器
return 'https://v3-feature.blacklake.cn/api';
} else {
// 生产环境使用黑湖API服务器
return 'https://v3-feature.blacklake.cn/api';
}
}
/**
* 构建完整的请求URL
* @param {string} endpoint - API端点
* @returns {string}
*/
buildURL(endpoint) {
// 如果endpoint已经是完整URL直接返回
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
return endpoint;
}
// 确保endpoint以/开头
if (!endpoint.startsWith('/')) {
endpoint = '/' + endpoint;
}
return this.baseURL + endpoint;
}
/**
* 获取请求头
* @param {Object} customHeaders - 自定义请求头
* @returns {Object}
*/
getHeaders(customHeaders = {}) {
const headers = { ...this.defaultHeaders, ...customHeaders };
// 添加认证头
if (authService && authService.isAuthenticated()) {
Object.assign(headers, authService.getAuthHeaders());
}
return headers;
}
/**
* 处理响应
* @param {Response} response - fetch响应对象
* @returns {Promise<any>}
*/
async handleResponse(response) {
// 解析响应数据
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
// 处理HTTP状态码错误
if (!response.ok) {
let errorMessage = `请求失败: ${response.status}`;
if (typeof data === 'object' && data.message) {
errorMessage = data.message;
} else if (typeof data === 'string') {
errorMessage = data;
}
// 处理认证错误
if (response.status === 401 || response.status === 403) {
if (authService) {
const canRetry = await authService.handleAuthError(response);
if (canRetry) {
throw new Error('AUTH_RETRY'); // 特殊错误,表示可以重试
}
}
}
throw new Error(errorMessage);
}
// 检查业务错误码即使HTTP状态码是200业务也可能返回错误
if (typeof data === 'object' && data.code !== undefined) {
// 业务错误码非200且非0表示错误
if (data.code !== 200 && data.code !== 0) {
// 处理认证相关的业务错误
if (data.subCode && (
data.subCode.includes('TOKEN') ||
data.subCode.includes('AUTH') ||
data.subCode.includes('APP_KEY') ||
data.subCode.includes('NOT_EXIST')
)) {
console.warn('业务认证错误:', data.subCode, data.message);
if (authService) {
const canRetry = await authService.handleAuthError(response);
if (canRetry) {
throw new Error('AUTH_RETRY');
}
}
}
throw new Error(data.message || `业务错误 (code: ${data.code}, subCode: ${data.subCode})`);
}
}
return data;
}
/**
* 发送HTTP请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async request(endpoint, options = {}) {
const url = this.buildURL(endpoint);
const config = {
method: 'GET',
headers: this.getHeaders(options.headers),
...options
};
// 如果有body且不是FormData转换为JSON字符串
if (config.body && !(config.body instanceof FormData)) {
if (typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
}
let retryCount = 0;
const maxRetries = 2;
while (retryCount <= maxRetries) {
try {
console.log(`API请求: ${config.method} ${url}`, config.body ? JSON.parse(config.body) : '');
const response = await fetch(url, config);
const result = await this.handleResponse(response);
console.log(`API响应: ${config.method} ${url}`, result);
return result;
} catch (error) {
console.error(`API错误: ${config.method} ${url}`, error);
// 如果是认证错误且可以重试
if (error.message === 'AUTH_RETRY' && retryCount < maxRetries) {
retryCount++;
// 更新请求头中的认证信息
config.headers = this.getHeaders(options.headers);
continue;
}
// 网络错误重试
if (error.name === 'TypeError' && error.message.includes('Failed to fetch') && retryCount < maxRetries) {
retryCount++;
await this.delay(1000 * retryCount); // 延迟重试
continue;
}
throw error;
}
}
}
/**
* GET请求
* @param {string} endpoint - API端点
* @param {Object} params - 查询参数
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async get(endpoint, params = {}, options = {}) {
// 构建查询字符串
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, {
method: 'GET',
...options
});
}
/**
* POST请求
* @param {string} endpoint - API端点
* @param {any} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async post(endpoint, data = null, options = {}) {
return this.request(endpoint, {
method: 'POST',
body: data,
...options
});
}
/**
* PUT请求
* @param {string} endpoint - API端点
* @param {any} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async put(endpoint, data = null, options = {}) {
return this.request(endpoint, {
method: 'PUT',
body: data,
...options
});
}
/**
* DELETE请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async delete(endpoint, options = {}) {
return this.request(endpoint, {
method: 'DELETE',
...options
});
}
/**
* 上传文件
* @param {string} endpoint - API端点
* @param {File|FileList} files - 文件对象
* @param {Object} additionalData - 额外数据
* @param {Function} onProgress - 进度回调
* @returns {Promise<any>}
*/
async upload(endpoint, files, additionalData = {}, onProgress = null) {
const formData = new FormData();
// 添加文件
if (files instanceof FileList) {
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
} else if (files instanceof File) {
formData.append('file', files);
}
// 添加额外数据
for (const key in additionalData) {
formData.append(key, additionalData[key]);
}
const options = {
method: 'POST',
body: formData,
headers: {} // 不设置Content-Type让浏览器自动设置
};
// 如果需要进度回调使用XMLHttpRequest
if (onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const result = JSON.parse(xhr.responseText);
resolve(result);
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error(`上传失败: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('上传失败'));
});
xhr.open('POST', this.buildURL(endpoint));
// 添加认证头
const headers = this.getHeaders();
for (const key in headers) {
if (key !== 'Content-Type') { // 不设置Content-Type
xhr.setRequestHeader(key, headers[key]);
}
}
xhr.send(formData);
});
}
return this.request(endpoint, options);
}
/**
* 延迟函数
* @param {number} ms - 延迟毫秒数
* @returns {Promise}
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 批量请求
* @param {Array} requests - 请求数组,每个元素包含 {method, endpoint, data}
* @returns {Promise<Array>}
*/
async batch(requests) {
const promises = requests.map(req => {
const { method = 'GET', endpoint, data, options = {} } = req;
switch (method.toUpperCase()) {
case 'GET':
return this.get(endpoint, data, options);
case 'POST':
return this.post(endpoint, data, options);
case 'PUT':
return this.put(endpoint, data, options);
case 'DELETE':
return this.delete(endpoint, options);
default:
return Promise.reject(new Error(`不支持的请求方法: ${method}`));
}
});
return Promise.allSettled(promises);
}
/**
* 设置基础URL
* @param {string} baseURL - 新的基础URL
*/
setBaseURL(baseURL) {
this.baseURL = baseURL;
}
/**
* 设置默认请求头
* @param {Object} headers - 请求头对象
*/
setDefaultHeaders(headers) {
this.defaultHeaders = { ...this.defaultHeaders, ...headers };
}
}
// 创建全局API服务实例
const apiService = new ApiService();
// ==================== 认证服务 ====================
/**
* 默认配置
* 修改登录账号时,需要同时更新 appKey
*/
const AUTH_CONFIG = {
// 登录信息
login: {
type: 1,
username: "cyy",
code: "67768820",
password: "794db7135639d5b59cd7e53b325f36a8f24fb906e95e403c49e89b99046654fae36a940c1e8496b75b9e69d4f79022c9123aa0d8cd665e2ea9cf584242a702664e77fdd2399c452bd03a174a3edbb41b86a406851b4da8b11b8faa7044925e3e9bffd4fd5afb14c70f592a2114ce5f45cf567e2e1f0d8688ef345ca28c2687c5"
},
// AppKey用于获取user-access token
appKey: "cli_1764100796489835"
};
/**
* 认证服务
* 处理token获取、存储和管理
*/
class AuthService {
constructor() {
this.token = null; // 最终用于业务接口的user-access token
this.tokenKey = 'work_report_user_access_token';
this.init();
}
/**
* 初始化认证服务
*/
async init() {
try {
// 1. 先查看URL里是否存在code
const code = getUrlParameter('code');
if (code) {
// 如果存在code直接使用code获取user-access token
console.log('从URL参数获取到code:', code);
await this.getUserAccessTokenByCode(code);
} else {
// 如果不存在code走登录流程
console.log('URL参数中没有code走登录流程...');
// 2.1 先登录获取token
console.log('开始登录...');
const loginResult = await this.loginAndGetToken();
if (!loginResult || !loginResult.token) {
throw new Error('登录失败未获取到token');
}
console.log('登录成功获取到登录token');
// 2.2 使用登录token获取code
console.log('使用登录token获取code...');
const codeFromApi = await this.getCodeByLoginToken(loginResult.token);
if (!codeFromApi) {
throw new Error('获取code失败');
}
console.log('成功获取code:', codeFromApi);
// 2.3 使用code获取user-access token
await this.getUserAccessTokenByCode(codeFromApi);
}
console.log('认证初始化完成user-access token已获取');
} catch (error) {
console.error('认证初始化失败:', error);
showToast('认证初始化失败,请刷新页面重试', 'error');
throw error; // 抛出错误,让调用方知道初始化失败
}
}
/**
* 使用code获取user-access token
* @param {string} code - code参数
*/
async getUserAccessTokenByCode(code) {
try {
if (!code) {
throw new Error('code参数不能为空');
}
console.log('使用code获取user-access token...');
const requestBody = {
code: code,
appKey: AUTH_CONFIG.appKey
};
const response = await fetch('https://v3-feature.blacklake.cn/api/openapi/domain/api/v1/access_token/_get_user_token_for_customized', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`获取user-access token失败: ${response.status}`);
}
const data = await response.json();
console.log('获取user-access token响应:', data);
// 根据实际响应格式提取token
if (data.code === 200 || data.code === 0) {
const tokenData = data.data || data.result || data;
const userAccessToken = tokenData.token ||
tokenData.userAccessToken ||
tokenData.accessToken ||
data.token ||
data.userAccessToken ||
data.accessToken;
if (userAccessToken) {
this.token = userAccessToken;
this.saveToken(userAccessToken);
console.log('成功获取user-access token');
} else {
throw new Error('响应中没有找到user-access token');
}
} else {
throw new Error(data.message || `获取user-access token失败 (code: ${data.code})`);
}
} catch (error) {
console.error('获取user-access token失败:', error);
throw error;
}
}
/**
* 使用登录token获取code
* @param {string} loginToken - 登录获取的token
* @returns {Promise<string>} 返回code
*/
async getCodeByLoginToken(loginToken) {
try {
if (!loginToken) {
throw new Error('登录token不能为空');
}
console.log('使用登录token获取code...');
const response = await fetch('https://v3-feature.blacklake.cn/api/openapiadmin/domain/web/v1/access_token/_code', {
method: 'POST',
headers: {
'X-AUTH': loginToken,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`获取code失败: ${response.status}`);
}
const data = await response.json();
console.log('获取code响应:', data);
// 根据实际响应格式提取code
if (data.code === 200 || data.code === 0) {
const codeData = data.data || data.result || data;
const code = codeData.code ||
codeData.accessToken ||
data.code ||
data.accessToken;
if (code) {
console.log('成功获取code');
return code;
} else {
throw new Error('响应中没有找到code');
}
} else {
throw new Error(data.message || `获取code失败 (code: ${data.code})`);
}
} catch (error) {
console.error('获取code失败:', error);
throw error;
}
}
/**
* 登录并获取token
*/
async loginAndGetToken() {
try {
const loginData = {
type: AUTH_CONFIG.login.type,
username: AUTH_CONFIG.login.username,
code: AUTH_CONFIG.login.code,
password: AUTH_CONFIG.login.password
};
console.log('尝试登录获取token...');
const response = await fetch('https://v3-feature.blacklake.cn/api/user/domain/web/v1/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(loginData)
});
if (!response.ok) {
throw new Error(`登录失败: ${response.status}`);
}
const data = await response.json();
console.log('登录响应完整数据:', JSON.stringify(data, null, 2));
console.log('登录响应状态码:', response.status);
console.log('登录响应headers:', Object.fromEntries(response.headers.entries()));
// 检查响应是否成功(支持多种响应格式)
const isSuccess = data.success === true ||
data.code === 0 ||
data.code === 200 ||
(data.code === undefined && response.ok);
console.log('登录响应是否成功:', isSuccess, 'code:', data.code, 'success:', data.success);
if (isSuccess) {
// 尝试从不同位置提取token这是登录token用于后续获取code
const tokenData = data.data || data.result || data;
const loginToken = tokenData?.token ||
tokenData?.access_token ||
tokenData?.accessToken ||
data.token ||
data.access_token ||
data.accessToken;
if (loginToken) {
console.log('登录成功获取到登录token');
return { token: loginToken };
} else {
const errorMsg = '登录响应中没有找到token';
console.error(errorMsg, data);
throw new Error(errorMsg);
}
} else {
// 登录失败,抛出错误
const errorMsg = data.message || data.msg || `登录失败 (code: ${data.code})`;
console.error('登录失败:', errorMsg, data);
throw new Error(errorMsg);
}
} catch (error) {
console.error('登录接口调用失败:', error);
throw error; // 重新抛出错误
}
}
/**
* 获取当前token
* @returns {string|null}
*/
getToken() {
return this.token;
}
/**
* 检查是否已认证
* @returns {boolean}
*/
isAuthenticated() {
return !!this.token;
}
/**
* 保存token到本地存储
* @param {string} token - 要保存的token
*/
saveToken(token) {
try {
const tokenData = {
token: token,
timestamp: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24小时后过期
};
localStorage.setItem(this.tokenKey, JSON.stringify(tokenData));
} catch (error) {
console.error('保存token失败:', error);
}
}
/**
* 从本地存储获取token
* @returns {string|null}
*/
getStoredToken() {
try {
const stored = localStorage.getItem(this.tokenKey);
if (!stored) {
return null;
}
const tokenData = JSON.parse(stored);
// 检查是否过期
if (tokenData.expiresAt && Date.now() > tokenData.expiresAt) {
console.log('存储的token已过期');
this.clearToken();
return null;
}
return tokenData.token;
} catch (error) {
console.error('获取存储token失败:', error);
return null;
}
}
/**
* 清除token
*/
clearToken() {
this.token = null;
try {
localStorage.removeItem(this.tokenKey);
} catch (error) {
console.error('清除token失败:', error);
}
}
/**
* 刷新token重新执行完整的认证流程
*/
async refreshToken() {
console.log('刷新token重新执行认证流程...');
this.clearToken();
await this.init();
}
/**
* 获取认证头部
* 根据接口文档,所有业务接口都需要使用 X-AUTH 头部,值为 user-access token
* @returns {Object}
*/
getAuthHeaders() {
const headers = {};
if (this.token) {
// 使用最终的user-access token
headers['X-AUTH'] = this.token;
}
return headers;
}
/**
* 处理认证错误
* @param {Response} response - HTTP响应对象
*/
async handleAuthError(response) {
if (response.status === 401 || response.status === 403) {
console.log('认证失败尝试刷新token');
showToast('认证已过期,正在重新获取...', 'info');
try {
await this.refreshToken();
return true; // 表示可以重试请求
} catch (error) {
console.error('刷新token失败:', error);
showToast('认证失败,请刷新页面', 'error');
return false;
}
}
return false;
}
/**
* 登出
*/
logout() {
this.clearToken();
showToast('已退出登录', 'info');
// 可以重定向到登录页面或刷新页面
setTimeout(() => {
window.location.reload();
}, 1000);
}
}
// 创建全局认证服务实例
const authService = new AuthService();

View File

@@ -0,0 +1,419 @@
<!-- 智筑报工页面 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>智筑报工页面</title>
<style>
/* CSS Reset & Base */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
-webkit-tap-highlight-color: transparent;
line-height: 1.5;
}
/* Utility Classes */
.flex { display: flex; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.flex-col { flex-direction: column; }
.flex-shrink-0 { flex-shrink: 0; }
.space-x-2 > * + * { margin-left: 0.5rem; }
.space-y-1 > * + * { margin-top: 0.25rem; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-3 > * + * { margin-top: 0.75rem; }
.hidden { display: none; }
.cursor-pointer { cursor: pointer; }
.w-full { width: 100%; }
.max-w-md { max-width: 28rem; margin-left: auto; margin-right: auto; }
.max-h-60 { max-height: 15rem; }
.overflow-y-auto { overflow-y: auto; }
.text-center { text-align: center; }
.text-right { text-align: right; }
/* Spacing */
.p-1 { padding: 0.25rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-5 { padding: 1.25rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-3\.5 { padding-top: 0.875rem; padding-bottom: 0.875rem; }
.pb-4 { padding-bottom: 1rem; }
.pb-10 { padding-bottom: 2.5rem; }
.pl-1 { padding-left: 0.25rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-0\.5 { margin-top: 0.125rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
/* Typography */
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-base { font-size: 1rem; }
.text-lg { font-size: 1.125rem; }
.font-medium { font-weight: 500; }
.font-bold { font-weight: 700; }
.font-mono { font-family: monospace; }
.leading-none { line-height: 1; }
/* Colors */
.bg-white { background-color: #ffffff; }
.bg-blue-600 { background-color: #2563eb; }
.bg-gray-50 { background-color: #f9fafb; }
.bg-gray-800 { background-color: #1f2937; }
.bg-black { background-color: rgba(0, 0, 0, 0.5); }
.text-white { color: #ffffff; }
.text-gray-400 { color: #9ca3af; }
.text-gray-500 { color: #6b7280; }
.text-gray-600 { color: #4b5563; }
.text-gray-800 { color: #1f2937; }
.text-blue-600 { color: #2563eb; }
.text-green-400 { color: #4ade80; }
.text-red-400 { color: #f87171; }
/* Borders */
.border { border-width: 1px; }
.border-b { border-bottom-width: 1px; }
.border-l-4 { border-left-width: 4px; }
.border-gray-100 { border-color: #f3f4f6; }
.border-gray-200 { border-color: #e5e7eb; }
.border-blue-300 { border-color: #93c5fd; }
.border-blue-600 { border-color: #2563eb; }
.border-transparent { border-color: transparent; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-2xl { border-radius: 1rem; }
.rounded-t-2xl { border-top-left-radius: 1rem; border-top-right-radius: 1rem; }
.rounded-full { border-radius: 9999px; }
/* Shadows */
.card-shadow { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); }
.shadow-md { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
.shadow-lg { box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); }
/* Positioning */
.fixed { position: fixed; }
.sticky { position: sticky; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.top-0 { top: 0; }
.bottom-10 { bottom: 2.5rem; }
.left-1\/2 { left: 50%; }
.z-10 { z-index: 10; }
.z-50 { z-index: 50; }
/* Transforms */
.transform { transform: translate(var(--tw-translate-x, 0), var(--tw-translate-y, 0)); }
.-translate-x-1\/2 { transform: translateX(-50%); }
.translate-y-full { transform: translateY(100%); }
.translate-y-0 { transform: translateY(0); }
.translate-y-10 { transform: translateY(2.5rem); }
/* Transitions */
.transition-all { transition: all 0.3s; }
.transition-opacity { transition: opacity 0.3s; }
.transition-transform { transition: transform 0.3s; }
.duration-300 { transition-duration: 300ms; }
/* Opacity */
.opacity-0 { opacity: 0; }
.pointer-events-none { pointer-events: none; }
/* Hover & Active */
.bg-blue-600:hover { background-color: #1d4ed8; }
.bg-gray-50:active, .bg-gray-50.active { background-color: #eff6ff; }
.border-transparent:hover { border-color: #93c5fd; }
.btn-active:active {
transform: scale(0.98);
opacity: 0.9;
}
/* Icons (simple SVG replacements) */
.icon {
display: inline-block;
width: 1.25rem;
height: 1.25rem;
vertical-align: middle;
}
.icon-chevron-right::after { content: ''; font-size: 1.5rem; }
.icon-check::after { content: '✓'; }
.icon-x::after { content: '×'; font-size: 1.5rem; }
.icon-clipboard::after { content: '📋'; }
.icon-alert::after { content: '⚠'; }
/* Responsive - Mobile First */
@media (min-width: 640px) {
.sm\:items-center { align-items: center; }
.sm\:rounded-xl { border-radius: 0.75rem; }
.sm\:translate-y-0 { transform: translateY(0); }
}
</style>
</head>
<body class="pb-10">
<!-- 顶部导航/标题 -->
<header class="bg-white sticky top-0 z-10 border-b border-gray-200 px-4 py-3 text-center">
<h1 class="text-lg font-bold text-gray-800">智筑报工页面</h1>
</header>
<main class="p-4 max-w-md mx-auto">
<!-- 上部:工单信息区域 -->
<div class="bg-white rounded-xl card-shadow p-5 mb-6 border border-gray-200" style="border-width: 2px;">
<!-- 生产工单选择行 -->
<div onclick="openOrderSelector()" class="flex items-center justify-between border-b border-gray-100 pb-4 mb-4 cursor-pointer btn-active">
<div class="flex flex-col">
<span class="text-gray-800 font-bold text-base mb-1">生产工单</span>
<!-- 模拟未选择或已选择的状态 -->
<span id="selectedOrderText" class="text-gray-400 text-sm">请选择生产工单</span>
</div>
<span class="icon icon-chevron-right text-gray-400"></span>
</div>
<!-- 物料信息 -->
<div class="space-y-3">
<div class="flex items-start">
<span class="text-gray-500 text-sm w-24 flex-shrink-0">加工物料名称:</span>
<span id="materialName" class="text-gray-800 text-sm font-medium">--</span>
</div>
<div class="flex items-start">
<span class="text-gray-500 text-sm w-24 flex-shrink-0">加工物料编号:</span>
<span id="materialCode" class="text-gray-800 text-sm font-medium">--</span>
</div>
<!-- 隐藏的任务ID字段模拟查询到的任务 -->
<div id="taskIdContainer" class="hidden flex items-start">
<span class="text-gray-500 text-sm w-24 flex-shrink-0">关联任务ID</span>
<span id="taskIdDisplay" class="text-gray-500 text-xs font-mono mt-0.5">--</span>
</div>
</div>
</div>
<!-- 中部:报工按钮 -->
<button onclick="handleAutoReport()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3.5 px-4 rounded-xl shadow-md btn-active flex items-center justify-center space-x-2 mb-8" style="border: 2px solid #1d4ed8;">
<span class="icon icon-clipboard"></span>
<span>点 此 报 工 (自动 10 个)</span>
</button>
<!-- 下部:报工记录 -->
<div>
<h2 class="text-lg font-bold text-gray-800 mb-4 pl-1 border-l-4 border-blue-600 leading-none">报工记录</h2>
<div id="recordList" class="space-y-3">
<!-- 记录项 1 (草图数据) -->
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center" style="border-width: 2px;">
<div class="space-y-1">
<div class="text-gray-500 text-sm">报工数量</div>
<div class="text-gray-500 text-sm">报工时间</div>
</div>
<div class="text-right space-y-1">
<div class="text-gray-800 font-bold text-lg">10 个</div>
<div class="text-gray-600 text-sm">2025.11.25 16:27</div>
</div>
</div>
<!-- 记录项 2 (草图数据) -->
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center" style="border-width: 2px;">
<div class="space-y-1">
<div class="text-gray-500 text-sm">报工数量</div>
<div class="text-gray-500 text-sm">报工时间</div>
</div>
<div class="text-right space-y-1">
<div class="text-gray-800 font-bold text-lg">10 个</div>
<div class="text-gray-600 text-sm">2025.11.25 15:20</div>
</div>
</div>
<!-- 记录项 3 (草图数据) -->
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center" style="border-width: 2px;">
<div class="space-y-1">
<div class="text-gray-500 text-sm">报工数量</div>
<div class="text-gray-500 text-sm">报工时间</div>
</div>
<div class="text-right space-y-1">
<div class="text-gray-800 font-bold text-lg">10 个</div>
<div class="text-gray-600 text-sm">2025.11.25 14:11</div>
</div>
</div>
</div>
<!-- 到底提示 -->
<div class="text-center text-gray-400 text-xs mt-6">
- 暂无更多记录 -
</div>
</div>
</main>
<!-- 模拟弹窗:选择工单 -->
<div id="orderModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-end sm:items-center justify-center transition-opacity duration-300">
<div class="bg-white w-full max-w-md rounded-t-2xl sm:rounded-xl p-5 transform transition-transform duration-300 translate-y-full sm:translate-y-0 shadow-lg" id="orderModalContent" style="border: 2px solid #e5e7eb;">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold">选择生产工单</h3>
<button onclick="closeOrderModal()" class="text-gray-500 p-1"><span class="icon icon-x"></span></button>
</div>
<div class="space-y-2 max-h-60 overflow-y-auto">
<div onclick="selectOrder('MO-20251125-001', '高强度螺栓 M24', 'MT-8823-A', 'TASK-001-A')" class="p-3 bg-gray-50 rounded-lg active:bg-blue-50 cursor-pointer border border-transparent hover:border-blue-300" style="border-width: 2px;">
<div class="font-bold text-gray-800">MO-20251125-001</div>
<div class="text-xs text-gray-500 mt-1">物料:高强度螺栓 M24</div>
</div>
<div onclick="selectOrder('MO-20251125-002', '精密齿轮 Z-50', 'MT-5501-B', 'TASK-002-B')" class="p-3 bg-gray-50 rounded-lg active:bg-blue-50 cursor-pointer border border-transparent hover:border-blue-300" style="border-width: 2px;">
<div class="font-bold text-gray-800">MO-20251125-002</div>
<div class="text-xs text-gray-500 mt-1">物料:精密齿轮 Z-50</div>
</div>
<div onclick="selectOrder('MO-20251125-003', '传动轴承 AX-99', 'MT-1209-C', 'TASK-003-C')" class="p-3 bg-gray-50 rounded-lg active:bg-blue-50 cursor-pointer border border-transparent hover:border-blue-300" style="border-width: 2px;">
<div class="font-bold text-gray-800">MO-20251125-003</div>
<div class="text-xs text-gray-500 mt-1">物料:传动轴承 AX-99</div>
</div>
</div>
</div>
</div>
<!-- Toast 提示组件 (修改了结构以支持动态图标) -->
<div id="toast" class="fixed bottom-10 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-6 py-3 rounded-full shadow-lg z-50 flex items-center space-x-2 transition-all duration-300 opacity-0 pointer-events-none translate-y-10" style="border: 2px solid #374151;">
<!-- 图标容器 -->
<div id="toast-icon-container">
<span class="icon icon-check text-green-400"></span>
</div>
<span id="toast-text" class="font-medium text-sm">报工成功</span>
</div>
<script>
// 状态变量
let currentOrder = null;
// --- 工单选择逻辑 ---
const orderModal = document.getElementById('orderModal');
const orderModalContent = document.getElementById('orderModalContent');
function openOrderSelector() {
orderModal.classList.remove('hidden');
setTimeout(() => {
orderModalContent.classList.remove('translate-y-full');
}, 10);
}
function closeOrderModal() {
orderModalContent.classList.add('translate-y-full');
setTimeout(() => {
orderModal.classList.add('hidden');
}, 300);
}
// 选择工单后,模拟查询到任务并展示物料信息
function selectOrder(orderNo, materialName, materialCode, taskId) {
currentOrder = { orderNo, materialName, materialCode, taskId };
// 更新 UI
document.getElementById('selectedOrderText').innerText = orderNo;
document.getElementById('selectedOrderText').classList.replace('text-gray-400', 'text-blue-600');
document.getElementById('selectedOrderText').classList.add('font-bold');
document.getElementById('materialName').innerText = materialName;
document.getElementById('materialCode').innerText = materialCode;
// 显示关联的任务ID
const taskContainer = document.getElementById('taskIdContainer');
const taskDisplay = document.getElementById('taskIdDisplay');
taskContainer.classList.remove('hidden');
taskDisplay.innerText = taskId;
closeOrderModal();
}
// --- 自动报工逻辑 ---
function handleAutoReport() {
// 1. 校验是否选择了工单
if (!currentOrder) {
showToast("请先选择生产工单", "error");
return;
}
// 2. 模拟自动报工过程
const autoAmount = 10;
const now = new Date();
const timeString = `${now.getFullYear()}.${(now.getMonth()+1).toString().padStart(2, '0')}.${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
// 3. 创建记录 DOM
const newRecordHtml = `
<div class="bg-white rounded-lg p-4 border border-gray-200 card-shadow flex justify-between items-center animate-pulse-once">
<div class="space-y-1">
<div class="text-gray-500 text-sm">报工数量</div>
<div class="text-gray-500 text-sm">报工时间</div>
</div>
<div class="text-right space-y-1">
<div class="text-gray-800 font-bold text-lg text-blue-600">${autoAmount} 个</div>
<div class="text-gray-600 text-sm">${timeString}</div>
</div>
</div>
`;
// 4. 插入到列表最前面
const list = document.getElementById('recordList');
const tempDiv = document.createElement('div');
tempDiv.innerHTML = newRecordHtml;
const newElement = tempDiv.firstElementChild;
newElement.style.transition = "all 0.5s ease";
newElement.style.opacity = "0";
newElement.style.transform = "translateY(-10px)";
list.insertBefore(newElement, list.firstChild);
setTimeout(() => {
newElement.style.opacity = "1";
newElement.style.transform = "translateY(0)";
}, 50);
// 5. 显示成功提示
showToast("报工成功");
}
// --- Toast 提示逻辑 (修复了 Null 报错问题) ---
let toastTimeout;
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
const toastIconContainer = document.getElementById('toast-icon-container'); // 使用容器获取
const toastText = document.getElementById('toast-text');
// 设置内容
toastText.innerText = message;
// 根据类型重置图标 HTML
if (type === 'error') {
toastIconContainer.innerHTML = '<span class="icon icon-alert text-red-400"></span>';
} else {
toastIconContainer.innerHTML = '<span class="icon icon-check text-green-400"></span>';
}
// 显示
toast.classList.remove('opacity-0', 'translate-y-10');
// 清除上一次的定时器
if (toastTimeout) clearTimeout(toastTimeout);
// 2秒后自动隐藏
toastTimeout = setTimeout(() => {
toast.classList.add('opacity-0', 'translate-y-10');
}, 2000);
}
// 点击遮罩层关闭弹窗
window.onclick = function(event) {
if (event.target == orderModal) {
closeOrderModal();
}
}
</script>
</body>
</html>