first commit

This commit is contained in:
Mrx
2026-05-16 15:47:07 +08:00
commit 03de10f800
146 changed files with 33663 additions and 0 deletions

508
src/pages/index.vue Normal file
View File

@@ -0,0 +1,508 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { getInquireCategoryConfig, getInquiryItemIconUrl } from '@/config/inquireCategories'
import { toolboxCategories, getCategoryHotTools } from '@/config/toolboxRegistry'
definePage({
style: {
navigationBarTitleText: '全能查',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
interface CaseItem {
id: string
tag: string
vin: string
model: string
}
interface ReviewItem {
id: string
name: string
content: string
}
const caseList = ref<CaseItem[]>([
{
id: '1',
tag: '新能源电池查询',
vin: 'LBV8********0981',
model: '某品牌新能源车型',
},
{
id: '2',
tag: '里程异常检测',
vin: 'LSGN********3389',
model: '凯迪拉克 XT5',
},
])
const vehicleItems = computed(() => getInquireCategoryConfig('vehicle')?.items ?? [])
const reviewList = ref<ReviewItem[]>([
{
id: '1',
name: '陈先生',
content: '查完车况再成交,心里更踏实,避免了重大事故车。',
},
{
id: '2',
name: '周女士',
content: '报告内容很详细,维保、出险一目了然,值得推荐。',
},
])
function reviewInitial(name: string) {
return name.slice(0, 1) || '?'
}
function goInquireFeature(feature: string) {
uni.navigateTo({
url: `/pages/inquire/index?feature=${encodeURIComponent(feature)}`,
})
}
/** 左侧大图:车辆出险详版查询 */
function goVinClaim() {
goInquireFeature('toc_VehicleClaimDetail')
}
/** 右上小图:车辆维保详细版查询 */
function goVinMaintain() {
goInquireFeature('toc_VehicleMaintenanceDetail')
}
function goEvHealth() {
uni.showToast({ title: '功能开发中', icon: 'none' })
}
function goToolboxItem(key: string) {
uni.navigateTo({
url: `/pages/toolbox/query?key=${encodeURIComponent(key)}`,
})
}
function goCategory(categoryKey: string) {
uni.navigateTo({
url: `/pages/toolbox/category?category=${encodeURIComponent(categoryKey)}`,
})
}
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<view class="banner">
<image
class="banner-img"
src="/static/home/images/Banner.png"
mode="widthFix"
/>
</view>
<view class="main-query">
<view class="main-left card card-img-wrap" @tap="goVinClaim">
<image
class="main-left-img"
src="/static/home/images/VIN.png"
mode="widthFix"
/>
</view>
<view class="main-right">
<view class="card card-img-wrap small-card-img" @tap="goVinMaintain">
<image
class="main-right-img"
src="/static/home/images/VIN2.png"
mode="aspectFill"
/>
</view>
<view class="card card-img-wrap small-card-img" @tap="goEvHealth">
<image
class="main-right-img"
src="/static/home/images/VIN3.png"
mode="aspectFill"
/>
</view>
</view>
</view>
<view
v-for="cat in toolboxCategories"
:key="cat.key"
class="card"
>
<view class="card-header">
<view class="card-header-left">
<view class="cat-badge" :style="{ background: `${cat.color}15` }">
<view :class="['cat-badge-icon', cat.icon]" :style="{ color: cat.color }" />
</view>
<text class="card-title">{{ cat.name }}</text>
</view>
<text class="card-more-link" @tap="goCategory(cat.key)">
查看更多
</text>
</view>
<view class="inq-grid">
<view
v-for="item in getCategoryHotTools(cat.key)"
:key="item.key"
class="inq-cell"
@tap="goToolboxItem(item.key)"
>
<view class="inq-icon-custom" :style="{ color: cat.color }">
<view :class="item.icon" />
</view>
<text class="inq-name">{{ item.name }}</text>
</view>
</view>
</view>
<view class="card">
<view class="card-header">
<text class="card-title">
车辆查询
</text>
<text class="card-sub">
请选择查询类型
</text>
</view>
<view class="inq-grid">
<view
v-for="item in vehicleItems"
:key="item.feature"
class="inq-cell"
@tap="goInquireFeature(item.feature)"
>
<image
class="inq-icon"
:src="getInquiryItemIconUrl(item)"
mode="aspectFit"
/>
<text class="inq-name">{{ item.name }}</text>
</view>
</view>
</view>
<view class="card">
<view class="card-header">
<text class="card-title">
查询案例
</text>
<text class="card-sub">
已服务 290000+ 车主
</text>
</view>
<view
v-for="item in caseList"
:key="item.id"
class="case-item"
>
<view class="case-line">
<text class="case-tag">
{{ item.tag }}
</text>
<text class="case-vin">
{{ item.vin }}
</text>
<text class="case-model">
{{ item.model }}
</text>
</view>
</view>
</view>
<view class="card">
<view class="card-header">
<text class="card-title">
客户评价
</text>
</view>
<view
v-for="item in reviewList"
:key="item.id"
class="review-item"
>
<view class="review-avatar">
<text class="review-avatar-text">
{{ reviewInitial(item.name) }}
</text>
</view>
<view class="review-content">
<view class="review-name">
{{ item.name }}
</view>
<view class="review-text">
{{ item.content }}
</view>
</view>
</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;
}
.banner {
margin-bottom: 24rpx;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 18rpx 40rpx rgba(15, 35, 52, 0.05);
}
.banner-img {
width: 100%;
display: block;
}
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.card-header-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.cat-badge {
width: 48rpx;
height: 48rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cat-badge-icon {
font-size: 28rpx;
}
.card-more-link {
font-size: 22rpx;
color: #86909c;
padding: 8rpx 16rpx;
background: #f2f3f5;
border-radius: 20rpx;
}
.card-more-link:active {
opacity: 0.7;
}
.card-title {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.card-sub {
font-size: 22rpx;
color: #86909c;
}
.grid-4 {
display: flex;
justify-content: space-between;
}
.grid-4-second {
margin-top: 16rpx;
}
.grid-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 8rpx;
}
.icon-svg {
width: 64rpx;
height: 64rpx;
margin-bottom: 8rpx;
}
.grid-text {
font-size: 24rpx;
color: #4e5969;
}
.inq-grid {
display: flex;
flex-wrap: wrap;
margin: 0 -8rpx;
}
.inq-cell {
width: 33.333%;
padding: 8rpx;
box-sizing: border-box;
}
.inq-icon {
width: 72rpx;
height: 72rpx;
display: block;
margin: 0 auto 10rpx;
}
.inq-icon-custom {
width: 72rpx;
height: 72rpx;
display: block;
margin: 0 auto 10rpx;
font-size: 48rpx;
color: #1768ff;
display: flex;
align-items: center;
justify-content: center;
}
.inq-name {
display: block;
font-size: 22rpx;
color: #1d2129;
text-align: center;
line-height: 1.35;
padding: 0 4rpx;
min-height: 60rpx;
}
.main-query {
display: flex;
align-items: stretch;
margin-bottom: 24rpx;
}
.main-left {
flex: 1.2;
margin-right: 16rpx;
min-width: 0;
}
.card-img-wrap {
padding: 0;
overflow: hidden;
}
.main-left-img {
width: 100%;
display: block;
}
.main-right {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
gap: 16rpx;
}
.small-card-img {
flex: 1;
padding: 0;
overflow: hidden;
min-height: 0;
}
.main-right-img {
width: 100%;
height: 100%;
display: block;
}
.case-item + .case-item {
margin-top: 12rpx;
}
.case-line {
font-size: 22rpx;
color: #4e5969;
}
.case-tag {
color: #1768ff;
margin-right: 10rpx;
}
.case-vin {
margin-right: 10rpx;
}
.review-item {
display: flex;
margin-top: 16rpx;
}
.review-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin-right: 16rpx;
background: linear-gradient(145deg, #e8f0ff 0%, #d4e4ff 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.review-avatar-text {
font-size: 26rpx;
font-weight: 600;
color: #1768ff;
}
.review-content {
flex: 1;
}
.review-name {
font-size: 24rpx;
color: #1d2129;
margin-bottom: 4rpx;
}
.review-text {
font-size: 22rpx;
color: #4e5969;
line-height: 1.6;
}
</style>

View File

@@ -0,0 +1,103 @@
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { getQueryExample } from '@/api'
import VehicleReportShell from '@/components/report/VehicleReportShell.vue'
import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize'
definePage({
style: {
navigationBarTitleText: '示例报告',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
const feature = ref('')
const loading = ref(true)
const errText = ref('')
const productName = ref('')
const queryParams = ref({})
const rows = ref(normalizeVehicleQueryData([]))
onLoad((options) => {
feature.value = options?.feature || ''
void load()
})
async function load() {
if (!feature.value) {
loading.value = false
errText.value = '缺少产品参数'
return
}
loading.value = true
errText.value = ''
try {
const res = await getQueryExample(feature.value)
if (res?.code === 200 && res.data) {
productName.value = res.data.product_name || feature.value
queryParams.value = res.data.query_params || {}
rows.value = normalizeVehicleQueryData(res.data.query_data || [])
if (!rows.value.length)
errText.value = '该产品暂无示例模块数据'
}
else if (res?.code === 200003) {
productName.value = feature.value
errText.value = '暂无示例报告'
}
else {
productName.value = feature.value
errText.value = res?.msg || '加载失败'
}
}
catch {
productName.value = feature.value
errText.value = '网络异常'
}
finally {
loading.value = false
}
}
</script>
<template>
<view class="page-root">
<view v-if="loading" class="state">
加载中
</view>
<view v-else-if="errText && !rows.length" class="state">
{{ errText }}
</view>
<view v-else class="content">
<VehicleReportShell
mode="example"
:product-name="productName"
:query-params="queryParams"
:rows="rows"
/>
</view>
</view>
</template>
<style scoped lang="scss">
.page-root {
min-height: 100vh;
background: #d2dffa;
box-sizing: border-box;
}
.state {
padding: 100rpx 32rpx;
text-align: center;
font-size: 28rpx;
color: #86909c;
}
/* 微信小程序scroll-view + flex:1 + height:0 常导致可视高度为 0改由页面整体滚动 */
.content {
padding: 24rpx 24rpx 48rpx;
box-sizing: border-box;
}
</style>

1128
src/pages/inquire/index.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { ref } from 'vue'
definePage({
style: {
navigationBarTitleText: '授权书',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
function formatSignDate(d: Date) {
const y = d.getFullYear()
const m = d.getMonth() + 1
const day = d.getDate()
return `${y}${m}${day}`
}
const signTime = ref(formatSignDate(new Date()))
</script>
<template>
<view class="legal-root">
<scroll-view scroll-y class="legal-scroll" :show-scrollbar="true">
<view class="legal-inner">
<view class="legal-page-title">
个人信息查询授权书
</view>
<view class="legal-p">
广西福铭网络科技有限公司
</view>
<view class="legal-p legal-indent-text">
本人________拟向贵司申请大数据分析报告查询业务贵司需要了解本人相关状况用于查询大数据分析报告因此本人同意向贵司提供本人的姓名和手机号等个人信息并同意贵司向第三方传送上述信息第三方将使用上述信息核实信息真实情况查询信用记录并生成报告
</view>
<view class="legal-h4">
授权内容如下
</view>
<view class="legal-ol">
<view class="legal-li">
1. 贵司向依法成立的第三方服务商根据本人提交的信息进行核实并有权通过前述第三方服务机构查询使用本人的身份信息设备信息运营商信息等查询本人信息包括但不限于学历婚姻资产状况及对信息主体产生负面影响的不良信息出具相关报告
</view>
<view class="legal-li">
2. 依法成立的第三方服务商查询或核实搜集保存处理共享使用含合法业务应用本人相关数据且不再另行告知本人但法律法规监管政策禁止的除外
</view>
<view class="legal-li">
3. 本人授权有效期为自授权之日起 1 个月本授权为不可撤销授权但法律法规另有规定的除外
</view>
</view>
<view class="legal-h4">
用户声明与承诺
</view>
<view class="legal-ol">
<view class="legal-li">
1. 本人在授权签署前已通过实名认证及动态验证码验证或其他身份验证手段确认本授权行为为本人真实意思表示平台已履行身份验证义务
</view>
<view class="legal-li">
2. 本人在此声明已充分理解上述授权条款含义知晓并自愿承担因授权数据使用可能带来的后果包括但不限于影响个人信用评分生活行为等本人确认授权范围内的相关信息由本人提供并真实有效
</view>
<view class="legal-li">
3. 若用户冒名签署或提供虚假信息由用户自行承担全部法律责任平台不承担任何后果
</view>
</view>
<view class="legal-h4">
特别提示
</view>
<view class="legal-ol">
<view class="legal-li">
1. 本产品所有数据均来自第三方可能部分数据未公开数据更新延迟或信息受到限制贵司不对数据的准确性真实性完整性做任何承诺用户需根据实际情况结合报告内容自行判断与决策
</view>
<view class="legal-li">
2. 本产品仅供用户本人查询或被授权查询除非用户取得合法授权用户不得利用本产品查询他人信息用户因未获得合法授权而擅自查询他人信息所产生的任何后果由用户自行承担责任
</view>
<view class="legal-li">
3. 本授权书涉及对本人敏感信息包括但不限于婚姻状态资产状况等的查询与使用本人已充分知晓相关信息的敏感性并明确同意贵司及其合作方依据授权范围使用相关信息
</view>
<view class="legal-li">
4. 平台声明本授权书涉及的信息核实及查询结果由第三方服务商提供平台不对数据的准确性完整性实时性承担责任用户根据报告所作决策的风险由用户自行承担平台对此不承担法律责任
</view>
<view class="legal-li">
5. 本授权书中涉及的数据查询和报告生成由依法成立的第三方服务商提供若因第三方行为导致数据错误或损失用户应向第三方主张权利平台不承担相关责任
</view>
</view>
<view class="legal-h4">
附加说明
</view>
<view class="legal-ol">
<view class="legal-li">
1. 本人在授权的相关数据将依据法律法规及贵司内部数据管理规范妥善存储存储期限为法律要求的最短必要时间超过存储期限或在数据使用目的达成后贵司将对相关数据进行销毁或匿名化处理
</view>
<view class="legal-li">
2. 本人有权随时撤回本授权书中的授权但撤回前的授权行为及其法律后果仍具有法律效力若需撤回授权本人可通过贵司官方渠道提交书面申请贵司将在收到申请后依法停止对本人数据的使用
</view>
<view class="legal-li">
3. 你通过全能查/天远查自愿支付相应费用用于购买广西福铭网络科技有限公司的大数据报告产品如若对产品内容存在异议可通过邮箱 admin@iieeii.com 或小程序内联系客服进行反馈贵司将在收到异议之日起 20 日内进行核查和处理并将结果答复
</view>
<view class="legal-li">
4. 你向广西福铭网络科技有限公司的支付方式为广西福铭网络科技有限公司及其经官方授权的相关企业的支付宝账户
</view>
</view>
<view class="legal-h4">
争议解决机制
</view>
<view class="legal-ul">
<view class="legal-li">
若因本授权书引发争议双方应友好协商解决协商不成的双方同意将争议提交至授权书签署地广西壮族自治区有管辖权的人民法院解决
</view>
</view>
<view class="legal-h4">
签署方式的法律效力声明
</view>
<view class="legal-ul">
<view class="legal-li">
本授权书通过用户在线勾选电子签名或其他网络签署方式完成与手写签名具有同等法律效力平台已通过技术手段保存签署过程的完整记录作为用户真实意思表示的证据
</view>
</view>
<view class="legal-p legal-mt">
本授权书于 {{ signTime }} 起生效
</view>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
@import './legal.scss';
.legal-indent-text {
text-indent: 2em;
}
</style>

125
src/pages/legal/legal.scss Normal file
View File

@@ -0,0 +1,125 @@
.legal-root {
height: 100vh;
display: flex;
flex-direction: column;
background: #ffffff;
box-sizing: border-box;
}
.legal-scroll {
flex: 1;
min-height: 0;
height: 0;
}
.legal-inner {
padding: 24rpx 28rpx calc(32rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.legal-page-title {
font-size: 36rpx;
font-weight: 600;
text-align: center;
margin-bottom: 28rpx;
color: #1d2129;
}
.legal-indent {
box-sizing: border-box;
}
.legal-section {
margin-bottom: 28rpx;
}
.legal-block,
.legal-para {
font-size: 26rpx;
line-height: 1.75;
color: #4e5969;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.legal-para view,
.legal-block view {
margin-bottom: 12rpx;
}
.legal-h3 {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 12rpx;
line-height: 1.5;
}
.legal-h4 {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
margin-top: 16rpx;
margin-bottom: 12rpx;
}
.legal-subhead {
font-size: 26rpx;
font-weight: 600;
color: #1d2129;
margin: 16rpx 0 8rpx;
}
.legal-strong {
font-weight: 600;
color: #1d2129;
}
.legal-link {
color: #1768ff;
}
.legal-date {
font-size: 24rpx;
color: #86909c;
}
.legal-date-right {
display: block;
text-align: right;
margin-top: 16rpx;
}
.legal-mt {
margin-top: 12rpx;
}
.legal-mt-lg {
margin-top: 24rpx;
}
.legal-my {
margin: 12rpx 0;
}
.legal-p {
font-size: 26rpx;
line-height: 1.75;
color: #4e5969;
margin: 12rpx 0;
}
.legal-ol,
.legal-ul {
padding-left: 32rpx;
margin: 12rpx 0 20rpx;
box-sizing: border-box;
}
.legal-li {
font-size: 26rpx;
line-height: 1.75;
color: #4e5969;
margin-bottom: 16rpx;
display: block;
}

View File

@@ -0,0 +1,499 @@
<script setup lang="ts">
definePage({
style: {
navigationBarTitleText: '隐私政策',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
</script>
<template>
<view class="legal-root">
<scroll-view scroll-y class="legal-scroll" :show-scrollbar="true">
<view class="legal-inner">
<view>
<!-- 页面标题 -->
<view class="legal-page-title">
隐私政策
</view>
<!-- 内容主体 -->
<view class="legal-indent">
<view class="legal-section">
<!-- 开篇说明 -->
<view class="legal-h3">
您的信任对我们非常重要
</view>
<view class="legal-para">
我们深知个人信息对您的重要性我们将按法律法规要求采取相应安全保护措施尽力保护您的个人信息安全可控
有鉴于此广西福铭网络科技有限公司以下简称我们天远查作为天远查产品及服务的提供者制定本隐私政策下称本政策并提醒您
</view>
<view class="legal-para">
本政策适用于全部天远查产品及服务如我们关联公司的产品或服务中使用了天远查提供的产品或服务但未设独立的隐私政策的
该部分天远查提供的产品或服务同样适用于本政策
</view>
<view class="legal-para">
需要特别说明的是本政策不适用于其他第三方通过网页或天远查客户端直接向您提供的服务统称第三方服务
您向该第三方服务提供者提供的信息不适用于本政策您在选择使用第三方服务前应充分了解第三方服务的产品功能及隐私保护政策再选择是否开通功能
</view>
<view class="legal-para">
在使用天远查产品或服务前请您务必仔细阅读并透彻理解本政策在确认充分理解使用相关产品或服务
一旦您开始使用天远查产品或服务即表示您已充分理解并同意本政策
</view>
</view>
<view class="legal-h3">
第一部分 定义
</view>
<view class="legal-section">
<!-- 第一部分 -->
<view class="legal-para">
<view>
1天远查服务提供者是指研发并提供天远查产品和服务法律主体广西福铭网络科技有限公司下称我们天远查
</view>
<view>
2天远查用户是指注册天远查账户的用户以下称
</view>
<view>
3个人信息指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息
</view>
<view>
4个人信息删除指在实现日常业务功能所涉及的系统中去除个人信息的行为使其保持不可被检索访问的状态具体指产品内的账号注销功能
</view>
<view>
5个人信息匿名化通过对个人信息的加密技术处理使得个人信息主体无法被识别且处理后的信息不能被复原的过程
</view>
</view>
</view>
<view class="legal-h3">
第二部分 隐私政策
</view>
<view class="legal-section">
<!-- 第一部分 -->
<view class="legal-h3">
我们如何收集您的个人信息
</view>
<view class="legal-para">
为了向您及天远查企业用户提供天远查服务维护天远查服务的正常运行改进及优化我们的服务体验并保障您的账号安全
我们会出于本政策下述目的及方式收集您在注册使用天远查服务时主动提供授权提供或基于您使用天远查服务时产生的信息
</view>
<!-- 注册天远查用户信息 -->
<view class="legal-section">
<view class="legal-h3">
注册天远查用户信息
</view>
<view class="legal-para">
为注册成为天远查用户以便我们为您提供天远查服务诸如数据查询视频查看等功能
您需要提供您的手机号码及短信验证码以注册并创建天远查账号否则您将不能使用天远查服务
</view>
<view class="legal-para">
如果您仅需使用浏览搜索天远查网页展示的产品功能及服务介绍您不需要注册成为天远查用户并提供上述信息
</view>
<view class="legal-para">
如您的账号是注册在企业下的关联账号当您所在企业用户注销天远查账户时我们将会匿名化处理或删除您在该组织的相关个人信息
但您作为天远查个人用户的个人信息仍将保留除非您主动注销天远查账户
</view>
<view class="legal-para">
在经过用户授权同意的情况下我司需要获取用户的手机号码以便开展相应业务
</view>
</view>
<!-- 使用天远查服务过程中收集信息 -->
<view class="legal-section">
<view class="legal-h3">
使用天远查服务过程中收集信息
</view>
<view class="legal-para">
当您在使用天远查服务过程中为向您提供您需求的天远查软件服务交互展示搜索结果识别账号异常状态维护天远查服务的正常运行改进及优化您对天远查服务的体验并保障您的账号安全包括您使用天远查服务以及使用方式的信息并将这些信息进行关联
</view>
<view class="legal-para">
<view>1日志信息</view>
<view>
当您使用我们的网站或客户端提供的产品或服务时我们会自动收集您对我们服务的详细使用情况作为有关网络日志保存例如您的搜索查询内容IP地址使用的语言访问日期和时间您访问的网页记录日志信息
</view>
<view>
请注意单独的设备信息日志信息是无法识别特定自然人身份的信息如果我们将这类非个人信息与其他信息结合用于识别特定自然人身份或者将其与个人信息结合使用则在结合使用期间这类非个人信息将有可能被视为个人信息除取得您授权或法律法规另有规定外我们会将该类个人信息做匿名化去标识化处理
</view>
</view>
<view class="legal-para">
<view>2您向我们提供的信息</view>
<view>
在服务使用过程中特别是在申请提现实名认证或佣金结算时您需要提供包括但不限于姓名身份证号银行卡号手机号税务身份信息等个人资料
您同意我们为履行合同义务税务申报身份核验财务结算等必要目的收集使用存储并在必要范围内共享该等信息
在进行税务代扣代缴结算服务时我们有权将必要信息提供给依法合作的第三方税务服务商结算服务商前提是该第三方承担同等信息保护义务
</view>
<view>
您可以对全能查产品及服务的体验问题反馈帮助我们更好地了解您使用我们产品或服务的体验和需求改善我们产品或服务,为此我们会记录您的联系信息反馈的问题或建议以便我们进一步联系您反馈您我们的处理意见
为向您提供更好的服务例如在不同的服务端或设备上提供体验一致的服务和您需求的客服接待了解产品适配性识别账号异常状态
</view>
</view>
<view class="legal-para">
<view>3为您提供安全保障收集信息</view>
<view>
为预防发现调查欺诈侵权危害安全非法或违反与我们或与我们关联公司的协议政策或规则的行为我们可能收集或整合您的用户个人信息服务使用信息设备信息日志信息以及我们关联公司合作伙伴取得您授权或依据法律共享的信息
您理解并同意我们向您提供的功能和服务场景是不断迭代升级的如我们未在上述场景中明示您需要收集的个人信息我们将会通过页面提示交互设计等方式另行向您明示信息收集的内容范围和目的并征得您同意
</view>
<view>
如我们停止运营天远查产品或服务我们将及时停止继续收集您个人信息的活动将停止运营的通知以公告或短信的形式通知您并依照所适用的法律对所持有的您的个人信息进行删除或匿名化处理
</view>
</view>
<view class="legal-para">
<view>4手机号码收集及其用途</view>
<view>
在您使用天远查服务的过程中我们可能会要求您提供手机号码我们收集您的手机号码主要是为了向您发送重要的通知服务更新账户安全信息促销活动服务相关的短信等为了确保您能及时获得关于您账号安全产品更新和优化系统维护等信息我们可能会向您发送有关服务变更功能更新版本升级等通知确保您能够持续享受我们的产品和服务
</view>
<view>
此外您的手机号码还可能用于为您提供个性化的短信推广内容帮助您了解我们新推出的服务产品或活动优惠我们承诺不会在未经您明确同意的情况下将您的手机号码用于任何与服务相关以外的用途且不会将您的信息出售或租赁给第三方为了保障您的权益您可以随时通过设置页面或联系客户服务停止接收短信通知或推广信息如果您选择取消订阅短信通知或推广您仍将继续收到与账户安全系统通知等相关的重要信息
</view>
<view>
我们会采取严格的措施保护您的手机号码不被滥用包括采用加密存储定期审查访问权限等技术和管理手段以确保您的个人信息安全同时我们也会根据适用的法律法规在您停止使用我们的服务或终止您的账户时删除或匿名化处理您的手机号码及其他相关信息
</view>
</view>
</view>
<view class="legal-section">
<!-- 第二部分 -->
<view class="legal-h3">
我们如何使用信息
</view>
<view class="legal-para">
收集您的信息是为了向您提供服务及提升服务质量为了实现这一目的我们会把您的信息用于下列用途
</view>
<view class="legal-para">
<view>
1向您提供您使用的天远查产品或服务并维护改进优化这些服务及服务体验
</view>
<view>
2为预防发现调查欺诈侵权危害安全非法或违反与我们或与我们关联公司的协议政策或规则的行为保护您其他用户或公众以及我们或我们关联公司的合法权益我们会使用或整合您的个人信息服务使用信息设备信息日志信息以及我们关联公司合作伙伴取得您授权或依据法律共享的信息来综合判断您的操作风险检测及防范安全事件并依法采取必要的记录审计分析处置措施
</view>
<view>3经您许可的其他用途</view>
</view>
</view>
<view class="legal-section">
<!-- 第三部分 -->
<view class="legal-h3">
我们如何使用Cookie 和同类技术
</view>
<view class="legal-para">
为使您获得更轻松的访问体验您使用天远查产品或服务时我们可能会通过采用各种技术收集和存储您访问天远查服务的相关数据
在您访问或再次访问天远查服务时我们能识别您的身份并通过分析数据为您提供更好更多的服务
</view>
<view class="legal-para">
包括使用小型数据文件识别您的身份这么做是为了解您的使用习惯帮您省去重复输入账户信息的步骤或者帮助判断您的账户安全
这些数据文件可能是CookieFlash
Cookie或您的浏览器或关联应用程序提供的其他本地存储统称Cookie
</view>
<view class="legal-para">
请您理解我们的某些服务只能通过使用Cookie才可得到实现如果您的浏览器或浏览器附加服务允许
您可以修改对Cookie的接受程度或者拒绝天远查的Cookie但拒绝天远查的Cookie在某些情况下您可能无法使用依赖于cookies的天远查服务的部分功能
</view>
</view>
<view class="legal-section">
<!-- 第四部分 -->
<view class="legal-h3">
我们如何共享转让公开披露您的信息
</view>
<!-- 共享 -->
<view class="legal-subhead">
() 共享
</view>
<view class="legal-para">
我们不会和其他公司组织和个人共享您的个人信息但以下情况除外
</view>
<view class="legal-para">
<view>
1在获取您同意的情况下共享获得您的明确同意后我们会与其他方共享您的个人信息
</view>
<view>
2在法定情形下的共享我们可能会根据法律法规规定诉讼争议解决需要或按行政司法机关依法提出的要求对外共享您的个人信息
</view>
<view>
3只有透露您的资料才能提供您所要求的第三方产品和服务在您通过天远查客户端购买查询服务的您同意天远查向实际产品提供者提供您的身份信息包括真实姓名和身份证号等为了提升实人认证的准确性您同意第三方公司仅限于个人信息进行验证相关服务将您提供的个人信息与法律法规允许的机构或政府机关授权的机构的数据进行校验
</view>
<view>
4在您被他人投诉侵犯知识产权或其他合法权利时需要向投诉人披露您的必要资料以便进行投诉处理的
</view>
<view>
5天远查服务可能含有其他网站的链接除法律另有规定外天远查对其他网站的隐私保护措施不负相应法律责任我们可能在需要的时候增加商业伙伴但是提供给他们的将仅是综合信息我们将不会公开您的个人信息
</view>
</view>
<!-- 转让 -->
<view class="legal-subhead">
() 转让
</view>
<view class="legal-para">
我们不会将您的个人信息转让给任何公司组织和个人但以下情况除外
</view>
<view class="legal-para">
<view>
1在获取明确同意的情况下转让获得您的明确同意后我们会向其他方转让您的个人信息
</view>
<view>
2在天远查发生合并收购或破产清算情形或其他涉及合并收购或破产清算情形时如涉及到个人信息转让我们会要求新的持有您个人信息的公司组织继续受本政策的约束否则我们将要求该公司组织和个人重新向您征求授权同意
</view>
</view>
<!-- 公开披露 -->
<view class="legal-subhead">
() 公开披露
</view>
<view class="legal-para">
我们仅会在以下情况下公开披露您的个人信息
</view>
<view class="legal-para">
<view>
1获得您明确同意或基于您的主动选择我们可能会公开披露您的个人信息
</view>
<view>
2如果我们确定您出现违反法律法规或严重违反天远查相关协议规则的情况或为保护天远查及其关联公司用户或公众的人身财产安全免遭侵害我们可能依据法律法规或天远查相关协议规则征得您同意的情况下披露关于您的个人信息包括相关违规行为以及天远查已对您采取的措施
</view>
</view>
<!-- 特殊情况 -->
<view class="legal-subhead">
()
共享转让公开披露个人信息时事先征得授权同意的例外
</view>
<view class="legal-para">
以下情形中共享转让公开披露您的个人信息无需事先征得您的授权同意
</view>
<view class="legal-para">
<view>1与国家安全国防安全有关的</view>
<view>
2与公共安全公共卫生重大公共利益有关的
</view>
<view>
3与犯罪侦查起诉审判和判决执行等有关的
</view>
<view>
4出于维护您或其他个人的生命财产等重大合法权益但又很难得到本人同意的
</view>
<view>5您自行向社会公众公开的个人信息</view>
<view>
6从合法公开披露的信息中收集个人信息的如合法的新闻报道政府信息公开等渠道
请您注意根据法律规定共享转让经匿名化处理的个人信息且确保数据接收方无法复原并重新识别个人信息主体的不属于个人信息的对外共享转让及公开披露行为对此类数据的保存及处理将无需另行向您通知并征得您的同意
</view>
</view>
</view>
<view class="legal-section">
<!-- 第五部分 -->
<view class="legal-h3">
我们如何保护您的信息
</view>
<view class="legal-para">
我们会采取各种预防措施来保护您的个人信息以保障您的个人信息免遭丢失盗用和误用以及被擅自取阅披露更改或销毁
为确保您个人信息的安全我们有严格的信息安全规定和流程并严格执行上述措施
</view>
<view class="legal-para">
天远查建立了全方位多维度的数据安全管理体系保证整个天远查各个平台的安全性
我们会采取合理可行的措施尽力避免收集无关的个人信息
并在限于达成本政策所述目的所需的期限以及所适用法律法规所要求的期限内对您的个人信息进行脱敏处理
在您使用查询过程中所涉及的用户姓名身份证号手机号/账号密码信息均采用的是AES加密方式
所有二次输出信息均经过脱敏处理数据库文件不存储用户明文数据
</view>
<view class="legal-para">
在不幸发生个人信息安全事件后我们将按照法律法规的要求最迟不迟于30个自然日内向您告知
安全事件的基本情况和可能的影响我们已采取或将要采取的处置措施您可自主防范和降低风险的建议对您的补救措施等
事件相关情况我们将以邮件信函电话通知等方式告知您
难以逐一告知个人信息主体时我们会采取合理有效的方式发布公告
同时我们还将按照监管部门要求上报个人信息安全事件的处置情况
</view>
<view class="legal-para">
互联网环境并非百分之百安全尽管我们有这些安全措施但仍然无法完全避免互联网中存在的各种风险我们将尽力确保您的信息的安全性
</view>
</view>
<view class="legal-section">
<!-- 第六部分 -->
<view class="legal-h3">
未成年人保护
</view>
<view class="legal-para">
我们重视未成年人的信息保护如您为未成年人的建议您请您的父母或监护人仔细阅读本隐私权政策
并在征得您的父母或监护人同意的前提下使用我们的服务或向我们提供信息
</view>
<view class="legal-para">
对于经父母或监护人同意使用我们的产品或服务而收集未成年人个人信息的情况
我们只会在法律法规允许父母或监护人明确同意或者保护未成年人所必要的情况下使用共享转让或披露此信息
</view>
<view class="legal-para">
我们将根据国家相关法律法规及本政策的规定保护未成年人的个人信息
</view>
</view>
<view class="legal-section">
<!-- 第七部分 -->
<view class="legal-h3">
您的个人信息存储
</view>
<!-- 存储地区 -->
<view class="legal-subhead">
() 存储地区
</view>
<view class="legal-para">
我们将在中华人民共和国境内运营天远查服务中收集和产生的个人信息存储在中华人民共和国境内
目前我们不会将上述信息传输至境外如果我们向境外传输我们将会遵循相关国家规定或者征求您的同意
</view>
<!-- 存储期限 -->
<view class="legal-subhead">
() 存储期限
</view>
<view class="legal-para">
您在使用本平台期间我们将保存您的个人脱敏加密信息保存期限将以不超过为您提供服务所必须的期间为原则
在您终止使用本平台后除法律法规对于特定信息保留期限另有规定外我们会对您的信息进行删除或做匿名化处理
如我们停止运营本平台服务我们将在合理期限内依照所适用的法律对所持有的您的个人信息进行删除或匿名化处理
</view>
</view>
<view class="legal-section">
<!-- 第八部分 -->
<view class="legal-h3">
您享有的权利及权利行使路径
</view>
<!-- 访问查询权 -->
<view class="legal-subhead">
() 访问查询权
</view>
<view class="legal-para">
您对您的天远查账号内的信息含个人信息依法享有访问查询权包括
</view>
<view class="legal-para">
<view>
<text class="legal-strong">账户信息</text>
您可以登录手机客户端通过我的-点击名字或头像可以访问您的头像信息姓名绑定手机号
</view>
<view>
<text class="legal-strong">使用信息</text>
您可以在天远查手机客户端相关页面访问查询您的使用信息包括订单信息
可以通过报告列表-查看详情进行访问查看
</view>
<view>
<text class="legal-strong">其他信息</text>
如您在此前述过程中遇到操作问题的或如需获取其他前述无法获知的个人信息内容
您可通过在线客服或邮箱联系我们我们将在核实您的身份后在合理期限内向您提供
但法律法规另有规定的或本政策另有约定的除外
</view>
</view>
<!-- 同意的撤回与变更 -->
<view class="legal-subhead">
() 同意的撤回与变更
</view>
<view class="legal-para">
若您需要更改相关权限的授权例如相机相册麦克风您可以通过您的硬件设备进行修改
您也可以通过注销天远查账户的方式永久撤回我们继续收集您个人信息的全部授权
如您在此过程中遇到操作问题的可以通过本政策帮助中心方式联系我们
</view>
<!-- 帮助反馈权 -->
<view class="legal-subhead">
() 帮助反馈权
</view>
<view class="legal-para">
我们为您提供了多种反馈渠道具体请见设置帮助中心
</view>
<!-- 提前获知产品与/或服务停止运营权 -->
<view class="legal-subhead">
() 提前获知产品与/或服务停止运营权
</view>
<view class="legal-para">
我们将持续为您提供优质服务若因特殊原因导致我们的部分或全部产品与/或服务被迫停止运营
我们将提前在显著位置或通知您并将停止对您个人信息的收集
同时在超出法律法规规定的必需且最短期限后我们将会对所持有的您的个人信息进行删除或匿名化处理
</view>
</view>
<view class="legal-section">
<!-- 第九部分 -->
<view class="legal-h3">
本政策如何更新
</view>
<view class="legal-para">
我们的隐私政策可能变更
未经您明确同意我们不会限制您按照本隐私政策所应享有的权利
我们会在天远查各个平台包括客户端相关网页上以首页弹窗形式发布对本隐私政策所做的任何变更并以交互设计提醒您阅读并完整理解
对于重大变更我们还会提供更为显著的通知可能包括公告通知甚至向您提供弹窗提示
</view>
<view class="legal-para">
本政策所指的重大变更包括但不限于
<view>
1我们的服务模式发生重大变化如处理用户信息的目的用户信息的使用方式等
</view>
<view>
2我们在控制权组织架构等方面发生重大变化如业务调整破产并购等引起的所有者变更等
</view>
<view>
3用户信息共享转让或公开披露的主要对象发生变化
</view>
<view>
4我们负责处理用户信息安全的责任部门联络方式及投诉渠道发生变化时
</view>
<view>
5用户信息安全影响评估报告表明存在高风险时
</view>
</view>
</view>
<view class="legal-section">
<!-- 第十部分 -->
<view class="legal-h3">
如何联系我们
</view>
<view class="legal-para">
如果您对本政策或数据处理有任何疑问意见或建议可以通过天远查产品内的联系客服或邮箱
<text class="legal-link"> admin@iieeii.com </text>
与我们联系我们将在收到您发送的响应请求或相关信息之日起十五15天内回复您
</view>
<view class="legal-para">
您理解并同意当涉及以下任一情形时我们无法响应您的请求
<view>1与国家安全国防安全有关的</view>
<view>
2与公共安全公共卫生重大公共利益有关的
</view>
<view>3与犯罪侦查起诉和审判等有关的</view>
<view>
4有充分证据表明您存在主观恶意或滥用权利的
</view>
<view>
5响应您的请求将导致您或其他个人组织的合法权益受到严重损害的
</view>
<view>6涉及天远查或任何第三方主体商业秘密的</view>
<view>7法律法规规定的其他情形</view>
</view>
<view class="legal-para">
如果您对我们的回复不满意特别是您认为我们的个人信息处理行为损害了您的合法权益
您还可以通过向有管辖权的法院提起诉讼来寻求解决方案
</view>
</view>
<view class="legal-section">
<!-- 第十一部分 -->
<view class="legal-h3">
十一其他
</view>
<view class="legal-para">
隐私政策的解释及争议解决均应适用中华人民共和国大陆地区法律
与本隐私政策相关的任何纠纷双方应协商友好解决若不能协商解决
应将争议提交至广西福铭网络科技有限公司注册地有管辖权的人民法院解决
</view>
<view class="legal-para">
隐私政策的标题仅为方便及阅读而设并不影响正文其中任何规定的含义或解释
</view>
</view>
<view class="legal-date legal-date-right">
<text>2024年11月19日</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
@import './legal.scss';
</style>

View File

@@ -0,0 +1,311 @@
<script setup lang="ts">
definePage({
style: {
navigationBarTitleText: '用户协议',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
</script>
<template>
<view class="legal-root">
<scroll-view scroll-y class="legal-scroll" :show-scrollbar="true">
<view class="legal-inner">
<view>
<!-- 页面标题 -->
<view class="legal-page-title">
用户协议
</view>
<!-- 内容主体 -->
<view class="legal-indent">
<view class="legal-block">
本协议是您以下又称用户在使用本服务时约定您和广西福铭网络科技有限公司之间权利义务关系的有效协议
</view>
<view class="legal-block">
在您使用本服务前请您务必仔细阅读本协议特别是隐私权保护及授权条款免除或者限制广西福铭网络科技有限公司责任的条款争议解决和法律适用条款一旦您有对本服务的任何部分或全部的注册查看定制使用等任何使用行为即视为您已充分阅读理解并接受本协议的全部内容并与广西福铭网络科技有限公司达成本协议如您对本协议有任何疑问应向广西福铭网络科技有限公司客服咨询如果您不同意本协议的部分或全部约定您应立即停止使用本服务
</view>
<view class="legal-block">
您与广西福铭网络科技有限公司达成本协议后您承诺接受并遵守本协议的约定并不得以未阅读本协议的内容或者未获得广西福铭网络科技有限公司对您问询的解答等理由主张本协议无效或要求撤销本协议在本协议履行过程中广西福铭网络科技有限公司可以依其单独判断暂时停止提供限制或改变本服务并有权根据自身业务需要修订本协议一旦本协议的内容发生变动广西福铭网络科技有限公司将通过平台公布最新的服务协议不再向您作个别通知如果您不同意广西福铭网络科技有限公司对本服务协议所做的修改您应立即停止使用本服务或通过广西福铭网络科技有限公司客服与广西福铭网络科技有限公司联系如果您继续使用本服务则视为您接受广西福铭网络科技有限公司对本协议所做的修改并应遵照修改后的协议执行
</view>
<view class="legal-section">
<view class="legal-h3">
服务内容
</view>
<view class="legal-para">
本服务向您提供多项个人信息整理服务您知悉并认可如您需使用该类服务必须满足如下所述条件且您承诺您向广西福铭网络科技有限公司提请服务申请时已经满足如下所述条件
</view>
<view class="legal-para">
<view>A.您已注册成为本服务的会员</view>
<view>
B.您已在服务页面对应框中填写被查询主体的姓名身份证号手机号银行卡号和被查询主体的手机号收到的动态验证码以下称被查询主体信息
</view>
<view>
C.您确保被查询主体信息是您本人的信息或者被查询主体已授权您本人使用被查询主体信息进行查询授权内容应包括本条D项所述内容并且被查询主体已知悉该授权的风险
</view>
<view>
D.被查询主体不可撤销地授权广西福铭网络科技有限公司为查询评估被查询主体的信息状况a.可以委托合法存续的第三方机构收集查询验证使用并提供您或被查询主体的个人信息b.可以向数据源机构采集您或被查询主体的个人信息c.可以整理保存加工使用您或被查询主体的个人信息并向您提供数据报告d.可以向为您提供服务的第三方商户提供脱敏后的个人信息或数据报告本条所述的个人信息包括但不限于身份信息联系方式职业和居住地址等个人基本信息个人社保公积金收入及在商业活动中形成的各类交易记录个人公共费用缴纳违法违规信息财产状况等
</view>
<view>
E.被查询主体已被明确告知提供被查询主体信息并作出D项授权可能给被查询主体带来的各类损失以及其他可能的不利后果包括采集上述个人信息对被查询主体信用方面可能产生不良影响以及上述信息被信息使用者依法提供给第三方后被他人不当利用的风险
</view>
<view>F.您已全额支付相应的查询服务费用</view>
<view>
G.验证码请不要轻易提供给他人一旦填入手机号对应验证码视为手机号机主本人操作
</view>
</view>
</view>
<view class="legal-section">
<view class="legal-h3">
服务中断或故障
</view>
<view class="legal-para">
您同意因下列原因导致广西福铭网络科技有限公司无法正常提供本服务的广西福铭网络科技有限公司不承担责任
</view>
<view class="legal-para">
<view>1承载本服务的系统停机维护期间</view>
<view>
2您的电脑手机软硬件和通信线路供电线路出现故障的
</view>
<view>
3您操作不当或通过非广西福铭网络科技有限公司授权或认可的方式使用本服务的
</view>
<view>
4因病毒木马恶意程序攻击网络拥堵系统不稳定系统或设备故障通讯故障电力故障或政府行为等原因
</view>
<view>
5由于黑客攻击网络供应商技术调整或故障网站升级手机运营商系统方面的问题等原因而造成的本服务中断或延迟
</view>
<view>
6因台风地震海啸洪水停电战争恐怖袭击等不可抗力之因素造成本服务系统障碍不能执行业务的
</view>
</view>
<view class="legal-para">
广西福铭网络科技有限公司不对因使用本服务而对用户造成的间接的附带的特殊的后果性的损失承担任何法律责任尽管有前款约定广西福铭网络科技有限公司将采取合理行动积极促使本服务恢复正常
</view>
</view>
<view class="legal-section">
<view class="legal-h3">
信息的使用和保护
</view>
<view class="legal-para">
广西福铭网络科技有限公司深知您注重个人信息安全和保护并理解保护被查询主体个人信息的重要性
广西福铭网络科技有限公司会严格遵守中国关于收集使用保存用户个人信息的相关法律法规
尽最大努力采用相应安全技术和管理手段保护您或被查询主体的个人信息
防止您或被查询主体个人信息遭受未经授权的访问适用或泄露毁损篡改或者丢失
未经您或被查询主体的授权不会向任何第三方提供
</view>
<view class="legal-para">
您使用本服务即表示您已授权广西福铭网络科技有限公司将您相关信息披露给广西福铭网络科技有限公司关联公司
关联公司是指直接或间接控制于本协议一方的任何法律实体或者与本协议一方共同于另一法律实体的任何法律实体使用
且广西福铭网络科技有限公司关联公司仅为了向您提供服务而使用您的相关信息
如广西福铭网络科技有限公司关联公司使用您的相关信息则受本协议约束且会按照与广西福铭网络科技有限公司同等谨慎程度保护您的相关信息
</view>
<view class="legal-para">
在您使用本服务过程中特别是在申请提现实名认证或佣金结算时您需要提供包括但不限于姓名身份证号银行卡号手机号税务身份信息等个人资料
您同意我们为履行合同义务税务申报身份核验财务结算等必要目的收集使用存储并在必要范围内共享该等信息
在进行税务代扣代缴结算服务时我们有权将必要信息提供给依法合作的第三方税务服务商结算服务商前提是该第三方承担同等信息保护义务
</view>
<view class="legal-para">
您有权查询更正您的个人信息也可以根据平台流程申请注销账户或停止使用相关服务我们将根据法律要求妥善处理相关信息
</view>
<view class="legal-para">
广西福铭网络科技有限公司就下列原因导致的您或被查询主体个人信息的泄露不承担任何法律责任
</view>
<view class="legal-para">
<view>
1由于您个人原因将本服务的会员账号和密码告知他人或与他人共享广西福铭网络科技有限公司服务账户由此导致的与您相关的信息的泄露
</view>
<view>
2您使用第三方提供的服务包括您向第三方提供的任何个人信息须受第三方自己的服务条款及个人信息保护协议而非本协议约束您需要仔细阅读其条款本协议仅适用于广西福铭网络科技有限公司所提供的服务并不适用于任何第三方提供的服务或第三方的信息使用规则广西福铭网络科技有限公司对任何第三方使用由您提供的信息不承担任何责任
</view>
<view>
3根据相关的法律法规相关政府主管部门或相关证券交易所的要求提供公布与您相关的信息
</view>
<view>
4或其他非因广西福铭网络科技有限公司原因导致的与您相关的信息的泄露
</view>
</view>
</view>
<view class="legal-section">
<!-- 第四部分 -->
<view class="legal-h3">
用户声明与保证
</view>
<view class="legal-para">
<view>
1您使用本服务的前提是您依照适用的法律是具有完全民事权利和民事行为能力能够独立承担民事责任的自然人
</view>
<view>
2您如违反本协议第一条款中的承诺您可能会对他人造成侵权如由此给广西福铭网络科技有限公司或他人造成损失的您需依照法律法规规定承担相应的法律责任
</view>
</view>
</view>
<view class="legal-section">
<!-- 第五部分 -->
<view class="legal-h3">
知识产权保护
</view>
<view class="legal-para">
本服务涉及的文档资料软件商标图案排版设计等以下简称广西福铭网络科技有限公司产品的著作权商标以及其他知识产权或权益均为广西福铭网络科技有限公司享有或广西福铭网络科技有限公司获得授权使用
用户不得出租出借拷贝仿冒复制或修改广西福铭网络科技有限公司产品任何部分或用于其他任何商业目的
也不得将广西福铭网络科技有限公司产品做反向工程反编译或反汇编或以其他方式或工具取得广西福铭网络科技有限公司产品之目标程序或源代码
如果用户违反此约定造成广西福铭网络科技有限公司及其他任何第三方任何损失的甲方应予以全额赔偿
</view>
</view>
<view class="legal-section">
<!-- 第六部分 -->
<view class="legal-h3">
隐私保护
</view>
<view class="legal-para">
天远查保证不对外公开或向第三方提供单个用户的注册资料及存储在天远查的非公开内容但下列情况下除外:
</view>
<view class="legal-para">
<view>1. 事先获得用户的明确授权;</view>
<view>2. 根据有关的法律法规要求;</view>
<view>3. 按照有关政府部门的要求;</view>
<view>4. 为维护社会公众的利益;</view>
<view>5. 为维护天远查的合法利益</view>
</view>
<view class="legal-para">
在不透露单个用户隐私资料的前提下天远查有权利对整个用户数据库进行分析并对用户数据库进行商业上的利用
</view>
</view>
<view class="legal-section">
<!-- 第七部分 -->
<view class="legal-h3">
免责条款
</view>
<view class="legal-para">
<view>
不管基于任何直接的间接的特殊的惩罚性的惩戒性的附带的或结果性的损害损失或费用我们均不对其承担责任即使有人告知我们或我们的员工存在出现这些损害损失或费用的可能性这些损害损失或费用由以下这些情况引起或与这些情况有关
</view>
<view>1. 使用我们网站上或其他链接网站上的信息</view>
<view>2. 无法使用这些信息</view>
<view>
3.
任何在操作或传输中出现的操作失败错误遗漏中断缺陷延迟计算机病毒断线或系统运行失败
</view>
</view>
<view class="legal-para">
<view>
我们可以在不事先通知的情况下更改信息并且不承担更新这些信息的义务不经任何种类的授权不做任何专门或暗指或法定的不侵犯第三方权利名称可出售性出于某种特殊目的适当措施或不携带计算机病毒的保证
</view>
</view>
<view class="legal-para">
<view>
我们不对您查询信息内容的正确性适当性完整性准确性可靠性或适时性做出任何证明声明和保证我们不对任何因个人平台产生的错误遗漏及失准承担任何责任
</view>
</view>
<view class="legal-para">
<view>
对于由于您违反本协议导致任何第三方针对我们及或我们的员工提出的任何申诉起诉要求或者诉讼或者其他法律程序您同意自费作出赔偿并令其免受上述损害
</view>
</view>
</view>
<view class="legal-section">
<!-- 第八部分 -->
<view class="legal-h3">
违约
</view>
<view class="legal-para">
用户不得利用本服务进行任何损害广西福铭网络科技有限公司及其他第三方权益的行为否则广西福铭网络科技有限公司有权立即终止为该用户提供本服务并要求用户赔偿损失由此产生的任何后果由用户自行承担与广西福铭网络科技有限公司无关
</view>
</view>
<view class="legal-section">
<!-- 第九部分 -->
<view class="legal-h3">
数据来源及准确性说明
</view>
<view class="legal-para">
本产品数据来源于第三方可能因数据未公开更新延迟或信息受到限制因此不一定能完全返回不同数据格式及记录详细程度会有所差异这是行业正常现象本报告仅供参考请结合实际情况做出决策
</view>
</view>
<view class="legal-section">
<!-- 第十部分 -->
<view class="legal-h3">
退款协议
</view>
<view class="legal-para">
除非由于本程序的技术性问题导致用户无法正常使用本产品否则我们不提供任何退款服务
用户在购买前应仔细阅读本用户协议及相关使用条款确保对本产品有充分了解
</view>
</view>
<view class="legal-section">
<!-- 第十一部分 -->
<view class="legal-h3">
十一协议的变更和终止
</view>
<view class="legal-para">
鉴于网络服务的特殊性我们变更本协议及其附件的若干条款时将提前通过我们平台公告有关变更事项
修订后的条款或将来可能发布或更新的各类规则-经在我们平台公布后立即自动生效
如您不同意相关修订应当立即停止使用该项服务
如您在发布上述协议变更的有关公告后继续使用互联网查询的视为您已接受协议的有关变更并受其约束
本协议中的相关条款根据该变更而自动做相应修改双方无须另行签订书面协议
</view>
</view>
<view class="legal-section">
<!-- 第十二部分 -->
<view class="legal-h3">
十二适用法律
</view>
<view class="legal-para">
本协议条款的解释效力及纠纷的解决适用中华人民共和国大陆地区法律法规
如用户和广西福铭网络科技有限公司之间发生任何争议首先应友好协商解决协商不成的应将争议提交至广西福铭网络科技有限公司注册地有管辖权的人民法院解决
</view>
</view>
<view class="legal-section">
<!-- 第十三部分 -->
<view class="legal-h3">
十三问题咨询
</view>
<view class="legal-para">
如您对本协议及本服务有任何问题请通过邮箱
<text class="legal-link"> admin@iieeii.com </text>
通过联系客服联系广西福铭网络科技有限公司进行咨询
广西福铭网络科技有限公司会尽最大努力解决您的问题
</view>
</view>
<view class="legal-section">
<!-- 第十四部分 -->
<view class="legal-h3">
十四附则
</view>
<view class="legal-para">
<view>
本协议的某一条款被确认无效均不影响本协议其他条款的效力
</view>
<view>
本协议未尽事宜根据我国相关法律法规及我们相关业务规定办理如需制定补充协议其法律效力同本协议
</view>
</view>
<view class="legal-block legal-mt">
本协议通过点击同意/勾选的方式签署自签署之日生效
</view>
<view class="legal-date legal-date-right">
<text>本协议于 2024 11 17 日生效</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
@import './legal.scss';
</style>

528
src/pages/login.vue Normal file
View File

@@ -0,0 +1,528 @@
<script setup lang="ts">
import { computed, onUnmounted, ref } from 'vue'
import { postAuthSendSms, postUserMobileCodeLogin } from '@/api'
import { saveAuthSession } from '@/utils/session'
import { tryWxMiniProgramAuth } from '@/utils/wxMiniAuth'
definePage({
style: {
navigationBarTitleText: '登录',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
const phoneMode = ref(false)
const mobile = ref('')
const code = ref('')
const agreed = ref(false)
const sending = ref(false)
const submitting = ref(false)
const wxLoading = ref(false)
const isCountingDown = ref(false)
const countdown = ref(60)
let timer: ReturnType<typeof setInterval> | null = null
const isPhoneValid = computed(() => /^1[3-9]\d{9}$/.test(mobile.value))
const canSendSms = computed(() => isPhoneValid.value && !isCountingDown.value && !sending.value)
const canSubmit = computed(
() => isPhoneValid.value && code.value.length >= 6 && agreed.value && !submitting.value,
)
const canWxMiniLogin = computed(() => agreed.value && !wxLoading.value)
function clearTimer() {
if (timer) {
clearInterval(timer)
timer = null
}
}
function startCountdown() {
clearTimer()
isCountingDown.value = true
countdown.value = 60
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
}
else {
clearTimer()
isCountingDown.value = false
}
}, 1000)
}
function openPhoneForm() {
phoneMode.value = true
}
function closePhoneForm() {
phoneMode.value = false
}
async function handleSendSms() {
if (!canSendSms.value)
return
if (!isPhoneValid.value) {
uni.showToast({ title: '手机号有误', icon: 'none' })
return
}
sending.value = true
try {
const res = await postAuthSendSms({ mobile: mobile.value }) as { code?: number }
if (res && res.code === 200) {
uni.showToast({ title: '已发送', icon: 'none' })
startCountdown()
}
}
finally {
sending.value = false
}
}
async function handleSubmit() {
if (!isPhoneValid.value) {
uni.showToast({ title: '手机号有误', icon: 'none' })
return
}
if (code.value.length < 6) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
if (!agreed.value) {
uni.showToast({ title: '请先勾选协议', icon: 'none' })
return
}
submitting.value = true
try {
const res = await postUserMobileCodeLogin({
mobile: mobile.value,
code: code.value,
}) as { code?: number, data?: { accessToken: string, refreshAfter: number | string, accessExpire: number | string } }
if (res && res.code === 200 && res.data) {
saveAuthSession(res.data)
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 400)
}
}
finally {
submitting.value = false
}
}
function toggleAgree() {
agreed.value = !agreed.value
}
function goLegalUserAgreement() {
uni.navigateTo({ url: '/pages/legal/user-agreement' })
}
function goLegalPrivacyPolicy() {
uni.navigateTo({ url: '/pages/legal/privacy-policy' })
}
function goLegalAuthorization() {
uni.navigateTo({ url: '/pages/legal/authorization' })
}
function onMobileInput(e: { detail?: { value?: string } }) {
const raw = e.detail?.value ?? ''
mobile.value = String(raw).replace(/\D/g, '').slice(0, 11)
}
function onCodeInput(e: { detail?: { value?: string } }) {
const raw = e.detail?.value ?? ''
code.value = String(raw).replace(/\D/g, '').slice(0, 6)
}
async function handleWxMiniLogin() {
if (!agreed.value) {
uni.showToast({ title: '请先勾选协议', icon: 'none' })
return
}
wxLoading.value = true
try {
const ok = await tryWxMiniProgramAuth({ silent: false })
if (!ok)
return
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 400)
}
catch {
uni.showToast({ title: '登录失败', icon: 'none' })
}
finally {
wxLoading.value = false
}
}
onUnmounted(() => {
clearTimer()
})
</script>
<template>
<view class="page-root">
<view class="bg-blob" />
<view class="page">
<!-- 入口仅按钮 + 一行协议 -->
<view v-show="!phoneMode" class="gate">
<view class="brand-mark" />
<view class="brand-title">
全能查
</view>
<view class="brand-sub">
安全查车况 · 更放心
</view>
<view class="btn-stack">
<!-- #ifdef MP-WEIXIN -->
<view
class="btn btn-wx"
:class="{ disabled: !canWxMiniLogin }"
@tap="handleWxMiniLogin"
>
{{ wxLoading ? '…' : '微信登录' }}
</view>
<!-- #endif -->
<view class="btn btn-phone" @tap="openPhoneForm">
手机号登录
</view>
</view>
<view class="agree-row" @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>
<!-- 手机号表单 -->
<view v-show="phoneMode" class="form-sheet">
<view class="form-head">
<view class="back" @tap="closePhoneForm">
</view>
<text class="form-title">手机号登录</text>
<view class="back-spacer" />
</view>
<view class="form-card">
<view class="inp-wrap">
<text class="inp-label">手机号</text>
<input
class="inp"
type="digit"
:value="mobile"
:maxlength="11"
placeholder="11 位手机号"
placeholder-class="inp-ph"
confirm-type="done"
@input="onMobileInput"
>
</view>
<view class="inp-wrap inp-row">
<view class="inp-flex">
<text class="inp-label">验证码</text>
<input
class="inp"
type="digit"
:value="code"
:maxlength="6"
placeholder="6 位验证码"
placeholder-class="inp-ph"
confirm-type="done"
@input="onCodeInput"
>
</view>
<view
class="sms"
:class="{ disabled: !canSendSms }"
@tap="handleSendSms"
>
{{ isCountingDown ? `${countdown}s` : '获取' }}
</view>
</view>
</view>
<view class="agree-row agree-form" @tap.stop>
<view class="agree-tap" @tap="toggleAgree">
<view class="check" :class="{ on: agreed }" />
<text class="agree-txt">同意</text>
</view>
<text class="link" @tap="goLegalUserAgreement">用户协议</text>
<text class="agree-gap"></text>
<text class="link" @tap="goLegalPrivacyPolicy">隐私政策</text>
<text class="agree-gap"></text>
<text class="link" @tap="goLegalAuthorization">授权书</text>
</view>
<view
class="btn btn-submit"
:class="{ disabled: !canSubmit }"
@tap="handleSubmit"
>
登录
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.page-root {
min-height: 100vh;
position: relative;
overflow: hidden;
background: linear-gradient(165deg, #eef4ff 0%, #f5f7fb 42%, #fafbff 100%);
}
.bg-blob {
position: absolute;
width: 520rpx;
height: 520rpx;
right: -120rpx;
top: -80rpx;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(23, 104, 255, 0.18), transparent 55%);
pointer-events: none;
}
.page {
position: relative;
z-index: 1;
padding: 56rpx 40rpx 48rpx;
box-sizing: border-box;
}
.gate {
padding-top: 48rpx;
}
.brand-mark {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
margin: 0 auto 28rpx;
background: linear-gradient(135deg, #1768ff 0%, #4d94ff 100%);
box-shadow: 0 16rpx 40rpx rgba(23, 104, 255, 0.28);
}
.brand-title {
text-align: center;
font-size: 44rpx;
font-weight: 700;
color: #1d2129;
letter-spacing: 2rpx;
}
.brand-sub {
text-align: center;
font-size: 26rpx;
color: #86909c;
margin-top: 12rpx;
margin-bottom: 72rpx;
}
.btn-stack {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.btn {
height: 96rpx;
line-height: 96rpx;
text-align: center;
font-size: 32rpx;
font-weight: 600;
border-radius: 48rpx;
box-sizing: border-box;
}
.btn-wx {
background: #07c160;
color: #fff;
box-shadow: 0 12rpx 32rpx rgba(7, 193, 96, 0.28);
}
.btn-wx.disabled {
opacity: 0.45;
pointer-events: none;
}
.btn-phone {
background: #fff;
color: #1768ff;
border: 2rpx solid rgba(23, 104, 255, 0.35);
box-shadow: 0 8rpx 24rpx rgba(15, 35, 52, 0.06);
}
.agree-row {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
margin-top: 40rpx;
font-size: 24rpx;
color: #86909c;
}
.agree-tap {
display: flex;
align-items: center;
margin-right: 6rpx;
}
.check {
width: 30rpx;
height: 30rpx;
border-radius: 8rpx;
border: 2rpx solid #c9cdd4;
margin-right: 10rpx;
box-sizing: border-box;
}
.check.on {
border-color: #1768ff;
background: #1768ff;
}
.agree-txt {
color: #4e5969;
}
.link {
color: #1768ff;
}
.agree-gap {
margin: 0 4rpx;
color: #86909c;
}
.agree-form {
margin-top: 0;
margin-bottom: 24rpx;
}
.form-sheet {
padding-top: 8rpx;
}
.form-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 36rpx;
}
.back {
width: 72rpx;
height: 72rpx;
line-height: 72rpx;
text-align: center;
font-size: 48rpx;
color: #1d2129;
font-weight: 300;
}
.back-spacer {
width: 72rpx;
}
.form-title {
font-size: 34rpx;
font-weight: 600;
color: #1d2129;
}
.form-card {
background: #fff;
border-radius: 28rpx;
padding: 8rpx 28rpx 8rpx;
border: 1rpx solid rgba(23, 104, 255, 0.08);
box-shadow: 0 20rpx 48rpx rgba(15, 35, 52, 0.06);
margin-bottom: 36rpx;
}
.inp-wrap {
padding: 22rpx 0;
border-bottom: 1rpx solid #f0f1f5;
}
.inp-wrap:last-child {
border-bottom: none;
}
.inp-row {
display: flex;
align-items: flex-end;
gap: 20rpx;
}
.inp-flex {
flex: 1;
min-width: 0;
}
.inp-label {
display: block;
font-size: 22rpx;
color: #86909c;
margin-bottom: 10rpx;
}
.inp {
width: 100%;
height: 72rpx;
font-size: 32rpx;
color: #1d2129;
box-sizing: border-box;
}
.inp-ph {
color: #c9cdd4;
}
.sms {
flex-shrink: 0;
height: 68rpx;
line-height: 68rpx;
padding: 0 28rpx;
font-size: 26rpx;
font-weight: 500;
color: #1768ff;
background: #f0f5ff;
border-radius: 34rpx;
}
.sms.disabled {
color: #c9cdd4;
background: #f7f8fa;
pointer-events: none;
}
.btn-submit {
background: linear-gradient(90deg, #1768ff 0%, #4d94ff 100%);
color: #fff;
box-shadow: 0 16rpx 40rpx rgba(23, 104, 255, 0.28);
}
.btn-submit.disabled {
opacity: 0.45;
pointer-events: none;
}
</style>

982
src/pages/mine.vue Normal file
View File

@@ -0,0 +1,982 @@
<script setup lang="ts">
import { onShareAppMessage, onShareTimeline, onShow } from '@dcloudio/uni-app'
import { onUnmounted, ref } from 'vue'
import { clearAuthStorage, getUserDetail, postAuthSendSmsBindMobile, postUserBindMobile } from '@/api'
import { hasToken, saveAuthSession } from '@/utils/session'
definePage({
style: {
navigationBarTitleText: '我的',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
// 微信小程序:允许使用 open-type="share" 与右上角转发
enableShareAppMessage: true,
enableShareTimeline: true,
},
})
/** 商务合作弹窗中的二维码图:将图片放到 `src/static/` 后改为 `/static/xxx.png` 或填网络地址 */
const BUSINESS_COOP_QR_SRC = ''
const SHARE_TITLE = '全能查 — 买车先查车况,更安心'
const SHARE_PATH = '/pages/index'
const WX_NICK_KEY = 'wx_display_name'
const isLogin = ref(false)
const nickname = ref('')
const userDesc = ref('')
const hasBoundMobile = ref(false)
const wxNickStorage = ref('')
const bindModalOpen = ref(false)
const bindPhone = ref('')
const bindCode = ref('')
const bindSending = ref(false)
const bindSubmitting = ref(false)
const bindCountingDown = ref(false)
const bindCountdown = ref(60)
let bindSmsTimer: ReturnType<typeof setInterval> | null = null
const coopModalOpen = ref(false)
onShareAppMessage(() => ({
title: SHARE_TITLE,
path: SHARE_PATH,
}))
onShareTimeline(() => ({
title: SHARE_TITLE,
query: '',
}))
function maskMobile(plain: string) {
const s = (plain || '').trim()
if (s.length >= 11)
return `${s.slice(0, 3)}****${s.slice(-4)}`
return s || ''
}
function loadWxNickFromStorage() {
try {
const n = uni.getStorageSync(WX_NICK_KEY)
wxNickStorage.value = typeof n === 'string' && n ? n : ''
}
catch {
wxNickStorage.value = ''
}
}
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)
async function refreshUserCard() {
isLogin.value = hasToken()
if (!isLogin.value) {
nickname.value = ''
userDesc.value = ''
hasBoundMobile.value = false
return
}
loadWxNickFromStorage()
try {
const res = await getUserDetail() as {
code?: number
data?: { userInfo?: { mobile?: string, nickName?: string } }
}
if (res && res.code === 200 && res.data?.userInfo) {
const u = res.data.userInfo
const mobile = (u.mobile || '').trim()
const apiNick = (u.nickName || '').trim()
hasBoundMobile.value = /^1[3-9]\d{9}$/.test(mobile)
if (hasBoundMobile.value) {
nickname.value = maskMobile(mobile)
userDesc.value = '已绑定手机号,可同步历史报告与收藏'
}
else {
nickname.value = wxNickStorage.value || apiNick || '微信用户'
userDesc.value = '绑定手机号后,可同步历史报告与收藏'
}
}
else {
nickname.value = wxNickStorage.value || '已登录'
userDesc.value = '获取资料失败,请稍后下拉刷新'
}
}
catch {
nickname.value = wxNickStorage.value || '已登录'
userDesc.value = '网络异常,请稍后重试'
}
}
onShow(() => {
void refreshUserCard()
// #ifdef MP-WEIXIN
try {
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'],
})
}
catch {
/* ignore */
}
// #endif
})
function handleUserTap() {
if (isLogin.value) {
uni.showActionSheet({
itemList: ['退出登录'],
success(res) {
if (res.tapIndex === 0) {
clearAuthStorage()
isLogin.value = false
nickname.value = ''
userDesc.value = ''
hasBoundMobile.value = false
uni.showToast({ title: '已退出', icon: 'none' })
}
},
})
return
}
uni.navigateTo({ url: '/pages/login' })
}
/** 微信小程序:用户主动触发才可调 getUserProfile */
async function syncWxNickname() {
// #ifdef MP-WEIXIN
try {
const res = await new Promise<{ userInfo?: { nickName?: string } }>((resolve, reject) => {
uni.getUserProfile({
desc: '用于在本页展示昵称',
lang: 'zh_CN',
success: resolve,
fail: reject,
})
})
const name = res.userInfo?.nickName?.trim()
if (name) {
uni.setStorageSync(WX_NICK_KEY, name)
wxNickStorage.value = name
if (!hasBoundMobile.value)
nickname.value = name
uni.showToast({ title: '昵称已更新', icon: 'none' })
}
}
catch {
uni.showToast({ title: '需要您确认授权', icon: 'none' })
}
// #endif
}
function openBindModal() {
bindPhone.value = ''
bindCode.value = ''
bindModalOpen.value = true
}
function closeBindModal() {
bindModalOpen.value = false
clearBindSmsTimer()
bindCountingDown.value = false
}
function onBindPhoneInput(e: { detail?: { value?: string } }) {
const raw = e.detail?.value ?? ''
bindPhone.value = String(raw).replace(/\D/g, '').slice(0, 11)
}
function onBindCodeInput(e: { detail?: { value?: string } }) {
const raw = e.detail?.value ?? ''
bindCode.value = String(raw).replace(/\D/g, '').slice(0, 6)
}
async function sendBindSms() {
if (bindSending.value || bindCountingDown.value)
return
if (!isBindPhoneValid()) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
bindSending.value = true
try {
const res = await postAuthSendSmsBindMobile({ mobile: bindPhone.value }) as { code?: number }
if (res && res.code === 200) {
uni.showToast({ title: '验证码已发送', icon: 'none' })
startBindCountdown()
}
}
finally {
bindSending.value = false
}
}
async function submitBindMobile() {
if (!isBindPhoneValid()) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
if (bindCode.value.length < 6) {
uni.showToast({ title: '请输入 6 位验证码', icon: 'none' })
return
}
bindSubmitting.value = true
try {
const res = await postUserBindMobile({
mobile: bindPhone.value,
code: bindCode.value,
}) as { code?: number, data?: { accessToken: string, refreshAfter: number | string, accessExpire: number | string } }
if (res && res.code === 200 && res.data) {
saveAuthSession(res.data)
uni.showToast({ title: '绑定成功', icon: 'success' })
closeBindModal()
await refreshUserCard()
}
}
finally {
bindSubmitting.value = false
}
}
function goHistoryReport() {
uni.switchTab({ url: '/pages/report' })
}
function goFreeValuation() {
uni.showToast({ title: '敬请期待', icon: 'none' })
}
/** 非微信小程序:无 open-type=share仅提示 */
function goShareFallback() {
uni.showToast({ title: '请在微信小程序内使用分享', icon: 'none' })
}
function openCoopModal() {
coopModalOpen.value = true
}
function closeCoopModal() {
coopModalOpen.value = false
}
function goLegalUserAgreement() {
uni.navigateTo({ url: '/pages/legal/user-agreement' })
}
function goLegalPrivacyPolicy() {
uni.navigateTo({ url: '/pages/legal/privacy-policy' })
}
function goLegalAuthorization() {
uni.navigateTo({ url: '/pages/legal/authorization' })
}
function goIllegalCode() {
uni.showToast({ title: '敬请期待', icon: 'none' })
}
function goOilPrice() {
uni.showToast({ title: '敬请期待', icon: 'none' })
}
function goHelp() {
uni.showToast({ title: '敬请期待', icon: 'none' })
}
function goAbout() {
uni.showToast({ title: '敬请期待', icon: 'none' })
}
function goSettings() {
uni.showToast({ title: '敬请期待', icon: 'none' })
}
/** 非微信小程序:无 open-type=contact */
function goServiceFallback() {
uni.showToast({ title: '请在微信小程序内使用在线客服', icon: 'none' })
}
onUnmounted(() => {
clearBindSmsTimer()
})
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<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">
<view class="mine-user-main" @tap="handleUserTap">
<view class="mine-avatar-placeholder" />
<view class="mine-user-text">
<view class="mine-user-name">
{{ isLogin ? nickname : '您还没有登录,立即登录' }}
</view>
<view class="mine-user-desc">
{{ isLogin ? userDesc : '登录后可同步历史报告与收藏' }}
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<view v-if="isLogin && !hasBoundMobile" class="mine-user-extra">
<text class="extra-link" @tap.stop="syncWxNickname">同步微信昵称</text>
<text class="extra-dot">·</text>
<text class="extra-hint">绑定手机后优先显示手机号</text>
</view>
<!-- #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 class="card">
<view class="grid-4">
<view class="grid-item" @tap="goHistoryReport">
<view class="icon-tool i-carbon-document" />
<view class="grid-text">
历史报告
</view>
</view>
<view class="grid-item" @tap="goFreeValuation">
<view class="icon-tool i-carbon-chart-line" />
<view class="grid-text">
免费估值
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<button class="grid-item grid-item-btn" open-type="share" hover-class="grid-item-hover">
<view class="icon-tool i-carbon-share" />
<view class="grid-text">
分享好友
</view>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="grid-item" @tap="goShareFallback">
<view class="icon-tool i-carbon-share" />
<view class="grid-text">
分享好友
</view>
</view>
<!-- #endif -->
<view class="grid-item" @tap="openCoopModal">
<view class="icon-tool i-carbon-enterprise" />
<view class="grid-text">
商务合作
</view>
</view>
</view>
</view>
<view class="card list-card">
<view class="list-item" @tap="goLegalUserAgreement">
<view class="list-icon i-carbon-document-blank" />
<text class="list-text">用户协议</text>
</view>
<view class="list-item" @tap="goLegalPrivacyPolicy">
<view class="list-icon i-carbon-security" />
<text class="list-text">隐私政策</text>
</view>
<view class="list-item" @tap="goLegalAuthorization">
<view class="list-icon i-carbon-certificate" />
<text class="list-text">授权书</text>
</view>
<view class="list-item" @tap="goHelp">
<view class="list-icon i-carbon-help" />
<text class="list-text">帮助中心</text>
</view>
<button
class="list-item list-item-contact no-border"
open-type="contact"
hover-class="list-item-contact-hover"
>
<view class="list-icon i-carbon-chat" />
<view class="service-row">
<text class="list-text">在线客服</text>
<text class="service-time">人工客服 周一至周日 9:00-20:00</text>
</view>
</button>
</view>
</view>
</scroll-view>
<view v-if="coopModalOpen" class="coop-mask" @tap.self="closeCoopModal">
<view class="coop-dialog" @tap.stop>
<view class="coop-title">
商务合作
</view>
<view class="coop-hint">
扫码添加商务微信图片路径可在代码中配置
</view>
<image
v-if="BUSINESS_COOP_QR_SRC"
class="coop-qr"
:src="BUSINESS_COOP_QR_SRC"
mode="aspectFit"
show-menu-by-longpress
/>
<view v-else class="coop-qr-placeholder">
<text class="coop-qr-placeholder-text">二维码图片路径待定</text>
<text class="coop-qr-placeholder-text coop-qr-placeholder-sub">请将图片放入 static 目录并在 mine.vue 中为 BUSINESS_COOP_QR_SRC 赋值</text>
</view>
<view class="coop-close" @tap="closeCoopModal">
知道了
</view>
</view>
</view>
<view v-if="bindModalOpen" class="bind-mask" @tap.self="closeBindModal">
<view class="bind-sheet" @tap.stop>
<view class="bind-sheet-title">
绑定手机号
</view>
<view class="bind-field">
<text class="bind-label">手机号</text>
<input
class="bind-input"
type="digit"
:value="bindPhone"
:maxlength="11"
placeholder="请输入手机号"
placeholder-class="bind-ph"
confirm-type="done"
@input="onBindPhoneInput"
>
</view>
<view class="bind-field bind-field-row">
<view class="bind-field-grow">
<text class="bind-label">验证码</text>
<input
class="bind-input"
type="digit"
:value="bindCode"
:maxlength="6"
placeholder="6 位短信码"
placeholder-class="bind-ph"
confirm-type="done"
@input="onBindCodeInput"
>
</view>
<view
class="bind-sms"
:class="{ disabled: bindSending || bindCountingDown || !isBindPhoneValid() }"
@tap="sendBindSms"
>
{{ bindCountingDown ? `${bindCountdown}s` : '获取验证码' }}
</view>
</view>
<view
class="bind-submit"
:class="{ disabled: bindSubmitting || !isBindPhoneValid() || bindCode.length < 6 }"
@tap="submitBindMobile"
>
确认绑定
</view>
<view class="bind-cancel" @tap="closeBindModal">
取消
</view>
</view>
</view>
</view>
</template>
<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;
}
.banner {
display: flex;
padding: 32rpx 28rpx;
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 {
flex: 1;
}
.banner-title {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 8rpx;
}
.banner-sub {
font-size: 24rpx;
color: #4e5969;
}
.banner-illustration {
width: 180rpx;
display: flex;
align-items: center;
justify-content: center;
}
.car-illus {
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;
}
.mine-user-text {
flex: 1;
min-width: 0;
}
.mine-user-name {
font-size: 28rpx;
color: #1d2129;
margin-bottom: 4rpx;
}
.mine-user-desc {
font-size: 22rpx;
color: #86909c;
}
.mine-user-extra {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
font-size: 22rpx;
color: #86909c;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.extra-link {
color: #1768ff;
}
.extra-dot {
margin: 0 8rpx;
color: #c9cdd4;
}
.extra-hint {
color: #86909c;
}
.mine-bind-row {
margin-top: 12rpx;
padding: 16rpx 0 4rpx;
display: flex;
align-items: center;
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 {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1000;
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;
}
.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>

325
src/pages/report.vue Normal file
View File

@@ -0,0 +1,325 @@
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { getQueryList } from '@/api'
definePage({
style: {
navigationBarTitleText: '查询报告',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
interface ReportItem {
id: string
orderId: number
typeText: string
status: string
statusText: string
vin: string
model: string
time: string
}
const totalCount = ref(0)
const reportList = ref<ReportItem[]>([])
const loading = ref(false)
function readQueryParams(row: Record<string, unknown>): Record<string, unknown> {
const raw = row.query_params
if (raw && typeof raw === 'object' && !Array.isArray(raw))
return raw as Record<string, unknown>
return {}
}
function pickVin(qp: Record<string, unknown>): string {
const keys = ['vin_code', 'vin', 'frame_no', 'VIN', '车架号']
for (const k of keys) {
const v = qp[k]
if (typeof v === 'string' && v.trim())
return v.trim()
if (v != null && typeof v !== 'object' && typeof v !== 'undefined')
return String(v).trim()
}
return ''
}
function pickModel(qp: Record<string, unknown>): string {
const keys = ['model', 'vehicle_model', 'car_model', '车型', 'name', 'car_name']
for (const k of keys) {
const v = qp[k]
if (typeof v === 'string' && v.trim())
return v.trim()
if (v != null && typeof v !== 'object' && typeof v !== 'undefined')
return String(v).trim()
}
return ''
}
function queryStateToUi(state: string): { status: string, statusText: string } {
switch (state) {
case 'success':
return { status: 'success', statusText: '已完成' }
case 'failed':
return { status: 'failed', statusText: '失败' }
case 'pending':
return { status: 'pending', statusText: '待处理' }
case 'processing':
return { status: 'processing', statusText: '查询中' }
case 'cleaned':
return { status: 'cleaned', statusText: '已清理' }
case 'refunded':
return { status: 'refunded', statusText: '已退款' }
default:
return { status: '', statusText: state || '未知' }
}
}
function mapRow(row: Record<string, unknown>): ReportItem {
const id = row.id != null ? String(row.id) : ''
const orderId = Number(row.order_id)
const qp = readQueryParams(row)
const vin = pickVin(qp) || '—'
let model = pickModel(qp)
if (!model && typeof row.product === 'string' && row.product.trim())
model = row.product.trim()
if (!model)
model = '—'
const typeText = typeof row.product_name === 'string' && row.product_name.trim()
? row.product_name.trim()
: '查询报告'
const { status, statusText } = queryStateToUi(
typeof row.query_state === 'string' ? row.query_state : '',
)
const time = typeof row.create_time === 'string' ? row.create_time : ''
return { id, orderId, typeText, status, statusText, vin, model, time }
}
async function loadList() {
loading.value = true
try {
const raw = await getQueryList({ page: 1, pageSize: 50 }, { skipLoading: true })
const res = raw as { code?: number, data?: { total?: number, list?: Record<string, unknown>[] } }
if (res?.code === 200 && res.data) {
const listRaw = Array.isArray(res.data.list) ? res.data.list : []
totalCount.value = typeof res.data.total === 'number' ? res.data.total : listRaw.length
reportList.value = listRaw.map(r => mapRow(r))
}
else {
totalCount.value = 0
reportList.value = []
}
}
catch {
totalCount.value = 0
reportList.value = []
}
finally {
loading.value = false
}
}
onShow(() => {
void loadList()
})
function handleReportTap(item: ReportItem) {
if (!item.orderId) {
uni.showToast({ title: '无法打开报告', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages/report/detail?orderId=${encodeURIComponent(String(item.orderId))}`,
})
}
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<view class="card">
<view class="card-header">
<text class="card-title">历史报告</text>
<text class="card-sub"> {{ totalCount }} </text>
</view>
<view v-if="loading" class="list-loading">
加载中
</view>
<template v-else-if="reportList.length">
<view
v-for="item in reportList"
:key="item.id"
class="report-item"
@tap="handleReportTap(item)"
>
<view class="report-top">
<text class="report-type">{{ item.typeText }}</text>
<text class="report-status" :class="item.status">{{ item.statusText }}</text>
</view>
<view class="report-middle">
<text class="report-vin">{{ item.vin }}</text>
<text class="report-model">{{ item.model }}</text>
</view>
<view class="report-bottom">
<text class="report-time">{{ item.time }}</text>
</view>
</view>
</template>
<view v-else class="empty">
<view class="empty-title">
暂无报告
</view>
<view class="empty-desc">
先去首页发起一次查询报告会自动出现在这里
</view>
</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;
}
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.card-title {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.card-sub {
font-size: 22rpx;
color: #86909c;
}
.list-loading {
padding: 32rpx 0;
text-align: center;
font-size: 24rpx;
color: #86909c;
}
.report-item {
padding: 18rpx 0;
border-top: 1rpx solid #f0f0f0;
}
.report-top {
display: flex;
justify-content: space-between;
margin-bottom: 6rpx;
}
.report-type {
font-size: 24rpx;
color: #1d2129;
}
.report-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 999rpx;
background-color: #f2f3f5;
color: #4e5969;
}
.report-status.success {
background-color: #e8fffb;
color: #15bb8a;
}
.report-status.failed {
background-color: #fff1f0;
color: #f53f3f;
}
.report-status.pending,
.report-status.processing {
background-color: #e8f3ff;
color: #1768ff;
}
.report-status.cleaned,
.report-status.refunded {
background-color: #f2f3f5;
color: #86909c;
}
.report-middle {
font-size: 22rpx;
color: #4e5969;
margin-bottom: 4rpx;
}
.report-vin {
margin-right: 8rpx;
}
.report-model {
color: #86909c;
}
.report-bottom {
font-size: 20rpx;
color: #c0c4cc;
}
.empty {
padding: 40rpx 10rpx 10rpx;
text-align: center;
}
.empty-title {
font-size: 26rpx;
color: #1d2129;
margin-bottom: 8rpx;
}
.empty-desc {
font-size: 22rpx;
color: #86909c;
}
</style>

100
src/pages/report/detail.vue Normal file
View File

@@ -0,0 +1,100 @@
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { getQueryDetailByOrderId, getQueryDetailByOrderNo } from '@/api'
import VehicleReportShell from '@/components/report/VehicleReportShell.vue'
import { normalizeVehicleQueryData } from '@/utils/vehicleReportNormalize'
definePage({
style: {
navigationBarTitleText: '报告详情',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
const orderNo = ref('')
const orderId = ref('')
const loading = ref(true)
const errText = ref('')
const productName = ref('')
const queryParams = ref({})
const rows = ref(normalizeVehicleQueryData([]))
onLoad((options) => {
orderNo.value = options?.orderNo || ''
orderId.value = options?.orderId || ''
void load()
})
async function load() {
if (!orderNo.value && !orderId.value) {
loading.value = false
errText.value = '缺少订单信息'
return
}
loading.value = true
errText.value = ''
try {
const res = orderId.value
? await getQueryDetailByOrderId(orderId.value)
: await getQueryDetailByOrderNo(orderNo.value)
if (res?.code === 200 && res.data) {
productName.value = res.data.product_name || '查询报告'
queryParams.value = res.data.query_params || {}
rows.value = normalizeVehicleQueryData(res.data.query_data || [])
if (!rows.value.length)
errText.value = '暂无报告模块数据'
}
else {
errText.value = res?.msg || '加载失败'
}
}
catch {
errText.value = '网络异常或未登录'
}
finally {
loading.value = false
}
}
</script>
<template>
<view class="page-root">
<view v-if="loading" class="state">
加载中
</view>
<view v-else-if="errText && !rows.length" class="state">
{{ errText }}
</view>
<view v-else class="content">
<VehicleReportShell
mode="detail"
:product-name="productName"
:query-params="queryParams"
:rows="rows"
/>
</view>
</view>
</template>
<style scoped lang="scss">
.page-root {
min-height: 100vh;
background: #d2dffa;
box-sizing: border-box;
}
.state {
padding: 100rpx 32rpx;
text-align: center;
font-size: 28rpx;
color: #86909c;
}
.content {
padding: 24rpx 24rpx 48rpx;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,205 @@
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { toolboxCategories, getCategoryAllTools } from '@/config/toolboxRegistry'
definePage({
style: {
navigationBarTitleText: '分类工具',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
const categoryKey = ref('')
const category = ref<any>(null)
const tools = ref<any[]>([])
onLoad((query) => {
const key = (query?.category as string) || ''
categoryKey.value = key
const cat = toolboxCategories.find(c => c.key === key)
if (cat) {
category.value = cat
tools.value = getCategoryAllTools(key)
uni.setNavigationBarTitle({ title: cat.name })
}
})
function goTool(key: string) {
uni.navigateTo({
url: `/pages/toolbox/query?key=${encodeURIComponent(key)}`,
})
}
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<view v-if="category" class="cat-header">
<view class="cat-icon-large" :style="{ background: `${category.color}15` }">
<view :class="['icon', category.icon]" :style="{ color: category.color }" />
</view>
<view class="cat-info">
<text class="cat-name">{{ category.name }}</text>
<text class="cat-count"> {{ tools.length }} 个工具</text>
</view>
</view>
<view class="tool-list">
<view
v-for="item in tools"
:key="item.key"
class="tool-item"
@tap="goTool(item.key)"
>
<view class="item-icon-wrap" :style="{ background: category ? `${category.color}12` : '#e8f0fe' }">
<view :class="['item-icon', item.icon]" :style="{ color: category?.color || '#1768ff' }" />
</view>
<view class="item-content">
<text class="item-name">{{ item.name }}</text>
<text class="item-desc">{{ item.desc }}</text>
</view>
<!-- <text class="item-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;
}
.cat-header {
display: flex;
align-items: center;
gap: 24rpx;
padding: 28rpx 32rpx;
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;
}
.cat-icon-large {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
font-size: 44rpx;
}
.cat-info {
flex: 1;
}
.cat-name {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 6rpx;
}
.cat-count {
display: block;
font-size: 24rpx;
color: #86909c;
}
.tool-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;
}
.tool-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx 28rpx;
border-bottom: 1rpx solid #f2f3f5;
}
.tool-item:last-child {
border-bottom: none;
}
.tool-item:active {
background: #f7f8fa;
}
.item-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-icon {
font-size: 34rpx;
}
.item-content {
flex: 1;
min-width: 0;
}
.item-name {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #1d2129;
margin-bottom: 4rpx;
}
.item-desc {
display: block;
font-size: 22rpx;
color: #86909c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-arrow {
font-size: 24rpx;
color: #c9cdd4;
flex-shrink: 0;
}
</style>

199
src/pages/toolbox/index.vue Normal file
View File

@@ -0,0 +1,199 @@
<script setup lang="ts">
import { toolboxCategories, getCategoryHotTools } from '@/config/toolboxRegistry'
definePage({
style: {
navigationBarTitleText: '实用工具',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
function goTool(key: string) {
uni.navigateTo({
url: `/pages/toolbox/query?key=${encodeURIComponent(key)}`,
})
}
function goCategory(categoryKey: string) {
uni.navigateTo({
url: `/pages/toolbox/category?category=${encodeURIComponent(categoryKey)}`,
})
}
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<view
v-for="cat in toolboxCategories"
:key="cat.key"
class="card"
>
<!-- 分类标题 -->
<view class="card-header" @tap="goCategory(cat.key)">
<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 class="tool-grid">
<view
v-for="item in getCategoryHotTools(cat.key)"
:key="item.key"
class="tool-cell"
@tap="goTool(item.key)"
>
<view class="tool-icon-wrap" :style="{ background: `${cat.color}12` }">
<view :class="['tool-icon', item.icon]" :style="{ color: cat.color }" />
</view>
<text class="tool-name">{{ item.name }}</text>
</view>
</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;
}
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.card-header-left {
display: flex;
align-items: center;
gap: 16rpx;
}
.cat-icon-wrap {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cat-icon {
font-size: 32rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #1d2129;
}
.card-more {
display: flex;
align-items: center;
gap: 4rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
background: #f2f3f5;
}
.more-text {
font-size: 22rpx;
color: #86909c;
}
.more-arrow {
font-size: 22rpx;
color: #c9cdd4;
}
.card-more:active {
opacity: 0.7;
}
.tool-grid {
display: flex;
flex-wrap: wrap;
margin: 0 -6rpx;
}
.tool-cell {
width: 33.333%;
padding: 6rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 12rpx;
padding-bottom: 12rpx;
}
.tool-cell:active {
opacity: 0.7;
}
.tool-icon-wrap {
width: 76rpx;
height: 76rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.tool-icon {
font-size: 36rpx;
}
.tool-name {
font-size: 24rpx;
font-weight: 500;
color: #1d2129;
text-align: center;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

610
src/pages/toolbox/query.vue Normal file
View File

@@ -0,0 +1,610 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getToolboxItem } from '@/config/toolboxRegistry'
import { postToolboxQuery } from '@/api/toolbox'
definePage({
style: {
navigationBarTitleText: '工具查询',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
enablePullDownRefresh: false,
},
})
const toolKey = ref('')
const tool = ref<ReturnType<typeof getToolboxItem>>(null)
const form = ref<Record<string, string>>({})
const loading = ref(false)
const result = ref<Record<string, any> | null>(null)
const error = ref('')
// 选择框相关状态
const popup = ref<any>(null)
const currentField = ref<any>(null)
const pickerValue = ref([0])
const indicatorStyle = `'height: 50px'`
onLoad((query) => {
const key = (query?.key as string) || ''
toolKey.value = key
tool.value = getToolboxItem(key)
if (!tool.value) {
error.value = '未找到该工具'
}
else {
uni.setNavigationBarTitle({ title: tool.value.name })
// 初始化表单默认值
if (tool.value.fields) {
tool.value.fields.forEach(field => {
if (field.default !== undefined && !form.value[field.key]) {
form.value[field.key] = field.default
}
})
}
// 如果工具标记了自动查询或不需要输入参数,打开即查
if (tool.value.autoQuery || tool.value.fields.length === 0) {
handleQuery()
}
}
})
// 获取选择框当前选中项的索引
function getSelectIndex(field, value) {
if (!field.options || !Array.isArray(field.options)) {
return 0
}
const defaultValue = field.options[0]?.value || ''
const targetValue = value || defaultValue
return field.options.findIndex(opt => opt.value === targetValue)
}
// 获取选择框当前选中项的标签
function getSelectedLabel(field, value) {
if (!field.options || !Array.isArray(field.options) || !value) {
return null
}
const option = field.options.find(opt => opt.value === value)
return option ? option.label : null
}
// 显示自定义选择器
function showPicker(field) {
currentField.value = field
const currentValue = form.value[field.key] || field.options[0]?.value || ''
const index = field.options.findIndex(opt => opt.value === currentValue)
pickerValue.value = [index]
popup.value.open()
}
// 关闭选择器
function closePicker() {
popup.value.close()
}
// 确认选择
function confirmPicker() {
if (currentField.value) {
const index = pickerValue.value[0]
form.value[currentField.value.key] = currentField.value.options[index].value
closePicker()
}
}
// 选择器变化处理
function onPickerChange(e) {
pickerValue.value = e.detail.value
}
async function handleQuery() {
if (!tool.value)
return
error.value = ''
result.value = null
revealedKeys.value = new Set()
if (!tool.value.validate(form.value)) {
error.value = tool.value.validateMsg
return
}
loading.value = true
try {
const res = await postToolboxQuery(toolKey.value, form.value)
if (res.code === 200 && res.data?.result) {
result.value = res.data.result
}
else {
error.value = res.msg || '查询失败'
}
}
catch {
error.value = '网络错误,请稍后重试'
}
finally {
loading.value = false
}
}
const resultEntries = computed(() => {
if (!result.value || !tool.value?.resultLabels)
return []
return Object.entries(tool.value.resultLabels)
.filter(([key]) => result.value![key] !== undefined)
.map(([key, labelOrObj]) => {
const val = result.value![key]
const display = val === '' || val === null ? '无' : val
if (typeof labelOrObj === 'object' && labelOrObj !== null) {
return { key, label: labelOrObj.label, hidden: !!labelOrObj.hidden, value: display }
}
return { key, label: labelOrObj, hidden: false, value: display }
})
})
const revealedKeys = ref<Set<string>>(new Set())
function toggleReveal(key: string) {
if (revealedKeys.value.has(key)) {
revealedKeys.value.delete(key)
}
else {
revealedKeys.value.add(key)
}
}
const resultList = computed(() => {
if (!result.value || tool.value?.resultType !== 'list')
return []
const list = result.value.list
if (!Array.isArray(list))
return []
return list
})
const listLabelEntries = computed(() => {
if (!tool.value?.resultLabels)
return []
return Object.entries(tool.value.resultLabels)
})
</script>
<template>
<view class="page-root">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<view v-if="tool" class="card">
<view class="card-desc">
<text class="desc-text">{{ tool.desc }}</text>
</view>
<!-- 动态表单 -->
<view class="form-area">
<view v-for="field in tool.fields" :key="field.key" class="field">
<text class="field-label">{{ field.label }}</text>
<!-- 选择框 -->
<picker
v-if="field.type === 'select'"
mode="selector"
:range="field.options.map(opt => opt.label)"
:value="getSelectIndex(field, form[field.key])"
@change="(e: any) => {
const index = e.detail.value
form[field.key] = field.options[index].value
}"
>
<view class="field-input field-picker">
{{ getSelectedLabel(field, form[field.key]) || field.placeholder }}
</view>
</picker>
<!-- 日期选择器 -->
<picker
v-else-if="field.type === 'date'"
mode="date"
:value="form[field.key] || ''"
@change="(e: any) => (form[field.key] = e.detail.value)"
>
<view class="field-input field-picker">
{{ form[field.key] || field.placeholder }}
</view>
</picker>
<!-- 文本域 -->
<textarea
v-else-if="field.type === 'textarea'"
class="field-input field-textarea"
:maxlength="field.maxlength"
:placeholder="field.placeholder"
:value="form[field.key] || ''"
placeholder-class="field-ph"
@input="(e: any) => (form[field.key] = e.detail.value)"
/>
<!-- 普通输入框 -->
<input
v-else
class="field-input"
:type="field.type"
:maxlength="field.maxlength"
:placeholder="field.placeholder"
:value="form[field.key] || ''"
placeholder-class="field-ph"
confirm-type="done"
@input="(e: any) => (form[field.key] = e.detail.value)"
>
</view>
</view>
<!-- 查询按钮 -->
<button
class="query-btn"
:disabled="loading"
@tap="handleQuery"
>
{{ loading ? '查询中...' : '立即查询' }}
</button>
<!-- 错误提示 -->
<view v-if="error" class="msg-area msg-error">
<text class="msg-text">{{ error }}</text>
</view>
<!-- 结果展示 - 普通键值对 -->
<view v-if="resultEntries.length > 0 && tool?.resultType !== 'list'" class="result-area">
<view class="result-title">查询结果</view>
<view class="result-list">
<view
v-for="item in resultEntries"
:key="item.key"
class="result-row"
>
<text class="result-label">{{ item.label }}</text>
<!-- 隐藏字段点击显示/隐藏 -->
<view v-if="item.hidden" class="result-reveal-wrap">
<view
v-if="!revealedKeys.has(item.key)"
class="result-reveal-btn"
@tap="toggleReveal(item.key)"
>
点击查看
</view>
<template v-else>
<text class="result-value">{{ item.value }}</text>
<text class="result-hide-btn" @tap="toggleReveal(item.key)">收起</text>
</template>
</view>
<!-- 普通字段 -->
<text v-else class="result-value">{{ item.value }}</text>
</view>
</view>
</view>
<!-- 结果展示 - 列表型 -->
<view v-if="resultList.length > 0" class="result-area">
<view class="result-title">查询结果</view>
<view class="result-card-list">
<view
v-for="(item, idx) in resultList"
:key="idx"
class="result-card-item"
>
<view
v-for="([fieldKey, fieldLabel], fIdx) in listLabelEntries"
:key="fieldKey"
class="card-item-row"
>
<template v-if="item[fieldKey] !== undefined && item[fieldKey] !== ''">
<text v-if="fIdx === 0" class="card-item-index">{{ idx + 1 }}</text>
<text v-else class="card-item-index-placeholder" />
<text class="card-item-field-label">{{ typeof fieldLabel === 'object' ? fieldLabel.label : fieldLabel }}</text>
<text class="card-item-field-value">{{ item[fieldKey] }}</text>
</template>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty">
<text class="empty-text">未找到该工具</text>
</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;
}
.card {
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
padding: 32rpx 28rpx;
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-desc {
margin-bottom: 28rpx;
}
.desc-text {
font-size: 24rpx;
color: #86909c;
line-height: 1.5;
}
.form-area {
margin-bottom: 28rpx;
}
.field {
margin-bottom: 20rpx;
}
.field-label {
display: block;
font-size: 24rpx;
color: #4e5969;
margin-bottom: 8rpx;
}
.field-input {
width: 100%;
height: 80rpx;
background: #f7f8fa;
border: 1rpx solid #e5e6f0;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1d2129;
box-sizing: border-box;
}
.field-picker {
display: flex;
align-items: center;
color: #1d2129;
}
.field-picker:empty::before {
content: attr(placeholder);
color: #c9cdd4;
}
.field-textarea {
height: 200rpx;
padding: 16rpx 24rpx;
line-height: 1.6;
}
.field-ph {
color: #c9cdd4;
}
.query-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #1768ff, #4e8cff);
color: #ffffff;
font-size: 30rpx;
font-weight: 500;
border-radius: 16rpx;
border: none;
margin-bottom: 20rpx;
}
.query-btn[disabled] {
opacity: 0.6;
}
.msg-area {
padding: 16rpx 20rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.msg-error {
background: #fff2f0;
border: 1rpx solid #ffccc7;
}
.msg-text {
font-size: 24rpx;
color: #f53f3f;
}
.result-area {
margin-top: 8rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f2f3f5;
}
.result-title {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 16rpx;
}
.result-list {
background: #f7f8fa;
border-radius: 16rpx;
padding: 8rpx 0;
}
.result-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 24rpx;
}
.result-label {
font-size: 26rpx;
color: #86909c;
}
.result-value {
font-size: 26rpx;
color: #1d2129;
font-weight: 500;
flex: 1;
text-align: right;
}
.result-reveal-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12rpx;
}
.result-reveal-btn {
font-size: 24rpx;
color: #ffffff;
background: linear-gradient(135deg, #1768ff, #4e8cff);
padding: 6rpx 24rpx;
border-radius: 24rpx;
}
.result-hide-btn {
font-size: 22rpx;
color: #86909c;
flex-shrink: 0;
}
.empty {
display: flex;
justify-content: center;
align-items: center;
padding: 120rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #86909c;
}
/* 列表型结果卡片 */
.result-card-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.result-card-item {
background: #ffffff;
border: 1rpx solid #e5e6f0;
border-radius: 16rpx;
padding: 20rpx 24rpx;
}
.card-item-row {
display: flex;
align-items: flex-start;
gap: 12rpx;
padding: 4rpx 0;
}
.card-item-index {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align: center;
background: linear-gradient(135deg, #1768ff, #4e8cff);
color: #ffffff;
font-size: 22rpx;
font-weight: 600;
border-radius: 50%;
}
.card-item-index-placeholder {
flex-shrink: 0;
width: 40rpx;
}
.card-item-field-label {
flex-shrink: 0;
font-size: 24rpx;
color: #86909c;
min-width: 80rpx;
}
.card-item-field-value {
flex: 1;
font-size: 26rpx;
color: #1d2129;
line-height: 1.6;
font-weight: 500;
}
/* 自定义选择器样式 */
.picker-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20rpx;
height: 88rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.picker-header text {
font-size: 32rpx;
}
.picker-header text:first-child {
color: #606266;
}
.picker-header text:last-child {
color: #1768ff;
}
.picker-view {
width: 100%;
height: 400rpx;
}
.picker-item {
line-height: 100rpx;
text-align: center;
font-size: 28rpx;
color: #333;
}
</style>