chore: initialize mini program frontend

This commit is contained in:
souplearn
2026-06-24 15:19:14 +08:00
commit fa73a3c184
96 changed files with 20561 additions and 0 deletions
+518
View File
@@ -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 })
}
},
})
},
})
+3
View File
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "下班报工"
}
+269
View File
@@ -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>
+290
View File
@@ -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;
}