Chore: Chore Update
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
2. 开发环境:
|
||||
- H5: `pnpm dev` 或 `pnpm dev:h5`
|
||||
- 微信小程序: `pnpm dev:mp`
|
||||
- 支付宝小程序: `pnpm dev:mp-alipay`
|
||||
- APP: `pnpm dev:app`
|
||||
|
||||
## 代码规范
|
||||
@@ -15,7 +16,8 @@
|
||||
|
||||
## 构建和部署
|
||||
- H5 构建:`pnpm build:h5`
|
||||
- 小程序构建:`pnpm build:mp`
|
||||
- 微信小程序构建:`pnpm build:mp`
|
||||
- 支付宝小程序构建:`pnpm build:mp-alipay`
|
||||
- APP 构建:`pnpm build:app`
|
||||
- 类型检查:`pnpm type-check`
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ alwaysApply: true
|
||||
## 项目特点
|
||||
- 支持 H5、小程序、APP 多平台开发
|
||||
- 使用最新的前端技术栈
|
||||
- 内置约定式路由、layout布局、请求封装等功能
|
||||
- 内置约定式路由、layout布局、请求封装、登录拦截、自定义tabbar等功能
|
||||
- 无需依赖 HBuilderX,支持命令行开发
|
||||
|
||||
## 核心配置文件
|
||||
@@ -26,9 +26,11 @@ alwaysApply: true
|
||||
- `src/http/` - HTTP 请求封装
|
||||
- `src/store/` - 状态管理
|
||||
- `src/tabbar/` - 底部导航栏
|
||||
- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
|
||||
|
||||
## 开发命令
|
||||
- `pnpm dev` - 开发 H5 版本
|
||||
- `pnpm dev:mp` - 开发微信小程序
|
||||
- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
|
||||
- `pnpm dev:app` - 开发 APP 版本
|
||||
- `pnpm build` - 构建生产版本
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
## 页面开发
|
||||
- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
|
||||
- 使用约定式路由,文件名即路由路径
|
||||
- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||
- 页面配置在仅需要在 宏`definePage` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||
|
||||
## 组件开发
|
||||
- 组件文件放在 [src/components/](mdc:src/components/) 目录下
|
||||
- 组件文件放在 [src/components/](mdc:src/components/) 或者 [src/pages/xx/components/](mdc:src/pages/xx/components/) 目录下
|
||||
- 使用 uni-app 内置组件和第三方组件库
|
||||
- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||
- 支持 wot-ui\uview-pro\uv-ui\sard-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||
- 自定义组件遵循 uni-app 组件规范
|
||||
|
||||
## 平台适配
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
- 使用 Composition API 和 `<script setup>` 语法
|
||||
- 组件文件使用 PascalCase 命名
|
||||
- 页面文件放在 `src/pages/` 目录下
|
||||
- 组件文件放在 `src/components/` 目录下
|
||||
- 全局组件文件放在 `src/components/` 目录下
|
||||
- 局部组件文件放在页面的 `/components/` 目录下
|
||||
|
||||
## Vue SFC 组件规范
|
||||
- `<script setup>` 标签必须是第一个子元素
|
||||
- `<script setup lang="ts">` 标签必须是第一个子元素
|
||||
- `<template>` 标签必须是第二个子元素
|
||||
- `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# 依赖目录
|
||||
node_modules
|
||||
|
||||
# 版本控制
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# 构建产物
|
||||
/dist
|
||||
|
||||
# 开发工具配置
|
||||
.vscode/
|
||||
.idea/
|
||||
.trae/
|
||||
.cursor/
|
||||
|
||||
# 其他配置文件
|
||||
.github/
|
||||
.husky/
|
||||
|
||||
# 日志文件
|
||||
logs/
|
||||
|
||||
# 缓存文件
|
||||
.cache/
|
||||
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
174
.github/workflows/auto-merge.yml
vendored
174
.github/workflows/auto-merge.yml
vendored
@@ -7,26 +7,8 @@ on:
|
||||
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
|
||||
merge-to-base:
|
||||
name: Merge main into base
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -35,154 +17,34 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||
|
||||
- name: Merge main into i18n
|
||||
- name: Merge main into base
|
||||
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
|
||||
git checkout base
|
||||
git merge main --no-ff -m "Auto merge main into base"
|
||||
git push origin base
|
||||
|
||||
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
|
||||
- name: Merge base into base-login
|
||||
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
|
||||
git checkout base-login
|
||||
git merge base --no-ff -m "Auto merge base into base-login"
|
||||
git push origin base-login
|
||||
|
||||
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
|
||||
- name: Merge base into base-i18n
|
||||
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
|
||||
git checkout base-i18n
|
||||
git merge base --no-ff -m "Auto merge base into base-i18n"
|
||||
git push origin base-i18n
|
||||
|
||||
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
|
||||
- name: Merge base into base-login-i18n
|
||||
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
|
||||
git checkout base-login-i18n
|
||||
git merge base --no-ff -m "Auto merge base into base-login-i18n"
|
||||
git push origin base-login-i18n
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -33,9 +33,9 @@ src/types
|
||||
src/manifest.json
|
||||
src/pages.json
|
||||
|
||||
# lock 文件还是不要了,我主要的版本写死就好了
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
# 2025-10-15 by 菲鸽: lock 文件还是需要加入版本管理,今天又遇到版本不一致导致无法运行的问题了。
|
||||
# pnpm-lock.yaml
|
||||
# package-lock.json
|
||||
|
||||
# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
|
||||
# `git rm -r --cached .` 然后提交 commit 即可。
|
||||
|
||||
1
.npmrc
1
.npmrc
@@ -6,3 +6,4 @@ auto-install-peers=true
|
||||
shamefully-hoist=true
|
||||
ignore-workspace-root-check=true
|
||||
install-workspace-root=true
|
||||
node-options=--max-old-space-size=8192
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
- `src/http/` - HTTP 请求封装
|
||||
- `src/store/` - 状态管理
|
||||
- `src/tabbar/` - 底部导航栏
|
||||
- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
|
||||
|
||||
## 开发命令
|
||||
- `pnpm dev` - 开发 H5 版本
|
||||
- `pnpm dev:mp` - 开发微信小程序
|
||||
- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
|
||||
- `pnpm dev:app` - 开发 APP 版本
|
||||
- `pnpm build` - 构建生产版本
|
||||
|
||||
@@ -34,7 +36,8 @@
|
||||
- 使用 Composition API 和 `<script setup>` 语法
|
||||
- 组件文件使用 PascalCase 命名
|
||||
- 页面文件放在 `src/pages/` 目录下
|
||||
- 组件文件放在 `src/components/` 目录下
|
||||
- 全局组件文件放在 `src/components/` 目录下
|
||||
- 局部组件文件放在页面的 `/components/` 目录下
|
||||
|
||||
## TypeScript 规范
|
||||
- 严格使用 TypeScript,避免使用 `any` 类型
|
||||
@@ -50,24 +53,25 @@
|
||||
|
||||
## UnoCSS 原子化 CSS
|
||||
- 项目使用 UnoCSS 作为原子化 CSS 框架
|
||||
- 配置在 [uno.config.ts](mdc:uno.config.ts)
|
||||
- 配置在 [uno.config.ts]
|
||||
- 支持预设和自定义规则
|
||||
- 优先使用原子化类名,减少自定义 CSS
|
||||
|
||||
## Vue SFC 组件规范
|
||||
- `<script setup>` 标签必须是第一个子元素
|
||||
- `<script setup lang="ts">` 标签必须是第一个子元素
|
||||
- `<template>` 标签必须是第二个子元素
|
||||
- `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
|
||||
|
||||
## 页面开发
|
||||
- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
|
||||
- 页面文件放在 [src/pages/]目录下
|
||||
- 使用约定式路由,文件名即路由路径
|
||||
- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||
- 页面配置在仅需要在 宏`definePage` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||
|
||||
## 组件开发
|
||||
- 组件文件放在 [src/components/](mdc:src/components/) 目录下
|
||||
- 全局组件文件放在 `src/components/` 目录下
|
||||
- 局部组件文件放在页面的 `/components/` 目录下
|
||||
- 使用 uni-app 内置组件和第三方组件库
|
||||
- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||
- 支持 wot-ui\uview-pro\uv-ui\sard-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||
- 自定义组件遵循 uni-app 组件规范
|
||||
|
||||
## 平台适配
|
||||
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"antfu.unocss",
|
||||
"antfu.iconify",
|
||||
@@ -11,7 +10,6 @@
|
||||
"uni-helper.uni-highlight-vscode",
|
||||
"uni-helper.uni-ui-snippets-vscode",
|
||||
"uni-helper.uni-app-snippets-vscode",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"christian-kohler.path-intellisense"
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -10,7 +10,7 @@
|
||||
"scss.validate": false, // 禁用 SCSS 内置验证
|
||||
"less.validate": false, // 禁用 LESS 内置验证
|
||||
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
|
||||
38
Dockerfile
38
Dockerfile
@@ -1,38 +0,0 @@
|
||||
# 使用 node:24-alpine 作为基础镜像,固定版本+减少体积
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
# 在容器中创建目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装pnpm(使用 npm 的 --global-style 可以减少依赖安装体积)
|
||||
RUN npm install -g pnpm@10.10.0 --global-style
|
||||
# 设置pnpm镜像源
|
||||
RUN pnpm config set registry https://registry.npmmirror.com
|
||||
# 复制依赖文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
# 先复制scripts目录,因为prepare脚本需要用到其中的文件
|
||||
COPY scripts ./scripts
|
||||
# 安装依赖,但跳过prepare脚本(这一步会缓存,只有 package.json 或 pnpm-lock.yaml 变化时才会重新运行)
|
||||
RUN pnpm install --ignore-scripts --frozen-lockfile
|
||||
# 手动执行我们需要的docker:prepare脚本
|
||||
RUN pnpm run docker:prepare
|
||||
# 复制其余源代码
|
||||
COPY . .
|
||||
# 构建项目
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
# 使用nginx作为服务
|
||||
FROM nginx:1.29.1-alpine3.22 AS production-stage
|
||||
|
||||
# 将构建好的项目复制到nginx下
|
||||
COPY --from=builder /app/dist/build/h5 /usr/share/nginx/html
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
# 启动nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -38,7 +38,7 @@
|
||||
<p align="center">
|
||||
<a href="https://unibest.tech/" target="_blank">📖 文档地址(new)</a>
|
||||
<span style="margin:0 10px;">|</span>
|
||||
<a href="https://feige996.github.io/hello-unibest/" target="_blank">📱 DEMO 地址</a>
|
||||
<a href="https://unibest-tech.github.io/hello-unibest" target="_blank">📱 DEMO 地址</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -60,6 +60,11 @@
|
||||
- Vue Official>=2.1.10
|
||||
- TypeScript>=5.0
|
||||
|
||||
## 新版分支
|
||||
- main == base
|
||||
- base --> base-i18n
|
||||
- base-login --> base-login-i18n
|
||||
|
||||
## 📂 快速开始
|
||||
|
||||
执行 `pnpm create unibest` 创建项目
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 参考代码
|
||||
|
||||
部分代码片段,供参考。
|
||||
28
docker.md
28
docker.md
@@ -1,28 +0,0 @@
|
||||
## Docker
|
||||
|
||||
根据提供的 `Dockerfile`,可以通过以下步骤构建并运行镜像:
|
||||
|
||||
### 1. 构建Docker镜像
|
||||
|
||||
在项目根目录执行以下命令:
|
||||
|
||||
- `-t unibest:v1-2025091701`:为镜像指定名称和标签,YYYYMMDD+编号
|
||||
- `.`:表示使用当前目录的Dockerfile
|
||||
|
||||
```bash
|
||||
docker build -t unibest:v1-2025091701 .
|
||||
docker build -t unibest:v1-2025091702 .
|
||||
```
|
||||
### 2. 运行Docker容器
|
||||
使用以下命令运行容器:
|
||||
|
||||
```bash
|
||||
docker run -d --name unibest-v1-2025091701 -p 80:80 unibest:v1-2025091701
|
||||
docker run -d --name unibest-v1-2025091702 -p 80:80 unibest:v1-2025091702
|
||||
```
|
||||
|
||||
- `-d`:表示在后台运行容器
|
||||
- `-p 80:80`:将容器的80端口映射到主机的80端口
|
||||
- `--name unibest-v1-2025091701`:为容器指定一个名称
|
||||
|
||||
|
||||
@@ -141,6 +141,15 @@ export default defineManifestConfig({
|
||||
optimization: {
|
||||
subPackages: true,
|
||||
},
|
||||
// 解决支付宝小程序开发工具报错 【globalThis is not defined】
|
||||
compileOptions: {
|
||||
globalObjectMode: 'enable',
|
||||
transpile: {
|
||||
script: {
|
||||
ignore: ['node_modules/**'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'mp-baidu': {
|
||||
usingComponents: true,
|
||||
|
||||
145
nginx.conf
145
nginx.conf
@@ -1,145 +0,0 @@
|
||||
# 配置工作进程数,通常设置为 CPU 核心数
|
||||
worker_processes auto;
|
||||
|
||||
# 错误日志配置
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
# 开启多路复用
|
||||
use epoll;
|
||||
}
|
||||
|
||||
# 文件描述符限制 - 移到这里,在http块之前
|
||||
worker_rlimit_nofile 65535;
|
||||
|
||||
http {
|
||||
# 日志格式定义
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
# 访问日志配置
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# 高效文件传输设置
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
|
||||
# 连接超时设置
|
||||
keepalive_timeout 65;
|
||||
keepalive_requests 100;
|
||||
|
||||
# gzip 压缩优化
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1000;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
# 增加更多文件类型
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
|
||||
|
||||
# 全局设置
|
||||
# 合理限制请求体大小,根据实际需求调整
|
||||
client_max_body_size 10m;
|
||||
client_body_buffer_size 128k;
|
||||
client_header_timeout 60s;
|
||||
client_body_timeout 60s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
gunzip on;
|
||||
gzip_static always;
|
||||
include /etc/nginx/mime.types;
|
||||
absolute_redirect off;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# 安全相关响应头
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
# 根据实际情况调整 CSP
|
||||
# add_header Content-Security-Policy "default-src 'self'";
|
||||
|
||||
# 处理 SPA 应用路由
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
# HTML 和 JSON 文件 - 短缓存策略
|
||||
location ~ .*\.(html|json)$ {
|
||||
add_header Cache-Control "public, max-age=300, must-revalidate";
|
||||
}
|
||||
|
||||
# 静态资源 - 长缓存策略
|
||||
location ~ .*\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|ttf|woff|woff2|eot|mp4|mp3|swf)$ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
expires 365d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# JS 和 CSS - 带版本号的长缓存
|
||||
location ~ .*\.(js|css)$ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
expires 365d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 接口转发 - 替换为实际后端地址
|
||||
# location ^~ /fg-api {
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header Host $host;
|
||||
|
||||
# # 后端是HTTPS时的必要配置
|
||||
# proxy_ssl_server_name on;
|
||||
# proxy_ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# proxy_ssl_session_reuse on;
|
||||
|
||||
# # 对于生产环境,应该尽量使用有效的证书而不是依赖``proxy_ssl_verify off;`` ,因为这会带来安全风险
|
||||
# proxy_ssl_verify off;
|
||||
|
||||
# # TODO:替换为实际后端服务地址
|
||||
# # 注意在URL末尾添加了斜杠,这样Nginx会去掉 /fg-api 前缀
|
||||
# # 前端请求 http://your-domain.com/fg-api/users 转发到 https://ukw0y1.laf.run/users
|
||||
# proxy_pass https://ukw0y1.laf.run/;
|
||||
|
||||
# # 上面一行的效果与下面2行一样的效果,都是为了去掉 /fg-api 前缀
|
||||
# # 显式移除/fg-api前缀
|
||||
# # rewrite ^/fg-api(.*)$ $1 break;
|
||||
# # 域名末尾不需要斜杠了
|
||||
# # proxy_pass https://ukw0y1.laf.run;
|
||||
|
||||
# proxy_connect_timeout 60s;
|
||||
# proxy_send_timeout 60s;
|
||||
# proxy_read_timeout 60s;
|
||||
|
||||
# proxy_buffers 8 32k;
|
||||
# proxy_buffer_size 64k;
|
||||
# proxy_busy_buffers_size 128k;
|
||||
|
||||
# proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
|
||||
# }
|
||||
|
||||
# 错误页面配置
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# 禁止访问隐藏文件
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { GenerateServiceProps } from 'openapi-ts-request'
|
||||
import { defineConfig } from 'openapi-ts-request'
|
||||
|
||||
export default [
|
||||
export default defineConfig([
|
||||
{
|
||||
describe: 'unibest-openapi-test',
|
||||
schemaPath: 'https://ukw0y1.laf.run/unibest-opapi-test.json',
|
||||
serversPath: './src/service',
|
||||
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions } from '@/http/types';`,
|
||||
requestOptionsType: 'CustomRequestOptions',
|
||||
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions_ } from '@/http/types';`,
|
||||
requestOptionsType: 'CustomRequestOptions_',
|
||||
isGenReactQuery: false,
|
||||
reactQueryMode: 'vue',
|
||||
isGenJavaScript: false,
|
||||
},
|
||||
] as GenerateServiceProps[]
|
||||
])
|
||||
|
||||
24
package.json
24
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "unibest",
|
||||
"type": "module",
|
||||
"version": "3.18.5",
|
||||
"unibest-version": "3.18.5",
|
||||
"update-time": "2025-10-03",
|
||||
"version": "4.1.0",
|
||||
"unibest-version": "4.1.0",
|
||||
"unibest-update-time": "2025-11-07",
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"description": "unibest - 最好的 uniapp 开发模板",
|
||||
"generate-time": "用户创建项目时生成",
|
||||
@@ -36,6 +36,7 @@
|
||||
"dev:app-android": "uni -p app-android",
|
||||
"dev:app-ios": "uni -p app-ios",
|
||||
"dev:custom": "uni -p",
|
||||
"predev": "pnpm init-baseFiles",
|
||||
"dev": "uni",
|
||||
"dev:test": "uni --mode test",
|
||||
"dev:prod": "uni --mode production",
|
||||
@@ -88,8 +89,10 @@
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"openapi": "openapi-ts",
|
||||
"prepare": "git init && husky && node ./scripts/create-base-files.js",
|
||||
"docker:prepare": "node ./scripts/create-base-files.js",
|
||||
"init-husky": "git init && husky",
|
||||
"init-baseFiles": "node ./scripts/create-base-files.js",
|
||||
"init-json": "pnpm init-baseFiles",
|
||||
"prepare": "pnpm init-husky & pnpm init-baseFiles",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
@@ -115,11 +118,11 @@
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"alova": "^3.3.3",
|
||||
"dayjs": "1.11.10",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pinia": "2.0.36",
|
||||
"pinia-plugin-persistedstate": "3.2.1",
|
||||
"vue": "^3.4.21",
|
||||
"wot-design-uni": "^1.12.4",
|
||||
"vue-i18n": "9.1.9",
|
||||
"vue-router": "4.5.1",
|
||||
"z-paging": "2.8.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -133,6 +136,7 @@
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@iconify-json/carbon": "^1.2.4",
|
||||
"@iconify/utils": "^3.0.2",
|
||||
"@rollup/rollup-darwin-x64": "^4.28.0",
|
||||
"@types/node": "^20.17.9",
|
||||
"@uni-helper/eslint-config": "0.5.0",
|
||||
@@ -142,8 +146,8 @@
|
||||
"@uni-helper/unocss-preset-uni": "0.2.11",
|
||||
"@uni-helper/vite-plugin-uni-components": "0.2.3",
|
||||
"@uni-helper/vite-plugin-uni-layouts": "0.1.11",
|
||||
"@uni-helper/vite-plugin-uni-manifest": "0.2.8",
|
||||
"@uni-helper/vite-plugin-uni-pages": "0.3.13",
|
||||
"@uni-helper/vite-plugin-uni-manifest": "0.2.11",
|
||||
"@uni-helper/vite-plugin-uni-pages": "0.3.19",
|
||||
"@uni-helper/vite-plugin-uni-platform": "0.0.5",
|
||||
"@uni-ku/bundle-optimizer": "v1.3.15-beta.2",
|
||||
"@uni-ku/root": "1.4.1",
|
||||
@@ -158,7 +162,7 @@
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.10",
|
||||
"miniprogram-api-typings": "^4.1.0",
|
||||
"openapi-ts-request": "^1.6.7",
|
||||
"openapi-ts-request": "^1.10.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-html": "^1.8.0",
|
||||
"postcss-scss": "^4.0.9",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isH5 } from '@uni-helper/uni-env'
|
||||
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
|
||||
import { tabBar } from './src/tabbar/config'
|
||||
|
||||
@@ -14,12 +13,10 @@ export default defineUniPages({
|
||||
autoscan: true,
|
||||
custom: {
|
||||
'^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
|
||||
'^wd-(.*)': 'wot-design-uni/components/wd-$1/wd-$1.vue',
|
||||
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
|
||||
'z-paging/components/z-paging$1/z-paging$1.vue',
|
||||
},
|
||||
},
|
||||
// tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
|
||||
// 无tabbar模式下,h5 设置为 {} 为了防止浏览器报错导致白屏
|
||||
tabBar: tabBar || (isH5 ? {} : undefined) as any,
|
||||
tabBar: tabBar as any,
|
||||
})
|
||||
|
||||
13980
pnpm-lock.yaml
generated
Normal file
13980
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,162 +9,9 @@ import { fileURLToPath } from 'node:url'
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const manifest = {
|
||||
'name': 'unibest',
|
||||
'appid': '__UNI__D1E5001',
|
||||
'description': '',
|
||||
'versionName': '1.0.0',
|
||||
'versionCode': '100',
|
||||
'transformPx': false,
|
||||
'app-plus': {
|
||||
usingComponents: true,
|
||||
nvueStyleCompiler: 'uni-app',
|
||||
compilerVersion: 3,
|
||||
splashscreen: {
|
||||
alwaysShowBeforeRender: true,
|
||||
waiting: true,
|
||||
autoclose: true,
|
||||
delay: 0,
|
||||
},
|
||||
modules: {},
|
||||
distribute: {
|
||||
android: {
|
||||
permissions: [
|
||||
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
|
||||
'<uses-permission android:name="android.permission.VIBRATE"/>',
|
||||
'<uses-permission android:name="android.permission.READ_LOGS"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
|
||||
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.CAMERA"/>',
|
||||
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
|
||||
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
|
||||
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
|
||||
'<uses-feature android:name="android.hardware.camera"/>',
|
||||
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
|
||||
],
|
||||
minSdkVersion: 21,
|
||||
targetSdkVersion: 30,
|
||||
abiFilters: [
|
||||
'armeabi-v7a',
|
||||
'arm64-v8a',
|
||||
],
|
||||
},
|
||||
ios: {},
|
||||
sdkConfigs: {},
|
||||
icons: {
|
||||
android: {
|
||||
hdpi: 'static/app/icons/72x72.png',
|
||||
xhdpi: 'static/app/icons/96x96.png',
|
||||
xxhdpi: 'static/app/icons/144x144.png',
|
||||
xxxhdpi: 'static/app/icons/192x192.png',
|
||||
},
|
||||
ios: {
|
||||
appstore: 'static/app/icons/1024x1024.png',
|
||||
ipad: {
|
||||
'app': 'static/app/icons/76x76.png',
|
||||
'app@2x': 'static/app/icons/152x152.png',
|
||||
'notification': 'static/app/icons/20x20.png',
|
||||
'notification@2x': 'static/app/icons/40x40.png',
|
||||
'proapp@2x': 'static/app/icons/167x167.png',
|
||||
'settings': 'static/app/icons/29x29.png',
|
||||
'settings@2x': 'static/app/icons/58x58.png',
|
||||
'spotlight': 'static/app/icons/40x40.png',
|
||||
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||
},
|
||||
iphone: {
|
||||
'app@2x': 'static/app/icons/120x120.png',
|
||||
'app@3x': 'static/app/icons/180x180.png',
|
||||
'notification@2x': 'static/app/icons/40x40.png',
|
||||
'notification@3x': 'static/app/icons/60x60.png',
|
||||
'settings@2x': 'static/app/icons/58x58.png',
|
||||
'settings@3x': 'static/app/icons/87x87.png',
|
||||
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||
'spotlight@3x': 'static/app/icons/120x120.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
compatible: {
|
||||
ignoreVersion: true,
|
||||
},
|
||||
},
|
||||
'quickapp': {},
|
||||
'mp-weixin': {
|
||||
appid: 'wxa2abb91f64032a2b',
|
||||
setting: {
|
||||
urlCheck: false,
|
||||
es6: true,
|
||||
minified: true,
|
||||
},
|
||||
usingComponents: true,
|
||||
optimization: {
|
||||
subPackages: true,
|
||||
},
|
||||
},
|
||||
'mp-alipay': {
|
||||
usingComponents: true,
|
||||
styleIsolation: 'shared',
|
||||
optimization: {
|
||||
subPackages: true,
|
||||
},
|
||||
},
|
||||
'mp-baidu': {
|
||||
usingComponents: true,
|
||||
},
|
||||
'mp-toutiao': {
|
||||
usingComponents: true,
|
||||
},
|
||||
'uniStatistics': {
|
||||
enable: false,
|
||||
},
|
||||
'vueVersion': '3',
|
||||
'h5': {
|
||||
router: {
|
||||
base: '/',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 最简可运行配置
|
||||
const manifest = { }
|
||||
const pages = {
|
||||
globalStyle: {
|
||||
navigationStyle: 'default',
|
||||
navigationBarTitleText: 'unibest',
|
||||
navigationBarBackgroundColor: '#f8f8f8',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
easycom: {
|
||||
autoscan: true,
|
||||
custom: {
|
||||
'^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
|
||||
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)': 'z-paging/components/z-paging$1/z-paging$1.vue',
|
||||
},
|
||||
},
|
||||
tabBar: {
|
||||
custom: true,
|
||||
color: '#999999',
|
||||
selectedColor: '#018d71',
|
||||
backgroundColor: '#F8F8F8',
|
||||
borderStyle: 'black',
|
||||
height: '50px',
|
||||
fontSize: '10px',
|
||||
iconWidth: '24px',
|
||||
spacing: '3px',
|
||||
list: [
|
||||
{
|
||||
text: '首页',
|
||||
pagePath: 'pages/index/index',
|
||||
},
|
||||
{
|
||||
text: '我的',
|
||||
pagePath: 'pages/me/me',
|
||||
},
|
||||
],
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
path: 'pages/index/index',
|
||||
@@ -182,20 +29,7 @@ const pages = {
|
||||
},
|
||||
},
|
||||
],
|
||||
subPackages: [
|
||||
{
|
||||
root: 'pages-sub',
|
||||
pages: [
|
||||
{
|
||||
path: 'demo/index',
|
||||
type: 'page',
|
||||
style: {
|
||||
navigationBarTitleText: '分包页面',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
subPackages: [],
|
||||
}
|
||||
|
||||
// 使用修复后的 __dirname 来解析文件路径
|
||||
@@ -209,11 +43,11 @@ if (!fs.existsSync(srcDir)) {
|
||||
}
|
||||
|
||||
// 如果 src/manifest.json 不存在,就创建它;存在就不处理,以免覆盖
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
if (!fs.existsSync(manifestPath) || fs.statSync(manifestPath).size === 0) {
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
}
|
||||
|
||||
// 如果 src/pages.json 不存在,就创建它;存在就不处理,以免覆盖
|
||||
if (!fs.existsSync(pagesPath)) {
|
||||
if (!fs.existsSync(pagesPath) || fs.statSync(pagesPath).size === 0) {
|
||||
fs.writeFileSync(pagesPath, JSON.stringify(pages, null, 2))
|
||||
}
|
||||
|
||||
@@ -13,19 +13,13 @@ const execPromise = promisify(exec)
|
||||
|
||||
// 定义要执行的命令
|
||||
const dependencies = [
|
||||
'@dcloudio/uni-app-harmony',
|
||||
// TODO: 如果不需要某个平台的小程序,请手动删除或注释掉
|
||||
'@dcloudio/uni-mp-alipay',
|
||||
'@dcloudio/uni-mp-baidu',
|
||||
'@dcloudio/uni-mp-jd',
|
||||
'@dcloudio/uni-mp-kuaishou',
|
||||
'@dcloudio/uni-mp-lark',
|
||||
'@dcloudio/uni-mp-qq',
|
||||
'@dcloudio/uni-mp-toutiao',
|
||||
'@dcloudio/uni-mp-xhs',
|
||||
'@dcloudio/uni-quickapp-webview',
|
||||
// i18n模板要注释掉下面的
|
||||
'vue-i18n',
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useThemeStore } from '@/store'
|
||||
import FgTabbar from '@/tabbar/index.vue'
|
||||
import { isPageTabbar } from './tabbar/store'
|
||||
import { currRoute } from './utils'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const isCurrentPageTabbar = ref(true)
|
||||
onShow(() => {
|
||||
console.log('App.ku.vue onShow', currRoute())
|
||||
@@ -31,7 +28,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-config-provider :theme-vars="themeStore.themeVars" :theme="themeStore.theme">
|
||||
<view>
|
||||
<!-- 这个先隐藏了,知道这样用就行 -->
|
||||
<view class="hidden text-center">
|
||||
{{ helloKuRoot }},这里可以配置全局的东西
|
||||
@@ -40,7 +37,5 @@ defineExpose({
|
||||
<KuRootView />
|
||||
|
||||
<FgTabbar v-if="isCurrentPageTabbar" />
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</wd-config-provider>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
15
src/App.vue
15
src/App.vue
@@ -3,10 +3,10 @@ import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { navigateToInterceptor } from '@/router/interceptor'
|
||||
|
||||
onLaunch((options) => {
|
||||
console.log('App Launch', options)
|
||||
console.log('App.vue onLaunch', options)
|
||||
})
|
||||
onShow((options) => {
|
||||
console.log('App Show', options)
|
||||
console.log('App.vue onShow', options)
|
||||
// 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
|
||||
// https://github.com/unibest-tech/unibest/issues/192
|
||||
if (options?.path) {
|
||||
@@ -22,16 +22,5 @@ onHide(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
swiper,
|
||||
scroll-view {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
116
src/hooks/useScroll.md
Normal file
116
src/hooks/useScroll.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 上拉刷新和下拉加载更多
|
||||
|
||||
在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。
|
||||
场景一 页面滚动
|
||||
|
||||
```
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '上拉刷新和下拉加载更多',
|
||||
enablePullDownRefresh: true,
|
||||
onReachBottomDistance: 100,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
场景二 局部滚动 结合 `scroll-view`
|
||||
|
||||
## 关键文件
|
||||
|
||||
- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。
|
||||
- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。
|
||||
|
||||
## `useScroll` Hook
|
||||
|
||||
`useScroll` 是一个 Vue Composition API Hook,它封装了处理下拉刷新和上拉加载的通用逻辑。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。
|
||||
- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。
|
||||
- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。
|
||||
- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。
|
||||
|
||||
### 使用方法
|
||||
|
||||
```typescript
|
||||
import { useScroll } from '@/hooks/useScroll'
|
||||
import { getList } from '@/service/list' // 你的数据请求API
|
||||
|
||||
const {
|
||||
list, // 响应式的数据列表
|
||||
loading, // 是否加载中
|
||||
finished, // 是否已全部加载
|
||||
error, // 是否加载失败
|
||||
onScrollToLower, // 滚动到底部时触发的事件
|
||||
onRefresherRefresh, // 下拉刷新时触发的事件
|
||||
} = useScroll(getList) // 将获取数据的API函数传入
|
||||
```
|
||||
|
||||
## `scroll-view` 组件
|
||||
|
||||
`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。
|
||||
|
||||
### 关键属性
|
||||
|
||||
- `scroll-y`: 允许纵向滚动。
|
||||
- `refresher-enabled`: 启用下拉刷新。
|
||||
- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。
|
||||
- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。
|
||||
- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。
|
||||
|
||||
## 示例代码
|
||||
|
||||
以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll` 和 `scroll-view` 结合使用。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="scroll-page">
|
||||
<scroll-view
|
||||
class="scroll-view"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="loading"
|
||||
@scrolltolower="onScrollToLower"
|
||||
@refresherrefresh="onRefresherRefresh"
|
||||
>
|
||||
<view v-for="item in list" :key="item.id" class="scroll-item">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
|
||||
<!-- 加载状态提示 -->
|
||||
<view v-if="loading" class="loading-tip">加载中...</view>
|
||||
<view v-if="finished" class="finished-tip">没有更多了</view>
|
||||
<view v-if="error" class="error-tip">加载失败,请重试</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useScroll } from '@/hooks/useScroll'
|
||||
import { getList } from '@/service/list'
|
||||
|
||||
const { list, loading, finished, error, onScrollToLower, onRefresherRefresh } = useScroll(getList)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式省略 */
|
||||
.scroll-page, .scroll-view {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 实现步骤总结
|
||||
|
||||
1. **创建API**: 确保你有一个返回分页数据的API请求函数(例如 `getList`),它应该接受页码和页面大小作为参数。
|
||||
2. **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook,将你的API函数作为参数传入。
|
||||
3. **模板绑定**:
|
||||
- 使用 `scroll-view` 组件作为滚动容器。
|
||||
- 将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。
|
||||
- 将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。
|
||||
- 将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。
|
||||
4. **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。
|
||||
5. **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。
|
||||
|
||||
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。
|
||||
@@ -4,7 +4,7 @@ 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 { toLoginPage } from '@/utils/toLoginPage'
|
||||
import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
|
||||
|
||||
// 配置动态Tag
|
||||
@@ -31,7 +31,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
|
||||
}
|
||||
catch (error) {
|
||||
// 切换到登录页
|
||||
await uni.reLaunch({ url: LOGIN_PAGE })
|
||||
toLoginPage({ mode: 'reLaunch' })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { IDoubleTokenRes } from '@/api/types/login'
|
||||
import type { CustomRequestOptions, IResponse } from '@/http/types'
|
||||
import { nextTick } from 'vue'
|
||||
import { LOGIN_PAGE } from '@/router/config'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { isDoubleTokenMode } from '@/utils'
|
||||
import { toLoginPage } from '@/utils/toLoginPage'
|
||||
import { ResultEnum } from './tools/enum'
|
||||
|
||||
// 刷新 token 状态管理
|
||||
@@ -32,7 +32,7 @@ export function http<T>(options: CustomRequestOptions) {
|
||||
if (!isDoubleTokenMode) {
|
||||
// 未启用双token策略,清理用户信息,跳转到登录页
|
||||
tokenStore.logout()
|
||||
uni.navigateTo({ url: LOGIN_PAGE })
|
||||
toLoginPage()
|
||||
return reject(res)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export function http<T>(options: CustomRequestOptions) {
|
||||
await tokenStore.logout()
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: LOGIN_PAGE })
|
||||
toLoginPage()
|
||||
}, 2000)
|
||||
}
|
||||
finally {
|
||||
@@ -96,9 +96,12 @@ export function http<T>(options: CustomRequestOptions) {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// 处理业务逻辑错误
|
||||
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
|
||||
throw new Error(`请求错误[${code}]:${responseData.message || responseData.msg}`)
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: responseData.msg || responseData.message || '请求错误',
|
||||
})
|
||||
}
|
||||
return resolve(responseData.data as T)
|
||||
return resolve(responseData.data)
|
||||
}
|
||||
|
||||
// 处理其他错误
|
||||
|
||||
@@ -7,6 +7,9 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||
hideErrorToast?: boolean
|
||||
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
||||
|
||||
/** 主要提供给 openapi-ts-request 生成的代码使用 */
|
||||
export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
|
||||
|
||||
export interface HttpRequestResult<T> {
|
||||
promise: Promise<T>
|
||||
requestTask: UniApp.RequestTask
|
||||
|
||||
@@ -4,7 +4,7 @@ import { http } from './http'
|
||||
/*
|
||||
* openapi-ts-request 工具的 request 跨客户端适配方法
|
||||
*/
|
||||
export default function request<T = unknown>(
|
||||
export default function request<T extends { data?: any }>(
|
||||
url: string,
|
||||
options: Omit<CustomRequestOptions, 'url'> & {
|
||||
params?: Record<string, unknown>
|
||||
@@ -26,5 +26,5 @@ export default function request<T = unknown>(
|
||||
delete requestOptions.headers
|
||||
}
|
||||
|
||||
return http<T>(requestOptions)
|
||||
return http<T['data']>(requestOptions)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
<script lang="ts" setup>
|
||||
const testUniLayoutExposedData = ref('testUniLayoutExposedData')
|
||||
defineExpose({
|
||||
testUniLayoutExposedData,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 404 页面
|
||||
|
||||
`404页面` 只有在路由不存在时才会显示,如果您不需要可以删除该页面。但是建议保留。
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { HOME_PAGE } from '@/utils'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
// 'custom' 表示开启自定义导航栏,默认 'default'
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
// 当pages.config.ts中配置了tabbar页面时,使用switchTab切换到首页
|
||||
// 否则使用navigateTo返回首页
|
||||
uni.switchTab({ url: HOME_PAGE })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="h-screen flex flex-col items-center justify-center">
|
||||
<view> 404 </view>
|
||||
<view> 页面不存在 </view>
|
||||
<button class="mt-6 w-40 text-center" @click="goBack">
|
||||
返回首页
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
# pages-fg 说明
|
||||
|
||||
为了尽量减少主包的大小,一些无关紧要但经常需要的页面(如登录页、注册页、404页等)放在了 `pages-fg` 目录下。
|
||||
@@ -1,20 +0,0 @@
|
||||
# 登录页
|
||||
需要输入账号、密码/验证码的登录页。
|
||||
|
||||
## 适用性
|
||||
|
||||
本页面主要用于 `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` 里面,默认会在登录后自动重定向到来源/配置的页面。
|
||||
|
||||
如果与您的业务不符,您可以自行修改。
|
||||
@@ -1,87 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { tabbarList } from '@/tabbar/config'
|
||||
import { isPageTabbar } from '@/tabbar/store'
|
||||
import { ensureDecodeURIComponent } from '@/utils'
|
||||
import { parseUrlToObj } from '@/utils/index'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '登录',
|
||||
},
|
||||
})
|
||||
|
||||
const redirectUrl = ref('')
|
||||
onLoad((options) => {
|
||||
console.log('login options: ', options)
|
||||
if (options.redirect) {
|
||||
redirectUrl.value = ensureDecodeURIComponent(options.redirect)
|
||||
}
|
||||
else {
|
||||
redirectUrl.value = tabbarList[0].pagePath
|
||||
}
|
||||
console.log('redirectUrl.value: ', redirectUrl.value)
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const tokenStore = useTokenStore()
|
||||
async function doLogin() {
|
||||
if (tokenStore.hasLogin) {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
try {
|
||||
// 调用登录接口
|
||||
await tokenStore.login({
|
||||
username: '菲鸽',
|
||||
password: '123456',
|
||||
})
|
||||
console.log(redirectUrl.value)
|
||||
}
|
||||
catch (error) {
|
||||
console.log('登录失败', error)
|
||||
}
|
||||
let path = redirectUrl.value
|
||||
if (!path.startsWith('/')) {
|
||||
path = `/${path}`
|
||||
}
|
||||
const { path: _path, query } = parseUrlToObj(path)
|
||||
console.log('_path:', _path, 'query:', query, 'path:', path)
|
||||
console.log('isPageTabbar(_path):', isPageTabbar(_path))
|
||||
if (isPageTabbar(_path)) {
|
||||
// 经过我的测试 switchTab 不能带 query 参数, 不管是放到 url 还是放到 query ,
|
||||
// 最后跳转过去的时候都会丢失 query 信息
|
||||
uni.switchTab({
|
||||
url: path,
|
||||
})
|
||||
// uni.switchTab({
|
||||
// url: _path,
|
||||
// query,
|
||||
// })
|
||||
}
|
||||
else {
|
||||
// 自己决定是 redirectTo 还是 navigateBack
|
||||
// uni.redirectTo({
|
||||
// url: path,
|
||||
// })
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login">
|
||||
<!-- 本页面是非MP的登录页,主要用于 h5 和 APP -->
|
||||
<view class="text-center">
|
||||
登录页
|
||||
</view>
|
||||
<button class="mt-4 w-40 text-center" @click="doLogin">
|
||||
点击模拟登录
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
||||
@@ -1,34 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { LOGIN_PAGE } from '@/router/config'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '注册',
|
||||
},
|
||||
})
|
||||
|
||||
function doRegister() {
|
||||
uni.showToast({
|
||||
title: '注册成功',
|
||||
})
|
||||
// 注册成功后跳转到登录页
|
||||
uni.navigateTo({
|
||||
url: LOGIN_PAGE,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login">
|
||||
<view class="text-center">
|
||||
注册页
|
||||
</view>
|
||||
<button class="mt-4 w-40 text-center" @click="doRegister">
|
||||
点击模拟注册
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
||||
@@ -1,135 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { isApp, isAppAndroid, isAppHarmony, isAppIOS, isAppPlus, isH5, isMpWeixin, isWeb } from '@uni-helper/uni-env'
|
||||
import { LOGIN_PAGE } from '@/router/config'
|
||||
import { useTokenStore } from '@/store'
|
||||
import RequestOpenApiComp from './components/request-openapi.vue'
|
||||
import RequestComp from './components/request.vue'
|
||||
import UploadComp from './components/Upload.vue'
|
||||
import VBindCss from './components/VBindCss.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '关于',
|
||||
},
|
||||
// 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 arc/router 文件夹
|
||||
excludeLoginPath: false,
|
||||
})
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
// 浏览器打印 isH5为true, isWeb为false,大家尽量用 isH5
|
||||
console.log({ isApp, isAppAndroid, isAppHarmony, isAppIOS, isAppPlus, isH5, isMpWeixin, isWeb })
|
||||
|
||||
function gotoLogin() {
|
||||
if (tokenStore.hasLogin) {
|
||||
uni.showToast({
|
||||
title: '已登录,不能去登录页',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages-sub/about/about?a=1&b=2')}`,
|
||||
})
|
||||
}
|
||||
function logout() {
|
||||
// 清空用户信息
|
||||
tokenStore.logout()
|
||||
// 执行退出登录逻辑
|
||||
uni.showToast({
|
||||
title: '退出登录成功',
|
||||
icon: 'success',
|
||||
})
|
||||
}
|
||||
|
||||
function gotoScroll() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-sub/demo/scroll',
|
||||
})
|
||||
}
|
||||
|
||||
function gotoAlova() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-sub/about/alova',
|
||||
})
|
||||
}
|
||||
function gotoSubPage() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-sub/demo/index',
|
||||
})
|
||||
}
|
||||
|
||||
// uniLayout里面的变量通过 expose 暴露出来后可以在 onReady 钩子获取到(onLoad 钩子不行)
|
||||
const uniLayout = ref()
|
||||
onLoad(() => {
|
||||
console.log('onLoad:', uniLayout.value) // onLoad: undefined
|
||||
})
|
||||
onReady(() => {
|
||||
console.log('onReady:', uniLayout.value) // onReady: Proxy(Object)
|
||||
console.log('onReady:', uniLayout.value.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
|
||||
})
|
||||
// 结论:第一次通过onShow获取不到,但是可以通过 onReady获取到,后面就可以通过onShow获取到了
|
||||
onShow(() => {
|
||||
console.log('onShow:', uniLayout.value) // onReady: Proxy(Object)
|
||||
console.log('onShow:', uniLayout.value?.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
|
||||
})
|
||||
|
||||
const uniKuRoot = ref()
|
||||
// 结论:(同上)第一次通过onShow获取不到,但是可以通过 onReady获取到,后面就可以通过onShow获取到了
|
||||
onReady(() => {
|
||||
console.log('onReady uniKuRoot exposeRef', uniKuRoot.value?.exposeRef)
|
||||
})
|
||||
onShow(() => {
|
||||
console.log('onShow uniKuRoot exposeRef', uniKuRoot.value?.exposeRef)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template root="uniKuRoot">
|
||||
<!-- page-meta 使用范例 -->
|
||||
<page-meta page-style="overflow: auto" />
|
||||
<view>
|
||||
<view class="mt-8 text-center text-xl text-gray-400">
|
||||
请求调用、unocss、static图片
|
||||
</view>
|
||||
<view class="my-2 text-center">
|
||||
<image src="/static/images/avatar.jpg" class="h-100px w-100px" />
|
||||
</view>
|
||||
<view class="my-2 text-center">
|
||||
当前是否登录:{{ tokenStore.hasLogin }}
|
||||
</view>
|
||||
<view class="m-auto max-w-600px flex items-center">
|
||||
<button class="mt-4 w-40 text-center" @click="gotoLogin">
|
||||
点击去登录页
|
||||
</button>
|
||||
<button class="mt-4 w-40 text-center" @click="logout">
|
||||
点击退出登录
|
||||
</button>
|
||||
</view>
|
||||
<RequestOpenApiComp />
|
||||
<RequestComp />
|
||||
<UploadComp />
|
||||
<VBindCss />
|
||||
<view class="mb-6 h-1px bg-#eee" />
|
||||
<view class="mb-2 text-center">
|
||||
<button type="primary" size="mini" class="w-240px" @click="gotoScroll">
|
||||
下拉刷新和下拉加载更多
|
||||
</button>
|
||||
<view>简单hooks(非z-paging组件)</view>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<button type="primary" size="mini" class="w-160px" @click="gotoAlova">
|
||||
前往 alova 示例页面
|
||||
</button>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<button type="primary" size="mini" class="w-160px" @click="gotoSubPage">
|
||||
前往分包页面
|
||||
</button>
|
||||
</view>
|
||||
<view class="mt-6 text-center text-sm">
|
||||
<view class="inline-block w-80% text-gray-400">
|
||||
为了方便脚手架动态生成不同UI模板,本页的按钮统一使用UI库无关的原生button
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-6" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,53 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRequest } from 'alova/client'
|
||||
import { foo } from '@/api/foo-alova'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: 'Alova 演示',
|
||||
},
|
||||
})
|
||||
|
||||
const initialData = undefined
|
||||
|
||||
const { loading, data, send } = useRequest(foo, {
|
||||
initialData,
|
||||
immediate: true,
|
||||
})
|
||||
console.log(data)
|
||||
function reset() {
|
||||
data.value = initialData
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="p-6 text-center">
|
||||
<button type="primary" size="mini" class="my-6 w-160px" @click="send">
|
||||
发送请求
|
||||
</button>
|
||||
<view class="h-16">
|
||||
<view v-if="loading">
|
||||
loading...
|
||||
</view>
|
||||
<block v-else>
|
||||
<view class="text-xl">
|
||||
请求数据如下
|
||||
</view>
|
||||
<view class="text-green leading-8">
|
||||
{{ JSON.stringify(data) }}
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view class="text-red">
|
||||
{{ data?.id }}
|
||||
</view>
|
||||
</view>
|
||||
<button type="default" size="mini" class="my-6 w-160px" @click="reset">
|
||||
重置数据
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<view class="p-4 text-center">
|
||||
<button type="primary" size="mini" class="w-160px" @click="run">
|
||||
选择图片并上传
|
||||
</button>
|
||||
<view v-if="loading" class="h-10 text-blue">
|
||||
上传...
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="m-2">
|
||||
上传后返回的接口数据:
|
||||
</view>
|
||||
<view class="m-2">
|
||||
{{ data }}
|
||||
</view>
|
||||
<view class="m-auto h-40 max-w-40">
|
||||
<image v-if="data" :src="data.url" mode="scaleToFill" />
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { loading, data, run } = useUpload()
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
// root 插件更新到 1.3.4之后,都正常了。
|
||||
const testBindCssVariable = ref('red')
|
||||
function changeTestBindCssVariable() {
|
||||
if (testBindCssVariable.value === 'red') {
|
||||
testBindCssVariable.value = 'green'
|
||||
}
|
||||
else {
|
||||
testBindCssVariable.value = 'red'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="mt-4 w-60 text-center" @click="changeTestBindCssVariable">
|
||||
toggle v-bind css变量
|
||||
</button>
|
||||
<view class="test-css my-2 text-center">
|
||||
测试v-bind css变量的具体文案
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.test-css {
|
||||
color: v-bind(testBindCssVariable);
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UserItem } from '@/service'
|
||||
import { infoUsingGet, listAllUsingGet } from '@/service'
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
const data = ref<UserItem>()
|
||||
|
||||
// openapi 请求示例
|
||||
async function getUserInfo() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await infoUsingGet({})
|
||||
console.log(res)
|
||||
data.value = res
|
||||
error.value = null
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err as Error
|
||||
data.value = null
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// openapi + useRequest 请求示例
|
||||
const { data: data2, loading: loading2, run } = useRequest(() => listAllUsingGet({}), {
|
||||
immediate: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="p-6 text-center">
|
||||
<view class="my-4 text-center">
|
||||
1)直接使用 openapi 生成的请求
|
||||
</view>
|
||||
<view class="my-4 text-center">
|
||||
<button type="primary" size="mini" class="w-160px" @click="getUserInfo">
|
||||
发送请求
|
||||
</button>
|
||||
<view class="text-xl">
|
||||
请求数据如下
|
||||
</view>
|
||||
<view class="text-green leading-8">
|
||||
{{ JSON.stringify(data) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="my-4 text-center">
|
||||
2)直接使用 openapi + useRequest 生成的请求
|
||||
</view>
|
||||
<view class="my-4 flex items-center gap-2 text-center">
|
||||
<button type="primary" size="mini" class="w-160px" @click="run">
|
||||
发送请求
|
||||
</button>
|
||||
</view>
|
||||
<view class="text-xl">
|
||||
请求数据如下
|
||||
</view>
|
||||
<view class="text-green leading-8">
|
||||
{{ JSON.stringify(data2) }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,75 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IFooItem } from '@/api/foo'
|
||||
import { getFooAPI } from '@/api/foo'
|
||||
|
||||
// const initialData = {
|
||||
// name: 'initialData',
|
||||
// id: '1234',
|
||||
// }
|
||||
const initialData = undefined
|
||||
|
||||
// 直接请求示例
|
||||
async function reqFooAPI() {
|
||||
try {
|
||||
const res = await getFooAPI('菲鸽')
|
||||
console.log('直接请求示例res', res)
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
reqFooAPI()
|
||||
|
||||
// 直接useRequest请求示例
|
||||
const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
|
||||
immediate: true,
|
||||
initialData,
|
||||
})
|
||||
|
||||
function reset() {
|
||||
data.value = initialData
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="p-6 text-center">
|
||||
<view class="my-2">
|
||||
pages 里面的 vue 文件会扫描成页面,将自动添加到 pages.json 里面。
|
||||
</view>
|
||||
<view class="my-2 text-green-400">
|
||||
但是 components 里面的 vue 不会。
|
||||
</view>
|
||||
|
||||
<view class="my-4 text-center">
|
||||
<button type="primary" size="mini" class="w-160px" @click="run">
|
||||
发送请求
|
||||
</button>
|
||||
</view>
|
||||
<view class="h-16">
|
||||
<view v-if="loading">
|
||||
loading...
|
||||
</view>
|
||||
<block v-else>
|
||||
<view v-if="error instanceof Error" class="text-red leading-8">
|
||||
错误: {{ error.message }}
|
||||
</view>
|
||||
<view v-else-if="error" class="text-red leading-8">
|
||||
错误: 未知错误
|
||||
</view>
|
||||
<view v-else>
|
||||
<view class="text-xl">
|
||||
请求数据如下
|
||||
</view>
|
||||
<view class="text-green leading-8">
|
||||
{{ JSON.stringify(data) }}
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="my-4 text-center">
|
||||
<button type="warn" size="mini" class="w-160px" :disabled="!data" @click="reset">
|
||||
重置数据
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,48 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IFooItem } from '@/api/foo'
|
||||
import { getFooAPI } from '@/api/foo'
|
||||
|
||||
const recommendUrl = ref('http://laf.run/signup?code=ohaOgIX')
|
||||
|
||||
// const initialData = {
|
||||
// name: 'initialData',
|
||||
// id: '1234',
|
||||
// }
|
||||
const initialData = undefined
|
||||
const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
|
||||
immediate: true,
|
||||
initialData,
|
||||
})
|
||||
|
||||
function reset() {
|
||||
data.value = initialData
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="p-6 text-center">
|
||||
<view class="my-2 text-center">
|
||||
<button type="primary" size="mini" class="w-160px" @click="run">
|
||||
发送请求
|
||||
</button>
|
||||
</view>
|
||||
<view class="h-16">
|
||||
<view v-if="loading">
|
||||
loading...
|
||||
</view>
|
||||
<block v-else>
|
||||
<view class="text-xl">
|
||||
请求数据如下
|
||||
</view>
|
||||
<view class="text-green leading-8">
|
||||
{{ JSON.stringify(data) }}
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="my-6 text-center">
|
||||
<button type="warn" size="mini" class="w-160px" :disabled="!data" @click="reset">
|
||||
重置数据
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
// code here
|
||||
import RequestComp from './components/request.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '分包页面',
|
||||
},
|
||||
})
|
||||
|
||||
function gotoScroll() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-sub/demo/scroll',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="text-center">
|
||||
<view class="m-8">
|
||||
http://localhost:9000/#/pages-sub/demo/index
|
||||
</view>
|
||||
<view class="my-4 text-green-500">
|
||||
分包页面demo
|
||||
</view>
|
||||
<view class="text-blue-500">
|
||||
分包页面里面的components示例
|
||||
</view>
|
||||
<button class="my-4" type="primary" size="mini" @click="gotoScroll">
|
||||
跳转到上拉刷新和下拉加载更多
|
||||
</button>
|
||||
<view>
|
||||
<RequestComp />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
||||
@@ -1,72 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// uniapp 页面生命周期
|
||||
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
|
||||
import { useScroll } from '@/hooks/useScroll'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '下拉刷新和下拉加载更多',
|
||||
enablePullDownRefresh: true,
|
||||
onReachBottomDistance: 100,
|
||||
},
|
||||
})
|
||||
|
||||
// 模拟异步获取数据的函数
|
||||
function mockFetchData(page: number, pageSize: number): Promise<{ id: number, name: string }[]> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (page > 5) {
|
||||
// 模拟没有更多数据
|
||||
resolve([])
|
||||
return
|
||||
}
|
||||
const data = Array.from({ length: pageSize }, (_, i) => ({
|
||||
id: (page - 1) * pageSize + i + 1,
|
||||
name: `item ${(page - 1) * pageSize + i + 1}`,
|
||||
}))
|
||||
resolve(data)
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
const { list, loading, finished, error, refresh, loadMore } = useScroll({
|
||||
fetchData: mockFetchData,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
console.log('onPullDownRefresh')
|
||||
console.log('onPullDownRefresh')
|
||||
console.log('onPullDownRefresh')
|
||||
await refresh()
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="h-screen p-4">
|
||||
<view v-if="error" class="text-center text-red-500">
|
||||
加载失败,请重试
|
||||
</view>
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="my-2 h-20 flex items-center justify-center rounded bg-gray-100"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view v-if="loading" class="py-4 text-center text-gray-500">
|
||||
加载中...
|
||||
</view>
|
||||
<view v-if="finished" class="py-4 text-center text-gray-500">
|
||||
没有更多了
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,7 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
import { useThemeStore } from '@/store'
|
||||
import { safeAreaInsets } from '@/utils/systemInfo'
|
||||
|
||||
defineOptions({
|
||||
name: 'Home',
|
||||
})
|
||||
@@ -15,8 +12,6 @@ definePage({
|
||||
},
|
||||
})
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const description = ref(
|
||||
'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite5 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
|
||||
)
|
||||
@@ -25,18 +20,10 @@ console.log('index/index 首页打印了')
|
||||
onLoad(() => {
|
||||
console.log('测试 uni API 自动引入: onLoad')
|
||||
})
|
||||
|
||||
// #region gotoAbout
|
||||
function gotoAbout() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-sub/about/about',
|
||||
})
|
||||
}
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-white px-4 pt-2" :style="{ marginTop: `${safeAreaInsets?.top}px` }">
|
||||
<view class="bg-white px-4 pt-safe">
|
||||
<view class="mt-10">
|
||||
<image src="/static/logo.svg" alt="" class="mx-auto block h-28 w-28" />
|
||||
</view>
|
||||
@@ -62,53 +49,5 @@ function gotoAbout() {
|
||||
https://unibest.tech
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="mt-4 text-center">
|
||||
<a href="https://unibest.tech/base/3-plugin" target="_blank" class="text-green-500">
|
||||
新手请看必看章节1:
|
||||
</a>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="mt-4 text-center">
|
||||
新手请看必看章节1:
|
||||
<text class="text-green-500">
|
||||
https://unibest.tech/base/3-plugin
|
||||
</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="mt-4 text-center">
|
||||
<a href="https://unibest.tech/base/14-faq" target="_blank" class="text-green-500">
|
||||
新手请看必看章节2:
|
||||
</a>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="mt-4 text-center">
|
||||
新手请看必看章节2:
|
||||
<text class="text-green-500">
|
||||
https://unibest.tech/base/14-faq
|
||||
</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<view class="mt-4 text-center">
|
||||
<wd-button type="primary" class="ml-2" @click="themeStore.setThemeVars({ colorTheme: 'red' })">
|
||||
设置主题变量
|
||||
</wd-button>
|
||||
</view>
|
||||
<view class="mt-4 text-center">
|
||||
UI组件官网:<text class="text-green-500">
|
||||
https://wot-design-uni.cn
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-4 text-center">
|
||||
<wd-button type="primary" class="ml-2" @click="gotoAbout">
|
||||
前往示例页
|
||||
</wd-button>
|
||||
</view>
|
||||
<view class="h-6" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,214 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IUploadSuccessInfo } from '@/api/types/login'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { LOGIN_PAGE } from '@/router/config'
|
||||
import { useUserStore } from '@/store'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { useUpload } from '@/utils/uploadFile'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '我的',
|
||||
},
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const tokenStore = useTokenStore()
|
||||
// 使用storeToRefs解构userInfo
|
||||
const { userInfo } = storeToRefs(userStore)
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 上传头像
|
||||
const { run: uploadAvatar } = useUpload<IUploadSuccessInfo>(
|
||||
'/upload',
|
||||
{},
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
console.log('h5头像上传成功', res)
|
||||
useUserStore().setUserAvatar(res.url)
|
||||
},
|
||||
},
|
||||
)
|
||||
// #endif
|
||||
|
||||
// 微信小程序下登录
|
||||
async function handleLogin() {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信登录
|
||||
await tokenStore.wxLogin()
|
||||
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.navigateTo({
|
||||
url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages/me/me')}`,
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
|
||||
// 微信小程序下选择头像事件
|
||||
function onChooseAvatar(e: any) {
|
||||
console.log('选择头像', e.detail)
|
||||
const { avatarUrl } = e.detail
|
||||
const { run } = useUpload<IUploadSuccessInfo>(
|
||||
'/upload',
|
||||
{},
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
console.log('wx头像上传成功', res)
|
||||
useUserStore().setUserAvatar(res.url)
|
||||
},
|
||||
},
|
||||
avatarUrl,
|
||||
)
|
||||
run()
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序下设置用户名
|
||||
function getUserInfo(e: any) {
|
||||
console.log(e.detail)
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 退出登录
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 清空用户信息
|
||||
useTokenStore().logout()
|
||||
// 执行退出登录逻辑
|
||||
uni.showToast({
|
||||
title: '退出登录成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序,去首页
|
||||
// uni.reLaunch({ url: '/pages/index/index' })
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信小程序,去登录页
|
||||
// uni.navigateTo({ url: LOGIN_PAGE })
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="profile-container">
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-info-section">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="avatar-wrapper" @click="uploadAvatar">
|
||||
<image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="user-details">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<input
|
||||
v-model="userInfo.username"
|
||||
type="nickname"
|
||||
class="weui-input"
|
||||
placeholder="请输入昵称"
|
||||
>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="username">
|
||||
{{ userInfo.username }}
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="user-id">
|
||||
ID: {{ userInfo.userId }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-3 break-all px-3">
|
||||
{{ JSON.stringify(userInfo, null, 2) }}
|
||||
</view>
|
||||
|
||||
<view class="mt-20 px-3">
|
||||
<view class="m-auto w-160px text-center">
|
||||
<button v-if="tokenStore.hasLogin" type="warn" class="w-full" @click="handleLogout">
|
||||
退出登录
|
||||
</button>
|
||||
<button v-else type="primary" class="w-full" @click="handleLogin">
|
||||
登录
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-10 text-center text-green-500">
|
||||
我的页面
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 基础样式 */
|
||||
.profile-container {
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
|
||||
// background-color: #f7f8fa;
|
||||
}
|
||||
/* 用户信息区域 */
|
||||
.user-info-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
margin: 30rpx 30rpx 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-right: 40rpx;
|
||||
overflow: hidden;
|
||||
border: 4rpx solid #f5f5f5;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.avatar-button {
|
||||
height: 160rpx;
|
||||
width: 160rpx;
|
||||
padding: 0;
|
||||
margin-right: 40rpx;
|
||||
overflow: hidden;
|
||||
border: 4rpx solid #f5f5f5;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.user-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 38rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-created {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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-fg/login/login'
|
||||
export const REGISTER_PAGE = '/pages-fg/login/register'
|
||||
export const NOT_FOUND_PAGE = '/pages-fg/404/index'
|
||||
|
||||
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', // 示例值
|
||||
'/pages-sub/xxx/index', // 示例值
|
||||
...excludeLoginPathList, // 都是以 / 开头的 path
|
||||
]
|
||||
|
||||
// 在小程序里面是否使用H5的登录页,默认为 false
|
||||
// 如果为 true 则复用 h5 的登录逻辑
|
||||
// TODO: 3/3 确定自己的登录页是否需要在小程序里面使用
|
||||
export const LOGIN_PAGE_ENABLE_IN_MP = false
|
||||
@@ -1,23 +1,12 @@
|
||||
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, NOT_FOUND_PAGE } from './config'
|
||||
import { tabbarStore } from '@/tabbar/store'
|
||||
import { getAllPages, getLastPage, parseUrlToObj } from '@/utils/index'
|
||||
|
||||
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 不同
|
||||
@@ -44,76 +33,19 @@ export const navigateToInterceptor = {
|
||||
}
|
||||
|
||||
// 处理路由不存在的情况
|
||||
if (getAllPages().every(page => page.path !== path) && path !== '/') {
|
||||
if (path !== '/' && !getAllPages().some(page => page.path !== path)) {
|
||||
console.warn('路由不存在:', path)
|
||||
uni.navigateTo({ url: NOT_FOUND_PAGE })
|
||||
return false // 明确表示阻止原路由继续执行
|
||||
}
|
||||
|
||||
// 插件页面
|
||||
if (url.startsWith('plugin://')) {
|
||||
FG_LOG_ENABLE && console.log('路由拦截器 4: plugin:// 路径 ==>', url)
|
||||
path = url
|
||||
}
|
||||
|
||||
// 处理直接进入路由非首页时,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,阻止原路由继续执行
|
||||
}
|
||||
return true // 明确表示允许路由继续执行
|
||||
}
|
||||
// #endregion 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
import request from '@/http/vue-query';
|
||||
import { CustomRequestOptions } from '@/http/types';
|
||||
import { CustomRequestOptions_ } from '@/http/types';
|
||||
|
||||
import * as API from './types';
|
||||
|
||||
/** 用户信息 GET /user/info */
|
||||
export async function infoUsingGet({
|
||||
options,
|
||||
}: {
|
||||
options?: CustomRequestOptions;
|
||||
}) {
|
||||
return request<API.UserItem>('/user/info', {
|
||||
export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) {
|
||||
return request<API.InfoUsingGetResponse>('/user/info', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
import request from '@/http/vue-query';
|
||||
import { CustomRequestOptions } from '@/http/types';
|
||||
import { CustomRequestOptions_ } from '@/http/types';
|
||||
|
||||
import * as API from './types';
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
/** 用户列表 GET /user/listAll */
|
||||
export async function listAllUsingGet({
|
||||
export function listAllUsingGet({
|
||||
options,
|
||||
}: {
|
||||
options?: CustomRequestOptions;
|
||||
options?: CustomRequestOptions_;
|
||||
}) {
|
||||
return request<API.UserItem[]>('/user/listAll', {
|
||||
return request<API.ListAllUsingGetResponse>('/user/listAll', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
|
||||
1
src/static/my-icons/copyright.svg
Normal file
1
src/static/my-icons/copyright.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762219859937" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8816" id="mx_n_1762219859938" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" p-id="8817"></path><path d="M517.6 351.3c53 0 89 33.8 93 83.4 0.3 4.2 3.8 7.4 8 7.4h56.7c2.6 0 4.7-2.1 4.7-4.7 0-86.7-68.4-147.4-162.7-147.4C407.4 290 344 364.2 344 486.8v52.3C344 660.8 407.4 734 517.3 734c94 0 162.7-58.8 162.7-141.4 0-2.6-2.1-4.7-4.7-4.7h-56.8c-4.2 0-7.6 3.2-8 7.3-4.2 46.1-40.1 77.8-93 77.8-65.3 0-102.1-47.9-102.1-133.6v-52.6c0.1-87 37-135.5 102.2-135.5z" p-id="8818"></path></svg>
|
||||
|
After Width: | Height: | Size: 934 B |
@@ -1,4 +1,4 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化
|
||||
|
||||
const store = createPinia()
|
||||
@@ -10,10 +10,11 @@ store.use(
|
||||
},
|
||||
}),
|
||||
)
|
||||
// 立即激活 Pinia 实例, 这样即使在 app.use(store)之前调用 store 也能正常工作 (解决APP端白屏问题)
|
||||
setActivePinia(store)
|
||||
|
||||
export default store
|
||||
|
||||
// 模块统一导出
|
||||
export * from './theme'
|
||||
export * from './token'
|
||||
export * from './user'
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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<ConfigProviderThemeVars>({
|
||||
// colorTheme: 'red',
|
||||
// buttonPrimaryBgColor: '#07c160',
|
||||
// buttonPrimaryColor: '#07c160',
|
||||
})
|
||||
|
||||
/** 设置主题变量 */
|
||||
const setThemeVars = (partialVars: Partial<ConfigProviderThemeVars>) => {
|
||||
themeVars.value = { ...themeVars.value, ...partialVars }
|
||||
}
|
||||
|
||||
/** 切换主题 */
|
||||
const toggleTheme = () => {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
return {
|
||||
/** 设置主题变量 */
|
||||
setThemeVars,
|
||||
/** 切换主题 */
|
||||
toggleTheme,
|
||||
/** 主题变量 */
|
||||
themeVars,
|
||||
/** 主题 */
|
||||
theme,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
@@ -5,17 +5,19 @@
|
||||
`tabbar` 分为 `4 种` 情况:
|
||||
|
||||
- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。
|
||||
- 1 `原生 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。
|
||||
- 优势:原生自带的 tabbar,最先渲染,有缓存。
|
||||
|
||||
- 1 `原生 tabbar`,使用 `switchTab` 切换 `tabbar`,`tabbar` 页面有缓存。
|
||||
- 优势:原生自带的 `tabbar`,最先渲染,有缓存。
|
||||
- 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。
|
||||
- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
|
||||
|
||||
- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 `tabbar`,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
|
||||
- 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。
|
||||
- 劣势:首次点击 tababr 会闪烁。
|
||||
- 劣势:首次点击 `tabbar` 会闪烁。
|
||||
|
||||
- 3 `无缓存自定义 tabbar`,使用 `navigateTo` 切换 `tabbar`,`tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。
|
||||
- 优势:可以随意配置自己想要的 svg icon,切换字体颜色方便。可以实现各种花里胡哨的动效等。
|
||||
- 劣势:首次点击 `tababr` 会闪烁,无缓存。
|
||||
|
||||
|
||||
> 注意:花里胡哨的效果需要自己实现,本模版不提供。
|
||||
|
||||
## tabbar 配置说明
|
||||
@@ -28,6 +30,7 @@
|
||||
`config.ts` 专门配置 `nativeTabbarList` 和 `customTabbarList` 的相关信息,请按照文件里面的注释配置相关项。
|
||||
|
||||
使用 `原生tabbar` 时,不需要关心下面2个文件:
|
||||
|
||||
- `store.ts` ,专门给 `自定义 tabbar` 提供状态管理,代码几乎不需要修改。
|
||||
- `index.vue` ,专门给 `自定义 tabbar` 提供渲染逻辑,代码可以稍微修改,以符合自己的需求。
|
||||
|
||||
@@ -42,6 +45,7 @@
|
||||
"icon": "home",
|
||||
}
|
||||
```
|
||||
|
||||
- unocss 图标
|
||||
|
||||
```js
|
||||
@@ -54,6 +58,7 @@
|
||||
icon: 'i-carbon-code',
|
||||
}
|
||||
```
|
||||
|
||||
- iconfont 图标
|
||||
|
||||
```js
|
||||
@@ -64,6 +69,7 @@
|
||||
icon: 'iconfont icon-my',
|
||||
}
|
||||
```
|
||||
|
||||
- image 本地图片
|
||||
|
||||
```js
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
|
||||
import type { CustomTabBarItem, NativeTabBarItem } from './types'
|
||||
|
||||
/**
|
||||
* tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
|
||||
@@ -22,8 +23,6 @@ export const TABBAR_STRATEGY_MAP = {
|
||||
// 如果是使用 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[] = [
|
||||
{
|
||||
@@ -40,18 +39,6 @@ export const nativeTabbarList: NativeTabBarItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改)
|
||||
export type CustomTabBarItemBadge = number | 'dot'
|
||||
|
||||
export interface CustomTabBarItem {
|
||||
text: string
|
||||
pagePath: string
|
||||
iconType: '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[] = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
// i-carbon-code
|
||||
import type { CustomTabBarItem } from './config'
|
||||
import type { CustomTabBarItem } from './types'
|
||||
import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
|
||||
import { tabbarList, tabbarStore } from './store'
|
||||
|
||||
@@ -39,7 +39,7 @@ function handleClick(index: number) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
}
|
||||
// #ifndef MP-WEIXIN
|
||||
// #ifndef MP-WEIXIN || MP-ALIPAY
|
||||
// 因为有了 custom:true, 微信里面不需要多余的hide操作
|
||||
onLoad(() => {
|
||||
// 解决原生 tabBar 未隐藏导致有2个 tabBar 的问题
|
||||
@@ -54,6 +54,21 @@ onLoad(() => {
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
onMounted(() => {
|
||||
// 解决支付宝自定义tabbar 未隐藏导致有2个 tabBar 的问题; 注意支付宝很特别,需要在 onMounted 钩子调用
|
||||
customTabbarEnable // 另外,支付宝里面,只要是 customTabbar 都需要隐藏
|
||||
&& uni.hideTabBar({
|
||||
fail(err) {
|
||||
console.log('hideTabBar fail: ', err)
|
||||
},
|
||||
success(res) {
|
||||
// console.log('hideTabBar success: ', res)
|
||||
},
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
const activeColor = 'var(--wot-color-theme, #1890ff)'
|
||||
const inactiveColor = '#666'
|
||||
function getColorByIndex(index: number) {
|
||||
@@ -93,7 +108,7 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
|
||||
<!-- 如:<wd-icon name="home" /> (https://wot-design-uni.cn/component/icon.html) -->
|
||||
<!-- 如:<uv-icon name="home" /> (https://www.uvui.cn/components/icon.html) -->
|
||||
<!-- 如:<sar-icon name="image" /> (https://sard.wzt.zone/sard-uniapp-docs/components/icon)(sar没有home图标^_^) -->
|
||||
<wd-icon :name="item.icon" size="20" />
|
||||
<!-- <wd-icon :name="item.icon" size="20" /> -->
|
||||
</template>
|
||||
<template v-if="item.iconType === 'unocss' || item.iconType === 'iconfont'">
|
||||
<view :class="item.icon" class="text-20px" />
|
||||
@@ -130,7 +145,8 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
border-top: 1px solid #eee;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { CustomTabBarItem, CustomTabBarItemBadge } from './config'
|
||||
import type { CustomTabBarItem, CustomTabBarItemBadge } from './types'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import { isNeedLoginMode } from '@/router/config'
|
||||
import { FG_LOG_ENABLE, judgeIsExcludePath } from '@/router/interceptor'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { tabbarList as _tabbarList, customTabbarEnable } from './config'
|
||||
import { tabbarList as _tabbarList, customTabbarEnable, selectedTabbarStrategy, TABBAR_STRATEGY_MAP } from './config'
|
||||
|
||||
// TODO 1/2: 中间的鼓包tabbarItem的开关
|
||||
const BULGE_ENABLE = false
|
||||
@@ -25,6 +22,9 @@ if (customTabbarEnable && BULGE_ENABLE) {
|
||||
}
|
||||
|
||||
export function isPageTabbar(path: string) {
|
||||
if (selectedTabbarStrategy === TABBAR_STRATEGY_MAP.NO_TABBAR) {
|
||||
return false
|
||||
}
|
||||
const _path = path.split('?')[0]
|
||||
return tabbarList.some(item => item.pagePath === _path)
|
||||
}
|
||||
@@ -38,12 +38,8 @@ const tabbarStore = reactive({
|
||||
curIdx: uni.getStorageSync('app-tabbar-index') || 0,
|
||||
prevIdx: uni.getStorageSync('app-tabbar-index') || 0,
|
||||
setCurIdx(idx: number) {
|
||||
const tokenStore = useTokenStore()
|
||||
// 已登录 或 (url 需要登录 && 在白名单 || 不需要登录 && 不在黑名单) (关于 白名单|黑名单 逻辑: src/router/interceptor.ts)
|
||||
if (tokenStore.hasLogin || (isNeedLoginMode && judgeIsExcludePath(tabbarList[idx].pagePath)) || (!isNeedLoginMode && !judgeIsExcludePath(tabbarList[idx].pagePath))) {
|
||||
this.curIdx = idx
|
||||
uni.setStorageSync('app-tabbar-index', idx)
|
||||
}
|
||||
this.curIdx = idx
|
||||
uni.setStorageSync('app-tabbar-index', idx)
|
||||
},
|
||||
setTabbarItemBadge(idx: number, badge: CustomTabBarItemBadge) {
|
||||
if (tabbarList[idx]) {
|
||||
@@ -57,7 +53,6 @@ const tabbarStore = reactive({
|
||||
return
|
||||
}
|
||||
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}`)
|
||||
|
||||
34
src/tabbar/types.ts
Normal file
34
src/tabbar/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
|
||||
import type { RemoveLeadingSlashFromUnion } from '@/typings'
|
||||
|
||||
/**
|
||||
* 原生 tabbar 的单个选项配置
|
||||
*/
|
||||
export type NativeTabBarItem = TabBar['list'][number] & {
|
||||
pagePath: RemoveLeadingSlashFromUnion<_LocationUrl>
|
||||
}
|
||||
|
||||
/** badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改) */
|
||||
export type CustomTabBarItemBadge = number | 'dot'
|
||||
|
||||
/** 自定义 tabbar 的单个选项配置 */
|
||||
export interface CustomTabBarItem {
|
||||
text: string
|
||||
pagePath: RemoveLeadingSlashFromUnion<_LocationUrl>
|
||||
/** 图标类型,不建议用 image 模式,因为需要配置 2 张图,更麻烦 */
|
||||
iconType: 'uiLib' | 'unocss' | 'iconfont' | 'image'
|
||||
/**
|
||||
* icon 的路径
|
||||
* - uiLib: wot-design-uni 图标的 icon prop
|
||||
* - unocss: unocss 图标的类名
|
||||
* - iconfont: iconfont 图标的类名
|
||||
* - image: 图片的路径
|
||||
*/
|
||||
icon: string
|
||||
/** 只有在 image 模式下才需要,传递的是高亮的图片(PS: 不建议用 image 模式) */
|
||||
iconActive?: string
|
||||
/** badge 显示一个数字或 小红点 */
|
||||
badge?: CustomTabBarItemBadge
|
||||
/** 是否是中间的鼓包tabbarItem */
|
||||
isBulge?: boolean
|
||||
}
|
||||
25
src/typings.d.ts
vendored
25
src/typings.d.ts
vendored
@@ -30,6 +30,31 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展 @uni-helper/vite-plugin-uni-pages 的 definePage 参数类型
|
||||
declare module '@uni-helper/vite-plugin-uni-pages' {
|
||||
interface UserPageMeta {
|
||||
/**
|
||||
* 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
|
||||
*
|
||||
* 尽量保证一个项目 只有一个 这个配置,如果有多个,会按照字母顺序来排列,最终可能不是您想要的效果。
|
||||
*/
|
||||
type?: 'home'
|
||||
/**
|
||||
* 页面布局类型, 模板默认只有 default, 如果在 src/layouts 下新增了 layout, 可以扩展当前属性
|
||||
* @default 'default'
|
||||
*
|
||||
* 当前属性供 https://github.com/uni-helper/vite-plugin-uni-layouts 插件使用
|
||||
*/
|
||||
layout?: 'default'
|
||||
/**
|
||||
* 是否从需要登录的路径中排除
|
||||
*
|
||||
* 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹
|
||||
*/
|
||||
excludeLoginPath?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// patch uni 类型
|
||||
// 1. 补全 uni.hideToast() 的 options 类型
|
||||
// 2. 补全 uni.hideLoading() 的 options 类型
|
||||
|
||||
@@ -13,3 +13,9 @@ export interface IUniUploadFileOptions {
|
||||
name?: string
|
||||
formData?: any
|
||||
}
|
||||
|
||||
/** 工具类型:删除字符串开头的第一个斜杠 */
|
||||
export type RemoveLeadingSlash<S extends string> = S extends `/${infer Rest}` ? Rest : S
|
||||
|
||||
/** 工具类型:删除联合类型中每个字符串的第一个斜杠 */
|
||||
export type RemoveLeadingSlashFromUnion<T extends string> = T extends any ? RemoveLeadingSlash<T> : never
|
||||
|
||||
166
src/utils/debounce.ts
Normal file
166
src/utils/debounce.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// fork from https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts
|
||||
// 文档可查看:https://es-toolkit.dev/reference/function/debounce.html
|
||||
// 如需要 throttle 功能,可 copy https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts
|
||||
|
||||
interface DebounceOptions {
|
||||
/**
|
||||
* An optional AbortSignal to cancel the debounced function.
|
||||
*/
|
||||
signal?: AbortSignal
|
||||
|
||||
/**
|
||||
* An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.
|
||||
* If `edges` includes "leading", the function will be invoked at the start of the delay period.
|
||||
* If `edges` includes "trailing", the function will be invoked at the end of the delay period.
|
||||
* If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period.
|
||||
* @default ["trailing"]
|
||||
*/
|
||||
edges?: Array<'leading' | 'trailing'>
|
||||
}
|
||||
|
||||
export interface DebouncedFunction<F extends (...args: any[]) => void> {
|
||||
(...args: Parameters<F>): void
|
||||
|
||||
/**
|
||||
* Schedules the execution of the debounced function after the specified debounce delay.
|
||||
* This method resets any existing timer, ensuring that the function is only invoked
|
||||
* after the delay has elapsed since the last call to the debounced function.
|
||||
* It is typically called internally whenever the debounced function is invoked.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
schedule: () => void
|
||||
|
||||
/**
|
||||
* Cancels any pending execution of the debounced function.
|
||||
* This method clears the active timer and resets any stored context or arguments.
|
||||
*/
|
||||
cancel: () => void
|
||||
|
||||
/**
|
||||
* Immediately invokes the debounced function if there is a pending execution.
|
||||
* This method executes the function right away if there is a pending execution.
|
||||
*/
|
||||
flush: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds
|
||||
* have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel`
|
||||
* method to cancel any pending execution.
|
||||
*
|
||||
* @template F - The type of function.
|
||||
* @param {F} func - The function to debounce.
|
||||
* @param {number} debounceMs - The number of milliseconds to delay.
|
||||
* @param {DebounceOptions} options - The options object
|
||||
* @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function.
|
||||
* @returns A new debounced function with a `cancel` method.
|
||||
*
|
||||
* @example
|
||||
* const debouncedFunction = debounce(() => {
|
||||
* console.log('Function executed');
|
||||
* }, 1000);
|
||||
*
|
||||
* // Will log 'Function executed' after 1 second if not called again in that time
|
||||
* debouncedFunction();
|
||||
*
|
||||
* // Will not log anything as the previous call is canceled
|
||||
* debouncedFunction.cancel();
|
||||
*
|
||||
* // With AbortSignal
|
||||
* const controller = new AbortController();
|
||||
* const signal = controller.signal;
|
||||
* const debouncedWithSignal = debounce(() => {
|
||||
* console.log('Function executed');
|
||||
* }, 1000, { signal });
|
||||
*
|
||||
* debouncedWithSignal();
|
||||
*
|
||||
* // Will cancel the debounced function call
|
||||
* controller.abort();
|
||||
*/
|
||||
export function debounce<F extends (...args: any[]) => void>(
|
||||
func: F,
|
||||
debounceMs: number,
|
||||
{ signal, edges }: DebounceOptions = {},
|
||||
): DebouncedFunction<F> {
|
||||
let pendingThis: any
|
||||
let pendingArgs: Parameters<F> | null = null
|
||||
|
||||
const leading = edges != null && edges.includes('leading')
|
||||
const trailing = edges == null || edges.includes('trailing')
|
||||
|
||||
const invoke = () => {
|
||||
if (pendingArgs !== null) {
|
||||
func.apply(pendingThis, pendingArgs)
|
||||
pendingThis = undefined
|
||||
pendingArgs = null
|
||||
}
|
||||
}
|
||||
|
||||
const onTimerEnd = () => {
|
||||
if (trailing) {
|
||||
invoke()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
cancel()
|
||||
}
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const schedule = () => {
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
|
||||
onTimerEnd()
|
||||
}, debounceMs)
|
||||
}
|
||||
|
||||
const cancelTimer = () => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = null
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
cancelTimer()
|
||||
pendingThis = undefined
|
||||
pendingArgs = null
|
||||
}
|
||||
|
||||
const flush = () => {
|
||||
invoke()
|
||||
}
|
||||
|
||||
const debounced = function (this: any, ...args: Parameters<F>) {
|
||||
if (signal?.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ts/no-this-alias
|
||||
pendingThis = this
|
||||
pendingArgs = args
|
||||
|
||||
const isFirstCall = timeoutId == null
|
||||
|
||||
schedule()
|
||||
|
||||
if (leading && isFirstCall) {
|
||||
invoke()
|
||||
}
|
||||
}
|
||||
|
||||
debounced.schedule = schedule
|
||||
debounced.cancel = cancel
|
||||
debounced.flush = flush
|
||||
|
||||
signal?.addEventListener('abort', cancel, { once: true })
|
||||
|
||||
return debounced
|
||||
}
|
||||
44
src/utils/toLoginPage.ts
Normal file
44
src/utils/toLoginPage.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getLastPage } from '@/utils'
|
||||
import { debounce } from '@/utils/debounce'
|
||||
|
||||
interface ToLoginPageOptions {
|
||||
/**
|
||||
* 跳转模式, uni.navigateTo | uni.reLaunch
|
||||
* @default 'navigateTo'
|
||||
*/
|
||||
mode?: 'navigateTo' | 'reLaunch'
|
||||
/**
|
||||
* 查询参数
|
||||
* @example '?redirect=/pages/home/index'
|
||||
*/
|
||||
queryString?: string
|
||||
}
|
||||
|
||||
// TODO: 自己增加登录页
|
||||
const LOGIN_PAGE = '/pages/login/index'
|
||||
|
||||
/**
|
||||
* 跳转到登录页, 带防抖处理
|
||||
*
|
||||
* 如果要立即跳转,不做延时,可以使用 `toLoginPage.flush()` 方法
|
||||
*/
|
||||
export const toLoginPage = debounce((options: ToLoginPageOptions = {}) => {
|
||||
const { mode = 'navigateTo', queryString = '' } = options
|
||||
|
||||
const url = `${LOGIN_PAGE}${queryString}`
|
||||
|
||||
// 获取当前页面路径
|
||||
const currentPage = getLastPage()
|
||||
const currentPath = `/${currentPage.route}`
|
||||
// 如果已经在登录页,则不跳转
|
||||
if (currentPath === LOGIN_PAGE) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'navigateTo') {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
else {
|
||||
uni.reLaunch({ url })
|
||||
}
|
||||
}, 500)
|
||||
@@ -3,8 +3,8 @@
|
||||
"composite": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@img/*": ["./src/static/*"]
|
||||
@@ -15,7 +15,6 @@
|
||||
"@uni-helper/uni-types",
|
||||
"@uni-helper/vite-plugin-uni-pages",
|
||||
"miniprogram-api-typings",
|
||||
"wot-design-uni/global.d.ts",
|
||||
"z-paging/types",
|
||||
"./src/types/async-component.d.ts",
|
||||
"./src/types/async-import.d.ts",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type {
|
||||
Preset,
|
||||
} from 'unocss'
|
||||
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
|
||||
|
||||
// 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,
|
||||
@@ -26,9 +26,27 @@ export default defineConfig({
|
||||
'display': 'inline-block',
|
||||
'vertical-align': 'middle',
|
||||
},
|
||||
collections: {
|
||||
// 注册本地 SVG 图标集合, 从本地文件系统加载图标
|
||||
// 在 './src/static/my-icons' 目录下的所有 svg 文件将被注册为图标,
|
||||
// my-icons 是图标集合名称,使用 `i-my-icons-图标名` 调用
|
||||
'my-icons': FileSystemIconLoader(
|
||||
'./src/static/my-icons',
|
||||
// 可选的,你可以提供一个 transform 回调来更改每个图标
|
||||
(svg) => {
|
||||
let svgStr = svg
|
||||
|
||||
// 如果 SVG 文件未定义 `fill` 属性,则默认填充 `currentColor`, 这样图标颜色会继承文本颜色,方便在不同场景下适配
|
||||
svgStr = svgStr.includes('fill="') ? svgStr : svgStr.replace(/^<svg /, '<svg fill="currentColor" ')
|
||||
|
||||
// 如果 svg 有 width, 和 height 属性,将这些属性改为 1em,否则无法显示图标
|
||||
svgStr = svgStr.replace(/(<svg.*?width=)"(.*?)"/, '$1"1em"').replace(/(<svg.*?height=)"(.*?)"/, '$1"1em"')
|
||||
|
||||
return svgStr
|
||||
},
|
||||
),
|
||||
},
|
||||
}),
|
||||
// 支持css class属性化
|
||||
presetAttributify(),
|
||||
// TODO: check 是否会有别的影响
|
||||
// 处理低端安卓机的样式问题
|
||||
// 将颜色函数 (rgb()和hsl()) 从空格分隔转换为逗号分隔,更好的兼容性app端,example:
|
||||
@@ -36,6 +54,8 @@ export default defineConfig({
|
||||
// `rgba(255 0 0 / 0.5)` -> `rgba(255, 0, 0, 0.5)`
|
||||
presetLegacyCompat({
|
||||
commaStyleColorFunction: true,
|
||||
legacyColorSpace: true, // by QQ4群-量子蔷薇
|
||||
// @菲鸽 unocss 配置中,建议在 presetLegacyCompat 中添加 legacyColorSpace: true,以去除生成的颜色样式中的 in oklch 关键字,现在发现有些渐变色生成不符合预期
|
||||
}) as Preset,
|
||||
],
|
||||
transformers: [
|
||||
@@ -74,4 +94,26 @@ export default defineConfig({
|
||||
'3xs': ['18rpx', '26rpx'],
|
||||
},
|
||||
},
|
||||
// windows 系统会报错:[plugin:unocss:transformers:pre] Cannot overwrite a zero-length range - use append Left or prependRight instead.
|
||||
// 去掉下面的就正常了
|
||||
// content: {
|
||||
// /**
|
||||
// * 解决小程序报错 `./app.wxss(78:2814): unexpected unexpected at pos 5198`
|
||||
// * 为什么同时使用include和exclude?虽然看起来多余,但同时配置两者是一种常见的 `防御性编程` 做法。
|
||||
// 1. 结构变化保障 : 如果未来项目结构发生变化,某些排除目录可能被移动到包含路径下,exclude配置可以确保它们仍被排除
|
||||
// 2. 明确性 : 明确列出要排除的目录使配置意图更加清晰
|
||||
// 3. 性能优化 : 避免处理不必要的文件,提高构建性能
|
||||
// 4. 防止冲突 : 排除第三方库和构建输出目录,避免潜在的CSS冲突
|
||||
// */
|
||||
// pipeline: {
|
||||
// exclude: [
|
||||
// 'node_modules/**/*',
|
||||
// 'public/**/*',
|
||||
// 'dist/**/*',
|
||||
// ],
|
||||
// include: [
|
||||
// './src/**/*',
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
})
|
||||
|
||||
@@ -64,19 +64,16 @@ export default defineConfig(({ command, mode }) => {
|
||||
envDir: './env', // 自定义env目录
|
||||
base: VITE_APP_PUBLIC_BASE,
|
||||
plugins: [
|
||||
UniLayouts(),
|
||||
UniPlatform(),
|
||||
UniManifest(),
|
||||
UniPages({
|
||||
exclude: ['**/components/**/**.*'],
|
||||
// pages 目录为 src/pages,分包目录不能配置在pages目录下!!
|
||||
// 是个数组,可以配置多个,但是不能为pages里面的目录!!
|
||||
subPackages: [
|
||||
'src/pages-fg', // 这个是相对必要的路由,尽量留着(登录页、注册页、404页等)
|
||||
'src/pages-sub', // 这个多为示例代码,参考用的,开发完后注释掉即可(或者直接删除)
|
||||
],
|
||||
subPackages: [],
|
||||
dts: 'src/types/uni-pages.d.ts',
|
||||
}),
|
||||
UniLayouts(),
|
||||
UniPlatform(),
|
||||
UniManifest(),
|
||||
// Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
|
||||
Optimization({
|
||||
enable: {
|
||||
@@ -90,6 +87,18 @@ export default defineConfig(({ command, mode }) => {
|
||||
logger: false,
|
||||
}),
|
||||
// UniXXX 需要在 Uni 之前引入
|
||||
// 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后
|
||||
UniKuRoot({
|
||||
excludePages: ['**/components/**/**.*'],
|
||||
}),
|
||||
// Components 需要在 Uni 之前引入
|
||||
Components({
|
||||
extensions: ['vue'],
|
||||
deep: true, // 是否递归扫描子目录,
|
||||
directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名,
|
||||
dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
|
||||
}),
|
||||
Uni(),
|
||||
{
|
||||
// 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG
|
||||
// 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952
|
||||
@@ -137,15 +146,6 @@ export default defineConfig(({ command, mode }) => {
|
||||
},
|
||||
),
|
||||
syncManifestPlugin(),
|
||||
Components({
|
||||
extensions: ['vue'],
|
||||
deep: true, // 是否递归扫描子目录,
|
||||
directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名,
|
||||
dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
|
||||
}),
|
||||
// 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后
|
||||
UniKuRoot(),
|
||||
Uni(),
|
||||
// 自动打开开发者工具插件 (必须修改 .env 文件中的 VITE_WX_APPID)
|
||||
openDevTools(),
|
||||
],
|
||||
@@ -186,7 +186,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
: undefined,
|
||||
},
|
||||
esbuild: {
|
||||
drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : ['debugger'],
|
||||
drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : [],
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
|
||||
Reference in New Issue
Block a user