chore: initialize mini program frontend
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
keyword: '',
|
||||
selectedMold: null,
|
||||
moldOptions: [],
|
||||
filteredMoldOptions: [],
|
||||
batchMoldOptions: [],
|
||||
batchKeyword: '',
|
||||
dropdownVisible: false,
|
||||
selectedMoldKeys: [],
|
||||
selectedMoldCount: 0,
|
||||
allFilteredSelected: false,
|
||||
batchModalVisible: false,
|
||||
batchGenerating: false,
|
||||
generatedMoldName: '',
|
||||
qrPath: '',
|
||||
qrUrl: '',
|
||||
qrImageUrl: '',
|
||||
qrLoadError: '',
|
||||
taskRows: [],
|
||||
taskPage: 1,
|
||||
taskPageInput: '1',
|
||||
taskPageSize: 5,
|
||||
taskTotal: 0,
|
||||
taskTotalPages: 1,
|
||||
taskLoading: false,
|
||||
swipedTaskId: null,
|
||||
},
|
||||
onShow() {
|
||||
this.loadMolds()
|
||||
this.loadBatchTasks()
|
||||
this.startTaskPolling()
|
||||
},
|
||||
onHide() {
|
||||
this.stopTaskPolling()
|
||||
},
|
||||
onUnload() {
|
||||
this.stopTaskPolling()
|
||||
},
|
||||
startTaskPolling() {
|
||||
this.stopTaskPolling()
|
||||
this.taskTimer = setInterval(() => {
|
||||
this.loadBatchTasks({ silent: true })
|
||||
}, 5000)
|
||||
},
|
||||
stopTaskPolling() {
|
||||
if (this.taskTimer) {
|
||||
clearInterval(this.taskTimer)
|
||||
this.taskTimer = null
|
||||
}
|
||||
},
|
||||
async loadMolds() {
|
||||
try {
|
||||
const moldOptions = await api.listMoldNames({ limit: 500 })
|
||||
const availableKeys = new Set(moldOptions.map(item => item.uniqueKey))
|
||||
const selectedMoldKeys = this.data.selectedMoldKeys.filter(key => availableKeys.has(key))
|
||||
this.setData({
|
||||
moldOptions,
|
||||
selectedMoldKeys,
|
||||
selectedMoldCount: selectedMoldKeys.length,
|
||||
...this.buildFilteredState(this.data.keyword, moldOptions, selectedMoldKeys),
|
||||
})
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || '加载模具失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async loadBatchTasks(options = {}) {
|
||||
if (this.data.taskLoading && options.silent) {
|
||||
return
|
||||
}
|
||||
if (!options.silent) {
|
||||
this.setData({ taskLoading: true })
|
||||
}
|
||||
try {
|
||||
const result = await api.listMoldQrBatchTasks({
|
||||
page: this.data.taskPage,
|
||||
pageSize: this.data.taskPageSize,
|
||||
})
|
||||
this.setData({
|
||||
taskRows: result.rows,
|
||||
taskPage: result.page,
|
||||
taskPageInput: String(result.page || 1),
|
||||
taskTotal: result.total,
|
||||
taskTotalPages: result.totalPages,
|
||||
swipedTaskId: result.rows.some(row => row.id === this.data.swipedTaskId) ? this.data.swipedTaskId : null,
|
||||
})
|
||||
} catch (error) {
|
||||
if (!options.silent) {
|
||||
wx.showToast({ title: error.message || '加载任务失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
if (!options.silent) {
|
||||
this.setData({ taskLoading: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
filterMolds(keyword, source = this.data.moldOptions) {
|
||||
const text = String(keyword || '').trim().toLowerCase()
|
||||
if (!text) {
|
||||
return source.slice(0, 500)
|
||||
}
|
||||
return source.filter(item => (
|
||||
String(item.displayName || '').toLowerCase().includes(text)
|
||||
|| String(item.moldName || item.name || '').toLowerCase().includes(text)
|
||||
|| String(item.processName || '').toLowerCase().includes(text)
|
||||
|| String(item.stampingMethod || '').toLowerCase().includes(text)
|
||||
|| String(item.attendancePointName || '').toLowerCase().includes(text)
|
||||
)).slice(0, 500)
|
||||
},
|
||||
buildFilteredState(keyword, source = this.data.moldOptions, selectedKeys = this.data.selectedMoldKeys) {
|
||||
const selectedSet = new Set(selectedKeys || [])
|
||||
const filteredMoldOptions = this.filterMolds(keyword, source).map(item => ({
|
||||
...item,
|
||||
checked: selectedSet.has(item.uniqueKey),
|
||||
}))
|
||||
return {
|
||||
filteredMoldOptions,
|
||||
allFilteredSelected: filteredMoldOptions.length > 0 && filteredMoldOptions.every(item => item.checked),
|
||||
}
|
||||
},
|
||||
buildBatchState(selectedKeys = this.data.selectedMoldKeys, keyword = this.data.batchKeyword) {
|
||||
const selectedSet = new Set(selectedKeys || [])
|
||||
const batchMoldOptions = this.filterMolds(keyword, this.data.moldOptions).map(item => ({
|
||||
...item,
|
||||
checked: selectedSet.has(item.uniqueKey),
|
||||
}))
|
||||
return {
|
||||
batchMoldOptions,
|
||||
allFilteredSelected: batchMoldOptions.length > 0 && batchMoldOptions.every(item => item.checked),
|
||||
}
|
||||
},
|
||||
setBatchSelection(selectedMoldKeys) {
|
||||
this.setData({
|
||||
selectedMoldKeys,
|
||||
selectedMoldCount: selectedMoldKeys.length,
|
||||
...this.buildFilteredState(this.data.keyword, this.data.moldOptions, selectedMoldKeys),
|
||||
...this.buildBatchState(selectedMoldKeys),
|
||||
})
|
||||
},
|
||||
onInput(e) {
|
||||
const keyword = e.detail.value
|
||||
this.setData({
|
||||
keyword,
|
||||
selectedMold: null,
|
||||
...this.buildFilteredState(keyword),
|
||||
dropdownVisible: true,
|
||||
})
|
||||
},
|
||||
showDropdown() {
|
||||
this.setData({
|
||||
dropdownVisible: true,
|
||||
...this.buildFilteredState(this.data.keyword),
|
||||
})
|
||||
},
|
||||
selectMold(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const mold = this.data.filteredMoldOptions[index]
|
||||
if (!mold) {
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
selectedMold: mold,
|
||||
keyword: mold.displayName,
|
||||
...this.buildFilteredState(mold.displayName),
|
||||
dropdownVisible: false,
|
||||
})
|
||||
},
|
||||
noop() {},
|
||||
openBatchModal() {
|
||||
this.setData({
|
||||
batchModalVisible: true,
|
||||
dropdownVisible: false,
|
||||
batchKeyword: '',
|
||||
...this.buildBatchState(this.data.selectedMoldKeys, ''),
|
||||
})
|
||||
},
|
||||
closeBatchModal() {
|
||||
if (this.data.batchGenerating) {
|
||||
return
|
||||
}
|
||||
this.setData({ batchModalVisible: false })
|
||||
},
|
||||
onBatchSelectionChange(e) {
|
||||
const visibleKeys = this.data.batchMoldOptions.map(item => item.uniqueKey)
|
||||
const selectedVisibleKeys = e.detail.value || []
|
||||
const selectedSet = new Set(this.data.selectedMoldKeys.filter(key => !visibleKeys.includes(key)))
|
||||
selectedVisibleKeys.forEach(key => selectedSet.add(key))
|
||||
this.setBatchSelection(Array.from(selectedSet))
|
||||
},
|
||||
onBatchInput(e) {
|
||||
const batchKeyword = e.detail.value
|
||||
this.setData({
|
||||
batchKeyword,
|
||||
...this.buildBatchState(this.data.selectedMoldKeys, batchKeyword),
|
||||
})
|
||||
},
|
||||
toggleSelectAll() {
|
||||
const batchKeys = this.data.batchMoldOptions.map(item => item.uniqueKey)
|
||||
if (!batchKeys.length) {
|
||||
wx.showToast({ title: '当前没有可选模具', icon: 'none' })
|
||||
return
|
||||
}
|
||||
let selectedKeys = []
|
||||
const selectedSet = new Set(this.data.selectedMoldKeys)
|
||||
if (this.data.allFilteredSelected) {
|
||||
batchKeys.forEach(key => selectedSet.delete(key))
|
||||
} else {
|
||||
batchKeys.forEach(key => selectedSet.add(key))
|
||||
}
|
||||
selectedKeys = Array.from(selectedSet)
|
||||
this.setBatchSelection(selectedKeys)
|
||||
},
|
||||
getSelectedMolds() {
|
||||
const selectedSet = new Set(this.data.selectedMoldKeys)
|
||||
return this.data.moldOptions.filter(item => selectedSet.has(item.uniqueKey))
|
||||
},
|
||||
batchZipFilePath(fileName) {
|
||||
const safeName = String(fileName || `模具二维码_${Date.now()}.zip`).replace(/[\\/:*?"<>|]/g, '_')
|
||||
return `${wx.env.USER_DATA_PATH}/${safeName}`
|
||||
},
|
||||
shareZipFile(filePath, fileName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!wx.shareFileMessage) {
|
||||
reject(new Error('当前微信版本不支持转发ZIP文件'))
|
||||
return
|
||||
}
|
||||
wx.shareFileMessage({
|
||||
filePath,
|
||||
fileName,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
},
|
||||
isRequestTimeout(error) {
|
||||
const message = `${error && error.message || ''} ${error && error.errMsg || ''}`.toLowerCase()
|
||||
return message.includes('timeout') || message.includes('超时')
|
||||
},
|
||||
async createBatchTask() {
|
||||
const molds = this.getSelectedMolds()
|
||||
if (!molds.length) {
|
||||
wx.showToast({ title: '请先勾选模具', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ batchGenerating: true })
|
||||
wx.showLoading({ title: '创建任务中' })
|
||||
try {
|
||||
const task = await api.createMoldQrBatchTask(molds)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: `已创建${task.itemCount || molds.length}个二维码任务`, icon: 'success' })
|
||||
this.setData({
|
||||
batchModalVisible: false,
|
||||
selectedMoldKeys: [],
|
||||
selectedMoldCount: 0,
|
||||
taskPage: 1,
|
||||
...this.buildFilteredState(this.data.keyword, this.data.moldOptions, []),
|
||||
...this.buildBatchState([], this.data.batchKeyword),
|
||||
})
|
||||
this.loadBatchTasks()
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
if (this.isRequestTimeout(error)) {
|
||||
this.setData({
|
||||
batchModalVisible: false,
|
||||
taskPage: 1,
|
||||
})
|
||||
this.loadBatchTasks()
|
||||
wx.showModal({
|
||||
title: '创建结果待确认',
|
||||
content: '请求超时,但任务可能已经创建。已刷新任务列表,请先查看列表状态,避免重复创建。',
|
||||
showCancel: false,
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: error.message || '创建任务失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
this.setData({ batchGenerating: false })
|
||||
}
|
||||
},
|
||||
changeTaskPage(e) {
|
||||
const direction = Number(e.currentTarget.dataset.direction || 0)
|
||||
const nextPage = Math.max(1, Math.min(this.data.taskTotalPages, this.data.taskPage + direction))
|
||||
if (nextPage === this.data.taskPage) {
|
||||
return
|
||||
}
|
||||
this.setData({ taskPage: nextPage }, () => this.loadBatchTasks())
|
||||
},
|
||||
onTaskPageInput(e) {
|
||||
this.setData({ taskPageInput: e.detail.value })
|
||||
},
|
||||
jumpTaskPage(e) {
|
||||
wx.hideKeyboard()
|
||||
const inputValue = e && e.detail && e.detail.value !== undefined ? e.detail.value : this.data.taskPageInput
|
||||
const target = Math.max(1, Math.min(Number(inputValue) || this.data.taskPage, this.data.taskTotalPages))
|
||||
if (target === this.data.taskPage) {
|
||||
this.setData({ taskPageInput: String(this.data.taskPage || 1) })
|
||||
return
|
||||
}
|
||||
this.setData({ taskPage: target }, () => this.loadBatchTasks())
|
||||
},
|
||||
refreshBatchTasks() {
|
||||
this.loadBatchTasks()
|
||||
},
|
||||
onTaskTouchStart(e) {
|
||||
const touch = e.touches && e.touches[0]
|
||||
if (!touch) {
|
||||
return
|
||||
}
|
||||
this.taskTouchStartX = touch.clientX
|
||||
this.taskTouchStartY = touch.clientY
|
||||
},
|
||||
onTaskTouchMove(e) {
|
||||
const touch = e.touches && e.touches[0]
|
||||
if (!touch) {
|
||||
return
|
||||
}
|
||||
const deltaX = touch.clientX - this.taskTouchStartX
|
||||
const deltaY = touch.clientY - this.taskTouchStartY
|
||||
if (Math.abs(deltaX) < 45 || Math.abs(deltaX) < Math.abs(deltaY) * 1.2) {
|
||||
return
|
||||
}
|
||||
const id = Number(e.currentTarget.dataset.id)
|
||||
if (deltaX < 0) {
|
||||
this.setData({ swipedTaskId: id })
|
||||
} else if (this.data.swipedTaskId === id) {
|
||||
this.setData({ swipedTaskId: null })
|
||||
}
|
||||
},
|
||||
onTaskTouchEnd() {
|
||||
this.taskTouchStartX = 0
|
||||
this.taskTouchStartY = 0
|
||||
},
|
||||
async stopBatchTask(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const task = this.data.taskRows[index]
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '停止中' })
|
||||
try {
|
||||
await api.stopMoldQrBatchTask(task.id)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已停止', icon: 'success' })
|
||||
this.loadBatchTasks()
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: error.message || '停止失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async resumeBatchTask(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const task = this.data.taskRows[index]
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '继续中' })
|
||||
try {
|
||||
await api.resumeMoldQrBatchTask(task.id)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已继续生成', icon: 'success' })
|
||||
this.loadBatchTasks()
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: error.message || '继续失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async shareBatchTask(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const task = this.data.taskRows[index]
|
||||
if (!task || !task.zipUrl) {
|
||||
wx.showToast({ title: 'ZIP还未生成完成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '准备转发' })
|
||||
try {
|
||||
const fileName = task.fileName || `模具二维码_${Date.now()}.zip`
|
||||
const filePath = await api.downloadFileByUrl(task.zipUrl, this.batchZipFilePath(fileName), 120000)
|
||||
wx.hideLoading()
|
||||
await this.shareZipFile(filePath, fileName)
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
const message = error && error.errMsg && error.errMsg.includes('cancel')
|
||||
? '已取消转发'
|
||||
: (error.message || '转发失败')
|
||||
wx.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
},
|
||||
async deleteBatchTask(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const task = this.data.taskRows[index]
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
wx.showModal({
|
||||
title: '删除ZIP任务',
|
||||
content: '删除后会同时删除这条记录及对应ZIP包,是否确定删除?',
|
||||
cancelText: '否',
|
||||
confirmText: '删除',
|
||||
confirmColor: '#d92d20',
|
||||
success: async res => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '删除中' })
|
||||
try {
|
||||
await api.deleteMoldQrBatchTask(task.id)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
this.setData({ swipedTaskId: null })
|
||||
this.loadBatchTasks()
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: error.message || '删除失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
async generate() {
|
||||
const mold = this.data.selectedMold
|
||||
if (!mold || !mold.moldName || !mold.processName) {
|
||||
wx.showToast({ title: '请选择产品名称、工序和冲压方式', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '生成中' })
|
||||
try {
|
||||
const result = await api.generateMoldQr(mold)
|
||||
const qrUrl = this.withCacheBuster(result.qrUrl)
|
||||
this.setData({
|
||||
generatedMoldName: result.displayName || result.moldName || result.deviceNo,
|
||||
qrPath: result.qrPath,
|
||||
qrUrl,
|
||||
qrImageUrl: '',
|
||||
qrLoadError: '',
|
||||
})
|
||||
if (!result.qrUrl) {
|
||||
wx.showToast({ title: '微信配置未生成图片', icon: 'none' })
|
||||
} else {
|
||||
this.prepareQrImage(qrUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
withCacheBuster(url) {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
const separator = url.indexOf('?') === -1 ? '?' : '&'
|
||||
return `${url}${separator}_t=${Date.now()}`
|
||||
},
|
||||
prepareQrImage(url) {
|
||||
wx.downloadFile({
|
||||
url,
|
||||
success: res => {
|
||||
if (res.statusCode === 200 && res.tempFilePath) {
|
||||
this.setData({ qrImageUrl: res.tempFilePath, qrLoadError: '' })
|
||||
return
|
||||
}
|
||||
this.setData({ qrImageUrl: url })
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({
|
||||
qrImageUrl: url,
|
||||
qrLoadError: '二维码图片下载失败,已切换为直接显示',
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
onQrLoad() {
|
||||
this.setData({ qrLoadError: '' })
|
||||
},
|
||||
onQrError() {
|
||||
this.setData({ qrLoadError: '二维码图片加载失败,点击空白区域可放大查看' })
|
||||
},
|
||||
preview() {
|
||||
if (!this.data.qrUrl) {
|
||||
return
|
||||
}
|
||||
wx.previewImage({
|
||||
urls: [this.data.qrUrl],
|
||||
current: this.data.qrUrl,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "模具二维码"
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<view class="page safe-bottom">
|
||||
<view class="header">
|
||||
<text class="title">模具二维码</text>
|
||||
<text class="subtitle">选择产品名称、工序和冲压方式生成扫码报工入口</text>
|
||||
</view>
|
||||
|
||||
<view class="card qr-work-card">
|
||||
<view class="form-item">
|
||||
<view class="field-head">
|
||||
<text class="label">产品名称 / 工序 / 冲压方式</text>
|
||||
<button class="btn mini secondary batch-entry-btn" bindtap="openBatchModal">批量生成</button>
|
||||
</view>
|
||||
<input
|
||||
class="input"
|
||||
value="{{keyword}}"
|
||||
placeholder="输入产品、工序或冲压方式搜索模具"
|
||||
bindfocus="showDropdown"
|
||||
bindinput="onInput"
|
||||
/>
|
||||
<view wx:if="{{dropdownVisible}}" class="mold-dropdown">
|
||||
<view
|
||||
wx:for="{{filteredMoldOptions}}"
|
||||
wx:key="uniqueKey"
|
||||
class="mold-option"
|
||||
data-index="{{index}}"
|
||||
bindtap="selectMold"
|
||||
>
|
||||
<text class="mold-option-name">{{item.moldName || item.name}}</text>
|
||||
<text wx:if="{{item.isMultiPerson}}" class="mold-option-tag">多人协作</text>
|
||||
<text wx:if="{{item.isContinuousDie}}" class="mold-option-tag continuous-tag">连续模</text>
|
||||
<text class="mold-option-process">考勤点 {{item.attendancePointName || '-'}}</text>
|
||||
<text class="mold-option-process">工序 {{item.processName || '-'}} · {{item.stampingMethod || '未填冲压方式'}}</text>
|
||||
</view>
|
||||
<view wx:if="{{!filteredMoldOptions.length}}" class="mold-empty">未找到模具</view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn primary full" bindtap="generate">生成二维码</button>
|
||||
|
||||
<view wx:if="{{qrUrl}}" class="qr-result">
|
||||
<view class="qr-frame" bindtap="preview">
|
||||
<image
|
||||
class="qr-image"
|
||||
src="{{qrImageUrl || qrUrl}}"
|
||||
mode="widthFix"
|
||||
show-menu-by-longpress="{{true}}"
|
||||
bindload="onQrLoad"
|
||||
binderror="onQrError"
|
||||
></image>
|
||||
</view>
|
||||
<text wx:if="{{qrLoadError}}" class="qr-error">{{qrLoadError}}</text>
|
||||
<text class="value">模具 {{generatedMoldName}}</text>
|
||||
<text wx:if="{{selectedMold && selectedMold.attendancePointName}}" class="subtitle">考勤点 {{selectedMold.attendancePointName}}</text>
|
||||
<text class="subtitle">{{qrPath}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card batch-task-card">
|
||||
<view class="task-head">
|
||||
<view>
|
||||
<text class="task-title">ZIP任务列表</text>
|
||||
<text class="subtitle">批量二维码后台生成,完成后可转发ZIP</text>
|
||||
</view>
|
||||
<button class="btn mini secondary" loading="{{taskLoading}}" bindtap="refreshBatchTasks">刷新</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!taskRows.length}}" class="mold-empty">暂无ZIP任务</view>
|
||||
|
||||
<view
|
||||
wx:for="{{taskRows}}"
|
||||
wx:key="id"
|
||||
wx:for-index="taskIndex"
|
||||
class="task-swipe-wrap"
|
||||
data-id="{{item.id}}"
|
||||
bindtouchstart="onTaskTouchStart"
|
||||
bindtouchmove="onTaskTouchMove"
|
||||
bindtouchend="onTaskTouchEnd"
|
||||
>
|
||||
<view
|
||||
class="task-delete-btn"
|
||||
data-index="{{taskIndex}}"
|
||||
catchtap="deleteBatchTask"
|
||||
>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
<view class="task-item {{swipedTaskId == item.id ? 'swiped' : ''}}">
|
||||
<view class="task-row">
|
||||
<view class="task-main">
|
||||
<text class="task-file">{{item.fileName}}</text>
|
||||
<text class="task-meta">二维码数量 {{item.itemCount}} · {{item.statusName}}</text>
|
||||
</view>
|
||||
<view class="task-side">
|
||||
<text class="task-status {{item.isCompleted ? 'success' : ''}} {{item.isPaused ? 'paused' : ''}} {{item.isFailed ? 'danger' : ''}}">{{item.progressPercent}}%</text>
|
||||
<button
|
||||
wx:if="{{item.isPending || item.isRunning}}"
|
||||
class="btn mini secondary task-small-btn"
|
||||
data-index="{{taskIndex}}"
|
||||
catchtap="stopBatchTask"
|
||||
>停止</button>
|
||||
<button
|
||||
wx:if="{{item.isPaused}}"
|
||||
class="btn mini primary task-small-btn"
|
||||
data-index="{{taskIndex}}"
|
||||
catchtap="resumeBatchTask"
|
||||
>继续</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill {{item.isPaused ? 'paused' : ''}} {{item.isFailed ? 'failed' : ''}}" style="width: {{item.progressPercent}}%;"></view>
|
||||
</view>
|
||||
<view class="task-time-grid">
|
||||
<view>
|
||||
<text class="label">开始时间</text>
|
||||
<text class="value">{{item.startedAtText || '-'}}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="label">完成时间</text>
|
||||
<text class="value">{{item.finishedAtText || '-'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text wx:if="{{item.errorMessage}}" class="task-error">{{item.errorMessage}}</text>
|
||||
<view class="task-actions-row">
|
||||
<button
|
||||
wx:if="{{item.isCompleted && item.zipUrl}}"
|
||||
class="btn primary full task-action-btn"
|
||||
data-index="{{taskIndex}}"
|
||||
catchtap="shareBatchTask"
|
||||
>转发ZIP</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{taskRows.length}}" class="button-row pager-row task-pager">
|
||||
<button class="btn secondary pager-nav" data-direction="-1" bindtap="changeTaskPage" disabled="{{taskPage <= 1}}">上一页</button>
|
||||
<view class="page-jump-control">
|
||||
<input class="page-jump-input" type="number" confirm-type="done" value="{{taskPageInput}}" bindinput="onTaskPageInput" bindconfirm="jumpTaskPage" />
|
||||
<text class="page-total-label">/ {{taskTotalPages}}</text>
|
||||
<text class="page-jump-action" bindtap="jumpTaskPage">跳</text>
|
||||
</view>
|
||||
<button class="btn secondary pager-nav" data-direction="1" bindtap="changeTaskPage" disabled="{{taskPage >= taskTotalPages}}">下一页</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{batchModalVisible}}" class="modal-mask batch-modal-mask" catchtap="closeBatchModal">
|
||||
<view class="modal-panel batch-modal-panel" catchtap="noop">
|
||||
<view class="batch-head">
|
||||
<view>
|
||||
<text class="batch-title">批量生成二维码</text>
|
||||
<text class="subtitle">已选择 {{selectedMoldCount}} 个模具</text>
|
||||
</view>
|
||||
<button class="btn mini secondary batch-select-btn" bindtap="toggleSelectAll">
|
||||
{{allFilteredSelected ? '取消全选' : '全选'}}
|
||||
</button>
|
||||
</view>
|
||||
<view class="batch-search">
|
||||
<input
|
||||
class="input"
|
||||
value="{{batchKeyword}}"
|
||||
placeholder="筛选产品、工序、冲压方式或考勤点"
|
||||
bindinput="onBatchInput"
|
||||
/>
|
||||
</view>
|
||||
<checkbox-group bindchange="onBatchSelectionChange">
|
||||
<scroll-view class="batch-list" scroll-y>
|
||||
<label
|
||||
wx:for="{{batchMoldOptions}}"
|
||||
wx:key="uniqueKey"
|
||||
class="batch-option {{item.checked ? 'checked' : ''}}"
|
||||
>
|
||||
<checkbox value="{{item.uniqueKey}}" checked="{{item.checked}}" />
|
||||
<view class="batch-option-text">
|
||||
<text class="mold-option-name">{{item.moldName || item.name}}</text>
|
||||
<text wx:if="{{item.isMultiPerson}}" class="mold-option-tag">多人协作</text>
|
||||
<text wx:if="{{item.isContinuousDie}}" class="mold-option-tag continuous-tag">连续模</text>
|
||||
<text class="mold-option-process">考勤点 {{item.attendancePointName || '-'}}</text>
|
||||
<text class="mold-option-process">工序 {{item.processName || '-'}} · {{item.stampingMethod || '未填冲压方式'}}</text>
|
||||
</view>
|
||||
</label>
|
||||
<view wx:if="{{!batchMoldOptions.length}}" class="mold-empty">未找到模具</view>
|
||||
</scroll-view>
|
||||
</checkbox-group>
|
||||
<view class="batch-actions">
|
||||
<button class="btn secondary full" bindtap="closeBatchModal">取消</button>
|
||||
<button
|
||||
class="btn primary full"
|
||||
loading="{{batchGenerating}}"
|
||||
bindtap="createBatchTask"
|
||||
>生成ZIP任务</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,440 @@
|
||||
.qr-work-card {
|
||||
border-left: 6rpx solid #12a375;
|
||||
}
|
||||
|
||||
.field-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.field-head .label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.batch-entry-btn {
|
||||
flex: 0 0 168rpx;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mold-dropdown {
|
||||
overflow-y: auto;
|
||||
max-height: 420rpx;
|
||||
margin-top: 12rpx;
|
||||
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);
|
||||
}
|
||||
|
||||
.mold-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-bottom: 1rpx solid #e6edf6;
|
||||
color: #162232;
|
||||
font-size: 27rpx;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.mold-option-name {
|
||||
color: #162232;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.mold-option-process {
|
||||
color: #667085;
|
||||
font-size: 24rpx;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.mold-option-tag {
|
||||
align-self: flex-start;
|
||||
padding: 6rpx 12rpx;
|
||||
border: 2rpx solid rgba(217, 119, 6, 0.58);
|
||||
border-radius: 10rpx;
|
||||
color: #9a4a05;
|
||||
background: #fff7e8;
|
||||
font-size: 22rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mold-option-tag.continuous-tag {
|
||||
color: #b45f00;
|
||||
border-color: rgba(217, 119, 6, 0.58);
|
||||
background: #fff4e0;
|
||||
}
|
||||
|
||||
.mold-option:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mold-option:active {
|
||||
background: #eef5ff;
|
||||
}
|
||||
|
||||
.mold-empty {
|
||||
padding: 32rpx 24rpx;
|
||||
color: #667085;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32rpx;
|
||||
background: rgba(15, 23, 42, 0.48);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
.batch-modal-mask {
|
||||
align-items: flex-start;
|
||||
padding: 28rpx 20rpx calc(env(safe-area-inset-bottom) + 28rpx);
|
||||
}
|
||||
|
||||
.batch-modal-panel {
|
||||
max-height: 92vh;
|
||||
}
|
||||
|
||||
.batch-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.batch-title {
|
||||
display: block;
|
||||
color: #162232;
|
||||
font-size: 30rpx;
|
||||
font-weight: 850;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.batch-select-btn {
|
||||
flex: 0 0 156rpx;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.batch-search {
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.batch-list {
|
||||
height: 64vh;
|
||||
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);
|
||||
}
|
||||
|
||||
.batch-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 18rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-bottom: 1rpx solid #e6edf6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-option.checked {
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.batch-option:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.batch-option-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.76fr) minmax(0, 1.24fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.batch-task-card {
|
||||
border-left: 6rpx solid #1463ff;
|
||||
}
|
||||
|
||||
.task-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
display: block;
|
||||
color: #162232;
|
||||
font-size: 30rpx;
|
||||
font-weight: 850;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.task-swipe-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.task-delete-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150rpx;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0 16rpx 16rpx 0;
|
||||
color: #ffffff;
|
||||
background: #d92d20;
|
||||
font-size: 28rpx;
|
||||
font-weight: 900;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.task-delete-btn text {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 0;
|
||||
padding: 22rpx;
|
||||
border: 2rpx solid #c8d3e3;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
|
||||
box-shadow:
|
||||
0 9rpx 0 rgba(180, 193, 213, 0.48),
|
||||
0 15rpx 24rpx rgba(22, 34, 50, 0.08),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.94);
|
||||
box-sizing: border-box;
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.task-item.swiped {
|
||||
transform: translateX(-150rpx);
|
||||
}
|
||||
|
||||
.task-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.task-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.task-file {
|
||||
color: #162232;
|
||||
font-size: 27rpx;
|
||||
font-weight: 850;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
color: #667085;
|
||||
font-size: 24rpx;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
min-width: 96rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #1463ff;
|
||||
background: #eef5ff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-side {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.task-small-btn {
|
||||
min-width: 108rpx;
|
||||
height: 54rpx;
|
||||
line-height: 54rpx;
|
||||
margin: 0;
|
||||
padding: 0 14rpx;
|
||||
font-size: 22rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.task-status.success {
|
||||
color: #087443;
|
||||
background: #e8fff4;
|
||||
}
|
||||
|
||||
.task-status.paused {
|
||||
color: #8a5a00;
|
||||
background: #fff7df;
|
||||
}
|
||||
|
||||
.task-status.danger {
|
||||
color: #b42318;
|
||||
background: #fff1f0;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
overflow: hidden;
|
||||
height: 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #dbe4f0;
|
||||
box-shadow: inset 0 2rpx 5rpx rgba(22, 34, 50, 0.12);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
min-width: 8rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #1463ff 0%, #12a375 100%);
|
||||
}
|
||||
|
||||
.progress-fill.failed {
|
||||
background: linear-gradient(90deg, #d92d20 0%, #f79009 100%);
|
||||
}
|
||||
|
||||
.progress-fill.paused {
|
||||
background: linear-gradient(90deg, #f79009 0%, #fdb022 100%);
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.task-error {
|
||||
color: #b42318;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.task-actions-row {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
|
||||
.task-action-btn {
|
||||
flex: 1;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
|
||||
.task-pager {
|
||||
gap: 14rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.qr-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-top: 26rpx;
|
||||
padding-top: 26rpx;
|
||||
border-top: 2rpx solid #e6edf6;
|
||||
}
|
||||
|
||||
.qr-frame {
|
||||
width: 520rpx;
|
||||
max-width: 100%;
|
||||
min-height: 520rpx;
|
||||
padding: 16rpx;
|
||||
border: 1rpx solid #ccd8e6;
|
||||
border-radius: 18rpx;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 12rpx 0 rgba(205, 216, 229, 0.58),
|
||||
0 18rpx 28rpx rgba(22, 34, 50, 0.1),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.qr-error {
|
||||
color: #b42318;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
Reference in New Issue
Block a user