613 lines
18 KiB
JavaScript
613 lines
18 KiB
JavaScript
const api = require('../../utils/api')
|
||
const locationGuard = require('../../utils/locationGuard')
|
||
|
||
Page({
|
||
data: {
|
||
user: null,
|
||
moldName: '',
|
||
processName: '',
|
||
attendancePointName: '',
|
||
stampingMethod: '',
|
||
isCleaning: false,
|
||
isMisc: false,
|
||
moldDisplayName: '',
|
||
sceneToken: '',
|
||
state: null,
|
||
roleChoices: [],
|
||
selectionToken: '',
|
||
toastVisible: false,
|
||
toastMessage: '',
|
||
locationChecking: true,
|
||
locationAllowed: false,
|
||
locationMessage: '正在校验当前位置',
|
||
currentLocation: null,
|
||
occupiedDetail: null,
|
||
temporaryWorkerModalVisible: false,
|
||
temporaryWorkerPrompt: '',
|
||
temporaryWorkerName: '',
|
||
},
|
||
toastTimer: null,
|
||
onLoad(options) {
|
||
this.resolveOpenMold(options)
|
||
},
|
||
async resolveOpenMold(options) {
|
||
const scene = this.decodeRepeated(options.scene || '')
|
||
let moldName = this.extractMoldName(options, scene)
|
||
let processName = this.extractProcessName(options, scene)
|
||
let attendancePointName = this.decodeRepeated(options.attendancePointName || options.attendance_point_name || '')
|
||
let stampingMethod = this.decodeRepeated(options.stampingMethod || options.stamping_method || '')
|
||
let isCleaning = false
|
||
let isMisc = false
|
||
let moldDisplayName = api.moldDisplayName(moldName, processName, stampingMethod)
|
||
const sceneToken = this.extractSceneToken(scene || options.moldName || '')
|
||
if (!moldName && sceneToken && api.getCurrentUser()) {
|
||
try {
|
||
const resolved = await api.resolveMoldScene(sceneToken)
|
||
moldName = resolved.moldName
|
||
processName = resolved.processName
|
||
attendancePointName = resolved.attendancePointName || ''
|
||
stampingMethod = resolved.stampingMethod || ''
|
||
isCleaning = !!resolved.isCleaning
|
||
isMisc = !!resolved.isMisc
|
||
moldDisplayName = resolved.displayName
|
||
} catch (error) {
|
||
this.showTip(error.message || '未找到模具二维码')
|
||
}
|
||
}
|
||
this.setData({ moldName, processName, attendancePointName, stampingMethod, isCleaning, isMisc, moldDisplayName, sceneToken })
|
||
this.verifyOpenLocation()
|
||
},
|
||
async resolveSceneAfterLogin() {
|
||
if (this.data.moldName || !this.data.sceneToken) {
|
||
return true
|
||
}
|
||
try {
|
||
const resolved = await api.resolveMoldScene(this.data.sceneToken)
|
||
this.setData({
|
||
moldName: resolved.moldName,
|
||
processName: resolved.processName,
|
||
attendancePointName: resolved.attendancePointName || '',
|
||
stampingMethod: resolved.stampingMethod || '',
|
||
isCleaning: !!resolved.isCleaning,
|
||
isMisc: !!resolved.isMisc,
|
||
moldDisplayName: resolved.displayName,
|
||
})
|
||
await this.verifyOpenLocation()
|
||
return false
|
||
} catch (error) {
|
||
this.showTip(error.message || '未找到模具二维码')
|
||
return false
|
||
}
|
||
},
|
||
extractMoldName(options, scene) {
|
||
const explicit = options.moldName || options.deviceNo
|
||
if (explicit) {
|
||
const value = this.decodeRepeated(explicit)
|
||
return value.startsWith('mold=') ? '' : value
|
||
}
|
||
const raw = this.decodeRepeated(scene)
|
||
const moldMatched = raw.match(/(?:^|[?&])?moldName=([^&]+)/)
|
||
if (moldMatched && moldMatched[1]) {
|
||
return this.decodeRepeated(moldMatched[1])
|
||
}
|
||
const oldMatched = raw.match(/(?:^|[?&])?deviceNo=([^&]+)/)
|
||
if (oldMatched && oldMatched[1]) {
|
||
return this.decodeRepeated(oldMatched[1])
|
||
}
|
||
if (raw.startsWith('mold=')) {
|
||
return ''
|
||
}
|
||
return raw
|
||
},
|
||
extractProcessName(options, scene) {
|
||
const explicit = options.processName || options.process_name
|
||
if (explicit) {
|
||
return this.decodeRepeated(explicit)
|
||
}
|
||
const raw = this.decodeRepeated(scene)
|
||
const matched = raw.match(/(?:^|[?&])?(?:processName|process_name)=([^&]+)/)
|
||
return matched && matched[1] ? this.decodeRepeated(matched[1]) : ''
|
||
},
|
||
extractSceneToken(scene) {
|
||
const raw = this.decodeRepeated(scene)
|
||
const matched = raw.match(/(?:^|[?&])?mold=([^&]+)/)
|
||
return matched && matched[1] ? this.decodeRepeated(matched[1]) : ''
|
||
},
|
||
decodeRepeated(value) {
|
||
let text = String(value || '').trim()
|
||
for (let index = 0; index < 3; index += 1) {
|
||
try {
|
||
const decoded = decodeURIComponent(text)
|
||
if (decoded === text) {
|
||
break
|
||
}
|
||
text = decoded
|
||
} catch (error) {
|
||
break
|
||
}
|
||
}
|
||
return text
|
||
},
|
||
isCleaningProcess() {
|
||
return !!this.data.isCleaning
|
||
|| String(this.data.stampingMethod || '').trim() === '清洗'
|
||
},
|
||
openCleaningReport() {
|
||
const moldName = encodeURIComponent(this.data.moldName || '')
|
||
const processName = encodeURIComponent(this.data.processName || '')
|
||
const stampingMethod = encodeURIComponent(this.data.stampingMethod || '')
|
||
const displayName = encodeURIComponent(this.data.moldDisplayName || '')
|
||
const attendancePointName = encodeURIComponent(this.data.attendancePointName || '')
|
||
wx.redirectTo({
|
||
url: `/pages/cleaningReport/cleaningReport?moldName=${moldName}&processName=${processName}&attendancePointName=${attendancePointName}&stampingMethod=${stampingMethod}&isCleaning=1&displayName=${displayName}`,
|
||
})
|
||
},
|
||
onShow() {
|
||
if (this.data.locationAllowed) {
|
||
this.load()
|
||
}
|
||
},
|
||
onUnload() {
|
||
if (this.toastTimer) {
|
||
clearTimeout(this.toastTimer)
|
||
this.toastTimer = null
|
||
}
|
||
},
|
||
showTip(message, duration = 5000) {
|
||
if (this.toastTimer) {
|
||
clearTimeout(this.toastTimer)
|
||
}
|
||
this.setData({
|
||
toastVisible: true,
|
||
toastMessage: String(message || ''),
|
||
})
|
||
this.toastTimer = setTimeout(() => {
|
||
this.setData({ toastVisible: false, toastMessage: '' })
|
||
this.toastTimer = null
|
||
}, duration)
|
||
},
|
||
async verifyOpenLocation() {
|
||
this.setData({
|
||
locationChecking: true,
|
||
locationAllowed: false,
|
||
locationMessage: '正在校验当前位置',
|
||
})
|
||
try {
|
||
const locationScope = api.getCurrentUser() ? 'accessible' : 'public'
|
||
const locationResult = await locationGuard.ensureFactoryLocation(this.data.attendancePointName, locationScope)
|
||
this.setData({
|
||
locationChecking: false,
|
||
locationAllowed: true,
|
||
locationMessage: '',
|
||
currentLocation: locationResult.current,
|
||
})
|
||
await this.load()
|
||
} catch (error) {
|
||
this.setData({
|
||
locationChecking: false,
|
||
locationAllowed: false,
|
||
locationMessage: error.message || '当前位置不在允许范围内,不能扫码报工',
|
||
currentLocation: null,
|
||
user: null,
|
||
state: null,
|
||
})
|
||
}
|
||
},
|
||
async load() {
|
||
let user = api.getCurrentUser()
|
||
let state = null
|
||
this.setData({ occupiedDetail: null })
|
||
if (user && !this.data.moldName && this.data.sceneToken) {
|
||
const shouldContinue = await this.resolveSceneAfterLogin()
|
||
if (!shouldContinue) {
|
||
return
|
||
}
|
||
}
|
||
if (user && this.data.moldName) {
|
||
try {
|
||
user = await api.refreshCurrentUser() || user
|
||
if (this.isCleaningProcess()) {
|
||
this.setData({ user, state: null }, () => {
|
||
this.openCleaningReport()
|
||
})
|
||
return
|
||
}
|
||
state = await api.getClockState(
|
||
this.data.moldName,
|
||
this.data.processName,
|
||
this.data.attendancePointName,
|
||
this.data.currentLocation,
|
||
)
|
||
if (state && state.isCleaning) {
|
||
this.setData({
|
||
user,
|
||
state: null,
|
||
stampingMethod: state.stampingMethod || '',
|
||
isCleaning: true,
|
||
isMisc: false,
|
||
}, () => {
|
||
this.openCleaningReport()
|
||
})
|
||
return
|
||
}
|
||
if (state && state.isMisc) {
|
||
this.setData({
|
||
stampingMethod: state.stampingMethod || '',
|
||
isMisc: true,
|
||
moldDisplayName: state.displayName || this.data.moldDisplayName,
|
||
})
|
||
}
|
||
} catch (error) {
|
||
if (await this.handleMoldOccupiedError(error)) {
|
||
user = api.getCurrentUser()
|
||
this.setData({ user, state: null })
|
||
return
|
||
}
|
||
if (await this.handleUnfinishedReportError(error)) {
|
||
user = api.getCurrentUser()
|
||
this.setData({ user, state: null })
|
||
return
|
||
}
|
||
this.showTip(error.message)
|
||
user = api.getCurrentUser()
|
||
}
|
||
}
|
||
this.setData({ user, state })
|
||
},
|
||
async wechatLogin(e) {
|
||
const phoneCode = e.detail && e.detail.code
|
||
if (!phoneCode) {
|
||
this.showTip('未获得手机号授权', 3000)
|
||
return
|
||
}
|
||
wx.showLoading({ title: '登录中' })
|
||
try {
|
||
const result = await api.loginWithPhoneCode(phoneCode)
|
||
if (result.needsTemporaryWorker) {
|
||
wx.hideLoading()
|
||
await this.confirmTemporaryWorker(result)
|
||
return
|
||
}
|
||
if (result.needsSelection) {
|
||
this.setData({
|
||
roleChoices: result.matchedPeople,
|
||
selectionToken: result.selectionToken,
|
||
})
|
||
return
|
||
}
|
||
await this.load()
|
||
} catch (error) {
|
||
this.showTip(error.message)
|
||
} finally {
|
||
wx.hideLoading()
|
||
}
|
||
},
|
||
confirmTemporaryWorker(result) {
|
||
return new Promise(resolve => {
|
||
this.temporaryWorkerResult = result
|
||
this.temporaryWorkerResolve = resolve
|
||
this.setData({
|
||
temporaryWorkerModalVisible: true,
|
||
temporaryWorkerPrompt: `该手机号${result.temporaryPhone || ''}不在人员清单中。临时工账号有效期只有24小时,请填写姓名后创建。`,
|
||
temporaryWorkerName: '',
|
||
})
|
||
})
|
||
},
|
||
onTemporaryWorkerNameInput(e) {
|
||
this.setData({ temporaryWorkerName: e.detail.value })
|
||
},
|
||
closeTemporaryWorkerModal() {
|
||
this.setData({
|
||
temporaryWorkerModalVisible: false,
|
||
temporaryWorkerPrompt: '',
|
||
temporaryWorkerName: '',
|
||
})
|
||
this.temporaryWorkerResult = null
|
||
if (this.temporaryWorkerResolve) {
|
||
this.temporaryWorkerResolve()
|
||
this.temporaryWorkerResolve = null
|
||
}
|
||
},
|
||
async submitTemporaryWorker() {
|
||
const result = this.temporaryWorkerResult
|
||
const name = String(this.data.temporaryWorkerName || '').trim()
|
||
if (!name) {
|
||
this.showTip('请输入姓名', 3000)
|
||
return
|
||
}
|
||
if (!result || !result.temporaryToken) {
|
||
this.showTip('临时工注册信息已失效,请重新登录')
|
||
this.closeTemporaryWorkerModal()
|
||
return
|
||
}
|
||
this.setData({ temporaryWorkerModalVisible: false })
|
||
wx.showLoading({ title: '定位中' })
|
||
try {
|
||
const attendancePoint = await locationGuard.ensureTemporaryWorkerLocation()
|
||
wx.showLoading({ title: '创建中' })
|
||
await api.createTemporaryWorker(result.temporaryToken, name, attendancePoint)
|
||
await this.load()
|
||
wx.showToast({ title: '临时工已生效', icon: 'success' })
|
||
} catch (error) {
|
||
this.showTip(error.message)
|
||
} finally {
|
||
wx.hideLoading()
|
||
this.closeTemporaryWorkerModal()
|
||
}
|
||
},
|
||
async chooseRole(e) {
|
||
const role = e.currentTarget.dataset.role
|
||
wx.showLoading({ title: '登录中' })
|
||
try {
|
||
await api.selectRole(this.data.selectionToken, role)
|
||
this.setData({ roleChoices: [], selectionToken: '' })
|
||
await this.load()
|
||
} catch (error) {
|
||
this.showTip(error.message)
|
||
} finally {
|
||
wx.hideLoading()
|
||
}
|
||
},
|
||
canOperate() {
|
||
const { user, moldName, state } = this.data
|
||
if (!user) {
|
||
this.showTip('请先登录', 3000)
|
||
return false
|
||
}
|
||
if (user.role !== 'worker') {
|
||
this.showTip('只有冲压工人可以报工', 3000)
|
||
return false
|
||
}
|
||
if (!moldName) {
|
||
this.showTip('缺少模具名称', 3000)
|
||
return false
|
||
}
|
||
if (!state) {
|
||
this.showTip('当前模具码不可用', 3000)
|
||
return false
|
||
}
|
||
return true
|
||
},
|
||
formatAutoSubmitHours(value) {
|
||
const hours = Number(value || 15)
|
||
if (!Number.isFinite(hours)) {
|
||
return '15'
|
||
}
|
||
return Number.isInteger(hours) ? String(hours) : String(Math.round(hours * 100) / 100)
|
||
},
|
||
confirmStartWork() {
|
||
const hoursText = this.formatAutoSubmitHours(this.data.state && this.data.state.autoSubmitHours)
|
||
return new Promise(resolve => {
|
||
wx.showModal({
|
||
title: '开始上班',
|
||
content: `该班次时长最多为${hoursText}个小时。${hoursText}小时后未下班报工,系统将自动报工`,
|
||
confirmText: '开始上班',
|
||
cancelText: '取消',
|
||
success: res => resolve(!!res.confirm),
|
||
fail: () => resolve(false),
|
||
})
|
||
})
|
||
},
|
||
async switchMold() {
|
||
if (!this.canOperate()) {
|
||
return
|
||
}
|
||
try {
|
||
await api.switchDevice(
|
||
this.data.moldName,
|
||
this.data.processName,
|
||
this.data.attendancePointName,
|
||
this.data.currentLocation,
|
||
)
|
||
wx.showToast({ title: '已记录换模具', icon: 'success' })
|
||
await this.load()
|
||
} catch (error) {
|
||
if (await this.handleMoldOccupiedError(error)) {
|
||
return
|
||
}
|
||
if (await this.handleUnfinishedReportError(error)) {
|
||
return
|
||
}
|
||
this.showTip(error.message)
|
||
}
|
||
},
|
||
async finishWork() {
|
||
if (!this.canOperate()) {
|
||
return
|
||
}
|
||
try {
|
||
const session = await api.finishWork(
|
||
this.data.moldName,
|
||
this.data.processName,
|
||
this.data.attendancePointName,
|
||
this.data.currentLocation,
|
||
)
|
||
wx.navigateTo({
|
||
url: `/pages/reportForm/reportForm?sessionId=${session.id}`,
|
||
})
|
||
} catch (error) {
|
||
if (await this.handleMoldOccupiedError(error)) {
|
||
return
|
||
}
|
||
if (await this.handleUnfinishedReportError(error)) {
|
||
return
|
||
}
|
||
this.showTip(error.message)
|
||
}
|
||
},
|
||
async handleAction() {
|
||
if (!this.canOperate()) {
|
||
return
|
||
}
|
||
const { moldName, state } = this.data
|
||
|
||
try {
|
||
if (state.action === 'start') {
|
||
const confirmed = await this.confirmStartWork()
|
||
if (!confirmed) {
|
||
return
|
||
}
|
||
await api.startWork(moldName, this.data.processName, this.data.attendancePointName, this.data.currentLocation)
|
||
wx.showToast({ title: '已开始上班', icon: 'success' })
|
||
await this.load()
|
||
return
|
||
}
|
||
|
||
if (state.action === 'switch') {
|
||
await this.switchMold()
|
||
return
|
||
}
|
||
|
||
if (state.action === 'report' || (state.action === 'finish' && state.endAtText && state.session)) {
|
||
wx.navigateTo({
|
||
url: `/pages/reportForm/reportForm?sessionId=${state.session.id}`,
|
||
})
|
||
return
|
||
}
|
||
|
||
if (state.action === 'refinish') {
|
||
await this.finishWork()
|
||
return
|
||
}
|
||
|
||
await this.finishWork()
|
||
} catch (error) {
|
||
if (await this.handleMoldOccupiedError(error)) {
|
||
return
|
||
}
|
||
if (await this.handleUnfinishedReportError(error)) {
|
||
return
|
||
}
|
||
this.showTip(error.message)
|
||
}
|
||
},
|
||
resolveUnfinishedReportDetail(error) {
|
||
if (!error || error.statusCode !== 400) {
|
||
return null
|
||
}
|
||
let detail = error.detail
|
||
if (typeof detail === 'string') {
|
||
try {
|
||
detail = JSON.parse(detail)
|
||
} catch (parseError) {
|
||
detail = null
|
||
}
|
||
}
|
||
return detail && detail.code === 'unfinished_report_mold_mismatch' ? detail : null
|
||
},
|
||
async openUnfinishedReport(detail) {
|
||
let sessionId = detail.session_id
|
||
if (detail.reporting_expired) {
|
||
wx.showLoading({ title: '处理中' })
|
||
try {
|
||
const session = await api.finishWork(
|
||
this.data.moldName,
|
||
this.data.processName,
|
||
this.data.attendancePointName,
|
||
this.data.currentLocation,
|
||
)
|
||
sessionId = session.id
|
||
} finally {
|
||
wx.hideLoading()
|
||
}
|
||
}
|
||
wx.navigateTo({
|
||
url: `/pages/reportForm/reportForm?sessionId=${sessionId}`,
|
||
})
|
||
},
|
||
handleUnfinishedReportError(error) {
|
||
const detail = this.resolveUnfinishedReportDetail(error)
|
||
if (!detail || !detail.session_id) {
|
||
return Promise.resolve(false)
|
||
}
|
||
const message = detail.message || error.message || '当前有未完成的报工'
|
||
return new Promise(resolve => {
|
||
wx.showModal({
|
||
title: '未完成报工',
|
||
content: `${message}\n是否立刻前往未完成的报工?`,
|
||
cancelText: '否',
|
||
confirmText: '是',
|
||
confirmColor: '#1463ff',
|
||
success: async res => {
|
||
if (!res.confirm) {
|
||
resolve(true)
|
||
return
|
||
}
|
||
try {
|
||
await this.openUnfinishedReport(detail)
|
||
} catch (openError) {
|
||
this.showTip(openError.message || '打开未完成报工失败')
|
||
} finally {
|
||
resolve(true)
|
||
}
|
||
},
|
||
fail: () => resolve(true),
|
||
})
|
||
})
|
||
},
|
||
resolveMoldOccupiedDetail(error) {
|
||
if (!error || error.statusCode !== 423) {
|
||
return null
|
||
}
|
||
let detail = error.detail
|
||
if (typeof detail === 'string') {
|
||
try {
|
||
detail = JSON.parse(detail)
|
||
} catch (parseError) {
|
||
detail = null
|
||
}
|
||
}
|
||
return detail && detail.code === 'mold_occupied' ? detail : null
|
||
},
|
||
handleMoldOccupiedError(error) {
|
||
const detail = this.resolveMoldOccupiedDetail(error)
|
||
if (!detail) {
|
||
return Promise.resolve(false)
|
||
}
|
||
this.setData({ occupiedDetail: detail })
|
||
return new Promise(resolve => {
|
||
wx.showModal({
|
||
title: '模具已被占用',
|
||
content: detail.message || `已经被${detail.occupied_name || detail.occupied_phone || '其他员工'}占用,请联系管理员解除占用`,
|
||
confirmText: '反馈给管理员',
|
||
cancelText: '确定',
|
||
confirmColor: '#1463ff',
|
||
success: async res => {
|
||
if (!res.confirm) {
|
||
resolve(true)
|
||
return
|
||
}
|
||
wx.showLoading({ title: '反馈中' })
|
||
try {
|
||
await api.createMoldLockFeedback(detail.lock_id)
|
||
wx.showToast({ title: '反馈成功', icon: 'success' })
|
||
} catch (feedbackError) {
|
||
this.showTip(feedbackError.message || '反馈失败')
|
||
} finally {
|
||
wx.hideLoading()
|
||
resolve(true)
|
||
}
|
||
},
|
||
fail: () => resolve(true),
|
||
})
|
||
})
|
||
},
|
||
async feedbackMoldOccupied() {
|
||
const detail = this.data.occupiedDetail
|
||
if (!detail || !detail.lock_id) {
|
||
this.showTip('占用信息缺失,无法反馈')
|
||
return
|
||
}
|
||
wx.showLoading({ title: '反馈中' })
|
||
try {
|
||
await api.createMoldLockFeedback(detail.lock_id)
|
||
wx.showToast({ title: '反馈成功', icon: 'success' })
|
||
} catch (error) {
|
||
this.showTip(error.message || '反馈失败')
|
||
} finally {
|
||
wx.hideLoading()
|
||
}
|
||
},
|
||
noop() {},
|
||
})
|