commit ee0fa309cae26da339b7d857c83db2d113238c51 Author: landaiqing Date: Sun Sep 14 16:49:40 2025 +0800 :tada: initial commit diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs new file mode 100644 index 0000000..98ee7df --- /dev/null +++ b/.commitlintrc.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +} diff --git a/.cursor/rules/api-http-patterns.mdc b/.cursor/rules/api-http-patterns.mdc new file mode 100644 index 0000000..79026c3 --- /dev/null +++ b/.cursor/rules/api-http-patterns.mdc @@ -0,0 +1,51 @@ +# API 和 HTTP 请求规范 + +## HTTP 请求封装 +- 可以使用 `简单http` 或者 `alova` 或者 `@tanstack/vue-query` 进行请求管理 +- HTTP 配置在 [src/http/](mdc:src/http/) 目录下 +- `简单http` - [src/http/http.ts](mdc:src/http/http.ts) +- `alova` - [src/http/alova.ts](mdc:src/http/alova.ts) +- `vue-query` - [src/http/vue-query.ts](mdc:src/http/vue-query.ts) +- 请求拦截器在 [src/http/interceptor.ts](mdc:src/http/interceptor.ts) +- 支持请求重试、缓存、错误处理 + +## API 接口规范 +- API 接口定义在 [src/api/](mdc:src/api/) 目录下 +- 按功能模块组织 API 文件 +- 使用 TypeScript 定义请求和响应类型 +- 支持 `简单http`、`alova` 和 `vue-query` 三种请求方式 + + +## 示例代码结构 +```typescript +// API 接口定义 +export interface LoginParams { + username: string + password: string +} + +export interface LoginResponse { + token: string + userInfo: UserInfo +} + +// alova 方式 +export const login = (params: LoginParams) => + http.Post('/api/login', params) + +// vue-query 方式 +export const useLogin = () => { + return useMutation({ + mutationFn: (params: LoginParams) => + http.post('/api/login', params) + }) +} +``` + +## 错误处理 +- 统一错误处理在拦截器中配置 +- 支持网络错误、业务错误、认证错误等 +- 自动处理 token 过期和刷新 +--- +globs: src/api/*.ts,src/http/*.ts +--- diff --git a/.cursor/rules/development-workflow.mdc b/.cursor/rules/development-workflow.mdc new file mode 100644 index 0000000..7a8c901 --- /dev/null +++ b/.cursor/rules/development-workflow.mdc @@ -0,0 +1,41 @@ +# 开发工作流程 + +## 项目启动 +1. 安装依赖:`pnpm install` +2. 开发环境: + - H5: `pnpm dev` 或 `pnpm dev:h5` + - 微信小程序: `pnpm dev:mp` + - APP: `pnpm dev:app` + +## 代码规范 +- 使用 ESLint 进行代码检查:`pnpm lint` +- 自动修复代码格式:`pnpm lint:fix` +- 使用 eslint 格式化代码 +- 遵循 TypeScript 严格模式 + +## 构建和部署 +- H5 构建:`pnpm build:h5` +- 小程序构建:`pnpm build:mp` +- APP 构建:`pnpm build:app` +- 类型检查:`pnpm type-check` + +## 开发工具 +- 推荐使用 VSCode 编辑器 +- 安装 Vue 和 TypeScript 相关插件 +- 使用 uni-app 开发者工具调试小程序 +- 使用 HBuilderX 调试 APP + +## 调试技巧 +- 使用 console.log 和 uni.showToast 调试 +- 利用 Vue DevTools 调试组件状态 +- 使用网络面板调试 API 请求 +- 平台差异测试和兼容性检查 + +## 性能优化 +- 使用懒加载和代码分割 +- 优化图片和静态资源 +- 减少不必要的重渲染 +- 合理使用缓存策略 +--- +description: 开发工作流程和最佳实践指南 +--- diff --git a/.cursor/rules/project-overview.mdc b/.cursor/rules/project-overview.mdc new file mode 100644 index 0000000..335b480 --- /dev/null +++ b/.cursor/rules/project-overview.mdc @@ -0,0 +1,34 @@ +--- +alwaysApply: true +--- +# unibest 项目概览 + +这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。 + +## 项目特点 +- 支持 H5、小程序、APP 多平台开发 +- 使用最新的前端技术栈 +- 内置约定式路由、layout布局、请求封装等功能 +- 无需依赖 HBuilderX,支持命令行开发 + +## 核心配置文件 +- [package.json](mdc:package.json) - 项目依赖和脚本配置 +- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置 +- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置 +- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置 +- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置 + +## 主要目录结构 +- `src/pages/` - 页面文件 +- `src/components/` - 组件文件 +- `src/layouts/` - 布局文件 +- `src/api/` - API 接口 +- `src/http/` - HTTP 请求封装 +- `src/store/` - 状态管理 +- `src/tabbar/` - 底部导航栏 + +## 开发命令 +- `pnpm dev` - 开发 H5 版本 +- `pnpm dev:mp` - 开发微信小程序 +- `pnpm dev:app` - 开发 APP 版本 +- `pnpm build` - 构建生产版本 diff --git a/.cursor/rules/styling-css-patterns.mdc b/.cursor/rules/styling-css-patterns.mdc new file mode 100644 index 0000000..25f14f2 --- /dev/null +++ b/.cursor/rules/styling-css-patterns.mdc @@ -0,0 +1,54 @@ +# 样式和 CSS 开发规范 + +## UnoCSS 原子化 CSS +- 项目使用 UnoCSS 作为原子化 CSS 框架 +- 配置在 [uno.config.ts](mdc:uno.config.ts) +- 支持预设和自定义规则 +- 优先使用原子化类名,减少自定义 CSS + +## SCSS 规范 +- 使用 SCSS 预处理器 +- 样式文件使用 `lang="scss"` 和 `scoped` 属性 +- 遵循 BEM 命名规范 +- 使用变量和混入提高复用性 + +## 样式组织 +- 全局样式在 [src/style/](mdc:src/style/) 目录下 +- 组件样式使用 scoped 作用域 +- 图标字体在 [src/style/iconfont.css](mdc:src/style/iconfont.css) +- 主题变量在 [src/uni_modules/uni-scss/](mdc:src/uni_modules/uni-scss/) 目录下 + +## 示例代码结构 +```vue + + + + +## 响应式设计 +- 使用 rpx 单位适配不同屏幕 +- 支持横屏和竖屏布局 +- 使用 flexbox 和 grid 布局 +- 考虑不同平台的样式差异 +--- +globs: *.vue,*.scss,*.css +--- diff --git a/.cursor/rules/uni-app-patterns.mdc b/.cursor/rules/uni-app-patterns.mdc new file mode 100644 index 0000000..956566d --- /dev/null +++ b/.cursor/rules/uni-app-patterns.mdc @@ -0,0 +1,62 @@ +# uni-app 开发规范 + +## 页面开发 +- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下 +- 使用约定式路由,文件名即路由路径 +- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中 + +## 组件开发 +- 组件文件放在 [src/components/](mdc:src/components/) 目录下 +- 使用 uni-app 内置组件和第三方组件库 +- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件 +- 自定义组件遵循 uni-app 组件规范 + +## 平台适配 +- 使用条件编译处理平台差异 +- 支持 H5、小程序、APP 多平台 +- 注意各平台的 API 差异 +- 使用 uni.xxx API 替代原生 API + +## 示例代码结构 +```vue + + + +``` + +## 生命周期 +- 使用 uni-app 页面生命周期 +- onLoad、onShow、onReady、onHide、onUnload +- 组件生命周期遵循 Vue3 规范 +- 注意页面栈和导航管理 +--- +globs: src/pages/*.vue,src/components/*.vue +--- diff --git a/.cursor/rules/vue-typescript-patterns.mdc b/.cursor/rules/vue-typescript-patterns.mdc new file mode 100644 index 0000000..f726299 --- /dev/null +++ b/.cursor/rules/vue-typescript-patterns.mdc @@ -0,0 +1,52 @@ +# Vue3 + TypeScript 开发规范 + +## Vue 组件规范 +- 使用 Composition API 和 ` + + + + +--- +globs: *.vue,*.ts,*.tsx +--- diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f09864 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] # 表示所有文件适用 +charset = utf-8 # 设置文件字符集为 utf-8 +indent_style = space # 缩进风格(tab | space) +indent_size = 2 # 缩进大小 +end_of_line = lf # 控制换行类型(lf | cr | crlf) +trim_trailing_whitespace = true # 去除行首的任意空白字符 +insert_final_newline = true # 始终在文件末尾插入一个新行 + +[*.md] # 表示仅 md 文件适用以下规则 +max_line_length = off # 关闭最大行长度限制 +trim_trailing_whitespace = false # 关闭末尾空格修剪 diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..6ae23b0 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,31 @@ +categories: + - title: 🚀 新功能 + labels: [feat, feature] + - title: 🛠️ 修复 + labels: [fix, bugfix] + - title: 💅 样式 + labels: [style] + - title: 📄 文档 + labels: [docs] + - title: ⚡️ 性能 + labels: [perf] + - title: 🧪 测试 + labels: [test] + - title: ♻️ 重构 + labels: [refactor] + - title: 📦 构建 + labels: [build] + - title: 🚨 补丁 + labels: [patch, hotfix] + - title: 🌐 发布 + labels: [release, publish] + - title: 🔧 流程 + labels: [ci, cd, workflow] + - title: ⚙️ 配置 + labels: [config, chore] + - title: 📁 文件 + labels: [file] + - title: 🎨 格式化 + labels: [format] + - title: 🔀 其他 + labels: [other, misc] diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..30a18db --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,188 @@ +name: Auto Merge Main to Other Branches + +on: + push: + branches: + - main + workflow_dispatch: # 手动触发 + +jobs: + # merge-to-release: + # name: Merge main into release + # runs-on: ubuntu-latest + # steps: + # - name: Checkout repository + # uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + # - name: Merge main into release + # run: | + # git config user.name "GitHub Actions" + # git config user.email "actions@github.com" + # git checkout release + # git merge main --no-ff -m "Auto merge main into release" + # git push origin release + + merge-to-i18n: + name: Merge main into i18n + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into i18n + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout i18n + git merge main --no-ff -m "Auto merge main into i18n" + git push origin i18n + + merge-to-base-sard-ui: + name: Merge main into base-sard-ui + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-sard-ui + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-sard-ui + git merge main --no-ff -m "Auto merge main into base-sard-ui" + git push origin base-sard-ui + + merge-to-base-uv-ui: + name: Merge main into base-uv-ui + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-uv-ui + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-uv-ui + git merge main --no-ff -m "Auto merge main into base-uv-ui" + git push origin base-uv-ui + + merge-to-base-uview-pro: + name: Merge main into base-uview-pro + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-uview-pro + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-uview-pro + git merge main --no-ff -m "Auto merge main into base-uview-pro" + git push origin base-uview-pro + + merge-to-base-uview-plus: + name: Merge main into base-uview-plus + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-uview-plus + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-uview-plus + git merge main --no-ff -m "Auto merge main into base-uview-plus" + git push origin base-uview-plus + + # merge-to-base-tm-ui: + # name: Merge main into base-tm-ui + # runs-on: ubuntu-latest + # steps: + # - name: Checkout repository + # uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + # - name: Merge main into base-tm-ui + # run: | + # git config user.name "GitHub Actions" + # git config user.email "actions@github.com" + # git checkout base-tm-ui + # git merge main --no-ff -m "Auto merge main into base-tm-ui" + # git push origin base-tm-ui + + merge-to-base-skiyee-ui: + name: Merge main into base-skiyee-ui + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-skiyee-ui + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-skiyee-ui + git merge main --no-ff -m "Auto merge main into base-skiyee-ui" + git push origin base-skiyee-ui + + merge-to-main-v4: + name: Merge main into main-v4 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into main-v4 + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout main-v4 + git merge main --no-ff -m "Auto merge main into main-v4" + git push origin main-v4 + + merge-to-i18n-v4: + name: Merge main into i18n-v4 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into i18n-v4 + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout i18n-v4 + git merge main --no-ff -m "Auto merge main into i18n-v4" + git push origin i18n-v4 diff --git a/.github/workflows/release-log.yml b/.github/workflows/release-log.yml new file mode 100644 index 0000000..c2887ab --- /dev/null +++ b/.github/workflows/release-log.yml @@ -0,0 +1,119 @@ +name: Auto Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + pull-requests: read + issues: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install yq + run: sudo snap install yq + + - name: Generate changelog + id: changelog + env: + CONFIG_FILE: .github/release.yml + run: | + # 解析配置文件 + declare -A category_map + while IFS=";" read -r title labels; do + for label in $labels; do + category_map[$label]="$title" + done + done < <(yq -o=tsv '.categories[] | [.title, (.labels | join(" "))] | join(";")' $CONFIG_FILE) + # 获取版本范围 + mapfile -t tags < <(git tag -l --sort=-version:refname) + current_tag=${tags[0]} + previous_tag=${tags[1]:-} + if [[ -z "$previous_tag" ]]; then + commit_range="$current_tag" + echo "首次发布版本: $current_tag" + else + commit_range="$previous_tag..$current_tag" + echo "版本范围: $commit_range" + fi + # 获取所有符合规范的提交 + commits=$(git log --pretty=format:"%s|%h" "$commit_range") + # 生成分类日志 + declare -A log_entries + while IFS="|" read -r subject hash; do + # type=$(echo "$subject" | cut -d':' -f1 | tr -d ' ') + type=$(echo "$subject" | sed -E 's/^([[:alnum:]]+)(\(.*\))?:.*/\1/' | tr -d ' ') + found=0 + for label in "${!category_map[@]}"; do + if [[ "$type" == "$label" ]]; then + entry="- ${subject} (${hash:0:7})" + log_entries[${category_map[$label]}]+="$entry"$'\n' + found=1 + break + fi + done + if [[ $found -eq 0 ]]; then + entry="- ${subject} (${hash:0:7})" + log_entries["其他"]+="$entry"$'\n' + fi + done <<< "$commits" + + # 统计提交数量 + commit_count=$(git log --oneline "$commit_range" | wc -l) + # 统计受影响的文件数量 + file_count=$(git diff --name-only "$commit_range" | wc -l) + # 统计贡献者信息 + contributor_stats=$(git shortlog -sn "$commit_range") + contributor_notes="" + while IFS= read -r line; do + commits=$(echo "$line" | awk '{print $1}') + name=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //') + contributor_notes+="- @${name} (${commits} commits)\n" + done <<< "$contributor_stats" + # 构建输出内容 + release_notes="## 版本更新日志 ($current_tag)\n\n" + while IFS= read -r category; do + if [[ -n "${log_entries[$category]}" ]]; then + release_notes+="### $category\n${log_entries[$category]}\n" + fi + done < <(yq '.categories[].title' $CONFIG_FILE) + # 构建输出内容 + release_notes="## 版本更新日志 ($current_tag)\n\n" + current_date=$(date +"%Y-%m-%d") + # 添加发布日期和下载统计信息 + release_notes+=" ### 📅 发布日期: ${current_date}\n" + while IFS= read -r category; do + if [[ -n "${log_entries[$category]}" ]]; then + release_notes+="### $category\n${log_entries[$category]}\n" + fi + done < <(yq '.categories[].title' $CONFIG_FILE) + + # 添加统计信息 + release_notes+="### 📊 统计信息\n" + release_notes+="- 本次发布包含 ${commit_count} 个提交\n" + release_notes+="- 影响 ${file_count} 个文件\n\n" + # 添加贡献者信息 + release_notes+="### 👥 贡献者\n" + release_notes+="感谢这些优秀的贡献者(按提交次数排序):\n" + release_notes+="${contributor_notes}\n" + release_notes+="---\n" + # 写入文件 + echo -e "$release_notes" > changelog.md + echo "生成日志内容:" + cat changelog.md + - name: Create Release + uses: ncipollo/release-action@v1 + with: + generateReleaseNotes: false + bodyFile: changelog.md + tag: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..418a108 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +*.local + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.hbuilderx + +.stylelintcache +.eslintcache + +docs/.vitepress/dist +docs/.vitepress/cache + +src/types +src/manifest.json +src/pages.json + +# lock 文件还是不要了,我主要的版本写死就好了 +pnpm-lock.yaml +package-lock.json + +# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作 +# `git rm -r --cached .` 然后提交 commit 即可。 + +# git rm -r --cached file1 file2 ## 针对某些文件 +# git rm -r --cached dir1 dir2 ## 针对某些文件夹 +# git rm -r --cached . ## 针对所有文件 + +# 更新 uni-app 官方版本 +# npx @dcloudio/uvm@latest diff --git a/.husky.back/commit-msg b/.husky.back/commit-msg new file mode 100644 index 0000000..36158d9 --- /dev/null +++ b/.husky.back/commit-msg @@ -0,0 +1 @@ +npx --no-install commitlint --edit "$1" \ No newline at end of file diff --git a/.husky.back/pre-commit b/.husky.back/pre-commit new file mode 100644 index 0000000..c3ec64b --- /dev/null +++ b/.husky.back/pre-commit @@ -0,0 +1 @@ +npx lint-staged --allow-empty \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..10ecfe2 --- /dev/null +++ b/.npmrc @@ -0,0 +1,8 @@ +# registry = https://registry.npmjs.org +registry = https://registry.npmmirror.com + +strict-peer-dependencies=false +auto-install-peers=true +shamefully-hoist=true +ignore-workspace-root-check=true +install-workspace-root=true diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..b614c90 --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,118 @@ +# unibest 项目概览 + +这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。 + +## 项目特点 +- 支持 H5、小程序、APP 多平台开发 +- 使用最新的前端技术栈 +- 内置约定式路由、layout布局、请求封装等功能 +- 无需依赖 HBuilderX,支持命令行开发 + +## 核心配置文件 +- [package.json](mdc:package.json) - 项目依赖和脚本配置 +- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置 +- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置 +- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置 +- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置 + +## 主要目录结构 +- `src/pages/` - 页面文件 +- `src/components/` - 组件文件 +- `src/layouts/` - 布局文件 +- `src/api/` - API 接口 +- `src/http/` - HTTP 请求封装 +- `src/store/` - 状态管理 +- `src/tabbar/` - 底部导航栏 + +## 开发命令 +- `pnpm dev` - 开发 H5 版本 +- `pnpm dev:mp` - 开发微信小程序 +- `pnpm dev:app` - 开发 APP 版本 +- `pnpm build` - 构建生产版本 + +## Vue 组件规范 +- 使用 Composition API 和 ` + + +``` + +## 生命周期 +- 使用 uni-app 页面生命周期 +- onLoad、onShow、onReady、onHide、onUnload +- 组件生命周期遵循 Vue3 规范 +- 注意页面栈和导航管理 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..39b01e3 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "vue.volar", + "stylelint.vscode-stylelint", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "antfu.unocss", + "antfu.iconify", + "evils.uniapp-vscode", + "uni-helper.uni-helper-vscode", + "uni-helper.uni-app-schemas-vscode", + "uni-helper.uni-highlight-vscode", + "uni-helper.uni-ui-snippets-vscode", + "uni-helper.uni-app-snippets-vscode", + "streetsidesoftware.code-spell-checker", + "foxundermoon.shell-format", + "christian-kohler.path-intellisense" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c2a5ac8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,95 @@ +{ + // 配置语言的文件关联 + "files.associations": { + "pages.json": "jsonc", // pages.json 可以写注释 + "manifest.json": "jsonc" // manifest.json 可以写注释 + }, + + "stylelint.enable": false, // 禁用 stylelint + "css.validate": false, // 禁用 CSS 内置验证 + "scss.validate": false, // 禁用 SCSS 内置验证 + "less.validate": false, // 禁用 LESS 内置验证 + + "typescript.tsdk": "node_modules\\typescript\\lib", + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.expand": false, + "explorer.fileNesting.patterns": { + "README.md": "index.html,favicon.ico,robots.txt,CHANGELOG.md", + "pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts", + "package.json": "tsconfig.json,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc", + "eslint.config.mjs": ".commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*" + }, + + // Disable the default formatter, use eslint instead + "prettier.enable": false, + "editor.formatOnSave": false, + + // Auto fix + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + + // Silent the stylistic rules in you IDE, but still auto fix them + "eslint.rules.customizations": [ + { "rule": "style/*", "severity": "off", "fixable": true }, + { "rule": "format/*", "severity": "off", "fixable": true }, + { "rule": "*-indent", "severity": "off", "fixable": true }, + { "rule": "*-spacing", "severity": "off", "fixable": true }, + { "rule": "*-spaces", "severity": "off", "fixable": true }, + { "rule": "*-order", "severity": "off", "fixable": true }, + { "rule": "*-dangle", "severity": "off", "fixable": true }, + { "rule": "*-newline", "severity": "off", "fixable": true }, + { "rule": "*quotes", "severity": "off", "fixable": true }, + { "rule": "*semi", "severity": "off", "fixable": true } + ], + + // Enable eslint for all supported languages + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "jsonc", + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" + ], + "cSpell.words": [ + "alova", + "Aplipay", + "attributify", + "chooseavatar", + "climblee", + "commitlint", + "dcloudio", + "iconfont", + "oxlint", + "qrcode", + "refresherrefresh", + "scrolltolower", + "tabbar", + "Toutiao", + "uniapp", + "unibest", + "unocss", + "uview", + "uvui", + "Wechat", + "WechatMiniprogram", + "Weixin" + ] +} diff --git a/.vscode/vue3.code-snippets b/.vscode/vue3.code-snippets new file mode 100644 index 0000000..b650b8a --- /dev/null +++ b/.vscode/vue3.code-snippets @@ -0,0 +1,77 @@ +{ + // Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Print unibest Vue3 SFC": { + "scope": "vue", + "prefix": "v3", + "body": [ + "\n", + "\n", + "\n", + ], + }, + "Print unibest style": { + "scope": "vue", + "prefix": "st", + "body": [ + "\n" + ], + }, + "Print unibest script": { + "scope": "vue", + "prefix": "sc", + "body": [ + "\n" + ], + }, + "Print unibest script with definePage": { + "scope": "vue", + "prefix": "scdp", + "body": [ + "\n" + ], + }, + "Print unibest template": { + "scope": "vue", + "prefix": "te", + "body": [ + "\n" + ], + }, +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9e91d10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 菲鸽 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1293a0 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +

+ + + +

+ +

+ unibest - 最好的 uniapp 开发框架 +

+ +
+旧仓库 codercup 进不去了,star 也拿不回来,这里也展示一下那个地址的 star. + +[![GitHub Repo stars](https://img.shields.io/github/stars/codercup/unibest?style=flat&logo=github)](https://github.com/codercup/unibest) +[![GitHub forks](https://img.shields.io/github/forks/codercup/unibest?style=flat&logo=github)](https://github.com/codercup/unibest) + +
+ +
+ +[![GitHub Repo stars](https://img.shields.io/github/stars/feige996/unibest?style=flat&logo=github)](https://github.com/feige996/unibest) +[![GitHub forks](https://img.shields.io/github/forks/feige996/unibest?style=flat&logo=github)](https://github.com/feige996/unibest) +[![star](https://gitee.com/feige996/unibest/badge/star.svg?theme=dark)](https://gitee.com/feige996/unibest/stargazers) +[![fork](https://gitee.com/feige996/unibest/badge/fork.svg?theme=dark)](https://gitee.com/feige996/unibest/members) +![node version](https://img.shields.io/badge/node-%3E%3D18-green) +![pnpm version](https://img.shields.io/badge/pnpm-%3E%3D7.30-green) +![GitHub package.json version (subfolder of monorepo)](https://img.shields.io/github/package-json/v/feige996/unibest) +![GitHub License](https://img.shields.io/github/license/feige996/unibest) + +
+ +`unibest` —— 最好的 `uniapp` 开发模板,由 `uniapp` + `Vue3` + `Ts` + `Vite5` + `UnoCss` + `wot-ui` + `z-paging` 构成,使用了最新的前端技术栈,无需依靠 `HBuilderX`,通过命令行方式运行 `web`、`小程序` 和 `App`(编辑器推荐 `VSCode`,可选 `webstorm`)。 + +`unibest` 内置了 `约定式路由`、`layout布局`、`请求封装`、`请求拦截`、`登录拦截`、`UnoCSS`、`i18n多语言` 等基础功能,提供了 `代码提示`、`自动格式化`、`统一配置`、`代码片段` 等辅助功能,让你编写 `uniapp` 拥有 `best` 体验 ( `unibest 的由来`)。 + +![](https://raw.githubusercontent.com/andreasbm/readme/master/screenshots/lines/rainbow.png) + +

+ 📖 文档地址(new) + | + 📱 DEMO 地址 +

+ +--- + +注意旧的地址 [codercup](https://github.com/codercup/unibest) 我进不去了,使用新的 [feige996](https://github.com/feige996/unibest)。PR和 issue 也请使用新地址,否则无法合并。 + +## 平台兼容性 + +| H5 | IOS | 安卓 | 微信小程序 | 字节小程序 | 快手小程序 | 支付宝小程序 | 钉钉小程序 | 百度小程序 | +| --- | --- | ---- | ---------- | ---------- | ---------- | ------------ | ---------- | ---------- | +| √ | √ | √ | √ | √ | √ | √ | √ | √ | + +注意每种 `UI框架` 支持的平台有所不同,详情请看各 `UI框架` 的官网,也可以看 `unibest` 文档。 + +## ⚙️ 环境 + +- node>=18 +- pnpm>=7.30 +- Vue Official>=2.1.10 +- TypeScript>=5.0 + +## 📂 快速开始 + +执行 `pnpm create unibest` 创建项目 +执行 `pnpm i` 安装依赖 +执行 `pnpm dev` 运行 `H5` +执行 `pnpm dev:mp` 运行 `微信小程序` + +## 📦 运行(支持热更新) + +- web平台: `pnpm dev:h5`, 然后打开 [http://localhost:9000/](http://localhost:9000/)。 +- weixin平台:`pnpm dev:mp` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。 +- APP平台:`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来运行到对应的平台。) + +## 🔗 发布 + +- web平台: `pnpm build:h5`,打包后的文件在 `dist/build/h5`,可以放到web服务器,如nginx运行。如果最终不是放在根目录,可以在 `manifest.config.ts` 文件的 `h5.router.base` 属性进行修改。 +- weixin平台:`pnpm build:mp`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。 +- APP平台:`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来发行到对应的平台。) + +## 📄 License + +[MIT](https://opensource.org/license/mit/) + +Copyright (c) 2025 菲鸽 + +## 捐赠 + +

+special sponsor appwrite +special sponsor appwrite +

diff --git a/codes/README.md b/codes/README.md new file mode 100644 index 0000000..38a29c3 --- /dev/null +++ b/codes/README.md @@ -0,0 +1,3 @@ +# 参考代码 + +部分代码片段,供参考。 \ No newline at end of file diff --git a/codes/router.txt b/codes/router.txt new file mode 100644 index 0000000..e9e28d5 --- /dev/null +++ b/codes/router.txt @@ -0,0 +1,222 @@ +import { getCurrentInstance, type App } from 'vue' +import { useUserLoginStore } from '@/store/login' +import { Pages } from './pages' +import { LoginPopupViewer } from './loginPopupServices' +import Loading from './Loading' + +/** 实时判断用户是否已登录(避免 computed 缓存) */ +function isUserLoggedIn(): boolean { + return useUserLoginStore().isLoggedIn +} + +// 路由相关配置 +// 这里可以根据实际情况调整 +// 例如:需要登录验证的页面等 +// 以及登录页面、会员中心页面等 + +// 需要登录验证的页面 +const authPages = [ + Pages.USER_INFO_EDIT, + Pages.VIP_CENTER, + //Pages.PRODUCT_LIST, + //Pages.PRODUCT_DETAILS, + Pages.USER_ACCOUNT_SECURITY, + Pages.USER_EDIT_NICKNAME, + Pages.USER_ORDER_LIST, + Pages.USER_ORDER_DETAILS, + Pages.USER_MOBILE, + Pages.DISTRIBUTION_CENTER, + Pages.DISTRIBUTION_CENTER_DETAILS, + Pages.USER_MOBILE_CHANGE, + Pages.USER_PERSONAL_INFO, + Pages.USER_REMARK, + Pages.PRODUCT_ORDER_CONFIRM, + Pages.PRODUCT_PAY_MODE, + Pages.COUPON_CENTER, + Pages.COUPON_LIST, + Pages.CUSTOMER_SERVICE, + Pages.SHIPPING_ADDRESS_ADDED_OR_EDIT, + Pages.SHIPPING_ADDRESS_LIST, + Pages.USER_PASSWORD_CONFIG, + Pages.WITHDRAWAL, + Pages.WITHDRAWAL_RECORD_LIST, +] + +/** 判断是否需要登录 */ +function getBasePath(url: string): string { + const index = url.indexOf('?') + return index !== -1 ? url.substring(0, index) : url +} + +function isAuthRequired(url: string): boolean { + const cleanUrl = getBasePath(url) + console.log(`URL数据源:${authPages}`) + console.log(`URL原始值: ${url}`) + console.log(`URL过滤值: ${cleanUrl}`) + return authPages.some((item) => item === cleanUrl) +} + +/** 缓存跳转路径 */ +function cacheRedirect(url: string) { + uni.setStorageSync('pending_redirect', url) +} + +/** 读取并清除缓存跳转路径 */ +function consumeRedirect(): string | null { + const url = uni.getStorageSync('pending_redirect') + uni.removeStorageSync('pending_redirect') + return url || null +} + +/** 路由核心跳转方法 */ +async function internalNavigate( + type: 'navigateTo' | 'redirectTo' | 'switchTab' | 'reLaunch', + url: string, + options: Record = {}, +) { + const originUrl: string = url.startsWith('/') ? url : `/${url}` + const isAuthPage = isAuthRequired(originUrl) + console.log(`[Router][${type}] 跳转到:`, originUrl, '需要登录:', isAuthPage) + console.log(`[Router][${type}] 是否登录:`, isUserLoggedIn) + + // 如果需要登录但未登录,则弹出登录框 + if (isAuthPage && !isUserLoggedIn()) { + cacheRedirect(originUrl) + const loginResult = await LoginPopupViewer.open() + console.log(`[Router][${type}] 登录弹窗结果:`, loginResult) + + // 如果登录失败(或用户取消),中断跳转 + if (!loginResult) { + console.log(`[Router][${type}] 已终止跳转,原因:用户未登录或取消登录`) + Loading.showError({ msg: '已取消登录' }) + return + } + } + + // 登录状态已满足,可以安全跳转 + try { + switch (type) { + case 'navigateTo': + return await uniNavigateTo(originUrl, options) + case 'redirectTo': + return await uniRedirectTo(originUrl, options) + case 'switchTab': + return await uniSwitchTab(originUrl) + case 'reLaunch': + return await uniReLaunch(originUrl) + } + } catch (error) { + console.error(`[Router][${type}] 跳转失败:`, error) + } +} + +/** ✅ Promise 封装 uni API **/ +function uniNavigateTo(url: string, options: any) { + return new Promise((resolve, reject) => { + uni.navigateTo({ + url, + ...options, + success: resolve, + fail: reject, + }) + }) +} +function uniRedirectTo(url: string, options: any) { + return new Promise((resolve, reject) => { + uni.redirectTo({ + url, + ...options, + success: resolve, + fail: reject, + }) + }) +} +function uniSwitchTab(url: string) { + return new Promise((resolve, reject) => { + uni.switchTab({ + url, + success: resolve, + fail: reject, + }) + }) +} +function uniReLaunch(url: string) { + return new Promise((resolve, reject) => { + uni.reLaunch({ + url, + success: resolve, + fail: reject, + }) + }) +} + +// ✅ Router API 对象 +// ✅ Router API 对象 +export const Router = { + // 页面跳转,支持登录鉴权 + async navigateTo(opt: { url: string; requiresAuth?: boolean } & Record) { + return await internalNavigate('navigateTo', opt.url, opt) + }, + + // 页面重定向,支持登录鉴权 + async redirectTo(opt: { url: string; requiresAuth?: boolean } & Record) { + return await internalNavigate('redirectTo', opt.url, opt) + }, + + // tab 页面切换 + async switchTab(opt: { url: string }) { + return await internalNavigate('switchTab', opt.url, opt) + }, + + // 重新启动应用跳转 + async reLaunch(opt: { url: string }) { + return await internalNavigate('reLaunch', opt.url, opt) + }, + + // 重定向别名 + async replace(opt: { url: string; requiresAuth?: boolean } & Record) { + return await internalNavigate('redirectTo', opt.url, opt) + }, + + // 返回上一级 + async back(delta = 1) { + return await new Promise((resolve, reject) => { + uni.navigateBack({ + delta, + success: resolve, + fail: reject, + }) + }) + }, + + consumeRedirect, +} + +let cachedRouter: typeof Router | null = null + +/** + * ✅ 全局安全获取 $Router 实例(推荐使用) + */ +export function useRouter(): typeof Router { + if (cachedRouter) return cachedRouter + + const instance = getCurrentInstance() + if (!instance) { + throw new Error('useRouter() 必须在 setup() 或生命周期中调用') + } + + const router = instance.appContext.config.globalProperties.$Router + if (!router) { + throw new Error('$Router 尚未注入,请在 main.ts 中使用 app.use(RouterPlugin)') + } + + cachedRouter = router + return router +} + +/** ✅ 注册为全局插件 */ +export default { + install(app: App) { + app.config.globalProperties.$Router = Router + }, +} diff --git a/env/.env b/env/.env new file mode 100644 index 0000000..1b60540 --- /dev/null +++ b/env/.env @@ -0,0 +1,28 @@ +VITE_APP_TITLE = 'unibest' +VITE_APP_PORT = 9000 + +VITE_UNI_APPID = '__UNI__D1E5001' +VITE_WX_APPID = 'wxa2abb91f64032a2b' + +# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base +# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router +VITE_APP_PUBLIC_BASE=/ + +# 后台请求地址 +VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run' +# 后台上传地址 +VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload' + +# 注意,如果是微信小程序,还有一套请求地址的配置,在 `src/utils/index.ts` 中 + +# h5是否需要配置代理 +VITE_APP_PROXY_ENABLE = true +VITE_APP_PROXY_PREFIX = '/api' +# 后端是否有统一前缀 /api,决定本地代码的时候是否需要去掉 /api 前缀。这里面默认是没有的,即前端会把/api 转发去掉 +VITE_SERVER_HAS_API_PREFIX = false + +# 第二个请求地址 (目前alova中可以使用) +VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run' + +# 认证模式,'single' | 'double' ==> 单token | 双token +VITE_AUTH_MODE = 'single' \ No newline at end of file diff --git a/env/.env.development b/env/.env.development new file mode 100644 index 0000000..ac1b65b --- /dev/null +++ b/env/.env.development @@ -0,0 +1,9 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'development' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = false +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = false + +# 后台请求地址 +# VITE_SERVER_BASEURL = 'https://dev.xxx.com' diff --git a/env/.env.production b/env/.env.production new file mode 100644 index 0000000..eef01df --- /dev/null +++ b/env/.env.production @@ -0,0 +1,9 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'production' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = true +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = false + +# 后台请求地址 +# VITE_SERVER_BASEURL = 'https://prod.xxx.com' diff --git a/env/.env.test b/env/.env.test new file mode 100644 index 0000000..5a975f8 --- /dev/null +++ b/env/.env.test @@ -0,0 +1,9 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'development' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = false +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = false + +# 后台请求地址 +# VITE_SERVER_BASEURL = 'https://test.xxx.com' diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..ad0274b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,55 @@ +import uniHelper from '@uni-helper/eslint-config' + +export default uniHelper({ + unocss: true, + vue: true, + markdown: false, + ignores: [ + 'src/uni_modules/', + 'dist', + // unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用 + 'auto-import.d.ts', + // vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore + 'uni-pages.d.ts', + // 插件生成的文件 + 'src/pages.json', + 'src/manifest.json', + // 忽略自动生成文件 + 'src/service/**', + ], + // https://eslint-config.antfu.me/rules + rules: { + 'no-useless-return': 'off', + 'no-console': 'off', + 'no-unused-vars': 'off', + 'vue/no-unused-refs': 'off', + 'unused-imports/no-unused-vars': 'off', + 'eslint-comments/no-unlimited-disable': 'off', + 'jsdoc/check-param-names': 'off', + 'jsdoc/require-returns-description': 'off', + 'ts/no-empty-object-type': 'off', + 'no-extend-native': 'off', + 'vue/singleline-html-element-content-newline': [ + 'error', + { + externalIgnores: ['text'], + }, + ], + // vue SFC 调换顺序改这里 + 'vue/block-order': ['error', { + order: [['script', 'template'], 'style'], + }], + }, + formatters: { + /** + * Format CSS, LESS, SCSS files, also the ` diff --git a/src/api/foo-alova.ts b/src/api/foo-alova.ts new file mode 100644 index 0000000..de35095 --- /dev/null +++ b/src/api/foo-alova.ts @@ -0,0 +1,17 @@ +import { API_DOMAINS, http } from '@/http/alova' + +export interface IFoo { + id: number + name: string +} + +export function foo() { + return http.Get('/foo', { + params: { + name: '菲鸽', + page: 1, + pageSize: 10, + }, + meta: { domain: API_DOMAINS.SECONDARY }, // 用于切换请求地址 + }) +} diff --git a/src/api/foo-vue-query.ts b/src/api/foo-vue-query.ts new file mode 100644 index 0000000..c7cbdd4 --- /dev/null +++ b/src/api/foo-vue-query.ts @@ -0,0 +1,11 @@ +import { queryOptions } from '@tanstack/vue-query' +import { getFooAPI } from './foo' + +export function getFooQueryOptions(name: string) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return getFooAPI(queryKey[1]) + }, + queryKey: ['getFoo', name], + }) +} diff --git a/src/api/foo.ts b/src/api/foo.ts new file mode 100644 index 0000000..cd8b188 --- /dev/null +++ b/src/api/foo.ts @@ -0,0 +1,43 @@ +import { http } from '@/http/http' + +export interface IFoo { + id: number + name: string +} + +export function foo() { + return http.Get('/foo', { + params: { + name: '菲鸽', + page: 1, + pageSize: 10, + }, + }) +} + +export interface IFooItem { + id: string + name: string +} + +/** GET 请求 */ +export function getFooAPI(name: string) { + return http.get('/foo', { name }) +} +/** GET 请求;支持 传递 header 的范例 */ +export function getFooAPI2(name: string) { + return http.get('/foo', { name }, { 'Content-Type-100': '100' }) +} + +/** POST 请求 */ +export function postFooAPI(name: string) { + return http.post('/foo', { name }) +} +/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */ +export function postFooAPI2(name: string) { + return http.post('/foo', { name }) +} +/** POST 请求;支持 传递 header 的范例 */ +export function postFooAPI3(name: string) { + return http.post('/foo', { name }, { name }, { 'Content-Type-100': '100' }) +} diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 0000000..67100c2 --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,87 @@ +import type { IAuthLoginRes, ICaptcha, IDoubleTokenRes, IUpdateInfo, IUpdatePassword, IUserInfoRes } from './types/login' +import { http } from '@/http/http' + +/** + * 登录表单 + */ +export interface ILoginForm { + username: string + password: string + code?: string + uuid?: string +} + +/** + * 获取验证码 + * @returns ICaptcha 验证码 + */ +export function getCode() { + return http.get('/user/getCode') +} + +/** + * 用户登录 + * @param loginForm 登录表单 + */ +export function login(loginForm: ILoginForm) { + return http.post('/auth/login', loginForm) +} + +/** + * 刷新token + * @param refreshToken 刷新token + */ +export function refreshToken(refreshToken: string) { + return http.post('/auth/refreshToken', { refreshToken }) +} + +/** + * 获取用户信息 + */ +export function getUserInfo() { + return http.get('/user/info') +} + +/** + * 退出登录 + */ +export function logout() { + return http.get('/auth/logout') +} + +/** + * 修改用户信息 + */ +export function updateInfo(data: IUpdateInfo) { + return http.post('/user/updateInfo', data) +} + +/** + * 修改用户密码 + */ +export function updateUserPassword(data: IUpdatePassword) { + return http.post('/user/updatePassword', data) +} + +/** + * 获取微信登录凭证 + * @returns Promise 包含微信登录凭证(code) + */ +export function getWxCode() { + return new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: res => resolve(res), + fail: err => reject(new Error(err)), + }) + }) +} + +/** + * 微信登录 + * @param params 微信登录参数,包含code + * @returns Promise 包含登录结果 + */ +export function wxLogin(data: { code: string }) { + return http.post('/auth/wxLogin', data) +} diff --git a/src/api/types/login.ts b/src/api/types/login.ts new file mode 100644 index 0000000..d703fd8 --- /dev/null +++ b/src/api/types/login.ts @@ -0,0 +1,97 @@ +// 认证模式类型 +export type AuthMode = 'single' | 'double' + +// 单Token响应类型 +export interface ISingleTokenRes { + token: string + expiresIn: number // 有效期(秒) +} + +// 双Token响应类型 +export interface IDoubleTokenRes { + accessToken: string + refreshToken: string + accessExpiresIn: number // 访问令牌有效期(秒) + refreshExpiresIn: number // 刷新令牌有效期(秒) +} + +/** + * 登录返回的信息,其实就是 token 信息 + */ +export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes + +/** + * 用户信息 + */ +export interface IUserInfoRes { + userId: number + username: string + nickname: string + avatar?: string + [key: string]: any // 允许其他扩展字段 +} + +// 认证存储数据结构 +export interface AuthStorage { + mode: AuthMode + tokens: ISingleTokenRes | IDoubleTokenRes + userInfo?: IUserInfoRes + loginTime: number // 登录时间戳 +} + +/** + * 获取验证码 + */ +export interface ICaptcha { + captchaEnabled: boolean + uuid: string + image: string +} +/** + * 上传成功的信息 + */ +export interface IUploadSuccessInfo { + fileId: number + originalName: string + fileName: string + storagePath: string + fileHash: string + fileType: string + fileBusinessType: string + fileSize: number +} +/** + * 更新用户信息 + */ +export interface IUpdateInfo { + id: number + name: string + sex: string +} +/** + * 更新用户信息 + */ +export interface IUpdatePassword { + id: number + oldPassword: string + newPassword: string + confirmPassword: string +} + +/** + * 判断是否为单Token响应 + * @param tokenRes 登录响应数据 + * @returns 是否为单Token响应 + */ +export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes { + return 'token' in tokenRes && !('refreshToken' in tokenRes) +} + +/** + * 判断是否为双Token响应 + * @param tokenRes 登录响应数据 + * @returns 是否为双Token响应 + */ +export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes { + return 'accessToken' in tokenRes && 'refreshToken' in tokenRes +} diff --git a/src/components/.gitkeep b/src/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/components/AncientButton.vue b/src/components/AncientButton.vue new file mode 100644 index 0000000..89d169f --- /dev/null +++ b/src/components/AncientButton.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/components/ZhuziCard.vue b/src/components/ZhuziCard.vue new file mode 100644 index 0000000..d4a39d9 --- /dev/null +++ b/src/components/ZhuziCard.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..c213701 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,38 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent<{}, {}, any> + export default component +} + +interface ImportMetaEnv { + /** 网站标题,应用名称 */ + readonly VITE_APP_TITLE: string + /** 服务端口号 */ + readonly VITE_SERVER_PORT: string + /** 后台接口地址 */ + readonly VITE_SERVER_BASEURL: string + /** H5是否需要代理 */ + readonly VITE_APP_PROXY_ENABLE: 'true' | 'false' + /** H5是否需要代理,需要的话有个前缀 */ + readonly VITE_APP_PROXY_PREFIX: string // 一般是/api + /** 后端是否有统一前缀 /api */ + readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false' + /** 认证模式,'single' | 'double' ==> 单token | 双token */ + readonly VITE_AUTH_MODE: 'single' | 'double' + /** 上传图片地址 */ + readonly VITE_UPLOAD_BASEURL: string + /** 是否清除console */ + readonly VITE_DELETE_CONSOLE: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare const __VITE_APP_PROXY__: 'true' | 'false' +declare const __UNI_PLATFORM__: 'app' | 'h5' | 'mp-alipay' | 'mp-baidu' | 'mp-kuaishou' | 'mp-lark' | 'mp-qq' | 'mp-tiktok' | 'mp-weixin' | 'mp-xiaochengxu' diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts new file mode 100644 index 0000000..017a710 --- /dev/null +++ b/src/hooks/useRequest.ts @@ -0,0 +1,51 @@ +import type { Ref } from 'vue' + +interface IUseRequestOptions { + /** 是否立即执行 */ + immediate?: boolean + /** 初始化数据 */ + initialData?: T +} + +interface IUseRequestReturn { + loading: Ref + error: Ref + data: Ref + run: () => Promise +} + +/** + * useRequest是一个定制化的请求钩子,用于处理异步请求和响应。 + * @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。 + * @param options 包含请求选项的对象 {immediate, initialData}。 + * @param options.immediate 是否立即执行请求,默认为false。 + * @param options.initialData 初始化数据,默认为undefined。 + * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。 + */ +export default function useRequest( + func: () => Promise>, + options: IUseRequestOptions = { immediate: false }, +): IUseRequestReturn { + const loading = ref(false) + const error = ref(false) + const data = ref(options.initialData) as Ref + const run = async () => { + loading.value = true + return func() + .then((res) => { + data.value = res.data + error.value = false + return data.value + }) + .catch((err) => { + error.value = err + throw err + }) + .finally(() => { + loading.value = false + }) + } + + options.immediate && run() + return { loading, error, data, run } +} diff --git a/src/hooks/useUpload.ts b/src/hooks/useUpload.ts new file mode 100644 index 0000000..3080d5a --- /dev/null +++ b/src/hooks/useUpload.ts @@ -0,0 +1,160 @@ +import { ref } from 'vue' +import { getEnvBaseUploadUrl } from '@/utils' + +const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}` + +type TfileType = 'image' | 'file' +type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*' +type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage + +interface TOptions { + formData?: Record + maxSize?: number + accept?: T extends 'image' ? TImage[] : TFile[] + fileType?: T + success?: (params: any) => void + error?: (err: any) => void +} + +export default function useUpload(options: TOptions = {} as TOptions) { + const { + formData = {}, + maxSize = 5 * 1024 * 1024, + accept = ['*'], + fileType = 'image', + success, + error: onError, + } = options + + const loading = ref(false) + const error = ref(null) + const data = ref(null) + + const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => { + if (size > maxSize) { + uni.showToast({ + title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`, + icon: 'none', + }) + return + } + + // const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase() + // const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension) + + // if (!isTypeValid) { + // uni.showToast({ + // title: `仅支持 ${accept.join(', ')} 格式的文件`, + // icon: 'none', + // }) + // return + // } + + loading.value = true + uploadFile({ + tempFilePath, + formData, + onSuccess: (res) => { + const { data: _data } = JSON.parse(res) + data.value = _data + // console.log('上传成功', res) + success?.(_data) + }, + onError: (err) => { + error.value = err + onError?.(err) + }, + onComplete: () => { + loading.value = false + }, + }) + } + + const run = () => { + // 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。 + // 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议 + const chooseFileOptions = { + count: 1, + success: (res: any) => { + console.log('File selected successfully:', res) + // 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]} + // h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]} + // h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"} + // App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]} + // App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976} + let tempFilePath = '' + let size = 0 + // #ifdef MP-WEIXIN + tempFilePath = res.tempFiles[0].tempFilePath + size = res.tempFiles[0].size + // #endif + // #ifndef MP-WEIXIN + tempFilePath = res.tempFilePaths[0] + size = res.tempFiles[0].size + // #endif + handleFileChoose({ tempFilePath, size }) + }, + fail: (err: any) => { + console.error('File selection failed:', err) + error.value = err + onError?.(err) + }, + } + + if (fileType === 'image') { + // #ifdef MP-WEIXIN + uni.chooseMedia({ + ...chooseFileOptions, + mediaType: ['image'], + }) + // #endif + + // #ifndef MP-WEIXIN + uni.chooseImage(chooseFileOptions) + // #endif + } + else { + uni.chooseFile({ + ...chooseFileOptions, + type: 'all', + }) + } + } + + return { loading, error, data, run } +} + +async function uploadFile({ + tempFilePath, + formData, + onSuccess, + onError, + onComplete, +}: { + tempFilePath: string + formData: Record + onSuccess: (data: any) => void + onError: (err: any) => void + onComplete: () => void +}) { + uni.uploadFile({ + url: VITE_UPLOAD_BASEURL, + filePath: tempFilePath, + name: 'file', + formData, + success: (uploadFileRes) => { + try { + const data = uploadFileRes.data + onSuccess(data) + } + catch (err) { + onError(err) + } + }, + fail: (err) => { + console.error('Upload failed:', err) + onError(err) + }, + complete: onComplete, + }) +} diff --git a/src/http/README.md b/src/http/README.md new file mode 100644 index 0000000..5bb5a4f --- /dev/null +++ b/src/http/README.md @@ -0,0 +1,13 @@ +# 请求库 + +目前unibest支持3种请求库: +- 菲鸽简单封装的 `简单版本http`,路径(src/http/http.ts),对应的示例在 src/api/foo.ts +- `alova 的 http`,路径(src/http/alova.ts),对应的示例在 src/api/foo-alova.ts +- `vue-query`, 路径(src/http/vue-query.ts), 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹 + +## 如何选择 +如果您以前用过 alova 或者 vue-query,可以优先使用您熟悉的。 +如果您的项目简单,简单版本的http 就够了,也不会增加包体积。(发版的时候可以去掉alova和vue-query,如果没有超过包体积,留着也无所谓 ^_^) + +## roadmap +菲鸽最近在优化脚手架,后续可以选择是否使用第三方的请求库,以及选择什么请求库。还在开发中,大概月底出来(8月31号)。 \ No newline at end of file diff --git a/src/http/alova.ts b/src/http/alova.ts new file mode 100644 index 0000000..696c795 --- /dev/null +++ b/src/http/alova.ts @@ -0,0 +1,117 @@ +import type { uniappRequestAdapter } from '@alova/adapter-uniapp' +import type { IResponse } from './types' +import AdapterUniapp from '@alova/adapter-uniapp' +import { createAlova } from 'alova' +import { createServerTokenAuthentication } from 'alova/client' +import VueHook from 'alova/vue' +import { LOGIN_PAGE } from '@/router/config' +import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum' + +// 配置动态Tag +export const API_DOMAINS = { + DEFAULT: import.meta.env.VITE_SERVER_BASEURL, + SECONDARY: import.meta.env.VITE_API_SECONDARY_URL, +} + +/** + * 创建请求实例 + */ +const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication< + typeof VueHook, + typeof uniappRequestAdapter +>({ + refreshTokenOnError: { + isExpired: (error) => { + return error.response?.status === ResultEnum.Unauthorized + }, + handler: async () => { + try { + // await authLogin(); + } + catch (error) { + // 切换到登录页 + await uni.reLaunch({ url: LOGIN_PAGE }) + throw error + } + }, + }, +}) + +/** + * alova 请求实例 + */ +const alovaInstance = createAlova({ + baseURL: import.meta.env.VITE_APP_PROXY_PREFIX, + ...AdapterUniapp(), + timeout: 5000, + statesHook: VueHook, + + beforeRequest: onAuthRequired((method) => { + // 设置默认 Content-Type + method.config.headers = { + ContentType: ContentTypeEnum.JSON, + Accept: 'application/json, text/plain, */*', + ...method.config.headers, + } + + const { config } = method + const ignoreAuth = !config.meta?.ignoreAuth + console.log('ignoreAuth===>', ignoreAuth) + // 处理认证信息 自行处理认证问题 + if (ignoreAuth) { + const token = 'getToken()' + if (!token) { + throw new Error('[请求错误]:未登录') + } + // method.config.headers.token = token; + } + + // 处理动态域名 + if (config.meta?.domain) { + method.baseURL = config.meta.domain + console.log('当前域名', method.baseURL) + } + }), + + responded: onResponseRefreshToken((response, method) => { + const { config } = method + const { requestType } = config + const { + statusCode, + data: rawData, + errMsg, + } = response as UniNamespace.RequestSuccessCallbackResult + + // 处理特殊请求类型(上传/下载) + if (requestType === 'upload' || requestType === 'download') { + return response + } + + // 处理 HTTP 状态码错误 + if (statusCode !== 200) { + const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]` + console.error('errorMessage===>', errorMessage) + uni.showToast({ + title: errorMessage, + icon: 'error', + }) + throw new Error(`${errorMessage}:${errMsg}`) + } + + // 处理业务逻辑错误 + const { code, message, data } = rawData as IResponse + if (code !== ResultEnum.Success) { + if (config.meta?.toast !== false) { + uni.showToast({ + title: message, + icon: 'none', + }) + } + throw new Error(`请求错误[${code}]:${message}`) + } + // 处理成功响应,返回业务数据 + return data + }), +}) + +export const http = alovaInstance diff --git a/src/http/http.ts b/src/http/http.ts new file mode 100644 index 0000000..1d3ce2e --- /dev/null +++ b/src/http/http.ts @@ -0,0 +1,182 @@ +import type { IDoubleTokenRes } from '@/api/types/login' +import type { CustomRequestOptions } from '@/http/types' +import { nextTick } from 'vue' +import { LOGIN_PAGE } from '@/router/config' +import { useTokenStore } from '@/store/token' +import { isDoubleTokenMode } from '@/utils' + +// 刷新 token 状态管理 +let refreshing = false // 防止重复刷新 token 标识 +let taskQueue: (() => void)[] = [] // 刷新 token 请求队列 + +export function http(options: CustomRequestOptions) { + // 1. 返回 Promise 对象 + return new Promise>((resolve, reject) => { + uni.request({ + ...options, + dataType: 'json', + // #ifndef MP-WEIXIN + responseType: 'json', + // #endif + // 响应成功 + success: async (res) => { + // 状态码 2xx,参考 axios 的设计 + if (res.statusCode >= 200 && res.statusCode < 300) { + // 2.1 提取核心数据 res.data + return resolve(res.data as IResData) + } + const resData: IResData = res.data as IResData + if ((res.statusCode === 401) || (resData.code === 401)) { + const tokenStore = useTokenStore() + if (!isDoubleTokenMode) { + // 未启用双token策略,清理用户信息,跳转到登录页 + tokenStore.logout() + uni.navigateTo({ url: LOGIN_PAGE }) + return reject(res) + } + /* -------- 无感刷新 token ----------- */ + const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {} + // token 失效的,且有刷新 token 的,才放到请求队列里 + if ((res.statusCode === 401 || resData.code === 401) && refreshToken) { + taskQueue.push(() => { + resolve(http(options)) + }) + } + // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求 + if ((res.statusCode === 401 || resData.code === 401) && refreshToken && !refreshing) { + refreshing = true + try { + // 发起刷新 token 请求(使用 store 的 refreshToken 方法) + await tokenStore.refreshToken() + // 刷新 token 成功 + refreshing = false + nextTick(() => { + // 关闭其他弹窗 + uni.hideToast() + uni.showToast({ + title: 'token 刷新成功', + icon: 'none', + }) + }) + // 将任务队列的所有任务重新请求 + taskQueue.forEach(task => task()) + } + catch (refreshErr) { + console.error('刷新 token 失败:', refreshErr) + refreshing = false + // 刷新 token 失败,跳转到登录页 + nextTick(() => { + // 关闭其他弹窗 + uni.hideToast() + uni.showToast({ + title: '登录已过期,请重新登录', + icon: 'none', + }) + }) + // 清除用户信息 + await tokenStore.logout() + // 跳转到登录页 + setTimeout(() => { + uni.navigateTo({ url: LOGIN_PAGE }) + }, 2000) + } + finally { + // 不管刷新 token 成功与否,都清空任务队列 + taskQueue = [] + } + } + } + else { + // 其他错误 -> 根据后端错误信息轻提示 + !options.hideErrorToast + && uni.showToast({ + icon: 'none', + title: (res.data as IResData).msg || '请求错误', + }) + reject(res) + } + }, + // 响应失败 + fail(err) { + uni.showToast({ + icon: 'none', + title: '网络错误,换个网络试试', + }) + reject(err) + }, + }) + }) +} + +/** + * GET 请求 + * @param url 后台地址 + * @param query 请求query参数 + * @param header 请求头,默认为json格式 + * @returns + */ +export function httpGet(url: string, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + method: 'GET', + header, + ...options, + }) +} + +/** + * POST 请求 + * @param url 后台地址 + * @param data 请求body参数 + * @param query 请求query参数,post请求也支持query,很多微信接口都需要 + * @param header 请求头,默认为json格式 + * @returns + */ +export function httpPost(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + data, + method: 'POST', + header, + ...options, + }) +} +/** + * PUT 请求 + */ +export function httpPut(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + data, + query, + method: 'PUT', + header, + ...options, + }) +} + +/** + * DELETE 请求(无请求体,仅 query) + */ +export function httpDelete(url: string, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + method: 'DELETE', + header, + ...options, + }) +} + +http.get = httpGet +http.post = httpPost +http.put = httpPut +http.delete = httpDelete + +// 支持与 alovaJS 类似的API调用 +http.Get = httpGet +http.Post = httpPost +http.Put = httpPut +http.Delete = httpDelete diff --git a/src/http/interceptor.ts b/src/http/interceptor.ts new file mode 100644 index 0000000..377302f --- /dev/null +++ b/src/http/interceptor.ts @@ -0,0 +1,66 @@ +import type { CustomRequestOptions } from '@/http/types' +import { useTokenStore } from '@/store' +import { getEnvBaseUrl } from '@/utils' +import { platform } from '@/utils/platform' +import { stringifyQuery } from './tools/queryString' + +// 请求基准地址 +const baseUrl = getEnvBaseUrl() + +// 拦截器配置 +const httpInterceptor = { + // 拦截前触发 + invoke(options: CustomRequestOptions) { + // 接口请求支持通过 query 参数配置 queryString + if (options.query) { + const queryStr = stringifyQuery(options.query) + if (options.url.includes('?')) { + options.url += `&${queryStr}` + } + else { + options.url += `?${queryStr}` + } + } + // 非 http 开头需拼接地址 + if (!options.url.startsWith('http')) { + // #ifdef H5 + // console.log(__VITE_APP_PROXY__) + if (JSON.parse(__VITE_APP_PROXY__)) { + // 自动拼接代理前缀 + options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url + } + else { + options.url = baseUrl + options.url + } + // #endif + // 非H5正常拼接 + // #ifndef H5 + options.url = baseUrl + options.url + // #endif + // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址 + } + // 1. 请求超时 + options.timeout = 60000 // 60s + // 2. (可选)添加小程序端请求头标识 + options.header = { + platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源 + ...options.header, + } + // 3. 添加 token 请求头标识 + const tokenStore = useTokenStore() + const token = tokenStore.validToken + + if (token) { + options.header.Authorization = `Bearer ${token}` + } + }, +} + +export const requestInterceptor = { + install() { + // 拦截 request 请求 + uni.addInterceptor('request', httpInterceptor) + // 拦截 uploadFile 文件上传 + uni.addInterceptor('uploadFile', httpInterceptor) + }, +} diff --git a/src/http/tools/enum.ts b/src/http/tools/enum.ts new file mode 100644 index 0000000..1868fe0 --- /dev/null +++ b/src/http/tools/enum.ts @@ -0,0 +1,66 @@ +export enum ResultEnum { + Success = 0, // 成功 + Error = 400, // 错误 + Unauthorized = 401, // 未授权 + Forbidden = 403, // 禁止访问(原为forbidden) + NotFound = 404, // 未找到(原为notFound) + MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed) + RequestTimeout = 408, // 请求超时(原为requestTimeout) + InternalServerError = 500, // 服务器错误(原为internalServerError) + NotImplemented = 501, // 未实现(原为notImplemented) + BadGateway = 502, // 网关错误(原为badGateway) + ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable) + GatewayTimeout = 504, // 网关超时(原为gatewayTimeout) + HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported) +} +export enum ContentTypeEnum { + JSON = 'application/json;charset=UTF-8', + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + FORM_DATA = 'multipart/form-data;charset=UTF-8', +} +/** + * 根据状态码,生成对应的错误信息 + * @param {number|string} status 状态码 + * @returns {string} 错误信息 + */ +export function ShowMessage(status: number | string): string { + let message: string + switch (status) { + case 400: + message = '请求错误(400)' + break + case 401: + message = '未授权,请重新登录(401)' + break + case 403: + message = '拒绝访问(403)' + break + case 404: + message = '请求出错(404)' + break + case 408: + message = '请求超时(408)' + break + case 500: + message = '服务器错误(500)' + break + case 501: + message = '服务未实现(501)' + break + case 502: + message = '网络错误(502)' + break + case 503: + message = '服务不可用(503)' + break + case 504: + message = '网络超时(504)' + break + case 505: + message = 'HTTP版本不受支持(505)' + break + default: + message = `连接出错(${status})!` + } + return `${message},请检查网络或联系管理员!` +} diff --git a/src/http/tools/queryString.ts b/src/http/tools/queryString.ts new file mode 100644 index 0000000..edf973e --- /dev/null +++ b/src/http/tools/queryString.ts @@ -0,0 +1,29 @@ +/** + * 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积 + * 支持基本类型值和数组,不支持嵌套对象 + * @param obj 要序列化的对象 + * @returns 序列化后的查询字符串 + */ +export function stringifyQuery(obj: Record): string { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) + return '' + + return Object.entries(obj) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + // 对键进行编码 + const encodedKey = encodeURIComponent(key) + + // 处理数组类型 + if (Array.isArray(value)) { + return value + .filter(item => item !== undefined && item !== null) + .map(item => `${encodedKey}=${encodeURIComponent(item)}`) + .join('&') + } + + // 处理基本类型 + return `${encodedKey}=${encodeURIComponent(value)}` + }) + .join('&') +} diff --git a/src/http/types.ts b/src/http/types.ts new file mode 100644 index 0000000..25cf472 --- /dev/null +++ b/src/http/types.ts @@ -0,0 +1,31 @@ +/** + * 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数 + */ +export type CustomRequestOptions = UniApp.RequestOptions & { + query?: Record + /** 出错时是否隐藏错误提示 */ + hideErrorToast?: boolean +} & IUniUploadFileOptions // 添加uni.uploadFile参数类型 + +// 通用响应格式 +export interface IResponse { + code: number | string + data: T + message: string + status: string | number +} + +// 分页请求参数 +export interface PageParams { + page: number + pageSize: number + [key: string]: any +} + +// 分页响应数据 +export interface PageResult { + list: T[] + total: number + page: number + pageSize: number +} diff --git a/src/http/vue-query.ts b/src/http/vue-query.ts new file mode 100644 index 0000000..31d1eb3 --- /dev/null +++ b/src/http/vue-query.ts @@ -0,0 +1,30 @@ +import type { CustomRequestOptions } from '@/http/types' +import { http } from './http' + +/* + * openapi-ts-request 工具的 request 跨客户端适配方法 + */ +export default function request( + url: string, + options: Omit & { + params?: Record + headers?: Record + }, +) { + const requestOptions = { + url, + ...options, + } + + if (options.params) { + requestOptions.query = requestOptions.params + delete requestOptions.params + } + + if (options.headers) { + requestOptions.header = options.headers + delete requestOptions.headers + } + + return http(requestOptions) +} diff --git a/src/layouts/default.vue b/src/layouts/default.vue new file mode 100644 index 0000000..88a55d4 --- /dev/null +++ b/src/layouts/default.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a357275 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,21 @@ +import { VueQueryPlugin } from '@tanstack/vue-query' +import { createSSRApp } from 'vue' +import App from './App.vue' +import { requestInterceptor } from './http/interceptor' +import { routeInterceptor } from './router/interceptor' + +import store from './store' +import '@/style/index.scss' +import 'virtual:uno.css' + +export function createApp() { + const app = createSSRApp(App) + app.use(store) + app.use(routeInterceptor) + app.use(requestInterceptor) + app.use(VueQueryPlugin) + + return { + app, + } +} diff --git a/src/mocks/article.ts b/src/mocks/article.ts new file mode 100644 index 0000000..a7a33ea --- /dev/null +++ b/src/mocks/article.ts @@ -0,0 +1,86 @@ +/** + * 文章内容相关Mock数据 + */ + +export interface Article { + id: string + title: string + content: string + coverImage: string + publishTime: string + author: string +} + +// 轮播图文章 +export const mockCarouselArticles: Article[] = [ + { + id: '1', + title: '朱子故里行:走进福建尤溪', + content: ` +
+

朱子故里:福建尤溪

+

福建尤溪,这片山清水秀的土地,孕育了一代大儒朱熹。公元1130年,朱熹就诞生在这里的南溪书院附近。

+ +

朱子文化的发源地

+

尤溪县是朱熹的诞生地,这里保存着丰富的朱子文化遗迹。朱子社、朱子祠、朱子码头等历史遗迹见证了这位大儒的成长轨迹。

+ +

朱子理学的启蒙

+

朱熹在尤溪度过了人生的前14年,这段时光为他日后的理学思想奠定了基础。山水之间的灵秀,培养了他对自然和人文的深刻理解。

+ +

如今的尤溪,依然保持着那份古朴与宁静,成为后人缅怀朱子、学习朱子文化的圣地。

+
+ `, + coverImage: '/static/images/article1.jpg', + publishTime: '2024-03-15', + author: '朱子文化研究院', + }, + { + id: '2', + title: '四书章句集注:朱子的不朽之作', + content: ` +
+

《四书章句集注》:理学经典

+

朱熹的《四书章句集注》是中国古代教育史上最重要的著作之一,影响了中国教育近700年。

+ +

四书的组成

+

四书包括《大学》、《中庸》、《论语》、《孟子》四部经典。朱熹将这四部著作编辑成册,并加以详细注解。

+ +

教育的里程碑

+

《四书章句集注》成为元、明、清三朝科举考试的标准教材,培养了无数读书人,传播了儒家思想。

+ +

现代价值

+

时至今日,《四书章句集注》仍然是学习儒家文化、理解中华传统文化的重要读本。

+
+ `, + coverImage: '/static/images/article2.jpg', + publishTime: '2024-03-12', + author: '教育研究所', + }, + { + id: '3', + title: '朱子教育思想对现代教育的启示', + content: ` +
+

朱子教育思想的现代意义

+

朱熹不仅是一位伟大的思想家,更是一位杰出的教育家。他的教育思想对现代教育仍有重要的指导意义。

+ +

因材施教

+

朱熹主张根据学生的不同特点进行教育,这与现代个性化教育理念不谋而合。

+ +

学思并重

+

"学而时习之"、"学而不思则罔,思而不学则殆",朱熹强调学习与思考的结合。

+ +

品德教育

+

朱熹认为教育的根本目的是培养品德高尚的人,这对现代素质教育具有重要启发。

+
+ `, + coverImage: '/static/images/article3.jpg', + publishTime: '2024-03-10', + author: '现代教育研究中心', + }, +] + +// 朱子介绍内容 +export const zhuziIntroduction = `朱熹(1130-1200),字元晦、仲晦,号晦庵、晦翁,别号紫阳,谥号"文",世称朱文公。祖居宋徽州婺源县(治所在今江西省婺源县),出生于南剑州尤溪县(治所在今福建省尤溪县),终老于建宁府建阳县(治所在今福建省南平市建阳区)。我国古代伟大的思想家、哲学家、教育家、文学家。在中国文化史上是与孔子并峙的两座高峰,素有"北孔南朱"之称,后世尊称为"朱子"。 + +朱子集南宋前儒学思想之大成,构建了"致广大,尽精微,综罗百代"的理学思想体系,影响了中国社会近千年。其所著《四书章句集注》《周易本义》《诗集传》等,为元明清科举考试的必读书目。朱子亦是唯一非孔子亲传弟子而入孔庙大成殿配享的先哲。以朱子理学为标志,中华文明进入新的千年发展期并延续至今。可以说,读懂了朱熹,就读懂了中华优秀传统文化。` diff --git a/src/mocks/auth.ts b/src/mocks/auth.ts new file mode 100644 index 0000000..b99234f --- /dev/null +++ b/src/mocks/auth.ts @@ -0,0 +1,61 @@ +/** + * 认证相关Mock数据 + */ + +// 学校数据 +export const mockSchools = [ + { id: 1, name: '第一小学' }, + { id: 2, name: '第二小学' }, + { id: 3, name: '第三小学' }, + { id: 4, name: '第四小学' }, + { id: 5, name: '第五小学' }, +] + +// 年级数据 +export const mockGrades = [ + { id: 1, name: '一年级' }, + { id: 2, name: '二年级' }, + { id: 3, name: '三年级' }, + { id: 4, name: '四年级' }, + { id: 5, name: '五年级' }, + { id: 6, name: '六年级' }, +] + +// 班级数据 +export const mockClasses = [ + { id: 1, name: '1班', gradeId: 1 }, + { id: 2, name: '2班', gradeId: 1 }, + { id: 3, name: '3班', gradeId: 1 }, + { id: 4, name: '1班', gradeId: 2 }, + { id: 5, name: '2班', gradeId: 2 }, +] + +// 用户注册响应 +export const mockRegisterResponse = { + success: true, + message: '注册成功', + data: { + userId: '123456', + phone: '13800138000', + }, +} + +// 用户登录响应 +export const mockLoginResponse = { + success: true, + message: '登录成功', + data: { + userId: '123456', + phone: '13800138000', + token: 'mock_token_123456', + hasChild: true, + childInfo: { + id: '789', + name: '小明', + schoolId: 1, + gradeId: 3, + classId: 5, + seatNumber: 15, + }, + }, +} diff --git a/src/mocks/index.ts b/src/mocks/index.ts new file mode 100644 index 0000000..9ab35e6 --- /dev/null +++ b/src/mocks/index.ts @@ -0,0 +1,9 @@ +/** + * Mock数据统一导出 + */ + +export * from './article' +export * from './auth' +export * from './quiz' +export * from './ranking' +export * from './user' diff --git a/src/mocks/quiz.ts b/src/mocks/quiz.ts new file mode 100644 index 0000000..ca6963c --- /dev/null +++ b/src/mocks/quiz.ts @@ -0,0 +1,248 @@ +/** + * 答题相关Mock数据 + */ + +export interface QuizQuestion { + id: string + type: 'single' | 'multiple' // 单选、多选 + title: string + options: Array<{ + id: string + text: string + }> + correctAnswers: string[] + score: number + difficulty: 'easy' | 'medium' | 'hard' +} + +// 家长题库(5道题) +export const mockParentQuestions: QuizQuestion[] = [ + { + id: 'p1', + type: 'single', + title: '朱熹字什么?', + options: [ + { id: 'a', text: '元晦' }, + { id: 'b', text: '仲晦' }, + { id: 'c', text: '晦庵' }, + { id: 'd', text: '以上都是' }, + ], + correctAnswers: ['d'], + score: 10, + difficulty: 'easy', + }, + { + id: 'p2', + type: 'single', + title: '朱熹被后世尊称为什么?', + options: [ + { id: 'a', text: '朱子' }, + { id: 'b', text: '朱文公' }, + { id: 'c', text: '紫阳先生' }, + { id: 'd', text: '以上都是' }, + ], + correctAnswers: ['d'], + score: 15, + difficulty: 'easy', + }, + { + id: 'p3', + type: 'single', + title: '朱熹的生卒年份是?', + options: [ + { id: 'a', text: '1130-1200' }, + { id: 'b', text: '1120-1190' }, + { id: 'c', text: '1140-1210' }, + { id: 'd', text: '1135-1205' }, + ], + correctAnswers: ['a'], + score: 20, + difficulty: 'medium', + }, + { + id: 'p4', + type: 'multiple', + title: '朱熹的主要著作有哪些?(多选)', + options: [ + { id: 'a', text: '四书章句集注' }, + { id: 'b', text: '周易本义' }, + { id: 'c', text: '诗集传' }, + { id: 'd', text: '论语' }, + ], + correctAnswers: ['a', 'b', 'c'], + score: 25, + difficulty: 'medium', + }, + { + id: 'p5', + type: 'single', + title: '"北孔南朱"中的"朱"指的是谁?', + options: [ + { id: 'a', text: '朱元璋' }, + { id: 'b', text: '朱熹' }, + { id: 'c', text: '朱舜水' }, + { id: 'd', text: '朱自清' }, + ], + correctAnswers: ['b'], + score: 30, + difficulty: 'hard', + }, +] + +// 学生题库(10道题) +export const mockStudentQuestions: QuizQuestion[] = [ + { + id: 's1', + type: 'single', + title: '朱熹是哪个朝代的人?', + options: [ + { id: 'a', text: '唐朝' }, + { id: 'b', text: '宋朝' }, + { id: 'c', text: '明朝' }, + { id: 'd', text: '清朝' }, + ], + correctAnswers: ['b'], + score: 10, + difficulty: 'easy', + }, + { + id: 's2', + type: 'single', + title: '朱熹出生在今天的哪个省?', + options: [ + { id: 'a', text: '江西省' }, + { id: 'b', text: '福建省' }, + { id: 'c', text: '浙江省' }, + { id: 'd', text: '安徽省' }, + ], + correctAnswers: ['b'], + score: 10, + difficulty: 'easy', + }, + { + id: 's3', + type: 'single', + title: '朱熹被称为什么?', + options: [ + { id: 'a', text: '诗仙' }, + { id: 'b', text: '诗圣' }, + { id: 'c', text: '朱子' }, + { id: 'd', text: '文豪' }, + ], + correctAnswers: ['c'], + score: 10, + difficulty: 'easy', + }, + { + id: 's4', + type: 'single', + title: '朱熹是伟大的什么家?', + options: [ + { id: 'a', text: '思想家' }, + { id: 'b', text: '教育家' }, + { id: 'c', text: '哲学家' }, + { id: 'd', text: '以上都是' }, + ], + correctAnswers: ['d'], + score: 15, + difficulty: 'easy', + }, + { + id: 's5', + type: 'single', + title: '"理学"的集大成者是谁?', + options: [ + { id: 'a', text: '孔子' }, + { id: 'b', text: '孟子' }, + { id: 'c', text: '朱熹' }, + { id: 'd', text: '老子' }, + ], + correctAnswers: ['c'], + score: 15, + difficulty: 'medium', + }, + { + id: 's6', + type: 'single', + title: '朱熹的思想体系被称为什么?', + options: [ + { id: 'a', text: '理学' }, + { id: 'b', text: '心学' }, + { id: 'c', text: '实学' }, + { id: 'd', text: '玄学' }, + ], + correctAnswers: ['a'], + score: 15, + difficulty: 'medium', + }, + { + id: 's7', + type: 'single', + title: '孔庙大成殿中,除了孔子的弟子外,还有一位入祀的先哲是?', + options: [ + { id: 'a', text: '孟子' }, + { id: 'b', text: '朱熹' }, + { id: 'c', text: '老子' }, + { id: 'd', text: '庄子' }, + ], + correctAnswers: ['b'], + score: 20, + difficulty: 'medium', + }, + { + id: 's8', + type: 'multiple', + title: '朱熹的别号有哪些?(多选)', + options: [ + { id: 'a', text: '晦庵' }, + { id: 'b', text: '晦翁' }, + { id: 'c', text: '紫阳' }, + { id: 'd', text: '东坡' }, + ], + correctAnswers: ['a', 'b', 'c'], + score: 20, + difficulty: 'hard', + }, + { + id: 's9', + type: 'single', + title: '朱熹的理学思想影响了中国社会多少年?', + options: [ + { id: 'a', text: '500年' }, + { id: 'b', text: '近千年' }, + { id: 'c', text: '200年' }, + { id: 'd', text: '100年' }, + ], + correctAnswers: ['b'], + score: 25, + difficulty: 'hard', + }, + { + id: 's10', + type: 'single', + title: '"读懂了朱熹,就读懂了____"', + options: [ + { id: 'a', text: '中国历史' }, + { id: 'b', text: '中华优秀传统文化' }, + { id: 'c', text: '宋朝文化' }, + { id: 'd', text: '理学思想' }, + ], + correctAnswers: ['b'], + score: 25, + difficulty: 'hard', + }, +] + +// 答题结果mock +export const mockQuizResult = { + totalScore: 85, + correctCount: 4, + totalCount: 5, + timeSpent: 120, // 秒 + achievement: '优秀', + ranking: { + class: 5, + grade: 12, + school: 35, + }, +} diff --git a/src/mocks/ranking.ts b/src/mocks/ranking.ts new file mode 100644 index 0000000..253b13b --- /dev/null +++ b/src/mocks/ranking.ts @@ -0,0 +1,91 @@ +/** + * 排行榜相关Mock数据 + */ + +export interface RankingItem { + rank: number + studentName: string + schoolName: string + gradeName: string + className: string + totalScore: number + monthScore: number + avatar?: string +} + +// 班级排行榜(前20名) +export const mockClassRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({ + rank: index + 1, + studentName: `同学${index + 1}`, + schoolName: '第一小学', + gradeName: '三年级', + className: '2班', + totalScore: 1000 - index * 20, + monthScore: 500 - index * 10, + avatar: `/static/images/avatar${(index % 5) + 1}.png`, +})) + +// 年级排行榜 +export const mockGradeRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({ + rank: index + 1, + studentName: `年级同学${index + 1}`, + schoolName: '第一小学', + gradeName: '三年级', + className: `${Math.floor(Math.random() * 5) + 1}班`, + totalScore: 1200 - index * 25, + monthScore: 600 - index * 12, + avatar: `/static/images/avatar${(index % 5) + 1}.png`, +})) + +// 全校排行榜 +export const mockSchoolRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({ + rank: index + 1, + studentName: `全校同学${index + 1}`, + schoolName: '第一小学', + gradeName: `${Math.floor(Math.random() * 6) + 1}年级`, + className: `${Math.floor(Math.random() * 5) + 1}班`, + totalScore: 1500 - index * 30, + monthScore: 750 - index * 15, + avatar: `/static/images/avatar${(index % 5) + 1}.png`, +})) + +// 全区排行榜 +export const mockDistrictRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({ + rank: index + 1, + studentName: `全区同学${index + 1}`, + schoolName: `第${Math.floor(Math.random() * 5) + 1}小学`, + gradeName: `${Math.floor(Math.random() * 6) + 1}年级`, + className: `${Math.floor(Math.random() * 5) + 1}班`, + totalScore: 1800 - index * 35, + monthScore: 900 - index * 18, + avatar: `/static/images/avatar${(index % 5) + 1}.png`, +})) + +// 全市排行榜 +export const mockCityRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({ + rank: index + 1, + studentName: `全市同学${index + 1}`, + schoolName: `第${Math.floor(Math.random() * 10) + 1}小学`, + gradeName: `${Math.floor(Math.random() * 6) + 1}年级`, + className: `${Math.floor(Math.random() * 5) + 1}班`, + totalScore: 2000 - index * 40, + monthScore: 1000 - index * 20, + avatar: `/static/images/avatar${(index % 5) + 1}.png`, +})) + +// 当前用户排名信息 +export const mockUserRankInfo = { + studentName: '小明', + schoolName: '第一小学', + gradeName: '三年级', + className: '2班', + totalScore: 850, + monthScore: 420, + rankings: { + class: { rank: 5, total: 35 }, + grade: { rank: 12, total: 180 }, + school: { rank: 35, total: 1200 }, + district: { rank: 120, total: 8500 }, + city: { rank: 560, total: 25000 }, + }, +} diff --git a/src/mocks/user.ts b/src/mocks/user.ts new file mode 100644 index 0000000..c4bbb11 --- /dev/null +++ b/src/mocks/user.ts @@ -0,0 +1,82 @@ +/** + * 用户相关Mock数据 + */ + +export interface QuizRecord { + id: string + date: string + type: 'parent' | 'student' // 家长答题或学生答题 + score: number + totalQuestions: number + correctAnswers: number + timeSpent: number // 秒 +} + +// 用户信息 +export const mockUserInfo = { + id: '123456', + phone: '13800138000', + avatar: '/static/images/default-avatar.png', + child: { + id: '789', + name: '小明', + schoolId: 1, + schoolName: '第一小学', + gradeId: 3, + gradeName: '三年级', + classId: 5, + className: '2班', + seatNumber: 15, + totalScore: 850, + monthScore: 420, + }, +} + +// 答题记录 +export const mockQuizRecords: QuizRecord[] = [ + { + id: 'r1', + date: '2024-03-15 10:30:00', + type: 'student', + score: 85, + totalQuestions: 10, + correctAnswers: 8, + timeSpent: 280, + }, + { + id: 'r2', + date: '2024-03-14 19:45:00', + type: 'parent', + score: 95, + totalQuestions: 5, + correctAnswers: 5, + timeSpent: 120, + }, + { + id: 'r3', + date: '2024-03-13 20:15:00', + type: 'student', + score: 70, + totalQuestions: 10, + correctAnswers: 7, + timeSpent: 300, + }, + { + id: 'r4', + date: '2024-03-12 18:20:00', + type: 'parent', + score: 80, + totalQuestions: 5, + correctAnswers: 4, + timeSpent: 135, + }, + { + id: 'r5', + date: '2024-03-11 16:50:00', + type: 'student', + score: 90, + totalQuestions: 10, + correctAnswers: 9, + timeSpent: 260, + }, +] diff --git a/src/pages-sub/demo/components/request.vue b/src/pages-sub/demo/components/request.vue new file mode 100644 index 0000000..7d7bff1 --- /dev/null +++ b/src/pages-sub/demo/components/request.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/pages-sub/demo/index.vue b/src/pages-sub/demo/index.vue new file mode 100644 index 0000000..bffc26f --- /dev/null +++ b/src/pages-sub/demo/index.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/pages/about/about.vue b/src/pages/about/about.vue new file mode 100644 index 0000000..2c8f68d --- /dev/null +++ b/src/pages/about/about.vue @@ -0,0 +1,149 @@ + + + diff --git a/src/pages/about/alova.vue b/src/pages/about/alova.vue new file mode 100644 index 0000000..4b0c43f --- /dev/null +++ b/src/pages/about/alova.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/pages/about/components/VBindCss.vue b/src/pages/about/components/VBindCss.vue new file mode 100644 index 0000000..bc5a048 --- /dev/null +++ b/src/pages/about/components/VBindCss.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/pages/about/components/request.vue b/src/pages/about/components/request.vue new file mode 100644 index 0000000..ac1b7a3 --- /dev/null +++ b/src/pages/about/components/request.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/pages/about/vue-query.vue b/src/pages/about/vue-query.vue new file mode 100644 index 0000000..6ffa94d --- /dev/null +++ b/src/pages/about/vue-query.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/pages/article/detail.vue b/src/pages/article/detail.vue new file mode 100644 index 0000000..fd506e1 --- /dev/null +++ b/src/pages/article/detail.vue @@ -0,0 +1,257 @@ + +{ + "style": { + "navigationBarTitleText": "文章详情", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/pages/auth/bind-child.vue b/src/pages/auth/bind-child.vue new file mode 100644 index 0000000..d9f0fb4 --- /dev/null +++ b/src/pages/auth/bind-child.vue @@ -0,0 +1,483 @@ + +{ + "style": { + "navigationBarTitleText": "绑定孩子信息", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/pages/auth/login.vue b/src/pages/auth/login.vue new file mode 100644 index 0000000..225502e --- /dev/null +++ b/src/pages/auth/login.vue @@ -0,0 +1,265 @@ + +{ + "style": { + "navigationStyle": "custom" + } +} + + + + + + + diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue new file mode 100644 index 0000000..6664902 --- /dev/null +++ b/src/pages/index/index.vue @@ -0,0 +1,692 @@ + +{ + "style": { + "navigationStyle": "custom" + } +} + + + + + + + diff --git a/src/pages/login/README.md b/src/pages/login/README.md new file mode 100644 index 0000000..c851a76 --- /dev/null +++ b/src/pages/login/README.md @@ -0,0 +1,20 @@ +# 登录页 +需要输入账号、密码/验证码的登录页。 + +## 适用性 + +本页面主要用于 `h5` 和 `APP`。 + +小程序通常有平台的登录方式 `uni.login` 通常用不到登录页,所以不适用于 `小程序`。(即默认情况下,小程序环境是不会走登录拦截逻辑的。) + +但是如果您的小程序也需要现实的 `登录页` 那也是可以使用的。 + +在 `src/router/config.ts` 中有一个变量 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在小程序中使用 `H5的登录页`。 + +更多信息请看 `src/router` 文件夹的内容。 + +## 登录跳转 + +目前登录的跳转逻辑主要在 `src/router/interceptor.ts` 和 `src/pages/login/login.vue` 里面,默认会在登录后自动重定向到来源/配置的页面。 + +如果与您的业务不符,您可以自行修改。 diff --git a/src/pages/login/login.vue b/src/pages/login/login.vue new file mode 100644 index 0000000..e68ae03 --- /dev/null +++ b/src/pages/login/login.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/src/pages/login/register.vue b/src/pages/login/register.vue new file mode 100644 index 0000000..e9809f0 --- /dev/null +++ b/src/pages/login/register.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/src/pages/me/me.vue b/src/pages/me/me.vue new file mode 100644 index 0000000..5dbbe37 --- /dev/null +++ b/src/pages/me/me.vue @@ -0,0 +1,515 @@ + +{ + "style": { + "navigationBarTitleText": "我的", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/pages/me/quiz-records.vue b/src/pages/me/quiz-records.vue new file mode 100644 index 0000000..6c38bde --- /dev/null +++ b/src/pages/me/quiz-records.vue @@ -0,0 +1,576 @@ + +{ + "style": { + "navigationBarTitleText": "答题记录", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/pages/quiz/parent-quiz.vue b/src/pages/quiz/parent-quiz.vue new file mode 100644 index 0000000..49ae9f7 --- /dev/null +++ b/src/pages/quiz/parent-quiz.vue @@ -0,0 +1,614 @@ + +{ + "style": { + "navigationBarTitleText": "家长答题", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/pages/quiz/result.vue b/src/pages/quiz/result.vue new file mode 100644 index 0000000..e82b9da --- /dev/null +++ b/src/pages/quiz/result.vue @@ -0,0 +1,680 @@ + +{ + "style": { + "navigationBarTitleText": "答题结果", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/pages/quiz/student-quiz.vue b/src/pages/quiz/student-quiz.vue new file mode 100644 index 0000000..f511834 --- /dev/null +++ b/src/pages/quiz/student-quiz.vue @@ -0,0 +1,614 @@ + +{ + "style": { + "navigationBarTitleText": "学生答题", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/pages/ranking/ranking.vue b/src/pages/ranking/ranking.vue new file mode 100644 index 0000000..9e2a797 --- /dev/null +++ b/src/pages/ranking/ranking.vue @@ -0,0 +1,664 @@ + +{ + "style": { + "navigationBarTitleText": "排行榜", + "navigationBarBackgroundColor": "#2D5E3E", + "navigationBarTextStyle": "white" + } +} + + + + + + + diff --git a/src/router/README.md b/src/router/README.md new file mode 100644 index 0000000..60e3084 --- /dev/null +++ b/src/router/README.md @@ -0,0 +1,55 @@ +# 登录 说明 + +## 登录 2种策略 +- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN +- 默认需要登录策略: DEFAULT_NEED_LOGIN + +### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN +进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。 + +比如大部分2C的应用,美团、今日头条、抖音等,都可以直接浏览,只有点赞、评论、分享等操作或者去特殊页面(比如个人中心),才需要登录。 + +### 默认需要登录策略: DEFAULT_NEED_LOGIN + +进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。 + +比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。 + +### EXCLUDE_LOGIN_PATH_LIST +`EXCLUDE_LOGIN_PATH_LIST` 表示排除的路由列表。 + +在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才需要登录,相当于黑名单。 + +在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才不需要登录,相当于白名单。 + +### excludeLoginPath +definePage 中可以通过 `excludeLoginPath` 来配置路由是否需要登录。(类似过去的 needLogin 的功能) + +```ts +definePage({ + style: { + navigationBarTitleText: '关于', + }, + // 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹 + excludeLoginPath: true, + // 角色授权(可选):如果需要根据角色授权,就配置这个 + roleAuth: { + field: 'role', + value: 'admin', + redirect: '/pages/auth/403', + }, +}) +``` + +## 登录注册页路由 + +登录页 `login.vue` 对应路由是 `/pages/login/login`. +注册页 `register.vue` 对应路由是 `/pages/login/register`. + +## 登录注册页适用性 + +登录注册页主要适用于 `h5` 和 `App`,默认不适用于 `小程序`,因为 `小程序` 通常会使用平台提供的快捷登录。 + +特殊情况例外,如业务需要跨平台复用登录注册页时,也可以用在 `小程序` 上,所以主要还是看业务需求。 + +通过一个参数 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在 `小程序` 中使用 `H5登录页` 的登录逻辑。 diff --git a/src/router/config.ts b/src/router/config.ts new file mode 100644 index 0000000..c5ccf1d --- /dev/null +++ b/src/router/config.ts @@ -0,0 +1,29 @@ +import { getAllPages } from '@/utils' + +export const LOGIN_STRATEGY_MAP = { + DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP + DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录 +} +// TODO: 1/3 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问 +export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN +export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN + +export const LOGIN_PAGE = '/pages/login/login' +export const REGISTER_PAGE = '/pages/login/register' + +export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE] + +// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同 +export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path) + +// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表 +// TODO: 2/3 在 definePage 配置 excludeLoginPath,或者在下面配置 EXCLUDE_LOGIN_PATH_LIST +export const EXCLUDE_LOGIN_PATH_LIST = [ + '/pages/xxx/index', + ...excludeLoginPathList, // 都是以 / 开头的 path +] + +// 在小程序里面是否使用H5的登录页,默认为 false +// 如果为 true 则复用 h5 的登录逻辑 +// TODO: 3/3 确定自己的登录页是否需要在小程序里面使用 +export const LOGIN_PAGE_ENABLE_IN_MP = false diff --git a/src/router/interceptor.ts b/src/router/interceptor.ts new file mode 100644 index 0000000..f998261 --- /dev/null +++ b/src/router/interceptor.ts @@ -0,0 +1,120 @@ +import { isMp } from '@uni-helper/uni-env' +/** + * by 菲鸽 on 2025-08-19 + * 路由拦截,通常也是登录拦截 + * 黑白名单的配置,请看 config.ts 文件, EXCLUDE_LOGIN_PATH_LIST + */ +import { useTokenStore } from '@/store/token' +import { isPageTabbar, tabbarStore } from '@/tabbar/store' +import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index' +import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP } from './config' + +export const FG_LOG_ENABLE = false +export function judgeIsExcludePath(path: string) { + const isDev = import.meta.env.DEV + if (!isDev) { + return EXCLUDE_LOGIN_PATH_LIST.includes(path) + } + const allExcludeLoginPages = getAllPages('excludeLoginPath') // dev 环境下,需要每次都重新获取,否则新配置就不会生效 + return EXCLUDE_LOGIN_PATH_LIST.includes(path) || (isDev && allExcludeLoginPages.some(page => page.path === path)) +} + +export const navigateToInterceptor = { + // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同 + // 增加对相对路径的处理,BY 网友 @ideal + invoke({ url, query }: { url: string, query?: Record }) { + if (url === undefined) { + return + } + let { path, query: _query } = parseUrlToObj(url) + + FG_LOG_ENABLE && console.log('\n\n路由拦截器:-------------------------------------') + FG_LOG_ENABLE && console.log('路由拦截器 1: url->', url, ', query ->', query) + const myQuery = { ..._query, ...query } + // /pages/route-interceptor/index?name=feige&age=30 + FG_LOG_ENABLE && console.log('路由拦截器 2: path->', path, ', _query ->', _query) + FG_LOG_ENABLE && console.log('路由拦截器 3: myQuery ->', myQuery) + + // 处理相对路径 + if (!path.startsWith('/')) { + const currentPath = getLastPage()?.route || '' + const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}` + const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/')) + path = `${baseDir}/${path}` + } + + // 处理直接进入路由非首页时,tabbarIndex 不正确的问题 + tabbarStore.setAutoCurIdx(path) + + // 小程序里面使用平台自带的登录,则不走下面的逻辑 + if (isMp && LOGIN_PAGE_ENABLE_IN_MP) { + return true // 明确表示允许路由继续执行 + } + + const tokenStore = useTokenStore() + FG_LOG_ENABLE && console.log('tokenStore.hasLogin:', tokenStore.hasLogin) + + // 不管黑白名单,登录了就直接去吧(但是当前不能是登录页) + if (tokenStore.hasLogin) { + if (path !== LOGIN_PAGE) { + return true // 明确表示允许路由继续执行 + } + else { + console.log('已经登录,但是还在登录页', myQuery.redirect) + const url = myQuery.redirect || HOME_PAGE + if (isPageTabbar(url)) { + uni.switchTab({ url }) + } + else { + uni.navigateTo({ url }) + } + return false // 明确表示阻止原路由继续执行 + } + } + let fullPath = path + + if (Object.keys(myQuery).length) { + fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}` + } + const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}` + + // #region 1/2 默认需要登录的情况(白名单策略) --------------------------- + if (isNeedLoginMode) { + // 需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示白名单,可以直接通过 + if (judgeIsExcludePath(path)) { + return true // 明确表示允许路由继续执行 + } + // 否则需要重定向到登录页 + else { + if (path === LOGIN_PAGE) { + return true // 明确表示允许路由继续执行 + } + FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl) + uni.navigateTo({ url: redirectUrl }) + return false // 明确表示阻止原路由继续执行 + } + } + // #endregion 1/2 默认需要登录的情况(白名单策略) --------------------------- + + // #region 2/2 默认不需要登录的情况(黑名单策略) --------------------------- + else { + // 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页 + if (judgeIsExcludePath(path)) { + FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl) + uni.navigateTo({ url: redirectUrl }) + return false // 修改为false,阻止原路由继续执行 + } + } + // #endregion 2/2 默认不需要登录的情况(黑名单策略) --------------------------- + return true // 明确表示允许路由继续执行 + }, +} + +export const routeInterceptor = { + install() { + uni.addInterceptor('navigateTo', navigateToInterceptor) + uni.addInterceptor('reLaunch', navigateToInterceptor) + uni.addInterceptor('redirectTo', navigateToInterceptor) + uni.addInterceptor('switchTab', navigateToInterceptor) + }, +} diff --git a/src/service/displayEnumLabel.ts b/src/service/displayEnumLabel.ts new file mode 100644 index 0000000..04b1487 --- /dev/null +++ b/src/service/displayEnumLabel.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// @ts-ignore +import * as API from './types'; + +export function displayStatusEnum(field: API.StatusEnum) { + return { available: 'available', pending: 'pending', sold: 'sold' }[field]; +} + +export function displayStatusEnum2(field: API.StatusEnum2) { + return { placed: 'placed', approved: 'approved', delivered: 'delivered' }[ + field + ]; +} diff --git a/src/service/index.ts b/src/service/index.ts new file mode 100644 index 0000000..45b6e53 --- /dev/null +++ b/src/service/index.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +// @ts-ignore +export * from './types'; +export * from './displayEnumLabel'; + +export * from './pet'; +export * from './pet.vuequery'; +export * from './store'; +export * from './store.vuequery'; +export * from './user'; +export * from './user.vuequery'; diff --git a/src/service/pet.ts b/src/service/pet.ts new file mode 100644 index 0000000..1fc0a92 --- /dev/null +++ b/src/service/pet.ts @@ -0,0 +1,185 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/http/vue-query'; +import type { CustomRequestOptions } from '@/http/types'; + +import * as API from './types'; + +/** Update an existing pet PUT /pet */ +export async function petUsingPut({ + body, + options, +}: { + body: API.Pet; + options?: CustomRequestOptions; +}) { + return request('/pet', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Add a new pet to the store POST /pet */ +export async function petUsingPost({ + body, + options, +}: { + body: API.Pet; + options?: CustomRequestOptions; +}) { + return request('/pet', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Find pet by ID Returns a single pet GET /pet/${param0} */ +export async function petPetIdUsingGet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petPetIdUsingGetParams; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + + return request(`/pet/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Updates a pet in the store with form data POST /pet/${param0} */ +export async function petPetIdUsingPost({ + params, + body, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petPetIdUsingPostParams; + body: API.PetPetIdUsingPostBody; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + + return request(`/pet/${param0}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + params: { ...queryParams }, + data: body, + ...(options || {}), + }); +} + +/** Deletes a pet DELETE /pet/${param0} */ +export async function petPetIdUsingDelete({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petPetIdUsingDeleteParams; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + + return request(`/pet/${param0}`, { + method: 'DELETE', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** uploads an image POST /pet/${param0}/uploadImage */ +export async function petPetIdUploadImageUsingPost({ + params, + body, + file, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petPetIdUploadImageUsingPostParams; + body: API.PetPetIdUploadImageUsingPostBody; + file?: File; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + const formData = new FormData(); + + if (file) { + formData.append('file', file); + } + + Object.keys(body).forEach((ele) => { + const item = (body as { [key: string]: any })[ele]; + + if (item !== undefined && item !== null) { + if (typeof item === 'object' && !(item instanceof File)) { + if (item instanceof Array) { + item.forEach((f) => formData.append(ele, f || '')); + } else { + formData.append(ele, JSON.stringify(item)); + } + } else { + formData.append(ele, item); + } + } + }); + + return request(`/pet/${param0}/uploadImage`, { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + params: { ...queryParams }, + data: formData, + ...(options || {}), + }); +} + +/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */ +export async function petFindByStatusUsingGet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petFindByStatusUsingGetParams; + options?: CustomRequestOptions; +}) { + return request('/pet/findByStatus', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + +/** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */ +export async function petFindByTagsUsingGet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petFindByTagsUsingGetParams; + options?: CustomRequestOptions; +}) { + return request('/pet/findByTags', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} diff --git a/src/service/pet.vuequery.ts b/src/service/pet.vuequery.ts new file mode 100644 index 0000000..a2369ea --- /dev/null +++ b/src/service/pet.vuequery.ts @@ -0,0 +1,151 @@ +/* eslint-disable */ +// @ts-ignore +import { queryOptions, useMutation } from '@tanstack/vue-query'; +import type { DefaultError } from '@tanstack/vue-query'; +import request from '@/http/vue-query'; +import type { CustomRequestOptions } from '@/http/types'; + +import * as apis from './pet'; +import * as API from './types'; + +/** Update an existing pet PUT /pet */ +export function usePetUsingPutMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.petUsingPut, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Add a new pet to the store POST /pet */ +export function usePetUsingPostMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.petUsingPost, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Find pet by ID Returns a single pet GET /pet/${param0} */ +export function petPetIdUsingGetQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petPetIdUsingGetParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.petPetIdUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['petPetIdUsingGet', options], + }); +} + +/** Updates a pet in the store with form data POST /pet/${param0} */ +export function usePetPetIdUsingPostMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.petPetIdUsingPost, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Deletes a pet DELETE /pet/${param0} */ +export function usePetPetIdUsingDeleteMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.petPetIdUsingDelete, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** uploads an image POST /pet/${param0}/uploadImage */ +export function usePetPetIdUploadImageUsingPostMutation(options?: { + onSuccess?: (value?: API.ApiResponse) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.petPetIdUploadImageUsingPost, + onSuccess(data: API.ApiResponse) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */ +export function petFindByStatusUsingGetQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petFindByStatusUsingGetParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.petFindByStatusUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['petFindByStatusUsingGet', options], + }); +} + +/** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */ +export function petFindByTagsUsingGetQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.petFindByTagsUsingGetParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.petFindByTagsUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['petFindByTagsUsingGet', options], + }); +} diff --git a/src/service/store.ts b/src/service/store.ts new file mode 100644 index 0000000..cc272f3 --- /dev/null +++ b/src/service/store.ts @@ -0,0 +1,72 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/http/vue-query'; +import type { CustomRequestOptions } from '@/http/types'; + +import * as API from './types'; + +/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */ +export async function storeInventoryUsingGet({ + options, +}: { + options?: CustomRequestOptions; +}) { + return request>('/store/inventory', { + method: 'GET', + ...(options || {}), + }); +} + +/** Place an order for a pet POST /store/order */ +export async function storeOrderUsingPost({ + body, + options, +}: { + body: API.Order; + options?: CustomRequestOptions; +}) { + return request('/store/order', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */ +export async function storeOrderOrderIdUsingGet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.storeOrderOrderIdUsingGetParams; + options?: CustomRequestOptions; +}) { + const { orderId: param0, ...queryParams } = params; + + return request(`/store/order/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */ +export async function storeOrderOrderIdUsingDelete({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.storeOrderOrderIdUsingDeleteParams; + options?: CustomRequestOptions; +}) { + const { orderId: param0, ...queryParams } = params; + + return request(`/store/order/${param0}`, { + method: 'DELETE', + params: { ...queryParams }, + ...(options || {}), + }); +} diff --git a/src/service/store.vuequery.ts b/src/service/store.vuequery.ts new file mode 100644 index 0000000..2f95787 --- /dev/null +++ b/src/service/store.vuequery.ts @@ -0,0 +1,75 @@ +/* eslint-disable */ +// @ts-ignore +import { queryOptions, useMutation } from '@tanstack/vue-query'; +import type { DefaultError } from '@tanstack/vue-query'; +import request from '@/http/vue-query'; +import type { CustomRequestOptions } from '@/http/types'; + +import * as apis from './store'; +import * as API from './types'; + +/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */ +export function storeInventoryUsingGetQueryOptions(options: { + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.storeInventoryUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['storeInventoryUsingGet', options], + }); +} + +/** Place an order for a pet POST /store/order */ +export function useStoreOrderUsingPostMutation(options?: { + onSuccess?: (value?: API.Order) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.storeOrderUsingPost, + onSuccess(data: API.Order) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */ +export function storeOrderOrderIdUsingGetQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.storeOrderOrderIdUsingGetParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.storeOrderOrderIdUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['storeOrderOrderIdUsingGet', options], + }); +} + +/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */ +export function useStoreOrderOrderIdUsingDeleteMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.storeOrderOrderIdUsingDelete, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} diff --git a/src/service/types.ts b/src/service/types.ts new file mode 100644 index 0000000..800530e --- /dev/null +++ b/src/service/types.ts @@ -0,0 +1,146 @@ +/* eslint-disable */ +// @ts-ignore + +export type ApiResponse = { + code?: number; + type?: string; + message?: string; +}; + +export type Category = { + id?: number; + name?: string; +}; + +export type Order = { + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + /** Order Status */ + status?: 'placed' | 'approved' | 'delivered'; + complete?: boolean; +}; + +export type Pet = { + id?: number; + category?: Category; + name: string; + photoUrls: string[]; + tags?: Tag[]; + /** pet status in the store */ + status?: 'available' | 'pending' | 'sold'; +}; + +export type petFindByStatusUsingGetParams = { + /** Status values that need to be considered for filter */ + status: ('available' | 'pending' | 'sold')[]; +}; + +export type petFindByTagsUsingGetParams = { + /** Tags to filter by */ + tags: string[]; +}; + +export type PetPetIdUploadImageUsingPostBody = { + /** Additional data to pass to server */ + additionalMetadata?: string; + /** file to upload */ + file?: string; +}; + +export type petPetIdUploadImageUsingPostParams = { + /** ID of pet to update */ + petId: number; +}; + +export type petPetIdUsingDeleteParams = { + /** Pet id to delete */ + petId: number; +}; + +export type petPetIdUsingGetParams = { + /** ID of pet to return */ + petId: number; +}; + +export type PetPetIdUsingPostBody = { + /** Updated name of the pet */ + name?: string; + /** Updated status of the pet */ + status?: string; +}; + +export type petPetIdUsingPostParams = { + /** ID of pet that needs to be updated */ + petId: number; +}; + +export enum StatusEnum { + 'available' = 'available', + 'pending' = 'pending', + 'sold' = 'sold', +} + +export type IStatusEnum = keyof typeof StatusEnum; + +export enum StatusEnum2 { + 'placed' = 'placed', + 'approved' = 'approved', + 'delivered' = 'delivered', +} + +export type IStatusEnum2 = keyof typeof StatusEnum2; + +export type storeOrderOrderIdUsingDeleteParams = { + /** ID of the order that needs to be deleted */ + orderId: number; +}; + +export type storeOrderOrderIdUsingGetParams = { + /** ID of pet that needs to be fetched */ + orderId: number; +}; + +export type Tag = { + id?: number; + name?: string; +}; + +export type User = { + id?: number; + username?: string; + firstName?: string; + lastName?: string; + email?: string; + password?: string; + phone?: string; + /** User Status */ + userStatus?: number; +}; + +export type UserCreateWithArrayUsingPostBody = User[]; + +export type UserCreateWithListUsingPostBody = User[]; + +export type userLoginUsingGetParams = { + /** The user name for login */ + username: string; + /** The password for login in clear text */ + password: string; +}; + +export type userUsernameUsingDeleteParams = { + /** The name that needs to be deleted */ + username: string; +}; + +export type userUsernameUsingGetParams = { + /** The name that needs to be fetched. Use user1 for testing. */ + username: string; +}; + +export type userUsernameUsingPutParams = { + /** name that need to be updated */ + username: string; +}; diff --git a/src/service/user.ts b/src/service/user.ts new file mode 100644 index 0000000..f3b2b78 --- /dev/null +++ b/src/service/user.ts @@ -0,0 +1,150 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/http/vue-query'; +import type { CustomRequestOptions } from '@/http/types'; + +import * as API from './types'; + +/** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */ +export async function userUsingPost({ + body, + options, +}: { + body: API.User; + options?: CustomRequestOptions; +}) { + return request('/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Get user by user name GET /user/${param0} */ +export async function userUsernameUsingGet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.userUsernameUsingGetParams; + options?: CustomRequestOptions; +}) { + const { username: param0, ...queryParams } = params; + + return request(`/user/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Updated user This can only be done by the logged in user. PUT /user/${param0} */ +export async function userUsernameUsingPut({ + params, + body, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.userUsernameUsingPutParams; + body: API.User; + options?: CustomRequestOptions; +}) { + const { username: param0, ...queryParams } = params; + + return request(`/user/${param0}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + params: { ...queryParams }, + data: body, + ...(options || {}), + }); +} + +/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */ +export async function userUsernameUsingDelete({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.userUsernameUsingDeleteParams; + options?: CustomRequestOptions; +}) { + const { username: param0, ...queryParams } = params; + + return request(`/user/${param0}`, { + method: 'DELETE', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */ +export async function userCreateWithArrayUsingPost({ + body, + options, +}: { + body: API.UserCreateWithArrayUsingPostBody; + options?: CustomRequestOptions; +}) { + return request('/user/createWithArray', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */ +export async function userCreateWithListUsingPost({ + body, + options, +}: { + body: API.UserCreateWithListUsingPostBody; + options?: CustomRequestOptions; +}) { + return request('/user/createWithList', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Logs user into the system GET /auth/login */ +export async function userLoginUsingGet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.userLoginUsingGetParams; + options?: CustomRequestOptions; +}) { + return request('/auth/login', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + +/** Logs out current logged in user session 返回值: successful operation GET /user/logout */ +export async function userLogoutUsingGet({ + options, +}: { + options?: CustomRequestOptions; +}) { + return request('/user/logout', { + method: 'GET', + ...(options || {}), + }); +} diff --git a/src/service/user.vuequery.ts b/src/service/user.vuequery.ts new file mode 100644 index 0000000..6e5851f --- /dev/null +++ b/src/service/user.vuequery.ts @@ -0,0 +1,149 @@ +/* eslint-disable */ +// @ts-ignore +import { queryOptions, useMutation } from '@tanstack/vue-query'; +import type { DefaultError } from '@tanstack/vue-query'; +import request from '@/http/vue-query'; +import type { CustomRequestOptions } from '@/http/types'; + +import * as apis from './user'; +import * as API from './types'; + +/** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */ +export function useUserUsingPostMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.userUsingPost, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Get user by user name GET /user/${param0} */ +export function userUsernameUsingGetQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.userUsernameUsingGetParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.userUsernameUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['userUsernameUsingGet', options], + }); +} + +/** Updated user This can only be done by the logged in user. PUT /user/${param0} */ +export function useUserUsernameUsingPutMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.userUsernameUsingPut, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */ +export function useUserUsernameUsingDeleteMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.userUsernameUsingDelete, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */ +export function useUserCreateWithArrayUsingPostMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.userCreateWithArrayUsingPost, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */ +export function useUserCreateWithListUsingPostMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.userCreateWithListUsingPost, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Logs user into the system GET /auth/login */ +export function userLoginUsingGetQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.userLoginUsingGetParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.userLoginUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['userLoginUsingGet', options], + }); +} + +/** Logs out current logged in user session 返回值: successful operation GET /user/logout */ +export function userLogoutUsingGetQueryOptions(options: { + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.userLogoutUsingGet(queryKey[1] as typeof options); + }, + queryKey: ['userLogoutUsingGet', options], + }); +} diff --git a/src/static/app/icons/1024x1024.png b/src/static/app/icons/1024x1024.png new file mode 100644 index 0000000..08dbd5f Binary files /dev/null and b/src/static/app/icons/1024x1024.png differ diff --git a/src/static/app/icons/120x120.png b/src/static/app/icons/120x120.png new file mode 100644 index 0000000..718ca79 Binary files /dev/null and b/src/static/app/icons/120x120.png differ diff --git a/src/static/app/icons/144x144.png b/src/static/app/icons/144x144.png new file mode 100644 index 0000000..f78346b Binary files /dev/null and b/src/static/app/icons/144x144.png differ diff --git a/src/static/app/icons/152x152.png b/src/static/app/icons/152x152.png new file mode 100644 index 0000000..f979721 Binary files /dev/null and b/src/static/app/icons/152x152.png differ diff --git a/src/static/app/icons/167x167.png b/src/static/app/icons/167x167.png new file mode 100644 index 0000000..d0aef20 Binary files /dev/null and b/src/static/app/icons/167x167.png differ diff --git a/src/static/app/icons/180x180.png b/src/static/app/icons/180x180.png new file mode 100644 index 0000000..24bd062 Binary files /dev/null and b/src/static/app/icons/180x180.png differ diff --git a/src/static/app/icons/192x192.png b/src/static/app/icons/192x192.png new file mode 100644 index 0000000..a8ea1a2 Binary files /dev/null and b/src/static/app/icons/192x192.png differ diff --git a/src/static/app/icons/20x20.png b/src/static/app/icons/20x20.png new file mode 100644 index 0000000..0abed04 Binary files /dev/null and b/src/static/app/icons/20x20.png differ diff --git a/src/static/app/icons/29x29.png b/src/static/app/icons/29x29.png new file mode 100644 index 0000000..a20d373 Binary files /dev/null and b/src/static/app/icons/29x29.png differ diff --git a/src/static/app/icons/40x40.png b/src/static/app/icons/40x40.png new file mode 100644 index 0000000..2b41be6 Binary files /dev/null and b/src/static/app/icons/40x40.png differ diff --git a/src/static/app/icons/58x58.png b/src/static/app/icons/58x58.png new file mode 100644 index 0000000..8e18b42 Binary files /dev/null and b/src/static/app/icons/58x58.png differ diff --git a/src/static/app/icons/60x60.png b/src/static/app/icons/60x60.png new file mode 100644 index 0000000..167826b Binary files /dev/null and b/src/static/app/icons/60x60.png differ diff --git a/src/static/app/icons/72x72.png b/src/static/app/icons/72x72.png new file mode 100644 index 0000000..ddb91e3 Binary files /dev/null and b/src/static/app/icons/72x72.png differ diff --git a/src/static/app/icons/76x76.png b/src/static/app/icons/76x76.png new file mode 100644 index 0000000..0d9d28e Binary files /dev/null and b/src/static/app/icons/76x76.png differ diff --git a/src/static/app/icons/80x80.png b/src/static/app/icons/80x80.png new file mode 100644 index 0000000..1877042 Binary files /dev/null and b/src/static/app/icons/80x80.png differ diff --git a/src/static/app/icons/87x87.png b/src/static/app/icons/87x87.png new file mode 100644 index 0000000..251fb24 Binary files /dev/null and b/src/static/app/icons/87x87.png differ diff --git a/src/static/app/icons/96x96.png b/src/static/app/icons/96x96.png new file mode 100644 index 0000000..eccf396 Binary files /dev/null and b/src/static/app/icons/96x96.png differ diff --git a/src/static/images/ancient-paper-bg.jpg b/src/static/images/ancient-paper-bg.jpg new file mode 100644 index 0000000..e9444e4 Binary files /dev/null and b/src/static/images/ancient-paper-bg.jpg differ diff --git a/src/static/images/avatar.jpg b/src/static/images/avatar.jpg new file mode 100644 index 0000000..2010a70 Binary files /dev/null and b/src/static/images/avatar.jpg differ diff --git a/src/static/images/bamboo-pattern.jpg b/src/static/images/bamboo-pattern.jpg new file mode 100644 index 0000000..fb057fb Binary files /dev/null and b/src/static/images/bamboo-pattern.jpg differ diff --git a/src/static/images/default-avatar.png b/src/static/images/default-avatar.png new file mode 100644 index 0000000..4eb5879 Binary files /dev/null and b/src/static/images/default-avatar.png differ diff --git a/src/static/logo.svg b/src/static/logo.svg new file mode 100644 index 0000000..eaee669 --- /dev/null +++ b/src/static/logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/static/tabbar/example.png b/src/static/tabbar/example.png new file mode 100644 index 0000000..fd1e942 Binary files /dev/null and b/src/static/tabbar/example.png differ diff --git a/src/static/tabbar/exampleHL.png b/src/static/tabbar/exampleHL.png new file mode 100644 index 0000000..7501011 Binary files /dev/null and b/src/static/tabbar/exampleHL.png differ diff --git a/src/static/tabbar/home.png b/src/static/tabbar/home.png new file mode 100644 index 0000000..8f82e21 Binary files /dev/null and b/src/static/tabbar/home.png differ diff --git a/src/static/tabbar/homeHL.png b/src/static/tabbar/homeHL.png new file mode 100644 index 0000000..26d3761 Binary files /dev/null and b/src/static/tabbar/homeHL.png differ diff --git a/src/static/tabbar/personal.png b/src/static/tabbar/personal.png new file mode 100644 index 0000000..0a569a2 Binary files /dev/null and b/src/static/tabbar/personal.png differ diff --git a/src/static/tabbar/personalHL.png b/src/static/tabbar/personalHL.png new file mode 100644 index 0000000..8c3e66e Binary files /dev/null and b/src/static/tabbar/personalHL.png differ diff --git a/src/static/tabbar/scan.png b/src/static/tabbar/scan.png new file mode 100644 index 0000000..f0f60c2 Binary files /dev/null and b/src/static/tabbar/scan.png differ diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..d5e3770 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,19 @@ +import { createPinia } from 'pinia' +import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化 + +const store = createPinia() +store.use( + createPersistedState({ + storage: { + getItem: uni.getStorageSync, + setItem: uni.setStorageSync, + }, + }), +) + +export default store + +// 模块统一导出 +export * from './theme' +export * from './token' +export * from './user' diff --git a/src/store/theme.ts b/src/store/theme.ts new file mode 100644 index 0000000..c3f1c55 --- /dev/null +++ b/src/store/theme.ts @@ -0,0 +1,42 @@ +import type { ConfigProviderThemeVars } from 'wot-design-uni' + +import { defineStore } from 'pinia' + +export const useThemeStore = defineStore( + 'theme-store', + () => { + /** 主题 */ + const theme = ref<'light' | 'dark'>('light') + + /** 主题变量 */ + const themeVars = ref({ + // colorTheme: 'red', + // buttonPrimaryBgColor: '#07c160', + // buttonPrimaryColor: '#07c160', + }) + + /** 设置主题变量 */ + const setThemeVars = (partialVars: Partial) => { + themeVars.value = { ...themeVars.value, ...partialVars } + } + + /** 切换主题 */ + const toggleTheme = () => { + theme.value = theme.value === 'light' ? 'dark' : 'light' + } + + return { + /** 设置主题变量 */ + setThemeVars, + /** 切换主题 */ + toggleTheme, + /** 主题变量 */ + themeVars, + /** 主题 */ + theme, + } + }, + { + persist: true, + }, +) diff --git a/src/store/token.ts b/src/store/token.ts new file mode 100644 index 0000000..126967b --- /dev/null +++ b/src/store/token.ts @@ -0,0 +1,288 @@ +import type { IAuthLoginRes } from '@/api/types/login' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' // 修复:导入 computed +import { + login as _login, + logout as _logout, + refreshToken as _refreshToken, + wxLogin as _wxLogin, + getWxCode, +} from '@/api/login' +import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login' +import { isDoubleTokenMode } from '@/utils' +import { useUserStore } from './user' + +// 初始化状态 +const tokenInfoState = isDoubleTokenMode + ? { + accessToken: '', + accessExpiresIn: 0, + refreshToken: '', + refreshExpiresIn: 0, + } + : { + token: '', + expiresIn: 0, + } + +export const useTokenStore = defineStore( + 'token', + () => { + // 定义用户信息 + const tokenInfo = ref({ ...tokenInfoState }) + // 设置用户信息 + const setTokenInfo = (val: IAuthLoginRes) => { + tokenInfo.value = val + + // 计算并存储过期时间 + const now = Date.now() + if (isSingleTokenRes(val)) { + // 单token模式 + const expireTime = now + val.expiresIn * 1000 + uni.setStorageSync('accessTokenExpireTime', expireTime) + } + else if (isDoubleTokenRes(val)) { + // 双token模式 + const accessExpireTime = now + val.accessExpiresIn * 1000 + const refreshExpireTime = now + val.refreshExpiresIn * 1000 + uni.setStorageSync('accessTokenExpireTime', accessExpireTime) + uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime) + } + } + + /** + * 判断token是否过期 + */ + const isTokenExpired = computed(() => { + if (!tokenInfo.value) { + return true + } + + const now = Date.now() + const expireTime = uni.getStorageSync('accessTokenExpireTime') + + if (!expireTime) + return true + return now >= expireTime + }) + + /** + * 判断refreshToken是否过期 + */ + const isRefreshTokenExpired = computed(() => { + if (!isDoubleTokenMode) + return true + + const now = Date.now() + const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime') + + if (!refreshExpireTime) + return true + return now >= refreshExpireTime + }) + + /** + * 登录成功后处理逻辑 + * @param tokenInfo 登录返回的token信息 + */ + async function _postLogin(tokenInfo: IAuthLoginRes) { + setTokenInfo(tokenInfo) + const userStore = useUserStore() + await userStore.fetchUserInfo() + } + + /** + * 用户登录 + * @param credentials 登录参数 + * @returns 登录结果 + */ + const login = async (credentials: { + username: string + password: string + code: string + uuid: string + }) => { + try { + const res = await _login(credentials) + console.log('普通登录-res: ', res) + await _postLogin(res.data) + uni.showToast({ + title: '登录成功', + icon: 'success', + }) + return res + } + catch (error) { + console.error('登录失败:', error) + uni.showToast({ + title: '登录失败,请重试', + icon: 'error', + }) + throw error + } + } + + /** + * 微信登录 + * @returns 登录结果 + */ + const wxLogin = async () => { + try { + // 获取微信小程序登录的code + const code = await getWxCode() + console.log('微信登录-code: ', code) + const res = await _wxLogin(code) + console.log('微信登录-res: ', res) + await _postLogin(res.data) + uni.showToast({ + title: '登录成功', + icon: 'success', + }) + return res + } + catch (error) { + console.error('微信登录失败:', error) + uni.showToast({ + title: '微信登录失败,请重试', + icon: 'error', + }) + throw error + } + } + + /** + * 退出登录 并 删除用户信息 + */ + const logout = async () => { + try { + // TODO 实现自己的退出登录逻辑 + await _logout() + } + catch (error) { + console.error('退出登录失败:', error) + } + finally { + // 无论成功失败,都需要清除本地token信息 + // 清除存储的过期时间 + uni.removeStorageSync('accessTokenExpireTime') + uni.removeStorageSync('refreshTokenExpireTime') + console.log('退出登录-清除用户信息') + tokenInfo.value = { ...tokenInfoState } + uni.removeStorageSync('token') + const userStore = useUserStore() + userStore.clearUserInfo() + } + } + + /** + * 刷新token + * @returns 刷新结果 + */ + const refreshToken = async () => { + if (!isDoubleTokenMode) { + console.error('单token模式不支持刷新token') + throw new Error('单token模式不支持刷新token') + } + + try { + // 安全检查,确保refreshToken存在 + if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) { + throw new Error('无效的refreshToken') + } + + const refreshToken = tokenInfo.value.refreshToken + const res = await _refreshToken(refreshToken) + console.log('刷新token-res: ', res) + setTokenInfo(res.data) + return res + } + catch (error) { + console.error('刷新token失败:', error) + throw error + } + } + + /** + * 获取有效的token + * 注意:在computed中不直接调用异步函数,只做状态判断 + * 实际的刷新操作应由调用方处理 + */ + const getValidToken = computed(() => { + // token已过期,返回空 + if (isTokenExpired.value) { + return '' + } + + if (!isDoubleTokenMode) { + return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : '' + } + else { + return isDoubleTokenRes(tokenInfo.value) ? tokenInfo.value.accessToken : '' + } + }) + + /** + * 检查是否有登录信息(不考虑token是否过期) + */ + const hasLoginInfo = computed(() => { + if (!tokenInfo.value) { + return false + } + if (isDoubleTokenMode) { + return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken + } + else { + return isSingleTokenRes(tokenInfo.value) && !!tokenInfo.value.token + } + }) + + /** + * 检查是否已登录且token有效 + */ + const hasValidLogin = computed(() => { + console.log('hasValidLogin', hasLoginInfo.value, !isTokenExpired.value) + return hasLoginInfo.value && !isTokenExpired.value + }) + + /** + * 尝试获取有效的token,如果过期且可刷新,则刷新token + * @returns 有效的token或空字符串 + */ + const tryGetValidToken = async (): Promise => { + if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) { + try { + await refreshToken() + return getValidToken.value + } + catch (error) { + console.error('尝试刷新token失败:', error) + return '' + } + } + return getValidToken.value + } + + return { + // 核心API方法 + login, + wxLogin, + logout, + + // 认证状态判断(最常用的) + hasLogin: hasValidLogin, + + // 内部系统使用的方法 + refreshToken, + tryGetValidToken, + validToken: getValidToken, + + // 调试或特殊场景可能需要直接访问的信息 + tokenInfo, + setTokenInfo, + } + }, + { + // 添加持久化配置,确保刷新页面后token信息不丢失 + persist: true, + }, +) diff --git a/src/store/user.ts b/src/store/user.ts new file mode 100644 index 0000000..6a63737 --- /dev/null +++ b/src/store/user.ts @@ -0,0 +1,61 @@ +import type { IUserInfoRes } from '@/api/types/login' +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { + getUserInfo, +} from '@/api/login' + +// 初始化状态 +const userInfoState: IUserInfoRes = { + userId: -1, + username: '', + nickname: '', + avatar: '/static/images/default-avatar.png', +} + +export const useUserStore = defineStore( + 'user', + () => { + // 定义用户信息 + const userInfo = ref({ ...userInfoState }) + // 设置用户信息 + const setUserInfo = (val: IUserInfoRes) => { + console.log('设置用户信息', val) + // 若头像为空 则使用默认头像 + if (!val.avatar) { + val.avatar = userInfoState.avatar + } + userInfo.value = val + } + const setUserAvatar = (avatar: string) => { + userInfo.value.avatar = avatar + console.log('设置用户头像', avatar) + console.log('userInfo', userInfo.value) + } + // 删除用户信息 + const clearUserInfo = () => { + userInfo.value = { ...userInfoState } + uni.removeStorageSync('user') + } + + /** + * 获取用户信息 + */ + const fetchUserInfo = async () => { + const res = await getUserInfo() + setUserInfo(res.data) + return res + } + + return { + userInfo, + clearUserInfo, + fetchUserInfo, + setUserInfo, + setUserAvatar, + } + }, + { + persist: true, + }, +) diff --git a/src/style/ancient-decorations.scss b/src/style/ancient-decorations.scss new file mode 100644 index 0000000..3178b93 --- /dev/null +++ b/src/style/ancient-decorations.scss @@ -0,0 +1,199 @@ +/** + * 古风装饰样式 + * 专门为朱子文化主题设计的装饰元素 + */ + +// 古风装饰通用样式 +.ancient-decoration { + position: relative; + + // 古风四角装饰 + &.corner-decoration { + &::before { + content: ''; + position: absolute; + top: -4rpx; + left: -4rpx; + width: 24rpx; + height: 24rpx; + border-top: 2rpx solid $accent-color; + border-left: 2rpx solid $accent-color; + opacity: 0.6; + } + + &::after { + content: ''; + position: absolute; + bottom: -4rpx; + right: -4rpx; + width: 24rpx; + height: 24rpx; + border-bottom: 2rpx solid $accent-color; + border-right: 2rpx solid $accent-color; + opacity: 0.6; + } + } + + // 古风边框纹饰 + &.border-pattern { + border: 2rpx solid transparent; + background: + linear-gradient($jade-white, $jade-white) padding-box, + linear-gradient(45deg, $accent-color 0%, $bamboo-cyan 50%, $accent-color 100%) border-box; + border-radius: 12rpx; + } +} + +// 古风文字装饰 +.ancient-text { + position: relative; + + // 书法底纹效果 + &.calligraphy { + &::before { + content: ''; + position: absolute; + bottom: -2rpx; + left: 0; + width: 100%; + height: 2rpx; + background: linear-gradient( + 90deg, + transparent 0%, + $accent-color 20%, + $bamboo-cyan 50%, + $accent-color 80%, + transparent 100% + ); + animation: textFlow 3s ease-in-out infinite; + } + } + + // 印章效果 + &.seal { + color: $primary-color; + font-weight: bold; + padding: 4rpx 12rpx; + border: 2rpx solid $primary-color; + border-radius: 6rpx; + background: rgba(142, 61, 62, 0.1); + position: relative; + + &::before { + content: ''; + position: absolute; + top: -1rpx; + left: -1rpx; + right: -1rpx; + bottom: -1rpx; + border: 1rpx solid rgba(142, 61, 62, 0.3); + border-radius: 6rpx; + } + } +} + +// 古风按钮装饰增强 +.ancient-btn-enhanced { + position: relative; + overflow: visible; + + // 古风云纹装饰 + &::before { + content: ''; + position: absolute; + top: -8rpx; + left: -8rpx; + right: -8rpx; + bottom: -8rpx; + background: + radial-gradient(circle at 20% 20%, rgba(217, 200, 163, 0.3) 0%, transparent 30%), + radial-gradient(circle at 80% 80%, rgba(122, 158, 126, 0.3) 0%, transparent 30%); + border-radius: inherit; + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; + } + + &:hover::before { + opacity: 1; + } + + // 古风光效 + &::after { + content: ''; + position: absolute; + top: 50%; + left: -100%; + width: 100%; + height: 2rpx; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.6) 50%, transparent 100%); + transform: translateY(-50%); + transition: left 0.6s ease; + } + + &:active::after { + left: 100%; + } +} + +// 古风卡片增强装饰 +.ancient-card-enhanced { + // 宣纸纹理 + background-image: + radial-gradient(circle at 20% 50%, rgba(217, 200, 163, 0.05) 0%, transparent 20%), + radial-gradient(circle at 80% 20%, rgba(122, 158, 126, 0.05) 0%, transparent 20%), + radial-gradient(circle at 40% 80%, rgba(142, 61, 62, 0.03) 0%, transparent 20%); + + // 古风印章装饰 + &.with-seal { + &::before { + content: '朱'; + position: absolute; + top: 16rpx; + right: 16rpx; + width: 32rpx; + height: 32rpx; + background: $primary-color; + color: $jade-white; + font-size: 20rpx; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4rpx; + opacity: 0.8; + transform: rotate(12deg); + } + } +} + +// 动画效果 +@keyframes textFlow { + 0%, + 100% { + opacity: 0.6; + transform: scaleX(1); + } + 50% { + opacity: 1; + transform: scaleX(1.1); + } +} + +// 古风悬浮效果 +.ancient-float { + animation: ancientFloat 4s ease-in-out infinite; +} + +@keyframes ancientFloat { + 0%, + 100% { + transform: translateY(0) rotate(0deg); + } + 25% { + transform: translateY(-2rpx) rotate(0.5deg); + } + 75% { + transform: translateY(2rpx) rotate(-0.5deg); + } +} diff --git a/src/style/iconfont.css b/src/style/iconfont.css new file mode 100644 index 0000000..35da86c --- /dev/null +++ b/src/style/iconfont.css @@ -0,0 +1,28 @@ +@font-face { + font-family: 'iconfont'; /* Project id 4543091 */ + src: + url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAOwAAsAAAAAB9AAAANjAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDHAqDBIJqATYCJAMQCwoABCAFhGcHPRvnBsgusG3kMyE15/44PsBX09waBHv0REDt97oHAQDFrOIyPirRiULQ+TJcXV0hCYTuVFcBC915/2vX/32Q80hkZ5PZGZ9snvwruVLloidKqYN6iKC53bOtbKwVLSIi3W6zCWZbs3VbER3j9JpGX3ySYcc94IQRTK5s4epS/jSqIgvg37qlY2/jwQN7D9ADpfRCmIknQByTscVZPTBr+hnnCKg2o4bjakvXEPjuY65DJGeJNtBUhn1JxOBuB2UZmUpBOXdsFp4oxOv4GHgs3h/+wRDcicqSZJG1q9kK1z/Af9NpqxjpC2QaAdpHlCFh4spcYXs5sMWpSk5wUj31G2dLQKVKkZ/w7f/8/i/A3JVUSZK9f7xIKJeU14IFpBI/Qfkkz46GT/CuaGREfCtKJUougWeQWHvVC5Lcz2BGS+SePR99vj3yjJx7h574tp7uWcOh4yfaTjS/245TT/vkQrN+a7RLkK8+Vd+bz+FSGh+9srDQKPeJ2s29z7ah4+efdoxefRbbGwfy7ht+SuIWukzsu1b6ePP+6kN1aamb47qsPim1Ia3xdEpDcl1dckPKGYnneI23+57r2W1Mmkqs6ajrChRCs5qyQ66rTVWhgZaG7toOeHm5cxn0sSQuNDEgcUTdNTSupKI1JRZih/JssAUKezPeOJJzbNozF6zWJuuVavVU5Tgtkop/SDzHa7ytvnCTq0PhkEfi4xLLtb0PuwyOAYqmrYQApFJyoJjTnfz+ve94vvv2f/yWgxl8Jd8Di2DRDPuob59mU/+VfDCROQyR8xSnmP9fXm7liagmN39OlmbvjqG0sMsJKrU0EFXogaRSH5bNY1CmxhyUq7QC1cY1T67RwuQk5CoM2RUQNLoEUb03kDS6h2XzcyjT7iOUa/QXqq1Hn6/GUBAaGcGcWJFlGUmCoVOp8kLvABHnVczGYiOE2SVEUH5OXj/TSnTCDjHAviAWcE4RZYaGWszNiKoayGSGTASeY+PcrMjNpVMvyREMDRoxBMYRVojFMkQiMOhohubdzxtAiOapMMbERpKMnQT9SL4ceQysVdJZVa9kEbsFogIcRyEUE2kN0mL7CDVIGhBzupWMEHA5bDvipgq5hKJcKef8ivbx1kC15KgcYkghhzLxYNntxoKCReJ82jAHAAA=') + format('woff2'), + url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.woff?t=1715485842402') format('woff'), + url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.ttf?t=1715485842402') format('truetype'); +} + +.iconfont { + font-family: 'iconfont' !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-my:before { + content: '\e78c'; +} + +.icon-package:before { + content: '\e9c2'; +} + +.icon-chat:before { + content: '\e600'; +} diff --git a/src/style/index.scss b/src/style/index.scss new file mode 100644 index 0000000..a891ced --- /dev/null +++ b/src/style/index.scss @@ -0,0 +1,40 @@ +// 测试用的 iconfont,可生效 +// @import './iconfont.css'; + +// 导入朱子文化主题样式 +@import './zhuzi-theme.scss'; +@import './ancient-decorations.scss'; + +.test { + // 可以通过 @apply 多个样式封装整体样式 + @apply mt-4 ml-4; + + padding-top: 4px; + color: red; +} + +:root, +page { + // 修改按主题色 + // --wot-color-theme: #37c2bc; + + // 修改按钮背景色 + // --wot-button-primary-bg-color: green; +} + +/* +border-t-1 +由于uniapp中无法使用*选择器,使用魔法代替*,加上此规则可以简化border与divide的使用,并提升布局的兼容性 +1. 防止padding和border影响元素宽度。 (https://github.com/mozdevs/cssremedy/issues/4) +2. 允许仅通过添加边框宽度来向元素添加边框。 (https://github.com/tailwindcss/tailwindcss/pull/116) +3. [UnoCSS]: 允许使用css变量'--un-default-border-color'覆盖默认边框颜色 +*/ +// 这个样式有重大BUG,先去掉!!(2025-08-15) +// :not(not), +// ::before, +// ::after { +// box-sizing: border-box; /* 1 */ +// border-width: 0; /* 2 */ +// border-style: solid; /* 2 */ +// border-color: var(--un-default-border-color, #e5e7eb); /* 3 */ +// } diff --git a/src/style/zhuzi-theme.scss b/src/style/zhuzi-theme.scss new file mode 100644 index 0000000..077f237 --- /dev/null +++ b/src/style/zhuzi-theme.scss @@ -0,0 +1,570 @@ +/** + * 朱子文化主题样式 + * 古风竹子风格,典雅大气 + */ + +// 朱子教育古风配色方案 - 主色调 +$primary-color: #8e3d3e; // 朱砂红:主色调,源自武夷丹霞地貌与传统文化中的朱红 +$secondary-color: #2e5a88; // 理学蓝:主色调,取自朱熹"青如许"的意象,代表理学思想的深邃与清澈 +$accent-color: #d9c8a3; // 绢帛黄:辅助色,模仿古代绢纸色调,营造温暖学习氛围 +$success-color: #4a7354; // 山水绿:辅助色,源于武夷山水的翠色,寓意生机与成长 +$warning-color: #a76363; // 茶汤红:点缀色,呼应武夷岩茶汤色,增加温暖感 +$error-color: #8e3d3e; // 朱砂红,错误状态 + +// 朱子教育古风配色方案 - 辅助色系 +$jade-white: #ffffff; // 玉兰白:背景色,中国人民大学校花玉兰花的洁白,传递纯净与平衡 +$paper-gray: #d8d0c0; // 宣纸灰:背景色,仿宣纸质感,减少视觉疲劳 +$tea-red: #a76363; // 茶汤红:点缀色,呼应武夷岩茶汤色,增加温暖感 +$bamboo-cyan: #7a9e7e; // 墨竹青:点缀色,文人喜爱的竹青色,增添雅致气息 +$sandalwood-brown: #b8824f; // 檀木棕:点缀色,传统家具常用色,增加沉稳感 +$light-paper: #f8f5f0; // 浅米白:仿古宣纸基底,降低视觉疲劳 + +// 文字色彩 - 传统墨色系 +$text-primary: #333333; // 墨黑:重要标题使用近黑色,保证可读性 +$text-secondary: #666666; // 灰墨:正文使用深灰色,减少纯黑对比度 +$text-tertiary: #999999; // 淡灰:辅助性文字使用浅灰色,降低视觉突出性 +$text-inverse: #ffffff; // 反色文字 + +// 背景色彩 - 宣纸质感 +$background-primary: #ffffff; // 玉兰白背景 +$background-secondary: #f8f5f0; // 浅米白:仿古宣纸基底 +$background-tertiary: #d8d0c0; // 宣纸灰:仿宣纸质感 + +// 古风渐变色 - 传统色彩组合 +$gradient-primary: linear-gradient(135deg, #8e3d3e 0%, #a76363 100%); // 朱砂红渐变 +$gradient-secondary: linear-gradient(135deg, #2e5a88 0%, #4a7354 100%); // 理学蓝到山水绿 +$gradient-warm: linear-gradient(135deg, #d9c8a3 0%, #b8824f 100%); // 绢帛黄到檀木棕 +$gradient-elegant: linear-gradient(135deg, #7a9e7e 0%, #4a7354 100%); // 墨竹青渐变 +$gradient-paper: linear-gradient(135deg, #f8f5f0 0%, #d8d0c0 100%); // 宣纸质感渐变 + +// 阴影 +$shadow-light: 0 2px 8px rgba(45, 94, 62, 0.1); +$shadow-medium: 0 4px 16px rgba(45, 94, 62, 0.15); +$shadow-heavy: 0 8px 24px rgba(45, 94, 62, 0.2); + +// 边框圆角 +$border-radius-sm: 8rpx; +$border-radius-md: 16rpx; +$border-radius-lg: 24rpx; +$border-radius-xl: 32rpx; + +// 字体大小 +$font-size-xs: 24rpx; +$font-size-sm: 28rpx; +$font-size-md: 32rpx; +$font-size-lg: 36rpx; +$font-size-xl: 40rpx; +$font-size-xxl: 48rpx; + +// 间距 +$spacing-xs: 8rpx; +$spacing-sm: 16rpx; +$spacing-md: 24rpx; +$spacing-lg: 32rpx; +$spacing-xl: 48rpx; + +// 朱子文化卡片样式 - 古风雅致 +.zhuzi-card { + background: $jade-white; + border: none; + border-radius: $border-radius-lg; + box-shadow: + 0 8rpx 24rpx rgba(142, 61, 62, 0.08), + 0 2rpx 8rpx rgba(142, 61, 62, 0.05); + padding: $spacing-lg; + position: relative; + overflow: hidden; + + // 古风宣纸质感背景 + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 50%, rgba(217, 200, 163, 0.03) 0%, transparent 30%), + radial-gradient(circle at 80% 50%, rgba(122, 158, 126, 0.03) 0%, transparent 30%); + pointer-events: none; + } + + // 古风装饰纹理 + &::after { + content: ''; + position: absolute; + top: -10%; + right: -10%; + width: 120%; + height: 120%; + background-image: + radial-gradient(circle at 30% 30%, rgba(142, 61, 62, 0.02) 0%, transparent 40%), + radial-gradient(circle at 70% 70%, rgba(46, 90, 136, 0.02) 0%, transparent 40%); + animation: float 25s ease-in-out infinite; + pointer-events: none; + } + + .card-title { + font-size: $font-size-lg; + font-weight: bold; + color: $primary-color; + margin-bottom: $spacing-md; + text-align: center; + } + + .card-content { + font-size: $font-size-md; + color: $text-primary; + line-height: 1.6; + } +} + +// 竹子装饰背景 +.bamboo-bg { + //background: $gradient-paper; + /* 使用实际的古风竹子背景图 */ + background: + url('/static/images/bamboo-pattern.jpg') repeat-y right center, + linear-gradient(135deg, #f8f5f0 0%, #d8d0c0 100%); + background-size: + 120rpx auto, + cover; + opacity: 0.8; +} + +// 古典按钮样式 - 朱子文化风格 +.ancient-btn { + background: $gradient-primary; + border: 1rpx solid $primary-color; + border-radius: $border-radius-xl; + padding: $spacing-md $spacing-xl; + color: $text-inverse; + font-weight: 500; + font-size: $font-size-md; + text-align: center; + box-shadow: + 0 6rpx 16rpx rgba(142, 61, 62, 0.2), + 0 2rpx 6rpx rgba(142, 61, 62, 0.1); + position: relative; + overflow: hidden; + transition: all 0.3s ease; + + // 古风光泽效果 + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%); + border-radius: 50%; + transform: translate(-50%, -50%); + transition: all 0.4s ease; + } + + &:hover { + transform: translateY(-1rpx); + box-shadow: + 0 8rpx 20rpx rgba(142, 61, 62, 0.25), + 0 4rpx 8rpx rgba(142, 61, 62, 0.15); + } + + &:active { + transform: translateY(0); + + &::before { + width: 200rpx; + height: 200rpx; + } + } + + &.disabled { + background: $paper-gray; + color: $text-tertiary; + border-color: $paper-gray; + cursor: not-allowed; + transform: none; + box-shadow: none; + } + + // 理学蓝变体 + &.secondary { + background: $gradient-secondary; + border-color: $secondary-color; + box-shadow: + 0 6rpx 16rpx rgba(46, 90, 136, 0.2), + 0 2rpx 6rpx rgba(46, 90, 136, 0.1); + + &:hover { + box-shadow: + 0 8rpx 20rpx rgba(46, 90, 136, 0.25), + 0 4rpx 8rpx rgba(46, 90, 136, 0.15); + } + } + + // 绢帛黄变体 + &.warm { + background: $gradient-warm; + border-color: $accent-color; + color: $text-primary; + box-shadow: + 0 6rpx 16rpx rgba(217, 200, 163, 0.3), + 0 2rpx 6rpx rgba(217, 200, 163, 0.2); + + &:hover { + box-shadow: + 0 8rpx 20rpx rgba(217, 200, 163, 0.35), + 0 4rpx 8rpx rgba(217, 200, 163, 0.25); + } + } +} + +// 排行榜样式 +.ranking-item { + background: rgba(255, 255, 255, 0.9); + border: 1rpx solid #e6e6e6; + border-radius: $border-radius-md; + margin-bottom: $spacing-sm; + padding: $spacing-md; + display: flex; + align-items: center; + box-shadow: $shadow-light; + + &.top-three { + background: linear-gradient(135deg, #ffd700 0%, #ffa500 100%); + border-color: #daa520; + } + + .rank-number { + font-size: $font-size-xl; + font-weight: bold; + color: $accent-color; + min-width: 60rpx; + text-align: center; + + &.first { + color: #ff6347; + } + &.second { + color: #32cd32; + } + &.third { + color: #1e90ff; + } + } + + .user-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + margin: 0 $spacing-md; + border: 2rpx solid #daa520; + } + + .user-info { + flex: 1; + + .name { + font-size: $font-size-md; + font-weight: bold; + color: $text-primary; + margin-bottom: 4rpx; + } + + .school { + font-size: $font-size-sm; + color: $text-secondary; + } + } + + .score { + font-size: $font-size-lg; + font-weight: bold; + color: $accent-color; + } +} + +// 答题页面样式 +.quiz-container { + min-height: 100vh; + //background: $gradient-paper; + /* 古纸背景图片暂时注释,后续添加实际图片资源 */ + background: url('/static/images/ancient-paper-bg.jpg') no-repeat center center; + background-size: cover; + padding: $spacing-lg; + + .quiz-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $spacing-lg; + + .question-number { + font-size: $font-size-lg; + font-weight: bold; + color: $primary-color; + } + + .timer { + background: #ff4444; + color: white; + padding: $spacing-sm $spacing-md; + border-radius: $border-radius-md; + font-weight: bold; + + &.warning { + animation: pulse 1s infinite; + } + } + } + + .question-card { + @extend .zhuzi-card; + margin-bottom: $spacing-lg; + + .question-title { + font-size: $font-size-lg; + color: $text-primary; + margin-bottom: $spacing-md; + line-height: 1.5; + } + + .question-type { + font-size: $font-size-sm; + color: $text-secondary; + margin-bottom: $spacing-sm; + } + + .question-score { + background: $gradient-warm; + color: white; + padding: 4rpx $spacing-sm; + border-radius: $border-radius-sm; + font-size: $font-size-sm; + display: inline-block; + } + } + + .options-list { + .option-item { + background: rgba(255, 255, 255, 0.8); + border: 2rpx solid #e6e6e6; + border-radius: $border-radius-md; + padding: $spacing-md; + margin-bottom: $spacing-sm; + transition: all 0.3s ease; + + &:hover, + &.selected { + border-color: $primary-color; + background: rgba(45, 94, 62, 0.1); + } + + &.correct { + border-color: #32cd32; + background: rgba(50, 205, 50, 0.1); + } + + &.incorrect { + border-color: #ff4444; + background: rgba(255, 68, 68, 0.1); + } + } + } +} + +// 动画定义 +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes slideInUp { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes float { + 0%, + 100% { + transform: translate(-50%, -50%) rotate(0deg); + } + 50% { + transform: translate(-50%, -50%) rotate(180deg); + } +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +// 现代化排行榜金字塔样式 +.podium-container { + display: flex; + align-items: end; + justify-content: center; + gap: 16rpx; + padding: 48rpx 0; + background: $gradient-elegant; + border-radius: $border-radius-lg; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + radial-gradient(circle at 20% 80%, rgba(245, 158, 11, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(99, 102, 241, 0.1) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(16, 185, 129, 0.1) 0%, transparent 50%); + pointer-events: none; + } + + .podium-item { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + + &.first { + order: 2; + .podium-base { + height: 160rpx; + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); + box-shadow: 0 8rpx 32rpx rgba(251, 191, 36, 0.4); + } + .rank-crown { + display: block; + } + } + + &.second { + order: 1; + .podium-base { + height: 120rpx; + background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%); + box-shadow: 0 8rpx 32rpx rgba(229, 231, 235, 0.4); + } + } + + &.third { + order: 3; + .podium-base { + height: 100rpx; + background: linear-gradient(135deg, #fbbf24 0%, #d97706 100%); + box-shadow: 0 8rpx 32rpx rgba(217, 119, 6, 0.4); + } + } + + .user-info { + margin-bottom: 24rpx; + text-align: center; + + .user-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + border: 4rpx solid rgba(255, 255, 255, 0.9); + margin-bottom: 12rpx; + box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.2); + } + + .user-name { + font-size: 24rpx; + color: $text-inverse; + font-weight: 600; + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); + display: block; + margin-bottom: 4rpx; + } + + .user-score { + font-size: 20rpx; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + } + } + + .podium-base { + width: 120rpx; + border-radius: 8rpx 8rpx 0 0; + position: relative; + transition: all 0.3s ease; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4rpx; + background: rgba(255, 255, 255, 0.3); + border-radius: 8rpx 8rpx 0 0; + } + } + + .rank-number { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: $text-inverse; + font-size: 36rpx; + font-weight: bold; + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); + } + + .rank-crown { + display: none; + position: absolute; + top: -40rpx; + left: 50%; + transform: translateX(-50%); + font-size: 48rpx; + color: #fbbf24; + text-shadow: 0 2rpx 8rpx rgba(251, 191, 36, 0.5); + animation: float 2s ease-in-out infinite; + } + } +} + +// 响应式设计 +@media screen and (max-width: 750rpx) { + .quiz-container { + padding: $spacing-md; + } + + .zhuzi-card { + padding: $spacing-md; + } +} diff --git a/src/tabbar/README.md b/src/tabbar/README.md new file mode 100644 index 0000000..c2b89eb --- /dev/null +++ b/src/tabbar/README.md @@ -0,0 +1,78 @@ +# tabbar 说明 + +## tabbar 4种策略 + +`tabbar` 分为 `4 种` 情况: + +- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。 +- 1 `原生 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。 + - 优势:原生自带的 tabbar,最先渲染,有缓存。 + - 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。 +- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。 + - 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。 + - 劣势:首次点击 tababr 会闪烁。 +- 3 `无缓存自定义 tabbar`,使用 `navigateTo` 切换 `tabbar`,`tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。 + - 优势:可以随意配置自己想要的 svg icon,切换字体颜色方便。可以实现各种花里胡哨的动效等。 + - 劣势:首次点击 `tababr` 会闪烁,无缓存。 + + +> 注意:花里胡哨的效果需要自己实现,本模版不提供。 + +## tabbar 配置说明 + +- 如果使用的是 `原生tabbar`,需要配置 `nativeTabbarList`,每个 `item` 需要配置 `path`、`text`、`iconPath`、`selectedIconPath` 等属性。 +- 如果使用的是 `自定义tabbar`,需要配置 `customTabbarList`,每个 `item` 需要配置 `path`、`text`、`icon` 、`iconType` 等属性(如果是 `image` 图片还需要配置2种图片)。 + +## 文件说明 + +`config.ts` 专门配置 `nativeTabbarList` 和 `customTabbarList` 的相关信息,请按照文件里面的注释配置相关项。 + +使用 `原生tabbar` 时,不需要关心下面2个文件: +- `store.ts` ,专门给 `自定义 tabbar` 提供状态管理,代码几乎不需要修改。 +- `index.vue` ,专门给 `自定义 tabbar` 提供渲染逻辑,代码可以稍微修改,以符合自己的需求。 + +## 自定义tabbar的不同类型的配置 + +- uniUi 图标 + + ```js + { + // ... 其他配置 + "iconType": "uniUi", + "icon": "home", + } + ``` +- unocss 图标 + + ```js + { + // ... 其他配置 + // 注意 unocss 图标需要如下处理:(二选一) + // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行) + // 2)配置到 unocss.config.ts 的 safelist 中 + iconType: 'unocss', + icon: 'i-carbon-code', + } + ``` +- iconfont 图标 + + ```js + { + // ... 其他配置 + // 注意 iconfont 图标需要额外加上 'iconfont',如下 + iconType: 'iconfont', + icon: 'iconfont icon-my', + } + ``` +- image 本地图片 + + ```js + { + // ... 其他配置 + // 使用 ‘image’时,需要配置 icon + iconActive 2张图片(不推荐) + // 既然已经用了自定义tabbar了,就不建议用图片了,所以不推荐 + iconType: 'image', + icon: '/static/tabbar/home.png', + iconActive: '/static/tabbar/homeHL.png', + } + ``` diff --git a/src/tabbar/config.ts b/src/tabbar/config.ts new file mode 100644 index 0000000..b7589a7 --- /dev/null +++ b/src/tabbar/config.ts @@ -0,0 +1,151 @@ +import type { TabBar } from '@uni-helper/vite-plugin-uni-pages' + +/** + * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件 + * 0: 'NO_TABBAR' `无 tabbar` + * 1: 'NATIVE_TABBAR' `完全原生 tabbar` + * 2: 'CUSTOM_TABBAR_WITH_CACHE' `有缓存自定义 tabbar` + * 3: 'CUSTOM_TABBAR_WITHOUT_CACHE' `无缓存自定义 tabbar` + * + * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致配置不生效 + */ +export const TABBAR_STRATEGY_MAP = { + NO_TABBAR: 0, + NATIVE_TABBAR: 1, + CUSTOM_TABBAR_WITH_CACHE: 2, + CUSTOM_TABBAR_WITHOUT_CACHE: 3, +} + +// TODO: 1/3. 通过这里切换使用tabbar的策略 +// 如果是使用 NO_TABBAR(0),nativeTabbarList 和 customTabbarList 都不生效(里面的配置不用管) +// 如果是使用 NATIVE_TABBAR(1),只需要配置 nativeTabbarList,customTabbarList 不生效 +// 如果是使用 CUSTOM_TABBAR(2,3),只需要配置 customTabbarList,nativeTabbarList 不生效 +export const selectedTabbarStrategy = TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE + +type NativeTabBarItem = TabBar['list'][number] + +// TODO: 2/3. 使用 NATIVE_TABBAR 时,更新下面的 tabbar 配置 +export const nativeTabbarList: NativeTabBarItem[] = [ + { + iconPath: 'static/tabbar/home.png', + selectedIconPath: 'static/tabbar/homeHL.png', + pagePath: 'pages/index/index', + text: '首页', + }, + { + iconPath: 'static/tabbar/example.png', + selectedIconPath: 'static/tabbar/exampleHL.png', + pagePath: 'pages/about/about', + text: '关于', + }, + { + iconPath: 'static/tabbar/personal.png', + selectedIconPath: 'static/tabbar/personalHL.png', + pagePath: 'pages/me/me', + text: '个人', + }, +] + +// badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改) +export type CustomTabBarItemBadge = number | 'dot' + +export interface CustomTabBarItem { + text: string + pagePath: string + iconType: 'uniUi' | 'uiLib' | 'unocss' | 'iconfont' | 'image' // 不建议用 image 模式,需要配置2张图 + icon: any // 其实是 string 类型,这里是为了避免 ts 报错 (tabbar/index.vue 里面 uni-icons 那行) + iconActive?: string // 只有在 image 模式下才需要,传递的是高亮的图片(PS: 不建议用 image 模式) + badge?: CustomTabBarItemBadge + isBulge?: boolean // 是否是中间的鼓包tabbarItem +} +// TODO: 3/3. 使用 CUSTOM_TABBAR(2,3) 时,更新下面的 tabbar 配置 +// 如果需要配置鼓包,需要在 'tabbar/store.ts' 里面设置,最后在 `tabbar/index.vue` 里面更改鼓包的图片 +export const customTabbarList: CustomTabBarItem[] = [ + { + text: '首页', + pagePath: 'pages/index/index', + // 朱子文化首页 + iconType: 'uniUi', + icon: 'home', + // badge: 'dot', + }, + { + text: '排行榜', + pagePath: 'pages/ranking/ranking', + // 积分排行榜 + iconType: 'uniUi', + icon: 'star-filled', + // badge: 10, + }, + { + pagePath: 'pages/me/me', + text: '我的', + iconType: 'uniUi', + icon: 'contact', + // badge: 100, + }, + // 其他类型演示 + // 1、uiLib + // { + // pagePath: 'pages/index/index', + // text: '首页', + // iconType: 'uiLib', + // icon: 'home', + // }, + // 2、iconfont + // { + // pagePath: 'pages/index/index', + // text: '首页', + // // 注意 iconfont 图标需要额外加上 'iconfont',如下 + // iconType: 'iconfont', + // icon: 'iconfont icon-my', + // }, + // 3、image + // { + // pagePath: 'pages/index/index', + // text: '首页', + // // 使用 ‘image’时,需要配置 icon + iconActive 2张图片 + // iconType: 'image', + // icon: '/static/tabbar/home.png', + // iconActive: '/static/tabbar/homeHL.png', + // }, +] + +/** + * 是否启用 tabbar 缓存 + * NATIVE_TABBAR(1) 和 CUSTOM_TABBAR_WITH_CACHE(2) 时,需要tabbar缓存 + */ +export const tabbarCacheEnable + = [TABBAR_STRATEGY_MAP.NATIVE_TABBAR, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE].includes(selectedTabbarStrategy) + +/** + * 是否启用自定义 tabbar + * CUSTOM_TABBAR(2,3) 时,启用自定义tabbar + */ +export const customTabbarEnable + = [TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITHOUT_CACHE].includes(selectedTabbarStrategy) + +/** + * 是否需要隐藏原生 tabbar + * CUSTOM_TABBAR_WITH_CACHE(2) 时,需要隐藏原生tabbar + */ +export const needHideNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE + +const _tabbarList = customTabbarEnable ? customTabbarList.map(item => ({ text: item.text, pagePath: item.pagePath })) : nativeTabbarList +export const tabbarList = customTabbarEnable ? customTabbarList : nativeTabbarList + +const _tabbar: TabBar = { + // 只有微信小程序支持 custom。App 和 H5 不生效 + custom: selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE, + color: '#999999', + selectedColor: '#DAA520', + backgroundColor: '#F5F5DC', + borderStyle: 'black', + height: '50px', + fontSize: '10px', + iconWidth: '24px', + spacing: '3px', + list: _tabbarList as unknown as TabBar['list'], +} + +export const tabBar = tabbarCacheEnable ? _tabbar : undefined diff --git a/src/tabbar/index.vue b/src/tabbar/index.vue new file mode 100644 index 0000000..487ceb3 --- /dev/null +++ b/src/tabbar/index.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/src/tabbar/store.ts b/src/tabbar/store.ts new file mode 100644 index 0000000..7919cc0 --- /dev/null +++ b/src/tabbar/store.ts @@ -0,0 +1,72 @@ +import type { CustomTabBarItem, CustomTabBarItemBadge } from './config' +import { reactive } from 'vue' + +import { FG_LOG_ENABLE } from '@/router/interceptor' +import { tabbarList as _tabbarList, customTabbarEnable } from './config' + +// TODO 1/2: 中间的鼓包tabbarItem的开关 +const BULGE_ENABLE = false + +/** tabbarList 里面的 path 从 pages.config.ts 得到 */ +const tabbarList = reactive(_tabbarList.map(item => ({ + ...item, + pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}`, +}))) + +if (customTabbarEnable && BULGE_ENABLE) { + if (tabbarList.length % 2) { + console.error('有鼓包时 tabbar 数量必须是偶数,否则样式很奇怪!!') + } + tabbarList.splice(tabbarList.length / 2, 0, { + isBulge: true, + } as CustomTabBarItem) +} + +export function isPageTabbar(path: string) { + const _path = path.split('?')[0] + return tabbarList.some(item => item.pagePath === _path) +} + +/** + * 自定义 tabbar 的状态管理,原生 tabbar 无需关注本文件 + * tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面 + * 使用reactive简单状态,而不是 pinia 全局状态 + */ +const tabbarStore = reactive({ + curIdx: uni.getStorageSync('app-tabbar-index') || 0, + prevIdx: uni.getStorageSync('app-tabbar-index') || 0, + setCurIdx(idx: number) { + this.curIdx = idx + uni.setStorageSync('app-tabbar-index', idx) + }, + setTabbarItemBadge(idx: number, badge: CustomTabBarItemBadge) { + if (tabbarList[idx]) { + tabbarList[idx].badge = badge + } + }, + setAutoCurIdx(path: string) { + const index = tabbarList.findIndex(item => item.pagePath === path) + FG_LOG_ENABLE && console.log('index:', index, path) + // console.log('tabbarList:', tabbarList) + if (index === -1) { + const pagesPathList = getCurrentPages().map(item => item.route.startsWith('/') ? item.route : `/${item.route}`) + // console.log(pagesPathList) + const flag = tabbarList.some(item => pagesPathList.includes(item.pagePath)) + if (!flag) { + this.setCurIdx(0) + return + } + } + else { + this.setCurIdx(index) + } + }, + restorePrevIdx() { + if (this.prevIdx === this.curIdx) + return + this.setCurIdx(this.prevIdx) + this.prevIdx = uni.getStorageSync('app-tabbar-index') || 0 + }, +}) + +export { tabbarList, tabbarStore } diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..e427177 --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1,146 @@ +// 全局要用的类型放到这里 + +declare global { + interface IResData { + code: number + msg: string + data: T + } + + // uni.uploadFile文件上传参数 + interface IUniUploadFileOptions { + file?: File + files?: UniApp.UploadFileOptionFiles[] + filePath?: string + name?: string + formData?: any + } + + interface IUserInfo { + nickname?: string + avatar?: string + /** 微信的 openid,非微信没有这个字段 */ + openid?: string + } + + interface IUserToken { + token: string + refreshToken?: string + refreshExpire?: number + } +} + +// patch uni 类型 +// 1. 补全 uni.hideToast() 的 options 类型 +// 2. 补全 uni.hideLoading() 的 options 类型 +// 3. 使用方式见:https://github.com/unibest-tech/unibest/pull/241 +declare global { + declare namespace UniNamespace { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + type HideLoadingCompleteCallback = (res: GeneralCallbackResult) => void + /** 接口调用失败的回调函数 */ + type HideLoadingFailCallback = (res: GeneralCallbackResult) => void + /** 接口调用成功的回调函数 */ + type HideLoadingSuccessCallback = (res: GeneralCallbackResult) => void + + interface HideLoadingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideLoadingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideLoadingFailCallback + test: UniNamespace.GeneralCallbackResult + /** + * 微信小程序:需要基础库: `2.22.1` + * + * 微信小程序:目前 toast 和 loading 相关接口可以相互混用,此参数可用于取消混用特性 + */ + noConflict?: boolean + /** 接口调用成功的回调函数 */ + success?: HideLoadingSuccessCallback + } + + // ---------------------------------------------------------- + + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + type HideToastCompleteCallback = (res: GeneralCallbackResult) => void + /** 接口调用失败的回调函数 */ + type HideToastFailCallback = (res: GeneralCallbackResult) => void + /** 接口调用成功的回调函数 */ + type HideToastSuccessCallback = (res: GeneralCallbackResult) => void + interface HideToastOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideToastCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideToastFailCallback + /** + * 微信小程序:需要基础库: `2.22.1` + * + * 微信小程序:目前 toast 和 loading 相关接口可以相互混用,此参数可用于取消混用特性 + */ + noConflict?: boolean + /** 接口调用成功的回调函数 */ + success?: HideToastSuccessCallback + } + } + interface Uni { + /** + * 隐藏 loading 提示框 + * + * 文档: [http://uniapp.dcloud.io/api/ui/prompt?id=hideloading](http://uniapp.dcloud.io/api/ui/prompt?id=hideloading) + * @example ```typescript + * uni.showLoading({ + * title: '加载中' + * }); + * + * setTimeout(function () { + * uni.hideLoading(); + * }, 2000); + * + * ``` + * @tutorial [](https://uniapp.dcloud.net.cn/api/ui/prompt.html#hideloading) + * @uniPlatform { + * "app": { + * "android": { + * "osVer": "4.4.4", + * "uniVer": "√", + * "unixVer": "3.9.0" + * }, + * "ios": { + * "osVer": "9.0", + * "uniVer": "√", + * "unixVer": "3.9.0" + * } + * } + * } + */ + // eslint-disable-next-line ts/method-signature-style + hideLoading(options?: T): void + /** + * 隐藏消息提示框 + * + * 文档: [http://uniapp.dcloud.io/api/ui/prompt?id=hidetoast](http://uniapp.dcloud.io/api/ui/prompt?id=hidetoast) + * @example ```typescript + * uni.hideToast(); + * ``` + * @tutorial [](https://uniapp.dcloud.net.cn/api/ui/prompt.html#hidetoast) + * @uniPlatform { + * "app": { + * "android": { + * "osVer": "4.4.4", + * "uniVer": "√", + * "unixVer": "3.9.0" + * }, + * "ios": { + * "osVer": "9.0", + * "uniVer": "√", + * "unixVer": "3.9.0" + * } + * } + * } + */ + // eslint-disable-next-line ts/method-signature-style + hideToast(options?: T): void + } +} + +export {} // 防止模块污染 diff --git a/src/typings.ts b/src/typings.ts new file mode 100644 index 0000000..b48b630 --- /dev/null +++ b/src/typings.ts @@ -0,0 +1,15 @@ +// 枚举定义 + +export enum TestEnum { + A = '1', + B = '2', +} + +// uni.uploadFile文件上传参数 +export interface IUniUploadFileOptions { + file?: File + files?: UniApp.UploadFileOptionFiles[] + filePath?: string + name?: string + formData?: any +} diff --git a/src/uni.scss b/src/uni.scss new file mode 100644 index 0000000..21b9e5f --- /dev/null +++ b/src/uni.scss @@ -0,0 +1,77 @@ +/* stylelint-disable comment-empty-line-before */ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量 */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color: #333; // 基本色 +$uni-text-color-inverse: #fff; // 反色 +$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable: #c0c0c0; + +/* 背景颜色 */ +$uni-bg-color: #fff; +$uni-bg-color-grey: #f8f8f8; +$uni-bg-color-hover: #f1f1f1; // 点击状态颜色 +$uni-bg-color-mask: rgb(0 0 0 / 40%); // 遮罩颜色 + +/* 边框颜色 */ +$uni-border-color: #c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm: 12px; +$uni-font-size-base: 14px; +$uni-font-size-lg: 16; + +/* 图片尺寸 */ +$uni-img-size-sm: 20px; +$uni-img-size-base: 26px; +$uni-img-size-lg: 40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2c405a; // 文章标题颜色 +$uni-font-size-title: 20px; +$uni-color-subtitle: #555; // 二级标题颜色 +$uni-font-size-subtitle: 18px; +$uni-color-paragraph: #3f536e; // 文章段落颜色 +$uni-font-size-paragraph: 15px; diff --git a/src/uni_modules/.gitkeep b/src/uni_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/uni_modules/uni-icons/changelog.md b/src/uni_modules/uni-icons/changelog.md new file mode 100644 index 0000000..0261131 --- /dev/null +++ b/src/uni_modules/uni-icons/changelog.md @@ -0,0 +1,42 @@ +## 2.0.10(2024-06-07) +- 优化 uni-app x 中,size 属性的类型 +## 2.0.9(2024-01-12) +fix: 修复图标大小默认值错误的问题 +## 2.0.8(2023-12-14) +- 修复 项目未使用 ts 情况下,打包报错的bug +## 2.0.7(2023-12-14) +- 修复 size 属性为 string 时,不加单位导致尺寸异常的bug +## 2.0.6(2023-12-11) +- 优化 兼容老版本icon类型,如 top ,bottom 等 +## 2.0.5(2023-12-11) +- 优化 兼容老版本icon类型,如 top ,bottom 等 +## 2.0.4(2023-12-06) +- 优化 uni-app x 下示例项目图标排序 +## 2.0.3(2023-12-06) +- 修复 nvue下引入组件报错的bug +## 2.0.2(2023-12-05) +-优化 size 属性支持单位 +## 2.0.1(2023-12-05) +- 新增 uni-app x 支持定义图标 +## 1.3.5(2022-01-24) +- 优化 size 属性可以传入不带单位的字符串数值 +## 1.3.4(2022-01-24) +- 优化 size 支持其他单位 +## 1.3.3(2022-01-17) +- 修复 nvue 有些图标不显示的bug,兼容老版本图标 +## 1.3.2(2021-12-01) +- 优化 示例可复制图标名称 +## 1.3.1(2021-11-23) +- 优化 兼容旧组件 type 值 +## 1.3.0(2021-11-19) +- 新增 更多图标 +- 优化 自定义图标使用方式 +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons) +## 1.1.7(2021-11-08) +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.5(2021-05-12) +- 新增 组件示例地址 +## 1.1.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/src/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue b/src/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue new file mode 100644 index 0000000..8740559 --- /dev/null +++ b/src/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/uni_modules/uni-icons/components/uni-icons/uni-icons.vue b/src/uni_modules/uni-icons/components/uni-icons/uni-icons.vue new file mode 100644 index 0000000..7da5356 --- /dev/null +++ b/src/uni_modules/uni-icons/components/uni-icons/uni-icons.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/uni_modules/uni-icons/components/uni-icons/uniicons.css b/src/uni_modules/uni-icons/components/uni-icons/uniicons.css new file mode 100644 index 0000000..0a6b6fe --- /dev/null +++ b/src/uni_modules/uni-icons/components/uni-icons/uniicons.css @@ -0,0 +1,664 @@ + +.uniui-cart-filled:before { + content: "\e6d0"; +} + +.uniui-gift-filled:before { + content: "\e6c4"; +} + +.uniui-color:before { + content: "\e6cf"; +} + +.uniui-wallet:before { + content: "\e6b1"; +} + +.uniui-settings-filled:before { + content: "\e6ce"; +} + +.uniui-auth-filled:before { + content: "\e6cc"; +} + +.uniui-shop-filled:before { + content: "\e6cd"; +} + +.uniui-staff-filled:before { + content: "\e6cb"; +} + +.uniui-vip-filled:before { + content: "\e6c6"; +} + +.uniui-plus-filled:before { + content: "\e6c7"; +} + +.uniui-folder-add-filled:before { + content: "\e6c8"; +} + +.uniui-color-filled:before { + content: "\e6c9"; +} + +.uniui-tune-filled:before { + content: "\e6ca"; +} + +.uniui-calendar-filled:before { + content: "\e6c0"; +} + +.uniui-notification-filled:before { + content: "\e6c1"; +} + +.uniui-wallet-filled:before { + content: "\e6c2"; +} + +.uniui-medal-filled:before { + content: "\e6c3"; +} + +.uniui-fire-filled:before { + content: "\e6c5"; +} + +.uniui-refreshempty:before { + content: "\e6bf"; +} + +.uniui-location-filled:before { + content: "\e6af"; +} + +.uniui-person-filled:before { + content: "\e69d"; +} + +.uniui-personadd-filled:before { + content: "\e698"; +} + +.uniui-arrowthinleft:before { + content: "\e6d2"; +} + +.uniui-arrowthinup:before { + content: "\e6d3"; +} + +.uniui-arrowthindown:before { + content: "\e6d4"; +} + +.uniui-back:before { + content: "\e6b9"; +} + +.uniui-forward:before { + content: "\e6ba"; +} + +.uniui-arrow-right:before { + content: "\e6bb"; +} + +.uniui-arrow-left:before { + content: "\e6bc"; +} + +.uniui-arrow-up:before { + content: "\e6bd"; +} + +.uniui-arrow-down:before { + content: "\e6be"; +} + +.uniui-arrowthinright:before { + content: "\e6d1"; +} + +.uniui-down:before { + content: "\e6b8"; +} + +.uniui-bottom:before { + content: "\e6b8"; +} + +.uniui-arrowright:before { + content: "\e6d5"; +} + +.uniui-right:before { + content: "\e6b5"; +} + +.uniui-up:before { + content: "\e6b6"; +} + +.uniui-top:before { + content: "\e6b6"; +} + +.uniui-left:before { + content: "\e6b7"; +} + +.uniui-arrowup:before { + content: "\e6d6"; +} + +.uniui-eye:before { + content: "\e651"; +} + +.uniui-eye-filled:before { + content: "\e66a"; +} + +.uniui-eye-slash:before { + content: "\e6b3"; +} + +.uniui-eye-slash-filled:before { + content: "\e6b4"; +} + +.uniui-info-filled:before { + content: "\e649"; +} + +.uniui-reload:before { + content: "\e6b2"; +} + +.uniui-micoff-filled:before { + content: "\e6b0"; +} + +.uniui-map-pin-ellipse:before { + content: "\e6ac"; +} + +.uniui-map-pin:before { + content: "\e6ad"; +} + +.uniui-location:before { + content: "\e6ae"; +} + +.uniui-starhalf:before { + content: "\e683"; +} + +.uniui-star:before { + content: "\e688"; +} + +.uniui-star-filled:before { + content: "\e68f"; +} + +.uniui-calendar:before { + content: "\e6a0"; +} + +.uniui-fire:before { + content: "\e6a1"; +} + +.uniui-medal:before { + content: "\e6a2"; +} + +.uniui-font:before { + content: "\e6a3"; +} + +.uniui-gift:before { + content: "\e6a4"; +} + +.uniui-link:before { + content: "\e6a5"; +} + +.uniui-notification:before { + content: "\e6a6"; +} + +.uniui-staff:before { + content: "\e6a7"; +} + +.uniui-vip:before { + content: "\e6a8"; +} + +.uniui-folder-add:before { + content: "\e6a9"; +} + +.uniui-tune:before { + content: "\e6aa"; +} + +.uniui-auth:before { + content: "\e6ab"; +} + +.uniui-person:before { + content: "\e699"; +} + +.uniui-email-filled:before { + content: "\e69a"; +} + +.uniui-phone-filled:before { + content: "\e69b"; +} + +.uniui-phone:before { + content: "\e69c"; +} + +.uniui-email:before { + content: "\e69e"; +} + +.uniui-personadd:before { + content: "\e69f"; +} + +.uniui-chatboxes-filled:before { + content: "\e692"; +} + +.uniui-contact:before { + content: "\e693"; +} + +.uniui-chatbubble-filled:before { + content: "\e694"; +} + +.uniui-contact-filled:before { + content: "\e695"; +} + +.uniui-chatboxes:before { + content: "\e696"; +} + +.uniui-chatbubble:before { + content: "\e697"; +} + +.uniui-upload-filled:before { + content: "\e68e"; +} + +.uniui-upload:before { + content: "\e690"; +} + +.uniui-weixin:before { + content: "\e691"; +} + +.uniui-compose:before { + content: "\e67f"; +} + +.uniui-qq:before { + content: "\e680"; +} + +.uniui-download-filled:before { + content: "\e681"; +} + +.uniui-pyq:before { + content: "\e682"; +} + +.uniui-sound:before { + content: "\e684"; +} + +.uniui-trash-filled:before { + content: "\e685"; +} + +.uniui-sound-filled:before { + content: "\e686"; +} + +.uniui-trash:before { + content: "\e687"; +} + +.uniui-videocam-filled:before { + content: "\e689"; +} + +.uniui-spinner-cycle:before { + content: "\e68a"; +} + +.uniui-weibo:before { + content: "\e68b"; +} + +.uniui-videocam:before { + content: "\e68c"; +} + +.uniui-download:before { + content: "\e68d"; +} + +.uniui-help:before { + content: "\e679"; +} + +.uniui-navigate-filled:before { + content: "\e67a"; +} + +.uniui-plusempty:before { + content: "\e67b"; +} + +.uniui-smallcircle:before { + content: "\e67c"; +} + +.uniui-minus-filled:before { + content: "\e67d"; +} + +.uniui-micoff:before { + content: "\e67e"; +} + +.uniui-closeempty:before { + content: "\e66c"; +} + +.uniui-clear:before { + content: "\e66d"; +} + +.uniui-navigate:before { + content: "\e66e"; +} + +.uniui-minus:before { + content: "\e66f"; +} + +.uniui-image:before { + content: "\e670"; +} + +.uniui-mic:before { + content: "\e671"; +} + +.uniui-paperplane:before { + content: "\e672"; +} + +.uniui-close:before { + content: "\e673"; +} + +.uniui-help-filled:before { + content: "\e674"; +} + +.uniui-paperplane-filled:before { + content: "\e675"; +} + +.uniui-plus:before { + content: "\e676"; +} + +.uniui-mic-filled:before { + content: "\e677"; +} + +.uniui-image-filled:before { + content: "\e678"; +} + +.uniui-locked-filled:before { + content: "\e668"; +} + +.uniui-info:before { + content: "\e669"; +} + +.uniui-locked:before { + content: "\e66b"; +} + +.uniui-camera-filled:before { + content: "\e658"; +} + +.uniui-chat-filled:before { + content: "\e659"; +} + +.uniui-camera:before { + content: "\e65a"; +} + +.uniui-circle:before { + content: "\e65b"; +} + +.uniui-checkmarkempty:before { + content: "\e65c"; +} + +.uniui-chat:before { + content: "\e65d"; +} + +.uniui-circle-filled:before { + content: "\e65e"; +} + +.uniui-flag:before { + content: "\e65f"; +} + +.uniui-flag-filled:before { + content: "\e660"; +} + +.uniui-gear-filled:before { + content: "\e661"; +} + +.uniui-home:before { + content: "\e662"; +} + +.uniui-home-filled:before { + content: "\e663"; +} + +.uniui-gear:before { + content: "\e664"; +} + +.uniui-smallcircle-filled:before { + content: "\e665"; +} + +.uniui-map-filled:before { + content: "\e666"; +} + +.uniui-map:before { + content: "\e667"; +} + +.uniui-refresh-filled:before { + content: "\e656"; +} + +.uniui-refresh:before { + content: "\e657"; +} + +.uniui-cloud-upload:before { + content: "\e645"; +} + +.uniui-cloud-download-filled:before { + content: "\e646"; +} + +.uniui-cloud-download:before { + content: "\e647"; +} + +.uniui-cloud-upload-filled:before { + content: "\e648"; +} + +.uniui-redo:before { + content: "\e64a"; +} + +.uniui-images-filled:before { + content: "\e64b"; +} + +.uniui-undo-filled:before { + content: "\e64c"; +} + +.uniui-more:before { + content: "\e64d"; +} + +.uniui-more-filled:before { + content: "\e64e"; +} + +.uniui-undo:before { + content: "\e64f"; +} + +.uniui-images:before { + content: "\e650"; +} + +.uniui-paperclip:before { + content: "\e652"; +} + +.uniui-settings:before { + content: "\e653"; +} + +.uniui-search:before { + content: "\e654"; +} + +.uniui-redo-filled:before { + content: "\e655"; +} + +.uniui-list:before { + content: "\e644"; +} + +.uniui-mail-open-filled:before { + content: "\e63a"; +} + +.uniui-hand-down-filled:before { + content: "\e63c"; +} + +.uniui-hand-down:before { + content: "\e63d"; +} + +.uniui-hand-up-filled:before { + content: "\e63e"; +} + +.uniui-hand-up:before { + content: "\e63f"; +} + +.uniui-heart-filled:before { + content: "\e641"; +} + +.uniui-mail-open:before { + content: "\e643"; +} + +.uniui-heart:before { + content: "\e639"; +} + +.uniui-loop:before { + content: "\e633"; +} + +.uniui-pulldown:before { + content: "\e632"; +} + +.uniui-scan:before { + content: "\e62a"; +} + +.uniui-bars:before { + content: "\e627"; +} + +.uniui-checkbox:before { + content: "\e62b"; +} + +.uniui-checkbox-filled:before { + content: "\e62c"; +} + +.uniui-shop:before { + content: "\e62f"; +} + +.uniui-headphones:before { + content: "\e630"; +} + +.uniui-cart:before { + content: "\e631"; +} diff --git a/src/uni_modules/uni-icons/components/uni-icons/uniicons.ttf b/src/uni_modules/uni-icons/components/uni-icons/uniicons.ttf new file mode 100644 index 0000000..14696d0 Binary files /dev/null and b/src/uni_modules/uni-icons/components/uni-icons/uniicons.ttf differ diff --git a/src/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts b/src/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts new file mode 100644 index 0000000..98e93aa --- /dev/null +++ b/src/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts @@ -0,0 +1,664 @@ + +export type IconsData = { + id : string + name : string + font_family : string + css_prefix_text : string + description : string + glyphs : Array +} + +export type IconsDataItem = { + font_class : string + unicode : string +} + + +export const fontData = [ + { + "font_class": "arrow-down", + "unicode": "\ue6be" + }, + { + "font_class": "arrow-left", + "unicode": "\ue6bc" + }, + { + "font_class": "arrow-right", + "unicode": "\ue6bb" + }, + { + "font_class": "arrow-up", + "unicode": "\ue6bd" + }, + { + "font_class": "auth", + "unicode": "\ue6ab" + }, + { + "font_class": "auth-filled", + "unicode": "\ue6cc" + }, + { + "font_class": "back", + "unicode": "\ue6b9" + }, + { + "font_class": "bars", + "unicode": "\ue627" + }, + { + "font_class": "calendar", + "unicode": "\ue6a0" + }, + { + "font_class": "calendar-filled", + "unicode": "\ue6c0" + }, + { + "font_class": "camera", + "unicode": "\ue65a" + }, + { + "font_class": "camera-filled", + "unicode": "\ue658" + }, + { + "font_class": "cart", + "unicode": "\ue631" + }, + { + "font_class": "cart-filled", + "unicode": "\ue6d0" + }, + { + "font_class": "chat", + "unicode": "\ue65d" + }, + { + "font_class": "chat-filled", + "unicode": "\ue659" + }, + { + "font_class": "chatboxes", + "unicode": "\ue696" + }, + { + "font_class": "chatboxes-filled", + "unicode": "\ue692" + }, + { + "font_class": "chatbubble", + "unicode": "\ue697" + }, + { + "font_class": "chatbubble-filled", + "unicode": "\ue694" + }, + { + "font_class": "checkbox", + "unicode": "\ue62b" + }, + { + "font_class": "checkbox-filled", + "unicode": "\ue62c" + }, + { + "font_class": "checkmarkempty", + "unicode": "\ue65c" + }, + { + "font_class": "circle", + "unicode": "\ue65b" + }, + { + "font_class": "circle-filled", + "unicode": "\ue65e" + }, + { + "font_class": "clear", + "unicode": "\ue66d" + }, + { + "font_class": "close", + "unicode": "\ue673" + }, + { + "font_class": "closeempty", + "unicode": "\ue66c" + }, + { + "font_class": "cloud-download", + "unicode": "\ue647" + }, + { + "font_class": "cloud-download-filled", + "unicode": "\ue646" + }, + { + "font_class": "cloud-upload", + "unicode": "\ue645" + }, + { + "font_class": "cloud-upload-filled", + "unicode": "\ue648" + }, + { + "font_class": "color", + "unicode": "\ue6cf" + }, + { + "font_class": "color-filled", + "unicode": "\ue6c9" + }, + { + "font_class": "compose", + "unicode": "\ue67f" + }, + { + "font_class": "contact", + "unicode": "\ue693" + }, + { + "font_class": "contact-filled", + "unicode": "\ue695" + }, + { + "font_class": "down", + "unicode": "\ue6b8" + }, + { + "font_class": "bottom", + "unicode": "\ue6b8" + }, + { + "font_class": "download", + "unicode": "\ue68d" + }, + { + "font_class": "download-filled", + "unicode": "\ue681" + }, + { + "font_class": "email", + "unicode": "\ue69e" + }, + { + "font_class": "email-filled", + "unicode": "\ue69a" + }, + { + "font_class": "eye", + "unicode": "\ue651" + }, + { + "font_class": "eye-filled", + "unicode": "\ue66a" + }, + { + "font_class": "eye-slash", + "unicode": "\ue6b3" + }, + { + "font_class": "eye-slash-filled", + "unicode": "\ue6b4" + }, + { + "font_class": "fire", + "unicode": "\ue6a1" + }, + { + "font_class": "fire-filled", + "unicode": "\ue6c5" + }, + { + "font_class": "flag", + "unicode": "\ue65f" + }, + { + "font_class": "flag-filled", + "unicode": "\ue660" + }, + { + "font_class": "folder-add", + "unicode": "\ue6a9" + }, + { + "font_class": "folder-add-filled", + "unicode": "\ue6c8" + }, + { + "font_class": "font", + "unicode": "\ue6a3" + }, + { + "font_class": "forward", + "unicode": "\ue6ba" + }, + { + "font_class": "gear", + "unicode": "\ue664" + }, + { + "font_class": "gear-filled", + "unicode": "\ue661" + }, + { + "font_class": "gift", + "unicode": "\ue6a4" + }, + { + "font_class": "gift-filled", + "unicode": "\ue6c4" + }, + { + "font_class": "hand-down", + "unicode": "\ue63d" + }, + { + "font_class": "hand-down-filled", + "unicode": "\ue63c" + }, + { + "font_class": "hand-up", + "unicode": "\ue63f" + }, + { + "font_class": "hand-up-filled", + "unicode": "\ue63e" + }, + { + "font_class": "headphones", + "unicode": "\ue630" + }, + { + "font_class": "heart", + "unicode": "\ue639" + }, + { + "font_class": "heart-filled", + "unicode": "\ue641" + }, + { + "font_class": "help", + "unicode": "\ue679" + }, + { + "font_class": "help-filled", + "unicode": "\ue674" + }, + { + "font_class": "home", + "unicode": "\ue662" + }, + { + "font_class": "home-filled", + "unicode": "\ue663" + }, + { + "font_class": "image", + "unicode": "\ue670" + }, + { + "font_class": "image-filled", + "unicode": "\ue678" + }, + { + "font_class": "images", + "unicode": "\ue650" + }, + { + "font_class": "images-filled", + "unicode": "\ue64b" + }, + { + "font_class": "info", + "unicode": "\ue669" + }, + { + "font_class": "info-filled", + "unicode": "\ue649" + }, + { + "font_class": "left", + "unicode": "\ue6b7" + }, + { + "font_class": "link", + "unicode": "\ue6a5" + }, + { + "font_class": "list", + "unicode": "\ue644" + }, + { + "font_class": "location", + "unicode": "\ue6ae" + }, + { + "font_class": "location-filled", + "unicode": "\ue6af" + }, + { + "font_class": "locked", + "unicode": "\ue66b" + }, + { + "font_class": "locked-filled", + "unicode": "\ue668" + }, + { + "font_class": "loop", + "unicode": "\ue633" + }, + { + "font_class": "mail-open", + "unicode": "\ue643" + }, + { + "font_class": "mail-open-filled", + "unicode": "\ue63a" + }, + { + "font_class": "map", + "unicode": "\ue667" + }, + { + "font_class": "map-filled", + "unicode": "\ue666" + }, + { + "font_class": "map-pin", + "unicode": "\ue6ad" + }, + { + "font_class": "map-pin-ellipse", + "unicode": "\ue6ac" + }, + { + "font_class": "medal", + "unicode": "\ue6a2" + }, + { + "font_class": "medal-filled", + "unicode": "\ue6c3" + }, + { + "font_class": "mic", + "unicode": "\ue671" + }, + { + "font_class": "mic-filled", + "unicode": "\ue677" + }, + { + "font_class": "micoff", + "unicode": "\ue67e" + }, + { + "font_class": "micoff-filled", + "unicode": "\ue6b0" + }, + { + "font_class": "minus", + "unicode": "\ue66f" + }, + { + "font_class": "minus-filled", + "unicode": "\ue67d" + }, + { + "font_class": "more", + "unicode": "\ue64d" + }, + { + "font_class": "more-filled", + "unicode": "\ue64e" + }, + { + "font_class": "navigate", + "unicode": "\ue66e" + }, + { + "font_class": "navigate-filled", + "unicode": "\ue67a" + }, + { + "font_class": "notification", + "unicode": "\ue6a6" + }, + { + "font_class": "notification-filled", + "unicode": "\ue6c1" + }, + { + "font_class": "paperclip", + "unicode": "\ue652" + }, + { + "font_class": "paperplane", + "unicode": "\ue672" + }, + { + "font_class": "paperplane-filled", + "unicode": "\ue675" + }, + { + "font_class": "person", + "unicode": "\ue699" + }, + { + "font_class": "person-filled", + "unicode": "\ue69d" + }, + { + "font_class": "personadd", + "unicode": "\ue69f" + }, + { + "font_class": "personadd-filled", + "unicode": "\ue698" + }, + { + "font_class": "personadd-filled-copy", + "unicode": "\ue6d1" + }, + { + "font_class": "phone", + "unicode": "\ue69c" + }, + { + "font_class": "phone-filled", + "unicode": "\ue69b" + }, + { + "font_class": "plus", + "unicode": "\ue676" + }, + { + "font_class": "plus-filled", + "unicode": "\ue6c7" + }, + { + "font_class": "plusempty", + "unicode": "\ue67b" + }, + { + "font_class": "pulldown", + "unicode": "\ue632" + }, + { + "font_class": "pyq", + "unicode": "\ue682" + }, + { + "font_class": "qq", + "unicode": "\ue680" + }, + { + "font_class": "redo", + "unicode": "\ue64a" + }, + { + "font_class": "redo-filled", + "unicode": "\ue655" + }, + { + "font_class": "refresh", + "unicode": "\ue657" + }, + { + "font_class": "refresh-filled", + "unicode": "\ue656" + }, + { + "font_class": "refreshempty", + "unicode": "\ue6bf" + }, + { + "font_class": "reload", + "unicode": "\ue6b2" + }, + { + "font_class": "right", + "unicode": "\ue6b5" + }, + { + "font_class": "scan", + "unicode": "\ue62a" + }, + { + "font_class": "search", + "unicode": "\ue654" + }, + { + "font_class": "settings", + "unicode": "\ue653" + }, + { + "font_class": "settings-filled", + "unicode": "\ue6ce" + }, + { + "font_class": "shop", + "unicode": "\ue62f" + }, + { + "font_class": "shop-filled", + "unicode": "\ue6cd" + }, + { + "font_class": "smallcircle", + "unicode": "\ue67c" + }, + { + "font_class": "smallcircle-filled", + "unicode": "\ue665" + }, + { + "font_class": "sound", + "unicode": "\ue684" + }, + { + "font_class": "sound-filled", + "unicode": "\ue686" + }, + { + "font_class": "spinner-cycle", + "unicode": "\ue68a" + }, + { + "font_class": "staff", + "unicode": "\ue6a7" + }, + { + "font_class": "staff-filled", + "unicode": "\ue6cb" + }, + { + "font_class": "star", + "unicode": "\ue688" + }, + { + "font_class": "star-filled", + "unicode": "\ue68f" + }, + { + "font_class": "starhalf", + "unicode": "\ue683" + }, + { + "font_class": "trash", + "unicode": "\ue687" + }, + { + "font_class": "trash-filled", + "unicode": "\ue685" + }, + { + "font_class": "tune", + "unicode": "\ue6aa" + }, + { + "font_class": "tune-filled", + "unicode": "\ue6ca" + }, + { + "font_class": "undo", + "unicode": "\ue64f" + }, + { + "font_class": "undo-filled", + "unicode": "\ue64c" + }, + { + "font_class": "up", + "unicode": "\ue6b6" + }, + { + "font_class": "top", + "unicode": "\ue6b6" + }, + { + "font_class": "upload", + "unicode": "\ue690" + }, + { + "font_class": "upload-filled", + "unicode": "\ue68e" + }, + { + "font_class": "videocam", + "unicode": "\ue68c" + }, + { + "font_class": "videocam-filled", + "unicode": "\ue689" + }, + { + "font_class": "vip", + "unicode": "\ue6a8" + }, + { + "font_class": "vip-filled", + "unicode": "\ue6c6" + }, + { + "font_class": "wallet", + "unicode": "\ue6b1" + }, + { + "font_class": "wallet-filled", + "unicode": "\ue6c2" + }, + { + "font_class": "weibo", + "unicode": "\ue68b" + }, + { + "font_class": "weixin", + "unicode": "\ue691" + } +] as IconsDataItem[] + +// export const fontData = JSON.parse(fontDataJson) diff --git a/src/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js b/src/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js new file mode 100644 index 0000000..1cd11e1 --- /dev/null +++ b/src/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js @@ -0,0 +1,649 @@ + +export const fontData = [ + { + "font_class": "arrow-down", + "unicode": "\ue6be" + }, + { + "font_class": "arrow-left", + "unicode": "\ue6bc" + }, + { + "font_class": "arrow-right", + "unicode": "\ue6bb" + }, + { + "font_class": "arrow-up", + "unicode": "\ue6bd" + }, + { + "font_class": "auth", + "unicode": "\ue6ab" + }, + { + "font_class": "auth-filled", + "unicode": "\ue6cc" + }, + { + "font_class": "back", + "unicode": "\ue6b9" + }, + { + "font_class": "bars", + "unicode": "\ue627" + }, + { + "font_class": "calendar", + "unicode": "\ue6a0" + }, + { + "font_class": "calendar-filled", + "unicode": "\ue6c0" + }, + { + "font_class": "camera", + "unicode": "\ue65a" + }, + { + "font_class": "camera-filled", + "unicode": "\ue658" + }, + { + "font_class": "cart", + "unicode": "\ue631" + }, + { + "font_class": "cart-filled", + "unicode": "\ue6d0" + }, + { + "font_class": "chat", + "unicode": "\ue65d" + }, + { + "font_class": "chat-filled", + "unicode": "\ue659" + }, + { + "font_class": "chatboxes", + "unicode": "\ue696" + }, + { + "font_class": "chatboxes-filled", + "unicode": "\ue692" + }, + { + "font_class": "chatbubble", + "unicode": "\ue697" + }, + { + "font_class": "chatbubble-filled", + "unicode": "\ue694" + }, + { + "font_class": "checkbox", + "unicode": "\ue62b" + }, + { + "font_class": "checkbox-filled", + "unicode": "\ue62c" + }, + { + "font_class": "checkmarkempty", + "unicode": "\ue65c" + }, + { + "font_class": "circle", + "unicode": "\ue65b" + }, + { + "font_class": "circle-filled", + "unicode": "\ue65e" + }, + { + "font_class": "clear", + "unicode": "\ue66d" + }, + { + "font_class": "close", + "unicode": "\ue673" + }, + { + "font_class": "closeempty", + "unicode": "\ue66c" + }, + { + "font_class": "cloud-download", + "unicode": "\ue647" + }, + { + "font_class": "cloud-download-filled", + "unicode": "\ue646" + }, + { + "font_class": "cloud-upload", + "unicode": "\ue645" + }, + { + "font_class": "cloud-upload-filled", + "unicode": "\ue648" + }, + { + "font_class": "color", + "unicode": "\ue6cf" + }, + { + "font_class": "color-filled", + "unicode": "\ue6c9" + }, + { + "font_class": "compose", + "unicode": "\ue67f" + }, + { + "font_class": "contact", + "unicode": "\ue693" + }, + { + "font_class": "contact-filled", + "unicode": "\ue695" + }, + { + "font_class": "down", + "unicode": "\ue6b8" + }, + { + "font_class": "bottom", + "unicode": "\ue6b8" + }, + { + "font_class": "download", + "unicode": "\ue68d" + }, + { + "font_class": "download-filled", + "unicode": "\ue681" + }, + { + "font_class": "email", + "unicode": "\ue69e" + }, + { + "font_class": "email-filled", + "unicode": "\ue69a" + }, + { + "font_class": "eye", + "unicode": "\ue651" + }, + { + "font_class": "eye-filled", + "unicode": "\ue66a" + }, + { + "font_class": "eye-slash", + "unicode": "\ue6b3" + }, + { + "font_class": "eye-slash-filled", + "unicode": "\ue6b4" + }, + { + "font_class": "fire", + "unicode": "\ue6a1" + }, + { + "font_class": "fire-filled", + "unicode": "\ue6c5" + }, + { + "font_class": "flag", + "unicode": "\ue65f" + }, + { + "font_class": "flag-filled", + "unicode": "\ue660" + }, + { + "font_class": "folder-add", + "unicode": "\ue6a9" + }, + { + "font_class": "folder-add-filled", + "unicode": "\ue6c8" + }, + { + "font_class": "font", + "unicode": "\ue6a3" + }, + { + "font_class": "forward", + "unicode": "\ue6ba" + }, + { + "font_class": "gear", + "unicode": "\ue664" + }, + { + "font_class": "gear-filled", + "unicode": "\ue661" + }, + { + "font_class": "gift", + "unicode": "\ue6a4" + }, + { + "font_class": "gift-filled", + "unicode": "\ue6c4" + }, + { + "font_class": "hand-down", + "unicode": "\ue63d" + }, + { + "font_class": "hand-down-filled", + "unicode": "\ue63c" + }, + { + "font_class": "hand-up", + "unicode": "\ue63f" + }, + { + "font_class": "hand-up-filled", + "unicode": "\ue63e" + }, + { + "font_class": "headphones", + "unicode": "\ue630" + }, + { + "font_class": "heart", + "unicode": "\ue639" + }, + { + "font_class": "heart-filled", + "unicode": "\ue641" + }, + { + "font_class": "help", + "unicode": "\ue679" + }, + { + "font_class": "help-filled", + "unicode": "\ue674" + }, + { + "font_class": "home", + "unicode": "\ue662" + }, + { + "font_class": "home-filled", + "unicode": "\ue663" + }, + { + "font_class": "image", + "unicode": "\ue670" + }, + { + "font_class": "image-filled", + "unicode": "\ue678" + }, + { + "font_class": "images", + "unicode": "\ue650" + }, + { + "font_class": "images-filled", + "unicode": "\ue64b" + }, + { + "font_class": "info", + "unicode": "\ue669" + }, + { + "font_class": "info-filled", + "unicode": "\ue649" + }, + { + "font_class": "left", + "unicode": "\ue6b7" + }, + { + "font_class": "link", + "unicode": "\ue6a5" + }, + { + "font_class": "list", + "unicode": "\ue644" + }, + { + "font_class": "location", + "unicode": "\ue6ae" + }, + { + "font_class": "location-filled", + "unicode": "\ue6af" + }, + { + "font_class": "locked", + "unicode": "\ue66b" + }, + { + "font_class": "locked-filled", + "unicode": "\ue668" + }, + { + "font_class": "loop", + "unicode": "\ue633" + }, + { + "font_class": "mail-open", + "unicode": "\ue643" + }, + { + "font_class": "mail-open-filled", + "unicode": "\ue63a" + }, + { + "font_class": "map", + "unicode": "\ue667" + }, + { + "font_class": "map-filled", + "unicode": "\ue666" + }, + { + "font_class": "map-pin", + "unicode": "\ue6ad" + }, + { + "font_class": "map-pin-ellipse", + "unicode": "\ue6ac" + }, + { + "font_class": "medal", + "unicode": "\ue6a2" + }, + { + "font_class": "medal-filled", + "unicode": "\ue6c3" + }, + { + "font_class": "mic", + "unicode": "\ue671" + }, + { + "font_class": "mic-filled", + "unicode": "\ue677" + }, + { + "font_class": "micoff", + "unicode": "\ue67e" + }, + { + "font_class": "micoff-filled", + "unicode": "\ue6b0" + }, + { + "font_class": "minus", + "unicode": "\ue66f" + }, + { + "font_class": "minus-filled", + "unicode": "\ue67d" + }, + { + "font_class": "more", + "unicode": "\ue64d" + }, + { + "font_class": "more-filled", + "unicode": "\ue64e" + }, + { + "font_class": "navigate", + "unicode": "\ue66e" + }, + { + "font_class": "navigate-filled", + "unicode": "\ue67a" + }, + { + "font_class": "notification", + "unicode": "\ue6a6" + }, + { + "font_class": "notification-filled", + "unicode": "\ue6c1" + }, + { + "font_class": "paperclip", + "unicode": "\ue652" + }, + { + "font_class": "paperplane", + "unicode": "\ue672" + }, + { + "font_class": "paperplane-filled", + "unicode": "\ue675" + }, + { + "font_class": "person", + "unicode": "\ue699" + }, + { + "font_class": "person-filled", + "unicode": "\ue69d" + }, + { + "font_class": "personadd", + "unicode": "\ue69f" + }, + { + "font_class": "personadd-filled", + "unicode": "\ue698" + }, + { + "font_class": "personadd-filled-copy", + "unicode": "\ue6d1" + }, + { + "font_class": "phone", + "unicode": "\ue69c" + }, + { + "font_class": "phone-filled", + "unicode": "\ue69b" + }, + { + "font_class": "plus", + "unicode": "\ue676" + }, + { + "font_class": "plus-filled", + "unicode": "\ue6c7" + }, + { + "font_class": "plusempty", + "unicode": "\ue67b" + }, + { + "font_class": "pulldown", + "unicode": "\ue632" + }, + { + "font_class": "pyq", + "unicode": "\ue682" + }, + { + "font_class": "qq", + "unicode": "\ue680" + }, + { + "font_class": "redo", + "unicode": "\ue64a" + }, + { + "font_class": "redo-filled", + "unicode": "\ue655" + }, + { + "font_class": "refresh", + "unicode": "\ue657" + }, + { + "font_class": "refresh-filled", + "unicode": "\ue656" + }, + { + "font_class": "refreshempty", + "unicode": "\ue6bf" + }, + { + "font_class": "reload", + "unicode": "\ue6b2" + }, + { + "font_class": "right", + "unicode": "\ue6b5" + }, + { + "font_class": "scan", + "unicode": "\ue62a" + }, + { + "font_class": "search", + "unicode": "\ue654" + }, + { + "font_class": "settings", + "unicode": "\ue653" + }, + { + "font_class": "settings-filled", + "unicode": "\ue6ce" + }, + { + "font_class": "shop", + "unicode": "\ue62f" + }, + { + "font_class": "shop-filled", + "unicode": "\ue6cd" + }, + { + "font_class": "smallcircle", + "unicode": "\ue67c" + }, + { + "font_class": "smallcircle-filled", + "unicode": "\ue665" + }, + { + "font_class": "sound", + "unicode": "\ue684" + }, + { + "font_class": "sound-filled", + "unicode": "\ue686" + }, + { + "font_class": "spinner-cycle", + "unicode": "\ue68a" + }, + { + "font_class": "staff", + "unicode": "\ue6a7" + }, + { + "font_class": "staff-filled", + "unicode": "\ue6cb" + }, + { + "font_class": "star", + "unicode": "\ue688" + }, + { + "font_class": "star-filled", + "unicode": "\ue68f" + }, + { + "font_class": "starhalf", + "unicode": "\ue683" + }, + { + "font_class": "trash", + "unicode": "\ue687" + }, + { + "font_class": "trash-filled", + "unicode": "\ue685" + }, + { + "font_class": "tune", + "unicode": "\ue6aa" + }, + { + "font_class": "tune-filled", + "unicode": "\ue6ca" + }, + { + "font_class": "undo", + "unicode": "\ue64f" + }, + { + "font_class": "undo-filled", + "unicode": "\ue64c" + }, + { + "font_class": "up", + "unicode": "\ue6b6" + }, + { + "font_class": "top", + "unicode": "\ue6b6" + }, + { + "font_class": "upload", + "unicode": "\ue690" + }, + { + "font_class": "upload-filled", + "unicode": "\ue68e" + }, + { + "font_class": "videocam", + "unicode": "\ue68c" + }, + { + "font_class": "videocam-filled", + "unicode": "\ue689" + }, + { + "font_class": "vip", + "unicode": "\ue6a8" + }, + { + "font_class": "vip-filled", + "unicode": "\ue6c6" + }, + { + "font_class": "wallet", + "unicode": "\ue6b1" + }, + { + "font_class": "wallet-filled", + "unicode": "\ue6c2" + }, + { + "font_class": "weibo", + "unicode": "\ue68b" + }, + { + "font_class": "weixin", + "unicode": "\ue691" + } +] + +// export const fontData = JSON.parse(fontDataJson) diff --git a/src/uni_modules/uni-icons/package.json b/src/uni_modules/uni-icons/package.json new file mode 100644 index 0000000..6b681b4 --- /dev/null +++ b/src/uni_modules/uni-icons/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-icons", + "displayName": "uni-icons 图标", + "version": "2.0.10", + "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。", + "keywords": [ + "uni-ui", + "uniui", + "icon", + "图标" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.2.14" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y", + "alipay": "n" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y", + "app-uvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "钉钉": "y", + "快手": "y", + "飞书": "y", + "京东": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/src/uni_modules/uni-icons/readme.md b/src/uni_modules/uni-icons/readme.md new file mode 100644 index 0000000..86234ba --- /dev/null +++ b/src/uni_modules/uni-icons/readme.md @@ -0,0 +1,8 @@ +## Icons 图标 +> **组件名:uni-icons** +> 代码块: `uIcons` + +用于展示 icons 图标 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/src/uni_modules/uni-scss/changelog.md b/src/uni_modules/uni-scss/changelog.md new file mode 100644 index 0000000..b863bb0 --- /dev/null +++ b/src/uni_modules/uni-scss/changelog.md @@ -0,0 +1,8 @@ +## 1.0.3(2022-01-21) +- 优化 组件示例 +## 1.0.2(2021-11-22) +- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题 +## 1.0.1(2021-11-22) +- 修复 vue3中scss语法兼容问题 +## 1.0.0(2021-11-18) +- init diff --git a/src/uni_modules/uni-scss/index.scss b/src/uni_modules/uni-scss/index.scss new file mode 100644 index 0000000..1744a5f --- /dev/null +++ b/src/uni_modules/uni-scss/index.scss @@ -0,0 +1 @@ +@import './styles/index.scss'; diff --git a/src/uni_modules/uni-scss/package.json b/src/uni_modules/uni-scss/package.json new file mode 100644 index 0000000..7cc0ccb --- /dev/null +++ b/src/uni_modules/uni-scss/package.json @@ -0,0 +1,82 @@ +{ + "id": "uni-scss", + "displayName": "uni-scss 辅助样式", + "version": "1.0.3", + "description": "uni-sass是uni-ui提供的一套全局样式 ,通过一些简单的类名和sass变量,实现简单的页面布局操作,比如颜色、边距、圆角等。", + "keywords": [ + "uni-scss", + "uni-ui", + "辅助样式" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.0" + }, + "dcloudext": { + "category": [ + "JS SDK", + "通用 SDK" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "n", + "联盟": "n" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/src/uni_modules/uni-scss/readme.md b/src/uni_modules/uni-scss/readme.md new file mode 100644 index 0000000..b7d1c25 --- /dev/null +++ b/src/uni_modules/uni-scss/readme.md @@ -0,0 +1,4 @@ +`uni-sass` 是 `uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/src/uni_modules/uni-scss/styles/index.scss b/src/uni_modules/uni-scss/styles/index.scss new file mode 100644 index 0000000..ffac4fe --- /dev/null +++ b/src/uni_modules/uni-scss/styles/index.scss @@ -0,0 +1,7 @@ +@import './setting/_variables.scss'; +@import './setting/_border.scss'; +@import './setting/_color.scss'; +@import './setting/_space.scss'; +@import './setting/_radius.scss'; +@import './setting/_text.scss'; +@import './setting/_styles.scss'; diff --git a/src/uni_modules/uni-scss/styles/setting/_border.scss b/src/uni_modules/uni-scss/styles/setting/_border.scss new file mode 100644 index 0000000..12a11c3 --- /dev/null +++ b/src/uni_modules/uni-scss/styles/setting/_border.scss @@ -0,0 +1,3 @@ +.uni-border { + border: 1px $uni-border-1 solid; +} \ No newline at end of file diff --git a/src/uni_modules/uni-scss/styles/setting/_color.scss b/src/uni_modules/uni-scss/styles/setting/_color.scss new file mode 100644 index 0000000..1ededd9 --- /dev/null +++ b/src/uni_modules/uni-scss/styles/setting/_color.scss @@ -0,0 +1,66 @@ + +// TODO 暂时不需要 class ,需要用户使用变量实现 ,如果使用类名其实并不推荐 +// @mixin get-styles($k,$c) { +// @if $k == size or $k == weight{ +// font-#{$k}:#{$c} +// }@else{ +// #{$k}:#{$c} +// } +// } +$uni-ui-color:( + // 主色 + primary: $uni-primary, + primary-disable: $uni-primary-disable, + primary-light: $uni-primary-light, + // 辅助色 + success: $uni-success, + success-disable: $uni-success-disable, + success-light: $uni-success-light, + warning: $uni-warning, + warning-disable: $uni-warning-disable, + warning-light: $uni-warning-light, + error: $uni-error, + error-disable: $uni-error-disable, + error-light: $uni-error-light, + info: $uni-info, + info-disable: $uni-info-disable, + info-light: $uni-info-light, + // 中性色 + main-color: $uni-main-color, + base-color: $uni-base-color, + secondary-color: $uni-secondary-color, + extra-color: $uni-extra-color, + // 背景色 + bg-color: $uni-bg-color, + // 边框颜色 + border-1: $uni-border-1, + border-2: $uni-border-2, + border-3: $uni-border-3, + border-4: $uni-border-4, + // 黑色 + black:$uni-black, + // 白色 + white:$uni-white, + // 透明 + transparent:$uni-transparent +) !default; +@each $key, $child in $uni-ui-color { + .uni-#{"" + $key} { + color: $child; + } + .uni-#{"" + $key}-bg { + background-color: $child; + } +} +.uni-shadow-sm { + box-shadow: $uni-shadow-sm; +} +.uni-shadow-base { + box-shadow: $uni-shadow-base; +} +.uni-shadow-lg { + box-shadow: $uni-shadow-lg; +} +.uni-mask { + background-color:$uni-mask; +} diff --git a/src/uni_modules/uni-scss/styles/setting/_radius.scss b/src/uni_modules/uni-scss/styles/setting/_radius.scss new file mode 100644 index 0000000..9a0428b --- /dev/null +++ b/src/uni_modules/uni-scss/styles/setting/_radius.scss @@ -0,0 +1,55 @@ +@mixin radius($r,$d:null ,$important: false){ + $radius-value:map-get($uni-radius, $r) if($important, !important, null); + // Key exists within the $uni-radius variable + @if (map-has-key($uni-radius, $r) and $d){ + @if $d == t { + border-top-left-radius:$radius-value; + border-top-right-radius:$radius-value; + }@else if $d == r { + border-top-right-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == b { + border-bottom-left-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == l { + border-top-left-radius:$radius-value; + border-bottom-left-radius:$radius-value; + }@else if $d == tl { + border-top-left-radius:$radius-value; + }@else if $d == tr { + border-top-right-radius:$radius-value; + }@else if $d == br { + border-bottom-right-radius:$radius-value; + }@else if $d == bl { + border-bottom-left-radius:$radius-value; + } + }@else{ + border-radius:$radius-value; + } +} + +@each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $key} { + @include radius($key) + } + }@else{ + .uni-radius { + @include radius($key) + } + } +} + +@each $direction in t, r, b, l,tl, tr, br, bl { + @each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $direction}-#{"" + $key} { + @include radius($key,$direction,false) + } + }@else{ + .uni-radius-#{$direction} { + @include radius($key,$direction,false) + } + } + } +} diff --git a/src/uni_modules/uni-scss/styles/setting/_space.scss b/src/uni_modules/uni-scss/styles/setting/_space.scss new file mode 100644 index 0000000..3c89528 --- /dev/null +++ b/src/uni_modules/uni-scss/styles/setting/_space.scss @@ -0,0 +1,56 @@ + +@mixin fn($space,$direction,$size,$n) { + @if $n { + #{$space}-#{$direction}: #{$size*$uni-space-root}px + } @else { + #{$space}-#{$direction}: #{-$size*$uni-space-root}px + } +} +@mixin get-styles($direction,$i,$space,$n){ + @if $direction == t { + @include fn($space, top,$i,$n); + } + @if $direction == r { + @include fn($space, right,$i,$n); + } + @if $direction == b { + @include fn($space, bottom,$i,$n); + } + @if $direction == l { + @include fn($space, left,$i,$n); + } + @if $direction == x { + @include fn($space, left,$i,$n); + @include fn($space, right,$i,$n); + } + @if $direction == y { + @include fn($space, top,$i,$n); + @include fn($space, bottom,$i,$n); + } + @if $direction == a { + @if $n { + #{$space}:#{$i*$uni-space-root}px; + } @else { + #{$space}:#{-$i*$uni-space-root}px; + } + } +} + +@each $orientation in m,p { + $space: margin; + @if $orientation == m { + $space: margin; + } @else { + $space: padding; + } + @for $i from 0 through 16 { + @each $direction in t, r, b, l, x, y, a { + .uni-#{$orientation}#{$direction}-#{$i} { + @include get-styles($direction,$i,$space,true); + } + .uni-#{$orientation}#{$direction}-n#{$i} { + @include get-styles($direction,$i,$space,false); + } + } + } +} \ No newline at end of file diff --git a/src/uni_modules/uni-scss/styles/setting/_styles.scss b/src/uni_modules/uni-scss/styles/setting/_styles.scss new file mode 100644 index 0000000..689afec --- /dev/null +++ b/src/uni_modules/uni-scss/styles/setting/_styles.scss @@ -0,0 +1,167 @@ +/* #ifndef APP-NVUE */ + +$-color-white:#fff; +$-color-black:#000; +@mixin base-style($color) { + color: #fff; + background-color: $color; + border-color: mix($-color-black, $color, 8%); + &:not([hover-class]):active { + background: mix($-color-black, $color, 10%); + border-color: mix($-color-black, $color, 20%); + color: $-color-white; + outline: none; + } +} +@mixin is-color($color) { + @include base-style($color); + &[loading] { + @include base-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &[loading], + &:not([hover-class]):active { + color: $-color-white; + border-color: mix(darken($color,10%), $-color-white); + background-color: mix($color, $-color-white); + } + } + +} +@mixin base-plain-style($color) { + color:$color; + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 70%); + &:not([hover-class]):active { + background: mix($-color-white, $color, 80%); + color: $color; + outline: none; + border-color: mix($-color-white, $color, 50%); + } +} +@mixin is-plain($color){ + &[plain] { + @include base-plain-style($color); + &[loading] { + @include base-plain-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &:active { + color: mix($-color-white, $color, 40%); + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 80%); + } + } + } +} + + +.uni-btn { + margin: 5px; + color: #393939; + border:1px solid #ccc; + font-size: 16px; + font-weight: 200; + background-color: #F9F9F9; + // TODO 暂时处理边框隐藏一边的问题 + overflow: visible; + &::after{ + border: none; + } + + &:not([type]),&[type=default] { + color: #999; + &[loading] { + background: none; + &::before { + margin-right:5px; + } + } + + + + &[disabled]{ + color: mix($-color-white, #999, 60%); + &, + &[loading], + &:active { + color: mix($-color-white, #999, 60%); + background-color: mix($-color-white,$-color-black , 98%); + border-color: mix($-color-white, #999, 85%); + } + } + + &[plain] { + color: #999; + background: none; + border-color: $uni-border-1; + &:not([hover-class]):active { + background: none; + color: mix($-color-white, $-color-black, 80%); + border-color: mix($-color-white, $-color-black, 90%); + outline: none; + } + &[disabled]{ + &, + &[loading], + &:active { + background: none; + color: mix($-color-white, #999, 60%); + border-color: mix($-color-white, #999, 85%); + } + } + } + } + + &:not([hover-class]):active { + color: mix($-color-white, $-color-black, 50%); + } + + &[size=mini] { + font-size: 16px; + font-weight: 200; + border-radius: 8px; + } + + + + &.uni-btn-small { + font-size: 14px; + } + &.uni-btn-mini { + font-size: 12px; + } + + &.uni-btn-radius { + border-radius: 999px; + } + &[type=primary] { + @include is-color($uni-primary); + @include is-plain($uni-primary) + } + &[type=success] { + @include is-color($uni-success); + @include is-plain($uni-success) + } + &[type=error] { + @include is-color($uni-error); + @include is-plain($uni-error) + } + &[type=warning] { + @include is-color($uni-warning); + @include is-plain($uni-warning) + } + &[type=info] { + @include is-color($uni-info); + @include is-plain($uni-info) + } +} +/* #endif */ diff --git a/src/uni_modules/uni-scss/styles/setting/_text.scss b/src/uni_modules/uni-scss/styles/setting/_text.scss new file mode 100644 index 0000000..a34d08f --- /dev/null +++ b/src/uni_modules/uni-scss/styles/setting/_text.scss @@ -0,0 +1,24 @@ +@mixin get-styles($k,$c) { + @if $k == size or $k == weight{ + font-#{$k}:#{$c} + }@else{ + #{$k}:#{$c} + } +} + +@each $key, $child in $uni-headings { + /* #ifndef APP-NVUE */ + .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ + /* #ifdef APP-NVUE */ + .container .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ +} diff --git a/src/uni_modules/uni-scss/styles/setting/_variables.scss b/src/uni_modules/uni-scss/styles/setting/_variables.scss new file mode 100644 index 0000000..557d3d7 --- /dev/null +++ b/src/uni_modules/uni-scss/styles/setting/_variables.scss @@ -0,0 +1,146 @@ +// @use "sass:math"; +@import '../tools/functions.scss'; +// 间距基础倍数 +$uni-space-root: 2 !default; +// 边框半径默认值 +$uni-radius-root:5px !default; +$uni-radius: () !default; +// 边框半径断点 +$uni-radius: map-deep-merge( + ( + 0: 0, + // TODO 当前版本暂时不支持 sm 属性 + // 'sm': math.div($uni-radius-root, 2), + null: $uni-radius-root, + 'lg': $uni-radius-root * 2, + 'xl': $uni-radius-root * 6, + 'pill': 9999px, + 'circle': 50% + ), + $uni-radius +); +// 字体家族 +$body-font-family: 'Roboto', sans-serif !default; +// 文本 +$heading-font-family: $body-font-family !default; +$uni-headings: () !default; +$letterSpacing: -0.01562em; +$uni-headings: map-deep-merge( + ( + 'h1': ( + size: 32px, + weight: 300, + line-height: 50px, + // letter-spacing:-0.01562em + ), + 'h2': ( + size: 28px, + weight: 300, + line-height: 40px, + // letter-spacing: -0.00833em + ), + 'h3': ( + size: 24px, + weight: 400, + line-height: 32px, + // letter-spacing: normal + ), + 'h4': ( + size: 20px, + weight: 400, + line-height: 30px, + // letter-spacing: 0.00735em + ), + 'h5': ( + size: 16px, + weight: 400, + line-height: 24px, + // letter-spacing: normal + ), + 'h6': ( + size: 14px, + weight: 500, + line-height: 18px, + // letter-spacing: 0.0125em + ), + 'subtitle': ( + size: 12px, + weight: 400, + line-height: 20px, + // letter-spacing: 0.00937em + ), + 'body': ( + font-size: 14px, + font-weight: 400, + line-height: 22px, + // letter-spacing: 0.03125em + ), + 'caption': ( + 'size': 12px, + 'weight': 400, + 'line-height': 20px, + // 'letter-spacing': 0.03333em, + // 'text-transform': false + ) + ), + $uni-headings +); + + + +// 主色 +$uni-primary: #2979ff !default; +$uni-primary-disable:lighten($uni-primary,20%) !default; +$uni-primary-light: lighten($uni-primary,25%) !default; + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37 !default; +$uni-success-disable:lighten($uni-success,20%) !default; +$uni-success-light: lighten($uni-success,25%) !default; + +$uni-warning: #f3a73f !default; +$uni-warning-disable:lighten($uni-warning,20%) !default; +$uni-warning-light: lighten($uni-warning,25%) !default; + +$uni-error: #e43d33 !default; +$uni-error-disable:lighten($uni-error,20%) !default; +$uni-error-light: lighten($uni-error,25%) !default; + +$uni-info: #8f939c !default; +$uni-info-disable:lighten($uni-info,20%) !default; +$uni-info-light: lighten($uni-info,25%) !default; + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a !default; // 主要文字 +$uni-base-color: #6a6a6a !default; // 常规文字 +$uni-secondary-color: #909399 !default; // 次要文字 +$uni-extra-color: #c7c7c7 !default; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0 !default; +$uni-border-2: #EDEDED !default; +$uni-border-3: #DCDCDC !default; +$uni-border-4: #B9B9B9 !default; + +// 常规色 +$uni-black: #000000 !default; +$uni-white: #ffffff !default; +$uni-transparent: rgba($color: #000000, $alpha: 0) !default; + +// 背景色 +$uni-bg-color: #f7f7f7 !default; + +/* 水平间距 */ +$uni-spacing-sm: 8px !default; +$uni-spacing-base: 15px !default; +$uni-spacing-lg: 30px !default; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5) !default; +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default; +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5) !default; + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4) !default; diff --git a/src/uni_modules/uni-scss/styles/tools/functions.scss b/src/uni_modules/uni-scss/styles/tools/functions.scss new file mode 100644 index 0000000..ac6f63e --- /dev/null +++ b/src/uni_modules/uni-scss/styles/tools/functions.scss @@ -0,0 +1,19 @@ +// 合并 map +@function map-deep-merge($parent-map, $child-map){ + $result: $parent-map; + @each $key, $child in $child-map { + $parent-has-key: map-has-key($result, $key); + $parent-value: map-get($result, $key); + $parent-type: type-of($parent-value); + $child-type: type-of($child); + $parent-is-map: $parent-type == map; + $child-is-map: $child-type == map; + + @if (not $parent-has-key) or ($parent-type != $child-type) or (not ($parent-is-map and $child-is-map)){ + $result: map-merge($result, ( $key: $child )); + }@else { + $result: map-merge($result, ( $key: map-deep-merge($parent-value, $child) )); + } + } + @return $result; +}; diff --git a/src/uni_modules/uni-scss/theme.scss b/src/uni_modules/uni-scss/theme.scss new file mode 100644 index 0000000..80ee62f --- /dev/null +++ b/src/uni_modules/uni-scss/theme.scss @@ -0,0 +1,31 @@ +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; +// 主色 +$uni-primary: #2979ff; +// 辅助色 +$uni-success: #4cd964; +// 警告色 +$uni-warning: #f0ad4e; +// 错误色 +$uni-error: #dd524d; +// 描述色 +$uni-info: #909399; +// 中性色 +$uni-main-color: #303133; +$uni-base-color: #606266; +$uni-secondary-color: #909399; +$uni-extra-color: #C0C4CC; +// 背景色 +$uni-bg-color: #f5f5f5; +// 边框颜色 +$uni-border-1: #DCDFE6; +$uni-border-2: #E4E7ED; +$uni-border-3: #EBEEF5; +$uni-border-4: #F2F6FC; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); diff --git a/src/uni_modules/uni-scss/variables.scss b/src/uni_modules/uni-scss/variables.scss new file mode 100644 index 0000000..1c062d4 --- /dev/null +++ b/src/uni_modules/uni-scss/variables.scss @@ -0,0 +1,62 @@ +@import './styles/setting/_variables.scss'; +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; + +// 主色 +$uni-primary: #2979ff; +$uni-primary-disable:mix(#fff,$uni-primary,50%); +$uni-primary-light: mix(#fff,$uni-primary,80%); + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37; +$uni-success-disable:mix(#fff,$uni-success,50%); +$uni-success-light: mix(#fff,$uni-success,80%); + +$uni-warning: #f3a73f; +$uni-warning-disable:mix(#fff,$uni-warning,50%); +$uni-warning-light: mix(#fff,$uni-warning,80%); + +$uni-error: #e43d33; +$uni-error-disable:mix(#fff,$uni-error,50%); +$uni-error-light: mix(#fff,$uni-error,80%); + +$uni-info: #8f939c; +$uni-info-disable:mix(#fff,$uni-info,50%); +$uni-info-light: mix(#fff,$uni-info,80%); + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a; // 主要文字 +$uni-base-color: #6a6a6a; // 常规文字 +$uni-secondary-color: #909399; // 次要文字 +$uni-extra-color: #c7c7c7; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0; +$uni-border-2: #EDEDED; +$uni-border-3: #DCDCDC; +$uni-border-4: #B9B9B9; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); + +// 背景色 +$uni-bg-color: #f7f7f7; + +/* 水平间距 */ +$uni-spacing-sm: 8px; +$uni-spacing-base: 15px; +$uni-spacing-lg: 30px; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5); +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2); +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5); + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..a1b6ae6 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,189 @@ +import { pages, subPackages } from '@/pages.json' +import { isMpWeixin } from './platform' + +export function getLastPage() { + // getCurrentPages() 至少有1个元素,所以不再额外判断 + // const lastPage = getCurrentPages().at(-1) + // 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】 + const pages = getCurrentPages() + return pages[pages.length - 1] +} + +/** + * 获取当前页面路由的 path 路径和 redirectPath 路径 + * path 如 '/pages/login/login' + * redirectPath 如 '/pages/demo/base/route-interceptor' + */ +export function currRoute() { + const lastPage = getLastPage() + if (!lastPage) { + return { + path: '', + query: {}, + } + } + const currRoute = (lastPage as any).$page + // console.log('lastPage.$page:', currRoute) + // console.log('lastPage.$page.fullpath:', currRoute.fullPath) + // console.log('lastPage.$page.options:', currRoute.options) + // console.log('lastPage.options:', (lastPage as any).options) + // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱 + const { fullPath } = currRoute as { fullPath: string } + // console.log(fullPath) + // eg: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序) + // eg: /pages/login/login?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5) + return parseUrlToObj(fullPath) +} + +export function ensureDecodeURIComponent(url: string) { + if (url.startsWith('%')) { + return ensureDecodeURIComponent(decodeURIComponent(url)) + } + return url +} +/** + * 解析 url 得到 path 和 query + * 比如输入url: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor + * 输出: {path: /pages/login/login, query: {redirect: /pages/demo/base/route-interceptor}} + */ +export function parseUrlToObj(url: string) { + const [path, queryStr] = url.split('?') + // console.log(path, queryStr) + + if (!queryStr) { + return { + path, + query: {}, + } + } + const query: Record = {} + queryStr.split('&').forEach((item) => { + const [key, value] = item.split('=') + // console.log(key, value) + query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y + }) + return { path, query } +} +/** + * 得到所有的需要登录的 pages,包括主包和分包的 + * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 excludeLoginPath, 与 route-block 配对使用 + * 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤 + */ +export function getAllPages(key = 'excludeLoginPath') { + // 这里处理主包 + const mainPages = pages + .filter(page => !key || page[key]) + .map(page => ({ + ...page, + path: `/${page.path}`, + })) + + // 这里处理分包 + const subPages: any[] = [] + subPackages.forEach((subPageObj) => { + // console.log(subPageObj) + const { root } = subPageObj + + subPageObj.pages + .filter(page => !key || page[key]) + .forEach((page: { path: string } & Record) => { + subPages.push({ + ...page, + path: `/${root}/${page.path}`, + }) + }) + }) + const result = [...mainPages, ...subPages] + // console.log(`getAllPages by ${key} result: `, result) + return result +} + +export function getCurrentPageI18nKey() { + const routeObj = currRoute() + const currPage = pages.find(page => `/${page.path}` === routeObj.path) + if (!currPage) { + console.warn('路由不正确') + return '' + } + console.log(currPage) + console.log(currPage.style.navigationBarTitleText) + return currPage.style.navigationBarTitleText +} + +/** + * 根据微信小程序当前环境,判断应该获取的 baseUrl + */ +export function getEnvBaseUrl() { + // 请求基准地址 + let baseUrl = import.meta.env.VITE_SERVER_BASEURL + + // # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。 + const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run' + const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run' + const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run' + + // 微信小程序端环境区分 + if (isMpWeixin) { + const { + miniProgram: { envVersion }, + } = uni.getAccountInfoSync() + + switch (envVersion) { + case 'develop': + baseUrl = VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl + break + case 'trial': + baseUrl = VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl + break + case 'release': + baseUrl = VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl + break + } + } + + return baseUrl +} + +/** + * 根据微信小程序当前环境,判断应该获取的 UPLOAD_BASEURL + */ +export function getEnvBaseUploadUrl() { + // 请求基准地址 + let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL + + const VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run/upload' + const VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run/upload' + const VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run/upload' + + // 微信小程序端环境区分 + if (isMpWeixin) { + const { + miniProgram: { envVersion }, + } = uni.getAccountInfoSync() + + switch (envVersion) { + case 'develop': + baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl + break + case 'trial': + baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl + break + case 'release': + baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl + break + } + } + + return baseUploadUrl +} + +/** + * 是否是双token模式 + */ +export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double' + +/** + * 首页路径,通过 page.json 里面的 type 为 home 的页面获取,如果没有,则默认是第一个页面 + * 通常为 /pages/index/index + */ +export const HOME_PAGE = `/${pages.find(page => page.type === 'home')?.path || pages[0].path}` diff --git a/src/utils/platform.ts b/src/utils/platform.ts new file mode 100644 index 0000000..86801f1 --- /dev/null +++ b/src/utils/platform.ts @@ -0,0 +1,26 @@ +/* + * @Author: 菲鸽 + * @Date: 2024-03-28 19:13:55 + * @Last Modified by: 菲鸽 + * @Last Modified time: 2024-03-28 19:24:55 + */ +export const platform = __UNI_PLATFORM__ +export const isH5 = __UNI_PLATFORM__ === 'h5' +export const isApp = __UNI_PLATFORM__ === 'app' +export const isMp = __UNI_PLATFORM__.startsWith('mp-') +export const isMpWeixin = __UNI_PLATFORM__.startsWith('mp-weixin') +export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay') +export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao') +export const isHarmony = __UNI_PLATFORM__.startsWith('app-harmony') + +const PLATFORM = { + platform, + isH5, + isApp, + isMp, + isMpWeixin, + isMpAplipay, + isMpToutiao, + isHarmony, +} +export default PLATFORM diff --git a/src/utils/systemInfo.ts b/src/utils/systemInfo.ts new file mode 100644 index 0000000..a60f82e --- /dev/null +++ b/src/utils/systemInfo.ts @@ -0,0 +1,38 @@ +/* eslint-disable import/no-mutable-exports */ +// 获取屏幕边界到安全区域距离 +let systemInfo +let safeAreaInsets + +// #ifdef MP-WEIXIN +// 微信小程序使用新的API +systemInfo = uni.getWindowInfo() +safeAreaInsets = systemInfo.safeArea + ? { + top: systemInfo.safeArea.top, + right: systemInfo.windowWidth - systemInfo.safeArea.right, + bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom, + left: systemInfo.safeArea.left, + } + : null +// #endif + +// #ifndef MP-WEIXIN +// 其他平台继续使用uni API +systemInfo = uni.getSystemInfoSync() +safeAreaInsets = systemInfo.safeAreaInsets +// #endif + +console.log('systemInfo', systemInfo) +// 微信里面打印 +// pixelRatio: 3 +// safeArea: {top: 47, left: 0, right: 390, bottom: 810, width: 390, …} +// safeAreaInsets: {top: 47, left: 0, right: 0, bottom: 34} +// screenHeight: 844 +// screenTop: 91 +// screenWidth: 390 +// statusBarHeight: 47 +// windowBottom: 0 +// windowHeight: 753 +// windowTop: 0 +// windowWidth: 390 +export { safeAreaInsets, systemInfo } diff --git a/src/utils/updateManager.wx.ts b/src/utils/updateManager.wx.ts new file mode 100644 index 0000000..20b8b50 --- /dev/null +++ b/src/utils/updateManager.wx.ts @@ -0,0 +1,29 @@ +export default () => { + if (!wx.canIUse('getUpdateManager')) { + return + } + + const updateManager = wx.getUpdateManager() + + updateManager.onCheckForUpdate((res) => { + // 请求完新版本信息的回调 + console.log('版本信息', res) + }) + + updateManager.onUpdateReady(() => { + wx.showModal({ + title: '更新提示', + content: '新版本已经准备好,是否重启应用?', + success(res) { + if (res.confirm) { + // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 + updateManager.applyUpdate() + } + }, + }) + }) + + updateManager.onUpdateFailed(() => { + // 新版本下载失败 + }) +} diff --git a/src/utils/uploadFile.ts b/src/utils/uploadFile.ts new file mode 100644 index 0000000..71848a2 --- /dev/null +++ b/src/utils/uploadFile.ts @@ -0,0 +1,325 @@ +/** + * 文件上传钩子函数使用示例 + * @example + * const { loading, error, data, progress, run } = useUpload( + * uploadUrl, + * {}, + * { + * maxSize: 5, // 最大5MB + * sourceType: ['album'], // 仅支持从相册选择 + * onProgress: (p) => console.log(`上传进度:${p}%`), + * onSuccess: (res) => console.log('上传成功', res), + * onError: (err) => console.error('上传失败', err), + * }, + * ) + */ + +/** + * 上传文件的URL配置 + */ +export const uploadFileUrl = { + /** 用户头像上传地址 */ + USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`, +} + +/** + * 通用文件上传函数(支持直接传入文件路径) + * @param url 上传地址 + * @param filePath 本地文件路径 + * @param formData 额外表单数据 + * @param options 上传选项 + */ +export function useFileUpload(url: string, filePath: string, formData: Record = {}, options: Omit = {}) { + return useUpload( + url, + formData, + { + ...options, + sourceType: ['album'], + sizeType: ['original'], + }, + filePath, + ) +} + +export interface UploadOptions { + /** 最大可选择的图片数量,默认为1 */ + count?: number + /** 所选的图片的尺寸,original-原图,compressed-压缩图 */ + sizeType?: Array<'original' | 'compressed'> + /** 选择图片的来源,album-相册,camera-相机 */ + sourceType?: Array<'album' | 'camera'> + /** 文件大小限制,单位:MB */ + maxSize?: number // + /** 上传进度回调函数 */ + onProgress?: (progress: number) => void + /** 上传成功回调函数 */ + onSuccess?: (res: Record) => void + /** 上传失败回调函数 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调函数(无论成功失败) */ + onComplete?: () => void +} + +/** + * 文件上传钩子函数 + * @template T 上传成功后返回的数据类型 + * @param url 上传地址 + * @param formData 额外的表单数据 + * @param options 上传选项 + * @returns 上传状态和控制对象 + */ +export function useUpload(url: string, formData: Record = {}, options: UploadOptions = {}, + /** 直接传入文件路径,跳过选择器 */ + directFilePath?: string) { + /** 上传中状态 */ + const loading = ref(false) + /** 上传错误状态 */ + const error = ref(false) + /** 上传成功后的响应数据 */ + const data = ref() + /** 上传进度(0-100) */ + const progress = ref(0) + + /** 解构上传选项,设置默认值 */ + const { + /** 最大可选择的图片数量 */ + count = 1, + /** 所选的图片的尺寸 */ + sizeType = ['original', 'compressed'], + /** 选择图片的来源 */ + sourceType = ['album', 'camera'], + /** 文件大小限制(MB) */ + maxSize = 10, + /** 进度回调 */ + onProgress, + /** 成功回调 */ + onSuccess, + /** 失败回调 */ + onError, + /** 完成回调 */ + onComplete, + } = options + + /** + * 检查文件大小是否超过限制 + * @param size 文件大小(字节) + * @returns 是否通过检查 + */ + const checkFileSize = (size: number) => { + const sizeInMB = size / 1024 / 1024 + if (sizeInMB > maxSize) { + uni.showToast({ + title: `文件大小不能超过${maxSize}MB`, + icon: 'none', + }) + return false + } + return true + } + /** + * 触发文件选择和上传 + * 根据平台使用不同的选择器: + * - 微信小程序使用 chooseMedia + * - 其他平台使用 chooseImage + */ + const run = () => { + if (directFilePath) { + // 直接使用传入的文件路径 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: directFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + return + } + + // #ifdef MP-WEIXIN + // 微信小程序环境下使用 chooseMedia API + uni.chooseMedia({ + count, + mediaType: ['image'], // 仅支持图片类型 + sourceType, + success: (res) => { + const file = res.tempFiles[0] + // 检查文件大小是否符合限制 + if (!checkFileSize(file.size)) + return + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: file.tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择媒体文件失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + + // #ifndef MP-WEIXIN + // 非微信小程序环境下使用 chooseImage API + uni.chooseImage({ + count, + sizeType, + sourceType, + success: (res) => { + console.log('选择图片成功:', res) + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: res.tempFilePaths[0], + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择图片失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + } + + return { loading, error, data, progress, run } +} + +/** + * 文件上传选项接口 + * @template T 上传成功后返回的数据类型 + */ +interface UploadFileOptions { + /** 上传地址 */ + url: string + /** 临时文件路径 */ + tempFilePath: string + /** 额外的表单数据 */ + formData: Record + /** 上传成功后的响应数据 */ + data: Ref + /** 上传错误状态 */ + error: Ref + /** 上传中状态 */ + loading: Ref + /** 上传进度(0-100) */ + progress: Ref + /** 上传进度回调 */ + onProgress?: (progress: number) => void + /** 上传成功回调 */ + onSuccess?: (res: Record) => void + /** 上传失败回调 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调 */ + onComplete?: () => void +} + +/** + * 执行文件上传 + * @template T 上传成功后返回的数据类型 + * @param options 上传选项 + */ +function uploadFile({ + url, + tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, +}: UploadFileOptions) { + try { + // 创建上传任务 + const uploadTask = uni.uploadFile({ + url, + filePath: tempFilePath, + name: 'file', // 文件对应的 key + formData, + header: { + // H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式 + // #ifndef H5 + 'Content-Type': 'multipart/form-data', + // #endif + }, + // 确保文件名称合法 + success: (uploadFileRes) => { + console.log('上传文件成功:', uploadFileRes) + try { + // 解析响应数据 + const { data: _data } = JSON.parse(uploadFileRes.data) + // 上传成功 + data.value = _data as T + onSuccess?.(_data) + } + catch (err) { + // 响应解析错误 + console.error('解析上传响应失败:', err) + error.value = true + onError?.(new Error('上传响应解析失败')) + } + }, + fail: (err) => { + // 上传请求失败 + console.error('上传文件失败:', err) + error.value = true + onError?.(err) + }, + complete: () => { + // 无论成功失败都执行 + loading.value = false + onComplete?.() + }, + }) + + // 监听上传进度 + uploadTask.onProgressUpdate((res) => { + progress.value = res.progress + onProgress?.(res.progress) + }) + } + catch (err) { + // 创建上传任务失败 + console.error('创建上传任务失败:', err) + error.value = true + loading.value = false + onError?.(new Error('创建上传任务失败')) + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0b55344 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "composite": true, + "lib": ["esnext", "dom"], + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "Node", + "paths": { + "@/*": ["./src/*"], + "@img/*": ["./src/static/*"] + }, + "resolveJsonModule": true, + "types": [ + "@dcloudio/types", + "@uni-helper/uni-types", + "@uni-helper/vite-plugin-uni-pages", + "miniprogram-api-typings", + "wot-design-uni/global.d.ts", + "z-paging/types", + "./src/typings.d.ts" + ], + "allowJs": true, + "noImplicitThis": true, + "outDir": "dist", + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true + }, + "vueCompilerOptions": { + "plugins": ["@uni-helper/uni-types/volar-plugin"] + }, + "include": [ + "package.json", + "src/**/*.ts", + "src/**/*.js", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.jsx", + "src/**/*.vue", + "src/**/*.json" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/uno.config.ts b/uno.config.ts new file mode 100644 index 0000000..01e6728 --- /dev/null +++ b/uno.config.ts @@ -0,0 +1,77 @@ +import type { + Preset, +} from 'unocss' +// https://www.npmjs.com/package/@uni-helper/unocss-preset-uni +import { presetUni } from '@uni-helper/unocss-preset-uni' + +// @see https://unocss.dev/presets/legacy-compat +import { presetLegacyCompat } from '@unocss/preset-legacy-compat' +import { + defineConfig, + presetAttributify, + presetIcons, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' + +export default defineConfig({ + presets: [ + presetUni({ + attributify: false, + }), + presetIcons({ + scale: 1.2, + warn: true, + extraProperties: { + 'display': 'inline-block', + 'vertical-align': 'middle', + }, + }), + // 支持css class属性化 + presetAttributify(), + // TODO: check 是否会有别的影响 + // 处理低端安卓机的样式问题 + // 将颜色函数 (rgb()和hsl()) 从空格分隔转换为逗号分隔,更好的兼容性app端,example: + // `rgb(255 0 0)` -> `rgb(255, 0, 0)` + // `rgba(255 0 0 / 0.5)` -> `rgba(255, 0, 0, 0.5)` + presetLegacyCompat({ + commaStyleColorFunction: true, + }) as Preset, + ], + transformers: [ + // 启用指令功能:主要用于支持 @apply、@screen 和 theme() 等 CSS 指令 + transformerDirectives(), + // 启用 () 分组功能 + // 支持css class组合,eg: `
测试 unocss
` + transformerVariantGroup(), + ], + shortcuts: [ + { + center: 'flex justify-center items-center', + }, + ], + // 动态图标需要在这里配置,或者写在vue页面中注释掉 + safelist: ['i-carbon-code'], + rules: [ + [ + 'p-safe', + { + padding: + 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)', + }, + ], + ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }], + ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }], + ], + theme: { + colors: { + /** 主题色,用法如: text-primary */ + primary: 'var(--wot-color-theme,#0957DE)', + }, + fontSize: { + /** 提供更小号的字体,用法如:text-2xs */ + '2xs': ['20rpx', '28rpx'], + '3xs': ['18rpx', '26rpx'], + }, + }, +}) diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0edbb1f --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,190 @@ +import path from 'node:path' +import process from 'node:process' +import Uni from '@uni-helper/plugin-uni' +import Components from '@uni-helper/vite-plugin-uni-components' +// @see https://uni-helper.js.org/vite-plugin-uni-layouts +import UniLayouts from '@uni-helper/vite-plugin-uni-layouts' +// @see https://github.com/uni-helper/vite-plugin-uni-manifest +import UniManifest from '@uni-helper/vite-plugin-uni-manifest' +// @see https://uni-helper.js.org/vite-plugin-uni-pages +import UniPages from '@uni-helper/vite-plugin-uni-pages' +// @see https://github.com/uni-helper/vite-plugin-uni-platform +// 需要与 @uni-helper/vite-plugin-uni-pages 插件一起使用 +import UniPlatform from '@uni-helper/vite-plugin-uni-platform' +/** + * 分包优化、模块异步跨包调用、组件异步跨包引用 + * @see https://github.com/uni-ku/bundle-optimizer + */ +import Optimization from '@uni-ku/bundle-optimizer' +// https://github.com/uni-ku/root +import UniKuRoot from '@uni-ku/root' +import dayjs from 'dayjs' +import { visualizer } from 'rollup-plugin-visualizer' +import UnoCSS from 'unocss/vite' +import AutoImport from 'unplugin-auto-import/vite' +import { defineConfig, loadEnv } from 'vite' +import ViteRestart from 'vite-plugin-restart' + +// https://vitejs.dev/config/ +export default ({ command, mode }) => { + // @see https://unocss.dev/ + // const UnoCSS = (await import('unocss/vite')).default + // console.log(mode === process.env.NODE_ENV) // true + + // mode: 区分生产环境还是开发环境 + console.log('command, mode -> ', command, mode) + // pnpm dev:h5 时得到 => serve development + // pnpm build:h5 时得到 => build production + // pnpm dev:mp-weixin 时得到 => build development (注意区别,command为build) + // pnpm build:mp-weixin 时得到 => build production + // pnpm dev:app 时得到 => build development (注意区别,command为build) + // pnpm build:app 时得到 => build production + // dev 和 build 命令可以分别使用 .env.development 和 .env.production 的环境变量 + + const { UNI_PLATFORM } = process.env + console.log('UNI_PLATFORM -> ', UNI_PLATFORM) // 得到 mp-weixin, h5, app 等 + + const env = loadEnv(mode, path.resolve(process.cwd(), 'env')) + const { + VITE_APP_PORT, + VITE_SERVER_BASEURL, + VITE_APP_TITLE, + VITE_DELETE_CONSOLE, + VITE_APP_PUBLIC_BASE, + VITE_APP_PROXY_ENABLE, + VITE_SERVER_HAS_API_PREFIX, + VITE_APP_PROXY_PREFIX, + } = env + console.log('环境变量 env -> ', env) + + return defineConfig({ + envDir: './env', // 自定义env目录 + base: VITE_APP_PUBLIC_BASE, + plugins: [ + UniPages({ + exclude: ['**/components/**/**.*'], + // homePage 通过 vue 文件的 route-block 的type="home"来设定 + // pages 目录为 src/pages,分包目录不能配置在pages目录下 + subPackages: ['src/pages-sub'], // 是个数组,可以配置多个,但是不能为pages里面的目录 + dts: 'src/types/uni-pages.d.ts', + }), + UniLayouts(), + UniPlatform(), + UniManifest(), + // UniXXX 需要在 Uni 之前引入 + { + // 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG + // 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952 + // 自定义插件禁用 vite:vue 插件的 devToolsEnabled,强制编译 vue 模板时 inline 为 true + name: 'fix-vite-plugin-vue', + configResolved(config) { + const plugin = config.plugins.find(p => p.name === 'vite:vue') + if (plugin && plugin.api && plugin.api.options) { + plugin.api.options.devToolsEnabled = false + } + }, + }, + UnoCSS(), + AutoImport({ + imports: ['vue', 'uni-app'], + dts: 'src/types/auto-import.d.ts', + dirs: ['src/hooks'], // 自动导入 hooks + vueTemplate: true, // default false + }), + // Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行 + Optimization({ + enable: { + 'optimization': true, + 'async-import': true, + 'async-component': true, + }, + dts: { + base: 'src/types', + }, + logger: false, + }), + + ViteRestart({ + // 通过这个插件,在修改vite.config.js文件则不需要重新运行也生效配置 + restart: ['vite.config.js'], + }), + // h5环境增加 BUILD_TIME 和 BUILD_BRANCH + UNI_PLATFORM === 'h5' && { + name: 'html-transform', + transformIndexHtml(html) { + return html.replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss')).replace('%VITE_APP_TITLE%', VITE_APP_TITLE) + }, + }, + // 打包分析插件,h5 + 生产环境才弹出 + UNI_PLATFORM === 'h5' + && mode === 'production' + && visualizer({ + filename: './node_modules/.cache/visualizer/stats.html', + open: true, + gzipSize: true, + brotliSize: true, + }), + // 只有在 app 平台时才启用 copyNativeRes 插件 + // UNI_PLATFORM === 'app' && copyNativeRes(), + Components({ + extensions: ['vue'], + deep: true, // 是否递归扫描子目录, + directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名, + dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持) + }), + // 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后 + UniKuRoot(), + Uni(), + ], + define: { + __UNI_PLATFORM__: JSON.stringify(UNI_PLATFORM), + __VITE_APP_PROXY__: JSON.stringify(VITE_APP_PROXY_ENABLE), + }, + css: { + postcss: { + plugins: [ + // autoprefixer({ + // // 指定目标浏览器 + // overrideBrowserslist: ['> 1%', 'last 2 versions'], + // }), + ], + }, + }, + + resolve: { + alias: { + '@': path.join(process.cwd(), './src'), + '@img': path.join(process.cwd(), './src/static/images'), + }, + }, + server: { + host: '0.0.0.0', + hmr: true, + port: Number.parseInt(VITE_APP_PORT, 10), + // 仅 H5 端生效,其他端不生效(其他端走build,不走devServer) + proxy: JSON.parse(VITE_APP_PROXY_ENABLE) + ? { + [VITE_APP_PROXY_PREFIX]: { + target: VITE_SERVER_BASEURL, + changeOrigin: true, + // 后端有/api前缀则不做处理,没有则需要去掉 + rewrite: path => JSON.parse(VITE_SERVER_HAS_API_PREFIX) + ? path + : path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''), + }, + } + : undefined, + }, + esbuild: { + drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : ['debugger'], + }, + build: { + sourcemap: false, + // 方便非h5端调试 + // sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false + target: 'es6', + // 开发环境不用压缩 + minify: mode === 'development' ? false : 'esbuild', + }, + }) +}