Chore: Chore Update

This commit is contained in:
Z.X.PING
2025-12-03 10:31:41 +08:00
67 changed files with 14605 additions and 1860 deletions

View File

@@ -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`

View File

@@ -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` - 构建生产版本

View File

@@ -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 组件规范
## 平台适配

View File

@@ -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>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)

View File

@@ -1,31 +0,0 @@
# 依赖目录
node_modules
# 版本控制
.git
.gitignore
# 构建产物
/dist
# 开发工具配置
.vscode/
.idea/
.trae/
.cursor/
# 其他配置文件
.github/
.husky/
# 日志文件
logs/
# 缓存文件
.cache/
*.swp
*.swo
# 操作系统文件
.DS_Store

View File

@@ -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
View File

@@ -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
View File

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

View File

@@ -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 组件规范
## 平台适配

View File

@@ -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"
]
}

View File

@@ -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": {

View File

@@ -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;"]

View File

@@ -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
## &#x1F4C2; 快速开始
执行 `pnpm create unibest` 创建项目

View File

@@ -1,3 +0,0 @@
# 参考代码
部分代码片段,供参考。

View File

@@ -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`:为容器指定一个名称

View File

@@ -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,

View File

@@ -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;
}
}
}

View File

@@ -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[]
])

View File

@@ -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",

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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))
}

View File

@@ -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',
]
/**

View File

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

View File

@@ -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
View 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` 状态,在列表底部显示不同的提示信息,提升用户体验。
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。

View File

@@ -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
}
},

View File

@@ -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)
}
// 处理其他错误

View File

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

View File

@@ -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)
}

View File

@@ -1,10 +1,3 @@
<script lang="ts" setup>
const testUniLayoutExposedData = ref('testUniLayoutExposedData')
defineExpose({
testUniLayoutExposedData,
})
</script>
<template>
<slot />
</template>

View File

@@ -1,3 +0,0 @@
# 404 页面
`404页面` 只有在路由不存在时才会显示,如果您不需要可以删除该页面。但是建议保留。

View File

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

View File

@@ -1,3 +0,0 @@
# pages-fg 说明
为了尽量减少主包的大小一些无关紧要但经常需要的页面如登录页、注册页、404页等放在了 `pages-fg` 目录下。

View File

@@ -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` 里面,默认会在登录后自动重定向到来源/配置的页面。
如果与您的业务不符,您可以自行修改。

View File

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

View File

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

View File

@@ -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">
请求调用unocssstatic图片
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 默认不需要登录的情况(黑名单策略) ---------------------------
},
}

View File

@@ -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 || {}),
});

View File

@@ -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 || {}),
});

View 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

View File

@@ -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'

View File

@@ -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,
},
)

View File

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

View File

@@ -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),只需要配置 customTabbarListnativeTabbarList 不生效
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[] = [

View File

@@ -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;
}

View File

@@ -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
View 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
View File

@@ -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 类型

View File

@@ -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
View 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
View 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)

View File

@@ -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",

View File

@@ -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/**/*',
// ],
// },
// },
})

View File

@@ -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,