JhHardwareWRS/pages/index/index.js
2026-07-10 10:12:03 +08:00

630 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const api = require('../../utils/api')
const locationGuard = require('../../utils/locationGuard')
const { buildErpLoginConfirmUrlFromScanResult } = require('../../utils/scanRouting')
const avatarKey = phone => `jh_wrs_avatar_${phone}`
Page({
data: {
activeTab: 'home',
currentUser: null,
currentRoleName: '',
currentRolesText: '',
canSwitchRole: false,
avatarPath: '',
avatarText: '',
roleChoices: [],
selectionToken: '',
summary: {},
showOverview: false,
showWorkerTrend: false,
workerTrend: {
total: 0,
max: 0,
points: [],
segments: [],
},
notices: [],
noticeList: [],
actions: [],
currentAttendancePointName: '',
locationStatusText: '当前位置:未进入考勤点',
locationEntered: false,
locationInitialized: false,
loading: false,
refreshing: false,
temporaryWorkerModalVisible: false,
temporaryWorkerPrompt: '',
temporaryWorkerName: '',
},
onShow() {
this.load({ refreshLocation: !this.data.locationInitialized }).catch(error => {
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
})
},
async load(options = {}) {
let currentUser = api.getCurrentUser()
let summary = {}
let showOverview = false
let showWorkerTrend = false
let workerTrend = { total: 0, max: 0, points: [], segments: [] }
let notices = []
let locationView = {
currentAttendancePointName: this.data.currentAttendancePointName,
locationStatusText: this.data.locationStatusText,
locationEntered: this.data.locationEntered,
}
let locationInitialized = this.data.locationInitialized
if (currentUser) {
try {
currentUser = await api.refreshCurrentUser() || currentUser
showOverview = currentUser.role !== 'worker'
showWorkerTrend = currentUser.role === 'worker'
if (showOverview) {
summary = await api.getHomeSummary(currentUser)
}
if (showWorkerTrend) {
workerTrend = await this.loadWorkerTrend()
}
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
currentUser = api.getCurrentUser()
showOverview = !!currentUser && currentUser.role !== 'worker'
showWorkerTrend = !!currentUser && currentUser.role === 'worker'
}
if (currentUser) {
notices = await this.loadNotices()
if (options.refreshLocation || !locationInitialized) {
locationView = await this.refreshLocationStatus()
locationInitialized = true
}
}
} else {
locationInitialized = false
locationView = {
currentAttendancePointName: '',
locationStatusText: '当前位置:未进入考勤点',
locationEntered: false,
}
}
const actions = this.buildActions(currentUser)
const overviewItems = this.buildOverviewItems(currentUser, summary)
const userView = this.buildUserView(currentUser)
this.setData({
currentUser,
summary,
overviewItems,
showOverview,
showWorkerTrend,
workerTrend,
notices,
noticeList: notices.slice(0, 3),
actions,
...locationView,
locationInitialized,
...userView,
})
},
async onPullDownRefresh() {
this.setData({ refreshing: true })
try {
await this.load({ refreshLocation: true })
} finally {
this.setData({ refreshing: false })
if (wx.stopPullDownRefresh) {
wx.stopPullDownRefresh()
}
}
},
async loadNotices() {
try {
const result = await api.listNotices({ page: 1, pageSize: 6 })
return result.rows || []
} catch (error) {
return []
}
},
async refreshLocationStatus() {
try {
const result = await locationGuard.getCurrentAttendancePoint()
if (result && result.factory) {
return {
currentAttendancePointName: result.factory.name,
locationStatusText: `当前位置:已进入 ${result.factory.name} 考勤点`,
locationEntered: true,
}
}
} catch (error) {
return {
currentAttendancePointName: '',
locationStatusText: '当前位置:未进入考勤点',
locationEntered: false,
}
}
return {
currentAttendancePointName: '',
locationStatusText: '当前位置:未进入考勤点',
locationEntered: false,
}
},
buildUserView(user) {
if (!user) {
return {
currentRoleName: '',
currentRolesText: '',
canSwitchRole: false,
avatarPath: '',
avatarText: '',
}
}
return {
currentRoleName: user.roleName || api.roleNames[user.role] || user.role,
currentRolesText: (user.roleNames || []).join('、') || user.roleName || '',
canSwitchRole: !!(user.rolesDisplay && user.rolesDisplay.length > 1),
avatarPath: wx.getStorageSync(avatarKey(user.phone)) || '',
avatarText: this.buildDefaultAvatarText(user.name),
}
},
buildOverviewItems(user, summary = {}) {
if (!user || user.role === 'worker') {
return []
}
const today = this.formatDate(new Date())
return [
{
key: 'products',
label: '产品',
value: summary.productCount || 0,
url: '/pages/manageProducts/manageProducts?readonly=1',
},
{
key: 'people',
label: '人员',
value: summary.peopleCount || 0,
url: '/pages/managePeople/managePeople?readonly=1',
},
...(user.role === 'admin' ? [{
key: 'pending',
label: '待审核',
value: summary.pendingCount || 0,
alert: (summary.pendingCount || 0) > 0,
url: '/pages/records/records?mode=pending&status=pending',
}] : []),
...(user.role === 'admin' ? [{
key: 'approved',
label: '今日通过',
value: summary.approvedToday || 0,
url: `/pages/records/records?mode=audit&status=approved&startDate=${today}&endDate=${today}`,
}] : []),
]
},
buildDefaultAvatarText(name) {
const text = String(name || '').trim()
if (!text) {
return '嘉'
}
const givenName = text.length > 1 ? text.slice(1) : text
return givenName.slice(0, 2)
},
switchTab(e) {
const activeTab = e.currentTarget.dataset.tab
this.setData({ activeTab })
},
buildRecentDates(days = 7) {
const dates = []
const today = new Date()
for (let index = days - 1; index >= 0; index -= 1) {
const date = new Date(today)
date.setDate(today.getDate() - index)
const value = this.formatDate(date)
dates.push({
value,
label: value.slice(5),
})
}
return dates
},
formatDate(date) {
const year = date.getFullYear()
const monthValue = date.getMonth() + 1
const dayValue = date.getDate()
const month = monthValue < 10 ? `0${monthValue}` : `${monthValue}`
const day = dayValue < 10 ? `0${dayValue}` : `${dayValue}`
return `${year}-${month}-${day}`
},
async loadWorkerTrend() {
const dates = this.buildRecentDates(7)
const result = await api.listReports({
startDate: dates[0].value,
endDate: dates[dates.length - 1].value,
page: 1,
pageSize: 100,
})
const totals = dates.reduce((map, item) => {
map[item.value] = 0
return map
}, {})
;(result.rows || []).forEach(report => {
if (totals[report.reportDate] === undefined) {
return
}
totals[report.reportDate] += Number(report.metrics && report.metrics.totalGood || 0)
})
const points = dates.map(item => ({
date: item.value,
label: item.label,
value: totals[item.value] || 0,
}))
const total = points.reduce((sum, item) => sum + item.value, 0)
const max = points.reduce((largest, item) => Math.max(largest, item.value), 0)
return this.buildTrendView(total, max, points)
},
buildTrendView(total, max, points) {
const maxValue = Math.max(1, max || 0)
const lastIndex = Math.max(1, points.length - 1)
const chartInset = 8
const chartSpan = 100 - chartInset * 2
const plottedPoints = points.map((item, index) => {
const left = chartInset + (index * chartSpan / lastIndex)
const top = 82 - (Number(item.value || 0) / maxValue) * 68
return {
...item,
left: left.toFixed(2),
top: top.toFixed(2),
edgeClass: index === 0 ? 'first' : (index === points.length - 1 ? 'last' : ''),
valueText: String(item.value || 0),
}
})
const segments = []
for (let index = 0; index < plottedPoints.length - 1; index += 1) {
const start = plottedPoints[index]
const end = plottedPoints[index + 1]
const dx = Number(end.left) - Number(start.left)
const dy = Number(end.top) - Number(start.top)
const dxRpx = dx * 5.6
const dyRpx = dy * 2.2
const width = Math.sqrt(dxRpx * dxRpx + dyRpx * dyRpx) / 5.6
const angle = Math.atan2(dyRpx, dxRpx) * 180 / Math.PI
segments.push({
left: start.left,
top: start.top,
width: width.toFixed(2),
angle: angle.toFixed(2),
})
}
return { total, max, points: plottedPoints, segments }
},
buildActions(user) {
if (!user) {
return []
}
if (user.role === 'worker') {
return [
{ key: 'records', icon: '记', title: '我的报工记录', desc: '按日期查看提交和审核状态', url: '/pages/records/records' },
]
}
if (user.role === 'admin') {
return [
{ key: 'products', icon: '产', title: '产品清单', desc: '导入、添加、修改产品', url: '/pages/manageProducts/manageProducts' },
{ key: 'people', icon: '人', title: '人员清单', desc: '维护员工、管理员和经理', url: '/pages/managePeople/managePeople' },
{ key: 'review', icon: '审', title: '报工审核', desc: '处理待审核报工', url: '/pages/review/review' },
{ key: 'audit', icon: '核', title: '审核记录', desc: '查看已审核报工', url: '/pages/records/records?mode=audit' },
{ key: 'monitor', icon: '监', title: '智能监控', desc: '查看模具占用和反馈', url: '/pages/smartMonitor/smartMonitor' },
{ key: 'usageStats', icon: '统', title: '设备模具使用统计', desc: '按时间统计设备和模具使用', url: '/pages/usageStats/usageStats' },
{ key: 'moldQr', icon: '码', title: '模具二维码', desc: '生成模具扫码入口', url: '/pages/deviceQr/deviceQr' },
{ key: 'equipment', icon: '设', title: '设备管理', desc: '维护设备号和备注', url: '/pages/manageEquipment/manageEquipment' },
{ key: 'notices', icon: '通', title: '通知配置', desc: '维护首页通知公告', url: '/pages/manageNotices/manageNotices' },
]
}
return [
{ key: 'products', icon: '产', title: '产品清单', desc: '导入或添加新产品', url: '/pages/manageProducts/manageProducts' },
{ key: 'people', icon: '人', title: '人员清单', desc: '维护员工、管理员和经理', url: '/pages/managePeople/managePeople' },
{ key: 'schedule', icon: '勤', title: '考勤设置', desc: '维护作息时间和考勤范围', url: '/pages/workSchedule/workSchedule' },
{ key: 'ledger', icon: '账', title: '对账小账本', desc: '按月核对最后工序成品数量', url: '/pages/reconciliationLedger/reconciliationLedger' },
{ key: 'dashboard', icon: '看', title: '报工看板', desc: '按日期查看工人日报', url: '/pages/dashboard/dashboard' },
{ key: 'usageStats', icon: '统', title: '设备模具使用统计', desc: '按时间统计设备和模具使用', url: '/pages/usageStats/usageStats' },
{ key: 'notices', icon: '通', title: '通知配置', desc: '维护首页通知公告', url: '/pages/manageNotices/manageNotices' },
]
},
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
},
extractMoldName(scanResult) {
const raw = this.decodeRepeated(scanResult)
if (!raw) {
return ''
}
const patterns = [
/(?:^|[?&])moldName=([^&]+)/i,
/(?:^|[?&])mold_name=([^&]+)/i,
/(?:^|[?&])deviceNo=([^&]+)/i,
/(?:^|[?&])device_no=([^&]+)/i,
/(?:^|[?&])scene=([^&]+)/i,
/moldName=([^&]+)/i,
/deviceNo=([^&]+)/i,
]
for (let index = 0; index < patterns.length; index += 1) {
const matched = raw.match(patterns[index])
if (matched && matched[1]) {
const value = this.decodeRepeated(matched[1])
if (value.includes('moldName=') || value.includes('deviceNo=')) {
return this.extractMoldName(value)
}
return String(value || '').trim()
}
}
return raw
},
async scanDevice() {
wx.scanCode({
onlyFromCamera: false,
success: async res => {
const erpLoginUrl = buildErpLoginConfirmUrlFromScanResult(res)
if (erpLoginUrl) {
wx.navigateTo({ url: erpLoginUrl })
return
}
const candidates = [res.path, res.result, res.rawData]
const moldName = candidates.reduce((found, item) => (
found || this.extractMoldName(item)
), '')
if (!moldName) {
wx.showToast({ title: '未识别模具名称', icon: 'none' })
return
}
const paramName = moldName.startsWith('mold=') ? 'scene' : 'moldName'
try {
wx.showLoading({ title: paramName === 'scene' ? '校验中' : '定位中' })
if (paramName === 'scene') {
const resolved = await api.resolveMoldScene(moldName)
await locationGuard.ensureFactoryLocation(resolved.attendancePointName)
} else {
await locationGuard.ensureFactoryLocation()
}
} catch (error) {
wx.showModal({
title: '无法扫码报工',
content: error.message || '当前位置不在允许范围内',
showCancel: false,
})
return
} finally {
wx.hideLoading()
}
wx.navigateTo({
url: `/pages/clock/clock?${paramName}=${encodeURIComponent(moldName)}`,
})
},
fail: error => {
if (error.errMsg && error.errMsg.includes('cancel')) {
return
}
wx.showToast({ title: '扫码失败', icon: 'none' })
},
})
},
async wechatLogin(e) {
const phoneCode = e.detail && e.detail.code
if (!phoneCode) {
wx.showToast({ title: '未获得手机号授权', icon: 'none' })
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({ refreshLocation: true })
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
} 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) {
wx.showToast({ title: '请输入姓名', icon: 'none' })
return
}
if (!result || !result.temporaryToken) {
wx.showToast({ title: '临时工注册信息已失效,请重新登录', icon: 'none' })
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({ refreshLocation: true })
wx.showToast({ title: '临时工已生效', icon: 'success' })
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
} finally {
wx.hideLoading()
this.closeTemporaryWorkerModal()
}
},
logout() {
api.logout()
this.setData({ locationInitialized: false })
this.load()
},
chooseAvatar() {
const user = this.data.currentUser
if (!user) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const save = tempFilePath => {
const saveToStorage = path => {
wx.setStorageSync(avatarKey(user.phone), path)
this.setData({ avatarPath: path })
}
if (wx.saveFile) {
wx.saveFile({
tempFilePath,
success: res => saveToStorage(res.savedFilePath),
fail: () => saveToStorage(tempFilePath),
})
return
}
saveToStorage(tempFilePath)
}
if (wx.chooseMedia) {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: res => {
const file = res.tempFiles && res.tempFiles[0]
if (file && file.tempFilePath) {
save(file.tempFilePath)
}
},
})
return
}
wx.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: res => {
const path = res.tempFilePaths && res.tempFilePaths[0]
if (path) {
save(path)
}
},
})
},
switchCurrentRole() {
const user = this.data.currentUser
const choices = (user && user.rolesDisplay ? user.rolesDisplay : [])
.filter(item => item.role !== user.role)
if (!choices.length) {
wx.showToast({ title: '没有可切换的角色', icon: 'none' })
return
}
wx.showActionSheet({
itemList: choices.map(item => item.roleName),
success: async res => {
const target = choices[res.tapIndex]
if (!target) {
return
}
wx.showLoading({ title: '切换中' })
try {
await api.switchRole(target.role)
await this.load({ refreshLocation: true })
wx.showToast({ title: '已切换角色', icon: 'success' })
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
} finally {
wx.hideLoading()
}
},
})
},
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({ refreshLocation: true })
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
} finally {
wx.hideLoading()
}
},
openAction(e) {
wx.navigateTo({
url: e.currentTarget.dataset.url,
})
},
openOverview(e) {
const url = e.currentTarget.dataset.url
const disabledText = e.currentTarget.dataset.disabledText
if (!url) {
wx.showToast({ title: disabledText || '当前角色无权限', icon: 'none' })
return
}
wx.navigateTo({ url })
},
noop() {},
resetDemo() {
wx.showModal({
title: '退出登录',
content: '退出当前账号后需要重新进行手机号快捷登录。',
success: res => {
if (res.confirm) {
api.logout()
this.setData({ roleChoices: [], selectionToken: '', locationInitialized: false })
this.load()
}
},
})
},
})