This commit is contained in:
2025-03-14 04:07:32 +08:00
commit cf0e3e4c57
163 changed files with 20591 additions and 0 deletions

37
.cursorrules Normal file
View File

@@ -0,0 +1,37 @@
// Uniapp Vue 3 best practices
const vue3CompositionApiBestPractices = [
"Use setup() function for component logic",
"Utilize ref and reactive for reactive state",
"Implement computed properties with computed()",
"Use watch and watchEffect for side effects",
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
"Utilize provide/inject for dependency injection",
];
// Folder structure
const folderStructure = `
src/
components/
composables/
views/
static/
ui/
App.vue
main.ts
`;
// Additional instructions
const additionalInstructions = `
1. Follow the uniapp vue3 version
2. Pay attention to the compatibility of mobile APP
3. Implement proper props and emits definitions
4. Utilize Vue 3's Teleport component when needed
5. Use Suspense for async components
6. Implement proper error handling
7. Follow Vue 3 style guide and naming conventions
8. Use Vite for fast development and building
`;

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

5
.env Normal file
View File

@@ -0,0 +1,5 @@
# 基础环境变量配置
VITE_APP_BASE_URL=https://www.quannengcha.com
VITE_APP_SHARE_URL=https://www.tianyuandata.com
# 默认环境配置
VITE_APP_DEBUG=false

5
.env.development Normal file
View File

@@ -0,0 +1,5 @@
# 开发环境配置
VITE_APP_BASE_URL=https://www.quannengcha.com
# 是否启用调试模式
VITE_APP_DEBUG=true

5
.env.production Normal file
View File

@@ -0,0 +1,5 @@
# 生产环境配置
VITE_APP_BASE_URL=https://www.quannengcha.com
# 关闭调试模式
VITE_APP_DEBUG=false

BIN
.github/images/preview.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

84
.github/workflows/check.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: Check
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
strategy:
matrix:
node_version: [18.x, 20.x, 22.x]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
cache: pnpm
- run: pnpm i
- run: pnpm run lint
typecheck:
strategy:
matrix:
node_version: [18.x, 20.x, 22.x]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
cache: pnpm
- run: pnpm i
- run: pnpm run type-check
build:
strategy:
matrix:
node_version: [18.x, 20.x, 22.x]
os: [ubuntu-latest, windows-latest, macos-latest]
build_cmd: [build, 'build:mp-weixin', 'build:app']
fail-fast: false
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
cache: pnpm
- run: pnpm i
- run: pnpm run ${{ matrix.build_cmd }}

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
./src/components.d.ts
./src/auto-imports.d.ts

15
.hbuilderx/launch.json Normal file
View File

@@ -0,0 +1,15 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-ios"
},
{
"app-plus" : {
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

3
.npmrc Normal file
View File

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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
20

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

@@ -0,0 +1,11 @@
{
"recommendations": [
"antfu.vite",
"antfu.iconify",
"antfu.unocss",
"vue.volar",
"dbaeumer.vscode-eslint",
"editorConfig.editorConfig",
"uni-helper.uni-helper-vscode"
]
}

16
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug h5",
"type": "chrome",
"runtimeArgs": [
"--remote-debugging-port=9222"
],
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}",
"preLaunchTask": "uni:h5"
}
]
}

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

@@ -0,0 +1,55 @@
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"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": {
"vite.config.*": "pages.config.*, manifest.config.*, uno.config.*, volar.config.*, *.env, .env.*"
}
}

16
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "uni:h5",
"type": "npm",
"script": "dev --devtools",
"isBackground": true,
"problemMatcher": "$vite",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-PRESENT KeJun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

31
README.md Normal file
View File

@@ -0,0 +1,31 @@
<p align="center">
<img src="https://github.com/uni-helper/vitesse-uni-app/raw/main/.github/images/preview.png" width="300"/>
</p>
<h2 align="center">
Vitesse for uni-app
</h2>
<p align="center">
<a href="https://vitesse-uni-app.netlify.app/">📱 在线预览</a>
<a href="https://vitesse-docs.netlify.app/">📖 阅读文档</a>
</p>
## 特性
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - 就是快!
- 🗂 [基于文件的路由](./src/pages)
- 📦 [组件自动化加载](./src/components)
- 📑 [布局系统](./src/layouts)
- 🎨 [UnoCSS](https://github.com/unocss/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
- 😃 [各种图标集为你所用](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
- 🔥 使用 [新的 `<script setup>` 语法](https://github.com/vuejs/rfcs/pull/227)
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
- 🦾 [TypeScript](https://www.typescriptlang.org/) & [ESLint](https://eslint.org/) - 保证代码质量

BIN
deploy/android/qnc.keystore Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFyzCCBLOgAwIBAgIQJWS27ihJedi6JX9bSk4CoTANBgkqhkiG9w0BAQsFADB1
MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBD
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzMxEzARBgNVBAoMCkFw
cGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTI0MTEyMDA2MjAyNVoXDTI1MTEyMDA2
MjAyNFowgaYxGjAYBgoJkiaJk/IsZAEBDAo3NlE3TThXRjZLMTAwLgYDVQQDDCdp
UGhvbmUgRGV2ZWxvcGVyOiBmdXNpIGxpdSAoM01WNzRUOEtUNSkxEzARBgNVBAsM
ClI0RFY3VkY2SDcxNDAyBgNVBAoMK0hhaW5hbiBYdWV5dXNpIE5ldHdvcmsgVGVj
aG5vbG9neSBDby4sIEx0ZC4xCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA3km9CiYZty9RJhn7X0hWZYi9rmfDFsSQjtDCxCQV9W7O
miLHSZIU3o+xTyr+X+9egXL2DIgMVpn8BDxNgC8BY6AMvhlBy8yHCbK2EJPrutqX
78CYY9G6Cre9R4OxOiUBsW34O0bObqagBC1POknQpL3Cla/7S6Kn7cjcIb4Dllco
r/n25Mr8rFVZEHgiOgMHju0hn5QzR/wVcA61vRfEZManHsB+MmKI323mq7pyV6D5
aLcG2m65OHi4HIz/eEm7as5NG4xIyYIQsfEgwAAZYsrpwzLRIg9uh7QsBmYr/NRZ
SJlK9gBPTnrPQ517RRnIA2As//ZoIG8bi59VSmUPQQIDAQABo4ICIzCCAh8wDAYD
VR0TAQH/BAIwADAfBgNVHSMEGDAWgBQJ/sAVkPmvZAqSErkmKGMMl+ynsjBwBggr
BgEFBQcBAQRkMGIwLQYIKwYBBQUHMAKGIWh0dHA6Ly9jZXJ0cy5hcHBsZS5jb20v
d3dkcmczLmRlcjAxBggrBgEFBQcwAYYlaHR0cDovL29jc3AuYXBwbGUuY29tL29j
c3AwMy13d2RyZzMwMzCCAR4GA1UdIASCARUwggERMIIBDQYJKoZIhvdjZAUBMIH/
MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUg
YnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBs
aWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2Vy
dGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRl
bWVudHMuMDcGCCsGAQUFBwIBFitodHRwczovL3d3dy5hcHBsZS5jb20vY2VydGlm
aWNhdGVhdXRob3JpdHkvMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQW
BBTX2EgxLl+irOnOYtlgQwBwxgvpWTAOBgNVHQ8BAf8EBAMCB4AwEwYKKoZIhvdj
ZAYBAgEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBACb1PyLRXf66cTo5D7Grn5Ll
BveYJO7Vn7b3rZgelm22O5fCuYTlpckwtFES1r4FG5GF+oTyd7RoSajbpBT8shI+
HPVcnJ++9aP1y/5EgpmKQno8IwqdXxGW+TKNsh4/PdAbVB5qUpiCxqRowAVHTHZI
fTiJT9kvvazA0wBeVQla59j1zC/UaZYoziiCDSLsfl7Kkd7UcwBxcgPTo2S+E6dU
ihA6H36Ef/7QLJwInDzN+zjIEsof7k/VNaet/Ub6CGOTKg8fAb3wakGVT547idE7
TjjCym78euH6lc+tNAnomqzntB1Pa3NkiW8PHP5mSL3RFaKhzbJu7FE8kCw/T/E=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDeSb0KJhm3L1Em
GftfSFZliL2uZ8MWxJCO0MLEJBX1bs6aIsdJkhTej7FPKv5f716BcvYMiAxWmfwE
PE2ALwFjoAy+GUHLzIcJsrYQk+u62pfvwJhj0boKt71Hg7E6JQGxbfg7Rs5upqAE
LU86SdCkvcKVr/tLoqftyNwhvgOWVyiv+fbkyvysVVkQeCI6AweO7SGflDNH/BVw
DrW9F8RkxqcewH4yYojfbearunJXoPlotwbabrk4eLgcjP94Sbtqzk0bjEjJghCx
8SDAABliyunDMtEiD26HtCwGZiv81FlImUr2AE9Oes9DnXtFGcgDYCz/9mggbxuL
n1VKZQ9BAgMBAAECggEAK0qKHf1gmbwF3llGCv8AEIMqidpcdMUwcBf5LpDmk+eS
JhOq40mnddbfTc9OPinUqnpUsAkY+g7ANaT25QWCjEAB603o8gNxszJSYfUk/j2P
RJqqMxqH1ImHO/PKYswNQdxscULvdj5OQLIAUZZJo6PvTw64brkNYnJxaMLkWPVg
gNqmFMwNlpCUlYdMbARrBf+aLWuaXHcb9PmkwkAjrEVS87AErJFRmyiOX+LZcaYp
YxUCp0DVmcyWeGrm0Jj3ai1PSxC9PjthMVWL1G84BQR5pb9ITpZJHypO0lY2sOM0
5NjQo0C/iTqKjGbItbHjuHo/QZNzzVEd7gLMIqHY6QKBgQD8BxKFz+piwtA2fAGV
xEuXA2oglYTn6baLS26PsJcfcgfgre1jTbmr8AQTph3U7kqwW+Q1QF6BTiaDUqZd
y7HzC9dVLgqPbyshLWd9V+3xG+9Q+MvKjX4cWzT687bGBLixidy2svyse70BWDZx
fC/5yIbI9dNSFmMXGrxg1Ia9HQKBgQDhyqrUhrWSIKyI0b17X/Kbh3pLs6XRaLto
Q8hMZnsrlX1GH+T6kb5KaGe3txfo7UVqoYeUWhTGOnxmsNywRAyV5Ob0Ebq4a7oK
K2Dtzx+yZVrYralxqxcMU1vr322j51zBWNT1f2ig0/ae78xijvIuYG/gDokRwGsp
6k7o6z5VdQKBgQDIjfg3c9TY8p4uPVAllidw7M6vT0yMIHebMVOyFJyt8VhDeyTF
OTzfjggweqiSoW3eGcHofmi2DTKonkS1rzrUZj/dEN5mddikdjU8kZRPoiM3FEVX
0nDgr9PhP7/apDc1ULfKy3Q27eDiCQiKFoE3DFP152lakWYZJLVzeKwgIQKBgQCY
0/vTBCcL9I+zv/t38ZadzRJrAPrghfordY2yoL8H3hRADcCIFXPum0JwqD5Hy6nr
pc/GCz76eYXQYC/QIw89ugTU+NcgktIGphviewpiTpYyCUGijYJ5NkcGnWFxyl0P
BZiHXqisCyxnobn+j/C+kUnIJy6B1HL0mQK2RtwXLQKBgQDDcfTi/UusJvrrHp6O
DZlnP7lMWvOrKHUH2alDq6+YK3ieJ+e6btv3TxX2Y6CtbeKutjax3HQ/MTMwkeT8
4mItZ+oHZcnYUKjYmDDKqxyUZLqdzuylNKd57gXl/BkfZBLk+MbgH9+zAb1VsnMz
/8t+U5kmczy90XBU3OOigpWj+g==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIDnTCCAoUCAQAwgfcxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAjCusKjw4TDjzER
MA8GA1UEBwwIwrrCo8K/w5oxQTA/BgNVBAoMOMK6wqPDhMOPw4rCocORwqfDk8Ou
w4vCvMONw7jDgsOnwr/DhsK8wrzDk8OQw4/DnsK5wqvDi8K+MUEwPwYDVQQLDDjC
usKjw4TDj8OKwqHDkcKnw5PDrsOLwrzDjcO4w4LDp8K/w4bCvMK8w5PDkMOPw57C
ucKrw4vCvjEbMBkGA1UEAwwSY29tLmFsbGlub25lLmNoZWNrMR8wHQYJKoZIhvcN
AQkBFhBhZG1pbkBpaWVlaWkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA3km9CiYZty9RJhn7X0hWZYi9rmfDFsSQjtDCxCQV9W7OmiLHSZIU3o+x
Tyr+X+9egXL2DIgMVpn8BDxNgC8BY6AMvhlBy8yHCbK2EJPrutqX78CYY9G6Cre9
R4OxOiUBsW34O0bObqagBC1POknQpL3Cla/7S6Kn7cjcIb4Dllcor/n25Mr8rFVZ
EHgiOgMHju0hn5QzR/wVcA61vRfEZManHsB+MmKI323mq7pyV6D5aLcG2m65OHi4
HIz/eEm7as5NG4xIyYIQsfEgwAAZYsrpwzLRIg9uh7QsBmYr/NRZSJlK9gBPTnrP
Q517RRnIA2As//ZoIG8bi59VSmUPQQIDAQABoGAwFQYJKoZIhvcNAQkHMQgMBjMy
MDMyMDBHBgkqhkiG9w0BCQIxOgw4wrrCo8OEw4/DisKhw5HCp8OTw67Di8K8w43D
uMOCw6fCv8OGwrzCvMOTw5DDj8OewrnCq8OLwr4wDQYJKoZIhvcNAQELBQADggEB
AI1CQFPrHE3xVWpnxynxK4DjM7eRjjCkPVUd3/IqNfiLnRNatDRzdU4K/Y5bLSVk
A4eEk479YeBw1Zyw2jlAvK0Ga0QLAE4/SWAvjCbSCqJqSe1/SR9skxaZO3DZECPG
EAgdiNsmYYlg7Oae3OT77OAFn6GkhjpRFBADO4F4Yb5yj75njNNava7gHvMn9NMx
12/IvIK1quS2V7XjcLaMIVkIfnYqGevbOnN3XGgwjGu2hCJz4Ob0iAI+kcNYdBrG
7rdwfSW7AbCSFiHmRL4n5rjJN6rs4VEHMnOPoB05E2inpIzLxZPa0wJ2k0UR5zZL
KKAkSMxpPrUvJyCnoRhX8BM=
-----END CERTIFICATE REQUEST-----

Binary file not shown.

View File

@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF8TCCBNmgAwIBAgIQLaG9i7JFETJnqW6Z8lt9hTANBgkqhkiG9w0BAQsFADB1
MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBD
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzMxEzARBgNVBAoMCkFw
cGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTI0MTEyMTE4NTczMVoXDTI1MTEyMTE4
NTczMFowgcwxGjAYBgoJkiaJk/IsZAEBDApSNERWN1ZGNkg3MVYwVAYDVQQDDE1p
UGhvbmUgRGlzdHJpYnV0aW9uOiBIYWluYW4gWHVleXVzaSBOZXR3b3JrIFRlY2hu
b2xvZ3kgQ28uLCBMdGQuIChSNERWN1ZGNkg3KTETMBEGA1UECwwKUjREVjdWRjZI
NzE0MDIGA1UECgwrSGFpbmFuIFh1ZXl1c2kgTmV0d29yayBUZWNobm9sb2d5IENv
LiwgTHRkLjELMAkGA1UEBhMCQ04wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDeSb0KJhm3L1EmGftfSFZliL2uZ8MWxJCO0MLEJBX1bs6aIsdJkhTej7FP
Kv5f716BcvYMiAxWmfwEPE2ALwFjoAy+GUHLzIcJsrYQk+u62pfvwJhj0boKt71H
g7E6JQGxbfg7Rs5upqAELU86SdCkvcKVr/tLoqftyNwhvgOWVyiv+fbkyvysVVkQ
eCI6AweO7SGflDNH/BVwDrW9F8RkxqcewH4yYojfbearunJXoPlotwbabrk4eLgc
jP94Sbtqzk0bjEjJghCx8SDAABliyunDMtEiD26HtCwGZiv81FlImUr2AE9Oes9D
nXtFGcgDYCz/9mggbxuLn1VKZQ9BAgMBAAGjggIjMIICHzAMBgNVHRMBAf8EAjAA
MB8GA1UdIwQYMBaAFAn+wBWQ+a9kCpISuSYoYwyX7KeyMHAGCCsGAQUFBwEBBGQw
YjAtBggrBgEFBQcwAoYhaHR0cDovL2NlcnRzLmFwcGxlLmNvbS93d2RyZzMuZGVy
MDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLXd3ZHJn
MzAyMIIBHgYDVR0gBIIBFTCCAREwggENBgkqhkiG92NkBQEwgf8wgcMGCCsGAQUF
BwICMIG2DIGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZpY2F0ZSBieSBhbnkgcGFy
dHkgYXNzdW1lcyBhY2NlcHRhbmNlIG9mIHRoZSB0aGVuIGFwcGxpY2FibGUgc3Rh
bmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNlLCBjZXJ0aWZpY2F0ZSBw
b2xpY3kgYW5kIGNlcnRpZmljYXRpb24gcHJhY3RpY2Ugc3RhdGVtZW50cy4wNwYI
KwYBBQUHAgEWK2h0dHBzOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhv
cml0eS8wFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYEFNfYSDEuX6Ks
6c5i2WBDAHDGC+lZMA4GA1UdDwEB/wQEAwIHgDATBgoqhkiG92NkBgEEAQH/BAIF
ADANBgkqhkiG9w0BAQsFAAOCAQEANk6dzI9SJXj35krhx6WhXcQJoE8fZ6gNq9hW
SvpLjffZyn9143zKGZ93izi10H1EwAbBHrZhiIW5Tl7PB6e7ZmUWPxuL/Syex9Js
In9DnZp5XEHtbFZVQnffQqmIh0ZwmM40wgCMaOxRqtw/Vvlovb/K0sWmDXTbN1z0
lFqALTqFbsfMDj6uE2Xl0RUn2Qc6XxqV7yh/qI9IYkMicrAmoOAhKTxX98ny7ioR
et7F0aDF+CKG4ZceQt4QeYm9EnPHYt3DWBu3wIiwJJ+YCaclPPFK3+TgB64yNT7M
0p4n7okj3vS5I7SnCy8wcYndluByp7G0pSAzLLP2eLO2I4oDyA==
-----END CERTIFICATE-----

Binary file not shown.

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDeSb0KJhm3L1Em
GftfSFZliL2uZ8MWxJCO0MLEJBX1bs6aIsdJkhTej7FPKv5f716BcvYMiAxWmfwE
PE2ALwFjoAy+GUHLzIcJsrYQk+u62pfvwJhj0boKt71Hg7E6JQGxbfg7Rs5upqAE
LU86SdCkvcKVr/tLoqftyNwhvgOWVyiv+fbkyvysVVkQeCI6AweO7SGflDNH/BVw
DrW9F8RkxqcewH4yYojfbearunJXoPlotwbabrk4eLgcjP94Sbtqzk0bjEjJghCx
8SDAABliyunDMtEiD26HtCwGZiv81FlImUr2AE9Oes9DnXtFGcgDYCz/9mggbxuL
n1VKZQ9BAgMBAAECggEAK0qKHf1gmbwF3llGCv8AEIMqidpcdMUwcBf5LpDmk+eS
JhOq40mnddbfTc9OPinUqnpUsAkY+g7ANaT25QWCjEAB603o8gNxszJSYfUk/j2P
RJqqMxqH1ImHO/PKYswNQdxscULvdj5OQLIAUZZJo6PvTw64brkNYnJxaMLkWPVg
gNqmFMwNlpCUlYdMbARrBf+aLWuaXHcb9PmkwkAjrEVS87AErJFRmyiOX+LZcaYp
YxUCp0DVmcyWeGrm0Jj3ai1PSxC9PjthMVWL1G84BQR5pb9ITpZJHypO0lY2sOM0
5NjQo0C/iTqKjGbItbHjuHo/QZNzzVEd7gLMIqHY6QKBgQD8BxKFz+piwtA2fAGV
xEuXA2oglYTn6baLS26PsJcfcgfgre1jTbmr8AQTph3U7kqwW+Q1QF6BTiaDUqZd
y7HzC9dVLgqPbyshLWd9V+3xG+9Q+MvKjX4cWzT687bGBLixidy2svyse70BWDZx
fC/5yIbI9dNSFmMXGrxg1Ia9HQKBgQDhyqrUhrWSIKyI0b17X/Kbh3pLs6XRaLto
Q8hMZnsrlX1GH+T6kb5KaGe3txfo7UVqoYeUWhTGOnxmsNywRAyV5Ob0Ebq4a7oK
K2Dtzx+yZVrYralxqxcMU1vr322j51zBWNT1f2ig0/ae78xijvIuYG/gDokRwGsp
6k7o6z5VdQKBgQDIjfg3c9TY8p4uPVAllidw7M6vT0yMIHebMVOyFJyt8VhDeyTF
OTzfjggweqiSoW3eGcHofmi2DTKonkS1rzrUZj/dEN5mddikdjU8kZRPoiM3FEVX
0nDgr9PhP7/apDc1ULfKy3Q27eDiCQiKFoE3DFP152lakWYZJLVzeKwgIQKBgQCY
0/vTBCcL9I+zv/t38ZadzRJrAPrghfordY2yoL8H3hRADcCIFXPum0JwqD5Hy6nr
pc/GCz76eYXQYC/QIw89ugTU+NcgktIGphviewpiTpYyCUGijYJ5NkcGnWFxyl0P
BZiHXqisCyxnobn+j/C+kUnIJy6B1HL0mQK2RtwXLQKBgQDDcfTi/UusJvrrHp6O
DZlnP7lMWvOrKHUH2alDq6+YK3ieJ+e6btv3TxX2Y6CtbeKutjax3HQ/MTMwkeT8
4mItZ+oHZcnYUKjYmDDKqxyUZLqdzuylNKd57gXl/BkfZBLk+MbgH9+zAb1VsnMz
/8t+U5kmczy90XBU3OOigpWj+g==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIDnTCCAoUCAQAwgfcxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAjCusKjw4TDjzER
MA8GA1UEBwwIwrrCo8K/w5oxQTA/BgNVBAoMOMK6wqPDhMOPw4rCocORwqfDk8Ou
w4vCvMONw7jDgsOnwr/DhsK8wrzDk8OQw4/DnsK5wqvDi8K+MUEwPwYDVQQLDDjC
usKjw4TDj8OKwqHDkcKnw5PDrsOLwrzDjcO4w4LDp8K/w4bCvMK8w5PDkMOPw57C
ucKrw4vCvjEbMBkGA1UEAwwSY29tLmFsbGlub25lLmNoZWNrMR8wHQYJKoZIhvcN
AQkBFhBhZG1pbkBpaWVlaWkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA3km9CiYZty9RJhn7X0hWZYi9rmfDFsSQjtDCxCQV9W7OmiLHSZIU3o+x
Tyr+X+9egXL2DIgMVpn8BDxNgC8BY6AMvhlBy8yHCbK2EJPrutqX78CYY9G6Cre9
R4OxOiUBsW34O0bObqagBC1POknQpL3Cla/7S6Kn7cjcIb4Dllcor/n25Mr8rFVZ
EHgiOgMHju0hn5QzR/wVcA61vRfEZManHsB+MmKI323mq7pyV6D5aLcG2m65OHi4
HIz/eEm7as5NG4xIyYIQsfEgwAAZYsrpwzLRIg9uh7QsBmYr/NRZSJlK9gBPTnrP
Q517RRnIA2As//ZoIG8bi59VSmUPQQIDAQABoGAwFQYJKoZIhvcNAQkHMQgMBjMy
MDMyMDBHBgkqhkiG9w0BCQIxOgw4wrrCo8OEw4/DisKhw5HCp8OTw67Di8K8w43D
uMOCw6fCv8OGwrzCvMOTw5DDj8OewrnCq8OLwr4wDQYJKoZIhvcNAQELBQADggEB
AI1CQFPrHE3xVWpnxynxK4DjM7eRjjCkPVUd3/IqNfiLnRNatDRzdU4K/Y5bLSVk
A4eEk479YeBw1Zyw2jlAvK0Ga0QLAE4/SWAvjCbSCqJqSe1/SR9skxaZO3DZECPG
EAgdiNsmYYlg7Oae3OT77OAFn6GkhjpRFBADO4F4Yb5yj75njNNava7gHvMn9NMx
12/IvIK1quS2V7XjcLaMIVkIfnYqGevbOnN3XGgwjGu2hCJz4Ob0iAI+kcNYdBrG
7rdwfSW7AbCSFiHmRL4n5rjJN6rs4VEHMnOPoB05E2inpIzLxZPa0wJ2k0UR5zZL
KKAkSMxpPrUvJyCnoRhX8BM=
-----END CERTIFICATE REQUEST-----

View File

@@ -0,0 +1,5 @@
2. 司法涉诉
4. 个人不良
股东人企关联
6. 借贷意向
7. 特殊名单

5
doc/婚恋报告.md Normal file
View File

@@ -0,0 +1,5 @@
2. 司法涉诉
3. 个人不良
4. 人企关联
5. 借贷意向
6. 单人婚姻状态

4
doc/家政服务.md Normal file
View File

@@ -0,0 +1,4 @@
2. 司法涉诉
3. 特殊名单
4. 个人不良
5. 借贷意向

4
doc/租赁服务.md Normal file
View File

@@ -0,0 +1,4 @@
2. 司法涉诉
4. 个人不良
5. 借贷意向
6. 特殊名单

View File

@@ -0,0 +1,7 @@
2. 司法涉诉
4. 个人不良
5. 股东人企关联
6. 借贷意向
7. 特殊名单
8. 借贷行为
9. 单人婚姻

7
doc/贷前背调.md Normal file
View File

@@ -0,0 +1,7 @@
2. 司法涉诉
4. 个人不良
5. 借贷行为
6. 借贷意向
7. 特殊名单
9. 婚姻状态
股东人企关联

View File

@@ -0,0 +1,53 @@
# 环境变量配置说明
## 环境变量文件
本项目使用 Vite 的环境变量功能,支持以下环境变量文件:
- `.env`:所有环境都会加载的默认变量
- `.env.development`:开发环境变量(`npm run dev` 时加载)
- `.env.production`:生产环境变量(`npm run build` 时加载)
## 已配置的环境变量
| 变量名 | 说明 | 示例值 |
|--------|------|--------|
| VITE_APP_BASE_URL | API 基础URL | https://www.quannengcha.com |
| VITE_APP_DEBUG | 调试模式 | true/false |
## 在项目中使用环境变量
### 在 Vue 组件中使用
```vue
<script setup>
// 使用 import.meta.env 访问环境变量
const baseUrl = import.meta.env.VITE_APP_BASE_URL
const isDebug = import.meta.env.VITE_APP_DEBUG === 'true'
</script>
```
### 在 JS/TS 文件中使用
```js
// 使用 import.meta.env 访问环境变量
const baseUrl = import.meta.env.VITE_APP_BASE_URL
const isDebug = import.meta.env.VITE_APP_DEBUG === 'true'
// 获取当前环境模式
const mode = import.meta.env.MODE
```
## 获取当前环境
```js
// 获取当前环境模式development、production等
const mode = import.meta.env.MODE
```
## 注意事项
1. 所有环境变量必须以 `VITE_` 开头才能在客户端代码中访问
2. 环境变量默认为字符串类型,需要自行转换为其他类型(如布尔值)
3. 修改环境变量后需要重启开发服务器才能生效
4. 不要在环境变量文件中存储敏感信息如API密钥等

21
index.html Normal file
View File

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

161
manifest.config.ts Normal file
View File

@@ -0,0 +1,161 @@
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
export default defineManifestConfig({
'name': '全能查',
'appid': '__UNI__CC3DA09',
'description': '',
'versionName': '1.0.0',
'versionCode': '104',
'transformPx': false,
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
background: '#000000',
compatible: {
ignoreVersion: true,
},
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {
Payment: {},
Share: {},
},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
package: 'com.quannengcha.app',
permissions: [
'<uses-permission android:name="android.permission.INTERNET"/>',
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
],
},
/* ios打包配置 */
ios: {
privacyDescription: {
NSLocalNetworkUsageDescription: '需要访问您的网络来提供更好的服务',
},
idfa: false,
bundleIdentifier: 'com.allinone.check',
},
/* SDK配置 */
sdkConfigs: {
payment: {
appleiap: {
inAppPurchase: true,
},
// #ifdef APP-ANDROID
alipay: {
__platform__: ['ios', 'android'],
},
// #endif
},
// share: {
// weixin: {
// appid: 'wx开头的微信开放平台AppID',
// UniversalLinks: 'https://www.quannengcha.com/app/',
// }
// },
// oauth: {
// weixin: {
// appid: 'wx开头的微信开放平台AppID',
// appsecret: '微信开放平台AppSecret',
// UniversalLinks: 'https://www.quannengcha.com/app/'
// }
// }
},
icons: {
android: {
hdpi: 'src/static/icons/72x72.png',
xhdpi: 'src/static/icons/96x96.png',
xxhdpi: 'src/static/icons/144x144.png',
xxxhdpi: 'src/static/icons/192x192.png',
},
ios: {
appstore: 'src/static/icons/1024x1024.png',
ipad: {
'app': 'src/static/icons/76x76.png',
'app@2x': 'src/static/icons/152x152.png',
'notification': 'src/static/icons/20x20.png',
'notification@2x': 'src/static/icons/40x40.png',
'proapp@2x': 'src/static/icons/167x167.png',
'settings': 'src/static/icons/29x29.png',
'settings@2x': 'src/static/icons/58x58.png',
'spotlight': 'src/static/icons/40x40.png',
'spotlight@2x': 'src/static/icons/80x80.png',
},
iphone: {
'app@2x': 'src/static/icons/120x120.png',
'app@3x': 'src/static/icons/180x180.png',
'notification@2x': 'src/static/icons/40x40.png',
'notification@3x': 'src/static/icons/60x60.png',
'settings@2x': 'src/static/icons/58x58.png',
'settings@3x': 'src/static/icons/87x87.png',
'spotlight@2x': 'src/static/icons/80x80.png',
'spotlight@3x': 'src/static/icons/120x120.png',
},
},
},
},
},
/* 快应用特有相关 */
'quickapp': {},
/* 小程序特有相关 */
'mp-weixin': {
appid: '',
setting: {
urlCheck: false,
},
usingComponents: true,
darkmode: false,
themeLocation: 'theme.json',
},
'mp-alipay': {
usingComponents: true,
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
'h5': {
darkmode: false,
themeLocation: 'theme.json',
},
'uniStatistics': {
enable: false,
},
'vueVersion': '3',
/* UTS 插件配置 */
'uts': {
'plugins': {
'webview': {
'version': '1.0.0',
'description': 'Web视图插件支持在App内打开网页',
'platforms': {
'android': {
'appid': '__UNI_WEBVIEW_ANDROID',
'autostart': false
},
'ios': {
'appid': '__UNI_WEBVIEW_IOS',
'autostart': false
}
}
}
}
}
})

105
package.json Normal file
View File

@@ -0,0 +1,105 @@
{
"name": "uni-qnc",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@9.9.0",
"license": "MIT",
"scripts": {
"dev": "uni",
"dev:app": "uni -p app",
"dev:app-plus": "uni -p app-plus",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build": "uni build",
"build:app": "uni build -p app",
"build:app-plus": "uni build -p app-plus",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4050520250307001",
"@dcloudio/uni-app-harmony": "3.0.0-4050520250307001",
"@dcloudio/uni-app-plus": "3.0.0-4050520250307001",
"@dcloudio/uni-components": "3.0.0-4050520250307001",
"@dcloudio/uni-h5": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-alipay": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-baidu": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-jd": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-lark": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-qq": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-weixin": "3.0.0-4050520250307001",
"@dcloudio/uni-mp-xhs": "3.0.0-4050520250307001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4050520250307001",
"@rollup/rollup-win32-x64-msvc": "^4.27.4",
"@vant/area-data": "^2.0.0",
"@vueuse/core": "^11.1.0",
"crypto-js": "^4.2.0",
"pinia": "^3.0.1",
"qrcode": "^1.5.4",
"uqrcodejs": "^4.0.7",
"vue": "~3.4.21",
"vue-i18n": "^9.14.1",
"wot-design-uni": "^1.3.12"
},
"devDependencies": {
"@dcloudio/types": "^3.4.14",
"@dcloudio/uni-automator": "3.0.0-4050520250307001",
"@dcloudio/uni-cli-shared": "3.0.0-4050520250307001",
"@dcloudio/uni-stacktracey": "3.0.0-4050520250307001",
"@dcloudio/uni-vue-devtools": "3.0.0-4020420240722002",
"@dcloudio/vite-plugin-uni": "3.0.0-4050520250307001",
"@iconify-json/carbon": "^1.2.3",
"@mini-types/alipay": "^3.0.14",
"@types/node": "^20.16.12",
"@uni-helper/eslint-config": "^0.1.0",
"@uni-helper/uni-env": "^0.1.4",
"@uni-helper/uni-types": "1.0.0-alpha.4",
"@uni-helper/unocss-preset-uni": "^0.2.10",
"@uni-helper/vite-plugin-uni-components": "^0.1.0",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.10",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.7",
"@uni-helper/vite-plugin-uni-pages": "^0.2.28",
"@uni-helper/volar-service-uni-pages": "^0.2.28",
"@unocss/eslint-config": "^0.62.4",
"@vue/runtime-core": "^3.5.12",
"@vue/tsconfig": "^0.5.1",
"miniprogram-api-typings": "^3.12.3",
"sass": "~1.79.0",
"sass-embedded": "~1.79.0",
"typescript": "~5.5.4",
"unocss": "^0.62.4",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^28.4.1",
"vite": "^5.2.8",
"vue-tsc": "^2.1.6"
}
}

44
pages.config.ts Normal file
View File

@@ -0,0 +1,44 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
pages: [],
globalStyle: {
backgroundColor: '@bgColor',
backgroundColorBottom: '@bgColorBottom',
backgroundColorTop: '@bgColorTop',
backgroundTextStyle: '@bgTxtStyle',
navigationBarBackgroundColor: '#000000',
navigationBarTextStyle: '@navTxtStyle',
navigationBarTitleText: '全能查',
navigationStyle: 'custom',
},
tabBar: {
backgroundColor: '@tabBgColor',
borderStyle: '@tabBorderStyle',
color: '@tabFontColor',
selectedColor: '@tabSelectedColor',
list: [
{
pagePath: 'pages/index',
text: '',
visible: false,
},
{
pagePath: 'pages/agent',
text: '',
visible: false,
},
{
pagePath: 'pages/ai',
text: '',
visible: false,
},
{
pagePath: 'pages/me',
text: '',
visible: false,
},
],
},
})

12209
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["github>uni-helper/renovate-config"]
}

88
src/App.vue Normal file
View File

@@ -0,0 +1,88 @@
<script setup>
import { onLaunch } from '@dcloudio/uni-app'
import { refreshToken, getUserInfo } from '@/api/apis'
import { getAgentInfo } from '@/apis/agent'
onLaunch(() => {
refreshTokenIfNeeded()
getUser()
getAgentInformation()
})
const refreshTokenIfNeeded = async () => {
const token = uni.getStorageSync("token")
const refreshAfter = uni.getStorageSync("refreshAfter")
const accessExpire = uni.getStorageSync("accessExpire")
const currentTime = new Date().getTime()
// 如果 token 已过期,直接返回
if (accessExpire) {
const accessExpireInMilliseconds = parseInt(accessExpire) * 1000 // 转换为毫秒级
if (currentTime > accessExpireInMilliseconds) {
return
}
}
// 如果没有 token直接返回
if (!token) {
return
}
// 如果有 refreshAfter检查当前时间是否超过 refreshAfter
if (refreshAfter) {
const refreshAfterInMilliseconds = parseInt(refreshAfter) * 1000 // 转换为毫秒级
if (currentTime < refreshAfterInMilliseconds) {
return
}
}
// 如果没有 refreshAfter 或者时间超过 refreshAfter执行刷新 token 的请求
try {
const res = await refreshToken()
if (res.code === 200) {
uni.setStorageSync("token", res.data.accessToken)
uni.setStorageSync("refreshAfter", res.data.refreshAfter)
uni.setStorageSync("accessExpire", res.data.accessExpire)
}
} catch (error) {
console.error('刷新token失败', error)
}
}
const getAgentInformation = async () => {
const token = uni.getStorageSync("token")
if (!token) {
return
}
try {
const res = await getAgentInfo()
if (res.code === 200 && res.data) {
// 将代理信息存入缓存
uni.setStorageSync("agentInfo", {
level: res.data.level,
isAgent: res.data.is_agent, // 判断是否是代理
status: res.data.status, // 获取代理状态 0=待审核1=审核通过2=审核未通过3=未申请
agentID: res.data.agent_id,
mobile: res.data.mobile
})
console.log('代理信息已获取并存入缓存')
}
} catch (error) {
console.error('获取代理信息失败', error)
}
}
const getUser = async () => {
const token = uni.getStorageSync("token")
if (!token) {
return
}
const res = await getUserInfo()
if (res.code === 200) {
console.log(res.data)
uni.setStorageSync("userInfo", res.data.userInfo)
}
}
</script>

135
src/api/apis.js Normal file
View File

@@ -0,0 +1,135 @@
// api/index.js
import request from '@/utils/request.js'
export function getUserInfo(data) {
return request({
url: '/user/detail',
method: 'GET',
data,
})
}
export function login(data) {
return request({
url: '/user/mobileCodeLogin',
method: 'POST',
data,
})
}
export function getCode(data) {
return request({
url: '/auth/sendSms',
method: 'POST',
data,
})
}
export function getProduct(en) {
return request({
url: `/product/en/${en}`,
method: 'GET',
})
}
export function queryExample(params) {
return request({
url: `/query/example`,
method: 'GET',
params,
})
}
export function queryMarriage(data) {
return request({
url: `/query/marriage`,
method: 'POST',
data,
})
}
export function queryhomeService(data) {
return request({
url: `/query/homeService`,
method: 'POST',
data,
})
}
export function queryriskAssessment(data) {
return request({
url: `/query/riskAssessment`,
method: 'POST',
data,
})
}
export function querycompanyInfo(data) {
return request({
url: `/query/companyInfo`,
method: 'POST',
data,
})
}
export function queryrentalInfo(data) {
return request({
url: `/query/rentalInfo`,
method: 'POST',
data,
})
}
export function querypreLoanBackgroundCheck(data) {
return request({
url: `/query/preLoanBackgroundCheck`,
method: 'POST',
data,
})
}
export function querybackgroundCheck(data) {
return request({
url: `/query/backgroundCheck`,
method: 'POST',
data,
})
}
export function queryResultByOrder(orderID) {
return request({
url: `/query/orderId/${orderID}`,
method: 'GET',
})
}
export function queryList(params) {
return request({
url: `/query/list`,
method: 'GET',
params,
})
}
export function queryProvisionalOrder(id) {
return request({
url: `/query/provisional_order/${id}`,
method: 'GET',
})
}
export function payment(data) {
return request({
url: `/pay/payment`,
method: 'POST',
data,
})
}
export function iapPaymentCallback(data) {
return request({
url: `/pay/iap_callback`,
method: 'POST',
data,
})
}
export function getAgentRevenue() {
return request({
url: '/agent/revenue',
method: 'GET'
})
}
export function refreshToken() {
return request({
url: '/user/getToken',
method: 'POST',
});
}

138
src/apis/agent.js Normal file
View File

@@ -0,0 +1,138 @@
// 代理相关API
import request from '@/utils/request'
/**
* 获取代理信息
*/
export const getAgentInfo = () => {
return request({
url: '/agent/info',
method: 'GET'
})
}
/**
* 获取代理佣金列表
* @param {Object} params 查询参数 {page, page_size}
*/
export const getAgentCommission = (params) => {
return request({
url: '/agent/commission',
method: 'GET',
params
})
}
/**
* 获取代理奖励列表
* @param {Object} params 查询参数 {page, page_size}
*/
export const getAgentRewards = (params) => {
return request({
url: '/agent/rewards',
method: 'GET',
params
})
}
/**
* 获取代理状态
*/
export const getAgentStatus = () => {
return request({
url: '/agent/status',
method: 'GET'
})
}
/**
* 代理申请
* @param {Object} data 申请数据 {region, mobile, wechat_id, code, ancestor?}
*/
export const applyAgent = (data) => {
return request({
url: '/agent/apply',
method: 'POST',
data
})
}
/**
* 获取代理会员用户配置
* @param {Object} params 查询参数 {product_id}
*/
export const getAgentMembershipUserConfig = (params) => {
return request({
url: '/agent/membership/user_config',
method: 'GET',
params
})
}
/**
* 保存代理会员用户配置
* @param {Object} data 配置数据
*/
export const saveAgentMembershipUserConfig = (data) => {
return request({
url: '/agent/membership/save_user_config',
method: 'POST',
data
})
}
/**
* 获取产品配置
*/
export const getProductConfig = () => {
return request({
url: '/agent/product_config',
method: 'GET'
})
}
/**
* 生成推广链接
* @param {Object} data 推广数据 {product, price}
*/
export const generatePromotionLink = (data) => {
return request({
url: '/agent/generating_link',
method: 'POST',
data
})
}
/**
* 获取代理收益
*/
export const getAgentRevenue = () => {
return request({
url: '/agent/revenue',
method: 'GET'
})
}
/**
* 代理提现
* @param {Object} data 提现数据 {payee_account, amount, payee_name}
*/
export const agentWithdrawal = (data) => {
return request({
url: '/agent/withdrawal',
method: 'POST',
data
})
}
/**
* 获取提现记录
* @param {Object} params 查询参数 {page, page_size}
*/
export const getWithdrawalRecords = (params) => {
return request({
url: '/agent/withdrawal',
method: 'GET',
params
})
}

50
src/app.scss Normal file
View File

@@ -0,0 +1,50 @@
:root {
--dark-bg: #18181c;
}
html {
margin: auto !important;
@apply max-w-lg;
font-size: 4px; // * 方便unocss计算1单位 = 0.25rem = 1px
}
body {
font-size: 16px;
}
html,
body,
page,
#app {
height: 100%;
margin: 0;
padding: 0;
background: #f8f8f8
}
html.dark {
background: var(--dark-bg);
}
.card{
@apply border border-gray-200 rounded-xl bg-white p-6 shadow-md;
}
.card-p-0{
@apply border border-gray-200 rounded-xl bg-white shadow-md;
}
.title {
@apply mx-auto mt-2 w-68 border rounded-3xl bg-gradient-to-r from-blue-400 via-green-500 to-teal-500 py-2 text-center text-white font-bold;
}
.scrollbar-hidden {
scrollbar-width: none; /* Firefox */
}
.scrollbar-hidden::-webkit-scrollbar {
display: none; /* Chrome, Safari, and Edge */
}
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}

641
src/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,641 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const aesDecrypt: typeof import('./utils/crypto.js')['aesDecrypt']
const aesEncrypt: typeof import('./utils/crypto.js')['aesEncrypt']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const chatCrypto: typeof import('./utils/chatCrypto.js')['default']
const chatEncrypt: typeof import('./utils/chatEncrypt.js')['default']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onError: typeof import('@dcloudio/uni-app')['onError']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onHide: typeof import('@dcloudio/uni-app')['onHide']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch']
const onLoad: typeof import('@dcloudio/uni-app')['onLoad']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']
const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound']
const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll']
const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh']
const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom']
const onReady: typeof import('@dcloudio/uni-app')['onReady']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onResize: typeof import('@dcloudio/uni-app')['onResize']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage']
const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline']
const onShow: typeof import('@dcloudio/uni-app')['onShow']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap']
const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange']
const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection']
const onUnload: typeof import('@dcloudio/uni-app')['onUnload']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const request: typeof import('./utils/request.js')['default']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const routerGuard: typeof import('./utils/routerGuard.js')['default']
const setupRouterGuard: typeof import('./utils/routerGuard.js')['setupRouterGuard']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCount: typeof import('./composables/useCount')['useCount']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useQuery: typeof import('./composables/useQuery')['useQuery']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRouterGuard: typeof import('./composables/useRouterGuard')['useRouterGuard']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUser: typeof import('./composables/useUser.js')['useUser']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly aesDecrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesDecrypt']>
readonly aesEncrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesEncrypt']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly chatCrypto: UnwrapRef<typeof import('./utils/chatCrypto.js')['default']>
readonly chatEncrypt: UnwrapRef<typeof import('./utils/chatEncrypt.js')['default']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onAddToFavorites: UnwrapRef<typeof import('@dcloudio/uni-app')['onAddToFavorites']>
readonly onBackPress: UnwrapRef<typeof import('@dcloudio/uni-app')['onBackPress']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onError: UnwrapRef<typeof import('@dcloudio/uni-app')['onError']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onHide: UnwrapRef<typeof import('@dcloudio/uni-app')['onHide']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLaunch: UnwrapRef<typeof import('@dcloudio/uni-app')['onLaunch']>
readonly onLoad: UnwrapRef<typeof import('@dcloudio/uni-app')['onLoad']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onNavigationBarButtonTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']>
readonly onNavigationBarSearchInputChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']>
readonly onNavigationBarSearchInputClicked: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']>
readonly onNavigationBarSearchInputConfirmed: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']>
readonly onNavigationBarSearchInputFocusChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']>
readonly onPageNotFound: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageNotFound']>
readonly onPageScroll: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageScroll']>
readonly onPullDownRefresh: UnwrapRef<typeof import('@dcloudio/uni-app')['onPullDownRefresh']>
readonly onReachBottom: UnwrapRef<typeof import('@dcloudio/uni-app')['onReachBottom']>
readonly onReady: UnwrapRef<typeof import('@dcloudio/uni-app')['onReady']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onResize: UnwrapRef<typeof import('@dcloudio/uni-app')['onResize']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onShareAppMessage: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareAppMessage']>
readonly onShareTimeline: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareTimeline']>
readonly onShow: UnwrapRef<typeof import('@dcloudio/uni-app')['onShow']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onTabItemTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onTabItemTap']>
readonly onThemeChange: UnwrapRef<typeof import('@dcloudio/uni-app')['onThemeChange']>
readonly onUnhandledRejection: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnhandledRejection']>
readonly onUnload: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnload']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly request: UnwrapRef<typeof import('./utils/request.js')['default']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly setupRouterGuard: UnwrapRef<typeof import('./utils/routerGuard.js')['setupRouterGuard']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
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 useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
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 useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
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 useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
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 useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
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 watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}

33
src/components.d.ts vendored Normal file
View File

@@ -0,0 +1,33 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by vite-plugin-uni-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
AgentApplicationForm: typeof import('./components/AgentApplicationForm.vue')['default']
EnvDemo: typeof import('./components/EnvDemo.vue')['default']
PriceInputPopup: typeof import('./components/PriceInputPopup.vue')['default']
PrivacyModel: typeof import('./components/PrivacyModel.vue')['default']
QRcode: typeof import('./components/QRcode.vue')['default']
VipBanner: typeof import('./components/VipBanner.vue')['default']
WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.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']
WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default']
WdFormItem: typeof import('wot-design-uni/components/wd-form-item/wd-form-item.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']
WdLoadmore: typeof import('wot-design-uni/components/wd-loadmore/wd-loadmore.vue')['default']
WdNavbar: typeof import('wot-design-uni/components/wd-navbar/wd-navbar.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']
WdProgress: typeof import('wot-design-uni/components/wd-progress/wd-progress.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']
WdToast: typeof import('wot-design-uni/components/wd-toast/wd-toast.vue')['default']
}
}

View File

@@ -0,0 +1,341 @@
<template>
<wd-popup v-model="popupVisible" position="bottom" close-on-click-modal @close="handleClose">
<view class="bg-white rounded-t-lg p-4">
<view class="flex justify-between items-center mb-4">
<text class="text-gray-400" @click="cancel">取消</text>
<text class="text-lg font-medium">申请成为代理</text>
<text class="text-gray-400" @click="cancel">关闭</text>
</view>
<wd-form :model="form" :rules="rules">
<wd-cell-group border>
<!-- 区域选择 -->
<wd-col-picker v-model="region" label="代理区域" label-width="100px" prop="region" placeholder="请选择代理区域"
:columns="columns" :column-change="handleColumnChange" @confirm="handleRegionConfirm"
:display-format="displayFormat" />
<!-- 手机号 -->
<wd-input label="手机号码" label-width="100px" type="number" v-model="form.mobile" prop="mobile"
placeholder="请输入您的手机号" :rules="[
{ required: true, message: '请输入手机号' },
{ required: true, pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
]" />
<!-- 验证码 -->
<wd-input label="验证码" label-width="100px" type="number" v-model="form.code" prop="code" placeholder="请输入验证码"
:rules="[{ required: true, message: '请输入验证码' }]" use-suffix-slot>
<template #suffix>
<button class="verify-btn" :disabled="countdown > 0" @click.stop="sendVerifyCode">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</button>
</template>
</wd-input>
<!-- 微信号 -->
<wd-input label="微信号" label-width="100px" v-model="form.wechat_id" prop="wechat_id" placeholder="请输入您的微信号"
:rules="[{ required: true, message: '请输入微信号' }]" />
</wd-cell-group>
<!-- 同意条款的复选框 -->
<view class="p-4">
<view class="flex items-start">
<wd-checkbox v-model="isAgreed" size="16px" class="flex-shrink-0 mr-2"></wd-checkbox>
<view class="text-xs text-gray-400 leading-tight">
我已阅读并同意
<text class="text-blue-400" @click.stop="toUserAgreement">用户协议</text>
<text class="text-blue-400" @click.stop="toServiceAgreement">信息技术服务合同</text>
<text class="text-blue-400" @click.stop="toAgentManageAgreement">推广方管理制度协议</text>
<view class="text-xs text-gray-400 mt-1">点击勾选即代表您同意上述法律文书的相关条款并签署上述法律文书</view>
<view class="text-xs text-gray-400 mt-1">手机号未在本平台注册账号则申请后将自动生成账号</view>
</view>
</view>
</view>
<view class="p-4">
<wd-button type="primary" block @click="submitForm">提交申请</wd-button>
<wd-button type="default" block class="mt-2" @click="cancel">取消</wd-button>
</view>
</wd-form>
</view>
</wd-popup>
<wd-toast />
</template>
<script setup>
import { ref, computed, watch, onUnmounted } from 'vue'
import { useColPickerData } from '../hooks/useColPickerData'
import { useToast } from 'wot-design-uni'
import { getCode } from '@/api/apis.js' // 导入getCode API
const props = defineProps({
show: {
type: Boolean,
default: false
},
ancestor: {
type: String,
default: ''
}
})
const region = ref([]) // 存储地区代码数组
const regionText = ref('') // 存储地区文本
const isAgreed = ref(false) // 用户是否同意协议
// 格式化显示文本
const displayFormat = (selectedItems) => {
if (selectedItems.length === 0) return ''
return selectedItems.map(item => item.label).join(' ')
}
const emit = defineEmits(['update:show', 'submit', 'close'])
const form = ref({
mobile: '',
code: '',
wechat_id: ''
})
// 使用wot-design-ui的toast组件
const toast = useToast()
// 不再需要监听region变化触发表单验证
// watch(region, (newVal) => {
// // 当region变化时触发表单验证
// if (formRef.value) {
// formRef.value.validate(['region'])
// }
// })
const popupVisible = ref(false)
const countdown = ref(0)
let timer = null
// 初始化省市区数据
const { colPickerData, findChildrenByCode } = useColPickerData()
const columns = ref([
colPickerData.map((item) => {
return {
value: item.value,
label: item.text
}
})
])
// 表单验证规则 - 仍然保留以供wd-form组件使用
const rules = {
region: [{
required: true,
message: '请选择代理区域',
validator: (value) => {
// 这里直接检查region.value而不是传入的value
if (Array.isArray(region.value) && region.value.length === 3) {
return Promise.resolve()
}
return Promise.reject('请选择完整的省市区')
}
}],
mobile: [
{ required: true, message: '请输入手机号' },
{ required: true, pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
],
code: [{ required: true, message: '请输入验证码' }],
wechat_id: [{ required: true, message: '请输入微信号' }]
}
// 协议跳转函数
const toUserAgreement = () => {
uni.navigateTo({
url: '/pages/agreement?type=user'
})
}
const toServiceAgreement = () => {
uni.navigateTo({
url: '/pages/agreement?type=service'
})
}
const toAgentManageAgreement = () => {
uni.navigateTo({
url: '/pages/agreement?type=manage'
})
}
// 监听show属性变化
watch(() => props.show, (newVal) => {
popupVisible.value = newVal
})
// 监听popupVisible同步回props.show
watch(() => popupVisible.value, (newVal) => {
emit('update:show', newVal)
})
// Popup关闭事件
const handleClose = () => {
emit('close')
}
// 取消按钮
const cancel = () => {
popupVisible.value = false
emit('close')
}
// 处理列变化
const handleColumnChange = ({ selectedItem, resolve, finish }) => {
const children = findChildrenByCode(colPickerData, selectedItem.value)
if (children && children.length) {
resolve(children.map(item => ({
label: item.text,
value: item.value
})))
} else {
finish()
}
}
// 区域选择确认 - 获取选中项的文本值
const handleRegionConfirm = ({ value, selectedItems }) => {
// 存储地区文本
regionText.value = selectedItems.map(item => item.label).join('-')
console.log('地区选择完成', regionText.value)
}
// 判断手机号是否有效
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(form.value.mobile)
})
// 发送验证码
const sendVerifyCode = async () => {
// 验证手机号
if (!form.value.mobile) {
toast.info('请输入手机号')
return
}
if (!isPhoneNumberValid.value) {
toast.info('手机号格式不正确')
return
}
// 发送验证码请求
getCode({
mobile: form.value.mobile,
actionType: 'agentApply',
}).then((res) => {
if (res.code === 200) {
toast.success('验证码已发送')
// 开始倒计时
startCountdown()
} else {
toast.error(res.msg || '发送失败')
}
}).catch((err) => {
toast.error('网络错误')
})
}
// 开始倒计时
const startCountdown = () => {
countdown.value = 60
if (timer) clearInterval(timer)
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
}
// 组件卸载时清除计时器
onUnmounted(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
// 提交表单 - 不再使用formRef验证
const submitForm = () => {
try {
// 检查区域是否已选择
if (!Array.isArray(region.value) || region.value.length < 3) {
console.log('区域选择不完整:', region.value)
toast.info('请选择完整的代理区域')
return
}
if (!regionText.value) {
toast.info('请选择代理区域')
return
}
// 检查手机号
if (!form.value.mobile) {
console.log('手机号为空')
toast.info('请输入手机号')
return
}
if (!isPhoneNumberValid.value) {
console.log('手机号格式不正确:', form.value.mobile)
toast.info('手机号格式不正确')
return
}
// 检查验证码
if (!form.value.code) {
console.log('验证码为空')
toast.info('请输入验证码')
return
}
// 检查微信号
if (!form.value.wechat_id) {
console.log('微信号为空')
toast.info('请输入微信号')
return
}
// 检查用户是否同意协议
if (!isAgreed.value) {
toast.info('请阅读并同意相关协议')
return
}
// 所有验证通过,构建表单数据
const formData = {
...form.value,
region: regionText.value,
}
console.log('提交的表单数据:', formData)
// 提交完整数据
emit('submit', formData)
} catch (error) {
console.error('提交表单过程中出现异常:', error)
toast.error('系统错误,请稍后重试')
}
}
</script>
<style scoped>
.verify-btn {
height: 32px;
padding: 0 12px;
background-color: #3b82f6;
color: white;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.verify-btn[disabled] {
background-color: #a0aec0;
}
</style>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
// 在Vue组件中访问环境变量
const baseUrl = import.meta.env.VITE_APP_BASE_URL
const isDebug = import.meta.env.VITE_APP_DEBUG === 'true'
// 获取当前环境模式
const mode = import.meta.env.MODE
// 用于测试的方法
function testApiCall() {
// 使用环境变量构建API URL
const apiUrl = `${baseUrl}/api/v1/test`
console.log('请求地址:', apiUrl)
// 这里可以添加实际的API调用
uni.showToast({
title: `环境: ${mode}, API: ${apiUrl}`,
icon: 'none'
})
}
</script>
<template>
<view class="env-demo">
<view class="env-info">
<text class="info-title">环境变量信息</text>
<view class="info-item">
<text class="label">基础URL:</text>
<text class="value">{{ baseUrl }}</text>
</view>
<view class="info-item">
<text class="label">调试模式:</text>
<text class="value">{{ isDebug ? '已开启' : '已关闭' }}</text>
</view>
<view class="info-item">
<text class="label">当前环境:</text>
<text class="value">{{ mode }}</text>
</view>
</view>
<button class="test-btn" @click="testApiCall">测试API调用</button>
</view>
</template>
<style>
.env-demo {
padding: 20px;
}
.env-info {
background-color: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.info-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
display: block;
}
.info-item {
display: flex;
margin-bottom: 8px;
}
.label {
font-weight: bold;
margin-right: 10px;
width: 80px;
}
.value {
flex: 1;
word-break: break-all;
}
.test-btn {
background-color: #007aff;
color: #ffffff;
}
</style>

View File

@@ -0,0 +1,179 @@
<template>
<wd-popup v-model="show" position="bottom" round close-on-click-modal destroy-on-close>
<div class="min-h-[500px] bg-gray-50 text-gray-600">
<div class="h-10 bg-white flex items-center justify-center font-semibold text-lg">设置客户查询价
</div>
<div class="card m-4">
<div class="flex items-center justify-between">
<div class="text-lg">
客户查询价 ()</div>
</div>
<div class="border-b border-gray-200">
<wd-input v-model="price" type="number" label="¥" label-width="28px" size="large"
:placeholder="`${productConfig.price_range_min} - ${productConfig.price_range_max}`"
@blur="onBlurPrice" custom-class="wd-input" />
</div>
<div class="flex items-center justify-between mt-2">
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span></div>
<div>我的成本为<span class="text-orange-500"> {{ costPrice }} </span></div>
</div>
</div>
<div class="card m-4">
<div class="text-lg mb-2">收益与成本说明</div>
<div>推广收益 = 客户查询价 - 我的成本</div>
<div>我的成本 = 提价成本 + 底价成本</div>
<div class="mt-1">提价成本超过平台标准定价部分平台会收取部分成本价</div>
<div class="">设定范围<span class="text-orange-500">{{
productConfig.price_range_min }}</span> - <span class="text-orange-500">{{
productConfig.price_range_max }}</span></div>
</div>
<div class="px-4 pb-4">
<wd-button class="w-full" round type="primary" size="large" @click="onConfirm">确认</wd-button>
</div>
</div>
</wd-popup>
<wd-toast />
</template>
<script setup>
import { ref, computed, watch, toRefs } from 'vue'
import { useToast } from 'wot-design-uni'
const props = defineProps({
defaultPrice: {
type: Number,
required: true
},
productConfig: {
type: Object,
required: true
}
})
const { defaultPrice, productConfig } = toRefs(props)
const emit = defineEmits(["change"])
const show = defineModel("show")
const price = ref(null)
const toast = useToast()
watch(show, () => {
price.value = defaultPrice.value
})
const costPrice = computed(() => {
if (!productConfig.value) return 0.00
// 平台定价成本
let platformPricing = 0
platformPricing += productConfig.value.cost_price
if (price.value > productConfig.value.p_pricing_standard) {
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio
}
if (productConfig.value.a_pricing_standard > platformPricing && productConfig.value.a_pricing_end > platformPricing && productConfig.value.a_overpricing_ratio > 0) {
if (price.value > productConfig.value.a_pricing_standard) {
if (price.value > productConfig.value.a_pricing_end) {
platformPricing += (productConfig.value.a_pricing_end - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
} else {
platformPricing += (price.value - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
}
}
}
return safeTruncate(platformPricing)
})
const promotionRevenue = computed(() => {
return safeTruncate(price.value - costPrice.value)
});
// 价格校验与修正逻辑
const validatePrice = (currentPrice) => {
const min = productConfig.value.price_range_min;
const max = productConfig.value.price_range_max;
let newPrice = Number(currentPrice);
let message = '';
// 处理无效输入
if (isNaN(newPrice)) {
newPrice = defaultPrice.value;
return { newPrice, message: '输入无效,请输入价格' };
}
// 处理小数位数(兼容科学计数法)
try {
const priceString = newPrice.toString()
const [_, decimalPart = ""] = priceString.split('.');
console.log(priceString, decimalPart)
// 当小数位数超过2位时处理
if (decimalPart.length > 2) {
newPrice = parseFloat(safeTruncate(newPrice));
message = '价格已自动格式化为两位小数';
}
} catch (e) {
console.error('价格格式化异常:', e);
}
// 范围校验(基于可能格式化后的值)
if (newPrice < min) {
message = `价格不能低于 ${min}`;
newPrice = min;
} else if (newPrice > max) {
message = `价格不能高于 ${max}`;
newPrice = max;
}
console.log(newPrice, message)
return { newPrice, message };
}
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
const isManualConfirm = ref(false)
const onConfirm = () => {
if (isManualConfirm.value) return
const { newPrice, message } = validatePrice(price.value)
if (message) {
price.value = newPrice
toast.show(message)
} else {
emit("change", price.value)
show.value = false
}
}
const onBlurPrice = () => {
const { newPrice, message } = validatePrice(price.value)
if (message) {
isManualConfirm.value = true
price.value = newPrice
toast.show(message)
}
setTimeout(() => {
isManualConfirm.value = false
}, 0)
}
</script>
<style lang="scss" scoped>
.wd-input {
display: flex !important;
align-items: center !important;
justify-content: center !important;
:deep(.wd-input__label-inner) {
font-size: 24px !important; /* 增大label字体 */
}
:deep(.wd-input__inner) {
font-size: 24px !important; /* 增大输入框内字体 */
}
:deep(.wd-input) {
height: auto !important; /* 确保高度自适应 */
padding: 8px 0 !important; /* 增加垂直内边距 */
}
}
</style>

View File

@@ -0,0 +1,150 @@
<script setup>
const showPrivacyModal = ref(true)
function openPage(url) {
uni.navigateTo({ url })
}
function acceptPrivacy() {
uni.setStorageSync('privacyAccepted', true)
showPrivacyModal.value = false
}
function refusePrivacy() {
uni.showModal({
title: '确认提示',
content: '您点击同意并继续视为您已同意上述协议的全部内容。',
confirmText: '同意并继续',
cancelText: '退出',
success: (res) => {
if (res.confirm) {
acceptPrivacy()
}
else {
const platform = uni.getSystemInfoSync().platform
if (platform === 'android') {
plus.runtime.quit()
}
else if (platform === 'ios') {
plus.runtime.launchApplication({ action: 'QUIT' })
}
}
},
})
}
// 检查隐私协议是否已经同意
onMounted(() => {
const accepted = uni.getStorageSync('privacyAccepted')
if (accepted) {
showPrivacyModal.value = false
}
})
</script>
<template>
<view v-if="showPrivacyModal" class="privacy-modal">
<view class="modal-mask" />
<view class="modal-content">
<view class="modal-title">
用户协议和隐私政策
</view>
<view class="modal-message">
感谢您使用全能查APP! 我们非常重视您的隐私保护和个人信息保护在您使用全能查APP前请您仔细阅读充分理解
<text class="link" @click="openPage('/pages/agreement?type=user')">
用户协议
</text>
<text class="link" @click="openPage('/pages/agreement?type=privacy')">
隐私政策
</text>
的各项条款如果您同意请点击下方按钮开始接受我们的服务
</view>
<view class="modal-buttons">
<view class="button refuse" @click="refusePrivacy">
退出应用
</view>
<view class="button accept" @click="acceptPrivacy">
同意并继续
</view>
</view>
</view>
</view>
</template>
<style scoped>
.privacy-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 10px;
width: 80%;
max-width: 320px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
z-index:1001;
}
.modal-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
text-align: center;
}
.modal-message {
font-size: 14px;
line-height: 1.5;
color: #333;
margin-bottom: 20px;
}
.link {
color: #007aff;
text-decoration: underline;
cursor: pointer;
}
.modal-buttons {
display: flex;
justify-content: space-between;
}
.button {
flex: 1;
padding: 10px 0;
margin: 0 5px;
border: none;
border-radius: 5px;
font-size: 14px;
text-align: center;
}
.button.accept {
background: #007aff;
color: #fff;
}
.button.accept:active{
background: #016ee2;
color: #fff;
}
.button.refuse {
background: #f0f0f0;
color: #333;
}
.button.refuse:active {
background: #c6c6c6;
color: #333;
}
</style>

545
src/components/QRcode.vue Normal file
View File

@@ -0,0 +1,545 @@
<template>
<wd-popup v-model="show" position="bottom" round>
<view class="max-h-[calc(100vh-100px)] m-4">
<view class="p-4 flex justify-center">
<!-- 添加最大高度限制容器 -->
<view class="max-h-[70vh] rounded-xl overflow-hidden">
<canvas canvas-id="posterCanvas" id="posterCanvas" class="rounded-xl shadow"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" ref="posterCanvas"></canvas>
</view>
</view>
<view class="divider">分享到好友</view>
<view class="flex items-center justify-around">
<view class="flex flex-col items-center justify-center" @click="savePoster">
<image src="/static/image/icon_share_img.svg" class="w-10 h-10 rounded-full" />
<view class="text-center mt-1 text-gray-600 text-xs">保存图片</view>
</view>
<!-- <view class="flex flex-col items-center justify-center" @click="shareToWechatFriend">
<image src="/static/image/icon_share_wechat.svg" class="w-10 h-10 rounded-full" />
<view class="text-center mt-1 text-gray-600 text-xs">微信好友</view>
</view>
<view class="flex flex-col items-center justify-center" @click="shareToWechatMoments">
<image src="/static/image/icon_share_friends.svg" class="w-10 h-10 rounded-full" />
<view class="text-center mt-1 text-gray-600 text-xs">微信朋友圈</view>
</view> -->
<view class="flex flex-col items-center justify-center" @click="copyUrl">
<image src="/static/image/icon_share_url.svg" class="w-10 h-10 rounded-full" />
<view class="text-center mt-1 text-gray-600 text-xs">复制链接</view>
</view>
</view>
</view>
</wd-popup>
</template>
<script setup>
import { ref, watch, nextTick, computed, onMounted, toRefs, getCurrentInstance } from 'vue';
import UQRCode from 'uqrcodejs';
const props = defineProps({
linkIdentifier: {
type: String,
required: true,
},
mode: {
type: String,
default: "promote", // 例如 "promote" | "invitation"
},
});
const { linkIdentifier, mode } = toRefs(props);
const posterCanvas = ref(null); // 用于绘制海报的canvas
const posterGenerated = ref(false); // 标记海报是否已经生成过
const show = defineModel('show');
const instance = getCurrentInstance();
// 添加画布尺寸变量 - 降低默认尺寸
const canvasWidth = ref(300); // 画布宽度
const canvasHeight = ref(500); // 画布高度
const url = computed(() => {
// 在uniapp中获取当前站点的基础URL
const baseUrl = import.meta.env.VITE_APP_SHARE_URL || 'https://www.tianyuandata.com';
return mode.value === "promote"
? `${baseUrl}/agent/promotionInquire/`
: `${baseUrl}/agent/invitationAgentApply/`;
});
const qrcodeImage = ref(null);
const loadPosterImage = async () => {
if (mode.value === "promote") {
return '/static/image/tg_qrcode_1.png';
} else {
return '/static/image/yq_qrcode_1.png';
}
};
onMounted(async () => {
// 只在挂载时加载图片路径,但不生成海报
qrcodeImage.value = await loadPosterImage();
});
// 计算适合屏幕的画布尺寸
const calculateCanvasSize = (imgWidth, imgHeight) => {
// 获取系统信息
const sysInfo = uni.getSystemInfoSync();
// 计算可用高度屏幕高度的70%
const maxHeight = sysInfo.windowHeight * 0.6;
// 控制最大宽度屏幕宽度的80%
const maxWidth = sysInfo.windowWidth * 0.8;
// 初始尺寸
let width = 300;
let height = width * (imgHeight / imgWidth);
// 如果高度超出限制,按高度缩放
if (height > maxHeight) {
height = maxHeight;
width = height * (imgWidth / imgHeight);
}
// 如果宽度超出限制,按宽度缩放
if (width > maxWidth) {
width = maxWidth;
height = width * (imgHeight / imgWidth);
}
// 返回最终计算的尺寸
return {
width: Math.floor(width),
height: Math.floor(height)
};
};
// 生成海报并合成二维码
const generatePoster = async () => {
console.log('[DEBUG] generatePoster: 开始生成海报');
// 如果已经生成过海报,就直接返回
if (posterGenerated.value) {
console.log('[DEBUG] generatePoster: 海报已生成,跳过');
return;
}
// 确保 DOM 已经渲染完成
await nextTick();
console.log('[DEBUG] generatePoster: DOM已渲染完成');
try {
// 显示加载提示
uni.showLoading({ title: '生成海报中...' });
console.log('[DEBUG] generatePoster: 在APP环境中执行');
const canvasContext = uni.createCanvasContext('posterCanvas', instance.proxy);
if (!canvasContext) {
throw new Error('创建Canvas上下文失败');
}
console.log('[DEBUG] generatePoster: 创建Canvas上下文成功', !!canvasContext);
// 加载海报图片 - 确保绝对路径
const posterImagePath = mode.value === "promote"
? '/static/image/tg_qrcode_1.png'
: '/static/image/yq_qrcode_1.png';
console.log('[DEBUG] generatePoster: 开始加载海报图片', posterImagePath);
try {
// 加载图片信息
const posterImgInfo = await new Promise((resolve, reject) => {
uni.getImageInfo({
src: posterImagePath,
success: res => {
console.log('[DEBUG] generatePoster: 海报图片加载成功', res);
// 计算适合屏幕的尺寸
const size = calculateCanvasSize(res.width, res.height);
canvasWidth.value = size.width;
canvasHeight.value = size.height;
console.log('[DEBUG] generatePoster: 计算画布尺寸', size);
resolve(res);
},
fail: err => {
console.error('[DEBUG] generatePoster: 海报图片加载失败', err);
reject(err);
}
});
});
// 完全清除画布
canvasContext.clearRect(0, 0, canvasWidth.value, canvasHeight.value);
// 绘制流程 - 多次测试以查找问题
// 先尝试绘制底色确认canvas工作正常
canvasContext.setFillStyle('#ffffff');
canvasContext.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
// 先绘制一次确保底色显示
console.log('[DEBUG] generatePoster: 绘制白色背景');
canvasContext.draw(false, () => {
console.log('[DEBUG] generatePoster: 白色背景绘制完成');
// 第一步完成后,绘制背景图
setTimeout(() => {
console.log('[DEBUG] generatePoster: 开始绘制背景图片', posterImgInfo.path);
canvasContext.drawImage(posterImgInfo.path, 0, 0, canvasWidth.value, canvasHeight.value);
console.log('[DEBUG] generatePoster: 背景图片绘制完成');
canvasContext.draw(true, () => {
console.log('[DEBUG] generatePoster: 背景图绘制完成,准备绘制二维码');
// 背景图绘制完成后,绘制二维码
setTimeout(async () => {
try {
// 计算二维码在海报上的位置和大小 - 使用比例计算
const scaleFactor = canvasWidth.value / posterImgInfo.width;
// 根据海报类型和缩放因子调整二维码尺寸和位置
const qrSize = mode.value === "promote"
? Math.min(Math.floor(300 * scaleFactor), canvasWidth.value * 0.4) // 控制最大尺寸
: Math.min(Math.floor(360 * scaleFactor), canvasWidth.value * 0.4);
// 计算水平位置,确保在画布内
const qrX = mode.value === "promote"
? Math.min(Math.floor(180 * scaleFactor), canvasWidth.value - qrSize)
: Math.min(Math.floor(360 * scaleFactor), canvasWidth.value - qrSize);
// 计算垂直位置,确保在画布底部合适位置
const qrY = mode.value === "promote"
? canvasHeight.value - Math.min(Math.floor(480 * scaleFactor), canvasHeight.value * 0.4)
: canvasHeight.value - Math.min(Math.floor(1370 * scaleFactor), canvasHeight.value * 0.8);
console.log('[DEBUG] generatePoster: 二维码参数', { qrX, qrY, qrSize, mode: mode.value, scaleFactor });
// 3. 使用UQRCode生成二维码并绘制到画布
// 创建uQRCode实例
const qr = new UQRCode();
// 设置二维码内容
qr.data = generalUrl();
// 设置二维码大小
qr.size = qrSize;
// 调用制作二维码方法
qr.make();
// 绘制QR码到指定位置
// 获取绘制模块信息
const drawModules = qr.getDrawModules();
for (const drawModule of drawModules) {
if (drawModule.type === 'tile') {
canvasContext.setFillStyle(drawModule.color);
canvasContext.fillRect(
qrX + drawModule.x,
qrY + drawModule.y,
drawModule.width,
drawModule.height
);
}
}
// 4. 最终绘制,这次保留先前绘制的背景图
canvasContext.draw(true, () => {
console.log('[DEBUG] generatePoster: 画布完整绘制完成');
posterGenerated.value = true;
uni.hideLoading();
});
} catch (error) {
console.error('[DEBUG] generatePoster: 二维码生成异常', error);
uni.hideLoading();
uni.showToast({
title: '生成二维码失败',
icon: 'none'
});
}
}, 300);
});
}, 300);
});
} catch (error) {
console.error('[DEBUG] generatePoster: APP环境图片处理失败', error);
uni.hideLoading();
uni.showToast({
title: '生成海报失败',
icon: 'none'
});
}
} catch (error) {
console.error('[DEBUG] generatePoster: 生成海报发生未捕获异常', error);
uni.hideLoading();
uni.showToast({
title: '生成海报失败',
icon: 'none'
});
}
};
// 监听 show 变化show 为 true 时生成海报
watch(show, (newVal) => {
if (newVal && !posterGenerated.value) {
generatePoster(); // 当弹窗显示且海报未生成时生成海报
}
});
// 保存海报图片
const savePoster = () => {
if (!posterGenerated.value) {
uni.showToast({
title: '海报生成中,请稍后再试',
icon: 'none'
});
return;
}
uni.canvasToTempFilePath({
canvasId: 'posterCanvas',
width: canvasWidth.value,
height: canvasHeight.value,
destWidth: canvasWidth.value * 2, // 输出图片宽度为 canvas 宽度的2倍提高清晰度
destHeight: canvasHeight.value * 2, // 输出图片高度为 canvas 高度的2倍提高清晰度
success: (res) => {
// 保存图片到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
console.error('保存失败:', err);
// 如果是用户拒绝授权导致的
if (err.errMsg && err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (res) => {
if (res.confirm) {
uni.openSetting();
}
}
});
} else {
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
}
});
},
fail: (err) => {
console.error('Canvas转图片失败:', err);
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
}, instance.proxy);
};
// 分享到微信好友
const shareToWechatFriend = () => {
if (!posterGenerated.value) {
uni.showToast({
title: '海报生成中,请稍后再试',
icon: 'none'
});
return;
}
uni.canvasToTempFilePath({
canvasId: 'posterCanvas',
width: canvasWidth.value,
height: canvasHeight.value,
destWidth: canvasWidth.value * 2,
destHeight: canvasHeight.value * 2,
success: (res) => {
// #ifdef APP-PLUS
shareWithSystem('WXSceneSession', res.tempFilePath);
// #endif
// #ifndef APP-PLUS
uni.showToast({
title: '分享功能仅支持APP',
icon: 'none'
});
// #endif
},
fail: (err) => {
console.error('Canvas转图片失败:', err);
uni.showToast({
title: '分享准备失败',
icon: 'none'
});
}
}, instance.proxy);
};
// 分享到微信朋友圈
const shareToWechatMoments = () => {
if (!posterGenerated.value) {
uni.showToast({
title: '海报生成中,请稍后再试',
icon: 'none'
});
return;
}
uni.canvasToTempFilePath({
canvasId: 'posterCanvas',
width: canvasWidth.value,
height: canvasHeight.value,
destWidth: canvasWidth.value * 2,
destHeight: canvasHeight.value * 2,
success: (res) => {
// #ifdef APP-PLUS
shareWithSystem('WXSceneTimeline', res.tempFilePath);
// #endif
// #ifndef APP-PLUS
uni.showToast({
title: '分享功能仅支持APP',
icon: 'none'
});
// #endif
},
fail: (err) => {
console.error('Canvas转图片失败:', err);
uni.showToast({
title: '分享准备失败',
icon: 'none'
});
}
}, instance.proxy);
};
// 通用的分享处理函数
const shareWithSystem = (scene, tempFilePath) => {
try {
// 判断是否安装微信
plus.share.getServices((services) => {
const wechatService = services.find((s) => s.id === 'weixin');
if (!wechatService) {
uni.showToast({
title: '未检测到微信应用',
icon: 'none'
});
return;
}
// 判断微信是否认证
wechatService.authorize(() => {
const shareOptions = {
provider: 'weixin',
scene: scene, // WXSceneSession或WXSceneTimeline
type: 2, // 图片类型
imageUrl: tempFilePath,
title: mode.value === "promote" ? '推广海报' : '邀请海报',
href: generalUrl(),
success: (res) => {
uni.showToast({
title: '分享成功',
icon: 'success'
});
},
fail: (err) => {
console.error('分享失败:', err);
uni.showToast({
title: '分享失败',
icon: 'none'
});
}
};
wechatService.send(shareOptions, () => {
console.log('分享成功');
}, (err) => {
console.error('分享失败:', err);
});
}, (err) => {
console.error('微信认证失败:', err);
uni.showToast({
title: '微信认证失败',
icon: 'none'
});
});
}, (err) => {
console.error('获取分享服务列表失败:', err);
// 如果获取服务失败,尝试使用系统分享
try {
plus.share.sendWithSystem({
pictures: [tempFilePath]
}, () => {
uni.showToast({
title: '分享成功',
icon: 'success'
});
}, (err) => {
uni.showToast({
title: '分享失败',
icon: 'none'
});
});
} catch (e) {
uni.showToast({
title: '当前设备不支持分享',
icon: 'none'
});
}
});
} catch (error) {
console.error('分享出错:', error);
uni.showToast({
title: '分享失败',
icon: 'none'
});
}
};
const generalUrl = () => {
return url.value + encodeURIComponent(linkIdentifier.value);
};
const copyUrl = () => {
uni.setClipboardData({
data: generalUrl(),
success: () => {
uni.showToast({
title: '链接已复制!',
icon: 'success'
});
}
});
};
</script>
<style lang="scss" scoped>
.divider {
position: relative;
display: flex;
align-items: center;
margin: 16px 0;
color: #969799;
font-size: 14px;
&::before,
&::after {
content: '';
height: 1px;
flex: 1;
background-color: #ebedf0;
}
&::before {
margin-right: 16px;
}
&::after {
margin-left: 16px;
}
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<view class="card mb-4 relative overflow-hidden" @click="goToVip">
<view class="absolute inset-0 bg-gradient-to-r from-yellow-400 to-yellow-300 opacity-40"></view>
<view class="p-2 relative z-10">
<view class="flex justify-between items-center">
<view>
<view class="text-lg font-bold text-yellow-800">会员专享特权</view>
<view class="text-sm text-yellow-700 mt-1">升级VIP获得更多收益</view>
</view>
<view class="bg-yellow-500 px-3 py-1 rounded-full text-white text-sm shadow-sm">
立即查看
</view>
</view>
</view>
<!-- 装饰元素 -->
<view class="absolute -right-4 -top-4 w-16 h-16 bg-yellow-200 rounded-full opacity-60"></view>
<view class="absolute right-5 -bottom-4 w-12 h-12 bg-yellow-100 rounded-full opacity-40"></view>
</view>
</template>
<script setup>
// 跳转到VIP页面
const goToVip = () => {
uni.navigateTo({
url: '/pages/agentVip'
})
}
</script>
<style scoped>
.card {
border-radius: 12px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
</style>

1
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,38 @@
// 可以将此代码放置于项目src/hooks/useColPickerData.ts中
import { useCascaderAreaData } from '@vant/area-data'
export type CascaderOption = {
text: string
value: string
children?: CascaderOption[]
}
/**
* 使用'@vant/area-data'作为数据源构造ColPicker组件的数据
* @returns
*/
export function useColPickerData() {
// '@vant/area-data' 数据源
const colPickerData: CascaderOption[] = useCascaderAreaData()
// 根据code查找子节点不传code则返回所有节点
function findChildrenByCode(data: CascaderOption[], code?: string): CascaderOption[] | null {
if (!code) {
return data
}
for (const item of data) {
if (item.value === code) {
return item.children || null
}
if (item.children) {
const childrenResult = findChildrenByCode(item.children, code)
if (childrenResult) {
return childrenResult
}
}
}
return null
}
return { colPickerData, findChildrenByCode }
}

74
src/layouts/home.vue Normal file
View File

@@ -0,0 +1,74 @@
<script setup>
import PrivacyModal from '@/components/PrivacyModel.vue'
const tabbar = ref('index')
const menu = reactive([{ title: '首页', icon: 'home', name: 'index' }, { title: '代理', icon: 'share', name: 'agent' }, { title: 'AI律师', icon: 'user-avatar', name: 'ai' }, { title: '我的', icon: 'user', name: 'me' }])
function tabChange({ value }) {
uni.switchTab({
url: `/pages/${value}`,
})
}
onShow(() => {
const currentPage = getCurrentPages()[getCurrentPages().length - 1].route
const pageName = currentPage.split('/').pop()
tabbar.value = pageName
})
onMounted(() => {
uni.hideTabBar()
const currentPage = getCurrentPages()[getCurrentPages().length - 1].route
const pageName = currentPage.split('/').pop()
tabbar.value = pageName
})
function toComplaint() {
// 直接使用plus.runtime.openURL在系统浏览器中打开链接
// #ifdef APP-PLUS
plus.runtime.openURL('https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9');
// #endif
// #ifdef H5
window.location.href = 'https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9';
// #endif
}
</script>
<script>
export default {
options: {
styleIsolation: 'shared',
},
}
</script>
<template>
<view class=" min-h-screen from-blue-100 to-white bg-gradient-to-b min-h-screen flex flex-col flex-1 pb-16 box-border">
<slot />
</view>
<view>
<wd-tabbar v-model="tabbar" custom-class="qnc-tabbar" shape="round" safe-area-inset-bottom fixed
@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>
<view
class="fixed bottom-24 right-4 z-1000 flex items-center rounded-3xl from-red-500 to-red-400 bg-gradient-to-b px-2 py-1 text-center text-white shadow-2xl">
<wd-icon name="warning" class="mr-1" size="18px" />
<view class="text-xs" @click="toComplaint">
投诉
</view>
</view>
<PrivacyModal />
</template>
<style scoped>
:deep(.qnc-tabbar) {
bottom: 16px !important;
}
</style>

31
src/layouts/login.vue Normal file
View File

@@ -0,0 +1,31 @@
<script setup>
function handleClickLeft() {
uni.reLaunch({
url: '/pages/index',
})
}
</script>
<template>
<!-- -->
<view class="h-screen bg-[#EBF1FD]">
<view class="login-layout min-h-full">
<wd-navbar
title="用户登录"
left-arrow
safe-area-inset-top
custom-style="background-color: transparent !important;"
@click-left="handleClickLeft"
/>
<slot />
</view>
</view>
</template>
<style scoped>
.login-layout{
background: url("/static/image/login_bg.png") no-repeat;
background-position: center;
background-size: cover;
}
</style>

35
src/layouts/page.vue Normal file
View File

@@ -0,0 +1,35 @@
<script setup>
import pagesJson from '@/pages.json'
const pagesConfig = pagesJson.pages
const title = ref('')
function getPageTitle() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] // 当前页面
const currentRoute = currentPage.route // 当前页面路径,例如 "pages/authorization"
// 根据路径查找 pages.json 中的配置
const currentPageConfig = pagesConfig.find(page => page.path === currentRoute)
console.log('currentPageConfig', currentPageConfig)
// 返回页面标题,如果未找到,则返回默认标题
return currentPageConfig?.title || ''
}
onLoad(() => {
title.value = getPageTitle()
})
function handleClickLeft() {
uni.navigateBack()
}
</script>
<template>
<wd-navbar :title="title" left-text="返回" placeholder left-arrow safe-area-inset-top fixed @click-left="handleClickLeft" />
<view class="box-border min-h-screen">
<slot />
</view>
</template>
<style scoped>
</style>

16
src/main.ts Normal file
View File

@@ -0,0 +1,16 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import 'uno.css'
import '@/app.scss'
import { setupRouterGuard } from '@/utils/routerGuard'
export function createApp() {
const app = createSSRApp(App)
// 初始化路由守卫
setupRouterGuard()
return {
app
}
}

134
src/manifest.json Normal file
View File

@@ -0,0 +1,134 @@
{
"name": "全能查",
"appid": "__UNI__CC3DA09",
"description": "",
"versionName": "1.0.0",
"versionCode": "104",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {
"Payment": {},
"Share": {}
},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>"
],
"package": "com.quannengcha.app"
},
"ios": {
"privacyDescription": {
"NSLocalNetworkUsageDescription": "需要访问您的网络来提供更好的服务"
},
"idfa": false,
"bundleIdentifier": "com.allinone.check"
},
"sdkConfigs": {
"payment": {
"appleiap": {
"inAppPurchase": true
},
"alipay": {
"__platform__": [
"ios",
"android"
]
}
}
},
"icons": {
"android": {
"hdpi": "src/static/icons/72x72.png",
"xhdpi": "src/static/icons/96x96.png",
"xxhdpi": "src/static/icons/144x144.png",
"xxxhdpi": "src/static/icons/192x192.png"
},
"ios": {
"appstore": "src/static/icons/1024x1024.png",
"ipad": {
"app": "src/static/icons/76x76.png",
"app@2x": "src/static/icons/152x152.png",
"notification": "src/static/icons/20x20.png",
"notification@2x": "src/static/icons/40x40.png",
"proapp@2x": "src/static/icons/167x167.png",
"settings": "src/static/icons/29x29.png",
"settings@2x": "src/static/icons/58x58.png",
"spotlight": "src/static/icons/40x40.png",
"spotlight@2x": "src/static/icons/80x80.png"
},
"iphone": {
"app@2x": "src/static/icons/120x120.png",
"app@3x": "src/static/icons/180x180.png",
"notification@2x": "src/static/icons/40x40.png",
"notification@3x": "src/static/icons/60x60.png",
"settings@2x": "src/static/icons/58x58.png",
"settings@3x": "src/static/icons/87x87.png",
"spotlight@2x": "src/static/icons/80x80.png",
"spotlight@3x": "src/static/icons/120x120.png"
}
}
}
},
"background": "#000000",
"compatible": {
"ignoreVersion": true
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"darkmode": false,
"themeLocation": "theme.json"
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"darkmode": false,
"themeLocation": "theme.json"
},
"uts": {
"plugins": {
"webview": {
"version": "1.0.0",
"description": "Web视图插件支持在App内打开网页",
"platforms": {
"android": {
"appid": "__UNI_WEBVIEW_ANDROID",
"autostart": false
},
"ios": {
"appid": "__UNI_WEBVIEW_IOS",
"autostart": false
}
}
}
}
}
}

159
src/pages.json Normal file
View File

@@ -0,0 +1,159 @@
{
"pages": [
{
"path": "pages/index",
"type": "home",
"layout": "home",
"style": {
"navigationBarTextStyle": "white",
"transparentTitle": "always"
}
},
{
"path": "pages/agent",
"type": "page",
"layout": "home"
},
{
"path": "pages/agentVip",
"type": "page",
"layout": "page",
"title": "代理会员",
"agent": true,
"auth": true
},
{
"path": "pages/agentVipConfig",
"type": "page",
"layout": "page",
"title": "会员代理报告配置",
"agent": true,
"auth": true
},
{
"path": "pages/agreement",
"type": "page"
},
{
"path": "pages/ai",
"type": "page",
"layout": "home"
},
{
"path": "pages/invitation",
"type": "page",
"layout": "page",
"title": "邀请下级",
"auth": true,
"agent": true
},
{
"path": "pages/invitationAgentApply",
"type": "page",
"layout": "page",
"title": "代理申请",
"auth": true
},
{
"path": "pages/login",
"type": "page",
"layout": "login",
"title": "登录"
},
{
"path": "pages/me",
"type": "page",
"layout": "home"
},
{
"path": "pages/promote",
"type": "page",
"layout": "page",
"title": "推广",
"agent": true,
"auth": true
},
{
"path": "pages/promoteDetails",
"type": "page",
"layout": "page",
"title": "直推报告",
"agent": true,
"auth": true
},
{
"path": "pages/queryHistory",
"type": "page",
"layout": "page",
"title": "历史报告",
"auth": true
},
{
"path": "pages/rewardsDetails",
"type": "page",
"layout": "page",
"title": "收益明细",
"agent": true,
"auth": true
},
{
"path": "pages/vip",
"type": "page"
},
{
"path": "pages/withdraw",
"type": "page",
"layout": "page",
"title": "提现",
"auth": true,
"agent": true
},
{
"path": "pages/withdrawDetails",
"type": "page",
"layout": "page",
"title": "提现记录",
"auth": true,
"agent": true
}
],
"globalStyle": {
"backgroundColor": "@bgColor",
"backgroundColorBottom": "@bgColorBottom",
"backgroundColorTop": "@bgColorTop",
"backgroundTextStyle": "@bgTxtStyle",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "@navTxtStyle",
"navigationBarTitleText": "全能查",
"navigationStyle": "custom"
},
"tabBar": {
"backgroundColor": "@tabBgColor",
"borderStyle": "@tabBorderStyle",
"color": "@tabFontColor",
"selectedColor": "@tabSelectedColor",
"list": [
{
"pagePath": "pages/index",
"text": "",
"visible": false
},
{
"pagePath": "pages/agent",
"text": "",
"visible": false
},
{
"pagePath": "pages/ai",
"text": "",
"visible": false
},
{
"pagePath": "pages/me",
"text": "",
"visible": false
}
]
},
"subPackages": []
}

249
src/pages/agent.vue Normal file
View File

@@ -0,0 +1,249 @@
<script setup>
import { ref, computed } from 'vue'
import { getAgentRevenue } from '@/api/apis'
// 日期选项映射
const dateRangeMap = {
today: 'today',
week: 'last7d',
month: 'last30d'
}
// 直推报告数据
const promoteDateOptions = [
{ label: '今日', value: 'today' },
{ label: '近7天', value: 'week' },
{ label: '近1月', value: 'month' }
]
const selectedPromoteDate = ref('today')
// 活跃下级数据
const activeDateOptions = [
{ label: '今日', value: 'today' },
{ label: '近7天', value: 'week' },
{ label: '近1月', value: 'month' }
]
const selectedActiveDate = ref('today')
const data = ref(null)
// 计算当前直推数据
const currentPromoteData = computed(() => {
const range = dateRangeMap[selectedPromoteDate.value]
return data.value?.direct_push?.[range] || { commission: 0, report: 0 }
})
// 计算当前活跃数据
const currentActiveData = computed(() => {
const range = dateRangeMap[selectedActiveDate.value]
return data.value?.active_reward?.[range] || {
active_reward: 0,
sub_promote_reward: 0,
sub_upgrade_reward: 0,
sub_withdraw_reward: 0
}
})
const getData = async () => {
try {
const res = await getAgentRevenue()
if (res.code === 200) {
data.value = res.data
}
} catch (error) {
console.error(error)
}
}
onBeforeMount(() => {
if (uni.getStorageSync('token')) {
getData()
}
})
// 路由跳转
function goToPromoteDetail() {
uni.navigateTo({
url: '/pages/promoteDetails'
})
}
function goToActiveDetail() {
uni.navigateTo({
url: '/pages/rewardsDetails'
})
}
function toWithdraw() {
uni.navigateTo({
url: '/pages/withdraw'
})
}
function toWithdrawDetails() {
uni.navigateTo({
url: '/pages/withdrawDetails'
})
}
</script>
<template>
<view class="safe-area-top p-4 min-h-screen">
<!-- 资产卡片 -->
<view class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-blue-50/70 to-blue-100/50 p-6">
<view class="flex justify-between items-center mb-3">
<view class="flex items-center">
<wd-icon name="balance-pay" class="text-blue-500 text-xl mr-2" />
<text class="text-lg font-bold text-gray-800">余额</text>
</view>
<text class="text-3xl text-blue-600 font-bold">¥ {{ (data?.balance || 0).toFixed(2) }}</text>
</view>
<view class="text-sm text-gray-500 mb-2">累计收益¥ {{ (data?.total_earnings || 0).toFixed(2) }}</view>
<view class="text-sm text-gray-500 mb-6">冻结余额¥ {{ (data?.frozen_balance || 0).toFixed(2) }}</view>
<view class="grid grid-cols-2 gap-3">
<view @click="toWithdraw"
class="bg-gradient-to-r from-blue-500 to-blue-400 text-white rounded-full py-2 px-4 shadow-md flex items-center justify-center">
<wd-icon name="gold-coin" class="mr-1" />
<text>提现</text>
</view>
<view @click="toWithdrawDetails"
class="bg-white/90 text-gray-600 border border-gray-200/50 rounded-full py-2 px-4 shadow-sm flex items-center justify-center">
<wd-icon name="notes" class="mr-1" />
<text>提现记录</text>
</view>
</view>
</view>
<!-- 直推报告收益 -->
<view class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-blue-50/40 to-cyan-50/50 p-6">
<view class="flex justify-between items-center mb-4">
<view class="flex items-center">
<wd-icon name="balance-list" class="text-blue-400 text-xl mr-2" />
<text class="text-lg font-bold text-gray-800">直推报告收益</text>
</view>
<view class="text-right">
<text class="text-2xl text-blue-600 font-bold">¥ {{ (data?.direct_push?.total_commission || 0).toFixed(2)
}}</text>
<view class="text-sm text-gray-500 mt-1">有效报告 {{ data?.direct_push?.total_report || 0 }} </view>
</view>
</view>
<!-- 日期选择 -->
<view class="grid grid-cols-3 gap-2 mb-6">
<view v-for="item in promoteDateOptions" :key="item.value" @click="selectedPromoteDate = item.value" :class="[
'rounded-full transition-all py-1 px-4 text-sm',
selectedPromoteDate === item.value
? 'bg-blue-500 text-white shadow-md'
: 'bg-white/90 text-gray-600 border border-gray-200/50'
]">
{{ item.label }}
</view>
</view>
<view class="grid grid-cols-2 gap-4 mb-6">
<view class="bg-blue-50/60 p-3 rounded-lg backdrop-blur-sm">
<view class="flex items-center text-sm text-gray-500">
<wd-icon name="gold-coin" class="mr-1" />
<text>本日收益</text>
</view>
<text class="text-xl text-blue-600 font-bold mt-1">¥ {{ currentPromoteData.commission?.toFixed(2) || '0.00'
}}</text>
</view>
<view class="bg-blue-50/60 p-3 rounded-lg backdrop-blur-sm">
<view class="flex items-center text-sm text-gray-500">
<wd-icon name="description" class="mr-1" />
<text>有效报告</text>
</view>
<text class="text-xl text-blue-600 font-bold mt-1">{{ currentPromoteData.report || 0 }} </text>
</view>
</view>
<view class="flex items-center justify-between text-blue-500 text-sm font-semibold" @click="goToPromoteDetail">
<text>查看收益明细</text>
<text class="text-lg"></text>
</view>
</view>
<!-- 活跃下级奖励 -->
<view class="rounded-xl shadow-lg bg-gradient-to-r from-green-50/40 to-cyan-50/30 p-6">
<view class="flex justify-between items-center mb-4">
<view class="flex items-center">
<wd-icon name="friends" class="text-green-500 text-xl mr-2" />
<text class="text-lg font-bold text-gray-800">活跃下级奖励</text>
</view>
<view class="text-right">
<text class="text-2xl text-green-600 font-bold">¥ {{ (data?.active_reward?.total_reward || 0).toFixed(2)
}}</text>
<view class="text-sm text-gray-500 mt-1">活跃下级 0 </view>
</view>
</view>
<!-- 日期选择 -->
<view class="grid grid-cols-3 gap-2 mb-6">
<view v-for="item in activeDateOptions" :key="item.value" @click="selectedActiveDate = item.value" :class="[
'rounded-full transition-all py-1 px-4 text-sm',
selectedActiveDate === item.value
? 'bg-green-500 text-white shadow-md'
: 'bg-white/90 text-gray-600 border border-gray-200/50'
]">
{{ item.label }}
</view>
</view>
<view class="grid grid-cols-2 gap-2 mb-6">
<view class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<view class="flex items-center text-sm text-gray-500">
<wd-icon name="medal" class="mr-1" />
<text>本日奖励</text>
</view>
<text class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.active_reward || 0).toFixed(2)
}}</text>
</view>
<view class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<view class="flex items-center text-sm text-gray-500">
<wd-icon name="discount" class="mr-1" />
<text>下级推广奖励</text>
</view>
<text class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.sub_promote_reward ||
0).toFixed(2) }}</text>
</view>
<view class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<view class="flex items-center text-sm text-gray-500">
<wd-icon name="contact" class="mr-1" />
<text>新增活跃奖励</text>
</view>
<text class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.sub_upgrade_reward ||
0).toFixed(2) }}</text>
</view>
<view class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<view class="flex items-center text-sm text-gray-500">
<wd-icon name="fire" class="mr-1" />
<text>下级转化奖励</text>
</view>
<text class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.sub_withdraw_reward ||
0).toFixed(2) }}</text>
</view>
</view>
<view class="flex items-center justify-between text-green-500 text-sm font-semibold" @click="goToActiveDetail">
<text>查看奖励明细</text>
<text class="text-lg"></text>
</view>
</view>
</view>
</template>
<style>
button {
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-1px);
}
</style>
<route lang="json">{
"layout": "home"
}</route>

63
src/pages/agentVip.vue Normal file
View File

@@ -0,0 +1,63 @@
<template>
<view class="relative">
<image class="w-full" src="/static/image/vip_bg.png" mode="widthFix" />
<view @click="toService" class="service-btn">
点击马上报名
</view>
</view>
</template>
<script setup>
function toService() {
// 直接使用plus.runtime.openURL在系统浏览器中打开链接
// #ifdef APP-PLUS
plus.runtime.openURL('https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9');
// #endif
// #ifdef H5
window.location.href = 'https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9';
// #endif
// #ifdef MP
uni.navigateTo({
url: '/pages/agreement?url=' + encodeURIComponent('https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9')
});
// #endif
}
</script>
<style lang="scss" scoped>
.relative {
position: relative;
width: 100%;
height: 100%;
}
.service-btn {
position: absolute;
left: 50%;
bottom: 600rpx;
transform: translateX(-50%);
background: linear-gradient(to right, #2d3748, #000000, #2d3748);
padding: 10rpx 20rpx;
border-radius: 16rpx;
color: #ffffff;
font-size: 48rpx;
font-weight: bold;
box-shadow: 0 0 30rpx rgba(255, 255, 255, 0.3);
transition: transform 0.3s;
&:active {
transform: translateX(-50%) scale(1.05);
}
}
</style>
<route lang="json">
{
"layout": "page",
"title": "代理会员",
"agent": true,
"auth": true
}
</route>

View File

@@ -0,0 +1,504 @@
<template>
<view class="p-4 mx-auto min-h-screen">
<!-- 标题部分 -->
<view class="card mb-4 p-4 bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg shadow-lg text-white">
<text class="text-2xl font-extrabold mb-2 block">专业报告定价配置</text>
<text class="opacity-90 block">请选择报告类型并设置定价策略助您实现精准定价</text>
</view>
<view class="mb-4 bg-white rounded-lg overflow-hidden px-4 flex items-center justify-between">
<span class="text-blue-600 font-medium text-sm">📝 选择报告</span>
<wd-picker custom-class="flex-1" v-model="selectedReportText" :columns="reportOptions" title="选择报告类型"
@confirm="onConfirm" />
</view>
<view v-if="selectedReportText" class="space-y-6">
<!-- 配置卡片 -->
<view class="card">
<!-- 当前报告标题 -->
<view class="flex items-center mb-6">
<text class="text-xl font-semibold text-gray-800">
{{ selectedReportText }}配置
</text>
</view>
<!-- 显示当前产品的基础成本信息 -->
<view v-if="productConfigData && productConfigData.cost_price"
class="px-4 py-2 mb-4 bg-gray-50 border border-gray-200 rounded-lg shadow-sm">
<text class="text-lg font-semibold text-gray-700 block">报告基础配置信息</text>
<view class="mt-1 text-sm text-gray-600">
<text class="block">基础成本价<text class="font-medium">{{ productConfigData.cost_price }}</text> </text>
<text class="block">最高设定金额上限<text class="font-medium">{{ productConfigData.price_range_max }}</text>
</text>
<text class="block">最高设定比例上限<text class="font-medium">{{ priceRatioMax }}</text> %</text>
</view>
</view>
<!-- 分隔线 -->
<view class="my-6 flex items-center justify-center">
<view class="bg-gray-200 h-px flex-1"></view>
<text class="mx-2 text-gray-400 text-sm">成本策略配置</text>
<view class="bg-gray-200 h-px flex-1"></view>
</view>
<!-- 表单部分 -->
<wd-form>
<!-- 加价金额 -->
<wd-form-item label="加价金额" prop="price_increase_amount">
<wd-input v-model="configData.price_increase_amount" type="number" placeholder="0"
@blur="validateDecimal('price_increase_amount')" />
</wd-form-item>
<view class="text-xs text-gray-400 mt-1">
<text class="block">提示最大加价金额为{{ priceIncreaseAmountMax }}</text>
<text class="block">说明加价金额是在基础成本价上增加的额外费用决定下级报告的最低定价您将获得所有输入的金额利润</text>
</view>
<!-- 分隔线 -->
<view class="my-6 flex items-center justify-center">
<view class="bg-gray-200 h-px flex-1"></view>
<text class="mx-2 text-gray-400 text-sm">定价策略配置</text>
<view class="bg-gray-200 h-px flex-1"></view>
</view>
<!-- 定价区间最低 -->
<wd-form-item label="定价区间最低" prop="price_range_from">
<wd-input v-model="configData.price_range_from" type="number" placeholder="0"
@blur="() => { validateDecimal('price_range_from'); validateRange(); }" />
</wd-form-item>
<view class="text-xs text-gray-400 mt-1">
<text class="block">提示定价区间最低金额不能低于基础最低 {{ productConfigData?.price_range_min || 0 }} + 加价金额</text>
<text class="block">说明设定的定价区间最低金额为定价区间的起始值若下级设定的报告金额在区间内则区间内部分将按比例获得收益</text>
</view>
<!-- 定价区间最高 -->
<wd-form-item label="定价区间最高" prop="price_range_to">
<wd-input v-model="configData.price_range_to" type="number" placeholder="0"
@blur="() => { validateDecimal('price_range_to'); validateRange(); }" />
</wd-form-item>
<view class="text-xs text-gray-400 mt-1">
<text class="block">提示定价区间最高金额不能超过上限{{ productConfigData?.price_range_max || 0 }}和大于定价区间最低金额{{
priceIncreaseMax
}}</text>
<text class="block">说明设定的定价区间最高金额为定价区间的结束值若下级设定的报告金额在区间内则区间内部分将按比例获得收益</text>
</view>
<!-- 收取比例 -->
<wd-form-item label="收取比例" prop="price_ratio">
<wd-input v-model="configData.price_ratio" type="number" placeholder="0" @blur="validateRatio" />
</wd-form-item>
<view class="text-xs text-gray-400 mt-1">
<text class="block">提示最大收取比例为{{ priceRatioMax }}%</text>
<text class="block">说明收取比例表示对定价区间内即报告金额超过最低金额小于最高金额的部分的金额按此比例进行利润分成</text>
</view>
</wd-form>
</view>
<!-- 保存按钮 -->
<button type="primary" class="bg-blue-500 text-white py-1 rounded-xl w-full" @click="handleSubmit">
保存当前报告配置
</button>
</view>
<!-- 未选择提示 -->
<view v-else class="text-center py-12">
<text class="text-gray-400 text-4xl block mb-4"></text>
<text class="text-gray-500 block">请先选择需要配置的报告类型</text>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { getAgentMembershipUserConfig, saveAgentMembershipUserConfig } from '@/apis/agent'
// 报告类型选项
const reportOptions = [
{ label: '人事背调', value: 1 },
{ label: '老板企业报告', value: 2 },
{ label: '家政风险', value: 3 },
{ label: '婚恋风险', value: 4 },
{ label: '贷前背调', value: 5 },
{ label: '租赁风险', value: 6 },
{ label: '个人风险', value: 7 },
]
// 状态管理
const showPicker = ref(false)
const selectedReportId = ref(1)
const selectedReportText = ref('人事背调')
const configData = ref({})
const productConfigData = ref({})
const priceIncreaseMax = ref(null)
const priceIncreaseAmountMax = ref(null)
const priceRatioMax = ref(null)
const rangeError = ref(false)
const ratioError = ref(false)
const increaseError = ref(false)
// 金额输入格式验证:确保最多两位小数
const validateDecimal = (field) => {
console.log(`validateDecimal开始: field=${field}, 值=${configData.value[field]}`)
const value = configData.value[field]
if (value === null || value === undefined) {
console.log(`validateDecimal: ${field}为空,退出验证`)
return
}
const numValue = Number(value)
if (isNaN(numValue)) {
console.log(`validateDecimal: ${field}无法转换为数字设置为null`)
configData.value[field] = null
return
}
const fixedValue = parseFloat(numValue.toFixed(2))
console.log(`validateDecimal: ${field}原值=${numValue},处理后=${fixedValue}`)
configData.value[field] = fixedValue
if (field === 'price_increase_amount') {
console.log(`validateDecimal: 检查加价金额上限 ${fixedValue} vs ${priceIncreaseAmountMax.value}`)
if (fixedValue > priceIncreaseAmountMax.value) {
configData.value[field] = priceIncreaseAmountMax.value
console.log(`validateDecimal: 加价金额超过上限,已修正为${priceIncreaseAmountMax.value}`)
uni.showToast({
title: `加价金额最大为${priceIncreaseAmountMax.value}`,
icon: 'none'
})
increaseError.value = true
setTimeout(() => {
increaseError.value = false
}, 2000)
} else {
increaseError.value = false
}
// 当加价金额改变后,重新验证价格区间
validateRange()
}
console.log(`validateDecimal结束: ${field}最终值=${configData.value[field]}`)
}
// 价格区间验证
const validateRange = () => {
console.log('validateRange开始:',
`最低=${configData.value.price_range_from}`,
`最高=${configData.value.price_range_to}`)
// if (configData.value.price_range_from === null || configData.value.price_range_to === null) {
// console.log('validateRange: 价格区间值为null退出验证')
// rangeError.value = false
// return
// }
if (isNaN(configData.value.price_range_from) || isNaN(configData.value.price_range_to)) {
console.log('validateRange: 价格区间值非数字,退出验证')
return
}
const additional = configData.value.price_increase_amount || 0
console.log(`validateRange: 加价金额=${additional}`)
const minAllowed = parseFloat(
(Number(productConfigData.value.cost_price) + Number(additional)).toFixed(2)
) // 使用成本价作为最小值
const maxAllowed = productConfigData.value.price_range_max // 使用产品配置中的最大价格作为最大值
console.log(`validateRange: 最低允许=${minAllowed}, 最高允许=${maxAllowed}`)
// 检查最低金额
if (configData.value.price_range_from < minAllowed) {
console.log(`validateRange: 定价区间最低金额(${configData.value.price_range_from})小于允许最低值(${minAllowed}),进行修正`)
configData.value.price_range_from = minAllowed
uni.showToast({
title: `定价区间最低金额不能低于成本价 ${minAllowed}`,
icon: 'none'
})
rangeError.value = true
closeRangeError()
configData.value.price_range_to = parseFloat(
(Number(configData.value.price_range_from) + Number(priceIncreaseMax.value)).toFixed(2)
)
console.log(`validateRange: 已调整最高金额为 ${configData.value.price_range_to}`)
return
}
// 检查最高金额是否小于最低金额
if (configData.value.price_range_to < configData.value.price_range_from) {
console.log(`validateRange: 定价区间最高金额(${configData.value.price_range_to})小于最低金额(${configData.value.price_range_from}),进行修正`)
uni.showToast({
title: '定价区间最高金额不能低于定价区间最低金额',
icon: 'none'
})
if (configData.value.price_range_from + priceIncreaseMax.value > maxAllowed) {
configData.value.price_range_to = maxAllowed
console.log(`validateRange: 最高值已修正为最大允许值 ${maxAllowed}`)
} else {
configData.value.price_range_to = configData.value.price_range_from + priceIncreaseMax.value
console.log(`validateRange: 最高值已修正为最低金额+最大增加值 ${configData.value.price_range_to}`)
}
rangeError.value = true
closeRangeError()
return
}
// 检查差值
const diff = parseFloat(
(configData.value.price_range_to - configData.value.price_range_from).toFixed(2)
)
console.log(`validateRange: 价格区间差值=${diff}, 最大允许差值=${priceIncreaseMax.value}`)
if (diff > priceIncreaseMax.value) {
console.log(`validateRange: 价格区间差值超过最大允许值,进行修正`)
uni.showToast({
title: `价格区间最大差值为${priceIncreaseMax.value}`,
icon: 'none'
})
configData.value.price_range_to = parseFloat(
(Number(configData.value.price_range_from) + Number(priceIncreaseMax.value)).toFixed(2)
)
console.log(`validateRange: 已调整最高金额为 ${configData.value.price_range_to}`)
closeRangeError()
return
}
// 检查最高金额是否超过上限
if (configData.value.price_range_to > maxAllowed) {
console.log(`validateRange: 定价区间最高金额(${configData.value.price_range_to})超过上限(${maxAllowed}),进行修正`)
configData.value.price_range_to = maxAllowed
uni.showToast({
title: `定价区间最高金额不能超过 ${maxAllowed}`,
icon: 'none'
})
closeRangeError()
}
if (!rangeError.value) {
rangeError.value = false
}
console.log('validateRange结束:',
`最终最低=${configData.value.price_range_from}`,
`最终最高=${configData.value.price_range_to}`)
}
// 收取比例验证(修改为保留两位小数,不再四舍五入取整)
const validateRatio = () => {
console.log(`validateRatio开始: 值=${configData.value.price_ratio}`)
let value = configData.value.price_ratio
if (value === null || value === undefined) {
console.log('validateRatio: 值为空,退出验证')
return
}
const numValue = Number(value)
if (isNaN(numValue)) {
console.log('validateRatio: 值无法转换为数字设置为null')
configData.value.price_ratio = null
ratioError.value = true
return
}
console.log(`validateRatio: 检查比例范围 ${numValue} vs 最大值${priceRatioMax.value}`)
if (numValue > priceRatioMax.value) {
console.log(`validateRatio: 比例超过最大值,已修正为${priceRatioMax.value}`)
configData.value.price_ratio = priceRatioMax.value
uni.showToast({
title: `收取比例最大为${priceRatioMax.value}%`,
icon: 'none'
})
ratioError.value = true
setTimeout(() => {
ratioError.value = false
}, 1000)
} else if (numValue < 0) {
console.log('validateRatio: 比例小于0已修正为0')
configData.value.price_ratio = 0
ratioError.value = true
} else {
configData.value.price_ratio = parseFloat(numValue.toFixed(2))
ratioError.value = false
}
console.log(`validateRatio结束: 最终值=${configData.value.price_ratio}`)
}
// 获取配置
const getConfig = async () => {
try {
console.log(`getConfig开始: 获取产品ID=${selectedReportId.value}的配置`)
const res = await getAgentMembershipUserConfig({ product_id: selectedReportId.value })
if (res.code === 200) {
const respConfigData = res.data.agent_membership_user_config
console.log("respConfigData", respConfigData)
configData.value = {
id: respConfigData.product_id,
price_range_from: respConfigData.price_range_from || null,
price_range_to: respConfigData.price_range_to || null,
price_ratio: respConfigData.price_ratio * 100 || null, // 转换为百分比
price_increase_amount: respConfigData.price_increase_amount || null,
}
productConfigData.value = res.data.product_config
// 设置动态限制值
priceIncreaseMax.value = res.data.price_increase_max
priceIncreaseAmountMax.value = res.data.price_increase_amount
priceRatioMax.value = res.data.price_ratio * 100
console.log('getConfig: 配置加载成功',
`最大差值=${priceIncreaseMax.value}`,
`最大加价=${priceIncreaseAmountMax.value}`,
`最大比例=${priceRatioMax.value}%`)
console.log('getConfig: 当前配置', configData.value)
}
} catch (error) {
console.error("getConfig错误:", error)
uni.showToast({
title: '配置加载失败',
icon: 'none'
})
}
}
// 提交处理
const handleSubmit = async () => {
try {
if (!finalValidation()) {
return
}
// 前端数据转换
const submitData = {
product_id: configData.value.id,
price_range_from: configData.value.price_range_from || 0,
price_range_to: configData.value.price_range_to || 0,
price_ratio: (configData.value.price_ratio || 0) / 100, // 转换为小数
price_increase_amount: configData.value.price_increase_amount || 0,
}
const res = await saveAgentMembershipUserConfig(submitData)
if (res.code === 200) {
uni.showToast({
title: '保存成功',
icon: 'success'
})
getConfig()
}
} catch (error) {
uni.showToast({
title: '保存失败,请稍后重试',
icon: 'none'
})
}
}
// 最终验证函数
const finalValidation = () => {
// 校验最低金额不能为空且大于0
if (!configData.value.price_range_from || configData.value.price_range_from <= 0) {
uni.showToast({
title: "定价区间最低金额不能为空",
icon: 'none'
})
return false
}
// 校验最高金额不能为空且大于0
if (!configData.value.price_range_to || configData.value.price_range_to <= 0) {
uni.showToast({
title: "定价区间最高金额不能为空",
icon: 'none'
})
return false
}
// 校验收取比例不能为空且大于0
if (!configData.value.price_ratio || configData.value.price_ratio <= 0) {
uni.showToast({
title: "收取比例不能为空",
icon: 'none'
})
return false
}
// 验证最低金额必须小于最高金额
if (configData.value.price_range_from >= configData.value.price_range_to) {
uni.showToast({
title: "定价区间最低金额必须小于定价区间最高金额",
icon: 'none'
})
return false
}
// 验证价格区间差值不能超过最大允许差值
const finalDiff = parseFloat(
(configData.value.price_range_to - configData.value.price_range_from).toFixed(2)
)
if (finalDiff > priceIncreaseMax.value) {
uni.showToast({
title: `价格区间最大差值为${priceIncreaseMax.value}`,
icon: 'none'
})
return false
}
// 验证最高金额不能超过产品配置中设定的上限
if (configData.value.price_range_to > productConfigData.value.price_range_max) {
uni.showToast({
title: `定价区间最高金额不能超过${productConfigData.value.price_range_max}`,
icon: 'none'
})
return false
}
// 验证最低金额不能低于成本价+加价金额(加价金额允许为空)
const additional = configData.value.price_increase_amount || 0
if (configData.value.price_range_from < productConfigData.value.cost_price + additional) {
uni.showToast({
title: `定价区间最低金额不能低于成本价${productConfigData.value.cost_price + additional}`,
icon: 'none'
})
return false
}
return true
}
// 选择器确认
const onConfirm = (e) => {
const { selectedItems } = e
selectedReportId.value = selectedItems.value
selectedReportText.value = selectedItems.label
showPicker.value = false
// 重置错误状态
rangeError.value = false
ratioError.value = false
increaseError.value = false
getConfig()
}
const closeRangeError = () => {
setTimeout(() => {
rangeError.value = false
}, 2000)
}
onMounted(() => {
getConfig()
})
</script>
<style scoped>
.card {
border-radius: 12px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 16px;
margin-bottom: 16px;
}
.space-y-6>* {
margin-bottom: 24px;
}
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "会员代理报告配置",
"agent": true,
"auth": true
}</route>

63
src/pages/agreement.vue Normal file
View File

@@ -0,0 +1,63 @@
<script setup>
const webviewStyles = ref({
top: `${uni.getSystemInfoSync().statusBarHeight + 44}px`, // 距离顶部的距离
height: `${uni.getSystemInfoSync().windowHeight - uni.getSystemInfoSync().statusBarHeight - 44}px`, // 高度
position: 'absolute', // 绝对定位
dock: 'bottom', // 停靠在底部
bounce: 'vertical', // 垂直方向的回弹效果
})
// 从环境变量获取基础URL
const BASE_URL = import.meta.env.VITE_APP_BASE_URL || 'https://www.quannengcha.com'
// 协议路径和标题的映射
const agreementMap = {
user: {
path: '/app/userAgreement',
title: '用户协议'
},
privacy: {
path: '/app/privacyPolicy',
title: '隐私政策'
},
authorization: {
path: '/app/authorization',
title: '授权书'
},
service: {
path: '/app/agentSerivceAgreement',
title: '信息技术服务合同'
},
manage: {
path: '/app/agentManageAgreement',
title: '推广方管理制度协议'
}
}
const agreementUrl = ref('')
const pageTitle = ref('协议')
// 使用 uniapp 的 onLoad 生命周期钩子获取页面参数
onLoad((option) => {
const type = option.type || 'user'
if (agreementMap[type]) {
agreementUrl.value = `${BASE_URL}${agreementMap[type].path}`
pageTitle.value = agreementMap[type].title
// 设置标题 - 此标题会传递给 page 布局中的 wd-navbar
uni.setNavigationBarTitle({
title: pageTitle.value
})
}
})
function handleClickLeft() {
uni.navigateBack()
}
</script>
<template>
<view>
<wd-navbar :title="pageTitle" left-text="返回" placeholder left-arrow safe-area-inset-top fixed @click-left="handleClickLeft" />
<web-view :webview-styles="webviewStyles" :src="agreementUrl" />
</view>
</template>

254
src/pages/ai.vue Normal file
View File

@@ -0,0 +1,254 @@
<template>
<view class="safe-area-top flex flex-col box-border from-blue-100 to-white bg-gradient-to-b pt-4 flex-1 pb-4">
<!-- 角色切换部分 -->
<view class="flex border-blue-300 mx-4 p-1 bg-white rounded-xl">
<view class="flex items-center flex-1 p-2 box-border rounded-xl cursor-pointer"
:class="selectedRole === 'legal' ? 'bg-blue-300 text-white' : ''" @click="selectRole('legal')">
<image src="/static/image/ai_picture.webp" class="w-10 h-10 rounded-xl mr-2" alt="AI律师" />
法律咨询
</view>
<view class="flex items-center flex-1 p-2 box-border rounded-xl cursor-pointer"
:class="selectedRole === 'emotional' ? 'bg-blue-300 text-white' : ''" @click="selectRole('emotional')">
<image src="/static/image/ai_qinggan.png" class="w-10 h-10 rounded-xl mr-2" alt="AI心理咨询师" />
情感咨询
</view>
</view>
<!-- 聊天窗口 -->
<view class="mx-4 flex flex-col flex-1 rounded-xl shadow-lg">
<view class="flex flex-col flex-1 p-4">
<view class="flex-1 overflow-y-auto">
<view v-for="(message, index) in currentMessages" :key="index" class="mb-4">
<view v-if="message.sender === 'ai'" class="flex justify-start items-start">
<!-- 根据角色显示不同的AI形象 -->
<image :src="selectedRole === 'legal' ? '/static/image/ai_picture.webp' : '/static/image/ai_qinggan.png'"
class=" flex-shrink-0 w-10 h-10 rounded-xl mr-2" :alt="selectedRole === 'legal' ? 'AI律师' : 'AI心理咨询师'" />
<view class="inline-block max-w-max rounded-xl bg-white p-2 text-left"
:class="selectedRole === 'legal' ? 'text-green-600' : 'text-purple-600'">
<!-- 如果是AI消息显示加载中或文本 -->
<view v-if="message.loading" class="flex justify-center items-center">
<view class="loader"></view> <!-- 加载动画 -->
</view>
<view v-else>
{{ message.text }}
</view>
</view>
</view>
<view v-else class="flex justify-end">
<view class="ml-auto inline-block max-w-max rounded-xl from-sky-300 via-sky-300 to-sky-300
bg-gradient-to-r p-2 text-right text-white font-medium shadow-md">
{{ message.text }}
</view>
</view>
</view>
</view>
<!-- 输入区域 -->
<view class="p-2 w-full input-area flex items-center gap-2">
<input v-model="userMessage" :placeholder="selectedRole === 'legal' ? '请输入您的法律问题...' : '请输入您的情感问题...'"
class="flex-1 p-2 border rounded-lg" type="text" @confirm="sendMessage" />
<button class="shadow p-2 bg-blue-500 text-white rounded-lg" @click="sendMessage">
发送
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onBeforeUnmount, computed } from 'vue'
import chatEncrypt from "@/utils/chatEncrypt"
// 当前选中的角色,默认是法律咨询
const selectedRole = ref('legal')
// 用户输入的消息
const userMessage = ref('')
// 存储每个角色的消息记录
const messages = ref({
legal: [
{ sender: 'ai', text: '欢迎!请问有什么法律问题需要帮助?' },
],
emotional: [
{ sender: 'ai', text: '欢迎!请问有什么情感问题需要帮助?' },
]
})
// 存储每个角色的 sessionID
const sessionID = ref({
legal: "",
emotional: ""
})
let reader = {
legal: null,
emotional: null
}
// 计算当前选中的角色的消息
const currentMessages = computed(() => messages.value[selectedRole.value])
// 选择角色
function selectRole(role) {
if (selectedRole.value !== role) {
selectedRole.value = role
// 如果该角色还没有消息记录,初始化欢迎消息
if (!messages.value[role]) {
messages.value[role] = [
{ sender: 'ai', text: role === 'legal' ? '欢迎!请问有什么法律问题需要帮助?' : '欢迎!请问有什么情感问题需要帮助?' },
]
sessionID.value[role] = ""
}
}
}
async function sendMessage() {
if (userMessage.value.trim() === '') return;
// 获取当前角色
const role = selectedRole.value
// 用户消息推入对应角色的消息列表
messages.value[role].push({ sender: 'user', text: userMessage.value })
const t = Date.now()
const x = chatEncrypt(String(t))
// 在消息列表中添加一个 AI 消息并标记为加载状态
const aiMessage = { sender: 'ai', text: '', loading: true }
messages.value[role].push(aiMessage)
console.log("import.meta.env.VITE_APP_BASE_URL", import.meta.env.VITE_APP_BASE_URL)
// 发起请求
try {
const response = await uni.request({
url: import.meta.env.VITE_APP_BASE_URL + '/api/v1/chat/send',
method: 'POST',
header: {
'Content-Type': 'application/json',
'x': x,
't': t,
},
data: {
prompt: userMessage.value,
platform_id: 2,
// roleid: role === 'legal' ? 1 : 2, // 根据角色选择不同的 roleid
role_id: 1, // 根据角色选择不同的 roleid
openid: 'openid' + uni.getStorageSync("token"),
userid: 'userid' + uni.getStorageSync("token"),
sessionid: sessionID.value[role] // 使用对应角色的 sessionID
},
responseType: 'text',
enableChunked: true, // 启用分块传输
enableStream: true, // 启用流式传输
success: (streamRes) => {
if (streamRes.statusCode === 200) {
aiMessage.loading = false;
// 处理流式响应
const chunks = streamRes.data.split('\n');
for (const chunk of chunks) {
if (chunk.trim()) {
let message = chunk;
if (message.startsWith('data:')) {
message = message.replace('data:', '').trim();
}
console.log("message", message);
try {
const parsedMessage = JSON.parse(message);
console.log("parsedMessage", parsedMessage);
if (parsedMessage?.output?.session_id) {
// 存储 session_id
sessionID.value[role] = parsedMessage?.output?.session_id;
}
// 获取 AI 回复的文本
const aiText = parsedMessage.output.text || '';
// 模拟打字机效果(逐字添加文本)
const addTextWithTypingEffect = (text) => {
if (!text) return;
// 直接添加到消息中(实时更新)
aiMessage.text += text;
// 强制更新视图
messages.value[role] = [...messages.value[role]];
};
// 执行打字机效果
addTextWithTypingEffect(aiText);
} catch (e) {
console.error('Failed to parse message:', e);
}
}
}
}
},
fail: (err) => {
console.error('Request failed:', err);
aiMessage.loading = false;
aiMessage.text = '抱歉,请求失败,请重试';
messages.value[role] = [...messages.value[role]];
}
});
} catch (error) {
console.error('Request error:', error);
}
userMessage.value = ''; // 清空输入框
}
onBeforeUnmount(() => {
// 在UniApp中不需要特别取消流读取UniApp会自动处理
})
</script>
<style scoped>
input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 0.5rem;
}
button {
padding: 0.5rem 1rem;
background-color: #007bff;
border: none;
border-radius: 0.5rem;
color: white;
}
button:hover {
background-color: #0056b3;
}
.loader {
border: 4px solid #f3f3f3;
/* Light gray */
border-top: 4px solid #3498db;
/* Blue */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Tab Styles */
.flex.border-blue-300>view {
transition: all 0.3s;
}
</style>
<route lang="json">{
"layout": "home"
}</route>

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

@@ -0,0 +1,227 @@
<script setup>
import indexIcon1 from '/static/image/index_icon_1.png'
import indexIcon2 from '/static/image/index_icon_2.png'
import indexIcon3 from '/static/image/index_icon_3.png'
import indexIcon4 from '/static/image/index_icon_4.png'
import indexIcon5 from '/static/image/index_icon_5.png'
import indexIcon6 from '/static/image/index_icon_6.png'
import indexIcon7 from '/static/image/index_icon_7.png'
// 引入 WebView 插件
// @ts-ignore
import WebViewPlugin from '@/uni_modules/webview/utssdk';
const webViewPlugin = new WebViewPlugin();
const services = ref(
[
{
title: '个人风险',
name: 'riskassessment',
subtitle: '一查全知',
bg: indexIcon2,
bgColor: 'bg-indigo-400',
position: 'rounded-tl-[35px] rounded-bl-[35px] rounded-tr-lg rounded-br-lg'
},
{
title: '婚恋风险',
name: 'marriage',
subtitle: '查婚姻状态让爱无忧',
bg: indexIcon1,
bgColor: ' bg-pink-400 ',
position: 'rounded-tr-[35px] rounded-br-[35px] rounded-tl-lg rounded-bl-lg'
},
{
title: '家政服务',
name: 'homeservice',
subtitle: '用人有保障',
bg: indexIcon3,
bgColor: ' bg-teal-500 ',
position: 'rounded-tl-[35px] rounded-bl-[35px] rounded-tr-lg rounded-br-lg'
},
{
title: '租赁风险',
name: 'rentalinfo',
subtitle: '一查明了',
bg: indexIcon4,
bgColor: ' bg-sky-500 ',
position: 'rounded-tl-[35px] rounded-bl-[35px] rounded-tr-lg rounded-br-lg'
},
{
title: '企业报告',
name: 'companyinfo',
subtitle: '合作更安心',
bg: indexIcon5,
bgColor: ' bg-blue-400 ',
position: 'rounded-tr-[35px] rounded-br-[35px] rounded-tl-lg rounded-bl-lg'
},
{
title: '人事背调',
name: 'backgroundcheck',
subtitle: '招聘有保障,选人更放心',
bg: indexIcon6,
bgColor: ' bg-orange-400 ',
position: 'rounded-tl-[35px] rounded-bl-[35px] rounded-tr-lg rounded-br-lg'
},
{
title: '贷前背调',
name: 'preloanbackgroundcheck',
subtitle: '招聘有保障,选人更放心',
bg: indexIcon7,
bgColor: ' bg-orange-400 ',
position: 'rounded-tr-[35px] rounded-br-[35px] rounded-tl-lg rounded-bl-lg'
},
]
)
function toInquire(name) {
// 使用服务名称构建URL
console.log('使用服务:', name);
// 判断环境,在 App 环境中使用 UTS 插件打开,非 App 环境使用普通导航
// #ifdef APP-PLUS
try {
// 构建服务的URL
const serviceUrl = `https://www.quannengcha.com/inquire/${name}`;
// 使用 UTS 插件打开网页
webViewPlugin.openUrl(serviceUrl);
} catch (error) {
console.error('打开URL失败:', error);
}
// #endif
}
function toHistory() {
uni.navigateTo({
url: '/pages/queryHistory',
})
}
function toInvitation() {
uni.navigateTo({
url: '/pages/invitation',
})
}
function toPromote() {
uni.navigateTo({
url: '/pages/promote',
})
}
function toHelp() {
uni.navigateTo({
url: '/pages/help',
})
}
</script>
<template>
<view class="box-border">
<view class="relative h-[190px]">
<image class="h-full w-full" src="/static/image/banner2.png" />
<image
class="absolute bottom-[-70px] right-0 w-38"
src="/static/image/banner_a.png"
mode="aspectFit"
/>
</view>
<view class="mt-4">
<view class="flex items-center justify-around gap-2 px-4 pb-1">
<view class="" @click="toPromote">
<view
class="h-12 w-12 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<image src="/static/image/icon_tg.svg" alt="直推报告" class="w-12 h-12" mode="aspectFit" />
</view>
<view class="text-center mt-1 font-bold">直推报告</view>
</view>
<view class="" @click="toInvitation">
<view
class="h-12 w-12 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<image src="/static/image/icon_xj.svg" alt="邀请下级" class="w-12 h-12" mode="aspectFit" />
</view>
<view class="text-center mt-1 font-bold">邀请下级</view>
</view>
<view class="" @click="toHelp">
<view
class="h-12 w-12 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<image src="/static/image/icon_bz.svg" alt="帮助中心" class="w-12 h-12" mode="aspectFit" />
</view>
<view class="text-center mt-1 font-bold">帮助中心</view>
</view>
<view class="" @click="toHistory">
<view
class="h-12 w-12 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<image src="/static/image/icon_bg.svg" alt="我的报告" class="w-12 h-12" mode="aspectFit" />
</view>
<view class="text-center mt-1 font-bold">我的报告</view>
</view>
</view>
</view>
<view class="relative p-4 pb-4 pt-2">
<view class="grid grid-cols-2 gap-3">
<template v-for="(service, index) in services" :key="index">
<view
class="relative flex flex-col px-4 py-2 shadow-lg " :class="[
service.position,
service.bgColor,
service.title === '婚恋风险' ? 'row-span-2' : '',
]" :style="`background: url(${service.bg}) no-repeat; background-size: cover; background-position: center;`"
@click="toInquire(service.name)"
>
<view class="min-h-18 flex flex-col items-start px-1">
<view class="mt-1 max-w-max text-left text-gray-600 font-bold">
{{ service.title }}
</view>
<view class="mt-2 rounded-2xl px-2 text-xs text-white" :class="[service.bgColor]">
GO >
</view>
</view>
</view>
</template>
</view>
<view
class="mt-4 box-border h-14 w-full flex items-center rounded-xl bg-white px-4 text-gray-700 shadow-xl"
@click="toHistory"
>
<image
class="mr-4 h-10 w-10"
src="/static/image/bg_icon.png"
mode="widthFix"
/>
<view class="">
<view class="font-bold">
我的历史查询记录
</view>
<view class="text-xs">
查询记录有效期为30天
</view>
</view>
</view>
<view
class="mb-16 mt-6 h-12 w-full flex items-center justify-center rounded-3xl from-blue-500 to-sky-400 bg-gradient-to-b text-center text-lg text-white line-height-12 shadow-xl"
>
<view>全能查邀您共赢共享数据新价值</view>
</view>
</view>
</view>
</template>
<style scoped>
.clip-left {
clip-path: polygon(0 0, 0 100%, 90% 100%, 0 100%);
}
.clip-right {
clip-path: polygon(0 0, 0 0, 90% 100%, 0 0);
}
</style>
<route type="home" lang="json">
{
"layout": "home",
"style": {
"navigationBarTextStyle": "white",
"transparentTitle": "always"
}
}
</route>

51
src/pages/invitation.vue Normal file
View File

@@ -0,0 +1,51 @@
<template>
<view>
<image src="/static/image/invitation.png" alt="邀请下级" mode="widthFix" class="w-full" />
<view @click="showQRcode = true"
class="bg-gradient-to-t from-orange-500 to-orange-300 fixed bottom-0 h-12 w-full shadow-xl text-white rounded-t-xl flex items-center justify-center font-bold">
立即邀请好友
</view>
<QRcode v-model:show="showQRcode" mode="invitation" :linkIdentifier="linkIdentifier" />
</view>
</template>
<script setup>
import { ref, onBeforeMount } from 'vue'
import { aesEncrypt } from "@/utils/crypto"
import QRcode from '@/components/QRcode.vue'
const showQRcode = ref(false)
const linkIdentifier = ref("")
const mobile = ref("")
const agentID = ref("")
onBeforeMount(() => {
// 从UniApp缓存获取用户信息
const userInfo = uni.getStorageSync('userInfo') || {}
mobile.value = userInfo.mobile || ''
agentID.value = userInfo.agentID || ''
encryptIdentifire(agentID.value, mobile.value)
})
const encryptIdentifire = (agentID, mobile) => {
const linkIdentifierJSON = {
agentID,
mobile
}
const linkIdentifierStr = JSON.stringify(linkIdentifierJSON)
const encodeData = aesEncrypt(linkIdentifierStr, "8e3e7a2f60edb49221e953b9c029ed10")
linkIdentifier.value = encodeURIComponent(encodeData)
}
</script>
<style>
/* 自定义样式 */
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "邀请下级",
"auth": true,
"agent": true
}</route>

View File

@@ -0,0 +1,199 @@
<template>
<view class="min-h-screen bg-[#D1D6FF]">
<image src="/static/image/invitation_agent_apply.png" alt="邀请代理申请" mode="widthFix" class="w-full" />
<!-- 统一状态处理容器 -->
<view class="flex flex-col items-center justify-center">
<!-- 审核中状态 -->
<view v-if="displayStatus === 0" class="text-center">
<text class="text-xs text-gray-500 block">您的申请正在审核中</text>
<view class="bg-gray-200 p-1 rounded-3xl shadow-xl mt-1">
<view class="text-xl font-bold px-8 py-2 bg-gray-400 text-white rounded-3xl shadow-lg cursor-not-allowed">
审核进行中
</view>
</view>
</view>
<!-- 审核通过状态 -->
<view v-if="displayStatus === 1" class="text-center">
<text class="text-xs text-gray-500 block">您已成为认证代理方</text>
<view class="bg-green-100 p-1 rounded-3xl shadow-xl mt-1" @click="goToHome">
<view
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-green-500 to-green-300 text-white rounded-3xl shadow-lg cursor-pointer">
进入应用首页
</view>
</view>
</view>
<!-- 审核未通过状态 -->
<view v-if="displayStatus === 2" class="text-center">
<text class="text-xs text-red-500 block">审核未通过请重新提交</text>
<view class="bg-red-100 p-1 rounded-3xl shadow-xl mt-1" @click="agentApply">
<view
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-red-500 to-red-300 text-white rounded-3xl shadow-lg cursor-pointer">
重新提交申请
</view>
</view>
</view>
<!-- 未申请状态包含邀请状态 -->
<view v-if="displayStatus === 3" class="text-center">
<text class="text-xs text-gray-500 block">{{ isSelf ? '立即申请成为代理人' : '邀您注册代理人' }}</text>
<view class="bg-gray-100 p-1 rounded-3xl shadow-xl mt-1" @click="agentApply">
<view
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-blue-500 to-blue-300 text-white rounded-3xl shadow-lg cursor-pointer">
立即成为代理方
</view>
</view>
</view>
</view>
<AgentApplicationForm v-model:show="showApplyPopup" @submit="submitApplication" @close="showApplyPopup = false"
:ancestor="ancestor" />
</view>
</template>
<script setup>
import { ref, computed, onBeforeMount } from 'vue'
import { getAgentInfo, applyAgent } from '@/apis/agent'
import AgentApplicationForm from '@/components/AgentApplicationForm.vue'
const showApplyPopup = ref(false)
const status = ref(3) // 默认为未申请状态
const ancestor = ref("")
const isSelf = ref(true)
let intervalId = null // 保存定时器 ID
// 计算显示状态当isSelf为false时强制显示为3
const displayStatus = computed(() => {
return !isSelf.value ? 3 : status.value
})
// 打开申请表单
const agentApply = () => {
showApplyPopup.value = true
}
// 跳转到首页
const goToHome = () => {
clearInterval(intervalId)
uni.switchTab({
url: '/pages/index'
})
}
const getAgentInformation = async () => {
const token = uni.getStorageSync("token")
if (!token) {
return
}
try {
const res = await getAgentInfo()
if (res.code === 200 && res.data) {
// 将代理信息存入缓存
uni.setStorageSync("agentInfo", {
level: res.data.level,
isAgent: res.data.is_agent, // 判断是否是代理
status: res.data.status, // 获取代理状态 0=待审核1=审核通过2=审核未通过3=未申请
agentID: res.data.agent_id,
mobile: res.data.mobile
})
status.value = res.data.status
console.log('代理信息已获取并存入缓存')
}
} catch (error) {
console.error('获取代理信息失败', error)
}
}
// 提交代理申请
const submitApplication = async (formData) => {
try {
const { region, mobile, wechat_id, code } = formData
let postData = {
region,
mobile,
wechat_id,
code,
}
if (!isSelf.value) {
postData.ancestor = ancestor.value
}
const res = await applyAgent(postData)
if (res.code === 200) {
showApplyPopup.value = false
uni.showToast({
title: "已提交申请",
icon: 'success'
})
if (res.data.accessToken) {
uni.setStorageSync('token', res.data.accessToken)
uni.setStorageSync('refreshAfter', res.data.refreshAfter)
uni.setStorageSync('accessExpire', res.data.accessExpire)
refreshAgentStatus()
}
} else {
uni.showToast({
title: res.msg || '申请失败',
icon: 'none'
})
}
} catch (error) {
uni.showToast({
title: '网络错误',
icon: 'none'
})
}
}
// 定时刷新代理状态
const refreshAgentStatus = () => {
if (status.value === 3) {
if (intervalId) clearInterval(intervalId)
intervalId = setInterval(() => {
if (status.value !== 3) {
clearInterval(intervalId)
intervalId = null
return
}
getAgentInformation()
}, 2000)
} else {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
}
onLoad(() => {
const token = uni.getStorageSync('token')
if (token) {
// 从缓存中获取代理信息
const agentInfo = uni.getStorageSync('agentInfo')
if (agentInfo) {
status.value = agentInfo.status || 3
}
getAgentInformation()
}
})
// 页面卸载时清除定时器
onUnload(() => {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
})
</script>
<route type="page" lang="json">{
"layout": "page",
"title": "代理申请",
"auth": true
}</route>

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

@@ -0,0 +1,223 @@
<script setup>
import { getCode, login, getUserInfo } from '@/api/apis'
import { getAgentInfo } from '@/apis/agent'
const phoneNumber = ref('')
const verificationCode = ref('')
const password = ref('')
const isPasswordLogin = ref(false)
const isAgreed = ref(false)
const isCountingDown = ref(false)
const countdown = ref(60)
let timer = null
// 聚焦状态变量
const phoneFocused = ref(false)
const codeFocused = ref(false)
const passwordFocused = ref(false)
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value)
})
const canLogin = computed(() => {
if (!isPhoneNumberValid.value)
return false
if (isPasswordLogin.value) {
return password.value.length >= 6
}
else {
return verificationCode.value.length === 6
}
})
function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value)
return
if (!isPhoneNumberValid.value) {
uni.showToast({ title: '请输入有效的手机号', icon: 'none' })
return
}
getCode({
mobile: phoneNumber.value,
actionType: 'login',
}).then((res) => {
if (res.code === 200) {
uni.showToast({ title: '获取成功', icon: 'none' })
startCountdown()
}
})
}
function startCountdown() {
isCountingDown.value = true
countdown.value = 60
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
}
else {
clearInterval(timer)
isCountingDown.value = false
}
}, 1000)
}
function handleLogin() {
if (!canLogin.value) {
uni.showToast({ title: '请完善信息', icon: 'none' })
return
}
if (!isAgreed.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
return
}
login({ mobile: phoneNumber.value, code: verificationCode.value }).then((res) => {
if (res.code === 200) {
uni.setStorageSync('token', res.data.accessToken)
uni.setStorageSync('refreshAfter', res.data.refreshAfter)
uni.setStorageSync('accessExpire', res.data.accessExpire)
uni.showToast({ title: '登录成功', icon: 'none' })
getUser()
getAgentInformation()
uni.reLaunch({
url: '/pages/index',
})
}
else {
uni.showToast({ title: res.msg, icon: 'none' })
}
})
}
function toUserAgreement() {
uni.navigateTo({
url: '/pages/agreement?type=user',
})
}
function toPrivacyPolicy() {
uni.navigateTo({
url: '/pages/agreement?type=privacy',
})
}
const getAgentInformation = async () => {
const token = uni.getStorageSync("token")
if (!token) {
return
}
try {
const res = await getAgentInfo()
if (res.code === 200 && res.data) {
// 将代理信息存入缓存
uni.setStorageSync("agentInfo", {
level: res.data.level,
isAgent: res.data.is_agent, // 判断是否是代理
status: res.data.status, // 获取代理状态 0=待审核1=审核通过2=审核未通过3=未申请
agentID: res.data.agent_id,
mobile: res.data.mobile
})
console.log('代理信息已获取并存入缓存')
}
} catch (error) {
console.error('获取代理信息失败', error)
}
}
const getUser = async () => {
const token = uni.getStorageSync("token")
if (!token) {
return
}
const res = await getUserInfo()
if (res.code === 200) {
console.log(res.data)
uni.setStorageSync("userInfo", res.data.userInfo)
}
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
<view class="login px-8">
<view class="mb-8 pt-8 text-left">
<view class="flex flex-col items-center">
<image class="h-18 w-18 rounded-full shadow" src="/static/image/logo.png" mode="scaleToFill" />
<image class="mt-4 h-10" src="/static/image/logo_title.png" mode="aspectFit" />
</view>
</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="number" placeholder="请输入手机号" maxlength="11"
@focus="phoneFocused = true" @blur="phoneFocused = false">
</view>
<view v-if="!isPasswordLogin">
<view class="flex items-center justify-between">
<view class="input-container bg-blue-300/20" :class="[codeFocused ? 'focused' : '']">
<input v-model="verificationCode" class="input-field" type="number" placeholder="请输入验证码" maxlength="6"
@focus="codeFocused = true" @blur="codeFocused = false">
</view>
<view
class="ml-2 flex-shrink-0 rounded-lg px-4 py-2 text-sm font-bold transition duration-300 focus:outline-none"
: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重新获取` : '获取验证码' }}
</view>
</view>
</view>
<view v-if="isPasswordLogin" class="input-container" :class="[passwordFocused ? 'focused' : '']">
<input v-model="password" class="input-field" type="password" placeholder="请输入密码"
@focus="passwordFocused = true" @blur="passwordFocused = false">
</view>
<view class="flex items-start space-x-2">
<wd-checkbox v-model="isAgreed" class="mt-1" />
<text class="text-xs text-gray-400 leading-tight">
未注册手机号登录后将自动生成账号并且代表您已阅读并同意
<text class="cursor-pointer text-blue-400" @click="toUserAgreement">
用户协议
</text>
<text class="cursor-pointer text-blue-400" @click="toPrivacyPolicy">
隐私政策
</text>
</text>
</view>
</view>
<button
class="mt-20 block w-full flex-shrink-0 rounded-full bg-blue-500 py-3 text-lg text-white font-bold transition duration-300"
@click="handleLogin">
登录
</button>
</view>
</template>
<style scoped>
.login {}
.input-container {
border: 2px solid rgba(125, 211, 252, 0.0);
border-radius: 1rem;
@apply transition duration-200
}
.input-container.focused {
border: 2px solid #3b82f6;
}
.input-field {
width: 100%;
padding: 1rem;
transition: border-color 0.3s ease;
outline: none;
background: transparent;
}
</style>
<route lang="json">{
"layout": "login",
"title": "登录"
}</route>

246
src/pages/me.vue Normal file
View File

@@ -0,0 +1,246 @@
<template>
<view class="safe-area-top box-border min-h-screen">
<view class="flex flex-col p-4 space-y-6">
<!-- 用户信息卡片 -->
<view
class="profile-section group relative flex items-center gap-4 rounded-xl bg-white p-6 shadow-lg transition-all hover:shadow-xl"
@click="!isLoggedIn ? redirectToLogin() : null">
<view class="relative">
<!-- 头像容器添加overflow-hidden解决边框问题 -->
<view class="flex items-center justify-center overflow-hidden rounded-full p-0.5"
:class="levelGradient.border">
<image :src="userAvatar || headShot" alt="User Avatar" class="h-24 w-24 rounded-full border-4 border-white">
</image>
</view>
<!-- 代理标识 -->
<view v-if="isAgent" class="absolute -bottom-2 -right-2">
<view class="flex items-center justify-center rounded-full px-3 py-1 text-xs font-bold text-white shadow-sm"
:class="levelGradient.badge">
{{ levelNames[level] }}
</view>
</view>
</view>
<view class="space-y-1">
<view class="text-2xl font-bold text-gray-800">
{{ isLoggedIn ? maskName(userName) : '点击登录' }}
</view>
<view v-if="isAgent" class="text-sm font-medium" :class="levelGradient.text">
🎖 {{ levelText[level] }}
</view>
</view>
</view>
<VipBanner v-if="isAgent && level === 'normal'" />
<!-- 功能菜单 -->
<view class="features-section space-y-3">
<template v-if="isAgent && ['VIP', 'SVIP'].includes(level)">
<button
class="feature-item bg-gradient-to-r from-purple-200/80 to-pink-200/80 hover:from-purple-300/80 hover:to-pink-300/80 text-purple-700"
@click="toVipConfig">
代理报告配置
</button>
</template>
<button class="feature-item hover:bg-blue-50" @click="toHistory">
📃 我的报告
</button>
<button class="feature-item hover:bg-blue-50" @click="toUserAgreement">
📜 用户协议
</button>
<button class="feature-item hover:bg-blue-50" @click="toService">
💬 联系客服
</button>
<button class="feature-item hover:bg-red-50 text-red-600" @click="handleLogout">
退出登录
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onBeforeMount } from 'vue'
import headShot from "@/static/image/head_shot.webp"
// 用户数据
const userName = ref('')
const userAvatar = ref('')
const isLoggedIn = ref(false)
// 代理数据
const isAgent = ref(false)
const level = ref('normal')
// 检查平台环境
onBeforeMount(() => {
// 从缓存获取用户信息
const token = uni.getStorageSync('token')
if (token) {
isLoggedIn.value = true
// 从缓存获取用户信息
const userInfo = uni.getStorageSync('userInfo')
console.log("userInfo", userInfo)
if (userInfo) {
userName.value = userInfo.nickName || '用户'
userAvatar.value = userInfo.avatar || ''
}
// 从缓存获取代理信息
const agentInfo = uni.getStorageSync('agentInfo')
if (agentInfo?.isAgent) {
isAgent.value = agentInfo.isAgent
level.value = agentInfo.level || 'normal'
}
}
})
const levelNames = {
normal: '普通代理',
VIP: 'VIP代理',
SVIP: 'SVIP代理'
}
const levelText = {
normal: '基础代理特权',
VIP: '高级代理特权',
SVIP: '尊享代理特权'
}
const levelGradient = computed(() => ({
border: {
'normal': 'bg-green-300',
'VIP': 'bg-gradient-to-r from-yellow-400 to-amber-500',
'SVIP': 'bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_15px_rgba(163,51,200,0.2)]'
}[level.value],
badge: {
'normal': 'bg-green-500',
'VIP': 'bg-gradient-to-r from-yellow-500 to-amber-600',
'SVIP': 'bg-gradient-to-r from-purple-500 to-pink-500'
}[level.value],
text: {
'normal': 'text-green-600',
'VIP': 'text-amber-600',
'SVIP': 'text-purple-600'
}[level.value]
}))
function maskName(name) {
if (!name || name.length < 11) return name
return name.substring(0, 3) + "****" + name.substring(7)
}
function toHistory() {
uni.navigateTo({
url: '/pages/queryHistory'
})
}
function toUserAgreement() {
uni.navigateTo({
url: '/pages/agreement?type=user'
})
}
function redirectToLogin() {
uni.navigateTo({
url: '/pages/login'
})
}
function handleLogout() {
uni.removeStorageSync('token')
uni.removeStorageSync('refreshAfter')
uni.removeStorageSync('accessExpire')
uni.removeStorageSync('userInfo')
uni.removeStorageSync('agentInfo')
// 重置状态
isLoggedIn.value = false
userName.value = ''
userAvatar.value = ''
isAgent.value = false
level.value = 'normal'
uni.reLaunch({
url: '/pages/index'
})
}
function toService() {
// #ifdef APP-PLUS
plus.runtime.openURL('https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9')
// #endif
// #ifdef H5
window.location.href = 'https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9'
// #endif
// #ifdef MP
uni.setClipboardData({
data: 'https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9',
success: function () {
uni.showToast({
title: '客服链接已复制,请在浏览器中打开',
icon: 'none'
})
}
})
// #endif
}
function toVipConfig() {
uni.navigateTo({
url: '/pages/agentVipConfig'
})
}
</script>
<style scoped>
.profile-section {
background: linear-gradient(135deg, #ffffff 50%, rgba(236, 253, 245, 0.3));
border: 1px solid rgba(209, 213, 219, 0.2);
}
.profile-section .relative>view:first-child {
transition: all 0.3s ease;
}
.feature-item {
transition:
transform 0.2s ease,
background 0.3s ease,
box-shadow 0.3s ease;
display: flex;
width: 100%;
align-items: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-right: 0.75rem;
border-radius: 0.75rem;
background-color: #ffffff;
padding: 1rem 1.5rem;
text-align: left;
color: #4b5563;
transition-property: all;
transition-duration: 300ms;
}
.feature-item:active {
transform: scale(1.02);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.border-gradient-to-r {
border-image: linear-gradient(to right, var(--tw-gradient-from), var(--tw-gradient-to)) 1;
}
.shadow-glow {
box-shadow: 0 0 8px rgba(163, 51, 200, 0.2);
}
</style>
<route lang="json">{
"layout": "home"
}</route>

245
src/pages/promote.vue Normal file
View File

@@ -0,0 +1,245 @@
<template>
<view class="min-h-screen p-4 promote">
<view class="mb-4 card !bg-gradient-to-b from-orange-200 to-orange-200/80">
<view>
<text class="text-lg font-bold text-orange-500 block">直推用户查询</text>
<text class="font-bold text-orange-400 mt-1 block">自定义价格赚取差价</text>
</view>
<view class="mt-6">
<view class="mt-2 text-gray-600 bg-orange-100 rounded-xl px-4 py-2">
在下方 "自定义价格" 处选择报告类型设置客户查询价即可立即推广
</view>
</view>
</view>
<VipBanner />
<!-- 推广内容 -->
<view>
<view class="card mb-4">
<view>
<text class="text-xl font-semibold mb-2 block">生成推广码</text>
<wd-form :model="formData" ref="promotionForm">
<wd-cell-group border>
<!-- 报告类型 -->
<wd-picker label="报告类型" label-width="100px" v-model="formData.productType" :columns="[reportTypes]"
title="选择报告类型" prop="productType" placeholder="请选择报告类型" @confirm="onConfirmType"
:rules="[{ required: true, message: '请选择报告类型' }]" />
<!-- 定价 -->
<wd-input label="客户查询价" label-width="100px" v-model="formData.clientPrice" placeholder="请输入价格" readonly
clickable @click="showPricePicker = true" prop="clientPrice" suffix-icon="arrow-right"
:rules="[{ required: true, message: '请输入客户查询价' }]" />
</wd-cell-group>
<view class="flex items-center justify-between my-2">
<text class="text-sm text-gray-500">推广收益为 <text class="text-orange-500">{{ promotionRevenue }}</text>
</text>
<text class="text-sm text-gray-500">我的成本为 <text class="text-orange-500">{{ costPrice }}</text> </text>
</view>
</wd-form>
</view>
<view class="mt-6">
<button type="primary" block @click="generatePromotionCode">点击立即推广</button>
</view>
</view>
</view>
<PriceInputPopup v-model:show="showPricePicker" :default-price="formData.clientPrice"
:product-config="pickerProductConfig" @change="onPriceChange" />
<QRcode v-model:show="showQRcode" :linkIdentifier="linkIdentifier" />
</view>
</template>
<script setup>
import { getProductConfig, generatePromotionLink } from '@/apis/agent'
import PriceInputPopup from '@/components/PriceInputPopup.vue'
import VipBanner from '@/components/VipBanner.vue'
import QRcode from '@/components/QRcode.vue'
// 报告类型
const reportTypes = [
{ label: '人事背调', value: 'backgroundcheck', id: 1 },
{ label: '老板企业报告', value: 'companyinfo', id: 2 },
{ label: '家政风险', value: 'homeservice', id: 3 },
{ label: '婚恋风险', value: 'marriage', id: 4 },
{ label: '贷前背调', value: 'preloanbackgroundcheck', id: 5 },
{ label: '租赁风险', value: 'rentalrisk', id: 6 },
{ label: '个人风险', value: 'riskassessment', id: 7 }
]
// 状态管理
const promotionForm = ref(null)
const showPricePicker = ref(false)
const pickerProductConfig = ref(null)
const productConfig = ref(null)
const linkIdentifier = ref("")
const showQRcode = ref(false)
// 表单数据对象
const formData = ref({
productType: '',
clientPrice: null
})
// 计算成本价格
const costPrice = computed(() => {
if (!pickerProductConfig.value) return '0.00'
// 平台定价成本
let platformPricing = 0
platformPricing += pickerProductConfig.value.cost_price
if (formData.value.clientPrice > pickerProductConfig.value.p_pricing_standard) {
platformPricing += (formData.value.clientPrice - pickerProductConfig.value.p_pricing_standard) * pickerProductConfig.value.p_overpricing_ratio
}
if (pickerProductConfig.value.a_pricing_standard > platformPricing &&
pickerProductConfig.value.a_pricing_end > platformPricing &&
pickerProductConfig.value.a_overpricing_ratio > 0) {
if (formData.value.clientPrice > pickerProductConfig.value.a_pricing_standard) {
if (formData.value.clientPrice > pickerProductConfig.value.a_pricing_end) {
platformPricing += (pickerProductConfig.value.a_pricing_end - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
} else {
platformPricing += (formData.value.clientPrice - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
}
}
}
return safeTruncate(platformPricing)
})
// 计算推广收益
const promotionRevenue = computed(() => {
return safeTruncate(formData.value.clientPrice - costPrice.value)
})
// 安全截断数字保留2位小数
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00"
const factor = 10 ** decimals
const scaled = Math.trunc(num * factor)
const truncated = scaled / factor
return truncated.toFixed(decimals)
}
// 生成推广码
const generatePromotionCode = async () => {
// 表单验证
try {
await promotionForm.value.validate()
} catch (e) {
return
}
try {
// 获取选中产品的完整信息
const reportType = reportTypes.find(item => item.value === formData.value.productType)
if (!reportType) {
uni.showToast({
title: '请选择有效的报告类型',
icon: 'none'
})
return
}
const res = await generatePromotionLink({
product: formData.value.productType,
price: formData.value.clientPrice
})
if (res.code === 200) {
linkIdentifier.value = res.data.link_identifier
showQRcode.value = true
} else {
uni.showToast({
title: res.msg || '生成推广码失败',
icon: 'none'
})
}
} catch (error) {
uni.showToast({
title: '网络错误',
icon: 'none'
})
}
}
// 选择类型
const selectProductType = (reportTypeValue) => {
const reportType = reportTypes.find(item => item.id === reportTypeValue || item.value === reportTypeValue)
if (!reportType) return
formData.value.productType = reportType.value
if (productConfig.value) {
for (let i of productConfig.value) {
if (i.product_id === reportType.id) {
pickerProductConfig.value = i
formData.value.clientPrice = i.p_pricing_standard.toString()
}
}
}
}
// 获取产品配置
const getPromoteConfig = async () => {
try {
const res = await getProductConfig()
if (res.code === 200) {
productConfig.value = res.data.AgentProductConfig
// 选择第一个报告类型
selectProductType(1) // 使用ID 1选择第一个报告类型
} else {
uni.showToast({
title: res.msg || '获取配置失败',
icon: 'none'
})
}
} catch (error) {
console.log(error)
uni.showToast({
title: '网络错误',
icon: 'none'
})
}
}
// 价格变更
const onPriceChange = (price) => {
formData.value.clientPrice = price
}
// 类型选择确认
const onConfirmType = (e) => {
// picker在单列模式下返回的是选中项的值
if (e && e.value && e.value.length > 0) {
const selectedValue = e.value[0]
selectProductType(selectedValue)
}
}
// 页面加载
onMounted(() => {
getPromoteConfig()
})
</script>
<style>
.card {
border-radius: 12px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 16px;
margin-bottom: 16px;
}
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "推广",
"agent": true,
"auth": true
}</route>

View File

@@ -0,0 +1,144 @@
<template>
<view class="min-h-screen bg-gray-50">
<!-- 收益列表 -->
<uni-list :loading="loading" :loadmore="loadMoreStatus" @loadmore="onLoadMore">
<!-- 空状态提示 -->
<view v-if="!loading && list.length === 0" class="flex flex-col items-center justify-center py-16">
<image src="/static/image/empty.svg" mode="aspectFit" class="w-48 h-48 mb-4" />
<text class="text-gray-400 text-base">暂无直推报告</text>
</view>
<view v-for="(item, index) in list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<view class="flex justify-between items-center mb-2">
<text class="text-gray-500 text-sm">{{ item.create_time || '-' }}</text>
<text class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</text>
</view>
<view class="flex items-center">
<text class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.product_name)">
<text class="w-2 h-2 rounded-full mr-1 inline-block" :class="getDotColor(item.product_name)"></text>
{{ item.product_name }}
</text>
</view>
</view>
<!-- 加载更多/加载完成提示 -->
<uni-load-more :status="loadMoreStatus" />
</uni-list>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getAgentCommission } from '@/apis/agent'
// 颜色配置(根据产品名称映射)
const typeColors = {
'老板企业报告': { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' },
'人事背调': { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' },
'家政风险': { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' },
'婚恋风险': { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' },
'贷前背调': { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' },
'租赁风险': { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' },
'个人风险': { bg: 'bg-red-100', text: 'text-red-800', dot: 'bg-red-500' },
// 默认类型
'default': { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' }
}
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const list = ref([])
const loading = ref(false)
const loadMoreStatus = ref('more') // 'more'|'loading'|'noMore'
// 获取颜色样式
const getReportTypeStyle = (name) => {
const color = typeColors[name] || typeColors.default
return `${color.bg} ${color.text}`
}
// 获取小圆点颜色
const getDotColor = (name) => {
return (typeColors[name] || typeColors.default).dot
}
// 加载更多数据
const onLoadMore = async () => {
if (loadMoreStatus.value === 'noMore') return
page.value++
await getData()
}
// 获取数据
const getData = async () => {
try {
loading.value = true
loadMoreStatus.value = 'loading'
const res = await getAgentCommission({
page: page.value,
page_size: pageSize.value
})
if (res.code === 200) {
// 首次加载
if (page.value === 1) {
list.value = res.data.list
total.value = res.data.total
} else {
// 分页加载
list.value.push(...res.data.list)
}
// 判断是否加载完成
if (list.value.length >= res.data.total || res.data.list.length < pageSize.value) {
loadMoreStatus.value = 'noMore'
} else {
loadMoreStatus.value = 'more'
}
} else {
uni.showToast({
title: res.msg || '加载失败',
icon: 'none'
})
}
} catch (error) {
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 页面加载
onMounted(() => {
getData()
})
// 页面下拉刷新
const onPullDownRefresh = () => {
page.value = 1
loadMoreStatus.value = 'more'
getData().then(() => {
uni.stopPullDownRefresh()
})
}
// 导出页面生命周期方法
defineExpose({
onPullDownRefresh
})
</script>
<route type="page" lang="json">
{
"layout": "page",
"title": "直推报告",
"agent": true,
"auth": true
}
</route>

153
src/pages/queryHistory.vue Normal file
View File

@@ -0,0 +1,153 @@
<script setup>
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { queryList } from '@/api/apis'
// 定义字典映射 id 和 product_name
const productMap = {
1: '背景调查',
2: '企业报告',
3: '家政服务',
4: '婚姻状态',
5: '贷前背景调查',
6: '租赁服务',
7: '个人风险评估',
}
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const reportList = ref([])
const loadingState = ref('loading')
const num = ref(0)
const max = ref(60)
// 初始加载数据
function fetchData() {
queryList({ page: page.value, page_size: pageSize.value }).then((res) => {
if (res.code === 200) {
total.value = res.data.total
if (res.data.list && res.data.list.length > 0) {
reportList.value.push(...res.data.list)
page.value += 1
}
if (reportList.value.length >= total.value) {
loadingState.value = 'finished'
}
else {
loadingState.value = 'loading'
}
}
})
}
// 根据 product_id 获取产品名称
function getProductName(productId) {
return productMap[productId] || '未知类型'
}
// 初始加载
onLoad(() => {
fetchData()
})
// 下拉触底加载更多
onReachBottom(() => {
if (loadingState.value !== 'finished') {
if (num.value >= max.value) {
loadingState.value = 'finished'
}
else {
fetchData()
}
}
})
function toDetail(item) {
if (item.query_state !== 'success')
return
uni.navigateTo({
url: `/pages/result?id=${item.order_id}`,
})
}
// 状态文字映射
function stateText(state) {
switch (state) {
case 'pending':
return '查询中'
case 'success':
return '查询成功'
case 'failed':
return '查询失败'
default:
return '未知状态'
}
}
// 状态颜色映射
function statusClass(state) {
switch (state) {
case 'pending':
return 'status-pending'
case 'success':
return 'status-success'
case 'failed':
return 'status-failed'
default:
return ''
}
}
</script>
<template>
<!-- <wd-notice-bar text="为保证用户的隐私以及数据安全您的报告生成30天之后将自动清除请及时保存您的报告。" prefix="warn-bold" color="#007aff"
background-color="#f8f8f8" /> -->
<view class="flex flex-col gap-4 p-4">
<view v-for="item in reportList" :key="item.id" class="card flex flex-col gap-2" @click="toDetail(item)">
<view class="flex items-center justify-between">
<view class="">
状态:
</view>
<view class="rounded-xl px-2 py-1" :class="[statusClass(item.query_state)]">
{{ stateText(item.query_state) }}
</view>
</view>
<view class="flex items-center justify-between">
<view class="">
报告类型
</view>
<view>
{{ item.product_name }}
</view>
</view>
<view class="flex items-center justify-between">
<view class="">
查询时间:
</view>
<view>
{{ item.create_time }}
</view>
</view>
</view>
<!-- 加载更多 -->
<wd-loadmore :state="loadingState" @reload="fetchData" />
</view>
</template>
<style lang="scss" scoped>
.status-pending {
@apply bg-yellow-100 text-yellow-600;
}
.status-success {
@apply bg-green-100 text-green-600;
}
.status-failed {
@apply bg-red-100 text-red-600;
}
</style>
<route lang="json">{
"layout": "page",
"title": "历史报告",
"auth": true
}</route>

View File

@@ -0,0 +1,165 @@
<template>
<view class="min-h-screen bg-gray-50">
<!-- 收益列表 -->
<uni-list :loading="loading" :loadmore="loadMoreStatus" @loadmore="onLoadMore">
<!-- 空状态提示 -->
<view v-if="!loading && list.length === 0" class="flex flex-col items-center justify-center py-16">
<image src="/static/image/empty.svg" mode="aspectFit" class="w-48 h-48 mb-4" />
<text class="text-gray-400 text-base">暂无收益记录</text>
</view>
<view v-for="(item, index) in list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<view class="flex justify-between items-center mb-2">
<text class="text-gray-500 text-sm">{{ item.create_time || '-' }}</text>
<text class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</text>
</view>
<view class="flex items-center">
<text class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.type)">
<text class="w-2 h-2 rounded-full mr-1 inline-block" :class="getDotColor(item.type)"></text>
{{ typeToChinese(item.type) }}
</text>
</view>
</view>
<!-- 加载更多/加载完成提示 -->
<uni-load-more :status="loadMoreStatus" />
</uni-list>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getAgentRewards } from '@/apis/agent'
// 类型映射配置
const typeConfig = {
descendant_promotion: {
chinese: '下级推广奖励',
color: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' }
},
descendant_upgrade_vip: {
chinese: '下级升级VIP奖励',
color: { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' }
},
descendant_upgrade_svip: {
chinese: '下级升级SVIP奖励',
color: { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' }
},
descendant_stay_activedescendant: {
chinese: '下级活跃奖励',
color: { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' }
},
new_active: {
chinese: '新增活跃奖励',
color: { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' }
},
descendant_withdraw: {
chinese: '下级提现奖励',
color: { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' }
},
default: {
chinese: '其他奖励',
color: { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' }
}
}
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const list = ref([])
const loading = ref(false)
const loadMoreStatus = ref('more') // 'more'|'loading'|'noMore'
// 类型转中文
const typeToChinese = (type) => {
return typeConfig[type]?.chinese || typeConfig.default.chinese
}
// 获取颜色样式
const getReportTypeStyle = (type) => {
const config = typeConfig[type] || typeConfig.default
return `${config.color.bg} ${config.color.text}`
}
// 获取小圆点颜色
const getDotColor = (type) => {
return typeConfig[type]?.color.dot || typeConfig.default.color.dot
}
// 加载更多数据
const onLoadMore = async () => {
if (loadMoreStatus.value === 'noMore') return
page.value++
await getData()
}
// 获取数据
const getData = async () => {
try {
loading.value = true
loadMoreStatus.value = 'loading'
const res = await getAgentRewards({
page: page.value,
page_size: pageSize.value
})
if (res.code === 200) {
if (page.value === 1) {
list.value = res.data.list
total.value = res.data.total
} else {
list.value.push(...res.data.list)
}
if (list.value.length >= res.data.total || res.data.list.length < pageSize.value) {
loadMoreStatus.value = 'noMore'
} else {
loadMoreStatus.value = 'more'
}
} else {
uni.showToast({
title: res.msg || '加载失败',
icon: 'none'
})
}
} catch (error) {
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 页面加载
onMounted(() => {
getData()
})
// 页面下拉刷新
const onPullDownRefresh = () => {
page.value = 1
loadMoreStatus.value = 'more'
getData().then(() => {
uni.stopPullDownRefresh()
})
}
// 导出页面生命周期方法
defineExpose({
onPullDownRefresh
})
</script>
<route type="page" lang="json">
{
"layout": "page",
"title": "收益明细",
"agent": true,
"auth": true
}
</route>

26
src/pages/vip.vue Normal file
View File

@@ -0,0 +1,26 @@
<template>
<view class="relative">
<image class="w-full" src="/static/images/vip_bg.png" mode="widthFix" />
<view @click="toService"
class="absolute left-[50%] translate-x-[-50%] bottom-80 bg-gradient-to-r from-gray-900 via-black to-gray-900 py-2 px-4 rounded-lg text-white text-[24px] font-bold shadow-[0_0_15px_rgba(255,255,255,0.3)]"
hover-class="scale-105">
点击马上报名
</view>
</view>
</template>
<script setup>
function toService() {
// 跳转到客服页面
uni.navigateTo({
url: 'https://work.weixin.qq.com/kfid/kfc5c19b2b93a5e73b9'
})
}
</script>
<style>
.scale-105 {
transform: scale(1.05);
transition: transform 0.3s;
}
</style>

427
src/pages/withdraw.vue Normal file
View File

@@ -0,0 +1,427 @@
<template>
<view class="min-h-screen bg-blue-50/30">
<view class="p-4">
<!-- 提现卡片 -->
<view class="bg-white rounded-lg p-5 mb-4">
<view class="flex items-center mb-6">
<view class="w-6 h-6 bg-orange-100 rounded-full flex items-center justify-center mr-2">
<text class="text-orange-400 text-sm"></text>
</view>
<text class="text-lg font-bold">支付宝提现</text>
</view>
<wd-form :model="form" :rules="rules" label-position="top" class="withdraw-form">
<!-- 支付宝账号 -->
<view class="mb-4">
<view class="mb-2">
<text class="text-red-500 mr-1">*</text>
<text class="text-base">支付宝账号</text>
</view>
<wd-input v-model="form.alipayAccount" prop="alipayAccount" placeholder="请输入支付宝账号"
custom-class="custom-input" />
<text class="text-gray-400 text-xs mt-1 block">可填写支付宝账户绑定的手机号</text>
</view>
<!-- 支付宝实名姓名 -->
<view class="mb-4">
<view class="mb-2">
<text class="text-red-500 mr-1">*</text>
<text class="text-base">实名姓名</text>
</view>
<wd-input v-model="form.realName" prop="realName" placeholder="请输入支付宝认证姓名" custom-class="custom-input" />
<text class="text-gray-400 text-xs mt-1 block">请填写支付宝账户认证的真实姓名</text>
</view>
<!-- 提现金额 -->
<view class="mb-4">
<view class="mb-2">
<text class="text-red-500 mr-1">*</text>
<text class="text-base">提现金额</text>
</view>
<view class="relative">
<wd-input v-model="form.amount" prop="amount" type="digit" placeholder="请输入提现金额"
custom-class="custom-input" @input="handleAmountInput" />
<view class="absolute right-2 top-1/2 transform -translate-y-1/2">
<view class="bg-blue-100 text-blue-600 rounded-full px-3 py-1 text-sm" @click="fillMaxAmount">
全部提现
</view>
</view>
</view>
</view>
</wd-form>
<!-- 金额提示 -->
<view class="text-sm text-gray-500 my-4">
可提现金额<text class="text-blue-600 font-semibold">¥{{ safeTruncate(availableAmount) }}</text>
</view>
<!-- 提现规则 -->
<view class="bg-gray-50 p-4 rounded-lg">
<view class="flex items-center">
<text class="text-orange-500 mr-1"></text>
<text class="text-blue-600 text-sm">提现须知</text>
</view>
<view class="text-xs text-gray-600 mt-2 space-y-1">
<text class="block">· 每日限提现1次最低50元</text>
<text class="block">· 超过800元需人工审核1-3个工作日</text>
<text class="block">· 到账时间24小时内</text>
</view>
</view>
</view>
<!-- 提交按钮 -->
<wd-button type="primary" block :loading="isSubmitting" @click="handleSubmit" custom-class="submit-button">
立即提现
</wd-button>
</view>
<!-- 状态弹窗 -->
<wd-popup v-model="popupVisible" custom-class="w-[85%] rounded-xl" closable close-on-click-modal>
<view class="p-8 bg-gradient-to-b from-white to-blue-50/30 relative">
<!-- 状态内容 -->
<view class="text-center space-y-5">
<!-- 状态图标 -->
<view class="relative inline-block">
<view class="w-14 h-14 rounded-full flex items-center justify-center" :class="statusIconClass[status]">
<text class="text-4xl">{{ statusIcon[status] }}</text>
</view>
</view>
<!-- 状态文案 -->
<view>
<text class="text-xl font-semibold mb-1 block" :class="statusTextColors[status]">
{{ statusMessages[status] }}
</text>
<template v-if="status === 2">
<text class="text-sm text-gray-500 block">
已向 <text class="text-blue-500">{{ form.alipayAccount }}</text> 转账
</text>
<text class="text-2xl font-bold text-green-600 mt-2 block">¥{{ form.amount }}</text>
</template>
<template v-if="status === 3">
<text class="text-red-500 text-sm px-4 block">{{ failMsg }}</text>
</template>
</view>
<!-- 进度条处理中状态 -->
<view v-if="status === 1" class="my-2">
<wd-progress :percentage="60" hide-text color="#4080ff" />
</view>
<!-- 辅助文案 -->
<view class="text-xs text-gray-400 space-y-1.5">
<template v-if="status === 2">
<text class="block">预计24小时内到账</text>
<text class="block">到账后可在支付宝账单中查看详情</text>
</template>
<template v-if="status === 1">
<text class="block">您的申请已进入处理队列</text>
<text class="block">5分钟后结果在提现记录中查看</text>
</template>
</view>
<!-- 操作按钮 -->
<wd-button type="primary" block class="mt-4" :style="{ backgroundColor: statusButtonColor[status] }"
@click="handlePopupAction">
{{ status === 1 ? '知道了' : status === 2 ? '完成' : '重新提现' }}
</wd-button>
</view>
</view>
</wd-popup>
<wd-toast />
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useToast } from 'wot-design-uni'
import { getAgentRevenue, agentWithdrawal } from '@/apis/agent'
// 安全截断数字到指定小数位数(不进行四舍五入)
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
// 表单数据
const form = ref({
alipayAccount: '',
realName: '',
amount: ''
})
const availableAmount = ref(0)
const isSubmitting = ref(false)
// 使用wot-design-ui的toast组件
const toast = useToast()
// 弹窗控制
const popupVisible = ref(false)
// 状态管理
const status = ref(null)
const failMsg = ref('')
// 表单验证规则
const rules = {
alipayAccount: [
{ required: true, message: '请输入支付宝账号' }
],
realName: [
{ required: true, message: '请输入姓名' },
{
required: true,
pattern: /^[\u4e00-\u9fa5]{2,4}$/,
message: '请输入2-4位中文姓名'
}
],
amount: [
{ required: true, message: '请输入提现金额' },
{
validator: (val) => {
const amountNum = Number(val)
if (isNaN(amountNum)) {
return '请输入有效金额'
}
// if (amountNum < 50) {
// return '提现金额不能低于50元'
// }
if (amountNum > availableAmount.value) {
return '超过可提现金额'
}
return true
}
}
]
}
// 样式配置
const statusIcon = {
1: '⏳',
2: '✅',
3: '❌'
}
const statusIconClass = {
1: 'bg-blue-100 text-blue-500 border-4 border-blue-200',
2: 'bg-green-100 text-green-500 border-4 border-green-200',
3: 'bg-red-100 text-red-500 border-4 border-red-200'
}
const statusTextColors = {
1: 'text-blue-600',
2: 'text-green-600',
3: 'text-red-600'
}
const statusButtonColor = {
1: '#3b82f6',
2: '#4ade80',
3: '#f87171'
}
const statusMessages = {
1: '提现申请处理中,请稍后再查询结果',
2: '提现成功',
3: '提现失败'
}
// 返回上一页
const handleBack = () => {
uni.navigateBack()
}
// 获取可提现金额
const getData = async () => {
try {
const res = await getAgentRevenue()
if (res.code === 200) {
availableAmount.value = res.data.balance
} else {
toast.error(res.msg || '获取余额失败')
}
} catch (error) {
toast.error('网络错误')
}
}
// 填充最大金额
const fillMaxAmount = () => {
// 使用safeTruncate确保金额正确截断到2位小数
form.value.amount = safeTruncate(availableAmount.value)
}
// 处理输入事件,控制输入格式
const handleAmountInput = (event) => {
let value = event.value
// 转为数字以便比较
let numValue = parseFloat(value)
// 如果超过可提现金额,设置为可提现金额并提示
if (!isNaN(numValue) && numValue > availableAmount.value) {
value = availableAmount.value
// 使用toast提示用户
toast.info(`金额已自动调整为最大可提现金额: ¥${value}`)
}
// 如果value的小数位数大于2位才进行截断处理
if (value.toString().includes('.') && value.toString().split('.')[1].length > 2) {
value = safeTruncate(value)
}
form.value.amount = value
console.log(value)
console.log(form.value.amount)
}
// 提交处理
const handleSubmit = async () => {
try {
// 提现金额转为数字
const amountNum = Number(form.value.amount)
// 验证表单
if (!form.value.alipayAccount.trim()) {
toast.info('请输入支付宝账号')
return
}
if (!form.value.realName.trim()) {
toast.info('请输入账户实名姓名')
return
}
if (!/^[\u4e00-\u9fa5]{2,4}$/.test(form.value.realName)) {
toast.info('请输入2-4位中文姓名')
return
}
if (!form.value.amount || isNaN(amountNum)) {
toast.info('请输入有效金额')
return
}
// if (amountNum < 50) {
// toast.info('提现金额不能低于50元')
// return
// }
if (amountNum > availableAmount.value) {
toast.info('超过可提现金额')
return
}
isSubmitting.value = true
const res = await agentWithdrawal({
payee_account: form.value.alipayAccount,
amount: amountNum,
payee_name: form.value.realName
})
if (res.code === 200) {
status.value = res.data.status
if (status.value === 3) {
failMsg.value = res.data.fail_msg
}
popupVisible.value = true
} else {
toast.error(res.msg || '提交失败')
}
} catch (error) {
toast.error('网络错误')
} finally {
isSubmitting.value = false
}
}
// 弹窗操作
const handlePopupAction = () => {
popupVisible.value = false
if (status.value === 2) {
resetForm()
getData()
}
}
// 重置表单
const resetForm = () => {
status.value = null
form.value = {
alipayAccount: '',
realName: '',
amount: ''
}
}
// 页面加载
onMounted(() => {
getData()
})
</script>
<style>
@keyframes progress {
0% {
width: 0;
}
100% {
width: 60%;
}
}
.animate-progress {
animation: progress 1.5s ease-in-out infinite;
animation-direction: alternate;
}
/* 自定义样式 */
.withdraw-form :deep(.wd-form-item) {
margin-bottom: 0;
}
.withdraw-form :deep(.wd-input__label) {
display: none;
}
.withdraw-form :deep(.custom-input .wd-input__inner) {
padding: 10px 15px;
font-size: 14px;
height: 45px;
border: none;
background-color: rgba(64, 128, 255, 0.05);
border-radius: 12px;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.withdraw-form :deep(.custom-input:focus-within .wd-input__inner) {
border: 2px solid #4080ff;
background-color: rgba(64, 128, 255, 0.08);
box-shadow: 0 0 0 2px rgba(64, 128, 255, 0.1);
}
.withdraw-form :deep(.wd-input__prefix) {
display: none;
}
.submit-button {
height: 45px;
border-radius: 6px;
background-color: #4080ff !important;
font-size: 16px;
}
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "提现",
"auth": true,
"agent": true
}</route>

View File

@@ -0,0 +1,181 @@
<template>
<view class="min-h-screen bg-gray-50">
<!-- 提现记录列表 -->
<uni-list :loading="loading" :loadmore="loadMoreStatus" @loadmore="onLoadMore">
<!-- 空状态提示 -->
<view v-if="!loading && list.length === 0" class="flex flex-col items-center justify-center py-16">
<image src="/static/image/empty.svg" mode="aspectFit" class="w-48 h-48 mb-4" />
<text class="text-gray-400 text-base">暂无提现记录</text>
</view>
<view v-for="(item, index) in list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<view class="flex justify-between items-center mb-2">
<text class="text-gray-500 text-sm">{{ item.create_time || '-' }}</text>
<text class="font-bold" :class="getAmountColor(item.status)">{{ item.amount.toFixed(2) }}</text>
</view>
<view class="flex items-center mb-2">
<text class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getStatusStyle(item.status)">
<text class="w-2 h-2 rounded-full mr-1 inline-block" :class="getDotColor(item.status)"></text>
{{ statusToChinese(item.status) }}
</text>
</view>
<view class="text-xs text-gray-500">
<text v-if="item.payee_account" class="block">收款账户{{ maskName(item.payee_account) }}</text>
<text v-if="item.remark" class="block">备注{{ item.remark }}</text>
</view>
</view>
<!-- 加载更多/加载完成提示 -->
<uni-load-more :status="loadMoreStatus" />
</uni-list>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getWithdrawalRecords } from '@/apis/agent'
// 状态映射配置
const statusConfig = {
1: {
chinese: '处理中',
color: {
bg: 'bg-yellow-100',
text: 'text-yellow-800',
dot: 'bg-yellow-500',
amount: 'text-yellow-500'
}
},
2: {
chinese: '提现成功',
color: {
bg: 'bg-green-100',
text: 'text-green-800',
dot: 'bg-green-500',
amount: 'text-green-500'
}
},
3: {
chinese: '提现失败',
color: {
bg: 'bg-red-100',
text: 'text-red-800',
dot: 'bg-red-500',
amount: 'text-red-500'
}
}
}
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const list = ref([])
const loading = ref(false)
const loadMoreStatus = ref('more') // 'more'|'loading'|'noMore'
// 账户脱敏处理
const maskName = (name) => {
if (!name || typeof name !== 'string') return ''
if (name.length <= 7) return name
return name.substring(0, 3) + '****' + name.substring(7)
}
// 状态转中文
const statusToChinese = (status) => {
return statusConfig[status]?.chinese || '未知状态'
}
// 获取状态样式
const getStatusStyle = (status) => {
const config = statusConfig[status] || {}
return `${config.color?.bg || 'bg-gray-100'} ${config.color?.text || 'text-gray-800'}`
}
// 获取小圆点颜色
const getDotColor = (status) => {
return statusConfig[status]?.color.dot || 'bg-gray-500'
}
// 获取金额颜色
const getAmountColor = (status) => {
return statusConfig[status]?.color.amount || 'text-gray-500'
}
// 加载更多数据
const onLoadMore = async () => {
if (loadMoreStatus.value === 'noMore') return
page.value++
await getData()
}
// 获取数据
const getData = async () => {
try {
loading.value = true
loadMoreStatus.value = 'loading'
const res = await getWithdrawalRecords({
page: page.value,
page_size: pageSize.value
})
if (res.code === 200) {
if (page.value === 1) {
list.value = res.data.list
total.value = res.data.total
} else {
list.value.push(...res.data.list)
}
if (list.value.length >= res.data.total || res.data.list.length < pageSize.value) {
loadMoreStatus.value = 'noMore'
} else {
loadMoreStatus.value = 'more'
}
} else {
uni.showToast({
title: res.msg || '加载失败',
icon: 'none'
})
}
} catch (error) {
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 页面加载
onMounted(() => {
getData()
})
// 页面下拉刷新
const onPullDownRefresh = () => {
page.value = 1
loadMoreStatus.value = 'more'
getData().then(() => {
uni.stopPullDownRefresh()
})
}
// 导出页面生命周期方法
defineExpose({
onPullDownRefresh
})
</script>
<style>
/* 自定义样式 */
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "提现记录",
"auth": true,
"agent": true
}</route>

6
src/shims.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export {}
declare module 'vue' {
type Hooks = App.AppInstance & Page.PageInstance
interface ComponentCustomOptions extends Hooks {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

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