This commit is contained in:
Mrx
2026-05-21 12:00:38 +08:00
parent 19a11516ec
commit 6137c69034
13 changed files with 2080 additions and 1036 deletions

View File

@@ -26,10 +26,10 @@ export default defineUniPages({
selectedIconPath: 'static/tabbar/home-active.png', selectedIconPath: 'static/tabbar/home-active.png',
}, },
{ {
pagePath: 'pages/report', pagePath: 'pages/toolbox/index',
text: '报告', text: '工具',
iconPath: 'static/tabbar/report.png', iconPath: 'static/tabbar/toolbox.png',
selectedIconPath: 'static/tabbar/report-active.png', selectedIconPath: 'static/tabbar/toolbox-active.png',
}, },
{ {
pagePath: 'pages/mine', pagePath: 'pages/mine',

View File

@@ -1087,6 +1087,63 @@ export const toolboxItems = [
index: '热度', index: '热度',
}, },
}, },
{
key: 'star',
name: '星座运势',
desc: '查询十二星座今日运势(娱乐)',
icon: 'i-carbon-star',
fields: [
{ key: 'astro', label: '星座', type: 'select', mode: 'selector', options: [
{ label: '白羊座', value: '白羊座' },
{ label: '金牛座', value: '金牛座' },
{ label: '双子座', value: '双子座' },
{ label: '巨蟹座', value: '巨蟹座' },
{ label: '狮子座', value: '狮子座' },
{ label: '处女座', value: '处女座' },
{ label: '天秤座', value: '天秤座' },
{ label: '天蝎座', value: '天蝎座' },
{ label: '射手座', value: '射手座' },
{ label: '摩羯座', value: '摩羯座' },
{ label: '水瓶座', value: '水瓶座' },
{ label: '双鱼座', value: '双鱼座' },
], placeholder: '请选择星座' },
],
validate: (form) => (form.astro || '').length > 0,
validateMsg: '请选择星座',
resultType: 'list',
resultLabels: {
type: '运势类型',
content: '运势内容',
},
},
{
key: 'tianqishiju',
name: '天气诗句',
desc: '根据天气类型随机返回经典诗句',
icon: 'i-carbon-cloud',
fields: [
{ key: 'tqtype', label: '天气类型', type: 'select', mode: 'selector', options: [
{ label: '风', value: 1 },
{ label: '云', value: 2 },
{ label: '雨', value: 3 },
{ label: '雪', value: 4 },
{ label: '霜', value: 5 },
{ label: '露', value: 6 },
{ label: '雾', value: 7 },
{ label: '雷', value: 8 },
{ label: '晴', value: 9 },
{ label: '阴', value: 10 },
], placeholder: '可选,选择天气类型' },
],
validate: () => true,
validateMsg: '',
resultLabels: {
content: '诗句',
author: '作者',
source: '出处',
weather: '天气',
},
},
{ {
key: 'verse', key: 'verse',
name: '诗词名句', name: '诗词名句',
@@ -1457,7 +1514,9 @@ export const toolboxItems = [
resultLabels: { resultLabels: {
name: '名称', name: '名称',
type_name: '分类', type_name: '分类',
explain: '说明', explain: '分类说明',
contain: '包含类型',
tip: '投放提示',
}, },
}, },
{ {
@@ -2214,17 +2273,19 @@ export const toolboxItems = [
}, },
{ {
key: 'anslajifenlei', key: 'anslajifenlei',
name: '按师垃圾分类', name: '垃圾分类问答',
desc: '按垃圾分类指南', desc: '随机出现一个物品,选出它属于哪种垃圾分类,考考你的环保知识',
icon: 'i-carbon-recycle', icon: 'i-carbon-recycle',
autoQuery: true, autoQuery: true,
isGame: true,
fields: [], fields: [],
validate: () => true, validate: () => true,
validateMsg: '', validateMsg: '',
resultType: 'list',
resultLabels: { resultLabels: {
name: '名称', name: { label: '物品名称' },
type_name: '分类', type: { label: '正确分类', hidden: true },
type_name: { label: '分类名称', hidden: true },
explain: { label: '分类说明', hidden: true },
}, },
}, },
{ {
@@ -2268,6 +2329,7 @@ export const toolboxItems = [
content: '问候语', content: '问候语',
}, },
}, },
{ {
key: 'story', key: 'story',
name: '故事大全', name: '故事大全',

View File

@@ -74,6 +74,16 @@
"navigationBarTextStyle": "black" "navigationBarTextStyle": "black"
} }
}, },
{
"path": "pages/inquire/list",
"type": "page",
"style": {
"navigationBarTitleText": "车辆查询服务",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{ {
"path": "pages/legal/authorization", "path": "pages/legal/authorization",
"type": "page", "type": "page",
@@ -128,7 +138,7 @@
"path": "pages/toolbox/index", "path": "pages/toolbox/index",
"type": "page", "type": "page",
"style": { "style": {
"navigationBarTitleText": "实用工具", "navigationBarTitleText": "工具分类",
"navigationStyle": "default", "navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff", "navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black" "navigationBarTextStyle": "black"
@@ -161,10 +171,10 @@
"selectedIconPath": "static/tabbar/home-active.png" "selectedIconPath": "static/tabbar/home-active.png"
}, },
{ {
"pagePath": "pages/report", "pagePath": "pages/toolbox/index",
"text": "报告", "text": "工具",
"iconPath": "static/tabbar/report.png", "iconPath": "static/tabbar/toolbox.png",
"selectedIconPath": "static/tabbar/report-active.png" "selectedIconPath": "static/tabbar/toolbox-active.png"
}, },
{ {
"pagePath": "pages/mine", "pagePath": "pages/mine",

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup>
import { onLoad } from '@dcloudio/uni-app' 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'
@@ -21,12 +21,7 @@ const loading = ref(true)
const productLoadOk = ref(false) const productLoadOk = ref(false)
const submitting = ref(false) const submitting = ref(false)
const paying = ref(false) const paying = ref(false)
const featureData = ref<{ const featureData = ref({})
product_name?: string
description?: string
sell_price?: number
product_en?: string
}>({})
const queryId = ref('') const queryId = ref('')
const showPaySheet = ref(false) const showPaySheet = ref(false)
@@ -35,7 +30,7 @@ const vlphotoPreviewPath = ref('')
const imageUrlUploading = ref(false) const imageUrlUploading = ref(false)
const countdown = ref(0) const countdown = ref(0)
let smsTimer: ReturnType<typeof setInterval> | null = null let smsTimer = null
const userTypeOptions = [ const userTypeOptions = [
{ text: 'ETC开户人', value: '1' }, { text: 'ETC开户人', value: '1' },
@@ -78,7 +73,7 @@ const isIdCardWomanValid = computed(() => /^\d{17}[\dX]$/i.test(formData.idCardW
const isCreditCodeValid = computed(() => /^.{18}$/.test(formData.entCode || '')) const isCreditCodeValid = computed(() => /^.{18}$/.test(formData.entCode || ''))
onLoad((options) => { onLoad((options) => {
feature.value = (options?.feature as string) || '' feature.value = (options?.feature) || ''
void loadProduct() void loadProduct()
}) })
@@ -119,10 +114,7 @@ async function loadProduct() {
} }
loading.value = true loading.value = true
try { try {
const res = await getProductByEn(feature.value) as { const res = await getProductByEn(feature.value)
code?: number
data?: typeof featureData.value
}
if (res?.code === 200 && res.data) { if (res?.code === 200 && res.data) {
featureData.value = res.data featureData.value = res.data
productLoadOk.value = true productLoadOk.value = true
@@ -145,7 +137,7 @@ async function loadProduct() {
} }
} }
function readToken(): string { function readToken() {
try { try {
const t = uni.getStorageSync('token') const t = uni.getStorageSync('token')
return typeof t === 'string' ? t : '' return typeof t === 'string' ? t : ''
@@ -155,17 +147,14 @@ function readToken(): string {
} }
} }
async function ensureLoginAndMobile(): Promise<boolean> { async function ensureLoginAndMobile() {
if (!readToken()) { if (!readToken()) {
uni.showToast({ title: '请先登录', icon: 'none' }) uni.showToast({ title: '请先登录', icon: 'none' })
uni.navigateTo({ url: '/pages/login' }) uni.navigateTo({ url: '/pages/login' })
return false return false
} }
try { try {
const res = await getUserDetail() as { const res = await getUserDetail()
code?: number
data?: { userInfo?: { mobile?: string } }
}
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({
@@ -188,11 +177,11 @@ async function ensureLoginAndMobile(): Promise<boolean> {
} }
function validateField( function validateField(
field: string, field,
value: unknown, value,
ok: (v: unknown) => boolean, ok,
msg: string, msg,
): boolean { ) {
if (!isHasInput(field)) if (!isHasInput(field))
return true return true
if (!ok(value)) { if (!ok(value)) {
@@ -202,7 +191,7 @@ function validateField(
return true return true
} }
function validateAll(): boolean { function validateAll() {
if (!formData.agreeToTerms) { if (!formData.agreeToTerms) {
uni.showToast({ title: '请阅读并同意用户协议与隐私政策', icon: 'none' }) uni.showToast({ title: '请阅读并同意用户协议与隐私政策', icon: 'none' })
return false return false
@@ -240,17 +229,17 @@ function validateAll(): boolean {
return true return true
} }
function onUserTypePick(e: { detail: { value: string | number } }) { function onUserTypePick(e) {
const idx = Number(e.detail.value) const idx = Number(e.detail.value)
formData.userType = userTypeOptions[idx]?.value ?? '1' formData.userType = userTypeOptions[idx]?.value ?? '1'
} }
function onAuthorizedPick(e: { detail: { value: string | number } }) { function onAuthorizedPick(e) {
const idx = Number(e.detail.value) const idx = Number(e.detail.value)
formData.authorized = authorizedOptions[idx]?.value ?? '1' formData.authorized = authorizedOptions[idx]?.value ?? '1'
} }
function onFirstRegChange(e: { detail: { value: string } }) { function onFirstRegChange(e) {
formData.firstRegistrationDate = (e.detail.value || '').slice(0, 7) formData.firstRegistrationDate = (e.detail.value || '').slice(0, 7)
} }
@@ -285,7 +274,7 @@ async function sendSmsCode() {
if (!(await ensureLoginAndMobile())) if (!(await ensureLoginAndMobile()))
return return
try { try {
const res = await postAuthSendSmsQuery({ mobile: formData.mobile, captchaVerifyParam: '' }) as { code?: number } const res = await postAuthSendSmsQuery({ mobile: formData.mobile, captchaVerifyParam: '' })
if (res?.code === 200) { if (res?.code === 200) {
uni.showToast({ title: '验证码已发送', icon: 'none' }) uni.showToast({ title: '验证码已发送', icon: 'none' })
startSmsCountdown() startSmsCountdown()
@@ -296,7 +285,7 @@ async function sendSmsCode() {
} }
} }
function chooseImageFile(maxBytes: number): Promise<{ path: string, base64: string }> { function chooseImageFile(maxBytes) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.chooseImage({ uni.chooseImage({
count: 1, count: 1,
@@ -338,7 +327,7 @@ async function onPickImageUrl() {
try { try {
const { base64 } = await chooseImageFile(3 * 1024 * 1024) const { base64 } = await chooseImageFile(3 * 1024 * 1024)
imageUrlUploading.value = true imageUrlUploading.value = true
const up = await postUploadImage(base64) as { code?: number, data?: { url?: string }, msg?: string } const up = await postUploadImage(base64)
if (up?.code === 200 && up.data?.url) if (up?.code === 200 && up.data?.url)
formData.imageUrl = up.data.url formData.imageUrl = up.data.url
else if (up?.msg) else if (up?.msg)
@@ -371,11 +360,7 @@ async function doCreateQuery(captchaVerifyParam = '') {
const res = await postQueryService(feature.value, { const res = await postQueryService(feature.value, {
data: enc, data: enc,
captchaVerifyParam, captchaVerifyParam,
}) as { })
code?: number
data?: { id?: string }
msg?: string
}
return res return res
} }
@@ -419,7 +404,7 @@ async function onConfirmPay() {
return return
} }
try { try {
await new Promise<void>((resolve, reject) => { await new Promise((resolve, reject) => {
uni.showModal({ uni.showModal({
title: '重要提示', title: '重要提示',
content: '请确认您了解本服务为大数据报告查询,付款前请仔细阅读产品说明。是否继续支付?', content: '请确认您了解本服务为大数据报告查询,付款前请仔细阅读产品说明。是否继续支付?',
@@ -443,17 +428,7 @@ async function onConfirmPay() {
id: queryId.value, id: queryId.value,
pay_method: 'wechat', pay_method: 'wechat',
pay_type: 'query', pay_type: 'query',
}) as { })
code?: number
data?: {
prepay_data?: Record<string, string>
prepayData?: Record<string, string>
prepay_id?: string
prepayId?: string
order_no?: string
orderNo?: string
}
}
if (payRes?.code !== 200 || !payRes.data) { if (payRes?.code !== 200 || !payRes.data) {
return return
@@ -478,7 +453,7 @@ async function onConfirmPay() {
return return
} }
await new Promise<void>((resolve, reject) => { await new Promise((resolve, reject) => {
uni.requestPayment({ uni.requestPayment({
provider: 'wxpay', provider: 'wxpay',
timeStamp: String(prepay.timeStamp ?? prepay.timestamp ?? ''), timeStamp: String(prepay.timeStamp ?? prepay.timestamp ?? ''),
@@ -502,7 +477,7 @@ async function onConfirmPay() {
resolve() resolve()
}, },
fail(err) { fail(err) {
const msg = (err as { errMsg?: string })?.errMsg || '支付未完成' const msg = err?.errMsg || '支付未完成'
uni.showToast({ title: msg.replace('requestPayment:fail ', ''), icon: 'none' }) uni.showToast({ title: msg.replace('requestPayment:fail ', ''), icon: 'none' })
reject(err) reject(err)
}, },
@@ -528,8 +503,15 @@ async function onConfirmPay() {
</view> </view>
<scroll-view v-else scroll-y class="scroll page-inner"> <scroll-view v-else scroll-y class="scroll page-inner">
<view class="card-container"> <view class="card-container">
<!-- 顶部产品长图 -->
<view class="inquire-banner">
<image class="inquire-banner-img" src="/static/home/images/VIN.png" mode="widthFix" />
<view class="inquire-banner-overlay">
<text class="inquire-banner-title">{{ featureData.product_name || '查询服务' }}</text>
</view>
</view>
<view class="card-header"> <view class="card-header">
{{ featureData.product_name || feature || '查询' }} 请输入查询信息
</view> </view>
<view class="section-row"> <view class="section-row">
@@ -577,7 +559,7 @@ async function onConfirmPay() {
</view> </view>
<view v-if="isHasInput('mobile')" class="field"> <view v-if="isHasInput('mobile')" class="field">
<text class="label">手机号</text> <text class="label">手机号</text>
<input v-model="formData.mobile" class="input" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="ph"> <input v-model="formData.mobile" class="input" type="number" :maxlength="11" placeholder="请输入手机号" placeholder-class="ph">
</view> </view>
<view v-if="isHasInput('carLicense')" class="field"> <view v-if="isHasInput('carLicense')" class="field">
<text class="label">车牌号</text> <text class="label">车牌号</text>
@@ -654,7 +636,7 @@ async function onConfirmPay() {
<view v-if="isHasInput('verificationCode')" class="field code-row"> <view v-if="isHasInput('verificationCode')" class="field code-row">
<text class="label">验证码</text> <text class="label">验证码</text>
<input v-model="formData.verificationCode" class="input flex1" maxlength="6" placeholder="请输入验证码" placeholder-class="ph"> <input v-model="formData.verificationCode" class="input flex1" :maxlength="6" placeholder="请输入验证码" placeholder-class="ph">
<view class="sms-btn" :class="{ disabled: isCountingDown || !isPhoneNumberValid }" @tap="sendSmsCode"> <view class="sms-btn" :class="{ disabled: isCountingDown || !isPhoneNumberValid }" @tap="sendSmsCode">
{{ isCountingDown ? `${countdown}s重新获取` : '获取验证码' }} {{ isCountingDown ? `${countdown}s重新获取` : '获取验证码' }}
</view> </view>
@@ -750,6 +732,33 @@ async function onConfirmPay() {
padding: 24rpx 24rpx 48rpx; padding: 24rpx 24rpx 48rpx;
} }
/* 顶部产品图 */
.inquire-banner {
position: relative;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
}
.inquire-banner-img {
width: 100%;
max-height: 260rpx;
display: block;
}
.inquire-banner-overlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 16rpx 24rpx;
background: linear-gradient(transparent, rgba(0,0,0,0.4));
}
.inquire-banner-title {
font-size: 26rpx;
font-weight: 600;
color: #fff;
text-shadow: 0 1rpx 4rpx rgba(0,0,0,0.3);
}
.card-container { .card-container {
background: #fff; background: #fff;
border-radius: 16rpx; border-radius: 16rpx;

190
src/pages/inquire/list.vue Normal file
View File

@@ -0,0 +1,190 @@
<script setup lang="ts">
import { computed } from 'vue'
import { getInquireCategoryConfig, getInquiryItemIconUrl } from '@/config/inquireCategories'
definePage({
style: {
navigationBarTitleText: '车辆查询服务',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
const vehicleItems = computed(() => getInquireCategoryConfig('vehicle')?.items ?? [])
function goInquireFeature(feature: string) {
uni.navigateTo({ url: `/pages/inquire/index?feature=${encodeURIComponent(feature)}` })
}
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<!-- 头部统计 -->
<view class="list-header">
<view class="list-header-icon">
<view class="i-carbon-car" />
</view>
<view class="list-header-info">
<text class="list-header-title">车辆查询服务</text>
<text class="list-header-desc"> {{ vehicleItems.length }} 项专业车况核验服务</text>
</view>
</view>
<!-- 服务列表 -->
<view class="service-list">
<view
v-for="item in vehicleItems"
:key="item.feature"
class="service-item"
@tap="goInquireFeature(item.feature)"
>
<view class="svc-icon-wrap">
<image class="svc-icon" :src="getInquiryItemIconUrl(item)" mode="aspectFit" />
</view>
<view class="svc-content">
<text class="svc-name">{{ item.name }}</text>
<text class="svc-desc">{{ item.desc }}</text>
</view>
<text class="svc-arrow"></text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
.page-root {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #f8faff 0%, #f3f5fb 100%);
}
.scrollarea {
flex: 1;
min-height: 0;
height: 0;
}
.page {
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
}
.list-header {
display: flex;
align-items: center;
gap: 20rpx;
padding: 28rpx 28rpx;
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
border: 1rpx solid #e5e6f0;
margin-bottom: 24rpx;
box-shadow: 0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
}
.list-header-icon {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #e8f0fe, #d4e4fd);
display: flex;
align-items: center;
justify-content: center;
font-size: 38rpx;
color: #1768ff;
flex-shrink: 0;
}
.list-header-info {
flex: 1;
}
.list-header-title {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 6rpx;
}
.list-header-desc {
display: block;
font-size: 24rpx;
color: #86909c;
}
.service-list {
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
border: 1rpx solid #e5e6f0;
overflow: hidden;
box-shadow: 0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
}
.service-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx 28rpx;
border-bottom: 1rpx solid #f2f3f5;
}
.service-item:last-child {
border-bottom: none;
}
.service-item:active {
background: #f7f8fa;
}
.svc-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
background: linear-gradient(135deg, #e8f0fe, #d4e4fd);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.svc-icon {
width: 40rpx;
height: 40rpx;
}
.svc-content {
flex: 1;
min-width: 0;
}
.svc-name {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #1d2129;
margin-bottom: 4rpx;
}
.svc-desc {
display: block;
font-size: 22rpx;
color: #86909c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.svc-arrow {
font-size: 32rpx;
color: #c9cdd4;
flex-shrink: 0;
}
</style>

View File

@@ -329,120 +329,160 @@ onUnmounted(() => {
<view class="page-root"> <view class="page-root">
<scroll-view scroll-y class="scrollarea"> <scroll-view scroll-y class="scrollarea">
<view class="page"> <view class="page">
<view class="banner mine-banner">
<view class="banner-text">
<view class="banner-title">
买车先查车况更安心
</view>
<view class="banner-sub">
减少隐蔽事故车泡水车调表车风险
</view>
</view>
<view class="banner-illustration">
<view class="car-illus" />
</view>
</view>
<view class="card mine-user"> <!-- 区块1: 个人信息栏 -->
<view class="mine-user-main" @tap="handleUserTap"> <view class="profile-card">
<view class="mine-avatar-placeholder" /> <view class="profile-bg" />
<view class="mine-user-text"> <view class="profile-content" @tap="handleUserTap">
<view class="mine-user-name"> <view class="profile-avatar">
{{ isLogin ? nickname : '您还没有登录,立即登录' }} <view class="avatar-inner">
</view> <view class="avatar-icon i-carbon-user" />
<view class="mine-user-desc">
{{ isLogin ? userDesc : '登录后可同步历史报告与收藏' }}
</view> </view>
</view> </view>
<view class="profile-info">
<text class="profile-name">{{ isLogin ? nickname : '点击登录' }}</text>
<text class="profile-desc">{{ isLogin ? userDesc : '登录后可同步历史报告与收藏' }}</text>
</view>
<view class="profile-arrow">
<view class="i-carbon-chevron-right" />
</view>
</view>
<!-- 已登录未绑定手机 -->
<view v-if="isLogin && !hasBoundMobile" class="profile-actions">
<view class="action-btn" @tap.stop="openBindModal">
<view class="action-icon i-carbon-phone" />
<text class="action-text">绑定手机号</text>
</view> </view>
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<view v-if="isLogin && !hasBoundMobile" class="mine-user-extra"> <view class="action-btn" @tap.stop="syncWxNickname">
<text class="extra-link" @tap.stop="syncWxNickname">同步微信昵称</text> <view class="action-icon i-carbon-edit" />
<text class="extra-dot">·</text> <text class="action-text">同步昵称</text>
<text class="extra-hint">绑定手机后优先显示手机号</text>
</view> </view>
<!-- #endif --> <!-- #endif -->
<view
v-if="isLogin && !hasBoundMobile"
class="mine-bind-row"
@tap.stop="openBindModal"
>
<text class="mine-bind-text">绑定手机号</text>
<text class="mine-bind-arrow"></text>
</view> </view>
</view> </view>
<view class="card"> <!-- 区块2: 快捷功能4宫格 -->
<view class="grid-4"> <view class="section">
<view class="grid-item" @tap="goHistoryReport"> <view class="section-title">快捷功能</view>
<view class="icon-tool i-carbon-document" /> <view class="quick-grid">
<view class="grid-text"> <view class="quick-item" @tap="goHistoryReport">
历史报告 <view class="quick-icon-wrap" style="background: rgba(23,104,255,0.08)">
<view class="quick-icon i-carbon-document" style="color: #1768ff" />
</view> </view>
<text class="quick-name">历史报告</text>
</view> </view>
<view class="grid-item" @tap="goFreeValuation"> <view class="quick-item" @tap="goFreeValuation">
<view class="icon-tool i-carbon-chart-line" /> <view class="quick-icon-wrap" style="background: rgba(250,140,22,0.08)">
<view class="grid-text"> <view class="quick-icon i-carbon-chart-line" style="color: #fa8c16" />
免费估值
</view> </view>
<text class="quick-name">免费估值</text>
</view> </view>
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<button class="grid-item grid-item-btn" open-type="share" hover-class="grid-item-hover"> <button class="quick-item quick-item-btn" open-type="share" hover-class="quick-item-hover">
<view class="icon-tool i-carbon-share" /> <view class="quick-icon-wrap" style="background: rgba(19,194,94,0.08)">
<view class="grid-text"> <view class="quick-icon i-carbon-share" style="color: #13c25e" />
分享好友
</view> </view>
<text class="quick-name">分享好友</text>
</button> </button>
<!-- #endif --> <!-- #endif -->
<!-- #ifndef MP-WEIXIN --> <!-- #ifndef MP-WEIXIN -->
<view class="grid-item" @tap="goShareFallback"> <view class="quick-item" @tap="goShareFallback">
<view class="icon-tool i-carbon-share" /> <view class="quick-icon-wrap" style="background: rgba(19,194,94,0.08)">
<view class="grid-text"> <view class="quick-icon i-carbon-share" style="color: #13c25e" />
分享好友
</view> </view>
<text class="quick-name">分享好友</text>
</view> </view>
<!-- #endif --> <!-- #endif -->
<view class="grid-item" @tap="openCoopModal"> <view class="quick-item" @tap="openCoopModal">
<view class="icon-tool i-carbon-enterprise" /> <view class="quick-icon-wrap" style="background: rgba(114,46,209,0.08)">
<view class="grid-text"> <view class="quick-icon i-carbon-enterprise" style="color: #722ed1" />
商务合作
</view> </view>
<text class="quick-name">商务合作</text>
</view> </view>
</view> </view>
</view> </view>
<view class="card list-card"> <!-- 区块3: 常用工具 -->
<view class="list-item" @tap="goLegalUserAgreement"> <view class="section">
<view class="list-icon i-carbon-document-blank" /> <view class="section-title">常用工具</view>
<text class="list-text">用户协议</text> <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> </view>
<view class="list-item" @tap="goLegalPrivacyPolicy"> <text class="tool-row-name">实时油价查询</text>
<view class="list-icon i-carbon-security" /> <text class="tool-row-arrow"></text>
<text class="list-text">隐私政策</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>
<text class="tool-row-name">违章代码查询</text>
<text class="tool-row-arrow"></text>
</view> </view>
<view class="list-item" @tap="goLegalAuthorization">
<view class="list-icon i-carbon-certificate" />
<text class="list-text">授权书</text>
</view> </view>
<view class="list-item" @tap="goHelp">
<view class="list-icon i-carbon-help" />
<text class="list-text">帮助中心</text>
</view> </view>
<!-- 区块4: 服务与支持 -->
<view class="section">
<button <view class="section-title">服务与支持</view>
class="list-item list-item-contact no-border" <view class="tool-list">
open-type="contact" <button class="tool-row tool-row-btn no-border" open-type="contact" hover-class="tool-row-hover">
hover-class="list-item-contact-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 class="list-icon i-carbon-chat" /> </view>
<view class="service-row"> <view class="tool-row-text">
<text class="list-text">在线客服</text> <text class="tool-row-name">在线客服</text>
<text class="service-time">人工客服 周一至周日 9:00-20:00</text> <text class="tool-row-sub">周一至周日 9:00-20:00</text>
</view> </view>
</button> </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" />
</view> </view>
<text class="tool-row-name">帮助中心</text>
<text class="tool-row-arrow"></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" />
</view>
<text class="tool-row-name">关于我们</text>
<text class="tool-row-arrow"></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>
<text class="tool-row-name">用户协议</text>
<text class="tool-row-arrow"></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>
<text class="tool-row-name">隐私政策</text>
<text class="tool-row-arrow"></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>
<text class="tool-row-name">授权书</text>
<text class="tool-row-arrow"></text>
</view>
</view>
</view>
<view style="height: 40rpx" />
</view> </view>
</scroll-view> </scroll-view>
@@ -531,7 +571,7 @@ onUnmounted(() => {
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: linear-gradient(180deg, #f8faff 0%, #f3f5fb 100%); background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 30%, #f3f5fb 100%);
} }
.scrollarea { .scrollarea {
@@ -541,143 +581,355 @@ onUnmounted(() => {
} }
.page { .page {
padding: 24rpx 24rpx 40rpx; padding: 0 24rpx 40rpx;
box-sizing: border-box; box-sizing: border-box;
} }
.banner { /* ═══ 区块1: 个人信息栏 ═══ */
display: flex; .profile-card {
padding: 32rpx 28rpx; position: relative;
border-radius: 0 0 32rpx 32rpx;
overflow: hidden;
margin-bottom: 24rpx; margin-bottom: 24rpx;
background: linear-gradient(135deg, #fff7f0 0%, #ffffff 100%);
border-radius: 24rpx;
box-shadow:
0 18rpx 40rpx rgba(15, 35, 52, 0.05),
0 0 0 1rpx rgba(226, 229, 239, 0.9);
} }
.banner-text { .profile-bg {
flex: 1; position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: linear-gradient(135deg, #1768ff 0%, #4d94ff 60%, #7bb8ff 100%);
} }
.banner-title { .profile-content {
font-size: 32rpx; position: relative;
font-weight: 600;
color: #1d2129;
margin-bottom: 8rpx;
}
.banner-sub {
font-size: 24rpx;
color: #4e5969;
}
.banner-illustration {
width: 180rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; padding: 48rpx 32rpx 32rpx;
gap: 20rpx;
} }
.car-illus { .profile-avatar {
width: 160rpx;
height: 120rpx;
border-radius: 16rpx;
background: linear-gradient(145deg, #ffe8dc 0%, #ffc9a8 100%);
opacity: 0.95;
}
.card {
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
padding: 24rpx 24rpx 20rpx;
margin-bottom: 24rpx;
border: 1rpx solid #e5e6f0;
box-shadow:
0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
}
.mine-user {
display: flex;
flex-direction: column;
align-items: stretch;
}
.mine-user-main {
display: flex;
align-items: center;
}
.mine-avatar-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: #f2f3f5;
margin-right: 20rpx;
flex-shrink: 0; flex-shrink: 0;
} }
.mine-user-text { .avatar-inner {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
border: 3rpx solid rgba(255, 255, 255, 0.4);
}
.avatar-icon {
font-size: 44rpx;
color: #fff;
}
.profile-info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.mine-user-name { .profile-name {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #fff;
margin-bottom: 6rpx;
}
.profile-desc {
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.8);
}
.profile-arrow {
flex-shrink: 0;
color: rgba(255, 255, 255, 0.6);
font-size: 28rpx; font-size: 28rpx;
}
.profile-actions {
position: relative;
display: flex;
gap: 24rpx;
padding: 0 32rpx 28rpx;
}
.action-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 24rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.18);
}
.action-icon {
font-size: 24rpx;
color: #fff;
}
.action-text {
font-size: 22rpx;
color: #fff;
}
/* ═══ 区块通用 ═══ */
.section {
background: #fff;
border-radius: 24rpx;
padding: 28rpx 24rpx 8rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 35, 52, 0.03);
}
.section-title {
font-size: 26rpx;
font-weight: 600;
color: #1d2129; color: #1d2129;
margin-bottom: 4rpx; margin-bottom: 20rpx;
padding-left: 4rpx;
} }
.mine-user-desc { /* ═══ 区块2: 快捷功能4宫格 ═══ */
font-size: 22rpx; .quick-grid {
color: #86909c;
}
.mine-user-extra {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
font-size: 22rpx;
color: #86909c;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding-bottom: 16rpx;
}
.quick-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center; align-items: center;
padding: 12rpx 0;
} }
.extra-link { .quick-item-btn {
color: #1768ff; margin: 0;
padding: 12rpx 0;
border: none;
background: transparent;
line-height: normal;
font-size: inherit;
color: inherit;
box-sizing: border-box;
} }
.extra-dot { .quick-item-btn::after {
margin: 0 8rpx; display: none;
}
.quick-item-hover {
opacity: 0.7;
}
.quick-icon-wrap {
width: 80rpx;
height: 80rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.quick-icon {
font-size: 36rpx;
}
.quick-name {
font-size: 22rpx;
color: #4e5969;
text-align: center;
}
/* ═══ 区块3/4/5: 列表行 ═══ */
.tool-list {
padding-bottom: 8rpx;
}
.tool-row {
display: flex;
align-items: center;
padding: 20rpx 4rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.tool-row:last-child {
border-bottom: none;
}
.tool-row:active {
background: #fafbfc;
border-radius: 12rpx;
}
.tool-row-btn {
margin: 0;
padding: 20rpx 4rpx;
border: none;
border-radius: 0;
background: transparent;
text-align: left;
line-height: inherit;
font-size: inherit;
color: inherit;
box-sizing: border-box;
width: 100%;
border-bottom: 1rpx solid #f5f5f5;
}
.tool-row-btn::after {
display: none;
}
.tool-row-hover {
background: rgba(23, 104, 255, 0.04);
}
.tool-row-icon-wrap {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
}
.tool-row-icon {
font-size: 28rpx;
}
.tool-row-text {
flex: 1;
min-width: 0;
}
.tool-row-name {
flex: 1;
font-size: 26rpx;
color: #1d2129;
}
.tool-row-sub {
display: block;
font-size: 20rpx;
color: #ffb020;
margin-top: 2rpx;
}
.tool-row-arrow {
font-size: 28rpx;
color: #c9cdd4;
flex-shrink: 0;
margin-left: 8rpx;
}
.no-border {
border-bottom: none !important;
}
/* ═══ 弹窗样式 ═══ */
.coop-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
box-sizing: border-box;
}
.coop-dialog {
width: 100%;
max-width: 600rpx;
background: #fff;
border-radius: 24rpx;
padding: 36rpx 32rpx 28rpx;
box-sizing: border-box;
}
.coop-title {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
text-align: center;
margin-bottom: 12rpx;
}
.coop-hint {
font-size: 24rpx;
color: #86909c;
text-align: center;
margin-bottom: 28rpx;
line-height: 1.5;
}
.coop-qr {
display: block;
width: 360rpx;
height: 360rpx;
margin: 0 auto 28rpx;
border-radius: 12rpx;
background: #f7f8fa;
}
.coop-qr-placeholder {
width: 360rpx;
height: 360rpx;
margin: 0 auto 28rpx;
border-radius: 12rpx;
border: 2rpx dashed #dcdfe6;
background: #fafbfc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
box-sizing: border-box;
gap: 12rpx;
}
.coop-qr-placeholder-text {
font-size: 22rpx;
color: #86909c;
line-height: 1.6;
text-align: center;
}
.coop-qr-placeholder-sub {
font-size: 20rpx;
color: #c9cdd4; color: #c9cdd4;
} }
.extra-hint { .coop-close {
color: #86909c; height: 80rpx;
} line-height: 80rpx;
text-align: center;
.mine-bind-row { background: linear-gradient(90deg, #1768ff 0%, #4d94ff 100%);
margin-top: 12rpx; color: #fff;
padding: 16rpx 0 4rpx; font-size: 28rpx;
display: flex; font-weight: 600;
align-items: center; border-radius: 40rpx;
justify-content: space-between;
}
.mine-bind-text {
font-size: 26rpx;
color: #1768ff;
font-weight: 500;
}
.mine-bind-arrow {
font-size: 36rpx;
color: #1768ff;
line-height: 1;
} }
/* ═══ 绑定手机弹窗 ═══ */
.bind-mask { .bind-mask {
position: fixed; position: fixed;
left: 0; left: 0;
@@ -785,198 +1037,4 @@ onUnmounted(() => {
color: #86909c; color: #86909c;
padding: 12rpx; padding: 12rpx;
} }
.grid-4 {
display: flex;
justify-content: space-between;
}
.grid-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 8rpx;
}
.grid-item-btn {
margin: 0;
padding: 8rpx 0 0;
border: none;
background: transparent;
line-height: normal;
font-size: inherit;
color: inherit;
box-sizing: border-box;
}
.grid-item-btn::after {
display: none;
}
.grid-item-hover {
opacity: 0.88;
}
.coop-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
box-sizing: border-box;
}
.coop-dialog {
width: 100%;
max-width: 600rpx;
background: #fff;
border-radius: 24rpx;
padding: 36rpx 32rpx 28rpx;
box-sizing: border-box;
}
.coop-title {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
text-align: center;
margin-bottom: 12rpx;
}
.coop-hint {
font-size: 24rpx;
color: #86909c;
text-align: center;
margin-bottom: 28rpx;
line-height: 1.5;
}
.coop-qr {
display: block;
width: 360rpx;
height: 360rpx;
margin: 0 auto 28rpx;
border-radius: 12rpx;
background: #f7f8fa;
}
.coop-qr-placeholder {
width: 360rpx;
height: 360rpx;
margin: 0 auto 28rpx;
border-radius: 12rpx;
border: 2rpx dashed #dcdfe6;
background: #fafbfc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
box-sizing: border-box;
gap: 12rpx;
}
.coop-qr-placeholder-text {
font-size: 22rpx;
color: #86909c;
line-height: 1.6;
text-align: center;
}
.coop-qr-placeholder-sub {
font-size: 20rpx;
color: #c9cdd4;
}
.coop-close {
height: 80rpx;
line-height: 80rpx;
text-align: center;
background: linear-gradient(90deg, #1768ff 0%, #4d94ff 100%);
color: #fff;
font-size: 28rpx;
font-weight: 600;
border-radius: 40rpx;
}
.icon-tool {
width: 48rpx;
height: 48rpx;
margin-bottom: 8rpx;
color: #1768ff;
}
.grid-text {
font-size: 24rpx;
color: #4e5969;
}
.list-card {
padding: 0 24rpx;
}
.list-item {
height: 96rpx;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f0f0f0;
}
.list-item-contact {
width: 100%;
margin: 0;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
text-align: left;
line-height: inherit;
font-size: inherit;
color: inherit;
box-sizing: border-box;
}
.list-item-contact::after {
display: none;
}
.list-item-contact-hover {
background: rgba(23, 104, 255, 0.06);
}
.list-icon {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
flex-shrink: 0;
color: #1768ff;
}
.no-border {
border-bottom-width: 0;
}
.list-text {
font-size: 26rpx;
color: #1d2129;
}
.service-row {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.service-time {
font-size: 22rpx;
color: #ffb020;
}
</style> </style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { toolboxCategories, getCategoryAllTools } from '@/config/toolboxRegistry' import { toolboxCategories, toolboxItems, getCategoryAllTools } from '@/config/toolboxRegistry'
definePage({ definePage({
style: { style: {
@@ -15,11 +15,44 @@ definePage({
const categoryKey = ref('') const categoryKey = ref('')
const category = ref<any>(null) const category = ref<any>(null)
const tools = ref<any[]>([]) const tools = ref<any[]>([])
const isAllCategories = ref(false)
/** 当前选中的 tab 索引 */
const activeTab = ref(0)
/** 搜索关键词 */
const searchKeyword = ref('')
/** 当前选中分类的完整数据 */
const activeCategory = computed(() => toolboxCategories[activeTab.value] || toolboxCategories[0])
/** 当前选中分类的全部工具 */
const activeTools = computed(() => getCategoryAllTools(activeCategory.value.key))
/** 是否正在搜索(有关键词) */
const isSearching = computed(() => searchKeyword.value.trim().length > 0)
/** 搜索结果:从所有工具中匹配名称 / 描述 / key */
const searchResults = computed(() => {
const kw = searchKeyword.value.trim().toLowerCase()
if (!kw) return []
return toolboxItems.filter(item =>
item.name.toLowerCase().includes(kw)
|| item.desc.toLowerCase().includes(kw)
|| item.key.toLowerCase().includes(kw),
)
})
onLoad((query) => { onLoad((query) => {
const key = (query?.category as string) || '' const key = (query?.category as string) || ''
categoryKey.value = key categoryKey.value = key
if (key === 'all') {
isAllCategories.value = true
uni.setNavigationBarTitle({ title: '全部分类' })
return
}
const cat = toolboxCategories.find(c => c.key === key) const cat = toolboxCategories.find(c => c.key === key)
if (cat) { if (cat) {
category.value = cat category.value = cat
@@ -28,6 +61,15 @@ onLoad((query) => {
} }
}) })
function switchTab(idx: number) {
activeTab.value = idx
searchKeyword.value = ''
}
function clearSearch() {
searchKeyword.value = ''
}
function goTool(key: string) { function goTool(key: string) {
uni.navigateTo({ uni.navigateTo({
url: `/pages/toolbox/query?key=${encodeURIComponent(key)}`, url: `/pages/toolbox/query?key=${encodeURIComponent(key)}`,
@@ -37,9 +79,109 @@ function goTool(key: string) {
<template> <template>
<view class="page-root"> <view class="page-root">
<!-- 全部分类视图 -->
<template v-if="isAllCategories">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrap">
<view class="search-icon i-carbon-search" />
<input
class="search-input"
type="text"
placeholder="搜索工具名称"
placeholder-class="search-placeholder"
:value="searchKeyword"
confirm-type="search"
@input="searchKeyword = $event.detail.value"
/>
<view v-if="isSearching" class="search-clear i-carbon-close-filled" @tap="clearSearch" />
</view>
</view>
<!-- Tab 搜索时隐藏 -->
<view v-show="!isSearching" class="tab-bar-wrap">
<scroll-view scroll-x class="tab-scroll" :scroll-into-view="'tab-' + activeTab" :scroll-with-animation="true">
<view class="tab-bar">
<view
v-for="(cat, idx) in toolboxCategories"
:id="'tab-' + idx"
:key="cat.key"
class="tab-item"
:class="{ 'tab-item--active': activeTab === idx }"
@tap="switchTab(idx)"
>
<text class="tab-text" :class="{ 'tab-text--active': activeTab === idx }">{{ cat.name }}</text>
</view>
</view>
</scroll-view>
<view class="tab-indicator-track">
<view
class="tab-indicator"
:style="{ width: `${100 / toolboxCategories.length}%`, transform: `translateX(${activeTab * 100}%)` }"
/>
</view>
</view>
<!-- 搜索结果 / 工具网格 -->
<scroll-view scroll-y class="tool-scrollarea">
<view class="tool-grid-page">
<!-- 搜索结果视图 -->
<template v-if="isSearching">
<view v-if="searchResults.length === 0" class="search-empty">
<view class="search-empty-icon i-carbon-search" />
<text class="search-empty-text">未找到相关工具</text>
</view>
<template v-else>
<view class="grid-header">
<text class="grid-header-name">搜索结果</text>
<text class="grid-header-count"> {{ searchResults.length }} </text>
</view>
<view class="tool-grid">
<view
v-for="item in searchResults"
:key="item.key"
class="tool-cell"
@tap="goTool(item.key)"
>
<view class="tool-cell-icon-wrap" style="background: #eef4ff">
<view :class="['tool-cell-icon', item.icon]" style="color: #1768ff" />
</view>
<text class="tool-cell-name">{{ item.name }}</text>
</view>
</view>
</template>
</template>
<!-- 正常 Tab 工具网格 -->
<template v-else>
<view class="grid-header">
<view class="grid-header-dot" :style="{ background: activeCategory.color }" />
<text class="grid-header-name">{{ activeCategory.name }}</text>
<text class="grid-header-count"> {{ activeTools.length }} </text>
</view>
<view class="tool-grid">
<view
v-for="item in activeTools"
:key="item.key"
class="tool-cell"
@tap="goTool(item.key)"
>
<view class="tool-cell-icon-wrap" :style="{ background: `${activeCategory.color}12` }">
<view :class="['tool-cell-icon', item.icon]" :style="{ color: activeCategory.color }" />
</view>
<text class="tool-cell-name">{{ item.name }}</text>
</view>
</view>
</template>
</view>
</scroll-view>
</template>
<!-- 单分类视图 -->
<template v-else-if="category">
<scroll-view scroll-y class="scrollarea"> <scroll-view scroll-y class="scrollarea">
<view class="page"> <view class="page">
<view v-if="category" class="cat-header"> <view class="cat-header">
<view class="cat-icon-large" :style="{ background: `${category.color}15` }"> <view class="cat-icon-large" :style="{ background: `${category.color}15` }">
<view :class="['icon', category.icon]" :style="{ color: category.color }" /> <view :class="['icon', category.icon]" :style="{ color: category.color }" />
</view> </view>
@@ -63,11 +205,11 @@ function goTool(key: string) {
<text class="item-name">{{ item.name }}</text> <text class="item-name">{{ item.name }}</text>
<text class="item-desc">{{ item.desc }}</text> <text class="item-desc">{{ item.desc }}</text>
</view> </view>
<!-- <text class="item-arrow"></text> -->
</view> </view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
</template>
</view> </view>
</template> </template>
@@ -79,6 +221,204 @@ function goTool(key: string) {
background: linear-gradient(180deg, #f8faff 0%, #f3f5fb 100%); background: linear-gradient(180deg, #f8faff 0%, #f3f5fb 100%);
} }
/* ============ 搜索栏 ============ */
.search-bar {
padding: 16rpx 24rpx 12rpx;
background: #fff;
flex-shrink: 0;
}
.search-input-wrap {
display: flex;
align-items: center;
height: 72rpx;
background: #f5f6fa;
border-radius: 36rpx;
padding: 0 24rpx;
gap: 12rpx;
}
.search-icon {
font-size: 30rpx;
color: #c0c4cc;
flex-shrink: 0;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #1d2129;
height: 72rpx;
line-height: 72rpx;
}
.search-placeholder {
color: #c0c4cc;
font-size: 26rpx;
}
.search-clear {
font-size: 30rpx;
color: #c0c4cc;
flex-shrink: 0;
padding: 8rpx;
}
.search-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0 80rpx;
}
.search-empty-icon {
font-size: 80rpx;
color: #dcdfe6;
margin-bottom: 20rpx;
}
.search-empty-text {
font-size: 26rpx;
color: #86909c;
}
/* ============ Tab 栏 ============ */
.tab-bar-wrap {
background: #fff;
flex-shrink: 0;
border-bottom: 1rpx solid #f0f1f5;
}
.tab-scroll {
width: 100%;
white-space: nowrap;
}
.tab-bar {
display: flex;
height: 88rpx;
line-height: 88rpx;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8rpx;
position: relative;
}
.tab-text {
font-size: 28rpx;
color: #86909c;
transition: color 0.25s, font-weight 0.25s;
}
.tab-text--active {
color: #1d2129;
font-weight: 600;
}
/* 下划线指示器 */
.tab-indicator-track {
height: 6rpx;
position: relative;
background: transparent;
}
.tab-indicator {
position: absolute;
top: 0;
left: 0;
height: 6rpx;
border-radius: 3rpx;
background: #1768ff;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* ============ 工具网格区域 ============ */
.tool-scrollarea {
flex: 1;
min-height: 0;
}
.tool-grid-page {
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
}
.grid-header {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 24rpx;
padding: 0 4rpx;
}
.grid-header-dot {
width: 12rpx;
height: 12rpx;
border-radius: 4rpx;
}
.grid-header-name {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.grid-header-count {
font-size: 22rpx;
color: #86909c;
margin-left: auto;
}
/* 4列网格 */
.tool-grid {
display: flex;
flex-wrap: wrap;
}
.tool-cell {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 4rpx;
box-sizing: border-box;
}
.tool-cell:active {
opacity: 0.7;
}
.tool-cell-icon-wrap {
width: 80rpx;
height: 80rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.tool-cell-icon {
font-size: 38rpx;
}
.tool-cell-name {
font-size: 22rpx;
color: #333;
text-align: center;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
/* ============ 单分类详情 ============ */
.scrollarea { .scrollarea {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -99,8 +439,7 @@ function goTool(key: string) {
border-radius: 24rpx; border-radius: 24rpx;
border: 1rpx solid #e5e6f0; border: 1rpx solid #e5e6f0;
margin-bottom: 24rpx; margin-bottom: 24rpx;
box-shadow: box-shadow: 0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset; 0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
} }
@@ -140,8 +479,7 @@ function goTool(key: string) {
border-radius: 24rpx; border-radius: 24rpx;
border: 1rpx solid #e5e6f0; border: 1rpx solid #e5e6f0;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow: 0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset; 0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
} }
@@ -196,10 +534,4 @@ function goTool(key: string) {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.item-arrow {
font-size: 24rpx;
color: #c9cdd4;
flex-shrink: 0;
}
</style> </style>

View File

@@ -1,69 +1,186 @@
<script setup lang="ts"> <script setup lang="ts">
import { toolboxCategories, getCategoryHotTools } from '@/config/toolboxRegistry' import { ref, computed } from 'vue'
import { toolboxCategories, getCategoryAllTools } from '@/config/toolboxRegistry'
import { getInquireCategoryConfig, getInquiryItemIconUrl } from '@/config/inquireCategories'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '实用工具', navigationBarTitleText: '工具分类',
navigationStyle: 'default', navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff', navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black', navigationBarTextStyle: 'black',
}, },
}) })
function goTool(key: string) { const vehicleItems = computed(() => getInquireCategoryConfig('vehicle')?.items ?? [])
uni.navigateTo({
url: `/pages/toolbox/query?key=${encodeURIComponent(key)}`, /** 左侧分类列表:热门推荐 + 车辆查询 + 所有工具分类 */
}) const sidebarItems = computed(() => [
{ key: 'hot', name: '热门推荐', icon: '', color: '#ff6b6b' },
{ key: 'vehicle', name: '车辆查询', icon: 'i-carbon-car', color: '#1768ff' },
...toolboxCategories.map(cat => ({
key: cat.key,
name: cat.name,
icon: cat.icon,
color: cat.color,
})),
])
const activeKey = ref('hot')
/** 当前选中分类的数据(仅工具分类有) */
const activeCategory = computed(() => {
if (activeKey.value === 'hot' || activeKey.value === 'vehicle') return null
return toolboxCategories.find(c => c.key === activeKey.value) || null
})
/** 当前分类的工具列表 */
const activeTools = computed(() => {
if (activeKey.value === 'hot' || activeKey.value === 'vehicle') return []
return getCategoryAllTools(activeKey.value)
})
/** 热门推荐的分类卡片:车辆查询 + 所有工具分类 */
const hotCategoryCards = computed(() => [
{ key: 'vehicle', name: '车辆查询', icon: 'i-carbon-car', color: '#1768ff', count: vehicleItems.value.length, previews: [] },
...toolboxCategories.map(cat => ({
key: cat.key,
name: cat.name,
icon: cat.icon,
color: cat.color,
count: getCategoryAllTools(cat.key).length,
previews: getCategoryAllTools(cat.key).slice(0, 4),
})),
])
function switchCategory(key: string) {
activeKey.value = key
} }
function goCategory(categoryKey: string) { function goTool(key: string) {
uni.navigateTo({ uni.navigateTo({ url: `/pages/toolbox/query?key=${encodeURIComponent(key)}` })
url: `/pages/toolbox/category?category=${encodeURIComponent(categoryKey)}`, }
})
function goInquireFeature(feature: string) {
uni.navigateTo({ url: `/pages/inquire/index?feature=${encodeURIComponent(feature)}` })
} }
</script> </script>
<template> <template>
<view class="page-root"> <view class="page-root">
<scroll-view scroll-y class="scrollarea"> <view class="layout">
<view class="page"> <!-- 左侧分类导航 -->
<scroll-view scroll-y class="sidebar">
<view <view
v-for="cat in toolboxCategories" v-for="item in sidebarItems"
:key="cat.key" :key="item.key"
class="card" class="sidebar-item"
:class="{ 'sidebar-item--active': activeKey === item.key }"
@tap="switchCategory(item.key)"
> >
<!-- 分类标题 --> <view v-if="activeKey === item.key" class="sidebar-indicator" />
<view class="card-header" @tap="goCategory(cat.key)"> <text class="sidebar-text" :class="{ 'sidebar-text--active': activeKey === item.key }">{{ item.name }}</text>
<view class="card-header-left">
<view class="cat-icon-wrap" :style="{ background: `${cat.color}15` }">
<view :class="['cat-icon', cat.icon]" :style="{ color: cat.color }" />
</view>
<text class="card-title">{{ cat.name }}</text>
</view>
<view class="card-more">
<text class="more-text">查看更多</text>
<!-- <text class="more-arrow">></text> -->
</view>
</view> </view>
</scroll-view>
<!-- 热门工具网格 --> <!-- 右侧内容区 -->
<scroll-view scroll-y class="content" :scroll-top="0">
<!-- 热门推荐分类卡片网格 -->
<template v-if="activeKey === 'hot'">
<view class="content-header">
<view class="content-icon-wrap" style="background: rgba(255,107,107,0.1)">
<view class="content-icon i-carbon-grid" style="color: #ff6b6b" />
</view>
<view class="content-info">
<text class="content-title">热门推荐</text>
<text class="content-count">全部分类入口</text>
</view>
</view>
<view class="cat-grid">
<view
v-for="card in hotCategoryCards"
:key="card.key"
class="cat-card"
@tap="switchCategory(card.key)"
>
<!-- 车辆查询用单图标 -->
<view v-if="card.key === 'vehicle'" class="cat-card-single-icon" :style="{ background: `${card.color}12` }">
<view :class="card.icon" :style="{ color: card.color, fontSize: '28rpx' }" />
</view>
<!-- 工具分类用四宫格 -->
<view v-else class="cat-card-grid-icon" :style="{ background: `${card.color}10` }">
<view
v-for="(preview, pIdx) in card.previews"
:key="preview.key"
class="mini-icon-cell"
>
<view :class="[preview.icon, 'mini-icon']" :style="{ color: card.color }" />
</view>
</view>
<view class="cat-card-text">
<text class="cat-card-name">{{ card.name }}</text>
<text class="cat-card-count">{{ card.count }}</text>
</view>
</view>
</view>
</template>
<!-- 车辆查询 -->
<template v-else-if="activeKey === 'vehicle'">
<view class="content-header">
<view class="content-icon-wrap" style="background: rgba(23,104,255,0.1)">
<view class="content-icon i-carbon-car" style="color: #1768ff" />
</view>
<view class="content-info">
<text class="content-title">车辆查询服务</text>
<text class="content-count">{{ vehicleItems.length }} 项专业车况核验</text>
</view>
</view>
<view class="tool-grid"> <view class="tool-grid">
<view <view
v-for="item in getCategoryHotTools(cat.key)" v-for="item in vehicleItems"
:key="item.key" :key="item.feature"
class="tool-cell" class="tool-cell"
@tap="goTool(item.key)" @tap="goInquireFeature(item.feature)"
> >
<view class="tool-icon-wrap" :style="{ background: `${cat.color}12` }"> <view class="tool-icon-wrap" style="background: rgba(23,104,255,0.08)">
<view :class="['tool-icon', item.icon]" :style="{ color: cat.color }" /> <image class="tool-icon-img" :src="getInquiryItemIconUrl(item)" mode="aspectFit" />
</view> </view>
<text class="tool-name">{{ item.name }}</text> <text class="tool-name">{{ item.name }}</text>
</view> </view>
</view> </view>
</template>
<!-- 工具分类 -->
<template v-else-if="activeCategory">
<view class="content-header">
<view class="content-icon-wrap" :style="{ background: `${activeCategory.color}15` }">
<view :class="['content-icon', activeCategory.icon]" :style="{ color: activeCategory.color }" />
</view>
<view class="content-info">
<text class="content-title">{{ activeCategory.name }}</text>
<text class="content-count">{{ activeTools.length }} 个工具</text>
</view> </view>
</view> </view>
<view class="tool-grid">
<view
v-for="item in activeTools"
:key="item.key"
class="tool-cell"
@tap="goTool(item.key)"
>
<view class="tool-icon-wrap" :style="{ background: `${activeCategory.color}12` }">
<view :class="['tool-icon-grid', item.icon]" :style="{ color: activeCategory.color }" />
</view>
<text class="tool-name">{{ item.name }}</text>
</view>
</view>
</template>
<view style="height: 32rpx" />
</scroll-view> </scroll-view>
</view> </view>
</view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -71,101 +188,201 @@ function goCategory(categoryKey: string) {
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: linear-gradient(180deg, #f8faff 0%, #f3f5fb 100%); background: #f5f6fa;
} }
.scrollarea { /* ============ 双栏布局 ============ */
.layout {
flex: 1; flex: 1;
min-height: 0;
height: 0;
}
.page {
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
}
.card {
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
padding: 24rpx 24rpx 20rpx;
margin-bottom: 24rpx;
border: 1rpx solid #e5e6f0;
box-shadow:
0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
}
.card-header {
display: flex; display: flex;
justify-content: space-between; min-height: 0;
align-items: center;
margin-bottom: 20rpx;
} }
.card-header-left { /* 左侧分类导航 */
.sidebar {
width: 180rpx;
height: 100%;
background: #f0f1f5;
flex-shrink: 0;
}
.sidebar-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 104rpx;
padding: 0 8rpx;
}
.sidebar-item:active {
background: #e8e9ed;
}
.sidebar-item--active {
background: #fff;
}
.sidebar-indicator {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 40rpx;
border-radius: 0 4rpx 4rpx 0;
background: #1768ff;
}
.sidebar-text {
font-size: 24rpx;
color: #86909c;
text-align: center;
line-height: 1.3;
}
.sidebar-text--active {
color: #1d2129;
font-weight: 600;
}
/* 右侧内容区 */
.content {
flex: 1;
height: 100%;
background: #fff;
}
.content-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; gap: 16rpx;
padding: 28rpx 28rpx 16rpx;
} }
.cat-icon-wrap { .content-icon-wrap {
width: 56rpx; width: 52rpx;
height: 56rpx; height: 52rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.content-icon {
font-size: 28rpx;
}
.content-info {
flex: 1;
}
.content-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 4rpx;
}
.content-count {
display: block;
font-size: 20rpx;
color: #86909c;
}
/* ============ 热门推荐:分类卡片网格 ============ */
.cat-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14rpx;
padding: 8rpx 20rpx;
}
.cat-card {
display: flex;
flex-direction: row;
align-items: center;
gap: 14rpx;
padding: 20rpx 16rpx;
border-radius: 16rpx; border-radius: 16rpx;
background: #f7f8fa;
}
.cat-card:active {
background: #eef1f5;
}
/* 车辆查询单图标 */
.cat-card-single-icon {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 四宫格图标 */
.cat-card-grid-icon {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
overflow: hidden;
flex-shrink: 0;
}
.mini-icon-cell {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.cat-icon { .mini-icon {
font-size: 32rpx; font-size: 16rpx;
} }
.card-title { .cat-card-text {
font-size: 30rpx; flex: 1;
font-weight: 600; min-width: 0;
color: #1d2129;
} }
.card-more { .cat-card-name {
display: flex; display: block;
align-items: center;
gap: 4rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
background: #f2f3f5;
}
.more-text {
font-size: 22rpx; font-size: 22rpx;
font-weight: 500;
color: #1d2129;
margin-bottom: 2rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cat-card-count {
display: block;
font-size: 18rpx;
color: #86909c; color: #86909c;
} }
.more-arrow { /* ============ 工具网格 ============ */
font-size: 22rpx;
color: #c9cdd4;
}
.card-more:active {
opacity: 0.7;
}
.tool-grid { .tool-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0 -6rpx; padding: 0 16rpx;
} }
.tool-cell { .tool-cell {
width: 33.333%; width: 25%;
padding: 6rpx;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding-top: 12rpx; padding: 16rpx 0;
padding-bottom: 12rpx; box-sizing: border-box;
} }
.tool-cell:active { .tool-cell:active {
@@ -173,27 +390,33 @@ function goCategory(categoryKey: string) {
} }
.tool-icon-wrap { .tool-icon-wrap {
width: 76rpx; width: 68rpx;
height: 76rpx; height: 68rpx;
border-radius: 20rpx; border-radius: 18rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 10rpx; margin-bottom: 8rpx;
} }
.tool-icon { .tool-icon-grid {
font-size: 36rpx; font-size: 34rpx;
}
.tool-icon-img {
width: 38rpx;
height: 38rpx;
} }
.tool-name { .tool-name {
font-size: 24rpx; font-size: 20rpx;
font-weight: 500; color: #333;
color: #1d2129;
text-align: center; text-align: center;
line-height: 1.3;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
padding: 0 4rpx;
} }
</style> </style>

View File

@@ -253,6 +253,14 @@ async function handleQuery() {
if (toolKey.value === 'idiom-quiz' || toolKey.value === 'poem-fill') { if (toolKey.value === 'idiom-quiz' || toolKey.value === 'poem-fill') {
prepareGameOptions() prepareGameOptions()
} }
// 百科题库:准备选项
if (toolKey.value === 'baiketiku') {
prepareBaiKeOptions()
}
// 垃圾分类问答:准备选项
if (toolKey.value === 'anslajifenlei') {
prepareGarbageOptions()
}
// 成语接龙:记录系统返回的成语 // 成语接龙:记录系统返回的成语
if (toolKey.value === 'chengyujielong') { if (toolKey.value === 'chengyujielong') {
if (result.value?.word) { if (result.value?.word) {
@@ -354,6 +362,59 @@ const prepareGameOptions = () => {
answered.value = false answered.value = false
} }
// 百科题库准备选项A/B/C/D 四个选项,标记正确答案)
const prepareBaiKeOptions = () => {
if (!result.value)
return
// answer 可能是选项字母标识(如 "A"),也可能是选项文本内容
const correctAnswer = String(result.value.answer || '').trim()
const optionKeys = ['answerA', 'answerB', 'answerC', 'answerD'] as const
const optionLabels = ['A', 'B', 'C', 'D'] as const
const options = optionKeys
.map((key, i) => {
const val = String(result.value[key] || '').trim()
if (!val) return null
// answer 可能是字母标识 "A"/"B"/...,也可能是选项文本本身
const isCorrect = correctAnswer === optionLabels[i] || correctAnswer === val
return { label: `${optionLabels[i]}. ${val}`, value: val, isCorrect }
})
.filter(Boolean) as Array<{ label: string; value: string; isCorrect: boolean }>
gameOptions.value = options
selectedOption.value = null
answered.value = false
}
// 垃圾分类问答:准备选项(四个分类,标记正确答案)
const prepareGarbageOptions = () => {
if (!result.value)
return
const correctType = result.value.type
const typeNames: Record<number, string> = {
0: '可回收物',
1: '有害垃圾',
2: '厨余垃圾',
3: '其他垃圾',
}
const options = Object.entries(typeNames).map(([typeKey, typeName]) => ({
label: typeName,
value: typeName,
isCorrect: Number(typeKey) === Number(correctType),
}))
// 打乱顺序
for (let i = options.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[options[i], options[j]] = [options[j], options[i]]
}
gameOptions.value = options
selectedOption.value = null
answered.value = false
}
// 点击选项 // 点击选项
function handleOptionClick(option: { label: string; value: string; isCorrect: boolean }) { function handleOptionClick(option: { label: string; value: string; isCorrect: boolean }) {
if (answered.value) if (answered.value)
@@ -538,11 +599,15 @@ function handleRestart() {
<!-- 题目 --> <!-- 题目 -->
<view class="game-question"> <view class="game-question">
<text class="game-question-text">{{ result.question }}</text> <text v-if="toolKey === 'anslajifenlei'" class="game-question-text game-question-garbage">
<text class="garbage-name">{{ result.name }}</text>
<text class="garbage-prompt">属于什么垃圾</text>
</text>
<text v-else class="game-question-text">{{ result.title || result.question }}</text>
</view> </view>
<!-- 选项按钮 --> <!-- 选项按钮 -->
<view class="game-options"> <view class="game-options" :class="{ 'game-options-col': toolKey === 'anslajifenlei' }">
<view <view
v-for="(option, index) in gameOptions" v-for="(option, index) in gameOptions"
:key="index" :key="index"
@@ -551,6 +616,10 @@ function handleRestart() {
'game-option-selected': selectedOption === option.value, 'game-option-selected': selectedOption === option.value,
'game-option-correct': answered && option.isCorrect, 'game-option-correct': answered && option.isCorrect,
'game-option-wrong': answered && selectedOption === option.value && !option.isCorrect, 'game-option-wrong': answered && selectedOption === option.value && !option.isCorrect,
'game-option-recyclable': toolKey === 'anslajifenlei' && option.value === '可回收物',
'game-option-hazardous': toolKey === 'anslajifenlei' && option.value === '有害垃圾',
'game-option-kitchen': toolKey === 'anslajifenlei' && option.value === '厨余垃圾',
'game-option-other': toolKey === 'anslajifenlei' && option.value === '其他垃圾',
}" }"
@tap="handleOptionClick(option)" @tap="handleOptionClick(option)"
> >
@@ -610,6 +679,30 @@ function handleRestart() {
<text class="game-explan-value">{{ result.note }}</text> <text class="game-explan-value">{{ result.note }}</text>
</view> </view>
</template> </template>
<!-- 百科题库显示正确答案和解析 -->
<template v-else-if="toolKey === 'baiketiku'">
<view class="game-answer">
<text class="game-answer-label">正确答案</text>
<text class="game-answer-value">{{ result.answer }}</text>
</view>
<view v-if="result.analytic" class="game-explan">
<text class="game-explan-label">解析</text>
<text class="game-explan-value">{{ result.analytic }}</text>
</view>
</template>
<!-- 垃圾分类问答显示正确分类 -->
<template v-else-if="toolKey === 'anslajifenlei'">
<view class="game-answer">
<text class="game-answer-label">正确分类</text>
<text class="game-answer-value garbage-answer-type">{{ result.type_name }}</text>
</view>
<view v-if="result.explain" class="game-explan">
<text class="game-explan-label">说明</text>
<text class="game-explan-value">{{ result.explain }}</text>
</view>
</template>
</view> </view>
</template> </template>
@@ -1473,4 +1566,50 @@ function handleRestart() {
.chain-empty-text { .chain-empty-text {
font-size: 24rpx; font-size: 24rpx;
} }
/* 垃圾分类游戏样式 */
.game-question-garbage {
display: flex;
flex-direction: column;
gap: 16rpx;
align-items: center;
}
.garbage-name {
font-size: 56rpx;
font-weight: 700;
color: #1d2129;
}
.garbage-prompt {
font-size: 30rpx;
color: #86909c;
font-weight: 400;
}
.garbage-answer-type {
color: #00b42a;
}
/* 垃圾分类选项布局 - 单列 */
.game-options-col {
grid-template-columns: 1fr;
}
/* 垃圾分类四个类别的默认色 */
.game-option-recyclable {
border-left: 6rpx solid #1768ff;
}
.game-option-hazardous {
border-left: 6rpx solid #f53f3f;
}
.game-option-kitchen {
border-left: 6rpx solid #00b42a;
}
.game-option-other {
border-left: 6rpx solid #86909c;
}
</style> </style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

3
uni-pages.d.ts vendored
View File

@@ -10,6 +10,7 @@ type _LocationUrl =
"/pages/report" | "/pages/report" |
"/pages/inquire/example" | "/pages/inquire/example" |
"/pages/inquire/index" | "/pages/inquire/index" |
"/pages/inquire/list" |
"/pages/legal/authorization" | "/pages/legal/authorization" |
"/pages/legal/privacy-policy" | "/pages/legal/privacy-policy" |
"/pages/legal/user-agreement" | "/pages/legal/user-agreement" |
@@ -24,7 +25,7 @@ interface NavigateToOptions {
interface RedirectToOptions extends NavigateToOptions {} interface RedirectToOptions extends NavigateToOptions {}
interface SwitchTabOptions { interface SwitchTabOptions {
url: "/pages/index" | "/pages/report" | "/pages/mine" url: "/pages/index" | "/pages/toolbox/index" | "/pages/mine"
} }
type ReLaunchOptions = NavigateToOptions | SwitchTabOptions; type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;