├── .nvmrc ├── src ├── styles │ ├── index.scss │ ├── variable │ │ ├── custom.scss │ │ └── uni.scss │ ├── funs │ │ └── index.scss │ └── mixins │ │ └── index.scss ├── types │ ├── dts │ │ ├── index.d.ts │ │ ├── api.d.ts │ │ ├── pages.d.ts │ │ └── components.d.ts │ ├── index.ts │ ├── authorize.ts │ └── userInfo.ts ├── constants │ ├── index.ts │ └── userInfo.ts ├── hooks │ ├── share │ │ ├── index.ts │ │ └── useShare.ts │ ├── flow │ │ └── index.ts │ ├── timer │ │ ├── index.ts │ │ └── usePolling.ts │ ├── async │ │ ├── index.ts │ │ └── useAsyncTask.ts │ ├── pagination │ │ ├── index.ts │ │ └── useListPagination.ts │ └── index.ts ├── static │ └── image │ │ ├── share.jpg │ │ ├── uniapp.ico │ │ ├── List │ │ ├── empty.png │ │ └── back-top.png │ │ ├── popup │ │ └── Popup │ │ │ └── close.png │ │ ├── layout │ │ └── TabBar │ │ │ ├── icon1-off.png │ │ │ ├── icon1-on.png │ │ │ ├── icon2-off.png │ │ │ └── icon2-on.png │ │ └── mp_wx.svg ├── apis │ ├── modules │ │ ├── index.ts │ │ ├── activity.ts │ │ └── userInfo.ts │ ├── interceptors │ │ ├── index.ts │ │ └── response.ts │ ├── index.ts │ └── request │ │ ├── log.ts │ │ └── index.ts ├── components │ ├── layout │ │ ├── index.ts │ │ └── Layout.vue │ ├── picker │ │ ├── type.ts │ │ └── index.ts │ ├── index.ts │ ├── provideComponentOptions.ts │ ├── popup │ │ └── index.ts │ ├── auth │ │ ├── index.ts │ │ ├── AuthPhoneNumberButton.vue │ │ ├── AuthAvatarButton.vue │ │ └── NickNameInput.vue │ ├── TitleBar.vue │ ├── AvatarNickname.vue │ ├── LabelValueBar.vue │ ├── dialog │ │ └── index.ts │ ├── Cell.vue │ ├── FooterActionsBar.vue │ ├── TabsPaneList.vue │ └── FormControl.vue ├── utils │ ├── dateTime │ │ └── index.ts │ ├── index.ts │ ├── form │ │ ├── index.ts │ │ └── identityCard.ts │ ├── media │ │ └── index.ts │ ├── tool │ │ └── index.ts │ ├── env │ │ └── index.ts │ ├── location │ │ └── index.ts │ ├── userInfo │ │ └── index.ts │ ├── miniProgram │ │ └── index.ts │ ├── pages │ │ └── index.ts │ └── url │ │ └── index.ts ├── main.ts ├── stores │ ├── index.ts │ ├── activity.ts │ ├── tabBar.ts │ └── userInfo.ts ├── routerInterceptor │ ├── login.ts │ └── index.ts ├── App.vue ├── pages │ ├── my.vue │ ├── list.vue │ ├── home.vue │ ├── launch.vue │ ├── test.vue │ └── login.vue ├── manifest.json ├── pages.json └── subPackages │ └── webview │ └── pages │ └── webview.vue ├── .husky └── commit-msg ├── .prettierrc.mjs ├── .gitignore ├── .npmrc ├── .editorconfig ├── .stylelintignore ├── .stylelintrc.mjs ├── tsconfig.json ├── .eslintrc.cjs ├── .prettierignore ├── .eslintignore ├── types ├── index.d.ts └── env.d.ts ├── tsconfig.node.json ├── vite └── utils.ts ├── uno.config.ts ├── tsconfig.app.json ├── LICENSE ├── .env ├── manifest.config.ts ├── .vscode └── extensions.json ├── package.json ├── README.md ├── pages.config.ts └── vite.config.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/iron 2 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: scss模块 3 | */ 4 | -------------------------------------------------------------------------------- /src/types/dts/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 全局类型声明补充文件 3 | */ 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | # Git提交时自动效验提交信息 2 | project-cli commit-lint $1 3 | -------------------------------------------------------------------------------- /src/styles/variable/custom.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: scss全局变量文件 3 | */ 4 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 常量模块 3 | */ 4 | 5 | /** 导出用户信息常量模块 */ 6 | export * from "./userInfo" 7 | -------------------------------------------------------------------------------- /src/hooks/share/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 分享相关hook 3 | */ 4 | 5 | /** 使用分享 */ 6 | export * from "./useShare" 7 | -------------------------------------------------------------------------------- /src/static/image/share.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/share.jpg -------------------------------------------------------------------------------- /src/hooks/flow/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 流程控制相关hook函数 3 | */ 4 | 5 | /** 导出 使用流程 */ 6 | export * from "./useFlow" 7 | -------------------------------------------------------------------------------- /src/static/image/uniapp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/uniapp.ico -------------------------------------------------------------------------------- /src/hooks/timer/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 时间控制相关hooks函数 3 | */ 4 | 5 | /** 使用轮询器 */ 6 | export * from "./usePolling" 7 | -------------------------------------------------------------------------------- /src/static/image/List/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/List/empty.png -------------------------------------------------------------------------------- /src/hooks/async/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 异步任务相关hook 3 | */ 4 | 5 | /** 使用异步任务执行器 */ 6 | export * from "./useAsyncTask" 7 | -------------------------------------------------------------------------------- /src/static/image/List/back-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/List/back-top.png -------------------------------------------------------------------------------- /src/static/image/popup/Popup/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/popup/Popup/close.png -------------------------------------------------------------------------------- /src/static/image/layout/TabBar/icon1-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/layout/TabBar/icon1-off.png -------------------------------------------------------------------------------- /src/static/image/layout/TabBar/icon1-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/layout/TabBar/icon1-on.png -------------------------------------------------------------------------------- /src/static/image/layout/TabBar/icon2-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/layout/TabBar/icon2-off.png -------------------------------------------------------------------------------- /src/static/image/layout/TabBar/icon2-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dyb-dev/uniapp-mp-wx-template/HEAD/src/static/image/layout/TabBar/icon2-on.png -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 类型模块 3 | */ 4 | 5 | /** 导出授权类型模块 */ 6 | export * from "./authorize" 7 | /** 导出用户信息类型模块 */ 8 | export * from "./userInfo" 9 | -------------------------------------------------------------------------------- /src/apis/modules/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 接口模块 3 | */ 4 | 5 | /** 导出本次活动相关接口 */ 6 | export * from "./activity" 7 | /** 导出用户信息相关接口 */ 8 | export * from "./userInfo" 9 | -------------------------------------------------------------------------------- /src/hooks/pagination/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 分页hooks模块 3 | */ 4 | 5 | // 导出分页器 6 | export * from "./usePagination" 7 | // 导出列表分页器 8 | export * from "./useListPagination" 9 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: prettier 配置 注意:每次配置的更改,建议重启一下编辑器,否则可能不会生效 3 | */ 4 | 5 | import prettierConfig from "@dyb-dev/prettier-config" 6 | export default prettierConfig 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 编辑目录和文件 2 | .idea 3 | .DS_Store 4 | coverage 5 | *.suo 6 | *.ntvs* 7 | *.njsproj 8 | *.sln 9 | *.sw? 10 | *.local 11 | 12 | # Logs 13 | logs 14 | *.log* 15 | 16 | # 忽略项目文件 17 | .history 18 | dist 19 | node_modules 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # 默认走淘宝镜像 2 | registry = https://registry.npmmirror.com 3 | 4 | # 私服npm用到,携带@xxx前缀的包名会走http://xxx.com:8080 5 | # @dyb-dev:registry = https://registry.npmjs.org/ 6 | 7 | # 将所有的依赖提升到项目的根目录下,解决 @dcloudio/vite-plugin-uni 插件找不到依赖的问题 8 | shamefully-hoist = true 9 | -------------------------------------------------------------------------------- /src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 导入相关组件类型 3 | */ 4 | 5 | /** 导出页面布局容器类型 */ 6 | export type * from "./Layout.vue" 7 | /** 导出顶部导航栏类型 */ 8 | export type * from "./NavBar.vue" 9 | /** 导出底部导航栏类型 */ 10 | export type * from "./TabBar.vue" 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 为了兼容不同的编辑器 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: hook模块 3 | */ 4 | 5 | /** 导出异步任务相关hook */ 6 | export * from "./async" 7 | /** 导出流程控制相关hook */ 8 | export * from "./flow" 9 | /** 导出分页相关hook */ 10 | export * from "./pagination" 11 | /** 导出分享相关hook */ 12 | export * from "./share" 13 | /** 导出时间控制相关hooks函数 */ 14 | export * from "./timer" 15 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # 编辑目录和文件 2 | .idea 3 | .DS_Store 4 | coverage 5 | *.suo 6 | *.ntvs* 7 | *.njsproj 8 | *.sln 9 | *.sw? 10 | *.local 11 | 12 | # Logs 13 | logs 14 | *.log* 15 | 16 | # 忽略的项目文件 17 | dist 18 | node_modules 19 | src/static/ 20 | types 21 | .* 22 | *-lock.* 23 | *.json 24 | *.yaml 25 | *.md 26 | *.ts 27 | *.d.ts 28 | *.*js 29 | -------------------------------------------------------------------------------- /src/apis/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: Api 拦截器模块 3 | */ 4 | 5 | import { setupResponseInterceptor } from "./response" 6 | 7 | /** 8 | * FUN: 设置接口拦截器 9 | * 10 | * @author dyb-dev 11 | * @date 17/10/2024/ 16:17:31 12 | */ 13 | export const setupApiInterceptor = () => { 14 | 15 | setupResponseInterceptor() 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/dateTime/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 日期时间相关工具函数 3 | */ 4 | 5 | /** 6 | * FUN: 延迟函数 7 | * 8 | * @author dyb-dev 9 | * @date 19/02/2025/ 15:55:58 10 | * @param {number} ms - 延迟时间(毫秒) 11 | * @returns {*} {Promise} - 返回一个 Promise 对象 12 | */ 13 | export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) 14 | -------------------------------------------------------------------------------- /.stylelintrc.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: stylelint 配置 注意:每次配置的更改,建议重启一下编辑器,否则可能不会生效 3 | */ 4 | 5 | export default { 6 | extends: ["@dyb-dev/stylelint-config"], 7 | rules: { 8 | // 忽略 `rpx` 单位的报错 9 | "unit-no-unknown": [true, { ignoreUnits: ["rpx"] }], 10 | // 忽略 `rpx` 单位的报错 11 | "declaration-property-value-no-unknown": null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: ts 配置 3 | */ 4 | 5 | { 6 | // 主项目本身不包含任何要编译的文件,文件编译由 `references` 引用的子项目控制 7 | "files": [], 8 | // 引用其他 TypeScript 项目,支持项目间的模块化开发和增量编译 9 | "references": [ 10 | { 11 | "path": "./tsconfig.app.json" 12 | }, 13 | { 14 | "path": "./tsconfig.node.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: eslint 配置 注意:每次配置的更改,建议重启一下编辑器,否则可能不会生效 3 | */ 4 | 5 | module.exports = { 6 | extends: ["@dyb-dev/eslint-config/vue"], 7 | overrides: [ 8 | // #region CODE: unocss 配置 9 | { 10 | files: ["**/*.vue", "**/*.jsx", "**/*.tsx"], 11 | extends: ["@unocss"] 12 | } 13 | // #endregion 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # 编辑目录和文件 2 | .idea 3 | .DS_Store 4 | coverage 5 | *.suo 6 | *.ntvs* 7 | *.njsproj 8 | *.sln 9 | *.sw? 10 | *.local 11 | 12 | # Logs 13 | logs 14 | *.log* 15 | 16 | # 忽略项目文件 17 | dist 18 | node_modules 19 | src/static/ 20 | src/manifest.json 21 | src/pages.json 22 | .* 23 | *-lock.* 24 | 25 | # 不忽略的项目文件 26 | !.vscode 27 | !.prettierrc* 28 | !.stylelintrc* 29 | !.eslintrc* 30 | -------------------------------------------------------------------------------- /src/types/authorize.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 授权类型模块 3 | */ 4 | 5 | /** 授权错误码 */ 6 | export const enum EAuthErrorCode { 7 | /** 拒绝授权 */ 8 | DENIED = 1, 9 | /** 未登录 */ 10 | NOT_LOGGED_IN, 11 | /** 接口调用失败 */ 12 | API_FAILED 13 | } 14 | 15 | /** 授权错误回调参数 */ 16 | export interface IAuthErrorOptions { 17 | /** 授权错误码 */ 18 | code: EAuthErrorCode 19 | /** 授权错误信息 */ 20 | message: string 21 | } 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # 编辑目录和文件 2 | .idea 3 | .DS_Store 4 | .hbuilderx 5 | coverage 6 | *.suo 7 | *.ntvs* 8 | *.njsproj 9 | *.sln 10 | *.sw 11 | *.local 12 | 13 | # Logs文件 14 | logs 15 | *.log* 16 | 17 | # 项目文件 18 | dist 19 | node_modules 20 | src/components/u-charts 21 | src/static/ 22 | src/manifest.json 23 | src/pages.json 24 | .* 25 | *-lock.* 26 | 27 | # 不忽略的项目文件 28 | !.vscode 29 | !.prettierrc* 30 | !.stylelintrc* 31 | !.eslintrc* 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 程序入口文件 3 | */ 4 | 5 | import { createSSRApp } from "vue" 6 | 7 | import { setupApi } from "@/apis" 8 | import App from "@/App.vue" 9 | import { setupRouterInterceptor } from "@/routerInterceptor" 10 | import { store } from "@/stores" 11 | 12 | import "virtual:uno.css" 13 | 14 | export function createApp () { 15 | 16 | const app = createSSRApp(App) 17 | // 使用商店 18 | app.use(store) 19 | // 初始化接口配置 20 | setupApi() 21 | // 初始化路由拦截器 22 | setupRouterInterceptor() 23 | 24 | return { 25 | app 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: app 和 node 共有的环境类型定义模块 3 | */ 4 | 5 | /** 项目信息(全局) */ 6 | interface IProjectInfo { 7 | /** 项目版本 */ 8 | version: string 9 | /** 项目最后构建时间 */ 10 | lastBuildTime: string 11 | /** 环境变量信息 */ 12 | env: ImportMetaEnv 13 | /** `package.json`信息 */ 14 | pkg: { 15 | /** 包名 */ 16 | name: string 17 | /** 包版本 */ 18 | version: string 19 | /** 生产依赖 */ 20 | dependencies: Record 21 | } 22 | } 23 | 24 | /** 项目信息(全局) */ 25 | declare const __PROJECT_INFO__: IProjectInfo 26 | -------------------------------------------------------------------------------- /src/components/picker/type.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 选择器组件公共类型 3 | */ 4 | 5 | import type { PickerBaseEvent } from "nutui-uniapp" 6 | 7 | /** 选择器组件 基础动作类型 */ 8 | export type TPickerBaseActionType = "click-cancel-button" | "click-confirm-button" 9 | 10 | /** 选择器组件 基础选择结果类型 */ 11 | export type TPickerBaseSelectedResult = PickerBaseEvent & { 12 | /** 动作类型 */ 13 | actionType: TPickerBaseActionType 14 | } 15 | 16 | /** 选择器组件 函数式调用时组件卸载回调参数 基础类型 */ 17 | export type TPickerBaseUnmountParam = [TPickerBaseSelectedResult] 18 | 19 | /** 选择器组件 函数式调用时基础结果类型 */ 20 | export type TShowPickerBaseResult = TPickerBaseUnmountParam 21 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 工具函数模块 3 | */ 4 | 5 | /** 导出数据处理相关工具函数 */ 6 | export * from "./data" 7 | /** 导出日期时间相关工具函数 */ 8 | export * from "./dateTime" 9 | /** 导出环境相关工具函数 */ 10 | export * from "./env" 11 | /** 导出表单相关工具函数 */ 12 | export * from "./form" 13 | /** 导出媒体相关工具函数 */ 14 | export * from "./media" 15 | /** 导出位置相关工具函数 */ 16 | export * from "./location" 17 | /** 导出小程序相关工具函数 */ 18 | export * from "./miniProgram" 19 | /** 导出页面相关工具函数 */ 20 | export * from "./pages" 21 | /** 导出工具相关工具函数 */ 22 | export * from "./tool" 23 | /** 导出url相关工具函数 */ 24 | export * from "./url" 25 | /** 导出用户信息相关工具函数 */ 26 | export * from "./userInfo" 27 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: store模块 3 | */ 4 | 5 | export * from "./activity" 6 | export * from "./tabBar" 7 | export * from "./userInfo" 8 | 9 | import { createPinia } from "pinia" 10 | import { createPersistedState } from "pinia-plugin-persistedstate" 11 | 12 | /** store 实例 */ 13 | export const store = createPinia() 14 | 15 | // 使用全局持久化状态插件 16 | store.use( 17 | createPersistedState({ 18 | // 设置全局存储键名 默认: store名称 19 | key: id => `persisted_${id}`, 20 | // 设置全局存储方式 默认: localStorage 21 | storage: { 22 | getItem: uni.getStorageSync, 23 | setItem: uni.setStorageSync 24 | } 25 | }) 26 | ) 27 | -------------------------------------------------------------------------------- /src/apis/interceptors/response.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 响应拦截器模块 3 | */ 4 | 5 | import { un } from "@uni-helper/uni-network" 6 | 7 | /** 8 | * FUN: 设置响应拦截器 9 | * 10 | * @author dyb-dev 11 | * @date 21/02/2025/ 21:55:38 12 | */ 13 | export const setupResponseInterceptor = () => { 14 | 15 | // 设置响应拦截器 16 | un.interceptors.response.use( 17 | response => { 18 | 19 | response.success = response.errMsg?.includes("request:fail") ? false : true 20 | response.message = response.errMsg || (response.success ? "请求成功" : "请求失败") 21 | return response 22 | 23 | }, 24 | undefined, 25 | { synchronous: true } 26 | ) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/form/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 表单相关工具函数 3 | */ 4 | 5 | /** 导出身份证相关工具函数 */ 6 | export * from "./identityCard" 7 | 8 | /** 9 | * FUN: 是否为手机号 10 | * 11 | * @author dyb-dev 12 | * @date 14/10/2024/ 15:34:16 13 | * @param {string} phone - 手机号 14 | * @returns {*} {boolean} - 是否为手机号 15 | */ 16 | export const isPhoneNumber = (phone: string): boolean => /^1[3456789]\d{9}$/.test(phone) 17 | 18 | /** 19 | * FUN: 是否为邮箱 20 | * 21 | * @author dyb-dev 22 | * @date 14/10/2024/ 15:35:02 23 | * @param {string} email - 邮箱 24 | * @returns {*} {boolean} - 是否为邮箱 25 | */ 26 | export const isEmail = (email: string): boolean => /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(email) 27 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: node环境ts配置 3 | */ 4 | 5 | { 6 | "extends": "@dyb-dev/ts-config/node", 7 | "include": [ 8 | "manifest.config.ts", 9 | "pages.config.ts", 10 | "vite.config.ts", 11 | "vite/**/*.ts", 12 | "vite/**/*.d.ts", 13 | "types/**/*.ts", 14 | "types/**/*.d.ts" 15 | ], 16 | "exclude": ["node_modules", "**/node_modules", ".history"], 17 | "compilerOptions": { 18 | // 指定要包含的类型定义文件 19 | "types": ["vite/client", "@dyb-dev/ts-config/types"], 20 | // 指定用于存储 TypeScript 编译器在增量编译模式下生成的编译信息的文件路径,以便下次编译时可以使用 21 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/types/userInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 用户信息类型模块 3 | */ 4 | 5 | /** 性别类型枚举 */ 6 | export const enum EGenderType { 7 | /** 男 */ 8 | Man = 1, 9 | /** 女 */ 10 | Woman = 2 11 | } 12 | 13 | /** 证件类型枚举 */ 14 | export const enum ECertificatesType { 15 | /** 身份证 */ 16 | IdCard = 1, 17 | /** 军人证 */ 18 | MilitaryCard, 19 | /** 护照 */ 20 | Passport, 21 | /** 出生证 */ 22 | BirthCertificate, 23 | /** 港澳台通行证 */ 24 | HkMoTwPass, 25 | /** 士兵证 */ 26 | SoldierCard, 27 | /** 警官证 */ 28 | PoliceCard, 29 | /** 港澳台居民居住证 */ 30 | HkMoTwResidentPermit, 31 | /** 外国人永久居留身份证 */ 32 | ForeignerPermanentResidentIdCard, 33 | /** 居民户口薄 */ 34 | ResidentAccountBook 35 | } 36 | -------------------------------------------------------------------------------- /src/types/dts/api.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: api 类型声明补充文件 3 | */ 4 | 5 | import type { UnResponse } from "@uni-helper/uni-network" 6 | 7 | declare module "@uni-helper/uni-network" { 8 | interface UnResponse { 9 | /** 请求是否成功 */ 10 | success: boolean 11 | /** 结果描述信息 */ 12 | message: string 13 | } 14 | } 15 | 16 | declare global { 17 | /** 测试请求配置 */ 18 | interface ITestRequestConfig = Record> { 19 | /** 是否启用测试模式 */ 20 | test: boolean 21 | /** 测试模式下请求延迟时间 单位: 毫秒 */ 22 | testDelay?: number 23 | /** 测试模式下请求结果 */ 24 | testResult: TModifyProperties, "success" | "message" | "data">, "data"> 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 接口模块 3 | */ 4 | 5 | /** 导出接口模块 */ 6 | export * from "./modules" 7 | 8 | import { un } from "@uni-helper/uni-network" 9 | 10 | import { getCurrentServerUrl, isDevEnv } from "@/utils" 11 | 12 | import { setupApiInterceptor } from "./interceptors" 13 | import { setGlobalTestRequestConfig } from "./request" 14 | 15 | /** 16 | * FUN: 设置接口配置 17 | * 18 | * @author dyb-dev 19 | * @date 08/10/2024/ 20:47:43 20 | */ 21 | export const setupApi = () => { 22 | 23 | // 设置请求基础路径 24 | un.defaults.baseUrl = getCurrentServerUrl() + __PROJECT_INFO__.env.VITE_API_BASE_PATH 25 | 26 | // 设置接口拦截器 27 | setupApiInterceptor() 28 | 29 | // 设置全局测试请求配置 30 | setGlobalTestRequestConfig({ 31 | test: isDevEnv() && true, 32 | testDelay: 500 33 | }) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /vite/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: vite配置工具函数 3 | */ 4 | 5 | import dayjs from "dayjs" 6 | 7 | import pkg from "../package.json" 8 | 9 | /** 10 | * FUN: 生成项目信息 11 | * 12 | * @author dyb-dev 13 | * @date 30/09/2024/ 15:00:27 14 | * @param {ImportMetaEnv} env - 环境变量 15 | * @returns {*} {IProjectInfo} 16 | */ 17 | export const generateProjectInfo = (env: ImportMetaEnv): IProjectInfo => { 18 | 19 | const { name, version, dependencies } = pkg 20 | const _dayObj = dayjs() 21 | const _projectVersion = _dayObj.format("YYYYMMDDHHmmss") 22 | const _lastBuildTime = _dayObj.format("YYYY-MM-DD HH:mm:ss") 23 | 24 | const _projectInfo: IProjectInfo = { 25 | version: _projectVersion, 26 | lastBuildTime: _lastBuildTime, 27 | env, 28 | pkg: { name, version, dependencies } 29 | } 30 | 31 | return _projectInfo 32 | 33 | } 34 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: unocss 配置 3 | */ 4 | 5 | import { presetUni } from "@uni-helper/unocss-preset-uni" 6 | import { defineConfig } from "unocss" 7 | 8 | export default defineConfig({ 9 | // 内容 10 | content: { 11 | // 管道 12 | pipeline: { 13 | // 包含文件 14 | include: [ 15 | // 默认支持文件 16 | /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/, 17 | // 支持 js/ts 文件 18 | /\.([jt]s)($|\?)/ 19 | ] 20 | } 21 | }, 22 | // 预设 23 | presets: [ 24 | presetUni({ 25 | // 是否启用属性化语法 26 | attributify: false 27 | }) 28 | ], 29 | // 快捷方式 30 | shortcuts: { 31 | "flex-center": "flex justify-center items-center", 32 | "flex-x-center": "flex justify-center", 33 | "flex-y-center": "flex items-center" 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 组件模块 3 | */ 4 | 5 | /** 导出选择器组件相关工具函数 */ 6 | export * from "./picker" 7 | /** 导出弹窗组件相关工具函数 */ 8 | export * from "./popup" 9 | /** 导出对话框组件相关工具函数 */ 10 | export * from "./dialog" 11 | /** 导出用户信息组件相关工具函数 */ 12 | export * from "./auth" 13 | /** 导出提供组件选项相关工具函数 */ 14 | export * from "./provideComponentOptions" 15 | /** 导出标签&值栏组件 */ 16 | export * from "./LabelValueBar.vue" 17 | /** 导出列表组件 */ 18 | export * from "./List.vue" 19 | /** 导出表单控件组件 */ 20 | export * from "./FormControl.vue" 21 | /** 导出头像昵称组件 */ 22 | export * from "./AvatarNickname.vue" 23 | /** 导出单元格组件 */ 24 | export * from "./Cell.vue" 25 | /** 导出标签&值栏组件 */ 26 | export * from "./LabelValueBar.vue" 27 | /** 导出选项卡面板列表组件 */ 28 | export * from "./TabsPaneList.vue" 29 | /** 导出上传文件组件 */ 30 | export * from "./Uploader.vue" 31 | /** 导出轮播组件 */ 32 | export * from "./SwiperPro.vue" 33 | /** 导出底部操作栏组件 */ 34 | export * from "./FooterActionsBar.vue" 35 | -------------------------------------------------------------------------------- /src/utils/media/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 媒体相关工具函数 3 | */ 4 | 5 | /** CONST: 常见的图片扩展名 */ 6 | const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"] 7 | 8 | /** 9 | * FUN: 是否为有效的图片路径 10 | * - 支持网络在线地址、绝对路径、相对路径或根目录文件,并忽略查询参数 11 | * 12 | * @author dyb-dev 13 | * @date 23/09/2024/ 11:31:25 14 | * @param {string} str 图片路径 15 | * @returns {*} {boolean} 是否为图片路径 16 | */ 17 | export const isImagePath = (str: string): boolean => { 18 | 19 | // 动态生成正则表达式来匹配图片扩展名 20 | const _pattern = new RegExp(`^(https?:\\/\\/|\\/|[^\\s]*\\/)?[^\\s]+\\.(${IMAGE_EXTENSIONS.join("|")})(\\?.*)?$`, "i") 21 | 22 | // 首先验证是否符合路径规则 23 | if (!_pattern.test(str)) { 24 | 25 | return false 26 | 27 | } 28 | 29 | // 提取文件扩展名并验证是否为图片扩展名 30 | const _extension = str.split(".").pop()?.split("?")[0].toLowerCase() || "" 31 | 32 | // 检查提取到的扩展名是否在 IMAGE_EXTENSIONS 中 33 | return IMAGE_EXTENSIONS.includes(_extension) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/routerInterceptor/login.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 登录拦截器模块 3 | */ 4 | 5 | import { useUserInfoStoreWithOut } from "@/stores" 6 | import { getPageConfig, navigateToLogin } from "@/utils" 7 | 8 | import type { TRouterPreInterceptor } from "." 9 | 10 | /** CONST: 用户信息Store */ 11 | const { userInfoStoreState } = useUserInfoStoreWithOut() 12 | 13 | /** 14 | * FUN: 设置登录前置拦截器 15 | * 16 | * @author dyb-dev 17 | * @date 02/10/2024/ 14:54:31 18 | * @param {string} url - 路由路径 19 | * @returns {*} 是否继续执行 20 | */ 21 | export const setupLoginPreInterceptor: TRouterPreInterceptor = ({ path, query }) => { 22 | 23 | // 如果是没有登录且需要登录的页面,跳转到登录页 24 | if (!userInfoStoreState.isLogin && getPageConfig(path)?.needLogin) { 25 | 26 | navigateToLogin({ 27 | redirectPath: path as NavigateToOptions["url"], 28 | query, 29 | findBackDelta: false 30 | }) 31 | return false 32 | 33 | } 34 | 35 | return true 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/types/dts/pages.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by vite-plugin-uni-pages 5 | 6 | interface NavigateToOptions { 7 | url: "/pages/home" | 8 | "/pages/launch" | 9 | "/pages/list" | 10 | "/pages/login" | 11 | "/pages/my" | 12 | "/pages/test" | 13 | "/subPackages/webview/pages/webview"; 14 | } 15 | interface RedirectToOptions extends NavigateToOptions {} 16 | 17 | interface SwitchTabOptions { 18 | url: "/pages/home" | "/pages/list" | "/pages/my" 19 | } 20 | 21 | type ReLaunchOptions = NavigateToOptions | SwitchTabOptions; 22 | 23 | declare interface Uni { 24 | navigateTo(options: UniNamespace.NavigateToOptions & NavigateToOptions): void; 25 | redirectTo(options: UniNamespace.RedirectToOptions & RedirectToOptions): void; 26 | switchTab(options: UniNamespace.SwitchTabOptions & SwitchTabOptions): void; 27 | reLaunch(options: UniNamespace.ReLaunchOptions & ReLaunchOptions): void; 28 | } 29 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | 47 | 51 | -------------------------------------------------------------------------------- /src/constants/userInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 用户信息常量模块 3 | */ 4 | 5 | import { ECertificatesType, EGenderType } from "@/types" 6 | 7 | /** CONST: 性别配置列表 */ 8 | export const GENDER_CONFIG_LIST = [ 9 | { type: EGenderType.Man, desc: "男" }, 10 | { type: EGenderType.Woman, desc: "女" } 11 | ] 12 | 13 | /** CONST: 证件配置列表 */ 14 | export const CERTIFICATE_CONFIG_LIST = [ 15 | { type: ECertificatesType.IdCard, desc: "身份证" }, 16 | { type: ECertificatesType.MilitaryCard, desc: "军人证" }, 17 | { type: ECertificatesType.Passport, desc: "护照" }, 18 | { type: ECertificatesType.BirthCertificate, desc: "出生证" }, 19 | { type: ECertificatesType.HkMoTwPass, desc: "港澳台通行证" }, 20 | { type: ECertificatesType.SoldierCard, desc: "士兵证" }, 21 | { type: ECertificatesType.PoliceCard, desc: "警官证" }, 22 | { type: ECertificatesType.HkMoTwResidentPermit, desc: "港澳台居民居住证" }, 23 | { type: ECertificatesType.ForeignerPermanentResidentIdCard, desc: "外国人永久居留身份证" }, 24 | { type: ECertificatesType.ResidentAccountBook, desc: "居民户口薄" } 25 | ] 26 | -------------------------------------------------------------------------------- /src/stores/activity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 活动状态管理 3 | */ 4 | 5 | import { defineStore } from "pinia" 6 | import { reactive } from "vue" 7 | 8 | import { store } from "." 9 | 10 | /** Store 状态类型 */ 11 | interface IActivityStoreState { 12 | count: number 13 | } 14 | 15 | /** Store 实例 */ 16 | export const useActivityStore = defineStore( 17 | "ActivityStore", 18 | () => { 19 | 20 | /** Store 状态 */ 21 | const activityStoreState = reactive({ 22 | count: 0 23 | }) 24 | 25 | return { activityStoreState } 26 | 27 | } 28 | // { 29 | // // 配置持久化状态,如果不需要可以删除掉 或 设置为false 30 | // persist: { 31 | // // 指定需要持久化的状态 32 | // pick: ['state.count'] 33 | // } 34 | // } 35 | ) 36 | 37 | /** 38 | * FUN: 使用状态管理 39 | * - 在没有Vue组件上下文的情况下使用 40 | * 41 | * @author dyb-dev 42 | * @date 15/09/2024/ 23:53:35 43 | * @returns store实例 44 | */ 45 | export const useActivityStoreWithOut = () => { 46 | 47 | return useActivityStore(store) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/components/provideComponentOptions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 提供组件选项相关工具函数 3 | */ 4 | 5 | import { provide, ref } from "vue" 6 | 7 | import type { Ref } from "vue" 8 | 9 | /** 10 | * 默认选项接口 11 | */ 12 | export interface IDefaultOptions { 13 | /** 是否显示 */ 14 | show: boolean 15 | /** 组件唯一标识key */ 16 | customKey: string 17 | /** 卸载组件回调 */ 18 | unmount: (...args: any[]) => void 19 | } 20 | 21 | /** 22 | * 用于过滤掉默认选项的类型工具 23 | * 24 | * @template Target - 目标类型 25 | */ 26 | export type TFilteredDefaultOptions = Omit> 27 | 28 | /** 29 | * 提供组件选项 30 | * 31 | * @author dyb-dev 32 | * @date 29/10/2024/ 21:55:37 33 | * @param {Key} customKey - 组件唯一标识key 34 | * @returns {*} {Ref} - 组件选项 35 | */ 36 | export const providerComponentOptions = = Record>( 37 | customKey: Key 38 | ): Ref => { 39 | 40 | const _options = ref({}) as Ref 41 | 42 | provide(customKey, _options as any) 43 | 44 | return _options 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: app环境ts配置 3 | */ 4 | 5 | { 6 | "extends": "@dyb-dev/ts-config/vue", 7 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "types/**/*.ts", "types/**/*.d.ts"], 8 | "exclude": ["node_modules", "**/node_modules", ".history"], 9 | "compilerOptions": { 10 | // 指定要包含的库 11 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 12 | // 指定要包含的类型定义文件 13 | "types": ["@dyb-dev/ts-config/types", "@uni-helper/uni-app-types", "miniprogram-api-typings"], 14 | // 指定用于存储 TypeScript 编译器在增量编译模式下生成的编译信息的文件路径,以便下次编译时可以使用 15 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 16 | // 设置基础路径,解决模块导入路径问题 17 | "baseUrl": "./", 18 | // 配置路径映射,简化模块导入路径 19 | "paths": { 20 | "@/*": ["./src/*"], 21 | "nutui-uniapp/components/*.vue": [""] 22 | } 23 | }, 24 | // Volar(Vue 3 的 TypeScript 支持插件)相关的配置项 25 | "vueCompilerOptions": { 26 | // 扩展模板组件的类型检查 例如: view、text组件 27 | "plugins": ["@uni-helper/uni-app-types/volar-plugin"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 dyb-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 组件目录 默认:components 2 | VITE_COMPONENT_DIR = components 3 | 4 | # 页面目录 默认:pages 5 | VITE_PAGE_DIR = pages 6 | 7 | # `src`下 分包目录 默认:subPackages 8 | VITE_SUB_PACKAGE_DIR = subPackages 9 | 10 | # `VITE_SUB_PACKAGE_DIR`下 分包子目录集合 如果涉及多个子分包,用逗号分隔 默认:webview 11 | VITE_SUB_PACKAGE_CHILD_DIRS = webview 12 | 13 | # 启动页路径 默认:pages/launch 14 | VITE_LAUNCH_PATH = ${VITE_PAGE_DIR}/launch 15 | 16 | # 是否使用启动页 默认:false 17 | # 可以在小程序运行时控制首次显示的页面,使用时注意: 18 | # 1.小程序首页变为`VITE_LAUNCH_PATH`,页面跳转逻辑将在 `launch.vue` 中的 `onLoad` 进行,可根据需求修改 19 | # 2.小程序码路径示例: 目标页面为`a`页面,则路径应该为 `/pages/launch?targetPath=pages/a&test=1`,其中`test=1`会传递给`a`页面 20 | VITE_USE_LAUNCH_PAGE = false 21 | 22 | # 登录页面的路径 默认:pages/login 23 | VITE_LOGIN_PATH = ${VITE_PAGE_DIR}/login 24 | 25 | # 首页路径 默认:pages/home 26 | VITE_HOME_PATH = ${VITE_PAGE_DIR}/home 27 | 28 | # TODO: wx小程序appid 29 | VITE_MP_WX_APPID = 30 | 31 | # 开发环境服务器网址(小程序开发版、体验版用到) 默认:http://xxx.com 32 | VITE_DEV_SERVER_URL = http://jsonplaceholder.typicode.com 33 | 34 | # 生产环境服务器网址(小程序体验版、线上版用到) 默认:http://xxx.com 35 | VITE_PROD_SERVER_URL = http://jsonplaceholder.typicode.com 36 | 37 | # 接口请求基础路径 默认:/api 38 | VITE_API_BASE_PATH = /posts 39 | -------------------------------------------------------------------------------- /src/static/image/mp_wx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/utils/tool/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 基础相关工具函数 3 | */ 4 | 5 | /** 任意函数类型声明 */ 6 | type TFuncType = (...args: any[]) => void 7 | 8 | /** 9 | * 创建防抖函数 10 | * 11 | * @author dyb-dev 12 | * @date 18/08/2024/ 17:04:51 13 | * @param {TFuncType} fn - 需要防抖的函数 14 | * @param {number} wait - 防抖时间 15 | * @param {boolean} [immediate=false] - 是否立即执行 16 | * @returns {*} {TFuncType} - 防抖函数 17 | */ 18 | export const debounce = (fn: TFuncType, wait: number, immediate = false): TFuncType => { 19 | 20 | let _timeout: ReturnType | undefined 21 | 22 | return (...args: any[]) => { 23 | 24 | const _this = this 25 | 26 | // 延迟执行函数 27 | const later = function() { 28 | 29 | _timeout = undefined 30 | if (!immediate) { 31 | 32 | fn.apply(_this, args) 33 | 34 | } 35 | 36 | } 37 | 38 | if (immediate) { 39 | 40 | fn.apply(_this, args) 41 | 42 | } 43 | else { 44 | 45 | clearTimeout(_timeout) 46 | _timeout = setTimeout(later, wait) 47 | 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/my.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 34 | 35 | 46 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniapp-mp-wx-template", 3 | "appid": "", 4 | "description": "基于`uni-app + vue3 + ts`搭建的微信小程序模板", 5 | "versionName": "1.3.3", 6 | "versionCode": "100", 7 | "transformPx": false, 8 | "app-plus": { 9 | "usingComponents": true, 10 | "nvueStyleCompiler": "uni-app", 11 | "compilerVersion": 3, 12 | "splashscreen": { 13 | "alwaysShowBeforeRender": true, 14 | "waiting": true, 15 | "autoclose": true, 16 | "delay": 0 17 | }, 18 | "modules": {}, 19 | "distribute": { 20 | "android": { 21 | "permissions": [] 22 | }, 23 | "ios": {}, 24 | "sdkConfigs": {} 25 | } 26 | }, 27 | "quickapp": {}, 28 | "mp-weixin": { 29 | "appid": "", 30 | "setting": { 31 | "urlCheck": false, 32 | "es6": true, 33 | "postcss": true, 34 | "minified": true, 35 | "bigPackageSizeSupport": true 36 | }, 37 | "usingComponents": true, 38 | "lazyCodeLoading": "requiredComponents", 39 | "mergeVirtualHostAttributes": true, 40 | "optimization": { 41 | "subPackages": true 42 | } 43 | }, 44 | "mp-alipay": { 45 | "usingComponents": true 46 | }, 47 | "mp-baidu": { 48 | "usingComponents": true 49 | }, 50 | "mp-toutiao": { 51 | "usingComponents": true 52 | }, 53 | "uniStatistics": { 54 | "enable": false 55 | }, 56 | "vueVersion": "3" 57 | } -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | /** vite环境变量 */ 2 | interface ImportMetaEnv { 3 | // CONST: 共用 4 | /** 组件目录 默认:components */ 5 | readonly VITE_COMPONENT_DIR: string 6 | /** 页面目录 默认:pages */ 7 | readonly VITE_PAGE_DIR: string 8 | /** `src`下 分包目录 默认:subPackages */ 9 | readonly VITE_SUB_PACKAGE_DIR: string 10 | /** `VITE_SUB_PACKAGE_DIR`下 分包子目录集合 如果涉及多个子分包,用逗号分隔 默认:webview */ 11 | readonly VITE_SUB_PACKAGE_CHILD_DIRS: string 12 | /** 启动页路径 默认:pages/launch */ 13 | readonly VITE_LAUNCH_PATH: string 14 | /** 15 | * 是否使用启动页 默认:false 16 | * - 可以在小程序运行时控制首次显示的页面,使用时注意: 17 | * - 小程序首页变为`VITE_LAUNCH_PATH`,页面跳转逻辑将在`App.vue`中进行,可根据需求修改 18 | * - 小程序码路径示例: 目标页面为`a`页面,则路径应该为 `/pages/launch?targetPath=pages/a&test=1`,其中`test=1`会传递给`a`页面 19 | */ 20 | readonly VITE_USE_LAUNCH_PAGE: string 21 | /** 登录页面的路径 默认:pages/login */ 22 | readonly VITE_LOGIN_PATH: string 23 | /** 首页路径 默认:pages/home */ 24 | readonly VITE_HOME_PATH: string 25 | /** 开发环境服务器网址(小程序开发版、体验版用到) 默认:http://xxx.com */ 26 | readonly VITE_DEV_SERVER_URL: string 27 | /** 生产环境服务器网址(小程序体验版、线上版用到) 默认:http://xxx.com */ 28 | readonly VITE_PROD_SERVER_URL: string 29 | /** 接口请求基础路径 默认:/api */ 30 | readonly VITE_API_BASE_PATH: string 31 | /** 32 | * 用户 node 环境 33 | * - development: 开发环境 34 | * - production: 生产环境 35 | */ 36 | readonly VITE_USER_NODE_ENV: "development" | "production" 37 | 38 | // CONST: wx小程序相关 39 | /** wx小程序appid */ 40 | readonly VITE_MP_WX_APPID: string 41 | } 42 | 43 | /** 扩展 ImportMeta 接口 */ 44 | interface ImportMeta { 45 | readonly env: ImportMetaEnv 46 | } 47 | -------------------------------------------------------------------------------- /src/components/popup/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 弹窗组件相关工具函数 3 | */ 4 | 5 | /** 导出基础弹窗类型 */ 6 | export type * from "./Popup.vue" 7 | 8 | import { providerComponentOptions } from "@/components" 9 | 10 | import type { IPopupOptions, TPopupUnmountParam, TPopupCustomKey } from "./Popup.vue" 11 | import type { TFilteredDefaultOptions } from "@/components" 12 | 13 | /** 显示弹窗的选项 */ 14 | export type TShowPopupOptions = TFilteredDefaultOptions 15 | 16 | /** 显示弹窗的结果 */ 17 | export type TShowPopupResult = TPopupUnmountParam 18 | 19 | /** 20 | * 使用弹窗 21 | * 22 | * @author dyb-dev 23 | * @date 29/10/2024/ 22:10:25 24 | * @param {TPopupCustomKey} [customKey='__POPUP__'] - 弹窗唯一标识key 默认: `__POPUP__` 25 | * @returns {*} {TUsePopup} - 弹窗相关函数 26 | */ 27 | export const usePopup = (customKey: string = "") => { 28 | 29 | const _customKey: TPopupCustomKey = `__POPUP__${customKey}` 30 | const _options = providerComponentOptions(_customKey) 31 | 32 | /** 33 | * 显示弹窗 34 | * 35 | * @author dyb-dev 36 | * @date 29/10/2024/ 22:05:36 37 | * @param {TShowPopupOptions} options - 弹窗选项 38 | * @returns {*} {Promise} - 显示弹窗的结果 39 | */ 40 | const showPopup = (options: TShowPopupOptions): Promise => { 41 | 42 | return new Promise(resolve => { 43 | 44 | _options.value = { 45 | ...options, 46 | show: true, 47 | unmount: (...args: TShowPopupResult) => resolve(args) 48 | } 49 | 50 | }) 51 | 52 | } 53 | return { 54 | showPopup 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/funs/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: scss全局函数文件 3 | */ 4 | 5 | /** 必须写在顶部 */ 6 | @use "sass:list"; 7 | @use "sass:color"; 8 | 9 | /** 10 | FUN: 创建不同方向的文本阴影函数 11 | $color - 阴影颜色 12 | $long - 阴影长度 可传 正负 数字控制阴影方向 13 | $direction - 阴影方向 "horizontal" | "vertical" | "all" 14 | $fade-step - 颜色透明度递减步长 15 | */ 16 | @function create-directional-text-shadow-fun($color: #0ebeff, $long: 50, $direction: "all", $fade-step: 0.02) { 17 | $current-color: $color; 18 | $val: 0 0 $current-color; 19 | $shadow-values: (#{$val}); 20 | 21 | @for $i from 1 through $long { 22 | $current-color: fade-out($current-color, $fade-step); 23 | 24 | @if $direction == "horizontal" { 25 | $val: #{$i}px 0 #{$current-color}; 26 | } @else if $direction == "vertical" { 27 | $val: 0 #{$i}px #{$current-color}; 28 | } @else { 29 | $val: #{$i}px #{$i}px #{$current-color}; 30 | } 31 | 32 | /* stylelint-disable-next-line order/order */ 33 | $shadow-values: list.append($shadow-values, $val, comma); 34 | } 35 | 36 | @return $shadow-values; 37 | } 38 | 39 | /** 40 | FUN: 创建具有递增模糊半径的文本阴影函数 41 | $color - 阴影颜色 42 | $iterations - 迭代次数 43 | $increment - 模糊半径递增值 44 | */ 45 | @function create-text-shadow-fun($color: #0ebeff, $iterations: 5, $increment: 5px) { 46 | $shadow-values: (); 47 | $current-radius: "0px"; 48 | 49 | @for $i from 1 through $iterations { 50 | $current-radius: $increment * $i; 51 | $shadow-values: list.append($shadow-values, 0 0 $current-radius $color, comma); 52 | } 53 | 54 | @return $shadow-values; 55 | } 56 | -------------------------------------------------------------------------------- /manifest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 应用配置 3 | */ 4 | 5 | import { defineManifestConfig } from "@uni-helper/vite-plugin-uni-manifest" 6 | 7 | import pkg from "./package.json" 8 | import { VITE_ENV } from "./vite.config" 9 | 10 | /** CONST: 获取.env文件的环境变量 */ 11 | const { VITE_MP_WX_APPID } = VITE_ENV 12 | 13 | export default defineManifestConfig({ 14 | // 应用标题 15 | name: pkg.name, 16 | // 应用的唯一标识符 17 | appid: "", 18 | // 应用的描述 19 | description: pkg.description, 20 | // 应用版本名称 21 | versionName: pkg.version, 22 | // 应用版本号,主要用于应用的内部版本控制 23 | versionCode: "100", 24 | // 是否自动转换 px 单位 默认: true 25 | transformPx: false, 26 | // uni 统计配置项 27 | uniStatistics: { 28 | // 是否启用统计 默认: true 29 | enable: false 30 | }, 31 | 32 | /* 小程序特有相关 */ 33 | "mp-weixin": { 34 | // 微信小程序的应用 ID 35 | appid: VITE_MP_WX_APPID, 36 | // 是否使用自定义组件 37 | usingComponents: true, 38 | // 是否开启小程序按需注入特性 39 | lazyCodeLoading: "requiredComponents", 40 | // 合并组件虚拟节点外层属性(目前仅支持 style、class 属性) 41 | mergeVirtualHostAttributes: true, 42 | // 微信小程序的优化配置 43 | optimization: { 44 | // 是否开启分包优化 45 | subPackages: true 46 | }, 47 | // 微信小程序的相关设置 48 | setting: { 49 | // 是否启用 URL 校验 50 | urlCheck: false, 51 | // 是否ES6 转 ES5 52 | es6: true, 53 | // 上传代码时样式是否自动补全 54 | postcss: true, 55 | // 上传代码时是否自动压缩 56 | minified: true, 57 | // 预览及真机调试时包体积上限是否调整为4M,默认: true 58 | bigPackageSizeSupport: true 59 | } 60 | } 61 | }) 62 | -------------------------------------------------------------------------------- /src/hooks/share/useShare.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 使用分享 3 | */ 4 | 5 | import { useUserInfoStoreWithOut } from "@/stores" 6 | import { setUrlQueryValue } from "@/utils" 7 | 8 | /** 分享配置 */ 9 | interface IShareConfig extends Page.CustomShareContent { 10 | /** 11 | * 转发路径,必须是以 / 开头的完整路径。默认值:当前页面 path 12 | */ 13 | path?: NavigateToOptions["url"] 14 | /** 15 | * 邀请用户ID 16 | */ 17 | inviteUserId?: string 18 | /** 19 | * 来源 20 | */ 21 | source?: string 22 | } 23 | 24 | /** CONST: 默认分享配置 */ 25 | const DEFAULT_SHARE_CONFIG: IShareConfig = { 26 | title: "祝您前程似锦,未来可期!", 27 | imageUrl: "/static/image/share.jpg" 28 | } 29 | 30 | // HOOKS: 使用用户信息Store 31 | const { userInfoStoreState } = useUserInfoStoreWithOut() 32 | 33 | /** 34 | * HOOKS: 使用分享 35 | * 36 | * @author dyb-dev 37 | * @date 30/10/2024/ 12:29:17 38 | * @param {IShareConfig} [shareConfig] 分享配置 39 | * @returns {*} {Page.CustomShareContent} 分享配置 40 | */ 41 | export const useShare = (shareConfig?: IShareConfig): Page.CustomShareContent => { 42 | 43 | /** 当前分享配置 */ 44 | const _shareConfig = { ...DEFAULT_SHARE_CONFIG, ...shareConfig } 45 | 46 | let _path = _shareConfig.path || "/" + __PROJECT_INFO__.env.VITE_HOME_PATH 47 | 48 | /** 邀请用户ID */ 49 | const _inviteUserId = _shareConfig.inviteUserId ?? userInfoStoreState.userId 50 | if (_inviteUserId) { 51 | 52 | _path = setUrlQueryValue(_path, "inviteUserId", _inviteUserId) 53 | delete _shareConfig.inviteUserId 54 | 55 | } 56 | 57 | // 来源 58 | const _source = _shareConfig.source ?? "share" 59 | if (_source) { 60 | 61 | _path = setUrlQueryValue(_path, "source", _source) 62 | delete _shareConfig.source 63 | 64 | } 65 | 66 | _shareConfig.path = _path as NavigateToOptions["url"] 67 | 68 | return _shareConfig 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/hooks/timer/usePolling.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 使用轮询器 3 | */ 4 | 5 | /** 使用轮询器返回值 */ 6 | export interface IUsePollingReturn { 7 | /** 开始轮询 */ 8 | start: () => void 9 | /** 停止轮询 */ 10 | stop: () => void 11 | } 12 | 13 | /** 14 | * HOOKS: 使用轮询器 15 | * 16 | * @author dyb-dev 17 | * @date 28/05/2025/ 23:01:57 18 | * @param {(() => boolean | void | Promise)} callback 回调函数 19 | * @param {number} [interval=1000] 轮询间隔,默认为 1000ms 20 | * @returns {*} {IUsePollingReturn} 返回值 21 | */ 22 | export const usePolling = ( 23 | callback: () => boolean | void | Promise, 24 | interval: number = 1000 25 | ): IUsePollingReturn => { 26 | 27 | /** 轮询器 */ 28 | let _timer: TTimeoutId 29 | /** 是否轮询中 */ 30 | let _polling: boolean = false 31 | 32 | /** 启动轮询 */ 33 | const start = () => { 34 | 35 | // 如果已经在轮询中,不重复启动 36 | if (_polling) { 37 | 38 | return 39 | 40 | } 41 | 42 | _polling = true 43 | 44 | const _poll = async() => { 45 | 46 | try { 47 | 48 | // 执行回调函数,判断是否需要继续轮询 49 | const _isStop = await callback() 50 | 51 | if (!_isStop) { 52 | 53 | // 如果不需要停止,继续轮询 54 | _timer = setTimeout(() => { 55 | 56 | _poll() 57 | 58 | }, interval) 59 | return 60 | 61 | } 62 | 63 | stop() 64 | 65 | } 66 | catch (error) { 67 | 68 | stop() 69 | console.error("usePolling()", error) 70 | 71 | } 72 | 73 | } 74 | 75 | _poll() 76 | 77 | } 78 | 79 | /** 停止轮询 */ 80 | const stop = () => { 81 | 82 | clearTimeout(_timer) 83 | _polling = false 84 | 85 | } 86 | 87 | return { start, stop } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/routerInterceptor/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 路由拦截器模块 3 | */ 4 | 5 | import queryString from "query-string" 6 | 7 | import { setupLoginPreInterceptor } from "./login" 8 | 9 | /** 10 | * 路由Invoke参数 11 | */ 12 | interface IRouterInvokeParams { 13 | /** url 调用除了`uni.navigateBack`方法时才有此参数 */ 14 | url?: string 15 | /** 返回层级 调用`uni.navigateBack`方法时才有此参数 */ 16 | delta?: number 17 | } 18 | 19 | /** 20 | * 路由前置拦截器参数 21 | */ 22 | export interface IRouterPreInterceptorParams { 23 | /** 目标路径 */ 24 | path: string 25 | /** 目标路由参数 */ 26 | query: Record 27 | /** 返回层级 */ 28 | delta: number 29 | } 30 | 31 | /** 路由前置拦截器函数 */ 32 | export type TRouterPreInterceptor = (params: IRouterPreInterceptorParams) => boolean 33 | 34 | /** 35 | * FUN: 设置路由拦截器 36 | * 37 | * @author dyb-dev 38 | * @date 02/10/2024/ 15:03:56 39 | */ 40 | export const setupRouterInterceptor = () => { 41 | 42 | const _options: UniNamespace.InterceptorOptions = { 43 | // 前置拦截器 44 | invoke({ url = "", delta = 0 }: IRouterInvokeParams) { 45 | 46 | /** url解析结果 */ 47 | const _parseResult = queryString.parseUrl(url) 48 | /** 路由前置拦截器参数 */ 49 | const _params: IRouterPreInterceptorParams = { 50 | path: _parseResult.url, 51 | query: _parseResult.query, 52 | delta 53 | } 54 | 55 | /** 是否继续执行 */ 56 | let isNext = true 57 | 58 | isNext = setupLoginPreInterceptor(_params) 59 | 60 | return isNext 61 | 62 | } 63 | // 后置拦截器(一般用不到) 64 | // returnValue() { 65 | 66 | // } 67 | } 68 | 69 | uni.addInterceptor("navigateTo", _options) 70 | uni.addInterceptor("redirectTo", _options) 71 | uni.addInterceptor("reLaunch", _options) 72 | uni.addInterceptor("switchTab", _options) 73 | uni.addInterceptor("navigateBack", _options) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/pages/list.vue: -------------------------------------------------------------------------------- 1 | 4 | 61 | 62 | 78 | -------------------------------------------------------------------------------- /src/styles/variable/uni.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里是uni-app内置的常用样式变量 3 | * 4 | * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 5 | * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App 6 | * 7 | */ 8 | 9 | /** 10 | * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 11 | * 12 | * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 13 | */ 14 | 15 | /* 颜色变量 */ 16 | 17 | /* 行为相关颜色 */ 18 | $uni-color-primary: #007aff; 19 | $uni-color-success: #4cd964; 20 | $uni-color-warning: #f0ad4e; 21 | $uni-color-error: #dd524d; 22 | 23 | /* 文字基本颜色 */ 24 | $uni-text-color: #333; /* 基本色 */ 25 | $uni-text-color-inverse: #fff; /* 反色 */ 26 | $uni-text-color-grey: #999; /* 辅助灰色,如加载更多的提示信息 */ 27 | $uni-text-color-placeholder: #808080; 28 | $uni-text-color-disable: #c0c0c0; 29 | 30 | /* 背景颜色 */ 31 | $uni-bg-color: #fff; 32 | $uni-bg-color-grey: #f8f8f8; 33 | $uni-bg-color-hover: #f1f1f1; /* 点击状态颜色 */ 34 | $uni-bg-color-mask: rgba(0, 0, 0, 0.4); /* 遮罩颜色 */ 35 | 36 | /* 边框颜色 */ 37 | $uni-border-color: #c8c7cc; 38 | 39 | /* 尺寸变量 */ 40 | 41 | /* 文字尺寸 */ 42 | $uni-font-size-sm: 12px; 43 | $uni-font-size-base: 14px; 44 | $uni-font-size-lg: 16px; 45 | 46 | /* 图片尺寸 */ 47 | $uni-img-size-sm: 20px; 48 | $uni-img-size-base: 26px; 49 | $uni-img-size-lg: 40px; 50 | 51 | /* Border Radius */ 52 | $uni-border-radius-sm: 2px; 53 | $uni-border-radius-base: 3px; 54 | $uni-border-radius-lg: 6px; 55 | $uni-border-radius-circle: 50%; 56 | 57 | /* 水平间距 */ 58 | $uni-spacing-row-sm: 5px; 59 | $uni-spacing-row-base: 10px; 60 | $uni-spacing-row-lg: 15px; 61 | 62 | /* 垂直间距 */ 63 | $uni-spacing-col-sm: 4px; 64 | $uni-spacing-col-base: 8px; 65 | $uni-spacing-col-lg: 12px; 66 | 67 | /* 透明度 */ 68 | $uni-opacity-disabled: 0.3; /* 组件禁用态的透明度 */ 69 | 70 | /* 文章场景相关 */ 71 | $uni-color-title: #2c405a; /* 文章标题颜色 */ 72 | $uni-font-size-title: 20px; 73 | $uni-color-subtitle: #555; /* 二级标题颜色 */ 74 | $uni-font-size-subtitle: 18px; 75 | $uni-color-paragraph: #3f536e; /* 文章段落颜色 */ 76 | $uni-font-size-paragraph: 15px; 77 | -------------------------------------------------------------------------------- /src/apis/modules/activity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 本次活动接口模块 3 | */ 4 | 5 | import un from "@uni-helper/uni-network" 6 | 7 | import { sendRequest } from "../request" 8 | 9 | import type { UnResponse } from "@uni-helper/uni-network" 10 | 11 | /** 获取用户信息的结果数据 */ 12 | export interface IGetUserInfoApiResultData { 13 | /** 内容 */ 14 | body: string 15 | /** id */ 16 | id: number 17 | /** 标题 */ 18 | title: string 19 | /** 用户唯一标识 */ 20 | userId: number 21 | } 22 | 23 | /** 24 | * FUN: 获取用户信息 25 | * 26 | * @author dyb-dev 27 | * @date 21/02/2025/ 22:46:30 28 | * @param {TModifyProperties, "test">} [testRequestConfig] 测试请求配置 29 | * @returns {*} {Promise>} 结果数据 30 | */ 31 | export const getUserInfoApi = async( 32 | testRequestConfig?: TModifyProperties, "test"> 33 | ): Promise> => { 34 | 35 | return sendRequest({ 36 | url: "/1", 37 | requestFn: un.get, 38 | testRequestConfig 39 | }) 40 | 41 | } 42 | 43 | /** 获取id的参数 */ 44 | export interface IGetIdApiParams { 45 | /** 用户唯一标识 */ 46 | userId: number 47 | } 48 | 49 | /** 获取id的结果数据 */ 50 | export interface IGetIdApiResultData { 51 | /** id */ 52 | id: number 53 | } 54 | 55 | /** 56 | * FUN: 获取id 57 | * 58 | * @author dyb-dev 59 | * @date 21/02/2025/ 22:48:09 60 | * @param {IGetIdApiParams} params 参数 61 | * @param {TModifyProperties, "test">} [testRequestConfig] 测试请求配置 62 | * @returns {*} {Promise>} 结果数据 63 | */ 64 | export const getIdApi = async( 65 | params: IGetIdApiParams, 66 | testRequestConfig?: TModifyProperties, "test"> 67 | ): Promise> => { 68 | 69 | return sendRequest({ 70 | url: "", 71 | params, 72 | testRequestConfig 73 | }) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/env/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 环境相关工具函数 3 | */ 4 | 5 | import { getEnvVersion } from "@/utils" 6 | 7 | /** CONST: 设备信息 */ 8 | let systemInfo: UniApp.GetSystemInfoResult 9 | 10 | /** 11 | * FUN: 获取系统信息 12 | * 13 | * @author dyb-dev 14 | * @date 06/10/2024/ 23:13:50 15 | * @param {boolean} [isForceRefresh=false] 是否强制刷新 16 | * @returns {*} {UniApp.GetSystemInfoResult} 系统信息 17 | */ 18 | export const getSystemInfo = (isForceRefresh = false) => { 19 | 20 | // 如果已经获取过系统信息,且不是强制刷新,则直接返回 21 | if (systemInfo && !isForceRefresh) { 22 | 23 | return systemInfo 24 | 25 | } 26 | 27 | try { 28 | 29 | systemInfo = uni.getSystemInfoSync() 30 | 31 | } 32 | catch (error) { 33 | 34 | console.error("getSystemInfo()", error) 35 | 36 | } 37 | 38 | return systemInfo 39 | 40 | } 41 | 42 | /** 43 | * FUN: 是否为开发者工具 44 | * 45 | * @author dyb-dev 46 | * @date 06/10/2024/ 23:15:36 47 | * @returns {*} {boolean} 是否为开发者工具 48 | */ 49 | export const isDevTool = (): boolean => getSystemInfo()?.platform === "devtools" 50 | 51 | /** 52 | * FUN: 是否启用调试 53 | * 54 | * @author dyb-dev 55 | * @date 09/10/2024/ 22:02:43 56 | * @returns {*} {boolean} 是否启用调试 57 | */ 58 | export const isEnableDebug = (): boolean => isDevTool() ? true : getSystemInfo()?.enableDebug ?? false 59 | 60 | /** 61 | * FUN: 是否为开发环境 62 | * 63 | * @author dyb-dev 64 | * @date 09/10/2024/ 17:34:47 65 | * @returns {*} {boolean} 是否为开发环境 66 | */ 67 | export const isDevEnv = (): boolean => __PROJECT_INFO__.env.VITE_USER_NODE_ENV === "development" 68 | 69 | /** 70 | * FUN: 获取当前服务器网址 71 | * - 小程序开发版和体验版 默认: `VITE_DEV_SERVER_URL` 72 | * - 小程序正式版 默认: `VITE_PROD_SERVER_URL` 73 | * 74 | * @author dyb-dev 75 | * @date 01/11/2024/ 20:34:09 76 | * @returns {*} {string} 当前服务器网址 77 | */ 78 | export const getCurrentServerUrl = (): string => { 79 | 80 | const { VITE_DEV_SERVER_URL, VITE_PROD_SERVER_URL } = __PROJECT_INFO__.env 81 | return getEnvVersion() === "release" ? VITE_PROD_SERVER_URL : VITE_DEV_SERVER_URL 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/components/auth/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 用户信息组件相关工具函数 3 | */ 4 | 5 | /** 导出授权头像按钮组件类型 */ 6 | export type * from "./AuthAvatarButton.vue" 7 | /** 导出授权头像昵称对话框类型 */ 8 | export type * from "./AuthAvatarNicknameDialog.vue" 9 | /** 导出授权手机号按钮组件类型 */ 10 | export type * from "./AuthPhoneNumberButton.vue" 11 | /** 导出昵称输入框组件类型 */ 12 | export type * from "./NickNameInput.vue" 13 | 14 | import { providerComponentOptions } from "@/components" 15 | 16 | import type { 17 | IAuthAvatarNicknameDialogOptions, 18 | TAuthAvatarNicknameDialogUnmountParam, 19 | TAuthAvatarNicknameDialogCustomKey 20 | } from "./AuthAvatarNicknameDialog.vue" 21 | 22 | /** 显示授权头像昵称对话框的结果 */ 23 | export type TShowAuthAvatarNicknameDialogResult = TAuthAvatarNicknameDialogUnmountParam 24 | 25 | /** 26 | * 使用授权头像昵称对话框 27 | * 28 | * @author dyb-dev 29 | * @date 29/10/2024/ 22:10:25 30 | * @param {TAuthAvatarNicknameDialogCustomKey} [customKey='__AUTH_AVATAR_NICKNAME_DIALOG__'] - 授权头像昵称对话框唯一标识key 默认: `__AUTH_AVATAR_NICKNAME_DIALOG__` 31 | * @returns {*} {TUseAuthAvatarNicknameDialog} - 授权头像昵称对话框相关函数 32 | */ 33 | export const useAuthAvatarNicknameDialog = (customKey: string = "") => { 34 | 35 | const _customKey: TAuthAvatarNicknameDialogCustomKey = `__AUTH_AVATAR_NICKNAME_DIALOG__${customKey}` 36 | const _options = providerComponentOptions(_customKey) 37 | 38 | /** 39 | * 显示授权头像昵称对话框 40 | * 41 | * @author dyb-dev 42 | * @date 30/10/2024/ 23:00:06 43 | * @returns {*} {Promise} - 显示授权头像昵称对话框的结果 44 | */ 45 | const showAuthAvatarNicknameDialog = (): Promise => { 46 | 47 | return new Promise(resolve => { 48 | 49 | _options.value = { 50 | show: true, 51 | unmount: (...args: TShowAuthAvatarNicknameDialogResult) => resolve(args) 52 | } 53 | 54 | }) 55 | 56 | } 57 | return { 58 | showAuthAvatarNicknameDialog 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/location/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 位置相关工具函数 3 | */ 4 | 5 | /** 位置 */ 6 | export interface ILocation { 7 | /** 纬度 */ 8 | latitude: number 9 | /** 经度 */ 10 | longitude: number 11 | } 12 | 13 | /** 14 | * FUN: 计算两个位置之间的距离 15 | * 16 | * @author dyb-dev 17 | * @date 15/10/2024/ 14:59:41 18 | * @param {ILocation} startLocation 起始位置 19 | * @param {ILocation} endLocation 结束位置 20 | * @returns {*} {number} 两个坐标之间的距离 (单位: 米) 21 | */ 22 | export const calculateDistanceBetweenLocations = (startLocation: ILocation, endLocation: ILocation): number => { 23 | 24 | // 解构起始位置的纬度和经度 25 | const { latitude: startLatitude, longitude: startLongitude } = startLocation 26 | // 解构结束位置的纬度和经度 27 | const { latitude: endLatitude, longitude: endLongitude } = endLocation 28 | 29 | /** 地球半径, 单位: 米 */ 30 | const _earthRadius = 6378136.49 31 | 32 | // 角度转换为弧度的函数 33 | const _rad = (d: number) => d * Math.PI / 180.0 34 | 35 | // 将起始和结束的纬度转换为弧度 36 | const _startRadLat = _rad(startLatitude) 37 | const _endRadLat = _rad(endLatitude) 38 | 39 | // 计算纬度之差的弧度 40 | const _latitudeDifference = _startRadLat - _endRadLat 41 | // 计算经度之差的弧度 42 | const _longitudeDifference = _rad(startLongitude) - _rad(endLongitude) 43 | 44 | // 计算 sin(纬度差/2) 的平方 45 | const _sinLatDiffSquared = Math.pow(Math.sin(_latitudeDifference / 2), 2) 46 | 47 | // 计算 cos(起始纬度) * cos(结束纬度) 48 | const _cosLatProduct = Math.cos(_startRadLat) * Math.cos(_endRadLat) 49 | 50 | // 计算 sin(经度差/2) 的平方 51 | const _sinLongDiffSquared = Math.pow(Math.sin(_longitudeDifference / 2), 2) 52 | 53 | // 计算 Haversine 公式的核心值,即 (sin(纬度差/2))^2 + cos(起始纬度) * cos(结束纬度) * (sin(经度差/2))^2 54 | const _haversineCore = _sinLatDiffSquared + _cosLatProduct * _sinLongDiffSquared 55 | 56 | // 计算两个点之间的弧度距离 57 | const _angularDistance = 2 * Math.asin(Math.sqrt(_haversineCore)) 58 | 59 | // 将弧度距离转换为米 60 | let _distance = _angularDistance * _earthRadius 61 | 62 | // 对结果进行四舍五入到最接近的整数 63 | _distance = Math.round(_distance * 10000) / 10000 64 | 65 | // 返回以米为单位的距离,结果为整数 66 | return parseFloat(_distance.toFixed(0)) 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/hooks/async/useAsyncTask.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 使用异步任务执行器 3 | */ 4 | 5 | import { ref } from "vue" 6 | 7 | import type { Ref } from "vue" 8 | 9 | /** 加载状态类型 */ 10 | export type TLoadStatus = "loading" | "success" | "fail" 11 | 12 | /** 使用异步任务执行器 返回值 */ 13 | export interface IUseAsyncTaskReturn { 14 | /** 15 | * 加载状态 16 | * 17 | * @default 'success' 18 | */ 19 | loadStatus: Ref 20 | /** 执行异步任务 */ 21 | run: (task: () => T | Promise, errorMessage?: string, successMessage?: string) => Promise 22 | } 23 | 24 | /** 25 | * HOOKS: 使用异步任务执行器 26 | * 27 | * @author dyb-dev 28 | * @date 29/06/2025/ 17:02:00 29 | * @returns {*} {IUseAsyncTaskReturn} 返回值 30 | */ 31 | export const useAsyncTask = (): IUseAsyncTaskReturn => { 32 | 33 | /** 34 | * REF: 加载状态 35 | * 36 | * @default 'success' 37 | */ 38 | const loadStatus: IUseAsyncTaskReturn["loadStatus"] = ref("success") 39 | 40 | /** 41 | * FUN: 执行异步任务 42 | * 43 | * @author dyb-dev 44 | * @date 29/06/2025/ 17:03:00 45 | * @param {*} task 异步任务 46 | * @param {string} [failMessage="加载失败"] 失败提示信息 47 | * @param {string} [successMessage=""] 成功提示信息 48 | * @returns {*} 返回任务结果 49 | */ 50 | const run: IUseAsyncTaskReturn["run"] = async(task, failMessage = "加载失败", successMessage = "") => { 51 | 52 | try { 53 | 54 | loadStatus.value = "loading" 55 | 56 | const _result = await task() 57 | 58 | loadStatus.value = "success" 59 | successMessage && 60 | uni.showToast({ 61 | icon: "none", 62 | title: successMessage, 63 | duration: 3000 64 | }) 65 | return _result 66 | 67 | } 68 | catch (error) { 69 | 70 | loadStatus.value = "fail" 71 | const _error = error as Error 72 | // 显示 tip 73 | uni.showToast({ 74 | icon: "none", 75 | title: _error?.message || failMessage, 76 | duration: 3000 77 | }) 78 | 79 | } 80 | 81 | } 82 | 83 | return { 84 | loadStatus, 85 | run 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/pages/home.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 44 | 45 | 65 | 66 | 88 | -------------------------------------------------------------------------------- /src/utils/userInfo/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 用户信息相关工具函数 3 | */ 4 | 5 | import { CERTIFICATE_CONFIG_LIST, GENDER_CONFIG_LIST } from "@/constants" 6 | import { isIdentityCard } from "@/utils" 7 | 8 | import { ECertificatesType, EGenderType } from "@/types" 9 | 10 | /** 11 | * FUN: 获取性别描述 12 | * 13 | * @author dyb-dev 14 | * @date 15/10/2024/ 15:17:06 15 | * @param {EGenderType} genderType - 性别类型 16 | * @returns {*} {string} - 性别字符串 如果未知性别,返回空字符串 17 | */ 18 | export const getGenderDesc = (genderType: EGenderType): string => 19 | GENDER_CONFIG_LIST.find(item => item.type === genderType)?.desc || "" 20 | 21 | /** 22 | * FUN: 获取证件描述 23 | * 24 | * @author dyb-dev 25 | * @date 16/10/2024/ 22:44:22 26 | * @param {ECertificatesType} certificateType - 证件类型 27 | * @returns {*} {string} - 证件描述字符串,如果未知证件,返回空字符串 28 | */ 29 | export const getCertificateDesc = (certificateType: ECertificatesType): string => 30 | CERTIFICATE_CONFIG_LIST.find(item => item.type === certificateType)?.desc || "" 31 | 32 | /** 33 | * FUN: 通过身份证号码获取性别 34 | * 35 | * @author dyb-dev 36 | * @date 15/10/2024/ 21:37:45 37 | * @param {string} identityCardNumber - 身份证号码 38 | * @returns {*} {string} - 返回性别字符串,如果身份证无效,返回空字符串 39 | */ 40 | export const getGenderFromIdentityCard = (identityCardNumber: string): string => { 41 | 42 | // 检查身份证号码是否合法 43 | if (!isIdentityCard(identityCardNumber)) { 44 | 45 | console.error("getGenderFromIdentityCard() 身份证号码无效 identityCardNumber:", identityCardNumber) 46 | return "" 47 | 48 | } 49 | 50 | // 提取性别信息,身份证号码的倒数第二位表示性别,奇数为男,偶数为女 51 | const _genderCode = identityCardNumber.charAt(16) 52 | 53 | const _gender = parseInt(_genderCode, 10) % 2 === 1 ? EGenderType.Man : EGenderType.Woman 54 | 55 | return getGenderDesc(_gender) 56 | 57 | } 58 | 59 | /** 60 | * FUN: 通过身份证号码获取生日 61 | * 62 | * @author dyb-dev 63 | * @date 15/10/2024/ 21:38:14 64 | * @param {string} identityCardNumber - 身份证号码 65 | * @returns {*} {string} - 返回生日字符串,格式为 YYYY/MM/DD,如果身份证无效,返回空字符串 66 | */ 67 | export const getBirthdayFromIdentityCard = (identityCardNumber: string): string => { 68 | 69 | // 检查身份证号码是否合法 70 | if (!isIdentityCard(identityCardNumber)) { 71 | 72 | console.error("getBirthdayFromIdentityCard() 身份证号码无效 identityCardNumber:", identityCardNumber) 73 | return "" 74 | 75 | } 76 | 77 | // 提取生日信息,身份证号码的第7位到第14位是生日 78 | const _birthYear = identityCardNumber.substring(6, 10) 79 | const _birthMonth = identityCardNumber.substring(10, 12) 80 | const _birthDay = identityCardNumber.substring(12, 14) 81 | 82 | return `${_birthYear}/${_birthMonth}/${_birthDay}` 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/miniProgram/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 小程序相关工具函数 3 | */ 4 | 5 | import pagesJson from "@/pages.json" 6 | 7 | /** CONST: 设备信息 */ 8 | let accountInfo: UniApp.AccountInfo 9 | 10 | /** 11 | * FUN: 获取账号信息 12 | * 13 | * @author dyb-dev 14 | * @date 09/10/2024/ 22:23:47 15 | * @param {boolean} [isForceRefresh=false] 是否强制刷新 16 | * @returns {*} 账号信息 17 | */ 18 | export const getAccountInfo = (isForceRefresh: boolean = false): UniApp.AccountInfo => { 19 | 20 | // 如果已经获取过系统信息,且不是强制刷新,则直接返回 21 | if (accountInfo && !isForceRefresh) { 22 | 23 | return accountInfo 24 | 25 | } 26 | 27 | try { 28 | 29 | accountInfo = uni.getAccountInfoSync() 30 | 31 | } 32 | catch (error) { 33 | 34 | console.error("getAccountInfo()", error) 35 | 36 | } 37 | 38 | return accountInfo 39 | 40 | } 41 | 42 | /** 43 | * FUN: 获取小程序环境版本 44 | * - 开发版: develop 45 | * - 体验版: trial 46 | * - 正式版: release 47 | * 48 | * @author dyb-dev 49 | * @date 01/11/2024/ 20:34:23 50 | * @returns {*} {string} 小程序环境版本 51 | */ 52 | export const getEnvVersion = () => getAccountInfo()?.miniProgram.envVersion 53 | 54 | /** 55 | * FUN: 获取小程序线上版本 56 | * - 线上小程序版本号(仅在正式版小程序上支持) 57 | * 58 | * @author dyb-dev 59 | * @date 09/10/2024/ 22:23:41 60 | * @returns {*} {string} 小程序线上版本 61 | */ 62 | export const getOnlineVersion = (): string => getAccountInfo()?.miniProgram.version 63 | 64 | /** LET: 胶囊按钮区域 */ 65 | let capsuleBoundingClientRect: UniApp.GetMenuButtonBoundingClientRectRes | null = null 66 | 67 | /** 68 | * FUN: 获取胶囊按钮区域 69 | * 70 | * @author dyb-dev 71 | * @date 02/12/2024/ 20:21:12 72 | * @param {boolean} [isForceRefresh=false] 是否强制刷新 73 | * @returns {*} {(UniApp.GetMenuButtonBoundingClientRectRes | null)} 胶囊按钮区域 74 | */ 75 | export const getCapsuleBoundingClientRect = (isForceRefresh = false): UniApp.GetMenuButtonBoundingClientRectRes | null => { 76 | 77 | // @ts-ignore 是否开启 自定义导航栏 78 | const _isCustomNavigationBar = pagesJson?.globalStyle?.navigationStyle === "custom" 79 | 80 | // 如果没有开启自定义导航栏, 进行警告提示 81 | !_isCustomNavigationBar && console.warn("getCapsuleBoundingClientRect() 未开启自定义导航栏, 不需要获取菜单按钮区域") 82 | 83 | // 如果已经获取过系统信息,且不是强制刷新,则直接返回 84 | if (capsuleBoundingClientRect && !isForceRefresh) { 85 | 86 | return capsuleBoundingClientRect 87 | 88 | } 89 | 90 | try { 91 | 92 | capsuleBoundingClientRect = uni.getMenuButtonBoundingClientRect() 93 | 94 | } 95 | catch (error) { 96 | 97 | console.error("getCapsuleBoundingClientRect()", error) 98 | 99 | } 100 | 101 | return capsuleBoundingClientRect 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/components/TitleBar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 49 | 50 | 61 | 62 | 87 | 88 | 111 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 推荐安装插件建议 3 | */ 4 | 5 | { 6 | "recommendations": [ 7 | // Chinese 插件 8 | "ms-ceintl.vscode-language-pack-zh-hans", 9 | // Code Spell Checker 插件 10 | "streetsidesoftware.code-spell-checker", 11 | // Color Info 插件 12 | "bierner.color-info", 13 | // comment tip 插件 14 | "yuechaoxu.vscode-comment-tip", 15 | // CSS Navigation 插件 16 | "pucelle.vscode-css-navigation", 17 | // CSS Peek 插件 18 | "pranaygp.vscode-css-peek", 19 | // dir tree generator 插件 20 | "openmynet.dir-tree-generator", 21 | // Document This 插件 22 | "oouo-diogo-perdigao.docthis", 23 | // EditorConfig for VS Code 插件 24 | "editorconfig.editorconfig", 25 | // Error Lens 插件 26 | "usernamehw.errorlens", 27 | // ESLint 插件 28 | "dbaeumer.vscode-eslint", 29 | // ESLint Chinese Rules 插件 30 | "maggie.eslint-rules-zh-plugin", 31 | // git-commit-plugin 插件 32 | "redjue.git-commit-plugin", 33 | // GitHub Copilot 插件 34 | "github.copilot", 35 | // GitHub Copilot Chat 插件 36 | "github.copilot-chat", 37 | // GitLens 插件 38 | "eamodio.gitlens", 39 | // Image Preview 插件 40 | "kisstkondoros.vscode-gutter-preview", 41 | // Import Cost 插件 42 | "wix.vscode-import-cost", 43 | // JavaScript (ES6) code snippets 插件 44 | "xabikos.javascriptsnippets", 45 | // koroFileHeader 插件 46 | "obkoro1.korofileheader", 47 | // Live Server 插件 48 | "ritwickdey.liveserver", 49 | // Local History 插件 50 | "xyz.local-history", 51 | // markdownlint 插件 52 | "davidanson.vscode-markdownlint", 53 | // Prettier ESLint 插件 54 | "rvest.vs-code-prettier-eslint", 55 | // SCSS IntelliSense 插件 56 | "mrmlnc.vscode-scss", 57 | // Stylelint 插件 58 | "stylelint.vscode-stylelint", 59 | // Template String Converter 插件 60 | "meganrogge.template-string-converter", 61 | // Todo Highlight 插件 62 | "wayou.vscode-todo-highlight", 63 | // Todo Tree 插件 64 | "gruntfuggly.todo-tree", 65 | // uni-app-schemas 插件 66 | "uni-helper.uni-app-schemas-vscode", 67 | // uni-app-snippets 插件 68 | "uni-helper.uni-app-snippets-vscode", 69 | // uni-create-view 插件 70 | "uni-helper.uni-highlight-vscode", 71 | // unocss 插件 72 | "antfu.unocss", 73 | // Var Conversion 插件 74 | "xiaoxintongxue.var-conv", 75 | // Vue - Official 插件 76 | "vue.volar", 77 | // Vue VSCode Snippets 插件 78 | "sdras.vue-vscode-snippets" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /src/pages/launch.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 76 | 77 | 83 | 84 | 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniapp-mp-wx-template", 3 | "version": "1.3.3", 4 | "description": "基于`uni-app + vue3 + ts`搭建的微信小程序模板", 5 | "author": "dyb-dev", 6 | "license": "MIT", 7 | "private": true, 8 | "main": "./src/main.ts", 9 | "scripts": { 10 | "prepare": "husky", 11 | "dev": "uni -p mp-weixin", 12 | "build": "npm run ts-check && npm run build-only", 13 | "build-only": "uni build -p mp-weixin", 14 | "ts-check": "vue-tsc --build --force", 15 | "release": "project-cli release" 16 | }, 17 | "engines": { 18 | "node": ">=18.0.0", 19 | "pnpm": ">=8.15.5 <10.0.0" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/dyb-dev/uniapp-mp-wx-template" 24 | }, 25 | "homepage": "https://github.com/dyb-dev/uniapp-mp-wx-template", 26 | "bugs": { 27 | "url": "https://github.com/dyb-dev/uniapp-mp-wx-template/issues", 28 | "directory": "https://github.com/dyb-dev/uniapp-mp-wx-template" 29 | }, 30 | "devDependencies": { 31 | "@dcloudio/types": "^3.4.14", 32 | "@dcloudio/vite-plugin-uni": "3.0.0-4040520250104002", 33 | "@dyb-dev/eslint-config": "^1.1.1", 34 | "@dyb-dev/prettier-config": "^0.0.5", 35 | "@dyb-dev/project-cli": "^0.0.10", 36 | "@dyb-dev/stylelint-config": "^0.0.8", 37 | "@dyb-dev/ts-config": "^0.1.4", 38 | "@uni-helper/uni-app-types": "1.0.0-alpha.4", 39 | "@uni-helper/unocss-preset-uni": "^0.2.11", 40 | "@uni-helper/vite-plugin-uni-components": "^0.1.0", 41 | "@uni-helper/vite-plugin-uni-manifest": "^0.2.7", 42 | "@uni-helper/vite-plugin-uni-pages": "^0.2.28", 43 | "@unocss/eslint-config": "^66.5.10", 44 | "eslint": "^8.57.1", 45 | "husky": "^9.1.6", 46 | "miniprogram-api-typings": "^4.0.1", 47 | "prettier": "^3.3.3", 48 | "prettier-eslint": "^16.3.0", 49 | "sass": "1.64.2", 50 | "stylelint": "^16.25.0", 51 | "typescript": "^5.6.3", 52 | "unocss": "~0.58.9", 53 | "vite": "^5.2.8", 54 | "vue-tsc": "^2.1.6" 55 | }, 56 | "dependencies": { 57 | "@dcloudio/uni-app": "3.0.0-4040520250104002", 58 | "@dcloudio/uni-components": "3.0.0-4040520250104002", 59 | "@dcloudio/uni-mp-weixin": "3.0.0-4040520250104002", 60 | "@qiun/uni-ucharts": "2.5.0-20230101", 61 | "@uni-helper/uni-network": "^0.19.3", 62 | "@uni-helper/uni-promises": "^0.2.1", 63 | "@vueuse/core": "^9.13.0", 64 | "dayjs": "^1.11.13", 65 | "es-toolkit": "^1.41.0", 66 | "mp-html": "^2.5.0", 67 | "nutui-uniapp": "^1.8.1", 68 | "pinia": "^2.2.4", 69 | "pinia-plugin-persistedstate": "^4.1.1", 70 | "query-string": "^9.1.1", 71 | "vue": "^3.5.13", 72 | "zod": "^4.1.12" 73 | } 74 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniApp-MP-WX-Template 2 | 3 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://zh.wikipedia.org/wiki/MIT%E8%A8%B1%E5%8F%AF%E8%AD%89) 4 | 5 | ## 项目简介 6 | 7 | UniApp-MP-WX-Template 提供了一个用于开发微信小程序的基本模板,该项目基于 [Uniapp](https://uniapp.dcloud.net.cn/) + [Vue3](https://cn.vuejs.org/) + [TS](https://www.typescriptlang.org/) 构建,该模板预配置了一些常用的开发工具和插件,帮助你快速开发微信小程序,为您带来极致的体验。 8 | 9 | ## 功能特点 10 | 11 | - **UniApp 工具链**: 支持自动生成 `pages.json` 和 `manifest.json` 配置文件,自动化处理页面和项目配置文件。 12 | - **TypeScript**: 项目使用 TypeScript 进行开发,集成 `vue-tsc` 插件自动进行 Vue 组件的类型检查,同时支持项目的模块化管理 13 | - **Vue 3 组件库**: 集成了 `nutui-uniapp` 组件库,支持 Vue 3 组件的按需自动导入,支持 TypeScript 类型提示。 14 | - **自定义布局组件**: 内置多个自定义布局组件(如顶部导航栏、底部导航栏),兼容 `pages.json` 配置。 15 | - **网络请求**: 提供 Promise 方式调用 UniApp API,处理网络请求和异步操作更加简洁高效。 16 | - **页面导航**: 内置多种跳转方法封装,支持跳转小程序内部页面、H5 页面和第三方小程序,提供统一的导航接口。 17 | - **登录拦截与路由守卫**: 集成登录拦截器和路由守卫功能,自动处理用户登录状态,确保未登录用户访问受限页面时跳转到登录页面,兼容 `pages.json` 配置。 18 | - **公用 WebView 页面**: 内置的公用 WebView 页面,用于展示 H5 页面或进行 H5 分享功能,兼容 `pages.json` 配置。 19 | - **业务开发**: 内置获取用户手机号、微信头像和昵称的组件,简化了微信授权和用户信息收集的流程。 20 | - **代码风格管理**: 预配置了 ESLint、Stylelint 及 Prettier 工具,集成自定义的 ESLint、Stylelint 规则集,帮助开发者统一代码风格。 21 | 22 | ## 安装与使用 23 | 24 | 你可以使用 npm、pnpm 或 yarn 等包管理器来安装项目依赖。推荐使用 pnpm 作为首选包管理器。在下面的示例中,我们默认使用 pnpm 进行演示: 25 | 26 | ### 环境要求 27 | 28 | - Node.js 版本 >= 18.0.0 29 | - 如果包管理器为 pnpm,版本需 >= 8.15.5 30 | 31 | ### 环境变量配置 32 | 33 | 该模板项目支持通过 `.env` 文件进行环境变量配置,你可以根据实际需要修改 `.env` 中的以下配置项: 34 | 35 | - `VITE_MP_WX_APPID`: 小程序的 AppID,用于微信小程序的配置和相关 API 调用。 36 | - `VITE_PAGE_DIR`: 页面目录,默认值为 `pages`,用于指定项目中主包页面所在的文件夹。 37 | - `VITE_SUB_PACKAGE_DIR`: 分包目录,默认值为 `subPackages`,用于指定 `src` 目录下的分包目录名称。 38 | - `VITE_SUB_PACKAGE_CHILD_DIRS`: 分包子目录集合,默认值为 `webview`,如果项目中存在多个子分包,可以用逗号分隔(例如:`webview,profile,shop`),用于配置分包子目录名称集合。 39 | - `VITE_LAUNCH_PATH`: 启动页路径,默认值为 `pages/launch`,用于指定小程序的启动页面路径。如果 `VITE_USE_LAUNCH_PAGE` 设为 `true`,小程序启动时将默认加载该页面。 40 | - `VITE_USE_LAUNCH_PAGE`: 是否使用启动页,默认值为 `false`。如果设为 `true`,启动页将作为小程序首次展示的页面,并且跳转逻辑将在 `App.vue` 中进行。启动页中可根据逻辑判断用户状态,再决定跳转到登录页或首页。使用时注意: 41 | 1. 设置 `VITE_USE_LAUNCH_PAGE=true` 后,小程序的首页将变为 `VITE_LAUNCH_PATH` 指定的路径。 42 | 2. 小程序码路径的格式为:目标页面为 `a` 页面,则路径应为 `/pages/launch?targetPath=pages/a¶m1=value1`,其中 `targetPath` 参数将指定跳转目标页面,而其他参数(如 `param1=value1`)会传递给目标页面作为查询参数使用。 43 | - `VITE_LOGIN_PATH`: 登录页面的路径,默认值为 `pages/login`,用于指定登录页面的位置。当用户未登录时,自动跳转到该页面进行登录。 44 | - `VITE_HOME_PATH`: 首页路径,默认值为 `pages/home`,用于指定应用程序的首页路径。用户登录成功后将跳转到该路径。 45 | - `VITE_DEV_SERVER_URL`: 开发环境服务器网址(小程序开发版、体验版用到),默认值为 `http://xxxx.com`,用于指定后端 API 的根路径。 46 | - `VITE_PROD_SERVER_URL`: 生产环境服务器网址(小程序体验版、线上版用到),默认值为 `http://xxxx.com`,用于指定后端 API 的根路径。 47 | - `VITE_API_BASE_PATH`: 接口请求基础路径,默认值为 `/test.aspx`,用于配置接口的基础路径,与 `VITE_DEV_SERVER_URL` 或者 `VITE_PROD_SERVER_URL` 结合使用,形成完整的 API 请求路径。 48 | 49 | ### 安装依赖 50 | 51 | ```bash 52 | pnpm install 53 | ``` 54 | 55 | ### 本地开发 56 | 57 | ```bash 58 | pnpm dev 59 | ``` 60 | 61 | ### 构建产物 62 | 63 | ```bash 64 | pnpm build 65 | ``` 66 | 67 | ## 许可证 68 | 69 | 本项目基于 `MIT 许可证` 开源。 70 | -------------------------------------------------------------------------------- /src/components/auth/AuthPhoneNumberButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 80 | 81 | 92 | 93 | 96 | 97 | 119 | -------------------------------------------------------------------------------- /src/apis/request/log.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 请求日志模块 3 | */ 4 | 5 | import { isAbsoluteUrl, isDevEnv, isEnableDebug, trimUrlSlashes } from "@/utils" 6 | 7 | /** 请求日志类型枚举 */ 8 | export const enum ERequestLogType { 9 | /** 测试请求参数 */ 10 | TEST_REQUEST_PARAMS, 11 | /** 测试请求结果, 测试成功 */ 12 | TEST_REQUEST_RESULT_SUCCESS, 13 | /** 测试请求结果, 测试失败 or 没有测试数据 */ 14 | TEST_REQUEST_RESULT_FAIL, 15 | /** 真实请求参数 */ 16 | REQUEST_PARAMS, 17 | /** 真实请求结果, 接口调用成功 */ 18 | REQUEST_RESULT_SUCCESS, 19 | /** 真实请求结果, 接口调用失败 */ 20 | REQUEST_RESULT_FAIL 21 | } 22 | 23 | /** 请求日志配置的类型 */ 24 | interface IRequestLogConfig { 25 | /** 样式字符串 */ 26 | style: string 27 | /** 描述 */ 28 | description: string 29 | } 30 | 31 | /** CONST: 请求日志配置列表 */ 32 | const REQUEST_LOG_CONFIG_LIST: Record = { 33 | [ERequestLogType.TEST_REQUEST_PARAMS]: { 34 | style: "color: #fff; background-color: #747474; padding: 5px 10px;", 35 | description: "测试请求参数" 36 | }, 37 | [ERequestLogType.TEST_REQUEST_RESULT_SUCCESS]: { 38 | style: "color: #fff; background-color: #9d9d9d; padding: 5px 10px;", 39 | description: "测试请求成功" 40 | }, 41 | [ERequestLogType.TEST_REQUEST_RESULT_FAIL]: { 42 | style: "color: #fff; background-color: #ff7781; padding: 5px 10px;", 43 | description: "测试请求失败" 44 | }, 45 | [ERequestLogType.REQUEST_PARAMS]: { 46 | style: "color: #fff; background-color: #0078fe; padding: 5px 10px;", 47 | description: "请求参数" 48 | }, 49 | [ERequestLogType.REQUEST_RESULT_SUCCESS]: { 50 | style: "color: #fff; background-color: #2da000; padding: 5px 10px;", 51 | description: "请求成功" 52 | }, 53 | [ERequestLogType.REQUEST_RESULT_FAIL]: { 54 | style: "color: #fff; background-color: #fe003c; padding: 5px 10px;", 55 | description: "请求失败" 56 | } 57 | } 58 | 59 | /** 输出请求日志选项 */ 60 | interface IRequestLogOptions { 61 | /** 请求日志类型 */ 62 | type: ERequestLogType 63 | /** 请求地址 */ 64 | url: string 65 | /** 请求id */ 66 | requestId: number 67 | /** 请求日志数据 */ 68 | data: Record 69 | } 70 | 71 | /** 72 | * FUN: 请求日志 73 | * 74 | * @author dyb-dev 75 | * @date 21/02/2025/ 18:32:32 76 | * @param {IRequestLogOptions} option 请求日志选项 77 | */ 78 | export const requestLog = (option: IRequestLogOptions) => { 79 | 80 | // 如果不是开发环境且不启用调试模式,则不输出请求日志 81 | if (!isDevEnv() && !isEnableDebug()) { 82 | 83 | return 84 | 85 | } 86 | 87 | let url = option.url 88 | 89 | // 如果不是绝对路径,则拼接基础 API 路径 90 | if (!isAbsoluteUrl(url)) { 91 | 92 | url = trimUrlSlashes(url) 93 | url = `${__PROJECT_INFO__.env.VITE_API_BASE_PATH}${url && `/${url}`}` 94 | 95 | } 96 | 97 | // 输出请求日志选项 98 | const { type, requestId, data } = option 99 | // 获取请求日志配置 100 | const { description, style } = REQUEST_LOG_CONFIG_LIST[type] 101 | 102 | console.log(`\n %c${requestId} ${description} :>>`, style, url, "\n", data) 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/pages/test.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 111 | 112 | 126 | -------------------------------------------------------------------------------- /src/components/AvatarNickname.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 74 | 75 | 85 | 86 | 113 | 114 | 144 | -------------------------------------------------------------------------------- /src/components/picker/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 选择器组件模块 3 | */ 4 | 5 | /** 导出选择器组件公共类型 */ 6 | export type * from "./type" 7 | /** 导出自定义选择器类型 */ 8 | export type * from "./Picker.vue" 9 | /** 导出日期时间选择器类型 */ 10 | export type * from "./DateTimePicker.vue" 11 | 12 | import { providerComponentOptions } from "@/components" 13 | 14 | import type { IDateTimePickerOptions, TDateTimePickerCustomKey, TShowDateTimePickerResult } from "./DateTimePicker.vue" 15 | import type { IPickerOptions, TPickerCustomKey } from "./Picker.vue" 16 | import type { TShowPickerBaseResult } from "./type" 17 | import type { TFilteredDefaultOptions } from "@/components" 18 | 19 | /** 显示选择器的选项 */ 20 | export type TShowPickerOptions = TFilteredDefaultOptions 21 | 22 | /** 23 | * 使用选择器 24 | * 25 | * @author dyb-dev 26 | * @date 04/11/2024/ 21:12:53 27 | * @param {string} [customKey=""] - 弹窗唯一标识key 默认: `__PICKER__` 28 | * @returns {*} {TUsePicker} - 选择器相关函数 29 | */ 30 | export const usePicker = (customKey: string = "") => { 31 | 32 | const _customKey: TPickerCustomKey = `__PICKER__${customKey}` 33 | const _options = providerComponentOptions(_customKey) 34 | 35 | /** 36 | * 显示选择器 37 | * 38 | * @author dyb-dev 39 | * @date 04/11/2024/ 21:13:25 40 | * @param {TShowPickerOptions} options - 选择器选项 41 | * @returns {*} {Promise} - 显示选择器的结果 42 | */ 43 | const showPicker = (options: TShowPickerOptions): Promise => { 44 | 45 | return new Promise(resolve => { 46 | 47 | _options.value = { 48 | ...options, 49 | show: true, 50 | unmount: (...args: TShowPickerBaseResult) => resolve(args) 51 | } 52 | 53 | }) 54 | 55 | } 56 | return { 57 | showPicker 58 | } 59 | 60 | } 61 | 62 | /** 显示日期时间选择器的选项 */ 63 | export type TShowDateTimePickerOptions = TFilteredDefaultOptions 64 | 65 | /** 66 | * 使用日期时间选择器 67 | * 68 | * @author dyb-dev 69 | * @date 04/11/2024/ 21:12:53 70 | * @param {string} [customKey=""] - 弹窗唯一标识key 默认: `__DATE_TIME_PICKER__` 71 | * @returns {*} {TUseDateTimePicker} - 日期时间选择器相关函数 72 | */ 73 | export const useDateTimePicker = (customKey: string = "") => { 74 | 75 | const _customKey: TDateTimePickerCustomKey = `__DATE_TIME_PICKER__${customKey}` 76 | const _options = providerComponentOptions(_customKey) 77 | 78 | /** 79 | * 显示日期时间选择器 80 | * 81 | * @author dyb-dev 82 | * @date 04/11/2024/ 21:13:25 83 | * @param {TShowDateTimePickerOptions} options - 日期时间选择器选项 84 | * @returns {*} {Promise} - 显示日期时间选择器的结果 85 | */ 86 | const showDateTimePicker = (options?: TShowDateTimePickerOptions): Promise => { 87 | 88 | return new Promise(resolve => { 89 | 90 | _options.value = { 91 | ...options, 92 | show: true, 93 | unmount: (...args: TShowDateTimePickerResult) => resolve(args) 94 | } 95 | 96 | }) 97 | 98 | } 99 | return { 100 | showDateTimePicker 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/components/LabelValueBar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 85 | 86 | 98 | 99 | 131 | 132 | 151 | -------------------------------------------------------------------------------- /src/types/dts/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by vite-plugin-uni-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AuthAvatarButton: typeof import('./../../components/auth/AuthAvatarButton.vue')['default'] 11 | AuthAvatarNicknameDialog: typeof import('./../../components/auth/AuthAvatarNicknameDialog.vue')['default'] 12 | AuthPhoneNumberButton: typeof import('./../../components/auth/AuthPhoneNumberButton.vue')['default'] 13 | AvatarNickname: typeof import('./../../components/AvatarNickname.vue')['default'] 14 | Cell: typeof import('./../../components/Cell.vue')['default'] 15 | DateTimePicker: typeof import('./../../components/picker/DateTimePicker.vue')['default'] 16 | Dialog: typeof import('./../../components/dialog/Dialog.vue')['default'] 17 | FooterActionsBar: typeof import('./../../components/FooterActionsBar.vue')['default'] 18 | FormControl: typeof import('./../../components/FormControl.vue')['default'] 19 | FormDialog: typeof import('./../../components/dialog/FormDialog.vue')['default'] 20 | LabelValueBar: typeof import('./../../components/LabelValueBar.vue')['default'] 21 | Layout: typeof import('./../../components/layout/Layout.vue')['default'] 22 | List: typeof import('./../../components/List.vue')['default'] 23 | NavBar: typeof import('./../../components/layout/NavBar.vue')['default'] 24 | NickNameInput: typeof import('./../../components/auth/NickNameInput.vue')['default'] 25 | NutAnimate: typeof import('nutui-uniapp/components/animate/animate.vue')['default'] 26 | NutAvatar: typeof import('nutui-uniapp/components/avatar/avatar.vue')['default'] 27 | NutBadge: typeof import('nutui-uniapp/components/badge/badge.vue')['default'] 28 | NutButton: typeof import('nutui-uniapp/components/button/button.vue')['default'] 29 | NutConfigProvider: typeof import('nutui-uniapp/components/configprovider/configprovider.vue')['default'] 30 | NutIcon: typeof import('nutui-uniapp/components/icon/icon.vue')['default'] 31 | NutInput: typeof import('nutui-uniapp/components/input/input.vue')['default'] 32 | NutPopup: typeof import('nutui-uniapp/components/popup/popup.vue')['default'] 33 | NutSwiper: typeof import('nutui-uniapp/components/swiper/swiper.vue')['default'] 34 | NutSwiperItem: typeof import('nutui-uniapp/components/swiperitem/swiperitem.vue')['default'] 35 | NutTabPane: typeof import('nutui-uniapp/components/tabpane/tabpane.vue')['default'] 36 | NutTabs: typeof import('nutui-uniapp/components/tabs/tabs.vue')['default'] 37 | NutTextarea: typeof import('nutui-uniapp/components/textarea/textarea.vue')['default'] 38 | NutUploader: typeof import('nutui-uniapp/components/uploader/uploader.vue')['default'] 39 | Picker: typeof import('./../../components/picker/Picker.vue')['default'] 40 | Popup: typeof import('./../../components/popup/Popup.vue')['default'] 41 | SwiperPro: typeof import('./../../components/SwiperPro.vue')['default'] 42 | TabBar: typeof import('./../../components/layout/TabBar.vue')['default'] 43 | TabsPaneList: typeof import('./../../components/TabsPaneList.vue')['default'] 44 | TitleBar: typeof import('./../../components/TitleBar.vue')['default'] 45 | UCharts: typeof import('./../../components/u-charts/u-charts.vue')['default'] 46 | Uploader: typeof import('./../../components/Uploader.vue')['default'] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/dialog/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 对话框组件相关工具函数 3 | */ 4 | 5 | /** 导出对话框类型 */ 6 | export type * from "./Dialog.vue" 7 | /** 导出表单对话框类型 */ 8 | export type * from "./FormDialog.vue" 9 | 10 | import { providerComponentOptions } from "@/components" 11 | 12 | import type { IDialogOptions, TDialogCustomKey, TDialogUnmountParam } from "./Dialog.vue" 13 | import type { IFormDialogOptions, TFormDialogCustomKey, TFormDialogUnmountParam } from "./FormDialog.vue" 14 | import type { TFilteredDefaultOptions } from "@/components" 15 | 16 | /** 显示对话框的选项 */ 17 | export type TShowDialogOptions = TFilteredDefaultOptions 18 | 19 | /** 显示对话框的结果 */ 20 | export type TShowDialogResult = TDialogUnmountParam 21 | 22 | /** 23 | * 使用对话框 24 | * 25 | * @author dyb-dev 26 | * @date 29/10/2024/ 22:10:25 27 | * @param {TDialogCustomKey} [customKey='__DIALOG__'] - 对话框唯一标识key 默认: `__DIALOG__` 28 | * @returns {*} {TUseDialog} - 对话框相关函数 29 | */ 30 | export const useDialog = (customKey: string = "") => { 31 | 32 | const _customKey: TDialogCustomKey = `__DIALOG__${customKey}` 33 | const _options = providerComponentOptions(_customKey) 34 | 35 | /** 36 | * 显示对话框 37 | * 38 | * @author dyb-dev 39 | * @date 29/10/2024/ 22:05:36 40 | * @param {TShowDialogOptions} options - 对话框选项 41 | * @returns {*} {Promise} - 显示对话框的结果 42 | */ 43 | const showDialog = (options: TShowDialogOptions): Promise => { 44 | 45 | return new Promise(resolve => { 46 | 47 | _options.value = { 48 | ...options, 49 | show: true, 50 | unmount: (...args: TShowDialogResult) => resolve(args) 51 | } 52 | 53 | }) 54 | 55 | } 56 | return { 57 | showDialog 58 | } 59 | 60 | } 61 | 62 | /** 显示表单对话框的选项 */ 63 | export type TShowFormDialogOptions> = TFilteredDefaultOptions> 64 | 65 | /** 显示表单对话框的结果 */ 66 | export type TShowFormDialogResult> = TFormDialogUnmountParam 67 | 68 | /** 69 | * 使用表单对话框 70 | * 71 | * @author dyb-dev 72 | * @date 29/10/2024/ 22:10:25 73 | * @param {TFormDialogCustomKey} [customKey='__FORM_DIALOG__'] - 表单对话框唯一标识key 默认: `__FORM_DIALOG__` 74 | * @returns {*} {TUseFormDialog} - 表单对话框相关函数 75 | */ 76 | export const useFormDialog = (customKey: string = "") => { 77 | 78 | const _customKey: TFormDialogCustomKey = `__FORM_DIALOG__${customKey}` 79 | const _options = providerComponentOptions>>(_customKey) 80 | 81 | /** 82 | * 显示表单对话框 83 | * 84 | * @author dyb-dev 85 | * @date 29/10/2024/ 22:05:36 86 | * @param {TShowFormDialogOptions} options - 表单对话框选项 87 | * @returns {*} {Promise>} - 显示表单对话框的结果 88 | */ 89 | const showFormDialog = >( 90 | options: TShowFormDialogOptions 91 | ): Promise> => { 92 | 93 | return new Promise(resolve => { 94 | 95 | _options.value = { 96 | ...options, 97 | show: true, 98 | unmount: (...args: TShowFormDialogResult) => resolve(args) 99 | } as IFormDialogOptions> 100 | 101 | }) 102 | 103 | } 104 | return { 105 | showFormDialog 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalStyle": { 3 | "navigationStyle": "custom", 4 | "navigationBarTextStyle": "black", 5 | "navigationBarBackgroundColor": "#ffffff", 6 | "backgroundColor": "#ffffff", 7 | "disableScroll": true 8 | }, 9 | "pages": [ 10 | { 11 | "path": "pages/home", 12 | "type": "home", 13 | "style": { 14 | "navigationBarTitleText": "首页" 15 | } 16 | }, 17 | { 18 | "path": "pages/launch", 19 | "type": "page", 20 | "style": {} 21 | }, 22 | { 23 | "path": "pages/list", 24 | "type": "page", 25 | "style": { 26 | "navigationBarTitleText": "列表" 27 | } 28 | }, 29 | { 30 | "path": "pages/login", 31 | "type": "page", 32 | "style": { 33 | "navigationBarTitleText": "登录" 34 | } 35 | }, 36 | { 37 | "path": "pages/my", 38 | "type": "page", 39 | "style": { 40 | "navigationBarTitleText": "我的" 41 | }, 42 | "needLogin": true 43 | }, 44 | { 45 | "path": "pages/test", 46 | "type": "page", 47 | "style": { 48 | "navigationBarTitleText": "测试", 49 | "disableScroll": false, 50 | "enablePullDownRefresh": true 51 | }, 52 | "needLogin": true 53 | } 54 | ], 55 | "condition": { 56 | "current": 0, 57 | "list": [ 58 | { 59 | "name": "启动页", 60 | "path": "pages/launch", 61 | "query": "targetPath=/pages/test&test=测试启动参数" 62 | }, 63 | { 64 | "name": "首页", 65 | "path": "pages/home", 66 | "query": "test=首页" 67 | }, 68 | { 69 | "name": "我的", 70 | "path": "pages/my" 71 | }, 72 | { 73 | "name": "测试", 74 | "path": "pages/test" 75 | } 76 | ] 77 | }, 78 | "tabBar": { 79 | "custom": true, 80 | "color": "#7d7e80", 81 | "selectedColor": "#29d446", 82 | "list": [ 83 | { 84 | "pagePath": "pages/home", 85 | "text": "首页", 86 | "iconfont": { 87 | "text": "home", 88 | "selectedText": "home" 89 | } 90 | }, 91 | { 92 | "pagePath": "pages/list", 93 | "text": "列表", 94 | "iconfont": { 95 | "text": "dongdong", 96 | "selectedText": "dongdong" 97 | } 98 | }, 99 | { 100 | "pagePath": "pages/my", 101 | "text": "我的", 102 | "iconfont": { 103 | "text": "my", 104 | "selectedText": "my" 105 | } 106 | } 107 | ] 108 | }, 109 | "subPackages": [ 110 | { 111 | "root": "subPackages/webview", 112 | "pages": [ 113 | { 114 | "path": "pages/webview", 115 | "type": "page", 116 | "style": {} 117 | } 118 | ] 119 | } 120 | ], 121 | "preloadRule": { 122 | "pages/home": { 123 | "packages": [ 124 | "subPackages/webview" 125 | ] 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/stores/tabBar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: TabBar状态管理 3 | */ 4 | 5 | import { defineStore } from "pinia" 6 | import { reactive } from "vue" 7 | 8 | import pagesJson from "@/pages.json" 9 | import { getCurrentPagePath } from "@/utils" 10 | 11 | import type { TTabBarItem } from "@/components/layout/TabBar.vue" 12 | import type { TabBarItem } from "@uni-helper/vite-plugin-uni-pages" 13 | 14 | import { store } from "." 15 | 16 | /** Store 状态类型 */ 17 | type TTabBarStoreState = { 18 | /** 当前TabBar索引 */ 19 | currentIndex: number 20 | /** TabBar 列表 */ 21 | list: TTabBarItem[] 22 | } 23 | 24 | /** Store 实例 */ 25 | export const useTabBarStore = defineStore("TabBarStore", () => { 26 | 27 | /** Store State */ 28 | const tabBarStoreState = reactive({ 29 | currentIndex: 0, 30 | list: 31 | // @ts-ignore 32 | pagesJson?.tabBar?.list.map((item: TabBarItem) => { 33 | 34 | return { 35 | ...item, 36 | // 优先级: 图标 > 图片路径 37 | // @ts-ignore 38 | icon: item?.iconfont?.text || item?.iconPath || "", 39 | // @ts-ignore 40 | selectedIcon: item?.iconfont?.selectedText || item?.selectedIconPath || "", 41 | classPrefix: "nut-icon", 42 | fontClassName: "nutui-iconfont", 43 | showDot: false, 44 | dotValue: 0 45 | } as TTabBarItem 46 | 47 | }) || [] 48 | }) 49 | 50 | /** 51 | * FUN: 获取指定 TabBarItem 52 | * 53 | * @author dyb-dev 54 | * @date 25/09/2024/ 17:41:06 55 | * @param {string} pagePath 页面路径 56 | * @returns {*} TabBarItem配置 57 | */ 58 | const getTabBarItem = (pagePath: string) => tabBarStoreState.list.find(item => item.pagePath === pagePath) 59 | 60 | /** 61 | * FUN: 获取当前 TabBarItem 62 | * 63 | * @author dyb-dev 64 | * @date 25/09/2024/ 17:41:55 65 | * @returns {*} 当前TabBarItem配置 66 | */ 67 | const getCurrentTabBarItem = () => getTabBarItem(getCurrentPagePath()) 68 | 69 | /** 70 | * FUN: 获取 TabBar 索引 71 | * 72 | * @author dyb-dev 73 | * @date 09/10/2024/ 12:29:36 74 | * @param {string} pagePath 页面路径 75 | * @returns {*} TabBar 索引 76 | */ 77 | const getTabBarIndex = (pagePath: string) => tabBarStoreState.list.findIndex(item => item.pagePath === pagePath) 78 | 79 | /** 80 | * FUN: 获取当前 TabBar 索引 81 | * 82 | * @author dyb-dev 83 | * @date 25/09/2024/ 17:42:12 84 | * @returns {*} 当前 TabBar 索引 85 | */ 86 | const getCurrentTabBarIndex = () => getTabBarIndex(getCurrentPagePath()) 87 | 88 | /** 89 | * FUN: 更新当前 TabBar 索引 90 | * 91 | * @author dyb-dev 92 | * @date 09/10/2024/ 12:34:31 93 | */ 94 | const updateCurrentTabBarIndex = () => { 95 | 96 | /** 获取当前TabBar索引 */ 97 | const _currentTabBarIndex = getCurrentTabBarIndex() 98 | // 只有在当前TabBar索引大于等于0时才会设置 99 | if (_currentTabBarIndex >= 0) { 100 | 101 | tabBarStoreState.currentIndex = _currentTabBarIndex 102 | 103 | } 104 | 105 | } 106 | 107 | return { 108 | tabBarStoreState, 109 | getTabBarItem, 110 | getCurrentTabBarItem, 111 | getTabBarIndex, 112 | getCurrentTabBarIndex, 113 | updateCurrentTabBarIndex 114 | } 115 | 116 | }) 117 | 118 | /** 119 | * 使用状态管理 120 | * - 在没有Vue组件上下文的情况下使用 121 | * 122 | * @author dyb-dev 123 | * @date 15/09/2024/ 23:53:35 124 | * @returns store实例 125 | */ 126 | export const useTabBarStoreWithOut = () => { 127 | 128 | return useTabBarStore(store) 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/utils/pages/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 页面相关工具函数 3 | */ 4 | 5 | /** 导出页面跳转相关工具函数 */ 6 | export * from "./navigate" 7 | 8 | import queryString from "query-string" 9 | 10 | import pagesJson from "@/pages.json" 11 | 12 | import { trimUrlSlashes } from "../url" 13 | 14 | import type { PageMetaDatum } from "@uni-helper/vite-plugin-uni-pages" 15 | 16 | /** 17 | * FUN: 获取当前页面实例 18 | * - 确保在页面 onLoad 之后调用 19 | * - 不要在 App.onLaunch 或者 App.onShow 的时候调用 getCurrentPages(),此时 page 还没有生成 20 | * 21 | * @author dyb-dev 22 | * @date 05/10/2024/ 20:54:47 23 | * @export 24 | * @returns {*} 当前页面实例 25 | */ 26 | export const getCurrentPageInstance = () => { 27 | 28 | const _pages = getCurrentPages() as WechatMiniprogram.Page.Instance< 29 | WechatMiniprogram.IAnyObject, 30 | WechatMiniprogram.IAnyObject 31 | >[] 32 | return _pages[_pages.length - 1] 33 | 34 | } 35 | 36 | /** 37 | * FUN: 获取当前页面的路径 38 | * - 确保在页面 onLoad 之后调用 39 | * - 不要在 App.onLaunch 或者 App.onShow 的时候调用 getCurrentPages(),此时 page 还没有生成 40 | * - H5 平台不支持这种方式获取 41 | * 42 | * @author dyb-dev 43 | * @date 05/10/2024/ 21:09:23 44 | * @returns {*} 当前页面路径 45 | */ 46 | export const getCurrentPagePath = () => getCurrentPageInstance().route 47 | 48 | /** 49 | * FUN: 获取当前页面的完整路径 50 | * - 确保在页面 onLoad 之后调用 51 | * - 不要在 App.onLaunch 或者 App.onShow 的时候调用 getCurrentPages(),此时 page 还没有生成 52 | * - H5 平台不支持这种方式获取 53 | * 54 | * @author dyb-dev 55 | * @date 05/10/2024/ 21:11:26 56 | * @returns {*} 当前页面完整路径 57 | */ 58 | export const getCurrentPageFullPath = () => `${getCurrentPagePath()}?${getCurrentPageSearch()}` 59 | 60 | /** 61 | * FUN: 获取当前页面的 query 参数 62 | * - 确保在页面 onLoad 之后调用 63 | * - 不要在 App.onLaunch 或者 App.onShow 的时候调用 getCurrentPages(),此时 page 还没有生成 64 | * - H5 平台不支持这种方式获取 65 | * 66 | * @author dyb-dev 67 | * @date 05/10/2024/ 21:03:56 68 | * @returns {*} 当前页面的 query 参数 69 | */ 70 | export const getCurrentPageQuery = () => getCurrentPageInstance()?.options || {} 71 | 72 | /** 73 | * FUN: 获取当前页面的 search 参数 74 | * - 确保在页面 onLoad 之后调用 75 | * - 不要在 App.onLaunch 或者 App.onShow 的时候调用 getCurrentPages(),此时 page 还没有生成 76 | * - H5 平台不支持这种方式获取 77 | * 78 | * @author dyb-dev 79 | * @date 05/10/2024/ 21:07:28 80 | * @returns {*} 当前页面 search 参数 81 | */ 82 | export const getCurrentPageSearch = () => queryString.stringify(getCurrentPageQuery(), { encode: false }) 83 | 84 | const { VITE_SUB_PACKAGE_DIR, VITE_PAGE_DIR } = __PROJECT_INFO__.env 85 | 86 | /** CONST: 分包页面路径正则表达式 */ 87 | const SUB_PACKAGE_PAGE_PATH_REGEX = new RegExp(`^(${VITE_SUB_PACKAGE_DIR}(?:\\/[^\\/]+)*)\\/((${VITE_PAGE_DIR}\\/.*))`) 88 | 89 | /** 90 | * FUN: 获取页面配置 91 | * - `pagePath`示例: "pages/home" 或 "subPackages/webview/pages/webview" 92 | * 93 | * @author dyb-dev 94 | * @date 06/10/2024/ 15:00:21 95 | * @param {string} pagePath 页面路径 96 | * @returns {*} 页面配置 97 | */ 98 | export const getPageConfig = (pagePath: string): PageMetaDatum | void => { 99 | 100 | // 如果路径开头携带斜杠,则去除开头的斜杠 101 | pagePath = trimUrlSlashes(pagePath, { trimStart: true }) 102 | 103 | // 匹配分包页面并提取结果 104 | const _result = pagePath.match(SUB_PACKAGE_PAGE_PATH_REGEX) 105 | 106 | // 如果匹配分包页面结果为空,则尝试匹配主包页面 107 | if (!_result) { 108 | 109 | return pagesJson?.pages.find((item: PageMetaDatum) => item.path === pagePath) 110 | 111 | } 112 | 113 | /** 分包根路径 */ 114 | const _subPackagesRootPath = _result[1] || "" 115 | /** 分包页面路径 */ 116 | const _subPackagesPagePath = _result[2] || "" 117 | 118 | /** 分包配置 */ 119 | const _subPackageConfig = pagesJson?.subPackages.find(item => item.root === _subPackagesRootPath) 120 | 121 | return _subPackageConfig?.pages.find((item: PageMetaDatum) => item.path === _subPackagesPagePath) 122 | 123 | } 124 | 125 | /** 126 | * FUN: 获取当前页面配置 127 | * 128 | * @author dyb-dev 129 | * @date 06/10/2024/ 15:01:07 130 | * @returns {*} 当前页面配置 131 | */ 132 | export const getCurrentPageConfig = (): PageMetaDatum | void => getPageConfig(getCurrentPagePath()) 133 | -------------------------------------------------------------------------------- /src/subPackages/webview/pages/webview.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 142 | 143 | 146 | -------------------------------------------------------------------------------- /src/utils/form/identityCard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 身份证相关工具函数 3 | */ 4 | 5 | /** 6 | * FUN: 是否为有效的省、直辖市代码 7 | * 8 | * @author dyb-dev 9 | * @date 15/10/2024/ 16:34:48 10 | * @param {string} identityCardNumber - 身份证号码 11 | * @returns {*} {boolean} 是否为有效的省、直辖市代码 12 | */ 13 | const _isProvinceCode = (identityCardNumber: string): boolean => { 14 | 15 | /** CONST: 省、直辖市代码表映射 */ 16 | const provinceCodeMap: Record = { 17 | 11: "北京", 18 | 12: "天津", 19 | 13: "河北", 20 | 14: "山西", 21 | 15: "内蒙古", 22 | 21: "辽宁", 23 | 22: "吉林", 24 | 23: "黑龙江", 25 | 31: "上海", 26 | 32: "江苏", 27 | 33: "浙江", 28 | 34: "安徽", 29 | 35: "福建", 30 | 36: "江西", 31 | 37: "山东", 32 | 41: "河南", 33 | 42: "湖北", 34 | 43: "湖南", 35 | 44: "广东", 36 | 45: "广西", 37 | 46: "海南", 38 | 50: "重庆", 39 | 51: "四川", 40 | 52: "贵州", 41 | 53: "云南", 42 | 54: "西藏", 43 | 61: "陕西", 44 | 62: "甘肃", 45 | 63: "青海", 46 | 64: "宁夏", 47 | 65: "新疆", 48 | 71: "台湾", 49 | 81: "香港", 50 | 82: "澳门", 51 | 91: "国外" 52 | } 53 | 54 | const _addressCode = identityCardNumber.substring(0, 6) 55 | const _check = /^[1-9]\d{5}$/.test(_addressCode) 56 | if (!_check) { 57 | 58 | return false 59 | 60 | } 61 | 62 | return !!provinceCodeMap[parseInt(_addressCode.substring(0, 2))] 63 | 64 | } 65 | 66 | /** 67 | * FUN: 是否为有效的生日 68 | * 69 | * @author dyb-dev 70 | * @date 15/10/2024/ 16:41:44 71 | * @param {string} identityCardNumber - 身份证号码 72 | * @returns {*} {boolean} 是否为有效的生日 73 | */ 74 | const _isBirthday = (identityCardNumber: string): boolean => { 75 | 76 | // 生日代码 77 | const _birDay = identityCardNumber.substring(6, 14) 78 | const _check = /^[1-9]\d{3}((0[1-9])|(1[0-2]))((0[1-9])|([1-2][0-9])|(3[0-1]))$/.test(_birDay) 79 | if (!_check) { 80 | 81 | return false 82 | 83 | } 84 | const _year = parseInt(_birDay.substring(0, 4)) 85 | const _month = parseInt(_birDay.substring(4, 6)) 86 | const _day = parseInt(_birDay.substring(6)) 87 | const _date = new Date(_year, _month - 1, _day) 88 | 89 | // 生日大于当前日期时 90 | if (_date > new Date()) { 91 | 92 | return false 93 | 94 | } 95 | return _date.getFullYear() === _year && _date.getMonth() === _month - 1 && _date.getDate() === _day 96 | 97 | } 98 | 99 | /** 100 | * FUN: 是否为有效的校验位 101 | * 102 | * @author dyb-dev 103 | * @date 15/10/2024/ 16:57:06 104 | * @param {string} identityCardNumber - 身份证号码 105 | * @returns {*} {boolean} 是否为有效的校验位 106 | */ 107 | const _isParityBit = (identityCardNumber: string): boolean => { 108 | 109 | /** CONST: 每位加权因子 */ 110 | const powers = ["7", "9", "10", "5", "8", "4", "2", "1", "6", "3", "7", "9", "10", "5", "8", "4", "2"] 111 | /** CONST: 第18位校检码 */ 112 | const parityBit = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"] 113 | 114 | // 18位身份证需要验证最后一位校验位 115 | const _parityBit = identityCardNumber.charAt(17).toUpperCase() 116 | 117 | // 加权因子 118 | let _power = 0 119 | for (let i = 0; i < 17; i++) { 120 | 121 | _power += parseInt(identityCardNumber.charAt(i)) * parseInt(powers[i]) 122 | 123 | } 124 | 125 | return _parityBit === parityBit[_power % 11] 126 | 127 | } 128 | 129 | /** 130 | * FUN: 是否为有效的身份证号码 131 | * 132 | * @author dyb-dev 133 | * @date 15/10/2024/ 16:59:56 134 | * @param {string} identityCardNumber - 身份证号码 135 | * @returns {*} {boolean} 是否为有效的身份证号码 136 | */ 137 | export const isIdentityCard = (identityCardNumber: string): boolean => { 138 | 139 | const _check = /^[1-9]\d{5}[1-9]\d{3}((0[1-9])|(1[0-2]))((0[1-9])|([1-2][0-9])|(3[0-1]))\d{3}(\d|x|X)$/.test( 140 | identityCardNumber 141 | ) 142 | return _check && _isProvinceCode(identityCardNumber) && _isBirthday(identityCardNumber) && _isParityBit(identityCardNumber) 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/components/auth/AuthAvatarButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 130 | 131 | 142 | 143 | 151 | 152 | 174 | -------------------------------------------------------------------------------- /src/apis/modules/userInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 用户信息相关接口 3 | */ 4 | 5 | import { sendRequest } from "../request" 6 | 7 | import type { UnResponse } from "@uni-helper/uni-network" 8 | 9 | /** 登录的参数 */ 10 | export interface ILoginApiParams { 11 | /** 12 | * 用户登录凭证(有效期五分钟)。开发者需要在开发者服务器后台调用 [code2Session](https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html),使用 code 换取 openid、unionid、session_key 等信息 13 | */ 14 | code: string 15 | } 16 | 17 | /** 登录的结果数据 */ 18 | export interface ILoginApiResultData { 19 | /** 用户在我们系统中的加密用户代码 */ 20 | userId: string 21 | /** 由微信生成的用户在开放平台的唯一标识符 */ 22 | unionId: string 23 | /** 由微信生成的用户在本小程序唯一标识 */ 24 | openId: string 25 | /** 昵称 */ 26 | nickName: string 27 | /** 头像 */ 28 | avatarUrl: string 29 | /** 绑定手机号 */ 30 | phoneNumber: string 31 | } 32 | 33 | /** 34 | * FUN: 登录 35 | * 36 | * @author dyb-dev 37 | * @date 21/02/2025/ 22:49:58 38 | * @param {ILoginApiParams} params 参数 39 | * @param {TModifyProperties, "test">} [testRequestConfig] 测试请求配置 40 | * @returns {*} {Promise>} 结果数据 41 | */ 42 | export const loginApi = async( 43 | params: ILoginApiParams, 44 | testRequestConfig?: TModifyProperties, "test"> 45 | ): Promise> => { 46 | 47 | return sendRequest({ 48 | url: "", 49 | params, 50 | testRequestConfig 51 | }) 52 | 53 | } 54 | 55 | /** 获取手机号的参数 */ 56 | export interface IGetPhoneNumberApiParams { 57 | /** 授权手机号code */ 58 | code: string 59 | } 60 | 61 | /** 获取手机号的结果数据 */ 62 | export interface IGetPhoneNumberApiResultData { 63 | /** 手机号 */ 64 | phoneNumber: string 65 | } 66 | 67 | /** 68 | * FUN: 获取手机号 69 | * 70 | * @author dyb-dev 71 | * @date 21/02/2025/ 22:54:45 72 | * @param {IGetPhoneNumberApiParams} params 参数 73 | * @param {TModifyProperties, "test">} [testRequestConfig] 测试请求配置 74 | * @returns {*} {Promise>} 结果数据 75 | */ 76 | export const getPhoneNumberApi = async( 77 | params: IGetPhoneNumberApiParams, 78 | testRequestConfig?: TModifyProperties, "test"> 79 | ): Promise> => { 80 | 81 | return sendRequest({ 82 | url: "", 83 | params, 84 | testRequestConfig 85 | }) 86 | 87 | } 88 | 89 | /** 上传头像的参数 */ 90 | export interface IUploadAvatarApiParams { 91 | /** 头像url */ 92 | avatarUrl: string 93 | } 94 | 95 | /** 96 | * FUN: 上传头像 97 | * 98 | * @author dyb-dev 99 | * @date 21/02/2025/ 23:03:54 100 | * @param {IUploadAvatarApiParams} params 参数 101 | * @param {TModifyProperties} [testRequestConfig] 测试请求配置 102 | * @returns {*} {Promise} 上传头像结果 103 | */ 104 | export const uploadAvatarApi = async( 105 | params: IUploadAvatarApiParams, 106 | testRequestConfig?: TModifyProperties 107 | ): Promise => { 108 | 109 | return sendRequest({ 110 | url: "", 111 | params, 112 | testRequestConfig 113 | }) 114 | 115 | } 116 | 117 | /** 上传用户信息的参数 */ 118 | export interface IUploadUserInfoApiParams { 119 | /** 头像url */ 120 | avatarUrl: string 121 | /** 昵称 */ 122 | nickName: string 123 | } 124 | 125 | /** 126 | * FUN: 上传用户信息 127 | * 128 | * @author dyb-dev 129 | * @date 21/02/2025/ 23:05:52 130 | * @param {IUploadUserInfoApiParams} params 参数 131 | * @param {TModifyProperties} [testRequestConfig] 测试请求配置 132 | * @returns {*} {Promise} 上传用户信息结果 133 | */ 134 | export const uploadUserInfoApi = async( 135 | params: IUploadUserInfoApiParams, 136 | testRequestConfig?: TModifyProperties 137 | ): Promise => { 138 | 139 | return sendRequest({ 140 | url: "", 141 | params, 142 | testRequestConfig 143 | }) 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/hooks/pagination/useListPagination.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 列表分页器 3 | */ 4 | 5 | import { usePagination } from "./usePagination" 6 | 7 | import type { IUsePaginationOptions, IUsePaginationReturn, TPaginationDataItem } from "./usePagination" 8 | 9 | /** 列表分页配置,继承自 IUsePaginationOptions */ 10 | export interface IUseListPaginationOptions 11 | extends Omit, "usePreviousDataOnFail"> {} 12 | 13 | /** 列表分页返回结果,继承自 IUsePaginationReturn */ 14 | export interface IUseListPaginationReturn 15 | extends Omit, "refresh" | "prev"> { 16 | /** 清空所有数据并刷新首页 */ 17 | clearRefresh: () => Promise 18 | /** 清空所有数据并初始化分页器 */ 19 | clearInitialize: () => Promise 20 | } 21 | 22 | /** 23 | * HOOKS: 列表分页器 24 | * 25 | * @author dyb-dev 26 | * @date 05/09/2024/ 14:19:07 27 | * @template T 列表分页数据类型 28 | * @param {IUseListPaginationOptions} options 列表分页配置 29 | * @returns {*} {IUseListPaginationReturn} 列表分页返回结果 30 | */ 31 | export const useListPagination = ( 32 | options: IUseListPaginationOptions 33 | ): IUseListPaginationReturn => { 34 | 35 | // 调用 usePagination 并传入参数 36 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 37 | const { refresh, prev, ..._pagination } = usePagination(options) 38 | 39 | /** 40 | * 清空重置分页数据和状态 41 | * 42 | * @author dyb-dev 43 | * @date 16/11/2024/ 00:53:21 44 | */ 45 | const _clear = () => { 46 | 47 | _pagination.currentTotalDataMap.value.clear() 48 | _pagination.totalSize.value = 0 49 | _pagination.initialized.value = false 50 | 51 | } 52 | 53 | /** 54 | * 清空所有数据并初始化分页器 55 | * 56 | * @author dyb-dev 57 | * @date 16/11/2024/ 00:53:54 58 | */ 59 | const clearInitialize = async() => { 60 | 61 | try { 62 | 63 | _clear() 64 | await _pagination.load(1) 65 | 66 | } 67 | catch (error) { 68 | 69 | throw `clearInitialize() ${error}` 70 | 71 | } 72 | 73 | } 74 | 75 | /** 76 | * 清空所有数据并刷新首页 77 | * 78 | * @author dyb-dev 79 | * @date 06/09/2024/ 22:24:56 80 | */ 81 | const clearRefresh = async() => { 82 | 83 | try { 84 | 85 | _clear() 86 | await refresh(1) 87 | 88 | } 89 | catch (error) { 90 | 91 | throw `clearRefresh() ${error}` 92 | 93 | } 94 | 95 | } 96 | 97 | /** 98 | * 列表分页的下一页逻辑 99 | * 100 | * @author dyb 101 | * @date 05/09/2024/ 13:16:57 102 | */ 103 | const next = async() => { 104 | 105 | try { 106 | 107 | const { initialized, currentLoadStatus, currentPage, finished } = _pagination 108 | 109 | if (!initialized.value) { 110 | 111 | console.warn("next() 初始化未完成取消执行下一页,开始执行初始化操作") 112 | await clearInitialize() 113 | return 114 | 115 | } 116 | if (finished.value) { 117 | 118 | console.warn(`next() 已经加载完所有数据,无法执行下一页 currentPage: ${currentPage.value}`) 119 | return 120 | 121 | } 122 | if (currentLoadStatus.value === "loading") { 123 | 124 | console.warn( 125 | `next() 当前页码数据正在加载中,取消执行下一页 currentPage: ${currentPage.value} currentLoadStatus: ${currentLoadStatus.value}` 126 | ) 127 | return 128 | 129 | } 130 | if (currentLoadStatus.value === "fail") { 131 | 132 | console.warn( 133 | `next() 当前页码数据加载失败,取消执行下一页,开始重新加载当前页数据 currentPage: ${currentPage.value} currentLoadStatus: ${currentLoadStatus.value}` 134 | ) 135 | 136 | // 加载失败,重新加载当前页码数据 137 | await _pagination.load() 138 | return 139 | 140 | } 141 | await _pagination.next() 142 | 143 | } 144 | catch (error) { 145 | 146 | throw `next() ${error}` 147 | 148 | } 149 | 150 | } 151 | 152 | return { 153 | ..._pagination, 154 | clearInitialize, 155 | clearRefresh, 156 | next 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /pages.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 页面配置 3 | */ 4 | 5 | import { defineUniPages } from "@uni-helper/vite-plugin-uni-pages" 6 | 7 | import { VITE_ENV } from "./vite.config" 8 | 9 | /** CONST: 获取.env文件的环境变量 */ 10 | const { VITE_PAGE_DIR, VITE_LAUNCH_PATH, VITE_HOME_PATH, VITE_LOGIN_PATH, VITE_SUB_PACKAGE_DIR } = VITE_ENV 11 | 12 | export default defineUniPages({ 13 | // 全局配置 14 | globalStyle: { 15 | // 默认使用自定义导航栏 16 | navigationStyle: "custom", 17 | // 默认设置状态栏前景颜色为`black`,解决状态栏不可见的问题 18 | navigationBarTextStyle: "black", 19 | // 默认设置导航栏背景颜色为白色 解决在 `webview` 中不支持自定义导航栏的问题,因此设置保持与自定义导航栏一致的背景颜色 20 | navigationBarBackgroundColor: "#ffffff", 21 | // 默认页面(page元素)为白色背景,解决系统暗黑主题下页面背景与导航栏背景不一致的问题 22 | backgroundColor: "#ffffff", 23 | // 默认禁用页面滑动,解决ios与安卓默认行为不一致的问题 24 | disableScroll: true 25 | }, 26 | // 页面配置 27 | pages: [ 28 | { 29 | path: VITE_LAUNCH_PATH 30 | }, 31 | { 32 | path: VITE_LOGIN_PATH, 33 | style: { 34 | navigationBarTitleText: "登录" 35 | } 36 | }, 37 | { 38 | path: VITE_HOME_PATH, 39 | style: { 40 | navigationBarTitleText: "首页" 41 | } 42 | }, 43 | { 44 | path: `${VITE_PAGE_DIR}/list`, 45 | style: { 46 | navigationBarTitleText: "列表" 47 | } 48 | }, 49 | { 50 | path: `${VITE_PAGE_DIR}/my`, 51 | style: { 52 | navigationBarTitleText: "我的" 53 | }, 54 | // 标识该页面是需要先登录 55 | needLogin: true 56 | }, 57 | { 58 | path: `${VITE_PAGE_DIR}/test`, 59 | style: { 60 | navigationBarTitleText: "测试", 61 | // 允许滑动 62 | disableScroll: false, 63 | // 开启下拉刷新 64 | enablePullDownRefresh: true 65 | }, 66 | // 标识该页面是需要先登录 67 | needLogin: true 68 | } 69 | ], 70 | // wx开发工具页面测试配置 71 | condition: { 72 | current: 0, 73 | list: [ 74 | { 75 | name: "启动页", 76 | path: VITE_LAUNCH_PATH, 77 | query: `targetPath=/${VITE_PAGE_DIR}/test&test=测试启动参数` 78 | }, 79 | { 80 | name: "首页", 81 | path: VITE_HOME_PATH, 82 | query: "test=首页" 83 | }, 84 | { 85 | name: "我的", 86 | path: `${VITE_PAGE_DIR}/my` 87 | }, 88 | { 89 | name: "测试", 90 | path: `${VITE_PAGE_DIR}/test` 91 | } 92 | ] 93 | }, 94 | // TabBar页面配置 95 | tabBar: { 96 | // 默认使用自定义TabBar 97 | custom: true, 98 | // 默认颜色 99 | color: "#7d7e80", 100 | // 选中颜色 101 | selectedColor: "#29d446", 102 | // tabBar 项列表 103 | list: [ 104 | { 105 | pagePath: VITE_HOME_PATH, 106 | text: "首页", 107 | iconfont: { 108 | text: "home", 109 | selectedText: "home" 110 | } 111 | }, 112 | { 113 | pagePath: `${VITE_PAGE_DIR}/list`, 114 | text: "列表", 115 | iconfont: { 116 | text: "dongdong", 117 | selectedText: "dongdong" 118 | } 119 | }, 120 | { 121 | pagePath: `${VITE_PAGE_DIR}/my`, 122 | text: "我的", 123 | iconfont: { 124 | text: "my", 125 | selectedText: "my" 126 | } 127 | } 128 | ] 129 | }, 130 | 131 | // 分包配置 132 | subPackages: [ 133 | { 134 | root: `${VITE_SUB_PACKAGE_DIR}/webview`, 135 | pages: [ 136 | { 137 | path: `${VITE_SUB_PACKAGE_DIR}/webview/${VITE_PAGE_DIR}/webview` 138 | } 139 | ] 140 | } 141 | ], 142 | // 分包预加载配置 143 | preloadRule: { 144 | [VITE_HOME_PATH]: { 145 | packages: [`${VITE_SUB_PACKAGE_DIR}/webview`] 146 | } 147 | } 148 | }) 149 | -------------------------------------------------------------------------------- /src/components/auth/NickNameInput.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 162 | 163 | 175 | 176 | 190 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: vite 配置 3 | */ 4 | 5 | import { resolve } from "node:path" 6 | 7 | import Uni from "@dcloudio/vite-plugin-uni" 8 | import UniHelperComponents from "@uni-helper/vite-plugin-uni-components" 9 | import UniHelperManifest from "@uni-helper/vite-plugin-uni-manifest" 10 | import UniHelperPages from "@uni-helper/vite-plugin-uni-pages" 11 | import { NutResolver } from "nutui-uniapp" 12 | import UnoCSS from "unocss/vite" 13 | import { defineConfig, loadEnv } from "vite" 14 | 15 | import { generateProjectInfo } from "./vite/utils" 16 | 17 | /** CONST: 项目根目录 */ 18 | const projectRootDir = process.cwd() 19 | 20 | /** CONST: 获取.env文件的环境变量 */ 21 | export const VITE_ENV = loadEnv(process.env.NODE_ENV as string, projectRootDir) as ImportMetaEnv 22 | 23 | /** CONST: 项目信息 */ 24 | const __PROJECT_INFO__ = generateProjectInfo(VITE_ENV) 25 | 26 | const { 27 | VITE_COMPONENT_DIR, 28 | VITE_PAGE_DIR, 29 | VITE_LAUNCH_PATH, 30 | VITE_USE_LAUNCH_PAGE, 31 | VITE_HOME_PATH, 32 | VITE_SUB_PACKAGE_DIR, 33 | VITE_SUB_PACKAGE_CHILD_DIRS 34 | } = VITE_ENV 35 | 36 | /** CONST: 分包子目录路径列表 */ 37 | const subPackageChildDirPathList = VITE_SUB_PACKAGE_CHILD_DIRS.split(",").map(item => `src/${VITE_SUB_PACKAGE_DIR}/${item}`) 38 | 39 | export default defineConfig(async () => { 40 | 41 | return { 42 | plugins: [ 43 | // 使用 `pages.config.ts` 文件来编写生成 `pages.json` 文件,注意: 生成的 `pages.json` 文件不要更改 44 | UniHelperPages({ 45 | // .d.ts文件输出路径 默认: 根目录 46 | dts: `src/types/dts/${VITE_PAGE_DIR}.d.ts`, 47 | // 扫描的页面目录 默认: src/pages 48 | dir: `src/${VITE_PAGE_DIR}`, 49 | // 首页路径 默认: pages/index 50 | homePage: VITE_USE_LAUNCH_PAGE === "true" ? VITE_LAUNCH_PATH : VITE_HOME_PATH, 51 | // subPackages 扫描的目录 默认: src/pages-sub 52 | subPackages: subPackageChildDirPathList, 53 | // 排除的文件,相对于 dir 和 subPackages 54 | exclude: [`**/${VITE_COMPONENT_DIR}/**/*.*`] 55 | }), 56 | 57 | // 使用 `manifest.config.ts` 来编写生成 `manifest.json` 文件,注意: 生成的 `manifest.json` 文件不要更改 58 | UniHelperManifest(), 59 | 60 | // 组件自动导入插件 61 | UniHelperComponents({ 62 | // 组件 扫描的目录 默认: src/components 63 | dirs: [`src/${VITE_COMPONENT_DIR}`, ...subPackageChildDirPathList.map(item => `${item}/${VITE_COMPONENT_DIR}`)], 64 | // .d.ts文件输出路径 65 | dts: resolve(projectRootDir, `./src/types/dts/${VITE_COMPONENT_DIR}.d.ts`), 66 | // 自定义自动导入逻辑 67 | resolvers: [NutResolver()] 68 | }), 69 | 70 | // 核心插件,能够在 `uni-app` 中使用 `vite` 来构建项目 71 | // 注意: 部分插件需要在 `Uni()` 的前面,这是因为其他插件代码最终会被 `Uni()` 做处理 72 | Uni(), 73 | // 处理原子 css 提取 74 | UnoCSS() 75 | ], 76 | 77 | build: { 78 | // js兼容目标 默认:modules 79 | target: "es6", 80 | // css兼容目标 默认:与 build.target 一致 81 | cssTarget: ["chrome61"] 82 | }, 83 | 84 | resolve: { 85 | // 设置路径别名 86 | alias: { 87 | "@": resolve(projectRootDir, "./src") 88 | }, 89 | // 导入时想要省略的扩展名集合 90 | extensions: [".js", ".ts", ".jsx", ".tsx", ".json", ".mjs", ".mts", ".cjs", ".cts"] 91 | }, 92 | 93 | // 定义变量,编译时会将使用的地方替换为硬编码的形式 94 | define: { 95 | __PROJECT_INFO__: JSON.stringify(__PROJECT_INFO__) 96 | }, 97 | 98 | css: { 99 | preprocessorOptions: { 100 | // scss全局文件 101 | scss: { 102 | additionalData: ` 103 | @use "@/styles/variable/uni.scss" as *; 104 | @use "@/styles/variable/custom.scss" as *; 105 | @use "@/styles/mixins/index.scss" as *; 106 | @use "@/styles/funs/index.scss" as *; 107 | @import "nutui-uniapp/styles/variables.scss"; 108 | ` 109 | } 110 | } 111 | }, 112 | 113 | json: { 114 | // 是否支持从 .json 文件中进行按名导入,示例:import { name } from './package.json'; 115 | namedExports: false, 116 | // 开启则会禁用按名导入,导入的 JSON 会被转换为 export default JSON.parse("...") 会比转译成对象字面量性能更好, 117 | stringify: true 118 | } 119 | } 120 | 121 | }) 122 | -------------------------------------------------------------------------------- /src/apis/request/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 封装请求模块 3 | */ 4 | 5 | import un from "@uni-helper/uni-network" 6 | 7 | import { delay, isDevEnv } from "@/utils" 8 | 9 | import { ERequestLogType, requestLog } from "./log" 10 | 11 | import type { UnResponse } from "@uni-helper/uni-network" 12 | 13 | /** LET: 全局测试请求配置 */ 14 | let _globalTestRequestConfig: Omit = { 15 | test: isDevEnv() && true, 16 | testDelay: 500 17 | } 18 | 19 | /** 20 | * FUN: 设置全局测试请求配置 21 | * 22 | * @author dyb-dev 23 | * @date 21/02/2025/ 19:50:27 24 | * @param {Omit} config 测试请求配置 25 | */ 26 | export const setGlobalTestRequestConfig = (config: Omit) => { 27 | 28 | _globalTestRequestConfig = config 29 | 30 | } 31 | 32 | /** LET: 全局请求id,用于进行日志输出 */ 33 | let _globalRequestId = 0 34 | 35 | /** 发送请求选项 */ 36 | export interface ISendRequestOptions, P extends Record = Record> { 37 | /** 请求地址 */ 38 | url: string 39 | /** 请求参数 */ 40 | params?: P 41 | /** 发送 http 请求函数 默认 axios.post 方法 */ 42 | requestFn?: (url: string, params?: P) => Promise> 43 | /** 测试请求配置 */ 44 | testRequestConfig?: TModifyProperties, "test"> 45 | } 46 | 47 | /** 48 | * FUN: 发送请求 49 | * 50 | * @author dyb-dev 51 | * @date 21/02/2025/ 14:18:09 52 | * @template T 53 | * @template P 54 | * @param {ISendRequestOptions} options 选项 55 | * @returns {*} {Promise>} 请求结果 56 | */ 57 | export const sendRequest = async , P extends Record = Record>( 58 | options: ISendRequestOptions 59 | ): Promise> => { 60 | 61 | _globalRequestId++ 62 | // 当前请求id 63 | const _currentRequestId = _globalRequestId 64 | 65 | const { url, params = {}, requestFn = un.post, testRequestConfig } = options 66 | 67 | // 是否使用测试模式 68 | if (testRequestConfig?.test ?? _globalTestRequestConfig.test) { 69 | 70 | // 输出测试请求参数日志 71 | requestLog({ 72 | type: ERequestLogType.TEST_REQUEST_PARAMS, 73 | url, 74 | requestId: _currentRequestId, 75 | data: params 76 | }) 77 | 78 | // 测试请求配置 79 | // eslint-disable-next-line prefer-const 80 | let { testResult, testDelay = _globalTestRequestConfig.testDelay ?? 0 } = testRequestConfig ?? {} 81 | 82 | // 如果测试数据未提供,则返回测试失败 83 | if (!testResult) { 84 | 85 | testResult = { 86 | success: false, 87 | message: "测试数据未提供" 88 | } 89 | 90 | // 输出测试请求结果日志 91 | requestLog({ 92 | type: ERequestLogType.TEST_REQUEST_RESULT_FAIL, 93 | url, 94 | requestId: _currentRequestId, 95 | data: testResult 96 | }) 97 | 98 | return testResult 99 | 100 | } 101 | 102 | // 模拟请求延迟 103 | await delay(testDelay) 104 | 105 | // 确定日志类型 106 | const _requestLogType = testResult.success 107 | ? ERequestLogType.TEST_REQUEST_RESULT_SUCCESS 108 | : ERequestLogType.TEST_REQUEST_RESULT_FAIL 109 | 110 | // 输出测试请求结果日志 111 | requestLog({ 112 | type: _requestLogType, 113 | url, 114 | requestId: _currentRequestId, 115 | data: testResult 116 | }) 117 | 118 | return testResult 119 | 120 | } 121 | 122 | try { 123 | 124 | // 输出真实请求参数日志 125 | requestLog({ 126 | type: ERequestLogType.REQUEST_PARAMS, 127 | url, 128 | requestId: _currentRequestId, 129 | data: params 130 | }) 131 | 132 | // 发送请求 133 | const _result = await requestFn(url, params) 134 | 135 | // 确定日志类型 136 | const _requestLogType = _result.success ? ERequestLogType.REQUEST_RESULT_SUCCESS : ERequestLogType.REQUEST_RESULT_FAIL 137 | 138 | requestLog({ 139 | type: _requestLogType, 140 | url, 141 | requestId: _currentRequestId, 142 | data: _result 143 | }) 144 | 145 | return _result 146 | 147 | } 148 | catch (error) { 149 | 150 | const _result = { 151 | success: false, 152 | message: "请求失败" 153 | } 154 | 155 | requestLog({ 156 | type: ERequestLogType.REQUEST_RESULT_FAIL, 157 | url, 158 | requestId: _currentRequestId, 159 | data: _result 160 | }) 161 | 162 | return _result 163 | 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/components/Cell.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 133 | 134 | 146 | 147 | 185 | 206 | -------------------------------------------------------------------------------- /src/pages/login.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 124 | 125 | 172 | 173 | 198 | -------------------------------------------------------------------------------- /src/utils/url/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: url相关工具函数 3 | */ 4 | 5 | import queryString from "query-string" 6 | 7 | import { getCurrentServerUrl } from "@/utils" 8 | 9 | /** 10 | * FUN: 获取基础 URL(去除查询参数) 11 | * 12 | * @author dyb-dev 13 | * @date 10/10/2024/ 11:36:50 14 | * @param {string} url - 完整的 URL 字符串 15 | * @returns {*} {string} - 去除查询参数后的基础 URL 16 | */ 17 | export const getBaseUrl = (url: string): string => queryString.parseUrl(url).url 18 | 19 | /** 20 | * FUN: 获取 url 的 查询参数对象 21 | * 22 | * @author dyb-dev 23 | * @date 14/07/2023/ 15:16:25 24 | * @param {string} url 需要解析的 URL 25 | * @returns {queryString.ParsedQuery} query对象 26 | */ 27 | export const getUrlQuery = (url: string): queryString.ParsedQuery => queryString.parseUrl(url).query 28 | 29 | /** 30 | * FUN: 根据 key 从 url 的 查询参数对象 中获取单个参数值 31 | * 32 | * @author dyb-dev 33 | * @date 14/07/2023/ 15:28:49 34 | * @param {string} [url] 需要解析的 URL 35 | * @param {string} key query 的 key 36 | * @returns {string} query 的 value 37 | */ 38 | export const getUrlQueryValue = (url: string, key: string): string => (getUrlQuery(url)[key] as string) || "" 39 | 40 | /** 41 | * FUN: 设置或更新 从 url 的 查询参数对象 中的指定参数,并返回更新后的 URL 字符串 42 | * 43 | * @author dyb-dev 44 | * @date 14/07/2023/ 16:06:14 45 | * @param {string} [url] 需要解析的 URL 46 | * @param {string} key 需要 设置或更新 参数 的 key 47 | * @param {string} value 需要 设置或更新 参数 的 value 48 | * @param {queryString.StringifyOptions} [options] stringifyUrl 的 options 49 | * @returns {string} 设置或更新后的 url 50 | */ 51 | export const setUrlQueryValue = (url: string, key: string, value: string, options?: queryString.StringifyOptions): string => { 52 | 53 | const _query = getUrlQuery(url) 54 | 55 | _query[key] = value 56 | 57 | return queryString.stringifyUrl({ url: url, query: _query }, options) 58 | 59 | } 60 | 61 | /** 62 | * FUN: 合并 URL 的查询参数,并返回更新后的 URL 字符串 63 | * 64 | * @author dyb-dev 65 | * @date 14/07/2023/ 16:10:39 66 | * @param {string} [url] 完整的 URL 字符串 67 | * @param {queryString.ParsedQuery} obj 需要合并到 URL 中的查询参数对象 68 | * @param {queryString.StringifyOptions} [options] stringifyUrl 的 options 69 | * @returns {string} 更新后的 URL 字符串 70 | */ 71 | export const mergeUrlQuery = ( 72 | url: string, 73 | obj: queryString.ParsedQuery, 74 | options?: queryString.StringifyOptions 75 | ): string => { 76 | 77 | const _query = getUrlQuery(url) 78 | 79 | Object.assign(_query, obj) 80 | 81 | return queryString.stringifyUrl({ url: url, query: _query }, options) 82 | 83 | } 84 | 85 | /** 86 | * FUN: 判断路径是否为绝对路径 87 | * - 匹配以 `协议` | `域名` | `端口号` 开头的路径 88 | * 89 | * @author dyb-dev 90 | * @date 15/10/2024/ 22:26:21 91 | * @param {string} path - 路径 92 | * @returns {*} {boolean} - 是否为绝对路径 93 | */ 94 | export const isAbsoluteUrl = (path: string): boolean => /^(https?:\/\/|:\/\/|[a-zA-Z0-9.-]+:\d+|:\d+)/.test(path) 95 | 96 | /** 相对路径转换为绝对路径的选项 */ 97 | interface IToAbsoluteUrlOptions { 98 | /** 相对 URL 路径 */ 99 | relativePath: string 100 | /** 101 | * 网址的协议、域名、端口号组成的字符串 默认: `getCurrentServerUrl()` 102 | */ 103 | urlOrigin?: string 104 | /** 基础路径 默认: '' */ 105 | basePath?: string 106 | /** 版本号 默认: `__PROJECT_INFO__.version` */ 107 | version?: string 108 | } 109 | 110 | /** 111 | * FUN: 将相对 Url 路径转换为绝对 Url 路径 112 | * 113 | * @author dyb-dev 114 | * @date 15/10/2024/ 11:43:52 115 | * @param {IToAbsoluteUrlOptions} options - 选项 116 | * @returns {*} {string} - 绝对路径 117 | */ 118 | export const toAbsoluteUrl = (options: IToAbsoluteUrlOptions): string => { 119 | 120 | const { relativePath, urlOrigin = getCurrentServerUrl(), basePath = "", version = __PROJECT_INFO__.version } = options 121 | 122 | if (!relativePath || typeof relativePath !== "string" || isAbsoluteUrl(relativePath)) { 123 | 124 | console.error("toAbsoluteUrl() relativePath:", relativePath) 125 | return relativePath 126 | 127 | } 128 | 129 | const _urlOrigin = trimUrlSlashes(urlOrigin) 130 | const _basePath = trimUrlSlashes(basePath) 131 | const _relativePath = trimUrlSlashes(relativePath) 132 | 133 | let _url = [_urlOrigin, _basePath, _relativePath].filter(Boolean).join("/") 134 | 135 | if (!isAbsoluteUrl(_url)) { 136 | 137 | _url = `/${_url}` 138 | 139 | } 140 | 141 | if (version) { 142 | 143 | _url = setUrlQueryValue(_url, "version", version, {}) 144 | 145 | } 146 | 147 | return _url 148 | 149 | } 150 | 151 | /** 152 | * FUN: 根据选项移除 URL 的首尾斜杠 153 | * 154 | * @author dyb-dev 155 | * @date 23/07/2024/ 20:28:05 156 | * @param {string} url - 需要处理的 URL 157 | * @param {object} [options={}] - 配置项 158 | * @param {boolean} [options.trimStart=true] - 是否移除开头的斜杠 159 | * @param {boolean} [options.trimEnd=true] - 是否移除结尾的斜杠 160 | * @returns {string} - 处理后的url 161 | */ 162 | export const trimUrlSlashes = (url: string, options: { trimStart?: boolean; trimEnd?: boolean } = {}): string => { 163 | 164 | const { trimStart = true, trimEnd = true } = options 165 | 166 | let _url = url 167 | if (trimStart) { 168 | 169 | _url = _url.replace(/^\//, "") 170 | 171 | } 172 | if (trimEnd) { 173 | 174 | _url = _url.replace(/\/$/, "") 175 | 176 | } 177 | return _url 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/styles/mixins/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: scss全局混合文件 3 | */ 4 | 5 | /** 必须写在顶部 */ 6 | @use "sass:list"; 7 | 8 | /** 9 | 开启滚动 10 | $direction - 滚动方向 "x" | "y" 11 | */ 12 | @mixin open-scroll-mixin($direction: "y") { 13 | @if $direction == "x" { 14 | overflow-x: auto; 15 | -webkit-overflow-scrolling: touch; 16 | } @else if $direction == "y" { 17 | overflow-y: auto; 18 | -webkit-overflow-scrolling: touch; 19 | } 20 | } 21 | 22 | /** 隐藏滚动条 */ 23 | @mixin hide-scroll-bar-mixin { 24 | &::-webkit-scrollbar { 25 | display: none; 26 | width: 0; 27 | height: 0; 28 | color: transparent; 29 | } 30 | } 31 | 32 | /** 33 | 鼠标悬停显示滚动条 34 | $scrollBarColor - 滚动条颜色 35 | $transition-duration - 过渡时间 36 | - 注意: 使用此 mixin 时字体颜色需要使用 `text-shadow: 0 0 #fff;` 来实现 37 | */ 38 | @mixin hover-show-scrollbar-mixin($scrollBarColor: rgba(0, 0, 0, 0.15), $transition-duration: 0.3s) { 39 | color: transparent; 40 | transition: color $transition-duration; 41 | 42 | &:hover { 43 | color: $scrollBarColor; 44 | } 45 | 46 | /* 滚动条整体样式 */ 47 | &::-webkit-scrollbar { 48 | width: 4px; 49 | } 50 | 51 | /* 滚动条滑块样式 */ 52 | &::-webkit-scrollbar-thumb { 53 | border-radius: 4px; 54 | box-shadow: inset 0 0 0 4px; 55 | } 56 | } 57 | 58 | /** 文本支持 \n 换行 */ 59 | @mixin rich-text-mixin { 60 | white-space: pre-wrap; 61 | word-break: break-word; 62 | } 63 | 64 | /** 65 | * 文本溢出省略号效果 66 | * 根据行数设置单行或多行文本的溢出显示省略号效果。 67 | * 68 | * $rows - 行数 69 | * - 当 $rows > 1 时,显示多行文本,并在超过指定行数时截断显示省略号。 70 | * - 当 $rows <= 1 时,显示单行文本,并在超出宽度时显示省略号。 71 | */ 72 | @mixin text-ellipsis-mixin($rows: 1) { 73 | text-overflow: ellipsis; 74 | @if $rows > 1 { 75 | display: -webkit-box; 76 | -webkit-box-orient: vertical; 77 | -webkit-line-clamp: $rows; 78 | overflow: hidden; 79 | } @else { 80 | overflow: hidden; 81 | white-space: nowrap; 82 | } 83 | } 84 | 85 | /** 86 | 高亮闪烁动画文本效果 87 | $sub-class-name - 子类名 88 | $sub-class-count - 子类数量 89 | $font-highlight-color - 高亮颜色 90 | $shadow-color - 阴影颜色 91 | $animation-duration - 动画持续时间 92 | $delay-step - 延迟步长 93 | */ 94 | @mixin text-flicker-animation-mixin( 95 | $sub-class-name, 96 | $sub-class-count, 97 | $font-highlight-color: #fff, 98 | $shadow-color: #42fff6, 99 | $animation-duration: 1s, 100 | $delay-step: 0.2s 101 | ) { 102 | $shadow-intensity: (5px, 10px, 20px, 50px); 103 | 104 | @for $i from 1 through $sub-class-count { 105 | .#{$sub-class-name}-#{$i} { 106 | animation-delay: $delay-step * $i; 107 | } 108 | } 109 | 110 | @keyframes flicker { 111 | 0% { 112 | color: inherit; 113 | } 114 | 115 | 5%, 116 | 15%, 117 | 25%, 118 | 30%, 119 | 100% { 120 | color: $font-highlight-color; 121 | text-shadow: 122 | 0 0 list.nth($shadow-intensity, 1) $shadow-color, 123 | 0 0 list.nth($shadow-intensity, 2) $shadow-color, 124 | 0 0 list.nth($shadow-intensity, 3) $shadow-color, 125 | 0 0 list.nth($shadow-intensity, 4) $shadow-color; 126 | } 127 | 128 | 10%, 129 | 20% { 130 | color: inherit; 131 | text-shadow: none; 132 | } 133 | } 134 | 135 | .#{$sub-class-name} { 136 | animation: flicker $animation-duration linear forwards; 137 | } 138 | } 139 | 140 | /** 文本遮罩 */ 141 | @mixin text-clip-mixin { 142 | color: transparent; 143 | background-clip: text; 144 | } 145 | 146 | /** 147 | 渐变背景 + 色相旋转动画的文本效果 148 | $animation-duration - 动画持续时间 149 | */ 150 | @mixin text-gradient-effect-mixin($animation-duration: 5s) { 151 | background-image: linear-gradient(45deg, #009688, yellowgreen, pink, #03a9f4, #9c27b0, #8bc34a); 152 | animation: hue-rotate $animation-duration infinite; 153 | @include text-clip-mixin; 154 | 155 | @keyframes hue-rotate { 156 | 100% { 157 | filter: hue-rotate(360deg); 158 | } 159 | } 160 | } 161 | 162 | /** 163 | 动态线性渐变背景滑动文本效果 164 | $highlight-color - 高亮颜色 165 | $animation-duration - 动画持续时间 166 | $background-color - 背景颜色 (注意: 实际展现效果其实是文字颜色) 167 | */ 168 | @mixin text-shine-mixin($highlight-color: white, $animation-duration: 5s, $background-color: transparent) { 169 | color: transparent; 170 | background-color: $background-color; 171 | background-image: linear-gradient( 172 | 125deg, 173 | transparent 0%, 174 | transparent 10%, 175 | $highlight-color 20%, 176 | transparent 30%, 177 | transparent 100% 178 | ); 179 | background-repeat: no-repeat; 180 | background-position: 100% 0; 181 | 182 | /** 注意: 当background-size设置的百分比大于或者小于100%时,有多余的空间移动了,background-position才会有效 */ 183 | background-size: 150% 100%; 184 | background-clip: text; 185 | animation: shine $animation-duration infinite linear; 186 | 187 | @keyframes shine { 188 | 0% { 189 | background-position: 100% 0; 190 | } 191 | 192 | 100% { 193 | background-position: -200% 0; 194 | } 195 | } 196 | } 197 | 198 | /** 199 | 创建具有遮罩效果的背景 200 | $width - 容器的宽度 201 | $height - 容器的高度 202 | $bg-image1 - 背景图片1的路径 203 | $bg-image2 - 背景图片2的路径 204 | */ 205 | @mixin background-mask-mixin($width, $height, $bg-image1, $bg-image2) { 206 | position: relative; 207 | width: $width; 208 | height: $height; 209 | background: url(#{$bg-image1}) no-repeat center/cover; 210 | 211 | &::before { 212 | position: absolute; 213 | inset: 0; 214 | background: url(#{$bg-image2}) no-repeat center/cover; 215 | content: ""; 216 | mask: linear-gradient(45deg, #000 40%, transparent 60%); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/components/FooterActionsBar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 147 | 148 | 158 | 159 | 208 | 209 | 242 | -------------------------------------------------------------------------------- /src/components/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 146 | 147 | 158 | 159 | 201 | 202 | 228 | -------------------------------------------------------------------------------- /src/components/TabsPaneList.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 173 | 174 | 186 | 187 | 215 | 216 | 255 | -------------------------------------------------------------------------------- /src/stores/userInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @FileDesc: 用户信息状态管理 3 | */ 4 | 5 | import { defineStore } from "pinia" 6 | import { reactive } from "vue" 7 | 8 | import { getPhoneNumberApi, loginApi, uploadAvatarApi, uploadUserInfoApi } from "@/apis" 9 | 10 | import type { IGetPhoneNumberApiParams, IUploadAvatarApiParams, IUploadUserInfoApiParams } from "@/apis" 11 | 12 | import { store } from "." 13 | 14 | /** Store 状态类型 */ 15 | interface IUserInfoStoreState { 16 | /** 17 | * 是否登录成功 18 | */ 19 | isLogin: boolean 20 | /** 21 | * 用户在我们系统中的加密用户代码 22 | */ 23 | userId: string 24 | /** 25 | * 由微信生成的用户在开放平台的唯一标识符 26 | */ 27 | unionId: string 28 | /** 29 | * 由微信生成的用户在本小程序唯一标识 30 | */ 31 | openId: string 32 | /** 33 | * 用户的微信昵称 34 | */ 35 | nickName: string 36 | /** 37 | * 用户的微信头像 38 | */ 39 | avatarUrl: string 40 | /** 41 | * 用户的微信绑定手机号(不带区号的手机号) 42 | */ 43 | phoneNumber: string 44 | } 45 | 46 | /** Store 实例 */ 47 | export const useUserInfoStore = defineStore("UserInfoStore", () => { 48 | 49 | /** Store 状态 */ 50 | const userInfoStoreState = reactive({ 51 | isLogin: false, 52 | 53 | userId: "", 54 | unionId: "", 55 | openId: "", 56 | 57 | nickName: "", 58 | avatarUrl: "", 59 | phoneNumber: "" 60 | }) 61 | 62 | /** 63 | * FUN: 微信登录 64 | * 65 | * @author dyb-dev 66 | * @date 09/10/2024/ 17:02:42 67 | * @returns {*} 微信登录结果 68 | */ 69 | const wxLogin = (): Promise => { 70 | 71 | return new Promise(resolve => { 72 | 73 | wx.login({ 74 | success: res => resolve(res), 75 | fail: err => resolve(err) 76 | }) 77 | 78 | }) 79 | 80 | } 81 | 82 | /** 83 | * FUN: 登录 84 | * 85 | * @author dyb-dev 86 | * @date 09/10/2024/ 17:02:57 87 | */ 88 | const login = async() => { 89 | 90 | try { 91 | 92 | uni.showLoading({ title: "正在登录...", mask: true }) 93 | 94 | const _wxLoginResult = await wxLogin() 95 | 96 | // @ts-ignore 97 | const _code = _wxLoginResult?.code || "" 98 | 99 | // 如果没有code 100 | if (!_code) { 101 | 102 | throw _wxLoginResult.errMsg 103 | 104 | } 105 | 106 | const _loginApiResult = await loginApi( 107 | { code: _code }, 108 | { 109 | testResult: { 110 | success: true, 111 | message: "登录成功", 112 | data: { 113 | userId: "userId", 114 | unionId: "unionId", 115 | openId: "openId", 116 | nickName: "xxx", 117 | avatarUrl: 118 | "https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png", 119 | phoneNumber: "111111111" 120 | } 121 | } 122 | } 123 | ) 124 | 125 | uni.hideLoading() 126 | 127 | // 如果登录失败 128 | if (!_loginApiResult.success || !_loginApiResult.data) { 129 | 130 | throw _loginApiResult.message 131 | 132 | } 133 | 134 | const { 135 | data: { userId = "", unionId = "", openId = "", nickName = "", avatarUrl = "", phoneNumber = "" } 136 | } = _loginApiResult 137 | 138 | userInfoStoreState.userId = userId 139 | userInfoStoreState.unionId = unionId 140 | userInfoStoreState.openId = openId 141 | userInfoStoreState.nickName = nickName 142 | userInfoStoreState.avatarUrl = avatarUrl 143 | userInfoStoreState.phoneNumber = phoneNumber 144 | 145 | userInfoStoreState.isLogin = true 146 | 147 | } 148 | catch (error) { 149 | 150 | uni.hideLoading() 151 | console.error("login()", error) 152 | 153 | uni.showModal({ 154 | title: "提示", 155 | content: "登录失败,请稍后重试!", 156 | showCancel: false 157 | }) 158 | 159 | } 160 | 161 | } 162 | 163 | /** 164 | * FUN: 获取手机号 165 | * 166 | * @author dyb-dev 167 | * @date 09/10/2024/ 20:22:11 168 | * @param {IGetPhoneNumberApiParams} params 参数 169 | * @returns {*} 获取手机号结果 170 | */ 171 | const getPhoneNumber = async(params: IGetPhoneNumberApiParams) => { 172 | 173 | const _result = await getPhoneNumberApi(params, { 174 | testResult: { 175 | success: true, 176 | message: "获取手机号成功", 177 | data: { 178 | phoneNumber: "111111111" 179 | } 180 | } 181 | }) 182 | 183 | // 如果获取成功 184 | if (_result.success && _result.data?.phoneNumber) { 185 | 186 | userInfoStoreState.phoneNumber = _result.data.phoneNumber 187 | 188 | } 189 | 190 | return _result 191 | 192 | } 193 | 194 | /** 195 | * FUN: 上传头像 196 | * 197 | * @author dyb-dev 198 | * @date 09/10/2024/ 20:33:52 199 | * @param {IUploadAvatarApiParams} params 参数 200 | * @returns {*} 上传头像结果 201 | */ 202 | const uploadAvatar = async(params: IUploadAvatarApiParams) => { 203 | 204 | const _result = await uploadAvatarApi(params, { 205 | testResult: { 206 | success: true, 207 | message: "上传头像成功" 208 | } 209 | }) 210 | 211 | // 如果上传头像成功 212 | if (_result.success) { 213 | 214 | userInfoStoreState.avatarUrl = params.avatarUrl 215 | 216 | } 217 | 218 | return _result 219 | 220 | } 221 | 222 | /** 223 | * FUN: 上传用户信息 224 | * 225 | * @author dyb-dev 226 | * @date 09/10/2024/ 20:39:29 227 | * @param {IUploadUserInfoApiParams} params 参数 228 | * @returns {*} 上传用户信息结果 229 | */ 230 | const uploadUserInfo = async(params: IUploadUserInfoApiParams) => { 231 | 232 | const _result = await uploadUserInfoApi(params, { 233 | testResult: { 234 | success: true, 235 | message: "上传用户信息成功" 236 | } 237 | }) 238 | 239 | // 如果上传用户信息成功 240 | if (_result.success) { 241 | 242 | userInfoStoreState.avatarUrl = params.avatarUrl 243 | userInfoStoreState.nickName = params.nickName 244 | 245 | } 246 | 247 | return _result 248 | 249 | } 250 | 251 | return { userInfoStoreState, login, getPhoneNumber, uploadAvatar, uploadUserInfo } 252 | 253 | }) 254 | 255 | /** 256 | * FUN: 使用状态管理 257 | * - 在没有Vue组件上下文的情况下使用 258 | * 259 | * @author dyb-dev 260 | * @date 15/09/2024/ 23:53:35 261 | * @returns store实例 262 | */ 263 | export const useUserInfoStoreWithOut = () => { 264 | 265 | return useUserInfoStore(store) 266 | 267 | } 268 | -------------------------------------------------------------------------------- /src/components/FormControl.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 197 | 198 | 210 | 211 | 269 | 270 | 308 | --------------------------------------------------------------------------------