1
106
.commitlintrc.cjs
Normal file
@ -0,0 +1,106 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
const scopes = fs
|
||||
.readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name.replace(/s$/, ''))
|
||||
|
||||
// precomputed scope
|
||||
const scopeComplete = execSync('git status --porcelain || true')
|
||||
.toString()
|
||||
.trim()
|
||||
.split('\n')
|
||||
.find((r) => ~r.indexOf('M src'))
|
||||
?.replace(/(\/)/g, '%%')
|
||||
?.match(/src%%((\w|-)*)/)?.[1]
|
||||
?.replace(/s$/, '')
|
||||
|
||||
module.exports = {
|
||||
ignores: [(commit) => commit.includes('init')],
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'body-leading-blank': [2, 'always'],
|
||||
'footer-leading-blank': [1, 'always'],
|
||||
'header-max-length': [2, 'always', 108],
|
||||
'subject-empty': [2, 'never'],
|
||||
'type-empty': [2, 'never'],
|
||||
'subject-case': [0],
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'style',
|
||||
'docs',
|
||||
'test',
|
||||
'refactor',
|
||||
'build',
|
||||
'ci',
|
||||
'chore',
|
||||
'revert',
|
||||
'wip',
|
||||
'workflow',
|
||||
'types',
|
||||
'release',
|
||||
],
|
||||
],
|
||||
},
|
||||
prompt: {
|
||||
/** @use `pnpm commit :f` */
|
||||
alias: {
|
||||
f: 'docs: fix typos',
|
||||
r: 'docs: update README',
|
||||
s: 'style: update code format',
|
||||
b: 'build: bump dependencies',
|
||||
c: 'chore: update config',
|
||||
},
|
||||
customScopesAlign: !scopeComplete ? 'top' : 'bottom',
|
||||
defaultScope: scopeComplete,
|
||||
scopes: [...scopes, 'mock'],
|
||||
allowEmptyIssuePrefixs: false,
|
||||
allowCustomIssuePrefixs: false,
|
||||
|
||||
// English
|
||||
typesAppend: [
|
||||
{ value: 'wip', name: 'wip: work in process' },
|
||||
{ value: 'workflow', name: 'workflow: workflow improvements' },
|
||||
{ value: 'types', name: 'types: type definition file changes' },
|
||||
],
|
||||
|
||||
// 中英文对照版
|
||||
// messages: {
|
||||
// type: '选择你要提交的类型 :',
|
||||
// scope: '选择一个提交范围 (可选):',
|
||||
// customScope: '请输入自定义的提交范围 :',
|
||||
// subject: '填写简短精炼的变更描述 :\n',
|
||||
// body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
|
||||
// breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
|
||||
// footerPrefixsSelect: '选择关联issue前缀 (可选):',
|
||||
// customFooterPrefixs: '输入自定义issue前缀 :',
|
||||
// footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
|
||||
// confirmCommit: '是否提交或修改commit ?',
|
||||
// },
|
||||
// types: [
|
||||
// { value: 'feat', name: 'feat: 新增功能' },
|
||||
// { value: 'fix', name: 'fix: 修复缺陷' },
|
||||
// { value: 'docs', name: 'docs: 文档变更' },
|
||||
// { value: 'style', name: 'style: 代码格式' },
|
||||
// { value: 'refactor', name: 'refactor: 代码重构' },
|
||||
// { value: 'perf', name: 'perf: 性能优化' },
|
||||
// { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
|
||||
// { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
|
||||
// { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
|
||||
// { value: 'revert', name: 'revert: 回滚 commit' },
|
||||
// { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
|
||||
// { value: 'wip', name: 'wip: 正在开发中' },
|
||||
// { value: 'workflow', name: 'workflow: 工作流程改进' },
|
||||
// { value: 'types', name: 'types: 类型定义文件修改' },
|
||||
// ],
|
||||
// emptyScopesAlias: 'empty: 不填写',
|
||||
// customScopesAlias: 'custom: 自定义',
|
||||
},
|
||||
}
|
||||
68
.cusorrules
Normal file
@ -0,0 +1,68 @@
|
||||
每一次会话请求结束后进行会话总结,
|
||||
无论生成新文件还是修改已有文件都需要做总结,
|
||||
并将总结内容Append写入到Readme.md文件中(说明文件中的内容是累积增加的)。
|
||||
|
||||
总结内容应包括:
|
||||
|
||||
-会话的主要目的
|
||||
|
||||
-完成的主要任务
|
||||
|
||||
-关键决策和解决方案
|
||||
|
||||
-使用的技术栈
|
||||
|
||||
-修改了哪些文件
|
||||
|
||||
-文件的修改内容
|
||||
|
||||
// 项目技术栈和目录结构
|
||||
项目技术栈:
|
||||
- 前端框架:Vue 3.4.21
|
||||
- 构建工具:Vite 5.2.8
|
||||
- 包管理器:pnpm 9.15.4
|
||||
- 状态管理:Pinia 2.0.36
|
||||
- UI组件库:wot-design-uni 1.4.0
|
||||
- 网络请求:luch-request 3.1.1
|
||||
- 工具库:
|
||||
- dayjs 1.11.10
|
||||
- qs 6.5.3
|
||||
- @tanstack/vue-query 5.62.16
|
||||
- 样式处理:
|
||||
- UnoCSS 0.58.9
|
||||
- SASS 1.77.8
|
||||
- PostCSS 8.4.49
|
||||
- 代码规范:
|
||||
- ESLint
|
||||
- Prettier
|
||||
- StyleLint
|
||||
- TypeScript 5.7.2
|
||||
- CommitLint
|
||||
|
||||
项目目录结构:
|
||||
├── src/ # 源代码目录
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── hooks/ # 自定义 hooks
|
||||
│ ├── interceptors/ # 拦截器
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ ├── pages/ # 页面
|
||||
│ ├── pages-sub/ # 分包页面
|
||||
│ ├── service/ # API 服务
|
||||
│ ├── static/ # 静态资源
|
||||
│ ├── store/ # 状态管理
|
||||
│ ├── style/ # 全局样式
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ ├── uni_modules/ # uni-app 模块
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── App.vue # 应用入口组件
|
||||
│ ├── main.ts # 应用入口文件
|
||||
│ ├── manifest.json # 应用配置文件
|
||||
│ ├── pages.json # 页面路由配置
|
||||
│ └── uni.scss # 全局样式变量
|
||||
├── vite-plugins/ # Vite 插件
|
||||
├── scripts/ # 脚本文件
|
||||
├── env/ # 环境配置
|
||||
├── .github/ # GitHub 配置
|
||||
├── .husky/ # Git hooks
|
||||
├── .vscode/ # VS Code 配置
|
||||
└── screenshots/ # 截图目录
|
||||
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*] # 表示所有文件适用
|
||||
charset = utf-8 # 设置文件字符集为 utf-8
|
||||
indent_style = space # 缩进风格(tab | space)
|
||||
indent_size = 2 # 缩进大小
|
||||
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||
trim_trailing_whitespace = true # 去除行首的任意空白字符
|
||||
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||
|
||||
[*.md] # 表示仅 md 文件适用以下规则
|
||||
max_line_length = off # 关闭最大行长度限制
|
||||
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
||||
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
src/uni_modules/
|
||||
101
.eslintrc-auto-import.json
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onAddToFavorites": true,
|
||||
"onBackPress": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onError": true,
|
||||
"onErrorCaptured": true,
|
||||
"onHide": true,
|
||||
"onLaunch": true,
|
||||
"onLoad": true,
|
||||
"onMounted": true,
|
||||
"onNavigationBarButtonTap": true,
|
||||
"onNavigationBarSearchInputChanged": true,
|
||||
"onNavigationBarSearchInputClicked": true,
|
||||
"onNavigationBarSearchInputConfirmed": true,
|
||||
"onNavigationBarSearchInputFocusChanged": true,
|
||||
"onPageNotFound": true,
|
||||
"onPageScroll": true,
|
||||
"onPullDownRefresh": true,
|
||||
"onReachBottom": true,
|
||||
"onReady": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onResize": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onShareAppMessage": true,
|
||||
"onShareTimeline": true,
|
||||
"onShow": true,
|
||||
"onTabItemTap": true,
|
||||
"onThemeChange": true,
|
||||
"onUnhandledRejection": true,
|
||||
"onUnload": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useRequest": true,
|
||||
"useSlots": true,
|
||||
"useUpload": true,
|
||||
"useUpload2": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"DirectiveBinding": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"onWatcherCleanup": true,
|
||||
"useId": true,
|
||||
"useModel": true,
|
||||
"useTemplateRef": true
|
||||
}
|
||||
}
|
||||
97
.eslintrc.cjs
Normal file
@ -0,0 +1,97 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
// eslint-plugin-import 插件, @see https://www.npmjs.com/package/eslint-plugin-import
|
||||
'plugin:import/recommended',
|
||||
// eslint-config-airbnb-base 插件 已经改用 eslint-config-standard 插件
|
||||
'standard',
|
||||
// 1. 接入 prettier 的规则
|
||||
'prettier',
|
||||
'plugin:prettier/recommended',
|
||||
'./.eslintrc-auto-import.json',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
files: ['.eslintrc.{js,cjs}'],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'vue',
|
||||
// 2. 加入 prettier 的 eslint 插件
|
||||
'prettier',
|
||||
// eslint-import-resolver-typescript 插件,@see https://www.npmjs.com/package/eslint-import-resolver-typescript
|
||||
'import',
|
||||
],
|
||||
rules: {
|
||||
// 3. 注意要加上这一句,开启 prettier 自动修复的功能
|
||||
'prettier/prettier': 'error',
|
||||
// turn on errors for missing imports
|
||||
'import/no-unresolved': 'off',
|
||||
// 对后缀的检测,否则 import 一个ts文件也会报错,需要手动添加'.ts', 增加了下面的配置后就不用了
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{ js: 'never', jsx: 'never', ts: 'never', tsx: 'never' },
|
||||
],
|
||||
// 只允许1个默认导出,关闭,否则不能随意export xxx
|
||||
'import/prefer-default-export': ['off'],
|
||||
'no-console': ['off'],
|
||||
// 'no-unused-vars': ['off'],
|
||||
// '@typescript-eslint/no-unused-vars': ['off'],
|
||||
// 解决vite.config.ts报错问题
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'no-shadow': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'no-undef': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
// 避免 `eslint` 对于 `typescript` 函数重载的误报
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
},
|
||||
// eslint-import-resolver-typescript 插件,@see https://www.npmjs.com/package/eslint-import-resolver-typescript
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {},
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
$t: true,
|
||||
uni: true,
|
||||
UniApp: true,
|
||||
wx: true,
|
||||
WechatMiniprogram: true,
|
||||
getCurrentPages: true,
|
||||
UniHelper: true,
|
||||
Page: true,
|
||||
App: true,
|
||||
NodeJS: true,
|
||||
},
|
||||
}
|
||||
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# 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?
|
||||
.hbuilderx
|
||||
|
||||
.stylelintcache
|
||||
.eslintcache
|
||||
|
||||
docs/.vitepress/dist
|
||||
docs/.vitepress/cache
|
||||
|
||||
# lock 文件还是不要了,我主要的版本写死就好了
|
||||
# pnpm-lock.yaml
|
||||
# package-lock.json
|
||||
|
||||
# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
|
||||
# `git rm -r --cached .` 然后提交 commit 即可。
|
||||
|
||||
# git rm -r --cached file1 file2 ## 针对某些文件
|
||||
# git rm -r --cached dir1 dir2 ## 针对某些文件夹
|
||||
# git rm -r --cached . ## 针对所有文件
|
||||
|
||||
# 更新 uni-app 官方版本
|
||||
# npx @dcloudio/uvm@latest
|
||||
6
.npmrc
Normal file
@ -0,0 +1,6 @@
|
||||
# registry = https://registry.npmjs.org
|
||||
registry = https://registry.npmmirror.com
|
||||
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
shamefully-hoist=true
|
||||
12
.prettierignore
Normal file
@ -0,0 +1,12 @@
|
||||
# unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
|
||||
auto-import.d.ts
|
||||
|
||||
# vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
|
||||
uni-pages.d.ts
|
||||
|
||||
# 插件生成的文件
|
||||
src/pages.json
|
||||
src/manifest.json
|
||||
|
||||
# 忽略自动生成文件
|
||||
src/service/app/**
|
||||
19
.prettierrc.cjs
Normal file
@ -0,0 +1,19 @@
|
||||
// @see https://prettier.io/docs/en/options
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
trailingComma: 'all',
|
||||
endOfLine: 'auto',
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
overrides: [
|
||||
{
|
||||
files: '*.json',
|
||||
options: {
|
||||
trailingComma: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
1
.stylelintignore
Normal file
@ -0,0 +1 @@
|
||||
src/uni_modules/
|
||||
58
.stylelintrc.cjs
Normal file
@ -0,0 +1,58 @@
|
||||
// .stylelintrc.cjs
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
// stylelint-config-standard 替换成了更宽松的 stylelint-config-recommended
|
||||
'stylelint-config-recommended',
|
||||
// stylelint-config-standard-scss 替换成了更宽松的 stylelint-config-recommended-scss
|
||||
'stylelint-config-recommended-scss',
|
||||
'stylelint-config-recommended-vue/scss',
|
||||
'stylelint-config-html/vue',
|
||||
'stylelint-config-recess-order',
|
||||
],
|
||||
plugins: ['stylelint-prettier'],
|
||||
overrides: [
|
||||
// 扫描 .vue/html 文件中的<style>标签内的样式
|
||||
{
|
||||
files: ['**/*.{vue,html}'],
|
||||
customSyntax: 'postcss-html',
|
||||
},
|
||||
{
|
||||
files: ['**/*.{css,scss}'],
|
||||
customSyntax: 'postcss-scss',
|
||||
},
|
||||
],
|
||||
// 自定义规则
|
||||
rules: {
|
||||
'prettier/prettier': true,
|
||||
// 允许 global 、export 、v-deep等伪类
|
||||
'selector-pseudo-class-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignorePseudoClasses: ['global', 'export', 'v-deep', 'deep'],
|
||||
},
|
||||
],
|
||||
'unit-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignoreUnits: ['rpx'],
|
||||
},
|
||||
],
|
||||
// 处理小程序page标签不认识的问题
|
||||
'selector-type-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignoreTypes: ['page'],
|
||||
},
|
||||
],
|
||||
'comment-empty-line-before': 'never', // never|always|always-multi-line|never-multi-line
|
||||
'custom-property-empty-line-before': 'never',
|
||||
'no-empty-source': null,
|
||||
'comment-no-empty': null,
|
||||
'no-duplicate-selectors': null,
|
||||
'scss/comment-no-empty': null,
|
||||
'selector-class-pattern': null,
|
||||
'font-family-no-missing-generic-family-keyword': null,
|
||||
},
|
||||
}
|
||||
18
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"stylelint.vscode-stylelint",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"antfu.unocss",
|
||||
"antfu.iconify",
|
||||
"evils.uniapp-vscode",
|
||||
"uni-helper.uni-helper-vscode",
|
||||
"uni-helper.uni-app-schemas-vscode",
|
||||
"uni-helper.uni-highlight-vscode",
|
||||
"uni-helper.uni-ui-snippets-vscode",
|
||||
"uni-helper.uni-app-snippets-vscode",
|
||||
"mrmlnc.vscode-json5",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
62
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
// 默认格式化工具选择prettier
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
// 保存的时候自动格式化
|
||||
"editor.formatOnSave": true,
|
||||
//开启自动修复
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
// 配置stylelint检查的文件类型范围
|
||||
"stylelint.validate": ["css", "scss", "vue", "html"], // 与package.json的scripts对应
|
||||
"stylelint.enable": true,
|
||||
"css.validate": false,
|
||||
"less.validate": false,
|
||||
"scss.validate": false,
|
||||
"[shellscript]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[dotenv]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
// 配置语言的文件关联
|
||||
"files.associations": {
|
||||
"pages.json": "jsonc", // pages.json 可以写注释
|
||||
"manifest.json": "jsonc" // manifest.json 可以写注释
|
||||
},
|
||||
"cSpell.words": [
|
||||
"Aplipay",
|
||||
"climblee",
|
||||
"commitlint",
|
||||
"dcloudio",
|
||||
"iconfont",
|
||||
"qrcode",
|
||||
"refresherrefresh",
|
||||
"scrolltolower",
|
||||
"tabbar",
|
||||
"Toutiao",
|
||||
"unibest",
|
||||
"uvui",
|
||||
"Wechat",
|
||||
"WechatMiniprogram",
|
||||
"Weixin"
|
||||
],
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
|
||||
".eslintrc.cjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,.stylelintrc.*,.eslintrc-auto-import.json,.editorconfig,.commitlint.cjs"
|
||||
}
|
||||
}
|
||||
56
.vscode/vue3.code-snippets
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
// Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"Print unibest Vue3 SFC": {
|
||||
"scope": "vue",
|
||||
"prefix": "v3",
|
||||
"body": [
|
||||
"<route lang=\"json5\" type=\"page\">",
|
||||
"{",
|
||||
" layout: 'default',",
|
||||
" style: {",
|
||||
" navigationBarTitleText: '$1',",
|
||||
" },",
|
||||
"}",
|
||||
"</route>\n",
|
||||
"<template>",
|
||||
" <view class=\"\">$2</view>",
|
||||
"</template>\n",
|
||||
"<script lang=\"ts\" setup>",
|
||||
"//$3",
|
||||
"</script>\n",
|
||||
"<style lang=\"scss\" scoped>",
|
||||
"//$4",
|
||||
"</style>\n",
|
||||
],
|
||||
},
|
||||
"Print unibest style": {
|
||||
"scope": "vue",
|
||||
"prefix": "st",
|
||||
"body": ["<style lang=\"scss\" scoped>", "//", "</style>\n"],
|
||||
},
|
||||
"Print unibest script": {
|
||||
"scope": "vue",
|
||||
"prefix": "sc",
|
||||
"body": ["<script lang=\"ts\" setup>", "//$3", "</script>\n"],
|
||||
},
|
||||
"Print unibest template": {
|
||||
"scope": "vue",
|
||||
"prefix": "te",
|
||||
"body": ["<template>", " <view class=\"\">$1</view>", "</template>\n"],
|
||||
},
|
||||
}
|
||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 菲鸽
|
||||
|
||||
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.
|
||||
BIN
art-exam-web-mobile.rar
Normal file
19
env/.env
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
VITE_APP_TITLE = 'unibest'
|
||||
VITE_APP_PORT = 9000
|
||||
|
||||
VITE_UNI_APPID = 'H57F2ACE4'
|
||||
VITE_WX_APPID = 'wx25fb5794b1c3026f'
|
||||
|
||||
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
||||
VITE_APP_PUBLIC_BASE=/
|
||||
|
||||
VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload'
|
||||
|
||||
# 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
|
||||
# 下面的变量如果没有设置,会默认使用 VITE_SERVER_BASEURL or VITE_UPLOAD_BASEURL
|
||||
|
||||
# h5是否需要配置代理
|
||||
VITE_APP_PROXY=true
|
||||
VITE_APP_PROXY_PREFIX = '/base'
|
||||
VITE_SERVER_BASEURL = 'http://192.168.40.1:8888/'
|
||||
|
||||
6
env/.env.development
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = false
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = true
|
||||
6
env/.env.production
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = true
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = false
|
||||
4
env/.env.test
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = false
|
||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 14 KiB |
26
index.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html build-time="%BUILD_TIME%">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
|
||||
<script>
|
||||
var 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>unibest</title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
134
manifest.config.ts
Normal file
@ -0,0 +1,134 @@
|
||||
// manifest.config.ts
|
||||
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
|
||||
import path from 'node:path'
|
||||
import { loadEnv } from 'vite'
|
||||
|
||||
// 获取环境变量的范例
|
||||
const env = loadEnv(process.env.NODE_ENV!, path.resolve(process.cwd(), 'env'))
|
||||
const {
|
||||
VITE_APP_TITLE,
|
||||
VITE_UNI_APPID,
|
||||
VITE_WX_APPID,
|
||||
VITE_APP_PUBLIC_BASE,
|
||||
VITE_FALLBACK_LOCALE,
|
||||
} = env
|
||||
|
||||
export default defineManifestConfig({
|
||||
name: VITE_APP_TITLE,
|
||||
appid: VITE_UNI_APPID,
|
||||
description: '',
|
||||
versionName: '1.0.0',
|
||||
versionCode: '100',
|
||||
transformPx: false,
|
||||
locale: VITE_FALLBACK_LOCALE, // 'zh-Hans'
|
||||
h5: {
|
||||
router: {
|
||||
base: VITE_APP_PUBLIC_BASE,
|
||||
},
|
||||
},
|
||||
/* 5+App特有相关 */
|
||||
'app-plus': {
|
||||
usingComponents: true,
|
||||
nvueStyleCompiler: 'uni-app',
|
||||
compilerVersion: 3,
|
||||
compatible: {
|
||||
ignoreVersion: true,
|
||||
},
|
||||
splashscreen: {
|
||||
alwaysShowBeforeRender: true,
|
||||
waiting: true,
|
||||
autoclose: true,
|
||||
delay: 0,
|
||||
},
|
||||
/* 模块配置 */
|
||||
modules: {},
|
||||
/* 应用发布信息 */
|
||||
distribute: {
|
||||
/* android打包配置 */
|
||||
android: {
|
||||
minSdkVersion: 30,
|
||||
targetSdkVersion: 30,
|
||||
abiFilters: ['armeabi-v7a', 'arm64-v8a'],
|
||||
permissions: [
|
||||
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
|
||||
'<uses-permission android:name="android.permission.VIBRATE"/>',
|
||||
'<uses-permission android:name="android.permission.READ_LOGS"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
|
||||
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.CAMERA"/>',
|
||||
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
|
||||
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
|
||||
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
|
||||
'<uses-feature android:name="android.hardware.camera"/>',
|
||||
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
|
||||
],
|
||||
},
|
||||
/* ios打包配置 */
|
||||
ios: {},
|
||||
/* SDK配置 */
|
||||
sdkConfigs: {},
|
||||
/* 图标配置 */
|
||||
icons: {
|
||||
android: {
|
||||
hdpi: 'static/app/icons/72x72.png',
|
||||
xhdpi: 'static/app/icons/96x96.png',
|
||||
xxhdpi: 'static/app/icons/144x144.png',
|
||||
xxxhdpi: 'static/app/icons/192x192.png',
|
||||
},
|
||||
ios: {
|
||||
appstore: 'static/app/icons/1024x1024.png',
|
||||
ipad: {
|
||||
app: 'static/app/icons/76x76.png',
|
||||
'app@2x': 'static/app/icons/152x152.png',
|
||||
notification: 'static/app/icons/20x20.png',
|
||||
'notification@2x': 'static/app/icons/40x40.png',
|
||||
'proapp@2x': 'static/app/icons/167x167.png',
|
||||
settings: 'static/app/icons/29x29.png',
|
||||
'settings@2x': 'static/app/icons/58x58.png',
|
||||
spotlight: 'static/app/icons/40x40.png',
|
||||
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||
},
|
||||
iphone: {
|
||||
'app@2x': 'static/app/icons/120x120.png',
|
||||
'app@3x': 'static/app/icons/180x180.png',
|
||||
'notification@2x': 'static/app/icons/40x40.png',
|
||||
'notification@3x': 'static/app/icons/60x60.png',
|
||||
'settings@2x': 'static/app/icons/58x58.png',
|
||||
'settings@3x': 'static/app/icons/87x87.png',
|
||||
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||
'spotlight@3x': 'static/app/icons/120x120.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
quickapp: {},
|
||||
/* 小程序特有相关 */
|
||||
'mp-weixin': {
|
||||
appid: VITE_WX_APPID,
|
||||
setting: {
|
||||
urlCheck: false,
|
||||
},
|
||||
usingComponents: true,
|
||||
// __usePrivacyCheck__: true,
|
||||
},
|
||||
'mp-alipay': {
|
||||
usingComponents: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
'mp-baidu': {
|
||||
usingComponents: true,
|
||||
},
|
||||
'mp-toutiao': {
|
||||
usingComponents: true,
|
||||
},
|
||||
uniStatistics: {
|
||||
enable: false,
|
||||
},
|
||||
vueVersion: '3',
|
||||
})
|
||||
13
openapi-ts-request.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// import type { GenerateServiceProps } from 'openapi-ts-request'
|
||||
|
||||
// export default [
|
||||
// {
|
||||
// schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
|
||||
// serversPath: './src/service/app',
|
||||
// requestLibPath: `import request from '@/utils/request';\n import { CustomRequestOptions } from '@/interceptors/request';`,
|
||||
// requestOptionsType: 'CustomRequestOptions',
|
||||
// isGenReactQuery: true,
|
||||
// reactQueryMode: 'vue',
|
||||
// isGenJavaScript: false,
|
||||
// },
|
||||
// ] as GenerateServiceProps[]
|
||||
175
package.json
Normal file
@ -0,0 +1,175 @@
|
||||
{
|
||||
"name": "wxmp",
|
||||
"type": "commonjs",
|
||||
"version": "2.6.2",
|
||||
"description": "unibest - 最好的 uniapp 开发模板",
|
||||
"author": {
|
||||
"name": "feige996",
|
||||
"zhName": "菲鸽",
|
||||
"email": "1020103647@qq.com",
|
||||
"github": "https://github.com/feige996",
|
||||
"gitee": "https://gitee.com/feige996"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/feige996/unibest",
|
||||
"repository-gitee": "https://gitee.com/feige996/unibest",
|
||||
"repository-deprecated": "https://github.com/codercup/unibest",
|
||||
"bugs": {
|
||||
"url": "https://github.com/feige996/unibest/issues"
|
||||
},
|
||||
"homepage": "https://feige996.github.io/unibest/",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"pnpm": ">=7.30"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"uvm": "npx @dcloudio/uvm@latest",
|
||||
"uvm-rm": "node ./scripts/postupgrade.js",
|
||||
"postuvm": "echo upgrade uni-app success!",
|
||||
"dev:app": "uni -p app",
|
||||
"dev:app-android": "uni -p app-android",
|
||||
"dev:app-ios": "uni -p app-ios",
|
||||
"dev:custom": "uni -p",
|
||||
"dev": "uni",
|
||||
"dev:h5": "uni",
|
||||
"dev:h5:ssr": "uni --ssr",
|
||||
"dev:mp": "uni -p mp-weixin",
|
||||
"dev:mp-alipay": "uni -p mp-alipay",
|
||||
"dev:mp-baidu": "uni -p mp-baidu",
|
||||
"dev:mp-jd": "uni -p mp-jd",
|
||||
"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:mp-xhs": "uni -p mp-xhs",
|
||||
"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:app": "uni build -p app",
|
||||
"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": "uni build",
|
||||
"build:h5:ssr": "uni build --ssr",
|
||||
"build:mp-alipay": "uni build -p mp-alipay",
|
||||
"build:mp": "uni build -p mp-weixin",
|
||||
"build:mp-baidu": "uni build -p mp-baidu",
|
||||
"build:mp-jd": "uni build -p mp-jd",
|
||||
"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:mp-xhs": "uni build -p mp-xhs",
|
||||
"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",
|
||||
"prepare": "git init && husky install",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"cz": "czg",
|
||||
"openapi-ts-request": "openapi-ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{html,vue,ts,cjs,json,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"**/*.{vue,js,ts,jsx,tsx}": [
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"**/*.{vue,css,scss,html}": [
|
||||
"stylelint --fix"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"bin-wrapper": "npm:bin-wrapper-china"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-components": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4020920240930001",
|
||||
"@tanstack/vue-query": "^5.62.16",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"dayjs": "1.11.10",
|
||||
"luch-request": "^3.1.1",
|
||||
"pinia": "2.0.36",
|
||||
"pinia-plugin-persistedstate": "3.2.1",
|
||||
"qs": "6.5.3",
|
||||
"vue": "3.4.21",
|
||||
"wot-design-uni": "^1.4.0",
|
||||
"z-paging": "^2.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18.6.1",
|
||||
"@commitlint/config-conventional": "^18.6.3",
|
||||
"@dcloudio/types": "^3.4.14",
|
||||
"@dcloudio/uni-automator": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4020920240930001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4020920240930001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4020920240930001",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@iconify-json/carbon": "^1.2.4",
|
||||
"@rollup/rollup-darwin-x64": "^4.28.0",
|
||||
"@types/node": "^20.17.9",
|
||||
"@types/wechat-miniprogram": "^3.4.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@uni-helper/uni-types": "1.0.0-alpha.3",
|
||||
"@uni-helper/vite-plugin-uni-layouts": "^0.1.10",
|
||||
"@uni-helper/vite-plugin-uni-manifest": "^0.2.7",
|
||||
"@uni-helper/vite-plugin-uni-pages": "0.2.20",
|
||||
"@uni-helper/vite-plugin-uni-platform": "^0.0.4",
|
||||
"@unocss/preset-legacy-compat": "^0.59.4",
|
||||
"@vue/runtime-core": "^3.5.13",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"commitlint": "^18.6.1",
|
||||
"czg": "^1.9.4",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^15.2.10",
|
||||
"openapi-ts-request": "^1.1.2",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-html": "^1.7.0",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "1.77.8",
|
||||
"stylelint": "^16.11.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recess-order": "^4.6.0",
|
||||
"stylelint-config-recommended": "^14.0.1",
|
||||
"stylelint-config-recommended-scss": "^14.1.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
"stylelint-prettier": "^5.0.2",
|
||||
"terser": "^5.36.0",
|
||||
"typescript": "^5.7.2",
|
||||
"unocss": "^0.58.9",
|
||||
"unocss-applet": "^0.7.8",
|
||||
"unplugin-auto-import": "^0.17.8",
|
||||
"vite": "5.2.8",
|
||||
"vite-plugin-restart": "^0.4.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
||||
}
|
||||
43
pages.config.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
|
||||
|
||||
export default defineUniPages({
|
||||
globalStyle: {
|
||||
navigationStyle: 'default',
|
||||
navigationBarTitleText: 'unibest',
|
||||
navigationBarBackgroundColor: '#f8f8f8',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
easycom: {
|
||||
autoscan: true,
|
||||
custom: {
|
||||
'^wd-(.*)': 'wot-design-uni/components/wd-$1/wd-$1.vue',
|
||||
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
|
||||
'z-paging/components/z-paging$1/z-paging$1.vue',
|
||||
},
|
||||
},
|
||||
tabBar: {
|
||||
color: '#222222',
|
||||
selectedColor: '#000000',
|
||||
backgroundColor: '#ffffff',
|
||||
borderStyle: 'black',
|
||||
height: '50px',
|
||||
fontSize: '10px',
|
||||
iconWidth: '20px',
|
||||
spacing: '3px',
|
||||
list: [
|
||||
{
|
||||
iconPath: 'static/tabbar/edit.svg',
|
||||
selectedIconPath: 'static/tabbar/editHL.svg',
|
||||
pagePath: 'pages/index/index',
|
||||
text: '报名',
|
||||
},
|
||||
{
|
||||
iconPath: 'static/tabbar/document.svg',
|
||||
selectedIconPath: 'static/tabbar/documentHL.svg',
|
||||
pagePath: 'pages/fate/fate',
|
||||
text: '考试',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
13210
pnpm-lock.yaml
generated
Normal file
25
project.config.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"setting": {
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"uglifyFileName": false,
|
||||
"enhance": true,
|
||||
"packNpmRelationList": [],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"useCompilerPlugins": false,
|
||||
"minifyWXML": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx4ec68802a3f48c39",
|
||||
"editorSetting": {}
|
||||
}
|
||||
14
project.private.config.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"libVersion": "3.8.7",
|
||||
"projectname": "xiangqingmp",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
}
|
||||
BIN
screenshots/pay-1.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
screenshots/pay-2.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
36
scripts/postupgrade.js
Normal file
@ -0,0 +1,36 @@
|
||||
// # 执行 `pnpm upgrade` 后会升级 `uniapp` 相关依赖
|
||||
// # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
|
||||
// # 只需要执行下面的命令即可
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { exec } = require('child_process')
|
||||
|
||||
// 定义要执行的命令
|
||||
const dependencies = [
|
||||
'@dcloudio/uni-app-harmony',
|
||||
// TODO: 如果需要某个平台的小程序,请手动删除或注释掉
|
||||
'@dcloudio/uni-mp-alipay',
|
||||
'@dcloudio/uni-mp-baidu',
|
||||
'@dcloudio/uni-mp-jd',
|
||||
'@dcloudio/uni-mp-kuaishou',
|
||||
'@dcloudio/uni-mp-lark',
|
||||
'@dcloudio/uni-mp-qq',
|
||||
'@dcloudio/uni-mp-toutiao',
|
||||
'@dcloudio/uni-mp-xhs',
|
||||
'@dcloudio/uni-quickapp-webview',
|
||||
// i18n模板要注释掉下面的
|
||||
'vue-i18n',
|
||||
]
|
||||
|
||||
// 使用exec执行命令
|
||||
exec(`pnpm un ${dependencies.join(' ')}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
// 如果有错误,打印错误信息
|
||||
console.error(`执行出错: ${error}`)
|
||||
return
|
||||
}
|
||||
// 打印正常输出
|
||||
console.log(`stdout: ${stdout}`)
|
||||
// 如果有错误输出,也打印出来
|
||||
console.error(`stderr: ${stderr}`)
|
||||
})
|
||||
67
src/App.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
|
||||
import { useUserStore } from './store'
|
||||
const userStore = useUserStore()
|
||||
onLaunch(() => {
|
||||
console.log('App Launch')
|
||||
console.log(userStore.userInfo)
|
||||
console.log(uni.getStorageSync('x-token'))
|
||||
if (!uni.getStorageSync('x-token')) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/login/index',
|
||||
})
|
||||
}
|
||||
})
|
||||
onShow(() => {
|
||||
console.log('App Show')
|
||||
})
|
||||
onHide(() => {
|
||||
console.log('App Hide')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* stylelint-disable selector-type-no-unknown */
|
||||
button::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
swiper,
|
||||
scroll-view {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// 单行省略,优先使用 unocss: text-ellipsis
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 两行省略
|
||||
.ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
// 三行省略
|
||||
.ellipsis-3 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
0
src/components/.gitkeep
Normal file
31
src/env.d.ts
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** 网站标题,应用名称 */
|
||||
readonly VITE_APP_TITLE: string
|
||||
/** 服务端口号 */
|
||||
readonly VITE_SERVER_PORT: string
|
||||
/** 后台接口地址 */
|
||||
readonly VITE_SERVER_BASEURL: string
|
||||
/** H5是否需要代理 */
|
||||
readonly VITE_APP_PROXY: 'true' | 'false'
|
||||
/** H5是否需要代理,需要的话有个前缀 */
|
||||
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
|
||||
/** 上传图片地址 */
|
||||
readonly VITE_UPLOAD_BASEURL: string
|
||||
/** 是否清除console */
|
||||
readonly VITE_DELETE_CONSOLE: string
|
||||
// 更多环境变量...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
0
src/hooks/.gitkeep
Normal file
44
src/hooks/useRequest.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { UnwrapRef } from 'vue'
|
||||
|
||||
type IUseRequestOptions<T> = {
|
||||
/** 是否立即执行 */
|
||||
immediate?: boolean
|
||||
/** 初始化数据 */
|
||||
initialData?: T
|
||||
}
|
||||
|
||||
/**
|
||||
* useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
|
||||
* @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
|
||||
* @param options 包含请求选项的对象 {immediate, initialData}。
|
||||
* @param options.immediate 是否立即执行请求,默认为false。
|
||||
* @param options.initialData 初始化数据,默认为undefined。
|
||||
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
|
||||
*/
|
||||
export default function useRequest<T>(
|
||||
func: () => Promise<IResData<T>>,
|
||||
options: IUseRequestOptions<T> = { immediate: false },
|
||||
) {
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const data = ref<T>(options.initialData)
|
||||
const run = async () => {
|
||||
loading.value = true
|
||||
return func()
|
||||
.then((res) => {
|
||||
data.value = res.data as UnwrapRef<T>
|
||||
error.value = false
|
||||
return data.value
|
||||
})
|
||||
.catch((err) => {
|
||||
error.value = err
|
||||
throw err
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
options.immediate && run()
|
||||
return { loading, error, data, run }
|
||||
}
|
||||
69
src/hooks/useUpload.ts
Normal file
@ -0,0 +1,69 @@
|
||||
// TODO: 别忘加更改环境变量的 VITE_UPLOAD_BASEURL 地址。
|
||||
import { getEnvBaseUploadUrl } from '@/utils'
|
||||
|
||||
const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}`
|
||||
|
||||
/**
|
||||
* useUpload 是一个定制化的请求钩子,用于处理上传图片。
|
||||
* @param formData 额外传递给后台的数据,如{name: '菲鸽'}。
|
||||
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
|
||||
*/
|
||||
export default function useUpload<T = string>(formData: Record<string, any> = {}) {
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const data = ref<T>()
|
||||
const run = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
|
||||
// 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
|
||||
uni.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
success: (res) => {
|
||||
loading.value = true
|
||||
const tempFilePath = res.tempFiles[0].tempFilePath
|
||||
uploadFile<T>({ tempFilePath, formData, data, error, loading })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('uni.chooseMedia err->', err)
|
||||
error.value = true
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
loading.value = true
|
||||
const tempFilePath = res.tempFilePaths[0]
|
||||
uploadFile<T>({ tempFilePath, formData, data, error, loading })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('uni.chooseImage err->', err)
|
||||
error.value = true
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
return { loading, error, data, run }
|
||||
}
|
||||
|
||||
function uploadFile<T>({ tempFilePath, formData, data, error, loading }) {
|
||||
uni.uploadFile({
|
||||
url: VITE_UPLOAD_BASEURL,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
formData,
|
||||
success: (uploadFileRes) => {
|
||||
data.value = uploadFileRes.data as T
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('uni.uploadFile err->', err)
|
||||
error.value = true
|
||||
},
|
||||
complete: () => {
|
||||
loading.value = false
|
||||
},
|
||||
})
|
||||
}
|
||||
2
src/interceptors/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { routeInterceptor } from './route'
|
||||
export { prototypeInterceptor } from './prototype'
|
||||
13
src/interceptors/prototype.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const prototypeInterceptor = {
|
||||
install() {
|
||||
// 解决低版本手机不识别 array.at() 导致运行报错的问题
|
||||
if (typeof Array.prototype.at !== 'function') {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Array.prototype.at = function (index: number) {
|
||||
if (index < 0) return this[this.length + index]
|
||||
if (index >= this.length) return undefined
|
||||
return this[index]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
54
src/interceptors/route.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* by 菲鸽 on 2024-03-06
|
||||
* 路由拦截,通常也是登录拦截
|
||||
* 可以设置路由白名单,或者黑名单,看业务需要选哪一个
|
||||
* 我这里应为大部分都可以随便进入,所以使用黑名单
|
||||
*/
|
||||
import { useUserStore } from '@/store'
|
||||
import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
|
||||
|
||||
// TODO Check
|
||||
const loginRoute = '/pages/login/index'
|
||||
|
||||
const isLogined = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.isLogined
|
||||
}
|
||||
|
||||
const isDev = import.meta.env.DEV
|
||||
|
||||
// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
|
||||
const navigateToInterceptor = {
|
||||
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
|
||||
invoke({ url }: { url: string }) {
|
||||
// console.log(url) // /pages/route-interceptor/index?name=feige&age=30
|
||||
const path = url.split('?')[0]
|
||||
let needLoginPages: string[] = []
|
||||
// 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好
|
||||
if (isDev) {
|
||||
needLoginPages = getNeedLoginPages()
|
||||
} else {
|
||||
needLoginPages = _needLoginPages
|
||||
}
|
||||
const isNeedLogin = needLoginPages.includes(path)
|
||||
if (!isNeedLogin) {
|
||||
return true
|
||||
}
|
||||
const hasLogin = isLogined()
|
||||
if (hasLogin) {
|
||||
return true
|
||||
}
|
||||
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
|
||||
uni.navigateTo({ url: redirectRoute })
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const routeInterceptor = {
|
||||
install() {
|
||||
uni.addInterceptor('navigateTo', navigateToInterceptor)
|
||||
uni.addInterceptor('reLaunch', navigateToInterceptor)
|
||||
uni.addInterceptor('redirectTo', navigateToInterceptor)
|
||||
uni.addInterceptor('switchTab', navigateToInterceptor)
|
||||
},
|
||||
}
|
||||
17
src/layouts/default.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<wd-config-provider :themeVars="themeVars">
|
||||
<slot />
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ConfigProviderThemeVars } from 'wot-design-uni'
|
||||
|
||||
const themeVars: ConfigProviderThemeVars = {
|
||||
// colorTheme: 'red',
|
||||
// buttonPrimaryBgColor: '#07c160',
|
||||
// buttonPrimaryColor: '#07c160',
|
||||
}
|
||||
</script>
|
||||
17
src/layouts/demo.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<wd-config-provider :themeVars="themeVars">
|
||||
<slot />
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ConfigProviderThemeVars } from 'wot-design-uni'
|
||||
|
||||
const themeVars: ConfigProviderThemeVars = {
|
||||
// colorTheme: 'red',
|
||||
// buttonPrimaryBgColor: '#07c160',
|
||||
// buttonPrimaryColor: '#07c160',
|
||||
}
|
||||
</script>
|
||||
20
src/main.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import '@/style/index.scss'
|
||||
import { VueQueryPlugin } from '@tanstack/vue-query'
|
||||
import 'virtual:uno.css'
|
||||
import { createSSRApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import { prototypeInterceptor, routeInterceptor } from './interceptors'
|
||||
import store from './store'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(store)
|
||||
app.use(routeInterceptor)
|
||||
app.use(prototypeInterceptor)
|
||||
app.use(VueQueryPlugin)
|
||||
|
||||
return {
|
||||
app,
|
||||
}
|
||||
}
|
||||
111
src/manifest.json
Normal file
@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "unibest",
|
||||
"appid": "H57F2ACE4",
|
||||
"description": "",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"modules": {},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
],
|
||||
"minSdkVersion": 30,
|
||||
"targetSdkVersion": 30,
|
||||
"abiFilters": [
|
||||
"armeabi-v7a",
|
||||
"arm64-v8a"
|
||||
]
|
||||
},
|
||||
"ios": {},
|
||||
"sdkConfigs": {},
|
||||
"icons": {
|
||||
"android": {
|
||||
"hdpi": "static/app/icons/72x72.png",
|
||||
"xhdpi": "static/app/icons/96x96.png",
|
||||
"xxhdpi": "static/app/icons/144x144.png",
|
||||
"xxxhdpi": "static/app/icons/192x192.png"
|
||||
},
|
||||
"ios": {
|
||||
"appstore": "static/app/icons/1024x1024.png",
|
||||
"ipad": {
|
||||
"app": "static/app/icons/76x76.png",
|
||||
"app@2x": "static/app/icons/152x152.png",
|
||||
"notification": "static/app/icons/20x20.png",
|
||||
"notification@2x": "static/app/icons/40x40.png",
|
||||
"proapp@2x": "static/app/icons/167x167.png",
|
||||
"settings": "static/app/icons/29x29.png",
|
||||
"settings@2x": "static/app/icons/58x58.png",
|
||||
"spotlight": "static/app/icons/40x40.png",
|
||||
"spotlight@2x": "static/app/icons/80x80.png"
|
||||
},
|
||||
"iphone": {
|
||||
"app@2x": "static/app/icons/120x120.png",
|
||||
"app@3x": "static/app/icons/180x180.png",
|
||||
"notification@2x": "static/app/icons/40x40.png",
|
||||
"notification@3x": "static/app/icons/60x60.png",
|
||||
"settings@2x": "static/app/icons/58x58.png",
|
||||
"settings@3x": "static/app/icons/87x87.png",
|
||||
"spotlight@2x": "static/app/icons/80x80.png",
|
||||
"spotlight@3x": "static/app/icons/120x120.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatible": {
|
||||
"ignoreVersion": true
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "wx25fb5794b1c3026f",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
},
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true,
|
||||
"styleIsolation": "shared"
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"vueVersion": "3",
|
||||
"h5": {
|
||||
"router": {
|
||||
"base": "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/pages-sub/demo/index.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
style: { navigationBarTitleText: '分包页面 标题' },
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="text-center">
|
||||
<view class="m-8">http://localhost:9000/#/pages-sub/demo/index</view>
|
||||
<view class="text-green-500">分包页面demo</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// code here
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
||||
209
src/pages.json
Normal file
@ -0,0 +1,209 @@
|
||||
{
|
||||
"globalStyle": {
|
||||
"navigationStyle": "default",
|
||||
"navigationBarTitleText": "unibest",
|
||||
"navigationBarBackgroundColor": "#f8f8f8",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue",
|
||||
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
|
||||
}
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#222222",
|
||||
"selectedColor": "#000000",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"height": "50px",
|
||||
"fontSize": "10px",
|
||||
"iconWidth": "20px",
|
||||
"spacing": "3px",
|
||||
"list": [
|
||||
{
|
||||
"iconPath": "static/tabbar/edit.svg",
|
||||
"selectedIconPath": "static/tabbar/editHL.svg",
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "报名"
|
||||
},
|
||||
{
|
||||
"iconPath": "static/tabbar/document.svg",
|
||||
"selectedIconPath": "static/tabbar/documentHL.svg",
|
||||
"pagePath": "pages/fate/fate",
|
||||
"text": "考试"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"type": "home",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/detail",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/agreement/privacy",
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "隐私政策"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/agreement/user",
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户协议"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/detail/index",
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/fate/fate",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "红娘"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/activities",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的活动"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/complaint",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发起投诉"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/feedback",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "意见反馈"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/invite",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "邀请好友"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/inviter",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的邀请人"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/matches",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的配对"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/my",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/profile",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的资料"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/recharge",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "充值记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/refund",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "退款记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/spend-detail",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消费记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/spend",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消费记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/subordinate",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/my/user-info",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户信息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/recommend/index",
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "推荐列表"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": []
|
||||
}
|
||||
173
src/pages/activity/detail.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="sticky top-0 z-10 bg-white shadow-sm">
|
||||
<view class="flex items-center justify-between px-4 py-3">
|
||||
<view class="flex items-center space-x-2">
|
||||
<text class="iconfont icon-arrow-left text-xl" @tap="goBack"></text>
|
||||
<text class="text-lg font-bold">活动详情</text>
|
||||
</view>
|
||||
<view class="flex items-center space-x-4">
|
||||
<text class="iconfont icon-share text-xl"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 活动详情内容 -->
|
||||
<scroll-view class="h-screen" scroll-y>
|
||||
<!-- 活动封面 -->
|
||||
<view class="relative">
|
||||
<image
|
||||
:src="activityDetail.eventAvatar"
|
||||
mode="aspectFill"
|
||||
class="w-full h-80 object-cover"
|
||||
/>
|
||||
<view class="absolute top-3 right-3 px-3 py-1 bg-white/90 rounded-full backdrop-blur-sm">
|
||||
<text class="text-sm text-primary font-medium">¥{{ activityDetail.eventPrice }}</text>
|
||||
</view>
|
||||
<view
|
||||
:class="[
|
||||
'absolute top-3 left-3 px-3 py-1 rounded-full text-sm font-medium',
|
||||
isOngoing ? 'bg-green-500/90 text-white' : 'bg-gray-500/90 text-white',
|
||||
]"
|
||||
>
|
||||
{{ isOngoing ? '报名中' : '已结束' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 活动信息 -->
|
||||
<view class="p-4 bg-white">
|
||||
<text class="block text-xl font-bold mb-2">{{ activityDetail.eventTitle }}</text>
|
||||
<view class="flex items-center text-sm text-gray-500 mb-2">
|
||||
<text class="iconfont icon-time mr-1"></text>
|
||||
<text>截止时间: {{ activityDetail.endTime }}</text>
|
||||
</view>
|
||||
<view class="flex items-center text-sm text-gray-500 mb-4">
|
||||
<text class="iconfont icon-location mr-1"></text>
|
||||
<text>{{ activityDetail.eventPlace }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<text class="text-sm text-gray-500">
|
||||
已有 {{ activityDetail.eventNumber }}/{{ activityDetail.eventLimitNumber }} 人报名
|
||||
</text>
|
||||
</view>
|
||||
<button
|
||||
class="px-8 py-2.5 text-white text-sm rounded-full flex items-center justify-center shadow-md transition-all duration-300"
|
||||
:class="{
|
||||
'bg-gradient-to-r from-pink-500 to-purple-500 hover:from-pink-600 hover:to-purple-600':
|
||||
isOngoing,
|
||||
'bg-gray-400 cursor-not-allowed': !isOngoing,
|
||||
}"
|
||||
:disabled="!isOngoing"
|
||||
@tap="handleSignUp"
|
||||
>
|
||||
<text class="iconfont icon-add mr-1" v-if="isOngoing"></text>
|
||||
{{ isOngoing ? '立即报名' : '已结束' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 活动详情 -->
|
||||
<view class="p-4 mt-4 bg-white">
|
||||
<text class="block text-lg font-bold mb-4">活动详情</text>
|
||||
<rich-text :nodes="activityDetail.eventContent"></rich-text>
|
||||
</view>
|
||||
|
||||
<!-- 活动信息补充 -->
|
||||
<view class="p-4 mt-4 bg-white">
|
||||
<text class="block text-lg font-bold mb-4">活动信息</text>
|
||||
<view class="space-y-2">
|
||||
<view class="flex justify-between">
|
||||
<text class="text-gray-500">发起时间</text>
|
||||
<text>{{ activityDetail.createTime }}</text>
|
||||
</view>
|
||||
<view class="flex justify-between">
|
||||
<text class="text-gray-500">有效天数</text>
|
||||
<text>{{ activityDetail.effectiveTime }}天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import type { EventItem } from '@/service/event/type'
|
||||
import { joinEventAPI } from '@/service/event'
|
||||
|
||||
onLoad(() => {
|
||||
// 页面加载时执行的逻辑
|
||||
const instance: any = getCurrentInstance().proxy
|
||||
const eventChannel = instance.getOpenerEventChannel()
|
||||
eventChannel.on('item', function (data) {
|
||||
const { item } = data
|
||||
activityDetail.value = item
|
||||
})
|
||||
})
|
||||
|
||||
// 活动详情数据
|
||||
const activityDetail = ref<EventItem>({
|
||||
id: '',
|
||||
eventTitle: '',
|
||||
eventContent: '',
|
||||
eventAvatar: '',
|
||||
eventPrice: '',
|
||||
eventPlace: '',
|
||||
createTime: '',
|
||||
endTime: '',
|
||||
effectiveTime: '',
|
||||
eventNumber: '',
|
||||
eventLimitNumber: '',
|
||||
})
|
||||
|
||||
// 计算活动是否进行中
|
||||
const isOngoing = computed(() => {
|
||||
if (!activityDetail.value.endTime) return false
|
||||
return new Date(activityDetail.value.endTime) > new Date()
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const isLoading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
|
||||
// 报名活动
|
||||
const handleSignUp = async () => {
|
||||
try {
|
||||
uni.showModal({
|
||||
title: '报名确认',
|
||||
content: '确定要报名参加此活动吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const result = await joinEventAPI(activityDetail.value.id)
|
||||
if (result.code === 500) {
|
||||
uni.showModal({
|
||||
title: '报名失败',
|
||||
content: result.message || '报名失败,请稍后重试',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: '报名成功',
|
||||
icon: 'success',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
uni.showModal({
|
||||
title: '报名失败',
|
||||
content: '系统异常,请稍后重试',
|
||||
showCancel: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
236
src/pages/activity/index.vue
Normal file
@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="sticky top-0 z-10 bg-white shadow-sm">
|
||||
<view class="flex items-center justify-between px-4 py-3">
|
||||
<view class="flex items-center space-x-2">
|
||||
<text class="iconfont icon-arrow-left text-xl" @tap="goBack"></text>
|
||||
<text class="text-2xl font-bold">相亲活动</text>
|
||||
</view>
|
||||
<view class="flex items-center space-x-4">
|
||||
<text class="iconfont icon-search text-xl"></text>
|
||||
<text class="iconfont icon-filter text-xl"></text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 筛选器 -->
|
||||
<view class="flex px-4 pb-3 space-x-4">
|
||||
<view
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full text-sm transition-all duration-300',
|
||||
currentFilter === '1' ? 'bg-primary/90 shadow-lg shadow-primary/20' : 'bg-gray-100',
|
||||
]"
|
||||
@tap="changeFilter('1')"
|
||||
>
|
||||
<text :class="currentFilter === '1' ? 'text-gray-600' : 'text-gray-600'">全部</text>
|
||||
</view>
|
||||
<view
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full text-sm transition-all duration-300',
|
||||
currentFilter === '2' ? 'bg-primary/90 shadow-lg shadow-primary/20' : 'bg-gray-100',
|
||||
]"
|
||||
@tap="changeFilter('2')"
|
||||
>
|
||||
<text :class="currentFilter === '2' ? 'text-gray-600' : 'text-gray-600'">报名中</text>
|
||||
</view>
|
||||
<view
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full text-sm transition-all duration-300',
|
||||
currentFilter === '3' ? 'bg-primary/90 shadow-lg shadow-primary/20' : 'bg-gray-100',
|
||||
]"
|
||||
@tap="changeFilter('3')"
|
||||
>
|
||||
<text :class="currentFilter === '3' ? 'text-gray-600' : 'text-gray-600'">已结束</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<view class="px-4 pt-2 pb-8 overflow-hidden">
|
||||
<scroll-view
|
||||
class="space-y-4"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore"
|
||||
refresher-enabled
|
||||
:refresher-triggered="loading"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="flex justify-center items-center py-8">
|
||||
<text class="text-gray-500">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view v-else-if="error" class="flex justify-center items-center py-8">
|
||||
<text class="text-red-500">{{ error }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="!activities.length" class="flex justify-center items-center py-8">
|
||||
<text class="text-gray-500">暂无活动</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="space-y-4">
|
||||
<view
|
||||
class="bg-white rounded-2xl shadow-lg overflow-hidden transform transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]"
|
||||
v-for="item in activities"
|
||||
:key="item.id"
|
||||
@tap="viewActivity(item)"
|
||||
>
|
||||
<view class="relative">
|
||||
<image :src="item.eventAvatar" mode="aspectFill" class="w-full h-40 object-cover" />
|
||||
<view
|
||||
class="absolute top-3 right-3 px-3 py-1 bg-white/90 rounded-full backdrop-blur-sm"
|
||||
>
|
||||
<text class="text-sm text-primary font-medium">¥{{ item.eventPrice }}</text>
|
||||
</view>
|
||||
<view
|
||||
:class="[
|
||||
'absolute top-3 left-3 px-3 py-1 rounded-full text-sm font-medium',
|
||||
isOngoing(item) ? 'bg-green-500/90 text-white' : 'bg-gray-500/90 text-white',
|
||||
]"
|
||||
>
|
||||
{{ isOngoing(item) ? '报名中' : '已结束' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="p-4">
|
||||
<text class="block text-lg font-bold mb-2">{{ item.eventTitle }}</text>
|
||||
<view class="flex items-center text-sm text-gray-500">
|
||||
<text class="iconfont icon-time mr-1"></text>
|
||||
<text>截止时间: {{ formatDate(item.endTime) }}</text>
|
||||
</view>
|
||||
<view class="mt-3 flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<view class="flex -space-x-2">
|
||||
<image
|
||||
v-for="(avatar, index) in getRandomAvatars(3)"
|
||||
:key="index"
|
||||
:src="avatar"
|
||||
class="w-8 h-8 rounded-full border-2 border-white"
|
||||
/>
|
||||
</view>
|
||||
<text class="ml-2 text-sm text-gray-500">已有 {{ item.eventNumber }} 人报名</text>
|
||||
</view>
|
||||
<view
|
||||
class="px-4 py-2 bg-primary text-white text-sm rounded-full"
|
||||
@tap.stop="handleJoin(item)"
|
||||
>
|
||||
立即报名
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载完成提示 -->
|
||||
<view
|
||||
v-if="!loading && activities.length > 0 && activities.length >= total"
|
||||
class="py-4 text-center text-gray-500"
|
||||
>
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { joinEventAPI } from '@/service/event'
|
||||
import type { EventItem } from '@/service/event/type'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const activities = ref<EventItem[]>([])
|
||||
const currentFilter = ref<'1' | '2' | '3'>('1') // 1-全部 2-报名中 3-已结束
|
||||
const total = ref(0)
|
||||
|
||||
// 获取活动列表
|
||||
const fetchActivities = async (isRefresh = false) => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
} catch (err) {
|
||||
error.value = '网络错误,请稍后重试'
|
||||
console.error('获取活动列表失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 判断活动是否进行中
|
||||
const isOngoing = (item: EventItem) => {
|
||||
const now = new Date().getTime()
|
||||
const endTime = new Date(item.endTime).getTime()
|
||||
return endTime > now
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (activities.value.length >= total.value) return
|
||||
fetchActivities()
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
fetchActivities(true)
|
||||
}
|
||||
|
||||
// 切换筛选
|
||||
const changeFilter = (filter: '1' | '2' | '3') => {
|
||||
currentFilter.value = filter
|
||||
fetchActivities(true)
|
||||
}
|
||||
|
||||
// 查看活动详情
|
||||
const viewActivity = (item: EventItem) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/activity/detail?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 处理报名
|
||||
const handleJoin = async (item: EventItem) => {
|
||||
try {
|
||||
const res = await joinEventAPI(item.id)
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '报名成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// 刷新列表
|
||||
fetchActivities(true)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '报名失败',
|
||||
icon: 'error',
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({
|
||||
title: '网络错误,请稍后重试',
|
||||
icon: 'error',
|
||||
})
|
||||
console.error('报名失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 获取随机头像(实际项目中应该使用真实数据)
|
||||
const getRandomAvatars = (count: number) => {
|
||||
return Array(count).fill(
|
||||
'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg',
|
||||
)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchActivities()
|
||||
})
|
||||
</script>
|
||||
103
src/pages/agreement/privacy.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
style: {
|
||||
navigationBarTitleText: '隐私政策',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-white p-4">
|
||||
<view class="prose prose-sm max-w-none">
|
||||
<view class="text-center mb-8">
|
||||
<text class="text-xl font-bold">隐私政策</text>
|
||||
<text class="block text-sm text-gray-500 mt-2">最后更新日期:2024年3月</text>
|
||||
</view>
|
||||
|
||||
<view class="space-y-6">
|
||||
<view>
|
||||
<text class="font-bold block mb-2">1. 引言</text>
|
||||
<text class="text-gray-600 block">
|
||||
我们非常重视您的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的个人信息。请您在使用我们的服务前,仔细阅读并了解本隐私政策的全部内容。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">2. 信息收集</text>
|
||||
<text class="text-gray-600 block">
|
||||
2.1 我们收集的信息包括: - 基本信息:昵称、头像、性别、年龄等 -
|
||||
位置信息:您的位置信息(需要您授权) - 设备信息:设备型号、操作系统版本、设备设置等 -
|
||||
日志信息:使用时间、使用时长、操作记录等 2.2 我们通过以下方式收集信息: -
|
||||
您主动提供的信息 - 在您使用服务时产生的信息 - 经您授权从第三方获取的信息
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">3. 信息使用</text>
|
||||
<text class="text-gray-600 block">
|
||||
3.1 我们使用收集的信息用于: - 提供、维护和改进我们的服务 - 开发新的服务和功能 -
|
||||
了解用户如何使用我们的服务 - 防止欺诈和滥用 - 向您推送可能感兴趣的内容
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">4. 信息共享</text>
|
||||
<text class="text-gray-600 block">
|
||||
4.1 我们不会向第三方出售您的个人信息。 4.2 在以下情况下,我们可能会共享您的信息: -
|
||||
获得您的明确同意 - 法律法规要求 - 维护我们的合法权益 - 与我们的关联公司共享
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">5. 信息安全</text>
|
||||
<text class="text-gray-600 block">
|
||||
5.1 我们采取各种安全措施保护您的个人信息: - 数据加密存储 - 访问控制机制 - 安全审计机制
|
||||
- 应急响应机制
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">6. 信息存储</text>
|
||||
<text class="text-gray-600 block">
|
||||
6.1 我们会将您的信息存储在中国境内的服务器上。 6.2
|
||||
我们会根据法律法规的要求,在必要的时间内保存您的信息。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">7. 您的权利</text>
|
||||
<text class="text-gray-600 block">
|
||||
您对您的个人信息享有以下权利: - 访问和查看您的个人信息 - 更正或更新您的个人信息 -
|
||||
删除您的个人信息 - 撤回您的授权同意 - 注销您的账号
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">8. 未成年人保护</text>
|
||||
<text class="text-gray-600 block">
|
||||
8.1 我们建议未成年人在监护人的指导下使用我们的服务。 8.2
|
||||
我们会采取特殊措施保护未成年人的个人信息。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">9. 政策更新</text>
|
||||
<text class="text-gray-600 block">
|
||||
我们可能会不时更新本隐私政策。当我们更新隐私政策时,我们会在小程序内发布更新后的版本,并更新"最后更新日期"。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">10. 联系我们</text>
|
||||
<text class="text-gray-600 block">
|
||||
如果您对本隐私政策有任何疑问、意见或建议,请通过小程序内的客服功能与我们联系。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 无需额外逻辑
|
||||
</script>
|
||||
100
src/pages/agreement/user.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
style: {
|
||||
navigationBarTitleText: '用户协议',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-white p-4">
|
||||
<view class="prose prose-sm max-w-none">
|
||||
<view class="text-center mb-8">
|
||||
<text class="text-xl font-bold">用户协议</text>
|
||||
<text class="block text-sm text-gray-500 mt-2">最后更新日期:2024年3月</text>
|
||||
</view>
|
||||
|
||||
<view class="space-y-6">
|
||||
<view>
|
||||
<text class="font-bold block mb-2">1. 协议的范围</text>
|
||||
<text class="text-gray-600 block">
|
||||
欢迎您使用我们的相亲小程序。本协议是您与我们之间关于使用本小程序服务所订立的协议。请您仔细阅读本协议的全部内容。如果您不同意本协议的任何内容,您应立即停止使用本小程序。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">2. 服务内容</text>
|
||||
<text class="text-gray-600 block">
|
||||
本小程序是一个相亲交友平台,为用户提供信息展示、交流互动等服务。我们会不断改进服务质量,但不对服务的及时性、安全性、准确性做出保证。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">3. 用户注册与账号</text>
|
||||
<text class="text-gray-600 block">
|
||||
3.1
|
||||
您承诺以真实身份注册成为本小程序的用户,并保证所提供的个人资料真实、准确、完整、合法有效。
|
||||
3.2 您应当妥善保管账号和密码,对账号下的所有行为负责。 3.3
|
||||
如发现任何未经授权使用您账号的情况,应立即通知我们。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">4. 用户行为规范</text>
|
||||
<text class="text-gray-600 block">
|
||||
4.1 您在使用本小程序时,必须遵守中华人民共和国相关法律法规。 4.2
|
||||
您不得利用本小程序从事违法违规活动,包括但不限于: - 发布虚假信息 - 侵犯他人知识产权 -
|
||||
传播色情、暴力等不良信息 - 从事诈骗等违法活动
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">5. 隐私保护</text>
|
||||
<text class="text-gray-600 block">
|
||||
我们重视对您个人信息的保护,具体隐私政策请参见《隐私政策》。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">6. 知识产权</text>
|
||||
<text class="text-gray-600 block">
|
||||
本小程序的所有内容,包括但不限于文字、图片、音频、视频、软件、程序、版面设计等,均受著作权法和国际著作权条约以及其他知识产权法律法规的保护。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">7. 免责声明</text>
|
||||
<text class="text-gray-600 block">
|
||||
7.1 对于因不可抗力或我们无法控制的原因造成的服务中断或其它缺陷,我们不承担任何责任。 7.2
|
||||
您理解并同意,我们不对您因使用本小程序而遭受的任何直接或间接损失负责。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">8. 协议修改</text>
|
||||
<text class="text-gray-600 block">
|
||||
我们有权在必要时修改本协议条款。您可以在本小程序中查阅最新版本的协议条款。本协议条款变更后,如果您继续使用本小程序,即视为您已接受修改后的协议。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">9. 法律适用</text>
|
||||
<text class="text-gray-600 block">
|
||||
本协议的订立、执行和解释及争议的解决均应适用中华人民共和国法律。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="font-bold block mb-2">10. 联系我们</text>
|
||||
<text class="text-gray-600 block">
|
||||
如果您对本协议有任何疑问,请通过小程序内的客服功能与我们联系。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 无需额外逻辑
|
||||
</script>
|
||||
384
src/pages/detail/index.vue
Normal file
@ -0,0 +1,384 @@
|
||||
<!-- 用户详情页面 -->
|
||||
<route lang="json5">
|
||||
{
|
||||
style: {
|
||||
navigationBarTitleText: '用户详情',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<!-- 用户基本信息卡片 -->
|
||||
<view class="bg-white rounded-b-3xl shadow-sm">
|
||||
<view class="p-6">
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
:src="userInfo.avatar"
|
||||
mode="aspectFill"
|
||||
class="w-24 h-24 rounded-xl object-cover"
|
||||
/>
|
||||
<view class="ml-4 flex-1">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<text class="text-xl font-semibold text-gray-900">{{ userInfo.nickName }}</text>
|
||||
<text class="ml-2 text-sm text-gray-500">
|
||||
{{ calculateAge(userInfo.birthday) }}岁
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="px-4 py-1.5 bg-primary text-sm rounded-full flex items-center"
|
||||
:class="{ 'opacity-50': isUnlocking }"
|
||||
@tap="handleUnlock"
|
||||
>
|
||||
<text v-if="isUnlocking">解锁中...</text>
|
||||
<text v-else>解锁</text>
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="px-4 py-1.5 bg-red-500 text-sm text-white rounded-full flex items-center"
|
||||
@tap="handleComplaint"
|
||||
>
|
||||
<text>投诉</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-2 flex items-center space-x-2">
|
||||
<text class="text-sm text-gray-500">{{ userInfo.height }}cm</text>
|
||||
<view class="w-px h-3 bg-gray-200"></view>
|
||||
<text class="text-sm text-gray-500">{{ getEducationText(userInfo.education) }}</text>
|
||||
<view class="w-px h-3 bg-gray-200"></view>
|
||||
<text class="text-sm text-gray-500">{{ userInfo.workArea }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详细信息列表 -->
|
||||
<view class="mt-4 bg-white rounded-3xl p-6">
|
||||
<view class="space-y-6">
|
||||
<!-- 基本信息 -->
|
||||
<view class="space-y-4">
|
||||
<text class="text-lg font-semibold text-gray-900">基本信息</text>
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">职业</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900">{{ userInfo.profession }}</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">月收入</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900">
|
||||
{{ getMonthlyIncomeText(userInfo.monthlyIncome) }}
|
||||
</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">婚姻状况</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900">
|
||||
{{ getMaritalStatusText(userInfo.maritalStatus) }}
|
||||
</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">住房情况</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900">
|
||||
{{ getHousingStatusText(userInfo.housingStatus) }}
|
||||
</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<view class="space-y-4">
|
||||
<text class="text-lg font-semibold text-gray-900">联系方式</text>
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">微信号</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900" @tap="copyText(userInfo.wechat)">
|
||||
{{ userInfo.wechat }}
|
||||
</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">QQ</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900" @tap="copyText(userInfo.qq)">
|
||||
{{ userInfo.qq }}
|
||||
</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">邮箱</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900" @tap="copyText(userInfo.email)">
|
||||
{{ userInfo.email }}
|
||||
</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<view class="space-y-4">
|
||||
<text class="text-lg font-semibold text-gray-900">其他信息</text>
|
||||
<view class="space-y-4">
|
||||
<view class="space-y-1">
|
||||
<text class="text-sm text-gray-500">个人介绍</text>
|
||||
<view class="relative">
|
||||
<text class="text-sm text-gray-900">{{ userInfo.selfIntroduction }}</text>
|
||||
<view
|
||||
v-if="!isUnlocked"
|
||||
class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center"
|
||||
>
|
||||
<text class="text-sm text-gray-500">点击解锁查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { LoginResponse } from '@/service/login/type'
|
||||
import { checkIsLoveAPI, unlockUserAPI } from '@/service/love'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref<LoginResponse>({} as LoginResponse)
|
||||
const isUnlocked = ref(false)
|
||||
const userId = ref('')
|
||||
const isLoading = ref(false)
|
||||
const isUnlocking = ref(false)
|
||||
|
||||
// 计算年龄
|
||||
const calculateAge = (birthday: string | undefined) => {
|
||||
if (!birthday) return '未知'
|
||||
const birthDate = new Date(birthday)
|
||||
const today = new Date()
|
||||
let age = today.getFullYear() - birthDate.getFullYear()
|
||||
const monthDiff = today.getMonth() - birthDate.getMonth()
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
||||
age--
|
||||
}
|
||||
return age
|
||||
}
|
||||
|
||||
// 获取婚姻状况文本
|
||||
const getMaritalStatusText = (status: string | undefined) => {
|
||||
const statusMap = {
|
||||
'1': '未婚',
|
||||
'2': '离异',
|
||||
'3': '丧偶',
|
||||
}
|
||||
return status ? statusMap[status as keyof typeof statusMap] || '未知' : '未知'
|
||||
}
|
||||
|
||||
// 获取学历文本
|
||||
const getEducationText = (education: string | undefined) => {
|
||||
const educationMap = {
|
||||
'1': '初中',
|
||||
'2': '高中',
|
||||
'3': '大专',
|
||||
'4': '本科',
|
||||
'5': '硕士',
|
||||
'6': '博士',
|
||||
}
|
||||
return education ? educationMap[education as keyof typeof educationMap] || '未知' : '未知'
|
||||
}
|
||||
|
||||
// 获取月收入文本
|
||||
const getMonthlyIncomeText = (income: string | undefined) => {
|
||||
const incomeMap = {
|
||||
'1': '3000以下',
|
||||
'2': '3000~5000',
|
||||
'3': '5000~8000',
|
||||
'4': '8000~10000',
|
||||
'5': '10000~20000',
|
||||
'6': '20000以上',
|
||||
}
|
||||
return income ? incomeMap[income as keyof typeof incomeMap] || '未知' : '未知'
|
||||
}
|
||||
|
||||
// 获取住房情况文本
|
||||
const getHousingStatusText = (status: string | undefined) => {
|
||||
const statusMap = {
|
||||
'1': '已购房(有贷款)',
|
||||
'2': '已购房(无贷款)',
|
||||
'3': '有能力购房',
|
||||
'4': '无房',
|
||||
'5': '无房希望对方解决',
|
||||
'6': '无房希望双方解决',
|
||||
'7': '与父母同住',
|
||||
'8': '独自租房',
|
||||
'9': '与人合租',
|
||||
'10': '住单位房',
|
||||
}
|
||||
return status ? statusMap[status as keyof typeof statusMap] || '未知' : '未知'
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
const getUserDetail = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
uni.showToast({
|
||||
title: '获取用户信息失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已解锁
|
||||
const checkIsUnlocked = async () => {
|
||||
try {
|
||||
const res = await checkIsLoveAPI(userId.value)
|
||||
if (res.code === 200) {
|
||||
isUnlocked.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查解锁状态失败:', error)
|
||||
uni.showToast({
|
||||
title: '检查解锁状态失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理解锁
|
||||
const handleUnlock = async () => {
|
||||
if (isUnlocking.value) return
|
||||
|
||||
try {
|
||||
isUnlocking.value = true
|
||||
const res = await unlockUserAPI(userId.value)
|
||||
if (res.code === 200) {
|
||||
isUnlocked.value = true
|
||||
uni.showToast({
|
||||
title: '解锁成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// 重新获取用户信息以显示联系方式
|
||||
await getUserDetail()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: (res.data as any) || '解锁失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解锁失败:', error)
|
||||
uni.showToast({
|
||||
title: '解锁失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
isUnlocking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制文本到剪贴板
|
||||
const copyText = (text: string | undefined) => {
|
||||
if (!text) return
|
||||
if (!isUnlocked.value) {
|
||||
uni.showToast({
|
||||
title: '请先解锁查看',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 处理投诉
|
||||
const handleComplaint = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/my/complaint?id=${userId.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.id) {
|
||||
userId.value = options.id
|
||||
getUserDetail()
|
||||
checkIsUnlocked()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/style/iconfont.css';
|
||||
</style>
|
||||
128
src/pages/fate/components/MatchmakerRecommend.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<view class="mb-8">
|
||||
<view class="flex justify-between items-center mb-4">
|
||||
<view class="flex items-center">
|
||||
<text class="text-2xl font-bold text-primary">红娘推荐</text>
|
||||
<view class="ml-2 px-2 py-1 bg-primary/10 rounded-full">
|
||||
<text class="text-xs text-primary font-medium">专业认证</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<text class="i-carbon-task-complete text-base text-primary"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="relative">
|
||||
<view
|
||||
class="p-5 bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow duration-300"
|
||||
>
|
||||
<view class="flex items-start">
|
||||
<view class="relative">
|
||||
<image
|
||||
:src="matchmakerInfo.avatar"
|
||||
mode="aspectFill"
|
||||
class="w-24 h-24 rounded-full border-2 border-primary/20 shadow-md"
|
||||
/>
|
||||
<view
|
||||
class="absolute -bottom-1 -right-1 w-7 h-7 bg-gradient-to-r from-primary to-primary/80 rounded-full flex items-center justify-center shadow-md"
|
||||
>
|
||||
<text class="i-carbon-task-complete text-xs text-white"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ml-5 flex-1">
|
||||
<view class="flex items-center mb-3">
|
||||
<text class="text-xl font-bold text-gray-800 mr-3">{{ matchmakerInfo.name }}</text>
|
||||
<view class="px-3 py-1 bg-gradient-to-r from-primary/10 to-primary/5 rounded-full">
|
||||
<text class="text-sm text-primary font-medium">金牌红娘</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-2.5">
|
||||
<view class="flex items-center">
|
||||
<text class="i-carbon-logo-wechat text-lg text-gray-500 mr-2"></text>
|
||||
<text class="text-sm text-gray-600">微信: {{ matchmakerInfo.wechat }}</text>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<text class="i-carbon-phone text-lg text-gray-500 mr-2"></text>
|
||||
<text class="text-sm text-gray-600">电话: {{ matchmakerInfo.phone }}</text>
|
||||
</view>
|
||||
<view class="flex items-center text-sm">
|
||||
<text class="i-carbon-star-filled text-lg text-yellow-400 mr-2"></text>
|
||||
<text class="text-gray-600">
|
||||
成功率:
|
||||
<text class="font-medium text-primary">{{ matchmakerInfo.successRate }}%</text>
|
||||
</text>
|
||||
<text class="mx-3 text-gray-300">|</text>
|
||||
<text class="text-gray-600">
|
||||
服务人数:
|
||||
<text class="font-medium text-primary">{{ matchmakerInfo.serviceCount }}+</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-5 pt-4 border-t border-gray-100">
|
||||
<view class="flex justify-between items-center">
|
||||
<view class="flex items-center">
|
||||
<text class="i-carbon-chat text-lg text-gray-500 mr-2"></text>
|
||||
<text class="text-sm text-gray-600">在线咨询</text>
|
||||
</view>
|
||||
<button
|
||||
class="flex items-center px-5 py-2.5 bg-gradient-to-r from-[#FF6B6B] to-[#FF8E8E] text-white rounded-full text-sm font-medium shadow-md hover:shadow-lg hover:from-[#FF8E8E] hover:to-[#FF6B6B] transition-all duration-300 active:scale-95 active:shadow-inner"
|
||||
@click="contactMatchmaker"
|
||||
>
|
||||
<text class="font-semibold">立即联系</text>
|
||||
<text class="i-carbon-arrow-right text-xs ml-1"></text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface MatchmakerInfo {
|
||||
name: string
|
||||
avatar: string
|
||||
wechat: string
|
||||
phone: string
|
||||
successRate: number
|
||||
serviceCount: number
|
||||
}
|
||||
|
||||
// 红娘信息
|
||||
const matchmakerInfo = ref<MatchmakerInfo>({
|
||||
name: '王红娘',
|
||||
avatar:
|
||||
'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg',
|
||||
wechat: 'matchmaker123',
|
||||
phone: '13800138000',
|
||||
successRate: 98,
|
||||
serviceCount: 1000,
|
||||
})
|
||||
|
||||
// 联系红娘方法
|
||||
const contactMatchmaker = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['复制微信号', '拨打电话'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.setClipboardData({
|
||||
data: matchmakerInfo.value.wechat,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '微信号已复制',
|
||||
icon: 'success',
|
||||
})
|
||||
},
|
||||
})
|
||||
} else if (res.tapIndex === 1) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: matchmakerInfo.value.phone,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
23
src/pages/fate/fate.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '红娘',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="px-4 py-3">
|
||||
<!-- 红娘推荐组件 -->
|
||||
<MatchmakerRecommend />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MatchmakerRecommend from './components/MatchmakerRecommend.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 自定义样式
|
||||
</style>
|
||||
284
src/pages/index/index.vue
Normal file
@ -0,0 +1,284 @@
|
||||
<!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
|
||||
<route lang="json5" type="home">
|
||||
{
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
<template>
|
||||
<view class="px-4 py-3" @click="closeUserMenu">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="flex items-center justify-between mb-6">
|
||||
<text class="text-lg font-bold">可报名考试</text>
|
||||
<view class="flex items-center relative" @click.stop>
|
||||
<i class="i-carbon-user-filled text-xl mr-2 text-gray-600"></i>
|
||||
<text
|
||||
class="text-base font-bold text-gray-700 mr-4 cursor-pointer transition-colors duration-200 hover:text-gray-900"
|
||||
@click="toggleUserMenu"
|
||||
>
|
||||
{{ userName }}
|
||||
</text>
|
||||
<!-- 用户菜单 -->
|
||||
<transition name="menu-fade" mode="out-in">
|
||||
<view
|
||||
v-if="showUserMenu"
|
||||
class="absolute top-full right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 z-50 min-w-32 transform origin-top-right"
|
||||
>
|
||||
<view
|
||||
class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer border-b border-gray-100 transition-colors duration-150"
|
||||
@click="handleModifyPassword"
|
||||
>
|
||||
修改密码
|
||||
</view>
|
||||
<view
|
||||
class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer border-b border-gray-100 transition-colors duration-150"
|
||||
@click="handleModifyProfile"
|
||||
>
|
||||
修改个人信息
|
||||
</view>
|
||||
<view
|
||||
class="px-4 py-2 text-sm text-red-600 hover:bg-red-50 cursor-pointer transition-colors duration-150"
|
||||
@click="handleLogout"
|
||||
>
|
||||
退出登录
|
||||
</view>
|
||||
</view>
|
||||
</transition>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 考试列表 -->
|
||||
<view class="space-y-6">
|
||||
<view
|
||||
v-for="exam in examList"
|
||||
:key="exam.id"
|
||||
class="bg-white rounded-lg shadow-xl p-4 border-2 border-gray-400 relative transition-all duration-300 hover:shadow-2xl hover:-translate-y-1 hover:border-gray-500 cursor-pointer"
|
||||
style="
|
||||
box-shadow:
|
||||
0 8px 16px -4px rgba(0, 0, 0, 0.15),
|
||||
0 4px 8px -2px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
"
|
||||
@mouseenter="handleCardHover(exam.id)"
|
||||
@mouseleave="handleCardLeave(exam.id)"
|
||||
>
|
||||
<!-- 第一行:考试名称 -->
|
||||
<view class="mb-2">
|
||||
<text class="text-xl font-bold text-gray-800">{{ exam.examName }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:考试时间 -->
|
||||
<view class="mb-2">
|
||||
<text class="text-base text-gray-600">
|
||||
{{ formatExamTime(exam.examDate, exam.startTime, exam.endTime) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 第三行:考试地点 -->
|
||||
<view class="mb-2">
|
||||
<text class="text-base text-gray-600">
|
||||
{{
|
||||
formatExamLocation(exam.provinceName, exam.cityName, exam.address, exam.locationName)
|
||||
}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 第四、五行:空白间距 -->
|
||||
<view class="h-4"></view>
|
||||
<view class="h-4"></view>
|
||||
|
||||
<!-- 第六行:报名截止信息 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-sm text-gray-500">报名截止:{{ exam.registrationDeadline }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 报名按钮:右下角 -->
|
||||
<view class="absolute bottom-4 right-4">
|
||||
<button
|
||||
class="bg-black text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-800 transition-colors duration-200"
|
||||
@click="handleRegisterExam(exam)"
|
||||
>
|
||||
报名
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="text-center py-8">
|
||||
<text class="text-gray-500">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && examList.length === 0" class="text-center py-8">
|
||||
<text class="text-gray-500">暂无可报名的考试</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { getCanBookExamListAPI } from '@/service/exam'
|
||||
|
||||
defineOptions({
|
||||
name: 'Home',
|
||||
})
|
||||
|
||||
const userName = ref('')
|
||||
const showUserMenu = ref(false)
|
||||
const examList = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取考试列表
|
||||
const getExamList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getCanBookExamListAPI()
|
||||
examList.value = res.data.list || []
|
||||
} catch (error) {
|
||||
console.error('获取考试列表失败:', error)
|
||||
uni.showToast({
|
||||
title: '获取考试列表失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化考试时间
|
||||
const formatExamTime = (examDate: string, startTime: string, endTime: string) => {
|
||||
if (!examDate || !startTime || !endTime) return ''
|
||||
return `${examDate} ${startTime}-${endTime}`
|
||||
}
|
||||
|
||||
// 格式化考试地点
|
||||
const formatExamLocation = (
|
||||
provinceName: string,
|
||||
cityName: string,
|
||||
address: string,
|
||||
locationName: string,
|
||||
) => {
|
||||
const parts = [provinceName, cityName, address, locationName].filter(Boolean)
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
// 报名考试
|
||||
const handleRegisterExam = (exam: any) => {
|
||||
uni.showModal({
|
||||
title: '确认报名',
|
||||
content: `确定要报名参加"${exam.examName}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// TODO: 调用报名接口
|
||||
uni.showToast({
|
||||
title: '报名成功',
|
||||
icon: 'success',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 卡片hover效果
|
||||
const handleCardHover = (examId: string) => {
|
||||
// 可以在这里添加额外的hover逻辑,比如改变按钮样式等
|
||||
console.log('卡片hover:', examId)
|
||||
}
|
||||
|
||||
const handleCardLeave = (examId: string) => {
|
||||
// 可以在这里添加离开时的逻辑
|
||||
console.log('卡片离开:', examId)
|
||||
}
|
||||
|
||||
// 切换用户菜单
|
||||
const toggleUserMenu = () => {
|
||||
showUserMenu.value = !showUserMenu.value
|
||||
}
|
||||
|
||||
// 关闭用户菜单
|
||||
const closeUserMenu = () => {
|
||||
showUserMenu.value = false
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const handleModifyPassword = () => {
|
||||
showUserMenu.value = false
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/modify-password',
|
||||
})
|
||||
}
|
||||
|
||||
// 修改个人信息
|
||||
const handleModifyProfile = () => {
|
||||
showUserMenu.value = false
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/profile',
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
showUserMenu.value = false
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 清除本地存储
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('loginData')
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
// 获取本地存储的用户昵称
|
||||
try {
|
||||
const loginData = uni.getStorageSync('loginData')
|
||||
userName.value = loginData.user.nickName
|
||||
} catch (e) {
|
||||
userName.value = ''
|
||||
}
|
||||
|
||||
// 获取考试列表
|
||||
getExamList()
|
||||
})
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh(() => {
|
||||
getExamList().then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 菜单淡入淡出动画 */
|
||||
.menu-fade-enter-active,
|
||||
.menu-fade-leave-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.menu-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
|
||||
.menu-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
|
||||
.menu-fade-enter-to,
|
||||
.menu-fade-leave-from {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
</style>
|
||||
86
src/pages/login/index.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen flex items-center justify-center bg-gray-200 px-4">
|
||||
<view
|
||||
class="w-full max-w-md mx-auto bg-white rounded-2xl shadow-lg p-8 border border-gray-100 box-border"
|
||||
>
|
||||
<view class="mb-6 text-center">
|
||||
<text class="text-2xl font-bold text-gray-800">艺术评测考试平台</text>
|
||||
</view>
|
||||
<view class="mb-4">
|
||||
<input
|
||||
v-model="account"
|
||||
type="text"
|
||||
placeholder="用户名/手机号"
|
||||
class="w-full h-12 px-4 border-2 border-gray-300 rounded-lg focus:border-pink-400 outline-none bg-gray-50 text-base transition-all duration-200 box-border"
|
||||
/>
|
||||
</view>
|
||||
<view class="mb-6">
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
class="w-full h-12 px-4 border-2 border-gray-300 rounded-lg focus:border-pink-400 outline-none bg-gray-50 text-base transition-all duration-200 box-border"
|
||||
/>
|
||||
</view>
|
||||
<view class="flex mb-2 mt-4 gap-3">
|
||||
<button
|
||||
class="flex-1 h-12 bg-black text-white rounded-l-lg text-base font-semibold shadow hover:shadow-md transition-all duration-200 flex items-center justify-center border border-gray-300"
|
||||
@click="handleLogin"
|
||||
>
|
||||
<span class="w-full text-center">登录</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 h-12 bg-white text-black rounded-r-lg text-base font-semibold hover:bg-gray-50 transition-all duration-200 flex items-center justify-center border border-gray-300"
|
||||
@click="handleRegister"
|
||||
>
|
||||
<span class="w-full text-center">注册</span>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { loginAPI } from '@/service/login'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
const useStore = useUserStore()
|
||||
const account = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!account.value || !password.value) {
|
||||
uni.showToast({ title: '请输入账号和密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await loginAPI({ username: account.value, password: password.value })
|
||||
uni.setStorageSync('loginData', res.data)
|
||||
uni.setStorageSync('x-token', res.data.token)
|
||||
useStore.setUserInfo()
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
// 跳转到注册页(假设有注册页面)
|
||||
uni.navigateTo({ url: '/pages/register/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #fafaff;
|
||||
}
|
||||
</style>
|
||||
170
src/pages/my/activities.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '我的活动',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<!-- 顶部筛选 -->
|
||||
<view class="sticky top-0 z-10 bg-white shadow-sm">
|
||||
<view class="flex items-center justify-around p-4 border-b border-gray-100">
|
||||
<view
|
||||
v-for="(item, index) in filterOptions"
|
||||
:key="index"
|
||||
class="flex-1 text-center"
|
||||
@tap="changeFilter(item.value)"
|
||||
>
|
||||
<text
|
||||
:class="[
|
||||
'text-sm py-2 px-4 rounded-full transition-colors',
|
||||
currentFilter === item.value
|
||||
? 'bg-sky-500 text-white'
|
||||
: 'text-gray-600 hover:bg-gray-100',
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<view class="p-4">
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="flex justify-center items-center py-8">
|
||||
<text class="text-gray-500">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view v-else-if="error" class="flex justify-center items-center py-8">
|
||||
<text class="text-red-500">{{ error }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="!activities.length" class="flex justify-center items-center py-8">
|
||||
<text class="text-gray-500">暂无活动</text>
|
||||
</view>
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<view v-else class="space-y-4">
|
||||
<view
|
||||
class="bg-white rounded-2xl shadow-lg overflow-hidden transform transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]"
|
||||
v-for="item in activities"
|
||||
:key="item.id"
|
||||
@tap="viewActivity(item)"
|
||||
>
|
||||
<view class="relative">
|
||||
<image :src="item.eventAvatar" mode="aspectFill" class="w-full h-40 object-cover" />
|
||||
<view
|
||||
class="absolute top-3 right-3 px-3 py-1 bg-white/90 rounded-full backdrop-blur-sm"
|
||||
>
|
||||
<text class="text-sm text-primary font-medium">¥{{ item.eventPrice }}</text>
|
||||
</view>
|
||||
<view
|
||||
:class="[
|
||||
'absolute top-3 left-3 px-3 py-1 rounded-full text-sm font-medium',
|
||||
item.status === '1' ? 'bg-green-500/90 text-white' : 'bg-gray-500/90 text-white',
|
||||
]"
|
||||
>
|
||||
{{ item.status === '1' ? '进行中' : '已结束' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="p-4">
|
||||
<text class="block text-lg font-bold mb-2">{{ item.eventTitle }}</text>
|
||||
<view class="flex items-center text-sm text-gray-500">
|
||||
<text class="iconfont icon-time mr-1"></text>
|
||||
<text>截止时间: {{ formatDate(item.eventEndTime) }}</text>
|
||||
</view>
|
||||
<view class="mt-3 flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<view class="flex -space-x-2">
|
||||
<image
|
||||
v-for="(avatar, index) in getRandomAvatars(3)"
|
||||
:key="index"
|
||||
:src="avatar"
|
||||
class="w-8 h-8 rounded-full border-2 border-white"
|
||||
/>
|
||||
</view>
|
||||
<text class="ml-2 text-sm text-gray-500">已有 {{ item.eventNumber }} 人报名</text>
|
||||
</view>
|
||||
<view
|
||||
class="px-4 py-2 bg-primary text-white text-sm rounded-full"
|
||||
@tap.stop="handleJoin(item)"
|
||||
>
|
||||
查看详情
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getMyEventListAPI } from '@/service/event'
|
||||
import type { MyEventItem } from '@/service/event/type'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
// 筛选选项
|
||||
const filterOptions = [
|
||||
{ label: '全部', value: '1' },
|
||||
{ label: '进行中', value: '2' },
|
||||
{ label: '已结束', value: '3' },
|
||||
]
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const activities = ref<MyEventItem[]>([])
|
||||
const currentFilter = ref('1')
|
||||
|
||||
// 获取活动列表
|
||||
const fetchActivities = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await getMyEventListAPI()
|
||||
if (res.code === 200) {
|
||||
activities.value = res.data
|
||||
} else {
|
||||
error.value = res.message || '获取活动列表失败'
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = '网络错误,请稍后重试'
|
||||
console.error('获取活动列表失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换筛选
|
||||
const changeFilter = (filter: string) => {
|
||||
currentFilter.value = filter
|
||||
fetchActivities()
|
||||
}
|
||||
|
||||
// 查看活动详情
|
||||
const viewActivity = (item: MyEventItem) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/activity/detail?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取随机头像(实际项目中应该使用真实数据)
|
||||
const getRandomAvatars = (count: number) => {
|
||||
return Array(count).fill(
|
||||
'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg',
|
||||
)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchActivities()
|
||||
})
|
||||
</script>
|
||||
212
src/pages/my/complaint.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '发起投诉',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 p-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-4">
|
||||
<!-- 投诉分类 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-sm text-gray-600 mb-2 block">投诉分类</text>
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view
|
||||
v-for="item in complaintClasses"
|
||||
:key="item.value"
|
||||
class="h-12 px-4 bg-gray-50 rounded-lg flex items-center justify-center"
|
||||
:class="{
|
||||
'bg-sky-50 border border-sky-200': selectedClass === item.value,
|
||||
'border border-gray-100': selectedClass !== item.value,
|
||||
}"
|
||||
@click="selectedClass = item.value"
|
||||
>
|
||||
<text
|
||||
:class="{
|
||||
'text-sky-600': selectedClass === item.value,
|
||||
'text-gray-600': selectedClass !== item.value,
|
||||
}"
|
||||
>
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 投诉内容 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-sm text-gray-600 mb-2 block">投诉详情</text>
|
||||
<textarea
|
||||
v-model="complaintContent"
|
||||
class="w-full h-40 px-4 py-3 bg-gray-50 rounded-lg text-gray-800 placeholder-gray-400"
|
||||
placeholder="请详细描述您遇到的问题..."
|
||||
maxlength="500"
|
||||
/>
|
||||
<text class="text-xs text-gray-400 text-right block mt-1">
|
||||
{{ complaintContent.length }}/500
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-sm text-gray-600 mb-2 block">上传证据(选填)</text>
|
||||
<view class="grid grid-cols-3 gap-2">
|
||||
<view v-for="(image, index) in imageList" :key="index" class="relative aspect-square">
|
||||
<image :src="image" class="w-full h-full rounded-lg object-cover" mode="aspectFill" />
|
||||
<view
|
||||
class="absolute top-1 right-1 w-6 h-6 bg-black/50 rounded-full flex items-center justify-center"
|
||||
@click="removeImage(index)"
|
||||
>
|
||||
<i class="i-carbon-close text-white text-sm"></i>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="imageList.length < 9"
|
||||
class="aspect-square bg-gray-50 rounded-lg flex items-center justify-center border border-dashed border-gray-200"
|
||||
@click="chooseImage"
|
||||
>
|
||||
<i class="i-carbon-image text-gray-400 text-2xl"></i>
|
||||
</view>
|
||||
</view>
|
||||
<text class="text-xs text-gray-400 mt-1 block">最多上传9张图片,每张不超过5MB</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button
|
||||
class="w-full h-12 bg-gradient-to-r from-sky-400 to-indigo-400 text-white rounded-lg text-base flex items-center justify-center shadow-sm hover:shadow-md transition-shadow"
|
||||
:disabled="!isValid"
|
||||
:class="{ 'opacity-50': !isValid }"
|
||||
@click="submitComplaint"
|
||||
>
|
||||
提交投诉
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { addComplaintAPI } from '@/service/feedback'
|
||||
import { uploadImageAPI } from '@/service/file'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
const id = ref('')
|
||||
onLoad((options) => {
|
||||
id.value = options.id
|
||||
})
|
||||
|
||||
// 投诉分类选项
|
||||
const complaintClasses = [
|
||||
{ label: '虚假宣传', value: '虚假宣传' },
|
||||
{ label: '微商', value: '微商' },
|
||||
{ label: '诈骗', value: '诈骗' },
|
||||
{ label: '个人信息不符', value: '个人信息不符' },
|
||||
]
|
||||
|
||||
const selectedClass = ref('')
|
||||
const complaintContent = ref('')
|
||||
const imageList = ref<string[]>([])
|
||||
|
||||
// 验证表单是否有效
|
||||
const isValid = computed(() => {
|
||||
return selectedClass.value && complaintContent.value.trim().length > 0
|
||||
})
|
||||
|
||||
// 选择图片
|
||||
const chooseImage = async () => {
|
||||
try {
|
||||
const res = await uni.chooseImage({
|
||||
count: 9 - imageList.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
})
|
||||
|
||||
// 检查图片大小
|
||||
const tempFiles = res.tempFiles as UniApp.ChooseImageSuccessCallbackResultFile[]
|
||||
const tempFilePaths = res.tempFilePaths as string[]
|
||||
|
||||
const validImages = tempFilePaths.filter((path) => {
|
||||
const file = tempFiles.find((f) => f.path === path)
|
||||
return file && file.size <= 5 * 1024 * 1024 // 5MB
|
||||
})
|
||||
|
||||
if (validImages.length !== tempFilePaths.length) {
|
||||
uni.showToast({
|
||||
title: '部分图片超过5MB限制',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
|
||||
imageList.value.push(...validImages)
|
||||
} catch (error) {
|
||||
console.error('选择图片失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除图片
|
||||
const removeImage = (index: number) => {
|
||||
imageList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
const uploadImages = async () => {
|
||||
const uploadedUrls: string[] = []
|
||||
|
||||
for (const image of imageList.value) {
|
||||
try {
|
||||
const res = await uploadImageAPI(image, 'complaint')
|
||||
if (res.url) {
|
||||
uploadedUrls.push(res.url)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传图片失败:', error)
|
||||
uni.showToast({
|
||||
title: '图片上传失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return uploadedUrls
|
||||
}
|
||||
|
||||
// 提交投诉
|
||||
const submitComplaint = async () => {
|
||||
if (!isValid.value) return
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
|
||||
// 上传图片
|
||||
const imageUrls = await uploadImages()
|
||||
|
||||
// 提交投诉
|
||||
await addComplaintAPI({
|
||||
userId: id.value, // TODO: 从路由参数或全局状态获取被投诉人ID
|
||||
complaintClass: selectedClass.value as any,
|
||||
complaintContent: complaintContent.value.trim(),
|
||||
complaintImageList: imageUrls,
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success',
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '提交失败,请重试',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
92
src/pages/my/feedback.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '意见反馈',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 p-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-4">
|
||||
<!-- 标题输入 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-sm text-gray-600 mb-2 block">反馈标题</text>
|
||||
<input
|
||||
v-model="feedbackTitle"
|
||||
class="w-full h-12 px-4 bg-gray-50 rounded-lg text-gray-800 placeholder-gray-400"
|
||||
placeholder="请输入反馈标题"
|
||||
maxlength="50"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 内容输入 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-sm text-gray-600 mb-2 block">反馈内容</text>
|
||||
<textarea
|
||||
v-model="feedbackContent"
|
||||
class="w-full h-40 px-4 py-3 bg-gray-50 rounded-lg text-gray-800 placeholder-gray-400"
|
||||
placeholder="请详细描述您遇到的问题或建议..."
|
||||
maxlength="500"
|
||||
/>
|
||||
<text class="text-xs text-gray-400 text-right block mt-1">
|
||||
{{ feedbackContent.length }}/500
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button
|
||||
class="w-full h-12 bg-gradient-to-r from-sky-400 to-indigo-400 text-white rounded-lg text-base flex items-center justify-center shadow-sm hover:shadow-md transition-shadow"
|
||||
:disabled="!isValid"
|
||||
:class="{ 'opacity-50': !isValid }"
|
||||
@click="submitFeedback"
|
||||
>
|
||||
提交反馈
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { addFeedbackAPI } from '@/service/feedback'
|
||||
|
||||
const feedbackTitle = ref('')
|
||||
const feedbackContent = ref('')
|
||||
|
||||
// 验证表单是否有效
|
||||
const isValid = computed(() => {
|
||||
return feedbackTitle.value.trim().length > 0 && feedbackContent.value.trim().length > 0
|
||||
})
|
||||
|
||||
// 提交反馈
|
||||
const submitFeedback = async () => {
|
||||
if (!isValid.value) return
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
await addFeedbackAPI({
|
||||
feedbackTitle: feedbackTitle.value.trim(),
|
||||
feedbackContent: feedbackContent.value.trim(),
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success',
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '提交失败,请重试',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
274
src/pages/my/invite.vue
Normal file
@ -0,0 +1,274 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '邀请好友',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<!-- 邀请码卡片 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-6">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold text-gray-800 mb-2 block">我的邀请码</text>
|
||||
<view class="flex items-center justify-center space-x-2 mb-4">
|
||||
<text class="text-3xl font-bold text-sky-500">{{ inviteCode }}</text>
|
||||
<button
|
||||
class="px-3 py-1 bg-sky-50 text-sky-500 rounded-full text-sm"
|
||||
@click="copyInviteCode"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">邀请好友注册时填写此邀请码,双方均可获得奖励</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 邀请记录 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||
<view class="p-4 border-b border-gray-100">
|
||||
<text class="text-base font-bold text-gray-800">邀请记录</text>
|
||||
</view>
|
||||
<view v-if="inviteList.length === 0" class="p-8 text-center">
|
||||
<text class="text-gray-500">暂无邀请记录</text>
|
||||
</view>
|
||||
<view v-else class="divide-y divide-gray-100">
|
||||
<view v-for="(item, index) in inviteList" :key="index" class="p-4">
|
||||
<!-- 一级用户 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex-1">
|
||||
<text class="text-gray-800 block">{{ item.nickname }}</text>
|
||||
<text class="text-sm text-gray-500">{{ item.createTime }}</text>
|
||||
</view>
|
||||
<view class="flex items-center space-x-2">
|
||||
<button
|
||||
class="px-2 py-1 bg-gradient-to-r from-sky-400 to-indigo-400 text-white rounded-lg text-xs flex items-center shadow-sm hover:shadow transition-all duration-300"
|
||||
@click.stop="
|
||||
navigateTo('/pages/my/spend-detail', {
|
||||
nickname: item.nickname,
|
||||
id: item.id,
|
||||
points: item.points,
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="i-carbon-money text-xs mr-1"></i>
|
||||
消费明细
|
||||
</button>
|
||||
<view class="w-8 h-8 flex items-center justify-center" @click="toggleExpand(index)">
|
||||
<i
|
||||
:class="[
|
||||
expandedItems[index] ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right',
|
||||
'text-gray-400',
|
||||
]"
|
||||
></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 子用户列表 -->
|
||||
<view
|
||||
v-if="item.child && item.child.length > 0 && expandedItems[index]"
|
||||
class="mt-3 ml-4 pl-4 border-l-2 border-gray-100"
|
||||
>
|
||||
<view v-for="(child, childIndex) in item.child" :key="childIndex" class="py-3">
|
||||
<!-- 子用户信息 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex-1">
|
||||
<text class="text-gray-800 block">{{ child.nickname }}</text>
|
||||
<text class="text-sm text-gray-500">{{ child.createTime }}</text>
|
||||
</view>
|
||||
<view class="flex items-center space-x-2">
|
||||
<button
|
||||
class="px-2 py-1 bg-gradient-to-r from-sky-400 to-indigo-400 text-white rounded-lg text-xs flex items-center shadow-sm hover:shadow transition-all duration-300"
|
||||
@click.stop="
|
||||
navigateTo('/pages/my/spend-detail', {
|
||||
nickname: child.nickname,
|
||||
id: child.id,
|
||||
points: child.points,
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="i-carbon-money text-xs mr-1"></i>
|
||||
消费明细
|
||||
</button>
|
||||
<view
|
||||
class="w-8 h-8 flex items-center justify-center"
|
||||
@click="toggleChildExpand(index, childIndex)"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
expandedChildItems[`${index}-${childIndex}`]
|
||||
? 'i-carbon-chevron-down'
|
||||
: 'i-carbon-chevron-right',
|
||||
'text-gray-400',
|
||||
]"
|
||||
></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 孙用户列表 -->
|
||||
<view
|
||||
v-if="
|
||||
child.child &&
|
||||
child.child.length > 0 &&
|
||||
expandedChildItems[`${index}-${childIndex}`]
|
||||
"
|
||||
class="mt-3 ml-4 pl-4 border-l-2 border-gray-100"
|
||||
>
|
||||
<view
|
||||
v-for="(grandChild, grandChildIndex) in child.child"
|
||||
:key="grandChildIndex"
|
||||
class="py-3 flex items-center justify-between"
|
||||
>
|
||||
<view class="flex-1">
|
||||
<text class="text-gray-800 block">{{ grandChild.nickname }}</text>
|
||||
<text class="text-sm text-gray-500">{{ grandChild.createTime }}</text>
|
||||
</view>
|
||||
<view class="flex items-center space-x-2">
|
||||
<button
|
||||
class="px-2 py-1 bg-gradient-to-r from-sky-400 to-indigo-400 text-white rounded-lg text-xs flex items-center shadow-sm hover:shadow transition-all duration-300"
|
||||
@click.stop="
|
||||
navigateTo('/pages/my/spend-detail', {
|
||||
nickname: grandChild.nickname,
|
||||
id: grandChild.id,
|
||||
points: grandChild.points,
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="i-carbon-money text-xs mr-1"></i>
|
||||
消费明细
|
||||
</button>
|
||||
<view class="w-8 h-8 flex items-center justify-center">
|
||||
<i class="i-carbon-chevron-right text-gray-300"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 邀请说明 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-4">
|
||||
<text class="text-base font-bold text-gray-800 mb-2 block">邀请说明</text>
|
||||
<view class="space-y-2 text-sm text-gray-600">
|
||||
<text class="block">1. 邀请好友填写您的邀请码</text>
|
||||
<text class="block">2. 好友注册成功后,双方均可获得奖励</text>
|
||||
<text class="block">3. 邀请记录将在好友注册成功后显示</text>
|
||||
<text class="block">4. 如有疑问,请联系客服</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getInviteCodeAPI, getMyInviteListAPI } from '@/service/login'
|
||||
|
||||
interface InviteUserItem {
|
||||
nickname: string
|
||||
id: string | number
|
||||
createTime?: string
|
||||
child?: InviteUserItem[]
|
||||
points?: number
|
||||
}
|
||||
|
||||
const inviteCode = ref('')
|
||||
const inviteList = ref<InviteUserItem[]>([])
|
||||
const expandedItems = ref<{ [key: number]: boolean }>({})
|
||||
const expandedChildItems = ref<{ [key: string]: boolean }>({})
|
||||
|
||||
// 切换一级用户展开/收起状态
|
||||
const toggleExpand = (index: number) => {
|
||||
expandedItems.value[index] = !expandedItems.value[index]
|
||||
}
|
||||
|
||||
// 切换子用户展开/收起状态
|
||||
const toggleChildExpand = (parentIndex: number, childIndex: number) => {
|
||||
const key = `${parentIndex}-${childIndex}`
|
||||
expandedChildItems.value[key] = !expandedChildItems.value[key]
|
||||
}
|
||||
|
||||
// 获取邀请码
|
||||
const getInviteCode = async () => {
|
||||
try {
|
||||
const res = await getInviteCodeAPI()
|
||||
if (res.code === 200) {
|
||||
inviteCode.value = res.data
|
||||
console.log('邀请码:', res.data) // 添加日志查看数据
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取邀请码失败:', error) // 添加错误日志
|
||||
uni.showToast({
|
||||
title: '获取邀请码失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取邀请记录
|
||||
const getInviteList = async () => {
|
||||
try {
|
||||
const res = await getMyInviteListAPI()
|
||||
if (res.code === 200 && Array.isArray(res.data)) {
|
||||
inviteList.value = res.data
|
||||
console.log('邀请记录数据:', res.data) // 添加日志查看数据
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取邀请记录失败:', error) // 添加错误日志
|
||||
uni.showToast({
|
||||
title: '获取邀请记录失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 复制邀请码
|
||||
const copyInviteCode = () => {
|
||||
if (!inviteCode.value) {
|
||||
uni.showToast({
|
||||
title: '邀请码获取失败',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.setClipboardData({
|
||||
data: inviteCode.value,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const navigateTo = (url: string, params?: Record<string, any>) => {
|
||||
if (params) {
|
||||
console.log('传递的参数:', params) // 添加日志
|
||||
const query = Object.entries(params)
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
|
||||
.join('&')
|
||||
url = `${url}?${query}`
|
||||
console.log('最终URL:', url) // 添加日志
|
||||
}
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getInviteCode()
|
||||
getInviteList()
|
||||
})
|
||||
</script>
|
||||
122
src/pages/my/inviter.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '我的邀请人',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-6">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold text-gray-800 mb-4 block">邀请人信息</text>
|
||||
|
||||
<!-- 有邀请人时显示 -->
|
||||
<template v-if="userInfo.inviterCode">
|
||||
<view class="bg-gradient-to-r from-pink-50 to-rose-50 rounded-xl p-4 mb-4">
|
||||
<text class="text-sm text-gray-500 mb-2 block">我的邀请人代码</text>
|
||||
<text class="text-2xl font-bold text-pink-500 tracking-wider">
|
||||
{{ userInfo.inviterCode }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">感谢您使用我们的服务</text>
|
||||
</template>
|
||||
|
||||
<!-- 无邀请人时显示 -->
|
||||
<template v-else>
|
||||
<view class="bg-gray-50 rounded-xl p-6 mb-4">
|
||||
<text class="text-gray-500 mb-4 block">您还没有绑定邀请人</text>
|
||||
<button
|
||||
class="w-full bg-gradient-to-r from-pink-400 to-rose-400 text-white py-3 rounded-xl shadow-sm hover:shadow transition-all duration-300"
|
||||
@click="showBindInviter"
|
||||
>
|
||||
绑定邀请人
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/store'
|
||||
import { bindInviterAPI } from '@/service/login'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = ref<any>({})
|
||||
const inviterCode = ref('')
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
await userStore.setUserInfo()
|
||||
userInfo.value = userStore.userInfo
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '获取用户信息失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 显示绑定邀请人弹窗
|
||||
const showBindInviter = () => {
|
||||
uni.showModal({
|
||||
title: '绑定邀请人',
|
||||
editable: true,
|
||||
placeholderText: '请输入邀请人代码',
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
inviterCode.value = res.content
|
||||
await bindInviter()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 绑定邀请人
|
||||
const bindInviter = async () => {
|
||||
if (!inviterCode.value) {
|
||||
uni.showToast({
|
||||
title: '请输入邀请人代码',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await bindInviterAPI(inviterCode.value)
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '绑定成功',
|
||||
icon: 'success',
|
||||
})
|
||||
getUserInfo()
|
||||
} else {
|
||||
throw new Error(res.message || '绑定失败')
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: error.message || '绑定失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 确保弹窗样式正确加载 */
|
||||
:deep(.uni-popup) {
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
211
src/pages/my/matches.vue
Normal file
@ -0,0 +1,211 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '我的配对',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<!-- 列表内容 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="h-screen"
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<!-- 配对列表 -->
|
||||
<view class="p-4 space-y-4">
|
||||
<view
|
||||
v-for="item in matchList"
|
||||
:key="item.id"
|
||||
class="bg-white rounded-2xl shadow-sm overflow-hidden"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="flex items-start">
|
||||
<!-- 头像 -->
|
||||
<image
|
||||
class="w-20 h-20 rounded-xl"
|
||||
:src="item.avatar || defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<!-- 基本信息 -->
|
||||
<view class="flex-1 ml-4">
|
||||
<view class="flex items-center mb-1">
|
||||
<text class="text-lg font-bold text-gray-800">{{ item.nickname }}</text>
|
||||
<text class="ml-2 text-sm text-gray-500">{{ item.age }}岁</text>
|
||||
</view>
|
||||
<view class="flex items-center space-x-2 mb-2">
|
||||
<text class="text-sm text-gray-600">{{ item.height }}cm</text>
|
||||
<text class="text-sm text-gray-600">|</text>
|
||||
<text class="text-sm text-gray-600">
|
||||
{{ getEducationText(item.educationLevel) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-sm text-gray-600">
|
||||
{{ item.workArea }} · {{ item.profession }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<view class="mt-4 flex space-x-3">
|
||||
<button
|
||||
class="flex-1 bg-gradient-to-r from-sky-400 to-indigo-400 text-white text-sm py-2 rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
||||
@tap="handleViewDetail(item)"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 bg-white border border-sky-200 text-sky-600 text-sm py-2 rounded-lg hover:bg-sky-50 transition-colors"
|
||||
@tap="handleViewDetail(item)"
|
||||
>
|
||||
联系TA
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="isLoading" class="flex justify-center py-6">
|
||||
<view class="flex items-center space-x-2">
|
||||
<view class="w-2 h-2 bg-sky-500 rounded-full animate-bounce"></view>
|
||||
<view
|
||||
class="w-2 h-2 bg-sky-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 0.2s"
|
||||
></view>
|
||||
<view
|
||||
class="w-2 h-2 bg-sky-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 0.4s"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!hasMore" class="flex justify-center py-6">
|
||||
<text class="text-sm text-gray-400">没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getMyLoveListAPI } from '@/service/love'
|
||||
import type { LoveUserItem } from '@/service/love/type'
|
||||
|
||||
const defaultAvatar = '/static/images/default-avatar.png'
|
||||
|
||||
// 列表数据
|
||||
const matchList = ref<LoveUserItem[]>([])
|
||||
const isLoading = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const hasMore = ref(true)
|
||||
|
||||
// 获取学历文本
|
||||
const getEducationText = (level: string) => {
|
||||
const educationMap: Record<string, string> = {
|
||||
'1': '初中',
|
||||
'2': '高中',
|
||||
'3': '大专',
|
||||
'4': '本科',
|
||||
'5': '硕士',
|
||||
'6': '博士',
|
||||
}
|
||||
return educationMap[level] || '未知'
|
||||
}
|
||||
|
||||
// 获取配对列表
|
||||
const getMatchList = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
const res = await getMyLoveListAPI()
|
||||
if (res.code === 200) {
|
||||
matchList.value = res.data
|
||||
hasMore.value = false // 由于API没有分页,所以直接设置为没有更多
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '获取配对列表失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配对列表失败:', error)
|
||||
uni.showToast({
|
||||
title: '获取配对列表失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
isRefreshing.value = true
|
||||
await getMatchList()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (!hasMore.value || isLoading.value) return
|
||||
getMatchList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (item: LoveUserItem) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 联系TA
|
||||
const handleContact = (item: LoveUserItem) => {
|
||||
if (!item.isUnlocked) {
|
||||
uni.showToast({
|
||||
title: '请先解锁查看联系方式',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.wechatId) {
|
||||
uni.showToast({
|
||||
title: '对方暂未设置联系方式',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '联系方式',
|
||||
content: `微信号:${item.wechatId}`,
|
||||
showCancel: false,
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMatchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
</style>
|
||||
364
src/pages/my/my.vue
Normal file
@ -0,0 +1,364 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '我的',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<!-- 顶部背景 -->
|
||||
<view
|
||||
class="h-56 bg-gradient-to-br from-pink-50 via-rose-50 to-purple-50 relative overflow-hidden"
|
||||
>
|
||||
<view class="absolute top-0 left-0 right-0 bottom-0">
|
||||
<view
|
||||
class="absolute top-0 right-0 w-72 h-72 bg-gradient-to-br from-pink-200/40 to-transparent rounded-full blur-3xl"
|
||||
></view>
|
||||
<view
|
||||
class="absolute bottom-0 left-0 w-72 h-72 bg-gradient-to-tr from-rose-200/40 to-transparent rounded-full blur-3xl"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人信息卡片 -->
|
||||
<view class="mx-4 -mt-24 relative">
|
||||
<view class="bg-white/95 backdrop-blur-md rounded-3xl shadow-xl p-6 border border-pink-100">
|
||||
<view class="flex items-center">
|
||||
<view class="relative">
|
||||
<image
|
||||
class="w-24 h-24 rounded-2xl border-4 border-white shadow-xl"
|
||||
:src="userInfo.avatar || defaultAvatar"
|
||||
mode="aspectFill"
|
||||
@click="handleUploadAvatar"
|
||||
/>
|
||||
<view
|
||||
v-if="userInfo.vipLevel"
|
||||
class="absolute -bottom-2 -right-2 bg-gradient-to-r from-pink-400 to-rose-400 text-white px-3 py-1 rounded-full text-xs flex items-center shadow-lg"
|
||||
>
|
||||
<i class="i-carbon-crown text-sm mr-1"></i>
|
||||
<text>VIP{{ userInfo.points }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-1 ml-6">
|
||||
<view class="flex flex-wrap items-center gap-2 mb-3">
|
||||
<text class="text-xl font-bold text-gray-800 truncate max-w-[140px]">
|
||||
{{ userInfo.nickName }}
|
||||
</text>
|
||||
<view
|
||||
class="px-3 py-1 bg-pink-50 text-pink-600 rounded-full text-xs border border-pink-100 whitespace-nowrap"
|
||||
>
|
||||
ID: {{ userInfo.id }}
|
||||
</view>
|
||||
<view
|
||||
class="px-3 py-1 bg-rose-50 text-rose-600 rounded-full text-xs border border-rose-100 whitespace-nowrap"
|
||||
>
|
||||
<i class="i-carbon-money text-xs mr-1"></i>
|
||||
余额: {{ userInfo.points || 0 }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex space-x-3">
|
||||
<button
|
||||
class="flex-1 bg-gradient-to-r from-pink-400 to-rose-400 text-white text-sm py-2 rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center justify-center"
|
||||
@click="navigateTo('/pages/my/profile')"
|
||||
>
|
||||
<i class="i-carbon-user-profile text-base mr-2"></i>
|
||||
<text class="truncate">我的资料</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<!-- <view class="mx-4 mt-4">
|
||||
<view class="bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg p-4 border border-gray-100">
|
||||
<view class="grid grid-cols-3 gap-4">
|
||||
<view class="text-center">
|
||||
<view
|
||||
class="w-12 h-12 bg-sky-50 rounded-full flex items-center justify-center mx-auto mb-2"
|
||||
>
|
||||
<i class="i-carbon-calendar text-2xl text-sky-500"></i>
|
||||
</view>
|
||||
<text class="text-2xl font-bold text-sky-500 block">
|
||||
{{ userInfo.activityCount || 0 }}
|
||||
</text>
|
||||
<text class="text-sm text-gray-600">活动</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<view
|
||||
class="w-12 h-12 bg-indigo-50 rounded-full flex items-center justify-center mx-auto mb-2"
|
||||
>
|
||||
<i class="i-carbon-favorite text-2xl text-indigo-500"></i>
|
||||
</view>
|
||||
<text class="text-2xl font-bold text-indigo-500 block">
|
||||
{{ userInfo.matchCount || 0 }}
|
||||
</text>
|
||||
<text class="text-sm text-gray-600">配对</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<view
|
||||
class="w-12 h-12 bg-purple-50 rounded-full flex items-center justify-center mx-auto mb-2"
|
||||
>
|
||||
<i class="i-carbon-group text-2xl text-purple-500"></i>
|
||||
</view>
|
||||
<text class="text-2xl font-bold text-purple-500 block">
|
||||
{{ userInfo.groupCount || 0 }}
|
||||
</text>
|
||||
<text class="text-sm text-gray-600">群组</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="mx-4 mt-6">
|
||||
<view
|
||||
class="bg-white/95 backdrop-blur-md rounded-3xl shadow-xl overflow-hidden border border-pink-100"
|
||||
>
|
||||
<view
|
||||
class="flex items-center justify-between px-6 py-4 border-b border-pink-50 active:bg-pink-50/50 transition-colors"
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="index"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="w-12 h-12 rounded-2xl bg-gradient-to-br from-pink-50 to-rose-50 flex items-center justify-center mr-4 shadow-sm"
|
||||
>
|
||||
<i :class="item.icon" class="text-xl text-pink-500"></i>
|
||||
</view>
|
||||
<view>
|
||||
<text class="text-base font-medium text-gray-800 block">{{ item.title }}</text>
|
||||
<text class="text-xs text-gray-500">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<i class="i-carbon-chevron-right text-pink-300"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮 -->
|
||||
<view class="mx-4 my-6 mb-20">
|
||||
<button
|
||||
class="w-full h-14 bg-white text-rose-500 rounded-xl text-base font-medium flex items-center justify-center shadow-md hover:shadow-lg transition-all duration-300 border border-rose-100"
|
||||
@click="handleLogout"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 在线客服 -->
|
||||
<!-- <view class="fixed bottom-6 left-0 right-0 px-4">
|
||||
<button
|
||||
class="w-full h-12 bg-gradient-to-r from-sky-400 to-indigo-400 text-white rounded-full text-base flex items-center justify-center shadow-lg hover:shadow-xl transition-shadow"
|
||||
open-type="contact"
|
||||
>
|
||||
<i class="i-carbon-chat text-lg mr-2"></i>
|
||||
在线客服
|
||||
</button>
|
||||
</view> -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { logoutAPI, updateUserInfoAPI } from '@/service/login'
|
||||
import { uploadImageAPI } from '@/service/file'
|
||||
import type { UserInfo as ApiUserInfo } from '@/service/login/type'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const defaultAvatar = '/static/images/default-avatar.png'
|
||||
|
||||
interface UserInfo {
|
||||
avatar: string
|
||||
name: string
|
||||
vipLevel: number
|
||||
userId: string
|
||||
profileCompletion: number
|
||||
activityCount: number
|
||||
matchCount: number
|
||||
groupCount: number
|
||||
points: number
|
||||
}
|
||||
|
||||
const userInfo = ref<UserInfo>({
|
||||
avatar: '',
|
||||
name: '未设置',
|
||||
vipLevel: 0,
|
||||
userId: '---',
|
||||
profileCompletion: 0,
|
||||
activityCount: 0,
|
||||
matchCount: 0,
|
||||
groupCount: 0,
|
||||
points: 0,
|
||||
})
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: '我的活动',
|
||||
desc: '查看已参与的活动',
|
||||
path: '/pages/my/activities',
|
||||
icon: 'i-carbon-calendar',
|
||||
},
|
||||
{
|
||||
title: '我的配对',
|
||||
desc: '查看配对信息',
|
||||
path: '/pages/my/matches',
|
||||
icon: 'i-carbon-favorite',
|
||||
},
|
||||
{
|
||||
title: '邀请好友',
|
||||
desc: '邀请好友加入',
|
||||
path: '/pages/my/invite',
|
||||
icon: 'i-carbon-user-multiple',
|
||||
},
|
||||
{
|
||||
title: '消费记录',
|
||||
desc: '查看消费明细',
|
||||
path: '/pages/my/spend',
|
||||
icon: 'i-carbon-money',
|
||||
},
|
||||
{
|
||||
title: '充值记录',
|
||||
desc: '查看充值',
|
||||
path: '/pages/my/recharge',
|
||||
icon: 'i-carbon-wallet',
|
||||
},
|
||||
{
|
||||
title: '退款记录',
|
||||
desc: '查看退款',
|
||||
path: '/pages/my/refund',
|
||||
icon: 'i-carbon-money',
|
||||
},
|
||||
// {
|
||||
// title: '投诉',
|
||||
// desc: '反馈问题或违规行为',
|
||||
// path: '/pages/my/complaint',
|
||||
// icon: 'i-carbon-warning',
|
||||
// },
|
||||
{
|
||||
title: '意见反馈',
|
||||
desc: '帮助我们提供更好的服务',
|
||||
path: '/pages/my/feedback',
|
||||
icon: 'i-carbon-chat-bot',
|
||||
},
|
||||
{
|
||||
title: '我的邀请人',
|
||||
desc: '查看或绑定邀请人',
|
||||
path: '/pages/my/inviter',
|
||||
icon: 'i-carbon-user-follow',
|
||||
},
|
||||
{
|
||||
title: '我的下级',
|
||||
desc: '查看我的下级',
|
||||
path: '/pages/my/subordinate',
|
||||
icon: 'i-carbon-user-multiple',
|
||||
},
|
||||
]
|
||||
|
||||
const navigateTo = (url: string) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
const res = await logoutAPI()
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '退出成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// 清除本地存储的用户信息
|
||||
uni.removeStorageSync('x-token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '退出失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadAvatar = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0]
|
||||
try {
|
||||
// 显示上传中提示
|
||||
uni.showLoading({
|
||||
title: '上传中...',
|
||||
mask: true,
|
||||
})
|
||||
|
||||
// 直接上传临时文件路径
|
||||
const uploadRes = await uploadImageAPI(tempFilePath, 'avatar')
|
||||
|
||||
if (uploadRes.url) {
|
||||
// 更新用户头像
|
||||
const updateRes = await userStore.updateUserInfo({
|
||||
avatar: uploadRes.url,
|
||||
})
|
||||
|
||||
if (updateRes.code === 200) {
|
||||
// 更新本地用户信息
|
||||
userInfo.value.avatar = uploadRes.url
|
||||
uni.showToast({
|
||||
title: '头像更新成功',
|
||||
icon: 'success',
|
||||
})
|
||||
} else {
|
||||
throw new Error(updateRes.message)
|
||||
}
|
||||
} else {
|
||||
throw new Error('上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: error.message || '上传失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
onShow(() => {
|
||||
// 页面显示时获取用户信息
|
||||
getUserInfo()
|
||||
})
|
||||
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
await userStore.setUserInfo()
|
||||
userInfo.value = userStore.userInfo as any
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '获取用户信息失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 移除之前的 iconfont 样式,因为现在使用 Carbon 图标
|
||||
</style>
|
||||
944
src/pages/my/profile.vue
Normal file
@ -0,0 +1,944 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '我的资料',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<!-- 头像区域 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="avatar-section">
|
||||
<image class="avatar" :src="userInfo.avatar || defaultAvatar" mode="aspectFill" />
|
||||
<view class="info">
|
||||
<view class="name">{{ userInfo.nickName || '未设置昵称' }}</view>
|
||||
<view class="id">ID: {{ userInfo.id }}</view>
|
||||
<view class="location">{{ userInfo.workArea || '未设置工作地区' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资料编辑表单 -->
|
||||
<view class="mx-4">
|
||||
<!-- 基本信息 -->
|
||||
<view class="card">
|
||||
<text class="card-title">基本信息</text>
|
||||
<view class="form-item">
|
||||
<text class="label">昵称</text>
|
||||
<input v-model="userInfo.nickName" class="value" placeholder="请输入昵称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">生日</text>
|
||||
<picker mode="date" :value="userInfo.birthday" @change="onBirthdayChange" class="value">
|
||||
<text>{{ userInfo.birthday || '请选择生日' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">身高(cm)</text>
|
||||
<input v-model="userInfo.height" type="number" class="value" placeholder="请输入身高" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">体重(kg)</text>
|
||||
<input v-model="userInfo.weight" type="number" class="value" placeholder="请输入体重" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">婚姻状况</text>
|
||||
<picker
|
||||
:range="maritalStatusOptions"
|
||||
:value="maritalStatusIndex"
|
||||
@change="onMaritalStatusChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ maritalStatusOptions[maritalStatusIndex] || '请选择婚姻状况' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">血型</text>
|
||||
<input v-model="userInfo.bloodType" class="value" placeholder="请输入血型" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">民族</text>
|
||||
<input v-model="userInfo.nation" class="value" placeholder="请输入民族" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工作信息 -->
|
||||
<view class="card">
|
||||
<text class="card-title">工作信息</text>
|
||||
<view class="form-item">
|
||||
<text class="label">工作省份</text>
|
||||
<picker
|
||||
:range="provinces"
|
||||
:value="provinceIndex"
|
||||
@change="onProvinceChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ userInfo.workProvinceName || '请选择省份' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">工作城市</text>
|
||||
<picker
|
||||
:range="getCurrentCities()"
|
||||
:value="cityIndex"
|
||||
@change="onCityChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ userInfo.workCityName || '请选择城市' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">工作区县</text>
|
||||
<picker
|
||||
:range="getCurrentDistricts()"
|
||||
:value="districtIndex"
|
||||
@change="onDistrictChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ userInfo.workCountryName || '请选择区县' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">工作地址</text>
|
||||
<input v-model="userInfo.workArea" class="value" placeholder="请输入工作地址" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">工作单位</text>
|
||||
<input v-model="userInfo.workUnit" class="value" placeholder="请输入工作单位" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">单位类型</text>
|
||||
<picker
|
||||
:range="companyTypeOptions"
|
||||
:value="companyTypeIndex"
|
||||
@change="onCompanyTypeChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ companyTypeOptions[companyTypeIndex] || '请选择单位类型' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">工作行业</text>
|
||||
<input v-model="userInfo.workIndustry" class="value" placeholder="请输入工作行业" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">职业</text>
|
||||
<input v-model="userInfo.profession" class="value" placeholder="请输入职业" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">月收入</text>
|
||||
<picker
|
||||
:range="incomeOptions"
|
||||
:value="incomeIndex"
|
||||
@change="onIncomeChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ incomeOptions[incomeIndex] || '请选择月收入' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">学历</text>
|
||||
<picker
|
||||
:range="educationOptions"
|
||||
:value="educationIndex"
|
||||
@change="onEducationChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ educationOptions[educationIndex] || '请选择学历' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">毕业院校</text>
|
||||
<input v-model="userInfo.graduationSchool" class="value" placeholder="请输入毕业院校" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 住房资产 -->
|
||||
<view class="card">
|
||||
<text class="card-title">住房资产</text>
|
||||
<view class="form-item">
|
||||
<text class="label">住房状况</text>
|
||||
<picker
|
||||
:range="housingStatusOptions"
|
||||
:value="housingStatusIndex"
|
||||
@change="onHousingStatusChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ housingStatusOptions[housingStatusIndex] || '请选择住房状况' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">购车情况</text>
|
||||
<picker
|
||||
:range="carOwnershipOptions"
|
||||
:value="carOwnershipIndex"
|
||||
@change="onCarOwnershipChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ carOwnershipOptions[carOwnershipIndex] || '请选择购车情况' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 家庭背景 -->
|
||||
<view class="card">
|
||||
<text class="card-title">家庭背景</text>
|
||||
<view class="form-item">
|
||||
<text class="label">父母状况</text>
|
||||
<picker
|
||||
:range="parentsStatusOptions"
|
||||
:value="parentsStatusIndex"
|
||||
@change="onParentsStatusChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ parentsStatusOptions[parentsStatusIndex] || '请选择父母状况' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">兄弟姐妹</text>
|
||||
<picker
|
||||
:range="siblingsOptions"
|
||||
:value="siblingsIndex"
|
||||
@change="onSiblingsChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ siblingsOptions[siblingsIndex] || '请选择兄弟姐妹' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">与父母同住</text>
|
||||
<picker
|
||||
:range="liveWithParentsOptions"
|
||||
:value="liveWithParentsIndex"
|
||||
@change="onLiveWithParentsChange"
|
||||
class="value"
|
||||
>
|
||||
<text>
|
||||
{{ liveWithParentsOptions[liveWithParentsIndex] || '请选择是否与父母同住' }}
|
||||
</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">婚娶形式</text>
|
||||
<picker
|
||||
:range="marriageFormOptions"
|
||||
:value="marriageFormIndex"
|
||||
@change="onMarriageFormChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ marriageFormOptions[marriageFormIndex] || '请选择婚娶形式' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">子女情况</text>
|
||||
<picker
|
||||
:range="childrenStatusOptions"
|
||||
:value="childrenStatusIndex"
|
||||
@change="onChildrenStatusChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ childrenStatusOptions[childrenStatusIndex] || '请选择子女情况' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 生活习惯 -->
|
||||
<view class="card">
|
||||
<text class="card-title">生活习惯</text>
|
||||
<view class="form-item">
|
||||
<text class="label">作息习惯</text>
|
||||
<picker
|
||||
:range="sleepHabitsOptions"
|
||||
:value="sleepHabitsIndex"
|
||||
@change="onSleepHabitsChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ sleepHabitsOptions[sleepHabitsIndex] || '请选择作息习惯' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">锻炼习惯</text>
|
||||
<picker
|
||||
:range="exerciseHabitsOptions"
|
||||
:value="exerciseHabitsIndex"
|
||||
@change="onExerciseHabitsChange"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ exerciseHabitsOptions[exerciseHabitsIndex] || '请选择锻炼习惯' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">是否吸烟</text>
|
||||
<switch
|
||||
:checked="userInfo.smoke === 1"
|
||||
@change="(e) => (userInfo.smoke = e.detail.value ? 1 : 0)"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">是否饮酒</text>
|
||||
<switch
|
||||
:checked="userInfo.drink === 1"
|
||||
@change="(e) => (userInfo.drink = e.detail.value ? 1 : 0)"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">兴趣爱好</text>
|
||||
<input v-model="userInfo.hobbies" class="value" placeholder="请输入兴趣爱好" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 择偶要求 -->
|
||||
<view class="card">
|
||||
<text class="card-title">择偶要求</text>
|
||||
<view class="form-item">
|
||||
<text class="label">期望结婚时间</text>
|
||||
<picker
|
||||
:range="expectedMarriageTimeOptions"
|
||||
:value="expectedMarriageTimeIndex"
|
||||
@change="onExpectedMarriageTimeChange"
|
||||
class="value"
|
||||
>
|
||||
<text>
|
||||
{{ expectedMarriageTimeOptions[expectedMarriageTimeIndex] || '请选择期望结婚时间' }}
|
||||
</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">婚况要求</text>
|
||||
<checkbox-group @change="onPartnerMaritalStatusChange">
|
||||
<label v-for="(item, index) in partnerMaritalStatusOptions" :key="index">
|
||||
<checkbox
|
||||
:value="item.value"
|
||||
:checked="selectedPartnerMaritalStatus.includes(item.value)"
|
||||
/>
|
||||
<text>{{ item.label }}</text>
|
||||
</label>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">年龄范围</text>
|
||||
<input v-model="userInfo.ageRange" class="value" placeholder="请输入年龄范围" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">身高范围</text>
|
||||
<input v-model="userInfo.heightRange" class="value" placeholder="请输入身高范围" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">最低学历</text>
|
||||
<picker
|
||||
:range="educationOptions"
|
||||
:value="Number(userInfo.minEducation) - 1"
|
||||
@change="(e) => (userInfo.minEducation = String(Number(e.detail.value) + 1))"
|
||||
class="value"
|
||||
>
|
||||
<text>
|
||||
{{ educationOptions[Number(userInfo.minEducation) - 1] || '请选择最低学历' }}
|
||||
</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">最低收入</text>
|
||||
<picker
|
||||
:range="incomeOptions"
|
||||
:value="Number(userInfo.minIncome) - 1"
|
||||
@change="(e) => (userInfo.minIncome = String(Number(e.detail.value) + 1))"
|
||||
class="value"
|
||||
>
|
||||
<text>{{ incomeOptions[Number(userInfo.minIncome) - 1] || '请选择最低收入' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">工作地区要求</text>
|
||||
<input
|
||||
v-model="userInfo.partnerWorkArea"
|
||||
class="value"
|
||||
placeholder="请输入工作地区要求"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">其他要求</text>
|
||||
<input v-model="userInfo.otherRequirements" class="value" placeholder="请输入其他要求" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<view class="card">
|
||||
<text class="card-title">联系方式</text>
|
||||
<view class="form-item">
|
||||
<text class="label">手机号</text>
|
||||
<input v-model="userInfo.phone" type="number" class="value" placeholder="请输入手机号" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">微信号</text>
|
||||
<input v-model="userInfo.wechat" class="value" placeholder="请输入微信号" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">QQ</text>
|
||||
<input v-model="userInfo.qq" type="number" class="value" placeholder="请输入QQ号" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">邮箱</text>
|
||||
<input v-model="userInfo.email" class="value" placeholder="请输入邮箱" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自我介绍 -->
|
||||
<view class="card">
|
||||
<text class="card-title">自我介绍</text>
|
||||
<textarea v-model="userInfo.selfIntroduction" placeholder="请输入自我介绍" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<button class="save-button" @click="saveUserInfo">保存修改</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { UserInfo } from '@/service/login/type'
|
||||
import { useUserStore } from '@/store'
|
||||
import { provinces, cities, districts } from '@/utils/area-data'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const defaultAvatar = '/static/images/default-avatar.png'
|
||||
const userInfo = ref<UserInfo>({} as UserInfo)
|
||||
|
||||
// 婚姻状况选项
|
||||
const maritalStatusOptions = ['未婚', '离异', '丧偶']
|
||||
const maritalStatusIndex = ref(0)
|
||||
|
||||
// 月收入选项
|
||||
const incomeOptions = [
|
||||
'3000以下',
|
||||
'3000~5000',
|
||||
'5000~8000',
|
||||
'8000~10000',
|
||||
'10000~20000',
|
||||
'20000以上',
|
||||
]
|
||||
const incomeIndex = ref(0)
|
||||
|
||||
// 学历选项
|
||||
const educationOptions = ['初中', '高中', '大专', '本科', '硕士', '博士']
|
||||
const educationIndex = ref(0)
|
||||
|
||||
// 住房状况选项
|
||||
const housingStatusOptions = [
|
||||
'已购房(有贷款)',
|
||||
'已购房(无贷款)',
|
||||
'有能力购房',
|
||||
'无房',
|
||||
'无房希望对方解决',
|
||||
'无房希望双方解决',
|
||||
'与父母同住',
|
||||
'独自租房',
|
||||
'与人合租',
|
||||
'住单位房',
|
||||
]
|
||||
const housingStatusIndex = ref(0)
|
||||
|
||||
// 购车情况选项
|
||||
const carOwnershipOptions = [
|
||||
'无车',
|
||||
'已购车(经济型)',
|
||||
'已购车(中档型)',
|
||||
'已购车(豪华型)',
|
||||
'单位用车',
|
||||
'需要时购置',
|
||||
]
|
||||
const carOwnershipIndex = ref(0)
|
||||
|
||||
// 期望结婚时间选项
|
||||
const expectedMarriageTimeOptions = ['随时', '半年内', '一年内', '两年内', '三年内']
|
||||
const expectedMarriageTimeIndex = ref(0)
|
||||
|
||||
// 父母状况选项
|
||||
const parentsStatusOptions = ['父母均建在', '只有母亲建在', '只有父亲建在', '父母均以离世']
|
||||
const parentsStatusIndex = ref(0)
|
||||
|
||||
// 兄弟姐妹选项
|
||||
const siblingsOptions = ['独生子女', '2个', '3个', '4个', '5个']
|
||||
const siblingsIndex = ref(0)
|
||||
|
||||
// 与父母同住选项
|
||||
const liveWithParentsOptions = ['愿意', '不愿意', '视具体情况而定', '尊重伴侣意见']
|
||||
const liveWithParentsIndex = ref(0)
|
||||
|
||||
// 婚娶形式选项
|
||||
const marriageFormOptions = ['嫁娶', '两顾', '上门']
|
||||
const marriageFormIndex = ref(0)
|
||||
|
||||
// 子女情况选项
|
||||
const childrenStatusOptions = ['未育', '子女归自己', '子女归对方']
|
||||
const childrenStatusIndex = ref(0)
|
||||
|
||||
// 修改择偶婚况选项为数字代码
|
||||
const partnerMaritalStatusOptions = [
|
||||
{ label: '未婚', value: '1' },
|
||||
{ label: '离异', value: '2' },
|
||||
{ label: '丧偶', value: '3' },
|
||||
]
|
||||
const selectedPartnerMaritalStatus = ref<string[]>([])
|
||||
|
||||
// 作息习惯选项
|
||||
const sleepHabitsOptions = [
|
||||
'早睡早起很规律',
|
||||
'经常夜猫子',
|
||||
'总是早起鸟',
|
||||
'偶尔懒散一下',
|
||||
'没有规律',
|
||||
]
|
||||
const sleepHabitsIndex = ref(0)
|
||||
|
||||
// 锻炼习惯选项
|
||||
const exerciseHabitsOptions = [
|
||||
'每天锻炼',
|
||||
'每周至少一次',
|
||||
'每月几次',
|
||||
'没时间锻炼',
|
||||
'集中时间锻炼',
|
||||
'不喜欢锻炼',
|
||||
]
|
||||
const exerciseHabitsIndex = ref(0)
|
||||
|
||||
// 单位类型选项
|
||||
const companyTypeOptions = [
|
||||
'政府机关',
|
||||
'事业单位',
|
||||
'外资企业',
|
||||
'合资企业',
|
||||
'国营企业',
|
||||
'私营企业',
|
||||
'自有公司',
|
||||
'其他',
|
||||
]
|
||||
const companyTypeIndex = ref(0)
|
||||
|
||||
// 省市区选择器
|
||||
const provinceIndex = ref(0)
|
||||
const cityIndex = ref(0)
|
||||
const districtIndex = ref(0)
|
||||
|
||||
// 当前选中的省市区数据
|
||||
const currentProvince = ref('')
|
||||
const currentCity = ref('')
|
||||
const currentDistrict = ref('')
|
||||
|
||||
// 获取当前省份的城市列表
|
||||
const getCurrentCities = () => {
|
||||
if (!currentProvince.value) return []
|
||||
return cities[currentProvince.value] || []
|
||||
}
|
||||
|
||||
// 获取当前城市的区县列表
|
||||
const getCurrentDistricts = () => {
|
||||
if (!currentProvince.value || !currentCity.value) return []
|
||||
return districts[`${currentProvince.value}-${currentCity.value}`] || []
|
||||
}
|
||||
|
||||
// 省份选择变化
|
||||
const onProvinceChange = (e: any) => {
|
||||
const index = e.detail.value
|
||||
provinceIndex.value = index
|
||||
currentProvince.value = provinces[index]
|
||||
cityIndex.value = 0
|
||||
districtIndex.value = 0
|
||||
currentCity.value = getCurrentCities()[0] || ''
|
||||
currentDistrict.value = getCurrentDistricts()[0] || ''
|
||||
|
||||
// 更新用户信息
|
||||
userInfo.value.workProvinceName = currentProvince.value
|
||||
userInfo.value.workCityName = currentCity.value
|
||||
userInfo.value.workCountryName = currentDistrict.value
|
||||
}
|
||||
|
||||
// 城市选择变化
|
||||
const onCityChange = (e: any) => {
|
||||
const index = e.detail.value
|
||||
cityIndex.value = index
|
||||
currentCity.value = getCurrentCities()[index]
|
||||
districtIndex.value = 0
|
||||
currentDistrict.value = getCurrentDistricts()[0] || ''
|
||||
|
||||
// 更新用户信息
|
||||
userInfo.value.workCityName = currentCity.value
|
||||
userInfo.value.workCountryName = currentDistrict.value
|
||||
}
|
||||
|
||||
// 区县选择变化
|
||||
const onDistrictChange = (e: any) => {
|
||||
const index = e.detail.value
|
||||
districtIndex.value = index
|
||||
currentDistrict.value = getCurrentDistricts()[index]
|
||||
|
||||
// 更新用户信息
|
||||
userInfo.value.workCountryName = currentDistrict.value
|
||||
}
|
||||
|
||||
// 在获取用户信息时设置省市区索引
|
||||
const setAreaIndexes = () => {
|
||||
// 设置省份索引
|
||||
const provinceIdx = provinces.findIndex((p) => p === userInfo.value.workProvinceName)
|
||||
if (provinceIdx !== -1) {
|
||||
provinceIndex.value = provinceIdx
|
||||
currentProvince.value = provinces[provinceIdx]
|
||||
|
||||
// 设置城市索引
|
||||
const cityList = getCurrentCities()
|
||||
const cityIdx = cityList.findIndex((c) => c === userInfo.value.workCityName)
|
||||
if (cityIdx !== -1) {
|
||||
cityIndex.value = cityIdx
|
||||
currentCity.value = cityList[cityIdx]
|
||||
|
||||
// 设置区县索引
|
||||
const districtList = getCurrentDistricts()
|
||||
const districtIdx = districtList.findIndex((d) => d === userInfo.value.workCountryName)
|
||||
if (districtIdx !== -1) {
|
||||
districtIndex.value = districtIdx
|
||||
currentDistrict.value = districtList[districtIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
const res = await userStore.setUserInfo()
|
||||
if (res.code === 200) {
|
||||
userInfo.value = res.data
|
||||
// 确保nickName字段存在
|
||||
if (!userInfo.value.nickName && userInfo.value.nickname) {
|
||||
userInfo.value.nickName = userInfo.value.nickname
|
||||
}
|
||||
// 设置各个选项的索引
|
||||
maritalStatusIndex.value = Number(userInfo.value.maritalStatus) - 1
|
||||
incomeIndex.value = Number(userInfo.value.monthlyIncome) - 1
|
||||
educationIndex.value = Number(userInfo.value.education) - 1
|
||||
housingStatusIndex.value = Number(userInfo.value.housingStatus) - 1
|
||||
carOwnershipIndex.value = Number(userInfo.value.carOwnership) - 1
|
||||
expectedMarriageTimeIndex.value = Number(userInfo.value.expectedMarriageTime) - 1
|
||||
parentsStatusIndex.value = Number(userInfo.value.parentsStatus) - 1
|
||||
siblingsIndex.value = Number(userInfo.value.siblings) - 1
|
||||
liveWithParentsIndex.value = Number(userInfo.value.liveWithParents) - 1
|
||||
marriageFormIndex.value = Number(userInfo.value.marriageForm) - 1
|
||||
childrenStatusIndex.value = Number(userInfo.value.childrenStatus) - 1
|
||||
sleepHabitsIndex.value = Number(userInfo.value.sleepHabits) - 1
|
||||
exerciseHabitsIndex.value = Number(userInfo.value.exerciseHabits) - 1
|
||||
companyTypeIndex.value = Number(userInfo.value.companyType) - 1
|
||||
|
||||
// 设置省市区索引
|
||||
setAreaIndexes()
|
||||
|
||||
// 确保minEducation和minIncome是字符串类型
|
||||
if (typeof userInfo.value.minEducation !== 'string') {
|
||||
userInfo.value.minEducation = String(userInfo.value.minEducation || '1')
|
||||
}
|
||||
if (typeof userInfo.value.minIncome !== 'string') {
|
||||
userInfo.value.minIncome = String(userInfo.value.minIncome || '1')
|
||||
}
|
||||
|
||||
// 确保smoke和drink是数字类型
|
||||
if (typeof userInfo.value.smoke !== 'number') {
|
||||
userInfo.value.smoke = Number(userInfo.value.smoke || 0)
|
||||
}
|
||||
if (typeof userInfo.value.drink !== 'number') {
|
||||
userInfo.value.drink = Number(userInfo.value.drink || 0)
|
||||
}
|
||||
|
||||
// 设置多选值
|
||||
if (typeof userInfo.value.partnerMaritalStatus === 'string') {
|
||||
selectedPartnerMaritalStatus.value = userInfo.value.partnerMaritalStatus
|
||||
? userInfo.value.partnerMaritalStatus.split(',')
|
||||
: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '获取用户信息失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
const saveUserInfo = async () => {
|
||||
try {
|
||||
// 处理多选值
|
||||
userInfo.value.partnerMaritalStatus = selectedPartnerMaritalStatus.value.join(',')
|
||||
|
||||
const res = await userStore.updateUserInfo(userInfo.value)
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存用户信息失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 各种选择器的change事件处理函数
|
||||
const onMaritalStatusChange = (e: any) => {
|
||||
maritalStatusIndex.value = e.detail.value
|
||||
userInfo.value.maritalStatus = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onHousingStatusChange = (e: any) => {
|
||||
housingStatusIndex.value = e.detail.value
|
||||
userInfo.value.housingStatus = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onCarOwnershipChange = (e: any) => {
|
||||
carOwnershipIndex.value = e.detail.value
|
||||
userInfo.value.carOwnership = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onExpectedMarriageTimeChange = (e: any) => {
|
||||
expectedMarriageTimeIndex.value = e.detail.value
|
||||
userInfo.value.expectedMarriageTime = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onParentsStatusChange = (e: any) => {
|
||||
parentsStatusIndex.value = e.detail.value
|
||||
userInfo.value.parentsStatus = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onSiblingsChange = (e: any) => {
|
||||
siblingsIndex.value = e.detail.value
|
||||
userInfo.value.siblings = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onLiveWithParentsChange = (e: any) => {
|
||||
liveWithParentsIndex.value = e.detail.value
|
||||
userInfo.value.liveWithParents = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onMarriageFormChange = (e: any) => {
|
||||
marriageFormIndex.value = e.detail.value
|
||||
userInfo.value.marriageForm = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onChildrenStatusChange = (e: any) => {
|
||||
childrenStatusIndex.value = e.detail.value
|
||||
userInfo.value.childrenStatus = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onSleepHabitsChange = (e: any) => {
|
||||
sleepHabitsIndex.value = e.detail.value
|
||||
userInfo.value.sleepHabits = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onExerciseHabitsChange = (e: any) => {
|
||||
exerciseHabitsIndex.value = e.detail.value
|
||||
userInfo.value.exerciseHabits = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
const onCompanyTypeChange = (e: any) => {
|
||||
companyTypeIndex.value = e.detail.value
|
||||
userInfo.value.companyType = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
// 多选处理函数
|
||||
const onPartnerMaritalStatusChange = (e: any) => {
|
||||
selectedPartnerMaritalStatus.value = e.detail.value
|
||||
}
|
||||
|
||||
// 生日选择器变化
|
||||
const onBirthdayChange = (e: any) => {
|
||||
userInfo.value.birthday = e.detail.value
|
||||
}
|
||||
|
||||
// 收入选择器变化
|
||||
const onIncomeChange = (e: any) => {
|
||||
incomeIndex.value = e.detail.value
|
||||
userInfo.value.monthlyIncome = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
// 学历选择器变化
|
||||
const onEducationChange = (e: any) => {
|
||||
educationIndex.value = e.detail.value
|
||||
userInfo.value.education = String(Number(e.detail.value) + 1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 输入框样式
|
||||
input {
|
||||
min-width: 200rpx;
|
||||
text-align: right;
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
// 选择器样式
|
||||
picker {
|
||||
min-width: 200rpx;
|
||||
text-align: right;
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
// 多选框组样式
|
||||
checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx 0;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
checkbox {
|
||||
transform: scale(0.8);
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开关样式
|
||||
switch {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
// 表单项样式
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本域样式
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
// 保存按钮样式
|
||||
.save-button {
|
||||
position: fixed;
|
||||
bottom: 48rpx;
|
||||
left: 48rpx;
|
||||
right: 48rpx;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(to right, #3b82f6, #6366f1);
|
||||
color: white;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.card {
|
||||
background-color: white;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 头像区域样式
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
background-color: white;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
border: 4rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 24rpx;
|
||||
flex: 1;
|
||||
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.id {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.location {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
157
src/pages/my/recharge.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '充值记录',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<!-- 消费统计 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-6">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold text-gray-800 mb-2 block">充值统计</text>
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-sky-500 block">¥{{ totalSpend }}</text>
|
||||
<text class="text-sm text-gray-500">总充值数</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-indigo-500 block">{{ spendCount }}</text>
|
||||
<text class="text-sm text-gray-500">充值次数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消费记录列表 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||
<view class="p-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<text class="text-base font-bold text-gray-800">充值明细</text>
|
||||
<text class="text-sm text-sky-500" @tap="refreshData">刷新</text>
|
||||
</view>
|
||||
<view v-if="loading" class="p-8 text-center">
|
||||
<text class="text-gray-500">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="error" class="p-8 text-center">
|
||||
<text class="text-red-500">{{ error }}</text>
|
||||
</view>
|
||||
<view v-else-if="spendList.length === 0" class="p-8 text-center">
|
||||
<text class="text-gray-500">暂无充值记录</text>
|
||||
</view>
|
||||
<view v-else class="divide-y divide-gray-100">
|
||||
<view
|
||||
v-for="(item, index) in spendList"
|
||||
:key="index"
|
||||
class="p-4 flex items-center justify-between"
|
||||
>
|
||||
<view>
|
||||
<view class="flex items-center mb-1">
|
||||
<view
|
||||
class="bg-pink-50 text-pink-600 px-2 py-0.5 rounded-full text-xs border border-pink-100 mr-2"
|
||||
>
|
||||
订单号
|
||||
</view>
|
||||
<text class="text-gray-800 font-mono">{{ item.orderNo }}</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">{{ formatDate(item.createTime) }}</text>
|
||||
</view>
|
||||
<text class="text-lg font-bold text-green-500">
|
||||
+¥{{ formatPrice(item.payPoints) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserPayInfoAPI } from '@/service/login'
|
||||
import type { PayInfoList } from '@/service/login/type'
|
||||
|
||||
const spendList = ref<PayInfoList[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// 计算总消费
|
||||
const totalSpend = computed(() => {
|
||||
return spendList.value.reduce((sum, item) => sum + Number(item.payPoints), 0).toFixed(2)
|
||||
})
|
||||
|
||||
// 计算消费次数
|
||||
const spendCount = computed(() => spendList.value.length)
|
||||
|
||||
// 获取消费类型文本
|
||||
const getSpendTypeText = (type: number) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '解锁用户',
|
||||
2: '充值',
|
||||
3: '其他',
|
||||
}
|
||||
return typeMap[type] || '未知类型'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
} catch (e) {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price: string) => {
|
||||
try {
|
||||
return Number(price).toFixed(2)
|
||||
} catch (e) {
|
||||
return '0.00'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取消费记录
|
||||
const getSpendList = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await getUserPayInfoAPI()
|
||||
if (res.code === 200) {
|
||||
spendList.value = res.data || []
|
||||
} else {
|
||||
error.value = res.message || '获取充值记录失败'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = '网络请求失败,请稍后重试'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
getSpendList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSpendList()
|
||||
})
|
||||
</script>
|
||||
240
src/pages/my/refund.vue
Normal file
@ -0,0 +1,240 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '退款记录',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<!-- 消费统计 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-6">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold text-gray-800 mb-2 block">退款统计</text>
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-sky-500 block">¥{{ totalSpend }}</text>
|
||||
<text class="text-sm text-gray-500">总退款数</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-indigo-500 block">{{ spendCount }}</text>
|
||||
<text class="text-sm text-gray-500">退款次数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消费记录列表 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||
<view class="p-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<text class="text-base font-bold text-gray-800">退款明细</text>
|
||||
<text class="text-sm text-sky-500" @tap="refreshData">刷新</text>
|
||||
</view>
|
||||
<view v-if="loading" class="p-8 text-center">
|
||||
<text class="text-gray-500">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="error" class="p-8 text-center">
|
||||
<text class="text-red-500">{{ error }}</text>
|
||||
</view>
|
||||
<view v-else-if="spendList.length === 0" class="p-8 text-center">
|
||||
<text class="text-gray-500">暂无退款记录</text>
|
||||
</view>
|
||||
<view v-else class="divide-y divide-gray-100">
|
||||
<view v-for="(item, index) in spendList" :key="index" class="p-4">
|
||||
<!-- 顶部:退款金额和状态 -->
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<text class="text-lg font-bold text-rose-500">
|
||||
-¥{{ formatPrice(item.refundPoints) }}
|
||||
</text>
|
||||
<view
|
||||
:class="[
|
||||
'px-3 py-1 rounded-full text-xs border',
|
||||
item.refundStatus === '0'
|
||||
? 'bg-yellow-50 text-yellow-600 border-yellow-100'
|
||||
: item.refundStatus === '1'
|
||||
? 'bg-green-50 text-green-600 border-green-100'
|
||||
: 'bg-red-50 text-red-600 border-red-100',
|
||||
]"
|
||||
>
|
||||
{{ getRefundStatusText(item.refundStatus) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间:退款原因 -->
|
||||
<view class="mb-3">
|
||||
<view class="flex items-center mb-1">
|
||||
<view
|
||||
class="bg-sky-50 text-sky-600 px-2 py-0.5 rounded-full text-xs border border-sky-100 mr-2"
|
||||
>
|
||||
退款原因
|
||||
</view>
|
||||
<text class="text-gray-600 text-sm flex-1">{{ item.refundReason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部:时间和操作按钮 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex flex-col space-y-1">
|
||||
<text class="text-xs text-gray-500">
|
||||
申请时间:{{ formatDate(item.createTime) }}
|
||||
</text>
|
||||
<text v-if="item.auditTime" class="text-xs text-gray-500">
|
||||
审核时间:{{ formatDate(item.auditTime) }}
|
||||
</text>
|
||||
</view>
|
||||
<button
|
||||
v-if="item.refundStatus === '0'"
|
||||
class="flex items-center justify-center bg-white border border-green-200 text-green-500 px-4 py-1.5 rounded-lg text-xs hover:bg-green-50 active:bg-green-100 transition-colors mr-0"
|
||||
@tap="handleCancelRefund(item.id)"
|
||||
>
|
||||
<i class="i-carbon-close text-sm mr-1"></i>
|
||||
撤销
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserRefundInfoAPI, cancelRefundAPI } from '@/service/login'
|
||||
import type { RefundInfoList } from '@/service/login/type'
|
||||
|
||||
const spendList = ref<RefundInfoList[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// 计算总消费
|
||||
const totalSpend = computed(() => {
|
||||
return spendList.value.reduce((sum, item) => sum + Number(item.refundPoints), 0).toFixed(2)
|
||||
})
|
||||
|
||||
// 计算消费次数
|
||||
const spendCount = computed(() => spendList.value.length)
|
||||
|
||||
// 获取消费类型文本
|
||||
const getSpendTypeText = (type: number) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '解锁用户',
|
||||
2: '充值',
|
||||
3: '其他',
|
||||
}
|
||||
return typeMap[type] || '未知类型'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
} catch (e) {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price: string) => {
|
||||
try {
|
||||
return Number(price).toFixed(2)
|
||||
} catch (e) {
|
||||
return '0.00'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取消费记录
|
||||
const getSpendList = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await getUserRefundInfoAPI()
|
||||
if (res.code === 200) {
|
||||
spendList.value = res.data || []
|
||||
} else {
|
||||
error.value = res.message || '获取充值记录失败'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = '网络请求失败,请稍后重试'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
getSpendList()
|
||||
}
|
||||
|
||||
// 获取退款状态文本
|
||||
const getRefundStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'0': '审核中',
|
||||
'1': '已通过',
|
||||
'2': '已拒绝',
|
||||
}
|
||||
return statusMap[status] || '未知状态'
|
||||
}
|
||||
|
||||
// 处理撤销退款
|
||||
const handleCancelRefund = async (id: string) => {
|
||||
try {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要撤销该退款申请吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({
|
||||
title: '处理中...',
|
||||
mask: true,
|
||||
})
|
||||
|
||||
const result = await cancelRefundAPI(id)
|
||||
|
||||
if (result.code === 200) {
|
||||
uni.showToast({
|
||||
title: '撤销成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// 刷新列表
|
||||
getSpendList()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '撤销失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '操作失败,请稍后重试',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSpendList()
|
||||
})
|
||||
</script>
|
||||
181
src/pages/my/spend-detail.vue
Normal file
@ -0,0 +1,181 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '消费记录',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<!-- 消费统计 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-6">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold text-gray-800 mb-2 block">消费统计</text>
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-sky-500 block">¥{{ totalSpend }}</text>
|
||||
<text class="text-sm text-gray-500">总消费</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-indigo-500 block">{{ spendCount }}</text>
|
||||
<text class="text-sm text-gray-500">消费次数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消费记录列表 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||
<view class="p-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<text class="text-base font-bold text-gray-800">消费明细</text>
|
||||
<text class="text-sm text-sky-500" @tap="refreshData">刷新</text>
|
||||
</view>
|
||||
<view v-if="loading" class="p-8 text-center">
|
||||
<text class="text-gray-500">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="error" class="p-8 text-center">
|
||||
<text class="text-red-500">{{ error }}</text>
|
||||
</view>
|
||||
<view v-else-if="spendList.length === 0" class="p-8 text-center">
|
||||
<text class="text-gray-500">暂无消费记录</text>
|
||||
</view>
|
||||
<view v-else class="divide-y divide-gray-100">
|
||||
<view
|
||||
v-for="(item, index) in spendList"
|
||||
:key="index"
|
||||
class="p-4 flex items-center justify-between"
|
||||
>
|
||||
<view>
|
||||
<text
|
||||
:class="[
|
||||
'block text-sm',
|
||||
item.spendType === '1'
|
||||
? 'text-yellow-300 font-semibold border-l-2 border-purple-500 pl-2'
|
||||
: item.spendType === '2'
|
||||
? 'text-emerald-300 font-semibold border-l-2 border-emerald-500 pl-2'
|
||||
: 'text-gray-600 border-l-2 border-gray-300 pl-2',
|
||||
]"
|
||||
>
|
||||
{{ getSpendTypeText(item.spendType) }}
|
||||
</text>
|
||||
<text class="text-sm text-gray-500">{{ formatDate(item.createTime) }}</text>
|
||||
</view>
|
||||
<text class="text-lg font-bold text-red-500">-¥{{ formatPrice(item.spendPrice) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserSpendHistoryAPI } from '@/service/login'
|
||||
import type { SpendHistoryItem } from '@/service/login/type'
|
||||
|
||||
const spendList = ref<SpendHistoryItem[]>([])
|
||||
const userId = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// 获取消费记录
|
||||
const getSpendList = async () => {
|
||||
if (!userId.value) {
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await getUserSpendHistoryAPI(userId.value)
|
||||
if (res.code === 200) {
|
||||
spendList.value = res.data || []
|
||||
} else {
|
||||
error.value = res.message || '获取消费记录失败'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = '网络请求失败,请稍后重试'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
getSpendList()
|
||||
}
|
||||
|
||||
// 计算总消费
|
||||
const totalSpend = computed(() => {
|
||||
return spendList.value.reduce((sum, item) => sum + Number(item.spendPrice), 0).toFixed(2)
|
||||
})
|
||||
|
||||
// 计算消费次数
|
||||
const spendCount = computed(() => spendList.value.length)
|
||||
|
||||
// 获取消费类型文本
|
||||
const getSpendTypeText = (type: number) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '解锁用户',
|
||||
2: '参加活动',
|
||||
3: '其他',
|
||||
}
|
||||
return typeMap[type] || '未知类型'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
} catch (e) {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price: string) => {
|
||||
try {
|
||||
return Number(price).toFixed(2)
|
||||
} catch (e) {
|
||||
return '0.00'
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取参数
|
||||
onLoad((options: Record<string, string>) => {
|
||||
console.log('onLoad参数:', options)
|
||||
if (options.id) {
|
||||
userId.value = options.id
|
||||
console.log('设置的用户ID:', userId.value)
|
||||
getSpendList()
|
||||
} else {
|
||||
error.value = '未获取到用户信息'
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getSpendList()
|
||||
})
|
||||
</script>
|
||||
185
src/pages/my/spend.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '消费记录',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<!-- 消费统计 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-6">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold text-gray-800 mb-2 block">消费统计</text>
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-sky-500 block">¥{{ totalSpend }}</text>
|
||||
<text class="text-sm text-gray-500">总消费</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-2xl font-bold text-indigo-500 block">{{ spendCount }}</text>
|
||||
<text class="text-sm text-gray-500">消费次数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消费记录列表 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||
<view class="p-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<text class="text-base font-bold text-gray-800">消费明细</text>
|
||||
<text class="text-sm text-sky-500" @tap="refreshData">刷新</text>
|
||||
</view>
|
||||
<view v-if="loading" class="p-8 text-center">
|
||||
<text class="text-gray-500">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="error" class="p-8 text-center">
|
||||
<text class="text-red-500">{{ error }}</text>
|
||||
</view>
|
||||
<view v-else-if="spendList.length === 0" class="p-8 text-center">
|
||||
<text class="text-gray-500">暂无消费记录</text>
|
||||
</view>
|
||||
<view v-else class="divide-y divide-gray-100">
|
||||
<view
|
||||
v-for="(item, index) in spendList"
|
||||
:key="index"
|
||||
class="p-4 flex items-center justify-between"
|
||||
>
|
||||
<view>
|
||||
<text
|
||||
:class="[
|
||||
'block text-sm',
|
||||
item.spendType === '1'
|
||||
? 'text-yellow-300 font-semibold border-l-2 border-purple-500 pl-2'
|
||||
: item.spendType === '2'
|
||||
? 'text-emerald-300 font-semibold border-l-2 border-emerald-500 pl-2'
|
||||
: 'text-gray-600 border-l-2 border-gray-300 pl-2',
|
||||
]"
|
||||
>
|
||||
{{ getSpendTypeText(item.spendType) }}
|
||||
</text>
|
||||
<text class="text-sm text-gray-500">{{ formatDate(item.createTime) }}</text>
|
||||
</view>
|
||||
<text class="text-lg font-bold text-red-500">-¥{{ formatPrice(item.spendPrice) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserSpendHistoryAPI } from '@/service/login'
|
||||
import type { SpendHistoryItem } from '@/service/login/type'
|
||||
|
||||
const spendList = ref<SpendHistoryItem[]>([])
|
||||
const userId = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// 获取用户ID
|
||||
const getUserInfo = () => {
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('loginData')
|
||||
if (!userInfo || !userInfo.id) {
|
||||
error.value = '未获取到用户信息,请重新登录'
|
||||
return false
|
||||
}
|
||||
userId.value = userInfo.id
|
||||
return true
|
||||
} catch (e) {
|
||||
error.value = '获取用户信息失败'
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总消费
|
||||
const totalSpend = computed(() => {
|
||||
return spendList.value.reduce((sum, item) => sum + Number(item.spendPrice), 0).toFixed(2)
|
||||
})
|
||||
|
||||
// 计算消费次数
|
||||
const spendCount = computed(() => spendList.value.length)
|
||||
|
||||
// 获取消费类型文本
|
||||
const getSpendTypeText = (type: number) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '解锁用户',
|
||||
2: '参加活动',
|
||||
3: '其他',
|
||||
}
|
||||
return typeMap[type] || '未知类型'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
} catch (e) {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price: string) => {
|
||||
try {
|
||||
return Number(price).toFixed(2)
|
||||
} catch (e) {
|
||||
return '0.00'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取消费记录
|
||||
const getSpendList = async () => {
|
||||
if (!getUserInfo()) {
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await getUserSpendHistoryAPI(userId.value)
|
||||
if (res.code === 200) {
|
||||
spendList.value = res.data || []
|
||||
} else {
|
||||
error.value = res.message || '获取消费记录失败'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = '网络请求失败,请稍后重试'
|
||||
uni.showToast({
|
||||
title: error.value,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
getSpendList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSpendList()
|
||||
})
|
||||
</script>
|
||||
240
src/pages/my/subordinate.vue
Normal file
@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<view class="subordinate-container">
|
||||
<view class="header">
|
||||
<text class="title">我的下级</text>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-state">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="subordinates.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无下级用户</text>
|
||||
</view>
|
||||
<view v-else class="subordinate-grid">
|
||||
<view class="subordinate-card" v-for="item in subordinates" :key="item.id">
|
||||
<view class="card-content">
|
||||
<view class="user-avatar">
|
||||
<text class="avatar-text">
|
||||
{{ item?.nickname?.charAt?.(0) ?? '' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="user-header">
|
||||
<text class="user-name">{{ item.nickname }}</text>
|
||||
<text class="user-id">ID: {{ item.id }}</text>
|
||||
</view>
|
||||
<view v-if="item.child && item.child.length > 0" class="child-list">
|
||||
<text class="child-count">直接下级: {{ item.child.length }}人</text>
|
||||
<view class="child-container">
|
||||
<view v-for="child in item.child" :key="child.id" class="child-item">
|
||||
<view class="child-content">
|
||||
<view class="child-avatar">
|
||||
<text class="child-avatar-text">
|
||||
{{ child?.nickname?.charAt?.(0) ?? '' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="child-info">
|
||||
<text class="child-name">{{ child.nickname }}</text>
|
||||
<text class="child-id">ID: {{ child.id }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getMyInviteListAPI } from '@/service/login'
|
||||
import type { InviteUserItem } from '@/service/login/type'
|
||||
|
||||
defineOptions({
|
||||
name: 'SubordinateList',
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
// 下级用户数据
|
||||
const subordinates = ref<InviteUserItem[]>([])
|
||||
|
||||
// 获取下级用户列表
|
||||
const fetchSubordinates = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await getMyInviteListAPI()
|
||||
if (res.code === 200 && res.data) {
|
||||
console.log(666, res)
|
||||
|
||||
subordinates.value = res.data
|
||||
loading.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下级用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSubordinates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.subordinate-container {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
.header {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx 0;
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.subordinate-grid {
|
||||
display: grid;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.subordinate-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
margin-right: 24rpx;
|
||||
background: linear-gradient(135deg, #4a90e2, #357abd);
|
||||
border-radius: 50%;
|
||||
|
||||
.avatar-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.user-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.child-list {
|
||||
padding-top: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
border-top: 2rpx solid #f0f2f5;
|
||||
|
||||
.child-count {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.child-container {
|
||||
padding-left: 24rpx;
|
||||
border-left: 4rpx solid #e8eaf6;
|
||||
}
|
||||
|
||||
.child-item {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.child-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.child-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin-right: 16rpx;
|
||||
background: linear-gradient(135deg, #e8eaf6, #c5cae9);
|
||||
border-radius: 50%;
|
||||
|
||||
.child-avatar-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #4a90e2;
|
||||
}
|
||||
}
|
||||
|
||||
.child-info {
|
||||
.child-name {
|
||||
margin-right: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.child-id {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
169
src/pages/my/user-info.vue
Normal file
@ -0,0 +1,169 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '用户信息',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50 pb-20">
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="mx-4 mt-4">
|
||||
<view class="bg-white rounded-2xl shadow-sm p-4">
|
||||
<view class="flex items-center mb-4">
|
||||
<image
|
||||
class="w-20 h-20 rounded-full border-2 border-gray-100"
|
||||
:src="userInfo.avatar || defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="ml-4 flex-1">
|
||||
<view class="flex items-center mb-2">
|
||||
<text class="text-lg font-bold text-gray-800">
|
||||
{{ userInfo.nickname || '未设置昵称' }}
|
||||
</text>
|
||||
<text class="ml-2 text-sm text-gray-500">ID: {{ userInfo.id }}</text>
|
||||
</view>
|
||||
<view class="text-sm text-gray-600">
|
||||
<text>{{ userInfo.workArea || '未设置工作地区' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息列表 -->
|
||||
<view class="mt-4 space-y-4">
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">手机号</text>
|
||||
<text class="text-gray-800">{{ userInfo.phone || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">生日</text>
|
||||
<text class="text-gray-800">{{ userInfo.birthday || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">身高</text>
|
||||
<text class="text-gray-800">
|
||||
{{ userInfo.height ? `${userInfo.height}cm` : '未设置' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">体重</text>
|
||||
<text class="text-gray-800">
|
||||
{{ userInfo.weight ? `${userInfo.weight}kg` : '未设置' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">学历</text>
|
||||
<text class="text-gray-800">{{ getEducationText(userInfo.education) }}</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">月收入</text>
|
||||
<text class="text-gray-800">{{ getIncomeText(userInfo.monthlyIncome) }}</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">职业</text>
|
||||
<text class="text-gray-800">{{ userInfo.profession || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">住房情况</text>
|
||||
<text class="text-gray-800">{{ getHousingStatusText(userInfo.housingStatus) }}</text>
|
||||
</view>
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="text-gray-600">购车情况</text>
|
||||
<text class="text-gray-800">{{ getCarOwnershipText(userInfo.carOwnership) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { UserInfo } from '@/service/login/type'
|
||||
import { useUserStore } from '@/store'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const defaultAvatar = '/static/images/default-avatar.png'
|
||||
const userInfo = ref<UserInfo>({} as UserInfo)
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
const res = await userStore.setUserInfo()
|
||||
// 从本地存储获取用户ID
|
||||
const userInfoStr = userStore.userInfo
|
||||
if (!userInfoStr) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/login/index',
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
userInfo.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '获取用户信息失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取学历文本
|
||||
const getEducationText = (education: number) => {
|
||||
const educationMap = {
|
||||
1: '初中',
|
||||
2: '高中',
|
||||
3: '大专',
|
||||
4: '本科',
|
||||
5: '硕士',
|
||||
6: '博士',
|
||||
}
|
||||
return educationMap[education] || '未设置'
|
||||
}
|
||||
|
||||
// 获取收入文本
|
||||
const getIncomeText = (income: number) => {
|
||||
const incomeMap = {
|
||||
1: '3000以下',
|
||||
2: '3000~5000',
|
||||
3: '5000~8000',
|
||||
4: '8000~10000',
|
||||
5: '10000~20000',
|
||||
6: '20000以上',
|
||||
}
|
||||
return incomeMap[income] || '未设置'
|
||||
}
|
||||
|
||||
// 获取住房情况文本
|
||||
const getHousingStatusText = (status: number) => {
|
||||
const statusMap = {
|
||||
1: '租房',
|
||||
2: '有房',
|
||||
3: '与父母同住',
|
||||
}
|
||||
return statusMap[status] || '未设置'
|
||||
}
|
||||
|
||||
// 获取购车情况文本
|
||||
const getCarOwnershipText = (status: number) => {
|
||||
const statusMap = {
|
||||
1: '无车',
|
||||
2: '有车',
|
||||
}
|
||||
return statusMap[status] || '未设置'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo()
|
||||
})
|
||||
</script>
|
||||
176
src/pages/recommend/index.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<!-- 推荐列表页面 -->
|
||||
<route lang="json5">
|
||||
{
|
||||
style: {
|
||||
navigationBarTitleText: '推荐列表',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b border-gray-100">
|
||||
<view class="flex items-center justify-between px-4 py-3">
|
||||
<view class="flex items-center">
|
||||
<text class="text-xl font-bold text-gray-900">推荐列表</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐列表 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="h-[calc(100vh-120rpx)]"
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view v-if="recommendList.length > 0" class="p-4 space-y-4">
|
||||
<view
|
||||
v-for="(item, index) in recommendList"
|
||||
:key="index"
|
||||
class="bg-white rounded-2xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md active:scale-[0.98]"
|
||||
@tap="viewDetail(item)"
|
||||
>
|
||||
<view class="flex p-4">
|
||||
<view class="relative">
|
||||
<image
|
||||
:src="item.avatarUrl || item.avatar"
|
||||
mode="aspectFill"
|
||||
class="w-24 h-24 rounded-xl object-cover"
|
||||
/>
|
||||
</view>
|
||||
<view class="flex-1 ml-4">
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<view class="flex items-center">
|
||||
<text class="text-lg font-semibold text-gray-900 mr-2">{{ item.nickname }}</text>
|
||||
<text class="text-sm text-gray-500">{{ item.age }}岁</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center space-x-2 mb-3">
|
||||
<view class="flex items-center">
|
||||
<text class="text-xs text-gray-500">身高 {{ item.height }}cm</text>
|
||||
</view>
|
||||
<view class="w-px h-3 bg-gray-200"></view>
|
||||
<view class="flex items-center">
|
||||
<text class="text-xs text-gray-500">{{ item.education }}</text>
|
||||
</view>
|
||||
<view class="w-px h-3 bg-gray-200"></view>
|
||||
<view class="flex items-center">
|
||||
<text class="text-xs text-gray-500">{{ item.workArea }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<text class="text-sm text-gray-600">{{ item.profession }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="!isLoading" class="flex flex-col items-center justify-center py-12">
|
||||
<image src="/static/empty.png" class="w-32 h-32 mb-4" />
|
||||
<text class="text-gray-400">暂无推荐数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="isLoading" class="flex justify-center py-6">
|
||||
<view class="flex items-center space-x-2">
|
||||
<view class="w-2 h-2 bg-primary rounded-full animate-bounce"></view>
|
||||
<view
|
||||
class="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||
style="animation-delay: 0.2s"
|
||||
></view>
|
||||
<view
|
||||
class="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||
style="animation-delay: 0.4s"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!hasMore && recommendList.length > 0" class="flex justify-center py-6">
|
||||
<text class="text-sm text-gray-400">没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { LoveUserItem, LoveListParams } from '@/service/love/type'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
// 列表数据
|
||||
const recommendList = ref<LoveUserItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const hasMore = ref(true)
|
||||
const isLoading = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
|
||||
// 获取推荐列表
|
||||
const getRecommendList = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
const params: LoveListParams = {
|
||||
pageIndex: page.value,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推荐列表失败:', error)
|
||||
uni.showToast({
|
||||
title: '获取推荐列表失败',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
isRefreshing.value = true
|
||||
page.value = 1
|
||||
await getRecommendList()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (!hasMore.value || isLoading.value) return
|
||||
page.value++
|
||||
getRecommendList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (item: LoveUserItem) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
getRecommendList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/style/iconfont.css';
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
</style>
|
||||
19
src/service/event/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type {
|
||||
EventItem,
|
||||
EventListParams,
|
||||
MyEventItem,
|
||||
JoinEventResponse,
|
||||
ApiResponse,
|
||||
} from './type'
|
||||
|
||||
/** 获取活动列表 */
|
||||
export const getEventListAPI = (params: EventListParams) =>
|
||||
request.post<ApiResponse<EventItem[]>>('/event/list', params)
|
||||
|
||||
/** 参加活动 */
|
||||
export const joinEventAPI = (id: string) =>
|
||||
request.get<ApiResponse<JoinEventResponse>>(`/event/join/${id}`)
|
||||
|
||||
/** 获取我参加的活动列表 */
|
||||
export const getMyEventListAPI = () => request.get<ApiResponse<MyEventItem[]>>('/event/myEvent')
|
||||
63
src/service/event/type.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/** 活动列表项 */
|
||||
export interface EventItem {
|
||||
/** 活动ID */
|
||||
id: string
|
||||
/** 活动标题 */
|
||||
eventTitle: string
|
||||
/** 活动内容 markdown格式 */
|
||||
eventContent: string
|
||||
/** 活动封面图 */
|
||||
eventAvatar: string
|
||||
/** 活动价格 */
|
||||
eventPrice: string
|
||||
/** 活动地点 */
|
||||
eventPlace: string
|
||||
/** 发起时间 */
|
||||
createTime: string
|
||||
/** 到期时间 */
|
||||
endTime: string
|
||||
/** 有效天数 */
|
||||
effectiveTime: string
|
||||
/** 已参与人数 */
|
||||
eventNumber: string
|
||||
/** 限制人数 */
|
||||
eventLimitNumber: string
|
||||
}
|
||||
|
||||
/** 我参加的活动列表项 */
|
||||
export interface MyEventItem extends EventItem {
|
||||
/** 我的活动表id,非活动id */
|
||||
id: string
|
||||
/** 活动发起时间 */
|
||||
enevtCreateTime: string
|
||||
/** 到期时间 */
|
||||
eventEndTime: string
|
||||
/** 活动状态 1-启用 0-禁用 */
|
||||
status: '1' | '0'
|
||||
/** 参加状态 1-已参加 0或者为空就是未参加 */
|
||||
joinStatus?: '1' | '0'
|
||||
/** 参加活动时间 */
|
||||
createTime: string
|
||||
}
|
||||
|
||||
/** 活动列表查询参数 */
|
||||
export interface EventListParams {
|
||||
/** 活动状态:1-全部 2-报名中 3-已结束 */
|
||||
status: '1' | '2' | '3'
|
||||
}
|
||||
|
||||
/** 参加活动响应 */
|
||||
export interface JoinEventResponse {
|
||||
/** 订单编号 */
|
||||
trackingNo: string
|
||||
}
|
||||
|
||||
/** 通用响应格式 */
|
||||
export interface ApiResponse<T> {
|
||||
/** 响应码 */
|
||||
code: number
|
||||
/** 响应消息 */
|
||||
message: string
|
||||
/** 响应数据 */
|
||||
data: T
|
||||
}
|
||||
20
src/service/exam/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { BaseResponse } from '../login/type'
|
||||
|
||||
/** 考试信息类型 */
|
||||
export interface ExamItem {
|
||||
id: string
|
||||
examName: string
|
||||
examDate: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
provinceName: string
|
||||
cityName: string
|
||||
address: string
|
||||
locationName: string
|
||||
estimatedAttendees: string
|
||||
}
|
||||
|
||||
/** 获取可报名考试列表 */
|
||||
export const getCanBookExamListAPI = () =>
|
||||
request.get<BaseResponse<ExamItem[]>>('/exam/getCanBookExamList')
|
||||
10
src/service/feedback/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { FeedbackParams, ComplaintParams, ApiResponse } from './type'
|
||||
|
||||
/** 提交反馈 */
|
||||
export const addFeedbackAPI = (data: FeedbackParams) =>
|
||||
request.post<ApiResponse>('/feedback/add', data)
|
||||
|
||||
/** 提交投诉 */
|
||||
export const addComplaintAPI = (data: ComplaintParams) =>
|
||||
request.post<ApiResponse>('/complaint/add', data)
|
||||
29
src/service/feedback/type.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/** 反馈参数 */
|
||||
export interface FeedbackParams {
|
||||
/** 反馈意见标题 */
|
||||
feedbackTitle: string
|
||||
/** 反馈意见内容 */
|
||||
feedbackContent: string
|
||||
}
|
||||
|
||||
/** 投诉参数 */
|
||||
export interface ComplaintParams {
|
||||
/** 被投诉人id */
|
||||
userId: string
|
||||
/** 投诉分类 */
|
||||
complaintClass: '虚假宣传' | '微商' | '诈骗' | '个人信息不符'
|
||||
/** 投诉详情 */
|
||||
complaintContent: string
|
||||
/** 附件图片url列表 */
|
||||
complaintImageList?: string[]
|
||||
}
|
||||
|
||||
/** API响应基础类型 */
|
||||
export interface ApiResponse<T = any> {
|
||||
/** 状态码 */
|
||||
code: '200' | '500'
|
||||
/** 消息 */
|
||||
message: string
|
||||
/** 数据 */
|
||||
data?: T
|
||||
}
|
||||
30
src/service/file/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { request } from '@/utils/request'
|
||||
import { getEnvBaseUrl } from '@/utils'
|
||||
|
||||
/** 上传图片 */
|
||||
export const uploadImageAPI = (filePath: string, imageClassId: string) => {
|
||||
return new Promise<{ url: string }>((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: getEnvBaseUrl() + 'attachment/uploadImageForUser',
|
||||
filePath,
|
||||
name: 'img',
|
||||
formData: {
|
||||
image_class_id: imageClassId,
|
||||
},
|
||||
header: {
|
||||
'x-token': uni.getStorageSync('x-token') || '',
|
||||
},
|
||||
success: (res) => {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
reject(new Error(data.message))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
11
src/service/index/foo.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { request } from '@/utils/request'
|
||||
export interface IFooItem {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/** GET 请求 */
|
||||
export const getFooAPI = (name: string) => request.get<IFooItem>('/foo', { name })
|
||||
|
||||
/** POST 请求 */
|
||||
export const postFooAPI = (name: string) => request.post<IFooItem>('/foo', { name }, { name })
|
||||
64
src/service/login/index.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type {
|
||||
BaseResponse,
|
||||
GetOpenIdResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
UserInfo,
|
||||
UpdateUserInfoRequest,
|
||||
InviteUserItem,
|
||||
SpendHistoryItem,
|
||||
PayInfoList,
|
||||
GetInviteCodeResponse,
|
||||
RefundInfoList,
|
||||
} from './type'
|
||||
|
||||
/** 获取用户openId */
|
||||
export const getOpenIdAPI = (code: string) =>
|
||||
request.post<BaseResponse<GetOpenIdResponse>>('/sys/getOpenId', { code })
|
||||
|
||||
/** 用户登录 */
|
||||
export const loginAPI = (data: LoginRequest) =>
|
||||
request.post<BaseResponse<LoginResponse>>('/base/mobileLogin', data)
|
||||
|
||||
/** 获取用户信息 */
|
||||
export const getUserInfoAPI = (id: string) =>
|
||||
request.get<BaseResponse<UserInfo>>(`/user/info/${id}`)
|
||||
|
||||
/** 更新用户信息 */
|
||||
export const updateUserInfoAPI = (data: UpdateUserInfoRequest) =>
|
||||
request.post<BaseResponse>('/user/update', data)
|
||||
|
||||
/**
|
||||
* banner图片
|
||||
*/
|
||||
export const getBannerImage = () => request.get<BaseResponse>('/attachment/getBannerImage')
|
||||
|
||||
/** 获取我邀请的用户列表 */
|
||||
export const getMyInviteListAPI = () =>
|
||||
request.get<BaseResponse<InviteUserItem[]>>('/user/myInvite')
|
||||
|
||||
/** 获取用户消费明细 */
|
||||
export const getUserSpendHistoryAPI = (id: string) =>
|
||||
request.get<BaseResponse<SpendHistoryItem[]>>(`/user/spendHistory/${id}`)
|
||||
|
||||
export const getUserPayInfoAPI = () => request.get<BaseResponse<PayInfoList[]>>(`/pay/payInfo`)
|
||||
|
||||
/** 获取用户退款记录 */
|
||||
export const getUserRefundInfoAPI = () =>
|
||||
request.get<BaseResponse<RefundInfoList[]>>('/pay/refundInfo')
|
||||
|
||||
/** 撤销退款申请 */
|
||||
export const cancelRefundAPI = (id: string) => request.get<BaseResponse>(`/pay/cancelRefund/${id}`)
|
||||
|
||||
/** 退出登录 */
|
||||
export const logoutAPI = () => request.get<BaseResponse>('/sys/logout')
|
||||
|
||||
/** 获取邀请码 */
|
||||
export const getInviteCodeAPI = () =>
|
||||
request.get<BaseResponse<GetInviteCodeResponse>>('/sys/getInviteCode')
|
||||
|
||||
/** 绑定邀请人 */
|
||||
export const bindInviterAPI = (code: string) => {
|
||||
return request.get(`/sys/bindInviteUser/${code}`)
|
||||
}
|
||||
176
src/service/login/type.ts
Normal file
@ -0,0 +1,176 @@
|
||||
/** 基础响应类型 */
|
||||
export interface BaseResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
/** 获取openId响应类型 */
|
||||
export type GetOpenIdResponse = string
|
||||
|
||||
/** 登录请求参数类型 */
|
||||
export interface LoginRequest {
|
||||
openId: string
|
||||
avatarUrl: string
|
||||
userName: string
|
||||
}
|
||||
|
||||
/** 登录响应类型 */
|
||||
export interface LoginResponse {
|
||||
/** 用户ID */
|
||||
id?: number
|
||||
/** 用户token */
|
||||
'x-token'?: string
|
||||
/** 账户余额,展示在个人信息页面 */
|
||||
points?: string
|
||||
/** 退款状态 */
|
||||
refundStatus?: string
|
||||
/** 手机号 */
|
||||
phone?: string
|
||||
/** 头像URL */
|
||||
avatar?: string
|
||||
/** 昵称 */
|
||||
nickName?: string
|
||||
/** 生日,日期Date类型 */
|
||||
birthday?: string
|
||||
/** 工作地址 */
|
||||
workArea?: string
|
||||
/** 工作省份名称 */
|
||||
workProvinceName?: string
|
||||
/** 工作城市名称 */
|
||||
workCityName?: string
|
||||
/** 工作区县名称 */
|
||||
workCountryName?: string
|
||||
/** 户籍地址 */
|
||||
householdArea?: string
|
||||
/** 婚姻状况 (1-未婚, 2-离异, 3-丧偶) */
|
||||
maritalStatus?: string
|
||||
/** 身高,单位:米 */
|
||||
height?: number
|
||||
/** 体重,单位:千克 */
|
||||
weight?: number
|
||||
/** 学历 (1-初中, 2-高中, 3-大专, 4-本科, 5-硕士, 6-博士) */
|
||||
education?: string
|
||||
/** 月收入 1-3000以下 2-3000~5000 3-5000~8000 4-8000~10000 5-10000~20000 6-20000以上 */
|
||||
monthlyIncome?: string
|
||||
/** 职业 */
|
||||
profession?: string
|
||||
/** 住房情况(1-已购房(有贷款) 2-已购房(无贷款) 3-有能力购房 4-无房 5-无房希望对方解决 6-无房希望双方解决 7-与父母同住 8-独自租房 9-与人合租 10-住单位房) */
|
||||
housingStatus?: string
|
||||
/** 购车情况(1-无车 2-已购车(经济型) 3-已购车(中档型) 4-已购车(豪华型) 5-单位用车 6-需要时购置) */
|
||||
carOwnership?: string
|
||||
/** 期望结婚时间(1-随时 2-半年内 3-一年内 4-两年内 5-三年内) */
|
||||
expectedMarriageTime?: string
|
||||
/** 自我介绍 */
|
||||
selfIntroduction?: string
|
||||
/** 微信 */
|
||||
wechat?: string
|
||||
/** 微信二维码url */
|
||||
wechatQrcode?: string
|
||||
/** QQ号 */
|
||||
qq?: string
|
||||
/** 邮箱 */
|
||||
email?: string
|
||||
/** 父母状况(1-父母均建在 2-只有母亲建在 3-只有父亲建在 4-父母均以离世) */
|
||||
parentsStatus?: string
|
||||
/** 兄弟姐妹 (1-独生子女 2-2 3-3 4-4 5-5) */
|
||||
siblings?: string
|
||||
/** 与Ta父母住(1-愿意 2-不愿意 3-视具体情况而定 4-尊重伴侣意见) */
|
||||
liveWithParents?: string
|
||||
/** 是否吸烟(1-是 0-否) */
|
||||
smoke?: number
|
||||
/** 婚娶形式(1-嫁娶 2-两顾 3-上门) */
|
||||
marriageForm?: string
|
||||
/** 是否饮酒(1-是 0-否) */
|
||||
drink?: number
|
||||
/** 子女情况(1-未育 2-子女归自己 3-子女归对方) */
|
||||
childrenStatus?: string
|
||||
/** 养宠物情况(多选)(猫,狗,鸟,鱼,兔,鼠,乌龟,蛇,爬行动物,另类动物,不喜欢养,可能会养,过敏) */
|
||||
pets?: string
|
||||
/** 兴趣爱好 */
|
||||
hobbies?: string
|
||||
/** 血型(A B AB O 其他) */
|
||||
bloodType?: string
|
||||
/** 作息习惯(早睡早起很规律,经常夜猫子,总是早起鸟,偶尔懒散一下,没有规律) */
|
||||
sleepHabits?: string
|
||||
/** 民族 */
|
||||
nation?: string
|
||||
/** 毕业院校 */
|
||||
graduationSchool?: string
|
||||
/** 锻炼习惯(每天锻炼,每周至少一次,每月几次,没时间锻炼,集中时间锻炼,不喜欢锻炼) */
|
||||
exerciseHabits?: string
|
||||
/** 工作行业 */
|
||||
workIndustry?: string
|
||||
/** 单位类型(政府机关,事业单位,外资企业,合资企业,国营企业,私营企业,自有公司,其他) */
|
||||
companyType?: string
|
||||
/** 工作单位 */
|
||||
workUnit?: string
|
||||
/** 我的标签(多选)(孝顺,酷,责任心,经济适用,憨直,感性,事业,睿智,猥琐,幽默,旅行,宅男,体贴,有魄力,仗义,稳重) */
|
||||
tags?: string
|
||||
/** 房产位置(多选)(市区 城区有房 老家有房) */
|
||||
propertyLocation?: string
|
||||
/** 期望婚况(多选)(1-未婚 2-离异 3-丧偶) */
|
||||
partnerMaritalStatus?: string
|
||||
/** 期望年龄范围 20-23 */
|
||||
ageRange?: string
|
||||
/** 期望身高范围 160-180 */
|
||||
heightRange?: string
|
||||
/** 期望最低学历(1-初中 2-高中 3-大专 4-本科 5-硕士 6-博士) */
|
||||
minEducation?: string
|
||||
/** 期望月收入 1-3000以下 2-3000~5000 3-5000~8000 4-8000~10000 5-10000~20000 6-20000以上 */
|
||||
minIncome?: string
|
||||
/** 期望工作地区,选省份代码 */
|
||||
partnerWorkArea?: string
|
||||
/** 期望其他要求 */
|
||||
otherRequirements?: string
|
||||
/** 创建时间 */
|
||||
createTime?: string
|
||||
/** 更新时间 */
|
||||
updateTime?: string
|
||||
/** 邀请人id */
|
||||
inviterId?: string
|
||||
}
|
||||
|
||||
/** 用户信息类型 */
|
||||
export type UserInfo = LoginResponse
|
||||
|
||||
/** 更新用户信息请求参数类型 */
|
||||
export type UpdateUserInfoRequest = Partial<UserInfo>
|
||||
|
||||
/** 邀请用户列表项类型 */
|
||||
export interface InviteUserItem {
|
||||
id: string
|
||||
nickname: string
|
||||
child: {
|
||||
id: string
|
||||
nickname: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/** 用户消费明细类型 */
|
||||
export interface SpendHistoryItem {
|
||||
id: string
|
||||
spendPrice: string
|
||||
spendType: number
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export interface PayInfoList {
|
||||
id: string
|
||||
payPoints: string
|
||||
createTime: string
|
||||
orderNo: string
|
||||
}
|
||||
|
||||
/** 获取邀请码响应类型 */
|
||||
export type GetInviteCodeResponse = string
|
||||
|
||||
/** 退款记录类型 */
|
||||
export interface RefundInfoList {
|
||||
id: string
|
||||
refundPoints: string
|
||||
refundReason: string
|
||||
refundStatus: string
|
||||
createTime: string
|
||||
auditTime: string
|
||||
}
|
||||
21
src/service/love/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { LoveUserItem, LoveListParams, ApiResponse, UnlockUserResult } from './type'
|
||||
|
||||
/** 获取我解锁的相亲对象列表 */
|
||||
export const getMyLoveListAPI = () => request.get<ApiResponse<LoveUserItem[]>>('/love/myList')
|
||||
|
||||
/** 获取推荐相亲对象列表 */
|
||||
export const getLoveListAPI = (params: LoveListParams) =>
|
||||
request.post<ApiResponse<LoveUserItem[]>>('/love/list', params)
|
||||
|
||||
/** 检查用户是否已解锁 */
|
||||
export const checkIsLoveAPI = (id: string) =>
|
||||
request.get<ApiResponse<boolean>>(`/user/isLove/${id}`)
|
||||
|
||||
/** 解锁用户 */
|
||||
export const unlockUserAPI = (id: string) =>
|
||||
request.get<ApiResponse<UnlockUserResult[]>>(`/user/love/${id}`)
|
||||
|
||||
/** 获取用户详细信息 */
|
||||
export const getUserDetailAPI = (id: string) =>
|
||||
request.get<ApiResponse<LoveUserItem>>(`/user/detail/${id}`)
|
||||
91
src/service/love/type.ts
Normal file
@ -0,0 +1,91 @@
|
||||
/** 相亲对象列表项 */
|
||||
export interface LoveUserItem {
|
||||
/** 用户ID */
|
||||
id: string | number
|
||||
/** 用户昵称 */
|
||||
nickname: string
|
||||
/** 用户头像 */
|
||||
avatar: string
|
||||
/** 年龄 */
|
||||
age: number
|
||||
/** 身高(cm) */
|
||||
height: number
|
||||
/** 学历 */
|
||||
education: string
|
||||
/** 职业 */
|
||||
occupation: string
|
||||
/** 收入 */
|
||||
income: string
|
||||
/** 所在地 */
|
||||
location: string
|
||||
/** 是否已解锁 */
|
||||
isUnlocked: boolean
|
||||
/** 微信ID */
|
||||
wechatId?: string
|
||||
/** 头像URL */
|
||||
avatarUrl: string
|
||||
/** 工作地区 */
|
||||
workArea: string
|
||||
/** 婚姻状况 (1-未婚, 2-离异, 3-丧偶) */
|
||||
maritalStatus: '1' | '2' | '3'
|
||||
/** 体重,单位:千克 */
|
||||
weight: string
|
||||
/** 学历 (1-初中, 2-高中, 3-大专, 4-本科, 5-硕士, 6-博士) */
|
||||
educationLevel: '1' | '2' | '3' | '4' | '5' | '6'
|
||||
/** 月收入 1-3000以下 2-3000~5000 3-5000~8000 4-8000~10000 5-10000~20000 6-20000以上 */
|
||||
monthlyIncome: '1' | '2' | '3' | '4' | '5' | '6'
|
||||
/** 职业 */
|
||||
profession: string
|
||||
/** 兴趣爱好 */
|
||||
hobbies: string
|
||||
/** 我的标签(多选) */
|
||||
tags: string
|
||||
/** 房产位置(多选) */
|
||||
propertyLocation: string
|
||||
/** 住房情况 */
|
||||
housingStatus: string
|
||||
/** QQ号 */
|
||||
qq: string
|
||||
/** 邮箱 */
|
||||
email: string
|
||||
/** 个人介绍 */
|
||||
selfIntroduction: string
|
||||
}
|
||||
|
||||
/** 相亲对象列表查询参数 */
|
||||
export interface LoveListParams {
|
||||
/** 页码 */
|
||||
pageIndex: number | string
|
||||
/** 年龄范围 示例:20-30 */
|
||||
ageRange?: string
|
||||
/** 体重范围 示例:20-30 */
|
||||
weightRange?: string
|
||||
/** 身高范围 示例:20-30 */
|
||||
heightRange?: string
|
||||
/** 工作地区 市6位代码 示例:211000 */
|
||||
workCityCode?: string
|
||||
/** 户籍地区 市6位代码 示例:211000 */
|
||||
householdCityCode?: string
|
||||
/** 民族 直接传中文即可 */
|
||||
nation?: string
|
||||
/** 婚姻状况 (1-未婚, 2-离异, 3-丧偶) */
|
||||
maritalStatus?: '1' | '2' | '3'
|
||||
/** 住房情况 1-10 */
|
||||
housingStatus?: string
|
||||
/** 是否吸烟(1-是 0-否) */
|
||||
smoke?: '1' | '0'
|
||||
/** 子女情况(1-未育 2-子女归自己 3-子女归对方) */
|
||||
childrenStatus?: '1' | '2' | '3'
|
||||
}
|
||||
|
||||
/** 解锁用户返回项 */
|
||||
export interface UnlockUserResult {
|
||||
trackingNo: string
|
||||
}
|
||||
|
||||
/** 通用API响应体 */
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
21
src/service/pay/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { PayParams, RefundParams, PayInfo, RefundInfo } from './type'
|
||||
|
||||
/** 发起支付 */
|
||||
export const payAPI = (data: PayParams) => request.post<{ outTradeNo: string }>('/pay/pay', data)
|
||||
|
||||
/** 查询支付结果 */
|
||||
export const queryPayResultAPI = (outTradeNo: string) =>
|
||||
request.get<boolean>(`/pay/queryPayResult/${outTradeNo}`)
|
||||
|
||||
/** 申请退款 */
|
||||
export const refundAPI = (data: RefundParams) => request.post('/pay/refund', data)
|
||||
|
||||
/** 获取退款详情 */
|
||||
export const getRefundInfoAPI = () => request.get<RefundInfo[]>('/pay/refundInfo')
|
||||
|
||||
/** 撤销退款 */
|
||||
export const cancelRefundAPI = (id: string) => request.get(`/pay/cancelRefund/${id}`)
|
||||
|
||||
/** 获取充值记录 */
|
||||
export const getPayInfoAPI = () => request.get<PayInfo[]>('/pay/payInfo')
|
||||
43
src/service/pay/type.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/** 支付参数 */
|
||||
export interface PayParams {
|
||||
/** 支付金额 */
|
||||
amount: number
|
||||
}
|
||||
|
||||
/** 退款参数 */
|
||||
export interface RefundParams {
|
||||
/** 退款金额 */
|
||||
amount: number
|
||||
/** 退款原因 */
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/** 支付记录 */
|
||||
export interface PayInfo {
|
||||
/** 支付记录ID */
|
||||
id: string
|
||||
/** 支付金额 */
|
||||
amount: number
|
||||
/** 支付状态:pending-待支付,success-支付成功,failed-支付失败 */
|
||||
status: 'pending' | 'success' | 'failed'
|
||||
/** 创建时间 */
|
||||
createTime: string
|
||||
/** 商户订单号 */
|
||||
outTradeNo: string
|
||||
}
|
||||
|
||||
/** 退款记录 */
|
||||
export interface RefundInfo {
|
||||
/** 退款记录ID */
|
||||
id: string
|
||||
/** 退款金额 */
|
||||
amount: number
|
||||
/** 退款状态:pending-待审核,approved-已通过,rejected-已拒绝,cancelled-已取消 */
|
||||
status: 'pending' | 'approved' | 'rejected' | 'cancelled'
|
||||
/** 退款原因 */
|
||||
reason?: string
|
||||
/** 创建时间 */
|
||||
createTime: string
|
||||
/** 更新时间 */
|
||||
updateTime: string
|
||||
}
|
||||
BIN
src/static/app/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
src/static/app/icons/120x120.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/static/app/icons/144x144.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/static/app/icons/152x152.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/static/app/icons/167x167.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/static/app/icons/180x180.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/static/app/icons/192x192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/static/app/icons/20x20.png
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
src/static/app/icons/29x29.png
Normal file
|
After Width: | Height: | Size: 780 B |
BIN
src/static/app/icons/40x40.png
Normal file
|
After Width: | Height: | Size: 985 B |
BIN
src/static/app/icons/58x58.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/static/app/icons/60x60.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/static/app/icons/72x72.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/static/app/icons/76x76.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/static/app/icons/80x80.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |