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() } }, }) }, })