feat: migrate app routes and business modules
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<view class="auth-shell">
|
||||
<view class="auth-shell__header">
|
||||
<image
|
||||
class="auth-shell__header-image"
|
||||
src="/static/assets/modules/auth/opaque/auth-header.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="auth-shell__brand-lockup">
|
||||
<image
|
||||
class="auth-shell__seal"
|
||||
src="/static/assets/foundation/transparent/brand-seal.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="auth-shell__project-name">代代相传</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="auth-shell__paper"><slot /></view>
|
||||
|
||||
<slot name="overlay" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: var(--app-viewport-height);
|
||||
overflow-x: hidden;
|
||||
background: #940400;
|
||||
color: #493323;
|
||||
}
|
||||
|
||||
.auth-shell__header {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: calc(var(--app-safe-top) + min(36.5vw, 175px));
|
||||
overflow: hidden;
|
||||
padding-top: var(--app-safe-top);
|
||||
background: #940400;
|
||||
}
|
||||
|
||||
.auth-shell__header-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.auth-shell__brand-lockup {
|
||||
position: absolute;
|
||||
top: calc(var(--app-safe-top) + clamp(46px, 11.5vw, 58px));
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auth-shell__seal {
|
||||
position: static;
|
||||
width: clamp(56px, 17vw, 78px);
|
||||
height: clamp(67px, 20.4vw, 94px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.auth-shell__paper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding-bottom: var(--app-safe-bottom);
|
||||
background-color: #f7f0e5;
|
||||
background-image:
|
||||
url("/static/assets/modules/auth/opaque/sign-in-backdrop.png"),
|
||||
url("/static/assets/foundation/opaque/auth-page-paper.jpg");
|
||||
background-position: center calc(-48.18vw), center top;
|
||||
background-size: 100% auto, 100% auto;
|
||||
background-repeat: no-repeat, repeat-y;
|
||||
}
|
||||
|
||||
.auth-shell__project-name {
|
||||
color: #f8dfa6;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: clamp(14px, 24rpx, 17px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<view
|
||||
v-show="visible"
|
||||
class="tac-layer"
|
||||
:aria-hidden="visible ? 'false' : 'true'"
|
||||
@click.stop
|
||||
>
|
||||
<view
|
||||
id="jiapu-tac-host"
|
||||
:prop="renderContext"
|
||||
:change:prop="tacRenderer.onContextChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TacVerification",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
context: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["success", "failure", "error", "cancel"],
|
||||
computed: {
|
||||
renderContext() {
|
||||
return {
|
||||
...(this.context || {}),
|
||||
visible: this.visible,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleTacSuccess(payload) {
|
||||
this.$emit("success", payload);
|
||||
},
|
||||
handleTacFailure(payload) {
|
||||
this.$emit("failure", payload);
|
||||
},
|
||||
handleTacError(payload) {
|
||||
this.$emit("error", payload);
|
||||
},
|
||||
handleTacCancel(payload) {
|
||||
this.$emit("cancel", payload);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<script module="tacRenderer" lang="renderjs">
|
||||
const TAC_STYLE_URL = "./static/tac/css/tac.css";
|
||||
const TAC_SCRIPT_URL = "./static/tac/js/tac.min.js";
|
||||
const TAC_ADAPTER_URL = "./static/tac/js/jiapu-tac-adapter.js";
|
||||
|
||||
const loadStyle = (href) => {
|
||||
const existing = document.querySelector(`link[data-jiapu-tac="${href}"]`);
|
||||
if (existing && (existing.dataset.jiapuState === "loaded" || existing.sheet)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (existing && existing.dataset.jiapuState === "loading") {
|
||||
return new Promise((resolve, reject) => {
|
||||
existing.addEventListener("load", resolve, { once: true });
|
||||
existing.addEventListener("error", () => reject(new Error(`安全验证样式加载失败:${href}`)), { once: true });
|
||||
});
|
||||
}
|
||||
if (existing) existing.remove();
|
||||
return new Promise((resolve, reject) => {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.setAttribute("data-jiapu-tac", href);
|
||||
link.dataset.jiapuState = "loading";
|
||||
link.onload = () => {
|
||||
link.dataset.jiapuState = "loaded";
|
||||
resolve();
|
||||
};
|
||||
link.onerror = () => {
|
||||
link.remove();
|
||||
reject(new Error(`安全验证样式加载失败:${href}`));
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
};
|
||||
|
||||
const loadScript = (src, ready) => {
|
||||
if (ready()) return Promise.resolve();
|
||||
const existing = document.querySelector(`script[data-jiapu-tac="${src}"]`);
|
||||
if (existing && existing.dataset.jiapuState === "loading") {
|
||||
return new Promise((resolve, reject) => {
|
||||
existing.addEventListener("load", () => {
|
||||
if (ready()) {
|
||||
existing.dataset.jiapuState = "loaded";
|
||||
resolve();
|
||||
} else {
|
||||
existing.remove();
|
||||
reject(new Error(`安全验证脚本未提供预期能力:${src}`));
|
||||
}
|
||||
}, { once: true });
|
||||
existing.addEventListener("error", () => {
|
||||
existing.remove();
|
||||
reject(new Error(`安全验证脚本加载失败:${src}`));
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
if (existing) existing.remove();
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.setAttribute("data-jiapu-tac", src);
|
||||
script.dataset.jiapuState = "loading";
|
||||
script.onload = () => {
|
||||
if (!ready()) {
|
||||
script.remove();
|
||||
reject(new Error(`安全验证脚本未提供预期能力:${src}`));
|
||||
return;
|
||||
}
|
||||
script.dataset.jiapuState = "loaded";
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => {
|
||||
script.remove();
|
||||
reject(new Error(`安全验证脚本加载失败:${src}`));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
generation: 0,
|
||||
tac: null,
|
||||
requestContext: null,
|
||||
challenge: null,
|
||||
activeXhr: null,
|
||||
fatalSent: false,
|
||||
completionSent: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onContextChange(nextContext) {
|
||||
const nextVisible = Boolean(nextContext && nextContext.visible === true);
|
||||
this.generation += 1;
|
||||
const generation = this.generation;
|
||||
this.destroyTac();
|
||||
this.requestContext = nextContext || null;
|
||||
this.challenge = null;
|
||||
this.fatalSent = false;
|
||||
this.completionSent = false;
|
||||
if (!nextContext || nextContext.visible !== true) return;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
loadStyle(TAC_STYLE_URL),
|
||||
loadScript(TAC_SCRIPT_URL, () => Boolean(window.TAC && window.CaptchaConfig)),
|
||||
]);
|
||||
await loadScript(TAC_ADAPTER_URL, () => Boolean(window.JiapuTacAdapter));
|
||||
if (generation !== this.generation || !this.requestContext || this.requestContext.visible !== true) return;
|
||||
this.createTac();
|
||||
} catch (error) {
|
||||
if (generation !== this.generation || !this.requestContext || this.requestContext.visible !== true) return;
|
||||
this.notifyFatal(error);
|
||||
}
|
||||
},
|
||||
createTac() {
|
||||
if (!window.TAC || !window.CaptchaConfig || !window.JiapuTacAdapter) {
|
||||
throw new Error("安全验证组件未完整加载");
|
||||
}
|
||||
this.completionSent = false;
|
||||
const generation = this.generation;
|
||||
const requestContext = this.requestContext;
|
||||
const isCurrent = () =>
|
||||
generation === this.generation &&
|
||||
this.requestContext === requestContext &&
|
||||
requestContext.visible === true;
|
||||
const config = new window.CaptchaConfig({
|
||||
bindEl: "#jiapu-tac-host",
|
||||
requestCaptchaDataUrl: this.requestContext.challengeUrl,
|
||||
validCaptchaUrl: this.requestContext.verifyUrl,
|
||||
requestHeaders: {
|
||||
clientid: this.requestContext.clientId,
|
||||
},
|
||||
validSuccess: (response, captcha, tac) => {
|
||||
if (!isCurrent() || this.completionSent) return;
|
||||
// 供应商脚本在动画结束、窗口销毁等边界下可能重复触发回调。
|
||||
// 先封闭本轮并使所有旧闭包失效,再通知逻辑层,避免重复发短信或重复提交认证。
|
||||
this.completionSent = true;
|
||||
this.generation += 1;
|
||||
this.abortActiveRequest();
|
||||
const verificationData = response && response.data;
|
||||
tac.destroyWindow();
|
||||
this.tac = null;
|
||||
this.$ownerInstance.callMethod("handleTacSuccess", {
|
||||
requestId: requestContext.requestId,
|
||||
validToken: verificationData && verificationData.validToken,
|
||||
expireSeconds: verificationData && verificationData.expireSeconds,
|
||||
});
|
||||
},
|
||||
validFail: (response, captcha, tac) => {
|
||||
if (!isCurrent()) return;
|
||||
this.$ownerInstance.callMethod("handleTacFailure", {
|
||||
requestId: requestContext.requestId,
|
||||
message: (response && response.msg) || "行为验证未通过,请重试",
|
||||
});
|
||||
tac.reloadCaptcha();
|
||||
},
|
||||
btnCloseFun: (event, tac) => {
|
||||
if (!isCurrent() || this.completionSent) return;
|
||||
this.completionSent = true;
|
||||
this.generation += 1;
|
||||
this.abortActiveRequest();
|
||||
tac.destroyWindow();
|
||||
this.tac = null;
|
||||
this.$ownerInstance.callMethod("handleTacCancel", {
|
||||
requestId: requestContext.requestId,
|
||||
});
|
||||
},
|
||||
btnRefreshFun: (event, tac) => {
|
||||
if (isCurrent()) tac.reloadCaptcha();
|
||||
},
|
||||
});
|
||||
|
||||
// SDK 原生传输会把空体 HTTP 500 当成功,并且 verify 只看外层 code。
|
||||
// 此处统一替换为严格 2xx JSON 传输,再由唯一适配器验证 passed 与 validToken。
|
||||
config.doSendRequest = (options) => this.sendStrictRequest(options);
|
||||
config.addRequestChain({
|
||||
preRequest: (type, request) => this.beforeRequest(type, request),
|
||||
postRequest: (type, request, response) => this.afterRequest(type, response),
|
||||
});
|
||||
this.tac = new window.TAC(config);
|
||||
this.tac.init();
|
||||
},
|
||||
beforeRequest(type, request) {
|
||||
if (type === "requestCaptchaData") {
|
||||
request.data = {
|
||||
tenantId: this.requestContext.tenantId,
|
||||
subject: this.requestContext.subject,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
if (type === "validCaptcha") {
|
||||
request.data = window.JiapuTacAdapter.buildVerifyBody(
|
||||
request.data,
|
||||
this.requestContext,
|
||||
this.challenge,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
afterRequest(type, response) {
|
||||
try {
|
||||
if (type === "requestCaptchaData") {
|
||||
const mapped = window.JiapuTacAdapter.normalizeChallengeResponse(response, this.requestContext);
|
||||
this.challenge = mapped.challenge;
|
||||
this.replaceResponse(response, mapped.sdkResponse);
|
||||
} else if (type === "validCaptcha") {
|
||||
const mapped = window.JiapuTacAdapter.normalizeVerifyResponse(response);
|
||||
this.replaceResponse(response, mapped);
|
||||
}
|
||||
} catch (error) {
|
||||
const failure = window.JiapuTacAdapter.toSdkFailure(error);
|
||||
this.replaceResponse(response, failure);
|
||||
if (type === "requestCaptchaData") this.notifyFatal(error);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
replaceResponse(target, source) {
|
||||
Object.keys(target).forEach((key) => delete target[key]);
|
||||
Object.assign(target, source);
|
||||
},
|
||||
sendStrictRequest(options) {
|
||||
return new Promise((resolve) => {
|
||||
this.abortActiveRequest();
|
||||
const generation = this.generation;
|
||||
const xhr = new XMLHttpRequest();
|
||||
this.activeXhr = xhr;
|
||||
let settled = false;
|
||||
const settle = (response) => {
|
||||
if (settled || generation !== this.generation || this.activeXhr !== xhr) return;
|
||||
settled = true;
|
||||
this.activeXhr = null;
|
||||
resolve(response);
|
||||
};
|
||||
xhr.open(options.method || "POST", options.url);
|
||||
xhr.timeout = 15000;
|
||||
Object.keys(options.headers || {}).forEach((name) => {
|
||||
xhr.setRequestHeader(name, String(options.headers[name]));
|
||||
});
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return;
|
||||
if (!(xhr.status >= 200 && xhr.status < 300)) {
|
||||
settle({ code: xhr.status || 503, msg: `安全验证服务请求失败(${xhr.status || "网络异常"})` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsedResponse = JSON.parse(xhr.responseText);
|
||||
settle(
|
||||
parsedResponse && typeof parsedResponse === "object"
|
||||
? parsedResponse
|
||||
: { code: 502, msg: "安全验证服务返回无效数据" },
|
||||
);
|
||||
} catch (error) {
|
||||
settle({ code: 502, msg: "安全验证服务返回了非 JSON 数据" });
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => settle({ code: 503, msg: "安全验证服务网络连接失败" });
|
||||
xhr.ontimeout = () => settle({ code: 504, msg: "安全验证服务请求超时" });
|
||||
const payload = typeof options.data === "string" ? options.data : JSON.stringify(options.data || {});
|
||||
xhr.send(payload);
|
||||
});
|
||||
},
|
||||
abortActiveRequest() {
|
||||
const xhr = this.activeXhr;
|
||||
this.activeXhr = null;
|
||||
if (!xhr || xhr.readyState === XMLHttpRequest.DONE) return;
|
||||
xhr.onreadystatechange = null;
|
||||
xhr.onerror = null;
|
||||
xhr.ontimeout = null;
|
||||
xhr.onabort = null;
|
||||
xhr.abort();
|
||||
},
|
||||
notifyFatal(error) {
|
||||
if (this.fatalSent) return;
|
||||
this.fatalSent = true;
|
||||
this.$ownerInstance.callMethod("handleTacError", {
|
||||
requestId: this.requestContext && this.requestContext.requestId,
|
||||
message: (error && error.message) || "安全验证暂不可用",
|
||||
});
|
||||
},
|
||||
destroyTac() {
|
||||
this.abortActiveRequest();
|
||||
if (this.tac) this.tac.destroyWindow();
|
||||
this.tac = null;
|
||||
const host = document.querySelector("#jiapu-tac-host");
|
||||
if (host) host.innerHTML = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tac-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user