This commit is contained in:
2026-04-23 14:57:35 +08:00
parent c77780fa0e
commit 739e08157b
97 changed files with 6120 additions and 14939 deletions

8
.env
View File

@@ -31,7 +31,7 @@ VITE_WITHDRAW_METHODS=both
# `useReportWebview`、`QRcode` 基地址;可与下一项二选一或同时填
VITE_SITE_ORIGIN=https://chimei.ronsafe.cn
# 报告 H5 根地址(可选,优先与 VITE_SITE_ORIGIN 配合使用)
VITE_REPORT_BASE_URL=
VITE_REPORT_BASE_URL=https://chimei.ronsafe.cn
# `src/pages/not-found.vue` 必需
VITE_SEO_SITE_NAME=赤眉
@@ -39,10 +39,10 @@ VITE_SEO_SITE_NAME=赤眉
#############################
# 客服(企业微信)
#############################
VITE_CUSTOMER_SERVICE_URL=
VITE_WXWORK_CORP_ID=
VITE_CUSTOMER_SERVICE_URL=https://work.weixin.qq.com/kfid/kfc3f3ce2a840439cca
VITE_WXWORK_CORP_ID=ww435b13218adec315
# 设为 1 则跳过微信 SDK直接系统打开客服链接基座不支持客服能力时使用
# VITE_CUSTOMER_SERVICE_SKIP_SDK=1
VITE_CUSTOMER_SERVICE_SKIP_SDK=1
#############################
# 邀请链接加密(须与后端代理链路 key 一致)

View File

@@ -4,7 +4,6 @@
"antfu.iconify",
"antfu.unocss",
"vue.volar",
"dbaeumer.vscode-eslint",
"editorConfig.editorConfig",
"uni-helper.uni-helper-vscode"
]

46
.vscode/settings.json vendored
View File

@@ -1,53 +1,11 @@
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
"prettier.enable": true,
"editor.formatOnSave": true,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
],
// Enable file nesting
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {

View File

@@ -30,4 +30,4 @@ Vitesse for uni-app
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
- 🦾 [TypeScript](https://www.typescriptlang.org/) & [ESLint](https://eslint.org/) - 保证代码质量
- 🦾 [TypeScript](https://www.typescriptlang.org/) - 保证代码质量

View File

@@ -1,5 +0,0 @@
import uni from '@uni-helper/eslint-config'
export default uni({
unocss: true,
})

View File

@@ -1,8 +1,8 @@
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
export default defineManifestConfig({
'name': 'bdrp-app',
'appid': '',
'name': '赤眉',
'appid': 'H5A4E639E',
'description': '',
'versionName': '1.0.0',
'versionCode': '100',
@@ -11,7 +11,11 @@ export default defineManifestConfig({
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
compatible: {
ignoreVersion: true,
runtimeVersion: "1.7.0,1.8.0,1.9.0",
compilerVersion: "4.87"
},
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
@@ -19,33 +23,73 @@ export default defineManifestConfig({
delay: 0,
},
/* 模块配置 */
modules: {},
modules: {
"UniNView": {
"description": "UniNView原生渲染"
},
"Payment": {},
"UIWebview": {},
"Camera": {}
},
/* 应用发布信息 */
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_NETWORK_STATE"/>',
'<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: {},
"ios": {
"privacyDescription": {
"NSLocalNetworkUsageDescription": "需要本地网络进行服务使用",
"NSPhotoLibraryAddUsageDescription": "需要保存二维码海报"
}
},
/* SDK配置 */
sdkConfigs: {},
"sdkConfigs": {
"payment": {
"alipay": {
"__platform__": ["ios", "android"]
}
}
},
"icons": {
"android": {
"hdpi": "static/icons/72x72.png",
"xhdpi": "static/icons/96x96.png",
"xxhdpi": "static/icons/144x144.png",
"xxxhdpi": "static/icons/192x192.png"
},
"ios": {
"appstore": "static/icons/1024x1024.png",
"ipad": {
"app": "static/icons/76x76.png",
"app@2x": "static/icons/152x152.png",
"notification": "static/icons/20x20.png",
"notification@2x": "static/icons/40x40.png",
"proapp@2x": "static/icons/167x167.png",
"settings": "static/icons/29x29.png",
"settings@2x": "static/icons/58x58.png",
"spotlight": "static/icons/40x40.png",
"spotlight@2x": "static/icons/80x80.png"
},
"iphone": {
"app@2x": "static/icons/120x120.png",
"app@3x": "static/icons/180x180.png",
"notification@2x": "static/icons/40x40.png",
"notification@3x": "static/icons/60x60.png",
"settings@2x": "static/icons/58x58.png",
"settings@3x": "static/icons/87x87.png",
"spotlight@2x": "static/icons/80x80.png",
"spotlight@3x": "static/icons/120x120.png"
}
}
}
},
},
// 迁移目标仅 App 端,微信 H5/小程序配置不启用

View File

@@ -9,14 +9,9 @@
"dev": "unh dev",
"build": "unh build",
"build:app": "unh build -p app",
"build:app-android": "unh build -p app-android",
"build:app-ios": "unh build -p app-ios",
"about": "unh info",
"type-check": "vue-tsc --noEmit",
"test": "vitest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"postinstall": "npx simple-git-hooks"
"test": "vitest"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4080720251210001",
@@ -60,7 +55,6 @@
"@iconify-json/carbon": "^1.2.11",
"@mini-types/alipay": "^3.0.14",
"@types/node": "^24.1.0",
"@uni-helper/eslint-config": "^0.4.0",
"@uni-helper/plugin-uni": "^0.1.0",
"@uni-helper/unh": "^0.2.3",
"@uni-helper/uni-env": "^0.1.7",
@@ -70,14 +64,10 @@
"@uni-helper/vite-plugin-uni-layouts": "^0.1.11",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.9",
"@uni-helper/vite-plugin-uni-pages": "^0.3.19",
"@unocss/eslint-config": "^66.3.3",
"@vue/runtime-core": "3.4.21",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.32.0",
"lint-staged": "^16.1.2",
"miniprogram-api-typings": "^4.1.0",
"sass": "^1.78.0",
"simple-git-hooks": "^2.13.0",
"typescript": "~5.8.3",
"unocss": "66.0.0",
"unplugin-auto-import": "^19.3.0",
@@ -91,11 +81,5 @@
"overrides": {
"unconfig": "7.3.2"
}
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
}
}
}

View File

@@ -2,6 +2,9 @@ import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
pages: [
{ path: 'pages/launch', style: { navigationStyle: 'custom', navigationBarTitleText: '' } },
{ path: 'pages/index', style: { navigationBarTitleText: '首页' } },
{ path: 'pages/privacy-consent', style: { navigationStyle: 'custom', navigationBarTitleText: '隐私政策授权' } },
{ path: 'pages/agent', style: { navigationBarTitleText: '代理中心' } },
{ path: 'pages/agent-manage-agreement', style: { navigationBarTitleText: '代理管理协议', navigationStyle: 'default' } },
{ path: 'pages/agent-promote-details', auth: true, style: { navigationBarTitleText: '直推收益明细' } },
@@ -15,7 +18,6 @@ export default defineUniPages({
{ path: 'pages/help-detail', style: { navigationBarTitleText: '帮助详情' } },
{ path: 'pages/help-guide', style: { navigationBarTitleText: '引导指南' } },
{ path: 'pages/history-query', auth: true, style: { navigationBarTitleText: '历史报告' } },
{ path: 'pages/index', style: { navigationBarTitleText: '首页' } },
{ path: 'pages/inquire', style: { navigationBarTitleText: '查询报告' } },
{ path: 'pages/invitation', auth: true, style: { navigationBarTitleText: '邀请下级' } },
{ path: 'pages/invitation-agent-apply', auth: true, style: { navigationBarTitleText: '代理申请' } },
@@ -26,7 +28,6 @@ export default defineUniPages({
{ path: 'pages/payment-result', auth: true, style: { navigationBarTitleText: '支付结果' } },
{ path: 'pages/privacy-policy', style: { navigationBarTitleText: '隐私政策', navigationStyle: 'default' } },
{ path: 'pages/promote', auth: true, style: { navigationBarTitleText: '推广管理' } },
{ path: 'pages/promotion-inquire', style: { navigationBarTitleText: '推广查询' } },
{ path: 'pages/report-example-webview', style: { navigationBarTitleText: '示例报告', navigationStyle: 'default' } },
{ path: 'pages/report-result-webview', auth: true, style: { navigationBarTitleText: '报告结果', navigationStyle: 'default' } },
{ path: 'pages/report-share', style: { navigationBarTitleText: '报告分享', navigationStyle: 'default' } },

9828
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import { useAppBootstrap } from '@/composables/useAppBootstrap'
const { bootstrap } = useAppBootstrap()
import { installPrivacyGuards } from '@/composables/usePrivacyConsent'
onLaunch(async () => {
await bootstrap()
installPrivacyGuards()
})
</script>

30
src/auto-imports.d.ts vendored
View File

@@ -50,16 +50,22 @@ declare global {
const getCurrentUniRoute: typeof import('./composables/uni-router')['getCurrentUniRoute']
const getLayoutPageTitle: typeof import('./composables/uni-router')['getLayoutPageTitle']
const getPageTitleByRoute: typeof import('./composables/uni-router')['getPageTitleByRoute']
const getPrivacyConsentPageUrl: typeof import('./composables/usePrivacyConsent')['getPrivacyConsentPageUrl']
const getPrivacyDecision: typeof import('./composables/usePrivacyConsent')['getPrivacyDecision']
const getToken: typeof import('./utils/storage')['getToken']
const getUserInfo: typeof import('./utils/storage')['getUserInfo']
const getWebPathForNotification: typeof import('./composables/uni-router')['getWebPathForNotification']
const h: typeof import('vue')['h']
const handlePosterRenderMergeDone: typeof import('./utils/posterRenderMergeBridge')['handlePosterRenderMergeDone']
const handlePosterRenderMergeFailed: typeof import('./utils/posterRenderMergeBridge')['handlePosterRenderMergeFailed']
const hasAcceptedPrivacyPolicy: typeof import('./composables/usePrivacyConsent')['hasAcceptedPrivacyPolicy']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const installNavigationAuthGuard: typeof import('./composables/useNavigationAuthGuard')['installNavigationAuthGuard']
const installPrivacyGuards: typeof import('./composables/usePrivacyConsent')['installPrivacyGuards']
const installPrivacyNavigationGuard: typeof import('./composables/usePrivacyConsent')['installPrivacyNavigationGuard']
const installPrivacyRequestGuard: typeof import('./composables/usePrivacyConsent')['installPrivacyRequestGuard']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
@@ -140,6 +146,7 @@ declare global {
const setAgentInfo: typeof import('./utils/storage')['setAgentInfo']
const setAuthSession: typeof import('./utils/storage')['setAuthSession']
const setPosterMergePending: typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending']
const setPrivacyDecision: typeof import('./composables/usePrivacyConsent')['setPrivacyDecision']
const setToken: typeof import('./utils/storage')['setToken']
const setUserInfo: typeof import('./utils/storage')['setUserInfo']
const shallowReactive: typeof import('vue')['shallowReactive']
@@ -169,6 +176,7 @@ declare global {
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch')['default']
const useAppBootstrap: typeof import('./composables/useAppBootstrap')['useAppBootstrap']
const useAppConfig: typeof import('./composables/useAppConfig')['useAppConfig']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
@@ -321,6 +329,7 @@ declare global {
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
@@ -373,9 +382,12 @@ declare global {
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { ApiEnvelope } from './composables/useApiFetch'
export type { ApiEnvelope, UseApiFetchOptions } from './composables/useApiFetch'
import('./composables/useApiFetch')
// @ts-ignore
export type { AppConfigPayload } from './composables/useAppConfig'
import('./composables/useAppConfig')
// @ts-ignore
export type { AppVersionPayload } from './composables/useHotUpdate'
import('./composables/useHotUpdate')
// @ts-ignore
@@ -432,16 +444,22 @@ declare module 'vue' {
readonly getCurrentUniRoute: UnwrapRef<typeof import('./composables/uni-router')['getCurrentUniRoute']>
readonly getLayoutPageTitle: UnwrapRef<typeof import('./composables/uni-router')['getLayoutPageTitle']>
readonly getPageTitleByRoute: UnwrapRef<typeof import('./composables/uni-router')['getPageTitleByRoute']>
readonly getPrivacyConsentPageUrl: UnwrapRef<typeof import('./composables/usePrivacyConsent')['getPrivacyConsentPageUrl']>
readonly getPrivacyDecision: UnwrapRef<typeof import('./composables/usePrivacyConsent')['getPrivacyDecision']>
readonly getToken: UnwrapRef<typeof import('./utils/storage')['getToken']>
readonly getUserInfo: UnwrapRef<typeof import('./utils/storage')['getUserInfo']>
readonly getWebPathForNotification: UnwrapRef<typeof import('./composables/uni-router')['getWebPathForNotification']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly handlePosterRenderMergeDone: UnwrapRef<typeof import('./utils/posterRenderMergeBridge')['handlePosterRenderMergeDone']>
readonly handlePosterRenderMergeFailed: UnwrapRef<typeof import('./utils/posterRenderMergeBridge')['handlePosterRenderMergeFailed']>
readonly hasAcceptedPrivacyPolicy: UnwrapRef<typeof import('./composables/usePrivacyConsent')['hasAcceptedPrivacyPolicy']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly installNavigationAuthGuard: UnwrapRef<typeof import('./composables/useNavigationAuthGuard')['installNavigationAuthGuard']>
readonly installPrivacyGuards: UnwrapRef<typeof import('./composables/usePrivacyConsent')['installPrivacyGuards']>
readonly installPrivacyNavigationGuard: UnwrapRef<typeof import('./composables/usePrivacyConsent')['installPrivacyNavigationGuard']>
readonly installPrivacyRequestGuard: UnwrapRef<typeof import('./composables/usePrivacyConsent')['installPrivacyRequestGuard']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@@ -522,6 +540,7 @@ declare module 'vue' {
readonly setAgentInfo: UnwrapRef<typeof import('./utils/storage')['setAgentInfo']>
readonly setAuthSession: UnwrapRef<typeof import('./utils/storage')['setAuthSession']>
readonly setPosterMergePending: UnwrapRef<typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending']>
readonly setPrivacyDecision: UnwrapRef<typeof import('./composables/usePrivacyConsent')['setPrivacyDecision']>
readonly setToken: UnwrapRef<typeof import('./utils/storage')['setToken']>
readonly setUserInfo: UnwrapRef<typeof import('./utils/storage')['setUserInfo']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
@@ -551,6 +570,7 @@ declare module 'vue' {
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useApiFetch: UnwrapRef<typeof import('./composables/useApiFetch')['default']>
readonly useAppBootstrap: UnwrapRef<typeof import('./composables/useAppBootstrap')['useAppBootstrap']>
readonly useAppConfig: UnwrapRef<typeof import('./composables/useAppConfig')['useAppConfig']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@@ -580,7 +600,6 @@ declare module 'vue' {
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCount: UnwrapRef<typeof import('./composables/useCount')['useCount']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
@@ -607,7 +626,6 @@ declare module 'vue' {
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEnv: UnwrapRef<typeof import('./composables/useEnv.js')['useEnv']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
@@ -623,7 +641,6 @@ declare module 'vue' {
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHotUpdate: UnwrapRef<typeof import('./composables/useHotUpdate')['useHotUpdate']>
readonly useHttp: UnwrapRef<typeof import('./composables/useHttp.js')['useHttp']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
@@ -672,7 +689,6 @@ declare module 'vue' {
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReportWebview: UnwrapRef<typeof import('./composables/useReportWebview')['useReportWebview']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRiskNotifier: UnwrapRef<typeof import('./composables/useRiskNotifier.js')['useRiskNotifier']>
readonly useRoute: UnwrapRef<typeof import('./composables/uni-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('./composables/uni-router')['useRouter']>
readonly useSEO: UnwrapRef<typeof import('./composables/useSEO.js')['useSEO']>
@@ -703,6 +719,7 @@ declare module 'vue' {
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
@@ -722,14 +739,11 @@ declare module 'vue' {
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebView: UnwrapRef<typeof import('./composables/useWebView.js')['useWebView']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWeixinShare: UnwrapRef<typeof import('./composables/useWeixinShare.js')['useWeixinShare']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly useZoomAdapter: UnwrapRef<typeof import('./composables/useZoomAdapter.js')['useZoomAdapter']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>

27
src/components.d.ts vendored
View File

@@ -10,41 +10,14 @@ declare module 'vue' {
AccountCancelAgreement: typeof import('./components/AccountCancelAgreement.vue')['default']
AgentApplicationForm: typeof import('./components/AgentApplicationForm.vue')['default']
BindPhoneDialog: typeof import('./components/BindPhoneDialog.vue')['default']
GaugeChart: typeof import('./components/GaugeChart.vue')['default']
ImageSaveGuide: typeof import('./components/ImageSaveGuide.vue')['default']
InquireForm: typeof import('./components/InquireForm.vue')['default']
LButtonGroup: typeof import('./components/LButtonGroup.vue')['default']
LEmpty: typeof import('./components/LEmpty.vue')['default']
LoginDialog: typeof import('./components/LoginDialog.vue')['default']
LRemark: typeof import('./components/LRemark.vue')['default']
LTable: typeof import('./components/LTable.vue')['default']
LTitle: typeof import('./components/LTitle.vue')['default']
Payment: typeof import('./components/Payment.vue')['default']
PriceInputPopup: typeof import('./components/PriceInputPopup.vue')['default']
QRcode: typeof import('./components/QRcode.vue')['default']
RealNameAuthDialog: typeof import('./components/RealNameAuthDialog.vue')['default']
SectionTitle: typeof import('./components/SectionTitle.vue')['default']
ShareReportButton: typeof import('./components/ShareReportButton.vue')['default']
StyledTabs: typeof import('./components/StyledTabs.vue')['default']
TitleBanner: typeof import('./components/TitleBanner.vue')['default']
VerificationCard: typeof import('./components/VerificationCard.vue')['default']
VipBanner: typeof import('./components/VipBanner.vue')['default']
WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default']
WdCell: typeof import('wot-design-uni/components/wd-cell/wd-cell.vue')['default']
WdCellGroup: typeof import('wot-design-uni/components/wd-cell-group/wd-cell-group.vue')['default']
WdCheckbox: typeof import('wot-design-uni/components/wd-checkbox/wd-checkbox.vue')['default']
WdColPicker: typeof import('wot-design-uni/components/wd-col-picker/wd-col-picker.vue')['default']
WdDivider: typeof import('wot-design-uni/components/wd-divider/wd-divider.vue')['default']
WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default']
WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
WdNavbar: typeof import('wot-design-uni/components/wd-navbar/wd-navbar.vue')['default']
WdPagination: typeof import('wot-design-uni/components/wd-pagination/wd-pagination.vue')['default']
WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default']
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default']
WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default']
WdRadioGroup: typeof import('wot-design-uni/components/wd-radio-group/wd-radio-group.vue')['default']
WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default']
WdTabbarItem: typeof import('wot-design-uni/components/wd-tabbar-item/wd-tabbar-item.vue')['default']
}
}

View File

@@ -187,50 +187,89 @@ watch(
<template>
<wd-popup v-model="show" destroy-on-close round position="bottom">
<view class="h-12 flex items-center justify-center font-semibold"
style="background-color: var(--van-theme-primary-light); color: var(--van-theme-primary);">
<view
class="h-12 flex items-center justify-center font-semibold"
style="background-color: var(--color-primary-light); color: var(--color-primary);"
>
成为代理
</view>
<view v-if="ancestor" class="my-2 text-center text-xs" style="color: var(--van-text-color-2);">
<view v-if="ancestor" class="my-2 text-center text-xs" style="color: var(--color-text-secondary);">
{{ maskName(ancestor) }}邀您成为赤眉代理方
</view>
<view class="p-4">
<wd-col-picker v-model="region" class="agent-form-field" label-width="42px" label="地区" placeholder="请选择地区"
:columns="columns" :column-change="handleColumnChange" :display-format="displayFormat" :align-right="false"
custom-value-class="agent-col-picker-value" @confirm="handleRegionConfirm" />
<wd-input v-model="form.mobile" class="agent-form-field" label-width="42px" label="手机号" name="mobile"
placeholder="请输入手机号" :align-right="false" :readonly="mobileReadonly" :disabled="mobileReadonly" />
<wd-col-picker
v-model="region"
class="agent-form-field"
label-width="42px"
label="地区"
placeholder="请选择地区"
:columns="columns"
:column-change="handleColumnChange"
:display-format="displayFormat"
:align-right="false"
custom-value-class="agent-col-picker-value"
@confirm="handleRegionConfirm"
/>
<wd-input
v-model="form.mobile"
class="agent-form-field"
label-width="42px"
label="手机号"
name="mobile"
placeholder="请输入手机号"
:align-right="false"
:readonly="mobileReadonly"
:disabled="mobileReadonly"
/>
<!-- 获取验证码按钮 -->
<wd-input v-model="form.code" class="agent-form-field" label-width="42px" label="验证码" name="code"
placeholder="请输入验证码" :align-right="false" use-suffix-slot>
<wd-input
v-model="form.code"
class="agent-form-field"
label-width="42px"
label="验证码"
name="code"
placeholder="请输入验证码"
:align-right="false"
use-suffix-slot
>
<template #suffix>
<button class="ml-2 flex-shrink-0 rounded-lg px-2 py-1 text-sm font-bold transition duration-300" :class="isCountingDown || !isPhoneNumberValid
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'text-white hover:opacity-90'" :style="isCountingDown || !isPhoneNumberValid
? ''
: 'background-color: var(--van-theme-primary);'" :disabled="isCountingDown || !isPhoneNumberValid"
@click.stop="getSmsCode">
{{
isCountingDown ? `${countdown}s重新获取` : '获取验证码'
}}
</button>
<wd-button
class="ml-2"
size="small"
type="primary"
plain
:disabled="isCountingDown || !isPhoneNumberValid"
@click.stop="getSmsCode"
>
{{ isCountingDown ? `${countdown}s重新获取` : '获取验证码' }}
</wd-button>
</template>
</wd-input>
<!-- 同意条款的复选框 -->
<view class="p-4">
<view class="flex items-start">
<wd-checkbox v-model="isAgreed" name="agree" icon-size="16px" class="mr-2 flex-shrink-0" />
<view class="text-xs leading-tight" style="color: var(--van-text-color-2);">
<view class="text-xs leading-tight" style="color: var(--color-text-secondary);">
我已阅读并同意
<a class="cursor-pointer hover:underline" style="color: var(--van-theme-primary);"
@click="toUserAgreement">用户协议</a>
<a class="cursor-pointer hover:underline" style="color: var(--van-theme-primary);"
@click="toAgentManageAgreement">推广方管理制度协议</a>
<view class="mt-1 text-xs" style="color: var(--van-text-color-2);">
<text
class="cursor-pointer hover:underline"
style="color: var(--color-primary);"
@click="toUserAgreement"
>
用户协议
</text>
<text
class="cursor-pointer hover:underline"
style="color: var(--color-primary);"
@click="toAgentManageAgreement"
>
推广方管理制度协议
</text>
<view class="mt-1 text-xs" style="color: var(--color-text-secondary);">
点击勾选即代表您同意上述法律文书的相关条款并签署上述法律文书
</view>
<view class="mt-1 text-xs" style="color: var(--van-text-color-2);">
<view class="mt-1 text-xs" style="color: var(--color-text-secondary);">
手机号未在本平台注册账号则申请后将自动生成账号
</view>
</view>

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, nextTick, ref } from 'vue'
import { computed, nextTick, onUnmounted, ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore'
import { setAuthSession } from '@/utils/storage'
@@ -25,10 +25,6 @@ function showToast(options) {
})
}
// 聚焦状态变量
const phoneFocused = ref(false)
const codeFocused = ref(false)
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value)
})
@@ -120,8 +116,11 @@ function closeDialog() {
phoneNumber.value = ''
verificationCode.value = ''
isAgreed.value = false
isCountingDown.value = false
countdown.value = 60
if (timer) {
clearInterval(timer)
timer = null
}
}
@@ -134,6 +133,12 @@ function toPrivacyPolicy() {
closeDialog()
router.push(`/privacyPolicy`)
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
@@ -164,56 +169,72 @@ function toPrivacyPolicy() {
</view>
<view class="space-y-5">
<!-- 手机号输入 -->
<view class="input-container bg-blue-300/20" :class="[
phoneFocused ? 'focused' : '',
]">
<input v-model="phoneNumber" class="input-field" type="tel" placeholder="请输入手机号" maxlength="11"
@focus="phoneFocused = true" @blur="phoneFocused = false">
<view class="form-item">
<text class="form-label">
手机号
</text>
<wd-input
v-model="phoneNumber"
class="field-wd-input"
type="number"
placeholder="请输入手机号"
maxlength="11"
no-border
clearable
/>
</view>
<!-- 验证码输入 -->
<view class="flex items-center justify-between">
<view class="input-container bg-blue-300/20" :class="[
codeFocused ? 'focused' : '',
]">
<input id="verificationCode" ref="verificationCodeInputRef" v-model="verificationCode"
class="input-field" placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
@blur="codeFocused = false">
</view>
<button class="ml-2 flex-shrink-0 rounded-lg px-4 py-2 text-sm font-bold transition duration-300" :class="isCountingDown || !isPhoneNumberValid
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'bg-blue-500 text-white hover:bg-blue-600'
" @click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
}}
</button>
<view class="form-item">
<text class="form-label">
验证码
</text>
<wd-input
ref="verificationCodeInputRef"
v-model="verificationCode"
class="field-wd-input"
placeholder="请输入验证码"
maxlength="6"
no-border
clearable
>
<template #suffix>
<wd-button
size="small"
type="primary"
plain
:disabled="isCountingDown || !isPhoneNumberValid"
@click="sendVerificationCode"
>
{{ isCountingDown ? `${countdown}s重新获取` : '获取验证码' }}
</wd-button>
</template>
</wd-input>
</view>
<!-- 协议同意框 -->
<view class="flex items-start space-x-2">
<input v-model="isAgreed" type="checkbox" class="mt-1">
<text class="text-xs text-gray-400 leading-tight">
<view class="agreement-wrapper">
<wd-checkbox v-model="isAgreed" shape="square" size="18px" />
<text class="agreement-text">
绑定手机号即代表您已阅读并同意
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
<text class="agreement-link" @click="toUserAgreement">
用户协议
</a>
</text>
<a class="cursor-pointer text-blue-400" @click="toPrivacyPolicy">
<text class="agreement-link" @click="toPrivacyPolicy">
隐私政策
</a>
</text>
</text>
</view>
</view>
<button
class="mt-10 w-full rounded-full bg-blue-500 py-3 text-lg text-white font-bold transition duration-300"
:class="{ 'opacity-50 cursor-not-allowed': !canBind }" @click="handleBind">
<wd-button
class="mt-10"
block
type="primary"
:disabled="!canBind"
@click="handleBind"
>
确认绑定
</button>
</wd-button>
</view>
</view>
</wd-popup>
@@ -242,22 +263,45 @@ function toPrivacyPolicy() {
cursor: pointer;
}
.input-container {
border: 2px solid rgba(125, 211, 252, 0);
border-radius: 1rem;
transition: duration-200;
.form-item {
margin-bottom: 1rem;
display: flex;
align-items: center;
border-radius: 12px;
background-color: #fff;
padding: 0 0.75rem;
min-height: 48px;
}
.input-container.focused {
border: 2px solid #3b82f6;
.form-label {
font-size: 0.9375rem;
color: #111827;
margin-right: 0.75rem;
font-weight: 500;
min-width: 3.25rem;
flex-shrink: 0;
}
.input-field {
width: 100%;
padding: 1rem;
background: transparent;
border: none;
outline: none;
transition: border-color 0.3s ease;
.field-wd-input {
flex: 1;
}
.agreement-wrapper {
display: flex;
align-items: flex-start;
margin-top: 1rem;
}
.agreement-text {
font-size: 0.75rem;
color: #6b7280;
line-height: 1.5;
margin-left: 0.5rem;
flex: 1;
}
.agreement-link {
color: #2563eb;
cursor: pointer;
}
</style>

View File

@@ -1,262 +0,0 @@
<script setup>
import * as echarts from 'echarts'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps({
score: {
type: Number,
required: true,
},
})
// 根据分数计算风险等级和颜色(分数越高越安全)
const riskLevel = computed(() => {
const score = props.score
if (score >= 75 && score <= 100) {
return {
level: '无任何风险',
color: '#52c41a',
gradient: [
{ offset: 0, color: '#52c41a' },
{ offset: 1, color: '#7fdb42' },
],
}
}
else if (score >= 50 && score < 75) {
return {
level: '风险指数较低',
color: '#faad14',
gradient: [
{ offset: 0, color: '#faad14' },
{ offset: 1, color: '#ffc53d' },
],
}
}
else if (score >= 25 && score < 50) {
return {
level: '风险指数较高',
color: '#fa8c16',
gradient: [
{ offset: 0, color: '#fa8c16' },
{ offset: 1, color: '#ffa940' },
],
}
}
else {
return {
level: '高风险警告',
color: '#f5222d',
gradient: [
{ offset: 0, color: '#f5222d' },
{ offset: 1, color: '#ff4d4f' },
],
}
}
})
// 评分解释文本(分数越高越安全)
const riskDescription = computed(() => {
const score = props.score
if (score >= 75 && score <= 100) {
return '根据综合分析,当前报告未检测到明显风险因素,各项指标表现正常,总体状况良好。'
}
else if (score >= 50 && score < 75) {
return '根据综合分析,当前报告存在少量风险信号,建议关注相关指标变化,保持警惕。'
}
else if (score >= 25 && score < 50) {
return '根据综合分析,当前报告风险指数较高,多项指标显示异常,建议进一步核实相关情况。'
}
else {
return '根据综合分析,当前报告显示高度风险状态,多项重要指标严重异常,请立即采取相应措施。'
}
})
const chartRef = ref(null)
let chartInstance = null
function initChart() {
if (!chartRef.value)
return
// 初始化ECharts实例
chartInstance = echarts.init(chartRef.value)
updateChart()
}
function updateChart() {
if (!chartInstance)
return
// 获取当前风险等级信息
const risk = riskLevel.value
// 配置项
const option = {
series: [
{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
radius: '100%',
center: ['50%', '80%'],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, risk.gradient),
shadowBlur: 6,
shadowColor: risk.color,
},
progress: {
show: true,
width: 20,
roundCap: true,
clip: false,
},
axisLine: {
roundCap: true,
lineStyle: {
width: 20,
color: [
[1, new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: `${risk.color}30`, // 使用风险颜色透明度20%
},
{
offset: 1,
color: `${risk.color}25`, // 使用风险颜色透明度10%
},
])],
],
},
},
axisTick: {
show: true,
distance: -30,
length: 6,
splitNumber: 10, // 每1分一个小刻度
lineStyle: {
color: risk.color,
width: 1,
opacity: 0.5,
},
},
splitLine: {
show: true,
distance: -36,
length: 12,
splitNumber: 9, // 9个大刻度100分分成9个区间
lineStyle: {
color: risk.color,
width: 2,
opacity: 0.5,
},
},
axisLabel: {
show: false,
},
anchor: {
show: false,
},
pointer: {
icon: 'triangle',
iconStyle: {
color: risk.color,
borderColor: risk.color,
borderWidth: 1,
},
offsetCenter: ['7%', '-67%'],
length: '10%',
width: 15,
},
detail: {
valueAnimation: true,
fontSize: 30,
fontWeight: 'bold',
color: risk.color,
offsetCenter: [0, '-25%'],
formatter(value) {
return `{value|${value}分}\n{level|${risk.level}}`
},
rich: {
value: {
fontSize: 30,
fontWeight: 'bold',
color: risk.color,
padding: [0, 0, 5, 0],
},
level: {
fontSize: 14,
fontWeight: 'normal',
color: risk.color,
padding: [5, 0, 0, 0],
},
},
},
data: [
{
value: props.score,
},
],
title: {
fontSize: 14,
color: risk.color,
offsetCenter: [0, '10%'],
formatter: risk.level,
},
},
],
}
// 使用配置项设置图表
chartInstance.setOption(option)
}
// 监听分数变化
watch(
() => props.score,
() => {
updateChart()
},
)
onMounted(() => {
initChart()
// 处理窗口大小变化
window.addEventListener('resize', () => {
if (chartInstance) {
chartInstance.resize()
}
})
})
// 在组件销毁前清理
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
window.removeEventListener('resize', chartInstance?.resize)
})
</script>
<template>
<view>
<view class="risk-description">
{{ riskDescription }}
</view>
<view ref="chartRef" :style="{ width: '100%', height: '200px' }" />
</view>
</template>
<style scoped>
.risk-description {
margin-bottom: 4px;
padding: 0 12px;
color: #666666;
font-size: 12px;
line-height: 1.5;
text-align: center;
}
</style>

View File

@@ -1,7 +1,5 @@
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
defineProps({
show: {
type: Boolean,
default: false,
@@ -36,9 +34,11 @@ function close() {
<view v-if="show" class="image-save-guide-overlay">
<view class="guide-content" @click.stop>
<!-- 关闭按钮 -->
<button class="close-button" @click="close">
<text class="close-icon">×</text>
</button>
<wd-button class="close-button" custom-class="image-guide-close-btn" plain @click="close">
<text class="close-icon">
×
</text>
</wd-button>
<!-- 图片区域 -->
<view v-if="imageUrl" class="image-container">
@@ -97,7 +97,6 @@ function close() {
right: 12px;
width: 32px;
height: 32px;
border: none;
background: rgba(0, 0, 0, 0.1);
border-radius: 50%;
display: flex;
@@ -108,7 +107,7 @@ function close() {
z-index: 10;
}
.close-button:hover {
:deep(.image-guide-close-btn.wd-button:hover) {
background: rgba(0, 0, 0, 0.2);
}

View File

@@ -5,7 +5,7 @@ import LoginDialog from '@/components/LoginDialog.vue'
import Payment from '@/components/Payment.vue'
import SectionTitle from '@/components/SectionTitle.vue'
import { useRouter } from '@/composables/uni-router'
import { useEnv } from '@/composables/useEnv'
import { useAppConfig } from '@/composables/useAppConfig'
import { useDialogStore } from '@/stores/dialogStore'
import { useUserStore } from '@/stores/userStore'
import { aesEncrypt } from '@/utils/crypto'
@@ -69,7 +69,8 @@ function loadProductBackground(productType) {
const router = useRouter()
const dialogStore = useDialogStore()
const userStore = useUserStore()
const { isWeChat } = useEnv()
const isWeChat = ref(false)
const { appConfig, loadAppConfig } = useAppConfig()
// 响应式数据
const showPayment = ref(false)
@@ -111,6 +112,7 @@ const buttonText = computed(() => {
})
const hasHeroImage = computed(() => Boolean(productBackground.value))
const queryRetentionDaysText = computed(() => `${appConfig.value.query.retention_days}`)
function loadTrapezoidBackground() {
trapezoidBgImage.value = TRAPEZOID_BACKGROUND_MAP[props.feature] || TRAPEZOID_BACKGROUND_MAP.default
@@ -380,6 +382,7 @@ function toHistory() {
onMounted(async () => {
loadBackgroundImage()
loadTrapezoidBackground()
await loadAppConfig()
})
// 加载背景图片
@@ -427,37 +430,76 @@ watch(feature, async () => {
<!-- 表单输入区域 -->
<view class="mb-6 space-y-4">
<view class="flex items-center border-b border-gray-100 py-3">
<text for="name" class="w-20 text-gray-700 font-medium">
<text class="w-20 text-gray-700 font-medium">
姓名
</text>
<input id="name" v-model="formData.name" type="text" placeholder="请输入正确的姓名"
class="flex-1 border-none outline-none" @click="handleInputClick">
<wd-input
v-model="formData.name"
class="inquire-wd-input flex-1"
type="text"
placeholder="请输入正确的姓名"
no-border
clearable
@focus="handleInputClick"
/>
</view>
<view class="flex items-center border-b border-gray-100 py-3">
<text for="idCard" class="w-20 text-gray-700 font-medium">
<text class="w-20 text-gray-700 font-medium">
身份证号
</text>
<input id="idCard" v-model="formData.idCard" type="text" placeholder="请输入准确的身份证号"
class="flex-1 border-none outline-none" @click="handleInputClick">
<wd-input
v-model="formData.idCard"
class="inquire-wd-input flex-1"
type="text"
placeholder="请输入准确的身份证号"
no-border
clearable
@focus="handleInputClick"
/>
</view>
<view class="flex items-center border-b border-gray-100 py-3">
<text for="mobile" class="w-20 text-gray-700 font-medium">
<text class="w-20 text-gray-700 font-medium">
手机号
</text>
<input id="mobile" v-model="formData.mobile" type="tel" placeholder="请输入手机号"
class="flex-1 border-none outline-none" @click="handleInputClick">
<wd-input
v-model="formData.mobile"
class="inquire-wd-input flex-1"
type="number"
placeholder="请输入手机号"
maxlength="11"
no-border
clearable
@focus="handleInputClick"
/>
</view>
<!-- 小微企业(companyinfo)暂不展示验证码 -->
<view v-if="needVerificationCode" class="flex items-center border-b border-gray-100 py-3">
<text for="verificationCode" class="w-20 text-gray-700 font-medium">
<text class="w-20 text-gray-700 font-medium">
验证码
</text>
<input id="verificationCode" ref="verificationCodeInputRef" v-model="formData.verificationCode"
placeholder="请输入验证码" maxlength="6" class="flex-1 border-none outline-none" @click="handleInputClick">
<wd-button class="captcha-wd-btn" size="small" type="primary" plain
:disabled="isCountingDown || !isPhoneNumberValid" @click="sendVerificationCode">
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
</wd-button>
<wd-input
ref="verificationCodeInputRef"
v-model="formData.verificationCode"
class="inquire-wd-input flex-1"
placeholder="请输入验证码"
maxlength="6"
no-border
clearable
@focus="handleInputClick"
>
<template #suffix>
<wd-button
class="captcha-wd-btn"
size="small"
type="primary"
plain
:disabled="isCountingDown || !isPhoneNumberValid"
@click="sendVerificationCode"
>
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
</wd-button>
</template>
</wd-input>
</view>
</view>
@@ -481,27 +523,27 @@ watch(feature, async () => {
</view>
<!-- 查询按钮 -->
<button
class="bg-primary mb-4 mt-10 w-full flex items-center justify-center rounded-[48px] py-4 text-lg text-white font-medium"
@click="handleSubmit">
<text>{{ buttonText }}</text>
<text class="ml-4">
¥{{ featureData.sell_price }}
</text>
</button>
<wd-button class="submit-wd-btn mb-4 mt-10" block type="primary" @click="handleSubmit">
<view class="w-full flex items-center justify-center">
<text>{{ buttonText }}</text>
<text class="ml-4">
¥{{ featureData.sell_price }}
</text>
</view>
</wd-button>
<!-- <view class="text-xs text-gray-500 leading-relaxed mt-8" v-html="featureData.description">
</view> -->
<!-- 免责声明 -->
<view class="mt-2 text-center text-xs text-gray-500 leading-relaxed">
为保证用户的隐私及数据安全查询结果生成30天后将自动删除
为保证用户的隐私及数据安全查询结果生成{{ queryRetentionDaysText }}后将自动删除
</view>
</view>
<!-- 报告包含内容 -->
<view v-if="featureData.features && featureData.features.length > 0" class="card mt-3">
<view class="mb-3 flex items-center text-base font-semibold" style="color: var(--van-text-color);">
<view class="mb-3 flex items-center text-base font-semibold" style="color: var(--color-text-primary);">
<view class="mr-2 h-5 w-1 rounded-full"
style="background: linear-gradient(to bottom, var(--van-theme-primary), var(--van-theme-primary-dark));" />
style="background: linear-gradient(to bottom, var(--color-primary), var(--color-primary-700));" />
报告包含内容
</view>
<view class="grid grid-cols-4 items-stretch gap-2">
@@ -650,9 +692,9 @@ watch(feature, async () => {
</view>
<view class="mt-3 text-center">
<view class="inline-flex items-center border rounded-full px-3 py-1.5 transition-all"
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.8)); border-color: var(--van-theme-primary);">
<view class="mr-1.5 h-1.5 w-1.5 rounded-full" style="background-color: var(--van-theme-primary);" />
<text class="text-xs font-medium" style="color: var(--van-theme-primary);">
style="background: linear-gradient(135deg, var(--color-primary-light), rgba(255,255,255,0.8)); border-color: var(--color-primary);">
<view class="mr-1.5 h-1.5 w-1.5 rounded-full" style="background-color: var(--color-primary);" />
<text class="text-xs font-medium" style="color: var(--color-primary);">
更多信息请解锁报告
</text>
</view>
@@ -661,11 +703,11 @@ watch(feature, async () => {
<!-- 产品详情卡片 -->
<view class="card mt-4">
<view class="mb-4 text-xl font-bold" style="color: var(--van-text-color);">
<view class="mb-4 text-xl font-bold" style="color: var(--color-text-primary);">
{{ featureData.product_name }}
</view>
<view class="mb-4 flex items-start justify-between">
<view class="text-lg" style="color: var(--van-text-color-2);">
<view class="text-lg" style="color: var(--color-text-secondary);">
价格:
</view>
<view>
@@ -676,9 +718,9 @@ watch(feature, async () => {
</view>
<image v-if="productMainImage" :src="productMainImage" alt="产品详情主图" class="mb-4 w-full rounded-lg" />
<view class="mb-4 leading-relaxed" style="color: var(--van-text-color-2);" v-html="featureData.description" />
<view class="mb-4 leading-relaxed" style="color: var(--color-text-secondary);" v-html="featureData.description" />
<view class="text-danger mb-2 text-xs italic">
为保证用户的隐私以及数据安全,查询的结果生成30天之后将自动清除。
为保证用户的隐私以及数据安全,查询的结果生成{{ queryRetentionDaysText }}之后将自动清除。
</view>
</view>
</view>
@@ -755,15 +797,6 @@ watch(feature, async () => {
0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* 按钮悬停效果 */
button:hover {
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
/* 梯形背景图片样式 */
.trapezoid-bg-image {
background-size: contain;
@@ -790,6 +823,15 @@ button:active {
margin-left: 12px;
}
:deep(.inquire-wd-input .wd-input__inner) {
text-align: left !important;
}
:deep(.submit-wd-btn.wd-button) {
border-radius: 48px;
min-height: 48px;
}
:deep(.captcha-wd-btn.wd-button) {
min-width: 96px;
}

View File

@@ -1,79 +0,0 @@
<script setup>
// 接收 type 和 options props 以及 v-model
const props = defineProps({
type: {
type: String,
default: 'purple-pink', // 默认颜色渐变
},
options: {
type: Array,
required: true, // 动态传入选项
},
modelValue: {
type: String,
default: '', // v-model 绑定的值
},
})
const emit = defineEmits(['update:modelValue'])
// 选中内容绑定 v-model
const selected = ref(props.modelValue)
// 监听 v-model 的变化
watch(() => props.modelValue, (newValue) => {
selected.value = newValue
})
// 根据type动态生成分割线的类名
const lineClass = computed(() => {
// 统一使用主题色渐变
return 'bg-gradient-to-r from-red-600 via-red-500 to-red-700'
})
// 计算滑动线的位置和宽度
const slideLineStyle = computed(() => {
const index = props.options.findIndex(option => option.value === selected.value)
const buttonWidth = 100 / props.options.length
return {
width: `${buttonWidth}%`,
transform: `translateX(${index * 100}%)`,
}
})
// 选择选项函数
function selectOption(option) {
selected.value = option.value
// 触发 v-model 的更新
emit('update:modelValue', option.value)
}
</script>
<template>
<view class="relative flex">
<view
v-for="(option, index) in options"
:key="index"
class="flex-1 shrink-0 cursor-pointer py-2 text-center text-size-sm font-bold transition-transform duration-200 ease-in-out"
:class="{ 'text-gray-900': selected === option.value, 'text-gray-500': selected !== option.value }"
@click="selectOption(option)"
>
{{ option.label }}
</view>
<view
class="absolute bottom-0 h-[3px] rounded transition-all duration-300"
:style="slideLineStyle"
:class="lineClass"
/>
</view>
</template>
<style scoped>
/* 自定义样式 */
button {
outline: none;
border: none;
cursor: pointer;
}
button:focus {
outline: none;
}
</style>

View File

@@ -1,42 +0,0 @@
<script setup>
const route = useRoute()
// 返回上一页逻辑
function goBack() {
route.goBack()
}
</script>
<template>
<view class="card flex flex-col items-center justify-center text-center">
<!-- 图片插画 -->
<image src="/static/images/empty.svg" alt="空状态" class="h-64 w-64" />
<!-- 提示文字 -->
<text class="mb-2 text-xl text-gray-700 font-semibold">
没有查询到相关结果
</text>
<text class="mb-2 text-sm text-gray-500 leading-relaxed">
订单已申请退款预计
<text class="font-medium" style="color: var(--van-theme-primary);">24小时内到账</text>
</text>
<text class="text-xs text-gray-400">
如果已到账您可以忽略本提示
</text>
<!-- 返回按钮 -->
<button
class="mt-4 rounded-lg px-6 py-2 text-white transition duration-300 ease-in-out"
style="background-color: var(--van-theme-primary);"
onmouseover="this.style.backgroundColor='var(--van-theme-primary-dark)'"
onmouseout="this.style.backgroundColor='var(--van-theme-primary)'"
@click="goBack"
>
返回上一页
</button>
</view>
</template>
<style scoped>
/* 你可以添加一些额外的样式(如果需要) */
</style>

View File

@@ -1,94 +0,0 @@
<script setup>
import { ref } from 'vue'
const props = defineProps({
content: {
type: String,
required: true,
},
})
const isExpanded = ref(false)
</script>
<template>
<view class="l-remark-card my-[20px]">
<!-- 顶部连接点 -->
<view class="connection-line">
<image
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAACTCAYAAADmz7tVAAAAAXNSR0IArs4c6QAAB59JREFUeF7tXFtsFFUY/s7sbreFUiyFglxaEJUgVaAYNBAjl/hgYrgU44PhRd+Ud3xQE4xP8izx0RfiGwX1xcQYMApegFakSghgWygIFVqope3e5phvzo47O53ZmcM2zT6cSTZsujPnfPP9l/P/w3xHQOM42S/rFxTQhCQ6bAtdkNgIidUSaOYwAhiFwDUI9Fo2upFH30gCY9tXiam404i4J/42KPcXJLosYOucRrTmckChANgFQEo1ihCAlQASCSCVAibGMWwDpxMC3RvaxdE4c0UC6rkhX4aNzyDxlGUhycldAFETECA/to08BK7AwjudK8T3la4LBXTmhmxosHEomcTBfD4+iLDJCCyZBPJ5HJ60cGjLCjEZdG4goL5+uSQrcCSdRlcmE8WD3u/pNJDJoLtO4kDHKnHbf/U0QASTEzglLKyx7fDJHH8pXs3vPFxT2hFmtSxA2ricktjmB1UGiGaqL+CoZaErDAz9N5cH7o0DQyPAv1PARFYBmlMHzKsHli8AWhqBVNKJvMCDoGwb3VMJ7Pear+z83kH5SV0dDgaZKWEBow+B6/eAm6PAVE4xxAG8DBEwGapPAcuagbYWoHkuUAhgm+bLZnF4Y7t4z0X9PyBGU8rCKYbzNLsK4NYI0HcTyOTD79p/HcGlk0DHMmDpguDAYHrI2djmRl8J0KD8UwBr/SHNu6dpegZKPqPnxoqxzpXKlEHjS+BSZ7t4pphcASY9W+JzAEnvZDTT0D3gXBVg3PEI6nmCagk0X94SeIvJU3A5aBL4ImFhr9+RxyaBX64B2bwuJ8Hn1yWBF1YDTQ3lv9PBCzaOj0m8KS5cla12Ehch0Oqlk/b/YwgYuBvfZ6Jgc8yVC4F1y8vHdIJCYtjK41lxYUDuqG/Edw/Hy4cjKycvAflC1DR6vycTwPa1ANnyHnMbgalx7BS91+WnqRQOZD0ZmYhvjQK//gUkrfAJyWjeLjkqqU8U16+wq3j+5ieApc3lDl6XBnI5HBG9g/K0lcCWgsdP6Mw/XwVuPwiOLALhHa5cBHS2Ay3zFJM3R4Dfh4C/R51QDkyKdO4l84EXnyx37kTSqRzOiN4BeRsCi73+Q0Df9gETmVLSc++Y5zHRvboeWN0KZHJArgBkCyqDE0j/MHCuHxjn9T6qeP2cNPBKRzmgoh/dET0Dksaq815HO584H3yHqQTw+mblnE5WtpXZCIZJcyoLTOWVyX+6Gpyhed2eTYH+mQ0F9OX56V7Au1uzFHhjs0p2PPgvzUWGyBaXFILiUvHjFWU+d2nxjrg7DJCOyWiat18C2haWhnYB8TeHoRwwmVW5iz74w2WALuA1eUWT6Tg1J/lgj4o8965pMrLhZ4hmI2Nf9QB0AW/GruzUGmFPBj7eBxSkCm8e/M7amoCyRZORITJF9o6dLQcUGfY6ibEioLwC5ZgsEw4oMjHqLB3VAoq1dOgsrtUCirW40g/ilh/VAIpdfrje3xOjQGNohzp1BR/iGqdVoBFUnBL2/CDw4e6QKAsB9HUvsKHtEUpYgooq8v8ZA7atVU38tLAPAMSouz+hOhDtIp+Aotog+sH6NpUU4wByGoKQPihWG0RQlRpFZuX17dUDit0oug4e1krPBCDtVtoFFfSwoRpAVT1s8JYJ3scxEkg+t0LPZHxWNCOPY/zVEJNnroA9m1ZhH507jlOzPygUcMwSODFjD6z8wKSUMnC1Dwj7VAOwZmFYnAW3AZFP0AygsP4p7O/GZFGMGYYMQz4GTB6KcgnDkGHI20qb1b7s6Yeph4rhYRbXqDxhGDIMmfLDNIrug3OzdJilIyojGoYMQ+YJGmC6DtN1BMWBqamj8qNhyDBkug7TdZiuwxcFJjGaxGgSo0mMJjGaxBiVCQ1DhqHKDJj/Jo/yEMOQYci8SGC6jqgoMAyFMVQzbwvXzPvUNfXGeU29k19TqoWa03XUlPKlam1QiALvkbVBWuopG6B6hUeoRpHbcEig+6w6V0s9paUv61ITufs1hKk4HY2iDXSfUxrF2PoyXQXeR/vURH5AfhUnhUpUTB0vAiKbsRR4uhrFQ3vVRK52ld+nKYGp5swr4FQUu7LSWBpFHRUnda7v71L0e33IUQJT6+oqgXNKeMvzqCj26lwjVZw68nZOcvA1Z8+Oksm47QaF226U8bWd4odSZr/wNlLnqqME5qTv7lR37FcCO4CKAm5HVV4E+c3Fcs3+jMrbeXe7NgKPP1ZSmjtRRj9yWSqCoW+NPFS6fS2ttI68nSbgxiI71pWk6fwb0wABOZ8iODLIrTnuPAjQ7M+UvJ2hS+H21qeB1ia1xYELyNXdkxmyxq1cLlwPFktynBmRt3Mghu7ctJKKLm5SjLggCI7MDI8Bl26pPWW0dyTQcWq3vCUoMrWoCWhvAeY3KKbuTwKDdxU7BBr04CDaqTXk7f56myCcrX2KPxCAs8FNhUcYkWGvkxijWqQ4v0cmRp2lI86Elc6JtXToLK7VAoq1uHKSuOVHNYBilx/uJHEKNLfk0AWmvf8QJ4hTws7qDk0EFVXkz+oeVgQU1Qa5mXrWdvnihDW1D5rrrDW1U5wLqqb20vOGdc3sNujPNTWzH6MX2GzsWPkfBLU1i3+dVUIAAAAASUVORK5CYII="
alt="左链条" class="connection-chain left" />
<image
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAACTCAYAAADmz7tVAAAAAXNSR0IArs4c6QAAB59JREFUeF7tXFtsFFUY/s7sbreFUiyFglxaEJUgVaAYNBAjl/hgYrgU44PhRd+Ud3xQE4xP8izx0RfiGwX1xcQYMApegFakSghgWygIFVqope3e5phvzo47O53ZmcM2zT6cSTZsujPnfPP9l/P/w3xHQOM42S/rFxTQhCQ6bAtdkNgIidUSaOYwAhiFwDUI9Fo2upFH30gCY9tXiam404i4J/42KPcXJLosYOucRrTmckChANgFQEo1ihCAlQASCSCVAibGMWwDpxMC3RvaxdE4c0UC6rkhX4aNzyDxlGUhycldAFETECA/to08BK7AwjudK8T3la4LBXTmhmxosHEomcTBfD4+iLDJCCyZBPJ5HJ60cGjLCjEZdG4goL5+uSQrcCSdRlcmE8WD3u/pNJDJoLtO4kDHKnHbf/U0QASTEzglLKyx7fDJHH8pXs3vPFxT2hFmtSxA2ricktjmB1UGiGaqL+CoZaErDAz9N5cH7o0DQyPAv1PARFYBmlMHzKsHli8AWhqBVNKJvMCDoGwb3VMJ7Pear+z83kH5SV0dDgaZKWEBow+B6/eAm6PAVE4xxAG8DBEwGapPAcuagbYWoHkuUAhgm+bLZnF4Y7t4z0X9PyBGU8rCKYbzNLsK4NYI0HcTyOTD79p/HcGlk0DHMmDpguDAYHrI2djmRl8J0KD8UwBr/SHNu6dpegZKPqPnxoqxzpXKlEHjS+BSZ7t4pphcASY9W+JzAEnvZDTT0D3gXBVg3PEI6nmCagk0X94SeIvJU3A5aBL4ImFhr9+RxyaBX64B2bwuJ8Hn1yWBF1YDTQ3lv9PBCzaOj0m8KS5cla12Ehch0Oqlk/b/YwgYuBvfZ6Jgc8yVC4F1y8vHdIJCYtjK41lxYUDuqG/Edw/Hy4cjKycvAflC1DR6vycTwPa1ANnyHnMbgalx7BS91+WnqRQOZD0ZmYhvjQK//gUkrfAJyWjeLjkqqU8U16+wq3j+5ieApc3lDl6XBnI5HBG9g/K0lcCWgsdP6Mw/XwVuPwiOLALhHa5cBHS2Ay3zFJM3R4Dfh4C/R51QDkyKdO4l84EXnyx37kTSqRzOiN4BeRsCi73+Q0Df9gETmVLSc++Y5zHRvboeWN0KZHJArgBkCyqDE0j/MHCuHxjn9T6qeP2cNPBKRzmgoh/dET0Dksaq815HO584H3yHqQTw+mblnE5WtpXZCIZJcyoLTOWVyX+6Gpyhed2eTYH+mQ0F9OX56V7Au1uzFHhjs0p2PPgvzUWGyBaXFILiUvHjFWU+d2nxjrg7DJCOyWiat18C2haWhnYB8TeHoRwwmVW5iz74w2WALuA1eUWT6Tg1J/lgj4o8965pMrLhZ4hmI2Nf9QB0AW/GruzUGmFPBj7eBxSkCm8e/M7amoCyRZORITJF9o6dLQcUGfY6ibEioLwC5ZgsEw4oMjHqLB3VAoq1dOgsrtUCirW40g/ilh/VAIpdfrje3xOjQGNohzp1BR/iGqdVoBFUnBL2/CDw4e6QKAsB9HUvsKHtEUpYgooq8v8ZA7atVU38tLAPAMSouz+hOhDtIp+Aotog+sH6NpUU4wByGoKQPihWG0RQlRpFZuX17dUDit0oug4e1krPBCDtVtoFFfSwoRpAVT1s8JYJ3scxEkg+t0LPZHxWNCOPY/zVEJNnroA9m1ZhH507jlOzPygUcMwSODFjD6z8wKSUMnC1Dwj7VAOwZmFYnAW3AZFP0AygsP4p7O/GZFGMGYYMQz4GTB6KcgnDkGHI20qb1b7s6Yeph4rhYRbXqDxhGDIMmfLDNIrug3OzdJilIyojGoYMQ+YJGmC6DtN1BMWBqamj8qNhyDBkug7TdZiuwxcFJjGaxGgSo0mMJjGaxBiVCQ1DhqHKDJj/Jo/yEMOQYci8SGC6jqgoMAyFMVQzbwvXzPvUNfXGeU29k19TqoWa03XUlPKlam1QiALvkbVBWuopG6B6hUeoRpHbcEig+6w6V0s9paUv61ITufs1hKk4HY2iDXSfUxrF2PoyXQXeR/vURH5AfhUnhUpUTB0vAiKbsRR4uhrFQ3vVRK52ld+nKYGp5swr4FQUu7LSWBpFHRUnda7v71L0e33IUQJT6+oqgXNKeMvzqCj26lwjVZw68nZOcvA1Z8+Oksm47QaF226U8bWd4odSZr/wNlLnqqME5qTv7lR37FcCO4CKAm5HVV4E+c3Fcs3+jMrbeXe7NgKPP1ZSmjtRRj9yWSqCoW+NPFS6fS2ttI68nSbgxiI71pWk6fwb0wABOZ8iODLIrTnuPAjQ7M+UvJ2hS+H21qeB1ia1xYELyNXdkxmyxq1cLlwPFktynBmRt3Mghu7ctJKKLm5SjLggCI7MDI8Bl26pPWW0dyTQcWq3vCUoMrWoCWhvAeY3KKbuTwKDdxU7BBr04CDaqTXk7f56myCcrX2KPxCAs8FNhUcYkWGvkxijWqQ4v0cmRp2lI86Elc6JtXToLK7VAoq1uHKSuOVHNYBilx/uJHEKNLfk0AWmvf8QJ4hTws7qDk0EFVXkz+oeVgQU1Qa5mXrWdvnihDW1D5rrrDW1U5wLqqb20vOGdc3sNujPNTWzH6MX2GzsWPkfBLU1i3+dVUIAAAAASUVORK5CYII="
alt="右链条" class="connection-chain right" />
</view>
<view>
<wd-icon name="info-o" class="tips-icon" />
<text class="tips-title">温馨提示</text>
</view>
<view>
<wd-text rows="2" :content="content" expand-text="展开" collapse-text="收起" />
</view>
</view>
</template>
<style scoped>
.l-remark-card {
position: relative;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
border-radius: 0.75rem;
background-color: #ffffff;
padding: 24px;
}
.tips-card {
background: var(--van-theme-primary-light);
border-radius: 8px;
padding: 12px;
}
.tips-icon {
color: var(--van-theme-primary);
margin-right: 5px;
}
.tips-title {
font-weight: bold;
font-size: 16px;
}
.tips-content {
font-size: 14px;
color: #333;
}
/* 连接链条样式 */
.connection-line {
position: absolute;
top: -40px;
left: 0;
right: 0;
height: 60px;
z-index: 20;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.connection-chain {
height: 60px;
object-fit: contain;
}
.connection-chain.left {
width: 80px;
margin-left: -10px;
}
.connection-chain.right {
width: 80px;
margin-right: -10px;
}
</style>

View File

@@ -1,80 +0,0 @@
<script setup>
import { computed, onMounted } from 'vue'
// 接收表格数据和类型的 props
const props = defineProps({
data: {
type: Array,
required: true,
},
type: {
type: String,
default: 'purple-pink', // 默认渐变颜色
},
})
// 根据 type 设置不同的渐变颜色(偶数行)
const evenClass = computed(() => {
// 统一使用主题色浅色背景
return 'bg-red-50/40'
})
// 动态计算表头的背景颜色和文本颜色
const headerClass = computed(() => {
// 统一使用主题色浅色背景
return 'bg-red-100'
})
// 斑马纹样式,偶数行带颜色,奇数行没有颜色,且从第二行开始
function zebraClass(index) {
return index % 2 === 1 ? evenClass.value : ''
}
</script>
<template>
<view class="l-table overflow-x-auto">
<table
class="min-w-full border-collapse table-auto text-center text-size-xs"
>
<thead :class="headerClass">
<tr>
<!-- 插槽渲染表头 -->
<slot name="header" />
</tr>
</thead>
<tbody>
<tr
v-for="(row, index) in props.data"
:key="index"
:class="zebraClass(index)"
class="border-t"
>
<slot :row="row" />
</tr>
</tbody>
</table>
</view>
</template>
<style scoped>
/* 基础表格样式 */
th {
font-weight: bold;
padding: 12px;
text-align: left;
border: 1px solid #e5e7eb;
}
/* 表格行样式 */
td {
padding: 12px;
border: 1px solid #e5e7eb;
}
table {
width: 100%;
border-spacing: 0;
}
.l-table {
@apply rounded-xl;
overflow: hidden;
}
</style>

View File

@@ -1,27 +0,0 @@
<script setup>
// 接收 props
const props = defineProps({
title: String,
})
const titleClass = computed(() => {
// 统一使用主题色
return 'bg-primary'
})
</script>
<template>
<view class="relative">
<!-- 标题部分 -->
<view :class="titleClass" class="inline-block rounded-lg px-2 py-1 text-white font-bold shadow-md">
{{ title }}
</view>
<!-- 左上角修饰 -->
<view
class="absolute left-0 top-0 h-4 w-4 transform rounded-full bg-white shadow-md -translate-x-2 -translate-y-2"
/>
</view>
</template>
<style scoped></style>

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, nextTick, ref } from 'vue'
import { computed, nextTick, onUnmounted, ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore'
import { useUserStore } from '@/stores/userStore'
import { setAuthSession } from '@/utils/storage'
@@ -103,24 +103,30 @@ async function handleLogin() {
}
async function performLogin() {
const { data, error } = await useApiFetch('/user/mobileCodeLogin')
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json()
uni.showLoading({ title: '登录中...', mask: true })
try {
const { data, error } = await useApiFetch('/user/mobileCodeLogin', { silent: true })
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
setAuthSession(data.value.data)
if (data.value && !error.value) {
if (data.value.code === 200) {
setAuthSession(data.value.data)
await userStore.fetchUserInfo()
await userStore.fetchUserInfo()
showToast({ message: '登录成功' })
closeDialog()
emit('login-success')
}
else {
showToast(data.value.msg)
showToast({ message: '登录成功' })
closeDialog()
emit('login-success')
}
else {
showToast(data.value.msg)
}
}
}
finally {
uni.hideLoading()
}
}
function closeDialog() {
@@ -146,11 +152,23 @@ function toPrivacyPolicy() {
closeDialog()
uni.navigateTo({ url: '/pages/privacy-policy' })
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
<wd-popup v-model="dialogStore.showLogin" round position="bottom" :style="{ maxHeight: '90vh' }" :z-index="2000"
@close="closeDialog">
<wd-popup
v-model="dialogStore.showLogin"
round
position="bottom"
:style="{ maxHeight: '90vh' }"
:z-index="2000"
@close="closeDialog"
>
<view class="login-dialog">
<view class="title-bar">
<view class="title-bar-text">
@@ -174,8 +192,15 @@ function toPrivacyPolicy() {
<text class="form-label">
手机号
</text>
<wd-input v-model="phoneNumber" class="phone-wd-input" type="number" placeholder="请输入手机号" maxlength="11"
no-border clearable />
<wd-input
v-model="phoneNumber"
class="phone-wd-input"
type="number"
placeholder="请输入手机号"
maxlength="11"
no-border
clearable
/>
</view>
<view v-if="!isPasswordLogin" class="form-item">
@@ -183,11 +208,23 @@ function toPrivacyPolicy() {
验证码
</text>
<view class="verification-input-wrapper">
<wd-input ref="verificationCodeInputRef" v-model="verificationCode" class="verification-wd-input"
placeholder="请输入验证码" maxlength="6" no-border clearable>
<wd-input
ref="verificationCodeInputRef"
v-model="verificationCode"
class="verification-wd-input"
placeholder="请输入验证码"
maxlength="6"
no-border
clearable
>
<template #suffix>
<wd-button size="small" type="primary" plain :disabled="isCountingDown || !isPhoneNumberValid"
@click="sendVerificationCode">
<wd-button
size="small"
type="primary"
plain
:disabled="isCountingDown || !isPhoneNumberValid"
@click="sendVerificationCode"
>
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
</wd-button>
</template>
@@ -199,8 +236,15 @@ function toPrivacyPolicy() {
<text class="form-label">
密码
</text>
<wd-input v-model="password" class="phone-wd-input" type="text" show-password placeholder="请输入密码" no-border
clearable />
<wd-input
v-model="password"
class="phone-wd-input"
type="text"
show-password
placeholder="请输入密码"
no-border
clearable
/>
</view>
<view class="flex items-center justify-end py-1">

View File

@@ -15,25 +15,14 @@ const props = defineProps({
required: true,
},
})
const { isWeChat } = useEnv()
const isDev = import.meta.env.DEV
/** APP 原生端同时展示微信与支付宝;其它端沿用微信内仅微信、否则仅支付宝 */
const isAppClient = computed(() => {
try {
return uni.getSystemInfoSync().uniPlatform === 'app'
}
catch {
return false
}
})
const appName = import.meta.env.VITE_APP_NAME || 'App'
const appLogo = '/static/images/logo.png'
const wechatPayIcon = '/static/images/wechatpay.svg'
const alipayIcon = '/static/images/alipay.svg'
const show = defineModel()
const selectedPaymentMethod = ref(isWeChat.value ? 'wechat' : 'alipay')
const selectedPaymentMethod = ref('alipay')
const paymentDisplayTime = ref('')
function toFiniteNumber(value) {
@@ -61,12 +50,6 @@ const displayAmount = computed(() => {
return payableAmount.value.toFixed(2)
})
const displayDiscountAmount = computed(() => {
if (payableAmount.value === null)
return '--'
return (payableAmount.value * 0.2).toFixed(2)
})
/** 支付弹窗展示用YYYY-MM-DD HH:mm:ss */
function formatPaymentTime(d = new Date()) {
const pad = n => String(n).padStart(2, '0')
@@ -84,11 +67,7 @@ function showToast(options) {
}
function setDefaultPaymentMethod() {
if (isAppClient.value) {
selectedPaymentMethod.value = 'alipay'
return
}
selectedPaymentMethod.value = isWeChat.value ? 'wechat' : 'alipay'
selectedPaymentMethod.value = 'alipay'
}
onMounted(setDefaultPaymentMethod)
@@ -101,7 +80,6 @@ watch(show, (v) => {
})
const router = useRouter()
const discountPrice = ref(false)
/** APP 端 wxpay服务端返回的 prepay_data 对象,供 uni.requestPayment 使用 */
function normalizeWxAppOrderInfo(raw) {
@@ -129,103 +107,54 @@ async function getPayment() {
const prepayData = respData.prepay_data
const orderNoFromResp = respData.order_no
if (prepayId === 'test_payment_success') {
if (selectedPaymentMethod.value === 'alipay' || selectedPaymentMethod.value === 'wechat') {
showToast({ message: '支付参数异常,请重试', type: 'fail' })
return
}
show.value = false
router.push({
path: '/payment/result',
query: { orderNo: orderNoFromResp },
})
return
}
// APP 原生:支付宝 / 微信uni.requestPayment
if (isAppClient.value) {
if (selectedPaymentMethod.value === 'alipay') {
if (!prepayId || typeof prepayId !== 'string') {
showToast({ message: '支付宝下单参数异常', type: 'fail' })
return
}
uni.requestPayment({
provider: 'alipay',
orderInfo: prepayId,
success: () => {
show.value = false
router.push({
path: '/payment/result',
query: { orderNo: orderNoFromResp },
})
},
fail: (e) => {
const msg = (e && (e.errMsg || e.message)) || '支付未完成'
showToast({ message: String(msg), type: 'fail' })
},
})
return
}
if (selectedPaymentMethod.value === 'wechat') {
const orderInfo = normalizeWxAppOrderInfo(prepayData)
if (!orderInfo) {
showToast({ message: '微信支付参数异常', type: 'fail' })
return
}
uni.requestPayment({
provider: 'wxpay',
orderInfo,
success: () => {
show.value = false
router.push({
path: '/payment/result',
query: { orderNo: orderNoFromResp },
})
},
fail: (e) => {
const msg = (e && (e.errMsg || e.message)) || '支付未完成'
showToast({ message: String(msg), type: 'fail' })
},
})
return
}
}
if (selectedPaymentMethod.value === 'alipay') {
if (typeof document === 'undefined') {
showToast({ message: '当前环境不支持网页支付宝支付', type: 'fail' })
if (!prepayId || typeof prepayId !== 'string') {
showToast({ message: '支付宝下单参数异常', type: 'fail' })
return
}
const prepayUrl = prepayId
const paymentForm = document.createElement('form')
paymentForm.method = 'POST'
paymentForm.action = prepayUrl
paymentForm.style.display = 'none'
document.body.appendChild(paymentForm)
paymentForm.submit()
show.value = false
return
}
const payload = prepayData
if (typeof WeixinJSBridge === 'undefined') {
showToast({ message: '请在微信内打开以完成支付', type: 'fail' })
return
}
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
payload,
(res) => {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
uni.requestPayment({
provider: 'alipay',
orderInfo: prepayId,
success: () => {
show.value = false
router.push({
path: '/payment/result',
query: { orderNo: orderNoFromResp },
})
}
},
)
},
fail: (e) => {
const msg = (e && (e.errMsg || e.message)) || '支付未完成'
showToast({ message: String(msg), type: 'fail' })
},
})
return
}
if (selectedPaymentMethod.value === 'wechat') {
const orderInfo = normalizeWxAppOrderInfo(prepayData)
if (!orderInfo) {
showToast({ message: '微信支付参数异常', type: 'fail' })
return
}
uni.requestPayment({
provider: 'wxpay',
orderInfo,
success: () => {
show.value = false
router.push({
path: '/payment/result',
query: { orderNo: orderNoFromResp },
})
},
fail: (e) => {
const msg = (e && (e.errMsg || e.message)) || '支付未完成'
showToast({ message: String(msg), type: 'fail' })
},
})
return
}
showToast({ message: '支付方式不支持', type: 'fail' })
}
function onCancel() {
@@ -234,14 +163,8 @@ function onCancel() {
</script>
<template>
<wd-popup
v-model="show"
round
position="bottom"
:safe-area-inset-bottom="true"
:z-index="2000"
custom-style="max-height: 88vh;"
>
<wd-popup v-model="show" round position="bottom" :safe-area-inset-bottom="true" :z-index="2000"
custom-style="max-height: 88vh;">
<view class="payment-popup">
<view class="payment-popup__header">
<text class="payment-popup__title">
@@ -280,21 +203,11 @@ function onCancel() {
应付金额
</view>
<view class="payment-popup__amount-price">
<view v-if="discountPrice" class="payment-popup__strike">
¥ {{ displayAmount }}
</view>
<view>
¥
{{
discountPrice
? displayDiscountAmount
: displayAmount
}}
{{ displayAmount }}
</view>
</view>
<view v-if="discountPrice" class="payment-popup__discount-tip">
活动价2折优惠
</view>
</view>
<view class="payment-popup__methods">
@@ -302,15 +215,15 @@ function onCancel() {
支付方式
</text>
<wd-radio-group v-model="selectedPaymentMethod" shape="dot" cell class="payment-radio-group">
<wd-radio v-if="isAppClient || isWeChat" value="wechat">
<!-- <wd-radio value="wechat">
<view class="payment-radio-row">
<image class="payment-radio-row__pay-icon" :src="wechatPayIcon" mode="aspectFit" />
<text>
微信支付
</text>
</view>
</wd-radio>
<wd-radio v-if="isAppClient || !isWeChat" value="alipay">
</wd-radio> -->
<wd-radio value="alipay">
<view class="payment-radio-row">
<image class="payment-radio-row__pay-icon" :src="alipayIcon" mode="aspectFit" />
<text>
@@ -318,23 +231,13 @@ function onCancel() {
</text>
</view>
</wd-radio>
<wd-radio v-if="isDev" value="test">
<view class="payment-radio-row">
<wd-icon size="24" name="description" color="#ff976a" class="payment-radio-row__icon" />
<text>
开发环境测试支付
</text>
</view>
</wd-radio>
</wd-radio-group>
</view>
<view class="payment-popup__actions">
<!-- eslint-disable-next-line unocss/order-attributify -- 组件属性 block UnoCSS -->
<wd-button block round size="large" type="primary" custom-class="payment-btn-primary" @click="getPayment">
确认支付
</wd-button>
<!-- eslint-disable-next-line unocss/order-attributify -- 组件属性 block UnoCSS -->
<wd-button block round size="small" type="text" custom-class="payment-btn-cancel" @click="onCancel">
取消
</wd-button>
@@ -433,19 +336,6 @@ function onCancel() {
color: #ee0a24;
}
.payment-popup__strike {
margin-bottom: 8rpx;
font-size: 28rpx;
color: #969799;
text-decoration: line-through;
}
.payment-popup__discount-tip {
margin-top: 8rpx;
font-size: 24rpx;
color: #ee0a24;
}
.payment-popup__section-label {
display: block;
margin-bottom: 16rpx;

View File

@@ -62,6 +62,9 @@ const promotionRevenue = computed(() => {
return safeTruncate(price.value - costPrice.value)
})
/** APP 端 placeholder 需用原生 style否则字号易偏小 */
const PRICE_PLACEHOLDER_STYLE = 'color:#9ca3af;font-size:40rpx;font-weight:500;'
// 价格校验与修正逻辑
function validatePrice(currentPrice) {
if (!productConfig.value) {
@@ -154,24 +157,20 @@ function onBlurPrice() {
</view>
<view class="card m-4">
<view class="flex items-center justify-between">
<view class="text-lg">
<view class="text-lg font-semibold text-gray-800">
客户查询价 ()
</view>
</view>
<view class="price-input-wrap border border-orange-100 rounded-xl bg-orange-50/40 px-2">
<wd-input
v-model="price"
type="number"
label="¥"
size="large"
label-width="34"
no-border
custom-input-class="price-input-inner"
custom-label-class="price-input-label"
<view class="price-input-surface">
<!-- 不用 label 而用 prefix避免 is-cell 下金额区与 被拉成分栏靠右 -->
<wd-input v-model="price" custom-class="price-wd-input" type="number" size="large" no-border
:placeholder="`${productConfig?.price_range_min || 0} - ${productConfig?.price_range_max || 0}`"
@blur="onBlurPrice"
/>
:placeholder-style="PRICE_PLACEHOLDER_STYLE" custom-input-class="price-input-inner" @blur="onBlurPrice">
<template #prefix>
<text class="price-currency"></text>
</template>
</wd-input>
</view>
<view class="mt-2 flex items-center justify-between">
<view>
@@ -222,27 +221,64 @@ function onBlurPrice() {
</template>
<style lang="scss" scoped>
.price-input-wrap {
box-shadow: inset 0 0 0 1px rgba(251, 146, 60, 0.08);
/* 可点击区域:白底+粗边框+阴影APP 上边界更清晰 */
.price-input-surface {
margin-top: 12px;
padding: 24rpx 20rpx;
min-height: 120rpx;
border: 2px solid #fb923c;
border-radius: 20rpx;
background: #fff;
box-shadow: 0 2px 12px rgba(251, 146, 60, 0.12);
}
.price-input-wrap :deep(.price-input-label) {
.price-input-surface :deep(.price-wd-input.wd-input) {
background: transparent;
padding: 0;
}
.price-input-surface :deep(.wd-input__body) {
width: 100%;
padding: 0;
}
/* prefix + 输入同一行,金额紧贴 ¥ 左侧起笔 */
.price-input-surface :deep(.wd-input__value) {
display: flex;
flex-direction: row;
align-items: center;
min-height: 96rpx;
width: 100%;
flex: 1;
}
.price-input-surface :deep(.wd-input__prefix) {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
margin-right: 0;
padding-right: 6rpx;
}
.price-currency {
color: #ea580c;
font-size: 22px;
font-size: 64rpx;
font-weight: 700;
line-height: 1;
}
.price-input-wrap :deep(.price-input-inner) {
height: 56px;
font-size: 40px;
/* 金额:与 ¥ 同档字号 */
.price-input-surface :deep(.wd-input__inner),
.price-input-surface :deep(input.wd-input__inner),
.price-input-surface :deep(.price-input-inner) {
flex: 1;
min-width: 0;
min-height: 88rpx;
font-size: 64rpx !important;
font-weight: 700;
color: #111827;
line-height: 56px;
text-align: left !important;
}
.price-input-wrap :deep(.wd-input__placeholder) {
font-size: 20px;
color: #111827 !important;
line-height: 1.15 !important;
text-align: left !important;
}
</style>

View File

@@ -131,14 +131,14 @@ function scaleQrRectForPoster(position, posterInfo, modeKey) {
// QR码位置配置为每个海报单独配置
const qrCodePositions = ref({
promote: [
{ x: 405, y: 1620, size: 480 }, // tg_qrcode_1.png
{ x: 405, y: 1620, size: 480 }, // tg_qrcode_2.jpg
{ x: 405, y: 1620, size: 480 }, // tg_qrcode_3.jpg
{ x: 405, y: 1620, size: 480 }, // tg_qrcode_4.jpg
{ x: 405, y: 1620, size: 480 }, // tg_qrcode_5.jpg
{ x: 180, y: 1440, size: 300 }, // tg_qrcode_6.jpg
{ x: 255, y: 940, size: 250 }, // tg_qrcode_7.jpg
{ x: 255, y: 940, size: 250 }, // tg_qrcode_8.jpg
{ x: 405, y: 1270, size: 440 }, // tg_qrcode_1.png
{ x: 405, y: 1270, size: 440 }, // tg_qrcode_2.jpg
{ x: 405, y: 1270, size: 440 }, // tg_qrcode_3.jpg
{ x: 405, y: 1270, size: 440 }, // tg_qrcode_4.jpg
{ x: 405, y: 1270, size: 440 }, // tg_qrcode_5.jpg
{ x: 210, y: 1660, size: 340 }, // tg_qrcode_6.jpg
{ x: 405, y: 1270, size: 440 }, // tg_qrcode_7.jpg
{ x: 405, y: 1270, size: 440 }, // tg_qrcode_8.jpg
],
// invitation模式的配置 (yq_qrcode)
invitation: [
@@ -784,37 +784,28 @@ export default {
<!-- 放在弹窗外避免 wd-popup 关闭时子树未挂载导致 APP 无法 createCanvasContext -->
<canvas v-if="!isWebPlatform" :id="QR_GEN_CANVAS_ID" :canvas-id="QR_GEN_CANVAS_ID" class="poster-qr-gen-canvas" />
<!-- App仅作 renderjs change 锚点真正画布在 renderjs createElement('canvas') -->
<view
v-if="!isWebPlatform" class="poster-renderjs-anchor" :poster-merge-req="posterMergeRequest"
:change:poster-merge-req="posterRender.onMergeReqChange" aria-hidden="true"
/>
<view v-if="!isWebPlatform" class="poster-renderjs-anchor" :poster-merge-req="posterMergeRequest"
:change:poster-merge-req="posterRender.onMergeReqChange" aria-hidden="true" />
<wd-popup v-model="show" round position="bottom" :style="{ maxHeight: '95vh' }">
<view class="qrcode-popup-container">
<view class="qrcode-content">
<swiper
class="poster-swiper rounded-lg shadow sm:rounded-xl" indicator-color="white" circular indicator-dots
@change="onSwipeChange"
>
<swiper class="poster-swiper rounded-lg shadow sm:rounded-xl" indicator-color="white" circular indicator-dots
@change="onSwipeChange">
<swiper-item v-for="(_, index) in posterImages" :key="index" class="poster-swiper-item">
<view class="poster-canvas-box">
<template v-if="isWebPlatform">
<view class="poster-preview-box web-preview">
<canvas
:id="getCanvasId(index)" :ref="(el) => (posterCanvasRefs[index] = el)"
<canvas :id="getCanvasId(index)" :ref="(el) => (posterCanvasRefs[index] = el)"
:canvas-id="getCanvasId(index)" :width="posterCanvasSizes[index]?.width ?? 300"
:height="posterCanvasSizes[index]?.height ?? 300"
:style="posterCanvasContainStyle(index)"
class="poster-canvas poster-canvas--h5-contain rounded-lg sm:rounded-xl"
/>
:height="posterCanvasSizes[index]?.height ?? 300" :style="posterCanvasContainStyle(index)"
class="poster-canvas poster-canvas--h5-contain rounded-lg sm:rounded-xl" />
</view>
</template>
<template v-else>
<!-- App预览与保存同源aspectFit 在轮播高度内整图可见等比宽可不拉满 -->
<view v-if="posterCompositePath[index]" class="poster-app-preview">
<image
:src="posterCompositePath[index]" mode="aspectFit"
class="poster-app-composite rounded-lg sm:rounded-xl"
/>
<image :src="posterCompositePath[index]" mode="aspectFit"
class="poster-app-composite rounded-lg sm:rounded-xl" />
</view>
<view v-else class="poster-preview-placeholder">
<text class="poster-preview-placeholder-text">
@@ -874,10 +865,8 @@ export default {
</wd-popup>
<!-- 图片保存指引遮罩层 -->
<ImageSaveGuide
:show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
@close="closeImageGuide"
/>
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
@close="closeImageGuide" />
</template>
<style lang="scss" scoped>
@@ -943,7 +932,7 @@ export default {
box-sizing: border-box;
}
.poster-canvas-box > view {
.poster-canvas-box>view {
flex: 1;
min-width: 0;
min-height: 0;
@@ -1085,13 +1074,13 @@ export default {
}
}
/* 优化 van-divider 在小屏幕上的间距 */
:deep(.van-divider) {
/* 优化 wd-divider 在小屏幕上的间距 */
:deep(.wd-divider) {
margin: 0.5rem 0;
}
@media (min-width: 640px) {
:deep(.van-divider) {
:deep(.wd-divider) {
margin: 0.75rem 0;
}
}

View File

@@ -33,7 +33,7 @@ const isPhoneNumberValid = computed(() => {
})
const isIdCardValid = computed(() => {
return /^(?:\d{15}|\d{17}[\dXx])$/.test(idCard.value)
return /^(?:\d{15}|\d{17}[\dX])$/i.test(idCard.value)
})
const isRealNameValid = computed(() => {
@@ -172,22 +172,22 @@ onUnmounted(() => {
>
<view
class="real-name-auth-dialog"
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.9));"
style="background: linear-gradient(135deg, var(--color-primary-light), rgba(255,255,255,0.9));"
>
<view class="title-bar">
<view class="text-base font-bold sm:text-lg" style="color: var(--van-text-color);">
<view class="text-base font-bold sm:text-lg" style="color: var(--color-text-primary);">
实名认证
</view>
<wd-icon name="cross" class="close-icon" style="color: var(--van-text-color-2);" @click="closeDialog" />
<wd-icon name="cross" class="close-icon" style="color: var(--color-text-secondary);" @click="closeDialog" />
</view>
<view class="dialog-content">
<view class="dialog-inner px-4 pb-4 pt-2">
<view
class="auth-notice mb-4 rounded-xl p-3 sm:p-4"
style="background-color: var(--van-theme-primary-light); border: 1px solid rgba(162, 37, 37, 0.2);"
style="background-color: var(--color-primary-light); border: 1px solid rgba(162, 37, 37, 0.2);"
>
<view class="text-xs space-y-1.5 sm:text-sm sm:space-y-2" style="color: var(--van-text-color);">
<text class="font-medium" style="color: var(--van-theme-primary);">
<view class="text-xs space-y-1.5 sm:text-sm sm:space-y-2" style="color: var(--color-text-primary);">
<text class="font-medium" style="color: var(--color-primary);">
实名认证说明
</text>
<text>1. 实名认证是提现的必要条件</text>
@@ -320,7 +320,7 @@ onUnmounted(() => {
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--van-border-color);
border-bottom: 1px solid var(--color-border-primary);
flex-shrink: 0;
}

View File

@@ -1,146 +0,0 @@
<script setup>
import { ref } from 'vue'
const props = defineProps({
orderId: {
type: String,
default: '',
},
orderNo: {
type: String,
default: '',
},
isExample: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
})
const isLoading = ref(false)
function showToast(options) {
const message = typeof options === 'string' ? options : (options?.message || options?.title || '')
if (!message)
return
uni.showToast({
title: message,
icon: options?.type === 'success' ? 'success' : 'none',
})
}
async function copyToClipboard(text) {
await navigator.clipboard.writeText(text)
showToast({
type: 'success',
message: '链接已复制到剪贴板',
position: 'bottom',
})
}
async function handleShare() {
if (isLoading.value || props.disabled)
return
// 如果是示例模式直接分享当前URL
if (props.isExample) {
try {
const currentUrl = window.location.href
await copyToClipboard(currentUrl)
showToast({
type: 'success',
message: '示例链接已复制到剪贴板',
position: 'bottom',
})
}
catch {
showToast({
type: 'fail',
message: '复制链接失败',
position: 'bottom',
})
}
return
}
// 优先使用 orderId如果没有则使用 orderNo
const orderIdentifier = props.orderId || props.orderNo
if (!orderIdentifier) {
showToast({
type: 'fail',
message: '缺少订单标识',
position: 'bottom',
})
return
}
isLoading.value = true
try {
// 根据实际使用的标识构建请求参数
const requestData = props.orderId
? { order_id: Number.parseInt(props.orderId) }
: { order_no: props.orderNo }
const { data, error } = await useApiFetch('/query/generate_share_link')
.post(requestData)
.json()
if (error.value) {
throw new Error(error.value)
}
if (data.value?.code === 200 && data.value.data?.share_link) {
const baseUrl = window.location.origin
const linkId = encodeURIComponent(data.value.data.share_link)
const fullShareUrl = `${baseUrl}/report/share/${linkId}`
try {
const { confirm } = await uni.showModal({
title: '分享链接已生成',
content: '链接将在7天后过期是否复制到剪贴板',
confirmText: '复制链接',
cancelText: '取消',
})
if (!confirm)
return
await copyToClipboard(fullShareUrl)
}
catch (dialogErr) {
console.error(dialogErr)
}
}
else {
throw new Error(data.value?.message || '生成分享链接失败')
}
}
catch (err) {
showToast({
type: 'fail',
message: err.message || '生成分享链接失败',
position: 'bottom',
})
}
finally {
isLoading.value = false
}
}
</script>
<template>
<view
class="bg-primary border-primary hover:bg-primary-600 flex cursor-pointer items-center justify-center border rounded-[40px] px-3 py-1 transition-colors duration-200"
:class="{ 'opacity-50 cursor-not-allowed': isLoading || disabled }" @click="handleShare"
>
<image src="/static/images/report/fx.png" alt="分享" class="mr-1 h-4 w-4" />
<text class="text-sm text-white font-medium">
{{ isLoading ? "生成中..." : (isExample ? "分享示例" : "分享报告") }}
</text>
</view>
</template>
<style lang="scss" scoped>
/* 样式已通过 Tailwind CSS 类实现 */
</style>

View File

@@ -1,44 +0,0 @@
<script setup>
// 透传所有属性和事件到 van-tabs
defineOptions({
inheritAttrs: false,
})
</script>
<template>
<wd-tabs v-bind="$attrs" type="card" class="styled-tabs">
<slot />
</wd-tabs>
</template>
<style scoped>
/* van-tabs 卡片样式定制 - 仅用于此组件 */
.styled-tabs:deep(.van-tabs__line) {
background-color: var(--van-theme-primary) !important;
}
.styled-tabs:deep(.van-tabs__nav) {
gap: 10px;
}
.styled-tabs:deep(.van-tabs__nav--card) {
border: unset !important;
}
.styled-tabs:deep(.van-tab--card) {
color: #666666 !important;
border-right: unset !important;
background-color: #eeeeee !important;
border-radius: 8px !important;
}
.styled-tabs:deep(.van-tab--active) {
color: white !important;
background-color: var(--van-theme-primary) !important;
}
.styled-tabs:deep(.van-tabs__wrap) {
background-color: #ffffff !important;
padding: 9px 0;
}
</style>

View File

@@ -1,23 +0,0 @@
<script setup>
// 不需要额外的 props 或逻辑,只是一个简单的样式组件
</script>
<template>
<view class="title-banner">
<slot />
</view>
</template>
<style scoped>
.title-banner {
@apply mx-auto mt-2 w-64 py-1.5 text-center text-white font-bold text-lg relative rounded-2xl;
background: var(--color-primary);
border: 1px solid var(--color-primary-300);
background-image:
linear-gradient(45deg, transparent 25%, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.1) 50%, transparent 50%, transparent 75%, rgba(255, 255, 255, 0.1) 75%);
background-size: 20px 20px;
background-position: 0 0;
position: relative;
overflow: hidden;
}
</style>

View File

@@ -1,187 +0,0 @@
<script setup>
import LTitle from './LTitle.vue'
import ShareReportButton from './ShareReportButton.vue'
const props = defineProps({
reportParams: {
type: Object,
required: true,
},
reportDateTime: {
type: [String, null],
required: false,
default: null,
},
reportName: {
type: String,
required: true,
},
isEmpty: {
type: Boolean,
required: true,
},
isShare: {
type: Boolean,
required: false,
default: false,
},
orderId: {
type: [String, Number],
required: false,
default: null,
},
orderNo: {
type: String,
required: false,
default: null,
},
})
// 脱敏函数
function maskValue(type, value) {
if (!value)
return value
if (type === 'name') {
// 姓名脱敏(保留首位)
if (value.length === 1) {
return '*' // 只保留一个字,返回 "*"
}
else if (value.length === 2) {
return `${value[0]}*` // 两个字,保留姓氏,第二个字用 "*" 替代
}
else {
return (
value[0]
+ '*'.repeat(value.length - 2)
+ value[value.length - 1]
) // 两个字以上,保留第一个和最后一个字,其余的用 "*" 替代
}
}
else if (type === 'id_card') {
// 身份证号脱敏保留前6位和最后4位
return value.replace(/^(.{6})\d+(.{4})$/, '$1****$2')
}
else if (type === 'mobile') {
if (value.length === 11) {
return `${value.substring(0, 3)}****${value.substring(7)}`
}
return value // 如果手机号不合法或长度不为 11 位,直接返回原手机号
}
return value
}
</script>
<template>
<view class="card" style="padding-left: 0; padding-right: 0; padding-bottom: 24px;">
<view class="flex flex-col gap-y-2">
<!-- 报告信息 -->
<view class="flex items-center justify-between py-2">
<LTitle title="报告信息" />
<!-- 分享按钮 -->
<ShareReportButton
v-if="!isShare" :order-id="orderId" :order-no="orderNo" :is-example="!orderId"
class="mr-4"
/>
</view>
<view class="mx-4 my-2 flex flex-col gap-2">
<view class="flex pb-2 pl-2">
<text class="w-[6em] text-[#666666]">报告时间</text>
<text class="text-gray-600">{{
reportDateTime
|| "2025-01-01 12:00:00"
}}</text>
</view>
<view v-if="!isEmpty" class="flex pb-2 pl-2">
<text class="w-[6em] text-[#666666]">报告项目</text>
<text class="text-gray-600 font-bold">
{{ reportName }}</text>
</view>
</view>
<!-- 报告对象 -->
<template v-if="Object.keys(reportParams).length != 0">
<LTitle title="报告对象" />
<view class="mx-4 my-2 flex flex-col gap-2">
<!-- 姓名 -->
<view v-if="reportParams?.name" class="flex pb-2 pl-2">
<text class="w-[6em] text-[#666666]">姓名</text>
<text class="text-gray-600">{{
maskValue(
"name",
reportParams?.name,
)
}}</text>
</view>
<!-- 身份证号 -->
<view v-if="reportParams?.id_card" class="flex pb-2 pl-2">
<text class="w-[6em] text-[#666666]">身份证号</text>
<text class="text-gray-600">{{
maskValue(
"id_card",
reportParams?.id_card,
)
}}</text>
</view>
<!-- 手机号 -->
<view v-if="reportParams?.mobile" class="flex pb-2 pl-2">
<text class="w-[6em] text-[#666666]">手机号</text>
<text class="text-gray-600">{{
maskValue(
"mobile",
reportParams?.mobile,
)
}}</text>
</view>
<!-- 验证卡片 -->
<view class="mt-4 flex flex-col gap-4">
<!-- 身份证检查结果 -->
<view class="flex flex-1 items-center border border-[#EEEEEE] rounded-lg bg-[#F9F9F9] px-4 py-3">
<view class="mr-4 h-11 w-11 flex items-center justify-center">
<image src="/static/images/report/sfz.png" alt="身份证" class="h-10 w-10 object-contain" />
</view>
<view class="flex-1">
<view class="text-lg text-gray-800 font-bold">
身份证检查结果
</view>
<view class="text-sm text-[#999999]">
身份证信息核验通过
</view>
</view>
<view class="ml-4 h-11 w-11 flex items-center justify-center">
<image src="/static/images/report/zq.png" alt="资金安全" class="h-10 w-10 object-contain" />
</view>
</view>
<!-- 手机号检测结果 -->
<!-- <view class="flex items-center px-4 py-3 flex-1 border border-[#EEEEEE] rounded-lg bg-[#F9F9F9]">
<view class="w-11 h-11 flex items-center justify-center mr-4">
<image src="/static/images/report/sjh.png" alt="手机号" class="w-10 h-10 object-contain" />
</view>
<view class="flex-1">
<view class="font-bold text-gray-800 text-lg">
手机号检测结果
</view>
<view class="text-sm text-[#999999]">
被查询人姓名与运营商提供的一致
</view>
<view class="text-sm text-[#999999]">
被查询人身份证与运营商提供的一致
</view>
</view>
<view class="w-11 h-11 flex items-center justify-center ml-4">
<image src="/static/images/report/zq.png" alt="资金安全" class="w-10 h-10 object-contain" />
</view>
</view> -->
</view>
</view>
</template>
</view>
</view>
</template>
<style scoped>
/* 组件样式已通过 Tailwind CSS 类实现 */
</style>

View File

@@ -78,11 +78,6 @@ export function resolveWebToUni(to: string | { name?: string, path?: string, que
const base = `/pages/invitation-agent-apply?linkIdentifier=${encodeURIComponent(id)}`
return queryPart ? `${base}&${queryPart}` : base
}
if (pathOnly.startsWith('/agent/promotionInquire/')) {
const id = pathOnly.replace(/^\/agent\/promotionInquire\//, '')
const base = `/pages/promotion-inquire?linkIdentifier=${encodeURIComponent(id)}`
return queryPart ? `${base}&${queryPart}` : base
}
if (pathOnly.startsWith('/report/share/')) {
const id = pathOnly.replace(/^\/report\/share\//, '')
const base = `/pages/report-share?linkIdentifier=${encodeURIComponent(id)}`
@@ -203,8 +198,6 @@ export function getWebPathForNotification(): string {
return `/agent/subordinateDetail/${q.id}`
if (r === 'pages/invitation-agent-apply' && q.linkIdentifier)
return `/agent/invitationAgentApply/${q.linkIdentifier}`
if (r === 'pages/promotion-inquire' && q.linkIdentifier)
return `/agent/promotionInquire/${q.linkIdentifier}`
if (r === 'pages/report-share' && q.linkIdentifier)
return `/report/share/${q.linkIdentifier}`
return UNI_TO_WEB_NOTIFY_PATH[r] || '/'

View File

@@ -4,6 +4,7 @@
*/
import type { Ref } from 'vue'
import { ref } from 'vue'
import { hasAcceptedPrivacyPolicy } from '@/composables/usePrivacyConsent'
import { envConfig } from '@/constants/env'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
@@ -16,6 +17,11 @@ export interface ApiEnvelope<T = unknown> {
data: T
}
/** `silent: true` 不弹出全屏 `uni.showLoading`(首屏/Tab/列表/轮询等用页面内态即可) */
export interface UseApiFetchOptions {
silent?: boolean
}
let loadingCount = 0
function showLoading() {
loadingCount++
@@ -90,6 +96,10 @@ function uniRequest<T>(
data?: unknown,
): Promise<{ statusCode: number, data: ApiEnvelope<T> }> {
return new Promise((resolve, reject) => {
if (!hasAcceptedPrivacyPolicy()) {
reject(new Error('用户未同意隐私政策,禁止请求'))
return
}
const token = getToken()
uni.request({
url: joinApiUrl(url),
@@ -116,14 +126,17 @@ function uniRequest<T>(
async function executeJson<T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
url: string,
data?: unknown,
data: unknown,
silent: boolean,
): Promise<{ data: Ref<ApiEnvelope<T> | null>, error: Ref<Error | null> }> {
const dataRef = ref<ApiEnvelope<T> | null>(null) as Ref<ApiEnvelope<T> | null>
const errorRef = ref<Error | null>(null)
showLoading()
if (!silent)
showLoading()
try {
const { statusCode, data: body } = await uniRequest<T>(method, url, data)
hideLoading()
if (!silent)
hideLoading()
if (statusCode === 401) {
clearAuthStorage()
@@ -158,7 +171,8 @@ async function executeJson<T>(
return { data: dataRef, error: errorRef }
}
catch (e) {
hideLoading()
if (!silent)
hideLoading()
const err = e instanceof Error ? e : new Error(String(e))
errorRef.value = err
uni.showToast({ title: '网络异常,请稍后再试', icon: 'none' })
@@ -166,31 +180,32 @@ async function executeJson<T>(
}
}
function chain(url: string) {
function chain(url: string, options?: UseApiFetchOptions) {
const silent = options?.silent === true
return {
get() {
return {
json: <T>() => executeJson<T>('GET', url),
json: <T>() => executeJson<T>('GET', url, undefined, silent),
}
},
post(data?: unknown) {
return {
json: <T>() => executeJson<T>('POST', url, data),
json: <T>() => executeJson<T>('POST', url, data, silent),
}
},
put(data?: unknown) {
return {
json: <T>() => executeJson<T>('PUT', url, data),
json: <T>() => executeJson<T>('PUT', url, data, silent),
}
},
delete() {
return {
json: <T>() => executeJson<T>('DELETE', url),
json: <T>() => executeJson<T>('DELETE', url, undefined, silent),
}
},
}
}
export default function useApiFetch(url: string) {
return chain(url)
export default function useApiFetch(url: string, options?: UseApiFetchOptions) {
return chain(url, options)
}

View File

@@ -0,0 +1,57 @@
import { ref } from 'vue'
import useApiFetch from '@/composables/useApiFetch'
const DEFAULT_RETENTION_DAYS = 30
export interface AppConfigPayload {
query: {
retention_days: number
}
}
const appConfig = ref<AppConfigPayload>({
query: {
retention_days: DEFAULT_RETENTION_DAYS,
},
})
let loadingPromise: Promise<AppConfigPayload> | null = null
function normalizeRetentionDays(value: unknown): number {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
return DEFAULT_RETENTION_DAYS
return Math.floor(value)
}
export function useAppConfig() {
async function loadAppConfig(force = false): Promise<AppConfigPayload> {
if (!force && loadingPromise)
return loadingPromise
loadingPromise = (async () => {
const { data, error } = await useApiFetch('/app/config', { silent: true }).get().json<AppConfigPayload>()
if (error.value || data.value?.code !== 200 || !data.value?.data) {
return appConfig.value
}
const payload = data.value.data
appConfig.value = {
query: {
retention_days: normalizeRetentionDays(payload.query?.retention_days),
},
}
return appConfig.value
})()
try {
return await loadingPromise
}
finally {
loadingPromise = null
}
}
return {
appConfig,
loadAppConfig,
}
}

View File

@@ -1,11 +0,0 @@
import { getToken } from '@/utils/storage'
export function useAuthGuard() {
const ensureLogin = () => {
if (getToken())
return true
uni.navigateTo({ url: '/pages/login' })
return false
}
return { ensureLogin }
}

View File

@@ -1,16 +0,0 @@
export function useCount() {
const count = ref(Math.round(Math.random() * 20))
function inc() {
count.value += 1
}
function dec() {
count.value -= 1
}
return {
count,
inc,
dec,
}
}

View File

@@ -1,10 +0,0 @@
import { ref } from 'vue'
/** App 端固定非微信 H5保留 isWeChat 以兼容迁移代码分支 */
const isWeChat = ref(false)
export function useEnv() {
return {
isWeChat,
}
}

View File

@@ -1,27 +0,0 @@
import { createFetch, useFetch } from '@vueuse/core'
export function useHttp(url, options = {}, token) {
const fetch = createFetch(url, {
baseUrl: '/api/v1',
options: {
async beforeFetch({ url, options, cancel }) {
console.log('asdasd', options)
options.headers = {
...options.headers,
Authorization: `${token}`,
}
return {
options,
}
},
async afterFetch(ctx) {
console.log('ctx', ctx)
// if (ctx.data.code !== 200) {
// throw new Error(ctx.data.message || '请求失败');
// }
return ctx
},
},
})
return fetch(url)
}

View File

@@ -0,0 +1,108 @@
const PRIVACY_DECISION_KEY = 'privacyDecision'
const PRIVACY_CONSENT_PAGE = '/pages/privacy-consent'
type PrivacyDecision = 'accepted' | 'rejected'
let privacyNavGuardInstalled = false
let privacyRequestGuardInstalled = false
function normalizeRoute(url = '') {
return String(url).replace(/^\//, '').split('?')[0]
}
function hasDecision() {
return Boolean(getPrivacyDecision())
}
export function getPrivacyConsentPageUrl() {
return PRIVACY_CONSENT_PAGE
}
export function getPrivacyDecision(): PrivacyDecision | '' {
return (uni.getStorageSync(PRIVACY_DECISION_KEY) as PrivacyDecision | '') || ''
}
export function hasAcceptedPrivacyPolicy() {
return getPrivacyDecision() === 'accepted'
}
export function setPrivacyDecision(decision: PrivacyDecision) {
uni.setStorageSync(PRIVACY_DECISION_KEY, decision)
}
function shouldBlockNavigation(url = '') {
if (hasDecision())
return false
const route = normalizeRoute(url)
return route !== 'pages/privacy-consent' && route !== 'pages/privacy-policy'
}
function redirectToConsent() {
uni.redirectTo({ url: PRIVACY_CONSENT_PAGE })
}
export function installPrivacyNavigationGuard() {
if (privacyNavGuardInstalled)
return
privacyNavGuardInstalled = true
uni.addInterceptor('navigateTo', {
invoke(args) {
if (shouldBlockNavigation(args?.url)) {
redirectToConsent()
return false
}
return args
},
})
uni.addInterceptor('redirectTo', {
invoke(args) {
if (shouldBlockNavigation(args?.url)) {
redirectToConsent()
return false
}
return args
},
})
uni.addInterceptor('reLaunch', {
invoke(args) {
if (shouldBlockNavigation(args?.url)) {
redirectToConsent()
return false
}
return args
},
})
uni.addInterceptor('switchTab', {
invoke(args) {
if (!hasDecision()) {
redirectToConsent()
return false
}
return args
},
})
}
export function installPrivacyRequestGuard() {
if (privacyRequestGuardInstalled)
return
privacyRequestGuardInstalled = true
uni.addInterceptor('request', {
invoke(args) {
if (hasAcceptedPrivacyPolicy())
return args
uni.showToast({ title: '请先阅读并处理隐私政策', icon: 'none' })
return false
},
})
}
export function installPrivacyGuards() {
installPrivacyNavigationGuard()
installPrivacyRequestGuard()
}

View File

@@ -1,8 +0,0 @@
export function useQuery(key?: MaybeRefOrGetter<string>) {
const query = ref<AnyObject>({})
onLoad((q) => {
query.value = q || {}
})
const value = computed(() => (key ? query.value[toValue(key)] : null))
return { query, value }
}

View File

@@ -1,18 +0,0 @@
import { watch } from 'vue'
/**
* 风险评分通知 composable
* 用于组件向父组件通知自己的风险评分0-100分分数越高越安全
*/
export function useRiskNotifier(props, riskScore) {
// 监听 riskScore 变化,通知父组件
watch(
riskScore,
(newValue) => {
if (props.apiId && props.notifyRiskStatus) {
props.notifyRiskStatus(props.apiId, props.index, newValue)
}
},
{ immediate: true },
)
}

View File

@@ -1,138 +0,0 @@
import { onMounted, ref } from "vue";
import "@/static/uni-webview.js";
import { getToken, setToken } from '@/utils/storage'
const WEBVIEW_PLATFORM_KEY = 'webview_platform'
export function useWebView() {
const platform = ref("");
const token = ref("");
// 检测环境并通知父窗口加载完毕
const handleBridgeReady = () => {
if (platform.value) {
h5PostMessage("loaded", true);
}
};
// 获取 Token从 URL 中解析)
const getTokenFromUrl = () => {
const urlParams = new URLSearchParams(window.location.search);
const tokenFromUrl = urlParams.get("token");
token.value = tokenFromUrl || ""; // 如果 URL 没有 token返回空字符串
if (token.value) {
setToken(token.value);
} else {
token.value = getToken() || "";
}
return tokenFromUrl;
};
// 封装 postMessage 方法
const postMessage = (data) => {
if (platform.value === "h5") {
h5PostMessage("postMessage", data);
} else if (uni && uni.webView.postMessage) {
uni.webView.postMessage(data);
} else {
console.error("uni.webView.postMessage is not available.");
}
};
const redirectTo = (data) => {
if (platform.value === "h5") {
h5PostMessage("redirectTo", data);
} else if (uni && uni.webView.redirectTo) {
// 非 H5 环境,调用 uni.webView.redirectTo
uni.webView.redirectTo(data);
} else {
console.error("uni.webView.redirectTo is not available.");
}
};
// 封装 navigateBack 方法
const navigateBack = (data) => {
if (platform.value === "h5") {
window.top.history.back();
// h5PostMessage("navigateBack", data)
} else if (uni && uni.webView.navigateBack) {
// 非 H5 环境,调用 uni.webView.navigateBack
uni.webView.navigateBack(data);
} else {
console.error("uni.webView.navigateBack is not available.");
}
};
// 封装 navigateTo 方法
const navigateTo = (data) => {
if (platform.value === "h5") {
// h5PostMessage("navigateTo", data)
window.top.location.href = `/app${data.url}`;
} else if (uni && uni.webView.navigateTo) {
uni.webView.navigateTo(data);
} else {
console.error("uni.webView.navigateTo is not available.");
}
};
const payment = (data) => {
if (platform.value === "h5") {
h5PostMessage("payment", data);
} else if (uni && uni.webView.navigateTo) {
// 非 H5 环境,调用 uni.webView.navigateTo
uni.webView.navigateTo(data);
} else {
console.error("uni.webView.navigateTo is not available.");
}
};
const getEnv = () => {
return new Promise((resolve, reject) => {
const env = uni.getStorageSync(WEBVIEW_PLATFORM_KEY);
if (env) {
platform.value = env;
resolve(env);
} else {
uni.webView.getEnv((env) => {
// 遍历 env 对象,找到值为 true 的键
const platformKey = Object.keys(env).find((key) => env[key] === true);
platform.value = platformKey;
if (platformKey) {
uni.setStorageSync(WEBVIEW_PLATFORM_KEY, platformKey);
resolve(platformKey); // 返回键名(如 'h5', 'mp-weixin' 等)
} else {
reject("未知平台");
}
});
}
});
};
onMounted(async () => {
try {
const envValue = await getEnv();
console.log("当前环境", envValue);
// 将返回的键名(如 'h5', 'mp-weixin')存储到 platform
handleBridgeReady();
} catch (error) {
console.error(error);
}
// 获取 Token
getTokenFromUrl();
});
return {
platform,
token,
getEnv,
redirectTo,
postMessage,
navigateTo,
navigateBack,
payment,
};
}
function h5PostMessage(action, data) {
window.parent.postMessage(
{ action, data, messageId: generateUniqueId(action) },
"*",
);
}
const generateUniqueId = (action) => `msg_${action}_${new Date().getTime()}`;

View File

@@ -1,8 +0,0 @@
/**
* App 端不启用微信 JSSDK 分享;占位以兼容 QRcode 等迁移组件。
*/
export function useWeixinShare() {
return {
configWeixinShare() {},
}
}

View File

@@ -1,34 +0,0 @@
import { onMounted, ref } from 'vue'
import zoomAdapter from '../utils/zoomAdapter.js'
/**
* 简化版缩放适配组合式函数
*/
export function useZoomAdapter() {
const currentZoom = ref(1)
const isTooHighZoom = ref(false)
const handleZoomChange = (event) => {
const { zoom } = event.detail
currentZoom.value = zoom
isTooHighZoom.value = zoom > 3
}
onMounted(() => {
if (typeof window === 'undefined')
return
if (!zoomAdapter.isInitialized) {
zoomAdapter.init()
}
window.addEventListener('zoomChanged', handleZoomChange)
})
return {
currentZoom,
isTooHighZoom,
getZoomAdaptiveClass: () => ({
'zoom-adaptive': true,
'too-high-zoom': isTooHighZoom.value,
}),
}
}

View File

@@ -1,44 +0,0 @@
export type MigrationMode = 'native' | 'webview'
export interface RouteMigrationItem {
webviewPath: string
appPath: string
mode: MigrationMode
feature: string
}
export const routeMigrationMap: RouteMigrationItem[] = [
{ webviewPath: '/', appPath: '/pages/index', mode: 'native', feature: '首页' },
{ webviewPath: '/login', appPath: '/pages/login', mode: 'native', feature: '登录' },
{ webviewPath: '/inquire/:feature', appPath: '/pages/inquire', mode: 'native', feature: '查询下单' },
{ webviewPath: '/historyQuery', appPath: '/pages/history-query', mode: 'native', feature: '历史报告' },
{ webviewPath: '/payment/result', appPath: '/pages/payment-result', mode: 'native', feature: '支付结果' },
{ webviewPath: '/agent', appPath: '/pages/agent', mode: 'native', feature: '代理中心' },
{ webviewPath: '/agent/promote', appPath: '/pages/promote', mode: 'native', feature: '推广管理' },
{ webviewPath: '/agent/promoteDetails', appPath: '/pages/agent-promote-details', mode: 'native', feature: '直推收益明细' },
{ webviewPath: '/agent/rewardsDetails', appPath: '/pages/agent-rewards-details', mode: 'native', feature: '奖励收益明细' },
{ webviewPath: '/agent/invitation', appPath: '/pages/invitation', mode: 'native', feature: '邀请下级' },
{ webviewPath: '/agent/vipApply', appPath: '/pages/agent-vip-apply', mode: 'native', feature: 'VIP申请' },
{ webviewPath: '/agent/vipConfig', appPath: '/pages/agent-vip-config', mode: 'native', feature: 'VIP配置' },
{ webviewPath: '/withdraw', appPath: '/pages/withdraw', mode: 'native', feature: '提现' },
{ webviewPath: '/agent/withdrawDetails', appPath: '/pages/withdraw-details', mode: 'native', feature: '提现记录' },
{ webviewPath: '/agent/subordinateList', appPath: '/pages/subordinate-list', mode: 'native', feature: '我的下级' },
{ webviewPath: '/agent/subordinateDetail/:id', appPath: '/pages/subordinate-detail', mode: 'native', feature: '下级详情' },
{ webviewPath: '/agent/invitationAgentApply/:linkIdentifier', appPath: '/pages/invitation-agent-apply', mode: 'native', feature: '代理申请' },
{ webviewPath: '/help', appPath: '/pages/help', mode: 'native', feature: '帮助中心' },
{ webviewPath: '/help/detail', appPath: '/pages/help-detail', mode: 'native', feature: '帮助详情' },
{ webviewPath: '/help/guide', appPath: '/pages/help-guide', mode: 'native', feature: '引导指南' },
{ webviewPath: '/authorization', appPath: '/pages/authorization', mode: 'webview', feature: '授权书' },
{ webviewPath: '/privacyPolicy', appPath: '/pages/privacy-policy', mode: 'webview', feature: '隐私政策' },
{ webviewPath: '/userAgreement', appPath: '/pages/user-agreement', mode: 'webview', feature: '用户协议' },
{ webviewPath: '/agentManageAgreement', appPath: '/pages/agent-manage-agreement', mode: 'webview', feature: '代理管理协议' },
{ webviewPath: '/agentSerivceAgreement', appPath: '/pages/agent-service-agreement', mode: 'webview', feature: '信息技术服务合同' },
{ webviewPath: '/me', appPath: '/pages/me', mode: 'native', feature: '我的' },
{ webviewPath: '/cancelAccount', appPath: '/pages/cancel-account', mode: 'native', feature: '注销账号' },
{ webviewPath: '/report/share/:linkIdentifier', appPath: '/pages/report-share', mode: 'webview', feature: '报告分享' },
{ webviewPath: '/agent/promotionInquire/:linkIdentifier', appPath: '/pages/promotion-inquire', mode: 'native', feature: '推广查询' },
{ webviewPath: '/:pathMatch(.*)*', appPath: '/pages/not-found', mode: 'native', feature: '404' },
{ webviewPath: '/maintenance', appPath: '/pages/maintenance', mode: 'native', feature: '维护页' },
{ webviewPath: '/app/example', appPath: '/pages/report-example-webview', mode: 'webview', feature: '示例报告' },
{ webviewPath: '/app/report', appPath: '/pages/report-result-webview', mode: 'webview', feature: '结果报告' },
]

File diff suppressed because it is too large Load Diff

View File

@@ -76,7 +76,7 @@ onShow(() => {
})
async function getGlobalNotify() {
const { data, error } = await useApiFetch('/notification/list').get().json()
const { data, error } = await useApiFetch('/notification/list', { silent: true }).get().json()
if (!data.value || error.value)
return
if (data.value.code !== 200)
@@ -152,8 +152,7 @@ onUnmounted(() => {
<slot />
<wd-popup v-model="showPopup" round>
<view class="popup-content p-8 text-center">
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="notify-html" v-html="currentNotify?.content" />
<view class="notify-html" v-html="currentNotify?.content" />
<view class="flex justify-center">
<wd-button type="primary" class="w-24" @click="showPopup = false">
关闭

View File

@@ -4,6 +4,7 @@ import { onMounted, reactive, ref } from 'vue'
import { getCurrentUniRoute } from '@/composables/uni-router'
import { openCustomerService } from '@/composables/useCustomerService'
import { ensurePageAccessByUrl } from '@/composables/useNavigationAuthGuard'
import { getPrivacyConsentPageUrl } from '@/composables/usePrivacyConsent'
const tabbar = ref('index')
const safeAreaTop = ref(0)
@@ -78,17 +79,28 @@ function tabChange(payload) {
function toComplaint() {
openCustomerService()
}
function clearCacheForPrivacyTest() {
uni.showModal({
title: '清除缓存',
content: '将清除本地缓存并回到首次启动隐私弹窗,用于测试。是否继续?',
success: (res) => {
if (!res.confirm)
return
uni.clearStorageSync()
uni.showToast({ title: '缓存已清除', icon: 'none' })
uni.reLaunch({ url: getPrivacyConsentPageUrl() })
},
})
}
</script>
<template>
<view class="home-layout min-h-screen flex flex-col">
<view :style="{ paddingTop: `${safeAreaTop}px` }">
<view class="header">
<image
class="logo overflow-hidden rounded-full"
src="/static/images/homelayout/title_logo.png"
mode="aspectFit"
/>
<image class="logo overflow-hidden rounded-full" src="/static/images/homelayout/title_logo.png"
mode="aspectFit" />
</view>
</view>
@@ -96,29 +108,20 @@ function toComplaint() {
<slot />
</view>
<wd-tabbar
v-model="tabbar"
fixed
bordered
safe-area-inset-bottom
placeholder
active-color="#1d4ed8"
inactive-color="#888888"
@change="tabChange"
>
<wd-tabbar-item
v-for="(item, index) in menu"
:key="index"
:name="item.name"
:title="item.title"
:icon="item.icon"
/>
<wd-tabbar v-model="tabbar" fixed bordered safe-area-inset-bottom placeholder active-color="#1d4ed8"
inactive-color="#888888" @change="tabChange">
<wd-tabbar-item v-for="(item, index) in menu" :key="index" :name="item.name" :title="item.title"
:icon="item.icon" />
</wd-tabbar>
<view class="complaint-button" @click="toComplaint">
<image src="/static/images/homelayout/ts.png" mode="aspectFit" class="complaint-icon" />
<text>投诉</text>
</view>
<!--
<view class="clear-cache-button" @click="clearCacheForPrivacyTest">
<text>清除缓存</text>
</view> -->
</view>
</template>
@@ -157,6 +160,22 @@ function toComplaint() {
z-index: 2000;
}
.clear-cache-button {
position: fixed;
bottom: 9.2rem;
right: 1rem;
background: #ffffff;
border: 1px solid #d1d5db;
border-radius: 1.5rem;
padding: 0.25rem 0.9rem;
color: #374151;
display: flex;
align-items: center;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
z-index: 2000;
font-size: 0.75rem;
}
.complaint-icon {
width: 1rem;
height: 1rem;

View File

@@ -54,8 +54,8 @@ function getDotColor(name) {
// 获取金额颜色
function getAmountColor(item) {
// 如果净佣金为0或状态为已退款,显示红色
if (item.net_amount <= 0 || item.status === 2) {
// 状态为已退款,显示红色
if (item.status === 2) {
return 'text-red-500'
}
// 如果有部分退款,显示橙色
@@ -68,7 +68,7 @@ function getAmountColor(item) {
// 获取金额前缀(+ 或 -
function getAmountPrefix(item) {
if (item.net_amount <= 0 || item.status === 2) {
if (item.status === 2) {
return '-'
}
return '+'
@@ -76,7 +76,7 @@ function getAmountPrefix(item) {
// 获取状态文本
function getStatusText(item) {
if (item.status === 2 || item.net_amount <= 0) {
if (item.status === 2) {
return '已退款'
}
if (item.status === 1) {
@@ -98,7 +98,7 @@ function getStatusText(item) {
// 获取状态样式
function getStatusStyle(item) {
if (item.status === 2 || item.net_amount <= 0) {
if (item.status === 2) {
return 'bg-red-100 text-red-800'
}
if (item.status === 1) {
@@ -124,6 +124,7 @@ async function getData() {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/commission?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true },
).get().json()
if (res.value?.code === 200 && !error.value) {

View File

@@ -54,6 +54,7 @@ async function getData() {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/rewards?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true },
).get().json()
if (res.value?.code === 200 && !error.value) {

View File

@@ -177,13 +177,9 @@ const revenueData = computed(() => {
// 加载价格配置
onMounted(async () => {
// #ifdef H5
document.documentElement.style.scrollBehavior = 'smooth'
// #endif
// 从API获取会员配置信息
try {
const { data, error } = await useApiFetch('/agent/membership/info').get().json()
const { data, error } = await useApiFetch('/agent/membership/info', { silent: true }).get().json()
if (data.value && !error.value && data.value.code === 200) {
const configData = data.value.data
@@ -290,69 +286,69 @@ function formatExpiryTime(expiryTimeStr) {
</script>
<template>
<div class="agent-VIP-apply min-h-screen w-full from-amber-50 via-amber-100 to-amber-50 bg-gradient-to-b pb-24">
<view class="agent-VIP-apply min-h-screen w-full from-amber-50 via-amber-100 to-amber-50 bg-gradient-to-b pb-24">
<!-- 装饰元素 -->
<div
<view
class="absolute right-0 top-0 h-32 w-32 rounded-bl-full from-amber-300 to-amber-500 bg-gradient-to-br opacity-20"
/>
<div
<view
class="absolute left-0 top-40 h-16 w-16 rounded-tr-full from-amber-400 to-amber-600 bg-gradient-to-tr opacity-20"
/>
<div
<view
class="absolute bottom-60 right-0 h-24 w-24 rounded-tl-full from-amber-300 to-amber-500 bg-gradient-to-bl opacity-20"
/>
<!-- 顶部标题区域 -->
<div class="header relative px-4 pb-6 pt-8 text-center">
<div
<view class="header relative px-4 pb-6 pt-8 text-center">
<view
class="absolute left-1/2 h-1 w-24 animate-pulse rounded-full from-amber-300 via-amber-500 to-amber-300 bg-gradient-to-r -top-2 -translate-x-1/2"
/>
<h1 class="mb-1 text-3xl text-amber-800 font-bold">
<text class="mb-1 text-3xl text-amber-800 font-bold">
{{ isVipOrSvip ? '代理会员续费' : 'VIP代理申请' }}
</h1>
<p class="mx-auto mt-2 max-w-xs text-sm text-amber-700">
</text>
<text class="mx-auto mt-2 max-w-xs text-sm text-amber-700">
<template v-if="isVipOrSvip">
您的会员有效期至 {{ formatExpiryTime(ExpiryTime) }}续费后有效期至
{{ renewalExpiryTime }}
</template>
<template v-else>
平台为疯狂推广者定制的赚买计划助您收益<span class="text-red-500 font-bold">翻倍增升</span>
平台为疯狂推广者定制的赚买计划助您收益<text class="text-red-500 font-bold">翻倍增升</text>
</template>
</p>
</text>
<!-- 装饰性金币图标 -->
<div class="absolute left-4 top-6 transform -rotate-12">
<div
<view class="absolute left-4 top-6 transform -rotate-12">
<view
class="h-8 w-8 flex items-center justify-center rounded-full from-yellow-300 to-yellow-500 bg-gradient-to-br shadow-lg"
>
<span class="text-xs text-white font-bold">¥</span>
</div>
</div>
<div class="absolute right-6 top-10 rotate-12 transform">
<div
<text class="text-xs text-white font-bold">¥</text>
</view>
</view>
<view class="absolute right-6 top-10 rotate-12 transform">
<view
class="h-6 w-6 flex items-center justify-center rounded-full from-yellow-400 to-yellow-600 bg-gradient-to-br shadow-lg"
>
<span class="text-xs text-white font-bold">¥</span>
</div>
</div>
</div>
<text class="text-xs text-white font-bold">¥</text>
</view>
</view>
</view>
<!-- 选择代理类型 -->
<div class="card-container mb-8 px-4">
<div class="transform overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg transition-all">
<h2
<view class="card-container mb-8 px-4">
<view class="transform overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg transition-all">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<span class="relative z-10">选择代理类型</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
<text class="relative z-10">选择代理类型</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
</div>
</h2>
</view>
</text>
<div class="flex gap-4 p-6">
<div
<view class="flex gap-4 p-6">
<view
class="relative flex-1 transform cursor-pointer border-2 rounded-lg p-4 text-center transition-all duration-300 hover:-translate-y-1"
:class="[
selectedType === 'vip'
@@ -360,24 +356,24 @@ function formatExpiryTime(expiryTimeStr) {
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('vip')"
>
<div class="text-xl text-amber-700 font-bold">
<view class="text-xl text-amber-700 font-bold">
VIP代理
</div>
<div class="mt-1 text-lg text-amber-600 font-bold">
</view>
<view class="mt-1 text-lg text-amber-600 font-bold">
{{ vipConfig.price }}{{ vipConfig.priceUnit }}
</div>
<div class="mt-2 text-sm text-gray-600">
</view>
<view class="mt-2 text-sm text-gray-600">
标准VIP权益
</div>
<div
</view>
<view
v-if="selectedType === 'vip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2"
>
<wd-icon name="check" custom-style="color:#fff;font-size:14px;" />
</div>
</div>
</view>
</view>
<div
<view
class="relative flex-1 transform cursor-pointer border-2 rounded-lg p-4 text-center transition-all duration-300 hover:-translate-y-1"
:class="[
selectedType === 'svip'
@@ -385,134 +381,134 @@ function formatExpiryTime(expiryTimeStr) {
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('svip')"
>
<div class="text-xl text-amber-700 font-bold">
<view class="text-xl text-amber-700 font-bold">
SVIP代理
</div>
<div class="mt-1 text-lg text-amber-600 font-bold">
</view>
<view class="mt-1 text-lg text-amber-600 font-bold">
{{ vipConfig.svipPrice }}{{ vipConfig.priceUnit }}
</div>
<div class="mt-2 text-sm text-gray-600">
</view>
<view class="mt-2 text-sm text-gray-600">
超级VIP权益
</div>
<div
</view>
<view
v-if="selectedType === 'svip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2"
>
<wd-icon name="check" custom-style="color:#fff;font-size:14px;" />
</div>
</div>
</div>
</div>
</div>
</view>
</view>
</view>
</view>
</view>
<!-- 六大超值权益 -->
<div class="card-container mb-8 px-4">
<div class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<h2
<view class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<span class="relative z-10">六大超值权益</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
<text class="relative z-10">六大超值权益</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
</div>
</h2>
</view>
</text>
<div class="grid grid-cols-2 gap-4 p-4">
<view class="grid grid-cols-2 gap-4 p-4">
<!-- 权益1 -->
<div
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
<div class="mb-2 flex items-center text-amber-800 font-bold">
<span
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>1</span>
>1</text>
下级贡献收益
</div>
<p class="text-sm text-gray-600">
下级完全收益您来定涨多少赚多少一单最高收益<span class="text-red-500 font-bold">10</span>
</p>
</div>
</view>
<text class="text-sm text-gray-600">
下级完全收益您来定涨多少赚多少一单最高收益<text class="text-red-500 font-bold">10</text>
</text>
</view>
<!-- 权益2 -->
<div
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
<div class="mb-2 flex items-center text-amber-800 font-bold">
<span
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>2</span>
>2</text>
下级提现收益
</div>
<p class="text-sm text-gray-600">
下级定价标准由您定超过标准部分收益更丰厚一单最高多赚<span class="text-red-500 font-bold">10</span>
</p>
</div>
</view>
<text class="text-sm text-gray-600">
下级定价标准由您定超过标准部分收益更丰厚一单最高多赚<text class="text-red-500 font-bold">10</text>
</text>
</view>
<!-- 权益3 -->
<div
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
<div class="mb-2 flex items-center text-amber-800 font-bold">
<span
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>3</span>
>3</text>
转换高额奖励
</div>
<p class="text-sm text-gray-600">
下级成为VIPSVIP高额奖励立马发放<span class="text-red-500 font-bold">399</span>
</p>
</div>
</view>
<text class="text-sm text-gray-600">
下级成为VIPSVIP高额奖励立马发放<text class="text-red-500 font-bold">399</text>
</text>
</view>
<!-- 权益4 -->
<div
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
<div class="mb-2 flex items-center text-amber-800 font-bold">
<span
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>4</span>
>4</text>
下级提现奖励
</div>
<p class="text-sm text-gray-600">
</view>
<text class="text-sm text-gray-600">
下级成为SVIP每次提现都奖励1%坐享被动收入
</p>
</div>
</text>
</view>
<!-- 权益6 -->
<div
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
<div class="mb-2 flex items-center text-amber-800 font-bold">
<span
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>6</span>
>6</text>
平台专项扶持
</div>
<p class="text-sm text-gray-600">
</view>
<text class="text-sm text-gray-600">
一对一专属客服服务为合作伙伴提供全方位成长赋能
</p>
</div>
</div>
</div>
</div>
</text>
</view>
</view>
</view>
</view>
<!-- 权益对比表 -->
<div v-if="selectedType" class="card-container mb-8 px-4">
<div class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<h2
<view v-if="selectedType" class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<span class="relative z-10">{{ selectedType === 'vip' ? 'VIP' : 'SVIP' }}代理权益对比</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
<text class="relative z-10">{{ selectedType === 'vip' ? 'VIP' : 'SVIP' }}代理权益对比</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
</div>
</h2>
</view>
</text>
<div class="overflow-x-auto p-4">
<view class="overflow-x-auto p-4">
<table class="w-full border-collapse">
<thead>
<tr class="from-amber-100 to-amber-200 bg-gradient-to-r">
@@ -715,61 +711,61 @@ function formatExpiryTime(expiryTimeStr) {
</tr>
</tbody>
</table>
</div>
</div>
</div>
</view>
</view>
</view>
<!-- 收益预估 -->
<div v-if="selectedType" class="card-container mb-8 px-4">
<div class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<h2
<view v-if="selectedType" class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<span class="relative z-10">收益预估对比</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
<text class="relative z-10">收益预估对比</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
</div>
</h2>
</view>
</text>
<div class="p-4">
<view class="p-4">
<!-- 顶部收益概览 -->
<div class="mb-6 overflow-hidden border border-amber-200 rounded-lg">
<div class="from-amber-100 to-amber-200 bg-gradient-to-r px-4 py-2 text-center text-amber-800 font-bold">
<view class="mb-6 overflow-hidden border border-amber-200 rounded-lg">
<view class="from-amber-100 to-amber-200 bg-gradient-to-r px-4 py-2 text-center text-amber-800 font-bold">
VIP与SVIP代理收益对比
</div>
<div class="grid grid-cols-2 divide-x divide-amber-200">
<div class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'vip' }">
<div class="mb-1 text-sm text-gray-600">
</view>
<view class="grid grid-cols-2 divide-x divide-amber-200">
<view class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'vip' }">
<view class="mb-1 text-sm text-gray-600">
VIP月预计收益
</div>
<div class="text-xl text-amber-600 font-bold">
</view>
<view class="text-xl text-amber-600 font-bold">
{{ revenueData.vipMonthly }}
</div>
<div class="mt-1 text-xs text-gray-500">
</view>
<view class="mt-1 text-xs text-gray-500">
年收益{{ revenueData.vipYearly }}
</div>
</div>
<div class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'svip' }">
<div class="mb-1 text-sm text-gray-600">
</view>
</view>
<view class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'svip' }">
<view class="mb-1 text-sm text-gray-600">
SVIP月预计收益
</div>
<div class="text-xl text-red-500 font-bold">
</view>
<view class="text-xl text-red-500 font-bold">
{{ revenueData.svipMonthly }}
</div>
<div class="mt-1 text-xs text-gray-500">
</view>
<view class="mt-1 text-xs text-gray-500">
年收益{{ revenueData.svipYearly }}
</div>
</div>
</div>
<div class="from-red-50 to-red-100 bg-gradient-to-r px-4 py-2 text-center text-red-600 font-medium">
选择SVIP相比VIP月增收益<span class="font-bold">{{ revenueData.monthlyDifference }}</span>
</div>
</div>
</view>
</view>
</view>
<view class="from-red-50 to-red-100 bg-gradient-to-r px-4 py-2 text-center text-red-600 font-medium">
选择SVIP相比VIP月增收益<text class="font-bold">{{ revenueData.monthlyDifference }}</text>
</view>
</view>
<!-- 详细收益表格 -->
<div class="overflow-x-auto">
<view class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="from-amber-100 to-amber-200 bg-gradient-to-r">
@@ -945,126 +941,128 @@ function formatExpiryTime(expiryTimeStr) {
</tr>
</tbody>
</table>
</div>
</view>
<!-- 投资回报率 -->
<div class="mt-6 border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-r p-4">
<div class="mb-3 text-center text-amber-800 font-bold">
<view class="mt-6 border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-r p-4">
<view class="mb-3 text-center text-amber-800 font-bold">
投资收益分析
</div>
<div class="grid grid-cols-1 gap-4">
<div class="rounded-lg bg-white p-3 shadow-sm">
<div class="flex items-center justify-between">
<div class="flex-1 border-r border-amber-100 pr-3">
<div class="mb-1 text-center text-amber-700 font-medium">
</view>
<view class="grid grid-cols-1 gap-4">
<view class="rounded-lg bg-white p-3 shadow-sm">
<view class="flex items-center justify-between">
<view class="flex-1 border-r border-amber-100 pr-3">
<view class="mb-1 text-center text-amber-700 font-medium">
VIP方案
</div>
<div class="text-center">
<div class="text-sm text-amber-600">
</view>
<view class="text-center">
<view class="text-sm text-amber-600">
投资{{ vipConfig.price }}
</div>
<div class="text-sm text-gray-600">
</view>
<view class="text-sm text-gray-600">
月收益{{ revenueData.vipMonthly }}
</div>
</div>
</div>
<div class="flex-1 pl-3">
<div class="mb-1 text-center text-red-500 font-medium">
</view>
</view>
</view>
<view class="flex-1 pl-3">
<view class="mb-1 text-center text-red-500 font-medium">
SVIP方案
</div>
<div class="text-center">
<div class="text-sm text-red-500">
</view>
<view class="text-center">
<view class="text-sm text-red-500">
投资{{ vipConfig.svipPrice }}
</div>
<div class="text-sm text-gray-600">
</view>
<view class="text-sm text-gray-600">
月收益{{ revenueData.svipMonthly }}
</div>
</div>
</div>
</div>
</div>
</view>
</view>
</view>
</view>
</view>
<!-- 升级收益对比 -->
<div class="rounded-lg from-red-50 to-amber-50 bg-gradient-to-r p-3 shadow-sm">
<div class="mb-2 text-center text-red-700 font-medium">
<view class="rounded-lg from-red-50 to-amber-50 bg-gradient-to-r p-3 shadow-sm">
<view class="mb-2 text-center text-red-700 font-medium">
SVIP升级优势分析
</div>
<div class="flex items-center justify-center gap-3">
<div class="text-center">
<div class="text-sm text-gray-600">
</view>
<view class="flex items-center justify-center gap-3">
<view class="text-center">
<view class="text-sm text-gray-600">
额外投资
</div>
<div class="text-red-600 font-bold">
</view>
<view class="text-red-600 font-bold">
{{ revenueData.priceDifference }}
</div>
</div>
<div
</view>
</view>
<view
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white"
>
<div class="transform -translate-y-px">
<view class="transform -translate-y-px">
</div>
</div>
<div class="text-center">
<div class="text-sm text-gray-600">
</view>
</view>
<view class="text-center">
<view class="text-sm text-gray-600">
每月额外收益
</div>
<div class="text-red-600 font-bold">
</view>
<view class="text-red-600 font-bold">
{{ revenueData.monthlyDifference }}
</div>
</div>
<div
</view>
</view>
<view
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white"
>
<span class="transform -translate-y-px"></span>
</div>
<div class="text-center">
<div class="text-sm text-gray-600">
<text class="transform -translate-y-px"></text>
</view>
<view class="text-center">
<view class="text-sm text-gray-600">
投资回收时间
</div>
<div class="text-red-600 font-bold">
</view>
<view class="text-red-600 font-bold">
{{ revenueData.recoverDays }}
</div>
</div>
</div>
<div class="mt-3 text-center text-red-500 font-medium">
额外投资{{ revenueData.priceDifference }}<span class="text-red-600 font-bold">年多赚{{
revenueData.yearlyDifference }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</view>
</view>
</view>
<view class="mt-3 text-center text-red-500 font-medium">
额外投资{{ revenueData.priceDifference }}<text class="text-red-600 font-bold">年多赚{{
revenueData.yearlyDifference }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 申请按钮固定在底部 -->
<div
<view
class="fixed bottom-0 left-0 right-0 z-30 from-amber-100 to-transparent bg-gradient-to-t px-4 py-3 backdrop-blur-sm"
>
<div class="flex flex-col gap-2">
<button :class="buttonClass" :disabled="!canPerformAction" @click="applyVip">
<span class="relative z-10">{{ buttonText }}</span>
<div
<view class="flex flex-col gap-2">
<wd-button :class="buttonClass" block :disabled="!canPerformAction" @click="applyVip">
<text class="relative z-10">{{ buttonText }}</text>
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
</button>
<button
class="w-full transform border border-amber-400 rounded-lg bg-white py-3 text-amber-700 font-medium shadow-md transition-transform active:scale-98 active:bg-amber-50"
</wd-button>
<wd-button
custom-class="contact-wd-btn"
block
plain
@click="contactService"
>
<div class="flex items-center justify-center">
<view class="flex items-center justify-center text-amber-700 font-medium">
<wd-icon name="service" custom-class="mr-1" />
<span>联系客服咨询</span>
</div>
</button>
<text>联系客服咨询</text>
</view>
</wd-button>
<!-- 最终解释权声明 -->
<div class="py-1 text-center text-xs text-gray-400">
<view class="py-1 text-center text-xs text-gray-400">
最终解释权归戎行技术有限公司所有
</div>
</div>
</div>
</div>
</view>
</view>
</view>
</view>
<Payment :id="payID" v-model="showPayment" :data="payData" type="agent_vip" @close="showPayment = false" />
</template>
@@ -1096,4 +1094,10 @@ function formatExpiryTime(expiryTimeStr) {
.active\:scale-98:active {
transform: scale(0.98);
}
:deep(.contact-wd-btn.wd-button) {
border-color: #fbbf24;
background-color: #fff;
box-shadow: 0 4px 10px rgba(120, 53, 15, 0.08);
}
</style>

View File

@@ -71,15 +71,6 @@ function validateDecimal(field) {
// 价格区间验证(在 @blur 中调用)
function validateRange() {
console.log(
'configData.value.price_range_from',
configData.value.price_range_from,
)
console.log(
'configData.value.price_range_to',
configData.value.price_range_to,
)
if (
configData.value.price_range_from === null
|| configData.value.price_range_to === null
@@ -192,6 +183,7 @@ async function getConfig() {
try {
const { data, error } = await useApiFetch(
`/agent/membership/user_config?product_id=${selectedReportId.value}`,
{ silent: true },
)
.get()
.json()
@@ -206,7 +198,6 @@ async function getConfig() {
price_increase_amount:
respConfigData.price_increase_amount || null,
}
console.log('configData', configData.value)
// const respProductConfigData = data.value.data.product_config
productConfigData.value = data.value.data.product_config
// 设置动态限制值
@@ -235,7 +226,6 @@ async function handleSubmit() {
price_ratio: (configData.value.price_ratio || 0) / 100, // 转换为小数
price_increase_amount: configData.value.price_increase_amount || 0,
}
console.log('submitData', submitData)
const { data, error } = await useApiFetch(
'/agent/membership/save_user_config',
)
@@ -340,18 +330,18 @@ onMounted(() => {
</script>
<template>
<div class="mx-auto max-w-3xl min-h-screen p-4">
<view class="mx-auto max-w-3xl min-h-screen p-4">
<!-- 标题部分 -->
<div class="card mb-4 rounded-lg from-blue-500 to-blue-600 bg-gradient-to-r p-4 text-white shadow-lg">
<h1 class="mb-2 text-2xl font-extrabold">
<view class="card mb-4 rounded-lg from-blue-500 to-blue-600 bg-gradient-to-r p-4 text-white shadow-lg">
<text class="mb-2 text-2xl font-extrabold">
专业报告定价配置
</h1>
<p class="opacity-90">
</text>
<text class="opacity-90">
请选择报告类型并设置定价策略助您实现精准定价
</p>
</div>
</text>
</view>
<div class="mb-4">
<view class="mb-4">
<view class="card selector" @click="showPicker = true">
<view class="selector-label">
📝 选择报告
@@ -376,53 +366,59 @@ onMounted(() => {
</view>
</view>
</wd-popup>
</div>
</view>
<div v-if="selectedReportText" class="space-y-6">
<view v-if="selectedReportText" class="space-y-6">
<!-- 配置卡片 -->
<div class="card">
<view class="card">
<!-- 当前报告标题 -->
<div class="mb-6 flex items-center">
<h2 class="text-xl text-gray-800 font-semibold">
<view class="mb-6 flex items-center">
<text class="text-xl text-gray-800 font-semibold">
{{ selectedReportText }}配置
</h2>
</div>
</text>
</view>
<!-- 显示当前产品的基础成本信息 -->
<div
<view
v-if="productConfigData && productConfigData.cost_price"
class="mb-4 border border-gray-200 rounded-lg bg-gray-50 px-4 py-2 shadow-sm"
>
<div class="text-lg text-gray-700 font-semibold">
<view class="text-lg text-gray-700 font-semibold">
报告基础配置信息
</div>
<div class="mt-1 text-sm text-gray-600">
<div>
基础成本价<span class="font-medium">{{
productConfigData.cost_price
}}</span>
</view>
<view class="mt-1 text-sm text-gray-600">
<view>
基础成本价<text class="font-medium">
{{
productConfigData.cost_price
}}
</text>
</div>
<!-- <div>区间起始价<span class="font-medium">{{ productConfigData.price_range_min }}</span> </div> -->
<div>
最高设定金额上限<span class="font-medium">{{
productConfigData.price_range_max
}}</span>
</view>
<!-- <view>区间起始价<text class="font-medium">{{ productConfigData.price_range_min }}</text> </view> -->
<view>
最高设定金额上限<text class="font-medium">
{{
productConfigData.price_range_max
}}
</text>
</div>
<div>
最高设定比例上限<span class="font-medium">{{
priceRatioMax
}}</span>
</view>
<view>
最高设定比例上限<text class="font-medium">
{{
priceRatioMax
}}
</text>
%
</div>
</div>
</div>
</view>
</view>
</view>
<!-- 分隔线 -->
<div class="section-divider my-6">
<view class="section-divider my-6">
成本策略配置
</div>
</view>
<!-- 加价金额 -->
<view class="custom-field" :class="{ 'field-error': increaseError }">
@@ -430,27 +426,29 @@ onMounted(() => {
🚀 加价金额
</text>
<view class="field-input-wrap">
<input
<wd-input
v-model.number="configData.price_increase_amount"
type="number"
placeholder="0"
class="field-input"
class="field-wd-input"
no-border
clearable
@blur="validateDecimal('price_increase_amount')"
>
/>
<text class="field-unit">
</text>
</view>
</view>
<div class="mt-1 text-xs text-gray-400">
<view class="mt-1 text-xs text-gray-400">
提示最大加价金额为{{ priceIncreaseAmountMax }}<br>
说明加价金额是在基础成本价上增加的额外费用决定下级报告的最低定价您将获得所有输入的金额利润
</div>
</view>
<!-- 分隔线 -->
<div class="section-divider my-6">
<view class="section-divider my-6">
定价策略配置
</div>
</view>
<!-- 定价区间最低 -->
<view class="custom-field" :class="{ 'field-error': rangeError }">
@@ -458,29 +456,31 @@ onMounted(() => {
💰 最低金额
</text>
<view class="field-input-wrap">
<input
<wd-input
v-model.number="configData.price_range_from"
type="number"
placeholder="0"
class="field-input"
class="field-wd-input"
no-border
clearable
@blur="
() => {
validateDecimal('price_range_from')
validateRange()
}
"
>
/>
<text class="field-unit">
</text>
</view>
</view>
<div class="mt-1 text-xs text-gray-400">
<view class="mt-1 text-xs text-gray-400">
提示最低金额不能低于基础最低
{{ productConfigData?.price_range_min || 0 }} +
加价金额<br>
说明设定的最低金额为定价区间的起始值若下级设定的报告金额在区间内则区间内部分将按比例获得收益
</div>
</view>
<!-- 定价区间最高 -->
<view class="custom-field" :class="{ 'field-error': rangeError }">
@@ -488,29 +488,31 @@ onMounted(() => {
💰 最高金额
</text>
<view class="field-input-wrap">
<input
<wd-input
v-model.number="configData.price_range_to"
type="number"
placeholder="0"
class="field-input"
class="field-wd-input"
no-border
clearable
@blur="
() => {
validateDecimal('price_range_to')
validateRange()
}
"
>
/>
<text class="field-unit">
</text>
</view>
</view>
<div class="mt-1 text-xs text-gray-400">
<view class="mt-1 text-xs text-gray-400">
提示最高金额不能超过上限{{
productConfigData?.price_range_max || 0
}}和大于最低金额{{ priceIncreaseMax }}<br>
说明设定的最高金额为定价区间的结束值若下级设定的报告金额在区间内则区间内部分将按比例获得收益
</div>
</view>
<!-- 收取比例 -->
<view class="custom-field" :class="{ 'field-error': ratioError }">
@@ -518,23 +520,25 @@ onMounted(() => {
📈 收取比例
</text>
<view class="field-input-wrap">
<input
<wd-input
v-model.number="configData.price_ratio"
type="number"
placeholder="0"
class="field-input"
class="field-wd-input"
no-border
clearable
@blur="validateRatio()"
>
/>
<text class="field-unit">
%
</text>
</view>
</view>
<div class="mt-1 text-xs text-gray-400">
<view class="mt-1 text-xs text-gray-400">
提示最大收取比例为{{ priceRatioMax }}%<br>
说明收取比例表示对定价区间内即报告金额超过最低金额小于最高金额的部分的金额按此比例进行利润分成
</div>
</div>
</view>
</view>
<!-- 保存按钮 -->
<wd-button
@@ -544,17 +548,17 @@ onMounted(() => {
>
保存当前报告配置
</wd-button>
</div>
</view>
<!-- 未选择提示 -->
<div v-else class="py-12 text-center">
<view v-else class="py-12 text-center">
<text class="mb-4 block text-4xl text-gray-400">
</text>
<p class="text-gray-500">
<text class="text-gray-500">
请先选择需要配置的报告类型
</p>
</div>
</div>
</text>
</view>
</view>
</template>
<style scoped>
@@ -645,11 +649,10 @@ onMounted(() => {
border: 1px solid transparent;
}
.field-input {
:deep(.field-wd-input .wd-input__inner) {
flex: 1;
border: none;
outline: none;
background: transparent;
background: transparent !important;
text-align: left !important;
}
.field-unit {

View File

@@ -52,7 +52,7 @@ const teamTimeText = computed(() => dateTextMap[selectedTeamDate.value] || '今
async function getData() {
try {
const { data: res, error } = await useApiFetch('/agent/revenue').get().json()
const { data: res, error } = await useApiFetch('/agent/revenue', { silent: true }).get().json()
if (res.value?.code === 200 && !error.value) {
data.value = res.value.data
}

View File

@@ -68,7 +68,7 @@ onLoad(async () => {
try {
await agentStore.fetchAgentStatus()
if (agentStore.isAgent) {
const { data, error } = await useApiFetch('/agent/revenue').get().json()
const { data, error } = await useApiFetch('/agent/revenue', { silent: true }).get().json()
if (!error.value && data.value?.code === 200)
revenueData.value = data.value.data
}

View File

@@ -59,7 +59,7 @@ function goToDetail(id, type) {
</script>
<template>
<div class="help-center">
<view class="help-center">
<view class="tab-bar">
<view
v-for="(category, index) in categories"
@@ -88,7 +88,7 @@ function goToDetail(id, type) {
</template>
</wd-cell>
</wd-cell-group>
</div>
</view>
</template>
<style lang="scss" scoped>

View File

@@ -10,7 +10,7 @@ const loading = ref(false)
// 初始加载数据
async function fetchData() {
loading.value = true
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`)
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`, { silent: true })
.get()
.json()
if (data.value && !error.value) {

View File

@@ -1,4 +1,5 @@
<script setup>
import { computed } from 'vue'
import bgIcon from '/static/images/bg_icon.png'
import bannerImg from '/static/images/index/banner_1.png'
import bannerImg2 from '/static/images/index/banner_2.png'
@@ -15,6 +16,7 @@ import rightIcon from '/static/images/index/right.png'
import indexPromoteIcon from '/static/images/index/tgbg.png'
import indexMyReportIcon from '/static/images/index/wdbg.png'
import indexInvitationIcon from '/static/images/index/yqhy.png'
import { useAppConfig } from '@/composables/useAppConfig'
definePage({ type: 'home', layout: 'home' })
@@ -32,6 +34,9 @@ const riskServices = [
{ title: '贷前', name: 'preloanbackgroundcheck', bg: loanCheckIcon },
{ title: '诚信租赁', name: 'rentalinfo', bg: rentalInfoBg },
]
const { appConfig, loadAppConfig } = useAppConfig()
const queryRetentionDaysText = computed(() => `${appConfig.value.query.retention_days}`)
void loadAppConfig()
function toInquire(name) {
uni.navigateTo({
@@ -159,7 +164,7 @@ function toHistory() {
我的历史查询记录
</view>
<view class="text-xs text-gray-500">
查询记录有效期为30天
查询记录有效期为{{ queryRetentionDaysText }}
</view>
</view>
<image :src="rightIcon" class="h-6 w-6" mode="aspectFit" />

View File

@@ -25,7 +25,7 @@ onLoad(async (query) => {
async function getProduct() {
if (!feature.value)
return
const { data } = await useApiFetch(`/product/en/${feature.value}`)
const { data } = await useApiFetch(`/product/en/${feature.value}`, { silent: true })
.get()
.json()

View File

@@ -20,6 +20,16 @@ const ancestor = ref('')
const isSelf = ref(false)
const isApplyPolling = ref(false)
function showToast(options) {
const message = typeof options === 'string' ? options : options?.message
if (!message)
return
uni.showToast({
title: message,
icon: 'none',
})
}
function stopApplyStatusPolling() {
if (intervalId) {
clearInterval(intervalId)

41
src/pages/launch.vue Normal file
View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { useAppBootstrap } from '@/composables/useAppBootstrap'
import { getPrivacyConsentPageUrl, hasAcceptedPrivacyPolicy } from '@/composables/usePrivacyConsent'
definePage({ layout: false })
const { bootstrap } = useAppBootstrap()
const routing = ref(false)
async function routeOnLaunch() {
if (routing.value)
return
routing.value = true
try {
if (!hasAcceptedPrivacyPolicy()) {
uni.reLaunch({ url: getPrivacyConsentPageUrl() })
return
}
await bootstrap()
uni.reLaunch({ url: '/pages/index' })
}
finally {
routing.value = false
}
}
onLoad(() => {
void routeOnLaunch()
})
</script>
<template>
<view class="launch-page" />
</template>
<style scoped>
.launch-page {
min-height: 100vh;
background: #f5f7fb;
}
</style>

View File

@@ -80,29 +80,35 @@ async function handleLogin() {
performLogin()
}
// 执行实际的登录逻辑
// 执行实际的登录逻辑(整段链式请求共用一个全屏 loading避免多请求轮流 show/hide
async function performLogin() {
const { data, error } = await useApiFetch('/user/mobileCodeLogin')
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json()
uni.showLoading({ title: '登录中...', mask: true })
try {
const { data, error } = await useApiFetch('/user/mobileCodeLogin', { silent: true })
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
uni.setStorageSync('token', data.value.data.accessToken)
uni.setStorageSync('refreshAfter', data.value.data.refreshAfter)
uni.setStorageSync('accessExpire', data.value.data.accessExpire)
if (data.value && !error.value) {
if (data.value.code === 200) {
uni.setStorageSync('token', data.value.data.accessToken)
uni.setStorageSync('refreshAfter', data.value.data.refreshAfter)
uni.setStorageSync('accessExpire', data.value.data.accessExpire)
await userStore.fetchUserInfo()
await agentStore.fetchAgentStatus()
await userStore.fetchUserInfo()
await agentStore.fetchAgentStatus()
uni.reLaunch({ url: redirectUrl.value || '/pages/index' })
uni.reLaunch({ url: redirectUrl.value || '/pages/index' })
}
else {
toast(data.value.msg || '登录失败')
}
}
else {
toast(data.value.msg || '登录失败')
toast('登录失败')
}
}
else {
toast('登录失败')
finally {
uni.hideLoading()
}
}
function toUserAgreement() {

View File

@@ -2,7 +2,6 @@
import { storeToRefs } from 'pinia'
import { computed, onBeforeMount, ref, watch } from 'vue'
import { openCustomerService } from '@/composables/useCustomerService'
import { useEnv } from '@/composables/useEnv'
import { useAgentStore } from '@/stores/agentStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useUserStore } from '@/stores/userStore'
@@ -15,7 +14,7 @@ const userStore = useUserStore()
const dialogStore = useDialogStore()
const { isAgent, level, ExpiryTime } = storeToRefs(agentStore)
const { userAvatar, isLoggedIn, mobile } = storeToRefs(userStore)
const { isWeChat } = useEnv()
const isWeChat = ref(false)
const levelNames = {
'normal': '普通代理',

View File

@@ -24,13 +24,13 @@ onMounted(() => {
</script>
<template>
<div class="not-found">
<div class="not-found-content">
<h1>404</h1>
<h2>页面未找到</h2>
<p>抱歉您访问的页面不存在或已被移除</p>
<div class="suggestions">
<h3>您可以尝试</h3>
<view class="not-found">
<view class="not-found-content">
<text class="title-404">404</text>
<text class="title-main">页面未找到</text>
<text class="desc-text">抱歉您访问的页面不存在或已被移除</text>
<view class="suggestions">
<text class="suggestions-title">您可以尝试</text>
<ul>
<li>
<router-link to="/">
@@ -48,17 +48,17 @@ onMounted(() => {
</router-link>
</li>
</ul>
</div>
<div class="actions">
</view>
<view class="actions">
<router-link to="/" class="home-link">
返回首页
</router-link>
<router-link to="/help" class="help-link">
帮助中心
</router-link>
</div>
</div>
</div>
</view>
</view>
</view>
</template>
<style scoped>
@@ -81,7 +81,8 @@ onMounted(() => {
width: 100%;
}
.not-found h1 {
.not-found .title-404 {
display: block;
font-size: 120px;
color: #667eea;
margin: 0 0 20px 0;
@@ -89,14 +90,16 @@ onMounted(() => {
line-height: 1;
}
.not-found h2 {
.not-found .title-main {
display: block;
font-size: 32px;
color: #333;
margin: 0 0 20px 0;
font-weight: 600;
}
.not-found p {
.not-found .desc-text {
display: block;
font-size: 18px;
color: #666;
margin-bottom: 30px;
@@ -108,7 +111,8 @@ onMounted(() => {
text-align: left;
}
.suggestions h3 {
.suggestions .suggestions-title {
display: block;
font-size: 20px;
color: #333;
margin-bottom: 15px;
@@ -189,11 +193,11 @@ onMounted(() => {
padding: 40px 20px;
}
.not-found h1 {
.not-found .title-404 {
font-size: 80px;
}
.not-found h2 {
.not-found .title-main {
font-size: 24px;
}

View File

@@ -131,7 +131,7 @@ async function checkPaymentStatus() {
}
try {
const { data, error } = await useApiFetch(`/pay/check`)
const { data, error } = await useApiFetch(`/pay/check`, { silent: true })
.post({
order_no: orderNo.value,
})
@@ -264,51 +264,59 @@ defineExpose({
</script>
<template>
<div class="payment-result-container flex flex-col items-center p-6">
<view class="payment-result-container flex flex-col items-center p-6">
<!-- 加载动画验证支付结果时显示 -->
<div v-if="isLoading" class="w-full">
<div class="flex flex-col items-center justify-center py-10">
<view v-if="isLoading" class="w-full">
<view class="flex flex-col items-center justify-center py-10">
<view class="loading-spinner" />
<p class="mt-4 text-lg text-gray-600">
<text class="mt-4 text-lg text-gray-600">
正在处理支付结果...
</p>
</div>
</div>
</text>
</view>
</view>
<!-- 支付结果展示 -->
<div v-else class="w-full">
<view v-else class="w-full">
<!-- 支付成功 -->
<div v-if="paymentStatus === 'paid'" class="success-result">
<div class="success-animation mb-6">
<view v-if="paymentStatus === 'paid'" class="success-result">
<view class="success-animation mb-6">
<text class="status-icon">
</text>
<div class="success-ring" />
</div>
<view class="success-ring" />
</view>
<h1 class="mb-4 text-center text-2xl text-gray-800 font-bold">
<text class="mb-4 text-center text-2xl text-gray-800 font-bold">
支付成功
</h1>
</text>
<div class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
<div class="mb-4 flex justify-between">
<span class="text-gray-600">订单编号</span>
<span class="text-gray-800">{{ orderNo }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}</span>
</div>
</div>
<div v-if="paymentType === 'agent_vip'" class="mb-4 text-center text-gray-600">
<view class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
<view class="mb-4 flex justify-between">
<text class="text-gray-600">
订单编号
</text>
<text class="text-gray-800">
{{ orderNo }}
</text>
</view>
<view class="flex justify-between">
<text class="text-gray-600">
支付类型
</text>
<text class="text-gray-800">
{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}
</text>
</view>
</view>
<view v-if="paymentType === 'agent_vip'" class="mb-4 text-center text-gray-600">
恭喜你成为高级代理会员享受更多权益
</div>
</view>
<div class="action-buttons grid grid-cols-1 gap-4">
<view class="action-buttons grid grid-cols-1 gap-4">
<wd-button block type="primary" class="rounded-lg" @click="handleNavigation">
{{
paymentType === "agent_vip"
@@ -316,83 +324,97 @@ defineExpose({
: "查看查询结果"
}}
</wd-button>
</div>
</div>
</view>
</view>
<!-- 退款状态 -->
<div v-else-if="paymentStatus === 'refunded'" class="refund-result">
<div v-if="paymentType === 'query'" class="success-animation mb-6">
<view v-else-if="paymentStatus === 'refunded'" class="refund-result">
<view v-if="paymentType === 'query'" class="success-animation mb-6">
<text class="status-icon">
</text>
<div class="success-ring" />
</div>
<div v-else class="info-animation mb-6">
<view class="success-ring" />
</view>
<view v-else class="info-animation mb-6">
<text class="status-icon">
</text>
<div class="info-ring" />
</div>
<view class="info-ring" />
</view>
<h1 class="mb-4 text-center text-2xl text-gray-800 font-bold">
<text class="mb-4 text-center text-2xl text-gray-800 font-bold">
{{ paymentType === "query" ? "已处理" : "订单已退款" }}
</h1>
</text>
<div class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
<div class="mb-4 flex justify-between">
<span class="text-gray-600">订单编号</span>
<span class="text-gray-800">{{ orderNo }}</span>
</div>
<div class="mb-4 flex justify-between">
<span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">订单状态</span>
<span class="text-blue-600">已退款</span>
</div>
</div>
<view class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
<view class="mb-4 flex justify-between">
<text class="text-gray-600">
订单编号
</text>
<text class="text-gray-800">
{{ orderNo }}
</text>
</view>
<view class="mb-4 flex justify-between">
<text class="text-gray-600">
支付类型
</text>
<text class="text-gray-800">
{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}
</text>
</view>
<view class="flex justify-between">
<text class="text-gray-600">
订单状态
</text>
<text class="text-blue-600">
已退款
</text>
</view>
</view>
<div v-if="paymentType === 'query'" class="action-buttons grid grid-cols-1 gap-4">
<view v-if="paymentType === 'query'" class="action-buttons grid grid-cols-1 gap-4">
<wd-button block type="primary" class="rounded-lg" @click="handleNavigation">
查看查询结果
</wd-button>
</div>
</view>
<div v-else class="message-box mb-6 rounded-lg bg-blue-50 p-4">
<p class="text-center text-blue-800">
<view v-else class="message-box mb-6 rounded-lg bg-blue-50 p-4">
<text class="text-center text-blue-800">
您的代理会员费用已退款如有疑问请联系客服
</p>
</div>
</text>
</view>
<div v-if="paymentType === 'agent_vip'" class="action-buttons grid grid-cols-1 gap-4">
<view v-if="paymentType === 'agent_vip'" class="action-buttons grid grid-cols-1 gap-4">
<wd-button block type="primary" class="rounded-lg" @click="contactService">
联系客服
</wd-button>
</div>
</div>
</view>
</view>
<!-- 其他状态待支付失败关闭 -->
<div v-else class="other-result">
<div class="info-animation mb-6">
<view v-else class="other-result">
<view class="info-animation mb-6">
<text class="status-icon" :style="{ color: getStatusColor }">
{{ getStatusIcon }}
</text>
<div class="info-ring" :class="getRingClass" />
</div>
<view class="info-ring" :class="getRingClass" />
</view>
<h1 class="mb-4 text-center text-2xl text-gray-800 font-bold">
<text class="mb-4 text-center text-2xl text-gray-800 font-bold">
{{ statusText }}
</h1>
</text>
<!-- 添加轮询状态提示 -->
<div v-if="paymentStatus === 'pending'" class="mb-4 text-center text-gray-500">
<p>正在等待支付结果请稍候...</p>
<p class="mt-1 text-sm">
<view v-if="paymentStatus === 'pending'" class="mb-4 text-center text-gray-500">
<text>
正在等待支付结果请稍候...
</text>
<text class="mt-1 text-sm">
已等待
{{
Math.floor(
@@ -400,47 +422,59 @@ defineExpose({
)
}}
</p>
</div>
</text>
</view>
<div class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
<div class="mb-4 flex justify-between">
<span class="text-gray-600">订单编号</span>
<span class="text-gray-800">{{ orderNo }}</span>
</div>
<div v-if="!isApiError" class="mb-4 flex justify-between">
<span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">订单状态</span>
<span :class="getStatusTextClass">{{
statusText
}}</span>
</div>
</div>
<view class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
<view class="mb-4 flex justify-between">
<text class="text-gray-600">
订单编号
</text>
<text class="text-gray-800">
{{ orderNo }}
</text>
</view>
<view v-if="!isApiError" class="mb-4 flex justify-between">
<text class="text-gray-600">
支付类型
</text>
<text class="text-gray-800">
{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}
</text>
</view>
<view class="flex justify-between">
<text class="text-gray-600">
订单状态
</text>
<text :class="getStatusTextClass">
{{
statusText
}}
</text>
</view>
</view>
<div class="message-box mb-6 rounded-lg bg-blue-50 p-4">
<p class="text-center" :class="getMessageClass">
<view class="message-box mb-6 rounded-lg bg-blue-50 p-4">
<text class="text-center" :class="getMessageClass">
{{ statusMessage }}
</p>
</div>
</text>
</view>
<div class="action-buttons grid grid-cols-2 gap-4">
<view class="action-buttons grid grid-cols-2 gap-4">
<wd-button block type="info" class="rounded-lg" @click="goHome">
返回首页
</wd-button>
<wd-button block type="primary" class="rounded-lg" @click="contactService">
联系客服
</wd-button>
</div>
</div>
</div>
</div>
</view>
</view>
</view>
</view>
</template>
<style scoped>

View File

@@ -0,0 +1,249 @@
<script setup lang="ts">
import { useAppBootstrap } from '@/composables/useAppBootstrap'
import { getPrivacyDecision, setPrivacyDecision } from '@/composables/usePrivacyConsent'
definePage({ layout: false })
const { bootstrap } = useAppBootstrap()
const handling = ref(false)
const rejectConfirming = ref(false)
function openPrivacyPolicy() {
uni.navigateTo({ url: '/pages/privacy-policy' })
}
async function handleAgree() {
if (handling.value)
return
handling.value = true
try {
setPrivacyDecision('accepted')
await bootstrap()
uni.reLaunch({ url: '/pages/index' })
}
finally {
handling.value = false
}
}
function handleReject() {
if (handling.value)
return
rejectConfirming.value = true
}
function handleAbandon() {
if (handling.value)
return
// 放弃使用不记录同意状态,避免下次启动绕过授权弹窗
uni.removeStorageSync('privacyDecision')
// #ifdef APP-PLUS
plus.runtime.quit()
// #endif
// #ifndef APP-PLUS
uni.reLaunch({ url: '/pages/privacy-consent' })
// #endif
}
onShow(() => {
const decision = getPrivacyDecision()
if (decision === 'accepted')
uni.reLaunch({ url: '/pages/index' })
})
</script>
<template>
<view class="privacy-page">
<view class="privacy-modal">
<template v-if="!rejectConfirming">
<view class="title">
欢迎使用赤眉 App
</view>
<view class="content">
我们将严格遵守法律法规保护您的个人信息请您在使用前阅读并同意
<text class="policy-link" @click="openPrivacyPolicy">
隐私政策
</text>
了解我们如何收集使用共享存储和保护您的信息
</view>
<view class="content">
我们仅在您同意后并在必要场景下使用对应权限主要包括
</view>
<view class="permissions">
<view class="permission-item">
1. 网络权限用于连接服务加载业务数据与提交操作请求
</view>
<view class="permission-item">
2. 存储权限用于缓存页面数据保存登录状态与图片文件
</view>
<view class="permission-item">
3. 设备信息权限用于运行稳定性安全风控与异常排查
</view>
</view>
<view class="hint">
点击同意并继续即表示您已阅读并同意隐私政策
</view>
<view class="actions">
<wd-button class="action-button agree-button" block :disabled="handling" @click="handleAgree">
同意并继续
</wd-button>
<wd-button class="text-button" custom-class="ghost-wd-btn" block plain :disabled="handling" @click="handleReject">
不同意
</wd-button>
</view>
</template>
<template v-else>
<view class="title">
需要您的同意才能继续使用
</view>
<view class="content">
如果您不同意隐私政策我们将无法继续为您提供赤眉相关服务
</view>
<view class="content">
我们会严格按照法律法规要求采取必要安全措施保护您的个人信息与隐私安全
</view>
<view class="content">
请您重新确认是否同意
<text class="policy-link" @click="openPrivacyPolicy">
隐私政策
</text>
</view>
<view class="actions">
<wd-button class="action-button agree-button" block :disabled="handling" @click="handleAgree">
同意
</wd-button>
<wd-button
class="text-button danger-text"
custom-class="ghost-wd-btn"
block
plain
:disabled="handling"
@click="handleAbandon"
>
放弃使用
</wd-button>
</view>
</template>
</view>
</view>
</template>
<style scoped>
.privacy-page {
min-height: 100vh;
background: #f5f7fb;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
box-sizing: border-box;
}
.privacy-modal {
width: 100%;
max-width: 720rpx;
height: 56vh;
min-height: 460px;
max-height: 620px;
background: #ffffff;
border-radius: 14px;
border: 1px solid #e5e7eb;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
padding: 20px 16px 14px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.title {
font-size: 18px;
line-height: 26px;
font-weight: 600;
color: #111827;
margin-bottom: 10px;
}
.content {
font-size: 14px;
line-height: 22px;
color: #1f2937;
margin-bottom: 8px;
}
.policy-link {
color: #1d4ed8;
text-decoration: underline;
}
.permissions {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 8px 10px;
margin: 8px 0 10px;
overflow-y: auto;
}
.permission-item {
font-size: 14px;
line-height: 20px;
color: #374151;
margin-bottom: 4px;
}
.permission-item:last-child {
margin-bottom: 0;
}
.hint {
color: #6b7280;
font-size: 12px;
line-height: 18px;
}
.actions {
margin-top: auto;
padding-top: 12px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
}
:deep(.action-button.wd-button) {
width: 100%;
height: 42px;
border-radius: 10px;
font-size: 15px;
font-weight: 500;
margin: 0;
}
:deep(.agree-button.wd-button) {
background: #2563eb;
color: #ffffff;
border-color: #2563eb;
}
:deep(.text-button.wd-button) {
width: 100%;
height: 34px;
border-color: transparent;
background: transparent !important;
color: #4b5563;
font-size: 14px;
line-height: 34px;
margin: 0;
padding: 0;
}
:deep(.danger-text.wd-button) {
color: #dc2626;
}
:deep(.ghost-wd-btn.wd-button) {
border-color: transparent;
}
</style>

View File

@@ -84,9 +84,13 @@ function selectProductType(reportTypeValue) {
}
function onConfirmType(e) {
const nextValue = e?.value?.[0] ?? e?.selectedOptions?.[0]?.value
if (nextValue)
selectProductType(nextValue)
// 单列 wd-picker 的 e.value 为标量;多列时为数组。不能用 e.value[0] 取标量,否则字符串会变成首字符。
const raw = e?.value
const nextValue = Array.isArray(raw) ? raw[0] : raw
const fromItem = e?.selectedItems
const resolved = nextValue ?? (Array.isArray(fromItem) ? fromItem[0]?.value : fromItem?.value)
if (resolved != null && resolved !== '')
selectProductType(resolved)
}
function onPriceChange(price) {
@@ -108,7 +112,7 @@ function openPricePicker() {
async function getPromoteConfig() {
loadingConfig.value = true
try {
const { data, error } = await useApiFetch('/agent/product_config').get().json()
const { data, error } = await useApiFetch('/agent/product_config', { silent: true }).get().json()
if (data.value && !error.value && data.value.code === 200) {
const list = data.value.data.AgentProductConfig || []
productConfig.value = list

View File

@@ -1,58 +0,0 @@
<script setup>
import { ref } from 'vue'
import InquireForm from '@/components/InquireForm.vue'
import LoginDialog from '@/components/LoginDialog.vue'
import { useRoute } from '@/composables/uni-router'
definePage({ layout: 'default' })
const route = useRoute()
const linkIdentifier = ref('')
const feature = ref('')
const featureData = ref({})
onLoad(async (query) => {
const q = query || {}
if (q.out_trade_no) {
uni.navigateTo({
url: `/pages/report-result-webview?out_trade_no=${encodeURIComponent(String(q.out_trade_no))}`,
})
return
}
await getProduct()
})
async function getProduct() {
linkIdentifier.value = route.params.linkIdentifier
const { data: agentLinkData, error: agentLinkError } = await useApiFetch(
`/agent/link?link_identifier=${linkIdentifier.value}`,
)
.get()
.json()
if (agentLinkData.value && !agentLinkError.value) {
if (agentLinkData.value.code === 200) {
feature.value = agentLinkData.value.data.product_en
featureData.value = agentLinkData.value.data
// 确保 FLXG0V4B 排在首位
if (
featureData.value.features
&& featureData.value.features.length > 0
) {
featureData.value.features.sort((a, b) => {
if (a.api_id === 'FLXG0V4B')
return -1
if (b.api_id === 'FLXG0V4B')
return 1
return 0
})
}
}
}
}
</script>
<template>
<InquireForm type="promotion" :feature="feature" :link-identifier="linkIdentifier" :feature-data="featureData" />
<LoginDialog />
</template>

View File

@@ -2,6 +2,7 @@
import { onMounted, ref } from 'vue'
import { useRoute } from '@/composables/uni-router'
import useApiFetch from '@/composables/useApiFetch'
definePage({ layout: 'default', auth: true })
const route = useRoute()
@@ -22,6 +23,7 @@ async function fetchRewardDetails() {
loading.value = true
const { data, error } = await useApiFetch(
`/agent/subordinate/contribution/detail?subordinate_id=${route.params.id}&page=${page.value}&page_size=${pageSize}`,
{ silent: true },
)
.get()
.json()
@@ -201,58 +203,58 @@ function formatNumber(num) {
</script>
<template>
<div class="reward-detail">
<view class="reward-detail">
<!-- 用户信息卡片 -->
<div class="p-4">
<div class="mb-4 rounded-xl bg-white p-5 shadow-sm">
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="text-xl text-gray-800 font-semibold">
<view class="p-4">
<view class="mb-4 rounded-xl bg-white p-5 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<view class="flex items-center space-x-3">
<view class="text-xl text-gray-800 font-semibold">
{{ userInfo.mobile }}
</div>
<span class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-600 font-medium">
</view>
<text class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-600 font-medium">
{{ userInfo.level }}代理
</span>
</div>
</div>
<div class="mb-4 text-sm text-gray-500">
</text>
</view>
</view>
<view class="mb-4 text-sm text-gray-500">
成为下级代理时间{{ formatTime(userInfo.createTime) }}
</div>
<div class="grid grid-cols-3 gap-4">
<div class="text-center">
<div class="mb-1 text-sm text-gray-500">
</view>
<view class="grid grid-cols-3 gap-4">
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总推广单量
</div>
<div class="text-xl text-blue-600 font-semibold">
</view>
<view class="text-xl text-blue-600 font-semibold">
{{ summary.totalOrders }}
</div>
</div>
<div class="text-center">
<div class="mb-1 text-sm text-gray-500">
</view>
</view>
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总收益
</div>
<div class="text-xl text-green-600 font-semibold">
</view>
<view class="text-xl text-green-600 font-semibold">
¥{{ formatNumber(summary.totalReward) }}
</div>
</div>
<div class="text-center">
<div class="mb-1 text-sm text-gray-500">
</view>
</view>
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总贡献
</div>
<div class="text-xl text-purple-600 font-semibold">
</view>
<view class="text-xl text-purple-600 font-semibold">
¥{{ formatNumber(summary.totalContribution) }}
</div>
</div>
</div>
</div>
</view>
</view>
</view>
</view>
<!-- 贡献统计卡片 -->
<div class="mb-4 rounded-xl bg-white p-4 shadow-sm">
<div class="mb-3 text-base text-gray-800 font-medium">
<view class="mb-4 rounded-xl bg-white p-4 shadow-sm">
<view class="mb-3 text-base text-gray-800 font-medium">
贡献统计
</div>
<div class="grid grid-cols-2 gap-3">
<div
</view>
<view class="grid grid-cols-2 gap-3">
<view
v-for="item in statistics"
:key="item.type"
class="flex items-center rounded-lg p-2"
@@ -263,62 +265,62 @@ function formatNumber(num) {
class="mr-2 text-lg"
:class="getRewardTypeClass(item.type).split(' ')[1]"
/>
<div class="flex-1">
<div class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
<view class="flex-1">
<view class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
{{ item.description }}
</div>
<div class="mt-1 flex items-center justify-between">
<div class="text-xs text-gray-500">
</view>
<view class="mt-1 flex items-center justify-between">
<view class="text-xs text-gray-500">
{{ item.count }}
</div>
<div class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
</view>
<view class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
¥{{ formatNumber(item.amount) }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mb-4 rounded-xl bg-white p-4 shadow-sm">
</view>
</view>
</view>
</view>
</view>
</view>
<view class="mb-4 rounded-xl bg-white p-4 shadow-sm">
<!-- 贡献记录列表 -->
<div class="text-base text-gray-800 font-medium">
<view class="text-base text-gray-800 font-medium">
贡献记录
</div>
<div class="detail-scroll p-4">
<div v-if="rewardDetails.length === 0" class="py-8 text-center text-gray-500">
</view>
<view class="detail-scroll p-4">
<view v-if="rewardDetails.length === 0" class="py-8 text-center text-gray-500">
暂无贡献记录
</div>
<div v-for="item in rewardDetails" v-else :key="item.id" class="reward-item">
<div class="mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
</view>
<view v-for="item in rewardDetails" v-else :key="item.id" class="reward-item">
<view class="mb-3 border-b border-gray-200 pb-3">
<view class="flex items-center justify-between">
<view class="flex items-center space-x-3">
<wd-icon
:name="getRewardTypeIcon(item.type)"
class="text-lg"
:class="getRewardTypeClass(item.type).split(' ')[1]"
/>
<div>
<div class="text-gray-800 font-medium">
<view>
<view class="text-gray-800 font-medium">
{{ getRewardTypeDescription(item.type) }}
</div>
<div class="text-xs text-gray-500">
</view>
<view class="text-xs text-gray-500">
{{ formatTime(item.create_time) }}
</div>
</div>
</div>
<div class="text-right">
<div class="text-base font-semibold" :class="getRewardTypeClass(item.type).split(' ')[1]">
</view>
</view>
</view>
<view class="text-right">
<view class="text-base font-semibold" :class="getRewardTypeClass(item.type).split(' ')[1]">
¥{{ formatNumber(item.amount) }}
</div>
</div>
</div>
</div>
</div>
<div v-if="loading" class="py-3 text-center text-sm text-gray-400">
</view>
</view>
</view>
</view>
</view>
<view v-if="loading" class="py-3 text-center text-sm text-gray-400">
加载中...
</div>
</div>
<div class="px-4 pb-4">
</view>
</view>
<view class="px-4 pb-4">
<wd-pagination
v-model="page"
:total="total"
@@ -327,10 +329,10 @@ function formatNumber(num) {
show-message
@change="onPageChange"
/>
</div>
</div>
</div>
</div>
</view>
</view>
</view>
</view>
</template>
<style scoped>

View File

@@ -1,6 +1,7 @@
<script setup>
import { onMounted, ref } from 'vue'
import useApiFetch from '@/composables/useApiFetch'
definePage({ layout: 'default', auth: true })
const subordinates = ref([])
@@ -18,7 +19,7 @@ async function fetchSubordinates() {
return
loading.value = true
const { data, error } = await useApiFetch(`/agent/subordinate/list?page=${page.value}&page_size=${pageSize}`)
const { data, error } = await useApiFetch(`/agent/subordinate/list?page=${page.value}&page_size=${pageSize}`, { silent: true })
.get()
.json()
if (data.value && !error.value) {
@@ -42,6 +43,11 @@ function formatNumber(num) {
return Number(num).toFixed(2)
}
/** 与后端 /agent/subordinate/list 一致level_name兼容历史 level 字段 */
function getLevelText(item) {
return item?.level_name || item?.level || '普通'
}
// 获取等级标签样式
function getLevelClass(level) {
switch (level) {
@@ -67,96 +73,97 @@ onMounted(() => {
</script>
<template>
<div class="subordinate-list">
<view class="subordinate-list">
<!-- 顶部统计卡片 -->
<div class="p-4 pb-0">
<div class="rounded-xl bg-white p-4 shadow-sm">
<div class="flex items-center justify-center">
<div class="text-center">
<div class="mb-1 text-sm text-gray-500">
<view class="p-4 pb-0">
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="flex items-center justify-center">
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
下级总数
</div>
<div class="text-2xl text-blue-600 font-semibold">
</view>
<view class="text-2xl text-blue-600 font-semibold">
{{ statistics.totalSubordinates }}
</div>
</div>
</div>
</div>
</div>
</view>
</view>
</view>
</view>
</view>
<div class="subordinate-scroll p-4">
<div v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item">
<div class="mb-4 flex flex-col rounded-xl bg-white p-5 shadow-sm">
<view class="subordinate-scroll p-4">
<view v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item">
<view class="mb-4 flex flex-col rounded-xl bg-white p-5 shadow-sm">
<!-- 顶部信息 -->
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div
<view class="mb-4 flex items-center justify-between">
<view class="flex items-center space-x-3">
<view
class="h-6 w-6 flex items-center justify-center rounded-full bg-blue-100 text-sm text-blue-600 font-medium"
>
{{ index + 1 }}
</div>
<div class="text-xl text-gray-800 font-semibold">
</view>
<view class="text-xl text-gray-800 font-semibold">
{{ item.mobile }}
</div>
<span class="rounded-full px-3 py-1 text-sm font-medium" :class="[getLevelClass(item.level)]">
{{ item.level ? item.level : '普通' }}代理
</span>
</div>
</div>
</view>
<text class="rounded-full px-3 py-1 text-sm font-medium" :class="[getLevelClass(getLevelText(item))]">
{{ getLevelText(item) }}代理
</text>
</view>
</view>
<!-- 加入时间 -->
<div class="mb-5 text-sm text-gray-500">
<view class="mb-5 text-sm text-gray-500">
成为下级代理时间{{ item.create_time }}
</div>
</view>
<!-- 数据统计 -->
<div class="grid grid-cols-3 mb-5 gap-6">
<div class="text-center">
<div class="mb-1 text-sm text-gray-500">
<view class="grid grid-cols-3 mb-5 gap-6">
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总推广单量
</div>
<div class="text-xl text-blue-600 font-semibold">
</view>
<view class="text-xl text-blue-600 font-semibold">
{{ item.total_orders }}
</div>
</div>
<div class="text-center">
<div class="mb-1 text-sm text-gray-500">
</view>
</view>
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总收益
</div>
<div class="text-xl text-green-600 font-semibold">
</view>
<view class="text-xl text-green-600 font-semibold">
¥{{ formatNumber(item.total_earnings) }}
</div>
</div>
<div class="text-center">
<div class="mb-1 text-sm text-gray-500">
</view>
</view>
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总贡献
</div>
<div class="text-xl text-purple-600 font-semibold">
</view>
<view class="text-xl text-purple-600 font-semibold">
¥{{ formatNumber(item.total_contribution) }}
</div>
</div>
</div>
</view>
</view>
</view>
<!-- 查看详情按钮 -->
<div class="flex justify-end">
<button
<view class="flex justify-end">
<wd-button
class="inline-flex items-center rounded-full from-blue-500 to-blue-400 bg-gradient-to-r px-4 py-2 text-sm text-white shadow-sm transition-all duration-200 hover:shadow-md"
custom-class="detail-wd-btn"
@click="viewDetail(item)"
>
<wd-icon name="view" custom-class="mr-1.5" />
查看详情
</button>
</div>
</div>
</div>
<div v-if="loading" class="py-4 text-center text-sm text-gray-400">
</wd-button>
</view>
</view>
</view>
<view v-if="loading" class="py-4 text-center text-sm text-gray-400">
加载中...
</div>
<div v-else-if="!subordinates.length" class="py-4 text-center text-sm text-gray-400">
</view>
<view v-else-if="!subordinates.length" class="py-4 text-center text-sm text-gray-400">
暂无下级代理
</div>
</div>
<div class="px-4 pb-4">
</view>
</view>
<view class="px-4 pb-4">
<wd-pagination
v-model="page"
:total="statistics.totalSubordinates"
@@ -165,8 +172,8 @@ onMounted(() => {
show-message
@change="onPageChange"
/>
</div>
</div>
</view>
</view>
</template>
<style scoped>
@@ -187,11 +194,11 @@ onMounted(() => {
transform: scale(0.98);
}
button {
:deep(.detail-wd-btn.wd-button) {
transition: all 0.2s ease;
}
button:hover {
:deep(.detail-wd-btn.wd-button:hover) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}

View File

@@ -91,6 +91,7 @@ async function getData() {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/withdrawal?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true },
)
.get()
.json()

View File

@@ -133,7 +133,7 @@ function onTabChange(name) {
// 加载银行卡信息
async function loadBankCardInfo() {
try {
const { data, error } = await useApiFetch('/agent/withdrawal/bank-card/info')
const { data, error } = await useApiFetch('/agent/withdrawal/bank-card/info', { silent: true })
.get()
.json()
if (data.value?.code === 200 && !error.value) {
@@ -162,7 +162,7 @@ function formatIdCard(idCard) {
}
async function getData() {
const { data: res, error } = await useApiFetch('/agent/revenue')
const { data: res, error } = await useApiFetch('/agent/revenue', { silent: true })
.get()
.json()
@@ -239,7 +239,7 @@ function openRealNameAuth() {
// 获取税务
async function getTax() {
const { data, error } = await useApiFetch('/agent/withdrawal/tax/exemption')
const { data, error } = await useApiFetch('/agent/withdrawal/tax/exemption', { silent: true })
.get()
.json()
if (data.value?.code === 200 && !error.value) {

View File

@@ -131,14 +131,6 @@
var(--color-danger-dark)
);
/* ===== Vant 兼容变量(用于历史样式平滑迁移) ===== */
--van-theme-primary: var(--color-primary);
--van-theme-primary-dark: var(--color-primary-700);
--van-theme-primary-light: var(--color-primary-light);
--van-text-color: var(--color-text-primary);
--van-text-color-2: var(--color-text-secondary);
--van-text-color-3: var(--color-text-tertiary);
--van-border-color: var(--color-border-primary);
}
/* ===== 暗色主题支持 ===== */

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
src/static/icons/20x20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

BIN
src/static/icons/29x29.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/static/icons/40x40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src/static/icons/58x58.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
src/static/icons/60x60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
src/static/icons/72x72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/static/icons/76x76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/static/icons/80x80.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
src/static/icons/87x87.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
src/static/icons/96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -35,7 +35,7 @@ export const useAgentStore = defineStore('agent', {
}
},
async fetchAgentStatus() {
const { data, error } = await useApiFetch('/agent/info').get().json()
const { data, error } = await useApiFetch('/agent/info', { silent: true }).get().json()
if (data.value && !error.value) {
if (data.value.code === 200) {
const d = data.value.data as Record<string, unknown>

View File

@@ -27,7 +27,7 @@ export const useUserStore = defineStore('user', {
}
},
async fetchUserInfo() {
const { data, error } = await useApiFetch('/user/detail').get().json()
const { data, error } = await useApiFetch('/user/detail', { silent: true }).get().json()
if (data.value && !error.value) {
if (data.value.code === 200) {
const userinfo = (data.value.data as { userInfo: Record<string, unknown> }).userInfo
@@ -63,19 +63,25 @@ export const useUserStore = defineStore('user', {
},
/** 与 webview `/user/mobileCodeLogin` 一致 */
async mobileCodeLogin(mobile: string, code: string) {
const { data, error } = await useApiFetch('/user/mobileCodeLogin')
.post({ mobile, code })
.json<{ accessToken: string, refreshAfter: string, accessExpire: string }>()
if (!data.value || error.value)
uni.showLoading({ title: '登录中...', mask: true })
try {
const { data, error } = await useApiFetch('/user/mobileCodeLogin', { silent: true })
.post({ mobile, code })
.json<{ accessToken: string, refreshAfter: string, accessExpire: string }>()
if (!data.value || error.value)
return data.value
if (data.value.code === 200) {
const d = data.value.data
setToken(d.accessToken)
uni.setStorageSync('refreshAfter', d.refreshAfter)
uni.setStorageSync('accessExpire', d.accessExpire)
await this.fetchUserInfo()
}
return data.value
if (data.value.code === 200) {
const d = data.value.data
setToken(d.accessToken)
uni.setStorageSync('refreshAfter', d.refreshAfter)
uni.setStorageSync('accessExpire', d.accessExpire)
await this.fetchUserInfo()
}
return data.value
finally {
uni.hideLoading()
}
},
},
})

3
src/uni-pages.d.ts vendored
View File

@@ -22,13 +22,14 @@ type _LocationUrl =
"/pages/inquire" |
"/pages/invitation-agent-apply" |
"/pages/invitation" |
"/pages/launch" |
"/pages/login" |
"/pages/me" |
"/pages/not-found" |
"/pages/payment-result" |
"/pages/privacy-consent" |
"/pages/privacy-policy" |
"/pages/promote" |
"/pages/promotion-inquire" |
"/pages/report-example-webview" |
"/pages/report-result-webview" |
"/pages/report-share" |

View File

@@ -1,252 +0,0 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS = CryptoJS || (function (u, p) {
const d = {}; const l = d.lib = {}; const s = function () { }; const t = l.Base = { extend(a) { s.prototype = this; const c = new s(); a && c.mixIn(a); c.hasOwnProperty('init') || (c.init = function () { c.$super.init.apply(this, arguments) }); c.init.prototype = c; c.$super = this; return c }, create() { const a = this.extend(); a.init.apply(a, arguments); return a }, init() { }, mixIn(a) { for (const c in a) a.hasOwnProperty(c) && (this[c] = a[c]); a.hasOwnProperty('toString') && (this.toString = a.toString) }, clone() { return this.init.prototype.extend(this) } }
var r = l.WordArray = t.extend({
init(a, c) { a = this.words = a || []; this.sigBytes = c != p ? c : 4 * a.length },
toString(a) { return (a || v).stringify(this) },
concat(a) {
const c = this.words; const e = a.words; const j = this.sigBytes; a = a.sigBytes; this.clamp(); if (j % 4) {
for (var k = 0; k < a; k++)c[j + k >>> 2] |= (e[k >>> 2] >>> 24 - 8 * (k % 4) & 255) << 24 - 8 * ((j + k) % 4)
}
else if (e.length > 65535) {
for (k = 0; k < a; k += 4)c[j + k >>> 2] = e[k >>> 2]
}
else {
c.push.apply(c, e)
} this.sigBytes += a; return this
},
clamp() {
const a = this.words; const c = this.sigBytes; a[c >>> 2] &= 4294967295
<< 32 - 8 * (c % 4); a.length = u.ceil(c / 4)
},
clone() { const a = t.clone.call(this); a.words = this.words.slice(0); return a },
random(a) { for (var c = [], e = 0; e < a; e += 4)c.push(4294967296 * u.random() | 0); return new r.init(c, a) },
}); const w = d.enc = {}; var v = w.Hex = {
stringify(a) { const c = a.words; a = a.sigBytes; for (var e = [], j = 0; j < a; j++) { const k = c[j >>> 2] >>> 24 - 8 * (j % 4) & 255; e.push((k >>> 4).toString(16)); e.push((k & 15).toString(16)) } return e.join('') },
parse(a) {
for (var c = a.length, e = [], j = 0; j < c; j += 2) {
e[j >>> 3] |= Number.parseInt(a.substr(j, 2), 16) << 24 - 4 * (j % 8)
} return new r.init(e, c / 2)
},
}; const b = w.Latin1 = { stringify(a) { const c = a.words; a = a.sigBytes; for (var e = [], j = 0; j < a; j++)e.push(String.fromCharCode(c[j >>> 2] >>> 24 - 8 * (j % 4) & 255)); return e.join('') }, parse(a) { for (var c = a.length, e = [], j = 0; j < c; j++)e[j >>> 2] |= (a.charCodeAt(j) & 255) << 24 - 8 * (j % 4); return new r.init(e, c) } }; const x = w.Utf8 = { stringify(a) {
try { return decodeURIComponent(escape(b.stringify(a))) }
catch (c) { throw new Error('Malformed UTF-8 data') }
}, parse(a) { return b.parse(unescape(encodeURIComponent(a))) } }
const q = l.BufferedBlockAlgorithm = t.extend({
reset() { this._data = new r.init(); this._nDataBytes = 0 },
_append(a) { typeof a == 'string' && (a = x.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes },
_process(a) { const c = this._data; const e = c.words; let j = c.sigBytes; const k = this.blockSize; var b = j / (4 * k); var b = a ? u.ceil(b) : u.max((b | 0) - this._minBufferSize, 0); a = b * k; j = u.min(4 * a, j); if (a) { for (var q = 0; q < a; q += k) this._doProcessBlock(e, q); q = e.splice(0, a); c.sigBytes -= j } return new r.init(q, j) },
clone() {
const a = t.clone.call(this)
a._data = this._data.clone(); return a
},
_minBufferSize: 0,
}); l.Hasher = q.extend({
cfg: t.extend(),
init(a) { this.cfg = this.cfg.extend(a); this.reset() },
reset() { q.reset.call(this); this._doReset() },
update(a) { this._append(a); this._process(); return this },
finalize(a) { a && this._append(a); return this._doFinalize() },
blockSize: 16,
_createHelper(a) { return function (b, e) { return (new a.init(e)).finalize(b) } },
_createHmacHelper(a) {
return function (b, e) {
return (new n.HMAC.init(a, e)).finalize(b)
}
},
}); var n = d.algo = {}; return d
}(Math));
(function () {
const u = CryptoJS; const p = u.lib.WordArray; u.enc.Base64 = {
stringify(d) {
let l = d.words; const p = d.sigBytes; const t = this._map; d.clamp(); d = []; for (let r = 0; r < p; r += 3) {
for (let w = (l[r >>> 2] >>> 24 - 8 * (r % 4) & 255) << 16 | (l[r + 1 >>> 2] >>> 24 - 8 * ((r + 1) % 4) & 255) << 8 | l[r + 2 >>> 2] >>> 24 - 8 * ((r + 2) % 4) & 255, v = 0; v < 4 && r + 0.75 * v < p; v++)d.push(t.charAt(w >>> 6 * (3 - v) & 63))
} if (l = t.charAt(64)) {
for (; d.length % 4;)d.push(l)
} return d.join('')
},
parse(d) {
let l = d.length; const s = this._map; var t = s.charAt(64); t && (t = d.indexOf(t), t != -1 && (l = t)); for (var t = [], r = 0, w = 0; w
< l; w++) {
if (w % 4) { const v = s.indexOf(d.charAt(w - 1)) << 2 * (w % 4); const b = s.indexOf(d.charAt(w)) >>> 6 - 2 * (w % 4); t[r >>> 2] |= (v | b) << 24 - 8 * (r % 4); r++ }
} return p.create(t, r)
},
_map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
}
})();
(function (u) {
function p(b, n, a, c, e, j, k) { b = b + (n & a | ~n & c) + e + k; return (b << j | b >>> 32 - j) + n } function d(b, n, a, c, e, j, k) { b = b + (n & c | a & ~c) + e + k; return (b << j | b >>> 32 - j) + n } function l(b, n, a, c, e, j, k) { b = b + (n ^ a ^ c) + e + k; return (b << j | b >>> 32 - j) + n } function s(b, n, a, c, e, j, k) { b = b + (a ^ (n | ~c)) + e + k; return (b << j | b >>> 32 - j) + n } for (var t = CryptoJS, r = t.lib, w = r.WordArray, v = r.Hasher, r = t.algo, b = [], x = 0; x < 64; x++)b[x] = 4294967296 * u.abs(u.sin(x + 1)) | 0; r = r.MD5 = v.extend({
_doReset() { this._hash = new w.init([1732584193, 4023233417, 2562383102, 271733878]) },
_doProcessBlock(q, n) {
for (var a = 0; a < 16; a++) { var c = n + a; var e = q[c]; q[c] = (e << 8 | e >>> 24) & 16711935 | (e << 24 | e >>> 8) & 4278255360 } var a = this._hash.words; var c = q[n + 0]; var e = q[n + 1]; const j = q[n + 2]; const k = q[n + 3]; const z = q[n + 4]; const r = q[n + 5]; const t = q[n + 6]; const w = q[n + 7]; const v = q[n + 8]; const A = q[n + 9]; const B = q[n + 10]; const C = q[n + 11]; const u = q[n + 12]; const D = q[n + 13]; const E = q[n + 14]; const x = q[n + 15]; var f = a[0]; var m = a[1]; var g = a[2]; var h = a[3]; var f = p(f, m, g, h, c, 7, b[0]); var h = p(h, f, m, g, e, 12, b[1]); var g = p(g, h, f, m, j, 17, b[2]); var m = p(m, g, h, f, k, 22, b[3]); var f = p(f, m, g, h, z, 7, b[4]); var h = p(h, f, m, g, r, 12, b[5]); var g = p(g, h, f, m, t, 17, b[6]); var m = p(m, g, h, f, w, 22, b[7])
var f = p(f, m, g, h, v, 7, b[8]); var h = p(h, f, m, g, A, 12, b[9]); var g = p(g, h, f, m, B, 17, b[10]); var m = p(m, g, h, f, C, 22, b[11]); var f = p(f, m, g, h, u, 7, b[12]); var h = p(h, f, m, g, D, 12, b[13]); var g = p(g, h, f, m, E, 17, b[14]); var m = p(m, g, h, f, x, 22, b[15]); var f = d(f, m, g, h, e, 5, b[16]); var h = d(h, f, m, g, t, 9, b[17]); var g = d(g, h, f, m, C, 14, b[18]); var m = d(m, g, h, f, c, 20, b[19]); var f = d(f, m, g, h, r, 5, b[20]); var h = d(h, f, m, g, B, 9, b[21]); var g = d(g, h, f, m, x, 14, b[22]); var m = d(m, g, h, f, z, 20, b[23]); var f = d(f, m, g, h, A, 5, b[24]); var h = d(h, f, m, g, E, 9, b[25]); var g = d(g, h, f, m, k, 14, b[26]); var m = d(m, g, h, f, v, 20, b[27]); var f = d(f, m, g, h, D, 5, b[28]); var h = d(h, f, m, g, j, 9, b[29]); var g = d(g, h, f, m, w, 14, b[30]); var m = d(m, g, h, f, u, 20, b[31]); var f = l(f, m, g, h, r, 4, b[32]); var h = l(h, f, m, g, v, 11, b[33]); var g = l(g, h, f, m, C, 16, b[34]); var m = l(m, g, h, f, E, 23, b[35]); var f = l(f, m, g, h, e, 4, b[36]); var h = l(h, f, m, g, z, 11, b[37]); var g = l(g, h, f, m, w, 16, b[38]); var m = l(m, g, h, f, B, 23, b[39]); var f = l(f, m, g, h, D, 4, b[40]); var h = l(h, f, m, g, c, 11, b[41]); var g = l(g, h, f, m, k, 16, b[42]); var m = l(m, g, h, f, t, 23, b[43]); var f = l(f, m, g, h, A, 4, b[44]); var h = l(h, f, m, g, u, 11, b[45]); var g = l(g, h, f, m, x, 16, b[46]); var m = l(m, g, h, f, j, 23, b[47]); var f = s(f, m, g, h, c, 6, b[48]); var h = s(h, f, m, g, w, 10, b[49]); var g = s(g, h, f, m, E, 15, b[50]); var m = s(m, g, h, f, r, 21, b[51]); var f = s(f, m, g, h, u, 6, b[52]); var h = s(h, f, m, g, k, 10, b[53]); var g = s(g, h, f, m, B, 15, b[54]); var m = s(m, g, h, f, e, 21, b[55]); var f = s(f, m, g, h, v, 6, b[56]); var h = s(h, f, m, g, x, 10, b[57]); var g = s(g, h, f, m, t, 15, b[58]); var m = s(m, g, h, f, D, 21, b[59]); var f = s(f, m, g, h, z, 6, b[60]); var h = s(h, f, m, g, C, 10, b[61]); var g = s(g, h, f, m, j, 15, b[62]); var m = s(m, g, h, f, A, 21, b[63]); a[0] = a[0] + f | 0; a[1] = a[1] + m | 0; a[2] = a[2] + g | 0; a[3] = a[3] + h | 0
},
_doFinalize() {
let b = this._data; let n = b.words; let a = 8 * this._nDataBytes; let c = 8 * b.sigBytes; n[c >>> 5] |= 128 << 24 - c % 32; const e = u.floor(a
/ 4294967296); n[(c + 64 >>> 9 << 4) + 15] = (e << 8 | e >>> 24) & 16711935 | (e << 24 | e >>> 8) & 4278255360; n[(c + 64 >>> 9 << 4) + 14] = (a << 8 | a >>> 24) & 16711935 | (a << 24 | a >>> 8) & 4278255360; b.sigBytes = 4 * (n.length + 1); this._process(); b = this._hash; n = b.words; for (a = 0; a < 4; a++)c = n[a], n[a] = (c << 8 | c >>> 24) & 16711935 | (c << 24 | c >>> 8) & 4278255360; return b
},
clone() { const b = v.clone.call(this); b._hash = this._hash.clone(); return b },
}); t.MD5 = v._createHelper(r); t.HmacMD5 = v._createHmacHelper(r)
})(Math);
(function () {
const u = CryptoJS; var p = u.lib; const d = p.Base; const l = p.WordArray; var p = u.algo; const s = p.EvpKDF = d.extend({ cfg: d.extend({ keySize: 4, hasher: p.MD5, iterations: 1 }), init(d) { this.cfg = this.cfg.extend(d) }, compute(d, r) { for (var p = this.cfg, s = p.hasher.create(), b = l.create(), u = b.words, q = p.keySize, p = p.iterations; u.length < q;) { n && s.update(n); var n = s.update(d).finalize(r); s.reset(); for (let a = 1; a < p; a++)n = s.finalize(n), s.reset(); b.concat(n) } b.sigBytes = 4 * q; return b } }); u.EvpKDF = function (d, l, p) {
return s.create(p).compute(d, l)
}
})()
CryptoJS.lib.Cipher || (function (u) {
var p = CryptoJS; const d = p.lib; const l = d.Base; const s = d.WordArray; const t = d.BufferedBlockAlgorithm; const r = p.enc.Base64; const w = p.algo.EvpKDF; const v = d.Cipher = t.extend({
cfg: l.extend(),
createEncryptor(e, a) { return this.create(this._ENC_XFORM_MODE, e, a) },
createDecryptor(e, a) { return this.create(this._DEC_XFORM_MODE, e, a) },
init(e, a, b) { this.cfg = this.cfg.extend(b); this._xformMode = e; this._key = a; this.reset() },
reset() { t.reset.call(this); this._doReset() },
process(e) { this._append(e); return this._process() },
finalize(e) { e && this._append(e); return this._doFinalize() },
keySize: 4,
ivSize: 4,
_ENC_XFORM_MODE: 1,
_DEC_XFORM_MODE: 2,
_createHelper(e) { return { encrypt(b, k, d) { return (typeof k == 'string' ? c : a).encrypt(e, b, k, d) }, decrypt(b, k, d) { return (typeof k == 'string' ? c : a).decrypt(e, b, k, d) } } },
}); d.StreamCipher = v.extend({ _doFinalize() { return this._process(!0) }, blockSize: 1 }); var b = p.mode = {}; const x = function (e, a, b) {
let c = this._iv; c ? this._iv = u : c = this._prevBlock; for (let d = 0; d < b; d++) {
e[a + d]
^= c[d]
}
}; let q = (d.BlockCipherMode = l.extend({ createEncryptor(e, a) { return this.Encryptor.create(e, a) }, createDecryptor(e, a) { return this.Decryptor.create(e, a) }, init(e, a) { this._cipher = e; this._iv = a } })).extend(); q.Encryptor = q.extend({ processBlock(e, a) { const b = this._cipher; const c = b.blockSize; x.call(this, e, a, c); b.encryptBlock(e, a); this._prevBlock = e.slice(a, a + c) } }); q.Decryptor = q.extend({
processBlock(e, a) {
const b = this._cipher; const c = b.blockSize; const d = e.slice(a, a + c); b.decryptBlock(e, a); x.call(this, e, a, c); this._prevBlock = d
},
}); b = b.CBC = q; q = (p.pad = {}).Pkcs7 = { pad(a, b) { for (var c = 4 * b, c = c - a.sigBytes % c, d = c << 24 | c << 16 | c << 8 | c, l = [], n = 0; n < c; n += 4)l.push(d); c = s.create(l, c); a.concat(c) }, unpad(a) { a.sigBytes -= a.words[a.sigBytes - 1 >>> 2] & 255 } }; d.BlockCipher = v.extend({
cfg: v.cfg.extend({ mode: b, padding: q }),
reset() {
v.reset.call(this); var a = this.cfg; const b = a.iv; var a = a.mode; if (this._xformMode == this._ENC_XFORM_MODE)
var c = a.createEncryptor; else c = a.createDecryptor, this._minBufferSize = 1; this._mode = c.call(a, this, b && b.words)
},
_doProcessBlock(a, b) { this._mode.processBlock(a, b) },
_doFinalize() {
const a = this.cfg.padding; if (this._xformMode == this._ENC_XFORM_MODE) { a.pad(this._data, this.blockSize); var b = this._process(!0) }
else {
b = this._process(!0), a.unpad(b)
} return b
},
blockSize: 4,
}); const n = d.CipherParams = l.extend({ init(a) { this.mixIn(a) }, toString(a) { return (a || this.formatter).stringify(this) } }); var b = (p.format = {}).OpenSSL = {
stringify(a) {
const b = a.ciphertext; a = a.salt; return (a
? s.create([1398893684, 1701076831]).concat(a).concat(b)
: b).toString(r)
},
parse(a) { a = r.parse(a); const b = a.words; if (b[0] == 1398893684 && b[1] == 1701076831) { var c = s.create(b.slice(2, 4)); b.splice(0, 4); a.sigBytes -= 16 } return n.create({ ciphertext: a, salt: c }) },
}; var a = d.SerializableCipher = l.extend({
cfg: l.extend({ format: b }),
encrypt(a, b, c, d) { d = this.cfg.extend(d); let l = a.createEncryptor(c, d); b = l.finalize(b); l = l.cfg; return n.create({ ciphertext: b, key: c, iv: l.iv, algorithm: a, mode: l.mode, padding: l.padding, blockSize: a.blockSize, formatter: d.format }) },
decrypt(a, b, c, d) { d = this.cfg.extend(d); b = this._parse(b, d.format); return a.createDecryptor(c, d).finalize(b.ciphertext) },
_parse(a, b) { return typeof a == 'string' ? b.parse(a, this) : a },
}); var p = (p.kdf = {}).OpenSSL = { execute(a, b, c, d) { d || (d = s.random(8)); a = w.create({ keySize: b + c }).compute(a, d); c = s.create(a.words.slice(b), 4 * c); a.sigBytes = 4 * b; return n.create({ key: a, iv: c, salt: d }) } }; var c = d.PasswordBasedCipher = a.extend({
cfg: a.cfg.extend({ kdf: p }),
encrypt(b, c, d, l) {
l = this.cfg.extend(l); d = l.kdf.execute(d, b.keySize, b.ivSize); l.iv = d.iv; b = a.encrypt.call(this, b, c, d.key, l); b.mixIn(d); return b
},
decrypt(b, c, d, l) { l = this.cfg.extend(l); c = this._parse(c, l.format); d = l.kdf.execute(d, b.keySize, b.ivSize, c.salt); l.iv = d.iv; return a.decrypt.call(this, b, c, d.key, l) },
})
}());
(function () {
for (var u = CryptoJS, p = u.lib.BlockCipher, d = u.algo, l = [], s = [], t = [], r = [], w = [], v = [], b = [], x = [], q = [], n = [], a = [], c = 0; c < 256; c++)a[c] = c < 128 ? c << 1 : c << 1 ^ 283; for (var e = 0, j = 0, c = 0; c < 256; c++) { var k = j ^ j << 1 ^ j << 2 ^ j << 3 ^ j << 4; var k = k >>> 8 ^ k & 255 ^ 99; l[e] = k; s[k] = e; const z = a[e]; const F = a[z]; const G = a[F]; let y = 257 * a[k] ^ 16843008 * k; t[e] = y << 24 | y >>> 8; r[e] = y << 16 | y >>> 16; w[e] = y << 8 | y >>> 24; v[e] = y; y = 16843009 * G ^ 65537 * F ^ 257 * z ^ 16843008 * e; b[k] = y << 24 | y >>> 8; x[k] = y << 16 | y >>> 16; q[k] = y << 8 | y >>> 24; n[k] = y; e ? (e = z ^ a[a[a[G ^ z]]], j ^= a[a[j]]) : e = j = 1 } const H = [0, 1, 2, 4, 8, 16, 32, 64, 128, 27, 54]; var d = d.AES = p.extend({
_doReset() {
for (var a = this._key, c = a.words, d = a.sigBytes / 4, a = 4 * ((this._nRounds = d + 6) + 1), e = this._keySchedule = [], j = 0; j < a; j++) {
if (j < d) {
e[j] = c[j]
}
else { var k = e[j - 1]; j % d ? d > 6 && j % d == 4 && (k = l[k >>> 24] << 24 | l[k >>> 16 & 255] << 16 | l[k >>> 8 & 255] << 8 | l[k & 255]) : (k = k << 8 | k >>> 24, k = l[k >>> 24] << 24 | l[k >>> 16 & 255] << 16 | l[k >>> 8 & 255] << 8 | l[k & 255], k ^= H[j / d | 0] << 24); e[j] = e[j - d] ^ k }
} c = this._invKeySchedule = []; for (d = 0; d < a; d++) {
j = a - d, k = d % 4 ? e[j] : e[j - 4], c[d] = d < 4 || j <= 4
? k
: b[l[k >>> 24]] ^ x[l[k >>> 16 & 255]] ^ q[l[k
>>> 8 & 255]] ^ n[l[k & 255]]
}
},
encryptBlock(a, b) { this._doCryptBlock(a, b, this._keySchedule, t, r, w, v, l) },
decryptBlock(a, c) { let d = a[c + 1]; a[c + 1] = a[c + 3]; a[c + 3] = d; this._doCryptBlock(a, c, this._invKeySchedule, b, x, q, n, s); d = a[c + 1]; a[c + 1] = a[c + 3]; a[c + 3] = d },
_doCryptBlock(a, b, c, d, e, j, l, f) {
for (var m = this._nRounds, g = a[b] ^ c[0], h = a[b + 1] ^ c[1], k = a[b + 2] ^ c[2], n = a[b + 3] ^ c[3], p = 4, r = 1; r < m; r++) {
var q = d[g >>> 24] ^ e[h >>> 16 & 255] ^ j[k >>> 8 & 255] ^ l[n & 255] ^ c[p++]; var s = d[h >>> 24] ^ e[k >>> 16 & 255] ^ j[n >>> 8 & 255] ^ l[g & 255] ^ c[p++]; var t
= d[k >>> 24] ^ e[n >>> 16 & 255] ^ j[g >>> 8 & 255] ^ l[h & 255] ^ c[p++]; var n = d[n >>> 24] ^ e[g >>> 16 & 255] ^ j[h >>> 8 & 255] ^ l[k & 255] ^ c[p++]; var g = q; var h = s; var k = t
} q = (f[g >>> 24] << 24 | f[h >>> 16 & 255] << 16 | f[k >>> 8 & 255] << 8 | f[n & 255]) ^ c[p++]; s = (f[h >>> 24] << 24 | f[k >>> 16 & 255] << 16 | f[n >>> 8 & 255] << 8 | f[g & 255]) ^ c[p++]; t = (f[k >>> 24] << 24 | f[n >>> 16 & 255] << 16 | f[g >>> 8 & 255] << 8 | f[h & 255]) ^ c[p++]; n = (f[n >>> 24] << 24 | f[g >>> 16 & 255] << 16 | f[h >>> 8 & 255] << 8 | f[k & 255]) ^ c[p++]; a[b] = q; a[b + 1] = s; a[b + 2] = t; a[b + 3] = n
},
keySize: 8,
}); u.AES = p._createHelper(d)
})()
CryptoJS.encrypt = function (word, key, iv) {
return encrypt(word, key, iv)
}
CryptoJS.decrypt = function (word, key, iv) {
return decrypt(word, key, iv)
}
/**
* 加密
* word原密码
* key key
* iv iv
*/
function encrypt(word, key, iv) {
key = CryptoJS.enc.Utf8.parse(key)
iv = CryptoJS.enc.Utf8.parse(iv)
const encrypted = CryptoJS.AES.encrypt(word, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
})
return encrypted.toString()
}
/**
* 解密
* word加密后的密码
* key key
* iv iv
*/
function decrypt(word, key, iv) {
key = CryptoJS.enc.Utf8.parse(key)
iv = CryptoJS.enc.Utf8.parse(iv)
let decrypted = CryptoJS.AES.decrypt(word, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
})
decrypted = CryptoJS.enc.Utf8.stringify(decrypted)
return decrypted
}
/**
* Electronic Codebook block mode.
*/
CryptoJS.mode.ECB = (function () {
const ECB = CryptoJS.lib.BlockCipherMode.extend()
ECB.Encryptor = ECB.extend({
processBlock(words, offset) {
this._cipher.encryptBlock(words, offset)
},
})
ECB.Decryptor = ECB.extend({
processBlock(words, offset) {
this._cipher.decryptBlock(words, offset)
},
})
return ECB
}())
/**
* @example
* var CryptoJS = require('./util/aes.js')
* var key = CryptoJS.enc.Utf8.parse("key");
* var iv = CryptoJS.enc.Utf8.parse("iv");
* var pwd = CryptoJS.encrypt(this.data.pwdVal, key, iv)
* var original = CryptoJS.encrypt(pwd, key, iv)
*/
export default CryptoJS

View File

@@ -1,25 +0,0 @@
import Crypto from '@/utils/chatCrypto'
// 秘钥转换成utf8格式字符串用于加密解密一般长度是16位由后端提供
const keyStr = import.meta.env.VITE_CHAT_AES_KEY
if (!keyStr)
throw new Error('缺少环境变量: VITE_CHAT_AES_KEY')
const key = Crypto.enc.Utf8.parse(keyStr)
// 偏移量转换成utf8格式字符串一般长度是16位(由后端提供)
const ivStr = import.meta.env.VITE_CHAT_AES_IV
if (!ivStr)
throw new Error('缺少环境变量: VITE_CHAT_AES_IV')
const iv = Crypto.enc.Utf8.parse(ivStr)
// 加密使用CBC模式
export default function Encrypt(value) {
// 使用外部包中的AES的加密方法
// value(加密内容)、key(密钥)
const encrypt = Crypto.AES.encrypt(value, key, {
iv, // 偏移量
mode: Crypto.mode.CBC, // 模式(五种加密模式)
padding: Crypto.pad.Pkcs7, // 填充
})
// 将加密的内容转成字符串返回出去
return encrypt.toString()
}

View File

@@ -1,6 +1,7 @@
/**
* 轻量请求(非链式)。与 `useApiFetch` 共用 env行为尽量一致。
*/
import { hasAcceptedPrivacyPolicy } from '@/composables/usePrivacyConsent'
import { envConfig } from '@/constants/env'
import { clearAuthStorage, getToken } from '@/utils/storage'
import { navigateLogin } from './navigate'
@@ -25,6 +26,10 @@ function joinUrl(path: string) {
export function request<T>(options: RequestOptions) {
return new Promise<ApiResponse<T>>((resolve, reject) => {
if (!hasAcceptedPrivacyPolicy()) {
reject(new Error('用户未同意隐私政策,禁止请求'))
return
}
const token = getToken()
uni.request({
url: joinUrl(options.url),

View File

@@ -1,115 +0,0 @@
/**
* 简化版缩放适配工具
*/
class ZoomAdapter {
constructor() {
// ===== 可调整的配置参数 =====
this.maxZoom = 3 // 触发调整的缩放阈值默认3倍
this.targetZoom = 2 // 调整后的目标缩放默认2倍
this.isInitialized = false
}
init() {
if (typeof window === 'undefined' || typeof document === 'undefined')
return
if (this.isInitialized)
return
// 绑定事件
window.addEventListener('resize', () => this.checkZoom())
window.addEventListener('orientationchange', () => {
setTimeout(() => this.checkZoom(), 500)
})
// 防止双击缩放
let lastTouchEnd = 0
document.addEventListener('touchend', (event) => {
const now = Date.now()
if (now - lastTouchEnd <= 300) {
event.preventDefault()
}
lastTouchEnd = now
}, false)
this.checkZoom()
this.isInitialized = true
}
getCurrentZoom() {
return Math.max(
window.outerWidth / window.innerWidth,
window.devicePixelRatio,
window.screen.width / window.innerWidth,
)
}
checkZoom() {
const zoom = this.getCurrentZoom()
if (zoom > this.maxZoom) {
this.adjust(zoom)
}
else {
this.reset()
}
}
adjust(zoom) {
try {
// 计算调整比例:目标缩放 / 当前缩放
const ratio = this.targetZoom / zoom
// 应用调整
document.body.style.transform = `scale(${ratio})`
document.body.style.transformOrigin = 'top left'
document.body.style.width = `${100 / ratio}%`
document.body.style.height = `${100 / ratio}%`
}
catch (e) {
console.warn('缩放调整失败:', e)
}
}
reset() {
try {
document.body.style.transform = ''
document.body.style.transformOrigin = ''
document.body.style.width = ''
document.body.style.height = ''
}
catch (e) {
console.warn('缩放重置失败:', e)
}
}
addZoomStyles() {
const style = document.createElement('style')
style.id = 'zoom-adapter-styles'
style.textContent = `
.zoom-adaptive {
font-size: 16px !important;
line-height: 1.5 !important;
}
.zoom-adaptive input,
.zoom-adaptive button,
.zoom-adaptive select,
.zoom-adaptive textarea {
font-size: 16px !important;
min-height: 44px !important;
padding: 8px 12px !important;
}
.zoom-adaptive img {
max-width: 100% !important;
height: auto !important;
}
.zoom-adaptive table {
max-width: 100% !important;
overflow-x: auto !important;
}
`
document.head.appendChild(style)
}
}
export default new ZoomAdapter()
export { ZoomAdapter }