From 06f99c5096ec7930714c11cbac4b5a7a7cfaedf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A4=E5=AD=A6=E4=BC=9A?= Date: Mon, 6 Jul 2026 10:50:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EERP=E6=89=AB=E7=A0=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E7=A1=AE=E8=AE=A4=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.json | 1 + pages/erpLoginConfirm/erpLoginConfirm.js | 119 +++++++++++++++++++++ pages/erpLoginConfirm/erpLoginConfirm.json | 3 + pages/erpLoginConfirm/erpLoginConfirm.wxml | 54 ++++++++++ pages/erpLoginConfirm/erpLoginConfirm.wxss | 82 ++++++++++++++ scripts/test-erp-login-confirm.mjs | 24 +++++ utils/api.js | 45 ++++++++ 7 files changed, 328 insertions(+) create mode 100644 pages/erpLoginConfirm/erpLoginConfirm.js create mode 100644 pages/erpLoginConfirm/erpLoginConfirm.json create mode 100644 pages/erpLoginConfirm/erpLoginConfirm.wxml create mode 100644 pages/erpLoginConfirm/erpLoginConfirm.wxss create mode 100644 scripts/test-erp-login-confirm.mjs diff --git a/app.json b/app.json index 759dc58..0d4f814 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,7 @@ { "pages": [ "pages/index/index", + "pages/erpLoginConfirm/erpLoginConfirm", "pages/clock/clock", "pages/cleaningReport/cleaningReport", "pages/reportForm/reportForm", diff --git a/pages/erpLoginConfirm/erpLoginConfirm.js b/pages/erpLoginConfirm/erpLoginConfirm.js new file mode 100644 index 0000000..b28282e --- /dev/null +++ b/pages/erpLoginConfirm/erpLoginConfirm.js @@ -0,0 +1,119 @@ +const api = require('../../utils/api') + +const finalStatuses = ['CONFIRMED', 'CANCELLED', 'FAILED', 'EXPIRED', 'CONSUMED'] + +Page({ + data: { + ticket: '', + sessionId: '', + preview: null, + loading: true, + confirming: false, + cancelling: false, + status: 'LOADING', + message: '正在读取登录信息', + canConfirm: false, + canCancel: false, + }, + onLoad(options) { + const scene = this.decodeRepeated(options.scene || '') + const fallback = scene || this.buildFallbackScene(options) + const ticket = this.extractTicket(fallback) + const sessionId = this.extractSessionId(fallback) + this.setData({ ticket, sessionId }) + this.loadPreview() + }, + buildFallbackScene(options) { + const ticket = options.ticket || '' + const sessionId = options.session_id || options.sessionId || '' + return `ticket=${ticket}&session_id=${sessionId}` + }, + 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 + }, + extractTicket(value) { + return this.extractSceneValue(value, ['ticket']) + }, + extractSessionId(value) { + return this.extractSceneValue(value, ['session_id', 'sessionId']) + }, + extractSceneValue(value, names) { + const raw = this.decodeRepeated(value) + for (let index = 0; index < names.length; index += 1) { + const name = names[index] + const matched = raw.match(new RegExp(`(?:^|[?&])${name}=([^&]+)`)) + if (matched && matched[1]) { + return this.decodeRepeated(matched[1]) + } + } + return '' + }, + async loadPreview() { + if (!this.data.ticket || !this.data.sessionId) { + this.updateStatus('FAILED', '登录二维码无效') + return + } + try { + const preview = await api.getErpLoginPreview(this.data.ticket, this.data.sessionId) + this.setData({ preview, loading: false }) + this.updateStatus('PENDING', '请确认是否登录 ERP') + } catch (error) { + this.updateStatus('FAILED', error.message || '登录信息读取失败') + } + }, + async confirmLogin(e) { + const phoneCode = e.detail && e.detail.code + if (!phoneCode) { + wx.showToast({ title: '未获得手机号授权', icon: 'none' }) + return + } + this.setData({ confirming: true, message: '正在确认登录' }) + try { + const result = await api.confirmErpLogin(this.data.ticket, this.data.sessionId, phoneCode) + const status = String(result.status || '').toUpperCase() + this.updateStatus( + status, + status === 'CONFIRMED' ? '已确认,请回到电脑端' : (result.failureReason || '确认失败'), + ) + } catch (error) { + this.updateStatus('FAILED', error.message || '确认失败') + } finally { + this.setData({ confirming: false }) + } + }, + async cancelLogin() { + this.setData({ cancelling: true, message: '正在取消登录' }) + try { + const result = await api.cancelErpLogin(this.data.ticket, this.data.sessionId) + this.updateStatus(result.status || 'CANCELLED', '已取消本次登录') + } catch (error) { + wx.showToast({ title: error.message || '取消失败', icon: 'none' }) + this.updateStatus(this.data.status || 'PENDING', this.data.message || '请确认是否登录 ERP') + } finally { + this.setData({ cancelling: false }) + } + }, + updateStatus(status, message) { + const normalized = String(status || '').toUpperCase() + const actionable = normalized === 'PENDING' || normalized === 'SCANNED' + this.setData({ + loading: false, + status: normalized, + message, + canConfirm: actionable && !finalStatuses.includes(normalized), + canCancel: actionable && !finalStatuses.includes(normalized), + }) + }, +}) diff --git a/pages/erpLoginConfirm/erpLoginConfirm.json b/pages/erpLoginConfirm/erpLoginConfirm.json new file mode 100644 index 0000000..0da8963 --- /dev/null +++ b/pages/erpLoginConfirm/erpLoginConfirm.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "ERP扫码登录" +} diff --git a/pages/erpLoginConfirm/erpLoginConfirm.wxml b/pages/erpLoginConfirm/erpLoginConfirm.wxml new file mode 100644 index 0000000..30010c5 --- /dev/null +++ b/pages/erpLoginConfirm/erpLoginConfirm.wxml @@ -0,0 +1,54 @@ + + + + + {{preview.systemName || '嘉恒智能五金 ERP'}} + {{message}} + + + + diff --git a/pages/erpLoginConfirm/erpLoginConfirm.wxss b/pages/erpLoginConfirm/erpLoginConfirm.wxss new file mode 100644 index 0000000..6780b20 --- /dev/null +++ b/pages/erpLoginConfirm/erpLoginConfirm.wxss @@ -0,0 +1,82 @@ +.erp-login-page { + padding-top: 0; +} + +.erp-login-nav { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + height: 176rpx; + margin: 0 -24rpx 22rpx; + padding-top: 88rpx; + border-bottom: 1rpx solid rgba(213, 222, 235, 0.9); + background: rgba(247, 248, 250, 0.96); + box-sizing: border-box; +} + +.erp-login-nav-title { + color: #111827; + font-size: 34rpx; + font-weight: 700; + line-height: 1; +} + +.login-card { + padding-top: 92rpx; +} + +.status-mark { + position: absolute; + right: 26rpx; + top: 24rpx; + min-width: 120rpx; + padding: 12rpx 18rpx; + border: 2rpx solid rgba(20, 99, 255, 0.25); + border-radius: 14rpx; + background: #edf4ff; + color: #1456c2; + font-size: 24rpx; + font-weight: 800; + line-height: 1; + text-align: center; + box-sizing: border-box; +} + +.status-mark.success { + border-color: rgba(8, 116, 67, 0.24); + background: #e7f7ef; + color: #087443; +} + +.status-mark.danger { + border-color: rgba(180, 35, 24, 0.24); + background: #fff1f0; + color: #b42318; +} + +.loading-text { + color: #6b778c; + font-size: 28rpx; + line-height: 1.5; +} + +.meta-row { + padding: 20rpx 0; + border-bottom: 1rpx solid rgba(102, 112, 133, 0.16); +} + +.meta-row:last-child { + border-bottom: 0; +} + +.meta-row .value { + word-break: break-all; +} + +.button-stack { + display: grid; + gap: 20rpx; + margin-top: 34rpx; +} diff --git a/scripts/test-erp-login-confirm.mjs b/scripts/test-erp-login-confirm.mjs new file mode 100644 index 0000000..56b07b8 --- /dev/null +++ b/scripts/test-erp-login-confirm.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const appJson = JSON.parse(readFileSync("app.json", "utf8")); +const apiSource = readFileSync("utils/api.js", "utf8"); +const pageJs = readFileSync("pages/erpLoginConfirm/erpLoginConfirm.js", "utf8"); +const pageWxml = readFileSync("pages/erpLoginConfirm/erpLoginConfirm.wxml", "utf8"); + +assert.ok(appJson.pages.includes("pages/erpLoginConfirm/erpLoginConfirm")); +assert.match(apiSource, /const getErpLoginPreview = async \(ticket, sessionId\) =>/); +assert.match(apiSource, /const confirmErpLogin = async \(ticket, sessionId, phoneCode\) =>/); +assert.match(apiSource, /const cancelErpLogin = async \(ticket, sessionId\) =>/); +assert.match(apiSource, /session_id: sessionId/); +assert.match(pageJs, /decodeRepeated\(options\.scene/); +assert.match(pageJs, /extractTicket/); +assert.match(pageJs, /extractSessionId/); +assert.match(pageJs, /confirmErpLogin/); +assert.match(pageWxml, /getPhoneNumber/); +assert.match(pageWxml, /确认登录/); +assert.match(pageWxml, /取消/); +assert.doesNotMatch(pageJs, /ensureFactoryLocation/); +assert.doesNotMatch(pageJs, /getClockState/); + +console.log("erp login confirm page checks passed"); diff --git a/utils/api.js b/utils/api.js index f1d7f7e..b77fa3b 100644 --- a/utils/api.js +++ b/utils/api.js @@ -1001,6 +1001,48 @@ const loginWithPhoneCode = async (phoneCode, selectedRole = '') => { return autoSelectDefaultRole(normalizeLoginResult(data)) } +const toCamelErpLoginPreview = data => ({ + sessionId: data.session_id, + systemName: data.system_name || '嘉恒智能五金 ERP', + startedAt: data.started_at || '', + startedAtText: data.started_at ? util.formatTime(data.started_at) : '', + expiresAt: data.expires_at || '', + expiresAtText: data.expires_at ? util.formatTime(data.expires_at) : '', + requestIpHint: data.request_ip_hint || '', + userAgentHint: data.user_agent_hint || '', +}) + +const toCamelErpLoginAction = data => ({ + status: data.status || '', + failureReason: data.failure_reason || '', +}) + +const getErpLoginPreview = async (ticket, sessionId) => toCamelErpLoginPreview(await requestClient.request({ + url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}`, + params: { + session_id: sessionId, + }, +})) + +const confirmErpLogin = async (ticket, sessionId, phoneCode) => toCamelErpLoginAction(await requestClient.request({ + url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}/confirm`, + method: 'POST', + params: { + session_id: sessionId, + }, + data: { + phone_code: phoneCode, + }, +})) + +const cancelErpLogin = async (ticket, sessionId) => toCamelErpLoginAction(await requestClient.request({ + url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}/cancel`, + method: 'POST', + params: { + session_id: sessionId, + }, +})) + const createTemporaryWorker = async (temporaryToken, temporaryName = '', attendancePoint = null) => { const data = await requestClient.request({ url: '/api/auth/wechat-login', @@ -1812,8 +1854,10 @@ module.exports = { approveReport, buildReportDraft, calculateStandardWorkload, + cancelErpLogin, classifyBeat, classifyWorkload, + confirmErpLogin, continueWork, createTemporaryWorker, createMoldLockFeedback, @@ -1837,6 +1881,7 @@ module.exports = { generateMoldQrBatch, getClockState, getCurrentUser, + getErpLoginPreview, getHomeSummary, getReport, getUsageStatsDetail,