chore: initialize mini program frontend
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
const api = require('../../utils/api')
|
||||
const locationGuard = require('../../utils/locationGuard')
|
||||
|
||||
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 itemKey = item => `${item.attendancePointName || ''}||${item.moldName || item.productName || item.name}||${item.processName || ''}`
|
||||
|
||||
Page({
|
||||
data: {
|
||||
items: [],
|
||||
attendancePointName: '',
|
||||
equipmentOptions: [],
|
||||
equipmentLabels: [],
|
||||
filling: false,
|
||||
submitting: false,
|
||||
deviceModalVisible: false,
|
||||
activeDeviceIndex: -1,
|
||||
deviceSelectOptions: [],
|
||||
pendingDeviceNos: [],
|
||||
currentLocation: null,
|
||||
},
|
||||
onLoad(options) {
|
||||
const user = api.getCurrentUser()
|
||||
if (!user || user.role !== 'worker') {
|
||||
wx.showToast({ title: '请先用冲压工人登录', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({ url: '/pages/index/index' })
|
||||
}, 800)
|
||||
return
|
||||
}
|
||||
const attendancePointName = decodeRepeated(options.attendancePointName || '')
|
||||
this.setData({ attendancePointName })
|
||||
this.loadCleaningEquipment(attendancePointName)
|
||||
this.addResolvedItem({
|
||||
attendancePointName,
|
||||
moldName: decodeRepeated(options.moldName || ''),
|
||||
processName: decodeRepeated(options.processName || ''),
|
||||
stampingMethod: decodeRepeated(options.stampingMethod || ''),
|
||||
isCleaning: options.isCleaning === '1',
|
||||
displayName: decodeRepeated(options.displayName || ''),
|
||||
})
|
||||
},
|
||||
addResolvedItem(raw) {
|
||||
const item = {
|
||||
attendancePointName: String(raw.attendancePointName || this.data.attendancePointName || '').trim(),
|
||||
moldName: String(raw.moldName || raw.name || '').trim(),
|
||||
processName: String(raw.processName || '').trim(),
|
||||
stampingMethod: String(raw.stampingMethod || '').trim(),
|
||||
isCleaning: !!raw.isCleaning || String(raw.stampingMethod || '').trim() === '清洗',
|
||||
displayName: raw.displayName || api.moldDisplayName(raw.moldName || raw.name, raw.processName, raw.stampingMethod),
|
||||
deviceNo: '',
|
||||
deviceNos: [],
|
||||
quantity: '',
|
||||
}
|
||||
if (!item.moldName) {
|
||||
wx.showToast({ title: '未识别清洗产品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!item.processName) {
|
||||
wx.showToast({ title: '未识别工序', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!item.isCleaning) {
|
||||
wx.showToast({ title: '只能添加冲压方式为清洗的二维码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.data.attendancePointName && item.attendancePointName !== this.data.attendancePointName) {
|
||||
wx.showToast({ title: '只能添加同一考勤点的清洗产品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const exists = this.data.items.some(existing => itemKey(existing) === itemKey(item))
|
||||
if (exists) {
|
||||
wx.showToast({ title: '该清洗产品已添加', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
items: this.data.items.concat(item),
|
||||
})
|
||||
},
|
||||
async loadCleaningEquipment(attendancePointName = this.data.attendancePointName) {
|
||||
try {
|
||||
const result = await api.listEquipment({
|
||||
attendancePointName,
|
||||
deviceType: '清洗设备',
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
})
|
||||
const equipmentOptions = result.rows || []
|
||||
this.setData({
|
||||
equipmentOptions,
|
||||
equipmentLabels: equipmentOptions.map(item => item.displayName || item.deviceNo),
|
||||
})
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || '加载清洗设备失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
extractScene(scanResult) {
|
||||
const raw = decodeRepeated(scanResult)
|
||||
if (!raw) {
|
||||
return ''
|
||||
}
|
||||
const patterns = [
|
||||
/(?:^|[?&])scene=([^&]+)/i,
|
||||
/(?:^|[?&])mold=([^&]+)/i,
|
||||
]
|
||||
for (let index = 0; index < patterns.length; index += 1) {
|
||||
const matched = raw.match(patterns[index])
|
||||
if (matched && matched[1]) {
|
||||
const value = decodeRepeated(matched[1])
|
||||
return value.startsWith('mold=') ? value : `mold=${value}`
|
||||
}
|
||||
}
|
||||
return raw.startsWith('mold=') ? raw : ''
|
||||
},
|
||||
async addByScanResult(scanResult) {
|
||||
const scene = this.extractScene(scanResult)
|
||||
if (!scene) {
|
||||
wx.showToast({ title: '未识别清洗二维码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const resolved = await api.resolveMoldScene(scene)
|
||||
await locationGuard.ensureFactoryLocation(resolved.attendancePointName)
|
||||
this.addResolvedItem(resolved)
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || '二维码解析失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
startFill() {
|
||||
if (!this.data.items.length) {
|
||||
wx.showToast({ title: '请先添加清洗产品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ filling: true })
|
||||
},
|
||||
async continueAdd() {
|
||||
try {
|
||||
await locationGuard.ensureFactoryLocation(this.data.attendancePointName)
|
||||
} catch (error) {
|
||||
wx.showModal({
|
||||
title: '无法继续扫码',
|
||||
content: error.message || '当前位置不在允许范围内',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.scanCode({
|
||||
onlyFromCamera: false,
|
||||
success: res => {
|
||||
const candidates = [res.path, res.result, res.rawData]
|
||||
const raw = candidates.find(item => item) || ''
|
||||
this.addByScanResult(raw)
|
||||
},
|
||||
})
|
||||
},
|
||||
abandon() {
|
||||
wx.showModal({
|
||||
title: '放弃报工',
|
||||
content: '确认放弃本次清洗报工?',
|
||||
confirmText: '放弃',
|
||||
confirmColor: '#d92d20',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
wx.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
onQuantityInput(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
this.setData({
|
||||
[`items[${index}].quantity`]: e.detail.value,
|
||||
})
|
||||
},
|
||||
noop() {},
|
||||
openDeviceSelector(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const item = this.data.items[index]
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
if (!this.data.equipmentOptions.length) {
|
||||
wx.showToast({ title: '暂无清洗设备', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const selected = new Set(item.deviceNos && item.deviceNos.length ? item.deviceNos : (item.deviceNo ? String(item.deviceNo).split('、') : []))
|
||||
const deviceSelectOptions = this.data.equipmentOptions.map(equipment => ({
|
||||
...equipment,
|
||||
checked: selected.has(equipment.deviceNo),
|
||||
}))
|
||||
this.setData({
|
||||
activeDeviceIndex: index,
|
||||
pendingDeviceNos: Array.from(selected).filter(Boolean),
|
||||
deviceSelectOptions,
|
||||
deviceModalVisible: true,
|
||||
})
|
||||
},
|
||||
closeDeviceSelector() {
|
||||
this.setData({
|
||||
deviceModalVisible: false,
|
||||
activeDeviceIndex: -1,
|
||||
deviceSelectOptions: [],
|
||||
pendingDeviceNos: [],
|
||||
})
|
||||
},
|
||||
onDeviceSelectionChange(e) {
|
||||
const pendingDeviceNos = e.detail.value || []
|
||||
const selected = new Set(pendingDeviceNos)
|
||||
this.setData({
|
||||
pendingDeviceNos,
|
||||
deviceSelectOptions: this.data.deviceSelectOptions.map(item => ({
|
||||
...item,
|
||||
checked: selected.has(item.deviceNo),
|
||||
})),
|
||||
})
|
||||
},
|
||||
confirmDeviceSelection() {
|
||||
const index = Number(this.data.activeDeviceIndex)
|
||||
if (index < 0 || !this.data.items[index]) {
|
||||
this.closeDeviceSelector()
|
||||
return
|
||||
}
|
||||
if (!this.data.pendingDeviceNos.length) {
|
||||
wx.showToast({ title: '请至少选择一台清洗设备', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const deviceNos = this.data.pendingDeviceNos
|
||||
this.setData({
|
||||
[`items[${index}].deviceNos`]: deviceNos,
|
||||
[`items[${index}].deviceNo`]: deviceNos.join('、'),
|
||||
deviceModalVisible: false,
|
||||
activeDeviceIndex: -1,
|
||||
deviceSelectOptions: [],
|
||||
pendingDeviceNos: [],
|
||||
})
|
||||
},
|
||||
async submit() {
|
||||
if (this.data.submitting) {
|
||||
return
|
||||
}
|
||||
const missingDevice = this.data.items.find(item => !item.deviceNo)
|
||||
if (missingDevice) {
|
||||
wx.showToast({ title: `${missingDevice.moldName}请选择清洗设备号`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
const invalid = this.data.items.find(item => Number(item.quantity || 0) <= 0)
|
||||
if (invalid) {
|
||||
wx.showToast({ title: `${invalid.moldName}请填写清洗数量`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ submitting: true })
|
||||
wx.showLoading({ title: '提交中' })
|
||||
try {
|
||||
const locationResult = await locationGuard.ensureFactoryLocation(this.data.attendancePointName)
|
||||
const report = await api.submitCleaningReport(this.data.items, locationResult.current)
|
||||
wx.reLaunch({
|
||||
url: `/pages/reportResult/reportResult?reportId=${report.id}`,
|
||||
})
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
this.setData({ submitting: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "清洗报工"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<scroll-view class="page safe-bottom" scroll-y type="list">
|
||||
<view class="header">
|
||||
<text class="title">清洗报工</text>
|
||||
<text class="subtitle">添加本次清洗产品,只填写清洗数量,不计算工时</text>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="row">
|
||||
<view class="row-left">
|
||||
<text class="label">已添加清洗产品</text>
|
||||
<text class="value">{{items.length}} 项</text>
|
||||
<text wx:if="{{attendancePointName}}" class="subtitle">考勤点 {{attendancePointName}}</text>
|
||||
</view>
|
||||
<text class="pill primary">清洗</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!items.length}}" class="list-empty">暂无清洗产品</view>
|
||||
<view wx:for="{{items}}" wx:key="displayName" wx:for-index="index" class="card cleaning-card">
|
||||
<view class="row">
|
||||
<view class="row-left">
|
||||
<text class="value">{{item.displayName || item.moldName}}</text>
|
||||
<text wx:if="{{item.attendancePointName}}" class="subtitle">考勤点 {{item.attendancePointName}}</text>
|
||||
<text class="subtitle">工序 {{item.processName}} · 冲压方式 {{item.stampingMethod || '-'}}</text>
|
||||
</view>
|
||||
<text class="pill success">已添加</text>
|
||||
</view>
|
||||
<view wx:if="{{filling}}" class="form-item">
|
||||
<text class="label">清洗设备号</text>
|
||||
<view
|
||||
wx:if="{{equipmentOptions.length}}"
|
||||
class="picker multi-device-picker {{item.deviceNo ? '' : 'field-error'}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="openDeviceSelector"
|
||||
>
|
||||
{{item.deviceNo || '请选择清洗设备号(可多选)'}}
|
||||
</view>
|
||||
<view wx:else class="readonly">暂无清洗设备,请先在设备管理中维护</view>
|
||||
</view>
|
||||
<view wx:if="{{filling}}" class="form-item">
|
||||
<text class="label">清洗数量</text>
|
||||
<input
|
||||
class="input number-input"
|
||||
type="digit"
|
||||
value="{{item.quantity}}"
|
||||
placeholder="请输入清洗数量"
|
||||
data-index="{{index}}"
|
||||
bindinput="onQuantityInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!filling}}" class="cleaning-actions">
|
||||
<button class="btn primary full" bindtap="startFill">开始报工</button>
|
||||
<button class="btn secondary full" bindtap="continueAdd">继续添加清洗产品</button>
|
||||
<button class="btn danger full" bindtap="abandon">放弃报工</button>
|
||||
</view>
|
||||
|
||||
<view wx:else class="cleaning-actions">
|
||||
<button class="btn secondary full" bindtap="continueAdd">继续添加清洗产品</button>
|
||||
<button class="btn primary full" disabled="{{submitting}}" bindtap="submit">{{submitting ? '提交中' : '提交清洗报工'}}</button>
|
||||
<button class="btn danger full" bindtap="abandon">放弃报工</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{deviceModalVisible}}" class="modal-mask" catchtap="closeDeviceSelector">
|
||||
<view class="modal-panel device-modal-panel" catchtap="noop">
|
||||
<view class="device-modal-head">
|
||||
<view>
|
||||
<text class="device-modal-title">选择清洗设备</text>
|
||||
<text class="subtitle">已选择 {{pendingDeviceNos.length}} 台</text>
|
||||
</view>
|
||||
</view>
|
||||
<checkbox-group bindchange="onDeviceSelectionChange">
|
||||
<scroll-view class="device-option-list" scroll-y>
|
||||
<label
|
||||
wx:for="{{deviceSelectOptions}}"
|
||||
wx:key="deviceNo"
|
||||
class="device-option {{item.checked ? 'checked' : ''}}"
|
||||
>
|
||||
<checkbox value="{{item.deviceNo}}" checked="{{item.checked}}" />
|
||||
<view class="device-option-text">
|
||||
<text class="device-option-title">{{item.deviceNo}}</text>
|
||||
<text wx:if="{{item.remark}}" class="device-option-subtitle">{{item.remark}}</text>
|
||||
</view>
|
||||
</label>
|
||||
</scroll-view>
|
||||
</checkbox-group>
|
||||
<view class="device-modal-actions">
|
||||
<button class="btn secondary full" bindtap="closeDeviceSelector">取消</button>
|
||||
<button class="btn primary full" bindtap="confirmDeviceSelection">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,142 @@
|
||||
.cleaning-card {
|
||||
border-left: 6rpx solid #12a375;
|
||||
}
|
||||
|
||||
.cleaning-card .form-item {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.cleaning-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 22rpx;
|
||||
margin-top: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.cleaning-actions .btn {
|
||||
width: 88%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.multi-device-picker {
|
||||
min-height: 84rpx;
|
||||
line-height: 1.35;
|
||||
padding-top: 22rpx;
|
||||
padding-bottom: 22rpx;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.multi-device-picker.field-error {
|
||||
border-color: #e5484d;
|
||||
background: linear-gradient(180deg, #fff7f7 0%, #fff1f1 100%);
|
||||
color: #b42318;
|
||||
box-shadow:
|
||||
inset 0 4rpx 8rpx rgba(180, 35, 24, 0.08),
|
||||
0 0 0 4rpx rgba(229, 72, 77, 0.12),
|
||||
0 6rpx 0 rgba(255, 197, 197, 0.65),
|
||||
0 10rpx 18rpx rgba(180, 35, 24, 0.08);
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 30rpx 22rpx calc(env(safe-area-inset-bottom) + 30rpx);
|
||||
background: rgba(15, 23, 42, 0.48);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-modal-panel {
|
||||
width: 100%;
|
||||
max-height: 86vh;
|
||||
overflow-y: auto;
|
||||
padding: 30rpx;
|
||||
border: 1rpx solid #e4e9f1;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
||||
box-shadow:
|
||||
0 16rpx 0 rgba(170, 184, 204, 0.62),
|
||||
0 30rpx 64rpx rgba(15, 23, 42, 0.24),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-modal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.device-modal-title {
|
||||
display: block;
|
||||
color: #162232;
|
||||
font-size: 30rpx;
|
||||
font-weight: 850;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.device-option-list {
|
||||
height: 56vh;
|
||||
margin-bottom: 22rpx;
|
||||
border: 2rpx solid #b8c6dc;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 10rpx 0 rgba(174, 188, 208, 0.46),
|
||||
0 18rpx 28rpx rgba(22, 34, 50, 0.12),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.device-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 18rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-bottom: 1rpx solid #e6edf6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-option.checked {
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.device-option:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.device-option-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.device-option-title {
|
||||
color: #162232;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.device-option-subtitle {
|
||||
color: #667085;
|
||||
font-size: 24rpx;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.device-modal-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user