first commit

This commit is contained in:
2026-04-08 14:14:10 +08:00
commit f62289c97b
110 changed files with 21888 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

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/) - 保证代码质量

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>

156
manifest.config.ts Normal file
View File

@@ -0,0 +1,156 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { readRuntimeConfig } from './src/config/runtimeConfig.node'
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
const projectRoot = path.dirname(fileURLToPath(import.meta.url))
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'
const rc = readRuntimeConfig(projectRoot, mode)
const appName = rc.appName
export default defineManifestConfig({
'name': appName,
'appid': '__UNI__CC3DA09',
'description': '',
'versionName': '1.0.0',
'versionCode': '107',
'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: {
Share: {},
Camera: {},
PhotoLibrary: {},
},
/* 应用发布信息 */
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: '需要访问您的网络来提供更好的服务',
NSPhotoLibraryAddUsageDescription: "此应用需要访问您的相册以保存图片",
},
idfa: false,
bundleIdentifier: 'com.allinone.check',
},
/* SDK配置 */
sdkConfigs: {
// share: {
// weixin: {
// appid: 'wx开头的微信开放平台AppID',
// UniversalLinks: 'https://chimei.ronsafe.cn/app/',
// }
// },
// oauth: {
// weixin: {
// appid: 'wx开头的微信开放平台AppID',
// appsecret: '微信开放平台AppSecret',
// UniversalLinks: 'https://chimei.ronsafe.cn/app/'
// }
// }
},
icons: {
android: {
hdpi: 'static/icons/72x72.png',
xhdpi: 'static/icons/96x96.png',
xxhdpi: 'static/icons/144x144.png',
xxxhdpi: 'static/icons/192x192.png',
},
ios: {
appstore: 'static/icons/1024x1024.png',
ipad: {
'app': 'static/icons/76x76.png',
'app@2x': 'static/icons/152x152.png',
'notification': 'static/icons/20x20.png',
'notification@2x': 'static/icons/40x40.png',
'proapp@2x': 'static/icons/167x167.png',
'settings': 'static/icons/29x29.png',
'settings@2x': 'static/icons/58x58.png',
'spotlight': 'static/icons/40x40.png',
'spotlight@2x': 'static/icons/80x80.png',
},
iphone: {
'app@2x': 'static/icons/120x120.png',
'app@3x': 'static/icons/180x180.png',
'notification@2x': 'static/icons/40x40.png',
'notification@3x': 'static/icons/60x60.png',
'settings@2x': 'static/icons/58x58.png',
'settings@3x': 'static/icons/87x87.png',
'spotlight@2x': 'static/icons/80x80.png',
'spotlight@3x': 'static/icons/120x120.png',
},
},
},
},
},
/* 快应用特有相关 */
'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': {
'lz-url-launch': {
'version': '1.0.0',
'description': 'SFSafariViewController插件支持在iOS中使用系统浏览器打开网页',
'platforms': {
'ios': {
'appid': '__UNI_LZ_URL_LAUNCH_IOS',
'autostart': false
}
}
}
}
}
})

106
package.json Normal file
View File

@@ -0,0 +1,106 @@
{
"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.42.0",
"@vant/area-data": "^2.0.0",
"@vueuse/core": "^11.3.0",
"crypto-js": "^4.2.0",
"pinia": "^3.0.3",
"qrcode": "^1.5.4",
"uqrcodejs": "^4.0.7",
"vue": "~3.5.16",
"vue-i18n": "^9.14.4",
"wot-design-uni": "^1.9.1"
},
"devDependencies": {
"@dcloudio/types": "^3.4.15",
"@dcloudio/uni-automator": "3.0.0-4050520250307001",
"@dcloudio/uni-cli-shared": "3.0.0-4050520250307001",
"@dcloudio/uni-stacktracey": "3.0.0-4050520250307001",
"@dcloudio/uni-uts-v1": "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.9",
"@mini-types/alipay": "^3.0.14",
"@types/node": "^20.19.0",
"@uni-helper/eslint-config": "^0.1.0",
"@uni-helper/uni-env": "^0.1.7",
"@uni-helper/uni-types": "1.0.0-alpha.4",
"@uni-helper/unocss-preset-uni": "^0.2.11",
"@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.8",
"@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.16",
"@vue/tsconfig": "^0.5.1",
"miniprogram-api-typings": "^3.12.3",
"sass": "~1.79.6",
"sass-embedded": "~1.79.6",
"typescript": "~5.5.4",
"unocss": "^0.62.4",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^28.7.0",
"vite": "^5.4.19",
"vue-tsc": "^2.2.10"
}
}

48
pages.config.ts Normal file
View File

@@ -0,0 +1,48 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { readRuntimeConfig } from './src/config/runtimeConfig.node'
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
const projectRoot = path.dirname(fileURLToPath(import.meta.url))
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'
const rc = readRuntimeConfig(projectRoot, mode)
const appName = rc.appName
export default defineUniPages({
pages: [],
globalStyle: {
backgroundColor: '@bgColor',
backgroundColorBottom: '@bgColorBottom',
backgroundColorTop: '@bgColorTop',
backgroundTextStyle: '@bgTxtStyle',
navigationBarBackgroundColor: '#000000',
navigationBarTextStyle: '@navTxtStyle',
navigationBarTitleText: appName,
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/me',
text: '',
visible: false,
},
],
},
})

13160
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"]
}

104
src/App.vue Normal file
View File

@@ -0,0 +1,104 @@
<script setup>
import { onLaunch } from '@dcloudio/uni-app'
import { refreshToken, getUserInfo, wxminiLogin } from '@/api/apis'
import { getAgentInfo } from '@/apis/agent'
onLaunch(() => {
login()
refreshTokenIfNeeded()
getUser()
getAgentInformation()
})
const login = () => {
const token = uni.getStorageSync("token")
if (token) {
return
}
uni.login({
success: (res) => {
let code = res.code
wxminiLogin({ code }).then(res => {
if (res.code === 200) {
uni.setStorageSync("token", res.data.accessToken)
uni.setStorageSync("refreshAfter", res.data.refreshAfter)
uni.setStorageSync("accessExpire", res.data.accessExpire)
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 {}
}
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,
isRealName: res.data.is_real_name,
expiryTime: res.data.expiry_time
})
}
} catch {}
}
const getUser = async () => {
const token = uni.getStorageSync("token")
if (!token) {
return
}
const res = await getUserInfo()
if (res.code === 200) {
uni.setStorageSync("userInfo", res.data.userInfo)
}
}
</script>

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

@@ -0,0 +1,183 @@
// 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 wxminiLogin(data) {
return request({
url: '/user/wxMiniAuth',
method: 'POST',
data,
})
}
export function bindMobile(data) {
return request({
url: '/user/bindMobile',
method: 'POST',
data,
})
}
/** 发短信验证码。小程序无滑块,与 webview 关滑块时一致:固定传 captchaVerifyParam 空串 */
export function getCode(data) {
return request({
url: '/auth/sendSms',
method: 'POST',
data: {
...data,
captchaVerifyParam: '',
},
})
}
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',
});
}
// 获取最新版本信息
export function getLatestVersion() {
return request({
url: '/app/version',
method: 'GET'
})
}
/**
* 注销账号
*/
export function cancelAccount() {
return request({
url: '/user/cancelOut',
method: 'POST'
})
}
/**
* 获取APP产品信息
* @param {string} feature - 产品标识
*/
export function getAppProduct(feature) {
return request({
url: `/product/app_en/${feature}`,
method: 'GET'
})
}

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

@@ -0,0 +1,163 @@
// 代理相关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} data 成为会员数据 {product_id}
*/
export const activateAgentMembership = (data) => {
return request({
url: '/agent/membership/activate',
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
})
}
/**
* 获取推广二维码图片
* @param {Object} params 查询参数 {qrcode_type, qrcode_url}
*/
export const getPromotionQrcode = (params) => {
return request({
url: '/agent/promotion/qrcode',
method: 'GET',
params,
responseType: 'blob' // 接收图片数据
})
}

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);
}

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

@@ -0,0 +1,686 @@
/* 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 POSTER_QR_POSITIONS: typeof import('./utils/posterQrWeixin.js')['POSTER_QR_POSITIONS']
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 autoUpdateCheck: typeof import('./utils/autoUpdateCheck.js')['default']
const buildPromotionH5Url: typeof import('./utils/promotionH5Url.js')['buildPromotionH5Url']
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 drawMergedPosterWeixin: typeof import('./utils/posterQrWeixin.js')['drawMergedPosterWeixin']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const formatExpiryTime: typeof import('./utils/format.js')['formatExpiryTime']
const getAgentTabShareTitle: typeof import('./utils/runtimeEnv.js')['getAgentTabShareTitle']
const getApiBaseUrl: typeof import('./utils/runtimeEnv.js')['getApiBaseUrl']
const getApiOrigin: typeof import('./utils/runtimeEnv.js')['getApiOrigin']
const getApiPrefix: typeof import('./utils/runtimeEnv.js')['getApiPrefix']
const getAppDebug: typeof import('./utils/runtimeEnv.js')['getAppDebug']
const getAppName: typeof import('./utils/runtimeEnv.js')['getAppName']
const getCompanyName: typeof import('./utils/runtimeEnv.js')['getCompanyName']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getCustomerServiceCorpId: typeof import('./utils/runtimeEnv.js')['getCustomerServiceCorpId']
const getCustomerServiceUrl: typeof import('./utils/runtimeEnv.js')['getCustomerServiceUrl']
const getInviteChannelKey: typeof import('./utils/runtimeEnv.js')['getInviteChannelKey']
const getMeShareTitle: typeof import('./utils/runtimeEnv.js')['getMeShareTitle']
const getPosterSrcList: typeof import('./utils/posterQrWeixin.js')['getPosterSrcList']
const getShareTitle: typeof import('./utils/runtimeEnv.js')['getShareTitle']
const getSiteOrigin: typeof import('./utils/runtimeEnv.js')['getSiteOrigin']
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 maskName: typeof import('./utils/format.js')['maskName']
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 setMiniPromotionShareFriend: typeof import('./utils/miniPromotionSharePayload.js')['setMiniPromotionShareFriend']
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 takeMiniPromotionShareFriend: typeof import('./utils/miniPromotionSharePayload.js')['takeMiniPromotionShareFriend']
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 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 useDisablePullRefresh: typeof import('./composables/useDisablePullRefresh.js')['useDisablePullRefresh']
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 useHotUpdate: typeof import('./composables/useHotUpdate.js')['useHotUpdate']
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 usePromotionShareHandlers: typeof import('./composables/usePromotionShareHandlers')['usePromotionShareHandlers']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
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('./composables/useShare')['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 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 POSTER_QR_POSITIONS: UnwrapRef<typeof import('./utils/posterQrWeixin.js')['POSTER_QR_POSITIONS']>
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 autoUpdateCheck: UnwrapRef<typeof import('./utils/autoUpdateCheck.js')['default']>
readonly buildPromotionH5Url: UnwrapRef<typeof import('./utils/promotionH5Url.js')['buildPromotionH5Url']>
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 drawMergedPosterWeixin: UnwrapRef<typeof import('./utils/posterQrWeixin.js')['drawMergedPosterWeixin']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly formatExpiryTime: UnwrapRef<typeof import('./utils/format.js')['formatExpiryTime']>
readonly getAgentTabShareTitle: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getAgentTabShareTitle']>
readonly getApiBaseUrl: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getApiBaseUrl']>
readonly getApiOrigin: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getApiOrigin']>
readonly getApiPrefix: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getApiPrefix']>
readonly getAppDebug: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getAppDebug']>
readonly getAppName: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getAppName']>
readonly getCompanyName: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getCompanyName']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getCustomerServiceCorpId: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getCustomerServiceCorpId']>
readonly getCustomerServiceUrl: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getCustomerServiceUrl']>
readonly getInviteChannelKey: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getInviteChannelKey']>
readonly getMeShareTitle: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getMeShareTitle']>
readonly getPosterSrcList: UnwrapRef<typeof import('./utils/posterQrWeixin.js')['getPosterSrcList']>
readonly getShareTitle: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getShareTitle']>
readonly getSiteOrigin: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getSiteOrigin']>
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 maskName: UnwrapRef<typeof import('./utils/format.js')['maskName']>
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 setMiniPromotionShareFriend: UnwrapRef<typeof import('./utils/miniPromotionSharePayload.js')['setMiniPromotionShareFriend']>
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 takeMiniPromotionShareFriend: UnwrapRef<typeof import('./utils/miniPromotionSharePayload.js')['takeMiniPromotionShareFriend']>
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 useHotUpdate: UnwrapRef<typeof import('./composables/useHotUpdate.js')['useHotUpdate']>
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 usePromotionShareHandlers: UnwrapRef<typeof import('./composables/usePromotionShareHandlers')['usePromotionShareHandlers']>
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('./composables/useShare')['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']
EmptyState: typeof import('./components/EmptyState.vue')['default']
GzhQrcode: typeof import('./components/GzhQrcode.vue')['default']
Payment: typeof import('./components/Payment.vue')['default']
PriceInputPopup: typeof import('./components/PriceInputPopup.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']
WdCell: typeof import('wot-design-uni/components/wd-cell/wd-cell.vue')['default']
WdCellGroup: typeof import('wot-design-uni/components/wd-cell-group/wd-cell-group.vue')['default']
WdCheckbox: typeof import('wot-design-uni/components/wd-checkbox/wd-checkbox.vue')['default']
WdColPicker: typeof import('wot-design-uni/components/wd-col-picker/wd-col-picker.vue')['default']
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']
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']
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,332 @@
<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>
<view>
</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="请输入您的手机号" :disabled="true" readonly :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-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, onMounted } 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: '',
})
// 使用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: '请输入验证码' }],
}
// 协议跳转函数
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)
})
// 组件挂载时获取用户信息并填入手机号
onMounted(() => {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo && userInfo.mobile) {
form.value.mobile = userInfo.mobile
}
})
// 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('-')
}
// 判断手机号是否有效
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) {
toast.info('请选择完整的代理区域')
return
}
if (!regionText.value) {
toast.info('请选择代理区域')
return
}
// 检查手机号
if (!form.value.mobile) {
toast.info('请输入手机号')
return
}
if (!isPhoneNumberValid.value) {
toast.info('手机号格式不正确')
return
}
// 检查验证码
if (!form.value.code) {
toast.info('请输入验证码')
return
}
// 检查用户是否同意协议
if (!isAgreed.value) {
toast.info('请阅读并同意相关协议')
return
}
// 所有验证通过,构建表单数据
const formData = {
...form.value,
region: regionText.value,
}
// 提交完整数据
emit('submit', formData)
} catch {
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,12 @@
<template>
<view 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 }}</text>
</view>
</template>
<script setup>
defineProps({
text: { type: String, default: '暂无数据' }
})
</script>

View File

@@ -0,0 +1,209 @@
<script setup>
import { ref, defineProps, defineEmits, computed } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'example', // 'example' | 'withdraw'
validator: (value) => ['example', 'withdraw'].includes(value)
}
})
const emit = defineEmits(['close'])
// 根据类型计算显示内容
const modalConfig = computed(() => {
if (props.type === 'withdraw') {
return {
title: '提现说明',
highlight: '点击二维码全屏查看',
instruction: '全屏后长按识别关注公众号',
description: '提现功能请在公众号内操作'
}
}
// 默认是查看示例报告
return {
title: '关注公众号',
highlight: '点击二维码全屏查看',
instruction: '全屏后长按识别关注公众号',
description: '关注公众号查看示例报告'
}
})
function handleClose() {
emit('close')
}
function handleMaskClick() {
handleClose()
}
// 预览图片
function previewImage() {
uni.previewImage({
urls: ['/static/qrcode/fwhqrcode.jpg'],
current: '/static/qrcode/fwhqrcode.jpg'
})
}
</script>
<template>
<view v-if="visible" class="gzh-qrcode-modal">
<view class="modal-mask" @click="handleMaskClick" />
<view class="modal-content">
<view class="modal-header">
<view class="modal-title">{{ modalConfig.title }}</view>
<view class="close-btn" @click="handleClose">
<text class="close-icon">×</text>
</view>
</view>
<view class="qrcode-container">
<image class="qrcode-image" src="/static/qrcode/fwhqrcode.jpg" mode="aspectFit" @click="previewImage" />
</view>
<view class="modal-message">
<text class="highlight">{{ modalConfig.highlight }}</text>
<text class="instruction">{{ modalConfig.instruction }}</text>
<text class="description">{{ modalConfig.description }}</text>
</view>
</view>
</view>
</template>
<style scoped>
.gzh-qrcode-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;
border-radius: 12px;
width: 300px;
max-width: 85%;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 1001;
overflow: hidden;
}
.modal-header {
position: relative;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
.modal-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.close-btn {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.close-icon {
font-size: 20px;
color: #999;
line-height: 1;
}
.qrcode-container {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.qrcode-image {
width: 200px;
height: 200px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.modal-message {
padding: 0 20px 24px;
text-align: center;
line-height: 1.6;
}
.modal-message text {
display: block;
font-size: 14px;
color: #666;
}
.highlight {
color: #007aff !important;
font-weight: 500;
margin-bottom: 4px;
}
/* 动画效果 */
.gzh-qrcode-modal {
animation: fadeIn 0.3s ease-out;
}
.modal-content {
animation: slideIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
</style>

180
src/components/Payment.vue Normal file
View File

@@ -0,0 +1,180 @@
<template>
<wd-popup v-model="show" position="bottom" z-index="3000" custom-style="height: 50%; display: flex; flex-direction: column; justify-content: space-between; padding: 24px;">
<view class="text-center">
<text class="text-lg font-bold block">支付</text>
</view>
<view class="text-center">
<text class="font-bold text-xl block">{{ data.product_name }}</text>
<view class="text-3xl text-red-500 font-bold">
<!-- 显示原价和折扣价格 -->
<view v-if="discountPrice" class="line-through text-gray-500 mt-4" :class="{ 'text-2xl': discountPrice }">
¥ {{ data.sell_price }}
</view>
<view>¥ {{ discountPrice ? (data.sell_price * 0.2).toFixed(2) : data.sell_price }}</view>
</view>
<!-- 仅在折扣时显示活动说明 -->
<text v-if="discountPrice" class="text-sm text-red-500 mt-1 block">活动价2折优惠</text>
</view>
<!-- 支付方式选择 -->
<view class="">
<wd-cell-group>
<wd-cell clickable>
<template #title>
<text class="text-lg font-medium">微信支付</text>
</template>
<template #right-icon>
<view class="w-5 h-5 rounded-full border-2 border-green-500 bg-green-500 flex items-center justify-center">
<view class="w-2 h-2 bg-white rounded-full"></view>
</view>
</template>
</wd-cell>
</wd-cell-group>
</view>
<!-- 确认按钮 -->
<view class="">
<view @click="getPayment" class="w-full py-4 bg-green-500 text-center rounded-full shadow-lg active:bg-green-600 transition-colors">
<text class="text-white text-lg font-medium">确认支付</text>
</view>
</view>
</wd-popup>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { payment } from '@/api/apis.js'
import { useToast } from 'wot-design-uni'
const toast = useToast()
const props = defineProps({
data: {
type: Object,
required: true,
},
id: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
})
const show = defineModel()
const orderNo = ref('')
const discountPrice = ref(false) // 是否应用折扣
async function getPayment() {
if (!props.id) {
toast.error('订单信息异常,请重试')
return
}
try {
const res = await payment({
id: String(props.id),
pay_method: 'wechat',
pay_type: props.type,
})
if (res.data) {
orderNo.value = res.data.order_no
// 微信支付 - 兼容多种返回格式wechatpay-go 返回 appId/timeStamp/nonceStr/package/signType/paySign
const paymentData = res.data.prepay_data || res.data.prepayData || res.data
// 若 prepay_data 是字符串则解析
const data = typeof paymentData === 'string' ? (() => { try { return JSON.parse(paymentData) } catch { return {} } })() : (paymentData || {})
const timeStamp = data.timeStamp || data.timestamp || data.time_stamp
const nonceStr = data.nonceStr || data.noncestr || data.nonce_str
const packageVal = data.package
const signType = data.signType || data.sign_type || 'MD5'
const paySign = data.paySign || data.pay_sign || data.sign
// 校验必要参数
if (!timeStamp || !nonceStr || !packageVal || !paySign) {
toast.error('支付参数异常,请联系客服')
return
}
// #ifdef MP-WEIXIN
uni.requestPayment({
provider: 'wxpay',
timeStamp: String(timeStamp),
nonceStr: String(nonceStr),
package: String(packageVal),
signType: String(signType),
paySign: String(paySign),
success: (result) => {
toast.success('支付成功')
show.value = false
handlePaymentSuccess()
},
fail: (error) => {
toast.error(error.errMsg || '支付失败')
// 用户取消不关闭弹窗,方便重试
if (error.errMsg && !error.errMsg.includes('cancel')) {
show.value = false
}
}
})
// #endif
// #ifdef APP-PLUS
uni.requestPayment({
provider: 'wxpay',
orderInfo: {
appid: paymentData.appid,
noncestr: nonceStr,
package: packageVal,
partnerid: paymentData.partnerid,
prepayid: paymentData.prepayid,
timestamp: timeStamp,
sign: paySign
},
success: (result) => {
toast.success('支付成功')
show.value = false
handlePaymentSuccess()
},
fail: (error) => {
toast.error('支付失败')
show.value = false
}
})
// #endif
// #ifdef H5
if (typeof WeixinJSBridge !== 'undefined') {
WeixinJSBridge.invoke('getBrandWCPayRequest', paymentData, function (result) {
if (result.err_msg === 'get_brand_wcpay_request:ok') {
toast.success('支付成功')
show.value = false
handlePaymentSuccess()
} else {
toast.error('支付失败')
}
})
} else {
toast.error('当前环境不支持微信支付')
}
// #endif
} else {
toast.error(res.msg || '获取支付信息失败')
}
} catch (error) {
toast.error(error?.data?.msg || error?.msg || '支付请求失败')
}
}
function handlePaymentSuccess() {
// 支付成功后的处理
setTimeout(() => {
uni.switchTab({
url: `/pages/me`
})
}, 1000)
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,175 @@
<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('.');
// 当小数位数超过2位时处理
if (decimalPart.length > 2) {
newPrice = parseFloat(safeTruncate(newPrice));
message = '价格已自动格式化为两位小数';
}
} catch {}
// 范围校验(基于可能格式化后的值)
if (newPrice < min) {
message = `价格不能低于 ${min}`;
newPrice = min;
} else if (newPrice > max) {
message = `价格不能高于 ${max}`;
newPrice = max;
}
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>

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

@@ -0,0 +1,522 @@
<template>
<wd-popup v-model="show" position="bottom" round>
<!-- #ifdef MP-WEIXIN -->
<view class="max-h-[calc(100vh-100px)] m-4">
<!-- canvas 离屏绘制避免 swiper 未挂载项取不到节点 -->
<canvas
id="mpPosterCanvas"
canvas-id="mpPosterCanvas"
type="2d"
class="mp-poster-canvas-hidden"
/>
<view class="p-2 flex justify-center">
<swiper
:key="swiperMountKey"
class="mp-poster-swiper w-full"
:style="{ height: swiperHeightPx + 'px' }"
:duration="280"
:easing-function="easeOutCubic"
@change="onSwiperChange"
>
<swiper-item v-for="(_, idx) in posterSrcList" :key="idx" class="mp-swiper-item">
<view class="mp-poster-item">
<image
v-if="renderedPaths[idx]"
:src="renderedPaths[idx]"
mode="aspectFit"
class="rounded-xl shadow poster-preview-mp"
:style="posterPreviewStyle"
/>
<view v-else class="text-gray-400 text-sm py-16">生成海报中</view>
</view>
</swiper-item>
</swiper>
</view>
<view
v-if="mode === 'promote'"
class="text-center text-gray-500 text-xs mb-2"
>
左右滑动切换海报
</view>
<view class="divider">分享与保存</view>
<view class="mp-share-actions">
<button
class="share-mp-btn flex flex-col items-center justify-center"
open-type="share"
plain
@tap="onShareFriendPrepare"
>
<image src="/static/image/icon_share_friends.svg" class="w-10 h-10 rounded-full" />
<text class="text-center mt-1 text-gray-600 text-xs">分享给好友</text>
</button>
<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="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>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<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">
<image
:src="posterImageUrlRemote"
class="rounded-xl shadow poster-image"
:style="{ width: imageWidth + 'px', height: imageHeight + 'px' }"
mode="aspectFit"
@load="onImageLoad"
@error="onImageError"
/>
</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="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>
<!-- #endif -->
</wd-popup>
</template>
<script setup>
import { ref, computed, toRefs, watch, nextTick, getCurrentInstance } from 'vue'
import { getApiBaseUrl, getAgentTabShareTitle, getShareTitle } from '@/utils/runtimeEnv.js'
import { buildPromotionH5Url } from '@/utils/promotionH5Url.js'
import { setMiniPromotionShareFriend } from '@/utils/miniPromotionSharePayload.js'
// #ifdef MP-WEIXIN
import { getPosterSrcList, drawMergedPosterWeixin } from '@/utils/posterQrWeixin.js'
// #endif
const props = defineProps({
linkIdentifier: {
type: String,
required: true,
},
mode: {
type: String,
default: 'promote',
},
})
const { linkIdentifier, mode } = toRefs(props)
const show = defineModel('show')
const imageWidth = ref(300)
const imageHeight = ref(500)
function generalUrl() {
return buildPromotionH5Url(mode.value, linkIdentifier.value)
}
// #ifndef MP-WEIXIN
const posterImageUrlRemote = computed(() => {
const qrcodeUrl = generalUrl()
return `${getApiBaseUrl()}/agent/promotion/qrcode?qrcode_type=${mode.value}&qrcode_url=${encodeURIComponent(qrcodeUrl)}`
})
// #endif
// #ifdef MP-WEIXIN
const instance = getCurrentInstance()
const posterSrcList = computed(() => getPosterSrcList(mode.value))
const renderedPaths = ref([])
const currentSwiperIndex = ref(0)
const swiperHeightPx = ref(420)
const previewWidthPx = ref(300)
/** 打开/切换模式时递增,强制 swiper 从第 0 页重建,避免受控 current 与手势打架导致卡顿 */
const swiperMountKey = ref(0)
/** 与基础库默认一致,缩短动画减少与异步生成叠在一起时的顿挫感 */
const easeOutCubic = 'easeOutCubic'
const posterPreviewStyle = computed(() => ({
width: `${previewWidthPx.value}px`,
height: `${swiperHeightPx.value}px`,
}))
/**
* 按本地底图宽高比,把整张海报缩进可视区域(避免 widthFix + 固定 swiper 高度裁切)
*/
function updateMpPosterLayout() {
const sys = uni.getSystemInfoSync()
const maxW = Math.min(sys.windowWidth * 0.92, 360)
const maxH = Math.min(sys.windowHeight * 0.62, 580)
const firstSrc = getPosterSrcList(mode.value)[0]
const fallbackRatio = 1920 / 1080
const applyRatio = (ratio) => {
let w = maxW
let h = w * ratio
if (h > maxH) {
h = maxH
w = h / ratio
}
previewWidthPx.value = Math.floor(w)
swiperHeightPx.value = Math.ceil(h)
}
return new Promise((resolve) => {
uni.getImageInfo({
src: firstSrc,
success: (info) => {
const ratio = info.width > 0 ? info.height / info.width : fallbackRatio
applyRatio(ratio)
resolve()
},
fail: () => {
applyRatio(fallbackRatio)
resolve()
},
})
})
}
/** 串行生成,避免多索引共用同一 canvas 竞态 */
let mpGenSeq = Promise.resolve()
function resetMpPosters() {
mpGenSeq = Promise.resolve()
const n = posterSrcList.value.length
renderedPaths.value = Array.from({ length: n }, () => '')
currentSwiperIndex.value = 0
}
function generatePosterMp(index) {
if (renderedPaths.value[index]) return Promise.resolve()
const proxy = instance?.proxy
if (!proxy) return Promise.resolve()
mpGenSeq = mpGenSeq
.catch(() => {})
.then(async () => {
if (renderedPaths.value[index]) return
await new Promise((r) => setTimeout(r, 80))
const canvas = await new Promise((resolve, reject) => {
uni.createSelectorQuery()
.in(proxy)
.select('#mpPosterCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const node = res?.[0]?.node
if (node) resolve(node)
else reject(new Error('未获取到 canvas 节点'))
})
})
const temp = await drawMergedPosterWeixin({
canvas,
linkUrl: generalUrl(),
posterSrc: posterSrcList.value[index],
mode: mode.value,
index,
componentInstance: proxy,
})
const next = [...renderedPaths.value]
next[index] = temp
renderedPaths.value = next
})
return mpGenSeq
}
function onSwiperChange(e) {
const cur = e.detail?.current ?? 0
currentSwiperIndex.value = cur
if (!renderedPaths.value[cur]) {
// 等 swiper 切换动画走一部分再跑 canvas减轻主线程卡顿
setTimeout(() => {
generatePosterMp(cur).catch((err) => {
console.error('生成海报失败', err)
uni.showToast({ title: '海报生成失败', icon: 'none' })
})
}, 160)
}
}
watch(show, (v) => {
if (!v)
return
updateMpPosterLayout().then(() => {
swiperMountKey.value += 1
resetMpPosters()
nextTick(() => {
generatePosterMp(0).catch((err) => {
console.error('生成海报失败', err)
uni.showToast({ title: '海报生成失败', icon: 'none' })
})
})
})
})
watch([mode, linkIdentifier], () => {
if (!show.value)
return
updateMpPosterLayout().then(() => {
swiperMountKey.value += 1
resetMpPosters()
nextTick(() => {
generatePosterMp(0).catch(() => {})
})
})
})
// #endif
// #ifdef MP-WEIXIN
function shareTitleForMode() {
return mode.value === 'invitation' ? getShareTitle() : getAgentTabShareTitle()
}
function onShareFriendPrepare() {
if (!linkIdentifier.value) {
uni.showToast({ title: '请先生成推广内容', icon: 'none' })
return
}
const idx = currentSwiperIndex.value
const img = renderedPaths.value[idx] || ''
setMiniPromotionShareFriend({
title: shareTitleForMode(),
path: `/pages/h5open?m=${mode.value}&id=${encodeURIComponent(linkIdentifier.value)}`,
imageUrl: img,
})
}
// #endif
function calculateImageSize(imgWidth, imgHeight) {
const sysInfo = uni.getSystemInfoSync()
const maxHeight = sysInfo.windowHeight * 0.6
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),
}
}
// #ifndef MP-WEIXIN
function onImageLoad() {
uni.getImageInfo({
src: posterImageUrlRemote.value,
success: (res) => {
const size = calculateImageSize(res.width, res.height)
imageWidth.value = size.width
imageHeight.value = size.height
},
fail: () => {},
})
}
function onImageError() {
uni.showToast({
title: '图片加载失败',
icon: 'none',
})
}
// #endif
function savePoster() {
uni.showLoading({ title: '正在保存…' })
// #ifdef MP-WEIXIN
const idx = currentSwiperIndex.value
const runSave = (filePath) => {
uni.saveImageToPhotosAlbum({
filePath,
success: () => {
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
},
fail: (err) => {
uni.hideLoading()
if (err.errMsg && err.errMsg.includes('auth')) {
uni.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (r) => {
if (r.confirm) uni.openSetting()
},
})
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
},
})
}
const path = renderedPaths.value[idx]
if (path) {
runSave(path)
return
}
generatePosterMp(idx)
.then(() => {
const p = renderedPaths.value[idx]
if (p) runSave(p)
else uni.hideLoading()
})
.catch(() => {
uni.hideLoading()
uni.showToast({ title: '请稍候再试', icon: 'none' })
})
// #endif
// #ifndef MP-WEIXIN
uni.downloadFile({
url: posterImageUrlRemote.value,
success: (downloadRes) => {
uni.saveImageToPhotosAlbum({
filePath: downloadRes.tempFilePath,
success: () => {
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
},
fail: (err) => {
uni.hideLoading()
if (err.errMsg && err.errMsg.includes('auth')) {
uni.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (r) => {
if (r.confirm) uni.openSetting()
},
})
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
},
})
},
fail: () => {
uni.hideLoading()
uni.showToast({ title: '图片下载失败', icon: 'none' })
},
})
// #endif
}
function 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;
}
}
.poster-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
}
/* #ifdef MP-WEIXIN */
.mp-poster-canvas-hidden {
position: fixed;
left: -2000px;
top: 0;
width: 400px;
height: 400px;
z-index: -1;
}
.mp-poster-swiper {
width: 100%;
}
.mp-swiper-item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.mp-poster-item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.poster-preview-mp {
display: block;
max-width: 100%;
max-height: 100%;
}
.mp-share-actions {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: flex-start;
row-gap: 18px;
column-gap: 8px;
padding: 0 4px 8px;
}
.share-mp-btn {
margin: 0;
padding: 0;
border: none;
background: transparent;
line-height: 1.2;
font-size: inherit;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.share-mp-btn::after {
border: none;
}
/* #endif */
</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/agentVipApply'
})
}
</script>
<style scoped>
.card {
border-radius: 12px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
</style>

View File

@@ -0,0 +1,341 @@
import { ref } from 'vue'
import { getApiBaseUrl } from '@/utils/runtimeEnv.js'
/**
* WGT热更新处理
* 静默下载更新,无需重启应用
*/
export function useHotUpdate() {
const updating = ref(false)
const hasNewVersion = ref(false)
const currentVersion = ref('')
const latestVersion = ref('')
const downloadProgress = ref(0)
const serverWgtUrl = ref('') // 保存服务器更新包地址
// 测试模式相关状态
const isTestMode = ref(false)
const mockWgtUrl = 'https://example.com/mock-update.wgt' // 模拟更新包地址
// 获取当前应用版本
const getCurrentVersion = () => {
return new Promise((resolve) => {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, function(inf) {
const wgtVer = inf.version
currentVersion.value = wgtVer
resolve(wgtVer)
})
// #endif
// #ifndef APP-PLUS
const defaultVersion = '1.0.0' // 非APP环境下的默认值
currentVersion.value = defaultVersion
resolve(defaultVersion)
// #endif
})
}
/**
* 只检查版本,不自动更新
* 适用于手动触发的更新检查
*/
const checkVersionOnly = async () => {
try {
// 测试模式直接返回有新版本
if (isTestMode.value) {
await getCurrentVersion()
latestVersion.value = incrementVersion(currentVersion.value)
serverWgtUrl.value = mockWgtUrl
hasNewVersion.value = true
return true
}
// 获取当前版本
await getCurrentVersion()
// 替换为使用回调函数的方式
return new Promise((resolve) => {
uni.request({
url: `${getApiBaseUrl()}/app/version`,
method: 'GET',
success: (res) => {
if (!res || res.statusCode !== 200 || !res.data || res.data.code !== 200 || !res.data.data) {
resolve(false)
return
}
const serverInfo = res.data.data
latestVersion.value = serverInfo.version
// 保存服务器wgt地址以便后续手动更新使用
if (serverInfo.wgtUrl) {
serverWgtUrl.value = serverInfo.wgtUrl
}
// 比较版本号,检查是否有新版本
hasNewVersion.value = compareVersion(serverInfo.version, currentVersion.value) > 0
resolve(hasNewVersion.value)
},
fail: (error) => {
console.error('检查版本失败', error)
resolve(false)
}
})
})
} catch (error) {
console.error('检查版本失败', error)
return false
}
}
/**
* 自动增加版本号,用于测试
*/
const incrementVersion = (version) => {
const parts = version.split('.')
const lastPart = parseInt(parts[parts.length - 1]) + 1
parts[parts.length - 1] = lastPart.toString()
return parts.join('.')
}
/**
* 启用或禁用测试模式
*/
const toggleTestMode = (enabled = true) => {
isTestMode.value = enabled
console.log('测试模式已' + (enabled ? '启用' : '禁用'))
return isTestMode.value
}
// 检查更新并自动静默更新(App.vue使用)
const checkUpdate = async () => {
try {
// 如果是测试模式,不进行实际的静默更新
if (isTestMode.value) {
await getCurrentVersion()
latestVersion.value = incrementVersion(currentVersion.value)
hasNewVersion.value = true
return
}
// 获取当前版本
await getCurrentVersion()
// 替换为使用回调函数的方式
uni.request({
url: `${getApiBaseUrl()}/app/version`,
method: 'GET',
success: (res) => {
if (!res || res.statusCode !== 200 || !res.data || res.data.code !== 200 || !res.data.data) return
console.log('update version res', res)
const serverInfo = res.data.data
latestVersion.value = serverInfo.version
// 比较版本号,检查是否有新版本
if (compareVersion(serverInfo.version, currentVersion.value) > 0) {
hasNewVersion.value = true
// 如果有wgt下载地址执行静默更新
if (serverInfo.wgtUrl) {
serverWgtUrl.value = serverInfo.wgtUrl
silentUpdate(serverInfo.wgtUrl)
}
} else {
hasNewVersion.value = false
}
},
fail: (error) => {
console.error('检查更新失败', error)
}
})
} catch (error) {
console.error('检查更新失败', error)
}
}
// 比较版本号 (v1 > v2 返回1v1 < v2 返回-1相等返回0)
const compareVersion = (v1, v2) => {
const v1Parts = v1.split('.').map(Number)
const v2Parts = v2.split('.').map(Number)
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0
const v2Part = v2Parts[i] || 0
if (v1Part > v2Part) return 1
if (v1Part < v2Part) return -1
}
return 0
}
// 静默下载并安装更新
const silentUpdate = (wgtUrl) => {
if (updating.value) return Promise.reject('更新已在进行中')
updating.value = true
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
const dtask = plus.downloader.createDownload(wgtUrl, {
filename: '_doc/update/'
}, (download, status) => {
if (status === 200) {
installing(download.filename)
.then(() => {
updating.value = false
resolve()
})
.catch(err => {
updating.value = false
reject(err)
})
} else {
updating.value = false
reject('下载更新包失败')
}
})
dtask.start()
// #endif
// #ifndef APP-PLUS
updating.value = false
resolve() // 非APP环境下直接返回成功
// #endif
})
}
// 安装wgt包
const installing = (filePath) => {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
plus.runtime.install(filePath, {
force: false // 不重启应用
}, () => {
console.log('安装wgt成功')
resolve()
// 删除下载的安装包
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.remove()
})
}, (error) => {
console.error('安装wgt失败', error)
reject(error)
})
// #endif
// #ifndef APP-PLUS
resolve() // 非APP环境下直接返回成功
// #endif
})
}
/**
* 手动更新,提供进度回调
* @param {string} wgtUrl 更新包下载地址
* @returns {Promise} 返回更新结果Promise
*/
const manualUpdate = (wgtUrl) => {
if (updating.value) return Promise.reject('更新已在进行中')
updating.value = true
downloadProgress.value = 0
// 如果是测试模式,模拟下载进度而不实际下载
if (isTestMode.value) {
return new Promise((resolve) => {
let progress = 0
const interval = setInterval(() => {
progress += 5
downloadProgress.value = progress
if (progress >= 100) {
clearInterval(interval)
setTimeout(() => {
updating.value = false
resolve()
}, 500)
}
}, 200)
})
}
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
const dtask = plus.downloader.createDownload(wgtUrl, {
filename: '_doc/update/'
}, (download, status) => {
if (status === 200) {
installing(download.filename)
.then(() => {
updating.value = false
resolve()
})
.catch(err => {
updating.value = false
reject(err)
})
} else {
updating.value = false
reject('下载更新包失败')
}
})
// 监听下载进度
dtask.addEventListener('statechanged', (task, status) => {
switch (task.state) {
case 1: // 开始
console.log('开始下载更新...')
break
case 2: // 已连接到服务器
console.log('已连接到服务器...')
break
case 3: // 下载中
const totalSize = task.totalSize
const downloadedSize = task.downloadedSize
const progress = Math.round(downloadedSize / totalSize * 100) || 0
downloadProgress.value = progress
console.log(`下载进度: ${progress}%`)
break
case 4: // 下载完成
console.log('下载完成')
downloadProgress.value = 100
break
}
})
dtask.start()
// #endif
// #ifndef APP-PLUS
// 模拟下载进度
let progress = 0
const timer = setInterval(() => {
progress += 5
downloadProgress.value = progress
if (progress >= 100) {
clearInterval(timer)
updating.value = false
resolve()
}
}, 200)
// #endif
})
}
return {
updating,
hasNewVersion,
currentVersion,
latestVersion,
downloadProgress,
serverWgtUrl,
isTestMode,
checkUpdate,
checkVersionOnly,
manualUpdate,
toggleTestMode
}
}

View File

@@ -0,0 +1,45 @@
/**
* 推广弹层「分享给好友」依赖页面级 onShareAppMessage挂在含 QRcode 的页面(如 promote / invitation
*/
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getShareTitle } from '@/utils/runtimeEnv.js'
import { takeMiniPromotionShareFriend } from '@/utils/miniPromotionSharePayload.js'
function defaultSharePath() {
let path = '/pages/index'
if (typeof getCurrentPages === 'function') {
const pages = getCurrentPages()
const last = pages[pages.length - 1] as { route?: string }
if (last?.route)
path = `/${last.route}`
}
return path
}
export interface PromotionShareHandlersOptions {
/** 未从弹层发起分享时的标题,默认 getShareTitle() */
defaultTitle?: string
}
export function usePromotionShareHandlers(options: PromotionShareHandlersOptions = {}) {
const defaultTitle = options.defaultTitle ?? getShareTitle()
onShareAppMessage(() => {
const pending = takeMiniPromotionShareFriend()
if (pending) {
return {
title: pending.title || defaultTitle,
path: pending.path,
imageUrl: pending.imageUrl || undefined,
}
}
return {
title: defaultTitle,
path: defaultSharePath(),
}
})
onShareTimeline(() => ({
title: defaultTitle,
}))
}

View File

@@ -0,0 +1,45 @@
/**
* 小程序分享:分享给好友、分享到朋友圈
* 在页面 setup 中调用 useShare(options) 即可启用右上角菜单的「转发」与「分享到朋友圈」
*/
import { getShareTitle } from '@/utils/runtimeEnv.js'
const DEFAULT_IMAGE_URL = '' // 留空则微信使用页面截图
export interface UseShareOptions {
/** 分享标题(好友 + 朋友圈) */
title?: string
/** 分享给好友时的跳转路径,如 /pages/index */
path?: string
/** 分享图 URL可选不填用截图 */
imageUrl?: string
/** 分享到朋友圈时的 query如 from=timeline */
query?: string
}
export function useShare(options: UseShareOptions = {}) {
const title = options.title ?? getShareTitle()
let path = options.path ?? ''
if (!path && typeof getCurrentPages === 'function') {
const pages = getCurrentPages()
const last = pages[pages.length - 1] as { route?: string }
if (last?.route) path = '/' + last.route
}
const imageUrl = options.imageUrl ?? DEFAULT_IMAGE_URL
const query = options.query ?? ''
// 分享给好友/群
onShareAppMessage(() => ({
title,
path: path || undefined,
imageUrl: imageUrl || undefined,
}))
// 分享到朋友圈(仅微信小程序支持)
onShareTimeline(() => ({
title,
query: query || undefined,
imageUrl: imageUrl || undefined,
}))
}

View File

@@ -0,0 +1,14 @@
{
"apiUrl": "http://127.0.0.1:8888",
"apiPrefix": "/api/v1",
"siteOrigin": "http://127.0.0.1:8888",
"appName": "赤眉",
"companyName": "戎行技术有限公司有限公司",
"shareTitle": "赤眉 - 大数据报告查询,即刻赚佣金",
"shareTitleMe": "赤眉 - 大数据报告查询",
"shareTitleAgent": "赤眉 - 资产与收益",
"customerServiceUrl": "https://work.weixin.qq.com/kfid/xxxxxxxxxxxxx",
"customerServiceCorpId": "",
"inviteChannelKey": "8e3e7a2f60edb49221e953b9c029ed10",
"appDebug": true
}

View File

@@ -0,0 +1,14 @@
{
"apiUrl": "https://chimei.ronsafe.cn",
"apiPrefix": "/api/v1",
"siteOrigin": "https://chimei.ronsafe.cn",
"appName": "赤眉",
"companyName": "戎行技术有限公司有限公司",
"shareTitle": "赤眉 - 大数据报告查询,即刻赚佣金",
"shareTitleMe": "赤眉 - 大数据报告查询",
"shareTitleAgent": "赤眉 - 资产与收益",
"customerServiceUrl": "https://work.weixin.qq.com/kfid/xxxxxxxxxxxxx",
"customerServiceCorpId": "",
"inviteChannelKey": "8e3e7a2f60edb49221e953b9c029ed10",
"appDebug": false
}

View File

@@ -0,0 +1,59 @@
import fs from 'node:fs'
import path from 'node:path'
/** 与 runtime.*.json 字段一致(仅 Nodevite / pages / manifest 读文件) */
export interface BdrpRuntimeConfig {
apiUrl: string
apiPrefix: string
siteOrigin: string
appName: string
companyName: string
shareTitle: string
shareTitleMe: string
shareTitleAgent: string
customerServiceUrl: string
customerServiceCorpId?: string
inviteChannelKey: string
/** 可选;未填视为 false */
appDebug?: boolean
}
const REQUIRED: (keyof BdrpRuntimeConfig)[] = [
'apiUrl',
'apiPrefix',
'siteOrigin',
'appName',
'companyName',
'shareTitle',
'shareTitleMe',
'shareTitleAgent',
'customerServiceUrl',
'inviteChannelKey',
]
export function resolveRuntimeConfigPath(projectRoot: string, mode: string): string {
const name = mode === 'production' ? 'production' : 'development'
return path.join(projectRoot, 'src', 'config', `runtime.${name}.json`)
}
/**
* 读取小程序运行时配置(仅 Nodevite / pages.config / manifest.config
*/
export function readRuntimeConfig(projectRoot: string, mode: string): BdrpRuntimeConfig {
const filePath = resolveRuntimeConfigPath(projectRoot, mode)
let raw: string
try {
raw = fs.readFileSync(filePath, 'utf8')
}
catch {
throw new Error(`缺少运行时配置文件: ${filePath}(请从同目录示例复制并填写)`)
}
const cfg = JSON.parse(raw) as BdrpRuntimeConfig
for (const k of REQUIRED) {
const v = cfg[k]
if (v === undefined || v === '') {
throw new Error(`运行时配置缺少或为空: ${filePath}${String(k)}`)
}
}
return cfg
}

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

@@ -0,0 +1,28 @@
/// <reference types="vite/client" />
/** 与 src/config/runtime.*.json 一致 */
interface BdrpRuntimeConfig {
apiUrl: string
apiPrefix: string
siteOrigin: string
appName: string
companyName: string
shareTitle: string
shareTitleMe: string
shareTitleAgent: string
customerServiceUrl: string
customerServiceCorpId?: string
inviteChannelKey: string
appDebug?: boolean
}
interface ImportMetaEnv {
readonly MODE: string
readonly DEV: boolean
readonly PROD: boolean
readonly SSR: boolean
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

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 }
}

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

@@ -0,0 +1,128 @@
<script setup>
import { getCustomerServiceUrl, getCustomerServiceCorpId } from '@/utils/runtimeEnv.js'
const tabbar = ref('index')
const menu = reactive([{ title: '首页', icon: 'home', name: 'index' }, { title: '资产', icon: 'money-circle', name: 'agent' }, { title: '我的', icon: 'user', name: 'me' }])
// 获取当前页面名称
const currentPage = ref('')
function updateCurrentPage() {
const pages = getCurrentPages()
if (pages.length > 0) {
const page = pages[pages.length - 1].route
currentPage.value = page.split('/').pop()
}
}
function tabChange({ value }) {
uni.switchTab({
url: `/pages/${value}`,
})
}
onShow(() => {
updateCurrentPage()
tabbar.value = currentPage.value
})
onMounted(() => {
uni.hideTabBar()
updateCurrentPage()
tabbar.value = currentPage.value
})
function toComplaint() {
const url = getCustomerServiceUrl()
const corpId = getCustomerServiceCorpId()
// #ifdef MP-WEIXIN
if (!corpId) {
uni.showToast({ title: '请先配置客服企业ID', icon: 'none' })
return
}
wx.openCustomerServiceChat({
extInfo: { url },
corpId,
success() {
console.log('打开客服成功')
},
fail(err) {
console.log('打开客服失败', err)
uni.showToast({ title: '打开客服失败', icon: 'none' })
},
})
// #endif
// #ifndef MP-WEIXIN
// #ifdef APP-PLUS
plus.runtime.openURL(url)
// #endif
// #ifdef H5
window.location.href = url
// #endif
// #endif
}
</script>
<script>
export default {
options: {
styleIsolation: 'shared',
},
}
</script>
<template>
<view
class="safe-area-top 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>
<!-- #ifdef MP-WEIXIN -->
<button v-if="currentPage !== 'ai'"
class="fixed bottom-24 right-4 z-1000 flex items-center justify-center py-2 rounded-full bg-gradient-to-r from-[#ffb86c] to-[#ff6e6a] shadow-lg active:scale-95 transition-all border-none"
style="min-width: 72px;" @click="toComplaint">
<view class="text-xs font-bold tracking-wide text-white text-center">投诉</view>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view v-if="currentPage !== 'ai'"
class="fixed bottom-24 right-4 z-1000 flex items-center justify-center py-2 rounded-full bg-gradient-to-r from-[#ffb86c] to-[#ff6e6a] shadow-lg active:scale-95 transition-all"
style="min-width: 72px;" @click="toComplaint">
<view class="text-xs font-bold tracking-wide text-white text-center">投诉</view>
</view>
<!-- #endif -->
</template>
<style scoped>
:deep(.qnc-tabbar) {
bottom: 16px !important;
}
/* 微信小程序按钮样式重置 */
/* #ifdef MP-WEIXIN */
button::after {
border: none;
}
button {
margin: 0 !important;
line-height: inherit !important;
font-size: inherit !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
border: none !important;
border-radius: 9999px !important;
/* rounded-full */
}
/* #endif */
</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>

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

@@ -0,0 +1,34 @@
<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)
// 返回页面标题,如果未找到,则返回默认标题
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>

41
src/main.ts Normal file
View File

@@ -0,0 +1,41 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import 'uno.css'
import '@/app.scss'
import { setupRouterGuard } from '@/utils/routerGuard'
import { getShareTitle } from '@/utils/runtimeEnv.js'
const DEFAULT_SHARE_TITLE = getShareTitle()
// 全局分享:所有页面右上角「转发」「分享到朋友圈」使用统一默认配置
const shareMixin = {
onShareAppMessage() {
let path = '/pages/index'
if (typeof getCurrentPages === 'function') {
const pages = getCurrentPages()
const last = pages[pages.length - 1] as { route?: string }
if (last?.route) path = '/' + last.route
}
return {
title: DEFAULT_SHARE_TITLE,
path,
}
},
onShareTimeline() {
return {
title: DEFAULT_SHARE_TITLE,
}
},
}
export function createApp() {
const app = createSSRApp(App)
app.mixin(shareMixin)
// 初始化路由守卫
setupRouterGuard()
return {
app
}
}

120
src/manifest.json Normal file
View File

@@ -0,0 +1,120 @@
{
"name": "赤眉",
"appid": "__UNI__CC3DA09",
"description": "",
"versionName": "1.0.0",
"versionCode": "107",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {
"Share": {},
"Camera": {},
"PhotoLibrary": {}
},
"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": "需要访问您的网络来提供更好的服务",
"NSPhotoLibraryAddUsageDescription": "此应用需要访问您的相册以保存图片"
},
"idfa": false,
"bundleIdentifier": "com.allinone.check"
},
"sdkConfigs": {},
"icons": {
"android": {
"hdpi": "static/icons/72x72.png",
"xhdpi": "static/icons/96x96.png",
"xxhdpi": "static/icons/144x144.png",
"xxxhdpi": "static/icons/192x192.png"
},
"ios": {
"appstore": "static/icons/1024x1024.png",
"ipad": {
"app": "static/icons/76x76.png",
"app@2x": "static/icons/152x152.png",
"notification": "static/icons/20x20.png",
"notification@2x": "static/icons/40x40.png",
"proapp@2x": "static/icons/167x167.png",
"settings": "static/icons/29x29.png",
"settings@2x": "static/icons/58x58.png",
"spotlight": "static/icons/40x40.png",
"spotlight@2x": "static/icons/80x80.png"
},
"iphone": {
"app@2x": "static/icons/120x120.png",
"app@3x": "static/icons/180x180.png",
"notification@2x": "static/icons/40x40.png",
"notification@3x": "static/icons/60x60.png",
"settings@2x": "static/icons/58x58.png",
"settings@3x": "static/icons/87x87.png",
"spotlight@2x": "static/icons/80x80.png",
"spotlight@3x": "static/icons/120x120.png"
}
}
}
},
"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": {
"lz-url-launch": {
"version": "1.0.0",
"description": "SFSafariViewController插件支持在iOS中使用系统浏览器打开网页",
"platforms": {
"ios": {
"appid": "__UNI_LZ_URL_LAUNCH_IOS",
"autostart": false
}
}
}
}
}
}

169
src/pages.json Normal file
View File

@@ -0,0 +1,169 @@
{
"pages": [
{
"path": "pages/index",
"type": "home",
"layout": "home",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#e3f0ff"
}
},
{
"path": "pages/agent",
"type": "page",
"layout": "home",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#e3f0ff"
}
},
{
"path": "pages/agentVip",
"type": "page",
"layout": "page",
"title": "代理会员",
"agent": true,
"auth": true
},
{
"path": "pages/agentVipApply",
"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",
"layout": "page",
"title": "协议",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#e3f0ff"
}
},
{
"path": "pages/h5open",
"type": "page",
"layout": "page",
"title": "推广详情",
"auth": false,
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"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",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#e3f0ff"
}
},
{
"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/rewardsDetails",
"type": "page",
"layout": "page",
"title": "收益明细",
"agent": true,
"auth": 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/me",
"text": "",
"visible": false
}
]
},
"__esModule": true,
"subPackages": []
}

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

@@ -0,0 +1,261 @@
<script setup>
import { ref, computed } from 'vue'
import { getAgentRevenue } from '@/api/apis'
import GzhQrcode from '@/components/GzhQrcode.vue'
import { getAgentTabShareTitle } from '@/utils/runtimeEnv.js'
// 分享给好友、分享到朋友圈
useShare({ title: getAgentTabShareTitle() })
// 日期选项映射
const dateRangeMap = {
today: 'today',
week: 'last7d',
month: 'last30d'
}
// 日期文本映射
const dateTextMap = {
today: '今日',
week: '近7天',
month: '近1月'
}
// 直推报告数据
const promoteDateOptions = [
{ label: '今日', value: 'today' },
{ label: '近7天', value: 'week' },
{ label: '近1月', value: 'month' }
]
const selectedPromoteDate = ref('today')
// 团队奖励数据(仅保留:下级推广/下级转化/下级提现)
const teamDateOptions = [
{ label: '今日', value: 'today' },
{ label: '近7天', value: 'week' },
{ label: '近1月', value: 'month' }
]
const selectedTeamDate = ref('today')
const data = ref(null)
// 控制公众号二维码弹窗显示
const showGzhQrcode = ref(false)
// 计算当前直推数据
const currentPromoteData = computed(() => {
const range = dateRangeMap[selectedPromoteDate.value]
return data.value?.direct_push?.[range] || { commission: 0, report: 0 }
})
const currentTeamData = computed(() => {
const range = dateRangeMap[selectedTeamDate.value]
return data.value?.active_reward?.[range] || {
sub_promote_reward: 0,
sub_upgrade_reward: 0,
sub_withdraw_reward: 0
}
})
// 计算日期文本
const promoteTimeText = computed(() => {
return dateTextMap[selectedPromoteDate.value] || '今日'
})
const teamTimeText = computed(() => {
return dateTextMap[selectedTeamDate.value] || '今日'
})
const getData = async () => {
try {
const res = await getAgentRevenue()
if (res.code === 200) {
data.value = res.data
}
} catch {}
}
onBeforeMount(() => {
if (uni.getStorageSync('token')) {
getData()
}
})
// 路由跳转
function goToPromoteDetail() {
uni.navigateTo({
url: '/pages/promoteDetails'
})
}
function goToRewardsDetail() {
uni.navigateTo({
url: '/pages/rewardsDetails'
})
}
function toWithdraw() {
// 弹出公众号二维码提示提现
showGzhQrcode.value = true
}
// 关闭公众号二维码弹窗
function closeGzhQrcode() {
showGzhQrcode.value = false
}
function toWithdrawDetails() {
uni.navigateTo({
url: '/pages/withdrawDetails'
})
}
</script>
<template>
<view class="safe-area-top min-h-screen">
<view class="p-4">
<!-- 资产卡片 -->
<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">
<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 shadow-md flex items-center justify-center">
<text>提现</text>
</view>
<view @click="toWithdrawDetails"
class="bg-white/90 text-gray-600 border border-gray-200/50 rounded-full py-2 shadow-sm flex items-center justify-center">
<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">
<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 text-center',
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">
<text>{{ promoteTimeText }}收益</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">
<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">
<text class="text-lg font-bold text-gray-800">团队奖励</text>
</view>
</view>
<view class="grid grid-cols-3 gap-2 mb-6">
<view v-for="item in teamDateOptions" :key="item.value" @click="selectedTeamDate = item.value" :class="[
'rounded-full transition-all py-1 px-4 text-sm text-center',
selectedTeamDate === 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-1 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">
<text>{{ teamTimeText }}下级推广奖励</text>
</view>
<text class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentTeamData.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">
<text>{{ teamTimeText }}下级转化奖励</text>
</view>
<text class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentTeamData.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">
<text>{{ teamTimeText }}下级提现奖励</text>
</view>
<text class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentTeamData.sub_withdraw_reward || 0).toFixed(2) }}</text>
</view>
</view>
<view class="flex items-center justify-between text-green-500 text-sm font-semibold" @click="goToRewardsDetail">
<text>查看奖励明细</text>
<text class="text-lg"></text>
</view>
</view>
</view>
<!-- 公众号二维码弹窗 -->
<GzhQrcode
:visible="showGzhQrcode"
type="withdraw"
@close="closeGzhQrcode"
/>
</view>
</template>
<style>
button {
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-1px);
}
</style>
<route lang="json">{
"layout": "home",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#e3f0ff"
}
}</route>

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

@@ -0,0 +1,65 @@
<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>
import { getCustomerServiceUrl } from '@/utils/runtimeEnv.js'
function toService() {
const url = getCustomerServiceUrl()
// #ifdef APP-PLUS
plus.runtime.openURL(url)
// #endif
// #ifdef H5
window.location.href = url
// #endif
// #ifdef MP
uni.navigateTo({
url: '/pages/agreement?url=' + encodeURIComponent(url),
})
// #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>

734
src/pages/agentVipApply.vue Normal file
View File

@@ -0,0 +1,734 @@
<template>
<view class="agent-VIP-apply w-full min-h-screen bg-gradient-to-b from-amber-50 via-amber-100 to-amber-50 pb-24">
<!-- 装饰元素 -->
<view
class="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-amber-300 to-amber-500 rounded-bl-full opacity-20">
</view>
<view
class="absolute top-40 left-0 w-16 h-16 bg-gradient-to-tr from-amber-400 to-amber-600 rounded-tr-full opacity-20">
</view>
<view
class="absolute bottom-60 right-0 w-24 h-24 bg-gradient-to-bl from-amber-300 to-amber-500 rounded-tl-full opacity-20">
</view>
<!-- 顶部标题区域 -->
<view class="header relative pt-8 px-4 pb-6 text-center">
<view
class="animate-pulse absolute -top-2 left-1/2 -translate-x-1/2 w-24 h-1 bg-gradient-to-r from-amber-300 via-amber-500 to-amber-300 rounded-full">
</view>
<text class="text-3xl font-bold text-amber-800 mb-1 block">
{{ isVipOrSvip ? '代理会员续费' : 'VIP代理申请' }}
</text>
<view class="text-sm text-amber-700 mt-2 max-w-xs mx-auto">
<block v-if="isVipOrSvip">
您的会员有效期至 {{ formatExpiryTime(ExpiryTime) }}续费后有效期至
{{ renewalExpiryTime }}
</block>
<block v-else>
平台为疯狂推广者定制的赚买计划助您收益<text class="text-red-500 font-bold">翻倍增升</text>
</block>
</view>
<!-- 装饰性金币图标 -->
<view class="absolute top-6 left-4 transform -rotate-12">
<view
class="w-8 h-8 bg-gradient-to-br from-yellow-300 to-yellow-500 rounded-full flex items-center justify-center shadow-lg">
<text class="text-white font-bold text-xs">¥</text>
</view>
</view>
<view class="absolute top-10 right-6 transform rotate-12">
<view
class="w-6 h-6 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center shadow-lg">
<text class="text-white font-bold text-xs">¥</text>
</view>
</view>
</view>
<!-- 选择代理类型 -->
<view class="card-container px-4 mb-8">
<view class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100 transform transition-all">
<view
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<text class="relative z-10">选择代理类型</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</view>
</view>
</view>
<view class="flex p-6 gap-4">
<view
class="flex-1 border-2 rounded-lg p-4 text-center cursor-pointer transition-all duration-300 relative transform hover:-translate-y-1"
:class="[
selectedType === 'vip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('vip')">
<text class="text-xl font-bold text-amber-700 block">VIP代理</text>
<text class="text-amber-600 font-bold mt-1 text-lg block">{{ vipConfig.price }}{{ vipConfig.priceUnit
}}</text>
<text class="mt-2 text-gray-600 text-sm block">标准VIP权益</text>
<view v-if="selectedType === 'vip'"
class="absolute -top-2 -right-2 w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center shadow-md">
<wd-icon name="check" color="#fff" size="14" />
</view>
</view>
<view
class="flex-1 border-2 rounded-lg p-4 text-center cursor-pointer transition-all duration-300 relative transform hover:-translate-y-1"
:class="[
selectedType === 'svip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('svip')">
<text class="text-xl font-bold text-amber-700 block">SVIP代理</text>
<text class="text-amber-600 font-bold mt-1 text-lg block">{{ vipConfig.svipPrice }}{{ vipConfig.priceUnit
}}</text>
<text class="mt-2 text-gray-600 text-sm block">超级VIP权益</text>
<view v-if="selectedType === 'svip'"
class="absolute -top-2 -right-2 w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center shadow-md">
<wd-icon name="check" color="#fff" size="14" />
</view>
</view>
</view>
</view>
</view>
<!-- 六大超值权益 -->
<view class="card-container px-4 mb-8">
<view class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100">
<view
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<text class="relative z-10">六大超值权益</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</view>
</view>
</view>
<view class="grid grid-cols-2 gap-4 p-4">
<!-- 权益1 -->
<view
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<view class="text-amber-800 font-bold mb-2 flex items-center">
<text
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">1</text>
下级贡献收益
</view>
<text class="text-sm text-gray-600 block">
下级完全收益您来定涨多少赚多少一单最高收益<text class="text-red-500 font-bold">10</text>
</text>
</view>
<!-- 权益2 -->
<view
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<view class="text-amber-800 font-bold mb-2 flex items-center">
<text
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">2</text>
下级提现收益
</view>
<text class="text-sm text-gray-600 block">
下级定价标准由您定超过标准部分收益更丰厚一单最高多赚<text class="text-red-500 font-bold">10</text>
</text>
</view>
<!-- 权益3 -->
<view
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<view class="text-amber-800 font-bold mb-2 flex items-center">
<text
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">3</text>
转换高额奖励
</view>
<text class="text-sm text-gray-600 block">
下级成为VIPSVIP高额奖励立马发放<text class="text-red-500 font-bold">399</text>
</text>
</view>
<!-- 权益4 -->
<view
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<view class="text-amber-800 font-bold mb-2 flex items-center">
<text
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">4</text>
下级提现奖励
</view>
<text class="text-sm text-gray-600 block">下级成为SVIP每次提现都奖励1%坐享被动收入</text>
</view>
<!-- 权益6 -->
<view
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<view class="text-amber-800 font-bold mb-2 flex items-center">
<text
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">6</text>
平台专项扶持
</view>
<text class="text-sm text-gray-600 block">一对一专属客服服务为合作伙伴提供全方位成长赋能</text>
</view>
</view>
</view>
</view>
<!-- 权益对比表 -->
<view class="card-container px-4 mb-8" v-if="selectedType">
<view class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100">
<view
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<text class="relative z-10">{{ selectedType === 'vip' ? 'VIP' : 'SVIP' }}代理权益对比</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</view>
</view>
</view>
<view class="p-4">
<!-- 权益对比表格 -->
<view class="w-full border border-amber-200 rounded-lg overflow-hidden">
<!-- 表头 -->
<view class="flex bg-gradient-to-r from-amber-100 to-amber-200">
<view class="w-16 flex-shrink-0 p-3 border-r border-amber-200 text-left font-bold text-amber-800">权益项目
</view>
<view class="flex-1 flex-shrink-0 p-3 border-r border-amber-200 text-center font-bold text-amber-800">普通代理
</view>
<view class="flex-1 flex-shrink-0 p-3 border-r border-amber-200 text-center font-bold text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }">VIP代理</view>
<view class="flex-1 flex-shrink-0 p-3 text-center font-bold text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }">SVIP代理</view>
</view>
<!-- 表格内容 -->
<view v-for="(item, index) in benefitsComparisonData" :key="index"
class="flex border-b border-amber-200 last:border-b-0" :class="{ 'bg-amber-50': index % 2 === 1 }">
<view class="w-16 flex-shrink-0 p-3 border-r border-amber-200 text-left font-medium">{{ item.benefit }}
</view>
<view class="flex-1 flex-shrink-0 p-3 border-r border-amber-200 text-center text-sm">{{ item.normal }}
</view>
<view class="flex-1 flex-shrink-0 p-3 border-r border-amber-200 text-center text-sm" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">{{ item.vip }}</view>
<view class="flex-1 flex-shrink-0 p-3 text-center text-sm" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">{{ item.svip }}</view>
</view>
</view>
</view>
</view>
</view>
<!-- 收益预估 -->
<view class="card-container px-4 mb-8" v-if="selectedType">
<view class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100">
<view
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<text class="relative z-10">收益预估对比</text>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</view>
</view>
</view>
<view class="p-4">
<!-- 顶部收益概览 -->
<view class="mb-6 rounded-lg overflow-hidden border border-amber-200">
<view class="bg-gradient-to-r from-amber-100 to-amber-200 py-2 px-4 text-center font-bold text-amber-800">
VIP与SVIP代理收益对比
</view>
<view class="grid grid-cols-2 divide-x divide-amber-200">
<view class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'vip' }">
<text class="text-sm text-gray-600 mb-1 block">VIP月预计收益</text>
<text class="text-amber-600 font-bold text-xl block">{{ revenueData.vipMonthly }}</text>
<text class="text-xs text-gray-500 mt-1 block">年收益{{ revenueData.vipYearly }}</text>
</view>
<view class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'svip' }">
<text class="text-sm text-gray-600 mb-1 block">SVIP月预计收益</text>
<text class="text-red-500 font-bold text-xl block">{{ revenueData.svipMonthly }}</text>
<text class="text-xs text-gray-500 mt-1 block">年收益{{ revenueData.svipYearly }}</text>
</view>
</view>
<view class="bg-gradient-to-r from-red-50 to-red-100 py-2 px-4 text-center text-red-600 font-medium">
选择SVIP相比VIP月增收益<text class="font-bold">{{ revenueData.monthlyDifference }}</text>
</view>
</view>
<!-- 详细收益表格 -->
<view>
<view class="w-full border border-amber-200 rounded-lg overflow-hidden">
<!-- 表头 -->
<view class="flex bg-gradient-to-r from-amber-100 to-amber-200">
<view class="w-24 flex-shrink-0 p-3 border-r border-amber-200 text-left font-bold text-amber-800">收益来源
</view>
<view class="flex-1 flex-shrink-0 p-3 border-r border-amber-200 text-center font-bold text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }">VIP代理</view>
<view class="flex-1 flex-shrink-0 p-3 text-center font-bold text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }">SVIP代理</view>
</view>
<!-- 表格内容 -->
<view v-for="(item, index) in revenueComparisonData" :key="index"
class="flex border-b border-amber-200 last:border-b-0" :class="{
'bg-amber-50': index % 2 === 1,
'bg-gradient-to-r from-amber-50 to-amber-100 font-bold': item.source.includes('收益')
}">
<view class="w-24 flex-shrink-0 p-3 border-r border-amber-200 text-left font-medium">{{ item.source }}
</view>
<view class="flex-1 flex-shrink-0 p-3 border-r border-amber-200 text-center text-sm" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
'text-amber-700': item.source.includes('收益'),
'border-amber-300': item.source.includes('收益') && selectedType === 'vip'
}">{{ item.vip }}</view>
<view class="flex-1 flex-shrink-0 p-3 text-center text-sm" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
'text-red-500': item.source.includes('收益'),
'border-amber-300': item.source.includes('收益') && selectedType === 'svip'
}">{{ item.svip }}</view>
</view>
</view>
</view>
<!-- 投资回报率 -->
<view class="mt-6 p-4 bg-gradient-to-r from-amber-50 to-amber-100 rounded-lg border border-amber-200">
<text class="text-center mb-3 font-bold text-amber-800 block">投资收益分析</text>
<view class="grid grid-cols-1 gap-4">
<view class="p-3 bg-white rounded-lg shadow-sm">
<view class="flex items-center justify-between">
<view class="flex-1 border-r border-amber-100 pr-3">
<text class="text-amber-700 font-medium text-center mb-1 block">VIP方案</text>
<view class="text-center">
<text class="text-amber-600 text-sm block">投资{{ vipConfig.price }}</text>
<text class="text-gray-600 text-sm block">月收益{{ revenueData.vipMonthly }}</text>
</view>
</view>
<view class="flex-1 pl-3">
<text class="text-red-500 font-medium text-center mb-1 block">SVIP方案</text>
<view class="text-center">
<text class="text-red-500 text-sm block">投资{{ vipConfig.svipPrice }}</text>
<text class="text-gray-600 text-sm block">月收益{{ revenueData.svipMonthly }}</text>
</view>
</view>
</view>
</view>
<!-- 升级收益对比 -->
<view class="p-3 bg-gradient-to-r from-red-50 to-amber-50 rounded-lg shadow-sm">
<text class="text-center font-medium text-red-700 mb-2 block">SVIP升级优势分析</text>
<view class="flex items-center justify-center gap-3">
<view class="text-center">
<text class="text-sm text-gray-600 block">额外投资</text>
<text class="text-red-600 font-bold block">{{ revenueData.priceDifference }}</text>
</view>
<view
class="bg-red-500 flex-shrink-0 text-white rounded-full w-6 h-6 flex items-center justify-center">
<text class="transform -translate-y-px"></text>
</view>
<view class="text-center">
<text class="text-sm text-gray-600 block">每月额外收益</text>
<text class="text-red-600 font-bold block">{{ revenueData.monthlyDifference }}</text>
</view>
<view
class="bg-red-500 flex-shrink-0 text-white rounded-full w-6 h-6 flex items-center justify-center">
<text class="transform -translate-y-px"></text>
</view>
<view class="text-center">
<text class="text-sm text-gray-600 block">投资回收时间</text>
<text class="text-red-600 font-bold block">{{ revenueData.recoverDays }}</text>
</view>
</view>
<view class="text-center text-red-500 font-medium mt-3">
额外投资{{ revenueData.priceDifference }}<text class="text-red-600 font-bold">年多赚{{
revenueData.yearlyDifference }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 申请按钮固定在底部 -->
<view
class="fixed bottom-0 left-0 right-0 px-4 py-3 bg-gradient-to-t from-amber-100 to-transparent backdrop-blur-sm z-30">
<view class="flex flex-col gap-2">
<button :class="buttonClass" @click="applyVip" :disabled="!canPerformAction">
<text class="relative z-10">{{ buttonText }}</text>
<view
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</view>
</button>
<!-- 最终解释权声明 -->
<text class="text-center text-xs text-gray-400 py-1 block">最终解释权归{{ copyrightOwner }}所有</text>
</view>
</view>
</view>
<Payment v-model="showPayment" :data="payData" :id="payID" type="agent_vip" @close="showPayment = false" />
</template>
<script setup>
import { ref, onMounted, reactive, computed } from 'vue'
import { useToast } from 'wot-design-uni'
import { activateAgentMembership } from '@/apis/agent'
import { formatExpiryTime } from '@/utils/format'
import { getCompanyName } from '@/utils/runtimeEnv.js'
const copyrightOwner = getCompanyName()
// 使用 wot-design-ui 的 toast
const toast = useToast()
// 代理相关数据
const level = ref('normal')
const ExpiryTime = ref('')
const isAgent = ref(false)
const agentStatus = ref(0)
const agentID = ref('')
const mobile = ref('')
const isRealName = ref(false)
// 计算是否已经是VIP或SVIP
const isVipOrSvip = computed(() => ['VIP', 'SVIP'].includes(level.value))
const isVip = computed(() => level.value === 'VIP')
const isSvip = computed(() => level.value === 'SVIP')
// 计算续费后的到期时间
const renewalExpiryTime = computed(() => {
if (!ExpiryTime.value) return '未知'
// 从格式化字符串中提取日期部分
const dateStr = ExpiryTime.value.split(' ')[0] // 假设格式是 "YYYY-MM-DD HH:MM:SS"
const [year, month, day] = dateStr.split('-').map(num => parseInt(num))
// 创建日期对象并加一年
const expiryDate = new Date(year, month - 1, day) // 月份从0开始所以要-1
expiryDate.setFullYear(expiryDate.getFullYear() + 1)
// 返回格式化的日期字符串
return `${expiryDate.getFullYear()}-${String(expiryDate.getMonth() + 1).padStart(2, '0')}-${String(expiryDate.getDate()).padStart(2, '0')}`
})
// 按钮文字 - 根据当前状态显示不同文案
const buttonText = computed(() => {
if (!isVipOrSvip.value) return '立即开通' // 非会员状态
if (selectedType.value === 'vip') {
if (isVip.value) return '续费VIP代理' // VIP续费VIP
return '降级不可用' // SVIP不能降级到VIP
} else {
if (isSvip.value) return '续费SVIP代理' // SVIP续费SVIP
return '升级SVIP代理' // VIP升级SVIP
}
})
// 是否可以操作按钮
const canPerformAction = computed(() => {
// 非会员可以开通任何会员
if (!isVipOrSvip.value) return true
// VIP不能降级到普通会员
if (isVip.value && selectedType.value === '') return false
// SVIP不能降级到VIP
if (isSvip.value && selectedType.value === 'vip') return false
return true
})
// 计算按钮类名
const buttonClass = computed(() => {
const baseClass =
'w-full py-4 rounded-lg font-bold text-lg shadow-lg transform transition-transform scale-btn relative overflow-hidden'
if (!canPerformAction.value) {
return `${baseClass} bg-gray-400 text-white cursor-not-allowed`
}
if (isVip.value && selectedType.value === 'svip') {
return `${baseClass} bg-gradient-to-r from-purple-500 to-indigo-600 text-white`
}
return `${baseClass} bg-gradient-to-r from-amber-500 to-amber-600 active:from-amber-600 active:to-amber-700 text-white`
})
// VIP价格配置
const vipConfig = reactive({
price: 399, // VIP价格
svipPrice: 599, // SVIP价格
priceUnit: '元/年',
vipCommission: 1.2, // VIP下级贡献收益(元/单)
svipCommission: 1.5, // SVIP下级贡献收益(元/单)
vipFloatingRate: 5, // VIP下级价格浮动收益率(%)
svipFloatingRate: 10, // SVIP下级价格浮动收益率(%)
withdrawRatio: 1, // 下级提现奖励比率(%)
vipConversionBonus: 299, // VIP下级转化奖励(元)
svipConversionBonus: 399, // SVIP下级转化奖励(元)
vipWithdrawalLimit: 1500, // VIP提现额度(元)
svipWithdrawalLimit: 3000, // SVIP提现额度(元)
})
// 权益对比表数据
const benefitsComparisonData = computed(() => {
return [
{
benefit: '会员权益',
normal: '普通代理 免费',
vip: `${vipConfig.price}${vipConfig.priceUnit}`,
svip: `${vipConfig.svipPrice}${vipConfig.priceUnit}`
},
{
benefit: '下级贡献收益',
normal: '1元/单',
vip: `${vipConfig.vipCommission}元/单`,
svip: `${vipConfig.svipCommission}元/单`
},
{
benefit: '自定义设置下级成本',
normal: '❌',
vip: '✓',
svip: '✓'
},
{
benefit: '下级价格浮动收益',
normal: '❌',
vip: `最高${vipConfig.vipFloatingRate}%`,
svip: `最高${vipConfig.svipFloatingRate}%`
},
{
benefit: '下级提现奖励',
normal: '❌',
vip: '❌',
svip: `${vipConfig.withdrawRatio}%`
},
{
benefit: '下级转化奖励',
normal: '❌',
vip: `${vipConfig.vipConversionBonus}元*10个`,
svip: `${vipConfig.svipConversionBonus}元*10个`
},
{
benefit: '提现次数额度',
normal: '800元/次',
vip: `${vipConfig.vipWithdrawalLimit}元/次`,
svip: `${vipConfig.svipWithdrawalLimit}元/次`
},
{
benefit: '提现次数',
normal: '1次/日',
vip: '1次/日',
svip: '2次/日'
}
]
})
// 收益对比表数据
const revenueComparisonData = computed(() => {
const revenue = revenueData.value
const withdrawReward = 20000 * (vipConfig.withdrawRatio / 100)
return [
{
source: '推广收益(月)',
vip: '300单×50元=15,000元',
svip: '300单×50元=15,000元'
},
{
source: '下级贡献收益(月)',
vip: `300单×${vipConfig.vipCommission}元=360元`,
svip: `300单×${vipConfig.svipCommission}元=450元`
},
{
source: '下级价格浮动收益(月)',
vip: `100单×100元×${vipConfig.vipFloatingRate}%=500元`,
svip: `200单×100元×${vipConfig.svipFloatingRate}%=2,000元`
},
{
source: '下级提现奖励(月)',
vip: '-',
svip: `${withdrawReward}`
},
{
source: '下级转化奖励(月)',
vip: `${vipConfig.vipConversionBonus}×2个=598元`,
svip: `${vipConfig.svipConversionBonus}×2个=798元`
},
{
source: '额外业务收益(月)',
vip: '约3,000元',
svip: '约6,000元'
},
{
source: '月计收益',
vip: `${revenue.vipMonthly}`,
svip: `${revenue.svipMonthly}`
},
{
source: '年计收益',
vip: `${revenue.vipYearly}`,
svip: `${revenue.svipYearly}`
}
]
})
// 计算得出的收益数据
const revenueData = computed(() => {
const baseOrders = 300 // 基础订单数
const pricePerOrder = 50 // 每单价格
const baseRevenue = baseOrders * pricePerOrder // 基础推广收益
const vipCommissionRevenue = baseOrders * vipConfig.vipCommission // VIP下级贡献收益
const svipCommissionRevenue = baseOrders * vipConfig.svipCommission // SVIP下级贡献收益
const vipFloatingRevenue = 100 * 100 * (vipConfig.vipFloatingRate / 100) // VIP浮动收益
const svipFloatingRevenue = 200 * 100 * (vipConfig.svipFloatingRate / 100) // SVIP浮动收益
const vipConversionRevenue = vipConfig.vipConversionBonus * 2 // VIP转化奖励
const svipConversionRevenue = vipConfig.svipConversionBonus * 2 // SVIP转化奖励
const vipExtraRevenue = 3000 // VIP额外收益估计
const svipExtraRevenue = 6000 // SVIP额外收益估计
// 平级提现奖励(只有SVIP才有)
const withdrawReward = 20000 * (vipConfig.withdrawRatio / 100)
// 计算月总收益
const vipMonthlyTotal =
baseRevenue +
vipCommissionRevenue +
vipFloatingRevenue +
vipConversionRevenue +
vipExtraRevenue
const svipMonthlyTotal =
baseRevenue +
svipCommissionRevenue +
svipFloatingRevenue +
withdrawReward +
svipConversionRevenue +
svipExtraRevenue
// 计算VIP和SVIP之间的差额
const monthlyDifference = svipMonthlyTotal - vipMonthlyTotal
const priceDifference = vipConfig.svipPrice - vipConfig.price
return {
vipMonthly: Math.round(vipMonthlyTotal),
svipMonthly: Math.round(svipMonthlyTotal),
vipYearly: Math.round(vipMonthlyTotal * 12),
svipYearly: Math.round(svipMonthlyTotal * 12),
monthlyDifference: Math.round(monthlyDifference),
yearlyDifference: Math.round(monthlyDifference * 12),
vipRate: Math.round(vipMonthlyTotal / vipConfig.price),
svipRate: Math.round(svipMonthlyTotal / vipConfig.svipPrice),
priceDifference,
recoverDays: Math.ceil(priceDifference / (monthlyDifference / 30)),
withdrawReward,
}
})
// 初始化代理数据和价格配置
onMounted(async () => {
// 从本地缓存获取代理信息
loadAgentInfo()
})
// 从本地缓存加载代理信息
const loadAgentInfo = () => {
try {
const agentInfo = uni.getStorageSync('agentInfo')
if (agentInfo) {
level.value = agentInfo.level || 'normal'
ExpiryTime.value = agentInfo.expiryTime || ''
isAgent.value = agentInfo.isAgent || false
agentStatus.value = agentInfo.status || 0
agentID.value = agentInfo.agentID || ''
mobile.value = agentInfo.mobile || ''
isRealName.value = agentInfo.isRealName || false
}
} catch {}
}
const selectedType = ref('vip') // 默认选择VIP
const showPayment = ref(false)
const payData = ref({
product_name: `${selectedType.value.toUpperCase()}代理`,
sell_price: vipConfig.price,
})
const payID = ref('')
// 选择代理类型
function selectType(type) {
selectedType.value = type
// 更新payData中的价格和产品名称
payData.value = {
product_name: `${type === 'vip' ? 'VIP' : 'SVIP'}代理`,
sell_price: type === 'vip' ? vipConfig.price : vipConfig.svipPrice,
}
}
// 申请VIP或SVIP
async function applyVip() {
// 如果是VIP想升级到SVIP提示联系客服
if (isVip.value && selectedType.value === 'svip') {
contactService()
return
}
// 如果是SVIP要降级到VIP提示不能降级
if (isSvip.value && selectedType.value === 'vip') {
toast.error('SVIP会员不能降级到VIP会员')
return
}
try {
const res = await activateAgentMembership({
type: selectedType.value.toUpperCase(),
})
if (res.code === 200 && res.data?.id) {
payID.value = String(res.data.id)
showPayment.value = true
} else {
toast.error(res.msg || '申请失败')
}
} catch {
toast.error('网络请求失败,请稍后重试')
}
}
</script>
<style scoped>
.agent-VIP-apply {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
}
@keyframes shimmer {
0% {
transform: translateX(-100%) skewX(-30deg);
}
100% {
transform: translateX(200%) skewX(-30deg);
}
}
.animate-shimmer {
animation: shimmer 3s infinite;
}
.scale-btn:active {
transform: scale(0.98);
}
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "开通代理会员",
"agent": true,
"auth": true
}</route>

View File

@@ -0,0 +1,421 @@
<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="selectedReportId" :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, getProductConfig } from '@/apis/agent'
// 报告类型选项 - 与 webview 保持一致,支持从后端动态加载
const reportOptions = ref([
{ label: '入职风险', value: 1 },
{ label: '小微企业', value: 2 },
{ label: '家政风险', value: 3 },
{ label: '婚恋风险', value: 4 },
{ label: '贷前风险', value: 5 },
{ label: '租赁风险', value: 6 },
{ label: '个人风险', value: 7 },
{ label: '个人大数据', value: 27 },
])
// 状态管理
const showPicker = ref(false)
const selectedReportId = ref(1)
const selectedReportText = computed(() => {
const opt = reportOptions.value.find((o) => o.value === selectedReportId.value)
return opt ? opt.label : '请选择'
})
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) => {
const value = configData.value[field]
if (value === null || value === undefined) return
const numValue = Number(value)
if (isNaN(numValue)) {
configData.value[field] = null
return
}
const fixedValue = parseFloat(numValue.toFixed(2))
configData.value[field] = fixedValue
if (field === 'price_increase_amount') {
if (priceIncreaseAmountMax.value != null && fixedValue > priceIncreaseAmountMax.value) {
configData.value[field] = priceIncreaseAmountMax.value
uni.showToast({ title: `加价金额最大为${priceIncreaseAmountMax.value}`, icon: 'none' })
increaseError.value = true
setTimeout(() => { increaseError.value = false }, 2000)
} else {
increaseError.value = false
}
validateRange()
}
}
// 价格区间验证
const validateRange = () => {
if (!productConfigData.value || priceIncreaseMax.value == null) return
if (isNaN(configData.value.price_range_from) || isNaN(configData.value.price_range_to)) return
const additional = configData.value.price_increase_amount || 0
const minAllowed = parseFloat(
(Number(productConfigData.value.cost_price) + Number(additional)).toFixed(2)
)
const maxAllowed = productConfigData.value.price_range_max
if (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)
)
return
}
if (configData.value.price_range_to < configData.value.price_range_from) {
uni.showToast({ title: '定价区间最高金额不能低于定价区间最低金额', icon: 'none' })
configData.value.price_range_to = configData.value.price_range_from + priceIncreaseMax.value > maxAllowed
? maxAllowed
: configData.value.price_range_from + priceIncreaseMax.value
rangeError.value = true
closeRangeError()
return
}
const diff = parseFloat(
(configData.value.price_range_to - configData.value.price_range_from).toFixed(2)
)
if (diff > priceIncreaseMax.value) {
uni.showToast({ title: `价格区间最大差值为${priceIncreaseMax.value}`, icon: 'none' })
configData.value.price_range_to = parseFloat(
(Number(configData.value.price_range_from) + Number(priceIncreaseMax.value)).toFixed(2)
)
closeRangeError()
return
}
if (configData.value.price_range_to > maxAllowed) {
configData.value.price_range_to = maxAllowed
uni.showToast({ title: `定价区间最高金额不能超过 ${maxAllowed}`, icon: 'none' })
closeRangeError()
}
rangeError.value = false
}
// 收取比例验证(保留两位小数)
const validateRatio = () => {
let value = configData.value.price_ratio
if (value === null || value === undefined) return
const numValue = Number(value)
if (isNaN(numValue)) {
configData.value.price_ratio = null
ratioError.value = true
return
}
if (priceRatioMax.value != null && numValue > 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) {
configData.value.price_ratio = 0
ratioError.value = true
} else {
configData.value.price_ratio = parseFloat(numValue.toFixed(2))
ratioError.value = false
}
}
// 获取配置
const getConfig = async () => {
try {
const res = await getAgentMembershipUserConfig({ product_id: selectedReportId.value })
if (res.code === 200) {
const respConfigData = res.data.agent_membership_user_config
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 != null ? 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 != null ? res.data.price_ratio * 100 : null
}
} catch {
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()
} else {
uni.showToast({ title: res.msg || '保存失败', icon: 'none' })
}
} catch (error) {
uni.showToast({ title: error?.data?.msg || '保存失败,请稍后重试', icon: 'none' })
}
}
// 最终验证函数
const finalValidation = () => {
if (!configData.value.price_range_from || configData.value.price_range_from <= 0) {
uni.showToast({ title: '定价区间最低金额不能为空', icon: 'none' })
return false
}
if (!configData.value.price_range_to || configData.value.price_range_to <= 0) {
uni.showToast({ title: '定价区间最高金额不能为空', icon: 'none' })
return false
}
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 (priceIncreaseMax.value != null && finalDiff > priceIncreaseMax.value) {
uni.showToast({ title: `价格区间最大差值为${priceIncreaseMax.value}`, icon: 'none' })
return false
}
if (productConfigData.value?.price_range_max != null && 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
const minAllowed = (productConfigData.value?.cost_price ?? 0) + additional
if (configData.value.price_range_from < minAllowed) {
uni.showToast({ title: `定价区间最低金额不能低于成本价${minAllowed}`, icon: 'none' })
return false
}
return true
}
// 选择器确认 - wd-picker 单列时 selectedItems 为选中项对象
const onConfirm = (e) => {
const { value, selectedItems } = e
const item = Array.isArray(selectedItems) ? selectedItems[0] : selectedItems
if (item) {
selectedReportId.value = item.value ?? value
}
// 重置错误状态
rangeError.value = false
ratioError.value = false
increaseError.value = false
getConfig()
}
const closeRangeError = () => {
setTimeout(() => {
rangeError.value = false
}, 2000)
}
// 从后端加载可选报告列表(可选,若失败则使用默认列表)
const loadReportOptions = async () => {
try {
const res = await getProductConfig()
if (res.code !== 200 || !res.data) return
const list = res.data.agent_product_config || res.data.AgentProductConfig || []
if (list.length) {
const productNameMap = {
1: '入职风险',
2: '小微企业',
3: '家政风险',
4: '婚恋风险',
5: '贷前风险',
6: '租赁风险',
7: '个人风险',
27: '个人大数据',
}
reportOptions.value = list.map((p) => ({
label: productNameMap[p.product_id] || `报告${p.product_id}`,
value: p.product_id,
}))
if (!reportOptions.value.find((o) => o.value === selectedReportId.value)) {
selectedReportId.value = reportOptions.value[0].value
}
}
} catch {}
}
onMounted(async () => {
await loadReportOptions()
getConfig()
})
</script>
<style>
.card {
border-radius: 24rpx;
background-color: white;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
padding: 32rpx;
margin-bottom: 32rpx;
}
.space-y-6 {
display: flex;
flex-direction: column;
}
.space-y-6 > view {
margin-bottom: 48rpx;
}
.space-y-6 > view:last-child {
margin-bottom: 0;
}
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "会员代理报告配置",
"agent": true,
"auth": true
}</route>

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

@@ -0,0 +1,73 @@
<script setup>
import { getSiteOrigin } from '@/utils/runtimeEnv.js'
const webviewStyles = ref({
top: `${uni.getSystemInfoSync().statusBarHeight + 44}px`, // 距离顶部的距离
height: `${uni.getSystemInfoSync().windowHeight - uni.getSystemInfoSync().statusBarHeight - 44}px`, // 高度
position: 'absolute', // 绝对定位
dock: 'bottom', // 停靠在底部
bounce: 'vertical', // 垂直方向的回弹效果
})
const BASE_URL = getSiteOrigin()
// 协议路径和标题的映射
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>
<route type="page" lang="json">{
"layout": "page",
"title": "协议",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#e3f0ff"
}
}</route>

93
src/pages/h5open.vue Normal file
View File

@@ -0,0 +1,93 @@
<script setup>
import { getShareTitle } from '@/utils/runtimeEnv.js'
import { buildPromotionH5Url } from '@/utils/promotionH5Url.js'
const webviewStyles = ref({
top: `${uni.getSystemInfoSync().statusBarHeight + 44}px`,
height: `${uni.getSystemInfoSync().windowHeight - uni.getSystemInfoSync().statusBarHeight - 44}px`,
position: 'absolute',
dock: 'bottom',
bounce: 'vertical',
})
const webSrc = ref('')
const shareMode = ref('promote')
const shareId = ref('')
function applyOptions(options) {
const m = options?.m === 'invitation' ? 'invitation' : 'promote'
// onLoad 已解码一次,勿再 decodeURIComponent避免邀请码等含 % 的串被破坏
const rawId = options?.id != null && options.id !== '' ? String(options.id) : ''
shareMode.value = m
shareId.value = rawId
if (!rawId) {
webSrc.value = ''
return
}
webSrc.value = buildPromotionH5Url(m, rawId)
}
onLoad((options) => {
applyOptions(options || {})
})
const shareQuery = computed(() => {
if (!shareId.value)
return ''
return `m=${shareMode.value}&id=${encodeURIComponent(shareId.value)}`
})
const shareMiniPath = computed(() => {
const q = shareQuery.value
return q ? `/pages/h5open?${q}` : '/pages/h5open'
})
onShareAppMessage(() => ({
title: getShareTitle(),
path: shareMiniPath.value,
}))
function handleClickLeft() {
uni.navigateBack({ fail: () => { uni.switchTab({ url: '/pages/index' }) } })
}
</script>
<template>
<view class="h5open-page">
<wd-navbar
title="推广详情"
left-text="返回"
placeholder
left-arrow
safe-area-inset-top
fixed
@click-left="handleClickLeft"
/>
<view v-if="!webSrc" class="h5open-empty">
<text class="text-gray-500 text-sm">链接无效或已过期</text>
</view>
<web-view v-else :webview-styles="webviewStyles" :src="webSrc" />
</view>
</template>
<style scoped>
.h5open-page {
min-height: 100vh;
}
.h5open-empty {
padding: 48px 24px;
text-align: center;
}
</style>
<route type="page" lang="json">{
"layout": "page",
"title": "推广详情",
"auth": false,
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#ffffff"
}
}</route>

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

@@ -0,0 +1,112 @@
<script setup>
import { ref } from 'vue'
import { getAppName } from '@/utils/runtimeEnv.js'
// 引入icon图片
import iconCard1 from '/static/image/icon_2.png'
import iconCard2 from '/static/image/icon_1.png'
const appDisplayName = getAppName()
// 分享给好友、分享到朋友圈(默认标题见 runtime.*.json shareTitle
useShare()
// 与 webview 首页一致的首页轮播图banner_1/2/3
const bannerList = [
'/static/image/banner_1.png',
'/static/image/banner_2.png',
'/static/image/banner_3.png',
]
// 公众号二维码弹窗
const showQrcodePopup = ref(false)
function toggleQrcodePopup() {
showQrcodePopup.value = !showQrcodePopup.value
}
function toInvitation() {
uni.navigateTo({
url: '/pages/invitation',
})
}
function toPromote() {
uni.navigateTo({
url: '/pages/promote',
})
}
</script>
<template>
<view class="box-border min-h-screen bg-[#f5faff]">
<!-- 顶部轮播 webview 相同素材3s 自动轮播 -->
<view class="m-4 overflow-hidden rounded-2xl">
<swiper class="banner-swiper h-[300rpx] w-full" circular autoplay :interval="3000" indicator-dots
indicator-color="rgba(255,255,255,0.45)" indicator-active-color="#ffffff">
<swiper-item v-for="(src, i) in bannerList" :key="i" class="h-full w-full">
<image class="h-full w-full" :src="src" mode="aspectFill" />
</swiper-item>
</swiper>
</view>
<view class="mt-4 px-4">
<view class="text-base font-bold mb-2 text-gray-800">即刻赚佣金</view>
<!-- 卡片1直推报告 -->
<view class="rounded-2xl shadow-xl p-0 mb-4 flex items-center card-gradient-1" @click="toPromote">
<view class="flex-1 flex flex-col justify-center items-start pl-4 py-4">
<view class="text-[20px] font-bold text-blue-700 mb-1">直推报告</view>
<view class="text-gray-700 text-xs mb-4">选择所需报告类型灵活定价一键分享客户客户下单即结算佣金实时到账</view>
<wd-button plain>立即推广</wd-button>
</view>
<image class="w-20 h-20 mr-4 ml-4 my-4" :src="iconCard1" mode="aspectFit" />
</view>
<!-- 卡片2邀请下级代理 -->
<view class="rounded-2xl shadow-xl p-0 flex items-center card-gradient-2" @click="toInvitation">
<view class="flex-1 flex flex-col justify-center items-start pl-4 py-4">
<view class="text-[20px] font-bold text-teal-700 mb-1 ">邀请下级代理</view>
<view class="text-gray-700 text-xs mb-4">邀请好友成为代理好友推广获客客户支付即返佣团队收益轻松到手</view>
<wd-button plain>立即邀请</wd-button>
</view>
<image class="w-20 h-20 mr-4 ml-4 my-4" :src="iconCard2" mode="aspectFit" />
</view>
<!-- 公众号卡片 -->
<view class="rounded-2xl overflow-hidden shadow-xl mt-4 mb-4 relative h-[120px]" @click="toggleQrcodePopup">
<image class="absolute inset-0 w-full h-full" src="/static/image/footbanner.png" mode="aspectFill" />
<view class="absolute inset-0 bg-black/10" />
</view>
</view>
<!-- 二维码弹窗 -->
<view v-if="showQrcodePopup" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
@click="toggleQrcodePopup">
<view class="bg-white rounded-2xl p-6 mx-8 flex flex-col items-center" @click.stop>
<image class="w-48 h-48 mb-4" src="/static/qrcode/fwhqrcode.jpg" mode="aspectFit" show-menu-by-longpress />
<view class="text-gray-600 text-sm text-center mb-2">长按识别保存或者扫码</view>
<view class="text-gray-400 text-xs">关注{{ appDisplayName }}公众号</view>
<view class="mt-4 text-blue-500 text-sm" @click="toggleQrcodePopup">
关闭
</view>
</view>
</view>
</view>
</template>
<style scoped>
.card-gradient-1 {
background: linear-gradient(135deg, #e3f0ff 0%, #fafdff 100%);
box-shadow: 0 6px 24px 0 rgba(60, 120, 255, 0.10), 0 1.5px 4px 0 rgba(60, 120, 255, 0.08);
}
.card-gradient-2 {
background: linear-gradient(135deg, #e6f7fa 0%, #fafdff 100%);
box-shadow: 0 6px 24px 0 rgba(0, 200, 180, 0.10), 0 1.5px 4px 0 rgba(0, 200, 180, 0.08);
}
</style>
<route type="home" lang="json">{
"layout": "home",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#e3f0ff"
}
}</route>

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

@@ -0,0 +1,55 @@
<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 { usePromotionShareHandlers } from '@/composables/usePromotionShareHandlers'
import { getInviteChannelKey } from '@/utils/runtimeEnv.js'
import QRcode from '@/components/QRcode.vue'
usePromotionShareHandlers()
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, getInviteChannelKey())
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,198 @@
<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,
isRealName: res.data.is_real_name,
expiryTime: res.data.expiry_time
})
status.value = res.data.status
}
} catch {}
}
// 提交代理申请
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>

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

@@ -0,0 +1,222 @@
<script setup>
import { getCode, bindMobile, getUserInfo } from '@/api/apis'
import { getAgentInfo } from '@/apis/agent'
import { getAppName } from '@/utils/runtimeEnv.js'
const appDisplayName = getAppName()
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: 'bindMobile',
}).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
}
bindMobile({ 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
})
}
} catch {}
}
const getUser = async () => {
const token = uni.getStorageSync("token")
if (!token) {
return
}
const res = await getUserInfo()
if (res.code === 200) {
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" />
<view class="mt-4 text-3xl font-bold text-gray-800">{{ appDisplayName }}</view>
</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>

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

@@ -0,0 +1,341 @@
<template>
<view class="safe-area-top box-border min-h-screen">
<view class="flex flex-col p-4 space-y-6">
<!-- 用户信息卡片 -->
<view
class="mb-4 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 || getDefaultAvatar()" 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">
<!-- @click.stop="handleVersionClickForTest" -->
<view class="text-lg font-bold text-gray-800" @click="userType === 0 ? toBindPhone() : null"
:class="userType === 0 ? 'cursor-pointer text-blue-600' : ''">
{{ !isLoggedIn ? '点击登录' : (userType === 0 ? '绑定手机号' : maskName(userName)) }}
</view>
<view v-if="isAgent" class="text-sm font-medium" :class="levelGradient.text">
🎖 {{ levelText[level] }}
</view>
</view>
</view>
<VipBanner v-if="isLoggedIn && !isVipOrSvip" />
<!-- 功能菜单 -->
<view class="features-section space-y-3">
<!-- 代理报告配置 + 续费VIP/SVIP 代理 -->
<template v-if="isLoggedIn && isAgent && isVipOrSvip">
<button
class=" flex items-center p-3 rounded-xl bg-gradient-to-r from-purple-200/80 to-pink-200/80 text-purple-700 font-medium shadow-sm transition-all active:shadow-md"
hover-class="opacity-80 scale-98" @click="toVipConfig">
<text class="mr-2"></text> 代理报告配置
</button>
<button
class="flex flex-col items-start p-3 rounded-xl bg-gradient-to-r from-amber-200/80 to-orange-200/80 text-amber-700 font-medium shadow-sm transition-all active:shadow-md"
hover-class="opacity-80 scale-98" @click="toVipRenewal">
<view class="flex items-center">
<text class="mr-2">🔄</text> 续费代理会员
</view>
<view v-if="ExpiryTime" class="text-xs text-gray-500 mt-1 ml-6">有效期至 {{ formatExpiryTime(ExpiryTime) }}
</view>
</button>
</template>
<!-- 升级/开通代理会员登录后显示非代理申请普通代理升级 -->
<button
class=" flex items-center text-gray-600 p-3 rounded-xl bg-white font-medium shadow-sm transition-all active:shadow-md"
hover-class="opacity-80 bg-blue-50" @click="toUserAgreement">
<text class="mr-2">📜</text> 用户协议
</button>
<button
class=" flex items-center text-gray-600 p-3 rounded-xl bg-white font-medium shadow-sm transition-all active:shadow-md"
hover-class="opacity-80 bg-blue-50" @click="toPrivacyPolicy">
<text class="mr-2">🔒</text> 隐私政策
</button>
<button
class=" flex items-center text-gray-600 p-3 rounded-xl bg-white font-medium shadow-sm transition-all active:shadow-md"
hover-class="opacity-80 bg-blue-50" @click="openCustomerService">
<text class="mr-2">💬</text> 联系客服
</button>
</view>
</view>
<!-- 更新进度弹窗 -->
<view v-if="showUpdateProgress" class="update-progress-mask">
<view class="update-progress-dialog">
<view class="update-progress-title">应用更新中</view>
<view class="update-progress-bar-container">
<view class="update-progress-bar" :style="{ width: downloadProgress + '%' }"></view>
</view>
<view class="update-progress-text">{{ downloadProgress }}%</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onBeforeMount } from 'vue'
import { maskName, formatExpiryTime } from '@/utils/format'
import { getMeShareTitle, getCustomerServiceUrl, getCustomerServiceCorpId } from '@/utils/runtimeEnv.js'
// 分享给好友、分享到朋友圈(「我的」页略短标题见 runtime.*.json shareTitleMe
useShare({ title: getMeShareTitle() })
// 用户数据
const userName = ref('')
const userAvatar = ref('')
const isLoggedIn = ref(false)
const userType = ref(null)
// 代理数据
const isAgent = ref(false)
const level = ref('normal')
const ExpiryTime = ref('')
const showUpdateProgress = ref(false)
// 检查平台环境
onBeforeMount(() => {
// 从缓存获取用户信息
const token = uni.getStorageSync('token')
if (token) {
isLoggedIn.value = true
// 从缓存获取用户信息
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
userName.value = userInfo.nickName || userInfo.mobile || '微信用户'
userAvatar.value = userInfo.avatar || ''
userType.value = userInfo.userType
}
// 从缓存获取代理信息
const agentInfo = uni.getStorageSync('agentInfo')
if (agentInfo?.isAgent) {
isAgent.value = agentInfo.isAgent
level.value = agentInfo.level || 'normal'
ExpiryTime.value = agentInfo.expiryTime || ''
}
}
})
const levelNames = {
normal: '普通代理',
'': '普通代理',
VIP: 'VIP代理',
SVIP: 'SVIP代理',
}
const levelText = {
normal: '基础代理特权',
'': '基础代理特权',
VIP: '高级代理特权',
SVIP: '尊享代理特权',
}
const isVipOrSvip = computed(() => {
const l = (level.value || '').toString()
return ['VIP', 'SVIP'].includes(l)
})
const levelGradient = computed(() => ({
border: {
normal: 'bg-green-300',
'': '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',
'': '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',
'': 'text-green-600',
VIP: 'text-amber-600',
SVIP: 'text-purple-600',
}[level.value],
}))
function toHistory() {
uni.navigateTo({
url: '/pages/queryHistory'
})
}
function toUserAgreement() {
uni.navigateTo({
url: '/pages/agreement?type=user'
})
}
function redirectToLogin() {
uni.navigateTo({
url: '/pages/login'
})
}
const toPrivacyPolicy = () => {
uni.navigateTo({
url: '/pages/agreement?type=privacy'
})
}
const openCustomerService = () => {
const url = getCustomerServiceUrl()
const corpId = getCustomerServiceCorpId()
// #ifdef MP-WEIXIN
if (!corpId) {
uni.showToast({ title: '请先配置客服企业ID', icon: 'none' })
return
}
wx.openCustomerServiceChat({
extInfo: { url },
corpId,
success() {},
fail() {
uni.showToast({ title: '打开客服失败', icon: 'none' })
},
})
// #endif
// #ifndef MP-WEIXIN
// #ifdef APP-PLUS
plus.runtime.openURL(url)
// #endif
// #ifdef H5
window.location.href = url
// #endif
// #endif
}
function toVipConfig() {
uni.navigateTo({
url: '/pages/agentVipConfig'
})
}
function toVipRenewal() {
uni.navigateTo({ url: '/pages/agentVipApply' })
}
function toBindPhone() {
uni.navigateTo({
url: '/pages/login'
})
}
const getDefaultAvatar = () => {
if (!isAgent.value) return '/static/image/head_shot.webp'
switch (level.value) {
case 'normal':
case '':
return '/static/image/shot_nonal.png'
case 'VIP':
return '/static/image/shot_vip.png'
case 'SVIP':
return '/static/image/shot_svip.png'
default:
return '/static/image/head_shot.webp'
}
}
</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;
}
.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);
}
/* 更新进度样式 */
.update-progress-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.update-progress-dialog {
width: 80%;
background-color: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.update-progress-title {
font-size: 18px;
font-weight: bold;
text-align: center;
margin-bottom: 16px;
color: #333;
}
.update-progress-bar-container {
height: 10px;
background-color: #f0f0f0;
border-radius: 5px;
overflow: hidden;
margin-bottom: 8px;
}
.update-progress-bar {
height: 100%;
background: linear-gradient(to right, #4299e1, #667eea);
border-radius: 5px;
transition: width 0.3s ease;
}
.update-progress-text {
text-align: center;
font-size: 14px;
color: #666;
}
</style>
<route lang="json">{
"layout": "home",
"style": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#e3f0ff"
}
}</route>

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

@@ -0,0 +1,268 @@
<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 class="card mb-4 !bg-gradient-to-r from-blue-50 to-blue-100 border-l-4 border-blue-400">
<view class="flex items-center justify-between">
<view>
<text class="text-sm text-blue-700 font-medium">不知道如何推广</text>
<text class="text-xs text-blue-600 mt-1 block">查看示例报告了解产品效果</text>
</view>
<text class="text-blue-500 text-sm underline" @click="showGzhQrcodeModal">查看示例</text>
</view>
</view>
<!-- 推广内容 -->
<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" />
<GzhQrcode :visible="showGzhQrcode" @close="showGzhQrcode = false" />
</view>
</template>
<script setup>
import { getProductConfig, generatePromotionLink } from '@/apis/agent'
import { usePromotionShareHandlers } from '@/composables/usePromotionShareHandlers'
import { getAgentTabShareTitle } from '@/utils/runtimeEnv.js'
import PriceInputPopup from '@/components/PriceInputPopup.vue'
import VipBanner from '@/components/VipBanner.vue'
import QRcode from '@/components/QRcode.vue'
import GzhQrcode from '@/components/GzhQrcode.vue'
usePromotionShareHandlers({ defaultTitle: getAgentTabShareTitle() })
// 报告类型
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 showGzhQrcode = 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 {
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)
}
}
// 显示公众号二维码
const showGzhQrcodeModal = () => {
showGzhQrcode.value = true
}
// 页面加载
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,140 @@
<template>
<view class="min-h-screen bg-gray-50">
<!-- 收益列表 -->
<uni-list :loading="loading" :loadmore="loadMoreStatus" @loadmore="onLoadMore">
<EmptyState v-if="!loading && list.length === 0" text="暂无直推报告" />
<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>

View File

@@ -0,0 +1,160 @@
<template>
<view class="min-h-screen bg-gray-50">
<!-- 收益列表 -->
<uni-list :loading="loading" :loadmore="loadMoreStatus" @loadmore="onLoadMore">
<EmptyState v-if="!loading && filteredList.length === 0" text="暂无收益记录" />
<view v-for="(item, index) in filteredList" :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, computed, 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_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 filteredList = computed(() => {
return (list.value || []).filter(item => {
const type = item?.type
return type !== 'descendant_stay_activedescendant' && type !== 'new_active'
})
})
// 类型转中文
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>

View File

@@ -0,0 +1,171 @@
<template>
<view class="min-h-screen bg-gray-50">
<!-- 提现记录列表 -->
<uni-list :loading="loading" :loadmore="loadMoreStatus" @loadmore="onLoadMore">
<EmptyState v-if="!loading && list.length === 0" text="暂无提现记录" />
<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'
import { maskName } from '@/utils/format'
// 状态映射配置
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 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: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/static/image/icon_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
src/static/image/icon_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" class="icon" viewBox="0 0 1024 1024"><path fill="#F26848" d="M491.52 0c-20.48 0-40.96 2.276-59.164 4.551-47.787 4.551-93.298 15.929-136.534 31.858 0 0 354.987 336.782 364.09 350.435V27.307c-11.379-4.551-20.48-6.827-31.859-9.103C584.818 6.827 539.307 0 491.52 0"/><path fill="#5D62D4" d="M853.333 118.329c-13.653-15.929-29.582-29.582-45.51-40.96C769.137 45.51 728.177 20.48 682.666 0c0 0 2.275 530.204 0 546.133L955.733 266.24c-4.55-11.378-11.377-20.48-15.929-31.858-22.755-40.96-52.337-79.644-86.47-116.053"/><path fill="#4694E9" d="M987.591 295.822S650.81 650.81 637.156 659.912h359.537c4.551-11.379 6.827-20.48 9.103-31.859C1017.173 584.818 1024 539.307 1024 491.52c0-20.48-2.276-40.96-4.551-59.164-4.551-47.787-15.929-93.298-31.858-136.534"/><path fill="#69DEF0" d="M757.76 955.733c9.102-4.55 20.48-11.377 29.582-15.929 38.685-22.755 75.094-52.337 106.951-86.47 13.654-13.654 27.307-29.583 38.685-45.512 27.306-38.684 52.338-79.644 70.542-125.155 0 0-484.693 2.275-500.622 0z"/><path fill="#83E45A" d="M364.089 637.156v359.537c11.378 4.551 20.48 6.827 31.858 9.103C439.182 1017.173 484.693 1024 532.48 1024c20.48 0 40.96-2.276 59.164-4.551q71.68-6.827 136.534-34.133c0 2.275-354.987-334.507-364.09-348.16"/><path fill="#BBDC64" d="M341.333 455.111 68.267 735.004c4.55 11.378 11.377 20.48 15.929 31.858 22.755 43.236 52.337 81.92 86.47 116.054 13.654 15.928 29.583 29.582 45.512 40.96 38.684 31.857 81.92 59.164 125.155 77.368 0 0-2.275-530.204 0-546.133"/><path fill="#F3DF68" d="M18.204 395.947C6.827 439.182 0 484.693 0 532.48c0 20.48 2.276 40.96 4.551 59.164q6.827 71.68 34.133 136.534S375.467 373.19 389.12 364.088H27.307c-4.551 11.379-6.827 20.48-9.103 31.859"/><path fill="#F7B964" d="M248.036 84.196c-40.96 22.755-77.37 52.337-111.503 86.47-13.653 13.654-27.306 29.583-40.96 45.512-29.582 38.684-54.613 79.644-72.817 125.155 0 0 507.448-2.275 523.377 0L277.618 68.267c-11.378 4.55-20.48 11.377-29.582 15.929"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" class="icon" viewBox="0 0 1024 1024"><path fill="#FA7268" d="M1024 512c0 282.795-229.205 512-512 512S0 794.795 0 512 229.205 0 512 0s512 229.205 512 512"/><path fill="#FFF" d="M685.227 355.157h-27.375a20.617 20.617 0 0 0-20.685 20.72c0 11.434 9.216 20.65 20.685 20.65h27.375c5.051 0 9.216 4.13 9.216 9.216v305.732a9.216 9.216 0 0 1-9.216 9.216H339.115a9.216 9.216 0 0 1-9.216-9.216V405.743a9.2 9.2 0 0 1 9.216-9.216h27.989a20.63 20.63 0 0 0 20.685-20.685 20.617 20.617 0 0 0-20.685-20.65h-27.99a50.517 50.517 0 0 0-50.585 50.55v305.733c0 27.99 22.562 50.586 50.552 50.586h345.804a50.517 50.517 0 0 0 50.586-50.586V405.743a50.005 50.005 0 0 0-50.244-50.586"/><path fill="#FFF" d="M480.666 500.565a20.924 20.924 0 0 0-29.287 0l-86.835 86.836a20.924 20.924 0 0 0 0 29.286c4.13 4.096 9.216 6.007 14.643 6.007a20.24 20.24 0 0 0 14.643-6.007l72.192-72.226 72.227 72.226a4.95 4.95 0 0 0 1.604 1.263 20.24 20.24 0 0 0 13.005 4.437 18.94 18.94 0 0 0 9.25-2.218 20.5 20.5 0 0 0 5.393-3.823l86.835-86.87a20.855 20.855 0 0 0 0-29.252 20.924 20.924 0 0 0-29.218 0l-72.26 72.226zm17.169-59.801c4.13 4.13 9.216 6.041 14.643 6.041a20.2 20.2 0 0 0 14.643-6.041l87.757-87.825a20.855 20.855 0 0 0 0-29.253 20.89 20.89 0 0 0-29.218 0l-52.839 52.805V231.424a20.617 20.617 0 0 0-20.65-20.65 20.617 20.617 0 0 0-20.685 20.65v144.418l-52.19-51.883a20.96 20.96 0 0 0-29.252 0 20.924 20.924 0 0 0 0 29.321z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" class="icon" viewBox="0 0 1024 1024"><path fill="#0087FF" d="M512 0C229.376 0 0 229.376 0 512s229.376 512 512 512 512-229.376 512-512S794.624 0 512 0"/><path fill="#FFF" d="m575.898 449.536-11.264-11.264c-5.735-5.734-11.264-5.734-16.999-5.734-14.131 0-25.6 11.264-25.6 25.6 0 5.734 2.867 11.264 5.735 16.998 0 0 2.867 2.867 5.734 2.867 2.867 2.867 5.734 2.867 5.734 5.735l2.868 2.867c25.6 25.6 16.998 70.86-8.602 96.256L422.912 693.453c-25.6 25.6-67.994 25.6-93.594 0l-2.867-2.867c-25.6-25.6-25.6-67.994 0-93.594l48.128-48.128c5.735-5.734 11.264-11.264 11.264-22.733 0-14.131-11.264-28.262-28.262-28.262-5.735 2.867-11.264 5.734-14.131 8.601-2.868 0-2.868 2.868-5.735 5.735l-50.995 48.128c-48.128 48.128-48.128 127.59 0 175.718l2.867 2.867c48.128 48.128 127.59 48.128 175.719 0l110.592-110.592c48.128-51.404 50.995-127.795 0-178.79m0 0"/><path fill="#FFF" d="M737.485 287.949c-50.995-50.995-130.458-50.995-175.719-2.867L448.512 395.674c-48.128 48.128-50.995 116.121-2.867 167.116h2.867l11.264 11.264c2.867 2.868 8.602 2.868 11.264 2.868 14.131 0 22.733-11.264 22.733-22.733 0-2.867 0-8.602-2.867-11.264 0-5.735-5.735-11.264-8.602-14.131l-2.867-2.868c-25.6-25.6-16.999-62.259 8.601-90.726L598.63 324.608c25.6-25.6 67.994-25.6 93.594 0l2.867 2.867c25.6 25.6 25.6 67.994 0 93.594l-48.128 48.128c-5.734 5.734-11.264 14.131-11.264 22.733 0 14.13 11.264 28.262 28.263 28.262 5.734 0 11.264 0 14.13-2.867 2.868-2.867 5.735-2.867 5.735-5.735l50.995-48.128c50.79-50.79 50.79-127.385 2.663-175.513m0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" class="icon" viewBox="0 0 1024 1024"><path fill="#15C521" d="M0 512a512 512 0 1 0 1024 0A512 512 0 1 0 0 512"/><path fill="#FFF" d="M665.006 409.477c3.666 0 7.25.062 10.834.246C656.876 305.275 550.154 225.28 421.335 225.28c-142.234 0-257.495 97.485-257.495 217.743 0 70.513 39.629 133.202 101.069 172.995l2.375 1.495-24.903 78.807 93.102-47.923 4.383 1.25a300.9 300.9 0 0 0 98.775 10.567 170.6 170.6 0 0 1-8.192-51.938c0-109.772 105.062-198.799 234.557-198.799m-154.071-76.84c19.968 0 36.147 15.83 36.147 35.41 0 19.517-16.179 35.389-36.147 35.389-19.988 0-36.147-15.872-36.147-35.39 0-19.579 16.179-35.41 36.147-35.41m-179.2 70.799c-19.968 0-36.147-15.872-36.147-35.39 0-19.579 16.179-35.41 36.147-35.41 19.989 0 36.188 15.831 36.188 35.41 0 19.518-16.2 35.39-36.188 35.39"/><path fill="#FFF" d="M450.56 609.935c0 99.349 96.297 179.897 215.081 179.897a253 253 0 0 0 71.68-10.26l77.701 39.628-20.767-65.126 1.987-1.23c51.282-32.87 84.398-84.643 84.398-142.909 0-99.328-96.256-179.855-214.999-179.855-118.784 0-215.081 80.527-215.081 179.855m259.686-61.952c0-16.138 13.497-29.204 30.167-29.204 16.712 0 30.229 13.066 30.229 29.204 0 16.18-13.517 29.266-30.229 29.266-16.67 0-30.167-13.107-30.167-29.266m-149.667 0c0-16.138 13.516-29.204 30.208-29.204 16.69 0 30.208 13.066 30.208 29.204 0 16.18-13.517 29.266-30.208 29.266-16.692 0-30.208-13.107-30.208-29.266"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
src/static/image/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

43
src/store/user.ts Normal file
View File

@@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref<string>(uni.getStorageSync('token') || '')
const userInfo = ref<any>(uni.getStorageSync('userInfo') || null)
const isAgent = ref<boolean>(false)
// 计算属性:是否已登录
const isLoggedIn = computed(() => !!token.value)
// 设置token
function setToken(newToken: string) {
token.value = newToken
uni.setStorageSync('token', newToken)
}
// 设置用户信息
function setUserInfo(info: any) {
userInfo.value = info
isAgent.value = info?.role === 'agent'
uni.setStorageSync('userInfo', info)
}
// 登出
function logout() {
token.value = ''
userInfo.value = null
isAgent.value = false
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
}
return {
token,
userInfo,
isAgent,
isLoggedIn,
setToken,
setUserInfo,
logout
}
})

26
src/theme.json Normal file
View File

@@ -0,0 +1,26 @@
{
"light": {
"bgColor": "#fcfcfc",
"bgColorBottom": "#fcfcfc",
"bgColorTop": "#ff6b00",
"bgTxtStyle": "dark",
"navBgColor": "#ff6b00",
"navTxtStyle": "black",
"tabBgColor": "#fcfcfc",
"tabBorderStyle": "black",
"tabFontColor": "#1f2937",
"tabSelectedColor": "#ff6b00"
},
"dark": {
"bgColor": "#181818",
"bgColorBottom": "#181818",
"bgColorTop": "#ff6b00",
"bgTxtStyle": "light",
"navBgColor": "#ff6b00",
"navTxtStyle": "white",
"tabBgColor": "#181818",
"tabBorderStyle": "white",
"tabFontColor": "#f3f4f6",
"tabSelectedColor": "#ff6b00"
}
}

36
src/uni-pages.d.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by vite-plugin-uni-pages
interface NavigateToOptions {
url: "/pages/index" |
"/pages/agent" |
"/pages/agentVip" |
"/pages/agentVipApply" |
"/pages/agentVipConfig" |
"/pages/agreement" |
"/pages/h5open" |
"/pages/invitation" |
"/pages/invitationAgentApply" |
"/pages/login" |
"/pages/me" |
"/pages/promote" |
"/pages/promoteDetails" |
"/pages/rewardsDetails" |
"/pages/withdrawDetails";
}
interface RedirectToOptions extends NavigateToOptions {}
interface SwitchTabOptions {
url: "/pages/index" | "/pages/agent" | "/pages/me"
}
type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;
declare interface Uni {
navigateTo(options: UniNamespace.NavigateToOptions & NavigateToOptions): void;
redirectTo(options: UniNamespace.RedirectToOptions & RedirectToOptions): void;
switchTab(options: UniNamespace.SwitchTabOptions & SwitchTabOptions): void;
reLaunch(options: UniNamespace.ReLaunchOptions & ReLaunchOptions): void;
}

76
src/uni.scss Normal file
View File

@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333; // 基本色
$uni-text-color-inverse: #fff; // 反色
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #fff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16px;
/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px;
$uni-color-subtitle: #555; // 二级标题颜色
$uni-font-size-subtitle: 18px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;

View File

@@ -0,0 +1,64 @@
import { useHotUpdate } from '@/composables/useHotUpdate'
/**
* 自动更新检查工具
* 用于在应用运行期间定期检查更新
*/
class AutoUpdateChecker {
constructor() {
this.checkInterval = 4 * 60 * 60 * 1000 // 默认4小时检查一次更新
this.timer = null
this.hotUpdate = useHotUpdate()
}
/**
* 启动定时检查
* @param {Number} interval 可选,检查间隔时间(毫秒)
*/
startAutoCheck(interval) {
// 先清除可能存在的定时器
this.stopAutoCheck()
// 设置检查间隔时间
if (interval && typeof interval === 'number') {
this.checkInterval = interval
}
// #ifdef APP-PLUS
// 启动定时器
this.timer = setInterval(() => {
this.hotUpdate.checkUpdate()
}, this.checkInterval)
// #endif
return this
}
/**
* 停止定时检查
*/
stopAutoCheck() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
return this
}
/**
* 立即执行一次检查
*/
checkNow() {
// #ifdef APP-PLUS
this.hotUpdate.checkUpdate()
// #endif
return this
}
}
// 创建单例
const autoUpdateChecker = new AutoUpdateChecker()
export default autoUpdateChecker

174
src/utils/chatCrypto.js Normal file
View File

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

19
src/utils/chatEncrypt.js Normal file
View File

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

85
src/utils/crypto.js Normal file
View File

@@ -0,0 +1,85 @@
import CryptoJS from 'crypto-js'
// AES CBC 加密IV 拼接在密文前面,并进行 Base64 编码// AES CBC 加密IV 拼接在密文前面,并进行 Base64 编码
export function aesEncrypt(plainText, hexKey) {
// 转换密钥为WordArray
const key = CryptoJS.enc.Hex.parse(hexKey)
// 生成一个随机的IV
const iv = generateRandomIV() // 生成 16 字节的随机 IV
// 加密
const encrypted = CryptoJS.AES.encrypt(plainText, key, {
iv,
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC,
})
// 拼接IV和密文IV在前密文在后最后Base64编码
const ivAndCipherText = iv.concat(encrypted.ciphertext)
return CryptoJS.enc.Base64.stringify(ivAndCipherText)
}
// AES CBC 解密IV 在密文前面,并且 Base64 解码
export function aesDecrypt(base64CipherText, hexKey) {
// 转换密钥为WordArray
const key = CryptoJS.enc.Hex.parse(hexKey)
// Base64解码并转换为WordArray
const cipherParams = CryptoJS.enc.Base64.parse(base64CipherText)
// 提取 IV前 16 字节)
const iv = cipherParams.clone().words.slice(0, 4) // 16 字节的 IV 对应 4 个字(每个字 4 字节)
// 提取密文
const cipherText = cipherParams.clone().words.slice(4) // 从第 4 个字开始到最后的密文
// 解密
const decrypted = CryptoJS.AES.decrypt({ ciphertext: CryptoJS.lib.WordArray.create(cipherText) }, key, {
iv: CryptoJS.lib.WordArray.create(iv),
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC,
})
// 返回解密后的明文
return decrypted.toString(CryptoJS.enc.Utf8)
}
function generateRandomIV() {
const iv = []
for (let i = 0; i < 16; i++) { // 16 字节的 IV
iv.push(Math.floor(Math.random() * 256)) // 0-255 的随机数
}
return CryptoJS.enc.Hex.parse(iv.map(b => b.toString(16).padStart(2, '0')).join(''))
}
// // 加密工具函数
// /**
// * AES加密
// * @param {string} plaintext 待加密的文本
// * @param {string} key 密钥
// * @returns {string} 加密后的文本
// */
// export const aesEncrypt = (plaintext, key) => {
// const keyHex = CryptoJS.enc.Utf8.parse(key)
// const encrypted = CryptoJS.AES.encrypt(plaintext, keyHex, {
// mode: CryptoJS.mode.ECB,
// padding: CryptoJS.pad.Pkcs7
// })
// return encrypted.toString()
// }
// /**
// * AES解密
// * @param {string} ciphertext 加密文本
// * @param {string} key 密钥
// * @returns {string} 解密后的文本
// */
// export const aesDecrypt = (ciphertext, key) => {
// const keyHex = CryptoJS.enc.Utf8.parse(key)
// const decrypted = CryptoJS.AES.decrypt(ciphertext, keyHex, {
// mode: CryptoJS.mode.ECB,
// padding: CryptoJS.pad.Pkcs7
// })
// return decrypted.toString(CryptoJS.enc.Utf8)
// }

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