This commit is contained in:
2026-02-28 14:50:27 +08:00
12 changed files with 335 additions and 29 deletions

4
.env
View File

@@ -1 +1,3 @@
VITE_API_URL="https://api.tianyuanapi.com"
VITE_API_URL="https://api.tianyuanapi.com"
VITE_CAPTCHA_SCENE_ID="wynt39to"
VITE_CAPTCHA_ENCRYPTED_MODE=false

View File

@@ -218,6 +218,7 @@
"until": true,
"upperCase": true,
"useActiveElement": true,
"useAliyunCaptcha": true,
"useAnimate": true,
"useAppStore": true,
"useArrayDifference": true,

2
auto-imports.d.ts vendored
View File

@@ -225,6 +225,7 @@ declare global {
const until: typeof import('@vueuse/core')['until']
const upperCase: typeof import('lodash-es')['upperCase']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAliyunCaptcha: typeof import('./src/composables/useAliyunCaptcha.js')['default']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAppStore: typeof import('./src/stores/app.js')['useAppStore']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
@@ -668,6 +669,7 @@ declare module 'vue' {
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly upperCase: UnwrapRef<typeof import('lodash-es')['upperCase']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAliyunCaptcha: UnwrapRef<typeof import('./src/composables/useAliyunCaptcha.js')['default']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAppStore: UnwrapRef<typeof import('./src/stores/app.js')['useAppStore']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>

View File

@@ -5,9 +5,18 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>天远数据</title>
<!-- 阿里云滑块验证码 -->
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "12zxnj" };
</script>
<script
type="text/javascript"
src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"
></script>
</head>
<body>
<div id="app"></div>
<div id="captcha-element"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -40,6 +40,14 @@ export const userApi = {
getUserStats: () => request.get('/users/admin/stats')
}
// 验证码(阿里云滑块)相关接口
export const captchaApi = {
// 获取加密场景 ID用于前端加密模式初始化滑块
getEncryptedSceneId: (params) => request.post('/captcha/encryptedSceneId', params || {}),
// 获取验证码配置(是否启用、场景 ID
getConfig: () => request.get('/captcha/config')
}
// 产品相关接口
export const productApi = {
// 产品列表(用户端接口)

View File

@@ -0,0 +1,176 @@
import { ElMessage } from 'element-plus'
import { captchaApi } from '@/api'
// 阿里云验证码场景 ID需与后端 config.sms.scene_id 一致;加密模式可由后端 config 下发)
const ALIYUN_CAPTCHA_SCENE_ID = import.meta.env.VITE_CAPTCHA_SCENE_ID || "wynt39to"
// 是否启用加密模式:通过环境变量 VITE_CAPTCHA_ENCRYPTED_MODE 控制,为 'true' 不加密
const ENABLE_ENCRYPTED = import.meta.env.VITE_CAPTCHA_ENCRYPTED_MODE === true
let captchaInitialised = false
let captchaReadyPromise = null
let captchaReadyResolve = null
async function ensureCaptchaInit() {
console.log("ENABLE_ENCRYPTED", ENABLE_ENCRYPTED)
if (captchaInitialised || typeof window === "undefined") return
if (typeof window.initAliyunCaptcha !== "function") return
captchaInitialised = true
window.captcha = null
window.__lastBizResponse = null
window.__onCaptchaBizSuccess = null
captchaReadyPromise = new Promise((resolve) => {
captchaReadyResolve = resolve
})
// 非加密模式:仅传 SceneId不调用后端接口
if (!ENABLE_ENCRYPTED) {
console.log("NON-ENCRYPTED")
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
mode: "popup",
element: "#captcha-element",
getInstance(instance) {
window.captcha = instance
if (typeof captchaReadyResolve === "function") {
captchaReadyResolve()
captchaReadyResolve = null
}
},
captchaVerifyCallback(param) {
console.log("captchaVerifyCallback", param)
return typeof window.__captchaVerifyCallback === "function"
? window.__captchaVerifyCallback(param)
: Promise.resolve({
captchaResult: false,
bizResult: false,
})
},
onBizResultCallback(bizResult) {
if (typeof window.__onBizResultCallback === "function") {
window.__onBizResultCallback(bizResult)
}
window.__lastBizResponse = null
window.__onCaptchaBizSuccess = null
},
slideStyle: { width: 360, height: 40 },
language: "cn",
})
return
}
// 加密模式:先从后端获取 EncryptedSceneId再初始化
try {
const resp = await captchaApi.getEncryptedSceneId()
const encryptedSceneId = resp?.data?.data?.encryptedSceneId ?? resp?.data?.encryptedSceneId
if (!encryptedSceneId) {
ElMessage.error("获取验证码参数失败,请稍后重试")
captchaInitialised = false
captchaReadyPromise = null
captchaReadyResolve = null
return
}
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
EncryptedSceneId: encryptedSceneId,
mode: "popup",
element: "#captcha-element",
getInstance(instance) {
window.captcha = instance
if (typeof captchaReadyResolve === "function") {
captchaReadyResolve()
captchaReadyResolve = null
}
},
captchaVerifyCallback(param) {
return typeof window.__captchaVerifyCallback === "function"
? window.__captchaVerifyCallback(param)
: Promise.resolve({ captchaResult: false, bizResult: false })
},
onBizResultCallback(bizResult) {
if (typeof window.__onBizResultCallback === "function") {
window.__onBizResultCallback(bizResult)
}
window.__lastBizResponse = null
window.__onCaptchaBizSuccess = null
},
slideStyle: { width: 360, height: 40 },
language: "cn",
})
} catch (error) {
ElMessage.error("获取验证码参数失败,请稍后重试")
captchaInitialised = false
captchaReadyPromise = null
captchaReadyResolve = null
}
}
/**
* 阿里云滑块验证码通用封装。
* 依赖 index.html 中已加载的 AliyunCaptcha.js初始化在首次调起时执行。
*/
export function useAliyunCaptcha() {
/**
* 先弹出滑块,通过后执行 bizVerify(captchaVerifyParam),再根据结果调用 onSuccess。
* @param { (captchaVerifyParam: string) => Promise<{ success: boolean, data: any, error?: any }> } bizVerify - 业务请求函数,接收滑块参数
* @param { (res: any) => void } onSuccess - 业务成功回调
*/
async function runWithCaptcha(bizVerify, onSuccess) {
if (typeof window === "undefined") {
ElMessage.error("验证码仅支持浏览器环境")
return
}
const loadingInstance = ElMessage({
message: "安全验证加载中...",
type: "info",
duration: 0,
iconClass: "el-icon-loading"
})
try {
window.__captchaVerifyCallback = async (captchaVerifyParam) => {
window.__lastBizResponse = null
try {
const result = await bizVerify(captchaVerifyParam)
window.__lastBizResponse = result
const captchaOk = result?.data?.captchaVerifyResult !== false
const bizOk = result.success === true
return { captchaResult: captchaOk, bizResult: bizOk }
} catch (error) {
return { captchaResult: false, bizResult: false }
}
}
window.__onBizResultCallback = (bizResult) => {
if (
bizResult === true &&
window.__lastBizResponse &&
typeof window.__onCaptchaBizSuccess === "function"
) {
window.__onCaptchaBizSuccess(window.__lastBizResponse)
}
}
await ensureCaptchaInit()
// 首次初始化时 SDK 会异步调用 getInstance需等待实例就绪后再 show
if (captchaReadyPromise) {
await captchaReadyPromise
captchaReadyPromise = null
}
if (!window.captcha) {
ElMessage.error("验证码未加载,请刷新页面重试")
return
}
window.__onCaptchaBizSuccess = onSuccess
window.captcha.show()
} finally {
loadingInstance.close()
}
}
return { runWithCaptcha }
}
export default useAliyunCaptcha

View File

@@ -115,10 +115,12 @@
<script setup name="UserLogin">
import { useUserStore } from '@/stores/user'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
// 登录方式
const loginMethod = ref('sms')
@@ -157,11 +159,19 @@ const sendCode = async () => {
sendingCode.value = true
try {
const result = await userStore.sendCode(form.value.phone, 'login')
if (result.success) {
ElMessage.success('验证码发送成功')
startCountdown()
}
await runWithCaptcha(
async (captchaVerifyParam) => {
return await userStore.sendCode(form.value.phone, 'login', captchaVerifyParam)
},
(res) => {
if (res.success) {
ElMessage.success('验证码发送成功')
startCountdown()
} else {
ElMessage.error(res?.error?.message || '验证码发送失败')
}
}
)
} catch (error) {
console.error('验证码发送失败:', error)
} finally {

View File

@@ -138,10 +138,12 @@
<script setup name="UserRegister">
import { useUserStore } from '@/stores/user'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
// 表单数据
const form = ref({
@@ -175,17 +177,25 @@ const canSubmit = computed(() => {
)
})
// 发送验证码
// 发送验证码(先通过滑块再请求后端发码)
const sendCode = async () => {
if (!canSendCode.value) return
sendingCode.value = true
try {
const result = await userStore.sendCode(form.value.phone, 'register')
if (result.success) {
ElMessage.success('验证码发送成功')
startCountdown()
}
await runWithCaptcha(
async (captchaVerifyParam) => {
return await userStore.sendCode(form.value.phone, 'register', captchaVerifyParam)
},
(res) => {
if (res.success) {
ElMessage.success('验证码发送成功')
startCountdown()
} else {
ElMessage.error(res?.error?.message || '验证码发送失败')
}
}
)
} catch (error) {
console.error('验证码发送失败:', error)
} finally {

View File

@@ -126,10 +126,12 @@
<script setup name="UserResetPassword">
import { useUserStore } from '@/stores/user'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
// 表单数据
const form = ref({
@@ -157,17 +159,25 @@ const canSubmit = computed(() => {
form.value.confirmNewPassword && form.value.confirmNewPassword === form.value.newPassword
})
// 发送验证码
// 发送验证码(先通过滑块再请求后端发码)
const sendCode = async () => {
if (!canSendCode.value) return
sendingCode.value = true
try {
const result = await userStore.sendCode(form.value.phone, 'reset_password')
if (result.success) {
ElMessage.success('验证码发送成功')
startCountdown()
}
await runWithCaptcha(
async (captchaVerifyParam) => {
return await userStore.sendCode(form.value.phone, 'reset_password', captchaVerifyParam)
},
(res) => {
if (res.success) {
ElMessage.success('验证码发送成功')
startCountdown()
} else {
ElMessage.error(res?.error?.message || '验证码发送失败')
}
}
)
} catch (error) {
console.error('验证码发送失败:', error)
} finally {

View File

@@ -27,7 +27,7 @@
<div class="success-text">
<h2 class="success-title">恭喜企业入驻已完成</h2>
<p class="success-desc">您的企业已完成入驻现在可以使用完整的API服务功能</p>
<p class="success-desc">下一步您只需要订阅贵司需要的api接口就可以实现在线调试和使用</p>
<div class="completion-info">
<h3 class="info-title">入驻信息</h3>
<div class="info-grid">

View File

@@ -190,7 +190,8 @@ import {
CheckIcon,
DocumentIcon
} from '@heroicons/vue/24/outline'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const props = defineProps({
formData: {
@@ -210,6 +211,7 @@ const props = defineProps({
const emit = defineEmits(['submit'])
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
// 表单引用
const enterpriseFormRef = ref()
@@ -345,14 +347,21 @@ const sendCode = async () => {
sendingCode.value = true
try {
const result = await userStore.sendCode(form.value.legalPersonPhone, 'certification')
if (result.success) {
ElMessage.success('验证码发送成功')
startCountdown()
}
await runWithCaptcha(
async (captchaVerifyParam) => {
return await userStore.sendCode(form.value.legalPersonPhone, 'certification', captchaVerifyParam)
},
(res) => {
if (res.success) {
ElMessage.success('验证码发送成功')
startCountdown()
} else {
ElMessage.error(res?.error?.message || '验证码发送失败')
}
}
)
} catch (error) {
console.error('验证码发送失败:', error)
ElMessage.error('验证码发送失败,请重试')
} finally {
sendingCode.value = false
}
@@ -428,6 +437,19 @@ const submitForm = async () => {
try {
await enterpriseFormRef.value.validate()
// 显示确认对话框
await ElMessageBox.confirm(
'提交的信息必须为法人真实信息(包括手机号),如信息有误请联系客服。',
'提交确认',
{
confirmButtonText: '确认提交',
cancelButtonText: '取消',
type: 'warning',
distinguishCancelAndClose: true,
customClass: 'submit-confirm-dialog'
}
)
submitting.value = true
// Mock API 调用
@@ -436,7 +458,10 @@ const submitForm = async () => {
emit('submit', form.value)
} catch (error) {
console.error('表单验证失败:', error)
// 用户点击取消或关闭对话框,不处理
if (error !== 'cancel' && error !== 'close') {
console.error('表单验证失败:', error)
}
} finally {
submitting.value = false
}
@@ -723,3 +748,51 @@ onUnmounted(() => {
}
}
</style>
<style>
/* 提交确认对话框样式 */
.submit-confirm-dialog {
max-width: 420px;
}
.submit-confirm-dialog .el-message-box__message {
font-size: 15px;
line-height: 1.6;
color: #606266;
padding: 10px 0;
}
.submit-confirm-dialog .el-message-box__title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.submit-confirm-dialog .el-message-box__btns {
padding-top: 20px;
}
.submit-confirm-dialog .el-button--warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border: none;
font-weight: 500;
}
.submit-confirm-dialog .el-button--warning:hover {
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(217, 119, 6, 0.3);
}
.submit-confirm-dialog .el-button--default {
border: 1px solid #dcdfe6;
color: #606266;
font-weight: 500;
}
.submit-confirm-dialog .el-button--default:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
</style>

View File

@@ -330,12 +330,17 @@ export const useUserStore = defineStore('user', () => {
}
// 发送验证码(使用自定义编码和签名)
const sendCode = async (phone, scene) => {
const sendCode = async (phone, scene, captchaVerifyParam = null) => {
try {
// 1. 生成签名并编码请求数据
const encodedRequest = await generateSMSRequest(phone, scene)
// 2. 发送编码后的请求只包含data字段
// 2. 如果有滑块验证码参数,添加到请求数据中
if (captchaVerifyParam) {
encodedRequest.captchaVerifyParam = captchaVerifyParam
}
// 3. 发送编码后的请求
const response = await userApi.sendCode(encodedRequest)
// 后端返回格式: { success: true, data: {...}, message, ... }