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

9
.env Normal file
View File

@@ -0,0 +1,9 @@
# 客户端可读变量必须以 VITE_ 开头
# 不配置 VITE_API_BASE_URL 时,逻辑在 src/api/http.js 的 resolveBaseUrl()
# - H5默认 /api/v1配合 vite.config.js 里 proxy
# - 非 H5默认 https://www.tianyuancha.cn/api/v1
#
# 需要覆盖时再取消注释其一:
# VITE_API_BASE_URL=/api/v1
VITE_API_BASE_URL=http://127.0.0.1:8888/api/v1
# VITE_API_BASE_URL=https://www.tianyuancha.cn/api/v1

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=true

15
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"recommendations": [
"vue.volar",
"mrmaoddxxaa.create-uniapp-view",
"uni-helper.uni-helper-vscode",
"uni-helper.uni-app-schemas-vscode",
"uni-helper.uni-highlight-vscode",
"uni-helper.uni-ui-snippets-vscode",
"uni-helper.uni-app-snippets-vscode",
"uni-helper.uni-cloud-snippets-vscode",
"dbaeumer.vscode-eslint",
"antfu.unocss",
"wot-ui.wot-ui-intellisense"
]
}

37
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,37 @@
{
"files.associations": {
"pages.json": "jsonc",
"manifest.json": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"prettier.enable": false,
"editor.formatOnSave": false,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"nvue",
"uvue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

32
components.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by vite-plugin-uni-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
AppFooter: typeof import('./src/components/AppFooter.vue')['default']
AppLogos: typeof import('./src/components/AppLogos.vue')['default']
InputEntry: typeof import('./src/components/InputEntry.vue')['default']
VehicleBlockFallback: typeof import('./src/components/report/VehicleBlockFallback.vue')['default']
VehicleBlockQCXG1H7Y: typeof import('./src/components/report/blocks/VehicleBlockQCXG1H7Y.vue')['default']
VehicleBlockQCXG1U4U: typeof import('./src/components/report/blocks/VehicleBlockQCXG1U4U.vue')['default']
VehicleBlockQCXG3Y6B: typeof import('./src/components/report/blocks/VehicleBlockQCXG3Y6B.vue')['default']
VehicleBlockQCXG3Z3L: typeof import('./src/components/report/blocks/VehicleBlockQCXG3Z3L.vue')['default']
VehicleBlockQCXG4D2E: typeof import('./src/components/report/blocks/VehicleBlockQCXG4D2E.vue')['default']
VehicleBlockQCXG4I1Z: typeof import('./src/components/report/blocks/VehicleBlockQCXG4I1Z.vue')['default']
VehicleBlockQCXG5F3A: typeof import('./src/components/report/blocks/VehicleBlockQCXG5F3A.vue')['default']
VehicleBlockQCXG5U0Z: typeof import('./src/components/report/blocks/VehicleBlockQCXG5U0Z.vue')['default']
VehicleBlockQCXG6B4E: typeof import('./src/components/report/blocks/VehicleBlockQCXG6B4E.vue')['default']
VehicleBlockQCXG7A2B: typeof import('./src/components/report/blocks/VehicleBlockQCXG7A2B.vue')['default']
VehicleBlockQCXG9P1C: typeof import('./src/components/report/blocks/VehicleBlockQCXG9P1C.vue')['default']
VehicleBlockQCXGGB2Q: typeof import('./src/components/report/blocks/VehicleBlockQCXGGB2Q.vue')['default']
VehicleBlockQCXGP00W: typeof import('./src/components/report/blocks/VehicleBlockQCXGP00W.vue')['default']
VehicleBlockQCXGY7F2: typeof import('./src/components/report/blocks/VehicleBlockQCXGY7F2.vue')['default']
VehicleBlockQCXGYTS2: typeof import('./src/components/report/blocks/VehicleBlockQCXGYTS2.vue')['default']
VehicleReportBlock: typeof import('./src/components/report/VehicleReportBlock.vue')['default']
VehicleReportShell: typeof import('./src/components/report/VehicleReportShell.vue')['default']
}
}

3
eslint.config.js Normal file
View File

@@ -0,0 +1,3 @@
import uniHelper from '@uni-helper/eslint-config'
export default uniHelper()

21
index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="static/logo.svg">
<script>
const coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)')
|| CSS.supports('top: constant(a)'))
document.write(
`<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0${
coverSupport ? ', viewport-fit=cover' : ''}" />`)
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

27
jsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
"types": [
"vite/client",
"@dcloudio/types",
"@mini-types/alipay",
"miniprogram-api-typings",
"@uni-helper/uni-types",
"@uni-helper/vite-plugin-uni-pages",
"vitest-environment-uniapp/types",
"uni-echarts/global",
"z-paging/types",
"@wot-ui/ui/global.d.ts"
]
},
"vueCompilerOptions": {
"plugins": [
"@uni-helper/uni-types/volar-plugin"
]
}
}

80
manifest.config.js Normal file
View File

@@ -0,0 +1,80 @@
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
export default defineManifestConfig({
'name': '',
'appid': '',
'description': '',
'versionName': '1.0.0',
'versionCode': '100',
'transformPx': false,
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
permissions: [
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
'<uses-permission android:name="android.permission.VIBRATE"/>',
'<uses-permission android:name="android.permission.READ_LOGS"/>',
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.CAMERA"/>',
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
'<uses-feature android:name="android.hardware.camera"/>',
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
],
},
/* ios打包配置 */
ios: {},
/* SDK配置 */
sdkConfigs: {},
},
},
/* 快应用特有相关 */
'quickapp': {},
/* 小程序特有相关 */
'mp-weixin': {
appid: '',
setting: {
urlCheck: false,
},
usingComponents: true,
darkmode: true,
themeLocation: 'theme.json',
},
'mp-alipay': {
usingComponents: true,
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
'h5': {
darkmode: true,
themeLocation: 'theme.json',
},
'uniStatistics': {
enable: false,
},
'vueVersion': '3',
})

88
package.json Normal file
View File

@@ -0,0 +1,88 @@
{
"name": "qnc-uniapp",
"type": "module",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "unh dev",
"build": "unh build",
"about": "unh info",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest",
"dev:mp-weixin": "unh dev mp-weixin",
"build:mp-weixin": "unh build mp-weixin"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-5000720260410001",
"@dcloudio/uni-app-harmony": "3.0.0-5000720260410001",
"@dcloudio/uni-app-plus": "3.0.0-5000720260410001",
"@dcloudio/uni-components": "3.0.0-5000720260410001",
"@dcloudio/uni-h5": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-alipay": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-baidu": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-harmony": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-jd": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-lark": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-qq": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
"@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
"@iconify/vue": "^5.0.1",
"@uni-helper/uni-network": "^0.23.1",
"@uni-helper/uni-promises": "^0.2.1",
"@uni-helper/uni-use": "^0.19.17",
"@vueuse/core": "9.13.0",
"@wot-ui/ui": "^2.0.0-alpha.9",
"crypto-js": "^4.2.0",
"echarts": "^6.0.0",
"pinia": "2.2.4",
"uni-echarts": "^2.5.1",
"vue": "3.4.21",
"vue-i18n": "9.6.2",
"vue-router": "4.5.1",
"z-paging": "^2.8.8"
},
"devDependencies": {
"@binbinji/vite-plugin-component-placeholder": "^0.0.15",
"@dcloudio/types": "3.4.28",
"@dcloudio/uni-automator": "3.0.0-5000720260410001",
"@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
"@dcloudio/uni-stacktracey": "3.0.0-5000720260410001",
"@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001",
"@iconify-json/carbon": "^1.2.20",
"@mini-types/alipay": "^3.0.14",
"@uni-helper/eslint-config": "^0.7.1",
"@uni-helper/plugin-uni": "0.1.0",
"@uni-helper/unh": "^0.3.1",
"@uni-helper/uni-types": "^1.0.0-alpha.8",
"@uni-helper/unocss-preset-uni": "^0.2.11",
"@uni-helper/vite-plugin-uni-components": "^0.2.10",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.11",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.12",
"@uni-helper/vite-plugin-uni-pages": "^0.3.24",
"@uni-helper/vite-plugin-uni-platform": "^0.0.5",
"@uni-ku/root": "^1.4.1",
"@vue/runtime-core": "3.4.21",
"eslint": "^10.2.1",
"miniprogram-api-typings": "^5.1.2",
"sass": "^1.78.0",
"unocss": "66.0.0",
"vite": "5.2.8",
"vitest": "^4.1.4",
"vitest-environment-uniapp": "^0.0.5"
},
"pnpm": {
"overrides": {
"unconfig": "7.3.2"
}
},
"overrides": {
"unconfig": "7.3.2"
},
"resolutions": {
"unconfig": "7.3.2"
}
}

42
pages.config.js Normal file
View File

@@ -0,0 +1,42 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
pages: [],
globalStyle: {
backgroundColor: '@bgColor',
backgroundColorBottom: '@bgColorBottom',
backgroundColorTop: '@bgColorTop',
backgroundTextStyle: '@bgTxtStyle',
navigationBarBackgroundColor: '#000000',
navigationBarTextStyle: '@navTxtStyle',
navigationBarTitleText: 'Uni Creator',
navigationStyle: 'custom',
},
subPackages: [],
tabBar: {
color: '#7A7E83',
selectedColor: '#1768FF',
borderStyle: 'black',
backgroundColor: '#ffffff',
list: [
{
pagePath: 'pages/index',
text: '首页',
iconPath: 'static/tabbar/home.png',
selectedIconPath: 'static/tabbar/home-active.png',
},
{
pagePath: 'pages/report',
text: '报告',
iconPath: 'static/tabbar/report.png',
selectedIconPath: 'static/tabbar/report-active.png',
},
{
pagePath: 'pages/mine',
text: '我的',
iconPath: 'static/tabbar/mine.png',
selectedIconPath: 'static/tabbar/mine-active.png',
},
],
},
})

14721
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')
const wv = path.resolve(root, '..', 'tyc-webview-v2', 'src', 'views')
function extractTemplate(vueSrc) {
const m = vueSrc.match(/<template>([\s\S]*?)<\/template>/)
return m ? m[1].trim() : ''
}
function convert(html) {
let s = html
s = s.replace(/<div class="p-4">/g, '<view>')
s = s.replace(/<div\b/g, '<view')
s = s.replace(/<\/div>/g, '</view>')
s = s.replace(/<span\b/g, '<text')
s = s.replace(/<\/span>/g, '</text>')
s = s.replace(/class="text-blue-500"/g, 'class="legal-link"')
s = s.replace(/class="mt-4 text-right text-sm text-gray-600"/g, 'class="legal-date legal-date-right"')
s = s.replace(/class="text-right text-sm"/g, 'class="legal-date legal-date-right"')
s = s.replace(/class="text-sm"/g, 'class="legal-date"')
s = s.replace(/class="mb-2 font-semibold"/g, 'class="legal-subhead"')
s = s.replace(/class="font-semibold"/g, 'class="legal-strong"')
s = s.replace(/class="mb-2 font-bold leading-relaxed"/g, 'class="legal-h3"')
s = s.replace(/class="mb-2 font-bold"/g, 'class="legal-h3"')
s = s.replace(/class="mb-4 text-center text-lg font-bold"/g, 'class="legal-page-title"')
s = s.replace(/class="indent-\[2em\]"/g, 'class="legal-indent"')
s = s.replace(/class="mb-4 leading-relaxed"/g, 'class="legal-block"')
s = s.replace(/class="leading-relaxed"/g, 'class="legal-para"')
s = s.replace(/class="mb-4"/g, 'class="legal-section"')
s = s.replace(/class="mt-2 leading-relaxed"/g, 'class="legal-block legal-mt"')
s = s.replace(/class="mt-2 font-bold"/g, 'class="legal-h4"')
s = s.replace(/class="mt-2"/g, 'class="legal-mt"')
s = s.replace(/class="mt-4"/g, 'class="legal-mt-lg"')
s = s.replace(/class="my-2"/g, 'class="legal-my"')
s = s.replace(/<p\b/g, '<view class="legal-p"')
s = s.replace(/<\/p>/g, '</view>')
s = s.replace(/<ol\b[^>]*>/g, '<view class="legal-ol">')
s = s.replace(/<\/ol>/g, '</view>')
s = s.replace(/<ul\b[^>]*>/g, '<view class="legal-ul">')
s = s.replace(/<\/ul>/g, '</view>')
s = s.replace(/<li>/g, '<view class="legal-li">')
s = s.replace(/<\/li>/g, '</view>')
return s
}
const definePageSnippet = title => `definePage({
style: {
navigationBarTitleText: '${title}',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})`
function makePage(title, inner) {
return `<script setup lang="ts">
${definePageSnippet(title)}
</script>
<template>
<view class="legal-root">
<scroll-view scroll-y class="legal-scroll" :show-scrollbar="true">
<view class="legal-inner">
${inner}
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
@import './legal.scss';
</style>
`
}
const ua = fs.readFileSync(path.join(wv, 'UserAgreement.vue'), 'utf8')
const pp = fs.readFileSync(path.join(wv, 'PrivacyPolicy.vue'), 'utf8')
const uaBody = convert(extractTemplate(ua))
const ppBody = convert(extractTemplate(pp))
const outDir = path.join(root, 'src', 'pages', 'legal')
fs.mkdirSync(outDir, { recursive: true })
fs.writeFileSync(path.join(outDir, 'user-agreement.vue'), makePage('用户协议', uaBody))
fs.writeFileSync(path.join(outDir, 'privacy-policy.vue'), makePage('隐私政策', ppBody))
console.log('OK', outDir)

44
src/App.ku.vue Normal file
View File

@@ -0,0 +1,44 @@
<script setup>
function handleClickGithub() {
if (window?.open) {
window.open('https://github.com/uni-ku/root')
}
else {
uni.showToast({
icon: 'none',
title: '请使用浏览器打开',
})
}
}
</script>
<template>
<div>
<KuRootView />
</div>
</template>
<style>
.root-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
line-height: 40px;
text-align: center;
box-sizing: border-box;
padding: 0 20px;
}
.color-blue {
color: #409eff;
cursor: pointer;
}
</style>

22
src/App.vue Normal file
View File

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

35
src/api/auth.js Normal file
View File

@@ -0,0 +1,35 @@
import { http } from './http'
/**
* 发送短信验证码。
* 注意:与 `/user/mobileCodeLogin` 校验的 Redis 键一致,必须使用 actionType `login`
*(后端 mobileCodeLogin 固定校验 `login:` 前缀;新用户同一接口会自动注册)。
*/
export async function postAuthSendSms({ mobile, captchaVerifyParam = '' }) {
const res = await http.post('/auth/sendSms', {
mobile,
actionType: 'login',
captchaVerifyParam,
})
return res.data
}
/** 绑定手机号场景Redis 键为 bindMobile:(须与后端 BindMobile 校验一致) */
export async function postAuthSendSmsBindMobile({ mobile, captchaVerifyParam = '' }) {
const res = await http.post('/auth/sendSms', {
mobile,
actionType: 'bindMobile',
captchaVerifyParam,
})
return res.data
}
/** 查询页发短信Redis 键为 `query:`,与 H5 `InquireForm` actionType `query` 一致) */
export async function postAuthSendSmsQuery({ mobile, captchaVerifyParam = '' }) {
const res = await http.post('/auth/sendSms', {
mobile,
actionType: 'query',
captchaVerifyParam,
})
return res.data
}

163
src/api/http.js Normal file
View File

@@ -0,0 +1,163 @@
import un from '@uni-helper/uni-network'
/** 未单独配置登录页时401 回到「我的」 */
const AUTH_FALLBACK_PAGE = '/pages/mine'
const TOKEN_KEY = 'token'
const SILENT_TOAST_CODES = new Set([200002, 200003, 200004, 100009])
let loadingCount = 0
function showRequestLoading() {
if (loadingCount++ === 0) {
uni.showLoading({ title: '加载中...', mask: true })
}
}
function hideRequestLoading() {
if (--loadingCount <= 0) {
loadingCount = 0
uni.hideLoading()
}
}
/** H5 与其它端分支由 uni 条件编译裁剪,源码中并存会触发 no-unreachable */
function resolveBaseUrl() {
const fromEnv = import.meta.env.VITE_API_BASE_URL
if (fromEnv)
return fromEnv.replace(/\/$/, '')
/* eslint-disable no-unreachable */
// #ifdef H5
return '/api/v1'
// #endif
// #ifndef H5
return 'https://www.tianyuancha.cn/api/v1'
// #endif
/* eslint-enable no-unreachable */
}
/** 对齐 tyc-webview-v2 useApiFetchh5 / wxh5其它端单独标识 */
function getXPlatform() {
let platform = 'h5'
// #ifdef H5
const ua = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''
if (/micromessenger/.test(ua))
platform = 'wxh5'
// #endif
// #ifdef MP-WEIXIN
// 须与 tyc-server-v2 model.PlatformWxMini"wxmini")一致,勿用 mp-weixin否则 JWT 生成会失败
platform = 'wxmini'
// #endif
// #ifdef MP-ALIPAY
platform = 'mp-alipay'
// #endif
// #ifdef APP-PLUS
platform = 'app'
// #endif
return platform
}
function readToken() {
try {
const t = uni.getStorageSync(TOKEN_KEY)
return typeof t === 'string' ? t : ''
}
catch {
return ''
}
}
export function clearAuthStorage() {
const keys = ['token', 'refreshAfter', 'accessExpire', 'userInfo', 'agentInfo']
for (const k of keys) {
try {
uni.removeStorageSync(k)
}
catch {
/* ignore */
}
}
}
function handleHttpUnauthorized() {
clearAuthStorage()
uni.showToast({ title: '登录已失效', icon: 'none' })
uni.reLaunch({ url: AUTH_FALLBACK_PAGE })
}
function handleForceLogout() {
clearAuthStorage()
uni.reLaunch({ url: '/pages/index' })
}
function toastBizIfNeeded(body) {
if (body.code === 200)
return
if (SILENT_TOAST_CODES.has(body.code))
return
const text = body.msg || '请求失败'
uni.showToast({ title: text, icon: 'none' })
}
/**
* 与 tyc-webview-v2 `useApiFetch` 对齐的实例:
* - baseUrl `/api/v1`H5 默认,可配 VITE_API_BASE_URL
* - Header`Authorization`、`X-Platform`
* - Query`t` 时间戳防缓存
* - 业务码401、100009、非 200 的 Toast 策略与 H5 一致(无 Pinia 时不重置 store
*/
export const http = un.create({
baseUrl: resolveBaseUrl(),
timeout: 60_000,
dataType: 'json',
headers: {
'Content-Type': 'application/json',
},
})
http.interceptors.request.use((config) => {
if (!config.skipLoading)
showRequestLoading()
const token = readToken()
config.headers = {
...config.headers,
'X-Platform': getXPlatform(),
...(token ? { Authorization: token } : {}),
}
config.params = {
...(config.params || {}),
t: Date.now(),
}
return config
})
http.interceptors.response.use(
(response) => {
if (!response.config?.skipLoading)
hideRequestLoading()
const status = response.status ?? 0
if (status === 401) {
handleHttpUnauthorized()
return response
}
const body = response.data
if (body && typeof body === 'object' && 'code' in body) {
if (body.code === 100009)
handleForceLogout()
else if (!response.config?.skipBizToast)
toastBizIfNeeded(body)
}
return response
},
(error) => {
const errCfg = error?.config || error?.response?.config
if (!errCfg?.skipLoading)
hideRequestLoading()
const status = error?.response?.status
if (status === 401)
handleHttpUnauthorized()
else if (!errCfg?.skipBizToast && typeof error?.message === 'string' && error.message)
uni.showToast({ title: error.message, icon: 'none' })
return Promise.reject(error)
},
)

8
src/api/index.js Normal file
View File

@@ -0,0 +1,8 @@
export * from './auth'
export { clearAuthStorage, http } from './http'
export * from './pay'
export * from './product'
export * from './query'
export * from './toolbox'
export * from './upload'
export * from './user'

7
src/api/pay.js Normal file
View File

@@ -0,0 +1,7 @@
import { http } from './http'
/** 发起支付(与 H5 `Payment.vue` 一致) */
export async function postPayPayment(body, requestConfig) {
const res = await http.post('/pay/payment', body, requestConfig)
return res.data
}

8
src/api/product.js Normal file
View File

@@ -0,0 +1,8 @@
import { http } from './http'
/** 与 H5 `Inquire.vue` 一致GET /api/v1/product/en/:product_en */
export async function getProductByEn(productEn) {
const enc = encodeURIComponent(productEn)
const res = await http.get(`/product/en/${enc}`)
return res.data
}

37
src/api/query.js Normal file
View File

@@ -0,0 +1,37 @@
import { http } from './http'
/** 示例报告(与 H5 `Example.vue` 一致GET /api/v1/query/example?feature= */
export async function getQueryExample(feature) {
const enc = encodeURIComponent(feature)
const res = await http.get(`/query/example?feature=${enc}`)
return res.data
}
/** 按订单号查报告详情(需 JWT与 H5 同源 Query 结构) */
export async function getQueryDetailByOrderNo(orderNo, requestConfig) {
const enc = encodeURIComponent(orderNo)
const res = await http.get(`/query/orderNo/${enc}`, requestConfig)
return res.data
}
/** 按订单 ID 查报告详情(与 GET /query/orderId/:id 一致) */
export async function getQueryDetailByOrderId(orderId, requestConfig) {
const enc = encodeURIComponent(String(orderId))
const res = await http.get(`/query/orderId/${enc}`, requestConfig)
return res.data
}
/** 当前用户历史查询列表GET /query/list */
export async function getQueryList(params = {}, requestConfig) {
const page = params.page != null ? Number(params.page) : 1
const pageSize = params.pageSize != null ? Number(params.pageSize) : 20
const res = await http.get(`/query/list?page=${page}&page_size=${pageSize}`, requestConfig)
return res.data
}
/** 创建查询临时单(与 H5 `InquireForm` 一致POST /api/v1/query/service/:product */
export async function postQueryService(productEn, body, requestConfig) {
const enc = encodeURIComponent(productEn)
const res = await http.post(`/query/service/${enc}`, body, requestConfig)
return res.data
}

17
src/api/toolbox.js Normal file
View File

@@ -0,0 +1,17 @@
import { http } from './http'
/**
* 工具箱统一接口
* @param {string} toolKey - 工具标识(对应 toolboxRegistry 中的 key
* @param {Record<string, any>} params - 工具参数
* @returns {Promise<{code: number, msg: string, data: {tool_key: string, result: Record<string, any>}}>}
*/
export async function postToolboxQuery(toolKey, params = {}) {
const res = await http.post('/toolbox/query', {
tool_key: toolKey,
params,
}, {
skipBizToast: true,
})
return res.data
}

7
src/api/upload.js Normal file
View File

@@ -0,0 +1,7 @@
import { http } from './http'
/** 行驶证等图片上传,返回可访问 URL与 H5 `InquireForm` `/upload/image` 一致) */
export async function postUploadImage(imageBase64, requestConfig) {
const res = await http.post('/upload/image', { image_base64: imageBase64 }, requestConfig)
return res.data
}

31
src/api/user.js Normal file
View File

@@ -0,0 +1,31 @@
import { http } from './http'
/** 微信小程序:使用 `uni.login` 得到的 `code` 换 tokentyc-server-v2 WxMiniAuth */
export async function postUserWxMiniAuth(body, requestConfig) {
const res = await http.post('/user/wxMiniAuth', body, requestConfig)
return res.data
}
/** 已登录用户绑定手机号(需 JWT见 tyc-server-v2 BindMobile */
export async function postUserBindMobile(body) {
const res = await http.post('/user/bindMobile', body)
return res.data
}
/** 手机号 + 短信验证码登录(新用户自动注册,见 tyc-server-v2 MobileCodeLoginLogic */
export async function postUserMobileCodeLogin(body) {
const res = await http.post('/user/mobileCodeLogin', body)
return res.data
}
/** 刷新 accessToken对齐 tyc-webview-v2 App.vue `refreshToken` */
export async function postUserGetToken() {
const res = await http.post('/user/getToken')
return res.data
}
/** 用户信息(对齐 tyc-webview-v2 userStore.fetchUserInfo */
export async function getUserDetail() {
const res = await http.get('/user/detail')
return res.data
}

View File

@@ -0,0 +1,24 @@
<script setup>
function handleClickGithub() {
if (window?.open) {
window.open('https://github.com/uni-helper/create-uni')
}
else {
uni.showToast({
icon: 'none',
title: '请使用浏览器打开',
})
}
}
</script>
<template>
<view
i-carbon:logo-github
absolute
bottom-1rem left="50%"
translate-x="-50%"
color="#888"
@click="handleClickGithub"
/>
</template>

View File

@@ -0,0 +1,26 @@
<template>
<view inline-flex cursor-default text-2xl font-300>
<view
flex
flex-col
items-center
hover-class="drop-shadow-md drop-shadow-color-green5"
>
<image inline-block h-18 w-18 src="/static/logo.svg" />
<text mt--2 text-green5 class="uni-helper-logo__label">
uni-helper
</text>
</view>
<view
text="3xl gray4"
m="x-4 y-auto"
i-carbon-add transform transition-all-500 hover:rotate-135
/>
<view flex flex-col hover-class="drop-shadow-md drop-shadow-color-purple5">
<image inline-block h-18 w-18 src="/static/vite.png" />
<text mt--2 text-purple5>
Vite
</text>
</view>
</view>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
import { ref } from 'vue'
const name = ref('')
const popupShow = ref(false)
function handleClick() {
popupShow.value = true
}
</script>
<template>
<view class="input-box">
<input
v-model="name"
placeholder="What's your name?"
>
</view>
<view>
<wd-button :disabled="!name" @click="handleClick">
Hello
</wd-button>
</view>
<wd-popup v-model="popupShow" custom-style="padding: 30px 40px;">
Hello{{ ` ${name}` }} 👏
</wd-popup>
</template>
<style scoped lang="scss">
.input-box {
margin: 1rem;
padding: 0.5rem;
border-bottom: 1px solid gray;
}
</style>

View File

@@ -0,0 +1,69 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
})
const bodyText = computed(() => {
const p = props.payload
if (p === '000' || p === 0)
return '(本模块暂无示例明细)'
if (p == null || p === '')
return '暂无数据'
if (typeof p === 'string')
return p
try {
return JSON.stringify(p, null, 2)
}
catch {
return String(p)
}
})
</script>
<template>
<view class="block-root">
<view class="hint">
以下为结构化数据预览专用版式请在VehicleReportBlock + blocks中按 apiID 注册勿只改文件名
</view>
<!-- 微信小程序scroll-view 须明确 height仅用 max-height 易导致高度为 0正文不显示 -->
<view class="pre-wrap">
<text class="pre" selectable>{{ bodyText }}</text>
</view>
</view>
</template>
<style scoped lang="scss">
.block-root {
width: 100%;
}
.hint {
font-size: 22rpx;
color: #86909c;
margin-bottom: 12rpx;
line-height: 1.4;
}
.pre-wrap {
width: 100%;
max-height: 520rpx;
overflow-y: auto;
background: #f7f8fa;
border-radius: 12rpx;
padding: 16rpx;
box-sizing: border-box;
}
.pre {
font-size: 22rpx;
color: #1d2129;
font-family: ui-monospace, monospace;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.45;
}
</style>

View File

@@ -0,0 +1,143 @@
<script setup>
/**
* 微信小程序端不支持动态 <component :is>,此处按 apiID 静态分支。
* 新增模块import 对应块并在下方增加一组 v-else-if。
*/
import VehicleBlockQCXG1H7Y from './blocks/VehicleBlockQCXG1H7Y.vue'
import VehicleBlockQCXG1U4U from './blocks/VehicleBlockQCXG1U4U.vue'
import VehicleBlockQCXG3Y6B from './blocks/VehicleBlockQCXG3Y6B.vue'
import VehicleBlockQCXG3Z3L from './blocks/VehicleBlockQCXG3Z3L.vue'
import VehicleBlockQCXG4D2E from './blocks/VehicleBlockQCXG4D2E.vue'
import VehicleBlockQCXG4I1Z from './blocks/VehicleBlockQCXG4I1Z.vue'
import VehicleBlockQCXG5F3A from './blocks/VehicleBlockQCXG5F3A.vue'
import VehicleBlockQCXG5U0Z from './blocks/VehicleBlockQCXG5U0Z.vue'
import VehicleBlockQCXG6B4E from './blocks/VehicleBlockQCXG6B4E.vue'
import VehicleBlockQCXG7A2B from './blocks/VehicleBlockQCXG7A2B.vue'
import VehicleBlockQCXG9P1C from './blocks/VehicleBlockQCXG9P1C.vue'
import VehicleBlockQCXGGB2Q from './blocks/VehicleBlockQCXGGB2Q.vue'
import VehicleBlockQCXGP00W from './blocks/VehicleBlockQCXGP00W.vue'
import VehicleBlockQCXGY7F2 from './blocks/VehicleBlockQCXGY7F2.vue'
import VehicleBlockQCXGYTS2 from './blocks/VehicleBlockQCXGYTS2.vue'
import VehicleBlockFallback from './VehicleBlockFallback.vue'
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
</script>
<template>
<VehicleBlockQCXG1H7Y
v-if="props.apiId === 'QCXG1H7Y'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG1U4U
v-else-if="props.apiId === 'QCXG1U4U'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG3Y6B
v-else-if="props.apiId === 'QCXG3Y6B'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG3Z3L
v-else-if="props.apiId === 'QCXG3Z3L'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG4D2E
v-else-if="props.apiId === 'QCXG4D2E'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG4I1Z
v-else-if="props.apiId === 'QCXG4I1Z'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG5F3A
v-else-if="props.apiId === 'QCXG5F3A'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG5U0Z
v-else-if="props.apiId === 'QCXG5U0Z'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG6B4E
v-else-if="props.apiId === 'QCXG6B4E'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG7A2B
v-else-if="props.apiId === 'QCXG7A2B'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXG9P1C
v-else-if="props.apiId === 'QCXG9P1C'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXGGB2Q
v-else-if="props.apiId === 'QCXGGB2Q'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXGP00W
v-else-if="props.apiId === 'QCXGP00W'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXGY7F2
v-else-if="props.apiId === 'QCXGY7F2'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockQCXGYTS2
v-else-if="props.apiId === 'QCXGYTS2'"
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
:query-params="props.queryParams"
/>
<VehicleBlockFallback
v-else
:block-title="props.blockTitle"
:api-id="props.apiId"
:payload="props.payload"
/>
</template>

View File

@@ -0,0 +1,164 @@
<script setup>
import { computed } from 'vue'
import { getVehicleModuleTitle } from '@/config/vehicleReportRegistry'
import VehicleReportBlock from './VehicleReportBlock.vue'
const props = defineProps({
productName: { type: String, default: '' },
queryParams: { type: Object, default: () => ({}) },
rows: { type: Array, default: () => [] },
/** 示例 / 正式 */
mode: { type: String, default: 'example' },
})
const modeText = computed(() => (props.mode === 'detail' ? '查询报告' : '示例报告'))
const paramLines = computed(() => {
const p = props.queryParams
if (!p || typeof p !== 'object')
return []
return Object.entries(p).map(([k, v]) => `${k}${String(v ?? '')}`)
})
function blockTitle(row) {
return getVehicleModuleTitle(row.apiId, row.featureName)
}
</script>
<template>
<view class="shell">
<view class="head-card">
<view class="head-title">
{{ props.productName || '报告' }}
</view>
<view class="head-badge">
免责声明
</view>
<view class="head-sub">
本平台所提供车辆信息报告仅作个人参考使用不构成任何投资交易及法律依据数据来源于正规合规接口仅供自查比对严禁用于商用倒卖及其他违规用途因用户私自滥用数据造成的一切后果均由使用者自行承担本平台不承担任何法律责任
</view>
</view>
<!-- <view v-if="paramLines.length" class="card params-card">
<view class="section-head">
<view class="accent" />
<text class="section-t">查询条件</text>
</view>
<view v-for="(line, idx) in paramLines" :key="idx" class="param-line">
{{ line }}
</view>
</view> -->
<view v-for="(row, idx) in props.rows" :key="`${row.apiId}-${idx}`" class="card block-card">
<view class="block-head">
<text class="block-title">{{ blockTitle(row) }}</text>
<text v-if="row.apiId && row.apiId !== '__UNLABELED__'" class="block-id">{{ row.apiId }}</text>
</view>
<VehicleReportBlock
:block-title="blockTitle(row)"
:api-id="row.apiId"
:payload="row.payload"
:query-params="props.queryParams"
/>
</view>
</view>
</template>
<style scoped lang="scss">
.shell {
width: 100%;
}
.head-card {
background: #fff;
border-radius: 16rpx;
padding: 28rpx 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 0 24rpx rgba(63, 63, 63, 0.06);
}
.head-title {
font-size: 32rpx;
font-weight: 700;
color: #1d2129;
text-align: center;
padding-bottom: 12rpx;
border-bottom: 1rpx solid #e5e6eb;
}
.head-badge {
margin-top: 12rpx;
text-align: center;
font-size: 24rpx;
color: #1768ff;
font-weight: 600;
}
.head-sub {
margin-top: 12rpx;
text-align: center;
font-size: 22rpx;
color: #86909c;
line-height: 1.45;
}
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 0 24rpx rgba(63, 63, 63, 0.06);
}
.section-head {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.accent {
width: 6rpx;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(180deg, #1768ff 0%, #1556d6 100%);
margin-right: 12rpx;
}
.section-t {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.param-line {
font-size: 24rpx;
color: #4e5969;
line-height: 1.55;
padding: 6rpx 0;
}
.block-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 16rpx;
padding-bottom: 12rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.block-title {
flex: 1;
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.block-id {
font-size: 20rpx;
color: #86909c;
font-family: ui-monospace, monospace;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,214 @@
<script setup>
import { computed } from 'vue'
/** 车辆过户简版(与 H5 CQCXG1H7Y */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const d = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const hasData = computed(() => Object.keys(d.value).length > 0)
const flag = computed(() => d.value.transferFlag)
const flagText = computed(() => {
if (flag.value === '1' || flag.value === 1)
return '已过户'
if (flag.value === '0' || flag.value === 0)
return '未过户'
return '未知'
})
const flagDetailText = computed(() => {
if (flag.value === '1' || flag.value === 1)
return '该车辆存在过户记录'
if (flag.value === '0' || flag.value === 0)
return '该车辆暂无过户记录'
return '未能识别过户状态'
})
const bandClass = computed(() => {
if (flag.value === '1' || flag.value === 1)
return 'band yes'
if (flag.value === '0' || flag.value === 0)
return 'band no'
return 'band unk'
})
const formattedTransferDate = computed(() => {
const raw = d.value.transferDate
if (!raw)
return '-'
if (raw === '近一年内过户')
return '近一年内过户'
const s = String(raw)
if (s.length === 6) {
const y = s.slice(0, 4)
const m = s.slice(4, 6)
return `${y}${m}`
}
return s
})
const transferNum = computed(() => {
const n = d.value.transferNum
if (n === '' || n == null)
return '0'
return String(n)
})
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆过户简版查询' }}</text>
<text class="sub">最近是否发生过户及过户次数</text>
<view class="tag" :class="bandClass">
<text class="tag-l">是否过户</text>
<text class="tag-v">{{ flagText }}</text>
</view>
</view>
<view v-if="hasData" class="summary" :class="bandClass">
<text class="sum-t">最近过户情况</text>
<text class="sum-d">{{ flagDetailText }}</text>
<view class="meta">
<text class="meta-k">最近过户时间</text>
<text class="meta-v">{{ formattedTransferDate }}</text>
</view>
<view class="meta">
<text class="meta-k">累计过户次数</text>
<text class="meta-v strong">{{ transferNum }} </text>
</view>
</view>
<view v-else class="empty">
<text class="t">暂无过户信息</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #e0f2fe 0%, #dbeafe 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #0c4a6e;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #0369a1;
}
.tag {
margin-top: 16rpx;
align-self: flex-end;
background: rgba(255, 255, 255, 0.9);
border-radius: 12rpx;
padding: 12rpx 16rpx;
display: inline-flex;
flex-direction: column;
align-items: flex-end;
}
.tag-l {
font-size: 22rpx;
color: #86909c;
}
.tag-v {
font-size: 30rpx;
font-weight: 700;
color: #1d2129;
}
.tag.band.yes .tag-v {
color: #b45309;
}
.tag.band.no .tag-v {
color: #047857;
}
.summary {
border-radius: 16rpx;
padding: 24rpx;
border: 1rpx solid #e5e6eb;
}
.summary.band.yes {
background: #fffbeb;
border-color: #fde68a;
}
.summary.band.no {
background: #ecfdf5;
border-color: #a7f3d0;
}
.summary.band.unk {
background: #f7f8fa;
}
.sum-t {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.sum-d {
display: block;
margin-top: 8rpx;
font-size: 26rpx;
color: #4e5969;
}
.meta {
display: flex;
justify-content: space-between;
margin-top: 16rpx;
font-size: 26rpx;
}
.meta-k {
color: #86909c;
}
.meta-v {
color: #1d2129;
font-weight: 500;
}
.meta-v.strong {
font-weight: 700;
font-size: 28rpx;
}
.empty {
padding: 40rpx;
text-align: center;
}
.t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,345 @@
<script setup>
import { computed } from 'vue'
import { formatDateZh, formatMileageKm } from '../vehicleReportBlockMaps'
/** 车辆里程记录混合查询(与 H5 CQCXG1U4U */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const d = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const hasData = computed(() => Object.keys(d.value).length > 0)
const vin = computed(() => d.value.vehicleInfo?.vin || '')
const mileageList = computed(() => d.value.mileageInfo?.mileageList || [])
const adjustList = computed(() => d.value.mileageInfo?.suspectedAdjustMileageList || [])
const suspectedAdjust = computed(() => d.value.mileageInfo?.suspectedAdjust)
const imageUrl = computed(() => d.value.imageUrl || '')
const latestRecord = computed(() => {
const list = mileageList.value
if (!list.length)
return null
return list[list.length - 1]
})
const latestMileageText = computed(() => {
if (!latestRecord.value)
return '-'
return formatMileageKm(latestRecord.value.mileage)
})
const latestReportTime = computed(() => latestRecord.value?.reportTime || '')
const suspectedText = computed(() => {
if (suspectedAdjust.value === 'true')
return '存在异常里程行为'
if (suspectedAdjust.value === 'false')
return '未发现里程异常'
return '未知'
})
const suspectClass = computed(() => {
if (suspectedAdjust.value === 'true')
return 'sum warn'
if (suspectedAdjust.value === 'false')
return 'sum ok'
return 'sum unk'
})
function sourceText(source) {
if (source === '0')
return '诊断里程'
if (source === '1')
return '维保里程'
return '其他'
}
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆里程记录(混合查询)' }}</text>
<text class="sub">里程变化与调表嫌疑综合展示</text>
</view>
<template v-if="hasData">
<view class="sum" :class="suspectClass">
<view class="sum-row">
<view>
<text class="lab">VIN</text>
<text class="val mono">{{ vin || '-' }}</text>
</view>
<view class="right">
<text class="lab">最新里程</text>
<text class="mile">{{ latestMileageText }}</text>
<text class="hint">最近记录{{ latestReportTime || '-' }}</text>
</view>
</view>
<text class="line">里程是否异常{{ suspectedText }}</text>
<image v-if="imageUrl" class="img" :src="imageUrl" mode="widthFix" />
</view>
<view class="card">
<text class="sec">里程记录时间轴</text>
<view v-if="mileageList.length" class="tl">
<view v-for="(item, idx) in mileageList" :key="idx" class="ti">
<view class="dot-col">
<view class="dot" :class="item.mileageStatus === '1' ? 'bad' : ''" />
<view v-if="idx !== mileageList.length - 1" class="line-v" />
</view>
<view class="body">
<view class="r1">
<text>{{ formatDateZh(item.reportTime) }}</text>
<text class="km">{{ formatMileageKm(item.mileage) }}</text>
</view>
<text class="r2">
来源{{ sourceText(item.source) }}
<text v-if="item.mileageStatus === '1'" class="badg">异常里程</text>
</text>
</view>
</view>
</view>
<text v-else class="empty-s">暂无里程记录</text>
</view>
<view v-if="adjustList.length" class="card">
<text class="sec">疑似调表记录</text>
<view v-for="(item, idx) in adjustList" :key="idx" class="adj">
<text class="t">{{ formatDateZh(item.reportTime) }}</text>
<view class="adj-row">
<text>调整前 {{ formatMileageKm(item.beforeMileage) }}</text>
<text class="ar"></text>
<text>调整后 {{ formatMileageKm(item.afterMileage) }}</text>
</view>
</view>
</view>
</template>
<view v-else class="empty">
<text class="t">暂无里程数据</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #e0f2fe 0%, #dbeafe 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #0c4a6e;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #0369a1;
}
.sum {
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
border: 1rpx solid #e5e6eb;
}
.sum.warn {
background: #fffbeb;
border-color: #fde68a;
}
.sum.ok {
background: #ecfdf5;
border-color: #a7f3d0;
}
.sum.unk {
background: #f7f8fa;
}
.sum-row {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.right {
text-align: right;
}
.lab {
display: block;
font-size: 22rpx;
color: #86909c;
}
.val {
display: block;
margin-top: 4rpx;
font-size: 26rpx;
color: #1d2129;
}
.mono {
font-family: ui-monospace, monospace;
}
.mile {
display: block;
margin-top: 4rpx;
font-size: 32rpx;
font-weight: 700;
color: #1768ff;
}
.hint {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #4e5969;
}
.line {
display: block;
margin-top: 12rpx;
font-size: 26rpx;
color: #1d2129;
}
.img {
width: 100%;
margin-top: 12rpx;
border-radius: 12rpx;
}
.card {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
border: 1rpx solid #e5e6eb;
}
.sec {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 12rpx;
}
.tl {
display: flex;
flex-direction: column;
gap: 0;
}
.ti {
display: flex;
gap: 12rpx;
}
.dot-col {
display: flex;
flex-direction: column;
align-items: center;
width: 24rpx;
}
.dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #1768ff;
}
.dot.bad {
background: #f53f3f;
}
.line-v {
flex: 1;
width: 2rpx;
min-height: 40rpx;
background: #e5e6eb;
margin-top: 4rpx;
}
.body {
flex: 1;
padding-bottom: 16rpx;
}
.r1 {
display: flex;
justify-content: space-between;
font-size: 26rpx;
color: #1d2129;
}
.km {
font-weight: 600;
color: #1768ff;
}
.r2 {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #4e5969;
}
.badg {
margin-left: 12rpx;
padding: 2rpx 8rpx;
background: #ffece8;
color: #f53f3f;
border-radius: 6rpx;
font-size: 20rpx;
}
.empty-s {
font-size: 24rpx;
color: #86909c;
}
.adj {
padding: 12rpx 0;
border-bottom: 1rpx solid #e5e6eb;
}
.adj-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 8rpx;
font-size: 24rpx;
}
.ar {
color: #1768ff;
}
.empty {
padding: 40rpx;
text-align: center;
}
.t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,233 @@
<script setup>
import { computed } from 'vue'
import { formatDateZh, formatMileageKm } from '../vehicleReportBlockMaps'
/** 车辆维保简版(与 H5 CQCXG3Y6Bdata.record[] */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const d = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const hasData = computed(() => Object.keys(d.value).length > 0)
const records = computed(() => (Array.isArray(d.value.record) ? d.value.record : []))
const vin = computed(() => {
if (records.value.length && records.value[0].vin)
return records.value[0].vin
return props.queryParams?.vin_code || ''
})
const totalCount = computed(() => records.value.length)
const lastRecord = computed(() => (records.value.length ? records.value[records.value.length - 1] : null))
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆维保简版查询' }}</text>
<text class="sub">按时间轴展示维保与材料明细</text>
</view>
<template v-if="hasData">
<view class="sum">
<view class="sum-l">
<text class="lab">车架号 VIN</text>
<text class="val mono">{{ vin || '-' }}</text>
</view>
<view class="sum-r">
<text class="cnt">维保记录 {{ totalCount }} </text>
<text v-if="lastRecord" class="last">
最近一次{{ formatDateZh(lastRecord.lastTime) }} · {{ formatMileageKm(lastRecord.mileage) }}
</text>
</view>
</view>
<view v-if="records.length" class="timeline">
<view v-for="(item, idx) in records" :key="idx" class="ti">
<view class="ti-main">
<text class="date">{{ formatDateZh(item.lastTime) }}</text>
<text class="km">{{ formatMileageKm(item.mileage) }}</text>
</view>
<text class="meta-line">{{ item.repairType || '维保' }} · VIN {{ item.vin || vin || '-' }}</text>
<view v-if="item.details && item.details.length" class="box">
<text class="box-t">维修项目</text>
<view v-for="(det, di) in item.details" :key="di" class="li">
<text v-if="det.type" class="tag">{{ det.type }}</text>
<text>{{ det.content }}</text>
</view>
</view>
<view v-if="item.materials && item.materials.length" class="box">
<text class="box-t">使用材料</text>
<view v-for="(m, mi) in item.materials" :key="mi" class="li">
<text v-if="m.type" class="tag m">{{ m.type }}</text>
<text>{{ m.content }}</text>
</view>
</view>
</view>
</view>
<view v-else class="empty-s">
<text>暂无维保记录</text>
</view>
</template>
<view v-else class="empty">
<text class="t">暂无维保数据</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #ecfdf5 0%, #e0f2fe 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #14532d;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #166534;
}
.sum {
display: flex;
justify-content: space-between;
gap: 16rpx;
background: #ecfdf5;
border: 1rpx solid #bbf7d0;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.lab {
display: block;
font-size: 22rpx;
color: #86909c;
}
.val {
display: block;
margin-top: 6rpx;
font-size: 26rpx;
color: #1d2129;
}
.mono {
font-family: ui-monospace, monospace;
}
.sum-r {
text-align: right;
}
.cnt {
display: block;
font-size: 26rpx;
font-weight: 600;
color: #1d2129;
}
.last {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: #166534;
}
.timeline {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.ti {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.ti-main {
display: flex;
justify-content: space-between;
}
.date {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.km {
font-size: 26rpx;
color: #1768ff;
font-weight: 600;
}
.meta-line {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: #4e5969;
}
.box {
margin-top: 12rpx;
padding-top: 12rpx;
border-top: 1rpx dashed #e5e6eb;
}
.box-t {
display: block;
font-size: 24rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 8rpx;
}
.li {
font-size: 24rpx;
color: #4e5969;
margin-bottom: 6rpx;
}
.tag {
display: inline-block;
margin-right: 8rpx;
padding: 2rpx 10rpx;
border-radius: 6rpx;
background: #1768ff;
color: #fff;
font-size: 20rpx;
}
.tag.m {
background: #059669;
}
.empty,
.empty-s {
padding: 32rpx;
text-align: center;
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,182 @@
<script setup>
import { computed } from 'vue'
import { formatDateZh, formatMileageKm } from '../vehicleReportBlockMaps'
/** 车辆维保详细版(与 H5 CQCXG3Z3L */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const d = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const hasData = computed(() => Object.keys(d.value).length > 0)
const records = computed(() => (Array.isArray(d.value.record) ? d.value.record : []))
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆维保详细版查询' }}</text>
<text class="sub">品牌VIN 及每次维保详细内容</text>
</view>
<template v-if="hasData">
<view class="sum">
<view class="row2">
<view>
<text class="lab">品牌名称</text>
<text class="val">{{ d.brandName || '未知品牌' }}</text>
</view>
<view>
<text class="lab">车架号 VIN</text>
<text class="val mono">{{ d.vin || '-' }}</text>
</view>
</view>
<text class="meta">车牌{{ d.licensePlate || '未提供' }} · 发动机号{{ d.engine || '-' }}</text>
<text v-if="records.length" class="meta">
维保 {{ records.length }} · 最近 {{ formatDateZh(records[records.length - 1].date) }}
</text>
</view>
<view v-if="records.length" class="timeline">
<view v-for="(item, idx) in records" :key="idx" class="ti">
<view class="ti-main">
<text class="date">{{ formatDateZh(item.date) }}</text>
<text class="km">{{ formatMileageKm(item.mileage) }}</text>
</view>
<text class="type">{{ item.type || '维保' }}</text>
<text v-if="item.content" class="block">维修内容{{ item.content }}</text>
<text v-if="item.material" class="block">材料{{ item.material }}</text>
<text v-if="item.remark" class="block">备注{{ item.remark }}</text>
</view>
</view>
<view v-else class="empty-s">
<text>暂无维保记录</text>
</view>
</template>
<view v-else class="empty">
<text class="t">暂无维保数据</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #eef2ff 0%, #ecfdf5 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #312e81;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #4338ca;
}
.sum {
background: #eef2ff;
border: 1rpx solid #c7d2fe;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.row2 {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.lab {
display: block;
font-size: 22rpx;
color: #86909c;
}
.val {
display: block;
margin-top: 4rpx;
font-size: 26rpx;
font-weight: 600;
color: #1d2129;
}
.mono {
font-family: ui-monospace, monospace;
}
.meta {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
color: #4e5969;
}
.timeline {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.ti {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.ti-main {
display: flex;
justify-content: space-between;
}
.date {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.km {
font-size: 26rpx;
color: #1768ff;
font-weight: 600;
}
.type {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #4e5969;
}
.block {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #1d2129;
line-height: 1.45;
}
.empty,
.empty-s {
padding: 32rpx;
text-align: center;
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,135 @@
<script setup>
import { computed } from 'vue'
import { plateColorLabel, vehicleTypeLabel } from '../vehicleReportBlockMaps'
/** 名下车辆数量(与 H5 CQCXG4D2EvehicleCount + list[] 对齐) */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const p = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const vehicleCount = computed(() => {
const n = p.value.vehicleCount
if (n === '' || n == null)
return null
const num = Number(n)
return Number.isFinite(num) ? num : null
})
const vehicleList = computed(() => {
const list = p.value.list
return Array.isArray(list) ? list : []
})
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle }}</text>
<view class="badge">
<text class="badge-t"> {{ vehicleCount != null ? vehicleCount : '—' }} </text>
</view>
</view>
<view v-if="vehicleList.length" class="list">
<view v-for="(v, idx) in vehicleList" :key="idx" class="item">
<text class="plate">{{ String(v.plateNum ?? '—') }}</text>
<view class="row">
<text class="tag">{{ plateColorLabel(v.plateColor) }}</text>
<text class="meta">类型{{ vehicleTypeLabel(v.vehicleType) }}</text>
</view>
</view>
</view>
<view v-else class="empty">
<text class="empty-t">暂无车辆信息</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
margin-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.title {
font-size: 30rpx;
font-weight: 700;
color: #1d2129;
}
.badge {
background: #e8f1ff;
border-radius: 999rpx;
padding: 8rpx 20rpx;
}
.badge-t {
font-size: 24rpx;
font-weight: 600;
color: #1768ff;
}
.list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.item {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.plate {
font-size: 32rpx;
font-weight: 700;
color: #1d2129;
letter-spacing: 2rpx;
}
.row {
margin-top: 12rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.tag {
font-size: 22rpx;
color: #fff;
background: #1768ff;
padding: 6rpx 14rpx;
border-radius: 8rpx;
}
.meta {
font-size: 24rpx;
color: #4e5969;
}
.empty {
padding: 48rpx 0;
text-align: center;
}
.empty-t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,239 @@
<script setup>
import { computed } from 'vue'
/** 车辆过户详版(与 H5 CQCXG4I1Zdata.retdata[] */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const rawList = computed(() => {
const p = props.payload
if (!p || typeof p !== 'object')
return []
return Array.isArray(p.retdata) ? p.retdata : []
})
const transfers = computed(() =>
rawList.value.map((item) => {
const changeMonth = item.changeMonth
let changeMonthFormatted = '-'
if (changeMonth === '近一年内过户') {
changeMonthFormatted = '近一年内过户'
}
else if (typeof changeMonth === 'string' && changeMonth.length === 6) {
const y = changeMonth.slice(0, 4)
const m = changeMonth.slice(4, 6)
changeMonthFormatted = `${y}${m}`
}
else if (changeMonth) {
changeMonthFormatted = String(changeMonth)
}
let intervalText = '-'
if (item.transYear || item.transMonth) {
const years = item.transYear ? `${item.transYear}` : ''
const months = item.transMonth ? `${item.transMonth}个月` : ''
intervalText = `${years}${months}` || '-'
}
return {
...item,
changeMonthFormatted,
intervalText,
}
}),
)
const totalTimes = computed(() => {
if (!transfers.value.length)
return ''
const last = transfers.value[transfers.value.length - 1]
return last.transTimeSum ?? ''
})
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆过户详版查询' }}</text>
<text class="sub">按时间轴展示过户与车牌变更</text>
<view v-if="totalTimes" class="tag">
<text class="tag-l">总过户次数</text>
<text class="tag-v">{{ totalTimes }} </text>
</view>
</view>
<view v-if="transfers.length" class="list">
<view v-for="(item, index) in transfers" :key="index" class="item">
<view class="item-h">
<text class="date">{{ item.changeMonthFormatted }}</text>
<text class="cnt"> {{ item.transTimeSum }} 次过户</text>
</view>
<view class="plates">
<view class="plate">
<text class="lab">过户前车牌</text>
<text class="num">{{ item.oldCp || '未知' }}</text>
<text v-if="item.cityBefore" class="city">所在城市{{ item.cityBefore }}</text>
</view>
<text class="arrow"></text>
<view class="plate">
<text class="lab">过户后车牌</text>
<text class="num">{{ item.newCp || '未知' }}</text>
<text v-if="item.cityAfter" class="city">所在城市{{ item.cityAfter }}</text>
</view>
</view>
<text class="gap">距上次过户{{ item.intervalText }}</text>
<text class="vin">VIN{{ item.vin || '-' }}</text>
</view>
</view>
<view v-else class="empty">
<text class="t">暂无过户明细</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #312e81;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #4338ca;
}
.tag {
margin-top: 12rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 12rpx;
padding: 12rpx 16rpx;
display: inline-flex;
flex-direction: column;
}
.tag-l {
font-size: 22rpx;
color: #86909c;
}
.tag-v {
font-size: 30rpx;
font-weight: 700;
color: #312e81;
}
.list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.item {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.item-h {
display: flex;
justify-content: space-between;
margin-bottom: 12rpx;
}
.date {
font-size: 28rpx;
font-weight: 700;
color: #1d2129;
}
.cnt {
font-size: 24rpx;
color: #86909c;
}
.plates {
display: flex;
align-items: stretch;
gap: 12rpx;
}
.plate {
flex: 1;
background: #fff;
border-radius: 12rpx;
padding: 12rpx;
border: 1rpx solid #e5e6eb;
}
.lab {
display: block;
font-size: 22rpx;
color: #86909c;
}
.num {
display: block;
margin-top: 6rpx;
font-size: 28rpx;
font-weight: 700;
color: #1d2129;
}
.city {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #4e5969;
}
.arrow {
align-self: center;
font-size: 28rpx;
color: #1768ff;
}
.gap {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
color: #4e5969;
}
.vin {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #1d2129;
font-family: ui-monospace, monospace;
}
.empty {
padding: 40rpx;
text-align: center;
}
.t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,135 @@
<script setup>
import { computed } from 'vue'
import { plateColorLabel, vehicleTypeLabel } from '../vehicleReportBlockMaps'
/** 名下车辆车牌查询 B与 H5 QCXG5F3AvehicleCount + list[] 对齐) */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const p = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const vehicleCount = computed(() => {
const n = p.value.vehicleCount
if (n === '' || n == null)
return null
const num = Number(n)
return Number.isFinite(num) ? num : null
})
const vehicleList = computed(() => {
const list = p.value.list
return Array.isArray(list) ? list : []
})
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle }}</text>
<view class="badge">
<text class="badge-t"> {{ vehicleCount != null ? vehicleCount : '—' }} </text>
</view>
</view>
<view v-if="vehicleList.length" class="list">
<view v-for="(v, idx) in vehicleList" :key="idx" class="item">
<text class="plate">{{ String(v.plateNum ?? '—') }}</text>
<view class="row">
<text class="tag">{{ plateColorLabel(v.plateColor) }}</text>
<text class="meta">类型{{ vehicleTypeLabel(v.vehicleType) }}</text>
</view>
</view>
</view>
<view v-else class="empty">
<text class="empty-t">暂无车辆信息</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
margin-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.title {
font-size: 30rpx;
font-weight: 700;
color: #1d2129;
}
.badge {
background: #e8f1ff;
border-radius: 999rpx;
padding: 8rpx 20rpx;
}
.badge-t {
font-size: 24rpx;
font-weight: 600;
color: #1768ff;
}
.list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.item {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.plate {
font-size: 32rpx;
font-weight: 700;
color: #1d2129;
letter-spacing: 2rpx;
}
.row {
margin-top: 12rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.tag {
font-size: 22rpx;
color: #fff;
background: #1768ff;
padding: 6rpx 14rpx;
border-radius: 8rpx;
}
.meta {
font-size: 24rpx;
color: #4e5969;
}
.empty {
padding: 48rpx 0;
text-align: center;
}
.empty-t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,202 @@
<script setup>
import { computed } from 'vue'
/** 车辆静态信息(与 H5 CQCXG5U0Z */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const records = computed(() => {
const raw = props.payload
if (!raw)
return []
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? parsed : []
}
catch {
return []
}
}
if (Array.isArray(raw))
return raw
if (typeof raw === 'object') {
if (Array.isArray(raw.list))
return raw.list
if (typeof raw.data === 'string') {
try {
const parsed = JSON.parse(raw.data)
return Array.isArray(parsed) ? parsed : []
}
catch {
return []
}
}
}
return []
})
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆静态信息查询' }}</text>
<text class="sub">生产排放燃料等静态信息</text>
</view>
<template v-if="records.length">
<view v-for="(item, idx) in records" :key="idx" class="card">
<view class="card-head">
<text class="chip">车辆 {{ idx + 1 }}</text>
<text class="model">{{ item.vType || '未知车型' }}</text>
<text class="badge">{{ item.vFuelType || '燃料未知' }}</text>
</view>
<view class="grid">
<view class="cell">
<text class="lab">发动机号</text>
<text class="val mono">{{ item.engineNO || '-' }}</text>
</view>
<view class="cell">
<text class="lab">发动机型号</text>
<text class="val mono">{{ item.engineType || '-' }}</text>
</view>
<view class="cell">
<text class="lab">生产日期</text>
<text class="val">{{ item.vScdate || '-' }}</text>
</view>
<view class="cell">
<text class="lab">排放阶段</text>
<text class="val">{{ item.dischargeStage || '-' }}</text>
</view>
<view class="cell">
<text class="lab">车辆分类</text>
<text class="val">{{ item.vClassification || '-' }}</text>
</view>
<view class="cell span">
<text class="lab">生产企业名称</text>
<text class="val">{{ item.vManufacturer || '-' }}</text>
</view>
<view class="cell span">
<text class="lab">生产厂地址</text>
<text class="val">{{ item.vSccdz || '-' }}</text>
</view>
</view>
</view>
</template>
<view v-else class="empty">
<text class="t">暂无车辆静态信息</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #e8f1ff 0%, #eef2ff 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #1d2129;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #4e5969;
}
.card {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
border: 1rpx solid #e5e6eb;
}
.card-head {
margin-bottom: 16rpx;
}
.chip {
display: inline-block;
font-size: 22rpx;
color: #1768ff;
background: #e8f1ff;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.model {
display: block;
margin-top: 8rpx;
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.badge {
display: inline-block;
margin-top: 8rpx;
font-size: 22rpx;
color: #1556d6;
background: #fff;
padding: 4rpx 12rpx;
border-radius: 999rpx;
}
.grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx 24rpx;
}
.cell {
width: 47%;
min-width: 280rpx;
}
.cell.span {
width: 100%;
}
.lab {
display: block;
font-size: 22rpx;
color: #86909c;
margin-bottom: 4rpx;
}
.val {
font-size: 26rpx;
color: #1d2129;
word-break: break-all;
}
.mono {
font-family: ui-monospace, monospace;
}
.empty {
padding: 40rpx;
text-align: center;
}
.t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,489 @@
<script setup>
import { computed } from 'vue'
/** 车辆出险记录核验(与 H5 CQCXG6B4E */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const data = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const hasData = computed(() => Object.keys(data.value).length > 0)
const usedCarPriceText = computed(() => {
const v = data.value.UsedCarPrice
if (!v)
return '-'
return `${v}`
})
const newCarPriceText = computed(() => {
const v = data.value.PurchasePrice
if (!v)
return '-'
return `${v}`
})
const carAgeText = computed(() => {
const m = data.value.CarAge
if (!m)
return '-'
return `${m} 个月`
})
const totalLossText = computed(() => {
const v = data.value.TotalLoss
if (v === '1')
return '存在全损记录'
if (v === '0')
return '无全损记录'
return '-'
})
const riskLevelText = computed(() => {
if (data.value.IfHighriskVehicle === '1')
return '高风险'
if (data.value.IsMajorAccidentLevel && data.value.IsMajorAccidentLevel !== '一般')
return '较高风险'
if (data.value.IsMajorAccidentData && data.value.IsMajorAccidentData !== '0')
return '有事故记录'
return '风险可控'
})
const riskBandClass = computed(() => {
const t = riskLevelText.value
if (t === '高风险')
return 'band high'
if (t === '较高风险' || t === '有事故记录')
return 'band mid'
return 'band low'
})
function flagClass(flag) {
return flag ? 'yes' : 'no'
}
function yesNoText(val, yesText, noText = '否') {
if (val === '1')
return yesText
if (val === '0')
return noText
return '-'
}
function ynText(val, yesText, noText) {
if (val === 'Y')
return yesText
if (val === 'N')
return noText
return '-'
}
function formatPolicyTime(val, label) {
if (!val || val === 'NULL')
return `当期无${label}保单`
const parts = String(val).split(':')
if (parts.length < 2)
return val
const daysRaw = parts[1]
if (!daysRaw || daysRaw.toLowerCase() === 'null')
return `${label}保单已过期`
const days = Number(daysRaw)
if (Number.isNaN(days))
return val
if (days < 0)
return `${label}保单已过期`
return `${label}保单剩余 ${days}`
}
function formatDangerCount(val, label) {
if (!val)
return '-'
const parts = String(val).split(':')
const countRaw = parts[1] ?? ''
if (!countRaw || countRaw.toLowerCase() === 'null')
return `${label}暂无出险记录`
const count = Number(countRaw)
if (Number.isNaN(count))
return val
if (count === 0)
return `${label}暂无出险记录`
return `${label}出险 ${count}`
}
function formatTransferCount(val) {
if (!val)
return '-'
const parts = String(val).split(':')
const countRaw = parts[1] ?? ''
if (!countRaw || countRaw.toLowerCase() === 'null')
return '-'
const count = Number(countRaw)
if (Number.isNaN(count))
return val
return `${count}`
}
function formatMajorAccident(val) {
if (!val)
return '-'
const map = { A: '碰撞', B: '火自燃', C: '水淹', D: '盗抢' }
const list = []
String(val).split(',').forEach((pair) => {
const [k, v] = pair.split(':')
if (v === '1' && map[k])
list.push(map[k])
})
if (!list.length)
return '无重大事故记录'
return `重大事故类型:${list.join('、')}`
}
function formatLossPart(val) {
if (!val)
return '-'
const partMap = {
1: '正前方',
2: '正后方',
3: '顶部',
4: '底部',
5: '前方左侧',
6: '后方左侧',
7: '中间左侧',
8: '前方右侧',
9: '后方右侧',
10: '中间右侧',
11: '内部',
12: '其它',
13: '不详',
}
const items = []
String(val).split(',').forEach((pair) => {
const [kRaw, vRaw] = pair.split(':')
const key = Number(kRaw)
const count = Number(vRaw)
if (!Number.isNaN(key) && !Number.isNaN(count) && count > 0) {
const label = partMap[key] || `部位${key}`
items.push(`${label}${count}`)
}
})
if (!items.length)
return '暂无损失部位信息'
return items.join('、')
}
</script>
<template>
<view class="wrap">
<view v-if="hasData" class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆出险记录核验' }}</text>
<text class="sub">综合出险脱保重大事故等信息</text>
</view>
<view class="risk" :class="riskBandClass">
<text class="rl">风险等级</text>
<text class="rv">{{ riskLevelText }}</text>
</view>
<view class="card">
<view class="top">
<view>
<text v-if="data.LicensePlate" class="plate">{{ data.LicensePlate }}</text>
<text class="ct">{{ data.CarType || '未知车型' }}</text>
</view>
<view class="pr">
<text class="lab">二手车参考价</text>
<text class="price">{{ usedCarPriceText }}</text>
<text class="hint">新车购置价 {{ newCarPriceText }}</text>
</view>
</view>
<text class="meta">燃料 {{ data.FuelType || '未知' }} · 发动机号 {{ data.EngineNumber || '-' }}</text>
<text class="meta">初登 {{ data.DebutDate || '-' }} · 车龄 {{ carAgeText }}</text>
</view>
<view class="card">
<text class="sec">核心风险指标</text>
<view class="row">
<text class="k">高风险车辆</text>
<text class="v" :class="flagClass(data.IfHighriskVehicle === '1')">{{ yesNoText(data.IfHighriskVehicle, '是', '否') }}</text>
</view>
<view class="row">
<text class="k">营运车辆</text>
<text class="v" :class="flagClass(data.IsOperation === '1')">{{ yesNoText(data.IsOperation, '是', '否') }}</text>
</view>
<view class="row">
<text class="k">车损险</text>
<text class="v" :class="flagClass(data.IfCarDamage === '1')">{{ yesNoText(data.IfCarDamage, '已投保', '未投保') }}</text>
</view>
<view class="row">
<text class="k">连续投保</text>
<text class="v" :class="flagClass(data.IsConInsure === '1')">{{ yesNoText(data.IsConInsure, '是', '否') }}</text>
</view>
<view class="row">
<text class="k">历史脱保</text>
<text class="v" :class="flagClass(data.IfTuoBao === '1')">{{ yesNoText(data.IfTuoBao, '有', '无') }}</text>
</view>
<view class="row">
<text class="k">最大脱保时间</text>
<text class="v">{{ data.TuoBaoTime || '-' }}</text>
</view>
<view class="row">
<text class="k">最高车损险损失比例</text>
<text class="v">{{ data.CompensationRatioo || '-' }}</text>
</view>
<view class="row">
<text class="k">车损险综合评分</text>
<text class="v strong">{{ data.Total || '-' }}</text>
</view>
</view>
<view class="card">
<text class="sec">出险与事故</text>
<view class="row">
<text class="k">商业险出险</text>
<text class="v">{{ formatDangerCount(data.CommercialPolicyDangerCount, '商业险') }}</text>
</view>
<view class="row">
<text class="k">交强险出险</text>
<text class="v">{{ formatDangerCount(data.CompulsoryPolicyDangerCount, '交强险') }}</text>
</view>
<view class="row">
<text class="k">三者险出险</text>
<text class="v">{{ formatDangerCount(data.ThreeRisksDangerCount, '三者险') }}</text>
</view>
<view class="row">
<text class="k">全损情况</text>
<text class="v">{{ totalLossText }}</text>
</view>
<view class="row">
<text class="k">重大事故标志</text>
<text class="v">{{ formatMajorAccident(data.MajorAccident) }}</text>
</view>
<view class="row">
<text class="k">事故次数</text>
<text class="v">{{ data.IsMajorAccidentData || '-' }}</text>
</view>
<view class="row">
<text class="k">事故等级</text>
<text class="v">{{ data.IsMajorAccidentLevel || '-' }}</text>
</view>
<view class="row">
<text class="k">损失部位</text>
<text class="v">{{ formatLossPart(data.LossPart) }}</text>
</view>
</view>
<view class="card">
<text class="sec">保单与承保</text>
<view class="row">
<text class="k">商业险保单</text>
<text class="v">{{ formatPolicyTime(data.CommercialPolicyTime, '商业险') }}</text>
</view>
<view class="row">
<text class="k">交强险保单</text>
<text class="v">{{ formatPolicyTime(data.CompulsoryPolicyTime, '交强险') }}</text>
</view>
<view class="row">
<text class="k">商业险过户次数</text>
<text class="v">{{ formatTransferCount(data.CommercialPolicyTransferCount) }}</text>
</view>
<view class="row">
<text class="k">交强险过户次数</text>
<text class="v">{{ formatTransferCount(data.CompulsoryPolicyTransferCount) }}</text>
</view>
<view class="row">
<text class="k">责任险可投保</text>
<text class="v" :class="flagClass(data.IsLiabilityAvailable === 'Y')">{{ ynText(data.IsLiabilityAvailable, '可投保', '不可投保') }}</text>
</view>
<view class="row">
<text class="k">延保可承保</text>
<text class="v" :class="flagClass(data.IsExtendAvailable === 'Y')">{{ ynText(data.IsExtendAvailable, '可承保', '不可承保') }}</text>
</view>
</view>
</view>
<view v-else class="empty">
<text class="t">暂无出险记录</text>
</view>
</view>
</template>
<style scoped lang="scss">
.wrap {
width: 100%;
}
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #fff1f2 0%, #ffe4e6 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 12rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #9f1239;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #be123c;
}
.risk {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 12rpx;
}
.risk.band.high {
background: #ffe4e6;
border: 1rpx solid #fda4af;
}
.risk.band.mid {
background: #fffbeb;
border: 1rpx solid #fde68a;
}
.risk.band.low {
background: #ecfdf5;
border: 1rpx solid #a7f3d0;
}
.rl {
font-size: 24rpx;
color: #4e5969;
}
.rv {
font-size: 32rpx;
font-weight: 800;
color: #1d2129;
}
.card {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 12rpx;
border: 1rpx solid #e5e6eb;
}
.top {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.plate {
display: block;
font-size: 32rpx;
font-weight: 800;
color: #1d2129;
}
.ct {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
color: #4e5969;
}
.pr {
text-align: right;
}
.lab {
font-size: 22rpx;
color: #86909c;
}
.price {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #ea580c;
}
.hint {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: #4e5969;
}
.meta {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #4e5969;
}
.sec {
display: block;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 12rpx;
color: #1d2129;
}
.row {
display: flex;
padding: 10rpx 0;
border-bottom: 1rpx solid #f0f0f0;
font-size: 24rpx;
}
.row:last-child {
border-bottom: none;
}
.k {
width: 240rpx;
color: #86909c;
flex-shrink: 0;
}
.v {
flex: 1;
color: #1d2129;
word-break: break-all;
}
.v.yes {
color: #f53f3f;
font-weight: 600;
}
.v.no {
color: #00b578;
}
.v.strong {
font-weight: 700;
}
.empty {
padding: 40rpx;
text-align: center;
}
.t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,55 @@
<script setup>
import { computed } from 'vue'
/** 名下车辆(与 H5 CQCXG7A2B 字段 carNum 对齐) */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const carNum = computed(() => {
const p = props.payload
if (!p || typeof p !== 'object')
return null
const n = Number(p.carNum)
return Number.isFinite(n) ? n : null
})
</script>
<template>
<view class="card">
<view class="banner">
<text class="h3">{{ props.blockTitle || '名下车辆' }}</text>
<text class="sub">此人名下拥有车辆{{ carNum != null ? `${carNum}` : '—' }}</text>
</view>
</view>
</template>
<style scoped lang="scss">
.card {
width: 100%;
}
.banner {
background: linear-gradient(135deg, #fef9c3 0%, #fef08a 100%);
border-radius: 16rpx;
padding: 24rpx;
border: 1rpx solid #fde047;
}
.h3 {
display: block;
font-size: 32rpx;
font-weight: 700;
color: #854d0e;
margin-bottom: 12rpx;
}
.sub {
font-size: 26rpx;
color: #a16207;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,135 @@
<script setup>
import { computed } from 'vue'
import { plateColorLabel, vehicleTypeLabel } from '../vehicleReportBlockMaps'
/** 名下车辆车牌查询 A与 H5 CQCXG9P1CvehicleCount + list[] 对齐) */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const p = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const vehicleCount = computed(() => {
const n = p.value.vehicleCount
if (n === '' || n == null)
return null
const num = Number(n)
return Number.isFinite(num) ? num : null
})
const vehicleList = computed(() => {
const list = p.value.list
return Array.isArray(list) ? list : []
})
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle }}</text>
<view class="badge">
<text class="badge-t"> {{ vehicleCount != null ? vehicleCount : '—' }} </text>
</view>
</view>
<view v-if="vehicleList.length" class="list">
<view v-for="(v, idx) in vehicleList" :key="idx" class="item">
<text class="plate">{{ String(v.plateNum ?? '—') }}</text>
<view class="row">
<text class="tag">{{ plateColorLabel(v.plateColor) }}</text>
<text class="meta">类型{{ vehicleTypeLabel(v.vehicleType) }}</text>
</view>
</view>
</view>
<view v-else class="empty">
<text class="empty-t">暂无车辆信息</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
margin-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.title {
font-size: 30rpx;
font-weight: 700;
color: #1d2129;
}
.badge {
background: #e8f1ff;
border-radius: 999rpx;
padding: 8rpx 20rpx;
}
.badge-t {
font-size: 24rpx;
font-weight: 600;
color: #1768ff;
}
.list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.item {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.plate {
font-size: 32rpx;
font-weight: 700;
color: #1d2129;
letter-spacing: 2rpx;
}
.row {
margin-top: 12rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.tag {
font-size: 22rpx;
color: #fff;
background: #1768ff;
padding: 6rpx 14rpx;
border-radius: 8rpx;
}
.meta {
font-size: 24rpx;
color: #4e5969;
}
.empty {
padding: 48rpx 0;
text-align: center;
}
.empty-t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,188 @@
<script setup>
import { computed } from 'vue'
/** 人车核验简版(与 H5 CQCXGGB2Qverify_code */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const p = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const maskedName = computed(() => {
const name = props.queryParams?.name || ''
if (!name)
return '-'
return name.length > 1 ? `${name[0]}${'*'.repeat(name.length - 1)}` : '*'
})
const isMatch = computed(() => {
const code = p.value.verify_code
const n = Number(code)
if (n === 1)
return true
if (n === 2)
return false
return null
})
const resultText = computed(() => {
if (isMatch.value === true)
return '一致'
if (isMatch.value === false)
return '不匹配'
return '暂无结果'
})
const sectionClass = computed(() => {
if (isMatch.value === true)
return 'section match'
if (isMatch.value === false)
return 'section mismatch'
return 'section unknown'
})
const iconChar = computed(() => {
if (isMatch.value === true)
return '✓'
if (isMatch.value === false)
return '✕'
return '?'
})
const hasParams = computed(() => {
const q = props.queryParams || {}
return !!(q.name || q.plate_no || q.car_license || q.carplate_type || q.car_type)
})
</script>
<template>
<view class="root">
<view class="header">
<text class="h3">{{ props.blockTitle || '人车核验(简版)' }}</text>
<text class="desc">校验人员姓名与车辆号牌是否匹配</text>
</view>
<view :class="sectionClass">
<text class="icon">{{ iconChar }}</text>
<text class="label">核验结果</text>
<text class="value">{{ resultText }}</text>
</view>
<view v-if="hasParams" class="rows">
<view class="row">
<text class="k">姓名</text>
<text class="v">{{ maskedName }}</text>
</view>
<view class="row">
<text class="k">车牌号</text>
<text class="v mono">{{ props.queryParams?.plate_no || props.queryParams?.car_license || '-' }}</text>
</view>
<view class="row">
<text class="k">号牌类型</text>
<text class="v">{{ props.queryParams?.carplate_type || props.queryParams?.car_type || '-' }}</text>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.header {
background: linear-gradient(135deg, #1768ff 0%, #1556d6 100%);
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.h3 {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #fff;
}
.desc {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.section {
border-radius: 16rpx;
padding: 28rpx;
text-align: center;
margin-bottom: 20rpx;
}
.section.match {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border: 1rpx solid rgba(76, 175, 80, 0.35);
}
.section.mismatch {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
border: 1rpx solid rgba(244, 67, 54, 0.35);
}
.section.unknown {
background: #f7f8fa;
border: 1rpx solid #e5e6eb;
}
.icon {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #1d2129;
margin-bottom: 8rpx;
}
.label {
display: block;
font-size: 24rpx;
color: #86909c;
}
.value {
display: block;
margin-top: 8rpx;
font-size: 34rpx;
font-weight: 700;
color: #1d2129;
}
.rows {
border-top: 1rpx solid #f0f0f0;
padding-top: 16rpx;
}
.row {
display: flex;
align-items: center;
padding: 12rpx 0;
font-size: 26rpx;
}
.k {
width: 160rpx;
color: #86909c;
flex-shrink: 0;
}
.v {
flex: 1;
color: #1d2129;
font-weight: 500;
}
.mono {
font-family: ui-monospace, monospace;
}
</style>

View File

@@ -0,0 +1,242 @@
<script setup>
import { computed } from 'vue'
/** 车辆出险详版(与 H5 CQCXGP00W小程序为精简版布局 */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const retdata = computed(() => {
const p = props.payload
if (!p || typeof p !== 'object')
return {}
return p.retdata && typeof p.retdata === 'object' ? p.retdata : {}
})
const hasData = computed(() => Object.keys(retdata.value).length > 0)
const clxx = computed(() => retdata.value.clxx || {})
const tjxx = computed(() => retdata.value.tjxx || {})
const pzlsmx = computed(() => retdata.value.pzlsmx || {})
const pzRecords = computed(() => (Array.isArray(pzlsmx.value.records) ? pzlsmx.value.records : []))
const pzVin = computed(() => (pzRecords.value[0]?.vin) || '')
function formatFen(val) {
if (val !== 0 && !val)
return '-'
const n = Number(val)
if (Number.isNaN(n))
return `${val}`
return `${(n / 100).toLocaleString()}`
}
function dangerTypeText(t) {
if (t === '1')
return '更换'
if (t === '2')
return '维修'
if (t === '3')
return '材料'
return '其他'
}
</script>
<template>
<view class="root">
<view class="head">
<text class="title">{{ props.blockTitle || '车辆出险详版查询' }}</text>
<text class="sub">碰撞记录与统计精简展示</text>
</view>
<template v-if="hasData">
<view class="sum">
<view class="row2">
<view>
<text class="lab">品牌</text>
<text class="val">{{ clxx.brandName || '未知品牌' }}</text>
<text v-if="clxx.vehicleStyle" class="hint">{{ clxx.vehicleStyle }}</text>
</view>
<view>
<text class="lab">VIN</text>
<text class="val mono">{{ pzVin || clxx.vin || '-' }}</text>
<text v-if="clxx.licensePlate" class="hint">车牌 {{ clxx.licensePlate }}</text>
</view>
</view>
<view class="stats">
<text>事故总次数 {{ tjxx.claimCount ?? '-' }}</text>
<text>总维修金额 {{ tjxx.totalAmount || '-' }}</text>
<text>最大单次维修 {{ tjxx.largestAmount || '-' }}</text>
<text>已结案 {{ tjxx.claimCacCount ?? 0 }} / 未结案 {{ tjxx.claimUnCacCount ?? 0 }}</text>
</view>
</view>
<view v-if="pzRecords.length" class="card">
<text class="sec">碰撞出险记录</text>
<view v-for="(rec, idx) in pzRecords" :key="idx" class="pz">
<view class="r1">
<text>{{ rec.date || '-' }}</text>
<text class="money">{{ formatFen(rec.serviceMoney) }}</text>
</view>
<text class="r2">{{ rec.accidentType || '出险' }} · {{ rec.claimStatus || '-' }}</text>
<view v-if="rec.result && rec.result.length" class="det">
<view v-for="(d, di) in rec.result" :key="di" class="det-li">
<text class="tag">{{ dangerTypeText(d.dangerSingleType) }}</text>
<text>{{ d.dangerSingleName }}</text>
<text v-if="d.dangerSingleNum">×{{ d.dangerSingleNum }}</text>
<text v-if="d.dangerSingleMoney" class="m">{{ formatFen(d.dangerSingleMoney) }}</text>
</view>
</view>
</view>
</view>
</template>
<view v-else class="empty">
<text class="t">暂无出险详版数据</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #9a3412;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #c2410c;
}
.sum {
background: #fff7ed;
border: 1rpx solid #fed7aa;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.row2 {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.lab {
display: block;
font-size: 22rpx;
color: #86909c;
}
.val {
display: block;
margin-top: 4rpx;
font-size: 26rpx;
font-weight: 600;
color: #1d2129;
}
.mono {
font-family: ui-monospace, monospace;
}
.hint {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #4e5969;
}
.stats {
margin-top: 12rpx;
font-size: 24rpx;
color: #4e5969;
line-height: 1.6;
}
.card {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.sec {
display: block;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.pz {
padding: 16rpx 0;
border-bottom: 1rpx solid #e5e6eb;
}
.r1 {
display: flex;
justify-content: space-between;
font-size: 26rpx;
font-weight: 600;
}
.money {
color: #ea580c;
}
.r2 {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
color: #4e5969;
}
.det {
margin-top: 10rpx;
}
.det-li {
font-size: 24rpx;
color: #1d2129;
margin-bottom: 6rpx;
}
.tag {
display: inline-block;
margin-right: 8rpx;
padding: 2rpx 8rpx;
background: #1768ff;
color: #fff;
border-radius: 6rpx;
font-size: 20rpx;
}
.m {
color: #ea580c;
}
.empty {
padding: 40rpx;
text-align: center;
}
.t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,208 @@
<script setup>
import { computed } from 'vue'
/** 二手车 VIN 估值(与 H5 CQCXGY7F2 */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const d = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const hasData = computed(() => Object.keys(d.value).length > 0)
</script>
<template>
<view class="root">
<view class="head">
<view class="head-l">
<text class="title">{{ props.blockTitle || '二手车 VIN 估值' }}</text>
<text class="sub">基于车型排量排放等给出参考估值</text>
</view>
<view class="head-r">
<text class="tag-l">估值</text>
<text class="tag-v">{{ d.estimatedValue || '-' }}</text>
</view>
</view>
<template v-if="hasData">
<view class="price-box">
<text class="price">{{ d.estimatedValue || '-' }}</text>
<text class="hint">参考估值仅供参考</text>
<text class="line">{{ d.seriesName || '未知车系' }} · {{ d.manufacturerName || '未知厂商' }}</text>
<text v-if="d.productionDate" class="line">{{ d.productionDate }} 年出厂</text>
<text class="line sm">
<text v-if="d.displacement">排量 {{ d.displacement }}</text>
<text v-if="d.transmissionType"> · 变速箱 {{ d.transmissionType }}</text>
<text v-if="d.emissionStandard"> · 排放 {{ d.emissionStandard }}</text>
</text>
</view>
<view class="section">
<text class="sec-t">基础信息</text>
<view class="row">
<text class="k">厂商</text>
<text class="v">{{ d.manufacturerName || '-' }}</text>
</view>
<view class="row">
<text class="k">车系</text>
<text class="v">{{ d.seriesName || '-' }}</text>
</view>
<view class="row">
<text class="k">车型年款</text>
<text class="v">{{ d.modelYear || d.productionDate || '-' }}</text>
</view>
<view class="row">
<text class="k">座位数</text>
<text class="v">{{ d.seatingCapacity ?? '-' }}</text>
</view>
<view class="row">
<text class="k">车型名称</text>
<text class="v">{{ d.modelName || '-' }}</text>
</view>
<view class="row">
<text class="k">指导价</text>
<text class="v">{{ d.msrp || '-' }}</text>
</view>
</view>
</template>
<view v-else class="empty">
<text class="t">暂无估值结果</text>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16rpx;
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #92400e;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #b45309;
}
.head-r {
background: rgba(255, 255, 255, 0.85);
border-radius: 12rpx;
padding: 12rpx 16rpx;
text-align: right;
flex-shrink: 0;
}
.tag-l {
display: block;
font-size: 22rpx;
color: #86909c;
}
.tag-v {
font-size: 32rpx;
font-weight: 800;
color: #b45309;
}
.price-box {
background: #fffbeb;
border: 1rpx solid #fde68a;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
}
.price {
display: block;
font-size: 40rpx;
font-weight: 800;
color: #92400e;
}
.hint {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: #b45309;
}
.line {
display: block;
margin-top: 12rpx;
font-size: 26rpx;
color: #1d2129;
}
.line.sm {
font-size: 24rpx;
color: #4e5969;
}
.section {
background: #f7f8fa;
border-radius: 16rpx;
padding: 20rpx;
border: 1rpx solid #e5e6eb;
}
.sec-t {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 12rpx;
}
.row {
display: flex;
padding: 10rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.row:last-child {
border-bottom: none;
}
.k {
width: 200rpx;
color: #86909c;
flex-shrink: 0;
}
.v {
flex: 1;
color: #1d2129;
word-break: break-all;
}
.empty {
padding: 40rpx;
text-align: center;
}
.t {
font-size: 26rpx;
color: #86909c;
}
</style>

View File

@@ -0,0 +1,228 @@
<script setup>
import { computed } from 'vue'
/** 人车核验详版(与 H5 CQCXGYTS2status */
const props = defineProps({
blockTitle: { type: String, default: '' },
apiId: { type: String, default: '' },
payload: { default: null },
queryParams: { type: Object, default: () => ({}) },
})
const p = computed(() => (props.payload && typeof props.payload === 'object' ? props.payload : {}))
const maskedName = computed(() => {
const name = props.queryParams?.name || ''
if (!name)
return '-'
return name.length > 1 ? `${name[0]}${'*'.repeat(name.length - 1)}` : '*'
})
const status = computed(() => {
const s = Number(p.value.status)
if ([0, -1, -2, -4].includes(s))
return s
return null
})
const resultText = computed(() => {
const s = status.value
if (s === 0)
return '一致'
if (s === -1)
return '不一致'
if (s === -2)
return '非法姓名'
if (s === -4)
return '无记录'
return '暂无结果'
})
const resultDesc = computed(() => {
const s = status.value
if (s === -2)
return '姓名长度或格式不正确,请核对后重试'
if (s === -4)
return '未查询到相关核验记录'
return ''
})
const sectionClass = computed(() => {
const s = status.value
if (s === 0)
return 'section match'
if (s === -1)
return 'section mismatch'
if (s === -2)
return 'section invalid'
if (s === -4)
return 'section norecord'
return 'section unknown'
})
const iconChar = computed(() => {
const s = status.value
if (s === 0)
return '✓'
if (s === -1)
return '✕'
if (s === -2)
return '!'
if (s === -4)
return '—'
return '?'
})
const hasParams = computed(() => {
const q = props.queryParams || {}
return !!(q.name || q.plate_no || q.car_license || q.carplate_type || q.car_type)
})
</script>
<template>
<view class="root">
<view class="header">
<text class="h3">{{ props.blockTitle || '人车核验(详版)' }}</text>
<text class="desc">展示人员与车辆的详细匹配结果及相关说明</text>
</view>
<view :class="sectionClass">
<text class="icon">{{ iconChar }}</text>
<text class="label">认证结果</text>
<text class="value">{{ resultText }}</text>
<text v-if="resultDesc" class="subdesc">{{ resultDesc }}</text>
</view>
<view v-if="hasParams" class="rows">
<view class="row">
<text class="k">姓名</text>
<text class="v">{{ maskedName }}</text>
</view>
<view class="row">
<text class="k">车牌号</text>
<text class="v mono">{{ props.queryParams?.plate_no || props.queryParams?.car_license || '-' }}</text>
</view>
<view class="row">
<text class="k">号牌类型</text>
<text class="v">{{ props.queryParams?.carplate_type || props.queryParams?.car_type || '-' }}</text>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.root {
width: 100%;
}
.header {
background: linear-gradient(135deg, #5c6bc0 0%, #3949ab 100%);
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.h3 {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #fff;
}
.desc {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.section {
border-radius: 16rpx;
padding: 28rpx;
text-align: center;
margin-bottom: 20rpx;
}
.section.match {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border: 1rpx solid rgba(76, 175, 80, 0.35);
}
.section.mismatch {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
border: 1rpx solid rgba(244, 67, 54, 0.35);
}
.section.invalid {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 1rpx solid rgba(255, 152, 0, 0.4);
}
.section.norecord {
background: linear-gradient(135deg, #eceff1 0%, #cfd8dc 100%);
border: 1rpx solid rgba(96, 125, 139, 0.35);
}
.section.unknown {
background: #f7f8fa;
border: 1rpx solid #e5e6eb;
}
.icon {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #1d2129;
margin-bottom: 8rpx;
}
.label {
display: block;
font-size: 24rpx;
color: #86909c;
}
.value {
display: block;
margin-top: 8rpx;
font-size: 34rpx;
font-weight: 700;
color: #1d2129;
}
.subdesc {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
color: #4e5969;
line-height: 1.45;
}
.rows {
border-top: 1rpx solid #f0f0f0;
padding-top: 16rpx;
}
.row {
display: flex;
align-items: center;
padding: 12rpx 0;
font-size: 26rpx;
}
.k {
width: 160rpx;
color: #86909c;
flex-shrink: 0;
}
.v {
flex: 1;
color: #1d2129;
font-weight: 500;
}
.mono {
font-family: ui-monospace, monospace;
}
</style>

View File

@@ -0,0 +1,67 @@
/** H5 CQCXG9P1C / 4D2E 等共用的车牌颜色、车辆类型文案 */
export const PLATE_COLOR_LABELS = {
0: '蓝色',
1: '黄色',
2: '黑色',
3: '白色',
4: '渐变绿',
5: '黄绿双拼',
6: '蓝白渐变',
7: '临牌',
11: '绿色',
12: '红色',
}
export function plateColorLabel(c) {
const n = Number(c)
if (Number.isNaN(n))
return '—'
return PLATE_COLOR_LABELS[n] ?? '其他'
}
const VEHICLE_TYPE_LABELS = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车',
}
export function vehicleTypeLabel(t) {
if (t == null || t === '')
return '—'
const n = Number(t)
if (!Number.isNaN(n) && VEHICLE_TYPE_LABELS[n])
return VEHICLE_TYPE_LABELS[n]
return String(t)
}
export function formatDateZh(val) {
if (!val)
return '-'
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/)
if (m)
return `${m[1]}${m[2]}${m[3]}`
return String(val)
}
export function formatMileageKm(val) {
if (val !== 0 && !val)
return '-'
const num = Number(val)
if (Number.isNaN(num))
return `${val} km`
return `${num.toLocaleString()} km`
}

View File

@@ -0,0 +1,196 @@
import type { Ref } from 'vue'
import { computed, reactive } from 'vue'
/**
* 与 tyc-server-v2 `queryservicelogic.go` 中 `productHasSmsCode` 保持一致。
* 含短信验证码的产品在提交查询时不再强制校验图形验证码(验证码发送时已过滑块/或微信 UA 豁免)。
*/
export function productHasSmsCode(product: string): boolean {
switch (product) {
case 'marriage':
case 'homeservice':
case 'riskassessment':
case 'companyinfo':
case 'rentalinfo':
case 'preloanbackgroundcheck':
case 'personalData':
case 'toc_PersonalLawsuit':
case 'toc_EnterpriseLawsuit':
case 'toc_Marriage':
case 'toc_PersonalMarriageStatus':
case 'toc_MarriageStatusRegisterTime':
case 'toc_MarriageStatusSupplement':
case 'toc_MarriageStatusVerify':
case 'toc_DualMarriageStatusRegisterTime':
case 'toc_VehiclesUnderName':
case 'toc_VehiclesUnderNamePlate':
return true
default:
return false
}
}
/** 与 tyc-webview-v2 `composables/useInquireForm.js` 对齐 */
export function useInquireForm(featureRef: Ref<string>) {
const formData = reactive({
name: '',
idCard: '',
mobile: '',
verificationCode: '',
nameMan: '',
idCardMan: '',
nameWoman: '',
idCardWoman: '',
entName: '',
entCode: '',
carLicense: '',
carType: '',
vinCode: '',
certificateNumber: '',
userType: '1',
returnUrl: '',
imageUrl: '',
vehicleLocation: '',
firstRegistrationDate: '',
vlphotoData: '',
authorized: '1',
bankCard: '',
photoData: '',
agreeToTerms: false,
})
const defaultInput = ['name', 'idCard', 'mobile', 'verificationCode']
const productFieldConfig: Record<string, string[]> = {
marriage: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_PersonalLawsuit: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_PersonalBadRecord: ['name', 'idCard'],
toc_PersonEnterprisePro: ['idCard'],
toc_EnterpriseLawsuitQYGL66SL: ['entName'],
toc_LimitHighExecuted: ['name', 'idCard', 'mobile'],
toc_DishonestExecutedPerson: ['name', 'idCard'],
toc_PersonalMarriageStatus: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_MarriageStatusRegisterTime: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_MarriageStatusSupplement: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_MarriageStatusVerify: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_DualMarriageStatusRegisterTime: [
'nameMan',
'idCardMan',
'nameWoman',
'idCardWoman',
'mobile',
'verificationCode',
],
toc_VehiclesUnderName: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_VehiclesUnderNamePlate: ['name', 'idCard', 'mobile', 'verificationCode'],
toc_PersonVehicleVerification: ['name', 'carType', 'carLicense'],
toc_PersonVehicleVerificationDetail: ['name', 'carType', 'carLicense'],
toc_VehiclesUnderNameCount: ['userType', 'idCard'],
toc_VehicleStaticInfo: ['vinCode'],
toc_VehicleMileageMixed: ['vinCode', 'imageUrl'],
toc_VehicleVinValuation: ['vinCode', 'vehicleLocation', 'firstRegistrationDate'],
toc_VehicleTransferSimple: ['vinCode', 'carLicense'],
toc_VehicleTransferDetail: ['vinCode'],
toc_VehicleMaintenanceSimple: ['vinCode'],
toc_VehicleMaintenanceDetail: ['vinCode'],
toc_VehicleClaimDetail: ['vinCode', 'vlphotoData'],
toc_VehicleClaimVerify: ['vinCode'],
toc_PoliceTwoFactors: ['mobile', 'idCard', 'name'],
toc_PoliceThreeFactors: ['photoData', 'idCard', 'name'],
toc_ProfessionalCertificate: ['idCard', 'name'],
toc_PersonalConsumptionCapacityLevel: ['mobile', 'idCard', 'name'],
toc_OperatorTwoFactors: ['mobile', 'name'],
toc_MobileThreeFactors: ['mobile', 'idCard', 'name'],
toc_NumberRecycle: ['mobile'],
toc_MobileEmptyCheck: ['mobile'],
toc_MobilePortability: ['mobile'],
toc_MobileOnlineStatus: ['mobile'],
toc_MobileOnlineDuration: ['mobile'],
toc_MobileAttribution: ['mobile'],
toc_MobileConsumptionRange: ['mobile', 'authorized'],
toc_EnterpriseRelation: ['idCard'],
toc_BankcardFourFactors: ['mobile', 'idCard', 'bankCard', 'name'],
toc_BankcardBlacklist: ['mobile', 'idCard', 'name', 'bankCard'],
backgroundcheck: ['name', 'idCard', 'mobile'],
}
const currentFeature = computed(() => featureRef.value)
function isHasInput(field: string) {
const key = currentFeature.value
if (key && productFieldConfig[key])
return productFieldConfig[key].includes(field)
return defaultInput.includes(field)
}
const isPhoneNumberValid = computed(() => {
if (!formData.mobile)
return false
return /^1[3-9]\d{9}$/.test(formData.mobile)
})
const isIdCardValid = computed(() => {
if (!formData.idCard)
return false
return /^\d{17}[\dX]$/i.test(formData.idCard)
})
function buildRequestPayload(): Record<string, string> {
const req: Record<string, string> = {}
if (isHasInput('name') && formData.name)
req.name = formData.name
if (isHasInput('idCard') && formData.idCard)
req.id_card = formData.idCard
if (isHasInput('mobile') && formData.mobile)
req.mobile = formData.mobile
if (isHasInput('verificationCode') && formData.verificationCode)
req.code = formData.verificationCode
if (isHasInput('nameMan') && formData.nameMan)
req.name_man = formData.nameMan
if (isHasInput('idCardMan') && formData.idCardMan)
req.id_card_man = formData.idCardMan
if (isHasInput('nameWoman') && formData.nameWoman)
req.name_woman = formData.nameWoman
if (isHasInput('idCardWoman') && formData.idCardWoman)
req.id_card_woman = formData.idCardWoman
if (isHasInput('entName') && formData.entName)
req.ent_name = formData.entName
if (isHasInput('entCode') && formData.entCode)
req.ent_code = formData.entCode
if (isHasInput('carLicense') && formData.carLicense)
req.car_license = formData.carLicense.trim()
if (isHasInput('carType') && formData.carType)
req.car_type = formData.carType
if (isHasInput('vinCode') && formData.vinCode)
req.vin_code = formData.vinCode.trim()
if (isHasInput('certificateNumber') && formData.certificateNumber)
req.certificate_number = formData.certificateNumber
if (isHasInput('userType') && formData.userType)
req.user_type = formData.userType
if (isHasInput('returnUrl') && formData.returnUrl)
req.return_url = formData.returnUrl.trim()
if (isHasInput('imageUrl') && formData.imageUrl)
req.image_url = formData.imageUrl.trim()
if (isHasInput('vehicleLocation') && formData.vehicleLocation)
req.vehicle_location = formData.vehicleLocation.trim()
if (isHasInput('firstRegistrationDate') && formData.firstRegistrationDate)
req.first_registrationdate = formData.firstRegistrationDate.trim()
if (isHasInput('vlphotoData') && formData.vlphotoData)
req.vlphoto_data = formData.vlphotoData
if (isHasInput('authorized') && formData.authorized !== undefined && formData.authorized !== '')
req.authorized = String(formData.authorized)
if (isHasInput('bankCard') && formData.bankCard)
req.bank_card = formData.bankCard.trim()
if (isHasInput('photoData') && formData.photoData)
req.photo_data = formData.photoData
return req
}
return {
formData,
isPhoneNumberValid,
isIdCardValid,
isHasInput,
buildRequestPayload,
}
}

View File

@@ -0,0 +1,114 @@
/**
* 与 tyc-webview-v2 `inquireCategories.js` 对齐的二级分类(当前首页使用 vehicle
*/
export const inquireCategoryConfig = {
/** 车辆查询 */
vehicle: {
title: '车辆查询',
banner: 'car_banner.png',
items: [
{
name: '名下车辆查询',
feature: 'toc_VehiclesUnderName',
desc: '个人名下车辆信息',
icon: 'mxcl_icon.svg',
iconFrom: 'category',
},
{
name: '人车核验(简版)',
feature: 'toc_PersonVehicleVerification',
desc: '根据车牌与号牌类型核验车辆与人员是否匹配',
icon: 'rchyjb.svg',
iconFrom: 'category',
},
{
name: '人车核验(详版)',
feature: 'toc_PersonVehicleVerificationDetail',
desc: '提供更丰富的人车匹配详细信息',
icon: 'rchyxb.svg',
iconFrom: 'category',
},
{
name: '名下车辆(车牌)',
feature: 'toc_VehiclesUnderNamePlate',
desc: '按身份证与姓名查询名下车辆信息',
icon: 'mxcl_icon.svg',
iconFrom: 'category',
},
{
name: '名下车辆(数量)',
feature: 'toc_VehiclesUnderNameCount',
desc: '查询名下车辆数量',
icon: 'mxcl_icon.svg',
iconFrom: 'category',
},
{
name: '车辆静态信息查询',
feature: 'toc_VehicleStaticInfo',
desc: '车辆静态信息',
icon: 'clxx.svg',
iconFrom: 'category',
},
{
name: '车辆里程记录(混合查询)',
feature: 'toc_VehicleMileageMixed',
desc: '车辆里程混合查询',
icon: 'clcx.svg',
iconFrom: 'category',
},
{
name: '二手车VIN估值',
feature: 'toc_VehicleVinValuation',
desc: '按VIN查询二手车估值',
icon: 'escgz.svg',
iconFrom: 'category',
},
{
name: '车辆过户简版查询',
feature: 'toc_VehicleTransferSimple',
desc: '车辆过户简版',
icon: 'esc.svg',
iconFrom: 'category',
},
{
name: '车辆过户详版查询',
feature: 'toc_VehicleTransferDetail',
desc: '车辆过户详版',
icon: 'esc.svg',
iconFrom: 'category',
},
{
name: '车辆维保简版查询',
feature: 'toc_VehicleMaintenanceSimple',
desc: '车辆维保简版',
icon: 'clxx.svg',
iconFrom: 'category',
},
{
name: '车辆出险记录核验',
feature: 'toc_VehicleClaimVerify',
desc: '车辆出险记录核验',
icon: 'cxcl.svg',
iconFrom: 'category',
},
],
},
}
export function getInquireCategoryConfig(category) {
return inquireCategoryConfig[category] || null
}
export function getInquireCategoryTitle(category) {
const c = inquireCategoryConfig[category]
return c ? c.title : '选择查询'
}
/** 列表图标:与 H5 `InquireCategory.vue` 的 `/inquire_icons/` + category 映射等效,统一走 static */
export function getInquiryItemIconUrl(item) {
if (!item?.icon)
return '/static/home/icons/icon-claim-detail.svg'
if (item.iconFrom === 'category')
return `/static/inquire/category/${item.icon}`
return `/static/inquire/category/${item.icon}`
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
/**
* 车辆类报告模块apiID产品能力编码→ 展示名称
* 与后端 ProductFeature / 上游 api 约定对齐;未列出的 apiID 仍可由接口 featureName 兜底。
*/
export const VEHICLE_API_TITLES = {
QCXG9F5C: '疑似营运车辆注册平台数',
QCXG3B8Z: '疑似运营车辆查询(月度里程)',
QCXGP1W3: '疑似运营车辆查询(季度里程)',
QCXGM7R9: '疑似运营车辆查询(半年度里程)',
QCXGU2K4: '疑似运营车辆查询(年度里程)',
QCXGY7F2: '二手车 VIN 估值',
QCXG5U0Z: '车辆静态信息查询',
QCXG3M7Z: '人车关系核验ETC',
QCXG1U4U: '车辆里程记录(混合查询)',
QCXG2T6S: '车辆里程记录(品牌查询)',
QCXG3Y6B: '车辆维保简版查询',
QCXG3Z3L: '车辆维保详细版查询',
QCXG1H7Y: '车辆过户简版查询',
QCXG4I1Z: '车辆过户详版查询',
QCXGGB2Q: '车辆二要素核验 V1',
QCXGP00W: '车辆出险详版查询',
QCXGYTS2: '车辆二要素核验 V2',
QCXGGJ3A: '车辆 VIN 码查询号牌简版',
QCXGJJ2A: '车辆 VIN 码查车辆信息详版',
QCXG5F3A: '名下车辆车牌查询 B',
QCXG4D2E: '名下车辆数量查询',
QCXG6B4E: '车辆出险记录核验',
QCXG8A3D: '车辆七项信息核验',
QCXG9P1C: '名下车辆车牌查询 A',
/** H5 BaseReport 常用:名下车辆(人数) */
QCXG7A2B: '名下车辆',
}
export function getVehicleModuleTitle(apiId, featureName) {
if (!apiId || apiId === '__UNLABELED__') {
if (featureName?.trim())
return featureName.trim()
return '报告模块'
}
const t = VEHICLE_API_TITLES[apiId]
if (t)
return t
if (featureName?.trim())
return featureName.trim()
return apiId
}

14
src/main.js Normal file
View File

@@ -0,0 +1,14 @@
import { createSSRApp } from 'vue'
import 'uno.css'
import * as Pinia from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
app.use(Pinia.createPinia())
return {
app,
Pinia,
}
}

76
src/manifest.json Normal file
View File

@@ -0,0 +1,76 @@
{
"name": "",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"app-harmony": {
"distribute": {}
},
"mp-harmony": {
"distribute": {}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"darkmode": true,
"themeLocation": "theme.json"
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"darkmode": true,
"themeLocation": "theme.json"
}
}

177
src/pages.json Normal file
View File

@@ -0,0 +1,177 @@
{
"globalStyle": {
"backgroundColor": "@bgColor",
"backgroundColorBottom": "@bgColorBottom",
"backgroundColorTop": "@bgColorTop",
"backgroundTextStyle": "@bgTxtStyle",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "@navTxtStyle",
"navigationBarTitleText": "Uni Creator",
"navigationStyle": "custom"
},
"pages": [
// #ifdef H5 || MP-WEIXIN
{
"path": "pages/index",
"type": "home",
"style": {
"navigationBarTitleText": "全能查",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
// #endif
{
"path": "pages/login",
"type": "page",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/mine",
"type": "page",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enableShareAppMessage": true,
"enableShareTimeline": true
}
},
{
"path": "pages/report",
"type": "page",
"style": {
"navigationBarTitleText": "查询报告",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/inquire/example",
"type": "page",
"style": {
"navigationBarTitleText": "示例报告",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/inquire/index",
"type": "page",
"style": {
"navigationBarTitleText": "查询",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/authorization",
"type": "page",
"style": {
"navigationBarTitleText": "授权书",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/privacy-policy",
"type": "page",
"style": {
"navigationBarTitleText": "隐私政策",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/user-agreement",
"type": "page",
"style": {
"navigationBarTitleText": "用户协议",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/report/detail",
"type": "page",
"style": {
"navigationBarTitleText": "报告详情",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/toolbox/category",
"type": "page",
"style": {
"navigationBarTitleText": "分类工具",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/toolbox/index",
"type": "page",
"style": {
"navigationBarTitleText": "实用工具",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/toolbox/query",
"type": "page",
"style": {
"navigationBarTitleText": "工具查询",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": false
}
}
],
"subPackages": [],
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#1768FF",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
// GENERATED BY UNI-PAGES, PLATFORM: MP-WEIXIN
{
"pagePath": "pages/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/report",
"text": "报告",
"iconPath": "static/tabbar/report.png",
"selectedIconPath": "static/tabbar/report-active.png"
},
{
"pagePath": "pages/mine",
"text": "我的",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine-active.png"
}
]
}
}

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>

View File

@@ -0,0 +1,317 @@
/**
* 贷款风险报告(CJRZQ5E9F)数据拆分工具
* 将完整的贷款风险报告数据拆分成多个独立的模块用于在不同的tab中显示
*/
/**
* 将CJRZQ5E9F数据拆分为多个独立的tab模块
* @param {Array} reportData - 原始报告数据数组
* @returns {Array} 拆分后的模块数组
*/
export function splitCJRZQ5E9FForTabs(reportData) {
// 查找CJRZQ5E9F数据
const cjrzq5e9fData = reportData.find(
(item) => item.data?.apiID === "JRZQ5E9F"
);
if (!cjrzq5e9fData || !cjrzq5e9fData.data?.data) {
return reportData; // 如果没有找到CJRZQ5E9F数据返回原数据
}
const originalData = cjrzq5e9fData.data.data;
const baseTimestamp = cjrzq5e9fData.data.timestamp;
// 创建拆分后的模块数组
const splitModules = [];
// 1. 风险概览
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_RiskOverview",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 2. 信用评分
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_CreditScores",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 3. 贷款行为分析
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_LoanBehaviorAnalysis",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 4. 机构分析
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_InstitutionAnalysis",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 5. 时间趋势分析
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_TimeTrendAnalysis",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 6. 风险指标详情
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_RiskIndicators",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 7. 专业建议
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_RiskAdvice",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 移除原始的JRZQ5E9F数据添加拆分后的模块
const otherData = reportData.filter(
(item) => item.data?.apiID !== "JRZQ5E9F"
);
return [...otherData, ...splitModules];
}
/**
* 解析区间化数值
* @param {string|number} value - 原始值
* @returns {number} 解析后的数值
*/
export function parseIntervalValue(value) {
if (!value || value === "" || value === "-1") return 0;
const num = parseInt(value);
if (isNaN(num)) return 0;
// 根据区间映射返回大致范围的中值
switch (num) {
case 1:
return 1;
case 2:
return 3;
case 3:
return 7;
case 4:
return 15;
case 5:
return 25;
default:
return num;
}
}
/**
* 格式化指标值显示
* @param {number} value - 数值
* @returns {string} 格式化后的显示文本
*/
export function formatMetricValue(value) {
if (value === 0) return "0";
if (value < 5) return `${value}`;
return `${value}+`;
}
/**
* 格式化天数显示
* @param {number} value - 天数
* @returns {string} 格式化后的显示文本
*/
export function formatDays(value) {
if (value === 0) return "无记录";
if (value < 30) return `${value}`;
if (value < 365) return `${Math.floor(value / 30)}个月`;
return `${Math.floor(value / 365)}`;
}
/**
* 格式化金额显示
* @param {number} value - 金额
* @returns {string} 格式化后的显示文本
*/
export function formatAmount(value) {
if (value === 0) return "0元";
if (value < 1000) return `${value}`;
if (value < 10000) return `${(value / 1000).toFixed(1)}千元`;
return `${(value / 10000).toFixed(1)}万元`;
}
/**
* 计算风险等级
* @param {number} creditScore - 信用风险评分
* @param {number} overdueIndex - 逾期指数
* @param {boolean} currentOverdue - 当前是否逾期
* @returns {object} 包含等级、颜色和描述的对象
*/
export function calculateRiskLevel(creditScore, overdueIndex, currentOverdue) {
if (creditScore > 0.7 || overdueIndex > 0.7 || currentOverdue) {
return {
level: "高风险",
color: "text-red-600",
bgColor: "bg-red-100",
iconColor: "bg-red-500",
description: "存在较高信用风险,建议谨慎放贷",
};
} else if (creditScore > 0.4 || overdueIndex > 0.4) {
return {
level: "中风险",
color: "text-yellow-600",
bgColor: "bg-yellow-100",
iconColor: "bg-yellow-500",
description: "信用风险适中,需要进一步评估",
};
} else {
return {
level: "低风险",
color: "text-green-600",
bgColor: "bg-green-100",
iconColor: "bg-green-500",
description: "信用风险较低,具备良好还款能力",
};
}
}
/**
* 计算信用评分显示
* @param {number} creditRiskScore - 信用风险评分
* @param {number} amountComplianceIndex - 履约金额综合指数
* @param {number} countComplianceIndex - 履约笔数综合指数
* @returns {object} 包含评分、进度和颜色的对象
*/
export function calculateCreditScore(
creditRiskScore,
amountComplianceIndex,
countComplianceIndex
) {
const avgRisk =
(creditRiskScore + amountComplianceIndex + countComplianceIndex) / 3;
// 风险越高,信用分越低
const score = Math.round((1 - avgRisk) * 850 + 150);
const progress = (score / 1000) * 283;
let color = "#ef4444";
if (score >= 750) color = "#10b981";
else if (score >= 650) color = "#f59e0b";
return {
score,
progress,
color,
};
}
/**
* 获取信用等级描述
* @param {number} score - 信用评分
* @returns {string} 等级描述
*/
export function getCreditScoreLevel(score) {
if (score >= 800) return "优秀";
if (score >= 700) return "良好";
if (score >= 600) return "一般";
if (score >= 500) return "较差";
return "很差";
}
/**
* 获取信用等级样式类
* @param {number} score - 信用评分
* @returns {string} 样式类名
*/
export function getCreditScoreBadgeClass(score) {
if (score >= 800) return "bg-green-100 text-green-800";
if (score >= 700) return "bg-blue-100 text-blue-800";
if (score >= 600) return "bg-yellow-100 text-yellow-800";
if (score >= 500) return "bg-orange-100 text-orange-800";
return "bg-red-100 text-red-800";
}
/**
* 获取评分样式类
* @param {number} score - 评分
* @returns {string} 样式类名
*/
export function getScoreClass(score) {
if (score === null) return "text-gray-400";
if (score >= 750) return "text-green-600";
if (score >= 650) return "text-yellow-600";
return "text-red-600";
}
/**
* 获取圆形进度样式
* @param {number} ratio - 比例值 (0-1)
* @returns {object} 样式对象
*/
export function getCircleStyle(ratio) {
let color = "#ef4444";
if (ratio >= 0.8) color = "#10b981";
else if (ratio >= 0.6) color = "#f59e0b";
// 确保至少显示10度让用户知道是图表
const minDegree = 10;
const actualDegree = Math.max(ratio * 360, minDegree);
return {
background: `conic-gradient(${color} ${actualDegree}deg, #e5e7eb 0deg)`,
};
}
/**
* 检查是否有风险数据
* @param {Object} data - 数据对象
* @returns {boolean} 是否有风险
*/
export function hasRiskData(data) {
if (!data) return false;
// 检查对象中是否有非0值
return Object.values(data).some((value) => {
if (typeof value === "number") return value > 0;
if (typeof value === "string")
return value !== "0" && value !== "-" && value !== "";
return false;
});
}

View File

@@ -0,0 +1,302 @@
/**
* CQYGL3F8E企业关联数据拆分工具
* 将企业关联数据拆分为投资企业记录、高管任职记录和涉诉风险三个独立模块
*/
/**
* 拆分CQYGL3F8E数据为多个独立的tab模块
* @param {Array} reportData - 报告数据数组
* @returns {Array} 拆分后的数据数组
*/
export function splitCQYGL3F8EForTabs(reportData) {
const result = []
reportData.forEach(item => {
if (item.data?.apiID === 'QYGL3F8E') {
// 将QYGL3F8E拆分成多个独立的tab
const qyglData = item.data.data
const baseTimestamp = item.data.timestamp
// 投资类关系
const investRelations = ["sh", "his_sh", "lp", "his_lp"]
// 高管类关系
const managerRelations = ["tm", "his_tm"]
// 获取投资企业记录(股东、历史股东、法人、历史法人)
const investCompanies = (qyglData?.items || []).filter((item) => {
const relationships = item?.relationship || []
return relationships.some((r) => investRelations.includes(r))
})
// 获取高管任职记录(高管、历史高管)
const managerPositions = (qyglData?.items || []).filter((item) => {
const relationships = item?.relationship || []
return relationships.some((r) => managerRelations.includes(r))
})
// 获取有涉诉风险的企业
const lawsuitCompanies = (qyglData?.items || []).filter((item) => {
const lawsuit = item?.lawsuitInfo || {}
return (
(lawsuit.entout && lawsuit.entout.data && Object.keys(lawsuit.entout.data).length > 0) ||
(lawsuit.sxbzxr && lawsuit.sxbzxr.data && lawsuit.sxbzxr.data.sxbzxr && lawsuit.sxbzxr.data.sxbzxr.length > 0) ||
(lawsuit.xgbzxr && lawsuit.xgbzxr.data && lawsuit.xgbzxr.data.xgbzxr && lawsuit.xgbzxr.data.xgbzxr.length > 0)
)
})
// 1. 投资企业记录模块
result.push({
data: {
apiID: 'CQYGL3F8E_Investment',
data: investCompanies,
success: true,
timestamp: baseTimestamp
}
})
// 2. 高管任职记录模块
result.push({
data: {
apiID: 'CQYGL3F8E_SeniorExecutive',
data: managerPositions,
success: true,
timestamp: baseTimestamp
}
})
// 3. 涉诉风险模块
result.push({
data: {
apiID: 'CQYGL3F8E_Lawsuit',
data: {
lawsuitCompanies: lawsuitCompanies,
totalCompanies: qyglData?.items?.length || 0
},
success: true,
timestamp: baseTimestamp
}
})
// 4. 对外投资历史模块 - 从所有企业中收集投资历史
const allInvestHistory = []
qyglData?.items?.forEach(company => {
if (company.invest_history?.items) {
company.invest_history.items.forEach(investment => {
allInvestHistory.push({
...investment,
companyName: company.orgName, // 添加企业名称
companyInfo: {
orgName: company.orgName,
relationship: company.relationship,
basicInfo: company.basicInfo
}
})
})
}
})
result.push({
data: {
apiID: 'CQYGL3F8E_InvestHistory',
data: { items: allInvestHistory, total: allInvestHistory.length },
success: true,
timestamp: baseTimestamp
}
})
// 5. 融资历史模块 - 从所有企业中收集融资历史
const allFinancingHistory = []
qyglData?.items?.forEach(company => {
if (company.financing_history?.items) {
company.financing_history.items.forEach(financing => {
allFinancingHistory.push({
...financing,
companyName: company.orgName, // 添加企业名称
companyInfo: {
orgName: company.orgName,
relationship: company.relationship,
basicInfo: company.basicInfo
}
})
})
}
})
result.push({
data: {
apiID: 'CQYGL3F8E_FinancingHistory',
data: { items: allFinancingHistory, total: allFinancingHistory.length },
success: true,
timestamp: baseTimestamp
}
})
// 6. 行政处罚模块 - 从所有企业中收集行政处罚
const allPunishmentInfo = []
qyglData?.items?.forEach(company => {
if (company.punishment_info?.items) {
company.punishment_info.items.forEach(punishment => {
allPunishmentInfo.push({
...punishment,
companyName: company.orgName, // 添加企业名称
companyInfo: {
orgName: company.orgName,
relationship: company.relationship,
basicInfo: company.basicInfo
}
})
})
}
})
result.push({
data: {
apiID: 'CQYGL3F8E_Punishment',
data: { items: allPunishmentInfo, total: allPunishmentInfo.length },
success: true,
timestamp: baseTimestamp
}
})
// 7. 经营异常模块 - 从所有企业中收集经营异常
const allAbnormalInfo = []
qyglData?.items?.forEach(company => {
if (company.abnormal_info?.items) {
company.abnormal_info.items.forEach(abnormal => {
allAbnormalInfo.push({
...abnormal,
companyName: company.orgName, // 添加企业名称
companyInfo: {
orgName: company.orgName,
relationship: company.relationship,
basicInfo: company.basicInfo
}
})
})
}
})
result.push({
data: {
apiID: 'CQYGL3F8E_Abnormal',
data: { items: allAbnormalInfo, total: allAbnormalInfo.length },
success: true,
timestamp: baseTimestamp
}
})
// 8. 税务风险模块 - 包含欠税公告和税收违法
const taxRiskCompanies = (qyglData?.items || []).filter((item) => {
const ownTax = item?.own_tax || {};
const taxContravention = item?.tax_contravention || {};
return (ownTax.total > 0 && ownTax.items && ownTax.items.length > 0) ||
(taxContravention.total > 0 && taxContravention.items && taxContravention.items.length > 0);
});
result.push({
data: {
apiID: 'CQYGL3F8E_TaxRisk',
data: { items: taxRiskCompanies },
success: true,
timestamp: baseTimestamp
}
})
} else {
// 其他数据直接添加
result.push(item)
}
})
return result
}
/**
* 获取关系文本描述
* @param {string} relation - 关系代码
* @returns {string} 关系文本
*/
export function getRelationshipText(relation) {
const relationshipMap = {
sh: '股东',
his_sh: '曾任股东',
lp: '法人',
his_lp: '曾任法人',
tm: '高管',
his_tm: '曾任高管'
}
return relationshipMap[relation] || relation
}
/**
* 获取关系样式类
* @param {string} relation - 关系代码
* @returns {string} 样式类名
*/
export function getRelationshipClass(relation) {
const relationshipMap = {
sh: 'bg-blue-100 text-blue-700',
his_sh: 'bg-blue-50 text-blue-600',
lp: 'bg-green-100 text-green-700',
his_lp: 'bg-green-50 text-green-600',
tm: 'bg-purple-100 text-purple-700',
his_tm: 'bg-purple-50 text-purple-600'
}
return relationshipMap[relation] || 'bg-gray-100 text-gray-600'
}
/**
* 获取企业状态对应的样式类
* @param {string} status - 企业状态
* @returns {string} 样式类名
*/
export function getStatusClass(status) {
if (!status) return 'bg-gray-100 text-gray-500'
if (status.includes('注销') || status.includes('吊销')) {
return 'bg-red-50 text-red-600'
} else if (status.includes('存续') || status.includes('在营')) {
return 'bg-green-50 text-green-600'
} else if (status.includes('筹建') || status.includes('新设')) {
return 'bg-blue-50 text-blue-600'
} else {
return 'bg-yellow-50 text-yellow-600'
}
}
/**
* 格式化资本金额显示
* @param {string|number} capital - 资本金额
* @param {string} currency - 货币类型
* @returns {string} 格式化后的金额
*/
export function formatCapital(capital, currency) {
if (!capital) return '—'
let unit = ''
let value = parseFloat(capital)
// 处理原始数据中可能带有的单位
if (typeof capital === 'string' && capital.includes('万')) {
unit = '万'
const numMatch = capital.match(/[\d.]+/)
value = numMatch ? parseFloat(numMatch[0]) : 0
} else if (value >= 10000) {
// 大额数字转换为万元显示
value = value / 10000
unit = '万'
}
// 格式化数字,保留两位小数(如果有小数部分)
const formattedValue = value.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
})
return `${formattedValue}${unit} ${currency || '人民币'}`
}
/**
* 格式化日期显示
* @param {string} dateStr - 日期字符串
* @returns {string} 格式化后的日期
*/
export function formatDate(dateStr) {
if (!dateStr) return '—'
return dateStr
}

View File

@@ -0,0 +1,311 @@
/**
* 司南报告(DWBG6A2C)数据拆分工具
* 将完整的司南报告数据拆分成多个独立的模块用于在不同的tab中显示
*/
/**
* 将DWBG6A2C数据拆分为多个独立的tab模块
* @param {Array} reportData - 原始报告数据数组
* @returns {Array} 拆分后的模块数组
*/
export function splitDWBG6A2CForTabs(reportData) {
// 查找DWBG6A2C数据
const dwbg6a2cData = reportData.find(item => item.data?.apiID === 'DWBG6A2C');
if (!dwbg6a2cData || !dwbg6a2cData.data?.data) {
return reportData; // 如果没有找到DWBG6A2C数据返回原数据
}
const originalData = dwbg6a2cData.data.data;
const baseTimestamp = dwbg6a2cData.data.timestamp;
// 创建拆分后的模块数组
const splitModules = [];
// 1. 基本信息
// if (originalData.baseInfo) {
// splitModules.push({
// data: {
// apiID: 'DWBG6A2C_BaseInfo',
// data: {
// baseInfo: originalData.baseInfo
// },
// success: true,
// timestamp: baseTimestamp
// }
// });
// }
// 2. 身份信息核验
if (originalData.standLiveInfo) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_StandLiveInfo',
data: {
standLiveInfo: originalData.standLiveInfo
},
success: true,
timestamp: baseTimestamp
}
});
}
// 3. 命中风险标注
if (originalData.riskPoint) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_RiskPoint',
data: {
riskPoint: originalData.riskPoint
},
success: true,
timestamp: baseTimestamp
}
});
}
// 4. 公安重点人员核验
if (originalData.securityInfo) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_SecurityInfo',
data: {
securityInfo: originalData.securityInfo
},
success: true,
timestamp: baseTimestamp
}
});
}
// 5. 涉赌涉诈人员核验
if (originalData.antiFraudInfo) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_AntiFraudInfo',
data: {
antiFraudInfo: originalData.antiFraudInfo
},
success: true,
timestamp: baseTimestamp
}
});
}
// 6. 风险名单
if (originalData.riskList) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_RiskList',
data: {
riskList: originalData.riskList
},
success: true,
timestamp: baseTimestamp
}
});
}
// 7. 历史借贷行为
if (originalData.applicationStatistics) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_ApplicationStatistics',
data: {
applicationStatistics: originalData.applicationStatistics
},
success: true,
timestamp: baseTimestamp
}
});
}
// 8. 近24个月放款情况
if (originalData.lendingStatistics) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_LendingStatistics',
data: {
lendingStatistics: originalData.lendingStatistics
},
success: true,
timestamp: baseTimestamp
}
});
}
// 9. 履约情况
if (originalData.performanceStatistics) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_PerformanceStatistics',
data: {
performanceStatistics: originalData.performanceStatistics
},
success: true,
timestamp: baseTimestamp
}
});
}
// 10. 历史逾期记录
if (originalData.overdueRecord) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_OverdueRecord',
data: {
overdueRecord: originalData.overdueRecord
},
success: true,
timestamp: baseTimestamp
}
});
}
// 11. 授信详情
if (originalData.creditDetail && Object.keys(originalData.creditDetail).length > 0) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_CreditDetail',
data: {
creditDetail: originalData.creditDetail
},
success: true,
timestamp: baseTimestamp
}
});
}
// 12. 租赁行为
if (originalData.rentalBehavior) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_RentalBehavior',
data: {
rentalBehavior: originalData.rentalBehavior
},
success: true,
timestamp: baseTimestamp
}
});
}
// 13. 关联风险监督
if (originalData.riskSupervision) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_RiskSupervision',
data: {
riskSupervision: originalData.riskSupervision
},
success: true,
timestamp: baseTimestamp
}
});
}
// 14. 法院风险信息
if (originalData.judiciaRiskInfos && originalData.judiciaRiskInfos.length > 0) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_CourtRiskInfo',
data: {
judiciaRiskInfos: originalData.judiciaRiskInfos
},
success: true,
timestamp: baseTimestamp
}
});
}
// 移除原始的DWBG6A2C数据添加拆分后的模块
const otherData = reportData.filter(item => item.data?.apiID !== 'DWBG6A2C');
return [...otherData, ...splitModules];
}
/**
* 格式化风险等级描述
* @param {string} level - 风险等级
* @returns {object} 包含颜色和文本的对象
*/
export function formatRiskLevel(level) {
const riskLevels = {
'0': { color: 'text-green-600', bg: 'bg-green-100', text: '无风险', icon: '✅' },
'1': { color: 'text-red-600', bg: 'bg-red-100', text: '有风险', icon: '⚠️' },
'A': { color: 'text-yellow-600', bg: 'bg-yellow-100', text: '较低风险', icon: '⚠️' },
'B': { color: 'text-orange-600', bg: 'bg-orange-100', text: '低风险', icon: '⚠️' },
'C': { color: 'text-red-600', bg: 'bg-red-100', text: '中风险', icon: '🚨' },
'D': { color: 'text-red-700', bg: 'bg-red-200', text: '高风险', icon: '🚨' }
};
return riskLevels[level] || { color: 'text-gray-600', bg: 'bg-gray-100', text: '未知', icon: '❓' };
}
/**
* 格式化手机号码状态
* @param {number} status - 状态码
* @returns {object} 包含颜色和文本的对象
*/
export function formatPhoneStatus(status) {
const statusMap = {
'-1': { color: 'text-gray-600', bg: 'bg-gray-100', text: '未查得', icon: '❓' },
'0': { color: 'text-red-600', bg: 'bg-red-100', text: '空号', icon: '❌' },
'1': { color: 'text-green-600', bg: 'bg-green-100', text: '实号', icon: '✅' },
'2': { color: 'text-orange-600', bg: 'bg-orange-100', text: '停机', icon: '⏸️' },
'3': { color: 'text-gray-600', bg: 'bg-gray-100', text: '库无', icon: '❓' },
'4': { color: 'text-yellow-600', bg: 'bg-yellow-100', text: '沉默号', icon: '😴' },
'5': { color: 'text-red-600', bg: 'bg-red-100', text: '风险号', icon: '⚠️' }
};
return statusMap[status.toString()] || { color: 'text-gray-600', bg: 'bg-gray-100', text: '未知', icon: '❓' };
}
/**
* 格式化身份核验结果
* @param {string} result - 核验结果
* @returns {object} 包含颜色和文本的对象
*/
export function formatVerificationResult(result) {
const resultMap = {
'0': { color: 'text-green-600', bg: 'bg-green-100', text: '一致', icon: '✅' },
'1': { color: 'text-red-600', bg: 'bg-red-100', text: '不一致或不存在', icon: '❌' }
};
return resultMap[result] || { color: 'text-gray-600', bg: 'bg-gray-100', text: '未知', icon: '❓' };
}
/**
* 格式化在网时长
* @param {string} inTime - 在网时长代码
* @returns {object} 包含颜色和文本的对象
*/
export function formatInTime(inTime) {
const timeMap = {
'0': { color: 'text-red-600', bg: 'bg-red-100', text: '0-3个月', icon: '📱' },
'3': { color: 'text-orange-600', bg: 'bg-orange-100', text: '3-6个月', icon: '📱' },
'6': { color: 'text-yellow-600', bg: 'bg-yellow-100', text: '6-12个月', icon: '📱' },
'12': { color: 'text-blue-600', bg: 'bg-blue-100', text: '12-24个月', icon: '📱' },
'24': { color: 'text-green-600', bg: 'bg-green-100', text: '24个月以上', icon: '📱' },
'99': { color: 'text-gray-600', bg: 'bg-gray-100', text: '状态异常', icon: '⚠️' },
'-1': { color: 'text-gray-600', bg: 'bg-gray-100', text: '查无记录', icon: '❓' }
};
return timeMap[inTime] || { color: 'text-gray-600', bg: 'bg-gray-100', text: '未知', icon: '❓' };
}
/**
* 检查是否有风险数据
* @param {Object} data - 数据对象
* @returns {boolean} 是否有风险
*/
export function hasRiskData(data) {
if (!data) return false;
// 检查对象中是否有非0值
return Object.values(data).some(value => {
if (typeof value === 'number') return value > 0;
if (typeof value === 'string') return value !== '0' && value !== '-' && value !== '';
return false;
});
}

View File

@@ -0,0 +1,157 @@
/**
* 简单的DWBG8B4D数据拆分演示
* 直接在BaseReport.vue中处理
*/
// 在BaseReport.vue的script部分添加这个函数
function splitDWBG8B4DForTabs(reportData) {
const result = []
reportData.forEach(item => {
if (item.data.apiID === 'DWBG8B4D') {
// 将DWBG8B4D拆分成多个独立的tab
const dwbgData = item.data.data
// 报告概览
result.push({
data: {
apiID: 'DWBG8B4D_Overview',
data: {
baseInfo: dwbgData.baseInfo,
checkSuggest: dwbgData.checkSuggest,
fraudScore: dwbgData.fraudScore,
creditScore: dwbgData.creditScore,
verifyRule: dwbgData.verifyRule,
fraudRule: dwbgData.fraudRule,
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 规则风险提示
result.push({
data: {
apiID: 'DWBG8B4D_RiskWarningTab',
data: {
riskWarning: dwbgData.riskWarning,
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 要素核查
result.push({
data: {
apiID: 'DWBG8B4D_ElementVerification',
data: {
sfzeysFlag: dwbgData.elementVerificationDetail?.sfzeysFlag || 0,
personCheckDetails: dwbgData.elementVerificationDetail?.personCheckDetails || {},
sjsysFlag: dwbgData.elementVerificationDetail?.sjsysFlag || 0,
phoneCheckDetails: dwbgData.elementVerificationDetail?.phoneCheckDetails || {},
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 运营商核验
result.push({
data: {
apiID: 'DWBG8B4D_Identity',
data: {
inTime: dwbgData.standLiveInfo?.inTime || '',
phoneVailRiskFlag: dwbgData.elementVerificationDetail?.phoneVailRiskFlag || 0,
phoneVailRisks: dwbgData.elementVerificationDetail?.phoneVailRisks || {},
belongRiskFlag: dwbgData.elementVerificationDetail?.belongRiskFlag || 0,
belongRisks: dwbgData.elementVerificationDetail?.belongRisks || {},
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 公安重点人员检验
result.push({
data: {
apiID: 'DWBG8B4D_RiskWarning',
data: {
highRiskFlag: dwbgData.elementVerificationDetail?.highRiskFlag || 0,
keyPersonCheckList: dwbgData.elementVerificationDetail?.keyPersonCheckList || {},
antiFraudInfo: dwbgData.elementVerificationDetail?.antiFraudInfo || {},
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 逾期风险
result.push({
data: {
apiID: 'DWBG8B4D_OverdueRisk',
data: {
overdueRiskProduct: dwbgData.overdueRiskProduct,
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 法院曝光台信息
result.push({
data: {
apiID: 'DWBG8B4D_CourtInfo',
data: {
multCourtInfo: dwbgData.multCourtInfo,
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 借贷评估
result.push({
data: {
apiID: 'DWBG8B4D_LoanEvaluation',
data: {
loanEvaluationVerificationDetail: dwbgData.loanEvaluationVerificationDetail,
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 租赁风险评估
result.push({
data: {
apiID: 'DWBG8B4D_LeasingRisk',
data: {
leasingRiskAssessment: dwbgData.leasingRiskAssessment,
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
// 关联风险监督
result.push({
data: {
apiID: 'DWBG8B4D_RiskSupervision',
data: {
riskSupervision: dwbgData.riskSupervision,
success: dwbgData.success,
timestamp: dwbgData.timestamp
}
}
})
} else {
// 其他数据直接添加
result.push(item)
}
})
return result
}
export { splitDWBG8B4DForTabs }

View File

@@ -0,0 +1,204 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆过户简版查询</h3>
<p class="header-desc">查看车辆最近是否发生过户及过户次数</p>
</div>
<div class="header-tag" :class="flagClass">
<span class="tag-label">是否过户</span>
<span class="tag-value">{{ flagText }}</span>
</div>
</div>
<template v-if="hasData">
<div class="summary-card" :class="flagClass">
<div class="summary-main">
<div class="summary-title">
最近过户情况
</div>
<div class="summary-flag">{{ flagDetailText }}</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span class="meta-label">最近过户时间</span>
<span class="meta-value">{{ formattedTransferDate }}</span>
</div>
<div class="meta-line">
<span class="meta-label">累计过户次数</span>
<span class="meta-value strong">{{ data.transferNum || '0' }} </span>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无过户信息</div>
<div class="sub">未查询到车辆过户记录</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
// transferFlag: 0-否1-是(字符串)
const flag = computed(() => props.data?.transferFlag);
const flagText = computed(() => {
if (flag.value === '1') return '已过户';
if (flag.value === '0') return '未过户';
return '未知';
});
const flagDetailText = computed(() => {
if (flag.value === '1') return '该车辆存在过户记录';
if (flag.value === '0') return '该车辆暂无过户记录';
return '未能识别过户状态';
});
const flagClass = computed(() => {
if (flag.value === '1') return 'flag-yes';
if (flag.value === '0') return 'flag-no';
return 'flag-unknown';
});
const formattedTransferDate = computed(() => {
const raw = props.data?.transferDate;
if (!raw) return '-';
if (raw === '近一年内过户') return '近一年内过户';
// 期望格式 yyyyMM
if (raw.length === 6) {
const y = raw.slice(0, 4);
const m = raw.slice(4, 6);
return `${y}${m}`;
}
return raw;
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center justify-between mb-5 px-5 py-4 rounded-2xl bg-gradient-to-r from-sky-50 via-blue-50 to-sky-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-sky-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-sky-800 opacity-90;
}
.header-tag {
@apply inline-flex flex-col items-end gap-1 px-3 py-2 rounded-xl bg-white/80 shadow-sm;
}
.header-tag.flag-yes {
@apply text-amber-800;
}
.header-tag.flag-no {
@apply text-emerald-800;
}
.header-tag.flag-unknown {
@apply text-gray-700;
}
.tag-label {
@apply text-sm text-gray-500;
}
.tag-value {
@apply text-xl font-bold leading-none whitespace-nowrap;
}
.summary-card {
@apply rounded-2xl border px-5 py-4 mb-4;
}
.summary-card.flag-yes {
@apply bg-amber-50 border-amber-100;
}
.summary-card.flag-no {
@apply bg-emerald-50 border-emerald-100;
}
.summary-card.flag-unknown {
@apply bg-gray-50 border-gray-200;
}
.summary-main {
@apply flex items-baseline justify-between mb-3;
}
.summary-title {
@apply text-lg font-semibold text-gray-900;
}
.summary-flag {
@apply text-base text-gray-700;
}
.summary-meta {
@apply space-y-2 text-base text-gray-800;
}
.meta-line {
@apply flex items-center gap-2;
}
.meta-label {
@apply text-gray-500;
}
.meta-value {
@apply font-medium;
}
.meta-value.strong {
@apply text-lg font-semibold;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,374 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆里程记录混合查询</h3>
<p class="header-desc">综合诊断与维保记录展示车辆里程变化与是否存在调表嫌疑</p>
</div>
</div>
<template v-if="hasData">
<!-- 概览里程是否异常 + 最新里程 -->
<div class="summary-card" :class="suspectClass">
<div class="summary-main">
<div class="summary-left">
<div class="vin-label">VIN</div>
<div class="vin-value font-mono">{{ vin || '-' }}</div>
</div>
<div class="summary-right">
<div class="summary-label">最新里程</div>
<div class="summary-mileage">{{ latestMileageText }}</div>
<div class="summary-sub">最近记录日期{{ latestReportTime || '-' }}</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span>里程是否异常</span>
<span class="strong" :class="suspectClass">{{ suspectedText }}</span>
</div>
<div class="meta-line" v-if="imageUrl">
<span>行驶证图片</span>
</div>
<div v-if="imageUrl" class="image-wrap">
<img :src="imageUrl" alt="行驶证图片" class="licence-img" />
</div>
</div>
</div>
<!-- 里程时间轴 -->
<div class="detail-card">
<h4 class="section-title">里程记录时间轴</h4>
<div v-if="mileageList && mileageList.length" class="timeline">
<div v-for="(item, idx) in mileageList" :key="idx" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot" :class="item.mileageStatus === '1' ? 'dot-abnormal' : 'dot-normal'">
</div>
<div v-if="idx !== mileageList.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ formatDate(item.reportTime) }}</div>
<div class="km">{{ formatMileage(item.mileage) }}</div>
</div>
<div class="row-sub">
<span>来源{{ sourceText(item.source) }}</span>
<span v-if="item.mileageStatus === '1'" class="badge-abnormal">异常里程</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-small">
暂无里程记录
</div>
</div>
<!-- 异常里程列表 -->
<div class="detail-card" v-if="adjustList && adjustList.length">
<h4 class="section-title">疑似调表记录</h4>
<div class="adjust-list">
<div v-for="(item, idx) in adjustList" :key="idx" class="adjust-item">
<div class="adjust-time">{{ formatDate(item.reportTime) }}</div>
<div class="adjust-body">
<div>
<div class="adjust-label">调整前</div>
<div class="adjust-value">{{ formatMileage(item.beforeMileage) }}</div>
</div>
<div class="adjust-arrow"></div>
<div>
<div class="adjust-label">调整后</div>
<div class="adjust-value">{{ formatMileage(item.afterMileage) }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无里程数据</div>
<div class="sub">未查询到车辆里程记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const vin = computed(() => props.data?.vehicleInfo?.vin || '');
const mileageList = computed(() => props.data?.mileageInfo?.mileageList || []);
const adjustList = computed(() => props.data?.mileageInfo?.suspectedAdjustMileageList || []);
const suspectedAdjust = computed(() => props.data?.mileageInfo?.suspectedAdjust);
const imageUrl = computed(() => props.data?.imageUrl || '');
const latestRecord = computed(() => {
const list = mileageList.value;
if (!list || !list.length) return null;
// 默认接口数据已按时间升序,直接取最后一条
return list[list.length - 1];
});
const latestMileageText = computed(() => {
if (!latestRecord.value) return '-';
return formatMileage(latestRecord.value.mileage);
});
const latestReportTime = computed(() => latestRecord.value?.reportTime || '');
const suspectedText = computed(() => {
if (suspectedAdjust.value === 'true') return '存在异常里程行为';
if (suspectedAdjust.value === 'false') return '未发现里程异常';
return '未知';
});
const suspectClass = computed(() => {
if (suspectedAdjust.value === 'true') return 'suspect-yes';
if (suspectedAdjust.value === 'false') return 'suspect-no';
return 'suspect-unknown';
});
const formatMileage = (val) => {
if (!val && val !== 0) return '-';
const num = Number(val);
if (Number.isNaN(num)) return `${val} km`;
// 默认 km按 km 展示
return `${num.toLocaleString()} km`;
};
const sourceText = (source) => {
if (source === '0') return '诊断里程';
if (source === '1') return '维保里程';
return '其他';
};
const formatDate = (val) => {
if (!val) return '-';
// 期望格式 yyyy-MM-dd
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
return `${m[1]}${m[2]}${m[3]}`;
}
return val;
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-sky-50 via-blue-50 to-indigo-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-sky-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-sky-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border px-5 py-4 mb-4;
}
.summary-card.suspect-yes {
@apply bg-amber-50 border-amber-100;
}
.summary-card.suspect-no {
@apply bg-emerald-50 border-emerald-100;
}
.summary-card.suspect-unknown {
@apply bg-gray-50 border-gray-200;
}
.summary-main {
@apply flex flex-col mb-3 gap-3;
}
.summary-left {
@apply flex flex-col gap-1;
}
.vin-label {
@apply text-sm text-gray-500;
}
.vin-value {
@apply text-base text-gray-900;
}
.summary-right {
@apply text-left;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-mileage {
@apply text-2xl font-bold text-sky-800 mt-1;
}
.summary-sub {
@apply text-sm text-sky-700 opacity-90;
}
.summary-meta {
@apply space-y-1 text-base text-gray-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2;
}
.meta-line .strong {
@apply font-semibold;
}
.image-wrap {
@apply mt-2 flex justify-center;
}
.licence-img {
@apply rounded-xl border border-gray-200 max-w-full;
max-height: 220px;
object-fit: contain;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex mb-3;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-stretch;
}
.dot {
@apply w-3 h-3 rounded-full bg-gray-400 self-center;
}
.dot-normal {
@apply bg-emerald-500;
}
.dot-abnormal {
@apply bg-red-500;
}
.line {
@apply flex-1 w-px bg-gray-300 mx-auto;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .km {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.badge-abnormal {
@apply inline-flex items-center px-2 py-0.5 rounded-full bg-red-50 text-red-700 text-xs font-medium;
}
.adjust-list {
@apply space-y-3;
}
.adjust-item {
@apply rounded-2xl border border-amber-100 bg-amber-50/70 px-4 py-3;
}
.adjust-time {
@apply text-sm text-gray-700 mb-2;
}
.adjust-body {
@apply flex items-center justify-between gap-4;
}
.adjust-label {
@apply text-xs text-gray-500 mb-1;
}
.adjust-value {
@apply text-base font-semibold text-gray-900;
}
.adjust-arrow {
@apply text-2xl text-gray-400;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty-small {
@apply text-center py-6 text-sm text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,284 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆维保简版查询</h3>
<p class="header-desc">按时间轴展示维保记录包含保养与更换材料明细</p>
</div>
</div>
<template v-if="hasData">
<!-- 概览VIN + 维保次数 + 最近一次维保 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="summary-label">车架号 VIN</div>
<div class="summary-vin font-mono">{{ vin || '-' }}</div>
</div>
<div class="summary-right">
<div class="summary-count">维保记录 {{ totalCount }} </div>
<div class="summary-last" v-if="lastRecord">
最近一次{{ formatDate(lastRecord.lastTime) }} · {{ formatMileage(lastRecord.mileage) }}
</div>
</div>
</div>
</div>
<!-- 维保时间轴 -->
<div class="detail-card">
<h4 class="section-title">维保记录时间轴</h4>
<div v-if="records && records.length" class="timeline">
<div v-for="(item, idx) in records" :key="idx" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot"></div>
<div v-if="idx !== records.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ formatDate(item.lastTime) }}</div>
<div class="km">{{ formatMileage(item.mileage) }}</div>
</div>
<div class="row-sub">
<span class="repair-type">{{ item.repairType || '维保' }}</span>
<span class="vin-small font-mono">VIN: {{ item.vin || vin || '-' }}</span>
</div>
<div v-if="item.details && item.details.length" class="sub-section">
<div class="sub-title">维修项目</div>
<ul class="sub-list">
<li v-for="(d, di) in item.details" :key="di">
<span v-if="d.type" class="tag">{{ d.type }}</span>
<span>{{ d.content }}</span>
</li>
</ul>
</div>
<div v-if="item.materials && item.materials.length" class="sub-section">
<div class="sub-title">使用材料</div>
<ul class="sub-list">
<li v-for="(m, mi) in item.materials" :key="mi">
<span v-if="m.type" class="tag tag-material">{{ m.type }}</span>
<span>{{ m.content }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div v-else class="empty-small">
暂无维保记录
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无维保数据</div>
<div class="sub">未查询到车辆维保记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const records = computed(() => Array.isArray(props.data?.record) ? props.data.record : []);
const vin = computed(() => {
if (records.value.length && records.value[0].vin) return records.value[0].vin;
return props.params?.vin_code || '';
});
const totalCount = computed(() => records.value.length || 0);
const lastRecord = computed(() => (records.value.length ? records.value[records.value.length - 1] : null));
const formatDate = (val) => {
if (!val) return '-';
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
return `${m[1]}${m[2]}${m[3]}`;
}
return val;
};
const formatMileage = (val) => {
if (!val && val !== 0) return '-';
const num = Number(val);
if (Number.isNaN(num)) return `${val} km`;
return `${num.toLocaleString()} km`;
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-emerald-50 via-sky-50 to-teal-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-emerald-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-emerald-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border border-emerald-100 bg-emerald-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-1;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-vin {
@apply text-base text-gray-900;
}
.summary-right {
@apply text-right;
}
.summary-count {
@apply text-base font-semibold text-gray-900;
}
.summary-last {
@apply text-sm text-emerald-800 mt-1;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex mb-3;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-stretch;
}
.dot {
@apply w-3 h-3 rounded-full bg-emerald-500 self-center;
}
.line {
@apply flex-1 w-px bg-gray-300 mx-auto;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .km {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.repair-type {
@apply font-medium text-gray-800;
}
.vin-small {
@apply text-xs text-gray-500;
}
.sub-section {
@apply mt-3;
}
.sub-title {
@apply text-sm font-semibold text-gray-800 mb-1;
}
.sub-list {
@apply text-sm text-gray-800 space-y-1;
}
.sub-list li {
@apply flex flex-wrap gap-1;
}
.tag {
@apply inline-flex items-center px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 text-xs font-medium;
}
.tag-material {
@apply bg-sky-50 text-sky-700;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty-small {
@apply text-center py-6 text-sm text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,288 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆维保详细版查询</h3>
<p class="header-desc">展示品牌车架号等基本信息及每次维保的详细内容</p>
</div>
</div>
<template v-if="hasData">
<!-- 概览品牌 + VIN + 车牌号 + 发动机号 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="summary-label">品牌名称</div>
<div class="summary-brand">{{ data.brandName || '未知品牌' }}</div>
</div>
<div class="summary-right">
<div class="summary-label">车架号 VIN</div>
<div class="summary-vin font-mono">{{ data.vin || '-' }}</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span>车牌号</span><span class="strong">{{ data.licensePlate || '未提供' }}</span>
<span class="dot"></span>
<span>发动机号</span><span class="strong code">{{ data.engine || '-' }}</span>
</div>
<div class="meta-line" v-if="records.length">
<span>维保记录</span><span class="strong">{{ records.length }} </span>
<span class="dot"></span>
<span>最近一次</span>
<span class="strong">{{ formatDate(records[records.length - 1].date) }}</span>
</div>
</div>
</div>
<!-- 维保时间轴 -->
<div class="detail-card">
<h4 class="section-title">维保记录时间轴</h4>
<div v-if="records && records.length" class="timeline">
<div v-for="(item, idx) in records" :key="idx" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot"></div>
<div v-if="idx !== records.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ formatDate(item.date) }}</div>
<div class="km">{{ formatMileage(item.mileage) }}</div>
</div>
<div class="row-sub">
<span class="repair-type">{{ item.type || '维保' }}</span>
<span class="brand-small" v-if="data.brandName">{{ data.brandName }}</span>
</div>
<div class="sub-section" v-if="item.content">
<div class="sub-title">维修内容</div>
<div class="sub-text">{{ item.content }}</div>
</div>
<div class="sub-section" v-if="item.material">
<div class="sub-title">材料</div>
<div class="sub-text">{{ item.material }}</div>
</div>
<div class="sub-section" v-if="item.remark">
<div class="sub-title">备注</div>
<div class="sub-text">{{ item.remark }}</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-small">
暂无维保记录
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无维保数据</div>
<div class="sub">未查询到车辆维保记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const records = computed(() => Array.isArray(props.data?.record) ? props.data.record : []);
const data = computed(() => props.data || {});
const formatDate = (val) => {
if (!val) return '-';
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
return `${m[1]}${m[2]}${m[3]}`;
}
return val;
};
const formatMileage = (val) => {
if (!val && val !== 0) return '-';
const num = Number(val);
if (Number.isNaN(num)) return `${val} km`;
return `${num.toLocaleString()} km`;
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-indigo-50 via-sky-50 to-emerald-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-indigo-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-indigo-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border border-indigo-100 bg-indigo-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-1;
}
.summary-right {
@apply text-right;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-brand {
@apply text-lg font-semibold text-gray-900;
}
.summary-vin {
@apply text-base text-gray-900;
}
.summary-meta {
@apply space-y-1 text-base text-gray-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.strong {
@apply font-semibold;
}
.code {
@apply font-mono tracking-wide;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex mb-3;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-stretch;
}
.dot {
@apply w-3 h-3 rounded-full bg-indigo-500 self-center;
}
.line {
@apply flex-1 w-px bg-gray-300 mx-auto;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .km {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.repair-type {
@apply font-medium text-gray-800;
}
.brand-small {
@apply text-xs text-gray-500;
}
.sub-section {
@apply mt-3;
}
.sub-title {
@apply text-sm font-semibold text-gray-800 mb-1;
}
.sub-text {
@apply text-sm text-gray-800 whitespace-pre-wrap break-words;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty-small {
@apply text-center py-6 text-sm text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<!-- 车辆总数统计 -->
<div class="flex justify-between items-center mb-6 pb-4 border-b border-gray-100">
<div class="flex items-center gap-3">
<div>
<div class="text-lg font-semibold text-gray-900">名下车辆(数量)</div>
</div>
</div>
<div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium">
{{ vehicleCount }}
</div>
</div>
<!-- 车辆列表 -->
<div class="space-y-3" v-if="vehicleList && vehicleList.length > 0">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:bg-blue-50 hover:border-blue-200 transition-colors duration-200"
v-for="(vehicle, index) in vehicleList" :key="index">
<div class="space-y-3">
<div class="text-xl font-bold text-gray-900 font-mono tracking-wider">
{{ vehicle.plateNum }}
</div>
<div class="flex items-center gap-3">
<div class="inline-flex items-center gap-1 px-3 py-1 rounded text-xs font-medium text-white"
:class="getPlateColorClass(vehicle.plateColor)">
<span>🏷</span>
<span>{{ getPlateColorText(vehicle.plateColor) }}</span>
</div>
<div class="text-sm text-gray-600">
<span class="text-gray-500">车辆类型:</span>
<span class="font-medium text-gray-900 ml-1">{{ getVehicleTypeText(vehicle.vehicleType)
}}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div class="text-center py-12 text-gray-500" v-else>
<div class="text-4xl mb-3">🚫</div>
<div class="text-lg font-medium mb-1">暂无车辆信息</div>
<div class="text-sm">No vehicle records found</div>
</div>
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const plateColorMap = {
0: '蓝色 - 普通燃油车',
1: '黄色 - 大型车/货车',
2: '黑色 - 外籍车辆/港澳台车',
3: '白色 - 警车/军车/武警车',
4: '渐变绿色 - 新能源汽车',
5: '黄绿双拼色 - 大型新能源汽车',
6: '蓝白渐变色 - 临时牌照',
7: '临时牌照 - 临时行驶车辆',
11: '绿色 - 新能源汽车',
12: '红色 - 教练车/试验车'
};
const vehicleTypeMap = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车'
};
const vehicleList = computed(() => props.data?.list || []);
const vehicleCount = computed(() => props.data?.vehicleCount || 0);
const getPlateColorText = (plateColor) => {
return plateColorMap[plateColor] || '未知颜色 - 未知类型';
};
const getPlateColorClass = (plateColor) => {
const colorClassMap = {
0: 'bg-blue-500',
1: 'bg-yellow-500',
2: 'bg-gray-800',
3: 'bg-gray-200 text-gray-800',
4: 'bg-green-500',
5: 'bg-gradient-to-r from-yellow-500 to-green-500',
6: 'bg-gradient-to-r from-blue-500 to-white text-blue-800',
7: 'bg-red-500',
11: 'bg-green-500',
12: 'bg-red-500'
};
return colorClassMap[plateColor] || 'bg-gray-500';
};
const getVehicleTypeText = (vehicleType) => {
return vehicleTypeMap[vehicleType] || '未知类型';
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
/* 保持与 CQCXG9P1C 一致的布局风格 */
</style>

View File

@@ -0,0 +1,256 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆过户详版查询</h3>
<p class="header-desc">按时间轴展示每一次车辆过户的车牌与地区变更情况</p>
</div>
<div class="header-tag" v-if="totalTimes">
<span class="tag-label">总过户次数</span>
<span class="tag-value">{{ totalTimes }} </span>
</div>
</div>
<template v-if="transfers && transfers.length">
<div class="timeline">
<div v-for="(item, index) in transfers" :key="index" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot"></div>
<div v-if="index !== transfers.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="transfer-header">
<div class="transfer-date">{{ item.changeMonthFormatted }}</div>
<div class="transfer-count"> {{ item.transTimeSum }} 次过户</div>
</div>
<div class="plates-row">
<div class="plate old">
<div class="label">过户前车牌</div>
<div class="value">{{ item.oldCp || '未知' }}</div>
<div class="city" v-if="item.cityBefore">所在城市{{ item.cityBefore }}</div>
</div>
<div class="arrow"></div>
<div class="plate new">
<div class="label">过户后车牌</div>
<div class="value">{{ item.newCp || '未知' }}</div>
<div class="city" v-if="item.cityAfter">所在城市{{ item.cityAfter }}</div>
</div>
</div>
<div class="interval-row">
<span>距上次过户</span>
<span class="strong">{{ item.intervalText }}</span>
</div>
<div class="vin-row">
<span class="vin-label">VIN</span>
<span class="vin-value font-mono">{{ item.vin || '-' }}</span>
</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无过户明细</div>
<div class="sub">未查询到车辆过户明细记录</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const rawList = computed(() => Array.isArray(props.data?.retdata) ? props.data.retdata : []);
const transfers = computed(() =>
rawList.value.map((item) => {
const changeMonth = item.changeMonth;
let changeMonthFormatted = '-';
if (changeMonth === '近一年内过户') {
changeMonthFormatted = '近一年内过户';
} else if (typeof changeMonth === 'string' && changeMonth.length === 6) {
const y = changeMonth.slice(0, 4);
const m = changeMonth.slice(4, 6);
changeMonthFormatted = `${y}${m}`;
} else if (changeMonth) {
changeMonthFormatted = changeMonth;
}
let intervalText = '-';
if (item.transYear || item.transMonth) {
const years = item.transYear ? `${item.transYear}` : '';
const months = item.transMonth ? `${item.transMonth}个月` : '';
intervalText = `${years}${months}` || '-';
}
return {
...item,
changeMonthFormatted,
intervalText,
};
})
);
const totalTimes = computed(() => {
if (!transfers.value.length) return '';
const last = transfers.value[transfers.value.length - 1];
return last.transTimeSum ?? '';
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center justify-between mb-5 px-5 py-4 rounded-2xl bg-gradient-to-r from-indigo-50 via-blue-50 to-indigo-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-indigo-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-indigo-800 opacity-90;
}
.header-tag {
@apply inline-flex flex-col items-end gap-1 px-3 py-2 rounded-xl bg-white/80 shadow-sm text-indigo-800;
}
.tag-label {
@apply text-sm text-gray-500;
}
.tag-value {
@apply text-2xl font-bold leading-none whitespace-nowrap;
}
.timeline {
@apply mt-4;
}
.timeline-item {
@apply flex mb-4;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-center;
}
.dot {
@apply w-3 h-3 rounded-full bg-indigo-500;
}
.line {
@apply flex-1 w-px bg-gray-300 mt-1;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-gray-50/70 px-4 py-3;
}
.transfer-header {
@apply flex items-baseline justify-between mb-2;
}
.transfer-date {
@apply text-lg font-semibold text-gray-900;
}
.transfer-count {
@apply text-sm text-gray-600;
}
.plates-row {
@apply flex items-stretch gap-3 mt-2;
}
.plate {
@apply flex-1 rounded-xl px-3 py-2 border border-gray-200 bg-white;
}
.plate .label {
@apply text-xs text-gray-500 mb-1;
}
.plate .value {
@apply text-lg font-semibold text-gray-900;
}
.plate .city {
@apply text-sm text-gray-600 mt-1;
}
.plate.old {
@apply bg-gray-50;
}
.plate.new {
@apply bg-indigo-50/60 border-indigo-100;
}
.arrow {
@apply flex items-center justify-center text-2xl text-gray-400;
}
.interval-row {
@apply mt-3 text-base text-gray-700;
}
.interval-row .strong {
@apply font-semibold;
}
.vin-row {
@apply mt-2 text-sm text-gray-600;
}
.vin-label {
@apply text-gray-500;
}
.vin-value {
@apply text-base text-gray-900;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆静态信息查询</h3>
<p class="header-desc">查看车辆生产排放标准及燃料等核心静态信息</p>
</div>
</div>
<template v-if="records && records.length">
<div v-for="(item, idx) in records" :key="idx" class="vehicle-card">
<div class="vehicle-title">
<div>
<div class="vehicle-chip">车辆 {{ idx + 1 }}</div>
<div class="vehicle-model">
{{ item.vType || '未知车型' }}
</div>
</div>
<div class="vehicle-meta">
<span class="badge">{{ item.vFuelType || '燃料未知' }}</span>
</div>
</div>
<div class="field-grid">
<div class="field">
<div class="field-label">发动机号</div>
<div class="field-value code">{{ item.engineNO || '-' }}</div>
</div>
<div class="field">
<div class="field-label">发动机型号</div>
<div class="field-value code">{{ item.engineType || '-' }}</div>
</div>
<div class="field">
<div class="field-label">生产日期</div>
<div class="field-value">{{ item.vScdate || '-' }}</div>
</div>
<div class="field">
<div class="field-label">排放阶段</div>
<div class="field-value">{{ item.dischargeStage || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车辆分类</div>
<div class="field-value">{{ item.vClassification || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">生产企业名称</div>
<div class="field-value">{{ item.vManufacturer || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">生产厂地址</div>
<div class="field-value">{{ item.vSccdz || '-' }}</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无车辆静态信息</div>
<div class="sub">未查询到车辆静态信息或返回格式不正确</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: [Object, String, Array], default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
// 解析返回的 JSON 字符串,得到数组
const records = computed(() => {
const raw = props.data;
if (!raw) return [];
// data 本身是字符串
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed;
return [];
} catch {
return [];
}
}
// data 已经是数组
if (Array.isArray(raw)) {
return raw;
}
// data 是对象,里层再包了一层字符串/数组的情况
if (typeof raw === 'object') {
if (Array.isArray(raw.list)) return raw.list;
if (typeof raw.data === 'string') {
try {
const parsed = JSON.parse(raw.data);
if (Array.isArray(parsed)) return parsed;
} catch {
return [];
}
}
}
return [];
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply rounded-2xl mb-5 px-5 py-4 bg-gradient-to-r from-blue-50 via-indigo-50 to-blue-50 flex items-center justify-between;
color: #1e40af;
}
.header-title {
@apply text-xl font-semibold m-0;
}
.header-desc {
@apply text-sm mt-2 m-0 opacity-80 text-gray-700;
}
.header-left {
@apply flex flex-col;
}
.header-tag {
@apply inline-flex items-center gap-2 px-3 py-1 rounded-full text-base font-medium bg-white/80 text-blue-700 shadow-sm;
}
.header-tag .dot {
@apply w-2 h-2 rounded-full bg-green-500;
}
.vehicle-card {
@apply mb-4 p-5 rounded-2xl border border-gray-100 bg-gray-50/80;
}
.vehicle-title {
@apply flex items-start justify-between text-base text-gray-700;
}
.vehicle-title .label {
@apply text-base uppercase tracking-wide text-gray-500;
}
.vehicle-title .value {
@apply font-semibold text-lg text-gray-900 mt-1;
}
.vehicle-model {
@apply text-base font-semibold text-gray-900 mt-1;
}
.vehicle-meta {
@apply flex items-center gap-2;
}
.badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-base font-medium bg-blue-100 text-blue-700;
}
.field-label {
@apply text-base text-gray-500 mb-1;
}
.field-value {
@apply text-base text-gray-900;
}
.field-value.code {
@apply font-mono tracking-wide;
}
.field-grid {
@apply grid gap-y-3 gap-x-6 mt-4;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.field-span {
grid-column: 1 / -1;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-base font-medium mb-1;
}
.empty .sub {
@apply text-base;
}
</style>

View File

@@ -0,0 +1,493 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆出险记录核验</h3>
<p class="header-desc">综合车辆出险脱保重大事故等信息评估风险</p>
</div>
</div>
<template v-if="hasData">
<div class="risk-band" :class="riskLevelClass">
<div class="risk-band-label">风险等级</div>
<div class="risk-band-text">{{ riskLevelText }}</div>
</div>
<!-- 顶部车辆与价格概览 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="plate" v-if="data.LicensePlate">{{ data.LicensePlate }}</div>
<div class="car-type">{{ data.CarType || '未知车型' }}</div>
</div>
<div class="summary-right">
<div class="summary-label">二手车价格参考</div>
<div class="summary-price">{{ usedCarPriceText }}</div>
<div class="summary-sub">新车购置价{{ newCarPriceText }}</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span>燃料</span><span class="strong">{{ data.FuelType || '未知' }}</span>
<span class="dot"></span>
<span>发动机号</span><span class="strong code">{{ data.EngineNumber || '-' }}</span>
</div>
<div class="meta-line">
<span>初登日期</span><span class="strong">{{ data.DebutDate || '-' }}</span>
<span class="dot"></span>
<span>车龄</span><span class="strong">{{ carAgeText }}</span>
</div>
</div>
</div>
<!-- 核心风险指标 -->
<div class="detail-card">
<h4 class="section-title">核心风险指标</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">是否高风险车辆</div>
<div class="field-value" :class="flagClass(data.IfHighriskVehicle === '1')">
{{ yesNoText(data.IfHighriskVehicle, '高风险车辆') }}
</div>
</div>
<div class="field">
<div class="field-label">是否营运车辆</div>
<div class="field-value" :class="flagClass(data.IsOperation === '1')">
{{ yesNoText(data.IsOperation, '营运车辆') }}
</div>
</div>
<div class="field">
<div class="field-label">是否投保车损险</div>
<div class="field-value" :class="flagClass(data.IfCarDamage === '1')">
{{ yesNoText(data.IfCarDamage, '已投保车损险', '未投保车损险') }}
</div>
</div>
<div class="field">
<div class="field-label">是否连续投保</div>
<div class="field-value" :class="flagClass(data.IsConInsure === '1')">
{{ yesNoText(data.IsConInsure, '连续投保', '非连续投保') }}
</div>
</div>
<div class="field">
<div class="field-label">历史是否脱保</div>
<div class="field-value" :class="flagClass(data.IfTuoBao === '1')">
{{ yesNoText(data.IfTuoBao, '有脱保记录', '无脱保记录') }}
</div>
</div>
<div class="field">
<div class="field-label">历史最大脱保时间</div>
<div class="field-value">{{ data.TuoBaoTime || '-' }}</div>
</div>
<div class="field">
<div class="field-label">最高车损险损失比例</div>
<div class="field-value">{{ data.CompensationRatioo || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车损险综合评分</div>
<div class="field-value strong">{{ data.Total || '-' }}</div>
</div>
</div>
</div>
<!-- 出险与事故情况 -->
<div class="detail-card">
<h4 class="section-title">出险与事故情况</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">商业险出险</div>
<div class="field-value">{{ formatDangerCount(data.CommercialPolicyDangerCount, '商业险') }}</div>
</div>
<div class="field">
<div class="field-label">交强险出险</div>
<div class="field-value">{{ formatDangerCount(data.CompulsoryPolicyDangerCount, '交强险') }}</div>
</div>
<div class="field">
<div class="field-label">三者险出险次数</div>
<div class="field-value">{{ formatDangerCount(data.ThreeRisksDangerCount, '三者险') }}</div>
</div>
<div class="field">
<div class="field-label">全损情况</div>
<div class="field-value">{{ totalLossText }}</div>
</div>
<div class="field field-span">
<div class="field-label">重大事故标志</div>
<div class="field-value">{{ formatMajorAccident(data.MajorAccident) }}</div>
</div>
<div class="field">
<div class="field-label">事故次数</div>
<div class="field-value">{{ data.IsMajorAccidentData || '-' }}</div>
</div>
<div class="field">
<div class="field-label">事故等级</div>
<div class="field-value">{{ data.IsMajorAccidentLevel || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">损失部位</div>
<div class="field-value">{{ formatLossPart(data.LossPart) }}</div>
</div>
</div>
</div>
<!-- 保单与责任险可保情况 -->
<div class="detail-card">
<h4 class="section-title">保单与责任险承保情况</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">商业险保单倒计时</div>
<div class="field-value">{{ formatPolicyTime(data.CommercialPolicyTime, '商业险') }}</div>
</div>
<div class="field">
<div class="field-label">交强险保单倒计时</div>
<div class="field-value">{{ formatPolicyTime(data.CompulsoryPolicyTime, '交强险') }}</div>
</div>
<div class="field">
<div class="field-label">商业险过户次数</div>
<div class="field-value">{{ formatTransferCount(data.CommercialPolicyTransferCount) }}</div>
</div>
<div class="field">
<div class="field-label">交强险过户次数</div>
<div class="field-value">{{ formatTransferCount(data.CompulsoryPolicyTransferCount) }}</div>
</div>
<div class="field">
<div class="field-label">是否可投保责任险</div>
<div class="field-value" :class="flagClass(data.IsLiabilityAvailable === 'Y')">
{{ ynText(data.IsLiabilityAvailable, '可投保', '不可投保') }}
</div>
</div>
<div class="field">
<div class="field-label">是否可承保延保</div>
<div class="field-value" :class="flagClass(data.IsExtendAvailable === 'Y')">
{{ ynText(data.IsExtendAvailable, '可承保', '不可承保') }}
</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无出险记录</div>
<div class="sub">未查询到车辆出险记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const data = computed(() => props.data || {});
const hasData = computed(() => !!data.value && Object.keys(data.value).length > 0);
const usedCarPriceText = computed(() => {
const v = data.value.UsedCarPrice;
if (!v) return '-';
return `${v}`;
});
const newCarPriceText = computed(() => {
const v = data.value.PurchasePrice;
if (!v) return '-';
return `${v}`;
});
const carAgeText = computed(() => {
const m = data.value.CarAge;
if (!m) return '-';
return `${m} 个月`;
});
const totalLossText = computed(() => {
const v = data.value.TotalLoss;
if (v === '1') return '存在全损记录';
if (v === '0') return '无全损记录';
return '-';
});
// 简单按高风险车辆/重大事故等情况给出一个文字风险等级
const riskLevelText = computed(() => {
if (data.value.IfHighriskVehicle === '1') return '高风险';
if (data.value.IsMajorAccidentLevel && data.value.IsMajorAccidentLevel !== '一般') return '较高风险';
if (data.value.IsMajorAccidentData && data.value.IsMajorAccidentData !== '0') return '有事故记录';
return '风险可控';
});
const riskLevelClass = computed(() => {
const t = riskLevelText.value;
if (t === '高风险') return 'risk-high';
if (t === '较高风险' || t === '有事故记录') return 'risk-mid';
return 'risk-low';
});
const flagClass = (flag) => {
return flag ? 'flag-yes' : 'flag-no';
};
const yesNoText = (val, yesText, noText = '否') => {
if (val === '1') return yesText;
if (val === '0') return noText;
return '-';
};
const ynText = (val, yesText, noText) => {
if (val === 'Y') return yesText;
if (val === 'N') return noText;
return '-';
};
const formatPolicyTime = (val, label) => {
if (!val || val === 'NULL') {
return `当期无${label}保单`;
}
const parts = String(val).split(':');
if (parts.length < 2) return val;
const daysRaw = parts[1];
if (!daysRaw || daysRaw.toLowerCase() === 'null') {
return `${label}保单已过期`;
}
const days = Number(daysRaw);
if (Number.isNaN(days)) return val;
if (days < 0) return `${label}保单已过期`;
return `${label}保单剩余 ${days}`;
};
const formatDangerCount = (val, label) => {
if (!val) return '-';
const parts = String(val).split(':');
const countRaw = parts[1] ?? '';
if (!countRaw || countRaw.toLowerCase() === 'null') {
return `${label}暂无出险记录`;
}
const count = Number(countRaw);
if (Number.isNaN(count)) return val;
if (count === 0) return `${label}暂无出险记录`;
return `${label}出险 ${count}`;
};
const formatTransferCount = (val) => {
if (!val) return '-';
const parts = String(val).split(':');
const countRaw = parts[1] ?? '';
if (!countRaw || countRaw.toLowerCase() === 'null') return '-';
const count = Number(countRaw);
if (Number.isNaN(count)) return val;
return `${count}`;
};
const formatMajorAccident = (val) => {
if (!val) return '-';
const map = {
A: '碰撞',
B: '火自燃',
C: '水淹',
D: '盗抢',
};
const list = [];
String(val)
.split(',')
.forEach((pair) => {
const [k, v] = pair.split(':');
if (v === '1' && map[k]) {
list.push(map[k]);
}
});
if (!list.length) return '无重大事故记录';
return `重大事故类型:${list.join('、')}`;
};
const formatLossPart = (val) => {
if (!val) return '-';
const partMap = {
1: '正前方',
2: '正后方',
3: '顶部',
4: '底部',
5: '前方左侧',
6: '后方左侧',
7: '中间左侧',
8: '前方右侧',
9: '后方右侧',
10: '中间右侧',
11: '内部',
12: '其它',
13: '不详',
};
const items = [];
String(val)
.split(',')
.forEach((pair) => {
const [kRaw, vRaw] = pair.split(':');
const key = Number(kRaw);
const count = Number(vRaw);
if (!Number.isNaN(key) && !Number.isNaN(count) && count > 0) {
const label = partMap[key] || `部位${key}`;
items.push(`${label}${count}`);
}
});
if (!items.length) return '暂无损失部位信息';
return items.join('、');
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-3 px-5 py-4 rounded-2xl bg-gradient-to-r from-rose-50 via-orange-50 to-amber-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-rose-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-rose-800 opacity-90;
}
.risk-band {
@apply mb-4 px-4 py-3 rounded-2xl flex items-center justify-between;
}
.risk-band.risk-high {
@apply bg-red-50 border border-red-100;
}
.risk-band.risk-mid {
@apply bg-amber-50 border border-amber-100;
}
.risk-band.risk-low {
@apply bg-emerald-50 border border-emerald-100;
}
.risk-band-label {
@apply text-sm text-gray-600;
}
.risk-band-text {
@apply text-xl font-bold;
}
.summary-card {
@apply rounded-2xl border border-amber-100 bg-amber-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-2;
}
.plate {
@apply inline-flex items-center px-4 py-2 rounded-full bg-slate-900 text-white text-xl font-semibold tracking-widest;
}
.car-type {
@apply text-lg font-medium text-gray-800;
}
.summary-right {
@apply text-right;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-price {
@apply text-2xl font-bold text-amber-800 mt-1;
}
.summary-sub {
@apply text-sm text-amber-700 opacity-90;
}
.summary-meta {
@apply space-y-1 text-base text-gray-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.strong {
@apply font-semibold;
}
.code {
@apply font-mono tracking-wide;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.field-grid {
@apply grid gap-y-3 gap-x-6;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.field-label {
@apply text-sm text-gray-500 mb-1;
}
.field-value {
@apply text-base text-gray-900;
}
.field-span {
grid-column: 1 / -1;
}
.flag-yes {
@apply text-red-700;
}
.flag-no {
@apply text-emerald-700;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="card">
<!-- 名下车辆信息展示 -->
<div class="bg-yellow-100 text-yellow-700 p-4 rounded-lg">
<h3 class="text-xl font-semibold">名下车辆</h3>
<p class="text-sm">此人名下拥有车辆{{ data?.carNum }} </p>
</div>
<!-- 校验对象展示 -->
</div>
</template>
<script setup>
import { defineProps, watch, computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
// 接收父组件传入的 props
const props = defineProps({
data: Object,
params: Object,
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
// 脱敏函数:姓名脱敏(保留首位)
const maskName = (name) => {
if (!name) return '';
return name.length > 1 ? name[0] + "*".repeat(name.length - 1) : "*";
};
// 脱敏函数身份证号脱敏保留前6位和最后4位
const maskIdCard = (idCard) => {
if (!idCard) return '';
return idCard.replace(/^(.{6})(?:\d+)(.{4})$/, "$1****$2");
};
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 名下车辆不算风险始终返回100分最安全
return 100;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
</script>
<style scoped>
/* 自定义样式 */
</style>

View File

@@ -0,0 +1,130 @@
<template>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<!-- 车辆总数统计 -->
<div class="flex justify-between items-center mb-6 pb-4 border-b border-gray-100">
<div class="flex items-center gap-3">
<div>
<div class="text-lg font-semibold text-gray-900">个人名下车辆</div>
</div>
</div>
<div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium">
{{ vehicleCount }}
</div>
</div>
<!-- 车辆列表 -->
<div class="space-y-3" v-if="vehicleList && vehicleList.length > 0">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:bg-blue-50 hover:border-blue-200 transition-colors duration-200"
v-for="(vehicle, index) in vehicleList" :key="index">
<div class="space-y-3">
<div class="text-xl font-bold text-gray-900 font-mono tracking-wider">
{{ vehicle.plateNum }}
</div>
<div class="flex items-center gap-3">
<div class="inline-flex items-center gap-1 px-3 py-1 rounded text-xs font-medium text-white"
:class="getPlateColorClass(vehicle.plateColor)">
<span>🏷</span>
<span>{{ getPlateColorText(vehicle.plateColor) }}</span>
</div>
<div class="text-sm text-gray-600">
<span class="text-gray-500">车辆类型:</span>
<span class="font-medium text-gray-900 ml-1">{{ getVehicleTypeText(vehicle.vehicleType)
}}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div class="text-center py-12 text-gray-500" v-else>
<div class="text-4xl mb-3">🚫</div>
<div class="text-lg font-medium mb-1">暂无车辆信息</div>
<div class="text-sm">No vehicle records found</div>
</div>
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue';
// 接收父组件传入的 props
const props = defineProps({
data: Object,
params: Object,
});
// 车牌颜色映射
const plateColorMap = {
0: '蓝色 - 普通燃油车',
1: '黄色 - 大型车/货车',
2: '黑色 - 外籍车辆/港澳台车',
3: '白色 - 警车/军车/武警车',
4: '渐变绿色 - 新能源汽车',
5: '黄绿双拼色 - 大型新能源汽车',
6: '蓝白渐变色 - 临时牌照',
7: '临时牌照 - 临时行驶车辆',
11: '绿色 - 新能源汽车',
12: '红色 - 教练车/试验车'
};
// 车辆类型映射
const vehicleTypeMap = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车'
};
// 计算属性
const vehicleList = computed(() => props.data?.list || []);
const vehicleCount = computed(() => props.data?.vehicleCount || 0);
// 获取车牌颜色文本
const getPlateColorText = (plateColor) => {
return plateColorMap[plateColor] || '未知颜色 - 未知类型';
};
// 获取车牌颜色样式类
const getPlateColorClass = (plateColor) => {
const colorClassMap = {
0: 'bg-blue-500',
1: 'bg-yellow-500',
2: 'bg-gray-800',
3: 'bg-gray-200 text-gray-800',
4: 'bg-green-500',
5: 'bg-gradient-to-r from-yellow-500 to-green-500',
6: 'bg-gradient-to-r from-blue-500 to-white text-blue-800',
7: 'bg-red-500',
11: 'bg-green-500',
12: 'bg-red-500'
};
return colorClassMap[plateColor] || 'bg-gray-500';
};
// 获取车辆类型文本
const getVehicleTypeText = (vehicleType) => {
return vehicleTypeMap[vehicleType] || '未知类型';
};
onMounted(() => {
console.log('车辆数据:', props.data);
});
</script>
<style scoped>
/* 自定义样式 - 仅保留必要的样式 */
</style>

View File

@@ -0,0 +1,216 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">人车核验简版</h3>
<p class="header-desc">校验人员姓名与车辆号牌是否匹配</p>
</div>
<div class="result-section" :class="resultSectionClass">
<div class="result-icon-wrap">
<span class="result-icon" :class="iconClass">
{{ iconChar }}
</span>
</div>
<div class="result-label">核验结果</div>
<div class="result-value" :class="resultClass">{{ resultText }}</div>
</div>
<div v-if="hasParams" class="info-rows">
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">车牌号</span>
<span class="info-value font-mono">{{ params?.plate_no || params?.car_license || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">号牌类型</span>
<span class="info-value">{{ params?.carplate_type || params?.car_type || '-' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
params: {
type: Object,
default: () => ({}),
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
// verify_code: 1 一致2 不匹配
const isMatch = computed(() => {
const code = props.data?.verify_code;
if (code === 1) return true;
if (code === 2) return false;
return null; // 无有效数据时
});
const resultText = computed(() => {
if (isMatch.value === true) return '一致';
if (isMatch.value === false) return '不匹配';
return '暂无结果';
});
const resultClass = computed(() => {
if (isMatch.value === true) return 'result-match';
if (isMatch.value === false) return 'result-mismatch';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
if (isMatch.value === true) return 'result-section match';
if (isMatch.value === false) return 'result-section mismatch';
return 'result-section unknown';
});
const iconClass = computed(() => {
if (isMatch.value === true) return 'icon-match';
if (isMatch.value === false) return 'icon-mismatch';
return 'icon-unknown';
});
const iconChar = computed(() => {
if (isMatch.value === true) return '✓';
if (isMatch.value === false) return '✕';
return '?';
});
const hasParams = computed(() => {
const p = props.params || {};
return p.name || p.plate_no || p.car_license || p.carplate_type || p.car_type;
});
// 简版人车核验本身不直接计为负面风险,给满分
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({
riskScore,
});
</script>
<style scoped>
.card {
@apply bg-white rounded-lg p-4 shadow-sm border border-gray-100;
}
.header-box {
@apply rounded-lg mb-4 p-4;
background: linear-gradient(135deg, #1768ff 0%, #1556d6 100%);
color: #fff;
}
.header-title {
@apply text-lg font-semibold m-0;
}
.header-desc {
@apply text-sm mt-1 opacity-90 m-0;
}
.result-section {
@apply rounded-xl p-5 text-center mb-4;
}
.result-section.match {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.result-section.mismatch {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
border: 1px solid rgba(244, 67, 54, 0.3);
}
.result-section.unknown {
@apply bg-gray-50 border border-gray-200;
}
.result-icon-wrap {
@apply mb-2;
}
.result-icon {
@apply inline-flex items-center justify-center w-12 h-12 rounded-full text-2xl font-bold text-white;
}
.result-icon.icon-match {
background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
}
.result-icon.icon-mismatch {
background: linear-gradient(135deg, #e53935 0%, #c62828 100%);
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.4);
}
.result-icon.icon-unknown {
background: linear-gradient(135deg, #78909c 0%, #546e7a 100%);
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.3);
}
.result-label {
@apply text-sm text-gray-500 mb-1;
}
.result-value {
@apply text-xl font-semibold;
}
.result-match {
color: #2e7d32;
}
.result-mismatch {
color: #c62828;
}
.result-unknown {
@apply text-gray-500;
}
.info-rows {
@apply space-y-3 pt-2 border-t border-gray-100;
}
.info-row {
@apply flex items-center text-sm;
}
.info-label {
@apply w-20 text-gray-500 shrink-0;
}
.info-value {
@apply font-medium text-gray-800;
}
</style>

View File

@@ -0,0 +1,634 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆出险详版查询</h3>
<p class="header-desc">展示多维出险记录碰撞部位及车况信息辅助评估车辆风险</p>
</div>
</div>
<template v-if="hasData">
<!-- 车辆基本信息 + 碰撞统计 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="summary-label">品牌名称</div>
<div class="summary-brand">{{ clxx.brandName || '未知品牌' }}</div>
<div class="summary-subline" v-if="clxx.vehicleStyle">
{{ clxx.vehicleStyle }}
</div>
</div>
<div class="summary-right">
<div class="summary-label">车架号 VIN</div>
<div class="summary-vin font-mono">{{ pzVin || clxxVin || '-' }}</div>
<div class="summary-subline" v-if="clxx.licensePlate">
车牌号{{ clxx.licensePlate }}
</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span class="meta-label">事故总次数</span>
<span class="meta-value strong">{{ tjxx.claimCount ?? '-' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">总维修金额</span>
<span class="meta-value strong">{{ tjxx.totalAmount || '-' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">最大单次维修金额</span>
<span class="meta-value strong">{{ tjxx.largestAmount || '-' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">已结案次数</span>
<span class="meta-value strong">{{ tjxx.claimCacCount ?? 0 }} </span>
</div>
<div class="meta-line">
<span class="meta-label">未结案次数</span>
<span class="meta-value strong">{{ tjxx.claimUnCacCount ?? 0 }} </span>
</div>
</div>
</div>
<!-- 碰撞记录时间轴 -->
<div class="detail-card" v-if="pzRecords && pzRecords.length">
<h4 class="section-title">碰撞出险记录</h4>
<div class="timeline">
<div v-for="(rec, idx) in pzRecords" :key="idx" class="timeline-item">
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ rec.date || '-' }}</div>
<div class="amount">{{ formatFen(rec.serviceMoney) }}</div>
</div>
<div class="row-sub">
<span>{{ rec.accidentType || '出险' }}</span>
<span class="status">{{ rec.claimStatus || '-' }}</span>
</div>
<div class="sub-section" v-if="rec.result && rec.result.length">
<div class="sub-title">维修明细</div>
<ul class="sub-list">
<li v-for="(d, di) in rec.result" :key="di">
<span class="tag">{{ dangerTypeText(d.dangerSingleType) }}</span>
<span>{{ d.dangerSingleName }}</span>
<span v-if="d.dangerSingleNum">×{{ d.dangerSingleNum }}</span>
<span v-if="d.dangerSingleMoney" class="money">
{{ formatFen(d.dangerSingleMoney) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 车况排查大类 -->
<div class="detail-card" v-if="ckdlpc">
<h4 class="section-title">车况排查大类</h4>
<div class="ckdlpc-grid">
<div v-for="item in ckdlpcList" :key="item.key" class="ckdlpc-item">
<div class="ckdlpc-name">{{ item.label }}</div>
<div class="ckdlpc-status" :class="ckLevelClass(item.value)">
{{ ckLevelText(item.value) }}
</div>
</div>
</div>
</div>
<!-- 车况明细排查部件所有分组都展示如整体无命中则显示一行暂无车况明细排查记录 -->
<div class="detail-card" v-if="ckpclbGroups && ckpclbGroups.length">
<h4 class="section-title">车况明细排查部件</h4>
<template v-if="hasCkpclbHit">
<div class="ckpclb-grid">
<div v-for="group in ckpclbGroups" :key="group && group.key" v-if="group" class="ckpclb-group">
<div class="ckpclb-title">{{ group.label }}</div>
<div class="ckpclb-tags" v-if="group.items && group.items.length">
<span v-for="(p, pi) in group.items" :key="pi" class="part-tag part-tag-hit">
{{ p.name }}{{ p.type }}
</span>
</div>
<div v-else class="ckpclb-empty">无相关排查记录</div>
</div>
</div>
</template>
<div v-else class="text-sm text-gray-500">暂无车况明细排查记录</div>
</div>
<!-- 车辆损失方位总结所有方位按矩阵展示有受损的高亮显示 -->
<div class="detail-card" v-if="clfwzjMatrix && clfwzjMatrix.length">
<h4 class="section-title">车辆损失方位总结</h4>
<div class="clfwzj-matrix">
<div v-for="(row, ri) in clfwzjMatrix" :key="ri" class="clfwzj-row">
<div v-for="(cell, ci) in row" :key="ci" class="clfwzj-cell">
<div v-if="cell && cell.label"
:class="['pos-box', cell.value === 1 ? 'pos-box-hit' : 'pos-box-normal']">
{{ cell.label }}
</div>
</div>
</div>
</div>
<p class="mt-4 text-sm text-gray-500">红色方位表示该部位存在受损记录灰色表示当前无受损记录</p>
</div>
<!-- 车况信息简要 -->
<div class="detail-card" v-if="ckxx">
<h4 class="section-title">车况信息概览</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">是否火烧</div>
<div class="field-value" :class="flagClass(ckxx.isFire === 1)">
{{ bool01Text(ckxx.isFire) }}
</div>
</div>
<div class="field">
<div class="field-label">是否水淹</div>
<div class="field-value" :class="flagClass(ckxx.isFlood === 1)">
{{ bool01Text(ckxx.isFlood) }}
</div>
</div>
<div class="field">
<div class="field-label">是否偷盗</div>
<div class="field-value" :class="flagClass(ckxx.isTheft === 1)">
{{ bool01Text(ckxx.isTheft) }}
</div>
</div>
<div class="field">
<div class="field-label">是否覆盖件损伤</div>
<div class="field-value" :class="flagClass(ckxx.isPanel === 1)">
{{ bool01Text(ckxx.isPanel) }}
</div>
</div>
<div class="field">
<div class="field-label">是否大额赔偿</div>
<div class="field-value">
{{ largeCostText(ckxx.isLargeCost) }}
</div>
</div>
<div class="field">
<div class="field-label">未结案记录</div>
<div class="field-value">{{ ynUnknown(ckxx.recordIcpending) }}</div>
</div>
<div class="field">
<div class="field-label">注销记录</div>
<div class="field-value">{{ ynUnknown(ckxx.recordIwriteoff) }}</div>
</div>
<div class="field">
<div class="field-label">拒赔记录</div>
<div class="field-value">{{ ynUnknown(ckxx.refusalRecord) }}</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无出险详版数据</div>
<div class="sub">未查询到车辆详细出险记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const retdata = computed(() => props.data?.retdata || {});
const hasData = computed(() => !!retdata.value && Object.keys(retdata.value).length > 0);
const pzlsmx = computed(() => retdata.value.pzlsmx || {});
const pzRecords = computed(() => Array.isArray(pzlsmx.value.records) ? pzlsmx.value.records : []);
const pzVin = computed(() => (pzRecords.value[0]?.vin) || '');
const ckdlpc = computed(() => retdata.value.ckdlpc || null);
const ckxx = computed(() => retdata.value.ckxx || null);
const ckpclb = computed(() => retdata.value.ckpclb || null);
const clxx = computed(() => retdata.value.clxx || {});
const clfwzj = computed(() => retdata.value.clfwzj || null);
const tjxx = computed(() => retdata.value.tjxx || {});
const clxxVin = computed(() => clxx.value.vin || '');
const ckdlpcList = computed(() => {
if (!ckdlpc.value) return [];
const map = [
{ key: 'type1', label: '骨架' },
{ key: 'type2', label: '外观' },
{ key: 'type3', label: '发动机/变速箱' },
{ key: 'type4', label: '火烧' },
{ key: 'type5', label: '水淹' },
{ key: 'type6', label: '气囊' },
{ key: 'type7', label: '加强件' },
];
return map.map((m) => ({
key: m.key,
label: m.label,
value: ckdlpc.value[m.key],
}));
});
const hasCkpclb = computed(() => {
const v = ckpclb.value;
if (!v) return false;
return Object.values(v).some((arr) => Array.isArray(arr) && arr.length > 0);
});
const hasCkpclbHit = computed(() => {
const v = ckpclb.value;
if (!v) return false;
return Object.values(v).some((arr) => Array.isArray(arr) && arr.length > 0);
});
const ckpclbGroups = computed(() => {
if (!ckpclb.value) return [];
const labelMap = {
dp: '底盘悬挂',
fdj: '发动机',
fspj: '附属配件',
gj: '骨架',
hs: '火烧',
jqj: '加强件',
qn: '气囊',
sy: '水淹',
wg: '外观',
};
return Object.entries(ckpclb.value).map(([key, arr]) => ({
key,
label: labelMap[key] || key,
items: Array.isArray(arr) ? arr : [],
}));
});
const hasClfwzj = computed(() => {
if (!clfwzj.value) return false;
return Object.values(clfwzj.value).some((v) => v === 1);
});
const clfwzjMatrix = computed(() => {
const src = clfwzj.value || {};
const val = (key) => (src[key] === 1 ? 1 : 0);
// 按大致方位排布成矩阵,便于理解
return [
[
{ label: '', value: null },
{ label: '正前方', value: val('正前方') },
{ label: '', value: null },
],
[
{ label: '前方左侧', value: val('前方左侧') },
{ label: '顶部', value: val('顶部') },
{ label: '前方右侧', value: val('前方右侧') },
],
[
{ label: '中间左侧', value: val('中间左侧') },
{ label: '内部', value: val('内部') },
{ label: '中间右侧', value: val('中间右侧') },
],
[
{ label: '后方左侧', value: val('后方左侧') },
{ label: '底部', value: val('底部') },
{ label: '后方右侧', value: val('后方右侧') },
],
[
{ label: '', value: null },
{ label: '正后方', value: val('正后方') },
{ label: '其他', value: val('其他') },
],
];
});
const ckLevelText = (v) => {
const num = Number(v);
if (Number.isNaN(num)) return '未知';
if (num === 0) return '正常';
if (num === 1) return '无法确定';
if (num === 2) return '疑似异常';
if (num === 3) return '维保异常';
if (num === 4) return '碰撞异常';
return '未知';
};
const ckLevelClass = (v) => {
const num = Number(v);
if (num === 0) return 'level-ok';
if (num === 1) return 'level-unknown';
if (num === 2) return 'level-suspect';
if (num === 3) return 'level-maintain';
if (num === 4) return 'level-collision';
return 'level-unknown';
};
const formatFen = (val) => {
if (!val && val !== 0) return '-';
const n = Number(val);
if (Number.isNaN(n)) return `${val}`;
const yuan = n / 100;
return `${yuan.toLocaleString()}`;
};
const dangerTypeText = (t) => {
if (t === '1') return '更换';
if (t === '2') return '维修';
if (t === '3') return '材料';
return '其他';
};
const bool01Text = (v) => {
if (v === 1) return '是';
if (v === 0) return '否';
return '未知';
};
const largeCostText = (v) => {
if (v === 0) return '无大额赔偿记录';
if (v === 1) return '有大额赔偿记录';
if (v === 2) return '无法确定是否大额赔偿';
return '未知';
};
const ynUnknown = (v) => {
if (v === '是') return '是';
if (v === '否') return '否';
if (v == null) return '未知';
return v;
};
const flagClass = (flag) => {
return flag ? 'flag-yes' : 'flag-no';
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-orange-50 via-amber-50 to-rose-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-orange-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-orange-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border border-amber-100 bg-amber-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-1;
}
.summary-right {
@apply text-right;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-brand {
@apply text-lg font-semibold text-gray-900;
}
.summary-vin {
@apply text-base text-gray-900;
}
.summary-subline {
@apply text-sm text-gray-700 mt-1;
}
.summary-meta {
@apply space-y-2 text-base text-gray-800;
}
.meta-line {
@apply flex justify-between items-center;
}
.meta-label {
@apply text-sm text-gray-600;
}
.meta-value {
@apply text-base;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.strong {
@apply font-semibold;
}
.code {
@apply font-mono tracking-wide;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .amount {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.status {
@apply text-xs px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 font-medium;
}
.sub-section {
@apply mt-3;
}
.sub-title {
@apply text-sm font-semibold text-gray-800 mb-1;
}
.sub-list {
@apply text-sm text-gray-800 space-y-1;
}
.sub-list li {
@apply flex flex-wrap gap-1;
}
.tag {
@apply inline-flex items-center px-2 py-0.5 rounded-full bg-orange-50 text-orange-700 text-xs font-medium;
}
.money {
@apply text-xs text-gray-500;
}
.ckdlpc-grid {
@apply grid gap-3;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.ckdlpc-item {
@apply p-3 rounded-xl bg-white border border-gray-100;
}
.ckdlpc-name {
@apply text-sm text-gray-600 mb-1;
}
.ckdlpc-status {
@apply text-sm font-semibold;
}
.level-ok {
@apply text-emerald-700;
}
.level-unknown {
@apply text-gray-600;
}
.level-suspect {
@apply text-amber-700;
}
.level-maintain {
@apply text-blue-700;
}
.level-collision {
@apply text-red-700;
}
.ckpclb-grid {
@apply grid gap-3;
}
.ckpclb-group {
@apply p-3 rounded-xl bg-white border border-gray-100;
}
.ckpclb-title {
@apply text-sm font-semibold text-gray-800 mb-2;
}
.ckpclb-tags {
@apply flex flex-wrap gap-2;
}
.part-tag {
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium;
}
.part-tag-hit {
@apply bg-sky-50 text-sky-700;
}
.clfwzj-matrix {
@apply grid gap-2;
}
.clfwzj-row {
@apply grid gap-2;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.clfwzj-cell {
@apply flex items-center justify-center;
}
.pos-box {
@apply w-full text-center px-3 py-2 rounded-xl text-xs font-medium;
}
.pos-box-hit {
@apply bg-rose-50 text-rose-700;
}
.pos-box-normal {
@apply bg-gray-100 text-gray-500;
}
.field-grid {
@apply grid gap-y-3 gap-x-6;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.field-label {
@apply text-sm text-gray-500 mb-1;
}
.field-value {
@apply text-base text-gray-900;
}
.flag-yes {
@apply text-red-700;
}
.flag-no {
@apply text-emerald-700;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,228 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">二手车VIN估值</h3>
<p class="header-desc">基于车型排量排放标准等信息给出参考估值</p>
</div>
<div class="header-tag">
<span class="tag-label">估值结果</span>
<span class="tag-value">{{ data.estimatedValue || '-' }}</span>
</div>
</div>
<template v-if="hasData">
<div class="summary-card">
<div class="summary-main">
<div class="summary-price">{{ data.estimatedValue || '-' }}</div>
<div class="summary-sub">参考估值仅供参考实际价格以市场为准</div>
</div>
<div class="summary-meta">
<div class="badge">{{ data.seriesName || '未知车系' }}</div>
<div class="meta-line">
<span>{{ data.manufacturerName || '未知厂商' }}</span>
<span v-if="data.productionDate" class="dot"></span>
<span v-if="data.productionDate">{{ data.productionDate }} 年出厂</span>
</div>
<div class="meta-line meta-small">
<span v-if="data.displacement">排量{{ data.displacement }}</span>
<span v-if="data.transmissionType" class="dot"></span>
<span v-if="data.transmissionType">变速箱{{ data.transmissionType }}</span>
<span v-if="data.emissionStandard" class="dot"></span>
<span v-if="data.emissionStandard">排放{{ data.emissionStandard }}</span>
</div>
</div>
</div>
<div class="detail-card">
<h4 class="section-title">基础信息</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">厂商品牌名称</div>
<div class="field-value">{{ data.manufacturerName || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车系名称</div>
<div class="field-value">{{ data.seriesName || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车型年款</div>
<div class="field-value">{{ data.modelYear || data.productionDate || '-' }}</div>
</div>
<div class="field">
<div class="field-label">座位数</div>
<div class="field-value">{{ data.seatingCapacity || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">车型名称</div>
<div class="field-value">{{ data.modelName || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">车型指导价</div>
<div class="field-value">{{ data.msrp || '-' }}</div>
</div>
</div>
</div>
<div class="detail-card">
<h4 class="section-title">技术参数</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">排量</div>
<div class="field-value">{{ data.displacement || '-' }}</div>
</div>
<div class="field">
<div class="field-label">变速箱类型</div>
<div class="field-value">{{ data.transmissionType || '-' }}</div>
</div>
<div class="field">
<div class="field-label">排放标准</div>
<div class="field-value">{{ data.emissionStandard || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车身颜色</div>
<div class="field-value">{{ data.color || '-' }}</div>
</div>
<div class="field field-span" v-if="data.seriesGroupName">
<div class="field-label">车系组名</div>
<div class="field-value">{{ data.seriesGroupName }}</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无估值结果</div>
<div class="sub">未查询到有效的估值数据请检查 VIN 与车辆信息是否正确</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center justify-between mb-5 px-5 py-4 rounded-2xl bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-amber-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-amber-800 opacity-90;
}
.header-tag {
@apply inline-flex flex-col items-end gap-1 px-3 py-2 rounded-xl bg-white/80 shadow-sm;
}
.tag-label {
@apply text-sm text-gray-500;
}
.tag-value {
@apply text-2xl font-bold text-amber-700 leading-none whitespace-nowrap;
}
.summary-card {
@apply rounded-2xl border border-amber-100 bg-amber-50/60 px-5 py-5 mb-4 flex flex-col gap-4;
}
.summary-main {}
.summary-price {
@apply text-3xl font-extrabold text-amber-800 leading-tight;
}
.summary-sub {
@apply text-sm text-amber-700 opacity-90;
}
.summary-meta {
@apply space-y-2 text-base text-gray-800;
}
.badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2 text-sm text-gray-700;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.meta-small {
@apply text-sm text-gray-600;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-4;
}
.field-grid {
@apply grid gap-y-3 gap-x-6;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.field-label {
@apply text-sm text-gray-500 mb-1.5;
}
.field-value {
@apply text-base text-gray-900;
}
.field-span {
grid-column: 1 / -1;
}
.empty {
@apply text-center py-12 text-gray-500;
}
.empty .icon {
@apply text-4xl mb-3;
}
.empty .title {
@apply text-lg font-medium mb-2;
}
.empty .sub {
@apply text-sm;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">人车核验详版</h3>
<p class="header-desc">展示人员与车辆的详细匹配结果及相关说明</p>
</div>
<div class="result-section" :class="resultSectionClass">
<div class="result-icon-wrap">
<span class="result-icon" :class="iconClass">
{{ iconChar }}
</span>
</div>
<div class="result-label">认证结果</div>
<div class="result-value" :class="resultTextClass">{{ resultText }}</div>
<p v-if="resultDesc" class="result-desc">{{ resultDesc }}</p>
</div>
<div v-if="hasParams" class="info-rows">
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">车牌号</span>
<span class="info-value font-mono">{{ params?.plate_no || params?.car_license || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">号牌类型</span>
<span class="info-value">{{ params?.carplate_type || params?.car_type || '-' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
// status: 0 一致, -1 不一致, -2 非法姓名, -4 无记录
const status = computed(() => {
const s = props.data?.status;
if (s === 0 || s === -1 || s === -2 || s === -4) return s;
return null;
});
const resultText = computed(() => {
const s = status.value;
if (s === 0) return '一致';
if (s === -1) return '不一致';
if (s === -2) return '非法姓名';
if (s === -4) return '无记录';
return '暂无结果';
});
const resultDesc = computed(() => {
const s = status.value;
if (s === -2) return '姓名长度或格式不正确,请核对后重试';
if (s === -4) return '未查询到相关核验记录';
return '';
});
const resultTextClass = computed(() => {
const s = status.value;
if (s === 0) return 'result-match';
if (s === -1) return 'result-mismatch';
if (s === -2) return 'result-invalid';
if (s === -4) return 'result-norecord';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
const s = status.value;
if (s === 0) return 'result-section match';
if (s === -1) return 'result-section mismatch';
if (s === -2) return 'result-section invalid';
if (s === -4) return 'result-section norecord';
return 'result-section unknown';
});
const iconClass = computed(() => {
const s = status.value;
if (s === 0) return 'icon-match';
if (s === -1) return 'icon-mismatch';
if (s === -2) return 'icon-invalid';
if (s === -4) return 'icon-norecord';
return 'icon-unknown';
});
const iconChar = computed(() => {
const s = status.value;
if (s === 0) return '✓';
if (s === -1) return '✕';
if (s === -2) return '!';
if (s === -4) return '—';
return '?';
});
const hasParams = computed(() => {
const p = props.params || {};
return p.name || p.plate_no || p.car_license || p.carplate_type || p.car_type;
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-lg p-4 shadow-sm border border-gray-100;
}
.header-box {
@apply rounded-lg mb-4 p-4;
background: linear-gradient(135deg, #5c6bc0 0%, #3949ab 100%);
color: #fff;
}
.header-title {
@apply text-lg font-semibold m-0;
}
.header-desc {
@apply text-sm mt-1 opacity-90 m-0;
}
.result-section {
@apply rounded-xl p-5 text-center mb-4;
}
.result-section.match {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.result-section.mismatch {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
border: 1px solid rgba(244, 67, 54, 0.3);
}
.result-section.invalid {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 1px solid rgba(255, 152, 0, 0.4);
}
.result-section.norecord {
background: linear-gradient(135deg, #eceff1 0%, #cfd8dc 100%);
border: 1px solid rgba(96, 125, 139, 0.3);
}
.result-section.unknown {
@apply bg-gray-50 border border-gray-200;
}
.result-icon-wrap {
@apply mb-2;
}
.result-icon {
@apply inline-flex items-center justify-center w-12 h-12 rounded-full text-2xl font-bold text-white;
}
.result-icon.icon-match {
background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
}
.result-icon.icon-mismatch {
background: linear-gradient(135deg, #e53935 0%, #c62828 100%);
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.4);
}
.result-icon.icon-invalid {
background: linear-gradient(135deg, #fb8c00 0%, #ef6c00 100%);
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.4);
}
.result-icon.icon-norecord {
background: linear-gradient(135deg, #78909c 0%, #546e7a 100%);
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.3);
}
.result-icon.icon-unknown {
background: linear-gradient(135deg, #78909c 0%, #546e7a 100%);
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.3);
}
.result-label {
@apply text-sm text-gray-500 mb-1;
}
.result-value {
@apply text-xl font-semibold;
}
.result-desc {
@apply text-sm mt-2 m-0 text-gray-600 max-w-xs mx-auto;
}
.result-match {
color: #2e7d32;
}
.result-mismatch {
color: #c62828;
}
.result-invalid {
color: #e65100;
}
.result-norecord {
color: #546e7a;
}
.result-unknown {
@apply text-gray-500;
}
.info-rows {
@apply space-y-3 pt-2 border-t border-gray-100;
}
.info-row {
@apply flex items-center text-sm;
}
.info-label {
@apply w-20 text-gray-500 shrink-0;
}
.info-value {
@apply font-medium text-gray-800;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="card">
<div class="bg-gray-100 text-gray-800 p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold">{{ title }}</h3>
<p class="text-sm mt-1">返回数据如下传参后续可按接口单独配置</p>
</div>
<div v-if="hasRawData" class="text-xs">
<pre
class="bg-gray-50 rounded p-3 overflow-x-auto whitespace-pre-wrap break-all border border-gray-200">{{ prettyData }}</pre>
</div>
<div v-else class="text-gray-500 text-sm">暂无数据</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const titleMap = {
QCXG4D2E: '名下车辆(数量)',
QCXG5U0Z: '车辆静态信息查询',
QCXG1U4U: '车辆里程记录(混合查询)',
QCXGY7F2: '二手车VIN估值',
QCXG1H7Y: '车辆过户简版查询',
QCXG4I1Z: '车辆过户详版查询',
QCXG3Y6B: '车辆维保简版查询',
QCXG3Z3L: '车辆维保详细版查询',
QCXGP00W: '车辆出险详版查询',
QCXG6B4E: '车辆出险记录核验',
};
const title = computed(() => titleMap[props.apiId] || props.apiId || '车辆查询');
const hasRawData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const prettyData = computed(() => {
try {
return JSON.stringify(props.data, null, 2);
} catch {
return String(props.data || '');
}
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-lg p-4 shadow-sm border border-gray-100;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<!-- 车辆总数统计 -->
<div class="flex justify-between items-center mb-6 pb-4 border-b border-gray-100">
<div class="flex items-center gap-3">
<div>
<div class="text-lg font-semibold text-gray-900">名下车辆(车牌)</div>
</div>
</div>
<div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium">
{{ vehicleCount }}
</div>
</div>
<!-- 车辆列表 -->
<div class="space-y-3" v-if="vehicleList && vehicleList.length > 0">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:bg-blue-50 hover:border-blue-200 transition-colors duration-200"
v-for="(vehicle, index) in vehicleList" :key="index">
<div class="space-y-3">
<div class="text-xl font-bold text-gray-900 font-mono tracking-wider">
{{ vehicle.plateNum }}
</div>
<div class="flex items-center gap-3">
<div class="inline-flex items-center gap-1 px-3 py-1 rounded text-xs font-medium text-white"
:class="getPlateColorClass(vehicle.plateColor)">
<span>🏷</span>
<span>{{ getPlateColorText(vehicle.plateColor) }}</span>
</div>
<div class="text-sm text-gray-600">
<span class="text-gray-500">车辆类型:</span>
<span class="font-medium text-gray-900 ml-1">{{ getVehicleTypeText(vehicle.vehicleType)
}}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div class="text-center py-12 text-gray-500" v-else>
<div class="text-4xl mb-3">🚫</div>
<div class="text-lg font-medium mb-1">暂无车辆信息</div>
<div class="text-sm">No vehicle records found</div>
</div>
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const plateColorMap = {
0: '蓝色 - 普通燃油车',
1: '黄色 - 大型车/货车',
2: '黑色 - 外籍车辆/港澳台车',
3: '白色 - 警车/军车/武警车',
4: '渐变绿色 - 新能源汽车',
5: '黄绿双拼色 - 大型新能源汽车',
6: '蓝白渐变色 - 临时牌照',
7: '临时牌照 - 临时行驶车辆',
11: '绿色 - 新能源汽车',
12: '红色 - 教练车/试验车'
};
const vehicleTypeMap = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车'
};
const vehicleList = computed(() => props.data?.list || []);
const vehicleCount = computed(() => props.data?.vehicleCount || 0);
const getPlateColorText = (plateColor) => {
return plateColorMap[plateColor] || '未知颜色 - 未知类型';
};
const getPlateColorClass = (plateColor) => {
const colorClassMap = {
0: 'bg-blue-500',
1: 'bg-yellow-500',
2: 'bg-gray-800',
3: 'bg-gray-200 text-gray-800',
4: 'bg-green-500',
5: 'bg-gradient-to-r from-yellow-500 to-green-500',
6: 'bg-gradient-to-r from-blue-500 to-white text-blue-800',
7: 'bg-red-500',
11: 'bg-green-500',
12: 'bg-red-500'
};
return colorClassMap[plateColor] || 'bg-gray-500';
};
const getVehicleTypeText = (vehicleType) => {
return vehicleTypeMap[vehicleType] || '未知类型';
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
/* 保持与 CQCXG9P1C 一致的布局风格 */
</style>

1
src/static/github.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" fill-rule="evenodd" d="M16 2a14 14 0 0 0-4.43 27.28c.7.13 1-.3 1-.67v-2.38c-3.89.84-4.71-1.88-4.71-1.88a3.71 3.71 0 0 0-1.62-2.05c-1.27-.86.1-.85.1-.85a2.94 2.94 0 0 1 2.14 1.45a3 3 0 0 0 4.08 1.16a2.93 2.93 0 0 1 .88-1.87c-3.1-.36-6.37-1.56-6.37-6.92a5.4 5.4 0 0 1 1.44-3.76a5 5 0 0 1 .14-3.7s1.17-.38 3.85 1.43a13.3 13.3 0 0 1 7 0c2.67-1.81 3.84-1.43 3.84-1.43a5 5 0 0 1 .14 3.7a5.4 5.4 0 0 1 1.44 3.76c0 5.38-3.27 6.56-6.39 6.91a3.33 3.33 0 0 1 .95 2.59v3.84c0 .46.25.81 1 .67A14 14 0 0 0 16 2"/></svg>

After

Width:  |  Height:  |  Size: 614 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772342923799" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2663" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M878.1824 579.6864c0 219.2384-261.12 325.376-345.1392 353.8432a78.4896 78.4896 0 0 1-51.2 0c-83.2-29.0304-340.8896-136.4992-340.8896-353.6384V297.5744a79.0016 79.0016 0 0 1 45.6192-71.68L474.112 92.16a78.848 78.848 0 0 1 66.56 0l291.84 134.2464a79.104 79.104 0 0 1 46.08 71.68z" fill="#80B7F9" p-id="2664"></path><path d="M836.9664 838.0416l-39.3216-42.4448a176.7936 176.7936 0 1 0-52.5312 48.7936l39.3216 42.3424a35.84 35.84 0 1 0 52.5312-48.6912zM547.84 692.7872a105.5232 105.5232 0 1 1 105.5232 105.472 105.6256 105.6256 0 0 1-105.5232-105.472z" fill="#80B7F9" p-id="2665"></path><path d="M653.5168 515.584a177.2032 177.2032 0 1 0 0 354.3552 179.2 179.2 0 0 0 22.9888-1.536c53.1456-31.0784 108.0832-72.8064 147.1488-126.6688a176.6912 176.6912 0 0 0-170.1376-226.1504z m0 282.6752a105.5232 105.5232 0 1 1 105.5232-105.472 105.6256 105.6256 0 0 1-105.5232 105.472z" fill="#3E8BF8" p-id="2666"></path><path d="M273.2032 585.984a40.96 40.96 0 0 1-40.96-40.96V374.6304a40.96 40.96 0 0 1 81.92 0v170.3936a40.96 40.96 0 0 1-40.96 40.96z" fill="#FFFFFF" p-id="2667"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772343266803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21136" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M507.65824 53.57568c-307.2 0-460.78976 153.6-460.78976 460.78976 0 307.2 153.58976 460.81024 460.78976 460.81024 307.21024 0 460.82048-153.6 460.82048-460.81024 0-307.18976-153.62048-460.78976-460.82048-460.78976z" fill="#4EAAE0" p-id="21137"></path><path d="M551.05536 254.70976h-86.76352v49.58208h86.76352v-49.58208z m99.15392 0h49.58208a61.99296 61.99296 0 0 1 61.97248 61.97248V750.4896a61.99296 61.99296 0 0 1-61.97248 61.97248H315.55584a61.99296 61.99296 0 0 1-61.97248-61.97248V316.68224a61.99296 61.99296 0 0 1 61.97248-61.97248h49.58208v49.58208h-29.7472a32.23552 32.23552 0 0 0-32.22528 32.22528v394.1376a32.1536 32.1536 0 0 0 32.22528 32.22528h344.56576a32.23552 32.23552 0 0 0 32.22528-32.22528v-394.1376a32.1536 32.1536 0 0 0-32.22528-32.22528h-29.7472v-49.58208z m-260.28032-13.63968a24.86272 24.86272 0 0 1 24.79104-24.79104 24.86272 24.86272 0 0 1 24.79104 24.79104v74.36288a24.86272 24.86272 0 0 1-24.79104 24.79104 24.86272 24.86272 0 0 1-24.79104-24.79104v-74.36288z m185.91744 0a24.86272 24.86272 0 0 1 24.79104-24.79104 24.86272 24.86272 0 0 1 24.79104 24.79104v74.36288a24.86272 24.86272 0 0 1-24.79104 24.79104 24.86272 24.86272 0 0 1-24.79104-24.79104v-74.36288z" fill="#FFFFFF" p-id="21138"></path><path d="M686.39744 718.1312c1.23904-2.9696 1.98656-7.18848-0.62464-10.65984l-33.5872-42.26048 1.86368-2.3552a102.656 102.656 0 0 0 22.55872-63.95904c0-56.2688-45.85472-102.13376-102.13376-102.13376s-102.13376 45.85472-102.13376 102.13376 45.85472 102.13376 102.13376 102.13376a100.53632 100.53632 0 0 0 40.0384-8.18176l2.60096-1.11616 34.21184 43.13088c0.99328 1.23904 3.2256 3.2256 7.55712 3.2256 1.23904 0 2.72384-0.12288 4.08576-0.49152 4.21888-0.99328 8.91904-3.34848 13.13792-6.81984 4.84352-3.84 8.56064-8.30464 10.2912-12.6464z m-111.79008-54.03648a66.60096 66.60096 0 0 1-47.34976-19.57888 66.56 66.56 0 0 1-19.57888-47.34976c0-36.93568 29.99296-66.92864 66.92864-66.92864s66.92864 29.99296 66.92864 66.92864a66.9696 66.9696 0 0 1-66.92864 66.92864zM606.69952 366.25408H360.79616c-13.2608 0-24.04352 10.78272-24.04352 24.04352s10.78272 24.04352 24.04352 24.04352h245.90336a24.064 24.064 0 0 0 24.04352-24.04352 23.97184 23.97184 0 0 0-24.04352-24.04352zM515.23584 481.52576l3.96288-2.3552H360.91904c-6.44096 0-12.3904 2.47808-16.97792 7.0656a23.71584 23.71584 0 0 0-7.0656 16.97792 24.064 24.064 0 0 0 24.04352 24.04352h104.11008l0.36864-0.49152a164.0448 164.0448 0 0 1 49.83808-45.24032zM360.79616 641.536h73.12384l-0.12288-1.36192a164.48512 164.48512 0 0 1 1.97632-45.24032l0.24576-1.4848h-75.35616a24.064 24.064 0 0 0-24.04352 24.04352 24.18688 24.18688 0 0 0 24.17664 24.04352z" fill="#FFFFFF" p-id="21139"></path></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772342660230" class="icon" viewBox="0 0 1200 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1915" xmlns:xlink="http://www.w3.org/1999/xlink" width="56.25" height="48"><path d="M921.6 471.04c34.816 0 61.44-26.624 61.44-61.44s-26.624-61.44-61.44-61.44h-67.584L808.96 174.08c-8.192-28.672-28.672-51.2-57.344-59.392-170.496-45.056-349.696-45.056-520.192 0-28.672 8.192-51.2 30.72-59.392 59.392l-43.008 174.08H81.92C47.104 348.16 20.48 374.784 20.48 409.6c0 26.624 16.384 49.152 40.96 57.344-36.864 20.48-61.44 61.44-61.44 106.496v163.84c0 38.912 26.624 69.632 61.44 79.872v43.008c0 22.528 18.432 40.96 40.96 40.96h122.88c22.528 0 40.96-18.432 40.96-40.96v-40.96h327.68v-247.808L870.4 471.04h51.2zM163.84 716.8c-45.056 0-81.92-36.864-81.92-81.92s36.864-81.92 81.92-81.92 81.92 36.864 81.92 81.92-36.864 81.92-81.92 81.92z m43.008-307.2l45.056-215.04C329.728 174.08 409.6 163.84 491.52 163.84s161.792 10.24 239.616 30.72l45.056 215.04H206.848z" fill="#f87129" p-id="1916"></path><path d="M880.64 532.48l-225.28 81.92v212.992c0 45.056 24.576 86.016 63.488 108.544L880.64 1024l161.792-88.064c38.912-22.528 63.488-63.488 63.488-108.544V614.4l-225.28-81.92z" fill="#f87129" p-id="1917"></path><path d="M826.88 788.992c7.168 41.472-3.072 65.024-30.72 69.632-7.68 1.536-18.432 1.536-32.256 0.512-2.048-7.68-4.096-15.36-6.656-23.552v77.824H732.16v-301.056h98.304v22.528c-13.312 39.424-23.552 69.12-31.232 90.112 14.848 20.48 24.064 41.472 27.648 64z m-40.96 46.08c15.872-1.536 20.992-17.408 15.36-47.616-3.072-16.896-13.312-36.352-29.184-58.88 10.24-29.184 20.48-60.416 30.72-94.208h-45.568v198.144c10.24 2.56 19.968 3.584 28.672 2.56z m173.568-238.592l-4.608 9.728c27.136 42.496 63.488 75.264 108.544 97.792-7.68 8.704-14.336 16.384-19.456 22.528-42.496-27.136-76.8-59.392-102.912-97.792-26.112 37.376-61.44 70.656-105.472 100.864-4.608-5.632-10.752-12.288-18.432-20.48 50.688-31.232 88.576-69.12 113.664-113.152h28.672z m-137.216 280.576h142.336c18.944-47.104 34.304-87.552 45.568-121.856l29.184 9.216c-8.704 23.04-23.552 60.416-44.544 112.128h59.392v24.064h-231.424v-23.552z m20.48-107.008l23.552-8.192c12.288 29.184 23.04 59.392 33.28 91.136l-27.136 8.192c-8.192-32.768-18.432-62.976-29.696-91.136z m15.872-55.808h160.768v23.552h-160.768v-23.552z m53.76 43.52l25.088-7.168c10.752 30.72 19.968 59.392 26.112 84.992l-28.16 6.656c-5.632-29.184-13.824-57.344-23.04-84.48z" fill="#ffffff" p-id="1918"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772342400535" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17142" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#1D7BE9" p-id="17143"></path><path d="M781.242182 512.930909a45.824 45.824 0 0 0-62.603637 0l-210.850909 209.733818-19.758545 82.362182 82.362182-19.758545 210.850909-210.850909a44.427636 44.427636 0 0 0 0-61.486546z m-254.766546 253.672727l6.586182-27.461818 171.310546-171.310545 20.945454 20.945454-171.310545 171.310546-27.461818 6.586182z m233.914182-211.921454l-14.266182 14.266182-20.945454-20.945455 14.266182-14.266182a14.103273 14.103273 0 0 1 10.984727-4.398545 19.735273 19.735273 0 0 1 9.890909 4.398545 15.266909 15.266909 0 0 1 0 20.945455z m-9.890909-136.168727v-64.814546a59.042909 59.042909 0 0 0-59.298909-59.298909H340.898909a59.042909 59.042909 0 0 0-59.298909 59.298909v323.956364a59.042909 59.042909 0 0 0 59.298909 59.298909h127.371636a19.781818 19.781818 0 0 0 0-39.563637h-127.371636a19.688727 19.688727 0 0 1-19.758545-19.758545v-323.933091a19.688727 19.688727 0 0 1 19.758545-19.758545H691.2a19.688727 19.688727 0 0 1 19.758545 19.758545v64.791273a19.781818 19.781818 0 0 0 39.563637 0z" fill="#FFFFFF" p-id="17144"></path><path d="M520.983273 439.342545h-141.661091a18.618182 18.618182 0 0 1 0-37.236363h140.567273a18.618182 18.618182 0 0 1 18.618181 18.618182 17.594182 17.594182 0 0 1-17.524363 18.618181z m107.613091 98.839273H379.322182a18.618182 18.618182 0 0 1 0-37.236363h249.274182a18.618182 18.618182 0 0 1 18.618181 18.618181 17.850182 17.850182 0 0 1-18.618181 18.618182z" fill="#FFFFFF" p-id="17145"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772342208143" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13290" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M728.847 738.396c0 4.307 1.713 8.457 4.736 11.479 3.067 3.045 7.172 4.759 11.479 4.759h47.52c8.978 0 16.216-7.263 16.216-16.237v-43.078h-79.951v43.077zM345.336 548.404a31.03 31.03 0 0 0-33.774 6.722c-8.841 8.842-11.48 22.171-6.687 33.74a30.962 30.962 0 0 0 28.609 19.126v0.901a31.298 31.298 0 0 0 22.136-9.495 31.311 31.311 0 0 0 8.852-22.373 30.961 30.961 0 0 0-19.136-28.621zM687.528 546.037c-17.073 0-30.966 13.87-30.966 30.988 0 17.096 13.893 30.967 30.966 30.967v0.901a31.413 31.413 0 0 0 22.17-9.495c5.773-5.977 8.954-14.027 8.842-22.373 0-17.118-13.894-30.988-31.012-30.988zM350.004 427.744h320.992c28.08 1.038 53.182-17.276 60.805-44.295l-29.545-110.917c-2.639-27.741-24.989-49.438-52.797-51.332H371.25c-27.324 2.075-49.336 23.23-52.505 50.475L288.354 383.45c7.668 27.379 33.278 45.76 61.65 44.294z m122.139-177.045h76.728c8.164 0 14.727 6.586 14.727 14.75 0 8.165-6.563 14.75-14.727 14.75h-76.728c-8.13 0-14.739-6.585-14.739-14.75 0-8.164 6.608-14.75 14.739-14.75zM212.236 738.396c0 4.307 1.692 8.457 4.725 11.479a16.26 16.26 0 0 0 11.491 4.759h47.498c8.965 0 16.238-7.263 16.238-16.237v-43.078h-79.952v43.077z" fill="#3366CC" p-id="13291"></path><path d="M826.188 398.244h-66.646c-14.546 36.041-49.662 59.496-88.545 59H350.004c-38.837 0.496-74.021-22.959-88.522-59h-67.243c-1.466 0-2.662 1.173-2.662 2.616v21.561c2.053 2.955 12.675 5.323 19.182 5.323l10.318 1.466a14.659 14.659 0 0 1 10.104 8.232c1.906 4.149 1.748 8.976-0.372 13.013a1190.064 1190.064 0 0 0-18.878 135.411v78.779h598.04v-81.418c-3.676-45.084-10.014-89.897-18.854-134.238a15.3 15.3 0 0 1 0-13.307c1.961-4.105 5.841-7.059 10.307-7.939h9.72a55.988 55.988 0 0 0 17.683-4.42V400.86a2.625 2.625 0 0 0-2.639-2.616zM356.635 632.598c-22.598 9.359-48.636 4.194-65.923-13.104-17.276-17.298-22.475-43.302-13.104-65.9 9.36-22.599 31.406-37.326 55.876-37.326 33.345 0.135 60.342 27.132 60.489 60.443 0 24.47-14.739 46.527-37.338 55.887z m227.35 3.134H436.463c-8.164 0-14.761-6.608-14.761-14.75 0-8.164 6.597-14.773 14.761-14.773h147.522c8.097 0 14.75 6.609 14.75 14.773 0 8.142-6.653 14.75-14.75 14.75z m0-59.022H436.463c-8.164 0-14.761-6.608-14.761-14.728 0-8.164 6.597-14.772 14.761-14.772h147.522c8.097 0 14.75 6.608 14.75 14.772 0 8.12-6.653 14.728-14.75 14.728z m126.683 55.888c-22.599 9.359-48.625 4.194-65.879-13.104-17.32-17.298-22.485-43.302-13.148-65.9 9.404-22.599 31.439-37.326 55.888-37.326 33.334 0.135 60.285 27.132 60.489 60.443 0 24.47-14.75 46.527-37.35 55.887z" fill="#3366CC" p-id="13292"></path><path d="M512.006 25.999c-267.856 0-484.99 217.145-484.99 484.99 0 267.868 217.133 485.013 484.99 485.013 267.834 0 484.979-217.146 484.979-485.013-0.001-267.845-217.145-484.99-484.979-484.99z m309.197 430.05c6.766 33.063 13.576 97.363 16.757 126.863v155.484c0 25.281-20.434 45.715-45.671 45.715h-47.543c-25.236 0-45.67-20.434-45.67-45.715v-43.078H321.395v43.078c0 25.281-20.489 45.715-45.727 45.715h-47.215c-25.238 0-45.727-20.434-45.727-45.715V584.378c3.225-31.553 8.852-95.266 16.802-128.623-16.521-4.127-37.461-8.863-37.461-33.65V400.86c0-8.48 3.383-16.69 9.427-22.711a32.115 32.115 0 0 1 22.745-9.427h67.559l28.338-103.836c5.886-40.754 40.01-71.472 81.114-73.186h278.209c41.565 1.646 75.892 32.883 81.417 74.042l28.058 102.979h67.254c17.682 0.158 31.867 14.524 31.867 32.139v21.561c0 24.787-20.636 30.989-36.852 33.628z" fill="#3366CC" p-id="13293"></path></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772343483998" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28638" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M445.376 599.616c113.28 0 205.12-132.48 205.12-262.784C650.496 206.72 558.72 128 445.44 128S240.192 206.656 240.192 336.832c0 130.24 91.904 262.784 205.184 262.784z m58.24 90.24a32.704 32.704 0 0 0-48.512-23.936l-12.032 6.72a37.952 37.952 0 0 0-14.72 52.672l7.424 12.8c3.008 5.12 3.584 11.328 1.472 16.96a20.416 20.416 0 0 1-12.16 12.096c-9.6 3.52-20.352-1.216-25.472-9.92l-67.648-115.456a43.904 43.904 0 0 0-63.232-13.12l-171.584 123.84c-62.912 45.44-30.336 143.488 47.616 143.488H613.12a347.968 347.968 0 0 1-109.44-206.08z m194.048 17.28c-10.24 0-18.56 8.192-18.56 18.304 0 10.112 8.32 18.304 18.56 18.304 10.24 0 18.624-8.192 18.624-18.304a18.56 18.56 0 0 0-18.56-18.304z m136.128-50.56a41.856 41.856 0 0 0-42.048-41.344h-46.464a42.112 42.112 0 0 0-42.112 41.344v8.128c20.544 3.52 41.408 5.44 62.336 5.632l19.584-0.384c16.32-0.64 32.64-2.368 48.64-5.248v-8.128z m5.568 50.56c-10.24 0-18.56 8.192-18.56 18.304 0 10.112 8.32 18.304 18.56 18.304 10.24 0 18.56-8.192 18.56-18.304a18.496 18.496 0 0 0-18.56-18.304z" p-id="28639" fill="#1296db"></path><path d="M768.512 501.888c-105.664 0-191.424 84.48-191.424 188.672 0 104.128 85.76 188.608 191.424 188.608 105.728 0 191.488-84.48 191.488-188.608 0-104.192-85.76-188.672-191.488-188.672z m115.584 189.44l-1.92 1.344-1.792 0.576v85.824c0 6.912-5.696 12.48-12.672 12.544h-23.04c-7.04 0-12.8-5.568-12.8-12.544v-10.24a44.352 44.352 0 0 1-7.424 0.64h-111.872a46.08 46.08 0 0 1-7.488-0.64v10.24c0 6.912-5.76 12.48-12.736 12.544h-22.976a12.672 12.672 0 0 1-12.736-12.544v-85.824l-1.92-0.576-1.92-1.28v-14.08c0.064-3.584 1.536-7.04 4.16-9.536l8.192-8.256a7.872 7.872 0 0 1 7.36-1.856c4.48-8.96 8.32-18.24 11.392-27.776a52.416 52.416 0 0 1 50.88-40.384h67.52a52.032 52.032 0 0 1 50.752 40.448c1.088 3.776 2.432 7.488 3.968 11.136 2.048 5.76 4.48 11.264 7.232 16.64a7.68 7.68 0 0 1 7.68 2.048l9.152 9.024c1.92 1.92 3.008 4.416 3.008 7.04v15.488z" p-id="28640" fill="#1296db"></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772343074532" class="icon" viewBox="0 0 1255 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12491" width="58.828125" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M647.836735 815.020408c0-150.465306 121.208163-271.673469 271.673469-271.673469 25.077551 0 48.065306 4.179592 71.053061 10.448979-10.44898-20.897959-25.077551-39.706122-45.97551-52.244898 31.346939-2.089796 56.42449-29.257143 56.42449-62.693877 0-35.526531-27.167347-62.693878-62.693878-62.693878h-68.963265l-45.97551-177.632653C815.020408 169.273469 794.122449 146.285714 764.865306 137.926531a1038.628571 1038.628571 0 0 0-530.808163 0C206.889796 146.285714 183.902041 169.273469 175.542857 198.530612L131.657143 376.163265H83.591837c-35.526531 0-62.693878 27.167347-62.693878 62.693878 0 27.167347 16.718367 50.155102 41.795919 58.514286-37.616327 20.897959-62.693878 62.693878-62.693878 108.669387v167.183674c0 39.706122 27.167347 71.053061 62.693878 81.502041V898.612245c0 22.987755 18.808163 41.795918 41.795918 41.795918h125.387755c22.987755 0 41.795918-18.808163 41.795918-41.795918v-41.795918h380.342858c-2.089796-14.628571-4.179592-27.167347-4.179592-41.795919zM167.183673 752.326531c-45.97551 0-83.591837-37.616327-83.591836-83.591837s37.616327-83.591837 83.591836-83.591837 83.591837 37.616327 83.591837 83.591837-37.616327 83.591837-83.591837 83.591837z m89.861225-532.89796C336.457143 198.530612 417.959184 188.081633 501.55102 188.081633s165.093878 10.44898 244.506123 31.346938L792.032653 438.857143H211.069388l45.97551-219.428572zM919.510204 606.040816c-114.938776 0-208.979592 94.040816-208.979592 208.979592s94.040816 208.979592 208.979592 208.979592 208.979592-94.040816 208.979592-208.979592-94.040816-208.979592-208.979592-208.979592z m-125.387755 146.285715h188.081633l-25.077551-33.436735c-10.44898-14.628571-8.359184-33.436735 6.269387-43.885714s33.436735-8.359184 43.885715 6.269387l62.693877 83.591837c6.269388 10.44898 8.359184 22.987755 2.089796 33.436735s-14.628571 16.718367-27.167347 16.718367H794.122449c-16.718367 0-31.346939-14.628571-31.346939-31.346939S777.404082 752.326531 794.122449 752.326531z m250.77551 146.285714h-188.081632l25.077551 31.346939c8.359184 12.538776 10.44898 29.257143 0 39.706122-6.269388 8.359184-16.718367 12.538776-25.077551 12.538776-10.44898 0-18.808163-4.179592-25.077551-12.538776l-62.693878-83.591837c-6.269388-10.44898-8.359184-22.987755-2.089796-33.436734s14.628571-16.718367 27.167347-16.718368h250.77551c16.718367 0 31.346939 14.628571 31.346939 31.346939S1061.616327 898.612245 1044.897959 898.612245z" fill="#1296db" p-id="12492" data-spm-anchor-id="a313x.search_index.0.i0.28623a815CRDQJ" class=""></path></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772343364595" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24451" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M510.75935 42.027642a257.977613 257.977613 0 0 0-257.869987 257.923801c0 90.243733 45.902149 169.402071 115.481843 215.250407-177.204898 52.252036-294.677808 269.439697-294.677808 408.599085 0 17.220033 19.426349-9.417205 36.754008-9.417205a29.973619 29.973619 0 0 0 22.655105-9.09433c6.027011-5.704136 21.740291 7.695202 21.525041-0.807189 0-143.94871 133.132377-346.553156 356.347049-346.553155 68.342004 0 133.939566-27.175364 182.263282-75.552893a257.70855 257.70855 0 0 0 75.552893-182.532346A258.192864 258.192864 0 0 0 510.75935 42.027642z m0.107626 452.671607a194.586368 194.586368 0 1 1 0-389.280362 194.371118 194.371118 0 0 1 194.42493 194.532556 194.478743 194.478743 0 0 1-194.42493 194.747806z" fill="#009FE8" p-id="24452"></path><path d="M575.49591 929.881759a39.337012 39.337012 0 0 1-39.283199 39.390825H112.976598a39.283199 39.283199 0 1 1 0-78.620211H536.051273c21.740291 0 39.444637 17.542908 39.444637 39.229386z" fill="#009FE8" p-id="24453"></path><path d="M754.745687 606.737086a39.821325 39.821325 0 0 0 0-55.965106 36.484944 36.484944 0 0 0-26.637238-12.000211 35.839193 35.839193 0 0 0-26.583425 12.000211l-104.557885 109.723895s0 1.829628-1.775816 1.829628c-13.02265 10.385832-18.188659 28.466866-13.02265 44.556834a38.153135 38.153135 0 0 0 35.839193 27.444427h353.764044c20.87929 0 37.776446-17.919596 37.884072-39.821325 0-21.901729-17.004782-39.606075-37.884072-39.767513h-262.551684l45.525461-48.00084zM971.449035 786.36355h-353.764045c-20.87929 0.107625-37.776446 17.919596-37.991697 39.928951 0 21.847916 17.112407 39.875138 37.991697 39.875138h260.506806l-45.740712 47.839403a40.73614 40.73614 0 0 0-11.300646 27.982553 39.821325 39.821325 0 0 0 11.300646 28.036365 38.206947 38.206947 0 0 0 26.744863 12.000211 36.915445 36.915445 0 0 0 26.583426-12.000211l104.396447-109.885333a38.47601 38.47601 0 0 0 19.049661-33.740501c0.107625-22.116979-16.735719-39.928951-37.776446-40.036576z" fill="#009FE8" p-id="24454"></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772343150587" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15709" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M492.495238 24.380952c269.312 0 487.619048 218.307048 487.619048 487.619048s-218.307048 487.619048-487.619048 487.619048-487.619048-218.258286-487.619048-487.619048c0-269.312 218.307048-487.619048 487.619048-487.619048z m295.740952 573.586286v-76.361143c-0.048762-25.648762-14.531048-61.001143-36.766476-80.164571a8.97219 8.97219 0 0 0-1.267809-1.365334l-20.675048-20.089904c17.212952-3.900952 35.742476-18.480762 35.742476-33.353143 0-17.359238-23.893333-23.454476-44.373333-23.454476-21.26019 0-23.747048 14.677333-23.990857 25.112381l-53.638095-51.931429a75.53219 75.53219 0 0 0-46.908953-19.358476H387.657143a75.53219 75.53219 0 0 0-46.957714 19.358476L287.207619 388.291048c-0.24381-10.48381-2.730667-25.161143-24.039619-25.161143-20.48 0-44.373333 6.144-44.373333 23.405714 0 14.921143 18.578286 29.403429 35.791238 33.450667l-20.675048 20.089904c-0.487619 0.487619-0.828952 0.975238-1.267809 1.365334-22.235429 19.017143-36.717714 54.467048-36.717715 80.164571v76.409905c0 17.846857 6.826667 34.962286 19.017143 47.981714v29.549715c0.195048 17.554286 14.57981 31.597714 32.085334 31.451428h38.814476a31.792762 31.792762 0 0 0 32.182857-31.451428V668.038095h348.16v7.606857c0.195048 17.554286 14.57981 31.597714 32.085333 31.451429h38.814476a31.841524 31.841524 0 0 0 32.134096-31.451429v-29.598476c12.190476-13.019429 19.017143-30.232381 19.017142-48.030476z m-72.752761-136.533333c16.579048 0 30.134857 14.872381 30.134857 33.450666 0 18.529524-13.507048 33.499429-30.134857 33.499429-16.579048 0-70.89981 4.291048-70.89981-14.140952 0-18.578286 54.320762-52.809143 70.89981-52.809143z m-322.023619 132.729905c-20.626286 0-37.254095-25.453714-37.254096-56.905143h271.60381c0 31.402667-16.62781 56.905143-37.205334 56.905143H393.45981z m230.15619-234.73981l70.217143 68.510476H290.182095l70.217143-68.510476c7.606857-6.339048 20.72381-10.971429 27.306667-10.971429h208.700952c6.582857 0 19.69981 4.681143 27.306667 10.971429h-0.097524zM252.635429 484.449524c18.041905 0 77.04381 34.864762 77.043809 53.638095 0 18.822095-59.001905 14.433524-77.043809 14.433524a33.353143 33.353143 0 0 1-32.670477-34.03581c0-18.773333 14.628571-34.03581 32.670477-34.035809z" fill="#3C8CFF" p-id="15710"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772342893854" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2440" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M167.03488 55.13216H33.91488A21.2992 21.2992 0 0 1 12.6976 33.91488 21.2992 21.2992 0 0 1 33.91488 12.6976h133.12a21.2992 21.2992 0 0 1 21.21728 21.21728 21.2992 21.2992 0 0 1-21.21728 21.21728z" fill="#0052D5" p-id="2441"></path><path d="M55.13216 33.91488v133.12a21.2992 21.2992 0 0 1-21.21728 21.21728 21.2992 21.2992 0 0 1-21.21728-21.21728V33.91488A21.2992 21.2992 0 0 1 33.91488 12.6976a21.2992 21.2992 0 0 1 21.21728 21.21728zM167.03488 950.8864H33.91488a21.2992 21.2992 0 0 0-21.21728 21.21728 21.2992 21.2992 0 0 0 21.21728 21.21728h133.12a21.2992 21.2992 0 0 0 21.21728-21.21728 21.2992 21.2992 0 0 0-21.21728-21.21728z" fill="#0052D5" p-id="2442"></path><path d="M55.13216 972.10368v-133.12a21.2992 21.2992 0 0 0-21.21728-21.21728 21.2992 21.2992 0 0 0-21.21728 21.21728v133.12a21.2992 21.2992 0 0 0 21.21728 21.21728 21.2992 21.2992 0 0 0 21.21728-21.21728zM835.25632 950.8864h133.12a21.2992 21.2992 0 0 1 21.21728 21.21728 21.2992 21.2992 0 0 1-21.21728 21.21728h-133.12a21.2992 21.2992 0 0 1-21.21728-21.21728 21.2992 21.2992 0 0 1 21.21728-21.21728z" fill="#0052D5" p-id="2443"></path><path d="M947.15904 972.10368v-133.12a21.2992 21.2992 0 0 1 21.21728-21.21728 21.2992 21.2992 0 0 1 21.21728 21.21728v133.12a21.2992 21.2992 0 0 1-21.21728 21.21728 21.2992 21.2992 0 0 1-21.21728-21.21728zM835.25632 55.13216h133.12a21.2992 21.2992 0 0 0 21.21728-21.21728 21.2992 21.2992 0 0 0-21.21728-21.21728h-133.12a21.2992 21.2992 0 0 0-21.21728 21.21728 21.2992 21.2992 0 0 0 21.21728 21.21728z" fill="#0052D5" p-id="2444"></path><path d="M947.15904 33.91488v133.12a21.2992 21.2992 0 0 0 21.21728 21.21728 21.2992 21.2992 0 0 0 21.21728-21.21728V33.91488a21.2992 21.2992 0 0 0-21.21728-21.21728 21.2992 21.2992 0 0 0-21.21728 21.21728z" fill="#0052D5" p-id="2445"></path><path d="M77.824 77.824h846.6432v846.6432H77.824z" fill="#F8F8F8" opacity=".01" p-id="2446"></path><path d="M786.80064 253.91104l-212.992-123.00288a143.40096 143.40096 0 0 0-143.48288 0L217.33376 253.952a143.48288 143.48288 0 0 0-71.76192 124.23168v246.00576c0 51.24096 27.36128 98.63168 71.72096 124.23168l212.992 123.00288a143.40096 143.40096 0 0 0 143.48288 0l212.992-123.00288a143.44192 143.44192 0 0 0 71.68-124.23168V378.14272A143.48288 143.48288 0 0 0 786.67776 253.952m-414.1056 375.35744H308.4288L241.8688 406.69184h56.44288l25.47712 101.5808c6.5536 23.92064 11.0592 46.53056 17.32608 71.10656h1.47456c6.59456-24.53504 11.0592-47.14496 17.32608-71.10656l25.06752-101.5808h54.31296l-66.7648 222.57664z m143.1552 0H462.4384V406.69184h53.4528l-0.12288 222.57664z m227.5328 0h-54.272l-54.96832-107.23328-21.79072-48.70144h-1.47456c2.37568 24.7808 6.84032 55.25504 6.84032 82.1248v73.80992h-50.7904V406.69184h54.31296l55.296 106.65984 21.79072 49.31584h1.47456a769.06496 769.06496 0 0 1-7.12704-82.16576V406.69184h50.74944v222.57664h-0.04096z" fill="#0052D5" p-id="2447"></path></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Some files were not shown because too many files have changed in this diff Show More