Compare commits
9
Commits
316d054664
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4003625fb1 | ||
|
|
25ed4e2da8 | ||
|
|
28b7b76815 | ||
|
|
5f6deee796 | ||
|
|
a2f26c8994 | ||
|
|
be55bd44fe | ||
|
|
28b8dc3773 | ||
|
|
91c43e703f | ||
|
|
06f99c5096 |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/erpLoginConfirm/erpLoginConfirm",
|
||||
"pages/clock/clock",
|
||||
"pages/cleaningReport/cleaningReport",
|
||||
"pages/reportForm/reportForm",
|
||||
|
||||
@@ -709,11 +709,32 @@ scroll-view.page {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.over-limit-watermark {
|
||||
position: absolute;
|
||||
right: 28rpx;
|
||||
top: 18rpx;
|
||||
z-index: 4;
|
||||
padding: 8rpx 18rpx;
|
||||
border: 2rpx solid rgba(197, 34, 31, 0.72);
|
||||
border-radius: 12rpx;
|
||||
color: rgba(148, 27, 23, 0.92);
|
||||
background: rgba(255, 238, 238, 0.94);
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
transform: rotate(10deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lower-watermark,
|
||||
.misc-watermark.lower-watermark,
|
||||
.cleaning-watermark.lower-watermark,
|
||||
.multi-person-watermark.lower-watermark,
|
||||
.continuous-die-watermark.lower-watermark {
|
||||
.continuous-die-watermark.lower-watermark,
|
||||
.timeout-watermark.lower-watermark,
|
||||
.voided-watermark.lower-watermark,
|
||||
.modified-watermark.lower-watermark,
|
||||
.over-limit-watermark.lower-watermark {
|
||||
top: 78rpx;
|
||||
}
|
||||
|
||||
@@ -721,10 +742,50 @@ scroll-view.page {
|
||||
.misc-watermark.third-watermark,
|
||||
.cleaning-watermark.third-watermark,
|
||||
.multi-person-watermark.third-watermark,
|
||||
.continuous-die-watermark.third-watermark {
|
||||
.continuous-die-watermark.third-watermark,
|
||||
.timeout-watermark.third-watermark,
|
||||
.voided-watermark.third-watermark,
|
||||
.modified-watermark.third-watermark,
|
||||
.over-limit-watermark.third-watermark {
|
||||
top: 138rpx;
|
||||
}
|
||||
|
||||
.fourth-watermark,
|
||||
.misc-watermark.fourth-watermark,
|
||||
.cleaning-watermark.fourth-watermark,
|
||||
.multi-person-watermark.fourth-watermark,
|
||||
.continuous-die-watermark.fourth-watermark,
|
||||
.timeout-watermark.fourth-watermark,
|
||||
.voided-watermark.fourth-watermark,
|
||||
.modified-watermark.fourth-watermark,
|
||||
.over-limit-watermark.fourth-watermark {
|
||||
top: 198rpx;
|
||||
}
|
||||
|
||||
.fifth-watermark,
|
||||
.misc-watermark.fifth-watermark,
|
||||
.cleaning-watermark.fifth-watermark,
|
||||
.multi-person-watermark.fifth-watermark,
|
||||
.continuous-die-watermark.fifth-watermark,
|
||||
.timeout-watermark.fifth-watermark,
|
||||
.voided-watermark.fifth-watermark,
|
||||
.modified-watermark.fifth-watermark,
|
||||
.over-limit-watermark.fifth-watermark {
|
||||
top: 258rpx;
|
||||
}
|
||||
|
||||
.sixth-watermark,
|
||||
.misc-watermark.sixth-watermark,
|
||||
.cleaning-watermark.sixth-watermark,
|
||||
.multi-person-watermark.sixth-watermark,
|
||||
.continuous-die-watermark.sixth-watermark,
|
||||
.timeout-watermark.sixth-watermark,
|
||||
.voided-watermark.sixth-watermark,
|
||||
.modified-watermark.sixth-watermark,
|
||||
.over-limit-watermark.sixth-watermark {
|
||||
top: 318rpx;
|
||||
}
|
||||
|
||||
.timeout-watermark {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
@@ -832,6 +893,63 @@ scroll-view.page {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.over-limit-panel {
|
||||
margin-top: 16rpx;
|
||||
padding: 16rpx;
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.over-limit-panel.danger {
|
||||
border: 2rpx solid rgba(197, 34, 31, 0.28);
|
||||
background: rgba(255, 239, 239, 0.78);
|
||||
}
|
||||
|
||||
.over-limit-panel.warn {
|
||||
border: 2rpx solid rgba(217, 119, 6, 0.28);
|
||||
background: rgba(255, 247, 232, 0.78);
|
||||
}
|
||||
|
||||
.over-limit-title {
|
||||
color: #9f1c19;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.over-limit-panel.warn .over-limit-title {
|
||||
color: #9a4a05;
|
||||
}
|
||||
|
||||
.over-limit-text {
|
||||
color: #7a271a;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.over-limit-panel.warn .over-limit-text {
|
||||
color: #7a4a12;
|
||||
}
|
||||
|
||||
.over-limit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6rpx 14rpx;
|
||||
color: #7a271a;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.over-limit-panel.warn .over-limit-grid {
|
||||
color: #7a4a12;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
||||
# 报工跨日期工时归属拆分设计
|
||||
|
||||
## 背景
|
||||
|
||||
现有报工记录使用 `report_date` 作为单一报工日期。这个字段被报工记录、管理员审核、经理看板、导出、对账小账本、设备/模具使用统计等模块复用。
|
||||
|
||||
当员工夜里或凌晨跨日期工作时,单一 `report_date` 无法准确表达工时归属。例如:
|
||||
|
||||
- `00:10` 上班,`11:00` 下班
|
||||
- `00:10-06:00` 是前一天夜班尾段
|
||||
- `06:00-08:00` 是作息空白区间,但按新规则应算加班
|
||||
- `08:00-11:00` 是当天白班
|
||||
|
||||
如果整条报工强行归前一天或当天,都会造成一部分工时、数量、工资统计不合理。
|
||||
|
||||
## 目标
|
||||
|
||||
实现一条原始报工记录可以按工时归属拆分到多个日期。
|
||||
|
||||
核心原则:
|
||||
|
||||
- 原始报工记录仍然只保留一条,不拆成多条审核记录。
|
||||
- 管理员审核、作废、更改、水印、员工记录仍围绕原始报工记录。
|
||||
- 经理看板、导出、对账小账本、设备/模具统计、概览等统计类模块按拆分后的归属明细汇总。
|
||||
- 跨日期时,成品数量、不良数量、报废数量、换料次数、参考工资等涉及统计的字段按有效工时比例拆分。
|
||||
- 作息空白区间统一算加班,但不直接改变原始 `report_date`。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不把一条原始报工拆成多条 `ProductionReport`。
|
||||
- 不开放管理员手工编辑 `report_date`。
|
||||
- 不批量回改历史记录的原始 `report_date`。
|
||||
- 不改变员工扫码上班、换模具、下班提交的主流程。
|
||||
|
||||
## 术语
|
||||
|
||||
- 原始报工:员工提交或系统自动提交后形成的一条 `ProductionReport`。
|
||||
- 工序明细:一条报工下的产品工序明细。
|
||||
- 归属明细:按日期和班种拆出来的统计明细。
|
||||
- 有效工时:扣除符合规则的吃饭时间后,用于统计、工资和数量分摊的工时。
|
||||
- 空白区间:不属于白班、夜班、原加班区间的作息时间,例如默认 `06:00-08:00`、`17:20-18:00`。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
采用“一条原始报工 + 多条归属明细”的方案。
|
||||
|
||||
### 原始记录
|
||||
|
||||
`ProductionReport` 继续保存员工真实提交的完整记录:
|
||||
|
||||
- 考勤点
|
||||
- 员工
|
||||
- 上班时间
|
||||
- 下班时间
|
||||
- 报工明细
|
||||
- 审核状态
|
||||
- 作废状态
|
||||
- 更改状态
|
||||
- 原始 `report_date`
|
||||
|
||||
原始 `report_date` 仍用于报工记录卡片的基础展示,但统计类模块不再只依赖它。
|
||||
|
||||
### 归属明细
|
||||
|
||||
新增一张后端表,例如 `production_report_allocations`。
|
||||
|
||||
建议字段:
|
||||
|
||||
- `id`
|
||||
- `report_id`
|
||||
- `report_item_id`
|
||||
- `attendance_point_name`
|
||||
- `employee_phone`
|
||||
- `allocation_date`
|
||||
- `day_minutes`
|
||||
- `overtime_minutes`
|
||||
- `night_minutes`
|
||||
- `effective_minutes`
|
||||
- `good_qty`
|
||||
- `defect_qty`
|
||||
- `scrap_qty`
|
||||
- `changeover_count`
|
||||
- `reference_wage`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
如果后续设备/模具统计需要直接按设备或模具拆分,可以再补:
|
||||
|
||||
- `device_no`
|
||||
- `mold_name`
|
||||
- `product_name`
|
||||
- `process_name`
|
||||
- `stamping_method`
|
||||
|
||||
第一版建议保留到 `report_item_id` 级别,设备/模具统计可以根据现有工序明细和设备段关联计算,避免一次性改表过重。
|
||||
|
||||
## 拆分规则
|
||||
|
||||
### 班种区间
|
||||
|
||||
按报工所属考勤点的作息配置计算:
|
||||
|
||||
- 白班:`day_start` 到 `day_end`
|
||||
- 原加班:`overtime_start` 到 `overtime_end`
|
||||
- 夜班:`night_start` 到 `night_end`
|
||||
- 午饭、晚饭:继续使用当前吃饭扣除规则
|
||||
|
||||
夜班跨天时,例如 `20:00-06:00`,按当前系统已有跨天区间逻辑处理。
|
||||
|
||||
### 空白区间
|
||||
|
||||
一天中不属于白班、原加班、夜班的时间,统一计入加班。
|
||||
|
||||
默认作息下:
|
||||
|
||||
- `06:00-08:00` 算加班
|
||||
- `17:20-18:00` 算加班
|
||||
|
||||
空白区间只影响工时分布,不单独改变原始 `report_date`。
|
||||
|
||||
### 日期归属
|
||||
|
||||
归属明细按时间片所在的自然日期和夜班归属拆分。
|
||||
|
||||
默认作息 `20:00-06:00` 下:
|
||||
|
||||
- `20:00-24:00` 夜班归属当天
|
||||
- `00:00-06:00` 夜班归属前一天
|
||||
- `06:00` 之后的空白加班、白班、加班归属当天
|
||||
|
||||
例子:`2026-07-25 00:10` 到 `2026-07-25 11:00`
|
||||
|
||||
- `2026-07-24`:夜班 `350` 分钟
|
||||
- `2026-07-25`:加班 `120` 分钟,白班 `180` 分钟
|
||||
|
||||
### 数量和金额分摊
|
||||
|
||||
每个工序明细先计算总有效分钟,再按日期归属比例拆分。
|
||||
|
||||
公式:
|
||||
|
||||
```text
|
||||
归属比例 = 某归属日有效分钟 / 当前工序明细总有效分钟
|
||||
归属数量 = 原始数量 * 归属比例
|
||||
归属工资 = 原始参考工资 * 归属比例
|
||||
```
|
||||
|
||||
适用字段:
|
||||
|
||||
- 成品数量
|
||||
- 不良数量
|
||||
- 报废数量
|
||||
- 换料次数
|
||||
- 参考工资
|
||||
|
||||
### 小数处理
|
||||
|
||||
后端内部保留两位小数。
|
||||
|
||||
导出和页面展示可以使用两位小数,避免比例拆分后出现不合理的整数强行取整。
|
||||
|
||||
如果未来现场要求成品必须整数,可以另开一个“整数分摊余数归属规则”,本次不引入。
|
||||
|
||||
## 数据流
|
||||
|
||||
### 员工提交报工
|
||||
|
||||
1. 员工扫码、填写报工并提交。
|
||||
2. 后端创建一条 `ProductionReport` 和对应 `ProductionReportItem`。
|
||||
3. 后端计算原始记录指标。
|
||||
4. 后端生成或刷新归属明细。
|
||||
5. 返回原始报工卡片。
|
||||
|
||||
### 系统自动提交
|
||||
|
||||
1. 自动提交服务创建原始报工。
|
||||
2. 原始结束时间为系统自动提交时间。
|
||||
3. 后端计算原始记录指标。
|
||||
4. 后端生成或刷新归属明细。
|
||||
|
||||
### 管理员审核或更改
|
||||
|
||||
管理员调整以下内容时,需要重算归属明细:
|
||||
|
||||
- 上班时间
|
||||
- 下班时间
|
||||
- 换模具时间
|
||||
- 产品工序
|
||||
- 设备
|
||||
- 成品数量
|
||||
- 不良数量
|
||||
- 报废数量
|
||||
- 换料次数
|
||||
- 工序单价
|
||||
- 作废/撤销作废
|
||||
|
||||
保存流程:
|
||||
|
||||
1. 校验管理员权限和编辑窗口。
|
||||
2. 保存原始记录修改。
|
||||
3. 重算原始记录指标。
|
||||
4. 删除旧归属明细。
|
||||
5. 生成新归属明细。
|
||||
6. 设置更改水印。
|
||||
|
||||
作废记录不参与统计。作废时可以保留归属明细,但统计查询必须过滤;也可以删除归属明细。第一版建议保留并在统计查询中过滤,方便撤销作废后立即恢复统计。
|
||||
|
||||
## 模块影响
|
||||
|
||||
### 员工报工记录
|
||||
|
||||
仍显示一条原始记录。
|
||||
|
||||
可在明细里补充展示工时归属,例如:
|
||||
|
||||
```text
|
||||
工时归属:2026-07-24 夜班 5小时50分;2026-07-25 加班 2小时、白班 3小时
|
||||
```
|
||||
|
||||
第一版只在管理员和经理侧展示归属摘要,员工侧报工记录保留现状。
|
||||
|
||||
### 管理员审核页
|
||||
|
||||
仍按原始记录审核。
|
||||
|
||||
管理员改时间或数量后,页面返回重算后的原始指标和归属摘要。
|
||||
|
||||
审核页可以展示归属摘要,帮助管理员知道这条记录会被拆到哪些日期。
|
||||
|
||||
### 经理报工看板
|
||||
|
||||
看板按归属明细汇总,不再只按 `ProductionReport.report_date` 汇总。
|
||||
|
||||
筛选日期查的是 `allocation_date`。
|
||||
|
||||
同人、同考勤点、同项目、同产品、同工序、同冲压方式的聚合,应基于归属明细汇总。
|
||||
|
||||
### 经理看板导出
|
||||
|
||||
第一张明细 sheet 按归属明细导出。
|
||||
|
||||
建议保留两个日期字段:
|
||||
|
||||
- 原始报工日期
|
||||
- 统计归属日期
|
||||
|
||||
这样经理既能追溯原始记录,也能看到工资和工时实际归属。
|
||||
|
||||
参考工资、工序单价、工种、备注等现有导出字段继续保留。
|
||||
|
||||
工资汇总 sheet 按归属明细汇总。
|
||||
|
||||
### 对账小账本
|
||||
|
||||
已报工成品数量使用归属明细统计。
|
||||
|
||||
最后一道工序判断仍按产品清单数字工序大小确定。
|
||||
|
||||
月份归属改为 `allocation_date` 所在月份。
|
||||
|
||||
### 设备/模具使用统计
|
||||
|
||||
默认统计审核通过且未作废记录。
|
||||
|
||||
时间筛选按归属明细日期。
|
||||
|
||||
普通设备/模具:
|
||||
|
||||
- 时长按归属明细有效分钟汇总
|
||||
- 报工次数按原始报工 ID 去重
|
||||
|
||||
建议第一版:
|
||||
|
||||
- 时长按归属明细汇总
|
||||
- 报工次数按原始报工 ID 去重
|
||||
|
||||
清洗设备:
|
||||
|
||||
- 清洗数量按归属明细数量汇总
|
||||
- 报工次数按原始报工 ID 去重
|
||||
|
||||
### 概览
|
||||
|
||||
涉及今日、历史待审核、已通过等指标时:
|
||||
|
||||
- 审核状态类数量仍按原始报工记录。
|
||||
- 工时、数量、工资类指标按归属明细。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 如果归属明细生成失败,原始报工保存应回滚,避免原始记录和统计数据不一致。
|
||||
- 如果旧数据没有归属明细,统计查询应提供兼容兜底:用原始记录实时计算或用迁移脚本补齐。
|
||||
- 如果有效分钟为 `0`,数量和工资不做比例拆分,统一归到原始 `report_date`,同时记录日志,避免除零。
|
||||
- 如果作息配置缺失,使用当前默认作息。
|
||||
|
||||
## 历史数据处理
|
||||
|
||||
历史原始 `report_date` 不批量回改。
|
||||
|
||||
但为了让统计类模块切换到新口径,建议提供一次性迁移脚本:
|
||||
|
||||
1. 扫描审核通过且未作废的历史报工。
|
||||
2. 根据记录当时的上下班时间、工序明细、考勤点作息生成归属明细。
|
||||
3. 不修改原始 `report_date`。
|
||||
4. 迁移后输出处理数量、跳过数量、异常记录 ID。
|
||||
|
||||
如果担心历史作息被后续修改导致还原不准,第一版接受使用当前考勤点作息生成历史归属明细。后续如要严格追溯,需要在报工时快照作息配置。
|
||||
|
||||
## 测试用例
|
||||
|
||||
核心用例:
|
||||
|
||||
- `2026-07-25 00:10` 到 `2026-07-25 11:00`
|
||||
- `2026-07-24` 夜班 `350` 分钟
|
||||
- `2026-07-25` 加班 `120` 分钟,白班 `180` 分钟
|
||||
- `2026-07-25 05:59` 到 `2026-07-25 11:00`
|
||||
- `2026-07-24` 夜班 `1` 分钟
|
||||
- `2026-07-25` 加班 `120` 分钟,白班 `180` 分钟
|
||||
- `2026-07-24 20:00` 到 `2026-07-25 06:30`
|
||||
- `2026-07-24` 夜班 `600` 分钟
|
||||
- `2026-07-25` 加班 `30` 分钟
|
||||
- `2026-07-25 06:00` 到 `2026-07-25 08:00`
|
||||
- `2026-07-25` 加班 `120` 分钟
|
||||
- `2026-07-25 17:20` 到 `2026-07-25 18:00`
|
||||
- `2026-07-25` 加班 `40` 分钟
|
||||
- 覆盖吃饭扣除和 5 分钟缓冲规则。
|
||||
- 覆盖管理员修改上下班时间后归属明细重算。
|
||||
- 覆盖管理员修改换模具时间后归属明细重算。
|
||||
- 覆盖作废记录从统计中排除。
|
||||
- 覆盖撤销作废后统计恢复。
|
||||
|
||||
## 实施顺序建议
|
||||
|
||||
1. 后端新增归属计算服务和单元测试。
|
||||
2. 新增归属明细表和迁移脚本。
|
||||
3. 报工提交、自动提交、管理员审核保存后写入归属明细。
|
||||
4. 经理看板和导出切换到归属明细。
|
||||
5. 对账小账本、设备/模具统计、概览按风险顺序切换。
|
||||
6. 前端增加归属摘要展示。
|
||||
7. 跑迁移脚本补齐历史归属明细。
|
||||
|
||||
## 已固定技术口径
|
||||
|
||||
当前方案已按以下业务口径固定:
|
||||
|
||||
- 涉及拆分的字段都按工时比例拆。
|
||||
- 空白作息区间算加班。
|
||||
- 空白作息区间不影响原始 `report_date`。
|
||||
- 原始报工记录不拆。
|
||||
- 报工次数统计按原始报工 ID 去重,不因一条报工拆到多个日期而重复计数。
|
||||
@@ -37,16 +37,18 @@
|
||||
|
||||
<view wx:if="{{!rows.length}}" class="list-empty">暂无已审核报工</view>
|
||||
<view wx:for="{{rows}}" wx:key="id" class="card dashboard-card {{item.isSystemAutoSubmitted ? 'system-auto' : ''}} {{item.isVoided ? 'voided' : ''}}">
|
||||
<text wx:if="{{item.isVoided}}" class="voided-watermark">作废</text>
|
||||
<text wx:elif="{{item.correctionPairs.length}}" class="modified-watermark">更改</text>
|
||||
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark">超时-系统自动提交</text>
|
||||
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length ? 'lower-watermark' : ''}}">清洗</text>
|
||||
<text wx:if="{{item.isMisc}}" class="misc-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length ? 'lower-watermark' : ''}}">包含杂活</text>
|
||||
<text wx:if="{{item.isMultiPerson}}" class="multi-person-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length || item.isMisc || item.isCleaning ? 'lower-watermark' : ''}}">多人协作</text>
|
||||
<text wx:if="{{item.isContinuousDie}}" class="continuous-die-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length || item.isMisc || item.isCleaning || item.isMultiPerson ? 'lower-watermark' : ''}} {{item.isMultiPerson && (item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length || item.isMisc || item.isCleaning) ? 'third-watermark' : ''}}">连续模</text>
|
||||
<text wx:if="{{item.hasOverLimit}}" class="over-limit-watermark {{item.watermarkClasses.overLimit}}">超额</text>
|
||||
<text wx:if="{{item.isVoided}}" class="voided-watermark {{item.watermarkClasses.state}}">作废</text>
|
||||
<text wx:elif="{{item.correctionPairs.length}}" class="modified-watermark {{item.watermarkClasses.state}}">更改</text>
|
||||
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark {{item.watermarkClasses.timeout}}">超时-系统自动提交</text>
|
||||
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.watermarkClasses.cleaning}}">清洗</text>
|
||||
<text wx:if="{{item.isMisc}}" class="misc-watermark {{item.watermarkClasses.misc}}">包含杂活</text>
|
||||
<text wx:if="{{item.isMultiPerson}}" class="multi-person-watermark {{item.watermarkClasses.multiPerson}}">多人协作</text>
|
||||
<text wx:if="{{item.isContinuousDie}}" class="continuous-die-watermark {{item.watermarkClasses.continuousDie}}">连续模</text>
|
||||
<view class="row">
|
||||
<view class="row-left">
|
||||
<text class="value">{{item.employeeName}} · {{item.reportDate}}</text>
|
||||
<text wx:if="{{item.sourceReportDatesText}}" class="allocation-summary">原始报工日期 {{item.sourceReportDatesText}}</text>
|
||||
<text class="subtitle">考勤点 {{item.attendancePointName || '-'}}</text>
|
||||
<text class="subtitle">{{item.employeePhone}} · {{item.projectNo}} / {{item.productName}}</text>
|
||||
<text class="subtitle">{{item.processDisplayName || item.productName || '-'}}</text>
|
||||
@@ -117,6 +119,10 @@
|
||||
<text class="label">备注</text>
|
||||
<text class="value">{{item.reviewRemark}}</text>
|
||||
</view>
|
||||
<view wx:if="{{item.hasOverLimitNotice}}" class="over-limit-panel {{item.hasOverLimit ? 'danger' : 'warn'}}">
|
||||
<text class="over-limit-title">{{item.overLimitSummary || (item.hasOverLimit ? '超额' : '无法校验')}}</text>
|
||||
<text wx:if="{{item.overLimitReasons}}" class="over-limit-text">{{item.overLimitReasons}}</text>
|
||||
</view>
|
||||
<view class="salary-divider"></view>
|
||||
<view class="salary-row">
|
||||
<text class="label">参考工资</text>
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.allocation-summary {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
color: #4b5563;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.cleaning-watermark {
|
||||
position: absolute;
|
||||
right: 28rpx;
|
||||
|
||||
@@ -381,9 +381,14 @@ Page({
|
||||
await this.shareZipFile(filePath, fileName)
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
const message = error && error.errMsg && error.errMsg.includes('cancel')
|
||||
? '已取消转发'
|
||||
: (error.message || '转发失败')
|
||||
let message = error.message || '转发失败'
|
||||
if (error && error.errMsg && error.errMsg.includes('cancel')) {
|
||||
message = '已取消转发'
|
||||
} else if (error && error.statusCode === 404) {
|
||||
message = 'ZIP文件不存在或已过期,请重新生成'
|
||||
} else if (error && error.statusCode === 409) {
|
||||
message = 'ZIP还未生成完成,请刷新后重试'
|
||||
}
|
||||
wx.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
const finalStatuses = ['CONFIRMED', 'CANCELLED', 'FAILED', 'EXPIRED', 'CONSUMED']
|
||||
|
||||
Page({
|
||||
data: {
|
||||
ticket: '',
|
||||
sessionId: '',
|
||||
preview: null,
|
||||
loading: true,
|
||||
confirming: false,
|
||||
cancelling: false,
|
||||
status: 'LOADING',
|
||||
message: '正在读取登录信息',
|
||||
canConfirm: false,
|
||||
canCancel: false,
|
||||
},
|
||||
onLoad(options) {
|
||||
const scene = this.decodeRepeated(options.scene || '')
|
||||
const fallback = scene || this.buildFallbackScene(options)
|
||||
const ticket = this.extractTicket(fallback)
|
||||
const sessionId = this.extractSessionId(fallback)
|
||||
this.setData({ ticket, sessionId })
|
||||
this.loadPreview()
|
||||
},
|
||||
buildFallbackScene(options) {
|
||||
const ticket = options.ticket || ''
|
||||
const sessionId = options.session_id || options.sessionId || ''
|
||||
return `ticket=${ticket}&session_id=${sessionId}`
|
||||
},
|
||||
decodeRepeated(value) {
|
||||
let text = String(value || '').trim()
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(text)
|
||||
if (decoded === text) {
|
||||
break
|
||||
}
|
||||
text = decoded
|
||||
} catch (error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return text
|
||||
},
|
||||
extractTicket(value) {
|
||||
return this.extractSceneValue(value, ['ticket', 't'])
|
||||
},
|
||||
extractSessionId(value) {
|
||||
return this.extractSceneValue(value, ['session_id', 'sessionId', 's'])
|
||||
},
|
||||
extractSceneValue(value, names) {
|
||||
const raw = this.decodeRepeated(value)
|
||||
for (let index = 0; index < names.length; index += 1) {
|
||||
const name = names[index]
|
||||
const matched = raw.match(new RegExp(`(?:^|[?&])${name}=([^&]+)`))
|
||||
if (matched && matched[1]) {
|
||||
return this.decodeRepeated(matched[1])
|
||||
}
|
||||
}
|
||||
return ''
|
||||
},
|
||||
async loadPreview() {
|
||||
if (!this.data.ticket || !this.data.sessionId) {
|
||||
this.updateStatus('FAILED', '登录二维码无效')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const preview = await api.getErpLoginPreview(this.data.ticket, this.data.sessionId)
|
||||
this.setData({ preview, loading: false })
|
||||
this.updateStatus('PENDING', '请确认是否登录 ERP')
|
||||
} catch (error) {
|
||||
this.updateStatus('FAILED', error.message || '登录信息读取失败')
|
||||
}
|
||||
},
|
||||
async confirmLogin(e) {
|
||||
const phoneCode = e.detail && e.detail.code
|
||||
if (!phoneCode) {
|
||||
wx.showToast({ title: '未获得手机号授权', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ confirming: true, message: '正在确认登录' })
|
||||
try {
|
||||
const result = await api.confirmErpLogin(this.data.ticket, this.data.sessionId, phoneCode)
|
||||
const status = String(result.status || '').toUpperCase()
|
||||
this.updateStatus(
|
||||
status,
|
||||
status === 'CONFIRMED' ? '已确认,请回到电脑端' : (result.failureReason || '确认失败'),
|
||||
)
|
||||
} catch (error) {
|
||||
this.updateStatus('FAILED', error.message || '确认失败')
|
||||
} finally {
|
||||
this.setData({ confirming: false })
|
||||
}
|
||||
},
|
||||
async cancelLogin() {
|
||||
this.setData({ cancelling: true, message: '正在取消登录' })
|
||||
try {
|
||||
const result = await api.cancelErpLogin(this.data.ticket, this.data.sessionId)
|
||||
this.updateStatus(result.status || 'CANCELLED', '已取消本次登录')
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || '取消失败', icon: 'none' })
|
||||
this.updateStatus(this.data.status || 'PENDING', this.data.message || '请确认是否登录 ERP')
|
||||
} finally {
|
||||
this.setData({ cancelling: false })
|
||||
}
|
||||
},
|
||||
updateStatus(status, message) {
|
||||
const normalized = String(status || '').toUpperCase()
|
||||
const actionable = normalized === 'PENDING' || normalized === 'SCANNED'
|
||||
this.setData({
|
||||
loading: false,
|
||||
status: normalized,
|
||||
message,
|
||||
canConfirm: actionable && !finalStatuses.includes(normalized),
|
||||
canCancel: actionable && !finalStatuses.includes(normalized),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "ERP扫码登录"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<view class="page safe-bottom erp-login-page">
|
||||
<view class="erp-login-nav">
|
||||
<text class="erp-login-nav-title">ERP扫码登录</text>
|
||||
</view>
|
||||
|
||||
<view class="header">
|
||||
<text class="title">{{preview.systemName || '嘉恒智能五金 ERP'}}</text>
|
||||
<text class="subtitle">{{message}}</text>
|
||||
</view>
|
||||
|
||||
<view class="card login-card">
|
||||
<view class="status-mark {{status === 'CONFIRMED' ? 'success' : ''}} {{status === 'FAILED' || status === 'EXPIRED' ? 'danger' : ''}}">
|
||||
<text>{{status === 'CONFIRMED' ? '已确认' : (status === 'CANCELLED' ? '已取消' : (status === 'FAILED' || status === 'EXPIRED' ? '不可用' : '待确认'))}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="loading-text">正在加载</view>
|
||||
|
||||
<block wx:if="{{preview}}">
|
||||
<view class="meta-row">
|
||||
<text class="label">发起时间</text>
|
||||
<text class="value">{{preview.startedAtText || preview.startedAt}}</text>
|
||||
</view>
|
||||
<view class="meta-row">
|
||||
<text class="label">过期时间</text>
|
||||
<text class="value">{{preview.expiresAtText || preview.expiresAt}}</text>
|
||||
</view>
|
||||
<view wx:if="{{preview.requestIpHint}}" class="meta-row">
|
||||
<text class="label">登录来源</text>
|
||||
<text class="value">{{preview.requestIpHint}}</text>
|
||||
</view>
|
||||
<view wx:if="{{preview.userAgentHint}}" class="meta-row">
|
||||
<text class="label">浏览器</text>
|
||||
<text class="value">{{preview.userAgentHint}}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view wx:if="{{canConfirm || canCancel}}" class="button-stack">
|
||||
<button
|
||||
class="btn primary full"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="confirmLogin"
|
||||
loading="{{confirming}}"
|
||||
disabled="{{confirming || cancelling}}"
|
||||
>确认登录</button>
|
||||
<button
|
||||
wx:if="{{canCancel}}"
|
||||
class="btn secondary full"
|
||||
bindtap="cancelLogin"
|
||||
loading="{{cancelling}}"
|
||||
disabled="{{confirming || cancelling}}"
|
||||
>取消</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,82 @@
|
||||
.erp-login-page {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.erp-login-nav {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 176rpx;
|
||||
margin: 0 -24rpx 22rpx;
|
||||
padding-top: 88rpx;
|
||||
border-bottom: 1rpx solid rgba(213, 222, 235, 0.9);
|
||||
background: rgba(247, 248, 250, 0.96);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.erp-login-nav-title {
|
||||
color: #111827;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding-top: 92rpx;
|
||||
}
|
||||
|
||||
.status-mark {
|
||||
position: absolute;
|
||||
right: 26rpx;
|
||||
top: 24rpx;
|
||||
min-width: 120rpx;
|
||||
padding: 12rpx 18rpx;
|
||||
border: 2rpx solid rgba(20, 99, 255, 0.25);
|
||||
border-radius: 14rpx;
|
||||
background: #edf4ff;
|
||||
color: #1456c2;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-mark.success {
|
||||
border-color: rgba(8, 116, 67, 0.24);
|
||||
background: #e7f7ef;
|
||||
color: #087443;
|
||||
}
|
||||
|
||||
.status-mark.danger {
|
||||
border-color: rgba(180, 35, 24, 0.24);
|
||||
background: #fff1f0;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #6b778c;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(102, 112, 133, 0.16);
|
||||
}
|
||||
|
||||
.meta-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.meta-row .value {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.button-stack {
|
||||
display: grid;
|
||||
gap: 20rpx;
|
||||
margin-top: 34rpx;
|
||||
}
|
||||
+20
-25
@@ -1,5 +1,6 @@
|
||||
const api = require('../../utils/api')
|
||||
const locationGuard = require('../../utils/locationGuard')
|
||||
const { buildErpLoginConfirmUrlFromScanResult } = require('../../utils/scanRouting')
|
||||
|
||||
const avatarKey = phone => `jh_wrs_avatar_${phone}`
|
||||
|
||||
@@ -373,22 +374,14 @@ Page({
|
||||
return raw
|
||||
},
|
||||
async scanDevice() {
|
||||
wx.showLoading({ title: '定位中' })
|
||||
try {
|
||||
await locationGuard.ensureFactoryLocation()
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '无法扫码报工',
|
||||
content: error.message || '当前位置不在允许范围内',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.scanCode({
|
||||
onlyFromCamera: false,
|
||||
success: async res => {
|
||||
const erpLoginUrl = buildErpLoginConfirmUrlFromScanResult(res)
|
||||
if (erpLoginUrl) {
|
||||
wx.navigateTo({ url: erpLoginUrl })
|
||||
return
|
||||
}
|
||||
const candidates = [res.path, res.result, res.rawData]
|
||||
const moldName = candidates.reduce((found, item) => (
|
||||
found || this.extractMoldName(item)
|
||||
@@ -398,21 +391,23 @@ Page({
|
||||
return
|
||||
}
|
||||
const paramName = moldName.startsWith('mold=') ? 'scene' : 'moldName'
|
||||
if (paramName === 'scene') {
|
||||
try {
|
||||
wx.showLoading({ title: '校验中' })
|
||||
try {
|
||||
wx.showLoading({ title: paramName === 'scene' ? '校验中' : '定位中' })
|
||||
if (paramName === 'scene') {
|
||||
const resolved = await api.resolveMoldScene(moldName)
|
||||
await locationGuard.ensureFactoryLocation(resolved.attendancePointName)
|
||||
} catch (error) {
|
||||
wx.showModal({
|
||||
title: '无法扫码报工',
|
||||
content: error.message || '当前位置不在该考勤点范围内',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
} else {
|
||||
await locationGuard.ensureFactoryLocation()
|
||||
}
|
||||
} catch (error) {
|
||||
wx.showModal({
|
||||
title: '无法扫码报工',
|
||||
content: error.message || '当前位置不在允许范围内',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
wx.navigateTo({
|
||||
url: `/pages/clock/clock?${paramName}=${encodeURIComponent(moldName)}`,
|
||||
|
||||
@@ -63,13 +63,14 @@
|
||||
data-id="{{item.id}}"
|
||||
bindtap="toggleRecordDetail"
|
||||
>
|
||||
<text wx:if="{{item.isVoided}}" class="voided-watermark">作废</text>
|
||||
<text wx:elif="{{item.isModified}}" class="modified-watermark">更改</text>
|
||||
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark">{{item.autoSubmitReason || '超时-系统自动提交'}}</text>
|
||||
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">清洗</text>
|
||||
<text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">包含杂活</text>
|
||||
<text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning ? 'lower-watermark' : ''}}">多人协作</text>
|
||||
<text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning || item.hasMultiPerson ? 'lower-watermark' : ''}} {{item.hasMultiPerson && (item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning) ? 'third-watermark' : ''}}">连续模</text>
|
||||
<text wx:if="{{item.hasOverLimit}}" class="over-limit-watermark {{item.watermarkClasses.overLimit}}">超额</text>
|
||||
<text wx:if="{{item.isVoided}}" class="voided-watermark {{item.watermarkClasses.state}}">作废</text>
|
||||
<text wx:elif="{{item.isModified}}" class="modified-watermark {{item.watermarkClasses.state}}">更改</text>
|
||||
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark {{item.watermarkClasses.timeout}}">{{item.autoSubmitReason || '超时-系统自动提交'}}</text>
|
||||
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.watermarkClasses.cleaning}}">清洗</text>
|
||||
<text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.watermarkClasses.misc}}">包含杂活</text>
|
||||
<text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.watermarkClasses.multiPerson}}">多人协作</text>
|
||||
<text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.watermarkClasses.continuousDie}}">连续模</text>
|
||||
<view class="row">
|
||||
<view class="row-left">
|
||||
<text class="value">{{item.employeeName}} · {{item.reportDate}}</text>
|
||||
@@ -149,6 +150,16 @@
|
||||
<text class="value">{{detail.changeoverCount || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{detail.hasOverLimitNotice}}" class="over-limit-panel {{detail.hasOverLimit ? 'danger' : 'warn'}}">
|
||||
<text class="over-limit-title">{{detail.hasOverLimit ? '超额' : '无法校验'}}</text>
|
||||
<text class="over-limit-text">{{detail.overLimitReason || '-'}}</text>
|
||||
<view class="over-limit-grid">
|
||||
<text>本次消耗 {{detail.overLimitCurrentReportQtyText}}</text>
|
||||
<text>累计消耗 {{detail.overLimitCurrentCumulativeQtyText}}</text>
|
||||
<text wx:if="{{detail.hasOverLimitMaxReportableQty}}">最大可报 {{detail.overLimitMaxReportableQtyText}}</text>
|
||||
<text wx:if="{{detail.hasOverLimitPreviousGoodQty}}">前序可用 {{detail.overLimitPreviousGoodQtyText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{detail.correctionPairs.length}}" class="correction-list detail-corrections">
|
||||
<view wx:for="{{detail.correctionPairs}}" wx:key="key" wx:for-item="pair" class="correction-pair">
|
||||
<text>{{pair.label}} </text>
|
||||
|
||||
@@ -326,6 +326,26 @@ Page({
|
||||
changeoverCount: detail.changeoverCount || 0,
|
||||
changeoverCountError: false,
|
||||
remark: detail.remark || '',
|
||||
overLimitStatus: detail.overLimitStatus || 'not_checked',
|
||||
overLimitType: detail.overLimitType || '',
|
||||
overLimitReason: detail.overLimitReason || '',
|
||||
overLimitCheckedAt: detail.overLimitCheckedAt || '',
|
||||
overLimitCheckSource: detail.overLimitCheckSource || '',
|
||||
overLimitBatchNo: detail.overLimitBatchNo || '',
|
||||
overLimitProductGrossWeightKg: detail.overLimitProductGrossWeightKg ?? '',
|
||||
overLimitIssuedWeightKg: detail.overLimitIssuedWeightKg ?? '',
|
||||
overLimitMaxReportableQty: detail.overLimitMaxReportableQty ?? '',
|
||||
overLimitPreviousGoodQty: detail.overLimitPreviousGoodQty ?? '',
|
||||
overLimitCurrentCumulativeQty: detail.overLimitCurrentCumulativeQty ?? '',
|
||||
overLimitCurrentReportQty: detail.overLimitCurrentReportQty ?? '',
|
||||
overLimitMaxReportableQtyText: detail.overLimitMaxReportableQtyText || '-',
|
||||
overLimitPreviousGoodQtyText: detail.overLimitPreviousGoodQtyText || '-',
|
||||
overLimitCurrentCumulativeQtyText: detail.overLimitCurrentCumulativeQtyText || '-',
|
||||
overLimitCurrentReportQtyText: detail.overLimitCurrentReportQtyText || '-',
|
||||
hasOverLimitMaxReportableQty: !!detail.hasOverLimitMaxReportableQty,
|
||||
hasOverLimitPreviousGoodQty: !!detail.hasOverLimitPreviousGoodQty,
|
||||
hasOverLimit: !!detail.hasOverLimit,
|
||||
hasOverLimitNotice: !!detail.hasOverLimitNotice,
|
||||
deviceError: false,
|
||||
deviceNo: detail.deviceNo,
|
||||
deviceIndex,
|
||||
|
||||
@@ -38,19 +38,21 @@
|
||||
data-id="{{item.id}}"
|
||||
bindtap="toggleReportDetail"
|
||||
>
|
||||
<text wx:if="{{item.isVoided}}" class="voided-watermark">作废</text>
|
||||
<text wx:elif="{{item.isModified}}" class="modified-watermark">更改</text>
|
||||
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark">{{item.autoSubmitReason || '超时-系统自动提交'}}</text>
|
||||
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">清洗</text>
|
||||
<text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">包含杂活</text>
|
||||
<text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning ? 'lower-watermark' : ''}}">多人协作</text>
|
||||
<text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning || item.hasMultiPerson ? 'lower-watermark' : ''}} {{item.hasMultiPerson && (item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning) ? 'third-watermark' : ''}}">连续模</text>
|
||||
<text wx:if="{{item.hasOverLimit}}" class="over-limit-watermark {{item.watermarkClasses.overLimit}}">超额</text>
|
||||
<text wx:if="{{item.isVoided}}" class="voided-watermark {{item.watermarkClasses.state}}">作废</text>
|
||||
<text wx:elif="{{item.isModified}}" class="modified-watermark {{item.watermarkClasses.state}}">更改</text>
|
||||
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark {{item.watermarkClasses.timeout}}">{{item.autoSubmitReason || '超时-系统自动提交'}}</text>
|
||||
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.watermarkClasses.cleaning}}">清洗</text>
|
||||
<text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.watermarkClasses.misc}}">包含杂活</text>
|
||||
<text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.watermarkClasses.multiPerson}}">多人协作</text>
|
||||
<text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.watermarkClasses.continuousDie}}">连续模</text>
|
||||
<view class="row">
|
||||
<view class="row-left">
|
||||
<text class="value">{{item.employeeName}} · {{item.reportDate}}</text>
|
||||
<text class="subtitle">考勤点 {{item.attendancePointName || '-'}}</text>
|
||||
<text class="subtitle">{{item.timeRangeText}}</text>
|
||||
<text class="subtitle">{{item.metrics.shiftDistributionText || ''}}</text>
|
||||
<text wx:if="{{item.allocationSummaryText}}" class="allocation-summary">归属拆分:{{item.allocationSummaryText}}</text>
|
||||
</view>
|
||||
<text class="pill {{item.status === 'approved' ? 'success' : 'warn'}}">{{item.statusName}}</text>
|
||||
</view>
|
||||
@@ -157,6 +159,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{detail.hasOverLimitNotice}}" class="over-limit-panel {{detail.hasOverLimit ? 'danger' : 'warn'}}">
|
||||
<text class="over-limit-title">{{detail.hasOverLimit ? '超额' : '无法校验'}}</text>
|
||||
<text class="over-limit-text">{{detail.overLimitReason || '-'}}</text>
|
||||
<view class="over-limit-grid">
|
||||
<text>本次消耗 {{detail.overLimitCurrentReportQtyText}}</text>
|
||||
<text>累计消耗 {{detail.overLimitCurrentCumulativeQtyText}}</text>
|
||||
<text wx:if="{{detail.hasOverLimitMaxReportableQty}}">最大可报 {{detail.overLimitMaxReportableQtyText}}</text>
|
||||
<text wx:if="{{detail.hasOverLimitPreviousGoodQty}}">前序可用 {{detail.overLimitPreviousGoodQtyText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{detail.isMisc && !item.isVoided && item.canEdit}}" class="review-edit-panel misc-remark-panel">
|
||||
<text class="panel-title">杂活事项备注</text>
|
||||
<textarea
|
||||
|
||||
@@ -53,6 +53,14 @@
|
||||
border-color: #aebbd0;
|
||||
}
|
||||
|
||||
.allocation-summary {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
color: #4b5563;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.cleaning-watermark {
|
||||
position: absolute;
|
||||
right: 28rpx;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const appJson = JSON.parse(readFileSync("app.json", "utf8"));
|
||||
const apiSource = readFileSync("utils/api.js", "utf8");
|
||||
const pageJs = readFileSync("pages/erpLoginConfirm/erpLoginConfirm.js", "utf8");
|
||||
const pageWxml = readFileSync("pages/erpLoginConfirm/erpLoginConfirm.wxml", "utf8");
|
||||
const previewApi = apiSource.match(/const getErpLoginPreview = async[\s\S]*?\n\}\)\)/)?.[0] ?? "";
|
||||
const confirmApi = apiSource.match(/const confirmErpLogin = async[\s\S]*?\n\}\)\)/)?.[0] ?? "";
|
||||
const cancelApi = apiSource.match(/const cancelErpLogin = async[\s\S]*?\n\}\)\)/)?.[0] ?? "";
|
||||
|
||||
assert.ok(appJson.pages.includes("pages/erpLoginConfirm/erpLoginConfirm"));
|
||||
assert.match(apiSource, /const getErpLoginPreview = async \(ticket, sessionId\) =>/);
|
||||
assert.match(apiSource, /const confirmErpLogin = async \(ticket, sessionId, phoneCode\) =>/);
|
||||
assert.match(apiSource, /const cancelErpLogin = async \(ticket, sessionId\) =>/);
|
||||
assert.match(previewApi, /session_id: sessionId/);
|
||||
assert.match(confirmApi, /session_id: sessionId/);
|
||||
assert.match(cancelApi, /session_id: sessionId/);
|
||||
assert.match(pageJs, /decodeRepeated\(options\.scene/);
|
||||
assert.match(pageJs, /options\.ticket/);
|
||||
assert.match(pageJs, /options\.session_id/);
|
||||
assert.match(pageJs, /options\.sessionId/);
|
||||
assert.match(pageJs, /extractTicket/);
|
||||
assert.match(pageJs, /extractSessionId/);
|
||||
assert.match(pageJs, /\['ticket', 't'\]/);
|
||||
assert.match(pageJs, /\['session_id', 'sessionId', 's'\]/);
|
||||
assert.match(pageJs, /confirmErpLogin/);
|
||||
assert.match(pageJs, /confirmErpLogin\(this\.data\.ticket,\s*this\.data\.sessionId,\s*phoneCode\)/);
|
||||
assert.match(pageJs, /cancelErpLogin\(this\.data\.ticket,\s*this\.data\.sessionId\)/);
|
||||
assert.match(pageWxml, /getPhoneNumber/);
|
||||
assert.match(pageWxml, /确认登录/);
|
||||
assert.match(pageWxml, /取消/);
|
||||
assert.doesNotMatch(pageJs, /ensureFactoryLocation/);
|
||||
assert.doesNotMatch(pageJs, /getClockState/);
|
||||
assert.doesNotMatch(pageJs, /access_token|AuthSession|saveAuth|setAuth/);
|
||||
assert.doesNotMatch(apiSource, /confirmErpLogin[\s\S]*access_token/);
|
||||
|
||||
console.log("erp login confirm page checks passed");
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createRequire } from "node:module";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const indexSource = readFileSync("pages/index/index.js", "utf8");
|
||||
const helperPath = "utils/scanRouting.js";
|
||||
|
||||
assert.ok(existsSync(helperPath), "scan routing helper should exist");
|
||||
|
||||
const { buildErpLoginConfirmUrlFromScanResult } = require("../utils/scanRouting.js");
|
||||
|
||||
assert.equal(
|
||||
buildErpLoginConfirmUrlFromScanResult({
|
||||
path: "pages/erpLoginConfirm/erpLoginConfirm?scene=t%3Dabc123%26s%3D7",
|
||||
}),
|
||||
"/pages/erpLoginConfirm/erpLoginConfirm?scene=t%3Dabc123%26s%3D7",
|
||||
);
|
||||
assert.equal(
|
||||
buildErpLoginConfirmUrlFromScanResult({
|
||||
result: "scene=t%3DcompactTicket%26s%3D9",
|
||||
}),
|
||||
"/pages/erpLoginConfirm/erpLoginConfirm?scene=t%3DcompactTicket%26s%3D9",
|
||||
);
|
||||
assert.equal(
|
||||
buildErpLoginConfirmUrlFromScanResult({
|
||||
path: "pages/erpLoginConfirm/erpLoginConfirm?ticket=legacyTicket&session_id=12",
|
||||
}),
|
||||
"/pages/erpLoginConfirm/erpLoginConfirm?scene=t%3DlegacyTicket%26s%3D12",
|
||||
);
|
||||
assert.equal(
|
||||
buildErpLoginConfirmUrlFromScanResult({
|
||||
result: "pages/clock/clock?scene=mold%3Dabc123",
|
||||
}),
|
||||
"",
|
||||
);
|
||||
assert.equal(
|
||||
buildErpLoginConfirmUrlFromScanResult({
|
||||
result: "mold=abc123",
|
||||
}),
|
||||
"",
|
||||
);
|
||||
|
||||
const scanDeviceIndex = indexSource.indexOf("async scanDevice()");
|
||||
const erpRoutingIndex = indexSource.indexOf("buildErpLoginConfirmUrlFromScanResult", scanDeviceIndex);
|
||||
const moldRoutingIndex = indexSource.indexOf("extractMoldName", scanDeviceIndex);
|
||||
const locationGuardIndex = indexSource.indexOf("ensureFactoryLocation", scanDeviceIndex);
|
||||
|
||||
assert.ok(scanDeviceIndex >= 0, "index page should have scanDevice");
|
||||
assert.ok(erpRoutingIndex > scanDeviceIndex, "scanDevice should check ERP login scan result");
|
||||
assert.ok(erpRoutingIndex < moldRoutingIndex, "ERP login scan should be routed before mold parsing");
|
||||
assert.ok(erpRoutingIndex < locationGuardIndex, "ERP login scan should not require report location guard");
|
||||
|
||||
console.log("erp login scan routing checks passed");
|
||||
+154
-10
@@ -544,6 +544,47 @@ const formatWorkTime = minutes => {
|
||||
return `${Math.round(safeMinutes * 100) / 100}分钟`
|
||||
}
|
||||
|
||||
const snapshotValueText = value => (
|
||||
value === null || value === undefined || value === '' ? '-' : String(value)
|
||||
)
|
||||
|
||||
const hasSnapshotValue = value => (
|
||||
value !== null && value !== undefined && value !== ''
|
||||
)
|
||||
|
||||
const watermarkClassAt = index => (
|
||||
['', 'lower-watermark', 'third-watermark', 'fourth-watermark', 'fifth-watermark', 'sixth-watermark'][index] || 'sixth-watermark'
|
||||
)
|
||||
|
||||
const buildWatermarkClasses = flags => {
|
||||
const classes = {}
|
||||
let index = 0
|
||||
const assign = (key, enabled) => {
|
||||
classes[key] = enabled ? watermarkClassAt(index++) : ''
|
||||
}
|
||||
assign('overLimit', flags.hasOverLimit)
|
||||
assign('state', flags.hasState)
|
||||
assign('timeout', flags.isSystemAutoSubmitted)
|
||||
assign('cleaning', flags.isCleaning)
|
||||
assign('misc', flags.hasMisc)
|
||||
assign('multiPerson', flags.hasMultiPerson)
|
||||
assign('continuousDie', flags.hasContinuousDie)
|
||||
return classes
|
||||
}
|
||||
|
||||
const toCamelAllocation = row => ({
|
||||
allocationDate: row.allocation_date || '',
|
||||
dayMinutes: Number(row.day_minutes || 0),
|
||||
overtimeMinutes: Number(row.overtime_minutes || 0),
|
||||
nightMinutes: Number(row.night_minutes || 0),
|
||||
effectiveMinutes: Number(row.effective_minutes || 0),
|
||||
goodQty: Number(row.good_qty || 0),
|
||||
defectQty: Number(row.defect_qty || 0),
|
||||
scrapQty: Number(row.scrap_qty || 0),
|
||||
changeoverCount: Number(row.changeover_count || 0),
|
||||
referenceWage: Number(row.reference_wage || 0),
|
||||
})
|
||||
|
||||
const toCamelReportItem = row => {
|
||||
const defectQty = Number(row.defect_qty || 0)
|
||||
const scrapQty = Number(row.scrap_qty || 0)
|
||||
@@ -564,6 +605,7 @@ const toCamelReportItem = row => {
|
||||
const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(actualBeat, standardBeat)
|
||||
const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(goodQty, standardWorkload)
|
||||
const corrections = row.corrections || {}
|
||||
const overLimitStatus = row.over_limit_status || 'not_checked'
|
||||
const correctionPairs = [
|
||||
correctionPair(corrections, 'device_no', '设备号', row.device_no),
|
||||
correctionPair(corrections, 'project_no', '项目号', row.project_no),
|
||||
@@ -617,9 +659,30 @@ const toCamelReportItem = row => {
|
||||
workloadCompareClass: workloadComparison.className,
|
||||
workloadReason: workloadComparison.reason,
|
||||
remark: row.remark || '',
|
||||
overLimitStatus,
|
||||
overLimitType: row.over_limit_type || '',
|
||||
overLimitReason: row.over_limit_reason || '',
|
||||
overLimitCheckedAt: row.over_limit_checked_at || '',
|
||||
overLimitCheckSource: row.over_limit_check_source || '',
|
||||
overLimitBatchNo: row.over_limit_batch_no || '',
|
||||
overLimitProductGrossWeightKg: row.over_limit_product_gross_weight_kg ?? '',
|
||||
overLimitIssuedWeightKg: row.over_limit_issued_weight_kg ?? '',
|
||||
overLimitMaxReportableQty: row.over_limit_max_reportable_qty ?? '',
|
||||
overLimitPreviousGoodQty: row.over_limit_previous_good_qty ?? '',
|
||||
overLimitCurrentCumulativeQty: row.over_limit_current_cumulative_qty ?? '',
|
||||
overLimitCurrentReportQty: row.over_limit_current_report_qty ?? '',
|
||||
overLimitMaxReportableQtyText: snapshotValueText(row.over_limit_max_reportable_qty),
|
||||
overLimitPreviousGoodQtyText: snapshotValueText(row.over_limit_previous_good_qty),
|
||||
overLimitCurrentCumulativeQtyText: snapshotValueText(row.over_limit_current_cumulative_qty),
|
||||
overLimitCurrentReportQtyText: snapshotValueText(row.over_limit_current_report_qty),
|
||||
hasOverLimitMaxReportableQty: hasSnapshotValue(row.over_limit_max_reportable_qty),
|
||||
hasOverLimitPreviousGoodQty: hasSnapshotValue(row.over_limit_previous_good_qty),
|
||||
hasOverLimit: overLimitStatus === 'exceeded',
|
||||
hasOverLimitNotice: overLimitStatus === 'exceeded' || overLimitStatus === 'uncheckable',
|
||||
corrections,
|
||||
correctionPairs,
|
||||
hasCorrections: correctionPairs.length > 0,
|
||||
allocations: (row.allocations || []).map(toCamelAllocation),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,6 +722,7 @@ const toCamelReport = row => {
|
||||
const hasMisc = items.some(item => item.isMisc)
|
||||
const hasContinuousDie = items.some(item => item.isContinuousDie)
|
||||
const hasMultiPerson = items.some(item => item.isMultiPerson)
|
||||
const hasOverLimit = !!row.has_over_limit || items.some(item => item.hasOverLimit)
|
||||
const totalDefect = Math.round(items.reduce((sum, item) => sum + Number(item.defectQty || 0), 0) * 100) / 100
|
||||
const corrections = row.corrections || {}
|
||||
const reportCorrectionPairs = [
|
||||
@@ -676,6 +740,15 @@ const toCamelReport = row => {
|
||||
label: `${segment.displayName} ${pair.label}`,
|
||||
}))
|
||||
)))
|
||||
const watermarkClasses = buildWatermarkClasses({
|
||||
hasOverLimit,
|
||||
hasState: !!row.is_voided || !!row.is_modified,
|
||||
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
|
||||
isCleaning,
|
||||
hasMisc,
|
||||
hasMultiPerson,
|
||||
hasContinuousDie,
|
||||
})
|
||||
return {
|
||||
id: row.id,
|
||||
attendancePointName: row.attendance_point_name || '',
|
||||
@@ -701,6 +774,8 @@ const toCamelReport = row => {
|
||||
submittedAtText: util.formatTime(row.submitted_at),
|
||||
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
|
||||
autoSubmitReason: row.auto_submit_reason || '超时-系统自动提交',
|
||||
hasOverLimit,
|
||||
overLimitSummary: row.over_limit_summary || '',
|
||||
isMultiPersonAssistant: !!row.is_multi_person_assistant,
|
||||
multiPersonSourceReportId: row.multi_person_source_report_id || null,
|
||||
isVoided: !!row.is_voided,
|
||||
@@ -714,12 +789,14 @@ const toCamelReport = row => {
|
||||
canUnvoid: !!row.can_unvoid,
|
||||
canEdit: !!row.can_edit,
|
||||
isModified: !!row.is_modified,
|
||||
allocationSummaryText: row.allocation_summary_text || '',
|
||||
deviceSegments,
|
||||
items,
|
||||
isCleaning,
|
||||
hasMisc,
|
||||
hasContinuousDie,
|
||||
hasMultiPerson,
|
||||
watermarkClasses,
|
||||
metrics: {
|
||||
...toCamelMetrics(row.metrics || {}),
|
||||
totalDefect,
|
||||
@@ -1001,6 +1078,48 @@ const loginWithPhoneCode = async (phoneCode, selectedRole = '') => {
|
||||
return autoSelectDefaultRole(normalizeLoginResult(data))
|
||||
}
|
||||
|
||||
const toCamelErpLoginPreview = data => ({
|
||||
sessionId: data.session_id,
|
||||
systemName: data.system_name || '嘉恒智能五金 ERP',
|
||||
startedAt: data.started_at || '',
|
||||
startedAtText: data.started_at ? util.formatTime(data.started_at) : '',
|
||||
expiresAt: data.expires_at || '',
|
||||
expiresAtText: data.expires_at ? util.formatTime(data.expires_at) : '',
|
||||
requestIpHint: data.request_ip_hint || '',
|
||||
userAgentHint: data.user_agent_hint || '',
|
||||
})
|
||||
|
||||
const toCamelErpLoginAction = data => ({
|
||||
status: data.status || '',
|
||||
failureReason: data.failure_reason || '',
|
||||
})
|
||||
|
||||
const getErpLoginPreview = async (ticket, sessionId) => toCamelErpLoginPreview(await requestClient.request({
|
||||
url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}`,
|
||||
params: {
|
||||
session_id: sessionId,
|
||||
},
|
||||
}))
|
||||
|
||||
const confirmErpLogin = async (ticket, sessionId, phoneCode) => toCamelErpLoginAction(await requestClient.request({
|
||||
url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}/confirm`,
|
||||
method: 'POST',
|
||||
params: {
|
||||
session_id: sessionId,
|
||||
},
|
||||
data: {
|
||||
phone_code: phoneCode,
|
||||
},
|
||||
}))
|
||||
|
||||
const cancelErpLogin = async (ticket, sessionId) => toCamelErpLoginAction(await requestClient.request({
|
||||
url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}/cancel`,
|
||||
method: 'POST',
|
||||
params: {
|
||||
session_id: sessionId,
|
||||
},
|
||||
}))
|
||||
|
||||
const createTemporaryWorker = async (temporaryToken, temporaryName = '', attendancePoint = null) => {
|
||||
const data = await requestClient.request({
|
||||
url: '/api/auth/wechat-login',
|
||||
@@ -1486,9 +1605,28 @@ const listDashboard = async (filters = {}) => {
|
||||
})
|
||||
return toCamelPage(data, row => {
|
||||
const isMisc = !!row.is_misc || isMiscRow(row)
|
||||
const noOutputMetric = isMisc || isCleaningRow(row)
|
||||
const isCleaning = isCleaningRow(row)
|
||||
const isContinuousDie = isContinuousDieRow(row)
|
||||
const isMultiPerson = isMultiPersonRow(row)
|
||||
const hasOverLimit = !!row.has_over_limit
|
||||
const correctionPairs = (row.correction_pairs || []).map(pair => ({
|
||||
key: pair.key,
|
||||
label: pair.label,
|
||||
oldValue: correctionDisplayValue(pair.old_value),
|
||||
newValue: correctionDisplayValue(pair.new_value),
|
||||
}))
|
||||
const noOutputMetric = isMisc || isCleaning
|
||||
const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(row.actual_beat, row.standard_beat)
|
||||
const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(row.total_good_qty, row.expected_workload)
|
||||
const watermarkClasses = buildWatermarkClasses({
|
||||
hasOverLimit,
|
||||
hasState: !!row.is_voided || correctionPairs.length > 0,
|
||||
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
|
||||
isCleaning,
|
||||
hasMisc: isMisc,
|
||||
hasMultiPerson: isMultiPerson,
|
||||
hasContinuousDie: isContinuousDie,
|
||||
})
|
||||
return {
|
||||
id: row.id,
|
||||
attendancePointName: row.attendance_point_name || '',
|
||||
@@ -1501,18 +1639,21 @@ const listDashboard = async (filters = {}) => {
|
||||
processDisplayName: moldDisplayName(row.product_name, row.process_name, row.stamping_method),
|
||||
rawMaterialBatchNo: row.raw_material_batch_no || '',
|
||||
stampingMethod: row.stamping_method || '',
|
||||
isCleaning: isCleaningRow(row),
|
||||
isContinuousDie: isContinuousDieRow(row),
|
||||
isCleaning,
|
||||
isContinuousDie,
|
||||
changeoverCount: row.changeover_count || 0,
|
||||
isMisc,
|
||||
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
|
||||
isVoided: !!row.is_voided,
|
||||
isMultiPerson: isMultiPersonRow(row),
|
||||
isMultiPerson,
|
||||
isMultiPersonAssistant: !!row.is_multi_person_assistant,
|
||||
workerType: row.worker_type || '正式工',
|
||||
reportCount: row.report_count,
|
||||
effectiveMinutes: row.effective_minutes,
|
||||
shiftDistributionText: row.shift_distribution_text || '',
|
||||
sourceReportDate: row.source_report_date || '',
|
||||
sourceReportDates: row.source_report_dates || [],
|
||||
sourceReportDatesText: (row.source_report_dates || []).join('、'),
|
||||
shiftDayMinutes: row.shift_day_minutes || 0,
|
||||
shiftOvertimeMinutes: row.shift_overtime_minutes || 0,
|
||||
shiftNightMinutes: row.shift_night_minutes || 0,
|
||||
@@ -1524,6 +1665,11 @@ const listDashboard = async (filters = {}) => {
|
||||
standardBeat: row.standard_beat,
|
||||
processUnitPriceYuan: row.process_unit_price_yuan || 0,
|
||||
referenceWage: round2(row.reference_wage || 0),
|
||||
hasOverLimit,
|
||||
overLimitSummary: row.over_limit_summary || '',
|
||||
overLimitReasons: row.over_limit_reasons || '',
|
||||
hasOverLimitNotice: !!(row.over_limit_summary || row.over_limit_reasons),
|
||||
watermarkClasses,
|
||||
expectedWorkload: row.expected_workload,
|
||||
paceRate: row.pace_rate,
|
||||
workloadRate: row.workload_rate,
|
||||
@@ -1534,12 +1680,7 @@ const listDashboard = async (filters = {}) => {
|
||||
workloadCompareClass: workloadComparison.className,
|
||||
workloadReason: workloadComparison.reason,
|
||||
reviewRemark: row.review_remark || '',
|
||||
correctionPairs: (row.correction_pairs || []).map(pair => ({
|
||||
key: pair.key,
|
||||
label: pair.label,
|
||||
oldValue: correctionDisplayValue(pair.old_value),
|
||||
newValue: correctionDisplayValue(pair.new_value),
|
||||
})),
|
||||
correctionPairs,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1812,8 +1953,10 @@ module.exports = {
|
||||
approveReport,
|
||||
buildReportDraft,
|
||||
calculateStandardWorkload,
|
||||
cancelErpLogin,
|
||||
classifyBeat,
|
||||
classifyWorkload,
|
||||
confirmErpLogin,
|
||||
continueWork,
|
||||
createTemporaryWorker,
|
||||
createMoldLockFeedback,
|
||||
@@ -1837,6 +1980,7 @@ module.exports = {
|
||||
generateMoldQrBatch,
|
||||
getClockState,
|
||||
getCurrentUser,
|
||||
getErpLoginPreview,
|
||||
getHomeSummary,
|
||||
getReport,
|
||||
getUsageStatsDetail,
|
||||
|
||||
+6
-2
@@ -111,7 +111,9 @@ const download = options => new Promise((resolve, reject) => {
|
||||
if (res.statusCode === 401) {
|
||||
clearAuth()
|
||||
}
|
||||
reject(new Error(`下载失败:${res.statusCode}`))
|
||||
const error = new Error(`下载失败:${res.statusCode}`)
|
||||
error.statusCode = res.statusCode
|
||||
reject(error)
|
||||
},
|
||||
fail: err => {
|
||||
reject(new Error(err.errMsg || '下载失败'))
|
||||
@@ -137,7 +139,9 @@ const downloadUrl = options => new Promise((resolve, reject) => {
|
||||
if (res.statusCode === 401) {
|
||||
clearAuth()
|
||||
}
|
||||
reject(new Error(`下载失败:${res.statusCode}`))
|
||||
const error = new Error(`下载失败:${res.statusCode}`)
|
||||
error.statusCode = res.statusCode
|
||||
reject(error)
|
||||
},
|
||||
fail: err => {
|
||||
reject(new Error(err.errMsg || '下载失败'))
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
const ERP_LOGIN_PAGE = '/pages/erpLoginConfirm/erpLoginConfirm'
|
||||
const ERP_LOGIN_PAGE_PATH = 'pages/erpLoginConfirm/erpLoginConfirm'
|
||||
|
||||
const decodeRepeated = value => {
|
||||
let text = String(value || '').trim()
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(text)
|
||||
if (decoded === text) {
|
||||
break
|
||||
}
|
||||
text = decoded
|
||||
} catch (error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const extractParam = (value, names) => {
|
||||
const raw = decodeRepeated(value)
|
||||
for (let index = 0; index < names.length; index += 1) {
|
||||
const name = names[index]
|
||||
const matched = raw.match(new RegExp(`(?:^|[?&])${name}=([^&]+)`))
|
||||
if (matched && matched[1]) {
|
||||
return decodeRepeated(matched[1])
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const extractRawParam = (value, names) => {
|
||||
const raw = String(value || '').trim()
|
||||
for (let index = 0; index < names.length; index += 1) {
|
||||
const name = names[index]
|
||||
const matched = raw.match(new RegExp(`(?:^|[?&])${name}=([^&]+)`))
|
||||
if (matched && matched[1]) {
|
||||
return matched[1]
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const erpSceneFromText = value => {
|
||||
const rawText = String(value || '').trim()
|
||||
const rawScene = extractRawParam(rawText, ['scene'])
|
||||
if (rawScene) {
|
||||
const decodedScene = decodeRepeated(rawScene)
|
||||
const nested = erpSceneFromText(decodedScene)
|
||||
if (nested) {
|
||||
return nested
|
||||
}
|
||||
const sceneTicket = extractParam(decodedScene, ['ticket', 't'])
|
||||
const sessionId = extractParam(rawText, ['session_id', 'sessionId', 's'])
|
||||
if (sceneTicket && sessionId) {
|
||||
return `t=${sceneTicket}&s=${sessionId}`
|
||||
}
|
||||
}
|
||||
const raw = decodeRepeated(value)
|
||||
if (!raw) {
|
||||
return ''
|
||||
}
|
||||
const nestedScene = extractParam(raw, ['scene'])
|
||||
if (nestedScene) {
|
||||
const nested = erpSceneFromText(nestedScene)
|
||||
if (nested) {
|
||||
return nested
|
||||
}
|
||||
}
|
||||
const ticket = extractParam(raw, ['ticket', 't'])
|
||||
const sessionId = extractParam(raw, ['session_id', 'sessionId', 's'])
|
||||
if (!ticket || !sessionId) {
|
||||
return ''
|
||||
}
|
||||
return `t=${ticket}&s=${sessionId}`
|
||||
}
|
||||
|
||||
const isErpLoginPath = value => decodeRepeated(value).replace(/^\/+/, '').includes(ERP_LOGIN_PAGE_PATH)
|
||||
|
||||
const buildErpLoginConfirmUrlFromScanResult = scanResult => {
|
||||
const candidates = [
|
||||
scanResult && scanResult.path,
|
||||
scanResult && scanResult.result,
|
||||
scanResult && scanResult.rawData,
|
||||
]
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const candidate = decodeRepeated(candidates[index])
|
||||
if (!candidate) {
|
||||
continue
|
||||
}
|
||||
const scene = erpSceneFromText(candidate)
|
||||
if (scene && (isErpLoginPath(candidate) || !extractParam(scene, ['mold']))) {
|
||||
return `${ERP_LOGIN_PAGE}?scene=${encodeURIComponent(scene)}`
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildErpLoginConfirmUrlFromScanResult,
|
||||
decodeRepeated,
|
||||
}
|
||||
Reference in New Issue
Block a user