Merge branch 'main' of http://1.117.67.95:3000/ZhangRongHong/tyapi-frontend
This commit is contained in:
2
.env
2
.env
@@ -1 +1,3 @@
|
||||
VITE_API_URL="https://api.tianyuanapi.com"
|
||||
VITE_CAPTCHA_SCENE_ID="wynt39to"
|
||||
VITE_CAPTCHA_ENCRYPTED_MODE=false
|
||||
@@ -218,6 +218,7 @@
|
||||
"until": true,
|
||||
"upperCase": true,
|
||||
"useActiveElement": true,
|
||||
"useAliyunCaptcha": true,
|
||||
"useAnimate": true,
|
||||
"useAppStore": true,
|
||||
"useArrayDifference": true,
|
||||
|
||||
2
auto-imports.d.ts
vendored
2
auto-imports.d.ts
vendored
@@ -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']>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
// 产品列表(用户端接口)
|
||||
|
||||
176
src/composables/useAliyunCaptcha.js
Normal file
176
src/composables/useAliyunCaptcha.js
Normal 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
|
||||
@@ -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) {
|
||||
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 {
|
||||
|
||||
@@ -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) {
|
||||
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 {
|
||||
|
||||
@@ -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) {
|
||||
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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
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) {
|
||||
// 用户点击取消或关闭对话框,不处理
|
||||
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>
|
||||
|
||||
@@ -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, ... }
|
||||
|
||||
Reference in New Issue
Block a user