'init'
8
mp-sc-frontend/.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
|
||||
11
mp-sc-frontend/.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "base",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
3
mp-sc-frontend/.commitlintrc.cjs
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
}
|
||||
51
mp-sc-frontend/.cursor/rules/api-http-patterns.mdc
Normal file
@@ -0,0 +1,51 @@
|
||||
# API 和 HTTP 请求规范
|
||||
|
||||
## HTTP 请求封装
|
||||
- 可以使用 `简单http` 或者 `alova` 或者 `@tanstack/vue-query` 进行请求管理
|
||||
- HTTP 配置在 [src/http/](mdc:src/http/) 目录下
|
||||
- `简单http` - [src/http/http.ts](mdc:src/http/http.ts)
|
||||
- `alova` - [src/http/alova.ts](mdc:src/http/alova.ts)
|
||||
- `vue-query` - [src/http/vue-query.ts](mdc:src/http/vue-query.ts)
|
||||
- 请求拦截器在 [src/http/interceptor.ts](mdc:src/http/interceptor.ts)
|
||||
- 支持请求重试、缓存、错误处理
|
||||
|
||||
## API 接口规范
|
||||
- API 接口定义在 [src/api/](mdc:src/api/) 目录下
|
||||
- 按功能模块组织 API 文件
|
||||
- 使用 TypeScript 定义请求和响应类型
|
||||
- 支持 `简单http`、`alova` 和 `vue-query` 三种请求方式
|
||||
|
||||
|
||||
## 示例代码结构
|
||||
```typescript
|
||||
// API 接口定义
|
||||
export interface LoginParams {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
userInfo: UserInfo
|
||||
}
|
||||
|
||||
// alova 方式
|
||||
export const login = (params: LoginParams) =>
|
||||
http.Post<LoginResponse>('/api/login', params)
|
||||
|
||||
// vue-query 方式
|
||||
export const useLogin = () => {
|
||||
return useMutation({
|
||||
mutationFn: (params: LoginParams) =>
|
||||
http.post<LoginResponse>('/api/login', params)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
- 统一错误处理在拦截器中配置
|
||||
- 支持网络错误、业务错误、认证错误等
|
||||
- 自动处理 token 过期和刷新
|
||||
---
|
||||
globs: src/api/*.ts,src/http/*.ts
|
||||
---
|
||||
43
mp-sc-frontend/.cursor/rules/development-workflow.mdc
Normal file
@@ -0,0 +1,43 @@
|
||||
# 开发工作流程
|
||||
|
||||
## 项目启动
|
||||
1. 安装依赖:`pnpm install`
|
||||
2. 开发环境:
|
||||
- H5: `pnpm dev` 或 `pnpm dev:h5`
|
||||
- 微信小程序: `pnpm dev:mp`
|
||||
- 支付宝小程序: `pnpm dev:mp-alipay`
|
||||
- APP: `pnpm dev:app`
|
||||
|
||||
## 代码规范
|
||||
- 使用 ESLint 进行代码检查:`pnpm lint`
|
||||
- 自动修复代码格式:`pnpm lint:fix`
|
||||
- 使用 eslint 格式化代码
|
||||
- 遵循 TypeScript 严格模式
|
||||
|
||||
## 构建和部署
|
||||
- H5 构建:`pnpm build:h5`
|
||||
- 微信小程序构建:`pnpm build:mp`
|
||||
- 支付宝小程序构建:`pnpm build:mp-alipay`
|
||||
- APP 构建:`pnpm build:app`
|
||||
- 类型检查:`pnpm type-check`
|
||||
|
||||
## 开发工具
|
||||
- 推荐使用 VSCode 编辑器
|
||||
- 安装 Vue 和 TypeScript 相关插件
|
||||
- 使用 uni-app 开发者工具调试小程序
|
||||
- 使用 HBuilderX 调试 APP
|
||||
|
||||
## 调试技巧
|
||||
- 使用 console.log 和 uni.showToast 调试
|
||||
- 利用 Vue DevTools 调试组件状态
|
||||
- 使用网络面板调试 API 请求
|
||||
- 平台差异测试和兼容性检查
|
||||
|
||||
## 性能优化
|
||||
- 使用懒加载和代码分割
|
||||
- 优化图片和静态资源
|
||||
- 减少不必要的重渲染
|
||||
- 合理使用缓存策略
|
||||
---
|
||||
description: 开发工作流程和最佳实践指南
|
||||
---
|
||||
36
mp-sc-frontend/.cursor/rules/project-overview.mdc
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# unibest 项目概览
|
||||
|
||||
这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
|
||||
|
||||
## 项目特点
|
||||
- 支持 H5、小程序、APP 多平台开发
|
||||
- 使用最新的前端技术栈
|
||||
- 内置约定式路由、layout布局、请求封装、登录拦截、自定义tabbar等功能
|
||||
- 无需依赖 HBuilderX,支持命令行开发
|
||||
|
||||
## 核心配置文件
|
||||
- [package.json](mdc:package.json) - 项目依赖和脚本配置
|
||||
- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
|
||||
- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
|
||||
- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
|
||||
- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
|
||||
|
||||
## 主要目录结构
|
||||
- `src/pages/` - 页面文件
|
||||
- `src/components/` - 组件文件
|
||||
- `src/layouts/` - 布局文件
|
||||
- `src/api/` - API 接口
|
||||
- `src/http/` - HTTP 请求封装
|
||||
- `src/store/` - 状态管理
|
||||
- `src/tabbar/` - 底部导航栏
|
||||
- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
|
||||
|
||||
## 开发命令
|
||||
- `pnpm dev` - 开发 H5 版本
|
||||
- `pnpm dev:mp` - 开发微信小程序
|
||||
- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
|
||||
- `pnpm dev:app` - 开发 APP 版本
|
||||
- `pnpm build` - 构建生产版本
|
||||
54
mp-sc-frontend/.cursor/rules/styling-css-patterns.mdc
Normal file
@@ -0,0 +1,54 @@
|
||||
# 样式和 CSS 开发规范
|
||||
|
||||
## UnoCSS 原子化 CSS
|
||||
- 项目使用 UnoCSS 作为原子化 CSS 框架
|
||||
- 配置在 [uno.config.ts](mdc:uno.config.ts)
|
||||
- 支持预设和自定义规则
|
||||
- 优先使用原子化类名,减少自定义 CSS
|
||||
|
||||
## SCSS 规范
|
||||
- 使用 SCSS 预处理器
|
||||
- 样式文件使用 `lang="scss"` 和 `scoped` 属性
|
||||
- 遵循 BEM 命名规范
|
||||
- 使用变量和混入提高复用性
|
||||
|
||||
## 样式组织
|
||||
- 全局样式在 [src/style/](mdc:src/style/) 目录下
|
||||
- 组件样式使用 scoped 作用域
|
||||
- 图标字体在 [src/style/iconfont.css](mdc:src/style/iconfont.css)
|
||||
- 主题变量在 [src/uni_modules/uni-scss/](mdc:src/uni_modules/uni-scss/) 目录下
|
||||
|
||||
## 示例代码结构
|
||||
```vue
|
||||
<template>
|
||||
<view class="container flex flex-col items-center p-4">
|
||||
<text class="title text-lg font-bold mb-2">标题</text>
|
||||
<view class="content bg-gray-100 rounded-lg p-3">
|
||||
<!-- 内容 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
|
||||
.title {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
## 响应式设计
|
||||
- 使用 rpx 单位适配不同屏幕
|
||||
- 支持横屏和竖屏布局
|
||||
- 使用 flexbox 和 grid 布局
|
||||
- 考虑不同平台的样式差异
|
||||
---
|
||||
globs: *.vue,*.scss,*.css
|
||||
---
|
||||
63
mp-sc-frontend/.cursor/rules/uni-app-patterns.mdc
Normal file
@@ -0,0 +1,63 @@
|
||||
# uni-app 开发规范
|
||||
|
||||
## 页面开发
|
||||
- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
|
||||
- 使用约定式路由,文件名即路由路径
|
||||
- 页面配置在仅需要在 宏`definePage` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||
- definePage的顺序在最上面
|
||||
|
||||
## 组件开发
|
||||
- 组件文件放在 [src/components/](mdc:src/components/) 或者 [src/pages/xx/components/](mdc:src/pages/xx/components/) 目录下
|
||||
- 使用 uni-app 内置组件和第三方组件库
|
||||
- 支持 wot-ui\uview-pro\uv-ui\sard-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||
- 自定义组件遵循 uni-app 组件规范
|
||||
|
||||
## 平台适配
|
||||
- 使用条件编译处理平台差异
|
||||
- 支持 H5、小程序、APP 多平台
|
||||
- 注意各平台的 API 差异
|
||||
- 使用 uni.xxx API 替代原生 API
|
||||
|
||||
## 示例代码结构
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// #ifdef H5
|
||||
import { h5Api } from '@/utils/h5'
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
import { mpApi } from '@/utils/mp'
|
||||
// #endif
|
||||
|
||||
const handleClick = () => {
|
||||
// #ifdef H5
|
||||
h5Api.showToast('H5 平台')
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
mpApi.showToast('微信小程序')
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- uni-app 组件 -->
|
||||
<button @click="handleClick">点击</button>
|
||||
|
||||
<!-- 条件渲染 -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view>H5 特有内容</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 生命周期
|
||||
- 使用 uni-app 页面生命周期
|
||||
- onLoad、onShow、onReady、onHide、onUnload
|
||||
- 组件生命周期遵循 Vue3 规范
|
||||
- 注意页面栈和导航管理
|
||||
---
|
||||
globs: src/pages/*.vue,src/components/*.vue
|
||||
---
|
||||
53
mp-sc-frontend/.cursor/rules/vue-typescript-patterns.mdc
Normal file
@@ -0,0 +1,53 @@
|
||||
# Vue3 + TypeScript 开发规范
|
||||
|
||||
## Vue 组件规范
|
||||
- 使用 Composition API 和 `<script setup>` 语法
|
||||
- 组件文件使用 PascalCase 命名
|
||||
- 页面文件放在 `src/pages/` 目录下
|
||||
- 全局组件文件放在 `src/components/` 目录下
|
||||
- 局部组件文件放在页面的 `/components/` 目录下
|
||||
|
||||
## Vue SFC 组件规范
|
||||
- `<script setup lang="ts">` 标签必须是第一个子元素
|
||||
- `<template>` 标签必须是第二个子元素
|
||||
- `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
|
||||
|
||||
## TypeScript 规范
|
||||
- 严格使用 TypeScript,避免使用 `any` 类型
|
||||
- 为 API 响应数据定义接口类型
|
||||
- 使用 `interface` 定义对象类型,`type` 定义联合类型
|
||||
- 导入类型时使用 `import type` 语法
|
||||
|
||||
## 状态管理
|
||||
- 使用 Pinia 进行状态管理
|
||||
- Store 文件放在 `src/store/` 目录下
|
||||
- 使用 `defineStore` 定义 store
|
||||
- 支持持久化存储
|
||||
|
||||
## 示例代码结构
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { UserInfo } from '@/types/user'
|
||||
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化逻辑
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 模板内容 -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
// 样式
|
||||
}
|
||||
</style>
|
||||
---
|
||||
globs: *.vue,*.ts,*.tsx
|
||||
---
|
||||
13
mp-sc-frontend/.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*] # 表示所有文件适用
|
||||
charset = utf-8 # 设置文件字符集为 utf-8
|
||||
indent_style = space # 缩进风格(tab | space)
|
||||
indent_size = 2 # 缩进大小
|
||||
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||
trim_trailing_whitespace = true # 去除行首的任意空白字符
|
||||
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||
|
||||
[*.md] # 表示仅 md 文件适用以下规则
|
||||
max_line_length = off # 关闭最大行长度限制
|
||||
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
||||
50
mp-sc-frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.hbuilderx
|
||||
|
||||
.stylelintcache
|
||||
.eslintcache
|
||||
|
||||
docs/.vitepress/dist
|
||||
docs/.vitepress/cache
|
||||
|
||||
src/types
|
||||
# 单独把这个文件排除掉,用以解决部分电脑生成的 auto-import.d.ts 的API不完整导致类型提示报错问题
|
||||
!src/types/auto-import.d.ts
|
||||
src/manifest.json
|
||||
src/pages.json
|
||||
|
||||
vite.config.ts.*
|
||||
|
||||
# 2025-10-15 by 菲鸽: lock 文件还是需要加入版本管理,今天又遇到版本不一致导致无法运行的问题了。
|
||||
# pnpm-lock.yaml
|
||||
# package-lock.json
|
||||
|
||||
# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
|
||||
# `git rm -r --cached .` 然后提交 commit 即可。
|
||||
|
||||
# git rm -r --cached file1 file2 ## 针对某些文件
|
||||
# git rm -r --cached dir1 dir2 ## 针对某些文件夹
|
||||
# git rm -r --cached . ## 针对所有文件
|
||||
|
||||
# 更新 uni-app 官方版本
|
||||
# npx @dcloudio/uvm@latest
|
||||
9
mp-sc-frontend/.npmrc
Normal file
@@ -0,0 +1,9 @@
|
||||
# registry = https://registry.npmjs.org
|
||||
registry = https://registry.npmmirror.com
|
||||
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
shamefully-hoist=true
|
||||
ignore-workspace-root-check=true
|
||||
install-workspace-root=true
|
||||
node-options=--max-old-space-size=8192
|
||||
123
mp-sc-frontend/.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# unibest 项目概览
|
||||
|
||||
这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
|
||||
|
||||
## 项目特点
|
||||
- 支持 H5、小程序、APP 多平台开发
|
||||
- 使用最新的前端技术栈
|
||||
- 内置约定式路由、layout布局、请求封装等功能
|
||||
- 无需依赖 HBuilderX,支持命令行开发
|
||||
|
||||
## 核心配置文件
|
||||
- [package.json](mdc:package.json) - 项目依赖和脚本配置
|
||||
- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
|
||||
- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
|
||||
- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
|
||||
- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
|
||||
|
||||
## 主要目录结构
|
||||
- `src/pages/` - 页面文件
|
||||
- `src/components/` - 组件文件
|
||||
- `src/layouts/` - 布局文件
|
||||
- `src/api/` - API 接口
|
||||
- `src/http/` - HTTP 请求封装
|
||||
- `src/store/` - 状态管理
|
||||
- `src/tabbar/` - 底部导航栏
|
||||
- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
|
||||
|
||||
## 开发命令
|
||||
- `pnpm dev` - 开发 H5 版本
|
||||
- `pnpm dev:mp` - 开发微信小程序
|
||||
- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
|
||||
- `pnpm dev:app` - 开发 APP 版本
|
||||
- `pnpm build` - 构建生产版本
|
||||
|
||||
## Vue 组件规范
|
||||
- 使用 Composition API 和 `<script setup>` 语法
|
||||
- 组件文件使用 PascalCase 命名
|
||||
- 页面文件放在 `src/pages/` 目录下
|
||||
- 全局组件文件放在 `src/components/` 目录下
|
||||
- 局部组件文件放在页面的 `/components/` 目录下
|
||||
|
||||
## TypeScript 规范
|
||||
- 严格使用 TypeScript,避免使用 `any` 类型
|
||||
- 为 API 响应数据定义接口类型
|
||||
- 使用 `interface` 定义对象类型,`type` 定义联合类型
|
||||
- 导入类型时使用 `import type` 语法
|
||||
|
||||
## 状态管理
|
||||
- 使用 Pinia 进行状态管理
|
||||
- Store 文件放在 `src/store/` 目录下
|
||||
- 使用 `defineStore` 定义 store
|
||||
- 支持持久化存储
|
||||
|
||||
## UnoCSS 原子化 CSS
|
||||
- 项目使用 UnoCSS 作为原子化 CSS 框架
|
||||
- 配置在 [uno.config.ts]
|
||||
- 支持预设和自定义规则
|
||||
- 优先使用原子化类名,减少自定义 CSS
|
||||
|
||||
## Vue SFC 组件规范
|
||||
- `<script setup lang="ts">` 标签必须是第一个子元素
|
||||
- `<template>` 标签必须是第二个子元素
|
||||
- `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
|
||||
|
||||
## 页面开发
|
||||
- 页面文件放在 [src/pages/]目录下
|
||||
- 使用约定式路由,文件名即路由路径
|
||||
- 页面配置在仅需要在 宏`definePage` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||
- definePage的顺序在最上面
|
||||
|
||||
## 组件开发
|
||||
- 全局组件文件放在 `src/components/` 目录下
|
||||
- 局部组件文件放在页面的 `/components/` 目录下
|
||||
- 使用 uni-app 内置组件和第三方组件库
|
||||
- 支持 wot-ui\uview-pro\uv-ui\sard-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||
- 自定义组件遵循 uni-app 组件规范
|
||||
|
||||
## 平台适配
|
||||
- 使用条件编译处理平台差异
|
||||
- 支持 H5、小程序、APP 多平台
|
||||
- 注意各平台的 API 差异
|
||||
- 使用 uni.xxx API 替代原生 API
|
||||
|
||||
## 示例代码结构
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// #ifdef H5
|
||||
import { h5Api } from '@/utils/h5'
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
import { mpApi } from '@/utils/mp'
|
||||
// #endif
|
||||
|
||||
const handleClick = () => {
|
||||
// #ifdef H5
|
||||
h5Api.showToast('H5 平台')
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
mpApi.showToast('微信小程序')
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- uni-app 组件 -->
|
||||
<button @click="handleClick">点击</button>
|
||||
|
||||
<!-- 条件渲染 -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view>H5 特有内容</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 生命周期
|
||||
- 使用 uni-app 页面生命周期
|
||||
- onLoad、onShow、onReady、onHide、onUnload
|
||||
- 组件生命周期遵循 Vue3 规范
|
||||
- 注意页面栈和导航管理
|
||||
15
mp-sc-frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"antfu.unocss",
|
||||
"antfu.iconify",
|
||||
"evils.uniapp-vscode",
|
||||
"uni-helper.uni-helper-vscode",
|
||||
"uni-helper.uni-app-schemas-vscode",
|
||||
"uni-helper.uni-highlight-vscode",
|
||||
"uni-helper.uni-ui-snippets-vscode",
|
||||
"uni-helper.uni-app-snippets-vscode",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
102
mp-sc-frontend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
// 配置语言的文件关联
|
||||
"files.associations": {
|
||||
"pages.json": "jsonc", // pages.json 可以写注释
|
||||
"manifest.json": "jsonc" // manifest.json 可以写注释
|
||||
},
|
||||
|
||||
"stylelint.enable": false, // 禁用 stylelint
|
||||
"css.validate": false, // 禁用 CSS 内置验证
|
||||
"scss.validate": false, // 禁用 SCSS 内置验证
|
||||
"less.validate": false, // 禁用 LESS 内置验证
|
||||
|
||||
// 新版本 VsCode 中这个配置已失效
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
|
||||
// 配置新版本 VsCode 工作区的 TypeScript 的版本
|
||||
"js/ts.tsdk.path": "node_modules/typescript/lib",
|
||||
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
|
||||
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"README.md": "index.html,favicon.ico,robots.txt,CHANGELOG.md",
|
||||
"docker.md": "Dockerfile,docker*.md,nginx*,.dockerignore",
|
||||
"pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts",
|
||||
"package.json": "tsconfig.json,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
|
||||
"eslint.config.mjs": ".commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*"
|
||||
},
|
||||
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
],
|
||||
"cSpell.words": [
|
||||
"alova",
|
||||
"Aplipay",
|
||||
"attributify",
|
||||
"chooseavatar",
|
||||
"climblee",
|
||||
"commitlint",
|
||||
"dcloudio",
|
||||
"iconfont",
|
||||
"oxlint",
|
||||
"qrcode",
|
||||
"refresherrefresh",
|
||||
"scrolltolower",
|
||||
"tabbar",
|
||||
"Toutiao",
|
||||
"uniapp",
|
||||
"unibest",
|
||||
"unocss",
|
||||
"uview",
|
||||
"uvui",
|
||||
"Wechat",
|
||||
"WechatMiniprogram",
|
||||
"Weixin"
|
||||
]
|
||||
}
|
||||
80
mp-sc-frontend/.vscode/vue3.code-snippets
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
// Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"Print unibest Vue3 SFC": {
|
||||
"scope": "vue",
|
||||
"prefix": "v3",
|
||||
"body": [
|
||||
"<script lang=\"ts\" setup>",
|
||||
"definePage({",
|
||||
" style: {",
|
||||
" navigationBarTitleText: '$1',",
|
||||
" },",
|
||||
"})",
|
||||
"defineOptions({",
|
||||
" name: '$2',",
|
||||
" options: { ",
|
||||
" virtualHost: true,",
|
||||
" },",
|
||||
"})",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
" <view class=\"\">$3</view>",
|
||||
"</template>\n",
|
||||
"<style lang=\"scss\" scoped>",
|
||||
"//$4",
|
||||
"</style>\n",
|
||||
],
|
||||
},
|
||||
"Print unibest style": {
|
||||
"scope": "vue",
|
||||
"prefix": "st",
|
||||
"body": [
|
||||
"<style lang=\"scss\" scoped>",
|
||||
"//",
|
||||
"</style>\n"
|
||||
],
|
||||
},
|
||||
"Print unibest script with definePage": {
|
||||
"scope": "vue",
|
||||
"prefix": "sc",
|
||||
"body": [
|
||||
"<script lang=\"ts\" setup>",
|
||||
"definePage({",
|
||||
" style: {",
|
||||
" navigationBarTitleText: '$1',",
|
||||
" },",
|
||||
"})",
|
||||
"defineOptions({",
|
||||
" name: '$2',",
|
||||
" options: { ",
|
||||
" virtualHost: true,",
|
||||
" },",
|
||||
"})",
|
||||
"</script>\n"
|
||||
],
|
||||
},
|
||||
"Print unibest template": {
|
||||
"scope": "vue",
|
||||
"prefix": "te",
|
||||
"body": [
|
||||
"<template>",
|
||||
" <view class=\"\">$1</view>",
|
||||
"</template>\n"
|
||||
],
|
||||
},
|
||||
}
|
||||
21
mp-sc-frontend/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 菲鸽
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
98
mp-sc-frontend/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/unibest-tech/unibest">
|
||||
<img width="160" src="./src/static/logo.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">
|
||||
<a href="https://github.com/unibest-tech/unibest" target="_blank">unibest - 最好的 uniapp 开发框架</a>
|
||||
</h1>
|
||||
|
||||
<div align="center">
|
||||
旧仓库 codercup 进不去了,star 也拿不回来,这里也展示一下那个地址的 star.
|
||||
|
||||
[](https://github.com/codercup/unibest)
|
||||
[](https://github.com/codercup/unibest)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/feige996/unibest)
|
||||
[](https://github.com/feige996/unibest)
|
||||
[](https://gitee.com/feige996/unibest/stargazers)
|
||||
[](https://gitee.com/feige996/unibest/members)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
`unibest` —— 最好的 `uniapp` 开发模板,由 `uniapp` + `Vue3` + `Ts` + `Vite5` + `UnoCss` + `wot-ui` + `z-paging` 构成,使用了最新的前端技术栈,无需依靠 `HBuilderX`,通过命令行方式运行 `web`、`小程序` 和 `App`(编辑器推荐 `VSCode`,可选 `webstorm`)。
|
||||
|
||||
`unibest` 内置了 `约定式路由`、`layout布局`、`请求封装`、`请求拦截`、`登录拦截`、`UnoCSS`、`i18n多语言` 等基础功能,提供了 `代码提示`、`自动格式化`、`统一配置`、`代码片段` 等辅助功能,让你编写 `uniapp` 拥有 `best` 体验 ( `unibest 的由来`)。
|
||||
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://unibest.tech/" target="_blank">📖 文档地址(new)</a>
|
||||
<span style="margin:0 10px;">|</span>
|
||||
<a href="https://unibest-tech.github.io/hello-unibest" target="_blank">📱 DEMO 地址</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
注意旧的地址 [codercup](https://github.com/codercup/unibest) 我进不去了,使用新的 [feige996](https://github.com/feige996/unibest)。PR和 issue 也请使用新地址,否则无法合并。
|
||||
|
||||
## 平台兼容性
|
||||
|
||||
| H5 | IOS | 安卓 | 微信小程序 | 字节小程序 | 快手小程序 | 支付宝小程序 | 钉钉小程序 | 百度小程序 |
|
||||
| --- | --- | ---- | ---------- | ---------- | ---------- | ------------ | ---------- | ---------- |
|
||||
| √ | √ | √ | √ | √ | √ | √ | √ | √ |
|
||||
|
||||
注意每种 `UI框架` 支持的平台有所不同,详情请看各 `UI框架` 的官网,也可以看 `unibest` 文档。
|
||||
|
||||
## ⚙️ 环境
|
||||
|
||||
- node>=18
|
||||
- pnpm>=7.30
|
||||
- Vue Official>=2.1.10
|
||||
- TypeScript>=5.0
|
||||
|
||||
## 新版分支
|
||||
- main == base
|
||||
- base --> base-i18n
|
||||
- base-login --> base-login-i18n
|
||||
|
||||
## 📂 快速开始
|
||||
|
||||
执行 `pnpm create unibest` 创建项目
|
||||
执行 `pnpm i` 安装依赖
|
||||
执行 `pnpm dev` 运行 `H5`
|
||||
执行 `pnpm dev:mp` 运行 `微信小程序`
|
||||
|
||||
## 📦 运行(支持热更新)
|
||||
|
||||
- web平台: `pnpm dev:h5`, 然后打开 [http://localhost:9000/](http://localhost:9000/)。
|
||||
- weixin平台:`pnpm dev:mp` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。
|
||||
- APP平台:`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来运行到对应的平台。)
|
||||
|
||||
## 🔗 发布
|
||||
|
||||
- web平台: `pnpm build:h5`,打包后的文件在 `dist/build/h5`,可以放到web服务器,如nginx运行。如果最终不是放在根目录,可以在 `manifest.config.ts` 文件的 `h5.router.base` 属性进行修改。
|
||||
- weixin平台:`pnpm build:mp`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。
|
||||
- APP平台:`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来发行到对应的平台。)
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](https://opensource.org/license/mit/)
|
||||
|
||||
Copyright (c) 2025 菲鸽
|
||||
|
||||
## 捐赠
|
||||
|
||||
<p align='center'>
|
||||
<img alt="special sponsor appwrite" src="https://oss.laf.run/ukw0y1-site/pay/wepay.png" height="330" style="display:inline-block; height:330px;">
|
||||
<img alt="special sponsor appwrite" src="https://oss.laf.run/ukw0y1-site/pay/alipay.jpg" height="330" style="display:inline-block; height:330px; margin-left:10px;">
|
||||
</p>
|
||||
42
mp-sc-frontend/env/.env
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
VITE_APP_TITLE = '四川凌空天行'
|
||||
VITE_APP_PORT = 9000
|
||||
|
||||
VITE_UNI_APPID = '__UNI__D1E5001'
|
||||
VITE_WX_APPID = 'wx4c5f6fdb5bfa3b3a'
|
||||
|
||||
# 微信开发者工具 CLI 路径,仅当默认安装路径不正确时配置(就是当 pnpm dev:mp 无法自动打开微信开发者工具时,才需要配置,通常是你更改了默认的安装位置导致的,一般出现在windows系统)
|
||||
# macOS 示例:
|
||||
# WECHAT_DEVTOOLS_CLI_PATH = '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
|
||||
# Windows 示例:
|
||||
# WECHAT_DEVTOOLS_CLI_PATH = 'C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat'
|
||||
|
||||
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
||||
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
|
||||
# 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/
|
||||
VITE_APP_PUBLIC_BASE=/
|
||||
|
||||
# 默认后台请求地址
|
||||
# 不同命令会按 mode 叠加读取 .env.development / .env.test / .env.production。
|
||||
# 微信小程序如果没有配置下面的专用地址,也会回退使用这个值。
|
||||
VITE_SERVER_BASEURL = 'http://localhost:28175'
|
||||
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
|
||||
|
||||
# 微信小程序专用后台请求地址,按微信开发者工具 envVersion 区分。
|
||||
# 不配置时会回退使用 VITE_SERVER_BASEURL。
|
||||
# VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://dev.xxx.com'
|
||||
# VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://trial.xxx.com'
|
||||
# VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://prod.xxx.com'
|
||||
|
||||
# h5是否需要配置代理
|
||||
VITE_APP_PROXY_ENABLE = false
|
||||
# 下面的不用修改,只要不跟你后台的统一前缀冲突就行。如果修改了,记得修改 `nginx` 里面的配置
|
||||
VITE_APP_PROXY_PREFIX = '/fg-api'
|
||||
|
||||
# 第二个请求地址 (目前alova中可以使用)
|
||||
VITE_SERVER_BASEURL_SECONDARY = 'http://localhost:28175'
|
||||
|
||||
# 认证模式,'single' | 'double' ==> 单token | 双token
|
||||
VITE_AUTH_MODE = 'single'
|
||||
|
||||
# 原生插件资源复制开关,启用后 App 构建会把根目录 nativeplugins 复制到 dist
|
||||
VITE_COPY_NATIVE_RES_ENABLE = true
|
||||
9
mp-sc-frontend/env/.env.development
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = false
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = false
|
||||
|
||||
# development mode 后台请求地址
|
||||
# VITE_SERVER_BASEURL = 'https://dev.xxx.com'
|
||||
9
mp-sc-frontend/env/.env.production
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'production'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = true
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = false
|
||||
|
||||
# production mode 后台请求地址
|
||||
# VITE_SERVER_BASEURL = 'https://prod.xxx.com'
|
||||
9
mp-sc-frontend/env/.env.test
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = false
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = false
|
||||
|
||||
# test mode 后台请求地址
|
||||
# VITE_SERVER_BASEURL = 'https://test.xxx.com'
|
||||
60
mp-sc-frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
import uniHelper from '@uni-helper/eslint-config'
|
||||
|
||||
export default uniHelper({
|
||||
unocss: true,
|
||||
vue: true,
|
||||
markdown: false,
|
||||
ignores: [
|
||||
// 忽略uni_modules目录
|
||||
'**/uni_modules/',
|
||||
// 忽略原生插件目录
|
||||
'**/nativeplugins/',
|
||||
'dist',
|
||||
// unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
|
||||
'auto-import.d.ts',
|
||||
// vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
|
||||
'uni-pages.d.ts',
|
||||
// 插件生成的文件
|
||||
'src/pages.json',
|
||||
'src/manifest.json',
|
||||
// 忽略自动生成文件
|
||||
'src/service/**',
|
||||
],
|
||||
// https://eslint-config.antfu.me/rules
|
||||
rules: {
|
||||
'no-useless-return': 'off',
|
||||
'no-console': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'vue/no-unused-refs': 'off',
|
||||
'unused-imports/no-unused-vars': 'off',
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
'jsdoc/check-param-names': 'off',
|
||||
'jsdoc/require-returns-description': 'off',
|
||||
'ts/no-empty-object-type': 'off',
|
||||
'no-extend-native': 'off',
|
||||
// uni 条件编译注释可能包裹 import,自动排序会破坏平台条件边界
|
||||
'perfectionist/sort-imports': 'off',
|
||||
'vue/singleline-html-element-content-newline': [
|
||||
'error',
|
||||
{
|
||||
externalIgnores: ['text'],
|
||||
},
|
||||
],
|
||||
// vue SFC 调换顺序改这里
|
||||
'vue/block-order': ['error', {
|
||||
order: [['script', 'template'], 'style'],
|
||||
}],
|
||||
},
|
||||
formatters: {
|
||||
/**
|
||||
* Format CSS, LESS, SCSS files, also the `<style>` blocks in Vue
|
||||
* By default uses Prettier
|
||||
*/
|
||||
css: true,
|
||||
/**
|
||||
* Format HTML files
|
||||
* By default uses Prettier
|
||||
*/
|
||||
html: true,
|
||||
},
|
||||
})
|
||||
BIN
mp-sc-frontend/favicon.ico
Normal file
|
After Width: | Height: | Size: 14 KiB |
26
mp-sc-frontend/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html build-time="%BUILD_TIME%">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
|
||||
<script>
|
||||
var coverSupport =
|
||||
'CSS' in window &&
|
||||
typeof CSS.supports === 'function' &&
|
||||
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') +
|
||||
'" />',
|
||||
)
|
||||
</script>
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
162
mp-sc-frontend/manifest.config.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
// manifest.config.ts
|
||||
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
|
||||
import { loadEnv } from 'vite'
|
||||
|
||||
// 手动解析命令行参数获取 mode
|
||||
function getMode() {
|
||||
const args = process.argv.slice(2)
|
||||
const modeFlagIndex = args.findIndex(arg => arg === '--mode')
|
||||
return modeFlagIndex !== -1 ? args[modeFlagIndex + 1] : args[0] === 'build' ? 'production' : 'development' // 默认 development
|
||||
}
|
||||
// 获取环境变量的范例
|
||||
const env = loadEnv(getMode(), path.resolve(process.cwd(), 'env'))
|
||||
const {
|
||||
VITE_APP_TITLE,
|
||||
VITE_UNI_APPID,
|
||||
VITE_WX_APPID,
|
||||
VITE_APP_PUBLIC_BASE,
|
||||
} = env
|
||||
// console.log('manifest.config.ts env:', env)
|
||||
|
||||
export default defineManifestConfig({
|
||||
'name': VITE_APP_TITLE,
|
||||
'appid': VITE_UNI_APPID,
|
||||
'description': '',
|
||||
'versionName': '1.0.0',
|
||||
'versionCode': '100',
|
||||
'transformPx': false,
|
||||
'h5': {
|
||||
router: {
|
||||
base: VITE_APP_PUBLIC_BASE,
|
||||
},
|
||||
},
|
||||
/* 5+App特有相关 */
|
||||
'app-plus': {
|
||||
usingComponents: true,
|
||||
nvueStyleCompiler: 'uni-app',
|
||||
compilerVersion: 3,
|
||||
compatible: {
|
||||
ignoreVersion: true,
|
||||
},
|
||||
splashscreen: {
|
||||
alwaysShowBeforeRender: true,
|
||||
waiting: true,
|
||||
autoclose: true,
|
||||
delay: 0,
|
||||
},
|
||||
/* 模块配置 */
|
||||
modules: {},
|
||||
/* 应用发布信息 */
|
||||
distribute: {
|
||||
/* android打包配置 */
|
||||
android: {
|
||||
minSdkVersion: 21,
|
||||
targetSdkVersion: 30,
|
||||
abiFilters: ['armeabi-v7a', 'arm64-v8a'],
|
||||
permissions: [
|
||||
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
|
||||
'<uses-permission android:name="android.permission.VIBRATE"/>',
|
||||
'<uses-permission android:name="android.permission.READ_LOGS"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
|
||||
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
|
||||
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.CAMERA"/>',
|
||||
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
|
||||
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
|
||||
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
|
||||
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
|
||||
'<uses-feature android:name="android.hardware.camera"/>',
|
||||
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
|
||||
],
|
||||
},
|
||||
/* ios打包配置 */
|
||||
ios: {},
|
||||
/* SDK配置 */
|
||||
sdkConfigs: {},
|
||||
/* 图标配置 */
|
||||
icons: {
|
||||
android: {
|
||||
hdpi: 'static/app/icons/72x72.png',
|
||||
xhdpi: 'static/app/icons/96x96.png',
|
||||
xxhdpi: 'static/app/icons/144x144.png',
|
||||
xxxhdpi: 'static/app/icons/192x192.png',
|
||||
},
|
||||
ios: {
|
||||
appstore: 'static/app/icons/1024x1024.png',
|
||||
ipad: {
|
||||
'app': 'static/app/icons/76x76.png',
|
||||
'app@2x': 'static/app/icons/152x152.png',
|
||||
'notification': 'static/app/icons/20x20.png',
|
||||
'notification@2x': 'static/app/icons/40x40.png',
|
||||
'proapp@2x': 'static/app/icons/167x167.png',
|
||||
'settings': 'static/app/icons/29x29.png',
|
||||
'settings@2x': 'static/app/icons/58x58.png',
|
||||
'spotlight': 'static/app/icons/40x40.png',
|
||||
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||
},
|
||||
iphone: {
|
||||
'app@2x': 'static/app/icons/120x120.png',
|
||||
'app@3x': 'static/app/icons/180x180.png',
|
||||
'notification@2x': 'static/app/icons/40x40.png',
|
||||
'notification@3x': 'static/app/icons/60x60.png',
|
||||
'settings@2x': 'static/app/icons/58x58.png',
|
||||
'settings@3x': 'static/app/icons/87x87.png',
|
||||
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||
'spotlight@3x': 'static/app/icons/120x120.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
'quickapp': {},
|
||||
/* 小程序特有相关 */
|
||||
'mp-weixin': {
|
||||
appid: VITE_WX_APPID,
|
||||
setting: {
|
||||
urlCheck: false,
|
||||
// 是否启用 ES6 转 ES5
|
||||
es6: true,
|
||||
minified: true,
|
||||
},
|
||||
optimization: {
|
||||
subPackages: true,
|
||||
},
|
||||
// 是否合并组件虚拟节点外层属性,uni-app 3.5.1+ 开始支持。目前仅支持 style、class 属性。
|
||||
// 默认不开启(undefined),这里设置为开启。
|
||||
mergeVirtualHostAttributes: true,
|
||||
// styleIsolation: 'shared',
|
||||
usingComponents: true,
|
||||
// __usePrivacyCheck__: true,
|
||||
},
|
||||
'mp-alipay': {
|
||||
usingComponents: true,
|
||||
styleIsolation: 'shared',
|
||||
optimization: {
|
||||
subPackages: true,
|
||||
},
|
||||
// 解决支付宝小程序开发工具报错 【globalThis is not defined】
|
||||
compileOptions: {
|
||||
globalObjectMode: 'enable',
|
||||
transpile: {
|
||||
script: {
|
||||
ignore: ['node_modules/**'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'mp-baidu': {
|
||||
usingComponents: true,
|
||||
},
|
||||
'mp-toutiao': {
|
||||
usingComponents: true,
|
||||
},
|
||||
'uniStatistics': {
|
||||
enable: false,
|
||||
},
|
||||
'vueVersion': '3',
|
||||
})
|
||||
14
mp-sc-frontend/openapi-ts-request.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'openapi-ts-request'
|
||||
|
||||
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_',
|
||||
isGenReactQuery: false,
|
||||
reactQueryMode: 'vue',
|
||||
isGenJavaScript: false,
|
||||
},
|
||||
])
|
||||
215
mp-sc-frontend/package.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"name": "sc-lktx-frontend",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"unibest": {
|
||||
"platforms": [
|
||||
"h5",
|
||||
"mp-weixin",
|
||||
"app"
|
||||
],
|
||||
"uiLibrary": "wot-ui-v2",
|
||||
"loginStrategy": true,
|
||||
"i18n": false,
|
||||
"chartLibraries": [],
|
||||
"cliVersion": "4.0.16",
|
||||
"unibestVersion": "4.4.1",
|
||||
"unibestUpdateTime": "2026-05-22",
|
||||
"createdAt": "2026-06-06 23:22:43"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"description": "unibest - 最好的 uniapp 开发模板",
|
||||
"generate-time": "用户创建项目时生成",
|
||||
"author": {
|
||||
"name": "feige996",
|
||||
"zhName": "菲鸽",
|
||||
"email": "1020103647@qq.com",
|
||||
"github": "https://github.com/feige996",
|
||||
"gitee": "https://gitee.com/feige996"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://unibest.tech",
|
||||
"repository": "https://github.com/feige996/unibest",
|
||||
"bugs": {
|
||||
"url": "https://github.com/feige996/unibest/issues",
|
||||
"url-old": "https://github.com/codercup/unibest/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
"pnpm": ">=9"
|
||||
},
|
||||
"scripts": {
|
||||
"openapi": "openapi-ts",
|
||||
"init-baseFiles": "node ./scripts/create-base-files.js",
|
||||
"prepare": "pnpm init-baseFiles",
|
||||
"predev": "pnpm init-baseFiles",
|
||||
"predev:app": "pnpm init-baseFiles",
|
||||
"predev:mp": "pnpm init-baseFiles",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"uvm": "npx @dcloudio/uvm@latest",
|
||||
"uvm-rm": "node ./scripts/postupgrade.js",
|
||||
"bump-version": "node ./scripts/bump-version.js",
|
||||
"postuvm": "echo upgrade uni-app success!",
|
||||
"dev:app": "uni -p app",
|
||||
"dev:app:test": "uni -p app --mode test",
|
||||
"dev:app:prod": "uni -p app --mode production",
|
||||
"dev:app-android": "uni -p app-android",
|
||||
"dev:app-ios": "uni -p app-ios",
|
||||
"dev:custom": "uni -p",
|
||||
"dev": "uni",
|
||||
"dev:test": "uni --mode test",
|
||||
"dev:prod": "uni --mode production",
|
||||
"dev:h5": "uni",
|
||||
"dev:h5:test": "uni --mode test",
|
||||
"dev:h5:prod": "uni --mode production",
|
||||
"dev:h5:ssr": "uni --ssr",
|
||||
"dev:mp": "uni -p mp-weixin",
|
||||
"dev:mp:test": "uni -p mp-weixin --mode test",
|
||||
"dev:mp:prod": "uni -p mp-weixin --mode production",
|
||||
"dev:mp-alipay": "uni -p mp-alipay",
|
||||
"dev:mp-baidu": "uni -p mp-baidu",
|
||||
"dev:mp-jd": "uni -p mp-jd",
|
||||
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||
"dev:mp-lark": "uni -p mp-lark",
|
||||
"dev:mp-qq": "uni -p mp-qq",
|
||||
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"dev:mp-xhs": "uni -p mp-xhs",
|
||||
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||
"build:app": "uni build -p app",
|
||||
"build:app:test": "uni build -p app --mode test",
|
||||
"build:app:prod": "uni build -p app --mode production",
|
||||
"build:app-android": "uni build -p app-android",
|
||||
"build:app-ios": "uni build -p app-ios",
|
||||
"build:custom": "uni build -p",
|
||||
"build:h5": "uni build",
|
||||
"build:h5:test": "uni build --mode test",
|
||||
"build:h5:prod": "uni build --mode production",
|
||||
"build": "uni build",
|
||||
"build:test": "uni build --mode test",
|
||||
"build:prod": "uni build --mode production",
|
||||
"build:h5:ssr": "uni build --ssr",
|
||||
"build:mp-alipay": "uni build -p mp-alipay",
|
||||
"build:mp": "uni build -p mp-weixin",
|
||||
"build:mp:test": "uni build -p mp-weixin --mode test",
|
||||
"build:mp:prod": "uni build -p mp-weixin --mode production",
|
||||
"build:mp-baidu": "uni build -p mp-baidu",
|
||||
"build:mp-jd": "uni build -p mp-jd",
|
||||
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||
"build:mp-lark": "uni build -p mp-lark",
|
||||
"build:mp-qq": "uni build -p mp-qq",
|
||||
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"build:mp-xhs": "uni build -p mp-xhs",
|
||||
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"upload:changeset": "pnpm changeset && pnpm changeset version",
|
||||
"upload:mp": "node ./scripts/upload-weixin.js",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/adapter-uniapp": "^2.0.14",
|
||||
"@alova/shared": "^1.3.1",
|
||||
"@changesets/cli": "^2.30.0",
|
||||
"@dcloudio/uni-app": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-components": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-harmony": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
|
||||
"@wot-ui/ui": "latest",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"alova": "^3.3.3",
|
||||
"dayjs": "1.11.10",
|
||||
"js-md5": "^0.8.3",
|
||||
"pinia": "2.0.36",
|
||||
"pinia-plugin-persistedstate": "3.2.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "4.5.1",
|
||||
"z-paging": "2.8.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4070620250821001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4070620250821001",
|
||||
"@iconify-json/carbon": "^1.2.4",
|
||||
"@iconify/utils": "^3.0.2",
|
||||
"@types/node": "^20.17.9",
|
||||
"@uni-helper/eslint-config": "0.5.0",
|
||||
"@uni-helper/plugin-uni": "0.1.0",
|
||||
"@uni-helper/uni-env": "0.1.8",
|
||||
"@uni-helper/uni-types": "1.0.0-alpha.6",
|
||||
"@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.12",
|
||||
"@uni-helper/vite-plugin-uni-pages": "0.3.23",
|
||||
"@uni-helper/vite-plugin-uni-platform": "0.0.5",
|
||||
"@uni-ku/bundle-optimizer": "v1.3.15-beta.2",
|
||||
"@uni-ku/root": "1.4.1",
|
||||
"@unocss/eslint-plugin": "^66.2.3",
|
||||
"@unocss/preset-legacy-compat": "66.0.0",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
"@vue/runtime-core": "^3.4.21",
|
||||
"@vue/test-utils": "^2.4.10",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^10.0.0",
|
||||
"enquirer": "^2.4.1",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-format": "^1.0.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"miniprogram-api-typings": "^4.1.0",
|
||||
"miniprogram-ci": "^2.1.26",
|
||||
"openapi-ts-request": "^1.10.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-html": "^1.8.0",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sass": "1.77.8",
|
||||
"std-env": "^3.9.0",
|
||||
"typescript": "~5.8.0",
|
||||
"unocss": "66.0.0",
|
||||
"unplugin-auto-import": "^20.0.0",
|
||||
"vite": "5.2.8",
|
||||
"vite-plugin-restart": "^1.0.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vitest": "3.2.4",
|
||||
"vue-tsc": "^3.0.6"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"unconfig": "7.3.2"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"unconfig": "7.3.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"bin-wrapper": "npm:bin-wrapper-china",
|
||||
"unconfig": "7.3.2"
|
||||
}
|
||||
}
|
||||
23
mp-sc-frontend/pages.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
|
||||
import { tabBar } from './src/tabbar/config'
|
||||
|
||||
export default defineUniPages({
|
||||
globalStyle: {
|
||||
navigationStyle: 'default',
|
||||
navigationBarTitleText: '四川凌空天行',
|
||||
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',
|
||||
'^wd-(.*)': '@wot-ui/ui/components/wd-$1/wd-$1.vue',
|
||||
},
|
||||
},
|
||||
// tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
|
||||
tabBar: tabBar as any,
|
||||
})
|
||||
22052
mp-sc-frontend/pnpm-lock.yaml
generated
Normal file
27
mp-sc-frontend/private.wx4c5f6fdb5bfa3b3a.key
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEA3KecWDuhVTo6aFpTGnJK8MEEjjkxfz3aCMpvRuX7Pg43QYSF
|
||||
99hLkyW9QRPKl16ANEL6WW7O6YozBPOp/g5NEym24q2B1s0NGGNj0WJLc7Qt4LEV
|
||||
2IYZX16rfpoY9aApLFlO1KFCt3UbS8sJMo2IpUuocCauS4R128Ycdu7qPgclwod9
|
||||
A/QmlLHAue/t6tAfb/Tf8fB7/+bJEmLgKT0w85I3v4hOt35mvvIRcciz/wB9U+B0
|
||||
cE+u6XUtqVik9fPjjSuqQYxg+oV8Aj/LO4lvMusSNFOQJJDEo42DJfyjRBCaQrjh
|
||||
9pBiOmQxeqFVZVPcosJ12ukdeZdFsmd43046RQIDAQABAoIBAQDY6SgHkK77Yl9S
|
||||
gCCbqelDnOtGiLDAvePdqmsTjjeafE0TahxsVUON5paSJ8uLXAm51nHWgtiCuimH
|
||||
X6Unq5VXFjXDxf8SUsbhx6qzheZYWrKS5GJuVP0SRLVfokqRA54WC8Ezw0cbo9Ju
|
||||
gqyK9plyrNprTYsfj5pwruMCg8DfsUEphlKQZj7bWmXYRYH/Ax7Z5PUn3aEk0van
|
||||
K/DL4ky250E3Iliab6TwBRN5hAhGgCLvQkiT+iRKfGtRHpoo0iFyLTaobQshkGcu
|
||||
CRBsI7rckIPMzQPN2LMjkhw9s3SqZKqAL0rAKkZ1wEE+gy/D1FgMGgMXaeYFIU6Z
|
||||
ZIxLsc8BAoGBAPoLxb2yQz8R6AdJcUuoBOzpjeopqERi2sihJjd+YNJuufNQACvA
|
||||
j/64zuu0Rb5o7XLI7FrRO3wWhkLIt43iTwbTTW6HfknbxI82CiE0XflVYqNrA+zf
|
||||
NH4gs1vzFxIpp8WN0kWPv/a+irRcCEnjU3xVQSJ73nwRMD5RXGfvvQXBAoGBAOHo
|
||||
rObDJXklOGaPI0R6RNWL3+aYjUpe/500bSgsZGtbWz69bJ3LNWjKzqaK5tAlDSBw
|
||||
lnVlGd0kdgZrn2FgrOA8zMw5g4pG6Kh9UxIxBO2kC5GHx2yE4nTZr8Be04RnctY8
|
||||
sSFeafAaXrpcnL9MPvoaiAxaaIh/Q+xLzsKHC32FAoGBAPUONpTsMTWNsg36L1wL
|
||||
ZhBd8SSuAOhMzcjVDqRSakeyFvHb1N8MUNM+giTEv5mWMihNvD5hUuARHzIyjpoy
|
||||
UmsJCZkql12BUglc196k+PiUcyBfkDBErKh0GfQisNivFGrrzEk6UdNb+Io8rC7l
|
||||
6Pswfq5yIaEMI3DfwiVm8qTBAoGBAM3fCJJTjLbWIIv2LaGd+1TQX375zujTogZV
|
||||
XJSbv/fGDWUjovQ517Zj++bx9l4BJfFGKRdaxzMsoxI+ycQoIeNIBSqnzyQYcrX5
|
||||
X9bYLTGTqac6IZbXkrgCGZQp1oB29cQfExzhuZFBtsoG1CHRDiNGQm1fhpu9vtx8
|
||||
STQldWcxAoGAUkjxh98VXGjwhzqW9CCm5EVnd5HT3/cB1bu9hzfscaRkS3Jy9MyP
|
||||
XtlhixpA4ZR8o0BDai85aVHlXDcbEPO39oybrS9b+kgm8KTuA4UfADo1v0fDc9yV
|
||||
OTC0CB3GXOayas9hf9fjMpOUUhtKDsgpXcDb6LwG69mCQF2iVuv2cXk=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
154
mp-sc-frontend/scripts/bump-version.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import enquirer from 'enquirer'
|
||||
import pc from 'picocolors'
|
||||
|
||||
const manifestPath = path.resolve(process.cwd(), 'manifest.config.ts')
|
||||
// 获取是否只是测试运行
|
||||
const dryRun = process.argv.includes('--dry-run')
|
||||
|
||||
/**
|
||||
* 将语义化版本号根据传递的类型进行递增
|
||||
* @param {string} version 当前版本号,如 '1.1.0'
|
||||
* @param {string} type 升级类型: 'patch' | 'minor' | 'major' | 'none'
|
||||
* @returns {string} 新的版本号
|
||||
*/
|
||||
function bumpVersionName(version, type) {
|
||||
if (type === 'none') return version
|
||||
|
||||
const parts = version.split('.')
|
||||
let major = Number.parseInt(parts[0] || '0', 10)
|
||||
let minor = Number.parseInt(parts[1] || '0', 10)
|
||||
let patch = Number.parseInt(parts[2] || '0', 10)
|
||||
|
||||
switch (type) {
|
||||
case 'major':
|
||||
major += 1; minor = 0; patch = 0;
|
||||
break;
|
||||
case 'minor':
|
||||
minor += 1; patch = 0;
|
||||
break;
|
||||
case 'patch':
|
||||
patch += 1;
|
||||
break;
|
||||
}
|
||||
return `${major}.${minor}.${patch}`
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const source = fs.readFileSync(manifestPath, 'utf8')
|
||||
|
||||
// 匹配 versionCode 和 versionName 的正则表达式,支持键名带有引号的情况,例如 'versionCode': '100'
|
||||
const versionCodeRegex = /((?:['"])?versionCode(?:['"])?\s*:\s*)(['"])(\d+)\2/
|
||||
const versionNameRegex = /((?:['"])?versionName(?:['"])?\s*:\s*)(['"])([\d\.]+)\2/
|
||||
|
||||
const codeMatch = source.match(versionCodeRegex)
|
||||
const nameMatch = source.match(versionNameRegex)
|
||||
|
||||
if (!codeMatch || !nameMatch) {
|
||||
console.error(pc.red('✖ [bump-version] 未在 manifest.config.ts 中找到合法的 versionCode 或 versionName'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const currentVersionCode = Number.parseInt(codeMatch[3], 10)
|
||||
const currentVersionName = nameMatch[3]
|
||||
const nextVersionCode = String(currentVersionCode + 1)
|
||||
|
||||
// 1. 检查命令行参数是否有 --type
|
||||
const typeArgMatch = process.argv.find(arg => arg.startsWith('--type='))
|
||||
let bumpType = typeArgMatch ? typeArgMatch.split('=')[1] : null
|
||||
|
||||
// 2. 环境判定:如果不在交互终端或者是CI环境,但是没有指定类型,则默认只升级 versionCode
|
||||
const isInteractive = process.stdout.isTTY && !process.env.CI
|
||||
|
||||
if (!bumpType) {
|
||||
if (!isInteractive) {
|
||||
console.log(pc.yellow('⚠ [bump-version] 非交互环境且未指定参数,默认不修改 versionName'))
|
||||
bumpType = 'none'
|
||||
} else {
|
||||
// 3. 在终端交互式询问用户怎么处理 versionName
|
||||
console.log('')
|
||||
console.log(pc.cyan('📦 准备发布新版本'))
|
||||
console.log(`${pc.gray('当前版本:')} ${pc.bold(currentVersionName)} ${pc.gray(`(v${currentVersionCode})`)}`)
|
||||
console.log('')
|
||||
|
||||
const response = await enquirer.prompt({
|
||||
type: 'select',
|
||||
name: 'selectedType',
|
||||
message: '请选择如何升级版本名称 (versionName)?',
|
||||
pointer: pc.cyan('❯'),
|
||||
choices: [
|
||||
{
|
||||
message: `${pc.bold('修复')} (Patch) ${pc.gray(currentVersionName)} → ${pc.green(bumpVersionName(currentVersionName, 'patch'))}`,
|
||||
name: 'patch',
|
||||
hint: pc.gray('修复Bug、极小的代码安全变动')
|
||||
},
|
||||
{
|
||||
message: `${pc.bold('特性')} (Minor) ${pc.gray(currentVersionName)} → ${pc.cyan(bumpVersionName(currentVersionName, 'minor'))}`,
|
||||
name: 'minor',
|
||||
hint: pc.gray('新增功能、向下兼容的API更新')
|
||||
},
|
||||
{
|
||||
message: `${pc.bold('重大')} (Major) ${pc.gray(currentVersionName)} → ${pc.magenta(bumpVersionName(currentVersionName, 'major'))}`,
|
||||
name: 'major',
|
||||
hint: pc.gray('重大重构、不兼容的API修改')
|
||||
},
|
||||
{
|
||||
message: `${pc.bold('仅Code')} (None) ${pc.gray('保持 ' + currentVersionName)}`,
|
||||
name: 'none',
|
||||
hint: pc.gray('保持名称不变,仅升级构建号(versionCode)')
|
||||
},
|
||||
{
|
||||
message: `${pc.bold('取消')} (Cancel) ${pc.gray('完全不升级版本')}`,
|
||||
name: 'cancel',
|
||||
hint: pc.gray('跳过版本修改,直接进入后续打包流程')
|
||||
},
|
||||
],
|
||||
// 如果用户按 ctrl+c 退出
|
||||
onCancel: () => {
|
||||
console.log(pc.red('✖ 取消操作并退出编译'))
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
bumpType = response.selectedType
|
||||
|
||||
// 用户选择了完全不升级
|
||||
if (bumpType === 'cancel') {
|
||||
console.log('')
|
||||
console.log(pc.green('✔ 已跳过版本升级操作'))
|
||||
console.log('')
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 根据类型计算下一代版本并进行替换计算
|
||||
const nextVersionName = bumpVersionName(currentVersionName, bumpType)
|
||||
|
||||
let updated = source.replace(versionCodeRegex, `${codeMatch[1]}${codeMatch[2]}${nextVersionCode}${codeMatch[2]}`)
|
||||
updated = updated.replace(versionNameRegex, `${nameMatch[1]}${nameMatch[2]}${nextVersionName}${nameMatch[2]}`)
|
||||
|
||||
// 5. 回填文件内容
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(manifestPath, updated, 'utf8')
|
||||
}
|
||||
|
||||
// 美化成功提示输出
|
||||
console.log('')
|
||||
console.log(pc.green(`✔ ${dryRun ? '(模拟运行) ' : ''}版本更新成功!`))
|
||||
|
||||
if (bumpType !== 'none') {
|
||||
console.log(` ${pc.gray('versionName:')} ${pc.strikethrough(pc.gray(currentVersionName))} → ${pc.bold(pc.green(nextVersionName))}`)
|
||||
} else {
|
||||
console.log(` ${pc.gray('versionName:')} ${pc.dim(currentVersionName)} (未更改)`)
|
||||
}
|
||||
|
||||
console.log(` ${pc.gray('versionCode:')} ${pc.strikethrough(pc.gray(currentVersionCode))} → ${pc.bold(pc.green(nextVersionCode))}`)
|
||||
console.log('')
|
||||
}
|
||||
|
||||
run().catch(err => {
|
||||
console.error(pc.red('✖ [bump-version] 发生错误:'), err)
|
||||
process.exit(1)
|
||||
})
|
||||
55
mp-sc-frontend/scripts/create-base-files.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// 基础配置文件生成脚本
|
||||
// 此脚本用于生成 src/manifest.json 和 src/pages.json 基础文件
|
||||
// 由于这两个配置文件会被添加到 .gitignore 中,因此需要通过此脚本确保项目能正常运行
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// 获取当前文件的目录路径(替代 CommonJS 中的 __dirname)
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// 最简可运行配置
|
||||
const manifest = { }
|
||||
const pages = {
|
||||
pages: [
|
||||
{
|
||||
path: 'pages/index/index',
|
||||
type: 'home',
|
||||
style: {
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '首页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pages/me/me',
|
||||
type: 'page',
|
||||
style: {
|
||||
navigationBarTitleText: '我的',
|
||||
},
|
||||
},
|
||||
],
|
||||
subPackages: [],
|
||||
}
|
||||
|
||||
// 使用修复后的 __dirname 来解析文件路径
|
||||
const manifestPath = path.resolve(__dirname, '../src/manifest.json')
|
||||
const pagesPath = path.resolve(__dirname, '../src/pages.json')
|
||||
|
||||
// 确保 src 目录存在
|
||||
const srcDir = path.resolve(__dirname, '../src')
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
fs.mkdirSync(srcDir, { recursive: true })
|
||||
}
|
||||
|
||||
const MIN_SIZE = `{ }`.length // 如果只有一个空对象,必定是不对的,需要重新生成
|
||||
|
||||
// 如果 src/manifest.json 不存在,就创建它;或者如果文件大小小于等于 MIN_SIZE,也重新创建
|
||||
if (!fs.existsSync(manifestPath) || fs.statSync(manifestPath).size <= MIN_SIZE) {
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
}
|
||||
|
||||
// 如果 src/pages.json 不存在,就创建它;或者如果文件大小小于等于 MIN_SIZE,也重新创建
|
||||
if (!fs.existsSync(pagesPath) || fs.statSync(pagesPath).size <= MIN_SIZE) {
|
||||
fs.writeFileSync(pagesPath, JSON.stringify(pages, null, 2))
|
||||
}
|
||||
107
mp-sc-frontend/scripts/open-dev-tools.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { exec } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
/**
|
||||
* 打开开发者工具
|
||||
* @param {string} env - 环境,'dev' 或 'build'
|
||||
* @param {object} options - 配置选项
|
||||
* @param {string} options.wechatDevtoolsCliPath - 微信开发者工具 CLI 路径
|
||||
*/
|
||||
function _openDevTools(env = 'dev', options = {}) {
|
||||
const { wechatDevtoolsCliPath } = options
|
||||
const platform = process.platform // darwin, win32, linux
|
||||
const { UNI_PLATFORM } = process.env // mp-weixin, mp-alipay, mp-lark
|
||||
|
||||
const uniPlatformText = UNI_PLATFORM === 'mp-weixin' ? '微信小程序' : UNI_PLATFORM === 'mp-alipay' ? '支付宝小程序' : UNI_PLATFORM === 'mp-lark' ? '抖音小程序' : '小程序'
|
||||
|
||||
// 项目路径(构建输出目录),根据环境选择不同目录
|
||||
const outputDir = env === 'build' ? `dist/build/${UNI_PLATFORM}` : `dist/dev/${UNI_PLATFORM}`
|
||||
const projectPath = path.resolve(process.cwd(), outputDir)
|
||||
|
||||
// 检查构建输出目录是否存在
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
console.log(`❌ ${uniPlatformText}构建目录不存在:`, projectPath)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`🚀 正在打开${uniPlatformText}开发者工具...`)
|
||||
|
||||
// 根据不同操作系统执行不同命令
|
||||
let command = ''
|
||||
|
||||
if (platform === 'darwin') {
|
||||
// macOS
|
||||
if (UNI_PLATFORM === 'mp-weixin') {
|
||||
const cliPath = wechatDevtoolsCliPath || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
|
||||
command = `"${cliPath}" -o "${projectPath}"`
|
||||
}
|
||||
else if (UNI_PLATFORM === 'mp-alipay') {
|
||||
command = `/Applications/小程序开发者工具.app/Contents/MacOS/小程序开发者工具 --p "${projectPath}"`
|
||||
}
|
||||
else if (UNI_PLATFORM === 'mp-lark') {
|
||||
command = `/Applications/抖音开发者工具.app/Contents/MacOS/抖音开发者工具 --p "${projectPath}"`
|
||||
}
|
||||
}
|
||||
else if (platform === 'win32' || platform === 'win64') {
|
||||
// Windows
|
||||
if (UNI_PLATFORM === 'mp-weixin') {
|
||||
const cliPath = wechatDevtoolsCliPath || 'C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat'
|
||||
command = `"${cliPath}" -o "${projectPath}"`
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Linux 或其他系统
|
||||
console.log('❌ 当前系统不支持自动打开微信开发者工具')
|
||||
return
|
||||
}
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(`❌ 打开${uniPlatformText}开发者工具失败:`, error.message)
|
||||
if (UNI_PLATFORM === 'mp-weixin') {
|
||||
console.log('💡 当前使用的微信开发者工具 CLI 命令:', command)
|
||||
console.log('💡 如果安装位置不同,可以在 env/.env 配置 WECHAT_DEVTOOLS_CLI_PATH 为本机实际 CLI 路径')
|
||||
}
|
||||
console.log(`💡 请确保${uniPlatformText}开发者工具服务端口已启用`)
|
||||
console.log(`💡 可以手动打开${uniPlatformText}开发者工具并导入项目:`, projectPath)
|
||||
return
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.log('⚠️ 警告:', stderr)
|
||||
}
|
||||
|
||||
console.log(`✅ ${uniPlatformText}开发者工具已打开`)
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Vite 插件,用于自动打开开发者工具
|
||||
* @param {object} options - 配置选项
|
||||
* @param {string} options.mode - 构建模式,'development' 或 'production'
|
||||
* @param {string} options.wechatDevtoolsCliPath - 微信开发者工具 CLI 路径
|
||||
*/
|
||||
export default function openDevTools(options = {}) {
|
||||
const { mode = 'development', wechatDevtoolsCliPath } = options
|
||||
// 根据 mode 确定环境:development -> dev, production -> build
|
||||
const env = mode === 'production' ? 'build' : 'dev'
|
||||
|
||||
// 首次构建标记
|
||||
let isFirstBuild = true
|
||||
|
||||
return {
|
||||
name: 'uni-devtools',
|
||||
writeBundle() {
|
||||
if (isFirstBuild && process.env.UNI_PLATFORM?.includes('mp')) {
|
||||
isFirstBuild = false
|
||||
_openDevTools(env, { wechatDevtoolsCliPath })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
95
mp-sc-frontend/scripts/postupgrade.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// # 执行 `pnpm upgrade` 后会升级 `uniapp` 相关依赖
|
||||
// # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
|
||||
// # 只需要执行下面的命令即可
|
||||
|
||||
import { exec } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
// 日志控制开关,设置为 true 可以启用所有日志输出
|
||||
const FG_LOG_ENABLE = true
|
||||
|
||||
// 将 exec 转换为返回 Promise 的函数
|
||||
const execPromise = promisify(exec)
|
||||
|
||||
// 定义要执行的命令
|
||||
const dependencies = [
|
||||
// TODO: 如果不需要某个平台的小程序,请手动删除或注释掉
|
||||
'@dcloudio/uni-mp-baidu',
|
||||
'@dcloudio/uni-mp-jd',
|
||||
'@dcloudio/uni-mp-kuaishou',
|
||||
'@dcloudio/uni-mp-qq',
|
||||
'@dcloudio/uni-mp-xhs',
|
||||
'@dcloudio/uni-quickapp-webview',
|
||||
]
|
||||
|
||||
/**
|
||||
* 带开关的日志输出函数
|
||||
* @param {string} message 日志消息
|
||||
* @param {string} type 日志类型 (log, error)
|
||||
*/
|
||||
function log(message, type = 'log') {
|
||||
if (FG_LOG_ENABLE) {
|
||||
if (type === 'error') {
|
||||
console.error(message)
|
||||
}
|
||||
else {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载单个依赖包
|
||||
* @param {string} dep 依赖包名
|
||||
* @returns {Promise<boolean>} 是否成功卸载
|
||||
*/
|
||||
async function uninstallDependency(dep) {
|
||||
try {
|
||||
log(`开始卸载依赖: ${dep}`)
|
||||
const { stdout, stderr } = await execPromise(`pnpm un ${dep}`)
|
||||
if (stdout) {
|
||||
log(`stdout [${dep}]: ${stdout}`)
|
||||
}
|
||||
if (stderr) {
|
||||
log(`stderr [${dep}]: ${stderr}`, 'error')
|
||||
}
|
||||
log(`成功卸载依赖: ${dep}`)
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
// 单个依赖卸载失败不影响其他依赖
|
||||
log(`卸载依赖 ${dep} 失败: ${error.message}`, 'error')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行卸载所有依赖包
|
||||
*/
|
||||
async function uninstallAllDependencies() {
|
||||
log(`开始串行卸载 ${dependencies.length} 个依赖包...`)
|
||||
|
||||
let successCount = 0
|
||||
let failedCount = 0
|
||||
|
||||
// 串行执行所有卸载命令
|
||||
for (const dep of dependencies) {
|
||||
const success = await uninstallDependency(dep)
|
||||
if (success) {
|
||||
successCount++
|
||||
}
|
||||
else {
|
||||
failedCount++
|
||||
}
|
||||
|
||||
// 为了避免命令执行过快导致的问题,添加短暂延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
log(`卸载操作完成: 成功 ${successCount} 个, 失败 ${failedCount} 个`)
|
||||
}
|
||||
|
||||
// 执行串行卸载
|
||||
uninstallAllDependencies().catch((err) => {
|
||||
log(`串行卸载过程中出现未捕获的错误: ${err}`, 'error')
|
||||
})
|
||||
270
mp-sc-frontend/scripts/upload-weixin.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 微信小程序 CLI 上传脚本
|
||||
*
|
||||
* 使用方法:
|
||||
* pnpm upload:mp # 版本号读取 package.json,描述使用最新 Git commit
|
||||
* pnpm upload:mp --version=1.0.1 # 指定版本号(覆盖 package.json)
|
||||
* pnpm upload:mp --desc="修复bug" # 指定版本描述(覆盖 Git commit)
|
||||
* pnpm upload:mp --robot=2 # 指定机器人编号(1-30)
|
||||
* pnpm upload:mp --version=2.0.0 --desc="重大更新" # 组合使用多个参数
|
||||
*
|
||||
* 版本号策略: 命令行参数 > package.json version
|
||||
* 描述策略: 命令行参数 > Git 最新 commit > 默认时间戳
|
||||
*
|
||||
* 注意事项:
|
||||
* 1. 确保已在微信公众平台开通 "小程序代码上传" 权限
|
||||
* 2. 确保私钥文件存在(private.${appid}.key),并且配置了上传IP白名单
|
||||
* 3. 上传前会自动执行 build:mp:prod 构建 并跳过打开开发者工具
|
||||
* 4. 秘钥文件的appid(VITE_WX_APPID)需要与微信公众平台的小程序appid一致
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import ci from 'miniprogram-ci'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(__dirname, '..')
|
||||
|
||||
// 从 package.json 读取版本号
|
||||
function getPackageVersion() {
|
||||
try {
|
||||
const pkgPath = path.resolve(ROOT_DIR, 'package.json')
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
||||
return pkg.version || '1.0.0'
|
||||
}
|
||||
catch {
|
||||
return '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最新的 Git commit 信息
|
||||
function getGitCommitMessage() {
|
||||
try {
|
||||
// 获取最新 commit 的作者和标题
|
||||
const message = execSync('git log -1 --pretty="%an: %s"', {
|
||||
cwd: ROOT_DIR,
|
||||
encoding: 'utf-8',
|
||||
}).trim()
|
||||
return message || null
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 生成默认描述
|
||||
function getDefaultDesc() {
|
||||
// 优先使用 Git commit 信息
|
||||
const gitMessage = getGitCommitMessage()
|
||||
if (gitMessage) {
|
||||
return gitMessage
|
||||
}
|
||||
// 回退到时间戳
|
||||
return `上传于 ${new Date().toLocaleString('zh-CN')}`
|
||||
}
|
||||
|
||||
// 解析命令行参数
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2)
|
||||
const params = {
|
||||
version: null, // 稍后设置,优先级:命令行 > package.json
|
||||
desc: null, // 稍后设置,优先级:命令行 > Git commit > 默认
|
||||
robot: 1, // 机器人编号 1-30
|
||||
}
|
||||
|
||||
args.forEach((arg) => {
|
||||
if (arg.startsWith('--version=')) {
|
||||
params.version = arg.split('=')[1]
|
||||
}
|
||||
else if (arg.startsWith('--desc=')) {
|
||||
params.desc = arg.split('=')[1]
|
||||
}
|
||||
else if (arg.startsWith('--robot=')) {
|
||||
params.robot = Number.parseInt(arg.split('=')[1], 10)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果命令行没有指定版本号,则读取 package.json
|
||||
if (!params.version) {
|
||||
params.version = getPackageVersion()
|
||||
}
|
||||
|
||||
// 如果命令行没有指定描述,则读取 Git commit 或使用默认
|
||||
if (!params.desc) {
|
||||
params.desc = getDefaultDesc()
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// 读取环境变量
|
||||
function loadEnvFile(mode = 'production') {
|
||||
const envPath = path.resolve(ROOT_DIR, 'env', `.env.${mode}`)
|
||||
const defaultEnvPath = path.resolve(ROOT_DIR, 'env', '.env')
|
||||
|
||||
const envContent = {}
|
||||
|
||||
// 先读取默认 .env
|
||||
if (fs.existsSync(defaultEnvPath)) {
|
||||
const content = fs.readFileSync(defaultEnvPath, 'utf-8')
|
||||
content.split('\n').forEach((line) => {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=')
|
||||
if (key) {
|
||||
envContent[key.trim()] = valueParts.join('=').trim().replace(/^['"]|['"]$/g, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 再读取对应模式的 .env 文件(会覆盖默认值)
|
||||
if (fs.existsSync(envPath)) {
|
||||
const content = fs.readFileSync(envPath, 'utf-8')
|
||||
content.split('\n').forEach((line) => {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=')
|
||||
if (key) {
|
||||
envContent[key.trim()] = valueParts.join('=').trim().replace(/^['"]|['"]$/g, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return envContent
|
||||
}
|
||||
|
||||
// 获取私钥路径
|
||||
function getPrivateKeyPath(appid) {
|
||||
// 查找私钥文件
|
||||
const keyPatterns = [
|
||||
`private.${appid}.key`,
|
||||
'private.key',
|
||||
]
|
||||
|
||||
for (const pattern of keyPatterns) {
|
||||
const keyPath = path.resolve(ROOT_DIR, pattern)
|
||||
if (fs.existsSync(keyPath)) {
|
||||
return keyPath
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`未找到私钥文件,请确保项目根目录存在 private.${appid}.key 文件`)
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
console.log('\n🚀 开始微信小程序上传流程...\n')
|
||||
|
||||
const params = parseArgs()
|
||||
const env = loadEnvFile('production')
|
||||
const appid = env.VITE_WX_APPID
|
||||
|
||||
if (!appid) {
|
||||
throw new Error('未找到 VITE_WX_APPID 环境变量,请检查 env/.env 文件')
|
||||
}
|
||||
|
||||
console.log(`📱 AppID: ${appid}`)
|
||||
console.log(`📌 版本号: ${params.version}`)
|
||||
console.log(`📝 版本描述: ${params.desc}`)
|
||||
console.log(`🤖 机器人编号: ${params.robot}`)
|
||||
|
||||
// 获取私钥路径
|
||||
const privateKeyPath = getPrivateKeyPath(appid)
|
||||
console.log(`🔑 私钥路径: ${privateKeyPath}`)
|
||||
|
||||
// 构建小程序(跳过自动打开开发者工具)
|
||||
console.log('\n📦 正在构建小程序...(跳过自动打开开发者工具)\n')
|
||||
try {
|
||||
execSync('pnpm build:mp:prod', {
|
||||
cwd: ROOT_DIR,
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
SKIP_OPEN_DEVTOOLS: 'true', // 上传时跳过打开开发者工具
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 构建失败:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 小程序代码目录
|
||||
const projectPath = path.resolve(ROOT_DIR, 'dist', 'build', 'mp-weixin')
|
||||
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
throw new Error(`构建产物不存在: ${projectPath}`)
|
||||
}
|
||||
|
||||
console.log(`📂 项目路径: ${projectPath}`)
|
||||
console.log('\n⬆️ 正在上传到微信服务器...\n')
|
||||
|
||||
// 创建项目实例
|
||||
const project = new ci.Project({
|
||||
appid,
|
||||
type: 'miniProgram',
|
||||
projectPath,
|
||||
privateKeyPath,
|
||||
ignores: ['node_modules/**/*'],
|
||||
})
|
||||
|
||||
try {
|
||||
// 上传代码
|
||||
const uploadResult = await ci.upload({
|
||||
project,
|
||||
version: params.version,
|
||||
desc: params.desc,
|
||||
robot: params.robot,
|
||||
setting: {
|
||||
es6: true,
|
||||
es7: true,
|
||||
minify: true,
|
||||
autoPrefixWXSS: true,
|
||||
minifyWXML: true,
|
||||
minifyWXSS: true,
|
||||
minifyJS: true,
|
||||
},
|
||||
onProgressUpdate: (task) => {
|
||||
if (task._status === 'done') {
|
||||
console.log(` ✅ ${task._msg}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
console.log('\n✅ 上传成功!')
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log(` 📌 版本号: ${params.version}`)
|
||||
console.log(` 📝 描述: ${params.desc}`)
|
||||
console.log(` 🤖 机器人: ${params.robot}`)
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||
console.log('\n📋 下一步操作:')
|
||||
console.log(' 1. 登录微信公众平台: https://mp.weixin.qq.com')
|
||||
console.log(' 2. 进入 "管理 -> 版本管理"')
|
||||
console.log(' 3. 在 "开发版本" 中找到刚上传的版本')
|
||||
console.log(' 4. 点击 "选为体验版" 按钮\n')
|
||||
|
||||
return uploadResult
|
||||
}
|
||||
catch (error) {
|
||||
console.error('\n❌ 上传失败:', error.message)
|
||||
if (error.message.includes('privateKeyPath')) {
|
||||
console.log('\n💡 提示: 请确保已在微信公众平台配置代码上传密钥')
|
||||
console.log(' 1. 登录微信公众平台')
|
||||
console.log(' 2. 进入 "开发 -> 开发设置"')
|
||||
console.log(' 3. 在 "小程序代码上传" 区域生成并下载密钥')
|
||||
console.log(' 4. 在 "小程序代码上传" 区域配置上传IP白名单')
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ 执行出错:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
37
mp-sc-frontend/scripts/vite-plugin-eruda.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @description 通过 vite 自定义条件动态导入 eruda
|
||||
* @description Eruda 配置参考 https://eruda.liriliri.io/zh/docs/
|
||||
* @param {object} options
|
||||
* @param {boolean} [options.open] - 是否开启 eruda
|
||||
* @param {object} [options.erudaOptions] - eruda 配置
|
||||
* @param {string} [options.erudaUrl] - eruda 地址
|
||||
*/
|
||||
export default function vitePluginEruda(options = {}) {
|
||||
const { open = true, erudaOptions = {}, erudaUrl = 'https://cdn.jsdelivr.net/npm/eruda' } = options
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-eruda',
|
||||
|
||||
transformIndexHtml(html) {
|
||||
const tags = [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
src: erudaUrl,
|
||||
},
|
||||
injectTo: 'head',
|
||||
},
|
||||
{
|
||||
tag: 'script',
|
||||
children: `eruda.init(${JSON.stringify(erudaOptions)});`,
|
||||
injectTo: 'head',
|
||||
},
|
||||
]
|
||||
|
||||
if (!open) {
|
||||
return html
|
||||
}
|
||||
return { html, tags }
|
||||
},
|
||||
}
|
||||
}
|
||||
40
mp-sc-frontend/src/App.ku.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import FgTabbar from '@/tabbar/index.vue'
|
||||
import { isPageTabbar } from './tabbar/store'
|
||||
import { currRoute } from './utils'
|
||||
|
||||
const isCurrentPageTabbar = ref(true)
|
||||
onShow(() => {
|
||||
const { path } = currRoute()
|
||||
// “蜡笔小开心”提到本地是 '/pages/index/index',线上是 '/' 导致线上 tabbar 不见了
|
||||
// 所以这里需要判断一下,如果是 '/' 就当做首页,也要显示 tabbar
|
||||
if (path === '/') {
|
||||
isCurrentPageTabbar.value = true
|
||||
}
|
||||
else {
|
||||
isCurrentPageTabbar.value = isPageTabbar(path)
|
||||
}
|
||||
})
|
||||
|
||||
const helloKuRoot = ref('Hello AppKuVue')
|
||||
|
||||
const exposeRef = ref('this is form app.Ku.vue')
|
||||
|
||||
defineExpose({
|
||||
exposeRef,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<!-- 这个先隐藏了,知道这样用就行 -->
|
||||
<view class="hidden text-center">
|
||||
{{ helloKuRoot }},这里可以配置全局的东西
|
||||
</view>
|
||||
|
||||
<KuRootView />
|
||||
|
||||
<FgTabbar v-if="isCurrentPageTabbar" />
|
||||
</view>
|
||||
</template>
|
||||
51
mp-sc-frontend/src/App.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { getCurrentInstance, onMounted, onUnmounted } from 'vue'
|
||||
import { navigateToInterceptor } from '@/router/interceptor'
|
||||
import { tabbarStore } from '@/tabbar/store'
|
||||
import { permission } from '@/router/permission'
|
||||
|
||||
const { proxy } = (getCurrentInstance() || {}) as any
|
||||
const router = proxy?.$router
|
||||
|
||||
router && permission.install(router)
|
||||
|
||||
onLaunch((options) => {
|
||||
console.log('App.vue onLaunch', options)
|
||||
})
|
||||
onShow((options) => {
|
||||
console.log('App.vue onShow', options)
|
||||
// 处理直接进入页面路由的情况
|
||||
if (options?.path) {
|
||||
navigateToInterceptor.invoke({ url: `/${options.path}`, query: options.query })
|
||||
}
|
||||
else {
|
||||
navigateToInterceptor.invoke({ url: '/' })
|
||||
}
|
||||
})
|
||||
onHide(() => {
|
||||
console.log('App Hide')
|
||||
})
|
||||
|
||||
// #ifdef H5
|
||||
function syncTabbarWhenPageVisible() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
tabbarStore.syncCurIdxByCurrentPageAsync()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', syncTabbarWhenPageVisible)
|
||||
window.addEventListener('pageshow', syncTabbarWhenPageVisible)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', syncTabbarWhenPageVisible)
|
||||
window.removeEventListener('pageshow', syncTabbarWhenPageVisible)
|
||||
})
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
588
mp-sc-frontend/src/api/appointment.ts
Normal file
@@ -0,0 +1,588 @@
|
||||
import type {
|
||||
IAppointment,
|
||||
IAppointmentDetail,
|
||||
IApprovalDetail,
|
||||
IApprovalInstance,
|
||||
IApprovalRecord,
|
||||
IApprovalTemplate,
|
||||
IBanner,
|
||||
IBannerAdmin,
|
||||
INotice,
|
||||
INoticeAdmin,
|
||||
IEmployee,
|
||||
IGoodsTemplate,
|
||||
IInvitation,
|
||||
ILoginRes,
|
||||
IPageParams,
|
||||
IPageResult,
|
||||
IPermission,
|
||||
IRoleApplication,
|
||||
ISystemModule,
|
||||
ISystemRole,
|
||||
ITodayStats,
|
||||
IUserInfo,
|
||||
IUserAdmin,
|
||||
IDepartment,
|
||||
IVisitRecord,
|
||||
IVisitorVO,
|
||||
IEmployeeCertification,
|
||||
IDepartmentItem,
|
||||
IDepartmentTreeNode,
|
||||
} from './types/appointment'
|
||||
import { http } from '@/http/http'
|
||||
import { getEnvBaseUrl } from '@/utils'
|
||||
|
||||
// ==================== Banner ====================
|
||||
|
||||
/** 获取首页轮播图 */
|
||||
export function getBanners() {
|
||||
return http.get<IBanner[]>('/v1/banners')
|
||||
}
|
||||
|
||||
/** 获取系统通知 */
|
||||
export function getNotices() {
|
||||
return http.get<INotice[]>('/v1/notices')
|
||||
}
|
||||
|
||||
// ==================== 认证 ====================
|
||||
|
||||
/** 微信登录 */
|
||||
export function loginByCode(code: string) {
|
||||
return http.post<ILoginRes>('/v1/auth/login', { code })
|
||||
}
|
||||
|
||||
/** 获取用户信息 */
|
||||
export function getUserInfo() {
|
||||
return http.get<IUserInfo>('/v1/auth/userinfo')
|
||||
}
|
||||
|
||||
/** 更新用户信息 */
|
||||
export function updateUserInfo(data: Record<string, any>) {
|
||||
return http.put('/v1/auth/userinfo', data)
|
||||
}
|
||||
|
||||
/** 绑定手机号 */
|
||||
export function bindPhone(data: { code: string, encrypted_data: string, iv: string }) {
|
||||
return http.post('/v1/auth/phone', data)
|
||||
}
|
||||
|
||||
// ==================== 访客预约 ====================
|
||||
|
||||
/** 搜索员工 */
|
||||
export function searchEmployees(keyword: string) {
|
||||
return http.post<IEmployee[]>('/v1/appointment/employees/search', { keyword })
|
||||
}
|
||||
|
||||
/** 验证员工姓名+手机号 */
|
||||
export function verifyEmployee(name: string, phone: string) {
|
||||
return http.post<IEmployee>('/v1/appointment/employees/verify', { name, phone })
|
||||
}
|
||||
|
||||
/** 获取当前用户历史访客列表 */
|
||||
export function getMyVisitors() {
|
||||
return http.get<IVisitorVO[]>('/v1/appointment/visitors/my')
|
||||
}
|
||||
|
||||
/** 保存为常用访客 */
|
||||
export function saveVisitorTemplate(data: {
|
||||
name: string
|
||||
phone: string
|
||||
company?: string
|
||||
gender?: number
|
||||
id_type?: string
|
||||
id_number?: string
|
||||
license_plate?: string
|
||||
}) {
|
||||
return http.post('/v1/appointment/visitor-templates', data)
|
||||
}
|
||||
|
||||
/** 删除常用访客 */
|
||||
export function deleteVisitorTemplate(id: number) {
|
||||
return http.delete(`/v1/appointment/visitor-templates/${id}`)
|
||||
}
|
||||
|
||||
/** 获取物品模板列表 */
|
||||
export function getGoodsTemplates() {
|
||||
return http.get<IGoodsTemplate[]>('/v1/appointment/goods-templates')
|
||||
}
|
||||
|
||||
/** 创建物品模板 */
|
||||
export function createGoodsTemplate(data: { name: string; quantity?: number; model?: string; remark?: string }) {
|
||||
return http.post<IGoodsTemplate>('/v1/appointment/goods-templates', data)
|
||||
}
|
||||
|
||||
/** 更新物品模板 */
|
||||
export function updateGoodsTemplate(id: number, data: Partial<IGoodsTemplate>) {
|
||||
return http.put(`/v1/appointment/goods-templates/${id}`, data)
|
||||
}
|
||||
|
||||
/** 删除物品模板 */
|
||||
export function deleteGoodsTemplate(id: number) {
|
||||
return http.delete(`/v1/appointment/goods-templates/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 车辆模板 ====================
|
||||
|
||||
/** 获取车辆模板列表 */
|
||||
export function getVehicleTemplates() {
|
||||
return http.get<any[]>('/v1/appointment/vehicles')
|
||||
}
|
||||
|
||||
/** 创建车辆模板 */
|
||||
export function createVehicleTemplate(data: { license_plate: string; brand?: string; color?: string }) {
|
||||
return http.post<any>('/v1/appointment/vehicles', data)
|
||||
}
|
||||
|
||||
/** 更新车辆模板 */
|
||||
export function updateVehicleTemplate(id: number, data: { license_plate: string; brand?: string; color?: string }) {
|
||||
return http.put(`/v1/appointment/vehicles/${id}`, data)
|
||||
}
|
||||
|
||||
/** 删除车辆模板 */
|
||||
export function deleteVehicleTemplate(id: number) {
|
||||
return http.delete(`/v1/appointment/vehicles/${id}`)
|
||||
}
|
||||
|
||||
/** 创建预约 */
|
||||
export function createAppointment(data: Record<string, any>) {
|
||||
return http.post<IAppointment>('/v1/appointment/appointments', data)
|
||||
}
|
||||
|
||||
/** 我的预约列表 */
|
||||
export function getMyAppointments(params: IPageParams) {
|
||||
return http.get<IPageResult<IAppointment>>('/v1/appointment/appointments/my', params)
|
||||
}
|
||||
|
||||
/** 全部预约列表(员工专属,按申请时间降序) */
|
||||
export function getAllAppointments(params: IPageParams & { status?: number }) {
|
||||
return http.get<IPageResult<IAppointment>>('/v1/appointment/appointments/all', params)
|
||||
}
|
||||
|
||||
/** 预约详情 */
|
||||
export function getAppointmentDetail(id: number) {
|
||||
return http.get<IAppointmentDetail>('/v1/appointment/appointments/' + id)
|
||||
}
|
||||
|
||||
/** 审核预约 */
|
||||
export function auditAppointment(id: number, data: { status: number, comment?: string }) {
|
||||
return http.put(`/v1/appointment/appointments/${id}/audit`, data)
|
||||
}
|
||||
|
||||
/** 获取预约的审批步骤 */
|
||||
export function getApprovalSteps(id: number) {
|
||||
return http.get<any[]>(`/v1/appointment/appointments/${id}/steps`)
|
||||
}
|
||||
|
||||
/** 取消预约 */
|
||||
export function cancelAppointment(id: number) {
|
||||
return http.put(`/v1/appointment/appointments/${id}/cancel`)
|
||||
}
|
||||
|
||||
// ==================== 邀请 ====================
|
||||
|
||||
/** 生成邀请 */
|
||||
export function createInvitation(data: Record<string, any>) {
|
||||
return http.post<IInvitation>('/v1/appointment/invitations', data)
|
||||
}
|
||||
|
||||
/** 公开邀请详情 */
|
||||
export function getInvitationDetail(code: string) {
|
||||
return http.get<IInvitation>(`/v1/public/invitations/${code}`)
|
||||
}
|
||||
|
||||
// ==================== 保安 ====================
|
||||
|
||||
/** 今日预约 */
|
||||
export function getTodayAppointments() {
|
||||
return http.get<{ stats: ITodayStats, appointments: IAppointment[] }>('/v1/guard/today-appointments')
|
||||
}
|
||||
|
||||
/** 登记进场 */
|
||||
export function checkIn(data: { appointment_id: number, remark?: string }) {
|
||||
return http.post<IVisitRecord>('/v1/guard/check-in', data)
|
||||
}
|
||||
|
||||
/** 登记出场 */
|
||||
export function checkOut(data: { appointment_id: number, remark?: string }) {
|
||||
return http.post<IVisitRecord>('/v1/guard/check-out', data)
|
||||
}
|
||||
|
||||
/** 获取预约的进出场记录 */
|
||||
export function getVisitRecords(appointmentId: number) {
|
||||
return http.get<IVisitRecord[]>('/v1/guard/records', { appointment_id: appointmentId })
|
||||
}
|
||||
|
||||
// ==================== 领导 ====================
|
||||
|
||||
/** 部门预约 */
|
||||
export function getLeaderAppointments(params: IPageParams) {
|
||||
return http.get<IPageResult<IAppointment>>('/v1/leader/appointments', params)
|
||||
}
|
||||
|
||||
// ==================== 管理 ====================
|
||||
|
||||
/** 角色列表 */
|
||||
export function getRoles() {
|
||||
return http.get<ISystemRole[]>('/v1/admin/roles')
|
||||
}
|
||||
|
||||
/** 创建角色 */
|
||||
export function createRole(data: Partial<ISystemRole>) {
|
||||
return http.post<ISystemRole>('/v1/admin/roles', data)
|
||||
}
|
||||
|
||||
/** 更新角色 */
|
||||
export function updateRole(id: number, data: Partial<ISystemRole>) {
|
||||
return http.put(`/v1/admin/roles/${id}`, data)
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
export function deleteRole(id: number) {
|
||||
return http.delete(`/v1/admin/roles/${id}`)
|
||||
}
|
||||
|
||||
/** 模块列表 */
|
||||
export function getModules() {
|
||||
return http.get<ISystemModule[]>('/v1/admin/modules')
|
||||
}
|
||||
|
||||
/** 创建模块 */
|
||||
export function createModule(data: Partial<ISystemModule>) {
|
||||
return http.post<ISystemModule>('/v1/admin/modules', data)
|
||||
}
|
||||
|
||||
/** 更新模块 */
|
||||
export function updateModule(id: number, data: Partial<ISystemModule>) {
|
||||
return http.put(`/v1/admin/modules/${id}`, data)
|
||||
}
|
||||
|
||||
/** 删除模块 */
|
||||
export function deleteModule(id: number) {
|
||||
return http.delete(`/v1/admin/modules/${id}`)
|
||||
}
|
||||
|
||||
/** 权限列表 */
|
||||
export function getPermissions(params?: { roleCode?: string, moduleCode?: string }) {
|
||||
return http.get<IPermission[]>('/v1/admin/permissions', params)
|
||||
}
|
||||
|
||||
/** 添加权限 */
|
||||
export function createPermission(data: { roleCode: string, moduleCode: string, resource: string, action: string }) {
|
||||
return http.post<IPermission>('/v1/admin/permissions', data)
|
||||
}
|
||||
|
||||
/** 删除权限 */
|
||||
export function deletePermission(id: number) {
|
||||
return http.delete(`/v1/admin/permissions/${id}`)
|
||||
}
|
||||
|
||||
/** 申请列表 */
|
||||
export function getApplications() {
|
||||
return http.get<IRoleApplication[]>('/v1/admin/applications')
|
||||
}
|
||||
|
||||
/** 审批申请 */
|
||||
export function auditApplication(id: number, data: { status: number }) {
|
||||
return http.put(`/v1/admin/applications/${id}/audit`, data)
|
||||
}
|
||||
|
||||
// ==================== 角色申请 ====================
|
||||
|
||||
/** 提交角色申请 */
|
||||
export function submitRoleApply(data: { apply_role: number, reason: string }) {
|
||||
return http.post('/v1/role/apply', data)
|
||||
}
|
||||
|
||||
/** 我的申请记录 */
|
||||
export function getMyApplications() {
|
||||
return http.get<IRoleApplication[]>('/v1/role/applications')
|
||||
}
|
||||
|
||||
// ==================== 上传(公开接口,无需权限) ====================
|
||||
|
||||
/** 上传图片到 MinIO */
|
||||
export function uploadImageToMinio(filePath: string) {
|
||||
return new Promise<{ url: string }>((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: '/v1/upload/minio',
|
||||
filePath,
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
reject(new Error(data.msg || '上传失败'))
|
||||
}
|
||||
} catch {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 上传图片到微信服务器 */
|
||||
export function uploadImage(filePath: string) {
|
||||
return new Promise<{ url: string }>((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${getEnvBaseUrl()}/v1/upload/image`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
reject(new Error(data.msg || '上传失败'))
|
||||
}
|
||||
} catch {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 通过图片链接上传到微信服务器 */
|
||||
export function uploadImageByURL(url: string) {
|
||||
return http.post<{ url: string }>('/v1/upload/image-by-url', { url })
|
||||
}
|
||||
|
||||
/** 更新用户头像(接受微信图片 URL) */
|
||||
export function updateAvatar(userId: number, url: string) {
|
||||
return http.put(`/v1/user/${userId}/avatar`, { url })
|
||||
}
|
||||
|
||||
// ==================== 审批流程 ====================
|
||||
|
||||
/** 发起审批 */
|
||||
export function startApproval(data: {
|
||||
template_code: string
|
||||
business_type: string
|
||||
business_id: number
|
||||
variables?: Record<string, any>
|
||||
}) {
|
||||
return http.post<IApprovalInstance>('/v1/approval/start', data)
|
||||
}
|
||||
|
||||
/** 审批通过 */
|
||||
export function approveRecord(data: {
|
||||
record_id: number
|
||||
comment?: string
|
||||
variables?: Record<string, any>
|
||||
}) {
|
||||
return http.post<IApprovalRecord>('/v1/approval/approve', data)
|
||||
}
|
||||
|
||||
/** 审批拒绝 */
|
||||
export function rejectRecord(data: {
|
||||
record_id: number
|
||||
comment?: string
|
||||
}) {
|
||||
return http.post<IApprovalRecord>('/v1/approval/reject', data)
|
||||
}
|
||||
|
||||
/** 获取待审批列表 */
|
||||
export function getPendingApprovals(params: {
|
||||
business_type?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) {
|
||||
return http.get<IPageResult<IApprovalRecord>>('/v1/approval/pending', params)
|
||||
}
|
||||
|
||||
/** 获取我的审批列表(按状态筛选) */
|
||||
export function getMyApprovals(params: {
|
||||
status?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) {
|
||||
return http.get<IPageResult<IApprovalRecord>>('/v1/approval/my', params)
|
||||
}
|
||||
|
||||
/** 获取审批实例详情 */
|
||||
export function getApprovalDetail(instanceId: number) {
|
||||
return http.get<IApprovalDetail>(`/v1/approval/instance/${instanceId}`)
|
||||
}
|
||||
|
||||
/** 获取审批模板列表 */
|
||||
export function getApprovalTemplates() {
|
||||
return http.get<IApprovalTemplate[]>('/v1/approval/templates')
|
||||
}
|
||||
|
||||
// ==================== 员工认证 ====================
|
||||
|
||||
/** 提交员工认证申请 */
|
||||
export function submitCertification(data: {
|
||||
real_name: string
|
||||
phone: string
|
||||
role_type: string
|
||||
snapshot: string[]
|
||||
position?: string
|
||||
department_id?: number | null
|
||||
}) {
|
||||
return http.post<IEmployeeCertification>('/v1/certification/submit', data)
|
||||
}
|
||||
|
||||
/** 获取我的认证申请 */
|
||||
export function getMyCertification() {
|
||||
return http.get<IEmployeeCertification | null>('/v1/certification/my')
|
||||
}
|
||||
|
||||
/** 撤回认证申请 */
|
||||
export function withdrawCertification(id: number) {
|
||||
return http.put(`/v1/certification/${id}/withdraw`)
|
||||
}
|
||||
|
||||
// ==================== 管理员:员工认证审核 ====================
|
||||
|
||||
/** 认证申请列表 */
|
||||
export function adminGetCertifications(params?: IPageParams & { status?: number }) {
|
||||
return http.get<IPageResult<IEmployeeCertification>>('/v1/admin/certifications', params)
|
||||
}
|
||||
|
||||
/** 审核认证申请 */
|
||||
export function adminAuditCertification(id: number, data: {
|
||||
status: number
|
||||
department_id?: number
|
||||
comment?: string
|
||||
}) {
|
||||
return http.put(`/v1/admin/certifications/${id}/audit`, data)
|
||||
}
|
||||
|
||||
// ==================== 管理员:部门管理 ====================
|
||||
|
||||
/** 获取部门树 */
|
||||
export function adminGetDepartmentTree() {
|
||||
return http.get<IDepartmentTreeNode[]>('/v1/admin/department-tree')
|
||||
}
|
||||
|
||||
/** 获取所有部门(扁平列表) */
|
||||
export function adminGetAllDepartments() {
|
||||
return http.get<IDepartmentItem[]>('/v1/admin/departments-all')
|
||||
}
|
||||
|
||||
/** 创建部门 */
|
||||
export function adminCreateDepartment(data: {
|
||||
name: string
|
||||
parent_id?: number
|
||||
leader_name?: string
|
||||
leader_phone?: string
|
||||
sort?: number
|
||||
}) {
|
||||
return http.post('/v1/admin/departments', data)
|
||||
}
|
||||
|
||||
/** 更新部门 */
|
||||
export function adminUpdateDepartment(id: number, data: Record<string, any>) {
|
||||
return http.put(`/v1/admin/departments/${id}`, data)
|
||||
}
|
||||
|
||||
/** 删除部门 */
|
||||
export function adminDeleteDepartment(id: number) {
|
||||
return http.delete(`/v1/admin/departments/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 审批配置 ====================
|
||||
|
||||
interface IApprovalConfig {
|
||||
id: number
|
||||
step: number
|
||||
approver_id: number
|
||||
approver?: { id: number; name: string; phone: string }
|
||||
description: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface IEmployeeSimple {
|
||||
id: number
|
||||
name: string
|
||||
phone: string
|
||||
department_name: string
|
||||
position: string
|
||||
}
|
||||
|
||||
/** 获取审批配置列表 */
|
||||
export function adminGetApprovalConfigs() {
|
||||
return http.get<IApprovalConfig[]>('/v1/admin/approval-config')
|
||||
}
|
||||
|
||||
/** 创建审批配置 */
|
||||
export function adminCreateApprovalConfig(data: {
|
||||
step: number
|
||||
approver_id: number
|
||||
description?: string
|
||||
}) {
|
||||
return http.post('/v1/admin/approval-config', data)
|
||||
}
|
||||
|
||||
/** 更新审批配置 */
|
||||
export function adminUpdateApprovalConfig(id: number, data: {
|
||||
step?: number
|
||||
approver_id?: number
|
||||
description?: string
|
||||
}) {
|
||||
return http.put(`/v1/admin/approval-config/${id}`, data)
|
||||
}
|
||||
|
||||
/** 删除审批配置 */
|
||||
export function adminDeleteApprovalConfig(id: number) {
|
||||
return http.delete(`/v1/admin/approval-config/${id}`)
|
||||
}
|
||||
|
||||
/** 获取所有员工(用于选择审批人) */
|
||||
export function adminGetAllEmployees() {
|
||||
return http.get<{list: IEmployeeSimple[]}>('/v1/admin/employees', { page: 1, page_size: 1000 })
|
||||
}
|
||||
|
||||
/** 获取知会通知配置列表 */
|
||||
export function adminGetApprovalNotifies() {
|
||||
return http.get<IApprovalNotifyInfo[]>('/v1/admin/approval-notify')
|
||||
}
|
||||
|
||||
/** 创建知会通知配置 */
|
||||
export function adminCreateApprovalNotify(data: { employee_id: number }) {
|
||||
return http.post('/v1/admin/approval-notify', data)
|
||||
}
|
||||
|
||||
/** 删除知会通知配置 */
|
||||
export function adminDeleteApprovalNotify(id: number) {
|
||||
return http.delete(`/v1/admin/approval-notify/${id}`)
|
||||
}
|
||||
|
||||
/** 为用户添加角色 */
|
||||
export function adminAddUserRole(userId: number, roleId: number) {
|
||||
return http.post(`/v1/admin/users/${userId}/roles`, { role_id: roleId })
|
||||
}
|
||||
|
||||
/** 为用户移除角色 */
|
||||
export function adminRemoveUserRole(userId: number, roleId: number) {
|
||||
return http.delete(`/v1/admin/users/${userId}/roles/${roleId}`)
|
||||
}
|
||||
|
||||
export interface IApprovalNotifyInfo {
|
||||
id: number
|
||||
employee_id: number
|
||||
employee?: { id: number; name: string; phone: string; department_name?: string }
|
||||
}
|
||||
|
||||
// ==================== 公共数据 ====================
|
||||
|
||||
/** 获取所有启用的来访目的 */
|
||||
export function getVisitTypes() {
|
||||
return http.get<any[]>('/v1/public/visit-types')
|
||||
}
|
||||
|
||||
/** 获取所有启用的到访区域 */
|
||||
export function getVisitorAreas() {
|
||||
return http.get<any[]>('/v1/public/visitor-areas')
|
||||
}
|
||||
17
mp-sc-frontend/src/api/foo-alova.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { API_DOMAINS, http } from '@/http/alova'
|
||||
|
||||
export interface IFoo {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export function foo() {
|
||||
return http.Get<IFoo>('/foo', {
|
||||
params: {
|
||||
name: '菲鸽',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
meta: { domain: API_DOMAINS.SECONDARY }, // 用于切换请求地址
|
||||
})
|
||||
}
|
||||
43
mp-sc-frontend/src/api/foo.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
export interface IFoo {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export function foo() {
|
||||
return http.Get<IFoo>('/foo', {
|
||||
params: {
|
||||
name: '菲鸽',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export interface IFooItem {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/** GET 请求 */
|
||||
export async function getFooAPI(name: string) {
|
||||
return await http.get<IFooItem>('/foo', { name })
|
||||
}
|
||||
/** GET 请求;支持 传递 header 的范例 */
|
||||
export function getFooAPI2(name: string) {
|
||||
return http.get<IFooItem>('/foo', { name }, { 'Content-Type-100': '100' })
|
||||
}
|
||||
|
||||
/** POST 请求 */
|
||||
export function postFooAPI(name: string) {
|
||||
return http.post<IFooItem>('/foo', { name })
|
||||
}
|
||||
/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */
|
||||
export function postFooAPI2(name: string) {
|
||||
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 })
|
||||
}
|
||||
/** POST 请求;支持 传递 header 的范例 */
|
||||
export function postFooAPI3(name: string) {
|
||||
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 }, { 'Content-Type-100': '100' })
|
||||
}
|
||||
85
mp-sc-frontend/src/api/login.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { IAuthLoginRes, ICaptcha, IDoubleTokenRes, IUpdateInfo, IUpdatePassword, IUserInfoRes } from './types/login'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/**
|
||||
* 登录表单
|
||||
*/
|
||||
export interface ILoginForm {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns ICaptcha 验证码
|
||||
*/
|
||||
export function getCode() {
|
||||
return http.get<ICaptcha>('/user/getCode')
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param loginForm 登录表单
|
||||
*/
|
||||
export function login(loginForm: ILoginForm) {
|
||||
return http.post<IAuthLoginRes>('/auth/login', loginForm)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @param refreshToken 刷新token
|
||||
*/
|
||||
export function refreshToken(refreshToken: string) {
|
||||
return http.post<IDoubleTokenRes>('/auth/refreshToken', { refreshToken })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfo() {
|
||||
return http.get<IUserInfoRes>('/user/info')
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export function logout() {
|
||||
return http.get<void>('/auth/logout')
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户信息
|
||||
*/
|
||||
export function updateInfo(data: IUpdateInfo) {
|
||||
return http.post('/user/updateInfo', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户密码
|
||||
*/
|
||||
export function updateUserPassword(data: IUpdatePassword) {
|
||||
return http.post('/user/updatePassword', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信登录凭证
|
||||
* @returns Promise 包含微信登录凭证(code)
|
||||
*/
|
||||
export function getWxCode() {
|
||||
return new Promise<UniApp.LoginRes>((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: res => resolve(res),
|
||||
fail: err => reject(new Error(err)),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
* @param params 微信登录参数,包含code
|
||||
* @returns Promise 包含登录结果
|
||||
*/
|
||||
export function wxLogin(data: { code: string }) {
|
||||
return http.post<IAuthLoginRes>('/auth/wxLogin', data)
|
||||
}
|
||||
428
mp-sc-frontend/src/api/types/appointment.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
// 员工认证信息(用于 UserInfo 中的嵌套字段)
|
||||
export interface ICertificationInfo {
|
||||
id: number
|
||||
real_name: string
|
||||
phone: string
|
||||
snapshot: string[]
|
||||
position?: string
|
||||
department_id?: number
|
||||
department?: IDepartmentItem
|
||||
status: number // 0 待审核 1 通过 2 拒绝
|
||||
approver_id?: number
|
||||
comment: string
|
||||
}
|
||||
|
||||
// 用户信息(扩展字段,支持多角色)
|
||||
export interface IUserInfo {
|
||||
id: number
|
||||
openid: string
|
||||
nickname: string
|
||||
real_name: string
|
||||
phone: string
|
||||
phone_verified: boolean
|
||||
avatar_url: string
|
||||
face_image_url: string
|
||||
system_role_id: number
|
||||
status: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
role?: string
|
||||
role_name?: string
|
||||
role_codes?: string[] // 所有角色编码列表
|
||||
roles?: IRoleBrief[] // 所有角色简要信息
|
||||
is_formal_employee?: boolean
|
||||
department_name?: string
|
||||
position?: string
|
||||
certification?: ICertificationInfo // 员工认证信息
|
||||
}
|
||||
|
||||
// 角色简要信息
|
||||
export interface IRoleBrief {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 员工信息
|
||||
export interface IEmployee {
|
||||
id: number
|
||||
user_id: number
|
||||
name: string
|
||||
phone: string
|
||||
department: string
|
||||
is_leader: boolean
|
||||
employee_type: string // employee / department_leader / hr_leader / boss
|
||||
parent_id: number
|
||||
status: number
|
||||
}
|
||||
|
||||
// 访客信息
|
||||
export interface IVisitor {
|
||||
id: number
|
||||
user_id: number
|
||||
company: string
|
||||
name: string
|
||||
phone: string
|
||||
gender: number
|
||||
id_type: string
|
||||
id_number: string
|
||||
license_plate: string
|
||||
face_verified: boolean
|
||||
phone_verified: boolean
|
||||
}
|
||||
|
||||
// 预约区域
|
||||
export interface IAppointmentArea {
|
||||
id?: number
|
||||
appointment_id?: number
|
||||
area: string
|
||||
}
|
||||
|
||||
// 携带物品
|
||||
export interface IGoods {
|
||||
id?: number
|
||||
appointment_id?: number
|
||||
name: string
|
||||
quantity: number
|
||||
model: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
// 预约信息
|
||||
export interface IAppointment {
|
||||
id: number
|
||||
creator_user_id: number
|
||||
creator?: IUserInfo
|
||||
visit_user_id: number
|
||||
visit_type: string
|
||||
visit_start_time: string
|
||||
visit_end_time: string
|
||||
status: number // 0审核中 1通过 2拒绝 3取消
|
||||
remark: string
|
||||
visit_purpose?: string
|
||||
visitor_company?: string
|
||||
qr_code_url: string
|
||||
visitor_info: string // JSON: 所有访客信息
|
||||
creator_info: string // JSON: 创建人信息 {id, name}
|
||||
employee_info: string // JSON: 被访人信息 {id, name, phone, department}
|
||||
goods_info: string // JSON: 物品列表
|
||||
areas_info: string // JSON: 区域列表
|
||||
vehicle_info: string // JSON: 车辆信息 {license_plates: [...]}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 预约详情响应(与列表响应格式不同,JSON 字段已解析为对象/数组)
|
||||
export interface IAppointmentDetail {
|
||||
id: number
|
||||
creator_user_id: number
|
||||
visit_user_id: number
|
||||
visit_type: string
|
||||
visit_start_time: string
|
||||
visit_end_time: string
|
||||
status: number
|
||||
remark: string
|
||||
comment?: string
|
||||
visit_purpose?: string
|
||||
visitor_company?: string
|
||||
qr_code_url: string
|
||||
visitors: {
|
||||
name: string
|
||||
phone: string
|
||||
company: string
|
||||
gender: number
|
||||
id_type?: string
|
||||
id_number?: string
|
||||
}[]
|
||||
creator_info: { id: number; name: string }
|
||||
employee_info: { id: number; name: string; phone: string; department: string }
|
||||
goods: { name: string; quantity: number; model: string; remark: string }[]
|
||||
areas: string[]
|
||||
vehicles: { brand?: string; plate: string; color?: string }[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 访问记录
|
||||
export interface IVisitRecord {
|
||||
id: number
|
||||
appointment_id: number
|
||||
appointment?: IAppointment
|
||||
guard_id: number
|
||||
check_type: number // 1 进场 2 出场
|
||||
check_time: string
|
||||
is_valid: boolean
|
||||
remark: string
|
||||
}
|
||||
|
||||
// 邀请
|
||||
export interface IInvitation {
|
||||
id: number
|
||||
employee_id: number
|
||||
employee?: IEmployee
|
||||
invite_code: string
|
||||
visitor_name: string
|
||||
visitor_phone: string
|
||||
visit_type: string
|
||||
visit_area: string
|
||||
valid_from: string
|
||||
valid_until: string
|
||||
max_use_count: number
|
||||
used_count: number
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
// 系统角色
|
||||
export interface ISystemRole {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
description: string
|
||||
is_system: boolean
|
||||
}
|
||||
|
||||
// 系统模块
|
||||
export interface ISystemModule {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
description: string
|
||||
status: number
|
||||
}
|
||||
|
||||
// 权限
|
||||
export interface IPermission {
|
||||
id: number
|
||||
role: string
|
||||
module_code: string
|
||||
resource: string
|
||||
action: string
|
||||
}
|
||||
|
||||
// 角色申请
|
||||
export interface IRoleApplication {
|
||||
id: number
|
||||
user_id: number
|
||||
user?: IUserInfo
|
||||
apply_role: number
|
||||
apply_role_info?: ISystemRole
|
||||
reason: string
|
||||
status: number
|
||||
approver_id: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 登录响应
|
||||
export interface ILoginRes {
|
||||
token: string
|
||||
user_info: IUserInfo
|
||||
}
|
||||
|
||||
// 分页参数
|
||||
export interface IPageParams {
|
||||
page: number
|
||||
page_size: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 分页响应
|
||||
export interface IPageResult<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 今日预约统计
|
||||
export interface ITodayStats {
|
||||
total: number
|
||||
checked_in: number
|
||||
checked_out: number
|
||||
}
|
||||
|
||||
// Banner 轮播图
|
||||
export interface IBanner {
|
||||
image_url: string
|
||||
jump_path: string
|
||||
is_jump: boolean
|
||||
}
|
||||
|
||||
// Banner 管理(含 id、sort、is_valid)
|
||||
export interface IBannerAdmin {
|
||||
id: number
|
||||
image_url: string
|
||||
jump_path: string
|
||||
is_jump: boolean
|
||||
sort: number
|
||||
is_valid: boolean
|
||||
}
|
||||
|
||||
// 系统通知
|
||||
export interface INotice {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
// 通知管理(含 sort、is_valid)
|
||||
export interface INoticeAdmin {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
sort: number
|
||||
is_valid: boolean
|
||||
}
|
||||
|
||||
// 用户管理视图
|
||||
export interface IUserAdmin {
|
||||
id: number
|
||||
openid: string
|
||||
nickname: string
|
||||
real_name: string
|
||||
phone: string
|
||||
avatar_url: string
|
||||
status: number
|
||||
system_role_id: number
|
||||
roles: IRoleBrief[]
|
||||
is_super_admin: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 部门视图
|
||||
export interface IDepartment {
|
||||
name: string
|
||||
employees: IEmployeeBrief[]
|
||||
}
|
||||
|
||||
export interface IEmployeeBrief {
|
||||
id: number
|
||||
user_id: number
|
||||
name: string
|
||||
phone: string
|
||||
is_leader: boolean
|
||||
}
|
||||
|
||||
// ==================== 审批流程 ====================
|
||||
|
||||
// 审批模板
|
||||
export interface IApprovalTemplate {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
description: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
// 审批节点
|
||||
export interface IApprovalNode {
|
||||
id: number
|
||||
template_id: number
|
||||
node_name: string
|
||||
node_type: string
|
||||
node_order: number
|
||||
is_required: boolean
|
||||
}
|
||||
|
||||
// 审批实例
|
||||
export interface IApprovalInstance {
|
||||
id: number
|
||||
template_id: number
|
||||
template?: IApprovalTemplate
|
||||
business_type: string
|
||||
business_id: number
|
||||
current_node_id: number
|
||||
status: number // 0审批中 1已通过 2已拒绝 3已撤销
|
||||
starter_id: number
|
||||
finished_at: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 审批记录
|
||||
export interface IApprovalRecord {
|
||||
id: number
|
||||
instance_id: number
|
||||
node_id: number
|
||||
approver_id: number
|
||||
approver?: IUserInfo
|
||||
status: number // 0待审批 1通过 2拒绝
|
||||
comment: string
|
||||
approved_at: string
|
||||
node_order: number
|
||||
node_name: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 审批实例详情
|
||||
export interface IApprovalDetail {
|
||||
instance: IApprovalInstance
|
||||
records: IApprovalRecord[]
|
||||
}
|
||||
|
||||
// ==================== 员工认证 ====================
|
||||
|
||||
// 员工认证申请
|
||||
export interface IEmployeeCertification {
|
||||
id: number
|
||||
user_id: number
|
||||
user?: IUserInfo
|
||||
real_name: string
|
||||
phone: string
|
||||
role_type?: string
|
||||
snapshot: string[]
|
||||
position?: string
|
||||
department_id?: number
|
||||
department?: IDepartmentItem
|
||||
status: number // 0待审核 1通过 2拒绝
|
||||
approver_id?: number
|
||||
approver?: IUserInfo
|
||||
comment: string
|
||||
created_at: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// 物品模板
|
||||
export interface IGoodsTemplate {
|
||||
id?: number
|
||||
user_id?: number
|
||||
name: string
|
||||
quantity: number
|
||||
model: string
|
||||
remark: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
// 访客历史视图
|
||||
export interface IVisitorVO {
|
||||
id: number
|
||||
company: string
|
||||
name: string
|
||||
phone: string
|
||||
gender: number
|
||||
id_type: string
|
||||
id_number: string
|
||||
license_plate: string
|
||||
}
|
||||
|
||||
// 部门(扁平列表)
|
||||
export interface IDepartmentItem {
|
||||
id: number
|
||||
name: string
|
||||
parent_id?: number
|
||||
leader_name: string
|
||||
leader_phone: string
|
||||
sort: number
|
||||
status: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 部门树节点
|
||||
export interface IDepartmentTreeNode {
|
||||
id: number
|
||||
name: string
|
||||
parent_id?: number
|
||||
leader_name: string
|
||||
leader_phone: string
|
||||
sort: number
|
||||
status: number
|
||||
children: IDepartmentTreeNode[]
|
||||
}
|
||||
102
mp-sc-frontend/src/api/types/login.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// 认证模式类型
|
||||
export type AuthMode = 'single' | 'double'
|
||||
|
||||
// 单Token响应类型
|
||||
export interface ISingleTokenRes {
|
||||
token: string
|
||||
expiresIn: number // 有效期(秒)
|
||||
}
|
||||
|
||||
// 双Token响应类型
|
||||
export interface IDoubleTokenRes {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
accessExpiresIn: number // 访问令牌有效期(秒)
|
||||
refreshExpiresIn: number // 刷新令牌有效期(秒)
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录返回的信息,其实就是 token 信息
|
||||
*/
|
||||
export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export type UserRole = string
|
||||
|
||||
export interface IUserInfoRes {
|
||||
userId: number
|
||||
username: string
|
||||
nickname: string
|
||||
avatar?: string
|
||||
/** 同时支持单角色和多角色,你自行选择一种就行 */
|
||||
role?: UserRole
|
||||
roles?: UserRole[]
|
||||
[key: string]: any // 允许其他扩展字段
|
||||
}
|
||||
|
||||
// 认证存储数据结构
|
||||
export interface AuthStorage {
|
||||
mode: AuthMode
|
||||
tokens: ISingleTokenRes | IDoubleTokenRes
|
||||
userInfo?: IUserInfoRes
|
||||
loginTime: number // 登录时间戳
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
export interface ICaptcha {
|
||||
captchaEnabled: boolean
|
||||
uuid: string
|
||||
image: string
|
||||
}
|
||||
/**
|
||||
* 上传成功的信息
|
||||
*/
|
||||
export interface IUploadSuccessInfo {
|
||||
fileId: number
|
||||
originalName: string
|
||||
fileName: string
|
||||
storagePath: string
|
||||
fileHash: string
|
||||
fileType: string
|
||||
fileBusinessType: string
|
||||
fileSize: number
|
||||
}
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export interface IUpdateInfo {
|
||||
id: number
|
||||
name: string
|
||||
sex: string
|
||||
}
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export interface IUpdatePassword {
|
||||
id: number
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为单Token响应
|
||||
* @param tokenRes 登录响应数据
|
||||
* @returns 是否为单Token响应
|
||||
*/
|
||||
export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
|
||||
return 'token' in tokenRes && !('refreshToken' in tokenRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为双Token响应
|
||||
* @param tokenRes 登录响应数据
|
||||
* @returns 是否为双Token响应
|
||||
*/
|
||||
export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
|
||||
return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
|
||||
}
|
||||
79
mp-sc-frontend/src/api/types/workflow.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// 工作流相关类型定义
|
||||
|
||||
export interface IProcessDefinition {
|
||||
id?: number;
|
||||
process_name: string; // 流程名称
|
||||
process_code: string; // 流程编码
|
||||
source?: string; // 流程来源
|
||||
description?: string; // 流程描述
|
||||
is_active?: boolean; // 是否启用
|
||||
revoke_events?: string[]; // 撤销事件
|
||||
nodes_json?: string; // 节点配置 JSON
|
||||
nodes?: INode[]; // 节点列表
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface INode {
|
||||
node_id: string; // 节点 ID
|
||||
node_name: string; // 节点名称
|
||||
node_type: number; // 节点类型:0-开始,1-审批,2-网关,3-结束
|
||||
prev_node_ids?: string[]; // 前置节点 ID 列表
|
||||
user_ids?: string[]; // 用户 ID 列表
|
||||
roles?: string[]; // 角色列表
|
||||
gw_config?: IGatewayConfig; // 网关配置
|
||||
is_cosigned?: number; // 是否会签:0-否,1-是
|
||||
node_start_events?: string[]; // 节点启动事件
|
||||
node_end_events?: string[]; // 节点结束事件
|
||||
task_finish_events?: string[]; // 任务完成事件
|
||||
}
|
||||
|
||||
export interface IGatewayConfig {
|
||||
conditions?: ICondition[]; // 条件分支
|
||||
inevitable_nodes?: string[]; // 必经节点
|
||||
wait_for_all_prev_node?: number; // 等待所有前置节点:0-否,1-是
|
||||
}
|
||||
|
||||
export interface ICondition {
|
||||
expression: string; // 条件表达式(如"$days>=3")
|
||||
node_id: string; // 满足条件时跳转的节点 ID
|
||||
}
|
||||
|
||||
export interface IProcessInstance {
|
||||
id?: number;
|
||||
process_def_id: number; // 流程定义 ID
|
||||
process_def?: IProcessDefinition;
|
||||
business_type: string; // 业务类型
|
||||
business_id: number; // 业务 ID
|
||||
status?: number; // 实例状态:0-运行中,1-已完成,2-已终止,3-已撤销
|
||||
starter_id?: number;
|
||||
starter?: IUser;
|
||||
current_node_id?: string; // 当前节点 ID
|
||||
variables?: string; // 流程变量 JSON
|
||||
finished_at?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ITask {
|
||||
id?: number;
|
||||
instance_id: number;
|
||||
instance?: IProcessInstance;
|
||||
node_id: string;
|
||||
node_name: string;
|
||||
status?: number; // 任务状态:0-待处理,1-已完成,2-已拒绝,3-已取消
|
||||
assignee_id?: number;
|
||||
assignee?: IUser;
|
||||
comment?: string;
|
||||
handled_at?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
id: number;
|
||||
openid: string;
|
||||
nickname?: string;
|
||||
real_name?: string;
|
||||
phone?: string;
|
||||
}
|
||||
76
mp-sc-frontend/src/api/workflow.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { http } from '@/http/http'
|
||||
import type { IProcessDefinition, IProcessInstance, ITask } from './types/workflow'
|
||||
|
||||
/**
|
||||
* 流程定义管理
|
||||
*/
|
||||
export const workflowAPI = {
|
||||
// 获取流程定义列表
|
||||
getProcessDefinitions() {
|
||||
return http.get<IProcessDefinition[]>('/v1/workflow/definitions')
|
||||
},
|
||||
|
||||
// 获取流程定义详情
|
||||
getProcessDefinition(id: number) {
|
||||
return http.get<IProcessDefinition>(`/v1/workflow/definitions/${id}`)
|
||||
},
|
||||
|
||||
// 创建流程定义
|
||||
createProcessDefinition(data: Partial<IProcessDefinition>) {
|
||||
return http.post<IProcessDefinition>('/v1/workflow/definitions', data)
|
||||
},
|
||||
|
||||
// 更新流程定义
|
||||
updateProcessDefinition(id: number, data: Partial<IProcessDefinition>) {
|
||||
return http.put<IProcessDefinition>(`/v1/workflow/definitions/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除流程定义
|
||||
deleteProcessDefinition(id: number) {
|
||||
return http.delete(`/v1/workflow/definitions/${id}`)
|
||||
},
|
||||
|
||||
// 启动流程实例
|
||||
startInstance(data: {
|
||||
process_code: string
|
||||
business_type: string
|
||||
business_id: number
|
||||
variables?: Record<string, any>
|
||||
}) {
|
||||
return http.post<IProcessInstance>('/v1/workflow/instances', data)
|
||||
},
|
||||
|
||||
// 获取流程实例列表
|
||||
getProcessInstances(params?: {
|
||||
business_type?: string
|
||||
business_id?: number
|
||||
status?: string
|
||||
}) {
|
||||
return http.get<IProcessInstance[]>('/v1/workflow/instances', params)
|
||||
},
|
||||
|
||||
// 获取流程实例详情
|
||||
getInstanceDetail(id: number) {
|
||||
return http.get<{
|
||||
instance: IProcessInstance
|
||||
tasks: ITask[]
|
||||
}>(`/v1/workflow/instances/${id}`)
|
||||
},
|
||||
|
||||
// 获取待办任务
|
||||
getPendingTasks() {
|
||||
return http.get<ITask[]>('/v1/workflow/tasks/pending')
|
||||
},
|
||||
|
||||
// 审批通过任务
|
||||
approveTask(id: number, comment?: string) {
|
||||
return http.post(`/v1/workflow/tasks/${id}/approve`, { comment })
|
||||
},
|
||||
|
||||
// 审批拒绝任务
|
||||
rejectTask(id: number, comment?: string) {
|
||||
return http.post(`/v1/workflow/tasks/${id}/reject`, { comment })
|
||||
}
|
||||
}
|
||||
|
||||
export default workflowAPI
|
||||
0
mp-sc-frontend/src/components/.gitkeep
Normal file
41
mp-sc-frontend/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** 网站标题,应用名称 */
|
||||
readonly VITE_APP_TITLE: string
|
||||
/** 服务端口号 */
|
||||
readonly VITE_SERVER_PORT: string
|
||||
/** 后台接口地址 */
|
||||
readonly VITE_SERVER_BASEURL: string
|
||||
/** 微信小程序开发版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
|
||||
readonly VITE_SERVER_BASEURL__WEIXIN_DEVELOP?: string
|
||||
/** 微信小程序体验版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
|
||||
readonly VITE_SERVER_BASEURL__WEIXIN_TRIAL?: string
|
||||
/** 微信小程序正式版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
|
||||
readonly VITE_SERVER_BASEURL__WEIXIN_RELEASE?: string
|
||||
/** H5是否需要代理 */
|
||||
readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
|
||||
/** H5是否需要代理,需要的话有个前缀 */
|
||||
readonly VITE_APP_PROXY_PREFIX: string
|
||||
/** 后端是否有统一前缀 /api */
|
||||
readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false'
|
||||
/** 认证模式,'single' | 'double' ==> 单token | 双token */
|
||||
readonly VITE_AUTH_MODE: 'single' | 'double'
|
||||
/** 是否清除console */
|
||||
readonly VITE_DELETE_CONSOLE: string
|
||||
// 更多环境变量...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare const __VITE_APP_PROXY__: 'true' | 'false'
|
||||
242
mp-sc-frontend/src/hooks/useNavBar.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
// hooks/useNavBar.ts
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
// 定义返回的类型
|
||||
export interface NavBarInfo {
|
||||
// 状态栏高度(px)
|
||||
statusBarHeight: Ref<number>
|
||||
// 标题栏内容高度(px)- 与胶囊对齐的区域
|
||||
titleBarHeight: Ref<number>
|
||||
// 整个导航栏总高度(px)- 状态栏 + 标题栏
|
||||
totalNavHeight: ComputedRef<number>
|
||||
// 胶囊按钮信息
|
||||
menuButtonInfo: Ref<MenuButtonInfo | null>
|
||||
// 是否是小程序环境
|
||||
isMiniProgram: Ref<boolean>
|
||||
// 重新获取系统信息
|
||||
refresh: () => void
|
||||
}
|
||||
|
||||
// 胶囊按钮信息类型
|
||||
export interface MenuButtonInfo {
|
||||
width: number
|
||||
height: number
|
||||
top: number
|
||||
right: number
|
||||
bottom: number
|
||||
left: number
|
||||
}
|
||||
|
||||
// 系统信息类型
|
||||
interface SystemInfo {
|
||||
statusBarHeight: number
|
||||
platform: string
|
||||
system: string
|
||||
model: string
|
||||
}
|
||||
|
||||
// 全局缓存
|
||||
let cachedNavBarInfo: {
|
||||
statusBarHeight: number
|
||||
titleBarHeight: number
|
||||
menuButtonInfo: MenuButtonInfo | null
|
||||
} | null = null
|
||||
|
||||
/**
|
||||
* 自定义导航栏 Hook
|
||||
* 用于处理 uniapp 自定义导航栏的高度适配和胶囊信息获取
|
||||
*/
|
||||
export function useNavBar(): NavBarInfo {
|
||||
// 状态栏高度
|
||||
const statusBarHeight = ref<number>(0)
|
||||
|
||||
// 标题栏内容高度(与胶囊对齐的区域)
|
||||
const titleBarHeight = ref<number>(44)
|
||||
|
||||
// 胶囊按钮信息
|
||||
const menuButtonInfo = ref<MenuButtonInfo | null>(null)
|
||||
|
||||
// 是否是小程序环境
|
||||
const isMiniProgram = ref<boolean>(false)
|
||||
|
||||
// 总高度(计算属性)
|
||||
const totalNavHeight = computed<number>(() => {
|
||||
return statusBarHeight.value + titleBarHeight.value
|
||||
})
|
||||
|
||||
/**
|
||||
* 判断是否是小程序环境
|
||||
*/
|
||||
const checkMiniProgramEnv = (): boolean => {
|
||||
// #ifdef MP-WEIXIN
|
||||
return true
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
return false
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取胶囊按钮信息(仅小程序有效)
|
||||
*/
|
||||
const getMenuButtonInfo = (): MenuButtonInfo | null => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const info = uni.getMenuButtonBoundingClientRect()
|
||||
if (info) {
|
||||
return {
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
top: info.top,
|
||||
right: info.right,
|
||||
bottom: info.bottom,
|
||||
left: info.left,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('获取胶囊信息失败:', error)
|
||||
}
|
||||
// #endif
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算标题栏高度(与胶囊居中对齐)
|
||||
*/
|
||||
const calculateTitleBarHeight = (statusHeight: number, menuBtn: MenuButtonInfo | null): number => {
|
||||
// 如果不是小程序环境或没有胶囊信息,返回默认值
|
||||
if (!isMiniProgram.value || !menuBtn) {
|
||||
return 44
|
||||
}
|
||||
|
||||
// 核心公式:让自定义标题栏与胶囊垂直居中对齐
|
||||
// 标题栏高度 = 胶囊高度 + (胶囊顶部 - 状态栏高度) * 2
|
||||
const calculatedHeight = menuBtn.height + (menuBtn.top - statusHeight) * 2
|
||||
|
||||
// 确保高度为正数且不低于最小高度
|
||||
return Math.max(calculatedHeight, 44)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
const getSystemInfo = (): SystemInfo => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
return {
|
||||
statusBarHeight: systemInfo.statusBarHeight || 20,
|
||||
platform: systemInfo.platform,
|
||||
system: systemInfo.system,
|
||||
model: systemInfo.model,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化导航栏信息
|
||||
*/
|
||||
const initNavBarInfo = (): void => {
|
||||
// 检查环境
|
||||
isMiniProgram.value = checkMiniProgramEnv()
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = getSystemInfo()
|
||||
|
||||
// 获取胶囊信息(仅小程序)
|
||||
const menuBtn = getMenuButtonInfo()
|
||||
|
||||
// 计算高度
|
||||
const calculatedTitleBarHeight = calculateTitleBarHeight(systemInfo.statusBarHeight, menuBtn)
|
||||
|
||||
// 更新响应式数据
|
||||
statusBarHeight.value = systemInfo.statusBarHeight
|
||||
titleBarHeight.value = calculatedTitleBarHeight
|
||||
menuButtonInfo.value = menuBtn
|
||||
|
||||
// 缓存结果
|
||||
cachedNavBarInfo = {
|
||||
statusBarHeight: systemInfo.statusBarHeight,
|
||||
titleBarHeight: calculatedTitleBarHeight,
|
||||
menuButtonInfo: menuBtn,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新导航栏信息(用于屏幕旋转等场景)
|
||||
*/
|
||||
const refresh = (): void => {
|
||||
// 清除缓存
|
||||
cachedNavBarInfo = null
|
||||
// 重新初始化
|
||||
initNavBarInfo()
|
||||
}
|
||||
|
||||
// 在组件挂载时初始化
|
||||
onMounted(() => {
|
||||
// 如果有缓存,直接使用缓存
|
||||
if (cachedNavBarInfo) {
|
||||
statusBarHeight.value = cachedNavBarInfo.statusBarHeight
|
||||
titleBarHeight.value = cachedNavBarInfo.titleBarHeight
|
||||
menuButtonInfo.value = cachedNavBarInfo.menuButtonInfo
|
||||
isMiniProgram.value = checkMiniProgramEnv()
|
||||
}
|
||||
else {
|
||||
initNavBarInfo()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
statusBarHeight,
|
||||
titleBarHeight,
|
||||
totalNavHeight,
|
||||
menuButtonInfo,
|
||||
isMiniProgram,
|
||||
refresh,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态栏高度的快捷方法(仅高度,不包含胶囊逻辑)
|
||||
*/
|
||||
export function useStatusBarHeight(): Ref<number> {
|
||||
const statusBarHeight = ref<number>(0)
|
||||
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 20
|
||||
})
|
||||
|
||||
return statusBarHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取胶囊信息的快捷方法(仅小程序有效)
|
||||
*/
|
||||
export function useMenuButtonInfo(): Ref<MenuButtonInfo | null> {
|
||||
const menuButtonInfo = ref<MenuButtonInfo | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const info = uni.getMenuButtonBoundingClientRect()
|
||||
if (info) {
|
||||
menuButtonInfo.value = {
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
top: info.top,
|
||||
right: info.right,
|
||||
bottom: info.bottom,
|
||||
left: info.left,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('获取胶囊信息失败:', error)
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
return menuButtonInfo
|
||||
}
|
||||
74
mp-sc-frontend/src/hooks/useRequest.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import useRequest from './useRequest'
|
||||
|
||||
/**
|
||||
* 在 Vue 应用上下文中运行 composable。
|
||||
* composable 的 ref/computed/onMounted 只能在 setup() 内使用,
|
||||
* withSetup 通过挂载一个临时组件来提供这个上下文。
|
||||
*/
|
||||
function withSetup<T>(composableFn: () => T): T {
|
||||
let result!: T
|
||||
const Comp = defineComponent({
|
||||
setup() {
|
||||
result = composableFn()
|
||||
return () => h('div')
|
||||
},
|
||||
})
|
||||
const wrapper = mount(Comp)
|
||||
wrapper.unmount()
|
||||
return result
|
||||
}
|
||||
|
||||
describe('useRequest', () => {
|
||||
it('初始状态:loading=false, error=false, data=undefined', () => {
|
||||
const asyncFn = vi.fn().mockResolvedValue('data')
|
||||
const { loading, error, data } = withSetup(() => useRequest(asyncFn))
|
||||
|
||||
expect(loading.value).toBe(false)
|
||||
expect(error.value).toBe(false)
|
||||
expect(data.value).toBeUndefined()
|
||||
})
|
||||
|
||||
it('initialData:初始 data 使用传入的默认值', () => {
|
||||
const asyncFn = vi.fn().mockResolvedValue('new')
|
||||
const { data } = withSetup(() => useRequest(asyncFn, { initialData: 'init' }))
|
||||
|
||||
expect(data.value).toBe('init')
|
||||
})
|
||||
|
||||
it('run 成功:loading 先变 true 后变 false,data 更新为返回值', async () => {
|
||||
const asyncFn = vi.fn().mockResolvedValue('result')
|
||||
const { loading, data, run } = withSetup(() => useRequest(asyncFn))
|
||||
|
||||
const runPromise = run()
|
||||
expect(loading.value).toBe(true)
|
||||
|
||||
await runPromise
|
||||
|
||||
expect(loading.value).toBe(false)
|
||||
expect(data.value).toBe('result')
|
||||
})
|
||||
|
||||
it('run 失败:抛出错误,error 被设置,loading 重置为 false', async () => {
|
||||
const err = new Error('network error')
|
||||
const asyncFn = vi.fn().mockRejectedValue(err)
|
||||
const { loading, error, run } = withSetup(() => useRequest(asyncFn))
|
||||
|
||||
await expect(run()).rejects.toThrow('network error')
|
||||
|
||||
expect(loading.value).toBe(false)
|
||||
expect(error.value).toBe(err)
|
||||
})
|
||||
|
||||
it('immediate=true:组件挂载时立即调用异步函数并更新 data', async () => {
|
||||
const asyncFn = vi.fn().mockResolvedValue('eager')
|
||||
const { data } = withSetup(() => useRequest(asyncFn, { immediate: true }))
|
||||
|
||||
expect(asyncFn).toHaveBeenCalledTimes(1)
|
||||
// 等待 Promise 完成
|
||||
await asyncFn.mock.results[0].value
|
||||
expect(data.value).toBe('eager')
|
||||
})
|
||||
})
|
||||
54
mp-sc-frontend/src/hooks/useRequest.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface IUseRequestOptions<T> {
|
||||
/** 是否立即执行 */
|
||||
immediate?: boolean
|
||||
/** 初始化数据 */
|
||||
initialData?: T
|
||||
}
|
||||
|
||||
interface IUseRequestReturn<T, P = undefined> {
|
||||
loading: Ref<boolean>
|
||||
error: Ref<boolean | Error>
|
||||
data: Ref<T | undefined>
|
||||
run: (args?: P) => Promise<T | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
|
||||
* @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
|
||||
* @param options 包含请求选项的对象 {immediate, initialData}。
|
||||
* @param options.immediate 是否立即执行请求,默认为false。
|
||||
* @param options.initialData 初始化数据,默认为undefined。
|
||||
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
|
||||
*/
|
||||
export default function useRequest<T, P = undefined>(
|
||||
func: (args?: P) => Promise<T>,
|
||||
options: IUseRequestOptions<T> = { immediate: false },
|
||||
): IUseRequestReturn<T, P> {
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
|
||||
const run = async (args?: P) => {
|
||||
loading.value = true
|
||||
return func(args)
|
||||
.then((res) => {
|
||||
data.value = res
|
||||
error.value = false
|
||||
return data.value
|
||||
})
|
||||
.catch((err) => {
|
||||
error.value = err
|
||||
throw err
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
if (options.immediate) {
|
||||
(run as (args: P) => Promise<T | undefined>)({} as P)
|
||||
}
|
||||
return { loading, error, data, run }
|
||||
}
|
||||
116
mp-sc-frontend/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` 状态,在列表底部显示不同的提示信息,提升用户体验。
|
||||
|
||||
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。
|
||||
74
mp-sc-frontend/src/hooks/useScroll.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
interface UseScrollOptions<T> {
|
||||
fetchData: (page: number, pageSize: number) => Promise<T[]>
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
interface UseScrollReturn<T> {
|
||||
list: Ref<T[]>
|
||||
loading: Ref<boolean>
|
||||
finished: Ref<boolean>
|
||||
error: Ref<any>
|
||||
refresh: () => Promise<void>
|
||||
loadMore: () => Promise<void>
|
||||
}
|
||||
|
||||
export function useScroll<T>({
|
||||
fetchData,
|
||||
pageSize = 10,
|
||||
}: UseScrollOptions<T>): UseScrollReturn<T> {
|
||||
const list = ref<T[]>([]) as Ref<T[]>
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref<any>(null)
|
||||
const page = ref(1)
|
||||
|
||||
const loadData = async () => {
|
||||
if (loading.value || finished.value)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const data = await fetchData(page.value, pageSize)
|
||||
if (data.length < pageSize) {
|
||||
finished.value = true
|
||||
}
|
||||
list.value.push(...data)
|
||||
page.value++
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
page.value = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
return {
|
||||
list,
|
||||
loading,
|
||||
finished,
|
||||
error,
|
||||
refresh,
|
||||
loadMore,
|
||||
}
|
||||
}
|
||||
171
mp-sc-frontend/src/hooks/useUpload.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { ref } from 'vue'
|
||||
import { getEnvBaseUrl } from '@/utils/index'
|
||||
|
||||
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/upload`
|
||||
|
||||
type TfileType = 'image' | 'file'
|
||||
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
|
||||
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
|
||||
|
||||
interface TOptions<T extends TfileType> {
|
||||
formData?: Record<string, any>
|
||||
maxSize?: number
|
||||
accept?: T extends 'image' ? TImage[] : TFile[]
|
||||
fileType?: T
|
||||
success?: (params: any) => void
|
||||
error?: (err: any) => void
|
||||
}
|
||||
|
||||
export default function useUpload<T extends TfileType>(options: TOptions<T> = {} as TOptions<T>) {
|
||||
const {
|
||||
formData = {},
|
||||
maxSize = 5 * 1024 * 1024,
|
||||
accept = ['*'],
|
||||
fileType = 'image',
|
||||
success,
|
||||
error: onError,
|
||||
} = options
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
const data = ref<any>(null)
|
||||
|
||||
const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
|
||||
if (size > maxSize) {
|
||||
uni.showToast({
|
||||
title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
|
||||
// const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
|
||||
|
||||
// if (!isTypeValid) {
|
||||
// uni.showToast({
|
||||
// title: `仅支持 ${accept.join(', ')} 格式的文件`,
|
||||
// icon: 'none',
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
|
||||
loading.value = true
|
||||
uploadFile({
|
||||
tempFilePath,
|
||||
formData,
|
||||
onSuccess: (res) => {
|
||||
// 修改这里的解析逻辑,适应不同平台的返回格式
|
||||
let parsedData = res
|
||||
try {
|
||||
// 尝试解析为JSON
|
||||
const jsonData = JSON.parse(res)
|
||||
// 检查是否包含data字段
|
||||
parsedData = jsonData.data || jsonData
|
||||
}
|
||||
catch (e) {
|
||||
// 如果解析失败,使用原始数据
|
||||
console.log('Response is not JSON, using raw data:', res)
|
||||
}
|
||||
data.value = parsedData
|
||||
// console.log('上传成功', res)
|
||||
success?.(parsedData)
|
||||
},
|
||||
onError: (err) => {
|
||||
error.value = err
|
||||
onError?.(err)
|
||||
},
|
||||
onComplete: () => {
|
||||
loading.value = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const run = () => {
|
||||
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
|
||||
// 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
|
||||
const chooseFileOptions = {
|
||||
count: 1,
|
||||
success: (res: any) => {
|
||||
console.log('File selected successfully:', res)
|
||||
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
|
||||
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
|
||||
// h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
|
||||
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
|
||||
// App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
|
||||
let tempFilePath = ''
|
||||
let size = 0
|
||||
// #ifdef MP-WEIXIN
|
||||
tempFilePath = res.tempFiles[0].tempFilePath
|
||||
size = res.tempFiles[0].size
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
tempFilePath = res.tempFilePaths[0]
|
||||
size = res.tempFiles[0].size
|
||||
// #endif
|
||||
handleFileChoose({ tempFilePath, size })
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('File selection failed:', err)
|
||||
error.value = err
|
||||
onError?.(err)
|
||||
},
|
||||
}
|
||||
|
||||
if (fileType === 'image') {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
...chooseFileOptions,
|
||||
mediaType: ['image'],
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage(chooseFileOptions)
|
||||
// #endif
|
||||
}
|
||||
else {
|
||||
uni.chooseFile({
|
||||
...chooseFileOptions,
|
||||
type: 'all',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { loading, error, data, run }
|
||||
}
|
||||
|
||||
async function uploadFile({
|
||||
tempFilePath,
|
||||
formData,
|
||||
onSuccess,
|
||||
onError,
|
||||
onComplete,
|
||||
}: {
|
||||
tempFilePath: string
|
||||
formData: Record<string, any>
|
||||
onSuccess: (data: any) => void
|
||||
onError: (err: any) => void
|
||||
onComplete: () => void
|
||||
}) {
|
||||
uni.uploadFile({
|
||||
url: VITE_UPLOAD_BASEURL,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
formData,
|
||||
success: (uploadFileRes) => {
|
||||
try {
|
||||
const data = uploadFileRes.data
|
||||
onSuccess(data)
|
||||
}
|
||||
catch (err) {
|
||||
onError(err)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('Upload failed:', err)
|
||||
onError(err)
|
||||
},
|
||||
complete: onComplete,
|
||||
})
|
||||
}
|
||||
13
mp-sc-frontend/src/http/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 请求库
|
||||
|
||||
目前unibest支持3种请求库:
|
||||
- 菲鸽简单封装的 `简单版本http`,路径(src/http/http.ts),对应的示例在 src/api/foo.ts
|
||||
- `alova 的 http`,路径(src/http/alova.ts),对应的示例在 src/api/foo-alova.ts
|
||||
- `vue-query`, 路径(src/http/vue-query.ts), 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹
|
||||
|
||||
## 如何选择
|
||||
如果您以前用过 alova 或者 vue-query,可以优先使用您熟悉的。
|
||||
如果您的项目简单,简单版本的http 就够了,也不会增加包体积。(发版的时候可以去掉alova和vue-query,如果没有超过包体积,留着也无所谓 ^_^)
|
||||
|
||||
## roadmap
|
||||
菲鸽最近在优化脚手架,后续可以选择是否使用第三方的请求库,以及选择什么请求库。还在开发中,大概月底出来(8月31号)。
|
||||
119
mp-sc-frontend/src/http/alova.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
|
||||
import type { IResponse } from './types'
|
||||
import AdapterUniapp from '@alova/adapter-uniapp'
|
||||
import { createAlova } from 'alova'
|
||||
import { createServerTokenAuthentication } from 'alova/client'
|
||||
import VueHook from 'alova/vue'
|
||||
import { toLoginPage } from '@/utils/toLoginPage'
|
||||
import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
|
||||
|
||||
// 配置动态Tag
|
||||
export const API_DOMAINS = {
|
||||
DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
|
||||
SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY,
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求实例
|
||||
*/
|
||||
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
|
||||
typeof VueHook,
|
||||
typeof uniappRequestAdapter
|
||||
>({
|
||||
// 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
|
||||
refreshTokenOnError: {
|
||||
isExpired: (error) => {
|
||||
return error.response?.status === ResultEnum.Unauthorized
|
||||
},
|
||||
handler: async () => {
|
||||
try {
|
||||
// await authLogin();
|
||||
}
|
||||
catch (error) {
|
||||
// 切换到登录页
|
||||
toLoginPage({ mode: 'reLaunch' })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* alova 请求实例
|
||||
*/
|
||||
const alovaInstance = createAlova({
|
||||
baseURL: API_DOMAINS.DEFAULT,
|
||||
...AdapterUniapp(),
|
||||
timeout: 5000,
|
||||
statesHook: VueHook,
|
||||
|
||||
beforeRequest: onAuthRequired((method) => {
|
||||
// 设置默认 Content-Type
|
||||
method.config.headers = {
|
||||
ContentType: ContentTypeEnum.JSON,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
...method.config.headers,
|
||||
}
|
||||
|
||||
const { config } = method
|
||||
const ignoreAuth = !config.meta?.ignoreAuth
|
||||
console.log('ignoreAuth===>', ignoreAuth)
|
||||
// 处理认证信息 自行处理认证问题
|
||||
if (ignoreAuth) {
|
||||
const token = 'getToken()'
|
||||
if (!token) {
|
||||
throw new Error('[请求错误]:未登录')
|
||||
}
|
||||
// method.config.headers.token = token;
|
||||
}
|
||||
|
||||
// 处理动态域名
|
||||
if (config.meta?.domain) {
|
||||
method.baseURL = config.meta.domain
|
||||
console.log('当前域名', method.baseURL)
|
||||
}
|
||||
}),
|
||||
|
||||
responded: onResponseRefreshToken((response, method) => {
|
||||
const { config } = method
|
||||
const { requestType } = config
|
||||
const {
|
||||
statusCode,
|
||||
data: rawData,
|
||||
errMsg,
|
||||
} = response as UniNamespace.RequestSuccessCallbackResult
|
||||
|
||||
// 处理特殊请求类型(上传/下载)
|
||||
if (requestType === 'upload' || requestType === 'download') {
|
||||
return response
|
||||
}
|
||||
|
||||
// 处理 HTTP 状态码错误
|
||||
if (statusCode !== 200) {
|
||||
const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
|
||||
console.error('errorMessage===>', errorMessage)
|
||||
uni.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
})
|
||||
throw new Error(`${errorMessage}:${errMsg}`)
|
||||
}
|
||||
|
||||
// 处理业务逻辑错误
|
||||
const { code, msg, data } = rawData as IResponse
|
||||
// 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
|
||||
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
|
||||
if (config.meta?.toast !== false) {
|
||||
uni.showToast({
|
||||
title: msg,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
throw new Error(`请求错误[${code}]:${msg}`)
|
||||
}
|
||||
// 处理成功响应,返回业务数据
|
||||
return data
|
||||
}),
|
||||
})
|
||||
|
||||
export const http = alovaInstance
|
||||
200
mp-sc-frontend/src/http/http.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import type { IDoubleTokenRes } from '@/api/types/login'
|
||||
import type { CustomRequestOptions, IResponse } from '@/http/types'
|
||||
import { nextTick } from 'vue'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { isDoubleTokenMode } from '@/utils'
|
||||
import { toLoginPage } from '@/utils/toLoginPage'
|
||||
import { ResultEnum } from './tools/enum'
|
||||
|
||||
// 刷新 token 状态管理
|
||||
let refreshing = false // 防止重复刷新 token 标识
|
||||
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
|
||||
|
||||
export function http<T>(options: CustomRequestOptions) {
|
||||
// 1. 返回 Promise 对象
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
uni.request({
|
||||
...options,
|
||||
dataType: 'json',
|
||||
// #ifndef MP-WEIXIN
|
||||
responseType: 'json',
|
||||
// #endif
|
||||
// 响应成功
|
||||
success: async (res) => {
|
||||
const responseData = res.data as IResponse<T>
|
||||
const { code } = responseData
|
||||
|
||||
// 检查是否是401错误(包括HTTP状态码401或业务码401)
|
||||
const isTokenExpired = res.statusCode === 401 || code === 401
|
||||
|
||||
if (isTokenExpired) {
|
||||
const tokenStore = useTokenStore()
|
||||
if (!isDoubleTokenMode) {
|
||||
// 未启用双token策略,清理用户信息,跳转到登录页
|
||||
tokenStore.logout()
|
||||
toLoginPage()
|
||||
return reject(res)
|
||||
}
|
||||
|
||||
/* -------- 无感刷新 token ----------- */
|
||||
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
|
||||
// token 失效的,且有刷新 token 的,才放到请求队列里
|
||||
if (refreshToken) {
|
||||
taskQueue.push(() => {
|
||||
resolve(http<T>(options))
|
||||
})
|
||||
}
|
||||
|
||||
// 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
|
||||
if (refreshToken && !refreshing) {
|
||||
refreshing = true
|
||||
try {
|
||||
// 发起刷新 token 请求(使用 store 的 refreshToken 方法)
|
||||
await tokenStore.refreshToken()
|
||||
// 刷新 token 成功
|
||||
refreshing = false
|
||||
nextTick(() => {
|
||||
// 关闭其他弹窗
|
||||
uni.hideToast()
|
||||
uni.showToast({
|
||||
title: 'token 刷新成功',
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
// 将任务队列的所有任务重新请求
|
||||
taskQueue.forEach(task => task())
|
||||
}
|
||||
catch (refreshErr) {
|
||||
console.error('刷新 token 失败:', refreshErr)
|
||||
refreshing = false
|
||||
// 刷新 token 失败,跳转到登录页
|
||||
nextTick(() => {
|
||||
// 关闭其他弹窗
|
||||
uni.hideToast()
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
// 清除用户信息
|
||||
await tokenStore.logout()
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
toLoginPage()
|
||||
}, 2000)
|
||||
}
|
||||
finally {
|
||||
// 不管刷新 token 成功与否,都清空任务队列
|
||||
taskQueue = []
|
||||
}
|
||||
}
|
||||
|
||||
return reject(res)
|
||||
}
|
||||
|
||||
// 处理其他成功状态(HTTP状态码200-299)
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// 处理业务逻辑错误
|
||||
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: responseData.msg || '请求错误',
|
||||
})
|
||||
return reject(responseData.data)
|
||||
}
|
||||
return resolve(responseData.data)
|
||||
}
|
||||
|
||||
// 处理其他错误
|
||||
!options.hideErrorToast
|
||||
&& uni.showToast({
|
||||
icon: 'none',
|
||||
title: (res.data as any).msg || '请求错误',
|
||||
})
|
||||
reject(res)
|
||||
},
|
||||
// 响应失败
|
||||
fail(err) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '网络错误,换个网络试试',
|
||||
})
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
* @param url 后台地址
|
||||
* @param query 请求query参数
|
||||
* @param header 请求头,默认为json格式
|
||||
* @returns
|
||||
*/
|
||||
export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
query,
|
||||
method: 'GET',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
* @param url 后台地址
|
||||
* @param data 请求body参数
|
||||
* @param query 请求query参数,post请求也支持query,很多微信接口都需要
|
||||
* @param header 请求头,默认为json格式
|
||||
* @returns
|
||||
*/
|
||||
export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
query,
|
||||
data,
|
||||
method: 'POST',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
export function httpPut<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
data,
|
||||
query,
|
||||
method: 'PUT',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求(无请求体,仅 query)
|
||||
*/
|
||||
export function httpDelete<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
query,
|
||||
method: 'DELETE',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
// 支持与 axios 类似的API调用
|
||||
http.get = httpGet
|
||||
http.post = httpPost
|
||||
http.put = httpPut
|
||||
http.delete = httpDelete
|
||||
|
||||
// 支持与 alovaJS 类似的API调用
|
||||
http.Get = httpGet
|
||||
http.Post = httpPost
|
||||
http.Put = httpPut
|
||||
http.Delete = httpDelete
|
||||
69
mp-sc-frontend/src/http/interceptor.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { CustomRequestOptions } from '@/http/types'
|
||||
import { useTokenStore } from '@/store'
|
||||
import { getEnvBaseUrl } from '@/utils'
|
||||
import { stringifyQuery } from './tools/queryString'
|
||||
|
||||
// 请求基准地址
|
||||
const baseUrl = getEnvBaseUrl()
|
||||
|
||||
// 拦截器配置
|
||||
const httpInterceptor = {
|
||||
// 拦截前触发
|
||||
invoke(options: CustomRequestOptions) {
|
||||
// 如果您使用了alova,则请把下面的代码放开注释
|
||||
// alova 执行流程:alova beforeRequest --> 本拦截器 --> alova responded
|
||||
// return options
|
||||
|
||||
// 非 alova 请求,正常执行
|
||||
// 接口请求支持通过 query 参数配置 queryString
|
||||
if (options.query) {
|
||||
const queryStr = stringifyQuery(options.query)
|
||||
if (options.url.includes('?')) {
|
||||
options.url += `&${queryStr}`
|
||||
}
|
||||
else {
|
||||
options.url += `?${queryStr}`
|
||||
}
|
||||
}
|
||||
// 非 http 开头需拼接地址
|
||||
if (!options.url.startsWith('http')) {
|
||||
// #ifdef H5
|
||||
if (JSON.parse(import.meta.env.VITE_APP_PROXY_ENABLE)) {
|
||||
// 自动拼接代理前缀
|
||||
options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
|
||||
}
|
||||
else {
|
||||
options.url = baseUrl + options.url
|
||||
}
|
||||
// #endif
|
||||
// 非H5正常拼接
|
||||
// #ifndef H5
|
||||
options.url = baseUrl + options.url
|
||||
// #endif
|
||||
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
|
||||
}
|
||||
// 1. 请求超时
|
||||
options.timeout = 60000 // 60s
|
||||
// 2. (可选)添加小程序端请求头标识
|
||||
options.header = {
|
||||
...options.header,
|
||||
}
|
||||
// 3. 添加 token 请求头标识
|
||||
const tokenStore = useTokenStore()
|
||||
const token = tokenStore.updateNowTime().validToken
|
||||
|
||||
if (token) {
|
||||
options.header.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return options
|
||||
},
|
||||
}
|
||||
|
||||
export const requestInterceptor = {
|
||||
install() {
|
||||
// 拦截 request 请求
|
||||
uni.addInterceptor('request', httpInterceptor)
|
||||
// 拦截 uploadFile 文件上传
|
||||
uni.addInterceptor('uploadFile', httpInterceptor)
|
||||
},
|
||||
}
|
||||
68
mp-sc-frontend/src/http/tools/enum.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export enum ResultEnum {
|
||||
// 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200)
|
||||
Success0 = 0, // 成功
|
||||
Success200 = 200, // 成功
|
||||
Error = 400, // 错误
|
||||
Unauthorized = 401, // 未授权
|
||||
Forbidden = 403, // 禁止访问(原为forbidden)
|
||||
NotFound = 404, // 未找到(原为notFound)
|
||||
MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
|
||||
RequestTimeout = 408, // 请求超时(原为requestTimeout)
|
||||
InternalServerError = 500, // 服务器错误(原为internalServerError)
|
||||
NotImplemented = 501, // 未实现(原为notImplemented)
|
||||
BadGateway = 502, // 网关错误(原为badGateway)
|
||||
ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
|
||||
GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
|
||||
HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
|
||||
}
|
||||
export enum ContentTypeEnum {
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||
}
|
||||
/**
|
||||
* 根据状态码,生成对应的错误信息
|
||||
* @param {number|string} status 状态码
|
||||
* @returns {string} 错误信息
|
||||
*/
|
||||
export function ShowMessage(status: number | string): string {
|
||||
let message: string
|
||||
switch (status) {
|
||||
case 400:
|
||||
message = '请求错误(400)'
|
||||
break
|
||||
case 401:
|
||||
message = '未授权,请重新登录(401)'
|
||||
break
|
||||
case 403:
|
||||
message = '拒绝访问(403)'
|
||||
break
|
||||
case 404:
|
||||
message = '请求出错(404)'
|
||||
break
|
||||
case 408:
|
||||
message = '请求超时(408)'
|
||||
break
|
||||
case 500:
|
||||
message = '服务器错误(500)'
|
||||
break
|
||||
case 501:
|
||||
message = '服务未实现(501)'
|
||||
break
|
||||
case 502:
|
||||
message = '网络错误(502)'
|
||||
break
|
||||
case 503:
|
||||
message = '服务不可用(503)'
|
||||
break
|
||||
case 504:
|
||||
message = '网络超时(504)'
|
||||
break
|
||||
case 505:
|
||||
message = 'HTTP版本不受支持(505)'
|
||||
break
|
||||
default:
|
||||
message = `连接出错(${status})!`
|
||||
}
|
||||
return `${message},请检查网络或联系管理员!`
|
||||
}
|
||||
29
mp-sc-frontend/src/http/tools/queryString.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
|
||||
* 支持基本类型值和数组,不支持嵌套对象
|
||||
* @param obj 要序列化的对象
|
||||
* @returns 序列化后的查询字符串
|
||||
*/
|
||||
export function stringifyQuery(obj: Record<string, any>): string {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
||||
return ''
|
||||
|
||||
return Object.entries(obj)
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => {
|
||||
// 对键进行编码
|
||||
const encodedKey = encodeURIComponent(key)
|
||||
|
||||
// 处理数组类型
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.filter(item => item !== undefined && item !== null)
|
||||
.map(item => `${encodedKey}=${encodeURIComponent(item)}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
// 处理基本类型
|
||||
return `${encodedKey}=${encodeURIComponent(value)}`
|
||||
})
|
||||
.join('&')
|
||||
}
|
||||
39
mp-sc-frontend/src/http/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
|
||||
*/
|
||||
export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||
query?: Record<string, any>
|
||||
/** 出错时是否隐藏错误提示 */
|
||||
hideErrorToast?: boolean
|
||||
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
||||
|
||||
/** 主要提供给 openapi-ts-request 生成的代码使用 */
|
||||
export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
|
||||
|
||||
export interface HttpRequestResult<T> {
|
||||
promise: Promise<T>
|
||||
requestTask: UniApp.RequestTask
|
||||
}
|
||||
|
||||
// 通用响应格式
|
||||
export type IResponse<T = any> = {
|
||||
code: number
|
||||
data: T
|
||||
msg: string
|
||||
[key: string]: any // 允许额外属性
|
||||
}
|
||||
|
||||
// 分页请求参数
|
||||
export interface PageParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 分页响应数据
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
30
mp-sc-frontend/src/http/vue-query.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { CustomRequestOptions } from '@/http/types'
|
||||
import { http } from './http'
|
||||
|
||||
/*
|
||||
* openapi-ts-request 工具的 request 跨客户端适配方法
|
||||
*/
|
||||
export default function request<T extends { data?: any }>(
|
||||
url: string,
|
||||
options: Omit<CustomRequestOptions, 'url'> & {
|
||||
params?: Record<string, unknown>
|
||||
headers?: Record<string, unknown>
|
||||
},
|
||||
) {
|
||||
const requestOptions = {
|
||||
url,
|
||||
...options,
|
||||
}
|
||||
|
||||
if (options.params) {
|
||||
requestOptions.query = requestOptions.params
|
||||
delete requestOptions.params
|
||||
}
|
||||
|
||||
if (options.headers) {
|
||||
requestOptions.header = options.headers
|
||||
delete requestOptions.headers
|
||||
}
|
||||
|
||||
return http<T['data']>(requestOptions)
|
||||
}
|
||||
3
mp-sc-frontend/src/layouts/default.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
10
mp-sc-frontend/src/lib/md5.js
Normal file
34
mp-sc-frontend/src/lib/uqrcode.js
Normal file
19
mp-sc-frontend/src/main.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { requestInterceptor } from './http/interceptor'
|
||||
import { routeInterceptor } from './router/interceptor'
|
||||
|
||||
import store from './store'
|
||||
import '@/style/index.scss'
|
||||
import 'virtual:uno.css'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(store)
|
||||
app.use(routeInterceptor)
|
||||
app.use(requestInterceptor)
|
||||
|
||||
return {
|
||||
app,
|
||||
}
|
||||
}
|
||||
20
mp-sc-frontend/src/pages/auth/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 登录页
|
||||
需要输入账号、密码/验证码的登录页。
|
||||
|
||||
## 适用性
|
||||
|
||||
本页面主要用于 `h5` 和 `APP`。
|
||||
|
||||
小程序通常有平台的登录方式 `uni.login` 通常用不到登录页,所以不适用于 `小程序`。(即默认情况下,小程序环境是不会走登录拦截逻辑的。)
|
||||
|
||||
但是如果您的小程序也需要现实的 `登录页` 那也是可以使用的。
|
||||
|
||||
在 `src/router/config.ts` 中有一个变量 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在小程序中使用 `H5的登录页`。
|
||||
|
||||
更多信息请看 `src/router` 文件夹的内容。
|
||||
|
||||
## 登录跳转
|
||||
|
||||
目前登录的跳转逻辑主要在 `src/router/interceptor.ts` 和 `src/pages/login/login.vue` 里面,默认会在登录后自动重定向到来源/配置的页面。
|
||||
|
||||
如果与您的业务不符,您可以自行修改。
|
||||
116
mp-sc-frontend/src/pages/auth/login.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { isPageTabbar } from '@/tabbar/store'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '登录',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
// 登录页不需要登录即可访问
|
||||
excludeLoginPath: true,
|
||||
})
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const mockCode = ref('mock_admin')
|
||||
const redirectPath = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
// 获取登录后要跳转的目标页面
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = (currentPage as any).$page?.options || currentPage.options || {}
|
||||
if (options.redirect) {
|
||||
redirectPath.value = decodeURIComponent(options.redirect)
|
||||
}
|
||||
})
|
||||
|
||||
// 登录成功后跳转
|
||||
function afterLogin() {
|
||||
if (redirectPath.value) {
|
||||
// 有 redirect 参数,跳转到目标页面
|
||||
const url = redirectPath.value
|
||||
if (isPageTabbar(url)) {
|
||||
uni.switchTab({ url })
|
||||
} else {
|
||||
uni.redirectTo({ url })
|
||||
}
|
||||
}
|
||||
else {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
// 暂不登录,返回上一页
|
||||
function handleSkip() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
async function handleWxLogin() {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
await tokenStore.wxLogin()
|
||||
afterLogin()
|
||||
}
|
||||
catch (error) {
|
||||
console.log('微信登录失败', error)
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.showToast({ title: '请在微信小程序中使用', icon: 'none' })
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-page h-screen">
|
||||
<view class="mx-xl text-center pt-40">
|
||||
<wd-img round :width="80" :height="80" src="//wx.qlogo.cn/mmhead/B2EfAOZfS1hcmGxwIBm2jqd7g94iaWziaURo0f8bsAu3GKKIsTY8NMtweRMJ8H7YXBQXfibkrCAKp0/0" />
|
||||
|
||||
<view class="text-white text-2xl font-bold mt-10">四川凌空天行科技有限公司</view>
|
||||
|
||||
<!-- 微信登录 -->
|
||||
<view class="mt-20">
|
||||
<wd-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
round
|
||||
@click="handleWxLogin"
|
||||
>
|
||||
微信一键登录
|
||||
</wd-button>
|
||||
|
||||
<view class="mt-4">
|
||||
<wd-button
|
||||
size="large"
|
||||
block
|
||||
round
|
||||
plain
|
||||
@click="handleSkip"
|
||||
>
|
||||
暂不登录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: url('//mmbiz.qpic.cn/sz_mmbiz_png/XekA4rfc3leUkNBAibc5GrYiaq0iaIibtdtbfHFLwJicJ5bLRjpjFDWdaib5ibLtCBSKJ7YA0JnhyK7a5orYOEOMqJy5R2h6zWicvj7HrOOceMibIdPs/0?from=appmsg')
|
||||
no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.brand-section {
|
||||
border-radius: 0 0 40% 40% / 30px;
|
||||
}
|
||||
</style>
|
||||
34
mp-sc-frontend/src/pages/auth/register.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<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>
|
||||
261
mp-sc-frontend/src/pages/index/index.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { useUserStore } from '@/store'
|
||||
import { getBanners, getNotices } from '@/api/appointment'
|
||||
import type { IBanner, INotice } from '@/api/types/appointment'
|
||||
|
||||
definePage({
|
||||
type: 'home',
|
||||
style: {
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '首页',
|
||||
},
|
||||
})
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const userStore = useUserStore()
|
||||
const { isEmployee, isGuard } = storeToRefs(userStore)
|
||||
|
||||
// ==================== 轮播图 ====================
|
||||
const banners = ref<IBanner[]>([
|
||||
{"image_url":"https://mmbiz.qpic.cn/mmbiz_jpg/XekA4rfc3lccQHzZScOHm74CUWic2L0mmnUYkThXMTyjk6XMeCmiacZ8FyH8yoeNicac5vImCT8TrYURwsvfZKWufHdHqVibXtoc6oWmbLWvAv0/0?from=appmsg","jump_path":"","is_jump":false},
|
||||
{"image_url":"https://mmbiz.qpic.cn/mmbiz_jpg/XekA4rfc3lcztReQUQC5VhIYPy5wibC6gnxflibgP7hcjw88ZlT2JT3j6RConSxMej5WVWxHEKEK7Z55uKJqb1CRibShyDkmGLQMCSrUo8LrY8/0?from=appmsg","jump_path":"","is_jump":false},
|
||||
{"image_url":"https://mmbiz.qpic.cn/mmbiz_jpg/XekA4rfc3leNOuzXa7ERs9lWSBZDnrqJlqDnjVdnoaB9PmyUh20dqD72D4xibhNdDPMs7JxHZFJpqficngc2DIDwptgmlZt8lY7jp03La43KM/0?from=appmsg","jump_path":"","is_jump":false},
|
||||
{"image_url":"https://mmbiz.qpic.cn/sz_mmbiz_jpg/XekA4rfc3ldCJC3G64VvKR4T3RVQavicFbvXooEg69icEamMyy4MGiaRlHVibgicvb3J1cGTkHlhD3oZhOiabSoP1u1hzUG4exaIwH6zJymGJk3dM/0?from=appmsg","jump_path":"","is_jump":false}
|
||||
])
|
||||
|
||||
async function loadBanners() {
|
||||
try { banners.value = await getBanners() } catch {}
|
||||
}
|
||||
|
||||
function onBannerClick(banner: IBanner) {
|
||||
if (!banner.is_jump || !banner.jump_path) return
|
||||
const encodedUrl = encodeURIComponent(banner.jump_path)
|
||||
uni.navigateTo({ url: `/pages/public/webview?url=${encodedUrl}` })
|
||||
}
|
||||
|
||||
// ==================== 消息通知(接口查询) ====================
|
||||
const notices = ref<INotice[]>([])
|
||||
const noticeScrollIndex = ref(0)
|
||||
let noticeTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// 通知详情弹窗
|
||||
const showNoticeDetail = ref(false)
|
||||
const currentNotice = ref<INotice | null>(null)
|
||||
|
||||
async function loadNotices() {
|
||||
try { notices.value = await getNotices() } catch {}
|
||||
}
|
||||
|
||||
function startNoticeScroll() {
|
||||
if (notices.value.length <= 1) return
|
||||
noticeTimer = setInterval(() => {
|
||||
noticeScrollIndex.value = (noticeScrollIndex.value + 1) % notices.value.length
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
function onNoticeClick() {
|
||||
if (notices.value.length === 0) return
|
||||
currentNotice.value = notices.value[noticeScrollIndex.value]
|
||||
showNoticeDetail.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBanners()
|
||||
loadNotices()
|
||||
startNoticeScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (noticeTimer) clearInterval(noticeTimer)
|
||||
})
|
||||
|
||||
// 分享
|
||||
onShareAppMessage(() => ({ title: '四川凌空天行 - 访客预约', path: '/pages/index/index' }))
|
||||
onShareTimeline(() => ({ title: '四川凌空天行 - 访客预约' }))
|
||||
|
||||
// ==================== 功能模块(仅保留访客预约入口) ====================
|
||||
interface ModuleEntry {
|
||||
title: string
|
||||
desc: string
|
||||
icon: string
|
||||
path: string
|
||||
show: boolean
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
const modules = computed<ModuleEntry[]>(() => [
|
||||
{
|
||||
title: '访客预约',
|
||||
desc: '预约来访、核验、审查、管理',
|
||||
icon: 'i-carbon-add',
|
||||
path: '/subpackages/appointment/index',
|
||||
show: true,
|
||||
color: '#2979ff',
|
||||
bgColor: '#e8f0fe',
|
||||
},
|
||||
])
|
||||
|
||||
const visibleModules = computed(() => modules.value.filter(m => m.show))
|
||||
|
||||
function goPage(path: string) {
|
||||
if (!tokenStore.hasLogin) {
|
||||
uni.navigateTo({ url: `/pages/auth/login?redirect=${encodeURIComponent(path)}` })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: path })
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
const navOpacity = ref(0)
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
const statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
|
||||
// #ifdef H5
|
||||
function onPageScroll(e: any) {
|
||||
navOpacity.value = Math.min((e?.scrollTop || 0) / 200, 0.95)
|
||||
}
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="home-page">
|
||||
<view :style="`height: ${statusBarHeight}px`" />
|
||||
<view class="flex items-center px-4" style="height: 44px;">
|
||||
<wd-img round :width="40" :height="40" src="//wx.qlogo.cn/mmhead/B2EfAOZfS1hcmGxwIBm2jqd7g94iaWziaURo0f8bsAu3GKKIsTY8NMtweRMJ8H7YXBQXfibkrCAKp0/0" />
|
||||
<view class="ml-2 text-white font-bold">
|
||||
<view>欢迎使用</view>
|
||||
<view class="text-xs">四川凌空访客预约系统</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 轮播图 -->
|
||||
<view class="banner-section px-4 pt-4">
|
||||
<swiper
|
||||
class="banner-swiper"
|
||||
:indicator-dots="banners.length > 1"
|
||||
:autoplay="true"
|
||||
:interval="4000"
|
||||
:duration="500"
|
||||
:circular="true"
|
||||
indicator-color="rgba(255,255,255,0.5)"
|
||||
indicator-active-color="#2979ff"
|
||||
>
|
||||
<swiper-item v-for="(item, idx) in banners" :key="idx">
|
||||
<image
|
||||
:src="item.image_url"
|
||||
class="banner-image h-full w-full rounded-2xl"
|
||||
mode="aspectFill"
|
||||
@click="onBannerClick(item)"
|
||||
/>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- 消息通知(接口查询) -->
|
||||
<view v-if="notices.length > 0" class="notice-section mx-4 mt-3">
|
||||
<view class="notice-bar flex items-center rounded-xl bg-white px-3 py-2.5 shadow-sm" @click="onNoticeClick">
|
||||
<view class="mr-2 h-7 w-7 flex items-center justify-center rounded-lg bg-[#fff3e0]">
|
||||
<view class="i-carbon-notification text-sm text-[#ff9900]" />
|
||||
</view>
|
||||
<view class="notice-content flex-1 overflow-hidden">
|
||||
<swiper class="notice-swiper" :vertical="true" :autoplay="true" :interval="4000" :duration="600" :circular="true" :disable-touch="true" :current="noticeScrollIndex">
|
||||
<swiper-item v-for="item in notices" :key="item.id">
|
||||
<view class="h-full flex items-center">
|
||||
<text class="notice-text truncate text-sm text-[#333]">{{ item.content }}</text>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<view class="i-carbon-chevron-right text-sm text-[#ccc]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 通知详情弹窗 -->
|
||||
<wd-popup v-model="showNoticeDetail" position="center" custom-style="width: 320px; border-radius: 12px; padding: 20px;">
|
||||
<view v-if="currentNotice">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold">{{ currentNotice.title }}</text>
|
||||
</view>
|
||||
<view class="mt-4 rounded-lg bg-gray-50 p-4">
|
||||
<text class="text-sm leading-relaxed text-[#333]">{{ currentNotice.content }}</text>
|
||||
</view>
|
||||
<view class="mt-4 flex justify-center">
|
||||
<wd-button @click="showNoticeDetail = false">关闭</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 功能模块(仅访客预约) -->
|
||||
<view class="module-section px-4 pb-6 pt-4">
|
||||
<view class="mb-3 flex items-center gap-2">
|
||||
<view class="flex h-6 w-6 items-center justify-center rounded-lg bg-[#e8f0fe]">
|
||||
<view class="i-carbon-list-boxes text-md text-[#2979ff]" />
|
||||
</view>
|
||||
<text class="text-base text-[#1a1a1a] font-semibold">功能模块</text>
|
||||
</view>
|
||||
<view class="flex flex-col gap-3">
|
||||
<view
|
||||
v-for="mod in visibleModules"
|
||||
:key="mod.title"
|
||||
class="module-card flex items-center rounded-2xl bg-white p-4 shadow-sm transition-transform active:scale-[0.98]"
|
||||
@click="goPage(mod.path)"
|
||||
>
|
||||
<view
|
||||
class="h-13 w-13 flex items-center justify-center rounded-2xl"
|
||||
:style="{ backgroundColor: mod.bgColor }"
|
||||
>
|
||||
<view :class="[mod.icon, 'text-3xl']" :style="{ color: mod.color }" />
|
||||
</view>
|
||||
<view class="ml-4 flex-1">
|
||||
<text class="block text-base text-[#1a1a1a] font-semibold">{{ mod.title }}</text>
|
||||
<text class="mt-0.5 block text-xs text-[#999]">{{ mod.desc }}</text>
|
||||
</view>
|
||||
<view class="i-carbon-chevron-right text-base text-[#ccc]" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="h-6" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: url('//mmbiz.qpic.cn/mmbiz_png/XekA4rfc3ld2iccHrK4eZNiaaHCdlmggda6GFmZHhuLo4hHPrbHWORiax6xyHExKPBKXa6OjHCWQbHJHibnZaaYYDjvk9R9qsaVzbbFmQwv5jYo/0?from=appmsg')
|
||||
#f3f4fc no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.banner-swiper {
|
||||
height: 320rpx;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notice-bar {
|
||||
border: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.notice-swiper {
|
||||
height: 40rpx;
|
||||
}
|
||||
.notice-text {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
</style>
|
||||
322
mp-sc-frontend/src/pages/me/me.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { useUserStore } from '@/store'
|
||||
import { uploadImage, bindPhone, updateAvatar } from '@/api/appointment'
|
||||
import type { ICertificationInfo } from '@/api/types/appointment'
|
||||
|
||||
definePage({
|
||||
style: { navigationBarTitleText: '个人中心' },
|
||||
})
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const userStore = useUserStore()
|
||||
const { userInfo } = storeToRefs(userStore)
|
||||
const { isFormalEmployee } = storeToRefs(userStore)
|
||||
|
||||
// 编辑姓名
|
||||
const showEditDialog = ref(false)
|
||||
const editName = ref('')
|
||||
|
||||
function openEdit() {
|
||||
editName.value = userInfo.value.real_name || ''
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
async function saveName() {
|
||||
if (!editName.value.trim()) {
|
||||
uni.showToast({ title: '姓名不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await userStore.updateUserInfo({ real_name: editName.value.trim() })
|
||||
showEditDialog.value = false
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} catch {
|
||||
uni.showToast({ title: '修改失败', icon: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
// 微信头像选择
|
||||
async function onChooseAvatar(e: any) {
|
||||
const { avatarUrl } = e.detail
|
||||
uni.showLoading({ title: '上传头像...' })
|
||||
try {
|
||||
const result = await uploadImage(avatarUrl)
|
||||
await updateAvatar(userInfo.value.id, result.url)
|
||||
userInfo.value.avatar_url = result.url
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '头像更新成功', icon: 'success' })
|
||||
} catch (err: any) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: err.message || '头像上传失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 微信昵称输入
|
||||
const showNicknameInput = ref(false)
|
||||
const nicknameValue = ref('')
|
||||
|
||||
function openNicknameEdit() {
|
||||
nicknameValue.value = userInfo.value.nickname || ''
|
||||
showNicknameInput.value = true
|
||||
}
|
||||
|
||||
async function onNicknameBlur(e: any) {
|
||||
const nickname = e.detail.value
|
||||
showNicknameInput.value = false
|
||||
if (!nickname || nickname === userInfo.value.nickname) return
|
||||
try {
|
||||
await userStore.updateUserInfo({ nickname })
|
||||
uni.showToast({ title: '昵称更新成功', icon: 'success' })
|
||||
} catch {
|
||||
uni.showToast({ title: '昵称更新失败', icon: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
const redirectUrl = encodeURIComponent('/pages/me/me')
|
||||
uni.navigateTo({ url: `/pages/auth/login?redirect=${redirectUrl}` })
|
||||
}
|
||||
|
||||
// ==================== 角色认证 ====================
|
||||
const certification = computed(() => userStore.userInfo.certification)
|
||||
|
||||
function goCertApply() {
|
||||
uni.navigateTo({ url: '/subpackages/system/certification-apply' })
|
||||
}
|
||||
|
||||
const certStatusText = computed(() => {
|
||||
if (!certification.value) return '未认证'
|
||||
const map: Record<number, string> = { 0: '待审核', 1: '已通过', 2: '已拒绝' }
|
||||
return map[certification.value.status] || '未认证'
|
||||
})
|
||||
|
||||
// ==================== 手机号绑定 ====================
|
||||
const showPhoneDialog = ref(false)
|
||||
const bindingPhone = ref(false)
|
||||
|
||||
function handlePhoneTap() {
|
||||
if (!userInfo.value.phone) {
|
||||
showPhoneDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function onGetPhoneNumber(e: any) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
uni.showToast({ title: '请授权手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindingPhone.value = true
|
||||
try {
|
||||
const loginRes = await uni.login()
|
||||
await bindPhone({
|
||||
code: loginRes.code!,
|
||||
encrypted_data: e.detail.encryptedData,
|
||||
iv: e.detail.iv,
|
||||
})
|
||||
await userStore.fetchUserInfo()
|
||||
showPhoneDialog.value = false
|
||||
uni.showToast({ title: '手机号绑定成功', icon: 'success' })
|
||||
} catch (err: any) {
|
||||
uni.showToast({ title: err.message || '手机号绑定失败', icon: 'error' })
|
||||
} finally {
|
||||
bindingPhone.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 常用 ====================
|
||||
function handleOpenSetting() {
|
||||
uni.openSetting({
|
||||
success: (res) => console.log('打开设置成功', res),
|
||||
})
|
||||
}
|
||||
|
||||
function handleOpenPrivacy() {
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.openPrivacyContract({
|
||||
success: () => console.log('打开隐私政策成功'),
|
||||
fail: (err) => {
|
||||
console.error('打开隐私政策失败:', err)
|
||||
uni.showToast({ title: '隐私政策加载失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
onShow(async () => {
|
||||
if (tokenStore.hasLogin) {
|
||||
try {
|
||||
await userStore.fetchUserInfo()
|
||||
} catch (err) {
|
||||
console.error('获取用户信息失败:', err)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
|
||||
<!-- ============ 未登录 ============ -->
|
||||
<view v-if="!tokenStore.hasLogin" class="flex flex-col items-center pt-20">
|
||||
<image src="/static/images/default-avatar.png" class="w-24 h-24 rounded-full" mode="aspectFill" />
|
||||
<text class="mt-5 text-lg font-bold text-gray-800" @click="handleLogin">立即登录</text>
|
||||
<text class="mt-1 text-sm text-gray-400">登录后享受更多服务</text>
|
||||
</view>
|
||||
|
||||
<!-- ============ 已登录 ============ -->
|
||||
<template v-if="tokenStore.hasLogin">
|
||||
<!-- 头部:头像 + 信息 -->
|
||||
<view class="bg-gradient-to-b from-blue-500 to-blue-400 px-5 pt-12 pb-10">
|
||||
<view class="flex items-center gap-4">
|
||||
<button class="w-16 h-16 rounded-full overflow-hidden border-2 border-white/40 bg-transparent p-0 shrink-0" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<image :src="userInfo.avatar_url || '/static/images/default-avatar.png'" class="w-full h-full" mode="aspectFill" />
|
||||
</button>
|
||||
<view class="flex-1">
|
||||
<text class="text-lg font-bold text-white">{{ userInfo.nickname || userInfo.real_name || '未设置昵称' }}</text>
|
||||
<view class="flex items-center gap-2 mt-1">
|
||||
<view class="rounded-full px-2.5 py-0.5 text-xs font-medium"
|
||||
:class="(userInfo.role || 'visitor') === 'employee' ? 'bg-green-200 text-green-800' : (userInfo.role || 'visitor') === 'guard' ? 'bg-yellow-200 text-yellow-800' : 'bg-white/30 text-white'">
|
||||
{{ userInfo.role_name || '访客' }}
|
||||
</view>
|
||||
<text v-if="isFormalEmployee && userInfo.department_name" class="text-xs text-white/80">{{ userInfo.department_name }}</text>
|
||||
<text v-if="isFormalEmployee && userInfo.position" class="text-xs text-white/80">{{ userInfo.position }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信息卡片 -->
|
||||
<view class="-mt-4 mx-4 rounded-xl bg-white shadow-sm px-4 py-3">
|
||||
<view class="flex items-center justify-between py-3">
|
||||
<text class="text-sm text-gray-500">账号ID</text>
|
||||
<text class="text-sm text-gray-400">{{ userInfo.id || '' }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-3 border-b border-gray-50" @click="openNicknameEdit">
|
||||
<text class="text-sm text-gray-500">昵称</text>
|
||||
<view class="flex items-center gap-1">
|
||||
<text class="text-sm text-gray-800">{{ userInfo.nickname || '未设置' }}</text>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-3 border-b border-gray-50" @click="openEdit">
|
||||
<text class="text-sm text-gray-500">姓名</text>
|
||||
<view class="flex items-center gap-1">
|
||||
<text class="text-sm text-gray-800">{{ userInfo.real_name || '未设置' }}</text>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-3 border-b border-gray-50" @click="handlePhoneTap">
|
||||
<text class="text-sm text-gray-500">手机号</text>
|
||||
<view class="flex items-center gap-1">
|
||||
<text :class="userInfo.phone ? 'text-gray-800' : 'text-orange-500'" class="text-sm">{{ userInfo.phone || '未绑定' }}</text>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 角色认证(仅访客) -->
|
||||
<view v-if="userInfo.role === 'visitor'" class="mx-4 mt-3 rounded-xl bg-white shadow-sm">
|
||||
<view class="flex items-center justify-between px-4 py-3.5" @click="goCertApply">
|
||||
<view class="flex items-center gap-3">
|
||||
<view class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<text class="text-blue-500 text-sm">📜</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-800 font-medium">申请角色认证</text>
|
||||
</view>
|
||||
<view class="flex items-center gap-2">
|
||||
<text class="text-xs text-gray-400">{{ certStatusText }}</text>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- ============ 常用功能 ============ -->
|
||||
<view class="mx-4 mt-3 rounded-xl bg-white shadow-sm">
|
||||
<view class="px-4 py-3 border-b border-gray-50">
|
||||
<text class="text-xs text-gray-400 font-medium">常用功能</text>
|
||||
</view>
|
||||
<button class="w-full bg-transparent border-none p-0 m-0 text-left" open-type="contact">
|
||||
<view class="flex items-center gap-3 px-4 py-3.5 border-b border-gray-50">
|
||||
<view class="w-8 h-8 rounded-lg bg-green-50 flex items-center justify-center">
|
||||
<text class="text-green-500 text-sm">💬</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-800 flex-1">联系客服</text>
|
||||
|
||||
</view>
|
||||
</button>
|
||||
<view class="flex items-center gap-3 px-4 py-3.5 border-b border-gray-50" @click="handleOpenSetting">
|
||||
<view class="w-8 h-8 rounded-lg bg-purple-50 flex items-center justify-center">
|
||||
<text class="text-purple-500 text-sm">⚙️</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-800 flex-1">权限管理</text>
|
||||
|
||||
</view>
|
||||
<view class="flex items-center gap-3 px-4 py-3.5" @click="handleOpenPrivacy">
|
||||
<view class="w-8 h-8 rounded-lg bg-gray-50 flex items-center justify-center">
|
||||
<text class="text-gray-500 text-sm">📄</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-800 flex-1">隐私政策</text>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ============ 弹窗 ============ -->
|
||||
|
||||
<!-- 昵称 -->
|
||||
<wd-popup v-model="showNicknameInput" position="center" custom-style="width: 300px; border-radius: 12px; padding: 20px;">
|
||||
<view class="text-center"><text class="text-lg font-bold">设置昵称</text></view>
|
||||
<view class="mt-4">
|
||||
<input type="nickname" class="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-center text-sm" placeholder="点击获取微信昵称" :value="nicknameValue" @blur="onNicknameBlur" @confirm="onNicknameBlur" />
|
||||
</view>
|
||||
<view class="mt-4 flex justify-center">
|
||||
<wd-button @click="showNicknameInput = false">取消</wd-button>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 姓名 -->
|
||||
<wd-popup v-model="showEditDialog" position="center" custom-style="width: 300px; border-radius: 12px; padding: 20px;">
|
||||
<view class="text-center"><text class="text-lg font-bold">修改姓名</text></view>
|
||||
<view class="mt-4"><wd-input v-model="editName" placeholder="请输入真实姓名" clearable /></view>
|
||||
<view class="mt-4 flex justify-center gap-3">
|
||||
<wd-button @click="showEditDialog = false">取消</wd-button>
|
||||
<wd-button type="primary" @click="saveName">确定</wd-button>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<wd-popup v-model="showPhoneDialog" position="center" custom-style="width: 300px; border-radius: 12px; padding: 20px;">
|
||||
<view class="text-center"><text class="text-lg font-bold">绑定手机号</text></view>
|
||||
<view class="mt-2 text-sm text-gray-500 text-center">绑定手机号</view>
|
||||
<view class="mt-5 flex flex-col items-center gap-3">
|
||||
<button class="w-full bg-transparent border-none p-0 m-0" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
|
||||
<wd-button type="primary" size="large" :loading="bindingPhone" block>微信手机号一键获取</wd-button>
|
||||
</button>
|
||||
<wd-button plain @click="showPhoneDialog = false">暂不绑定</wd-button>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 底部安全区 -->
|
||||
<view class="h-[env(safe-area-inset-bottom,16px)]" />
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
button {
|
||||
&::after { border: none; }
|
||||
}
|
||||
.contact-btn-style {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: inherit;
|
||||
text-align: left;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
98
mp-sc-frontend/src/pages/public/invitation.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IInvitation } from '@/api/types/appointment'
|
||||
import { getInvitationDetail } from '@/api/appointment'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '访客邀请',
|
||||
},
|
||||
// 邀请页不需要登录
|
||||
excludeLoginPath: true,
|
||||
})
|
||||
|
||||
const invitation = ref<IInvitation | null>(null)
|
||||
const loading = ref(true)
|
||||
|
||||
async function loadInvitation() {
|
||||
const pages = getCurrentPages()
|
||||
const current = pages[pages.length - 1] as any
|
||||
const code = current?.options?.code
|
||||
if (!code) {
|
||||
uni.showToast({ title: '无效的邀请码', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
invitation.value = await getInvitationDetail(code)
|
||||
} catch {
|
||||
uni.showToast({ title: '邀请不存在或已过期', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goCreate() {
|
||||
if (!invitation.value) return
|
||||
uni.navigateTo({
|
||||
url: `/subpackages/appointment/appointment-form?invite_code=${invitation.value.invite_code}`,
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadInvitation()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="invitation-page min-h-screen bg-[#f5f5f5]">
|
||||
<wd-loading v-if="loading" custom-class="flex justify-center py-20" />
|
||||
|
||||
<template v-else-if="invitation">
|
||||
<!-- 邀请头部 -->
|
||||
<view class="bg-gradient-to-r from-purple-500 to-purple-700 px-5 py-8 text-center">
|
||||
<text class="text-2xl font-bold text-white">访客邀请</text>
|
||||
<text class="mt-2 block text-sm text-white/80">
|
||||
{{ invitation.employee?.name }} 邀请您来访
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 邀请详情 -->
|
||||
<view class="mx-3 mt-3 rounded-xl bg-white">
|
||||
<wd-cell-group title="邀请详情" border>
|
||||
<wd-cell title="邀请人" :value="invitation.employee?.name || '-'" />
|
||||
<wd-cell title="部门" :value="invitation.employee?.department || '-'" />
|
||||
<wd-cell v-if="invitation.visitor_name" title="访客姓名" :value="invitation.visitor_name" />
|
||||
<wd-cell v-if="invitation.visitor_phone" title="访客手机" :value="invitation.visitor_phone" />
|
||||
<wd-cell title="来访目的" :value="invitation.visit_type" />
|
||||
<wd-cell v-if="invitation.visit_area" title="预设区域" :value="invitation.visit_area" />
|
||||
<wd-cell title="有效期" :value="`${invitation.valid_from?.slice(0, 10)} ~ ${invitation.valid_until?.slice(0, 10)}`" />
|
||||
<wd-cell title="剩余次数" :value="`${invitation.max_use_count - invitation.used_count}/${invitation.max_use_count}`" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 使用邀请 -->
|
||||
<view v-if="invitation.is_active && invitation.used_count < invitation.max_use_count" class="px-3 pt-4">
|
||||
<wd-button type="primary" size="large" block @click="goCreate">
|
||||
使用邀请创建预约
|
||||
</wd-button>
|
||||
</view>
|
||||
<view v-else class="px-3 pt-4">
|
||||
<wd-button type="info" size="large" block disabled>
|
||||
邀请已失效
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="flex flex-col items-center justify-center py-20">
|
||||
<wd-icon name="warning" size="48px" color="#ccc" />
|
||||
<text class="mt-4 text-[#999]">邀请不存在或已过期</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.invitation-page {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
35
mp-sc-frontend/src/pages/public/webview.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
},
|
||||
excludeLoginPath: true,
|
||||
})
|
||||
|
||||
const webviewUrl = ref('')
|
||||
|
||||
onLoad((options?: any) => {
|
||||
if (options?.url) {
|
||||
webviewUrl.value = decodeURIComponent(options.url)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="webview-page">
|
||||
<web-view v-if="webviewUrl" :src="webviewUrl" />
|
||||
<view v-else class="flex h-screen items-center justify-center text-[#999]">
|
||||
链接无效
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.webview-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
55
mp-sc-frontend/src/router/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 登录 说明
|
||||
|
||||
## 登录 2种策略
|
||||
- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
|
||||
- 默认需要登录策略: DEFAULT_NEED_LOGIN
|
||||
|
||||
### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
|
||||
进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。
|
||||
|
||||
比如大部分2C的应用,美团、今日头条、抖音等,都可以直接浏览,只有点赞、评论、分享等操作或者去特殊页面(比如个人中心),才需要登录。
|
||||
|
||||
### 默认需要登录策略: DEFAULT_NEED_LOGIN
|
||||
|
||||
进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。
|
||||
|
||||
比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。
|
||||
|
||||
### EXCLUDE_LOGIN_PATH_LIST
|
||||
`EXCLUDE_LOGIN_PATH_LIST` 表示排除的路由列表。
|
||||
|
||||
在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才需要登录,相当于黑名单。
|
||||
|
||||
在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才不需要登录,相当于白名单。
|
||||
|
||||
### excludeLoginPath
|
||||
definePage 中可以通过 `excludeLoginPath` 来配置路由是否需要登录。(类似过去的 needLogin 的功能)
|
||||
|
||||
```ts
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '关于',
|
||||
},
|
||||
// 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹
|
||||
excludeLoginPath: true,
|
||||
// 角色授权(可选):如果需要根据角色授权,就配置这个
|
||||
roleAuth: {
|
||||
field: 'role',
|
||||
value: 'admin',
|
||||
redirect: '/pages/auth/403',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## 登录注册页路由
|
||||
|
||||
登录页 `login.vue` 对应路由是 `/pages/login/login`.
|
||||
注册页 `register.vue` 对应路由是 `/pages/login/register`.
|
||||
|
||||
## 登录注册页适用性
|
||||
|
||||
登录注册页主要适用于 `h5` 和 `App`,默认不适用于 `小程序`,因为 `小程序` 通常会使用平台提供的快捷登录。
|
||||
|
||||
特殊情况例外,如业务需要跨平台复用登录注册页时,也可以用在 `小程序` 上,所以主要还是看业务需求。
|
||||
|
||||
通过一个参数 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在 `小程序` 中使用 `H5登录页` 的登录逻辑。
|
||||
28
mp-sc-frontend/src/router/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getAllPages } from '@/utils'
|
||||
|
||||
export const LOGIN_STRATEGY_MAP = {
|
||||
DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP
|
||||
DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录
|
||||
}
|
||||
// 使用白名单策略:默认需要登录才能访问
|
||||
export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
|
||||
export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
|
||||
|
||||
export const LOGIN_PAGE = '/pages/auth/login'
|
||||
export const REGISTER_PAGE = '/pages/auth/register'
|
||||
|
||||
export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE]
|
||||
|
||||
// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同
|
||||
export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path)
|
||||
|
||||
// 白名单:这些页面不需要登录即可访问
|
||||
export const EXCLUDE_LOGIN_PATH_LIST = [
|
||||
'/pages/auth/login',
|
||||
'/pages/auth/register',
|
||||
'/pages/public/invitation',
|
||||
...excludeLoginPathList,
|
||||
]
|
||||
|
||||
// 在小程序里面是否使用H5的登录页,默认为 false
|
||||
export const LOGIN_PAGE_ENABLE_IN_MP = false
|
||||
145
mp-sc-frontend/src/router/interceptor.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { isMp } from '@uni-helper/uni-env'
|
||||
/**
|
||||
* by 菲鸽 on 2025-08-19
|
||||
* 路由拦截,通常也是登录拦截
|
||||
* 黑、白名单的配置,请看 config.ts 文件, EXCLUDE_LOGIN_PATH_LIST
|
||||
*/
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { isPageTabbar, tabbarStore } from '@/tabbar/store'
|
||||
import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index'
|
||||
import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP } from './config'
|
||||
|
||||
export const FG_LOG_ENABLE = false
|
||||
|
||||
export function judgeIsExcludePath(path: string) {
|
||||
const isDev = import.meta.env.DEV
|
||||
if (!isDev) {
|
||||
return EXCLUDE_LOGIN_PATH_LIST.includes(path)
|
||||
}
|
||||
const allExcludeLoginPages = getAllPages('excludeLoginPath') // dev 环境下,需要每次都重新获取,否则新配置就不会生效
|
||||
return EXCLUDE_LOGIN_PATH_LIST.includes(path) || (isDev && allExcludeLoginPages.some(page => page.path === path))
|
||||
}
|
||||
|
||||
export const navigateToInterceptor = {
|
||||
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
|
||||
// 增加对相对路径的处理,BY 网友 @ideal
|
||||
invoke({ url, query }: { url: string, query?: Record<string, string> }) {
|
||||
if (url === undefined) {
|
||||
return
|
||||
}
|
||||
let { path, query: _query } = parseUrlToObj(url)
|
||||
|
||||
FG_LOG_ENABLE && console.log('\n\n路由拦截器:-------------------------------------')
|
||||
FG_LOG_ENABLE && console.log('路由拦截器 1: url->', url, ', query ->', query)
|
||||
const myQuery = { ..._query, ...query }
|
||||
// /pages/route-interceptor/index?name=feige&age=30
|
||||
FG_LOG_ENABLE && console.log('路由拦截器 2: path->', path, ', _query ->', _query)
|
||||
FG_LOG_ENABLE && console.log('路由拦截器 3: myQuery ->', myQuery)
|
||||
|
||||
// 处理相对路径
|
||||
if (!path.startsWith('/')) {
|
||||
const currentPath = getLastPage()?.route || ''
|
||||
const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`
|
||||
const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/'))
|
||||
path = `${baseDir}/${path}`
|
||||
}
|
||||
|
||||
// // 处理路由不存在的情况
|
||||
// if (path !== '/' && !getAllPages().some(page => page.path === path)) {
|
||||
// console.warn('路由不存在:', path)
|
||||
// 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 默认不需要登录的情况(黑名单策略) ---------------------------
|
||||
},
|
||||
}
|
||||
|
||||
// 针对 chooseLocation 的特殊处理
|
||||
export const chooseLocationInterceptor = {
|
||||
invoke(options: any) {
|
||||
// 直接放行 chooseLocation 调用
|
||||
FG_LOG_ENABLE && console.log('chooseLocation 调用,直接放行:', options)
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
export const routeInterceptor = {
|
||||
install() {
|
||||
uni.addInterceptor('navigateTo', navigateToInterceptor)
|
||||
uni.addInterceptor('reLaunch', navigateToInterceptor)
|
||||
uni.addInterceptor('redirectTo', navigateToInterceptor)
|
||||
uni.addInterceptor('switchTab', navigateToInterceptor)
|
||||
|
||||
// 添加 chooseLocation 的拦截器,确保直接放行
|
||||
uni.addInterceptor('chooseLocation', chooseLocationInterceptor)
|
||||
},
|
||||
}
|
||||
11
mp-sc-frontend/src/router/permission.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { tabbarStore } from '@/tabbar/store'
|
||||
|
||||
export const permission = {
|
||||
install(router) {
|
||||
router.beforeEach((to, from, next) => {
|
||||
const path = to.path
|
||||
tabbarStore.setAutoCurIdx(path)
|
||||
next()
|
||||
})
|
||||
},
|
||||
}
|
||||
6
mp-sc-frontend/src/service/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
export * from './types';
|
||||
|
||||
export * from './listAll';
|
||||
export * from './info';
|
||||
14
mp-sc-frontend/src/service/info.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
import request from '@/http/vue-query';
|
||||
import { CustomRequestOptions_ } from '@/http/types';
|
||||
|
||||
import * as API from './types';
|
||||
|
||||
/** 用户信息 GET /user/info */
|
||||
export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) {
|
||||
return request<API.InfoUsingGetResponse>('/user/info', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
18
mp-sc-frontend/src/service/listAll.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
import request from '@/http/vue-query';
|
||||
import { CustomRequestOptions_ } from '@/http/types';
|
||||
|
||||
import * as API from './types';
|
||||
|
||||
/** 用户列表 GET /user/listAll */
|
||||
export function listAllUsingGet({
|
||||
options,
|
||||
}: {
|
||||
options?: CustomRequestOptions_;
|
||||
}) {
|
||||
return request<API.ListAllUsingGetResponse>('/user/listAll', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
29
mp-sc-frontend/src/service/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
|
||||
export type InfoUsingGetResponse = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: UserItem;
|
||||
};
|
||||
|
||||
export type InfoUsingGetResponses = {
|
||||
200: InfoUsingGetResponse;
|
||||
};
|
||||
|
||||
export type ListAllUsingGetResponse = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: UserItem[];
|
||||
};
|
||||
|
||||
export type ListAllUsingGetResponses = {
|
||||
200: ListAllUsingGetResponse;
|
||||
};
|
||||
|
||||
export type UserItem = {
|
||||
userId: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
};
|
||||
BIN
mp-sc-frontend/src/static/app/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
mp-sc-frontend/src/static/app/icons/120x120.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
mp-sc-frontend/src/static/app/icons/144x144.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
mp-sc-frontend/src/static/app/icons/152x152.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
mp-sc-frontend/src/static/app/icons/167x167.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
mp-sc-frontend/src/static/app/icons/180x180.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
mp-sc-frontend/src/static/app/icons/192x192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
mp-sc-frontend/src/static/app/icons/20x20.png
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
mp-sc-frontend/src/static/app/icons/29x29.png
Normal file
|
After Width: | Height: | Size: 780 B |
BIN
mp-sc-frontend/src/static/app/icons/40x40.png
Normal file
|
After Width: | Height: | Size: 985 B |
BIN
mp-sc-frontend/src/static/app/icons/58x58.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
mp-sc-frontend/src/static/app/icons/60x60.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
mp-sc-frontend/src/static/app/icons/72x72.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
mp-sc-frontend/src/static/app/icons/76x76.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
mp-sc-frontend/src/static/app/icons/80x80.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
mp-sc-frontend/src/static/app/icons/87x87.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
mp-sc-frontend/src/static/app/icons/96x96.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |