f
This commit is contained in:
22
.env
22
.env
@@ -1,9 +1,17 @@
|
||||
# 客户端可读变量必须以 VITE_ 开头
|
||||
# 不配置 VITE_API_BASE_URL 时,逻辑在 src/api/http.js 的 resolveBaseUrl():
|
||||
# - H5:默认 /api/v1(配合 vite.config.js 里 proxy)
|
||||
# - 非 H5:默认 https://www.tianyuancha.cn/api/v1
|
||||
# API 基础地址控制
|
||||
# ──────────────────────────────────────────────
|
||||
# 方式1:不设置 VITE_API_BASE_URL(留空),由代码自动选择:
|
||||
# H5 dev → /api/v1(走 Vite proxy)
|
||||
# 小程序 dev → http://127.0.0.1:8888/api/v1(直连本地)
|
||||
# 生产 → https://www.quannengcha.com/api/v1
|
||||
#
|
||||
# 需要覆盖时再取消注释其一:
|
||||
# VITE_API_BASE_URL=/api/v1
|
||||
# 方式2:设置 VITE_API_BASE_URL 强制覆盖所有端
|
||||
# 连线上:VITE_API_BASE_URL=https://www.quannengcha.com/api/v1
|
||||
# 连本地:VITE_API_BASE_URL=http://127.0.0.1:8888/api/v1
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
# 想用线上接口时取消下面这行注释:
|
||||
# VITE_API_BASE_URL=https://www.quannengcha.com/api/v1
|
||||
|
||||
# 想用本地接口时注释掉上面那行,取消下面这行注释:
|
||||
VITE_API_BASE_URL=http://127.0.0.1:8888/api/v1
|
||||
# VITE_API_BASE_URL=https://www.tianyuancha.cn/api/v1
|
||||
|
||||
2
.env.development
Normal file
2
.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
# 开发环境:一般留空,由 http.js 按平台默认连 qnc-server-v3(8888)
|
||||
# H5 默认 /api/v1 + Vite 代理;小程序默认 http://127.0.0.1:8888/api/v1
|
||||
2
.env.production
Normal file
2
.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
# 生产构建:指向 qnc-server-v3 线上网关
|
||||
VITE_API_BASE_URL=https://www.quannengcha.com/api/v1
|
||||
12
env.d.ts
vendored
Normal file
12
env.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** 覆盖全部端的 API 根路径,如 /api/v1 或 http://127.0.0.1:8888/api/v1 */
|
||||
readonly VITE_API_BASE_URL?: string
|
||||
/** H5 开发代理目标,仅 vite 使用,默认 http://127.0.0.1:8888 */
|
||||
readonly VITE_API_PROXY_TARGET?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
<script setup>
|
||||
import { onLaunch } from '@dcloudio/uni-app'
|
||||
// #ifdef MP-WEIXIN
|
||||
import { hasToken } from '@/utils/session'
|
||||
import { tryWxMiniProgramAuth } from '@/utils/wxMiniAuth'
|
||||
// #endif
|
||||
|
||||
onLaunch(async () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (hasToken())
|
||||
return
|
||||
try {
|
||||
await tryWxMiniProgramAuth({ silent: true })
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('[app] wx silent login', e)
|
||||
console.warn('[app] wx silent login failed', e)
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import un from '@uni-helper/uni-network'
|
||||
import { QNC_API } from '@/config/api'
|
||||
|
||||
/** 未单独配置登录页时,401 回到「我的」 */
|
||||
const AUTH_FALLBACK_PAGE = '/pages/mine'
|
||||
/** 未单独配置登录页时,401 回到登录页 */
|
||||
const AUTH_FALLBACK_PAGE = '/pages/login'
|
||||
|
||||
const TOKEN_KEY = 'token'
|
||||
const SILENT_TOAST_CODES = new Set([200002, 200003, 200004, 100009])
|
||||
@@ -21,22 +22,28 @@ function hideRequestLoading() {
|
||||
}
|
||||
}
|
||||
|
||||
/** H5 与其它端分支由 uni 条件编译裁剪,源码中并存会触发 no-unreachable */
|
||||
/**
|
||||
* 解析 API 根路径(含 /api/v1)。
|
||||
* 优先级:VITE_API_BASE_URL > 按端/环境的 QNC_API 默认值。
|
||||
*/
|
||||
function resolveBaseUrl() {
|
||||
const fromEnv = import.meta.env.VITE_API_BASE_URL
|
||||
const fromEnv = import.meta.env.VITE_API_BASE_URL?.trim()
|
||||
if (fromEnv)
|
||||
return fromEnv.replace(/\/$/, '')
|
||||
/* eslint-disable no-unreachable */
|
||||
// #ifdef H5
|
||||
return '/api/v1'
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return 'https://www.tianyuancha.cn/api/v1'
|
||||
// #endif
|
||||
/* eslint-enable no-unreachable */
|
||||
if (import.meta.env.DEV) {
|
||||
/* eslint-disable no-unreachable */
|
||||
// #ifdef H5
|
||||
return QNC_API.devH5Base
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return QNC_API.devMpBase
|
||||
// #endif
|
||||
/* eslint-enable no-unreachable */
|
||||
}
|
||||
return QNC_API.prodBase
|
||||
}
|
||||
|
||||
/** 对齐 tyc-webview-v2 useApiFetch:h5 / wxh5,其它端单独标识 */
|
||||
/** 对齐 qnc-server-v3 平台标识:h5 / wxh5 / wxmini 等 */
|
||||
function getXPlatform() {
|
||||
let platform = 'h5'
|
||||
// #ifdef H5
|
||||
@@ -45,7 +52,7 @@ function getXPlatform() {
|
||||
platform = 'wxh5'
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
// 须与 tyc-server-v2 model.PlatformWxMini("wxmini")一致,勿用 mp-weixin,否则 JWT 生成会失败
|
||||
// 须与 qnc-server-v3 model.PlatformWxMini("wxmini")一致,勿用 mp-weixin,否则 JWT 失败
|
||||
platform = 'wxmini'
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
@@ -100,7 +107,7 @@ function toastBizIfNeeded(body) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 tyc-webview-v2 `useApiFetch` 对齐的实例:
|
||||
* 请求 qnc-server-v3 的 HTTP 实例:
|
||||
* - baseUrl `/api/v1`(H5 默认,可配 VITE_API_BASE_URL)
|
||||
* - Header:`Authorization`、`X-Platform`
|
||||
* - Query:`t` 时间戳防缓存
|
||||
|
||||
133
src/composables/useBindMobile.ts
Normal file
133
src/composables/useBindMobile.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { postAuthSendSmsBindMobile, postUserBindMobile } from '@/api'
|
||||
import { saveAuthSession } from '@/utils/session'
|
||||
|
||||
/**
|
||||
* 绑定手机号弹窗逻辑(可在任意页面复用)
|
||||
* 使用方式:
|
||||
* const { bindVisible, bindPhone, bindCode, ... } = useBindMobile(onSuccess)
|
||||
*/
|
||||
export function useBindMobile(onSuccess?: () => void | Promise<void>) {
|
||||
const bindVisible = ref(false)
|
||||
const bindPhone = ref('')
|
||||
const bindCode = ref('')
|
||||
const bindSending = ref(false)
|
||||
const bindSubmitting = ref(false)
|
||||
const bindCountingDown = ref(false)
|
||||
const bindCountdown = ref(60)
|
||||
let bindSmsTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function clearBindSmsTimer() {
|
||||
if (bindSmsTimer) {
|
||||
clearInterval(bindSmsTimer)
|
||||
bindSmsTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function startBindCountdown() {
|
||||
clearBindSmsTimer()
|
||||
bindCountingDown.value = true
|
||||
bindCountdown.value = 60
|
||||
bindSmsTimer = setInterval(() => {
|
||||
if (bindCountdown.value > 0) {
|
||||
bindCountdown.value--
|
||||
}
|
||||
else {
|
||||
clearBindSmsTimer()
|
||||
bindCountingDown.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const isBindPhoneValid = () => /^1[3-9]\d{9}$/.test(bindPhone.value)
|
||||
|
||||
function openBindModal() {
|
||||
bindPhone.value = ''
|
||||
bindCode.value = ''
|
||||
bindVisible.value = true
|
||||
}
|
||||
|
||||
function closeBindModal() {
|
||||
bindVisible.value = false
|
||||
clearBindSmsTimer()
|
||||
bindCountingDown.value = false
|
||||
}
|
||||
|
||||
function onBindPhoneInput(e: { detail?: { value?: string } }) {
|
||||
const raw = e.detail?.value ?? ''
|
||||
bindPhone.value = String(raw).replace(/\D/g, '').slice(0, 11)
|
||||
}
|
||||
|
||||
function onBindCodeInput(e: { detail?: { value?: string } }) {
|
||||
const raw = e.detail?.value ?? ''
|
||||
bindCode.value = String(raw).replace(/\D/g, '').slice(0, 6)
|
||||
}
|
||||
|
||||
async function sendBindSms() {
|
||||
if (bindSending.value || bindCountingDown.value)
|
||||
return
|
||||
if (!isBindPhoneValid()) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindSending.value = true
|
||||
try {
|
||||
const res = await postAuthSendSmsBindMobile({ mobile: bindPhone.value }) as { code?: number }
|
||||
if (res && res.code === 200) {
|
||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||
startBindCountdown()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
bindSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBindMobile() {
|
||||
if (!isBindPhoneValid()) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (bindCode.value.length < 6) {
|
||||
uni.showToast({ title: '请输入 6 位验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindSubmitting.value = true
|
||||
try {
|
||||
const res = await postUserBindMobile({
|
||||
mobile: bindPhone.value,
|
||||
code: bindCode.value,
|
||||
}) as { code?: number, data?: { accessToken: string, refreshAfter: number | string, accessExpire: number | string } }
|
||||
if (res && res.code === 200 && res.data) {
|
||||
saveAuthSession(res.data)
|
||||
uni.showToast({ title: '绑定成功', icon: 'success' })
|
||||
closeBindModal()
|
||||
await onSuccess?.()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
bindSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearBindSmsTimer()
|
||||
})
|
||||
|
||||
return {
|
||||
bindVisible,
|
||||
bindPhone,
|
||||
bindCode,
|
||||
bindSending,
|
||||
bindSubmitting,
|
||||
bindCountingDown,
|
||||
bindCountdown,
|
||||
isBindPhoneValid,
|
||||
openBindModal,
|
||||
closeBindModal,
|
||||
onBindPhoneInput,
|
||||
onBindCodeInput,
|
||||
sendBindSms,
|
||||
submitBindMobile,
|
||||
}
|
||||
}
|
||||
16
src/config/api.js
Normal file
16
src/config/api.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 与 qnc-server-v3 对齐的 API 地址(main.yaml Host:8888,生产域名见 Promotion.OfficialDomain)
|
||||
* 开发时一般无需改此处;可用根目录 .env 的 VITE_API_BASE_URL 覆盖全部端。
|
||||
*/
|
||||
export const QNC_API = {
|
||||
/** 本地 qnc-server-v3 监听地址 */
|
||||
devServerOrigin: 'http://127.0.0.1:8888',
|
||||
/** H5 开发:走 vite.config.js 代理,避免跨域 */
|
||||
devH5Base: '/api/v1',
|
||||
/** 小程序 / App 开发:直连本机后端(微信开发者工具需关闭域名校验或配置合法域名) */
|
||||
get devMpBase() {
|
||||
return `${this.devServerOrigin}/api/v1`
|
||||
},
|
||||
/** 生产环境(与 qnc-server-v3 etc/main.yaml 一致) */
|
||||
prodBase: 'https://www.quannengcha.com/api/v1',
|
||||
}
|
||||
@@ -184,4 +184,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { getQueryExample } from '@/api'
|
||||
import VehicleReportShell from '@/components/report/VehicleReportShell.vue'
|
||||
import { parseEncryptedQueryReport } from '@/utils/queryReportParse'
|
||||
import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize'
|
||||
|
||||
definePage({
|
||||
@@ -36,20 +37,19 @@ async function load() {
|
||||
errText.value = ''
|
||||
try {
|
||||
const res = await getQueryExample(feature.value)
|
||||
if (res?.code === 200 && res.data) {
|
||||
productName.value = res.data.product_name || feature.value
|
||||
queryParams.value = res.data.query_params || {}
|
||||
rows.value = normalizeVehicleQueryData(res.data.query_data || [])
|
||||
const parsed = parseEncryptedQueryReport(res)
|
||||
productName.value = parsed.productName || feature.value
|
||||
if (parsed.ok) {
|
||||
queryParams.value = parsed.queryParams
|
||||
rows.value = parsed.rows
|
||||
if (!rows.value.length)
|
||||
errText.value = '该产品暂无示例模块数据'
|
||||
}
|
||||
else if (res?.code === 200003) {
|
||||
productName.value = feature.value
|
||||
else if (parsed.empty) {
|
||||
errText.value = '暂无示例报告'
|
||||
}
|
||||
else {
|
||||
productName.value = feature.value
|
||||
errText.value = res?.msg || '加载失败'
|
||||
errText.value = parsed.msg || '加载失败'
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { getProductByEn, getUserDetail, postAuthSendSmsQuery, postPayPayment, postQueryService, postUploadImage } from '@/api'
|
||||
import { productHasSmsCode, useInquireForm } from '@/composables/useInquireForm'
|
||||
import { useBindMobile } from '@/composables/useBindMobile'
|
||||
import { aesEncrypt, QUERY_PAYLOAD_AES_HEX_KEY } from '@/utils/crypto.js'
|
||||
|
||||
definePage({
|
||||
@@ -17,6 +18,23 @@ definePage({
|
||||
const feature = ref('')
|
||||
const { formData, isPhoneNumberValid, isIdCardValid, isHasInput, buildRequestPayload } = useInquireForm(feature)
|
||||
|
||||
const {
|
||||
bindVisible: bindModalOpen,
|
||||
bindPhone,
|
||||
bindCode,
|
||||
bindSending,
|
||||
bindSubmitting,
|
||||
bindCountingDown,
|
||||
bindCountdown,
|
||||
isBindPhoneValid,
|
||||
openBindModal,
|
||||
closeBindModal,
|
||||
onBindPhoneInput,
|
||||
onBindCodeInput,
|
||||
sendBindSms,
|
||||
submitBindMobile,
|
||||
} = useBindMobile()
|
||||
|
||||
const loading = ref(true)
|
||||
const productLoadOk = ref(false)
|
||||
const submitting = ref(false)
|
||||
@@ -149,7 +167,15 @@ function readToken() {
|
||||
|
||||
async function ensureLoginAndMobile() {
|
||||
if (!readToken()) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const { tryWxMiniProgramAuth } = await import('@/utils/wxMiniAuth')
|
||||
const ok = await tryWxMiniProgramAuth({ silent: false })
|
||||
if (ok)
|
||||
return true
|
||||
}
|
||||
catch { /* fallback to login page */ }
|
||||
// #endif
|
||||
uni.navigateTo({ url: '/pages/login' })
|
||||
return false
|
||||
}
|
||||
@@ -158,12 +184,12 @@ async function ensureLoginAndMobile() {
|
||||
const mobile = (res?.data?.userInfo?.mobile || '').trim()
|
||||
if (res?.code !== 200 || !/^1[3-9]\d{9}$/.test(mobile)) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先在「我的」中绑定手机号后再发起查询。',
|
||||
title: '绑定手机号',
|
||||
content: '为保证查询报告可同步查看,请先绑定手机号。',
|
||||
confirmText: '去绑定',
|
||||
success(r) {
|
||||
if (r.confirm)
|
||||
uni.switchTab({ url: '/pages/mine' })
|
||||
showCancel: false,
|
||||
success() {
|
||||
openBindModal()
|
||||
},
|
||||
})
|
||||
return false
|
||||
@@ -703,6 +729,59 @@ async function onConfirmPay() {
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="bindModalOpen" class="bind-mask" @tap.self="closeBindModal">
|
||||
<view class="bind-sheet" @tap.stop>
|
||||
<view class="bind-sheet-title">
|
||||
绑定手机号
|
||||
</view>
|
||||
<view class="bind-field">
|
||||
<text class="bind-label">手机号</text>
|
||||
<input
|
||||
class="bind-input"
|
||||
type="digit"
|
||||
:value="bindPhone"
|
||||
:maxlength="11"
|
||||
placeholder="请输入手机号"
|
||||
placeholder-class="bind-ph"
|
||||
confirm-type="done"
|
||||
@input="onBindPhoneInput"
|
||||
>
|
||||
</view>
|
||||
<view class="bind-field bind-field-row">
|
||||
<view class="bind-field-grow">
|
||||
<text class="bind-label">验证码</text>
|
||||
<input
|
||||
class="bind-input"
|
||||
type="digit"
|
||||
:value="bindCode"
|
||||
:maxlength="6"
|
||||
placeholder="6 位短信码"
|
||||
placeholder-class="bind-ph"
|
||||
confirm-type="done"
|
||||
@input="onBindCodeInput"
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="bind-sms"
|
||||
:class="{ disabled: bindSending || bindCountingDown || !isBindPhoneValid() }"
|
||||
@tap="sendBindSms"
|
||||
>
|
||||
{{ bindCountingDown ? `${bindCountdown}s` : '获取验证码' }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="bind-submit"
|
||||
:class="{ disabled: bindSubmitting || !isBindPhoneValid() || bindCode.length < 6 }"
|
||||
@tap="submitBindMobile"
|
||||
>
|
||||
确认绑定
|
||||
</view>
|
||||
<view class="bind-cancel" @tap="closeBindModal">
|
||||
取消
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1134,4 +1213,113 @@ async function onConfirmPay() {
|
||||
.pay-ok.disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* ═══ 绑定手机弹窗 ═══ */
|
||||
.bind-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bind-sheet {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding: 28rpx 28rpx calc(28rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bind-sheet-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bind-field {
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.bind-field-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.bind-field-grow {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bind-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #86909c;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.bind-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
font-size: 30rpx;
|
||||
color: #1d2129;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bind-ph {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
|
||||
.bind-sms {
|
||||
flex-shrink: 0;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #1768ff;
|
||||
background: #f0f5ff;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bind-sms.disabled {
|
||||
color: #c9cdd4;
|
||||
background: #f7f8fa;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bind-submit {
|
||||
margin-top: 12rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(90deg, #1768ff 0%, #4d94ff 100%);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
|
||||
.bind-submit.disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bind-cancel {
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #86909c;
|
||||
padding: 12rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { postAuthSendSms, postUserMobileCodeLogin } from '@/api'
|
||||
import { saveAuthSession } from '@/utils/session'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { tryWxMiniProgramAuth } from '@/utils/wxMiniAuth'
|
||||
|
||||
definePage({
|
||||
@@ -13,105 +11,10 @@ definePage({
|
||||
},
|
||||
})
|
||||
|
||||
const phoneMode = ref(false)
|
||||
const mobile = ref('')
|
||||
const code = ref('')
|
||||
const agreed = ref(false)
|
||||
const sending = ref(false)
|
||||
const submitting = ref(false)
|
||||
const wxLoading = ref(false)
|
||||
const isCountingDown = ref(false)
|
||||
const countdown = ref(60)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const isPhoneValid = computed(() => /^1[3-9]\d{9}$/.test(mobile.value))
|
||||
const canSendSms = computed(() => isPhoneValid.value && !isCountingDown.value && !sending.value)
|
||||
const canSubmit = computed(
|
||||
() => isPhoneValid.value && code.value.length >= 6 && agreed.value && !submitting.value,
|
||||
)
|
||||
const canWxMiniLogin = computed(() => agreed.value && !wxLoading.value)
|
||||
|
||||
function clearTimer() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
clearTimer()
|
||||
isCountingDown.value = true
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--
|
||||
}
|
||||
else {
|
||||
clearTimer()
|
||||
isCountingDown.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function openPhoneForm() {
|
||||
phoneMode.value = true
|
||||
}
|
||||
|
||||
function closePhoneForm() {
|
||||
phoneMode.value = false
|
||||
}
|
||||
|
||||
async function handleSendSms() {
|
||||
if (!canSendSms.value)
|
||||
return
|
||||
if (!isPhoneValid.value) {
|
||||
uni.showToast({ title: '手机号有误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
sending.value = true
|
||||
try {
|
||||
const res = await postAuthSendSms({ mobile: mobile.value }) as { code?: number }
|
||||
if (res && res.code === 200) {
|
||||
uni.showToast({ title: '已发送', icon: 'none' })
|
||||
startCountdown()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!isPhoneValid.value) {
|
||||
uni.showToast({ title: '手机号有误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (code.value.length < 6) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先勾选协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await postUserMobileCodeLogin({
|
||||
mobile: mobile.value,
|
||||
code: code.value,
|
||||
}) as { code?: number, data?: { accessToken: string, refreshAfter: number | string, accessExpire: number | string } }
|
||||
if (res && res.code === 200 && res.data) {
|
||||
saveAuthSession(res.data)
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
const canWxMiniLogin = ref(true)
|
||||
|
||||
function toggleAgree() {
|
||||
agreed.value = !agreed.value
|
||||
@@ -129,22 +32,13 @@ function goLegalAuthorization() {
|
||||
uni.navigateTo({ url: '/pages/legal/authorization' })
|
||||
}
|
||||
|
||||
function onMobileInput(e: { detail?: { value?: string } }) {
|
||||
const raw = e.detail?.value ?? ''
|
||||
mobile.value = String(raw).replace(/\D/g, '').slice(0, 11)
|
||||
}
|
||||
|
||||
function onCodeInput(e: { detail?: { value?: string } }) {
|
||||
const raw = e.detail?.value ?? ''
|
||||
code.value = String(raw).replace(/\D/g, '').slice(0, 6)
|
||||
}
|
||||
|
||||
async function handleWxMiniLogin() {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先勾选协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wxLoading.value = true
|
||||
canWxMiniLogin.value = false
|
||||
try {
|
||||
const ok = await tryWxMiniProgramAuth({ silent: false })
|
||||
if (!ok)
|
||||
@@ -159,20 +53,16 @@ async function handleWxMiniLogin() {
|
||||
}
|
||||
finally {
|
||||
wxLoading.value = false
|
||||
canWxMiniLogin.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-root">
|
||||
<view class="bg-blob" />
|
||||
<view class="page">
|
||||
<!-- 入口:仅按钮 + 一行协议 -->
|
||||
<view v-show="!phoneMode" class="gate">
|
||||
<view class="gate">
|
||||
<view class="brand-mark" />
|
||||
<view class="brand-title">
|
||||
全能查
|
||||
@@ -185,15 +75,17 @@ onUnmounted(() => {
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view
|
||||
class="btn btn-wx"
|
||||
:class="{ disabled: !canWxMiniLogin }"
|
||||
:class="{ disabled: !canWxMiniLogin || wxLoading }"
|
||||
@tap="handleWxMiniLogin"
|
||||
>
|
||||
{{ wxLoading ? '…' : '微信登录' }}
|
||||
{{ wxLoading ? '登录中...' : '微信一键登录' }}
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="btn btn-phone" @tap="openPhoneForm">
|
||||
手机号登录
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="btn btn-wx" @tap="handleWxMiniLogin">
|
||||
一键登录
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
|
||||
<view class="agree-row" @tap.stop>
|
||||
@@ -208,75 +100,6 @@ onUnmounted(() => {
|
||||
<text class="link" @tap="goLegalAuthorization">《授权书》</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号表单 -->
|
||||
<view v-show="phoneMode" class="form-sheet">
|
||||
<view class="form-head">
|
||||
<view class="back" @tap="closePhoneForm">
|
||||
‹
|
||||
</view>
|
||||
<text class="form-title">手机号登录</text>
|
||||
<view class="back-spacer" />
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="inp-wrap">
|
||||
<text class="inp-label">手机号</text>
|
||||
<input
|
||||
class="inp"
|
||||
type="digit"
|
||||
:value="mobile"
|
||||
:maxlength="11"
|
||||
placeholder="11 位手机号"
|
||||
placeholder-class="inp-ph"
|
||||
confirm-type="done"
|
||||
@input="onMobileInput"
|
||||
>
|
||||
</view>
|
||||
<view class="inp-wrap inp-row">
|
||||
<view class="inp-flex">
|
||||
<text class="inp-label">验证码</text>
|
||||
<input
|
||||
class="inp"
|
||||
type="digit"
|
||||
:value="code"
|
||||
:maxlength="6"
|
||||
placeholder="6 位验证码"
|
||||
placeholder-class="inp-ph"
|
||||
confirm-type="done"
|
||||
@input="onCodeInput"
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="sms"
|
||||
:class="{ disabled: !canSendSms }"
|
||||
@tap="handleSendSms"
|
||||
>
|
||||
{{ isCountingDown ? `${countdown}s` : '获取' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="agree-row agree-form" @tap.stop>
|
||||
<view class="agree-tap" @tap="toggleAgree">
|
||||
<view class="check" :class="{ on: agreed }" />
|
||||
<text class="agree-txt">同意</text>
|
||||
</view>
|
||||
<text class="link" @tap="goLegalUserAgreement">《用户协议》</text>
|
||||
<text class="agree-gap">与</text>
|
||||
<text class="link" @tap="goLegalPrivacyPolicy">《隐私政策》</text>
|
||||
<text class="agree-gap">及</text>
|
||||
<text class="link" @tap="goLegalAuthorization">《授权书》</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="btn btn-submit"
|
||||
:class="{ disabled: !canSubmit }"
|
||||
@tap="handleSubmit"
|
||||
>
|
||||
登录
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -333,7 +156,7 @@ onUnmounted(() => {
|
||||
font-size: 26rpx;
|
||||
color: #86909c;
|
||||
margin-top: 12rpx;
|
||||
margin-bottom: 72rpx;
|
||||
margin-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.btn-stack {
|
||||
@@ -363,13 +186,6 @@ onUnmounted(() => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-phone {
|
||||
background: #fff;
|
||||
color: #1768ff;
|
||||
border: 2rpx solid rgba(23, 104, 255, 0.35);
|
||||
box-shadow: 0 8rpx 24rpx rgba(15, 35, 52, 0.06);
|
||||
}
|
||||
|
||||
.agree-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -412,117 +228,4 @@ onUnmounted(() => {
|
||||
margin: 0 4rpx;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.agree-form {
|
||||
margin-top: 0;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-sheet {
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.form-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
font-size: 48rpx;
|
||||
color: #1d2129;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.back-spacer {
|
||||
width: 72rpx;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #fff;
|
||||
border-radius: 28rpx;
|
||||
padding: 8rpx 28rpx 8rpx;
|
||||
border: 1rpx solid rgba(23, 104, 255, 0.08);
|
||||
box-shadow: 0 20rpx 48rpx rgba(15, 35, 52, 0.06);
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.inp-wrap {
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid #f0f1f5;
|
||||
}
|
||||
|
||||
.inp-wrap:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.inp-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.inp-flex {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.inp-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #86909c;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.inp {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
font-size: 32rpx;
|
||||
color: #1d2129;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inp-ph {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
|
||||
.sms {
|
||||
flex-shrink: 0;
|
||||
height: 68rpx;
|
||||
line-height: 68rpx;
|
||||
padding: 0 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #1768ff;
|
||||
background: #f0f5ff;
|
||||
border-radius: 34rpx;
|
||||
}
|
||||
|
||||
.sms.disabled {
|
||||
color: #c9cdd4;
|
||||
background: #f7f8fa;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: linear-gradient(90deg, #1768ff 0%, #4d94ff 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 16rpx 40rpx rgba(23, 104, 255, 0.28);
|
||||
}
|
||||
|
||||
.btn-submit.disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { onShareAppMessage, onShareTimeline, onShow } from '@dcloudio/uni-app'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { clearAuthStorage, getUserDetail, postAuthSendSmsBindMobile, postUserBindMobile } from '@/api'
|
||||
import { hasToken, saveAuthSession } from '@/utils/session'
|
||||
import { ref } from 'vue'
|
||||
import { getUserDetail } from '@/api'
|
||||
import { hasToken } from '@/utils/session'
|
||||
import { useBindMobile } from '@/composables/useBindMobile'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
@@ -30,14 +31,22 @@ const userDesc = ref('')
|
||||
const hasBoundMobile = ref(false)
|
||||
const wxNickStorage = ref('')
|
||||
|
||||
const bindModalOpen = ref(false)
|
||||
const bindPhone = ref('')
|
||||
const bindCode = ref('')
|
||||
const bindSending = ref(false)
|
||||
const bindSubmitting = ref(false)
|
||||
const bindCountingDown = ref(false)
|
||||
const bindCountdown = ref(60)
|
||||
let bindSmsTimer: ReturnType<typeof setInterval> | null = null
|
||||
const {
|
||||
bindVisible: bindModalOpen,
|
||||
bindPhone,
|
||||
bindCode,
|
||||
bindSending,
|
||||
bindSubmitting,
|
||||
bindCountingDown,
|
||||
bindCountdown,
|
||||
isBindPhoneValid,
|
||||
openBindModal,
|
||||
closeBindModal,
|
||||
onBindPhoneInput,
|
||||
onBindCodeInput,
|
||||
sendBindSms,
|
||||
submitBindMobile,
|
||||
} = useBindMobile(refreshUserCard)
|
||||
|
||||
const coopModalOpen = ref(false)
|
||||
|
||||
@@ -68,30 +77,10 @@ function loadWxNickFromStorage() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearBindSmsTimer() {
|
||||
if (bindSmsTimer) {
|
||||
clearInterval(bindSmsTimer)
|
||||
bindSmsTimer = null
|
||||
}
|
||||
function openBindModalFromProfile() {
|
||||
openBindModal()
|
||||
}
|
||||
|
||||
function startBindCountdown() {
|
||||
clearBindSmsTimer()
|
||||
bindCountingDown.value = true
|
||||
bindCountdown.value = 60
|
||||
bindSmsTimer = setInterval(() => {
|
||||
if (bindCountdown.value > 0) {
|
||||
bindCountdown.value--
|
||||
}
|
||||
else {
|
||||
clearBindSmsTimer()
|
||||
bindCountingDown.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const isBindPhoneValid = () => /^1[3-9]\d{9}$/.test(bindPhone.value)
|
||||
|
||||
async function refreshUserCard() {
|
||||
isLogin.value = hasToken()
|
||||
if (!isLogin.value) {
|
||||
@@ -148,19 +137,6 @@ onShow(() => {
|
||||
|
||||
function handleUserTap() {
|
||||
if (isLogin.value) {
|
||||
uni.showActionSheet({
|
||||
itemList: ['退出登录'],
|
||||
success(res) {
|
||||
if (res.tapIndex === 0) {
|
||||
clearAuthStorage()
|
||||
isLogin.value = false
|
||||
nickname.value = ''
|
||||
userDesc.value = ''
|
||||
hasBoundMobile.value = false
|
||||
uni.showToast({ title: '已退出', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: '/pages/login' })
|
||||
@@ -193,77 +169,8 @@ async function syncWxNickname() {
|
||||
// #endif
|
||||
}
|
||||
|
||||
function openBindModal() {
|
||||
bindPhone.value = ''
|
||||
bindCode.value = ''
|
||||
bindModalOpen.value = true
|
||||
}
|
||||
|
||||
function closeBindModal() {
|
||||
bindModalOpen.value = false
|
||||
clearBindSmsTimer()
|
||||
bindCountingDown.value = false
|
||||
}
|
||||
|
||||
function onBindPhoneInput(e: { detail?: { value?: string } }) {
|
||||
const raw = e.detail?.value ?? ''
|
||||
bindPhone.value = String(raw).replace(/\D/g, '').slice(0, 11)
|
||||
}
|
||||
|
||||
function onBindCodeInput(e: { detail?: { value?: string } }) {
|
||||
const raw = e.detail?.value ?? ''
|
||||
bindCode.value = String(raw).replace(/\D/g, '').slice(0, 6)
|
||||
}
|
||||
|
||||
async function sendBindSms() {
|
||||
if (bindSending.value || bindCountingDown.value)
|
||||
return
|
||||
if (!isBindPhoneValid()) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindSending.value = true
|
||||
try {
|
||||
const res = await postAuthSendSmsBindMobile({ mobile: bindPhone.value }) as { code?: number }
|
||||
if (res && res.code === 200) {
|
||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||
startBindCountdown()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
bindSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBindMobile() {
|
||||
if (!isBindPhoneValid()) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (bindCode.value.length < 6) {
|
||||
uni.showToast({ title: '请输入 6 位验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindSubmitting.value = true
|
||||
try {
|
||||
const res = await postUserBindMobile({
|
||||
mobile: bindPhone.value,
|
||||
code: bindCode.value,
|
||||
}) as { code?: number, data?: { accessToken: string, refreshAfter: number | string, accessExpire: number | string } }
|
||||
if (res && res.code === 200 && res.data) {
|
||||
saveAuthSession(res.data)
|
||||
uni.showToast({ title: '绑定成功', icon: 'success' })
|
||||
closeBindModal()
|
||||
await refreshUserCard()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
bindSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goHistoryReport() {
|
||||
uni.switchTab({ url: '/pages/report' })
|
||||
uni.navigateTo({ url: '/pages/report' })
|
||||
}
|
||||
|
||||
function goFreeValuation() {
|
||||
@@ -296,11 +203,11 @@ function goLegalAuthorization() {
|
||||
}
|
||||
|
||||
function goIllegalCode() {
|
||||
uni.showToast({ title: '敬请期待', icon: 'none' })
|
||||
uni.navigateTo({ url: '/pages/toolbox/query?key=jtwfcode' })
|
||||
}
|
||||
|
||||
function goOilPrice() {
|
||||
uni.showToast({ title: '敬请期待', icon: 'none' })
|
||||
uni.navigateTo({ url: '/pages/toolbox/query?key=oilprice' })
|
||||
}
|
||||
|
||||
function goHelp() {
|
||||
@@ -319,10 +226,6 @@ function goSettings() {
|
||||
function goServiceFallback() {
|
||||
uni.showToast({ title: '请在微信小程序内使用在线客服', icon: 'none' })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearBindSmsTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -349,7 +252,7 @@ onUnmounted(() => {
|
||||
</view>
|
||||
<!-- 已登录未绑定手机 -->
|
||||
<view v-if="isLogin && !hasBoundMobile" class="profile-actions">
|
||||
<view class="action-btn" @tap.stop="openBindModal">
|
||||
<view class="action-btn" @tap.stop="openBindModalFromProfile">
|
||||
<view class="action-icon i-carbon-phone" />
|
||||
<text class="action-text">绑定手机号</text>
|
||||
</view>
|
||||
@@ -406,20 +309,18 @@ onUnmounted(() => {
|
||||
<!-- ═══ 区块3: 常用工具 ═══ -->
|
||||
<view class="section">
|
||||
<view class="section-title">常用工具</view>
|
||||
<view class="tool-list">
|
||||
<view class="tool-row" @tap="goOilPrice">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(23,104,255,0.08)">
|
||||
<view class="tool-row-icon i-carbon-gas-station" style="color: #1768ff" />
|
||||
<view class="quick-grid">
|
||||
<view class="quick-item" @tap="goOilPrice">
|
||||
<view class="quick-icon-wrap" style="background: rgba(23,104,255,0.08)">
|
||||
<view class="quick-icon i-carbon-gas-station" style="color: #1768ff" />
|
||||
</view>
|
||||
<text class="tool-row-name">实时油价查询</text>
|
||||
<text class="tool-row-arrow">›</text>
|
||||
<text class="quick-name">实时油价查询</text>
|
||||
</view>
|
||||
<view class="tool-row" @tap="goIllegalCode">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(250,140,22,0.08)">
|
||||
<view class="tool-row-icon i-carbon-search" style="color: #fa8c16" />
|
||||
<view class="quick-item" @tap="goIllegalCode">
|
||||
<view class="quick-icon-wrap" style="background: rgba(250,140,22,0.08)">
|
||||
<view class="quick-icon i-carbon-search" style="color: #fa8c16" />
|
||||
</view>
|
||||
<text class="tool-row-name">违章代码查询</text>
|
||||
<text class="tool-row-arrow">›</text>
|
||||
<text class="quick-name">违章代码查询</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -427,57 +328,52 @@ onUnmounted(() => {
|
||||
<!-- ═══ 区块4: 服务与支持 ═══ -->
|
||||
<view class="section">
|
||||
<view class="section-title">服务与支持</view>
|
||||
<view class="tool-list">
|
||||
<button class="tool-row tool-row-btn no-border" open-type="contact" hover-class="tool-row-hover">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(19,194,94,0.08)">
|
||||
<view class="tool-row-icon i-carbon-chat" style="color: #13c25e" />
|
||||
</view>
|
||||
<view class="tool-row-text">
|
||||
<text class="tool-row-name">在线客服</text>
|
||||
<text class="tool-row-sub">周一至周日 9:00-20:00</text>
|
||||
<view class="quick-grid">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="quick-item quick-item-btn" open-type="contact" hover-class="quick-item-hover">
|
||||
<view class="quick-icon-wrap" style="background: rgba(19,194,94,0.08)">
|
||||
<view class="quick-icon i-carbon-chat" style="color: #13c25e" />
|
||||
</view>
|
||||
<text class="quick-name">在线客服</text>
|
||||
</button>
|
||||
<view class="tool-row" @tap="goHelp">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(114,46,209,0.08)">
|
||||
<view class="tool-row-icon i-carbon-help" style="color: #722ed1" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="quick-item" @tap="goServiceFallback">
|
||||
<view class="quick-icon-wrap" style="background: rgba(19,194,94,0.08)">
|
||||
<view class="quick-icon i-carbon-chat" style="color: #13c25e" />
|
||||
</view>
|
||||
<text class="tool-row-name">帮助中心</text>
|
||||
<text class="tool-row-arrow">›</text>
|
||||
<text class="quick-name">在线客服</text>
|
||||
</view>
|
||||
<view class="tool-row" @tap="goAbout">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(78,89,105,0.08)">
|
||||
<view class="tool-row-icon i-carbon-information" style="color: #4e5969" />
|
||||
<!-- #endif -->
|
||||
<view class="quick-item" @tap="goHelp">
|
||||
<view class="quick-icon-wrap" style="background: rgba(114,46,209,0.08)">
|
||||
<view class="quick-icon i-carbon-help" style="color: #722ed1" />
|
||||
</view>
|
||||
<text class="tool-row-name">关于我们</text>
|
||||
<text class="tool-row-arrow">›</text>
|
||||
<text class="quick-name">帮助中心</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ═══ 区块5: 法律条款 ═══ -->
|
||||
<view class="section">
|
||||
<view class="section-title">法律条款</view>
|
||||
<view class="tool-list">
|
||||
<view class="tool-row" @tap="goLegalUserAgreement">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(78,89,105,0.06)">
|
||||
<view class="tool-row-icon i-carbon-document-blank" style="color: #86909c" />
|
||||
<view class="quick-item" @tap="goAbout">
|
||||
<view class="quick-icon-wrap" style="background: rgba(78,89,105,0.08)">
|
||||
<view class="quick-icon i-carbon-information" style="color: #4e5969" />
|
||||
</view>
|
||||
<text class="tool-row-name">用户协议</text>
|
||||
<text class="tool-row-arrow">›</text>
|
||||
<text class="quick-name">关于我们</text>
|
||||
</view>
|
||||
<view class="tool-row" @tap="goLegalPrivacyPolicy">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(78,89,105,0.06)">
|
||||
<view class="tool-row-icon i-carbon-security" style="color: #86909c" />
|
||||
<view class="quick-item" @tap="goLegalUserAgreement">
|
||||
<view class="quick-icon-wrap" style="background: rgba(78,89,105,0.06)">
|
||||
<view class="quick-icon i-carbon-document-blank" style="color: #86909c" />
|
||||
</view>
|
||||
<text class="tool-row-name">隐私政策</text>
|
||||
<text class="tool-row-arrow">›</text>
|
||||
<text class="quick-name">用户协议</text>
|
||||
</view>
|
||||
<view class="tool-row no-border" @tap="goLegalAuthorization">
|
||||
<view class="tool-row-icon-wrap" style="background: rgba(78,89,105,0.06)">
|
||||
<view class="tool-row-icon i-carbon-certificate" style="color: #86909c" />
|
||||
<view class="quick-item" @tap="goLegalPrivacyPolicy">
|
||||
<view class="quick-icon-wrap" style="background: rgba(78,89,105,0.06)">
|
||||
<view class="quick-icon i-carbon-security" style="color: #86909c" />
|
||||
</view>
|
||||
<text class="tool-row-name">授权书</text>
|
||||
<text class="tool-row-arrow">›</text>
|
||||
<text class="quick-name">隐私政策</text>
|
||||
</view>
|
||||
<view class="quick-item" @tap="goLegalAuthorization">
|
||||
<view class="quick-icon-wrap" style="background: rgba(78,89,105,0.06)">
|
||||
<view class="quick-icon i-carbon-certificate" style="color: #86909c" />
|
||||
</view>
|
||||
<text class="quick-name">授权书</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -735,7 +631,7 @@ onUnmounted(() => {
|
||||
.quick-icon-wrap {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 24rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { getQueryList } from '@/api'
|
||||
import { hasToken } from '@/utils/session'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
@@ -124,6 +125,10 @@ async function loadList() {
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
if (!hasToken()) {
|
||||
uni.redirectTo({ url: '/pages/login' })
|
||||
return
|
||||
}
|
||||
void loadList()
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { getQueryDetailByOrderId, getQueryDetailByOrderNo } from '@/api'
|
||||
import VehicleReportShell from '@/components/report/VehicleReportShell.vue'
|
||||
import { parseEncryptedQueryReport } from '@/utils/queryReportParse'
|
||||
import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize'
|
||||
|
||||
definePage({
|
||||
@@ -40,15 +41,16 @@ async function load() {
|
||||
const res = orderId.value
|
||||
? await getQueryDetailByOrderId(orderId.value)
|
||||
: await getQueryDetailByOrderNo(orderNo.value)
|
||||
if (res?.code === 200 && res.data) {
|
||||
productName.value = res.data.product_name || '查询报告'
|
||||
queryParams.value = res.data.query_params || {}
|
||||
rows.value = normalizeVehicleQueryData(res.data.query_data || [])
|
||||
const parsed = parseEncryptedQueryReport(res)
|
||||
productName.value = parsed.productName || '查询报告'
|
||||
if (parsed.ok) {
|
||||
queryParams.value = parsed.queryParams
|
||||
rows.value = parsed.rows
|
||||
if (!rows.value.length)
|
||||
errText.value = '暂无报告模块数据'
|
||||
}
|
||||
else {
|
||||
errText.value = res?.msg || '加载失败'
|
||||
errText.value = parsed.msg || '加载失败'
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -20,5 +20,23 @@ function generateRandomIV() {
|
||||
return CryptoJS.enc.Hex.parse(iv.map(b => b.toString(16).padStart(2, '0')).join(''))
|
||||
}
|
||||
|
||||
/** 与 H5 `InquireForm.vue` 中 `aesEncrypt(..., key)` 使用的密钥一致 */
|
||||
/** 与后端 `Encrypt.SecretKey`、H5 `VITE_INQUIRE_AES_KEY` 一致 */
|
||||
export const QUERY_PAYLOAD_AES_HEX_KEY = 'ff83609b2b24fc73196aac3d3dfb874f'
|
||||
|
||||
/** AES-CBC 解密:密文为 IV(16 字节) + 密文,整体 Base64(与 H5 `utils/crypto.js` 一致) */
|
||||
export function aesDecrypt(base64CipherText, hexKey) {
|
||||
const key = CryptoJS.enc.Hex.parse(hexKey)
|
||||
const cipherParams = CryptoJS.enc.Base64.parse(base64CipherText)
|
||||
const iv = cipherParams.clone().words.slice(0, 4)
|
||||
const cipherText = cipherParams.clone().words.slice(4)
|
||||
const decrypted = CryptoJS.AES.decrypt(
|
||||
{ ciphertext: CryptoJS.lib.WordArray.create(cipherText) },
|
||||
key,
|
||||
{
|
||||
iv: CryptoJS.lib.WordArray.create(iv),
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
},
|
||||
)
|
||||
return decrypted.toString(CryptoJS.enc.Utf8)
|
||||
}
|
||||
|
||||
40
src/utils/queryReportParse.js
Normal file
40
src/utils/queryReportParse.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { aesDecrypt, QUERY_PAYLOAD_AES_HEX_KEY } from './crypto'
|
||||
import { normalizeVehicleQueryData } from './vehicleReportNormalize'
|
||||
|
||||
/**
|
||||
* 解析 /query/example、/query/orderNo 等接口返回体。
|
||||
* 成功时 data 为 AES 加密字符串,需解密后取 query_data。
|
||||
*/
|
||||
export function parseEncryptedQueryReport(apiBody) {
|
||||
if (!apiBody)
|
||||
return { ok: false, msg: '无响应数据' }
|
||||
|
||||
if (apiBody.code === 200003)
|
||||
return { ok: false, empty: true, msg: apiBody.msg || '暂无示例报告' }
|
||||
|
||||
if (apiBody.code !== 200 || apiBody.data == null || apiBody.data === '')
|
||||
return { ok: false, msg: apiBody.msg || '加载失败' }
|
||||
|
||||
let payload = apiBody.data
|
||||
if (typeof payload === 'string') {
|
||||
try {
|
||||
const plain = aesDecrypt(payload, QUERY_PAYLOAD_AES_HEX_KEY)
|
||||
payload = JSON.parse(plain)
|
||||
}
|
||||
catch {
|
||||
return { ok: false, msg: '报告数据解密失败' }
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload || typeof payload !== 'object')
|
||||
return { ok: false, msg: '报告数据格式错误' }
|
||||
|
||||
const rows = normalizeVehicleQueryData(payload.query_data || [])
|
||||
return {
|
||||
ok: true,
|
||||
productName: payload.product_name || '',
|
||||
queryParams: payload.query_params || {},
|
||||
rows,
|
||||
createTime: payload.create_time || null,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { loadEnv } from 'vite'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import Components from '@uni-helper/vite-plugin-uni-components'
|
||||
@@ -14,19 +15,21 @@ import { UniEcharts } from 'uni-echarts/vite'
|
||||
import Uni from '@uni-helper/plugin-uni'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
strictPort: false,
|
||||
// 与 tyc-webview-v2 一致:H5 开发时将 /api/v1 代理到线上网关(本地可改 target)
|
||||
proxy: {
|
||||
'/api/v1': {
|
||||
// target: 'https://www.tianyuancha.cn',
|
||||
target: 'http://127.0.0.1:8888',
|
||||
changeOrigin: true,
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const proxyTarget = env.VITE_API_PROXY_TARGET || env.VITE_API_BASE_URL?.replace(/\/api\/v1$/, '') || 'https://www.quannengcha.com'
|
||||
return {
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 5678,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
"/api/v1": {
|
||||
target: proxyTarget,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -66,7 +69,8 @@ export default defineConfig({
|
||||
"vue-demi",
|
||||
"uni-echarts"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user