This commit is contained in:
Mrx
2026-05-21 17:05:09 +08:00
parent 6137c69034
commit d18feb3090
18 changed files with 577 additions and 544 deletions

22
.env
View File

@@ -1,9 +1,17 @@
# 客户端可读变量必须以 VITE_ 开头 # API 基础地址控制
# 不配置 VITE_API_BASE_URL 时,逻辑在 src/api/http.js 的 resolveBaseUrl() # ──────────────────────────────────────────────
# - H5默认 /api/v1配合 vite.config.js 里 proxy # 方式1不设置 VITE_API_BASE_URL留空由代码自动选择
# - 非 H5默认 https://www.tianyuancha.cn/api/v1 # H5 dev → /api/v1走 Vite proxy
# 小程序 dev → http://127.0.0.1:8888/api/v1直连本地
# 生产 → https://www.quannengcha.com/api/v1
# #
# 需要覆盖时再取消注释其一: # 方式2设置 VITE_API_BASE_URL 强制覆盖所有端
# VITE_API_BASE_URL=/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.quannengcha.com/api/v1
# 想用本地接口时注释掉上面那行,取消下面这行注释:
VITE_API_BASE_URL=http://127.0.0.1:8888/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
View File

@@ -0,0 +1,2 @@
# 开发环境:一般留空,由 http.js 按平台默认连 qnc-server-v38888
# H5 默认 /api/v1 + Vite 代理;小程序默认 http://127.0.0.1:8888/api/v1

2
.env.production Normal file
View 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
View 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
}

View File

@@ -1,19 +1,16 @@
<script setup> <script setup>
import { onLaunch } from '@dcloudio/uni-app' import { onLaunch } from '@dcloudio/uni-app'
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
import { hasToken } from '@/utils/session'
import { tryWxMiniProgramAuth } from '@/utils/wxMiniAuth' import { tryWxMiniProgramAuth } from '@/utils/wxMiniAuth'
// #endif // #endif
onLaunch(async () => { onLaunch(async () => {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
if (hasToken())
return
try { try {
await tryWxMiniProgramAuth({ silent: true }) await tryWxMiniProgramAuth({ silent: true })
} }
catch (e) { catch (e) {
console.warn('[app] wx silent login', e) console.warn('[app] wx silent login failed', e)
} }
// #endif // #endif
}) })

View File

@@ -1,7 +1,8 @@
import un from '@uni-helper/uni-network' import un from '@uni-helper/uni-network'
import { QNC_API } from '@/config/api'
/** 未单独配置登录页时401 回到「我的」 */ /** 未单独配置登录页时401 回到登录页 */
const AUTH_FALLBACK_PAGE = '/pages/mine' const AUTH_FALLBACK_PAGE = '/pages/login'
const TOKEN_KEY = 'token' const TOKEN_KEY = 'token'
const SILENT_TOAST_CODES = new Set([200002, 200003, 200004, 100009]) 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() { function resolveBaseUrl() {
const fromEnv = import.meta.env.VITE_API_BASE_URL const fromEnv = import.meta.env.VITE_API_BASE_URL?.trim()
if (fromEnv) if (fromEnv)
return fromEnv.replace(/\/$/, '') return fromEnv.replace(/\/$/, '')
if (import.meta.env.DEV) {
/* eslint-disable no-unreachable */ /* eslint-disable no-unreachable */
// #ifdef H5 // #ifdef H5
return '/api/v1' return QNC_API.devH5Base
// #endif // #endif
// #ifndef H5 // #ifndef H5
return 'https://www.tianyuancha.cn/api/v1' return QNC_API.devMpBase
// #endif // #endif
/* eslint-enable no-unreachable */ /* eslint-enable no-unreachable */
} }
return QNC_API.prodBase
}
/** 对齐 tyc-webview-v2 useApiFetchh5 / wxh5其它端单独标识 */ /** 对齐 qnc-server-v3 平台标识h5 / wxh5 / wxmini 等 */
function getXPlatform() { function getXPlatform() {
let platform = 'h5' let platform = 'h5'
// #ifdef H5 // #ifdef H5
@@ -45,7 +52,7 @@ function getXPlatform() {
platform = 'wxh5' platform = 'wxh5'
// #endif // #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
// 须与 tyc-server-v2 model.PlatformWxMini"wxmini")一致,勿用 mp-weixin否则 JWT 生成会失败 // 须与 qnc-server-v3 model.PlatformWxMini"wxmini")一致,勿用 mp-weixin否则 JWT 失败
platform = 'wxmini' platform = 'wxmini'
// #endif // #endif
// #ifdef MP-ALIPAY // #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 * - baseUrl `/api/v1`H5 默认,可配 VITE_API_BASE_URL
* - Header`Authorization`、`X-Platform` * - Header`Authorization`、`X-Platform`
* - Query`t` 时间戳防缓存 * - Query`t` 时间戳防缓存

View 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
View 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',
}

View File

@@ -3,6 +3,7 @@ import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue' import { ref } from 'vue'
import { getQueryExample } from '@/api' import { getQueryExample } from '@/api'
import VehicleReportShell from '@/components/report/VehicleReportShell.vue' import VehicleReportShell from '@/components/report/VehicleReportShell.vue'
import { parseEncryptedQueryReport } from '@/utils/queryReportParse'
import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize' import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize'
definePage({ definePage({
@@ -36,20 +37,19 @@ async function load() {
errText.value = '' errText.value = ''
try { try {
const res = await getQueryExample(feature.value) const res = await getQueryExample(feature.value)
if (res?.code === 200 && res.data) { const parsed = parseEncryptedQueryReport(res)
productName.value = res.data.product_name || feature.value productName.value = parsed.productName || feature.value
queryParams.value = res.data.query_params || {} if (parsed.ok) {
rows.value = normalizeVehicleQueryData(res.data.query_data || []) queryParams.value = parsed.queryParams
rows.value = parsed.rows
if (!rows.value.length) if (!rows.value.length)
errText.value = '该产品暂无示例模块数据' errText.value = '该产品暂无示例模块数据'
} }
else if (res?.code === 200003) { else if (parsed.empty) {
productName.value = feature.value
errText.value = '暂无示例报告' errText.value = '暂无示例报告'
} }
else { else {
productName.value = feature.value errText.value = parsed.msg || '加载失败'
errText.value = res?.msg || '加载失败'
} }
} }
catch { catch {

View File

@@ -3,6 +3,7 @@ import { onLoad } from '@dcloudio/uni-app'
import { computed, onUnmounted, ref } from 'vue' import { computed, onUnmounted, ref } from 'vue'
import { getProductByEn, getUserDetail, postAuthSendSmsQuery, postPayPayment, postQueryService, postUploadImage } from '@/api' import { getProductByEn, getUserDetail, postAuthSendSmsQuery, postPayPayment, postQueryService, postUploadImage } from '@/api'
import { productHasSmsCode, useInquireForm } from '@/composables/useInquireForm' import { productHasSmsCode, useInquireForm } from '@/composables/useInquireForm'
import { useBindMobile } from '@/composables/useBindMobile'
import { aesEncrypt, QUERY_PAYLOAD_AES_HEX_KEY } from '@/utils/crypto.js' import { aesEncrypt, QUERY_PAYLOAD_AES_HEX_KEY } from '@/utils/crypto.js'
definePage({ definePage({
@@ -17,6 +18,23 @@ definePage({
const feature = ref('') const feature = ref('')
const { formData, isPhoneNumberValid, isIdCardValid, isHasInput, buildRequestPayload } = useInquireForm(feature) 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 loading = ref(true)
const productLoadOk = ref(false) const productLoadOk = ref(false)
const submitting = ref(false) const submitting = ref(false)
@@ -149,7 +167,15 @@ function readToken() {
async function ensureLoginAndMobile() { async function ensureLoginAndMobile() {
if (!readToken()) { 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' }) uni.navigateTo({ url: '/pages/login' })
return false return false
} }
@@ -158,12 +184,12 @@ async function ensureLoginAndMobile() {
const mobile = (res?.data?.userInfo?.mobile || '').trim() const mobile = (res?.data?.userInfo?.mobile || '').trim()
if (res?.code !== 200 || !/^1[3-9]\d{9}$/.test(mobile)) { if (res?.code !== 200 || !/^1[3-9]\d{9}$/.test(mobile)) {
uni.showModal({ uni.showModal({
title: '提示', title: '绑定手机号',
content: '请先在「我的」中绑定手机号后再发起查询。', content: '为保证查询报告可同步查看,请先绑定手机号。',
confirmText: '去绑定', confirmText: '去绑定',
success(r) { showCancel: false,
if (r.confirm) success() {
uni.switchTab({ url: '/pages/mine' }) openBindModal()
}, },
}) })
return false return false
@@ -703,6 +729,59 @@ async function onConfirmPay() {
</view> </view>
</view> </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> </view>
</template> </template>
@@ -1134,4 +1213,113 @@ async function onConfirmPay() {
.pay-ok.disabled { .pay-ok.disabled {
opacity: 0.55; 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> </style>

View File

@@ -1,7 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onUnmounted, ref } from 'vue' import { onUnmounted, ref } from 'vue'
import { postAuthSendSms, postUserMobileCodeLogin } from '@/api'
import { saveAuthSession } from '@/utils/session'
import { tryWxMiniProgramAuth } from '@/utils/wxMiniAuth' import { tryWxMiniProgramAuth } from '@/utils/wxMiniAuth'
definePage({ definePage({
@@ -13,105 +11,10 @@ definePage({
}, },
}) })
const phoneMode = ref(false)
const mobile = ref('')
const code = ref('')
const agreed = ref(false) const agreed = ref(false)
const sending = ref(false)
const submitting = ref(false)
const wxLoading = 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 canWxMiniLogin = ref(true)
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
}
}
function toggleAgree() { function toggleAgree() {
agreed.value = !agreed.value agreed.value = !agreed.value
@@ -129,22 +32,13 @@ function goLegalAuthorization() {
uni.navigateTo({ url: '/pages/legal/authorization' }) 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() { async function handleWxMiniLogin() {
if (!agreed.value) { if (!agreed.value) {
uni.showToast({ title: '请先勾选协议', icon: 'none' }) uni.showToast({ title: '请先勾选协议', icon: 'none' })
return return
} }
wxLoading.value = true wxLoading.value = true
canWxMiniLogin.value = false
try { try {
const ok = await tryWxMiniProgramAuth({ silent: false }) const ok = await tryWxMiniProgramAuth({ silent: false })
if (!ok) if (!ok)
@@ -159,20 +53,16 @@ async function handleWxMiniLogin() {
} }
finally { finally {
wxLoading.value = false wxLoading.value = false
canWxMiniLogin.value = true
} }
} }
onUnmounted(() => {
clearTimer()
})
</script> </script>
<template> <template>
<view class="page-root"> <view class="page-root">
<view class="bg-blob" /> <view class="bg-blob" />
<view class="page"> <view class="page">
<!-- 入口仅按钮 + 一行协议 --> <view class="gate">
<view v-show="!phoneMode" class="gate">
<view class="brand-mark" /> <view class="brand-mark" />
<view class="brand-title"> <view class="brand-title">
全能查 全能查
@@ -185,15 +75,17 @@ onUnmounted(() => {
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<view <view
class="btn btn-wx" class="btn btn-wx"
:class="{ disabled: !canWxMiniLogin }" :class="{ disabled: !canWxMiniLogin || wxLoading }"
@tap="handleWxMiniLogin" @tap="handleWxMiniLogin"
> >
{{ wxLoading ? '' : '微信登录' }} {{ wxLoading ? '登录中...' : '微信一键登录' }}
</view> </view>
<!-- #endif --> <!-- #endif -->
<view class="btn btn-phone" @tap="openPhoneForm"> <!-- #ifndef MP-WEIXIN -->
手机号登录 <view class="btn btn-wx" @tap="handleWxMiniLogin">
一键登录
</view> </view>
<!-- #endif -->
</view> </view>
<view class="agree-row" @tap.stop> <view class="agree-row" @tap.stop>
@@ -208,75 +100,6 @@ onUnmounted(() => {
<text class="link" @tap="goLegalAuthorization">授权书</text> <text class="link" @tap="goLegalAuthorization">授权书</text>
</view> </view>
</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>
</view> </view>
</template> </template>
@@ -333,7 +156,7 @@ onUnmounted(() => {
font-size: 26rpx; font-size: 26rpx;
color: #86909c; color: #86909c;
margin-top: 12rpx; margin-top: 12rpx;
margin-bottom: 72rpx; margin-bottom: 120rpx;
} }
.btn-stack { .btn-stack {
@@ -363,13 +186,6 @@ onUnmounted(() => {
pointer-events: none; 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 { .agree-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -412,117 +228,4 @@ onUnmounted(() => {
margin: 0 4rpx; margin: 0 4rpx;
color: #86909c; 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> </style>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { onShareAppMessage, onShareTimeline, onShow } from '@dcloudio/uni-app' import { onShareAppMessage, onShareTimeline, onShow } from '@dcloudio/uni-app'
import { onUnmounted, ref } from 'vue' import { ref } from 'vue'
import { clearAuthStorage, getUserDetail, postAuthSendSmsBindMobile, postUserBindMobile } from '@/api' import { getUserDetail } from '@/api'
import { hasToken, saveAuthSession } from '@/utils/session' import { hasToken } from '@/utils/session'
import { useBindMobile } from '@/composables/useBindMobile'
definePage({ definePage({
style: { style: {
@@ -30,14 +31,22 @@ const userDesc = ref('')
const hasBoundMobile = ref(false) const hasBoundMobile = ref(false)
const wxNickStorage = ref('') const wxNickStorage = ref('')
const bindModalOpen = ref(false) const {
const bindPhone = ref('') bindVisible: bindModalOpen,
const bindCode = ref('') bindPhone,
const bindSending = ref(false) bindCode,
const bindSubmitting = ref(false) bindSending,
const bindCountingDown = ref(false) bindSubmitting,
const bindCountdown = ref(60) bindCountingDown,
let bindSmsTimer: ReturnType<typeof setInterval> | null = null bindCountdown,
isBindPhoneValid,
openBindModal,
closeBindModal,
onBindPhoneInput,
onBindCodeInput,
sendBindSms,
submitBindMobile,
} = useBindMobile(refreshUserCard)
const coopModalOpen = ref(false) const coopModalOpen = ref(false)
@@ -68,29 +77,9 @@ function loadWxNickFromStorage() {
} }
} }
function clearBindSmsTimer() { function openBindModalFromProfile() {
if (bindSmsTimer) { openBindModal()
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)
async function refreshUserCard() { async function refreshUserCard() {
isLogin.value = hasToken() isLogin.value = hasToken()
@@ -148,19 +137,6 @@ onShow(() => {
function handleUserTap() { function handleUserTap() {
if (isLogin.value) { 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 return
} }
uni.navigateTo({ url: '/pages/login' }) uni.navigateTo({ url: '/pages/login' })
@@ -193,77 +169,8 @@ async function syncWxNickname() {
// #endif // #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() { function goHistoryReport() {
uni.switchTab({ url: '/pages/report' }) uni.navigateTo({ url: '/pages/report' })
} }
function goFreeValuation() { function goFreeValuation() {
@@ -296,11 +203,11 @@ function goLegalAuthorization() {
} }
function goIllegalCode() { function goIllegalCode() {
uni.showToast({ title: '敬请期待', icon: 'none' }) uni.navigateTo({ url: '/pages/toolbox/query?key=jtwfcode' })
} }
function goOilPrice() { function goOilPrice() {
uni.showToast({ title: '敬请期待', icon: 'none' }) uni.navigateTo({ url: '/pages/toolbox/query?key=oilprice' })
} }
function goHelp() { function goHelp() {
@@ -319,10 +226,6 @@ function goSettings() {
function goServiceFallback() { function goServiceFallback() {
uni.showToast({ title: '请在微信小程序内使用在线客服', icon: 'none' }) uni.showToast({ title: '请在微信小程序内使用在线客服', icon: 'none' })
} }
onUnmounted(() => {
clearBindSmsTimer()
})
</script> </script>
<template> <template>
@@ -349,7 +252,7 @@ onUnmounted(() => {
</view> </view>
<!-- 已登录未绑定手机 --> <!-- 已登录未绑定手机 -->
<view v-if="isLogin && !hasBoundMobile" class="profile-actions"> <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" /> <view class="action-icon i-carbon-phone" />
<text class="action-text">绑定手机号</text> <text class="action-text">绑定手机号</text>
</view> </view>
@@ -406,20 +309,18 @@ onUnmounted(() => {
<!-- 区块3: 常用工具 --> <!-- 区块3: 常用工具 -->
<view class="section"> <view class="section">
<view class="section-title">常用工具</view> <view class="section-title">常用工具</view>
<view class="tool-list"> <view class="quick-grid">
<view class="tool-row" @tap="goOilPrice"> <view class="quick-item" @tap="goOilPrice">
<view class="tool-row-icon-wrap" style="background: rgba(23,104,255,0.08)"> <view class="quick-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-icon i-carbon-gas-station" style="color: #1768ff" />
</view> </view>
<text class="tool-row-name">实时油价查询</text> <text class="quick-name">实时油价查询</text>
<text class="tool-row-arrow"></text>
</view> </view>
<view class="tool-row" @tap="goIllegalCode"> <view class="quick-item" @tap="goIllegalCode">
<view class="tool-row-icon-wrap" style="background: rgba(250,140,22,0.08)"> <view class="quick-icon-wrap" style="background: rgba(250,140,22,0.08)">
<view class="tool-row-icon i-carbon-search" style="color: #fa8c16" /> <view class="quick-icon i-carbon-search" style="color: #fa8c16" />
</view> </view>
<text class="tool-row-name">违章代码查询</text> <text class="quick-name">违章代码查询</text>
<text class="tool-row-arrow"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -427,57 +328,52 @@ onUnmounted(() => {
<!-- 区块4: 服务与支持 --> <!-- 区块4: 服务与支持 -->
<view class="section"> <view class="section">
<view class="section-title">服务与支持</view> <view class="section-title">服务与支持</view>
<view class="tool-list"> <view class="quick-grid">
<button class="tool-row tool-row-btn no-border" open-type="contact" hover-class="tool-row-hover"> <!-- #ifdef MP-WEIXIN -->
<view class="tool-row-icon-wrap" style="background: rgba(19,194,94,0.08)"> <button class="quick-item quick-item-btn" open-type="contact" hover-class="quick-item-hover">
<view class="tool-row-icon i-carbon-chat" style="color: #13c25e" /> <view class="quick-icon-wrap" style="background: rgba(19,194,94,0.08)">
</view> <view class="quick-icon i-carbon-chat" style="color: #13c25e" />
<view class="tool-row-text">
<text class="tool-row-name">在线客服</text>
<text class="tool-row-sub">周一至周日 9:00-20:00</text>
</view> </view>
<text class="quick-name">在线客服</text>
</button> </button>
<view class="tool-row" @tap="goHelp"> <!-- #endif -->
<view class="tool-row-icon-wrap" style="background: rgba(114,46,209,0.08)"> <!-- #ifndef MP-WEIXIN -->
<view class="tool-row-icon i-carbon-help" style="color: #722ed1" /> <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> </view>
<text class="tool-row-name">帮助中心</text> <text class="quick-name">在线客服</text>
<text class="tool-row-arrow"></text>
</view> </view>
<view class="tool-row" @tap="goAbout"> <!-- #endif -->
<view class="tool-row-icon-wrap" style="background: rgba(78,89,105,0.08)"> <view class="quick-item" @tap="goHelp">
<view class="tool-row-icon i-carbon-information" style="color: #4e5969" /> <view class="quick-icon-wrap" style="background: rgba(114,46,209,0.08)">
<view class="quick-icon i-carbon-help" style="color: #722ed1" />
</view> </view>
<text class="tool-row-name">关于我们</text> <text class="quick-name">帮助中心</text>
<text class="tool-row-arrow"></text>
</view> </view>
<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> </view>
<text class="quick-name">关于我们</text>
</view> </view>
<view class="quick-item" @tap="goLegalUserAgreement">
<!-- 区块5: 法律条款 --> <view class="quick-icon-wrap" style="background: rgba(78,89,105,0.06)">
<view class="section"> <view class="quick-icon i-carbon-document-blank" style="color: #86909c" />
<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> </view>
<text class="tool-row-name">用户协议</text> <text class="quick-name">用户协议</text>
<text class="tool-row-arrow"></text>
</view> </view>
<view class="tool-row" @tap="goLegalPrivacyPolicy"> <view class="quick-item" @tap="goLegalPrivacyPolicy">
<view class="tool-row-icon-wrap" style="background: rgba(78,89,105,0.06)"> <view class="quick-icon-wrap" style="background: rgba(78,89,105,0.06)">
<view class="tool-row-icon i-carbon-security" style="color: #86909c" /> <view class="quick-icon i-carbon-security" style="color: #86909c" />
</view> </view>
<text class="tool-row-name">隐私政策</text> <text class="quick-name">隐私政策</text>
<text class="tool-row-arrow"></text>
</view> </view>
<view class="tool-row no-border" @tap="goLegalAuthorization"> <view class="quick-item" @tap="goLegalAuthorization">
<view class="tool-row-icon-wrap" style="background: rgba(78,89,105,0.06)"> <view class="quick-icon-wrap" style="background: rgba(78,89,105,0.06)">
<view class="tool-row-icon i-carbon-certificate" style="color: #86909c" /> <view class="quick-icon i-carbon-certificate" style="color: #86909c" />
</view> </view>
<text class="tool-row-name">授权书</text> <text class="quick-name">授权书</text>
<text class="tool-row-arrow"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -735,7 +631,7 @@ onUnmounted(() => {
.quick-icon-wrap { .quick-icon-wrap {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 24rpx; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -2,6 +2,7 @@
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue' import { ref } from 'vue'
import { getQueryList } from '@/api' import { getQueryList } from '@/api'
import { hasToken } from '@/utils/session'
definePage({ definePage({
style: { style: {
@@ -124,6 +125,10 @@ async function loadList() {
} }
onShow(() => { onShow(() => {
if (!hasToken()) {
uni.redirectTo({ url: '/pages/login' })
return
}
void loadList() void loadList()
}) })

View File

@@ -3,6 +3,7 @@ import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue' import { ref } from 'vue'
import { getQueryDetailByOrderId, getQueryDetailByOrderNo } from '@/api' import { getQueryDetailByOrderId, getQueryDetailByOrderNo } from '@/api'
import VehicleReportShell from '@/components/report/VehicleReportShell.vue' import VehicleReportShell from '@/components/report/VehicleReportShell.vue'
import { parseEncryptedQueryReport } from '@/utils/queryReportParse'
import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize' import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize'
definePage({ definePage({
@@ -40,15 +41,16 @@ async function load() {
const res = orderId.value const res = orderId.value
? await getQueryDetailByOrderId(orderId.value) ? await getQueryDetailByOrderId(orderId.value)
: await getQueryDetailByOrderNo(orderNo.value) : await getQueryDetailByOrderNo(orderNo.value)
if (res?.code === 200 && res.data) { const parsed = parseEncryptedQueryReport(res)
productName.value = res.data.product_name || '查询报告' productName.value = parsed.productName || '查询报告'
queryParams.value = res.data.query_params || {} if (parsed.ok) {
rows.value = normalizeVehicleQueryData(res.data.query_data || []) queryParams.value = parsed.queryParams
rows.value = parsed.rows
if (!rows.value.length) if (!rows.value.length)
errText.value = '暂无报告模块数据' errText.value = '暂无报告模块数据'
} }
else { else {
errText.value = res?.msg || '加载失败' errText.value = parsed.msg || '加载失败'
} }
} }
catch { catch {

View File

@@ -20,5 +20,23 @@ function generateRandomIV() {
return CryptoJS.enc.Hex.parse(iv.map(b => b.toString(16).padStart(2, '0')).join('')) 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' 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)
}

View 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,
}
}

View File

@@ -1,4 +1,5 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { loadEnv } from 'vite'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import Components from '@uni-helper/vite-plugin-uni-components' import Components from '@uni-helper/vite-plugin-uni-components'
@@ -14,17 +15,19 @@ import { UniEcharts } from 'uni-echarts/vite'
import Uni from '@uni-helper/plugin-uni' import Uni from '@uni-helper/plugin-uni'
import UnoCSS from 'unocss/vite' import UnoCSS from 'unocss/vite'
export default defineConfig({ 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: { server: {
host: '0.0.0.0', host: "0.0.0.0",
port: 5173, port: 5678,
strictPort: false, strictPort: true,
// 与 tyc-webview-v2 一致H5 开发时将 /api/v1 代理到线上网关(本地可改 target
proxy: { proxy: {
'/api/v1': { "/api/v1": {
// target: 'https://www.tianyuancha.cn', target: proxyTarget,
target: 'http://127.0.0.1:8888',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path,
}, },
}, },
}, },
@@ -67,6 +70,7 @@ export default defineConfig({
"uni-echarts" "uni-echarts"
] ]
} }
}
}) })