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