新增ERP扫码登录确认页
This commit is contained in:
parent
316d054664
commit
06f99c5096
1
app.json
1
app.json
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
"pages/index/index",
|
"pages/index/index",
|
||||||
|
"pages/erpLoginConfirm/erpLoginConfirm",
|
||||||
"pages/clock/clock",
|
"pages/clock/clock",
|
||||||
"pages/cleaningReport/cleaningReport",
|
"pages/cleaningReport/cleaningReport",
|
||||||
"pages/reportForm/reportForm",
|
"pages/reportForm/reportForm",
|
||||||
|
|||||||
119
pages/erpLoginConfirm/erpLoginConfirm.js
Normal file
119
pages/erpLoginConfirm/erpLoginConfirm.js
Normal file
@ -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),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
3
pages/erpLoginConfirm/erpLoginConfirm.json
Normal file
3
pages/erpLoginConfirm/erpLoginConfirm.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "ERP扫码登录"
|
||||||
|
}
|
||||||
54
pages/erpLoginConfirm/erpLoginConfirm.wxml
Normal file
54
pages/erpLoginConfirm/erpLoginConfirm.wxml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<view class="page safe-bottom erp-login-page">
|
||||||
|
<view class="erp-login-nav">
|
||||||
|
<text class="erp-login-nav-title">ERP扫码登录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="header">
|
||||||
|
<text class="title">{{preview.systemName || '嘉恒智能五金 ERP'}}</text>
|
||||||
|
<text class="subtitle">{{message}}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card login-card">
|
||||||
|
<view class="status-mark {{status === 'CONFIRMED' ? 'success' : ''}} {{status === 'FAILED' || status === 'EXPIRED' ? 'danger' : ''}}">
|
||||||
|
<text>{{status === 'CONFIRMED' ? '已确认' : (status === 'CANCELLED' ? '已取消' : (status === 'FAILED' || status === 'EXPIRED' ? '不可用' : '待确认'))}}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view wx:if="{{loading}}" class="loading-text">正在加载</view>
|
||||||
|
|
||||||
|
<block wx:if="{{preview}}">
|
||||||
|
<view class="meta-row">
|
||||||
|
<text class="label">发起时间</text>
|
||||||
|
<text class="value">{{preview.startedAtText || preview.startedAt}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="meta-row">
|
||||||
|
<text class="label">过期时间</text>
|
||||||
|
<text class="value">{{preview.expiresAtText || preview.expiresAt}}</text>
|
||||||
|
</view>
|
||||||
|
<view wx:if="{{preview.requestIpHint}}" class="meta-row">
|
||||||
|
<text class="label">登录来源</text>
|
||||||
|
<text class="value">{{preview.requestIpHint}}</text>
|
||||||
|
</view>
|
||||||
|
<view wx:if="{{preview.userAgentHint}}" class="meta-row">
|
||||||
|
<text class="label">浏览器</text>
|
||||||
|
<text class="value">{{preview.userAgentHint}}</text>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<view wx:if="{{canConfirm || canCancel}}" class="button-stack">
|
||||||
|
<button
|
||||||
|
class="btn primary full"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
bindgetphonenumber="confirmLogin"
|
||||||
|
loading="{{confirming}}"
|
||||||
|
disabled="{{confirming || cancelling}}"
|
||||||
|
>确认登录</button>
|
||||||
|
<button
|
||||||
|
wx:if="{{canCancel}}"
|
||||||
|
class="btn secondary full"
|
||||||
|
bindtap="cancelLogin"
|
||||||
|
loading="{{cancelling}}"
|
||||||
|
disabled="{{confirming || cancelling}}"
|
||||||
|
>取消</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
82
pages/erpLoginConfirm/erpLoginConfirm.wxss
Normal file
82
pages/erpLoginConfirm/erpLoginConfirm.wxss
Normal file
@ -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;
|
||||||
|
}
|
||||||
24
scripts/test-erp-login-confirm.mjs
Normal file
24
scripts/test-erp-login-confirm.mjs
Normal file
@ -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");
|
||||||
45
utils/api.js
45
utils/api.js
@ -1001,6 +1001,48 @@ const loginWithPhoneCode = async (phoneCode, selectedRole = '') => {
|
|||||||
return autoSelectDefaultRole(normalizeLoginResult(data))
|
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 createTemporaryWorker = async (temporaryToken, temporaryName = '', attendancePoint = null) => {
|
||||||
const data = await requestClient.request({
|
const data = await requestClient.request({
|
||||||
url: '/api/auth/wechat-login',
|
url: '/api/auth/wechat-login',
|
||||||
@ -1812,8 +1854,10 @@ module.exports = {
|
|||||||
approveReport,
|
approveReport,
|
||||||
buildReportDraft,
|
buildReportDraft,
|
||||||
calculateStandardWorkload,
|
calculateStandardWorkload,
|
||||||
|
cancelErpLogin,
|
||||||
classifyBeat,
|
classifyBeat,
|
||||||
classifyWorkload,
|
classifyWorkload,
|
||||||
|
confirmErpLogin,
|
||||||
continueWork,
|
continueWork,
|
||||||
createTemporaryWorker,
|
createTemporaryWorker,
|
||||||
createMoldLockFeedback,
|
createMoldLockFeedback,
|
||||||
@ -1837,6 +1881,7 @@ module.exports = {
|
|||||||
generateMoldQrBatch,
|
generateMoldQrBatch,
|
||||||
getClockState,
|
getClockState,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
|
getErpLoginPreview,
|
||||||
getHomeSummary,
|
getHomeSummary,
|
||||||
getReport,
|
getReport,
|
||||||
getUsageStatsDetail,
|
getUsageStatsDetail,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user