chore: initialize mini program frontend
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
const pad = value => {
|
||||
const text = String(value)
|
||||
return text.length > 1 ? text : `0${text}`
|
||||
}
|
||||
|
||||
const parseDateValue = value => {
|
||||
const text = String(value || '')
|
||||
if (text.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(text)) {
|
||||
return new Date(text)
|
||||
}
|
||||
return new Date(text.replace(/-/g, '/').replace('T', ' '))
|
||||
}
|
||||
|
||||
const formatDate = value => {
|
||||
const date = parseDateValue(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return ''
|
||||
}
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
}
|
||||
|
||||
const formatTimeValue = value => {
|
||||
const date = parseDateValue(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return [0, 0, 0]
|
||||
}
|
||||
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
||||
}
|
||||
|
||||
const formatTimeText = value => {
|
||||
const parts = formatTimeValue(value)
|
||||
return `${pad(parts[0])}:${pad(parts[1])}:${pad(parts[2])}`
|
||||
}
|
||||
|
||||
const formatTimePartsText = parts => `${pad(parts[0])}:${pad(parts[1])}:${pad(parts[2])}`
|
||||
|
||||
const buildDateTimeValue = (date, timeValue) => (
|
||||
date ? `${date}T${pad(timeValue[0])}:${pad(timeValue[1])}:${pad(timeValue[2])}` : ''
|
||||
)
|
||||
|
||||
const timeOptions = [
|
||||
Array.from({ length: 24 }, (_, index) => pad(index)),
|
||||
Array.from({ length: 60 }, (_, index) => pad(index)),
|
||||
Array.from({ length: 60 }, (_, index) => pad(index)),
|
||||
]
|
||||
|
||||
const productKey = product => `${String(product.projectNo || '').trim()}||${String(product.productName || '').trim()}||${String(product.process || '').trim()}`
|
||||
|
||||
const blockDisplay = block => block.moldDisplayName || block.moldName || block.deviceNo
|
||||
|
||||
const productToItem = (product, startAt = '', availableOptions = null, equipmentOptions = [], deviceNo = '') => {
|
||||
const selectedDeviceNo = deviceNo || ''
|
||||
const deviceIndex = selectedDeviceNo
|
||||
? Math.max(0, equipmentOptions.findIndex(item => item.deviceNo === selectedDeviceNo))
|
||||
: -1
|
||||
return {
|
||||
productIndex: product ? 0 : -1,
|
||||
attendancePointName: product ? product.attendancePointName : '',
|
||||
deviceIndex,
|
||||
deviceNo: selectedDeviceNo,
|
||||
availableOptions: availableOptions || null,
|
||||
startAt,
|
||||
startAtText: startAt ? `${formatDate(startAt)} ${formatTimeText(startAt)}` : '',
|
||||
projectNo: product ? product.projectNo : '',
|
||||
productName: product ? product.productName : '',
|
||||
materialCode: product ? product.materialCode : '',
|
||||
materialName: product ? product.materialName : '',
|
||||
rawMaterialBatchNo: '',
|
||||
rawMaterialBatchOptions: [],
|
||||
rawMaterialBatchIndex: -1,
|
||||
rawMaterialBatchError: false,
|
||||
process: product ? product.process : '',
|
||||
stampingMethod: product ? product.stampingMethod : '',
|
||||
operatorCount: product ? product.operatorCount : 1,
|
||||
isMultiPerson: product ? !!product.isMultiPerson : false,
|
||||
isMisc: product ? !!product.isMisc : false,
|
||||
standardBeat: product ? product.standardBeat : 0,
|
||||
standardWorkload: product ? product.standardWorkload : 0,
|
||||
goodQty: product && product.isMisc ? 0 : '',
|
||||
defectQty: '',
|
||||
remark: '',
|
||||
deviceError: false,
|
||||
}
|
||||
}
|
||||
|
||||
const REPORTING_LIMIT_MINUTES = 15
|
||||
|
||||
Page({
|
||||
data: {
|
||||
sessionId: '',
|
||||
draft: null,
|
||||
remainingText: '',
|
||||
remainingSeconds: 0,
|
||||
reminderShown: false,
|
||||
confirmVisible: false,
|
||||
confirmSummary: null,
|
||||
submitting: false,
|
||||
continuing: false,
|
||||
addStartVisible: false,
|
||||
addStartForm: null,
|
||||
timeOptions,
|
||||
},
|
||||
onLoad(options) {
|
||||
this.setData({ sessionId: options.sessionId || '' })
|
||||
this.load()
|
||||
},
|
||||
onUnload() {
|
||||
this.clearCountdown()
|
||||
},
|
||||
async load() {
|
||||
try {
|
||||
const draft = await api.buildReportDraft(this.data.sessionId)
|
||||
this.setData({ draft }, () => {
|
||||
this.showReportReminder()
|
||||
this.startCountdown(draft)
|
||||
})
|
||||
} catch (error) {
|
||||
if (!this.handleExpiredReport(error)) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
handleExpiredReport(error) {
|
||||
const message = error && error.message ? error.message : ''
|
||||
if (message.indexOf('超过15分钟') === -1) {
|
||||
return false
|
||||
}
|
||||
wx.showModal({
|
||||
title: '报工已超时',
|
||||
content: '请重新扫码点击重新报工,下班时间会更新为重新报工时间。',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.navigateBack({ delta: 1 })
|
||||
},
|
||||
})
|
||||
return true
|
||||
},
|
||||
showReportReminder() {
|
||||
if (this.data.reminderShown) {
|
||||
return
|
||||
}
|
||||
this.setData({ reminderShown: true })
|
||||
wx.showModal({
|
||||
title: '填报提醒',
|
||||
content: `报工填报时限为${REPORTING_LIMIT_MINUTES}分钟,请尽快完成填报`,
|
||||
showCancel: false,
|
||||
})
|
||||
},
|
||||
parseTime(value) {
|
||||
if (!value) {
|
||||
return 0
|
||||
}
|
||||
const text = String(value)
|
||||
if (text.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(text)) {
|
||||
return new Date(text).getTime()
|
||||
}
|
||||
return new Date(text.replace(/-/g, '/').replace('T', ' ')).getTime()
|
||||
},
|
||||
formatRemaining(seconds) {
|
||||
const safeSeconds = Math.max(0, Number(seconds || 0))
|
||||
const minutes = Math.floor(safeSeconds / 60)
|
||||
const second = safeSeconds % 60
|
||||
const minuteText = minutes < 10 ? `0${minutes}` : `${minutes}`
|
||||
const secondText = second < 10 ? `0${second}` : `${second}`
|
||||
return `${minuteText}:${secondText}`
|
||||
},
|
||||
clearCountdown() {
|
||||
if (this.countdownTimer) {
|
||||
clearInterval(this.countdownTimer)
|
||||
this.countdownTimer = null
|
||||
}
|
||||
},
|
||||
startCountdown(draft) {
|
||||
this.clearCountdown()
|
||||
const endTime = this.parseTime(draft && draft.endAt)
|
||||
if (!endTime) {
|
||||
this.setData({ remainingText: '', remainingSeconds: 0 })
|
||||
return
|
||||
}
|
||||
const deadline = endTime + REPORTING_LIMIT_MINUTES * 60 * 1000
|
||||
const tick = () => {
|
||||
const remainingSeconds = Math.max(0, Math.ceil((deadline - Date.now()) / 1000))
|
||||
this.setData({
|
||||
remainingSeconds,
|
||||
remainingText: this.formatRemaining(remainingSeconds),
|
||||
})
|
||||
if (remainingSeconds <= 0) {
|
||||
this.clearCountdown()
|
||||
}
|
||||
}
|
||||
tick()
|
||||
this.countdownTimer = setInterval(tick, 1000)
|
||||
},
|
||||
itemOptions(block, item) {
|
||||
return item.availableOptions || block.options || []
|
||||
},
|
||||
onProductChange(e) {
|
||||
const blockIndex = e.currentTarget.dataset.blockIndex
|
||||
const itemIndex = e.currentTarget.dataset.itemIndex
|
||||
const productIndex = Number(e.detail.value)
|
||||
const block = this.data.draft.deviceBlocks[blockIndex]
|
||||
const currentItem = block.items[itemIndex] || {}
|
||||
const options = this.itemOptions(block, currentItem)
|
||||
const product = options[productIndex]
|
||||
if (!product) {
|
||||
return
|
||||
}
|
||||
const item = productToItem(
|
||||
product,
|
||||
currentItem.startAt || block.startAt || this.data.draft.startAt,
|
||||
currentItem.availableOptions || null,
|
||||
block.equipmentOptions || [],
|
||||
currentItem.deviceNo,
|
||||
)
|
||||
item.productIndex = productIndex
|
||||
item.deviceError = !!currentItem.deviceError && !item.deviceNo
|
||||
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}]`]: item,
|
||||
})
|
||||
},
|
||||
onDeviceChange(e) {
|
||||
const blockIndex = e.currentTarget.dataset.blockIndex
|
||||
const itemIndex = e.currentTarget.dataset.itemIndex
|
||||
const deviceIndex = Number(e.detail.value)
|
||||
const equipment = this.data.draft.deviceBlocks[blockIndex].equipmentOptions[deviceIndex]
|
||||
if (!equipment) {
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceIndex`]: deviceIndex,
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceNo`]: equipment.deviceNo,
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceError`]: false,
|
||||
})
|
||||
},
|
||||
onRawMaterialBatchChange(e) {
|
||||
const blockIndex = e.currentTarget.dataset.blockIndex
|
||||
const itemIndex = e.currentTarget.dataset.itemIndex
|
||||
const batchIndex = Number(e.detail.value)
|
||||
const item = this.data.draft.deviceBlocks[blockIndex].items[itemIndex]
|
||||
const selected = (item.rawMaterialBatchOptions || [])[batchIndex]
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchIndex`]: batchIndex,
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchNo`]: selected,
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchError`]: false,
|
||||
})
|
||||
},
|
||||
onQtyInput(e) {
|
||||
const blockIndex = e.currentTarget.dataset.blockIndex
|
||||
const itemIndex = e.currentTarget.dataset.itemIndex
|
||||
const field = e.currentTarget.dataset.field
|
||||
const updates = {
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].${field}`]: e.detail.value,
|
||||
}
|
||||
if (field === 'changeoverCount') {
|
||||
updates[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].changeoverCountError`] = false
|
||||
}
|
||||
this.setData(updates)
|
||||
},
|
||||
addItem(e) {
|
||||
const blockIndex = e.currentTarget.dataset.blockIndex
|
||||
const block = this.data.draft.deviceBlocks[blockIndex]
|
||||
if (block.options.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const usedKeys = new Set(
|
||||
(block.items || [])
|
||||
.filter(item => item.productName)
|
||||
.map(item => productKey(item)),
|
||||
)
|
||||
const availableOptions = (block.options || []).filter(option => !usedKeys.has(productKey(option)))
|
||||
if (!availableOptions.length) {
|
||||
wx.showToast({ title: '该模具没有可新增的产品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const previousItem = block.items[block.items.length - 1] || {}
|
||||
const defaultStartAt = previousItem.startAt || block.startAt || this.data.draft.startAt
|
||||
this.setData({
|
||||
addStartVisible: true,
|
||||
addStartForm: {
|
||||
blockIndex,
|
||||
availableOptions,
|
||||
previousStartAt: defaultStartAt,
|
||||
date: formatDate(defaultStartAt),
|
||||
timeValue: formatTimeValue(defaultStartAt),
|
||||
timeText: formatTimeText(defaultStartAt),
|
||||
},
|
||||
})
|
||||
},
|
||||
onAddStartDateChange(e) {
|
||||
this.setData({
|
||||
'addStartForm.date': e.detail.value,
|
||||
})
|
||||
},
|
||||
onAddStartTimeChange(e) {
|
||||
const timeValue = e.detail.value.map(value => Number(value))
|
||||
this.setData({
|
||||
'addStartForm.timeValue': timeValue,
|
||||
'addStartForm.timeText': formatTimePartsText(timeValue),
|
||||
})
|
||||
},
|
||||
cancelAddStart() {
|
||||
this.setData({
|
||||
addStartVisible: false,
|
||||
addStartForm: null,
|
||||
})
|
||||
},
|
||||
confirmAddStart() {
|
||||
const form = this.data.addStartForm
|
||||
if (!form) {
|
||||
return
|
||||
}
|
||||
const block = this.data.draft.deviceBlocks[form.blockIndex]
|
||||
const startAt = buildDateTimeValue(form.date, form.timeValue)
|
||||
const startTime = this.parseTime(startAt)
|
||||
const blockStartTime = this.parseTime(block.startAt || this.data.draft.startAt)
|
||||
const blockEndTime = this.parseTime(block.endAt || this.data.draft.endAt)
|
||||
const previousStartTime = this.parseTime(form.previousStartAt)
|
||||
if (!startTime || startTime < blockStartTime || startTime > blockEndTime) {
|
||||
wx.showToast({ title: '开始时间需在本设备工作时间内', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (previousStartTime && startTime < previousStartTime) {
|
||||
wx.showToast({ title: '开始时间不能早于上一产品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const item = productToItem(
|
||||
form.availableOptions && form.availableOptions[0],
|
||||
startAt,
|
||||
form.availableOptions || [],
|
||||
block.equipmentOptions || [],
|
||||
)
|
||||
const items = block.items.concat(item)
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${form.blockIndex}].items`]: items,
|
||||
addStartVisible: false,
|
||||
addStartForm: null,
|
||||
})
|
||||
},
|
||||
removeItem(e) {
|
||||
const blockIndex = e.currentTarget.dataset.blockIndex
|
||||
const itemIndex = e.currentTarget.dataset.itemIndex
|
||||
const block = this.data.draft.deviceBlocks[blockIndex]
|
||||
if (block.items.length <= 1) {
|
||||
return
|
||||
}
|
||||
const items = block.items.filter((_, index) => index !== itemIndex)
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${blockIndex}].items`]: items,
|
||||
})
|
||||
},
|
||||
noop() {},
|
||||
validateDraft(draft) {
|
||||
const blocks = draft.deviceBlocks || []
|
||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
|
||||
const block = blocks[blockIndex]
|
||||
const items = block.items || []
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) {
|
||||
const item = items[itemIndex]
|
||||
if (item.isMisc) {
|
||||
continue
|
||||
}
|
||||
if ((block.options || []).length > 1 && (!item.productName || item.productIndex < 0)) {
|
||||
wx.showToast({
|
||||
title: `${blockDisplay(block)}缺少产品信息`,
|
||||
icon: 'none',
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (!item.deviceNo) {
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceError`]: true,
|
||||
})
|
||||
wx.showToast({
|
||||
title: `${blockDisplay(block)}请选择设备号`,
|
||||
icon: 'none',
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (!item.startAt) {
|
||||
wx.showToast({
|
||||
title: `${blockDisplay(block)}请选择开始工作时间`,
|
||||
icon: 'none',
|
||||
})
|
||||
return false
|
||||
}
|
||||
if ((item.rawMaterialBatchOptions || []).length && !item.rawMaterialBatchNo) {
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchError`]: true,
|
||||
})
|
||||
wx.showToast({
|
||||
title: `${blockDisplay(block)}请选择材料库存批次号`,
|
||||
icon: 'none',
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (item.isContinuousDie) {
|
||||
const text = String(item.changeoverCount || '').trim()
|
||||
const value = Number(text)
|
||||
if (!text || !Number.isInteger(value) || value < 0) {
|
||||
this.setData({
|
||||
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].changeoverCountError`]: true,
|
||||
})
|
||||
wx.showToast({
|
||||
title: `${blockDisplay(block)}请填写非负整数换料次数`,
|
||||
icon: 'none',
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
const goodQty = Number(item.goodQty || 0)
|
||||
const defectQty = Number(item.defectQty || 0)
|
||||
if (!Number.isFinite(goodQty) || goodQty < 0 || !Number.isFinite(defectQty) || defectQty < 0) {
|
||||
wx.showToast({
|
||||
title: `${blockDisplay(block)}产出数量不能为负数`,
|
||||
icon: 'none',
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
buildConfirmSummary(draft) {
|
||||
const totalGood = draft.deviceBlocks.reduce((sum, block) => (
|
||||
sum + block.items.reduce((inner, item) => inner + (item.isMisc ? 0 : Number(item.goodQty || 0)), 0)
|
||||
), 0)
|
||||
const totalDefect = draft.deviceBlocks.reduce((sum, block) => (
|
||||
sum + block.items.reduce((inner, item) => inner + (item.isMisc ? 0 : Number(item.defectQty || 0)), 0)
|
||||
), 0)
|
||||
const hasMisc = draft.deviceBlocks.some(block => (block.items || []).some(item => item.isMisc))
|
||||
return {
|
||||
deviceText: draft.deviceBlocks.map(block => blockDisplay(block)).join('、'),
|
||||
totalGood,
|
||||
totalDefect,
|
||||
note: hasMisc ? '包含处理杂活,提交后由管理员补充杂活事项' : '提交后进入管理员审核',
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
if (this.data.continuing) {
|
||||
return
|
||||
}
|
||||
const draft = this.data.draft
|
||||
if (!this.validateDraft(draft)) {
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
confirmVisible: true,
|
||||
confirmSummary: this.buildConfirmSummary(draft),
|
||||
})
|
||||
},
|
||||
cancelConfirm() {
|
||||
if (this.data.submitting) {
|
||||
return
|
||||
}
|
||||
this.setData({ confirmVisible: false })
|
||||
},
|
||||
async confirmSubmit() {
|
||||
if (this.data.submitting || this.data.continuing) {
|
||||
return
|
||||
}
|
||||
this.setData({ submitting: true })
|
||||
wx.showLoading({ title: '提交中' })
|
||||
try {
|
||||
const report = await api.submitReport(this.data.draft)
|
||||
this.setData({ confirmVisible: false })
|
||||
wx.reLaunch({
|
||||
url: `/pages/reportResult/reportResult?reportId=${report.id}`,
|
||||
})
|
||||
} catch (error) {
|
||||
if (!this.handleExpiredReport(error)) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
this.setData({ submitting: false })
|
||||
}
|
||||
},
|
||||
continueWork() {
|
||||
if (this.data.submitting || this.data.continuing) {
|
||||
return
|
||||
}
|
||||
wx.showModal({
|
||||
title: '继续上班',
|
||||
content: '确认取消本次下班报工填报,继续保持上班状态?当前填写内容不会保存。',
|
||||
confirmText: '继续上班',
|
||||
cancelText: '返回填写',
|
||||
success: async res => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
this.setData({ continuing: true })
|
||||
wx.showLoading({ title: '处理中' })
|
||||
try {
|
||||
await api.continueWork(this.data.sessionId)
|
||||
this.clearCountdown()
|
||||
wx.showToast({ title: '已继续上班', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/index/index',
|
||||
})
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
this.setData({ continuing: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "下班报工"
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<scroll-view class="page safe-bottom" scroll-y type="list">
|
||||
<view class="header">
|
||||
<view class="report-title-row">
|
||||
<view class="report-title-main">
|
||||
<text class="title">下班报工</text>
|
||||
<text class="subtitle">按模具和设备填写产量,提交后进入审核</text>
|
||||
</view>
|
||||
<view wx:if="{{remainingText}}" class="countdown-pill">
|
||||
<text class="countdown-label">剩余</text>
|
||||
<text class="countdown-value">{{remainingText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<block wx:if="{{draft}}">
|
||||
<view class="card">
|
||||
<view class="row">
|
||||
<view>
|
||||
<text class="label">上班打卡时间</text>
|
||||
<text class="value">{{draft.startAtText}}</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text class="label">下班打卡时间</text>
|
||||
<text class="value">{{draft.endAtText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="row">
|
||||
<text class="muted">考勤点</text>
|
||||
<text class="value">{{draft.attendancePointName || '-'}}</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="row">
|
||||
<text class="muted">报工时长</text>
|
||||
<text class="value">{{draft.durationMinutes}} 分钟</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
wx:for="{{draft.deviceBlocks}}"
|
||||
wx:key="blockKey"
|
||||
wx:for-index="blockIndex"
|
||||
wx:for-item="block"
|
||||
class="card device-card"
|
||||
>
|
||||
<view class="row device-head">
|
||||
<view>
|
||||
<text class="label">模具名称</text>
|
||||
<text class="value">{{block.moldDisplayName || block.moldName || block.deviceNo}}</text>
|
||||
<text wx:if="{{block.attendancePointName}}" class="subtitle">考勤点 {{block.attendancePointName}}</text>
|
||||
<text class="subtitle">累计工时 {{block.durationMinutes || 0}} 分钟</text>
|
||||
</view>
|
||||
<button
|
||||
wx:if="{{block.canAddItem}}"
|
||||
class="btn mini secondary add-product-btn"
|
||||
data-block-index="{{blockIndex}}"
|
||||
bindtap="addItem"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!block.options.length}}" class="list-empty">该模具暂无产品</view>
|
||||
|
||||
<view
|
||||
wx:for="{{block.items}}"
|
||||
wx:key="index"
|
||||
wx:for-index="itemIndex"
|
||||
wx:for-item="reportItem"
|
||||
class="item-box {{reportItem.isMultiPerson || reportItem.isContinuousDie ? 'multi-item-box' : ''}} {{reportItem.isMultiPerson && reportItem.isContinuousDie ? 'stacked-item-box' : ''}}"
|
||||
>
|
||||
<text wx:if="{{reportItem.isMultiPerson}}" class="multi-item-stamp">多人协作</text>
|
||||
<text wx:if="{{reportItem.isContinuousDie}}" class="continuous-item-stamp {{reportItem.isMultiPerson ? 'lower-item-stamp' : ''}}">连续模</text>
|
||||
<view class="row">
|
||||
<text class="pill primary">报工 {{itemIndex + 1}}</text>
|
||||
<button
|
||||
wx:if="{{block.items.length > 1}}"
|
||||
class="btn mini secondary"
|
||||
data-block-index="{{blockIndex}}"
|
||||
data-item-index="{{itemIndex}}"
|
||||
bindtap="removeItem"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">开始工作时间</text>
|
||||
<view class="readonly">{{reportItem.startAtText || '-'}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{reportItem.isMisc}}" class="misc-auto-box">
|
||||
<text class="label">处理杂活</text>
|
||||
<text class="value">系统自动生成该报工卡片,只记录本段工时,杂活事项由管理员审核时备注。</text>
|
||||
<text class="subtitle">累计工时 {{block.durationMinutes || 0}} 分钟</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!reportItem.isMisc}}" class="form-item">
|
||||
<text class="label">设备号</text>
|
||||
<picker
|
||||
wx:if="{{block.equipmentOptions.length}}"
|
||||
mode="selector"
|
||||
range="{{block.equipmentOptions}}"
|
||||
range-key="displayName"
|
||||
value="{{reportItem.deviceIndex}}"
|
||||
data-block-index="{{blockIndex}}"
|
||||
data-item-index="{{itemIndex}}"
|
||||
bindchange="onDeviceChange"
|
||||
>
|
||||
<view class="picker {{reportItem.deviceError ? 'field-error' : ''}}">{{reportItem.deviceNo || '请选择设备号'}}</view>
|
||||
</picker>
|
||||
<view wx:else class="readonly {{reportItem.deviceError ? 'field-error' : ''}}">暂无可选设备</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!reportItem.isMisc}}" class="form-item">
|
||||
<text class="label">材料库存批次号</text>
|
||||
<picker
|
||||
wx:if="{{reportItem.rawMaterialBatchOptions.length}}"
|
||||
mode="selector"
|
||||
range="{{reportItem.rawMaterialBatchOptions}}"
|
||||
value="{{reportItem.rawMaterialBatchIndex}}"
|
||||
data-block-index="{{blockIndex}}"
|
||||
data-item-index="{{itemIndex}}"
|
||||
bindchange="onRawMaterialBatchChange"
|
||||
>
|
||||
<view class="picker {{reportItem.rawMaterialBatchError ? 'field-error' : ''}}">{{reportItem.rawMaterialBatchNo || '请选择材料库存批次号'}}</view>
|
||||
</picker>
|
||||
<view wx:else class="readonly">暂无可选材料库存批次号</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{reportItem.isContinuousDie}}" class="form-item">
|
||||
<text class="label">换料次数</text>
|
||||
<input
|
||||
class="input number-input {{reportItem.changeoverCountError ? 'field-error' : ''}}"
|
||||
type="number"
|
||||
value="{{reportItem.changeoverCount}}"
|
||||
placeholder="请输入换料次数"
|
||||
data-block-index="{{blockIndex}}"
|
||||
data-item-index="{{itemIndex}}"
|
||||
data-field="changeoverCount"
|
||||
bindinput="onQtyInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!reportItem.isMisc}}" class="readonly-grid">
|
||||
<view>
|
||||
<text class="label">产品名称</text>
|
||||
<text class="value">{{reportItem.productName || '-'}}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="label">考勤点</text>
|
||||
<text class="value">{{reportItem.attendancePointName || block.attendancePointName || '-'}}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="label">项目号</text>
|
||||
<text class="value">{{reportItem.projectNo || '-'}}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="label">物料编码</text>
|
||||
<text class="value">{{reportItem.materialCode || '-'}}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="label">工序</text>
|
||||
<text class="value">{{reportItem.process || '-'}}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="label">冲压方式</text>
|
||||
<text class="value">{{reportItem.stampingMethod || '-'}}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="label">标准节拍</text>
|
||||
<text class="value">{{reportItem.standardBeat || 0}} 秒</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!reportItem.isMisc}}" class="qty-grid">
|
||||
<view class="form-item">
|
||||
<text class="label">成品数量</text>
|
||||
<input
|
||||
class="input number-input"
|
||||
type="number"
|
||||
value="{{reportItem.goodQty}}"
|
||||
data-block-index="{{blockIndex}}"
|
||||
data-item-index="{{itemIndex}}"
|
||||
data-field="goodQty"
|
||||
bindinput="onQtyInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">不良数量</text>
|
||||
<input
|
||||
class="input number-input"
|
||||
type="number"
|
||||
value="{{reportItem.defectQty}}"
|
||||
data-block-index="{{blockIndex}}"
|
||||
data-item-index="{{itemIndex}}"
|
||||
data-field="defectQty"
|
||||
bindinput="onQtyInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn primary full submit-btn" disabled="{{continuing}}" bindtap="submit">提交报工</button>
|
||||
<button
|
||||
class="btn secondary full continue-work-btn"
|
||||
disabled="{{submitting || continuing}}"
|
||||
bindtap="continueWork"
|
||||
>
|
||||
{{continuing ? '处理中' : '继续上班'}}
|
||||
</button>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
<view wx:if="{{confirmVisible}}" class="confirm-mask" catchtap="cancelConfirm">
|
||||
<view class="confirm-panel" catchtap="noop">
|
||||
<text class="confirm-title">确认提交报工</text>
|
||||
<view class="confirm-lines">
|
||||
<view class="confirm-line">
|
||||
<text class="confirm-label">模具</text>
|
||||
<text class="confirm-value">{{confirmSummary.deviceText}}</text>
|
||||
</view>
|
||||
<view class="confirm-line">
|
||||
<text class="confirm-label">成品数量</text>
|
||||
<text class="confirm-value">{{confirmSummary.totalGood}}</text>
|
||||
</view>
|
||||
<view class="confirm-line">
|
||||
<text class="confirm-label">不良数量</text>
|
||||
<text class="confirm-value">{{confirmSummary.totalDefect}}</text>
|
||||
</view>
|
||||
<view class="confirm-line">
|
||||
<text class="confirm-label">审核说明</text>
|
||||
<text class="confirm-value">{{confirmSummary.note}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="confirm-actions">
|
||||
<button class="btn secondary confirm-btn" bindtap="cancelConfirm">取消</button>
|
||||
<button class="btn primary confirm-btn" disabled="{{submitting}}" bindtap="confirmSubmit">{{submitting ? '提交中' : '确定'}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{addStartVisible}}" class="confirm-mask" catchtap="cancelAddStart">
|
||||
<view class="confirm-panel add-start-panel" catchtap="noop">
|
||||
<text class="confirm-title">新增产品开始时间</text>
|
||||
<view class="form-item">
|
||||
<text class="label">开始日期</text>
|
||||
<picker mode="date" value="{{addStartForm.date}}" bindchange="onAddStartDateChange">
|
||||
<view class="picker">{{addStartForm.date}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">开始时间</text>
|
||||
<picker
|
||||
mode="multiSelector"
|
||||
range="{{timeOptions}}"
|
||||
value="{{addStartForm.timeValue}}"
|
||||
bindchange="onAddStartTimeChange"
|
||||
>
|
||||
<view class="picker">{{addStartForm.timeText}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="confirm-actions">
|
||||
<button class="btn secondary confirm-btn" bindtap="cancelAddStart">取消</button>
|
||||
<button class="btn primary confirm-btn" bindtap="confirmAddStart">确定新增</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,290 @@
|
||||
.report-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.report-title-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.countdown-pill {
|
||||
flex: 0 0 auto;
|
||||
min-width: 132rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
border: 1rpx solid #ffd9a8;
|
||||
border-radius: 16rpx;
|
||||
background: #fff7e8;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.countdown-label {
|
||||
display: block;
|
||||
color: #a95605;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.countdown-value {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
color: #7a3d00;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
padding-bottom: 8rpx;
|
||||
border-left: 6rpx solid #1463ff;
|
||||
}
|
||||
|
||||
.device-head {
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.btn.mini.add-product-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 78rpx;
|
||||
min-width: 78rpx;
|
||||
height: 78rpx;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
border-radius: 20rpx;
|
||||
font-size: 68rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.item-box {
|
||||
position: relative;
|
||||
margin-top: 18rpx;
|
||||
padding: 22rpx 18rpx 18rpx;
|
||||
border: 1rpx solid #dce4ef;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
|
||||
box-shadow:
|
||||
0 10rpx 0 rgba(205, 216, 229, 0.5),
|
||||
0 16rpx 24rpx rgba(22, 34, 50, 0.08),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.94);
|
||||
}
|
||||
|
||||
.item-box.multi-item-box {
|
||||
padding-top: 72rpx;
|
||||
}
|
||||
|
||||
.item-box.stacked-item-box {
|
||||
padding-top: 104rpx;
|
||||
}
|
||||
|
||||
.multi-item-stamp {
|
||||
position: absolute;
|
||||
right: 18rpx;
|
||||
top: 18rpx;
|
||||
z-index: 2;
|
||||
padding: 7rpx 14rpx;
|
||||
border: 2rpx solid rgba(217, 119, 6, 0.62);
|
||||
border-radius: 10rpx;
|
||||
color: rgba(154, 74, 5, 0.88);
|
||||
background: rgba(255, 247, 232, 0.92);
|
||||
font-size: 23rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.continuous-item-stamp {
|
||||
position: absolute;
|
||||
right: 18rpx;
|
||||
top: 18rpx;
|
||||
z-index: 2;
|
||||
padding: 7rpx 14rpx;
|
||||
border: 2rpx solid rgba(217, 119, 6, 0.62);
|
||||
border-radius: 10rpx;
|
||||
color: rgba(154, 74, 5, 0.88);
|
||||
background: rgba(255, 247, 232, 0.92);
|
||||
font-size: 23rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.lower-item-stamp {
|
||||
top: 60rpx;
|
||||
}
|
||||
|
||||
.readonly {
|
||||
min-height: 78rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
border: 1rpx solid #ccd8e6;
|
||||
border-radius: 14rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
box-shadow:
|
||||
inset 0 4rpx 8rpx rgba(22, 34, 50, 0.05),
|
||||
0 6rpx 0 rgba(205, 216, 229, 0.45),
|
||||
0 10rpx 18rpx rgba(22, 34, 50, 0.06);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.picker.field-error,
|
||||
.readonly.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);
|
||||
}
|
||||
|
||||
.readonly-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18rpx 20rpx;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.qty-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.misc-auto-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
margin-top: 16rpx;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #f3b35b;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(180deg, #fffaf1 0%, #fff3df 100%);
|
||||
box-shadow:
|
||||
0 8rpx 0 rgba(220, 139, 34, 0.32),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 30rpx;
|
||||
height: 92rpx;
|
||||
line-height: 92rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.continue-work-btn {
|
||||
margin: 28rpx auto 16rpx;
|
||||
width: 430rpx;
|
||||
height: 78rpx;
|
||||
line-height: 78rpx;
|
||||
color: #1463ff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.add-start-panel .form-item {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.confirm-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 80;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 44rpx;
|
||||
background: rgba(15, 23, 42, 0.52);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.confirm-panel {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 44rpx 34rpx 40rpx;
|
||||
border: 1rpx solid #e4e9f1;
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
||||
box-shadow:
|
||||
0 16rpx 0 rgba(170, 184, 204, 0.64),
|
||||
0 30rpx 64rpx rgba(15, 23, 42, 0.28),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
display: block;
|
||||
margin-bottom: 28rpx;
|
||||
color: #162232;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirm-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 22rpx;
|
||||
border: 1rpx solid #dce4ef;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
box-shadow:
|
||||
inset 0 4rpx 8rpx rgba(22, 34, 50, 0.05),
|
||||
0 8rpx 0 rgba(205, 216, 229, 0.48),
|
||||
0 12rpx 18rpx rgba(22, 34, 50, 0.06);
|
||||
}
|
||||
|
||||
.confirm-line {
|
||||
display: grid;
|
||||
grid-template-columns: 150rpx minmax(0, 1fr);
|
||||
gap: 18rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.confirm-label {
|
||||
color: #667085;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.confirm-value {
|
||||
min-width: 0;
|
||||
color: #162232;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18rpx;
|
||||
margin-top: 30rpx;
|
||||
padding: 0 2rpx 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.15;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
Reference in New Issue
Block a user