🎉 initial commit
This commit is contained in:
3
.commitlintrc.cjs
Normal file
3
.commitlintrc.cjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
}
|
||||||
51
.cursor/rules/api-http-patterns.mdc
Normal file
51
.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
|
||||||
|
---
|
||||||
41
.cursor/rules/development-workflow.mdc
Normal file
41
.cursor/rules/development-workflow.mdc
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 开发工作流程
|
||||||
|
|
||||||
|
## 项目启动
|
||||||
|
1. 安装依赖:`pnpm install`
|
||||||
|
2. 开发环境:
|
||||||
|
- H5: `pnpm dev` 或 `pnpm dev:h5`
|
||||||
|
- 微信小程序: `pnpm dev:mp`
|
||||||
|
- APP: `pnpm dev:app`
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
- 使用 ESLint 进行代码检查:`pnpm lint`
|
||||||
|
- 自动修复代码格式:`pnpm lint:fix`
|
||||||
|
- 使用 eslint 格式化代码
|
||||||
|
- 遵循 TypeScript 严格模式
|
||||||
|
|
||||||
|
## 构建和部署
|
||||||
|
- H5 构建:`pnpm build:h5`
|
||||||
|
- 小程序构建:`pnpm build:mp`
|
||||||
|
- APP 构建:`pnpm build:app`
|
||||||
|
- 类型检查:`pnpm type-check`
|
||||||
|
|
||||||
|
## 开发工具
|
||||||
|
- 推荐使用 VSCode 编辑器
|
||||||
|
- 安装 Vue 和 TypeScript 相关插件
|
||||||
|
- 使用 uni-app 开发者工具调试小程序
|
||||||
|
- 使用 HBuilderX 调试 APP
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
- 使用 console.log 和 uni.showToast 调试
|
||||||
|
- 利用 Vue DevTools 调试组件状态
|
||||||
|
- 使用网络面板调试 API 请求
|
||||||
|
- 平台差异测试和兼容性检查
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
- 使用懒加载和代码分割
|
||||||
|
- 优化图片和静态资源
|
||||||
|
- 减少不必要的重渲染
|
||||||
|
- 合理使用缓存策略
|
||||||
|
---
|
||||||
|
description: 开发工作流程和最佳实践指南
|
||||||
|
---
|
||||||
34
.cursor/rules/project-overview.mdc
Normal file
34
.cursor/rules/project-overview.mdc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# unibest 项目概览
|
||||||
|
|
||||||
|
这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
|
||||||
|
|
||||||
|
## 项目特点
|
||||||
|
- 支持 H5、小程序、APP 多平台开发
|
||||||
|
- 使用最新的前端技术栈
|
||||||
|
- 内置约定式路由、layout布局、请求封装等功能
|
||||||
|
- 无需依赖 HBuilderX,支持命令行开发
|
||||||
|
|
||||||
|
## 核心配置文件
|
||||||
|
- [package.json](mdc:package.json) - 项目依赖和脚本配置
|
||||||
|
- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
|
||||||
|
- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
|
||||||
|
- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
|
||||||
|
- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
|
||||||
|
|
||||||
|
## 主要目录结构
|
||||||
|
- `src/pages/` - 页面文件
|
||||||
|
- `src/components/` - 组件文件
|
||||||
|
- `src/layouts/` - 布局文件
|
||||||
|
- `src/api/` - API 接口
|
||||||
|
- `src/http/` - HTTP 请求封装
|
||||||
|
- `src/store/` - 状态管理
|
||||||
|
- `src/tabbar/` - 底部导航栏
|
||||||
|
|
||||||
|
## 开发命令
|
||||||
|
- `pnpm dev` - 开发 H5 版本
|
||||||
|
- `pnpm dev:mp` - 开发微信小程序
|
||||||
|
- `pnpm dev:app` - 开发 APP 版本
|
||||||
|
- `pnpm build` - 构建生产版本
|
||||||
54
.cursor/rules/styling-css-patterns.mdc
Normal file
54
.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
|
||||||
|
---
|
||||||
62
.cursor/rules/uni-app-patterns.mdc
Normal file
62
.cursor/rules/uni-app-patterns.mdc
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# uni-app 开发规范
|
||||||
|
|
||||||
|
## 页面开发
|
||||||
|
- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
|
||||||
|
- 使用约定式路由,文件名即路由路径
|
||||||
|
- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||||
|
|
||||||
|
## 组件开发
|
||||||
|
- 组件文件放在 [src/components/](mdc:src/components/) 目录下
|
||||||
|
- 使用 uni-app 内置组件和第三方组件库
|
||||||
|
- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||||
|
- 自定义组件遵循 uni-app 组件规范
|
||||||
|
|
||||||
|
## 平台适配
|
||||||
|
- 使用条件编译处理平台差异
|
||||||
|
- 支持 H5、小程序、APP 多平台
|
||||||
|
- 注意各平台的 API 差异
|
||||||
|
- 使用 uni.xxx API 替代原生 API
|
||||||
|
|
||||||
|
## 示例代码结构
|
||||||
|
```vue
|
||||||
|
<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
|
||||||
|
---
|
||||||
52
.cursor/rules/vue-typescript-patterns.mdc
Normal file
52
.cursor/rules/vue-typescript-patterns.mdc
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Vue3 + TypeScript 开发规范
|
||||||
|
|
||||||
|
## Vue 组件规范
|
||||||
|
- 使用 Composition API 和 `<script setup>` 语法
|
||||||
|
- 组件文件使用 PascalCase 命名
|
||||||
|
- 页面文件放在 `src/pages/` 目录下
|
||||||
|
- 组件文件放在 `src/components/` 目录下
|
||||||
|
|
||||||
|
## Vue SFC 组件规范
|
||||||
|
- `<script setup>` 标签必须是第一个子元素
|
||||||
|
- `<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
.editorconfig
Normal file
13
.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 # 关闭末尾空格修剪
|
||||||
31
.github/release.yml
vendored
Normal file
31
.github/release.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
categories:
|
||||||
|
- title: 🚀 新功能
|
||||||
|
labels: [feat, feature]
|
||||||
|
- title: 🛠️ 修复
|
||||||
|
labels: [fix, bugfix]
|
||||||
|
- title: 💅 样式
|
||||||
|
labels: [style]
|
||||||
|
- title: 📄 文档
|
||||||
|
labels: [docs]
|
||||||
|
- title: ⚡️ 性能
|
||||||
|
labels: [perf]
|
||||||
|
- title: 🧪 测试
|
||||||
|
labels: [test]
|
||||||
|
- title: ♻️ 重构
|
||||||
|
labels: [refactor]
|
||||||
|
- title: 📦 构建
|
||||||
|
labels: [build]
|
||||||
|
- title: 🚨 补丁
|
||||||
|
labels: [patch, hotfix]
|
||||||
|
- title: 🌐 发布
|
||||||
|
labels: [release, publish]
|
||||||
|
- title: 🔧 流程
|
||||||
|
labels: [ci, cd, workflow]
|
||||||
|
- title: ⚙️ 配置
|
||||||
|
labels: [config, chore]
|
||||||
|
- title: 📁 文件
|
||||||
|
labels: [file]
|
||||||
|
- title: 🎨 格式化
|
||||||
|
labels: [format]
|
||||||
|
- title: 🔀 其他
|
||||||
|
labels: [other, misc]
|
||||||
188
.github/workflows/auto-merge.yml
vendored
Normal file
188
.github/workflows/auto-merge.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
name: Auto Merge Main to Other Branches
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch: # 手动触发
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# merge-to-release:
|
||||||
|
# name: Merge main into release
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - name: Checkout repository
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# fetch-depth: 0
|
||||||
|
# token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
# - name: Merge main into release
|
||||||
|
# run: |
|
||||||
|
# git config user.name "GitHub Actions"
|
||||||
|
# git config user.email "actions@github.com"
|
||||||
|
# git checkout release
|
||||||
|
# git merge main --no-ff -m "Auto merge main into release"
|
||||||
|
# git push origin release
|
||||||
|
|
||||||
|
merge-to-i18n:
|
||||||
|
name: Merge main into i18n
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into i18n
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout i18n
|
||||||
|
git merge main --no-ff -m "Auto merge main into i18n"
|
||||||
|
git push origin i18n
|
||||||
|
|
||||||
|
merge-to-base-sard-ui:
|
||||||
|
name: Merge main into base-sard-ui
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into base-sard-ui
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout base-sard-ui
|
||||||
|
git merge main --no-ff -m "Auto merge main into base-sard-ui"
|
||||||
|
git push origin base-sard-ui
|
||||||
|
|
||||||
|
merge-to-base-uv-ui:
|
||||||
|
name: Merge main into base-uv-ui
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into base-uv-ui
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout base-uv-ui
|
||||||
|
git merge main --no-ff -m "Auto merge main into base-uv-ui"
|
||||||
|
git push origin base-uv-ui
|
||||||
|
|
||||||
|
merge-to-base-uview-pro:
|
||||||
|
name: Merge main into base-uview-pro
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into base-uview-pro
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout base-uview-pro
|
||||||
|
git merge main --no-ff -m "Auto merge main into base-uview-pro"
|
||||||
|
git push origin base-uview-pro
|
||||||
|
|
||||||
|
merge-to-base-uview-plus:
|
||||||
|
name: Merge main into base-uview-plus
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into base-uview-plus
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout base-uview-plus
|
||||||
|
git merge main --no-ff -m "Auto merge main into base-uview-plus"
|
||||||
|
git push origin base-uview-plus
|
||||||
|
|
||||||
|
# merge-to-base-tm-ui:
|
||||||
|
# name: Merge main into base-tm-ui
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - name: Checkout repository
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# fetch-depth: 0
|
||||||
|
# token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
# - name: Merge main into base-tm-ui
|
||||||
|
# run: |
|
||||||
|
# git config user.name "GitHub Actions"
|
||||||
|
# git config user.email "actions@github.com"
|
||||||
|
# git checkout base-tm-ui
|
||||||
|
# git merge main --no-ff -m "Auto merge main into base-tm-ui"
|
||||||
|
# git push origin base-tm-ui
|
||||||
|
|
||||||
|
merge-to-base-skiyee-ui:
|
||||||
|
name: Merge main into base-skiyee-ui
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into base-skiyee-ui
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout base-skiyee-ui
|
||||||
|
git merge main --no-ff -m "Auto merge main into base-skiyee-ui"
|
||||||
|
git push origin base-skiyee-ui
|
||||||
|
|
||||||
|
merge-to-main-v4:
|
||||||
|
name: Merge main into main-v4
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into main-v4
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout main-v4
|
||||||
|
git merge main --no-ff -m "Auto merge main into main-v4"
|
||||||
|
git push origin main-v4
|
||||||
|
|
||||||
|
merge-to-i18n-v4:
|
||||||
|
name: Merge main into i18n-v4
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
|
||||||
|
|
||||||
|
- name: Merge main into i18n-v4
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout i18n-v4
|
||||||
|
git merge main --no-ff -m "Auto merge main into i18n-v4"
|
||||||
|
git push origin i18n-v4
|
||||||
119
.github/workflows/release-log.yml
vendored
Normal file
119
.github/workflows/release-log.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
name: Auto Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: read
|
||||||
|
issues: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install yq
|
||||||
|
run: sudo snap install yq
|
||||||
|
|
||||||
|
- name: Generate changelog
|
||||||
|
id: changelog
|
||||||
|
env:
|
||||||
|
CONFIG_FILE: .github/release.yml
|
||||||
|
run: |
|
||||||
|
# 解析配置文件
|
||||||
|
declare -A category_map
|
||||||
|
while IFS=";" read -r title labels; do
|
||||||
|
for label in $labels; do
|
||||||
|
category_map[$label]="$title"
|
||||||
|
done
|
||||||
|
done < <(yq -o=tsv '.categories[] | [.title, (.labels | join(" "))] | join(";")' $CONFIG_FILE)
|
||||||
|
# 获取版本范围
|
||||||
|
mapfile -t tags < <(git tag -l --sort=-version:refname)
|
||||||
|
current_tag=${tags[0]}
|
||||||
|
previous_tag=${tags[1]:-}
|
||||||
|
if [[ -z "$previous_tag" ]]; then
|
||||||
|
commit_range="$current_tag"
|
||||||
|
echo "首次发布版本: $current_tag"
|
||||||
|
else
|
||||||
|
commit_range="$previous_tag..$current_tag"
|
||||||
|
echo "版本范围: $commit_range"
|
||||||
|
fi
|
||||||
|
# 获取所有符合规范的提交
|
||||||
|
commits=$(git log --pretty=format:"%s|%h" "$commit_range")
|
||||||
|
# 生成分类日志
|
||||||
|
declare -A log_entries
|
||||||
|
while IFS="|" read -r subject hash; do
|
||||||
|
# type=$(echo "$subject" | cut -d':' -f1 | tr -d ' ')
|
||||||
|
type=$(echo "$subject" | sed -E 's/^([[:alnum:]]+)(\(.*\))?:.*/\1/' | tr -d ' ')
|
||||||
|
found=0
|
||||||
|
for label in "${!category_map[@]}"; do
|
||||||
|
if [[ "$type" == "$label" ]]; then
|
||||||
|
entry="- ${subject} (${hash:0:7})"
|
||||||
|
log_entries[${category_map[$label]}]+="$entry"$'\n'
|
||||||
|
found=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ $found -eq 0 ]]; then
|
||||||
|
entry="- ${subject} (${hash:0:7})"
|
||||||
|
log_entries["其他"]+="$entry"$'\n'
|
||||||
|
fi
|
||||||
|
done <<< "$commits"
|
||||||
|
|
||||||
|
# 统计提交数量
|
||||||
|
commit_count=$(git log --oneline "$commit_range" | wc -l)
|
||||||
|
# 统计受影响的文件数量
|
||||||
|
file_count=$(git diff --name-only "$commit_range" | wc -l)
|
||||||
|
# 统计贡献者信息
|
||||||
|
contributor_stats=$(git shortlog -sn "$commit_range")
|
||||||
|
contributor_notes=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
commits=$(echo "$line" | awk '{print $1}')
|
||||||
|
name=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //')
|
||||||
|
contributor_notes+="- @${name} (${commits} commits)\n"
|
||||||
|
done <<< "$contributor_stats"
|
||||||
|
# 构建输出内容
|
||||||
|
release_notes="## 版本更新日志 ($current_tag)\n\n"
|
||||||
|
while IFS= read -r category; do
|
||||||
|
if [[ -n "${log_entries[$category]}" ]]; then
|
||||||
|
release_notes+="### $category\n${log_entries[$category]}\n"
|
||||||
|
fi
|
||||||
|
done < <(yq '.categories[].title' $CONFIG_FILE)
|
||||||
|
# 构建输出内容
|
||||||
|
release_notes="## 版本更新日志 ($current_tag)\n\n"
|
||||||
|
current_date=$(date +"%Y-%m-%d")
|
||||||
|
# 添加发布日期和下载统计信息
|
||||||
|
release_notes+=" ### 📅 发布日期: ${current_date}\n"
|
||||||
|
while IFS= read -r category; do
|
||||||
|
if [[ -n "${log_entries[$category]}" ]]; then
|
||||||
|
release_notes+="### $category\n${log_entries[$category]}\n"
|
||||||
|
fi
|
||||||
|
done < <(yq '.categories[].title' $CONFIG_FILE)
|
||||||
|
|
||||||
|
# 添加统计信息
|
||||||
|
release_notes+="### 📊 统计信息\n"
|
||||||
|
release_notes+="- 本次发布包含 ${commit_count} 个提交\n"
|
||||||
|
release_notes+="- 影响 ${file_count} 个文件\n\n"
|
||||||
|
# 添加贡献者信息
|
||||||
|
release_notes+="### 👥 贡献者\n"
|
||||||
|
release_notes+="感谢这些优秀的贡献者(按提交次数排序):\n"
|
||||||
|
release_notes+="${contributor_notes}\n"
|
||||||
|
release_notes+="---\n"
|
||||||
|
# 写入文件
|
||||||
|
echo -e "$release_notes" > changelog.md
|
||||||
|
echo "生成日志内容:"
|
||||||
|
cat changelog.md
|
||||||
|
- name: Create Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
generateReleaseNotes: false
|
||||||
|
bodyFile: changelog.md
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.hbuilderx
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
docs/.vitepress/dist
|
||||||
|
docs/.vitepress/cache
|
||||||
|
|
||||||
|
src/types
|
||||||
|
src/manifest.json
|
||||||
|
src/pages.json
|
||||||
|
|
||||||
|
# lock 文件还是不要了,我主要的版本写死就好了
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
|
||||||
|
# `git rm -r --cached .` 然后提交 commit 即可。
|
||||||
|
|
||||||
|
# git rm -r --cached file1 file2 ## 针对某些文件
|
||||||
|
# git rm -r --cached dir1 dir2 ## 针对某些文件夹
|
||||||
|
# git rm -r --cached . ## 针对所有文件
|
||||||
|
|
||||||
|
# 更新 uni-app 官方版本
|
||||||
|
# npx @dcloudio/uvm@latest
|
||||||
1
.husky.back/commit-msg
Normal file
1
.husky.back/commit-msg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx --no-install commitlint --edit "$1"
|
||||||
1
.husky.back/pre-commit
Normal file
1
.husky.back/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx lint-staged --allow-empty
|
||||||
8
.npmrc
Normal file
8
.npmrc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# registry = https://registry.npmjs.org
|
||||||
|
registry = https://registry.npmmirror.com
|
||||||
|
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
auto-install-peers=true
|
||||||
|
shamefully-hoist=true
|
||||||
|
ignore-workspace-root-check=true
|
||||||
|
install-workspace-root=true
|
||||||
118
.trae/rules/project_rules.md
Normal file
118
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# unibest 项目概览
|
||||||
|
|
||||||
|
这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
|
||||||
|
|
||||||
|
## 项目特点
|
||||||
|
- 支持 H5、小程序、APP 多平台开发
|
||||||
|
- 使用最新的前端技术栈
|
||||||
|
- 内置约定式路由、layout布局、请求封装等功能
|
||||||
|
- 无需依赖 HBuilderX,支持命令行开发
|
||||||
|
|
||||||
|
## 核心配置文件
|
||||||
|
- [package.json](mdc:package.json) - 项目依赖和脚本配置
|
||||||
|
- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
|
||||||
|
- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
|
||||||
|
- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
|
||||||
|
- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
|
||||||
|
|
||||||
|
## 主要目录结构
|
||||||
|
- `src/pages/` - 页面文件
|
||||||
|
- `src/components/` - 组件文件
|
||||||
|
- `src/layouts/` - 布局文件
|
||||||
|
- `src/api/` - API 接口
|
||||||
|
- `src/http/` - HTTP 请求封装
|
||||||
|
- `src/store/` - 状态管理
|
||||||
|
- `src/tabbar/` - 底部导航栏
|
||||||
|
|
||||||
|
## 开发命令
|
||||||
|
- `pnpm dev` - 开发 H5 版本
|
||||||
|
- `pnpm dev:mp` - 开发微信小程序
|
||||||
|
- `pnpm dev:app` - 开发 APP 版本
|
||||||
|
- `pnpm build` - 构建生产版本
|
||||||
|
|
||||||
|
## Vue 组件规范
|
||||||
|
- 使用 Composition API 和 `<script setup>` 语法
|
||||||
|
- 组件文件使用 PascalCase 命名
|
||||||
|
- 页面文件放在 `src/pages/` 目录下
|
||||||
|
- 组件文件放在 `src/components/` 目录下
|
||||||
|
|
||||||
|
## TypeScript 规范
|
||||||
|
- 严格使用 TypeScript,避免使用 `any` 类型
|
||||||
|
- 为 API 响应数据定义接口类型
|
||||||
|
- 使用 `interface` 定义对象类型,`type` 定义联合类型
|
||||||
|
- 导入类型时使用 `import type` 语法
|
||||||
|
|
||||||
|
## 状态管理
|
||||||
|
- 使用 Pinia 进行状态管理
|
||||||
|
- Store 文件放在 `src/store/` 目录下
|
||||||
|
- 使用 `defineStore` 定义 store
|
||||||
|
- 支持持久化存储
|
||||||
|
|
||||||
|
## UnoCSS 原子化 CSS
|
||||||
|
- 项目使用 UnoCSS 作为原子化 CSS 框架
|
||||||
|
- 配置在 [uno.config.ts](mdc:uno.config.ts)
|
||||||
|
- 支持预设和自定义规则
|
||||||
|
- 优先使用原子化类名,减少自定义 CSS
|
||||||
|
|
||||||
|
## Vue SFC 组件规范
|
||||||
|
- `<script setup>` 标签必须是第一个子元素
|
||||||
|
- `<template>` 标签必须是第二个子元素
|
||||||
|
- `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
|
||||||
|
|
||||||
|
## 页面开发
|
||||||
|
- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
|
||||||
|
- 使用约定式路由,文件名即路由路径
|
||||||
|
- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
|
||||||
|
|
||||||
|
## 组件开发
|
||||||
|
- 组件文件放在 [src/components/](mdc:src/components/) 目录下
|
||||||
|
- 使用 uni-app 内置组件和第三方组件库
|
||||||
|
- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
|
||||||
|
- 自定义组件遵循 uni-app 组件规范
|
||||||
|
|
||||||
|
## 平台适配
|
||||||
|
- 使用条件编译处理平台差异
|
||||||
|
- 支持 H5、小程序、APP 多平台
|
||||||
|
- 注意各平台的 API 差异
|
||||||
|
- 使用 uni.xxx API 替代原生 API
|
||||||
|
|
||||||
|
## 示例代码结构
|
||||||
|
```vue
|
||||||
|
<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 规范
|
||||||
|
- 注意页面栈和导航管理
|
||||||
19
.vscode/extensions.json
vendored
Normal file
19
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"vue.volar",
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"antfu.unocss",
|
||||||
|
"antfu.iconify",
|
||||||
|
"evils.uniapp-vscode",
|
||||||
|
"uni-helper.uni-helper-vscode",
|
||||||
|
"uni-helper.uni-app-schemas-vscode",
|
||||||
|
"uni-helper.uni-highlight-vscode",
|
||||||
|
"uni-helper.uni-ui-snippets-vscode",
|
||||||
|
"uni-helper.uni-app-snippets-vscode",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"foxundermoon.shell-format",
|
||||||
|
"christian-kohler.path-intellisense"
|
||||||
|
]
|
||||||
|
}
|
||||||
95
.vscode/settings.json
vendored
Normal file
95
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
// 配置语言的文件关联
|
||||||
|
"files.associations": {
|
||||||
|
"pages.json": "jsonc", // pages.json 可以写注释
|
||||||
|
"manifest.json": "jsonc" // manifest.json 可以写注释
|
||||||
|
},
|
||||||
|
|
||||||
|
"stylelint.enable": false, // 禁用 stylelint
|
||||||
|
"css.validate": false, // 禁用 CSS 内置验证
|
||||||
|
"scss.validate": false, // 禁用 SCSS 内置验证
|
||||||
|
"less.validate": false, // 禁用 LESS 内置验证
|
||||||
|
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
|
"explorer.fileNesting.enabled": true,
|
||||||
|
"explorer.fileNesting.expand": false,
|
||||||
|
"explorer.fileNesting.patterns": {
|
||||||
|
"README.md": "index.html,favicon.ico,robots.txt,CHANGELOG.md",
|
||||||
|
"pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts",
|
||||||
|
"package.json": "tsconfig.json,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
|
||||||
|
"eslint.config.mjs": ".commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"svelte",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
],
|
||||||
|
"cSpell.words": [
|
||||||
|
"alova",
|
||||||
|
"Aplipay",
|
||||||
|
"attributify",
|
||||||
|
"chooseavatar",
|
||||||
|
"climblee",
|
||||||
|
"commitlint",
|
||||||
|
"dcloudio",
|
||||||
|
"iconfont",
|
||||||
|
"oxlint",
|
||||||
|
"qrcode",
|
||||||
|
"refresherrefresh",
|
||||||
|
"scrolltolower",
|
||||||
|
"tabbar",
|
||||||
|
"Toutiao",
|
||||||
|
"uniapp",
|
||||||
|
"unibest",
|
||||||
|
"unocss",
|
||||||
|
"uview",
|
||||||
|
"uvui",
|
||||||
|
"Wechat",
|
||||||
|
"WechatMiniprogram",
|
||||||
|
"Weixin"
|
||||||
|
]
|
||||||
|
}
|
||||||
77
.vscode/vue3.code-snippets
vendored
Normal file
77
.vscode/vue3.code-snippets
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
// Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||||
|
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||||
|
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||||
|
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||||
|
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||||
|
// Placeholders with the same ids are connected.
|
||||||
|
// Example:
|
||||||
|
// "Print to console": {
|
||||||
|
// "scope": "javascript,typescript",
|
||||||
|
// "prefix": "log",
|
||||||
|
// "body": [
|
||||||
|
// "console.log('$1');",
|
||||||
|
// "$2"
|
||||||
|
// ],
|
||||||
|
// "description": "Log output to console"
|
||||||
|
// }
|
||||||
|
"Print unibest Vue3 SFC": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "v3",
|
||||||
|
"body": [
|
||||||
|
"<script lang=\"ts\" setup>",
|
||||||
|
"definePage({",
|
||||||
|
" style: {",
|
||||||
|
" navigationBarTitleText: '$1',",
|
||||||
|
" },",
|
||||||
|
"})",
|
||||||
|
"</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": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "sc",
|
||||||
|
"body": [
|
||||||
|
"<script lang=\"ts\" setup>",
|
||||||
|
"//$1",
|
||||||
|
"</script>\n"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Print unibest script with definePage": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "scdp",
|
||||||
|
"body": [
|
||||||
|
"<script lang=\"ts\" setup>",
|
||||||
|
"definePage({",
|
||||||
|
" style: {",
|
||||||
|
" navigationBarTitleText: '$1',",
|
||||||
|
" },",
|
||||||
|
"})",
|
||||||
|
"</script>\n"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Print unibest template": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "te",
|
||||||
|
"body": [
|
||||||
|
"<template>",
|
||||||
|
" <view class=\"\">$1</view>",
|
||||||
|
"</template>\n"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
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.
|
||||||
93
README.md
Normal file
93
README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<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://feige996.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
|
||||||
|
|
||||||
|
## 📂 快速开始
|
||||||
|
|
||||||
|
执行 `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>
|
||||||
3
codes/README.md
Normal file
3
codes/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 参考代码
|
||||||
|
|
||||||
|
部分代码片段,供参考。
|
||||||
222
codes/router.txt
Normal file
222
codes/router.txt
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { getCurrentInstance, type App } from 'vue'
|
||||||
|
import { useUserLoginStore } from '@/store/login'
|
||||||
|
import { Pages } from './pages'
|
||||||
|
import { LoginPopupViewer } from './loginPopupServices'
|
||||||
|
import Loading from './Loading'
|
||||||
|
|
||||||
|
/** 实时判断用户是否已登录(避免 computed 缓存) */
|
||||||
|
function isUserLoggedIn(): boolean {
|
||||||
|
return useUserLoginStore().isLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由相关配置
|
||||||
|
// 这里可以根据实际情况调整
|
||||||
|
// 例如:需要登录验证的页面等
|
||||||
|
// 以及登录页面、会员中心页面等
|
||||||
|
|
||||||
|
// 需要登录验证的页面
|
||||||
|
const authPages = [
|
||||||
|
Pages.USER_INFO_EDIT,
|
||||||
|
Pages.VIP_CENTER,
|
||||||
|
//Pages.PRODUCT_LIST,
|
||||||
|
//Pages.PRODUCT_DETAILS,
|
||||||
|
Pages.USER_ACCOUNT_SECURITY,
|
||||||
|
Pages.USER_EDIT_NICKNAME,
|
||||||
|
Pages.USER_ORDER_LIST,
|
||||||
|
Pages.USER_ORDER_DETAILS,
|
||||||
|
Pages.USER_MOBILE,
|
||||||
|
Pages.DISTRIBUTION_CENTER,
|
||||||
|
Pages.DISTRIBUTION_CENTER_DETAILS,
|
||||||
|
Pages.USER_MOBILE_CHANGE,
|
||||||
|
Pages.USER_PERSONAL_INFO,
|
||||||
|
Pages.USER_REMARK,
|
||||||
|
Pages.PRODUCT_ORDER_CONFIRM,
|
||||||
|
Pages.PRODUCT_PAY_MODE,
|
||||||
|
Pages.COUPON_CENTER,
|
||||||
|
Pages.COUPON_LIST,
|
||||||
|
Pages.CUSTOMER_SERVICE,
|
||||||
|
Pages.SHIPPING_ADDRESS_ADDED_OR_EDIT,
|
||||||
|
Pages.SHIPPING_ADDRESS_LIST,
|
||||||
|
Pages.USER_PASSWORD_CONFIG,
|
||||||
|
Pages.WITHDRAWAL,
|
||||||
|
Pages.WITHDRAWAL_RECORD_LIST,
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 判断是否需要登录 */
|
||||||
|
function getBasePath(url: string): string {
|
||||||
|
const index = url.indexOf('?')
|
||||||
|
return index !== -1 ? url.substring(0, index) : url
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthRequired(url: string): boolean {
|
||||||
|
const cleanUrl = getBasePath(url)
|
||||||
|
console.log(`URL数据源:${authPages}`)
|
||||||
|
console.log(`URL原始值: ${url}`)
|
||||||
|
console.log(`URL过滤值: ${cleanUrl}`)
|
||||||
|
return authPages.some((item) => item === cleanUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 缓存跳转路径 */
|
||||||
|
function cacheRedirect(url: string) {
|
||||||
|
uni.setStorageSync('pending_redirect', url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取并清除缓存跳转路径 */
|
||||||
|
function consumeRedirect(): string | null {
|
||||||
|
const url = uni.getStorageSync('pending_redirect')
|
||||||
|
uni.removeStorageSync('pending_redirect')
|
||||||
|
return url || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 路由核心跳转方法 */
|
||||||
|
async function internalNavigate(
|
||||||
|
type: 'navigateTo' | 'redirectTo' | 'switchTab' | 'reLaunch',
|
||||||
|
url: string,
|
||||||
|
options: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
const originUrl: string = url.startsWith('/') ? url : `/${url}`
|
||||||
|
const isAuthPage = isAuthRequired(originUrl)
|
||||||
|
console.log(`[Router][${type}] 跳转到:`, originUrl, '需要登录:', isAuthPage)
|
||||||
|
console.log(`[Router][${type}] 是否登录:`, isUserLoggedIn)
|
||||||
|
|
||||||
|
// 如果需要登录但未登录,则弹出登录框
|
||||||
|
if (isAuthPage && !isUserLoggedIn()) {
|
||||||
|
cacheRedirect(originUrl)
|
||||||
|
const loginResult = await LoginPopupViewer.open()
|
||||||
|
console.log(`[Router][${type}] 登录弹窗结果:`, loginResult)
|
||||||
|
|
||||||
|
// 如果登录失败(或用户取消),中断跳转
|
||||||
|
if (!loginResult) {
|
||||||
|
console.log(`[Router][${type}] 已终止跳转,原因:用户未登录或取消登录`)
|
||||||
|
Loading.showError({ msg: '已取消登录' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录状态已满足,可以安全跳转
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'navigateTo':
|
||||||
|
return await uniNavigateTo(originUrl, options)
|
||||||
|
case 'redirectTo':
|
||||||
|
return await uniRedirectTo(originUrl, options)
|
||||||
|
case 'switchTab':
|
||||||
|
return await uniSwitchTab(originUrl)
|
||||||
|
case 'reLaunch':
|
||||||
|
return await uniReLaunch(originUrl)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Router][${type}] 跳转失败:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ✅ Promise 封装 uni API **/
|
||||||
|
function uniNavigateTo(url: string, options: any) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url,
|
||||||
|
...options,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function uniRedirectTo(url: string, options: any) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url,
|
||||||
|
...options,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function uniSwitchTab(url: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.switchTab({
|
||||||
|
url,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function uniReLaunch(url: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Router API 对象
|
||||||
|
// ✅ Router API 对象
|
||||||
|
export const Router = {
|
||||||
|
// 页面跳转,支持登录鉴权
|
||||||
|
async navigateTo(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
|
||||||
|
return await internalNavigate('navigateTo', opt.url, opt)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 页面重定向,支持登录鉴权
|
||||||
|
async redirectTo(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
|
||||||
|
return await internalNavigate('redirectTo', opt.url, opt)
|
||||||
|
},
|
||||||
|
|
||||||
|
// tab 页面切换
|
||||||
|
async switchTab(opt: { url: string }) {
|
||||||
|
return await internalNavigate('switchTab', opt.url, opt)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重新启动应用跳转
|
||||||
|
async reLaunch(opt: { url: string }) {
|
||||||
|
return await internalNavigate('reLaunch', opt.url, opt)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重定向别名
|
||||||
|
async replace(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
|
||||||
|
return await internalNavigate('redirectTo', opt.url, opt)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 返回上一级
|
||||||
|
async back(delta = 1) {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
uni.navigateBack({
|
||||||
|
delta,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
consumeRedirect,
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedRouter: typeof Router | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ 全局安全获取 $Router 实例(推荐使用)
|
||||||
|
*/
|
||||||
|
export function useRouter(): typeof Router {
|
||||||
|
if (cachedRouter) return cachedRouter
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error('useRouter() 必须在 setup() 或生命周期中调用')
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = instance.appContext.config.globalProperties.$Router
|
||||||
|
if (!router) {
|
||||||
|
throw new Error('$Router 尚未注入,请在 main.ts 中使用 app.use(RouterPlugin)')
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedRouter = router
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ✅ 注册为全局插件 */
|
||||||
|
export default {
|
||||||
|
install(app: App) {
|
||||||
|
app.config.globalProperties.$Router = Router
|
||||||
|
},
|
||||||
|
}
|
||||||
28
env/.env
vendored
Normal file
28
env/.env
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
VITE_APP_TITLE = 'unibest'
|
||||||
|
VITE_APP_PORT = 9000
|
||||||
|
|
||||||
|
VITE_UNI_APPID = '__UNI__D1E5001'
|
||||||
|
VITE_WX_APPID = 'wxa2abb91f64032a2b'
|
||||||
|
|
||||||
|
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
||||||
|
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
|
||||||
|
VITE_APP_PUBLIC_BASE=/
|
||||||
|
|
||||||
|
# 后台请求地址
|
||||||
|
VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
|
||||||
|
# 后台上传地址
|
||||||
|
VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload'
|
||||||
|
|
||||||
|
# 注意,如果是微信小程序,还有一套请求地址的配置,在 `src/utils/index.ts` 中
|
||||||
|
|
||||||
|
# h5是否需要配置代理
|
||||||
|
VITE_APP_PROXY_ENABLE = true
|
||||||
|
VITE_APP_PROXY_PREFIX = '/api'
|
||||||
|
# 后端是否有统一前缀 /api,决定本地代码的时候是否需要去掉 /api 前缀。这里面默认是没有的,即前端会把/api 转发去掉
|
||||||
|
VITE_SERVER_HAS_API_PREFIX = false
|
||||||
|
|
||||||
|
# 第二个请求地址 (目前alova中可以使用)
|
||||||
|
VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run'
|
||||||
|
|
||||||
|
# 认证模式,'single' | 'double' ==> 单token | 双token
|
||||||
|
VITE_AUTH_MODE = 'single'
|
||||||
9
env/.env.development
vendored
Normal file
9
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
|
||||||
|
|
||||||
|
# 后台请求地址
|
||||||
|
# VITE_SERVER_BASEURL = 'https://dev.xxx.com'
|
||||||
9
env/.env.production
vendored
Normal file
9
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
|
||||||
|
|
||||||
|
# 后台请求地址
|
||||||
|
# VITE_SERVER_BASEURL = 'https://prod.xxx.com'
|
||||||
9
env/.env.test
vendored
Normal file
9
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
|
||||||
|
|
||||||
|
# 后台请求地址
|
||||||
|
# VITE_SERVER_BASEURL = 'https://test.xxx.com'
|
||||||
55
eslint.config.mjs
Normal file
55
eslint.config.mjs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import uniHelper from '@uni-helper/eslint-config'
|
||||||
|
|
||||||
|
export default uniHelper({
|
||||||
|
unocss: true,
|
||||||
|
vue: true,
|
||||||
|
markdown: false,
|
||||||
|
ignores: [
|
||||||
|
'src/uni_modules/',
|
||||||
|
'dist',
|
||||||
|
// unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
|
||||||
|
'auto-import.d.ts',
|
||||||
|
// vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
|
||||||
|
'uni-pages.d.ts',
|
||||||
|
// 插件生成的文件
|
||||||
|
'src/pages.json',
|
||||||
|
'src/manifest.json',
|
||||||
|
// 忽略自动生成文件
|
||||||
|
'src/service/**',
|
||||||
|
],
|
||||||
|
// https://eslint-config.antfu.me/rules
|
||||||
|
rules: {
|
||||||
|
'no-useless-return': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'vue/no-unused-refs': 'off',
|
||||||
|
'unused-imports/no-unused-vars': 'off',
|
||||||
|
'eslint-comments/no-unlimited-disable': 'off',
|
||||||
|
'jsdoc/check-param-names': 'off',
|
||||||
|
'jsdoc/require-returns-description': 'off',
|
||||||
|
'ts/no-empty-object-type': 'off',
|
||||||
|
'no-extend-native': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
externalIgnores: ['text'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// vue SFC 调换顺序改这里
|
||||||
|
'vue/block-order': ['error', {
|
||||||
|
order: [['script', 'template'], 'style'],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
formatters: {
|
||||||
|
/**
|
||||||
|
* Format CSS, LESS, SCSS files, also the `<style>` blocks in Vue
|
||||||
|
* By default uses Prettier
|
||||||
|
*/
|
||||||
|
css: true,
|
||||||
|
/**
|
||||||
|
* Format HTML files
|
||||||
|
* By default uses Prettier
|
||||||
|
*/
|
||||||
|
html: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
26
index.html
Normal file
26
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>
|
||||||
149
manifest.config.ts
Normal file
149
manifest.config.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
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,
|
||||||
|
VITE_FALLBACK_LOCALE,
|
||||||
|
} = 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,
|
||||||
|
'locale': VITE_FALLBACK_LOCALE, // 'zh-Hans'
|
||||||
|
'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,
|
||||||
|
},
|
||||||
|
// styleIsolation: 'shared',
|
||||||
|
usingComponents: true,
|
||||||
|
// __usePrivacyCheck__: true,
|
||||||
|
},
|
||||||
|
'mp-alipay': {
|
||||||
|
usingComponents: true,
|
||||||
|
styleIsolation: 'shared',
|
||||||
|
},
|
||||||
|
'mp-baidu': {
|
||||||
|
usingComponents: true,
|
||||||
|
},
|
||||||
|
'mp-toutiao': {
|
||||||
|
usingComponents: true,
|
||||||
|
},
|
||||||
|
'uniStatistics': {
|
||||||
|
enable: false,
|
||||||
|
},
|
||||||
|
'vueVersion': '3',
|
||||||
|
})
|
||||||
13
openapi-ts-request.config.ts
Normal file
13
openapi-ts-request.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { GenerateServiceProps } from 'openapi-ts-request'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
|
||||||
|
serversPath: './src/service',
|
||||||
|
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions } from '@/http/types';`,
|
||||||
|
requestOptionsType: 'CustomRequestOptions',
|
||||||
|
isGenReactQuery: true,
|
||||||
|
reactQueryMode: 'vue',
|
||||||
|
isGenJavaScript: false,
|
||||||
|
},
|
||||||
|
] as GenerateServiceProps[]
|
||||||
180
package.json
Normal file
180
package.json
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"name": "zhuzi-uniapp",
|
||||||
|
"type": "module",
|
||||||
|
"version": "3.15.1",
|
||||||
|
"unibest-version": "3.15.1",
|
||||||
|
"update-time": "2025-09-11",
|
||||||
|
"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": ">=22",
|
||||||
|
"pnpm": ">=9"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"uvm": "npx @dcloudio/uvm@latest",
|
||||||
|
"uvm-rm": "node ./scripts/postupgrade.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": "node --experimental-loader ./scripts/window-path-loader.js node_modules/@dcloudio/vite-plugin-uni/bin/uni.js",
|
||||||
|
"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",
|
||||||
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"openapi-ts-request": "openapi-ts",
|
||||||
|
"prepare": "git init && husky && node ./scripts/create-base-files.js",
|
||||||
|
"lint": "eslint",
|
||||||
|
"lint:fix": "eslint --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@alova/adapter-uniapp": "^2.0.14",
|
||||||
|
"@alova/shared": "^1.3.1",
|
||||||
|
"@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",
|
||||||
|
"@tanstack/vue-query": "^5.62.16",
|
||||||
|
"abortcontroller-polyfill": "^1.7.8",
|
||||||
|
"alova": "^3.3.3",
|
||||||
|
"dayjs": "1.11.10",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"pinia": "2.0.36",
|
||||||
|
"pinia-plugin-persistedstate": "3.2.1",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"wot-design-uni": "^1.11.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",
|
||||||
|
"@esbuild/darwin-arm64": "0.20.2",
|
||||||
|
"@esbuild/darwin-x64": "0.20.2",
|
||||||
|
"@iconify-json/carbon": "^1.2.4",
|
||||||
|
"@rollup/rollup-darwin-x64": "^4.28.0",
|
||||||
|
"@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.0",
|
||||||
|
"@uni-helper/vite-plugin-uni-layouts": "0.1.11",
|
||||||
|
"@uni-helper/vite-plugin-uni-manifest": "^0.2.8",
|
||||||
|
"@uni-helper/vite-plugin-uni-pages": "^0.3.8",
|
||||||
|
"@uni-helper/vite-plugin-uni-platform": "^0.0.5",
|
||||||
|
"@uni-ku/bundle-optimizer": "^1.3.3",
|
||||||
|
"@uni-ku/root": "^1.3.4",
|
||||||
|
"@unocss/eslint-plugin": "^66.2.3",
|
||||||
|
"@unocss/preset-legacy-compat": "66.0.0",
|
||||||
|
"@vue/runtime-core": "^3.4.21",
|
||||||
|
"@vue/tsconfig": "^0.1.3",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"cross-env": "^10.0.0",
|
||||||
|
"eslint": "^9.31.0",
|
||||||
|
"eslint-plugin-format": "^1.0.1",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
|
"miniprogram-api-typings": "^4.1.0",
|
||||||
|
"openapi-ts-request": "^1.6.7",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"postcss-html": "^1.8.0",
|
||||||
|
"postcss-scss": "^4.0.9",
|
||||||
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
|
"sass": "1.77.8",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"unocss": "66.0.0",
|
||||||
|
"unplugin-auto-import": "^20.0.0",
|
||||||
|
"vite": "5.2.8",
|
||||||
|
"vite-plugin-restart": "^1.0.0",
|
||||||
|
"vue-tsc": "^3.0.6"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"bin-wrapper": "npm:bin-wrapper-china"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "eslint --fix"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
pages.config.ts
Normal file
25
pages.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { isH5 } from '@uni-helper/uni-env'
|
||||||
|
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
|
||||||
|
import { tabBar } from './src/tabbar/config'
|
||||||
|
|
||||||
|
export default defineUniPages({
|
||||||
|
globalStyle: {
|
||||||
|
navigationStyle: 'default',
|
||||||
|
navigationBarTitleText: 'unibest',
|
||||||
|
navigationBarBackgroundColor: '#f8f8f8',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
easycom: {
|
||||||
|
autoscan: true,
|
||||||
|
custom: {
|
||||||
|
'^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
|
||||||
|
'^wd-(.*)': 'wot-design-uni/components/wd-$1/wd-$1.vue',
|
||||||
|
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
|
||||||
|
'z-paging/components/z-paging$1/z-paging$1.vue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
|
||||||
|
// 无tabbar模式下,h5 设置为 {} 为了防止浏览器报错导致白屏
|
||||||
|
tabBar: tabBar || (isH5 ? {} : undefined) as any,
|
||||||
|
})
|
||||||
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
ignoredBuiltDependencies:
|
||||||
|
- '@uni-helper/unocss-preset-uni'
|
||||||
|
- core-js
|
||||||
|
- es5-ext
|
||||||
|
- esbuild
|
||||||
|
- vue-demi
|
||||||
30
scripts/create-base-files.js
Normal file
30
scripts/create-base-files.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// 生成 src/manifest.json 和 src/pages.json
|
||||||
|
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 = {
|
||||||
|
name: 'unibest',
|
||||||
|
description: 'unibest - 最好的 uniapp 开发模板',
|
||||||
|
versionName: '1.0.0',
|
||||||
|
versionCode: '100',
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
path: 'pages/index/index',
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: 'uni-app',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用修复后的 __dirname 来解析文件路径
|
||||||
|
fs.writeFileSync(path.resolve(__dirname, '../src/manifest.json'), JSON.stringify(manifest, null, 2))
|
||||||
|
fs.writeFileSync(path.resolve(__dirname, '../src/pages.json'), JSON.stringify(pages, null, 2))
|
||||||
101
scripts/postupgrade.js
Normal file
101
scripts/postupgrade.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// # 执行 `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 = [
|
||||||
|
'@dcloudio/uni-app-harmony',
|
||||||
|
// TODO: 如果不需要某个平台的小程序,请手动删除或注释掉
|
||||||
|
'@dcloudio/uni-mp-alipay',
|
||||||
|
'@dcloudio/uni-mp-baidu',
|
||||||
|
'@dcloudio/uni-mp-jd',
|
||||||
|
'@dcloudio/uni-mp-kuaishou',
|
||||||
|
'@dcloudio/uni-mp-lark',
|
||||||
|
'@dcloudio/uni-mp-qq',
|
||||||
|
'@dcloudio/uni-mp-toutiao',
|
||||||
|
'@dcloudio/uni-mp-xhs',
|
||||||
|
'@dcloudio/uni-quickapp-webview',
|
||||||
|
// i18n模板要注释掉下面的
|
||||||
|
'vue-i18n',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带开关的日志输出函数
|
||||||
|
* @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')
|
||||||
|
})
|
||||||
29
scripts/window-path-loader.js
Normal file
29
scripts/window-path-loader.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// fix: https://github.com/unibest-tech/unibest/issues/219
|
||||||
|
|
||||||
|
// Windows path loader for Node.js ESM
|
||||||
|
// This loader converts Windows absolute paths to file:// URLs
|
||||||
|
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve hook for ESM loader
|
||||||
|
* Converts Windows absolute paths to file:// URLs
|
||||||
|
*/
|
||||||
|
export function resolve(specifier, context, defaultResolve) {
|
||||||
|
// Check if this is a Windows absolute path (starts with drive letter like C:)
|
||||||
|
if (specifier.match(/^[a-z]:\\/i) || specifier.match(/^[a-z]:\//i)) {
|
||||||
|
// Convert Windows path to file:// URL
|
||||||
|
const fileUrl = pathToFileURL(specifier).href
|
||||||
|
return defaultResolve(fileUrl, context, defaultResolve)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other specifiers, use the default resolve
|
||||||
|
return defaultResolve(specifier, context, defaultResolve)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load hook for ESM loader
|
||||||
|
*/
|
||||||
|
export function load(url, context, defaultLoad) {
|
||||||
|
return defaultLoad(url, context, defaultLoad)
|
||||||
|
}
|
||||||
39
src/App.ku.vue
Normal file
39
src/App.ku.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useThemeStore } from '@/store'
|
||||||
|
import FgTabbar from '@/tabbar/index.vue'
|
||||||
|
import { isPageTabbar } from './tabbar/store'
|
||||||
|
import { currRoute } from './utils'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
const isCurrentPageTabbar = ref(true)
|
||||||
|
onShow(() => {
|
||||||
|
console.log('App.ku.vue onShow', currRoute())
|
||||||
|
const { path } = currRoute()
|
||||||
|
isCurrentPageTabbar.value = isPageTabbar(path)
|
||||||
|
})
|
||||||
|
|
||||||
|
const helloKuRoot = ref('Hello AppKuVue')
|
||||||
|
|
||||||
|
const exposeRef = ref('this is form app.Ku.vue')
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
exposeRef,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<wd-config-provider :theme-vars="themeStore.themeVars" :theme="themeStore.theme">
|
||||||
|
<!-- 这个先隐藏了,知道这样用就行 -->
|
||||||
|
<view class="hidden text-center">
|
||||||
|
{{ helloKuRoot }},这里可以配置全局的东西
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<KuRootView />
|
||||||
|
|
||||||
|
<FgTabbar v-if="isCurrentPageTabbar" />
|
||||||
|
<wd-toast />
|
||||||
|
<wd-message-box />
|
||||||
|
</wd-config-provider>
|
||||||
|
</template>
|
||||||
38
src/App.vue
Normal file
38
src/App.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||||
|
import { navigateToInterceptor } from '@/router/interceptor'
|
||||||
|
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
|
||||||
|
|
||||||
|
onLaunch((options) => {
|
||||||
|
console.log('App Launch', options)
|
||||||
|
})
|
||||||
|
onShow((options) => {
|
||||||
|
console.log('App Show', options)
|
||||||
|
// 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
|
||||||
|
// https://github.com/unibest-tech/unibest/issues/192
|
||||||
|
if (options?.path) {
|
||||||
|
navigateToInterceptor.invoke({ url: `/${options.path}`, query: options.query })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigateToInterceptor.invoke({ url: '/' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onHide(() => {
|
||||||
|
console.log('App Hide')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
swiper,
|
||||||
|
scroll-view {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
src/api/foo-alova.ts
Normal file
17
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 }, // 用于切换请求地址
|
||||||
|
})
|
||||||
|
}
|
||||||
11
src/api/foo-vue-query.ts
Normal file
11
src/api/foo-vue-query.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { queryOptions } from '@tanstack/vue-query'
|
||||||
|
import { getFooAPI } from './foo'
|
||||||
|
|
||||||
|
export function getFooQueryOptions(name: string) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return getFooAPI(queryKey[1])
|
||||||
|
},
|
||||||
|
queryKey: ['getFoo', name],
|
||||||
|
})
|
||||||
|
}
|
||||||
43
src/api/foo.ts
Normal file
43
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 function getFooAPI(name: string) {
|
||||||
|
return 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 })
|
||||||
|
}
|
||||||
|
/** POST 请求;支持 传递 header 的范例 */
|
||||||
|
export function postFooAPI3(name: string) {
|
||||||
|
return http.post<IFooItem>('/foo', { name }, { name }, { 'Content-Type-100': '100' })
|
||||||
|
}
|
||||||
87
src/api/login.ts
Normal file
87
src/api/login.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { IAuthLoginRes, ICaptcha, IDoubleTokenRes, IUpdateInfo, IUpdatePassword, IUserInfoRes } from './types/login'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录表单
|
||||||
|
*/
|
||||||
|
export interface ILoginForm {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
code?: string
|
||||||
|
uuid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码
|
||||||
|
* @returns ICaptcha 验证码
|
||||||
|
*/
|
||||||
|
export function getCode() {
|
||||||
|
return http.get<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)
|
||||||
|
}
|
||||||
97
src/api/types/login.ts
Normal file
97
src/api/types/login.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// 认证模式类型
|
||||||
|
export type AuthMode = 'single' | 'double'
|
||||||
|
|
||||||
|
// 单Token响应类型
|
||||||
|
export interface ISingleTokenRes {
|
||||||
|
token: string
|
||||||
|
expiresIn: number // 有效期(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双Token响应类型
|
||||||
|
export interface IDoubleTokenRes {
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
accessExpiresIn: number // 访问令牌有效期(秒)
|
||||||
|
refreshExpiresIn: number // 刷新令牌有效期(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录返回的信息,其实就是 token 信息
|
||||||
|
*/
|
||||||
|
export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息
|
||||||
|
*/
|
||||||
|
export interface IUserInfoRes {
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickname: string
|
||||||
|
avatar?: string
|
||||||
|
[key: string]: any // 允许其他扩展字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证存储数据结构
|
||||||
|
export interface AuthStorage {
|
||||||
|
mode: AuthMode
|
||||||
|
tokens: ISingleTokenRes | IDoubleTokenRes
|
||||||
|
userInfo?: IUserInfoRes
|
||||||
|
loginTime: number // 登录时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码
|
||||||
|
*/
|
||||||
|
export interface ICaptcha {
|
||||||
|
captchaEnabled: boolean
|
||||||
|
uuid: string
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 上传成功的信息
|
||||||
|
*/
|
||||||
|
export interface IUploadSuccessInfo {
|
||||||
|
fileId: number
|
||||||
|
originalName: string
|
||||||
|
fileName: string
|
||||||
|
storagePath: string
|
||||||
|
fileHash: string
|
||||||
|
fileType: string
|
||||||
|
fileBusinessType: string
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
export interface IUpdateInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
sex: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
export interface IUpdatePassword {
|
||||||
|
id: number
|
||||||
|
oldPassword: string
|
||||||
|
newPassword: string
|
||||||
|
confirmPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为单Token响应
|
||||||
|
* @param tokenRes 登录响应数据
|
||||||
|
* @returns 是否为单Token响应
|
||||||
|
*/
|
||||||
|
export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
|
||||||
|
return 'token' in tokenRes && !('refreshToken' in tokenRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为双Token响应
|
||||||
|
* @param tokenRes 登录响应数据
|
||||||
|
* @returns 是否为双Token响应
|
||||||
|
*/
|
||||||
|
export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
|
||||||
|
return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
|
||||||
|
}
|
||||||
0
src/components/.gitkeep
Normal file
0
src/components/.gitkeep
Normal file
38
src/components/AncientButton.vue
Normal file
38
src/components/AncientButton.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="ancient-btn"
|
||||||
|
:class="{ disabled }"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<slot>{{ text }}</slot>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
text?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
click: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
text: '',
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!props.disabled) {
|
||||||
|
emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 样式已在 zhuzi-theme.scss 中定义
|
||||||
|
</style>
|
||||||
26
src/components/ZhuziCard.vue
Normal file
26
src/components/ZhuziCard.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<view class="zhuzi-card" :class="customClass">
|
||||||
|
<view v-if="title" class="card-title">
|
||||||
|
{{ title }}
|
||||||
|
</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
title?: string
|
||||||
|
customClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
title: '',
|
||||||
|
customClass: '',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 样式已在 zhuzi-theme.scss 中定义
|
||||||
|
</style>
|
||||||
38
src/env.d.ts
vendored
Normal file
38
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/// <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
|
||||||
|
/** H5是否需要代理 */
|
||||||
|
readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
|
||||||
|
/** H5是否需要代理,需要的话有个前缀 */
|
||||||
|
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
|
||||||
|
/** 后端是否有统一前缀 /api */
|
||||||
|
readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false'
|
||||||
|
/** 认证模式,'single' | 'double' ==> 单token | 双token */
|
||||||
|
readonly VITE_AUTH_MODE: 'single' | 'double'
|
||||||
|
/** 上传图片地址 */
|
||||||
|
readonly VITE_UPLOAD_BASEURL: string
|
||||||
|
/** 是否清除console */
|
||||||
|
readonly VITE_DELETE_CONSOLE: string
|
||||||
|
// 更多环境变量...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const __VITE_APP_PROXY__: 'true' | 'false'
|
||||||
|
declare const __UNI_PLATFORM__: 'app' | 'h5' | 'mp-alipay' | 'mp-baidu' | 'mp-kuaishou' | 'mp-lark' | 'mp-qq' | 'mp-tiktok' | 'mp-weixin' | 'mp-xiaochengxu'
|
||||||
51
src/hooks/useRequest.ts
Normal file
51
src/hooks/useRequest.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
interface IUseRequestOptions<T> {
|
||||||
|
/** 是否立即执行 */
|
||||||
|
immediate?: boolean
|
||||||
|
/** 初始化数据 */
|
||||||
|
initialData?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUseRequestReturn<T> {
|
||||||
|
loading: Ref<boolean>
|
||||||
|
error: Ref<boolean | Error>
|
||||||
|
data: Ref<T | undefined>
|
||||||
|
run: () => 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>(
|
||||||
|
func: () => Promise<IResData<T>>,
|
||||||
|
options: IUseRequestOptions<T> = { immediate: false },
|
||||||
|
): IUseRequestReturn<T> {
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(false)
|
||||||
|
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
|
||||||
|
const run = async () => {
|
||||||
|
loading.value = true
|
||||||
|
return func()
|
||||||
|
.then((res) => {
|
||||||
|
data.value = res.data
|
||||||
|
error.value = false
|
||||||
|
return data.value
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error.value = err
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
options.immediate && run()
|
||||||
|
return { loading, error, data, run }
|
||||||
|
}
|
||||||
160
src/hooks/useUpload.ts
Normal file
160
src/hooks/useUpload.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { getEnvBaseUploadUrl } from '@/utils'
|
||||||
|
|
||||||
|
const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}`
|
||||||
|
|
||||||
|
type TfileType = 'image' | 'file'
|
||||||
|
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
|
||||||
|
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
|
||||||
|
|
||||||
|
interface TOptions<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) => {
|
||||||
|
const { data: _data } = JSON.parse(res)
|
||||||
|
data.value = _data
|
||||||
|
// console.log('上传成功', res)
|
||||||
|
success?.(_data)
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
error.value = err
|
||||||
|
onError?.(err)
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
loading.value = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
|
||||||
|
// 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
|
||||||
|
const chooseFileOptions = {
|
||||||
|
count: 1,
|
||||||
|
success: (res: any) => {
|
||||||
|
console.log('File selected successfully:', res)
|
||||||
|
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
|
||||||
|
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
|
||||||
|
// h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
|
||||||
|
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
|
||||||
|
// App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
|
||||||
|
let tempFilePath = ''
|
||||||
|
let size = 0
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
tempFilePath = res.tempFiles[0].tempFilePath
|
||||||
|
size = res.tempFiles[0].size
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
tempFilePath = res.tempFilePaths[0]
|
||||||
|
size = res.tempFiles[0].size
|
||||||
|
// #endif
|
||||||
|
handleFileChoose({ tempFilePath, size })
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
console.error('File selection failed:', err)
|
||||||
|
error.value = err
|
||||||
|
onError?.(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType === 'image') {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
uni.chooseMedia({
|
||||||
|
...chooseFileOptions,
|
||||||
|
mediaType: ['image'],
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
uni.chooseImage(chooseFileOptions)
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uni.chooseFile({
|
||||||
|
...chooseFileOptions,
|
||||||
|
type: 'all',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loading, error, data, run }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile({
|
||||||
|
tempFilePath,
|
||||||
|
formData,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
onComplete,
|
||||||
|
}: {
|
||||||
|
tempFilePath: string
|
||||||
|
formData: Record<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
src/http/README.md
Normal file
13
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号)。
|
||||||
117
src/http/alova.ts
Normal file
117
src/http/alova.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
|
||||||
|
import type { IResponse } from './types'
|
||||||
|
import AdapterUniapp from '@alova/adapter-uniapp'
|
||||||
|
import { createAlova } from 'alova'
|
||||||
|
import { createServerTokenAuthentication } from 'alova/client'
|
||||||
|
import VueHook from 'alova/vue'
|
||||||
|
import { LOGIN_PAGE } from '@/router/config'
|
||||||
|
import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
|
||||||
|
|
||||||
|
// 配置动态Tag
|
||||||
|
export const API_DOMAINS = {
|
||||||
|
DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
|
||||||
|
SECONDARY: import.meta.env.VITE_API_SECONDARY_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建请求实例
|
||||||
|
*/
|
||||||
|
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
|
||||||
|
typeof VueHook,
|
||||||
|
typeof uniappRequestAdapter
|
||||||
|
>({
|
||||||
|
refreshTokenOnError: {
|
||||||
|
isExpired: (error) => {
|
||||||
|
return error.response?.status === ResultEnum.Unauthorized
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
try {
|
||||||
|
// await authLogin();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// 切换到登录页
|
||||||
|
await uni.reLaunch({ url: LOGIN_PAGE })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* alova 请求实例
|
||||||
|
*/
|
||||||
|
const alovaInstance = createAlova({
|
||||||
|
baseURL: import.meta.env.VITE_APP_PROXY_PREFIX,
|
||||||
|
...AdapterUniapp(),
|
||||||
|
timeout: 5000,
|
||||||
|
statesHook: VueHook,
|
||||||
|
|
||||||
|
beforeRequest: onAuthRequired((method) => {
|
||||||
|
// 设置默认 Content-Type
|
||||||
|
method.config.headers = {
|
||||||
|
ContentType: ContentTypeEnum.JSON,
|
||||||
|
Accept: 'application/json, text/plain, */*',
|
||||||
|
...method.config.headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config } = method
|
||||||
|
const ignoreAuth = !config.meta?.ignoreAuth
|
||||||
|
console.log('ignoreAuth===>', ignoreAuth)
|
||||||
|
// 处理认证信息 自行处理认证问题
|
||||||
|
if (ignoreAuth) {
|
||||||
|
const token = 'getToken()'
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('[请求错误]:未登录')
|
||||||
|
}
|
||||||
|
// method.config.headers.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理动态域名
|
||||||
|
if (config.meta?.domain) {
|
||||||
|
method.baseURL = config.meta.domain
|
||||||
|
console.log('当前域名', method.baseURL)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
responded: onResponseRefreshToken((response, method) => {
|
||||||
|
const { config } = method
|
||||||
|
const { requestType } = config
|
||||||
|
const {
|
||||||
|
statusCode,
|
||||||
|
data: rawData,
|
||||||
|
errMsg,
|
||||||
|
} = response as UniNamespace.RequestSuccessCallbackResult
|
||||||
|
|
||||||
|
// 处理特殊请求类型(上传/下载)
|
||||||
|
if (requestType === 'upload' || requestType === 'download') {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 HTTP 状态码错误
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
|
||||||
|
console.error('errorMessage===>', errorMessage)
|
||||||
|
uni.showToast({
|
||||||
|
title: errorMessage,
|
||||||
|
icon: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(`${errorMessage}:${errMsg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理业务逻辑错误
|
||||||
|
const { code, message, data } = rawData as IResponse
|
||||||
|
if (code !== ResultEnum.Success) {
|
||||||
|
if (config.meta?.toast !== false) {
|
||||||
|
uni.showToast({
|
||||||
|
title: message,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw new Error(`请求错误[${code}]:${message}`)
|
||||||
|
}
|
||||||
|
// 处理成功响应,返回业务数据
|
||||||
|
return data
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const http = alovaInstance
|
||||||
182
src/http/http.ts
Normal file
182
src/http/http.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import type { IDoubleTokenRes } from '@/api/types/login'
|
||||||
|
import type { CustomRequestOptions } from '@/http/types'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { LOGIN_PAGE } from '@/router/config'
|
||||||
|
import { useTokenStore } from '@/store/token'
|
||||||
|
import { isDoubleTokenMode } from '@/utils'
|
||||||
|
|
||||||
|
// 刷新 token 状态管理
|
||||||
|
let refreshing = false // 防止重复刷新 token 标识
|
||||||
|
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
|
||||||
|
|
||||||
|
export function http<T>(options: CustomRequestOptions) {
|
||||||
|
// 1. 返回 Promise 对象
|
||||||
|
return new Promise<IResData<T>>((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
...options,
|
||||||
|
dataType: 'json',
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
responseType: 'json',
|
||||||
|
// #endif
|
||||||
|
// 响应成功
|
||||||
|
success: async (res) => {
|
||||||
|
// 状态码 2xx,参考 axios 的设计
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
// 2.1 提取核心数据 res.data
|
||||||
|
return resolve(res.data as IResData<T>)
|
||||||
|
}
|
||||||
|
const resData: IResData<T> = res.data as IResData<T>
|
||||||
|
if ((res.statusCode === 401) || (resData.code === 401)) {
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
if (!isDoubleTokenMode) {
|
||||||
|
// 未启用双token策略,清理用户信息,跳转到登录页
|
||||||
|
tokenStore.logout()
|
||||||
|
uni.navigateTo({ url: LOGIN_PAGE })
|
||||||
|
return reject(res)
|
||||||
|
}
|
||||||
|
/* -------- 无感刷新 token ----------- */
|
||||||
|
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
|
||||||
|
// token 失效的,且有刷新 token 的,才放到请求队列里
|
||||||
|
if ((res.statusCode === 401 || resData.code === 401) && refreshToken) {
|
||||||
|
taskQueue.push(() => {
|
||||||
|
resolve(http<T>(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
|
||||||
|
if ((res.statusCode === 401 || resData.code === 401) && refreshToken && !refreshing) {
|
||||||
|
refreshing = true
|
||||||
|
try {
|
||||||
|
// 发起刷新 token 请求(使用 store 的 refreshToken 方法)
|
||||||
|
await tokenStore.refreshToken()
|
||||||
|
// 刷新 token 成功
|
||||||
|
refreshing = false
|
||||||
|
nextTick(() => {
|
||||||
|
// 关闭其他弹窗
|
||||||
|
uni.hideToast()
|
||||||
|
uni.showToast({
|
||||||
|
title: 'token 刷新成功',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// 将任务队列的所有任务重新请求
|
||||||
|
taskQueue.forEach(task => task())
|
||||||
|
}
|
||||||
|
catch (refreshErr) {
|
||||||
|
console.error('刷新 token 失败:', refreshErr)
|
||||||
|
refreshing = false
|
||||||
|
// 刷新 token 失败,跳转到登录页
|
||||||
|
nextTick(() => {
|
||||||
|
// 关闭其他弹窗
|
||||||
|
uni.hideToast()
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录已过期,请重新登录',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// 清除用户信息
|
||||||
|
await tokenStore.logout()
|
||||||
|
// 跳转到登录页
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({ url: LOGIN_PAGE })
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
// 不管刷新 token 成功与否,都清空任务队列
|
||||||
|
taskQueue = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 其他错误 -> 根据后端错误信息轻提示
|
||||||
|
!options.hideErrorToast
|
||||||
|
&& uni.showToast({
|
||||||
|
icon: 'none',
|
||||||
|
title: (res.data as IResData<T>).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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
66
src/http/interceptor.ts
Normal file
66
src/http/interceptor.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { CustomRequestOptions } from '@/http/types'
|
||||||
|
import { useTokenStore } from '@/store'
|
||||||
|
import { getEnvBaseUrl } from '@/utils'
|
||||||
|
import { platform } from '@/utils/platform'
|
||||||
|
import { stringifyQuery } from './tools/queryString'
|
||||||
|
|
||||||
|
// 请求基准地址
|
||||||
|
const baseUrl = getEnvBaseUrl()
|
||||||
|
|
||||||
|
// 拦截器配置
|
||||||
|
const httpInterceptor = {
|
||||||
|
// 拦截前触发
|
||||||
|
invoke(options: CustomRequestOptions) {
|
||||||
|
// 接口请求支持通过 query 参数配置 queryString
|
||||||
|
if (options.query) {
|
||||||
|
const queryStr = stringifyQuery(options.query)
|
||||||
|
if (options.url.includes('?')) {
|
||||||
|
options.url += `&${queryStr}`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
options.url += `?${queryStr}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 非 http 开头需拼接地址
|
||||||
|
if (!options.url.startsWith('http')) {
|
||||||
|
// #ifdef H5
|
||||||
|
// console.log(__VITE_APP_PROXY__)
|
||||||
|
if (JSON.parse(__VITE_APP_PROXY__)) {
|
||||||
|
// 自动拼接代理前缀
|
||||||
|
options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
options.url = baseUrl + options.url
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// 非H5正常拼接
|
||||||
|
// #ifndef H5
|
||||||
|
options.url = baseUrl + options.url
|
||||||
|
// #endif
|
||||||
|
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
|
||||||
|
}
|
||||||
|
// 1. 请求超时
|
||||||
|
options.timeout = 60000 // 60s
|
||||||
|
// 2. (可选)添加小程序端请求头标识
|
||||||
|
options.header = {
|
||||||
|
platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源
|
||||||
|
...options.header,
|
||||||
|
}
|
||||||
|
// 3. 添加 token 请求头标识
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
const token = tokenStore.validToken
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
options.header.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestInterceptor = {
|
||||||
|
install() {
|
||||||
|
// 拦截 request 请求
|
||||||
|
uni.addInterceptor('request', httpInterceptor)
|
||||||
|
// 拦截 uploadFile 文件上传
|
||||||
|
uni.addInterceptor('uploadFile', httpInterceptor)
|
||||||
|
},
|
||||||
|
}
|
||||||
66
src/http/tools/enum.ts
Normal file
66
src/http/tools/enum.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export enum ResultEnum {
|
||||||
|
Success = 0, // 成功
|
||||||
|
Error = 400, // 错误
|
||||||
|
Unauthorized = 401, // 未授权
|
||||||
|
Forbidden = 403, // 禁止访问(原为forbidden)
|
||||||
|
NotFound = 404, // 未找到(原为notFound)
|
||||||
|
MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
|
||||||
|
RequestTimeout = 408, // 请求超时(原为requestTimeout)
|
||||||
|
InternalServerError = 500, // 服务器错误(原为internalServerError)
|
||||||
|
NotImplemented = 501, // 未实现(原为notImplemented)
|
||||||
|
BadGateway = 502, // 网关错误(原为badGateway)
|
||||||
|
ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
|
||||||
|
GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
|
||||||
|
HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
|
||||||
|
}
|
||||||
|
export enum ContentTypeEnum {
|
||||||
|
JSON = 'application/json;charset=UTF-8',
|
||||||
|
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 根据状态码,生成对应的错误信息
|
||||||
|
* @param {number|string} status 状态码
|
||||||
|
* @returns {string} 错误信息
|
||||||
|
*/
|
||||||
|
export function ShowMessage(status: number | string): string {
|
||||||
|
let message: string
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
message = '请求错误(400)'
|
||||||
|
break
|
||||||
|
case 401:
|
||||||
|
message = '未授权,请重新登录(401)'
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
message = '拒绝访问(403)'
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
message = '请求出错(404)'
|
||||||
|
break
|
||||||
|
case 408:
|
||||||
|
message = '请求超时(408)'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
message = '服务器错误(500)'
|
||||||
|
break
|
||||||
|
case 501:
|
||||||
|
message = '服务未实现(501)'
|
||||||
|
break
|
||||||
|
case 502:
|
||||||
|
message = '网络错误(502)'
|
||||||
|
break
|
||||||
|
case 503:
|
||||||
|
message = '服务不可用(503)'
|
||||||
|
break
|
||||||
|
case 504:
|
||||||
|
message = '网络超时(504)'
|
||||||
|
break
|
||||||
|
case 505:
|
||||||
|
message = 'HTTP版本不受支持(505)'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message = `连接出错(${status})!`
|
||||||
|
}
|
||||||
|
return `${message},请检查网络或联系管理员!`
|
||||||
|
}
|
||||||
29
src/http/tools/queryString.ts
Normal file
29
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('&')
|
||||||
|
}
|
||||||
31
src/http/types.ts
Normal file
31
src/http/types.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
|
||||||
|
*/
|
||||||
|
export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||||
|
query?: Record<string, any>
|
||||||
|
/** 出错时是否隐藏错误提示 */
|
||||||
|
hideErrorToast?: boolean
|
||||||
|
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
||||||
|
|
||||||
|
// 通用响应格式
|
||||||
|
export interface IResponse<T = any> {
|
||||||
|
code: number | string
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
status: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页请求参数
|
||||||
|
export interface PageParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应数据
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
30
src/http/vue-query.ts
Normal file
30
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 = unknown>(
|
||||||
|
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>(requestOptions)
|
||||||
|
}
|
||||||
10
src/layouts/default.vue
Normal file
10
src/layouts/default.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const testUniLayoutExposedData = ref('testUniLayoutExposedData')
|
||||||
|
defineExpose({
|
||||||
|
testUniLayoutExposedData,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
21
src/main.ts
Normal file
21
src/main.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { VueQueryPlugin } from '@tanstack/vue-query'
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import { requestInterceptor } from './http/interceptor'
|
||||||
|
import { routeInterceptor } from './router/interceptor'
|
||||||
|
|
||||||
|
import store from './store'
|
||||||
|
import '@/style/index.scss'
|
||||||
|
import 'virtual:uno.css'
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
app.use(store)
|
||||||
|
app.use(routeInterceptor)
|
||||||
|
app.use(requestInterceptor)
|
||||||
|
app.use(VueQueryPlugin)
|
||||||
|
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/mocks/article.ts
Normal file
86
src/mocks/article.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* 文章内容相关Mock数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
coverImage: string
|
||||||
|
publishTime: string
|
||||||
|
author: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮播图文章
|
||||||
|
export const mockCarouselArticles: Article[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: '朱子故里行:走进福建尤溪',
|
||||||
|
content: `
|
||||||
|
<div class="article-content">
|
||||||
|
<h2>朱子故里:福建尤溪</h2>
|
||||||
|
<p>福建尤溪,这片山清水秀的土地,孕育了一代大儒朱熹。公元1130年,朱熹就诞生在这里的南溪书院附近。</p>
|
||||||
|
|
||||||
|
<h3>朱子文化的发源地</h3>
|
||||||
|
<p>尤溪县是朱熹的诞生地,这里保存着丰富的朱子文化遗迹。朱子社、朱子祠、朱子码头等历史遗迹见证了这位大儒的成长轨迹。</p>
|
||||||
|
|
||||||
|
<h3>朱子理学的启蒙</h3>
|
||||||
|
<p>朱熹在尤溪度过了人生的前14年,这段时光为他日后的理学思想奠定了基础。山水之间的灵秀,培养了他对自然和人文的深刻理解。</p>
|
||||||
|
|
||||||
|
<p>如今的尤溪,依然保持着那份古朴与宁静,成为后人缅怀朱子、学习朱子文化的圣地。</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
coverImage: '/static/images/article1.jpg',
|
||||||
|
publishTime: '2024-03-15',
|
||||||
|
author: '朱子文化研究院',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: '四书章句集注:朱子的不朽之作',
|
||||||
|
content: `
|
||||||
|
<div class="article-content">
|
||||||
|
<h2>《四书章句集注》:理学经典</h2>
|
||||||
|
<p>朱熹的《四书章句集注》是中国古代教育史上最重要的著作之一,影响了中国教育近700年。</p>
|
||||||
|
|
||||||
|
<h3>四书的组成</h3>
|
||||||
|
<p>四书包括《大学》、《中庸》、《论语》、《孟子》四部经典。朱熹将这四部著作编辑成册,并加以详细注解。</p>
|
||||||
|
|
||||||
|
<h3>教育的里程碑</h3>
|
||||||
|
<p>《四书章句集注》成为元、明、清三朝科举考试的标准教材,培养了无数读书人,传播了儒家思想。</p>
|
||||||
|
|
||||||
|
<h3>现代价值</h3>
|
||||||
|
<p>时至今日,《四书章句集注》仍然是学习儒家文化、理解中华传统文化的重要读本。</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
coverImage: '/static/images/article2.jpg',
|
||||||
|
publishTime: '2024-03-12',
|
||||||
|
author: '教育研究所',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: '朱子教育思想对现代教育的启示',
|
||||||
|
content: `
|
||||||
|
<div class="article-content">
|
||||||
|
<h2>朱子教育思想的现代意义</h2>
|
||||||
|
<p>朱熹不仅是一位伟大的思想家,更是一位杰出的教育家。他的教育思想对现代教育仍有重要的指导意义。</p>
|
||||||
|
|
||||||
|
<h3>因材施教</h3>
|
||||||
|
<p>朱熹主张根据学生的不同特点进行教育,这与现代个性化教育理念不谋而合。</p>
|
||||||
|
|
||||||
|
<h3>学思并重</h3>
|
||||||
|
<p>"学而时习之"、"学而不思则罔,思而不学则殆",朱熹强调学习与思考的结合。</p>
|
||||||
|
|
||||||
|
<h3>品德教育</h3>
|
||||||
|
<p>朱熹认为教育的根本目的是培养品德高尚的人,这对现代素质教育具有重要启发。</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
coverImage: '/static/images/article3.jpg',
|
||||||
|
publishTime: '2024-03-10',
|
||||||
|
author: '现代教育研究中心',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 朱子介绍内容
|
||||||
|
export const zhuziIntroduction = `朱熹(1130-1200),字元晦、仲晦,号晦庵、晦翁,别号紫阳,谥号"文",世称朱文公。祖居宋徽州婺源县(治所在今江西省婺源县),出生于南剑州尤溪县(治所在今福建省尤溪县),终老于建宁府建阳县(治所在今福建省南平市建阳区)。我国古代伟大的思想家、哲学家、教育家、文学家。在中国文化史上是与孔子并峙的两座高峰,素有"北孔南朱"之称,后世尊称为"朱子"。
|
||||||
|
|
||||||
|
朱子集南宋前儒学思想之大成,构建了"致广大,尽精微,综罗百代"的理学思想体系,影响了中国社会近千年。其所著《四书章句集注》《周易本义》《诗集传》等,为元明清科举考试的必读书目。朱子亦是唯一非孔子亲传弟子而入孔庙大成殿配享的先哲。以朱子理学为标志,中华文明进入新的千年发展期并延续至今。可以说,读懂了朱熹,就读懂了中华优秀传统文化。`
|
||||||
61
src/mocks/auth.ts
Normal file
61
src/mocks/auth.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 认证相关Mock数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 学校数据
|
||||||
|
export const mockSchools = [
|
||||||
|
{ id: 1, name: '第一小学' },
|
||||||
|
{ id: 2, name: '第二小学' },
|
||||||
|
{ id: 3, name: '第三小学' },
|
||||||
|
{ id: 4, name: '第四小学' },
|
||||||
|
{ id: 5, name: '第五小学' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 年级数据
|
||||||
|
export const mockGrades = [
|
||||||
|
{ id: 1, name: '一年级' },
|
||||||
|
{ id: 2, name: '二年级' },
|
||||||
|
{ id: 3, name: '三年级' },
|
||||||
|
{ id: 4, name: '四年级' },
|
||||||
|
{ id: 5, name: '五年级' },
|
||||||
|
{ id: 6, name: '六年级' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 班级数据
|
||||||
|
export const mockClasses = [
|
||||||
|
{ id: 1, name: '1班', gradeId: 1 },
|
||||||
|
{ id: 2, name: '2班', gradeId: 1 },
|
||||||
|
{ id: 3, name: '3班', gradeId: 1 },
|
||||||
|
{ id: 4, name: '1班', gradeId: 2 },
|
||||||
|
{ id: 5, name: '2班', gradeId: 2 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 用户注册响应
|
||||||
|
export const mockRegisterResponse = {
|
||||||
|
success: true,
|
||||||
|
message: '注册成功',
|
||||||
|
data: {
|
||||||
|
userId: '123456',
|
||||||
|
phone: '13800138000',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户登录响应
|
||||||
|
export const mockLoginResponse = {
|
||||||
|
success: true,
|
||||||
|
message: '登录成功',
|
||||||
|
data: {
|
||||||
|
userId: '123456',
|
||||||
|
phone: '13800138000',
|
||||||
|
token: 'mock_token_123456',
|
||||||
|
hasChild: true,
|
||||||
|
childInfo: {
|
||||||
|
id: '789',
|
||||||
|
name: '小明',
|
||||||
|
schoolId: 1,
|
||||||
|
gradeId: 3,
|
||||||
|
classId: 5,
|
||||||
|
seatNumber: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
9
src/mocks/index.ts
Normal file
9
src/mocks/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Mock数据统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './article'
|
||||||
|
export * from './auth'
|
||||||
|
export * from './quiz'
|
||||||
|
export * from './ranking'
|
||||||
|
export * from './user'
|
||||||
248
src/mocks/quiz.ts
Normal file
248
src/mocks/quiz.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* 答题相关Mock数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface QuizQuestion {
|
||||||
|
id: string
|
||||||
|
type: 'single' | 'multiple' // 单选、多选
|
||||||
|
title: string
|
||||||
|
options: Array<{
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
}>
|
||||||
|
correctAnswers: string[]
|
||||||
|
score: number
|
||||||
|
difficulty: 'easy' | 'medium' | 'hard'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 家长题库(5道题)
|
||||||
|
export const mockParentQuestions: QuizQuestion[] = [
|
||||||
|
{
|
||||||
|
id: 'p1',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹字什么?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '元晦' },
|
||||||
|
{ id: 'b', text: '仲晦' },
|
||||||
|
{ id: 'c', text: '晦庵' },
|
||||||
|
{ id: 'd', text: '以上都是' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['d'],
|
||||||
|
score: 10,
|
||||||
|
difficulty: 'easy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p2',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹被后世尊称为什么?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '朱子' },
|
||||||
|
{ id: 'b', text: '朱文公' },
|
||||||
|
{ id: 'c', text: '紫阳先生' },
|
||||||
|
{ id: 'd', text: '以上都是' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['d'],
|
||||||
|
score: 15,
|
||||||
|
difficulty: 'easy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p3',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹的生卒年份是?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '1130-1200' },
|
||||||
|
{ id: 'b', text: '1120-1190' },
|
||||||
|
{ id: 'c', text: '1140-1210' },
|
||||||
|
{ id: 'd', text: '1135-1205' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['a'],
|
||||||
|
score: 20,
|
||||||
|
difficulty: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p4',
|
||||||
|
type: 'multiple',
|
||||||
|
title: '朱熹的主要著作有哪些?(多选)',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '四书章句集注' },
|
||||||
|
{ id: 'b', text: '周易本义' },
|
||||||
|
{ id: 'c', text: '诗集传' },
|
||||||
|
{ id: 'd', text: '论语' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['a', 'b', 'c'],
|
||||||
|
score: 25,
|
||||||
|
difficulty: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p5',
|
||||||
|
type: 'single',
|
||||||
|
title: '"北孔南朱"中的"朱"指的是谁?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '朱元璋' },
|
||||||
|
{ id: 'b', text: '朱熹' },
|
||||||
|
{ id: 'c', text: '朱舜水' },
|
||||||
|
{ id: 'd', text: '朱自清' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['b'],
|
||||||
|
score: 30,
|
||||||
|
difficulty: 'hard',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 学生题库(10道题)
|
||||||
|
export const mockStudentQuestions: QuizQuestion[] = [
|
||||||
|
{
|
||||||
|
id: 's1',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹是哪个朝代的人?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '唐朝' },
|
||||||
|
{ id: 'b', text: '宋朝' },
|
||||||
|
{ id: 'c', text: '明朝' },
|
||||||
|
{ id: 'd', text: '清朝' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['b'],
|
||||||
|
score: 10,
|
||||||
|
difficulty: 'easy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's2',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹出生在今天的哪个省?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '江西省' },
|
||||||
|
{ id: 'b', text: '福建省' },
|
||||||
|
{ id: 'c', text: '浙江省' },
|
||||||
|
{ id: 'd', text: '安徽省' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['b'],
|
||||||
|
score: 10,
|
||||||
|
difficulty: 'easy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's3',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹被称为什么?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '诗仙' },
|
||||||
|
{ id: 'b', text: '诗圣' },
|
||||||
|
{ id: 'c', text: '朱子' },
|
||||||
|
{ id: 'd', text: '文豪' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['c'],
|
||||||
|
score: 10,
|
||||||
|
difficulty: 'easy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's4',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹是伟大的什么家?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '思想家' },
|
||||||
|
{ id: 'b', text: '教育家' },
|
||||||
|
{ id: 'c', text: '哲学家' },
|
||||||
|
{ id: 'd', text: '以上都是' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['d'],
|
||||||
|
score: 15,
|
||||||
|
difficulty: 'easy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's5',
|
||||||
|
type: 'single',
|
||||||
|
title: '"理学"的集大成者是谁?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '孔子' },
|
||||||
|
{ id: 'b', text: '孟子' },
|
||||||
|
{ id: 'c', text: '朱熹' },
|
||||||
|
{ id: 'd', text: '老子' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['c'],
|
||||||
|
score: 15,
|
||||||
|
difficulty: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's6',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹的思想体系被称为什么?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '理学' },
|
||||||
|
{ id: 'b', text: '心学' },
|
||||||
|
{ id: 'c', text: '实学' },
|
||||||
|
{ id: 'd', text: '玄学' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['a'],
|
||||||
|
score: 15,
|
||||||
|
difficulty: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's7',
|
||||||
|
type: 'single',
|
||||||
|
title: '孔庙大成殿中,除了孔子的弟子外,还有一位入祀的先哲是?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '孟子' },
|
||||||
|
{ id: 'b', text: '朱熹' },
|
||||||
|
{ id: 'c', text: '老子' },
|
||||||
|
{ id: 'd', text: '庄子' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['b'],
|
||||||
|
score: 20,
|
||||||
|
difficulty: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's8',
|
||||||
|
type: 'multiple',
|
||||||
|
title: '朱熹的别号有哪些?(多选)',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '晦庵' },
|
||||||
|
{ id: 'b', text: '晦翁' },
|
||||||
|
{ id: 'c', text: '紫阳' },
|
||||||
|
{ id: 'd', text: '东坡' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['a', 'b', 'c'],
|
||||||
|
score: 20,
|
||||||
|
difficulty: 'hard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's9',
|
||||||
|
type: 'single',
|
||||||
|
title: '朱熹的理学思想影响了中国社会多少年?',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '500年' },
|
||||||
|
{ id: 'b', text: '近千年' },
|
||||||
|
{ id: 'c', text: '200年' },
|
||||||
|
{ id: 'd', text: '100年' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['b'],
|
||||||
|
score: 25,
|
||||||
|
difficulty: 'hard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's10',
|
||||||
|
type: 'single',
|
||||||
|
title: '"读懂了朱熹,就读懂了____"',
|
||||||
|
options: [
|
||||||
|
{ id: 'a', text: '中国历史' },
|
||||||
|
{ id: 'b', text: '中华优秀传统文化' },
|
||||||
|
{ id: 'c', text: '宋朝文化' },
|
||||||
|
{ id: 'd', text: '理学思想' },
|
||||||
|
],
|
||||||
|
correctAnswers: ['b'],
|
||||||
|
score: 25,
|
||||||
|
difficulty: 'hard',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 答题结果mock
|
||||||
|
export const mockQuizResult = {
|
||||||
|
totalScore: 85,
|
||||||
|
correctCount: 4,
|
||||||
|
totalCount: 5,
|
||||||
|
timeSpent: 120, // 秒
|
||||||
|
achievement: '优秀',
|
||||||
|
ranking: {
|
||||||
|
class: 5,
|
||||||
|
grade: 12,
|
||||||
|
school: 35,
|
||||||
|
},
|
||||||
|
}
|
||||||
91
src/mocks/ranking.ts
Normal file
91
src/mocks/ranking.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 排行榜相关Mock数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface RankingItem {
|
||||||
|
rank: number
|
||||||
|
studentName: string
|
||||||
|
schoolName: string
|
||||||
|
gradeName: string
|
||||||
|
className: string
|
||||||
|
totalScore: number
|
||||||
|
monthScore: number
|
||||||
|
avatar?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 班级排行榜(前20名)
|
||||||
|
export const mockClassRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({
|
||||||
|
rank: index + 1,
|
||||||
|
studentName: `同学${index + 1}`,
|
||||||
|
schoolName: '第一小学',
|
||||||
|
gradeName: '三年级',
|
||||||
|
className: '2班',
|
||||||
|
totalScore: 1000 - index * 20,
|
||||||
|
monthScore: 500 - index * 10,
|
||||||
|
avatar: `/static/images/avatar${(index % 5) + 1}.png`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 年级排行榜
|
||||||
|
export const mockGradeRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({
|
||||||
|
rank: index + 1,
|
||||||
|
studentName: `年级同学${index + 1}`,
|
||||||
|
schoolName: '第一小学',
|
||||||
|
gradeName: '三年级',
|
||||||
|
className: `${Math.floor(Math.random() * 5) + 1}班`,
|
||||||
|
totalScore: 1200 - index * 25,
|
||||||
|
monthScore: 600 - index * 12,
|
||||||
|
avatar: `/static/images/avatar${(index % 5) + 1}.png`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 全校排行榜
|
||||||
|
export const mockSchoolRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({
|
||||||
|
rank: index + 1,
|
||||||
|
studentName: `全校同学${index + 1}`,
|
||||||
|
schoolName: '第一小学',
|
||||||
|
gradeName: `${Math.floor(Math.random() * 6) + 1}年级`,
|
||||||
|
className: `${Math.floor(Math.random() * 5) + 1}班`,
|
||||||
|
totalScore: 1500 - index * 30,
|
||||||
|
monthScore: 750 - index * 15,
|
||||||
|
avatar: `/static/images/avatar${(index % 5) + 1}.png`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 全区排行榜
|
||||||
|
export const mockDistrictRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({
|
||||||
|
rank: index + 1,
|
||||||
|
studentName: `全区同学${index + 1}`,
|
||||||
|
schoolName: `第${Math.floor(Math.random() * 5) + 1}小学`,
|
||||||
|
gradeName: `${Math.floor(Math.random() * 6) + 1}年级`,
|
||||||
|
className: `${Math.floor(Math.random() * 5) + 1}班`,
|
||||||
|
totalScore: 1800 - index * 35,
|
||||||
|
monthScore: 900 - index * 18,
|
||||||
|
avatar: `/static/images/avatar${(index % 5) + 1}.png`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 全市排行榜
|
||||||
|
export const mockCityRanking: RankingItem[] = Array.from({ length: 20 }, (_, index) => ({
|
||||||
|
rank: index + 1,
|
||||||
|
studentName: `全市同学${index + 1}`,
|
||||||
|
schoolName: `第${Math.floor(Math.random() * 10) + 1}小学`,
|
||||||
|
gradeName: `${Math.floor(Math.random() * 6) + 1}年级`,
|
||||||
|
className: `${Math.floor(Math.random() * 5) + 1}班`,
|
||||||
|
totalScore: 2000 - index * 40,
|
||||||
|
monthScore: 1000 - index * 20,
|
||||||
|
avatar: `/static/images/avatar${(index % 5) + 1}.png`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 当前用户排名信息
|
||||||
|
export const mockUserRankInfo = {
|
||||||
|
studentName: '小明',
|
||||||
|
schoolName: '第一小学',
|
||||||
|
gradeName: '三年级',
|
||||||
|
className: '2班',
|
||||||
|
totalScore: 850,
|
||||||
|
monthScore: 420,
|
||||||
|
rankings: {
|
||||||
|
class: { rank: 5, total: 35 },
|
||||||
|
grade: { rank: 12, total: 180 },
|
||||||
|
school: { rank: 35, total: 1200 },
|
||||||
|
district: { rank: 120, total: 8500 },
|
||||||
|
city: { rank: 560, total: 25000 },
|
||||||
|
},
|
||||||
|
}
|
||||||
82
src/mocks/user.ts
Normal file
82
src/mocks/user.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* 用户相关Mock数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface QuizRecord {
|
||||||
|
id: string
|
||||||
|
date: string
|
||||||
|
type: 'parent' | 'student' // 家长答题或学生答题
|
||||||
|
score: number
|
||||||
|
totalQuestions: number
|
||||||
|
correctAnswers: number
|
||||||
|
timeSpent: number // 秒
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
export const mockUserInfo = {
|
||||||
|
id: '123456',
|
||||||
|
phone: '13800138000',
|
||||||
|
avatar: '/static/images/default-avatar.png',
|
||||||
|
child: {
|
||||||
|
id: '789',
|
||||||
|
name: '小明',
|
||||||
|
schoolId: 1,
|
||||||
|
schoolName: '第一小学',
|
||||||
|
gradeId: 3,
|
||||||
|
gradeName: '三年级',
|
||||||
|
classId: 5,
|
||||||
|
className: '2班',
|
||||||
|
seatNumber: 15,
|
||||||
|
totalScore: 850,
|
||||||
|
monthScore: 420,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 答题记录
|
||||||
|
export const mockQuizRecords: QuizRecord[] = [
|
||||||
|
{
|
||||||
|
id: 'r1',
|
||||||
|
date: '2024-03-15 10:30:00',
|
||||||
|
type: 'student',
|
||||||
|
score: 85,
|
||||||
|
totalQuestions: 10,
|
||||||
|
correctAnswers: 8,
|
||||||
|
timeSpent: 280,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r2',
|
||||||
|
date: '2024-03-14 19:45:00',
|
||||||
|
type: 'parent',
|
||||||
|
score: 95,
|
||||||
|
totalQuestions: 5,
|
||||||
|
correctAnswers: 5,
|
||||||
|
timeSpent: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r3',
|
||||||
|
date: '2024-03-13 20:15:00',
|
||||||
|
type: 'student',
|
||||||
|
score: 70,
|
||||||
|
totalQuestions: 10,
|
||||||
|
correctAnswers: 7,
|
||||||
|
timeSpent: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r4',
|
||||||
|
date: '2024-03-12 18:20:00',
|
||||||
|
type: 'parent',
|
||||||
|
score: 80,
|
||||||
|
totalQuestions: 5,
|
||||||
|
correctAnswers: 4,
|
||||||
|
timeSpent: 135,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r5',
|
||||||
|
date: '2024-03-11 16:50:00',
|
||||||
|
type: 'student',
|
||||||
|
score: 90,
|
||||||
|
totalQuestions: 10,
|
||||||
|
correctAnswers: 9,
|
||||||
|
timeSpent: 260,
|
||||||
|
},
|
||||||
|
]
|
||||||
48
src/pages-sub/demo/components/request.vue
Normal file
48
src/pages-sub/demo/components/request.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { IFooItem } from '@/api/foo'
|
||||||
|
import { getFooAPI } from '@/api/foo'
|
||||||
|
|
||||||
|
const recommendUrl = ref('http://laf.run/signup?code=ohaOgIX')
|
||||||
|
|
||||||
|
// const initialData = {
|
||||||
|
// name: 'initialData',
|
||||||
|
// id: '1234',
|
||||||
|
// }
|
||||||
|
const initialData = undefined
|
||||||
|
const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
|
||||||
|
immediate: true,
|
||||||
|
initialData,
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
data.value = initialData
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="p-6 text-center">
|
||||||
|
<view class="my-2 text-center">
|
||||||
|
<button type="primary" size="mini" class="w-160px" @click="run">
|
||||||
|
发送请求
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="h-16">
|
||||||
|
<view v-if="loading">
|
||||||
|
loading...
|
||||||
|
</view>
|
||||||
|
<block v-else>
|
||||||
|
<view class="text-xl">
|
||||||
|
请求数据如下
|
||||||
|
</view>
|
||||||
|
<view class="text-green leading-8">
|
||||||
|
{{ JSON.stringify(data) }}
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<view class="my-6 text-center">
|
||||||
|
<button type="warn" size="mini" class="w-160px" :disabled="!data" @click="reset">
|
||||||
|
重置数据
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
31
src/pages-sub/demo/index.vue
Normal file
31
src/pages-sub/demo/index.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
// code here
|
||||||
|
import RequestComp from './components/request.vue'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '分包页面',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="text-center">
|
||||||
|
<view class="m-8">
|
||||||
|
http://localhost:9000/#/pages-sub/demo/index
|
||||||
|
</view>
|
||||||
|
<view class="my-4 text-green-500">
|
||||||
|
分包页面demo
|
||||||
|
</view>
|
||||||
|
<view class="text-blue-500">
|
||||||
|
分包页面里面的components示例
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<RequestComp />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
//
|
||||||
|
</style>
|
||||||
149
src/pages/about/about.vue
Normal file
149
src/pages/about/about.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { isApp, isAppAndroid, isAppHarmony, isAppIOS, isAppPlus, isH5, isMpWeixin, isWeb } from '@uni-helper/uni-env'
|
||||||
|
import { LOGIN_PAGE } from '@/router/config'
|
||||||
|
import { useTokenStore } from '@/store'
|
||||||
|
import { tabbarStore } from '@/tabbar/store'
|
||||||
|
import RequestComp from './components/request.vue'
|
||||||
|
import VBindCss from './components/VBindCss.vue'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '关于',
|
||||||
|
},
|
||||||
|
// 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 arc/router 文件夹
|
||||||
|
excludeLoginPath: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
// 浏览器打印 isH5为true, isWeb为false,大家尽量用 isH5
|
||||||
|
console.log({ isApp, isAppAndroid, isAppHarmony, isAppIOS, isAppPlus, isH5, isMpWeixin, isWeb })
|
||||||
|
|
||||||
|
function gotoLogin() {
|
||||||
|
if (tokenStore.hasLogin) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已登录,不能去登录页',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages/about/about?a=1&b=2')}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function logout() {
|
||||||
|
// 清空用户信息
|
||||||
|
tokenStore.logout()
|
||||||
|
// 执行退出登录逻辑
|
||||||
|
uni.showToast({
|
||||||
|
title: '退出登录成功',
|
||||||
|
icon: 'success',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoTabbar() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #region setTabbarBadge
|
||||||
|
function setTabbarBadge() {
|
||||||
|
tabbarStore.setTabbarItemBadge(1, 100)
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
function gotoAlova() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/about/alova',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function gotoVueQuery() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/about/vue-query',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function gotoSubPage() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages-sub/demo/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// uniLayout里面的变量通过 expose 暴露出来后可以在 onReady 钩子获取到(onLoad 钩子不行)
|
||||||
|
const uniLayout = ref()
|
||||||
|
onLoad(() => {
|
||||||
|
console.log('onLoad:', uniLayout.value) // onLoad: undefined
|
||||||
|
})
|
||||||
|
onReady(() => {
|
||||||
|
console.log('onReady:', uniLayout.value) // onReady: Proxy(Object)
|
||||||
|
console.log('onReady:', uniLayout.value.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
|
||||||
|
})
|
||||||
|
// 结论:第一次通过onShow获取不到,但是可以通过 onReady获取到,后面就可以通过onShow获取到了
|
||||||
|
onShow(() => {
|
||||||
|
console.log('onShow:', uniLayout.value) // onReady: Proxy(Object)
|
||||||
|
console.log('onShow:', uniLayout.value?.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
|
||||||
|
})
|
||||||
|
|
||||||
|
const uniKuRoot = ref()
|
||||||
|
// 结论:(同上)第一次通过onShow获取不到,但是可以通过 onReady获取到,后面就可以通过onShow获取到了
|
||||||
|
onReady(() => {
|
||||||
|
console.log('onReady uniKuRoot exposeRef', uniKuRoot.value?.exposeRef)
|
||||||
|
})
|
||||||
|
onShow(() => {
|
||||||
|
console.log('onShow uniKuRoot exposeRef', uniKuRoot.value?.exposeRef)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template root="uniKuRoot">
|
||||||
|
<!-- page-meta 使用范例 -->
|
||||||
|
<page-meta page-style="overflow: auto" />
|
||||||
|
<view>
|
||||||
|
<view class="mt-8 text-center text-xl text-gray-400">
|
||||||
|
请求调用、unocss、static图片
|
||||||
|
</view>
|
||||||
|
<view class="my-2 text-center">
|
||||||
|
<image src="/static/images/avatar.jpg" class="h-100px w-100px" />
|
||||||
|
</view>
|
||||||
|
<view class="my-2 text-center">
|
||||||
|
当前是否登录:{{ tokenStore.hasLogin }}
|
||||||
|
</view>
|
||||||
|
<view class="m-auto max-w-600px flex items-center">
|
||||||
|
<button class="mt-4 w-40 text-center" @click="gotoLogin">
|
||||||
|
点击去登录页
|
||||||
|
</button>
|
||||||
|
<button class="mt-4 w-40 text-center" @click="logout">
|
||||||
|
点击退出登录
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<button class="mt-4 w-60 text-center" @click="setTabbarBadge">
|
||||||
|
设置tabbarBadge
|
||||||
|
</button>
|
||||||
|
<RequestComp />
|
||||||
|
<VBindCss />
|
||||||
|
<view class="mb-6 h-1px bg-#eee" />
|
||||||
|
<view class="text-center">
|
||||||
|
<button type="primary" size="mini" class="w-160px" @click="gotoAlova">
|
||||||
|
前往 alova 示例页面
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="text-center">
|
||||||
|
<button type="primary" size="mini" class="w-160px" @click="gotoTabbar">
|
||||||
|
切换tabbar
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="text-center">
|
||||||
|
<button type="primary" size="mini" class="w-160px" @click="gotoVueQuery">
|
||||||
|
vue-query 示例页面
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="text-center">
|
||||||
|
<button type="primary" size="mini" class="w-160px" @click="gotoSubPage">
|
||||||
|
前往分包页面
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="mt-6 text-center text-sm">
|
||||||
|
<view class="inline-block w-80% text-gray-400">
|
||||||
|
为了方便脚手架动态生成不同UI模板,本页的按钮统一使用UI库无关的原生button
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="h-6" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
53
src/pages/about/alova.vue
Normal file
53
src/pages/about/alova.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRequest } from 'alova/client'
|
||||||
|
import { foo } from '@/api/foo-alova'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: 'Alova 演示',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialData = undefined
|
||||||
|
|
||||||
|
const { loading, data, send } = useRequest(foo, {
|
||||||
|
initialData,
|
||||||
|
immediate: true,
|
||||||
|
})
|
||||||
|
console.log(data)
|
||||||
|
function reset() {
|
||||||
|
data.value = initialData
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="p-6 text-center">
|
||||||
|
<button type="primary" size="mini" class="my-6 w-160px" @click="send">
|
||||||
|
发送请求
|
||||||
|
</button>
|
||||||
|
<view class="h-16">
|
||||||
|
<view v-if="loading">
|
||||||
|
loading...
|
||||||
|
</view>
|
||||||
|
<block v-else>
|
||||||
|
<view class="text-xl">
|
||||||
|
请求数据如下
|
||||||
|
</view>
|
||||||
|
<view class="text-green leading-8">
|
||||||
|
{{ JSON.stringify(data) }}
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<view class="text-red">
|
||||||
|
{{ data?.id }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<button type="default" size="mini" class="my-6 w-160px" @click="reset">
|
||||||
|
重置数据
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
//
|
||||||
|
</style>
|
||||||
28
src/pages/about/components/VBindCss.vue
Normal file
28
src/pages/about/components/VBindCss.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
// root 插件更新到 1.3.4之后,都正常了。
|
||||||
|
const testBindCssVariable = ref('red')
|
||||||
|
function changeTestBindCssVariable() {
|
||||||
|
if (testBindCssVariable.value === 'red') {
|
||||||
|
testBindCssVariable.value = 'green'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
testBindCssVariable.value = 'red'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="mt-4 w-60 text-center" @click="changeTestBindCssVariable">
|
||||||
|
toggle v-bind css变量
|
||||||
|
</button>
|
||||||
|
<view class="test-css my-2 text-center">
|
||||||
|
测试v-bind css变量的具体文案
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.test-css {
|
||||||
|
color: v-bind(testBindCssVariable);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
src/pages/about/components/request.vue
Normal file
54
src/pages/about/components/request.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { IFooItem } from '@/api/foo'
|
||||||
|
import { getFooAPI } from '@/api/foo'
|
||||||
|
|
||||||
|
// const initialData = {
|
||||||
|
// name: 'initialData',
|
||||||
|
// id: '1234',
|
||||||
|
// }
|
||||||
|
const initialData = undefined
|
||||||
|
|
||||||
|
const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
|
||||||
|
immediate: true,
|
||||||
|
initialData,
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
data.value = initialData
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="p-6 text-center">
|
||||||
|
<view class="my-2">
|
||||||
|
pages 里面的 vue 文件会扫描成页面,将自动添加到 pages.json 里面。
|
||||||
|
</view>
|
||||||
|
<view class="my-2 text-green-400">
|
||||||
|
但是 pages/components 里面的 vue 不会。
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="my-4 text-center">
|
||||||
|
<button type="primary" size="mini" class="w-160px" @click="run">
|
||||||
|
发送请求
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view class="h-16">
|
||||||
|
<view v-if="loading">
|
||||||
|
loading...
|
||||||
|
</view>
|
||||||
|
<block v-else>
|
||||||
|
<view class="text-xl">
|
||||||
|
请求数据如下
|
||||||
|
</view>
|
||||||
|
<view class="text-green leading-8">
|
||||||
|
{{ JSON.stringify(data) }}
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<view class="my-4 text-center">
|
||||||
|
<button type="warn" size="mini" class="w-160px" :disabled="!data" @click="reset">
|
||||||
|
重置数据
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
50
src/pages/about/vue-query.vue
Normal file
50
src/pages/about/vue-query.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { foo } from '@/api/foo'
|
||||||
|
import { getFooQueryOptions } from '@/api/foo-vue-query'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: 'Vue Query 演示',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 简单使用
|
||||||
|
onShow(async () => {
|
||||||
|
const res = await foo()
|
||||||
|
console.log('res: ', res)
|
||||||
|
})
|
||||||
|
|
||||||
|
// vue-query 版
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
isLoading: loading,
|
||||||
|
refetch: send,
|
||||||
|
} = useQuery(getFooQueryOptions('菲鸽-vue-query'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="p-6 text-center">
|
||||||
|
<button type="primary" size="mini" class="my-6 w-160px" @click="send">
|
||||||
|
发送请求
|
||||||
|
</button>
|
||||||
|
<view class="h-16">
|
||||||
|
<view v-if="loading">
|
||||||
|
loading...
|
||||||
|
</view>
|
||||||
|
<block v-else>
|
||||||
|
<view class="text-xl">
|
||||||
|
请求数据如下
|
||||||
|
</view>
|
||||||
|
<view class="text-green leading-8">
|
||||||
|
{{ JSON.stringify(data) }}
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
//
|
||||||
|
</style>
|
||||||
257
src/pages/article/detail.vue
Normal file
257
src/pages/article/detail.vue
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "文章详情",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="article-detail-container">
|
||||||
|
<scroll-view class="article-content" scroll-y>
|
||||||
|
<!-- 文章头部 -->
|
||||||
|
<view class="article-header">
|
||||||
|
<text class="article-title">{{ article?.title }}</text>
|
||||||
|
<view class="article-meta">
|
||||||
|
<text class="publish-time">{{ formatDate(article?.publishTime) }}</text>
|
||||||
|
<text class="author">{{ article?.author }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 文章封面 -->
|
||||||
|
<view v-if="article?.coverImage" class="article-cover">
|
||||||
|
<image
|
||||||
|
:src="article.coverImage"
|
||||||
|
class="cover-image"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 文章正文 -->
|
||||||
|
<ZhuziCard class="content-card">
|
||||||
|
<view class="article-body" v-html="article?.content" />
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<!-- 相关推荐 -->
|
||||||
|
<view class="related-section">
|
||||||
|
<view class="section-title">
|
||||||
|
<uni-icons type="list" size="20" color="#2D5E3E" />
|
||||||
|
<text class="title-text">相关推荐</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="related-list">
|
||||||
|
<view
|
||||||
|
v-for="relatedArticle in relatedArticles"
|
||||||
|
:key="relatedArticle.id"
|
||||||
|
class="related-item"
|
||||||
|
@tap="goToArticle(relatedArticle.id)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
:src="relatedArticle.coverImage || '/static/logo.svg'"
|
||||||
|
class="related-image"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="related-content">
|
||||||
|
<text class="related-title">{{ relatedArticle.title }}</text>
|
||||||
|
<text class="related-author">{{ relatedArticle.author }}</text>
|
||||||
|
</view>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#999999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Article } from '@/mocks/article'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { mockCarouselArticles } from '@/mocks/article'
|
||||||
|
|
||||||
|
const article = ref<Article | null>(null)
|
||||||
|
const relatedArticles = ref<Article[]>([])
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(dateStr?: string) {
|
||||||
|
if (!dateStr)
|
||||||
|
return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到其他文章
|
||||||
|
function goToArticle(articleId: string) {
|
||||||
|
// 替换当前页面,避免页面栈过深
|
||||||
|
uni.redirectTo({
|
||||||
|
url: `/pages/article/detail?id=${articleId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载
|
||||||
|
onLoad((options: any) => {
|
||||||
|
const articleId = options.id
|
||||||
|
|
||||||
|
// 根据ID查找文章
|
||||||
|
const foundArticle = mockCarouselArticles.find(a => a.id === articleId)
|
||||||
|
|
||||||
|
if (foundArticle) {
|
||||||
|
article.value = foundArticle
|
||||||
|
|
||||||
|
// 加载相关文章(排除当前文章)
|
||||||
|
relatedArticles.value = mockCarouselArticles
|
||||||
|
.filter(a => a.id !== articleId)
|
||||||
|
.slice(0, 3)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '文章不存在',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 延迟返回上一页
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article-detail-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
|
||||||
|
.article-content {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.article-header {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.article-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.publish-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #daa520;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-cover {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(45, 94, 62, 0.15);
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.article-body {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #2f4f4f;
|
||||||
|
line-height: 1.8;
|
||||||
|
|
||||||
|
// 深度选择器,处理HTML内容样式
|
||||||
|
:deep(h2) {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin: 32rpx 0 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(h3) {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin: 24rpx 0 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(p) {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-section {
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-list {
|
||||||
|
.related-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(45, 94, 62, 0.1);
|
||||||
|
|
||||||
|
.related-image {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.related-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2f4f4f;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-author {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
483
src/pages/auth/bind-child.vue
Normal file
483
src/pages/auth/bind-child.vue
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "绑定孩子信息",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="bind-child-container">
|
||||||
|
<!-- 进度指示器 -->
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-step active">
|
||||||
|
<view class="step-number">
|
||||||
|
1
|
||||||
|
</view>
|
||||||
|
<text class="step-text">选择学校</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-line" :class="{ active: currentStep >= 2 }" />
|
||||||
|
<view class="progress-step" :class="{ active: currentStep >= 2 }">
|
||||||
|
<view class="step-number">
|
||||||
|
2
|
||||||
|
</view>
|
||||||
|
<text class="step-text">选择年级班级</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-line" :class="{ active: currentStep >= 3 }" />
|
||||||
|
<view class="progress-step" :class="{ active: currentStep >= 3 }">
|
||||||
|
<view class="step-number">
|
||||||
|
3
|
||||||
|
</view>
|
||||||
|
<text class="step-text">填写学生信息</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 绑定表单 -->
|
||||||
|
<ZhuziCard class="form-card" title="绑定孩子信息">
|
||||||
|
<!-- 步骤1:选择学校 -->
|
||||||
|
<view v-if="currentStep === 1" class="form-step">
|
||||||
|
<view class="step-title">
|
||||||
|
请选择孩子所在的学校
|
||||||
|
</view>
|
||||||
|
<picker
|
||||||
|
:value="selectedSchoolIndex"
|
||||||
|
:range="schoolNames"
|
||||||
|
@change="onSchoolChange"
|
||||||
|
>
|
||||||
|
<view class="picker-item">
|
||||||
|
<text>{{ formData.schoolName || '请选择学校' }}</text>
|
||||||
|
<uni-icons type="arrow-down" size="16" color="#999999" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 步骤2:选择年级班级 -->
|
||||||
|
<view v-if="currentStep === 2" class="form-step">
|
||||||
|
<view class="step-title">
|
||||||
|
请选择年级和班级
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 年级选择 -->
|
||||||
|
<picker
|
||||||
|
:value="selectedGradeIndex"
|
||||||
|
:range="gradeNames"
|
||||||
|
@change="onGradeChange"
|
||||||
|
>
|
||||||
|
<view class="picker-item">
|
||||||
|
<text>{{ formData.gradeName || '请选择年级' }}</text>
|
||||||
|
<uni-icons type="arrow-down" size="16" color="#999999" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<!-- 班级选择 -->
|
||||||
|
<picker
|
||||||
|
:value="selectedClassIndex"
|
||||||
|
:range="classNames"
|
||||||
|
:disabled="!formData.gradeId"
|
||||||
|
@change="onClassChange"
|
||||||
|
>
|
||||||
|
<view class="picker-item" :class="{ disabled: !formData.gradeId }">
|
||||||
|
<text>{{ formData.className || '请先选择年级' }}</text>
|
||||||
|
<uni-icons type="arrow-down" size="16" color="#999999" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 步骤3:填写学生信息 -->
|
||||||
|
<view v-if="currentStep === 3" class="form-step">
|
||||||
|
<view class="step-title">
|
||||||
|
请填写学生基本信息
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">学生姓名</text>
|
||||||
|
<input
|
||||||
|
v-model="formData.studentName"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入学生姓名"
|
||||||
|
maxlength="10"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">班内座号</text>
|
||||||
|
<input
|
||||||
|
v-model="seatNumber"
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入座号(防止重名)"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 信息确认 -->
|
||||||
|
<view class="info-confirm">
|
||||||
|
<view class="confirm-title">
|
||||||
|
请确认以下信息:
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">学校:</text>
|
||||||
|
<text class="info-value">{{ formData.schoolName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">年级:</text>
|
||||||
|
<text class="info-value">{{ formData.gradeName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">班级:</text>
|
||||||
|
<text class="info-value">{{ formData.className }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">姓名:</text>
|
||||||
|
<text class="info-value">{{ formData.studentName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">座号:</text>
|
||||||
|
<text class="info-value">{{ seatNumber }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="button-group">
|
||||||
|
<AncientButton
|
||||||
|
v-if="currentStep > 1"
|
||||||
|
class="btn secondary"
|
||||||
|
@click="prevStep"
|
||||||
|
>
|
||||||
|
上一步
|
||||||
|
</AncientButton>
|
||||||
|
|
||||||
|
<AncientButton
|
||||||
|
class="btn primary"
|
||||||
|
:disabled="!canProceed || isSubmitting"
|
||||||
|
@click="nextStep"
|
||||||
|
>
|
||||||
|
{{ currentStep === 3 ? (isSubmitting ? '绑定中...' : '完成绑定') : '下一步' }}
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { mockClasses, mockGrades, mockSchools } from '@/mocks/auth'
|
||||||
|
|
||||||
|
const currentStep = ref(1)
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref({
|
||||||
|
schoolId: '',
|
||||||
|
schoolName: '',
|
||||||
|
gradeId: '',
|
||||||
|
gradeName: '',
|
||||||
|
classId: '',
|
||||||
|
className: '',
|
||||||
|
studentName: '',
|
||||||
|
seatNumber: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const seatNumber = ref('')
|
||||||
|
|
||||||
|
// 选择索引
|
||||||
|
const selectedSchoolIndex = ref(0)
|
||||||
|
const selectedGradeIndex = ref(0)
|
||||||
|
const selectedClassIndex = ref(0)
|
||||||
|
|
||||||
|
// 数据源
|
||||||
|
const schoolNames = computed(() => mockSchools.map(school => school.name))
|
||||||
|
const gradeNames = computed(() => mockGrades.map(grade => grade.name))
|
||||||
|
const classNames = computed(() => {
|
||||||
|
if (!formData.value.gradeId)
|
||||||
|
return []
|
||||||
|
return mockClasses
|
||||||
|
.filter(cls => cls.gradeId === Number(formData.value.gradeId))
|
||||||
|
.map(cls => cls.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否可以继续下一步
|
||||||
|
const canProceed = computed(() => {
|
||||||
|
switch (currentStep.value) {
|
||||||
|
case 1:
|
||||||
|
return !!formData.value.schoolId
|
||||||
|
case 2:
|
||||||
|
return !!formData.value.gradeId && !!formData.value.classId
|
||||||
|
case 3:
|
||||||
|
return !!formData.value.studentName && !!seatNumber.value
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 学校选择
|
||||||
|
function onSchoolChange(e: any) {
|
||||||
|
const index = e.detail.value
|
||||||
|
selectedSchoolIndex.value = index
|
||||||
|
const school = mockSchools[index]
|
||||||
|
formData.value.schoolId = school.id.toString()
|
||||||
|
formData.value.schoolName = school.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 年级选择
|
||||||
|
function onGradeChange(e: any) {
|
||||||
|
const index = e.detail.value
|
||||||
|
selectedGradeIndex.value = index
|
||||||
|
const grade = mockGrades[index]
|
||||||
|
formData.value.gradeId = grade.id.toString()
|
||||||
|
formData.value.gradeName = grade.name
|
||||||
|
// 重置班级选择
|
||||||
|
formData.value.classId = ''
|
||||||
|
formData.value.className = ''
|
||||||
|
selectedClassIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 班级选择
|
||||||
|
function onClassChange(e: any) {
|
||||||
|
const index = e.detail.value
|
||||||
|
selectedClassIndex.value = index
|
||||||
|
const availableClasses = mockClasses.filter(cls => cls.gradeId === Number(formData.value.gradeId))
|
||||||
|
const cls = availableClasses[index]
|
||||||
|
formData.value.classId = cls.id.toString()
|
||||||
|
formData.value.className = cls.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一步
|
||||||
|
async function nextStep() {
|
||||||
|
if (currentStep.value < 3) {
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 第3步,提交绑定
|
||||||
|
await handleSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上一步
|
||||||
|
function prevStep() {
|
||||||
|
if (currentStep.value > 1) {
|
||||||
|
currentStep.value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交绑定
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
// 设置座号
|
||||||
|
formData.value.seatNumber = Number(seatNumber.value)
|
||||||
|
|
||||||
|
// 模拟绑定请求
|
||||||
|
await simulateBindChild()
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '绑定成功!',
|
||||||
|
icon: 'success',
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index',
|
||||||
|
})
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('绑定失败:', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: '绑定失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟绑定请求
|
||||||
|
function simulateBindChild(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (Math.random() > 0.1) {
|
||||||
|
// 保存孩子信息到本地存储
|
||||||
|
uni.setStorageSync('childInfo', formData.value)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reject(new Error('绑定失败'))
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bind-child-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
|
||||||
|
.progress-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #cccccc;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active .step-number {
|
||||||
|
background: #2d5e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-line {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 2rpx;
|
||||||
|
background: #cccccc;
|
||||||
|
margin: 0 16rpx -24rpx;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.form-step {
|
||||||
|
.step-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item {
|
||||||
|
border: 2rpx solid #e6e6e6;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #2d5e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2f4f4f;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 2rpx solid #e6e6e6;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
background: white;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-confirm {
|
||||||
|
background: rgba(45, 94, 62, 0.05);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
|
||||||
|
.confirm-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
min-width: 120rpx;
|
||||||
|
color: #696969;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #2f4f4f;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
color: #2d5e3e;
|
||||||
|
border-color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
265
src/pages/auth/login.vue
Normal file
265
src/pages/auth/login.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="login-container">
|
||||||
|
<!-- 背景装饰 -->
|
||||||
|
<view class="bg-decoration">
|
||||||
|
<!-- <image src="/static/images/bamboo-bg.png" class="bamboo-bg-img" mode="aspectFill" /> -->
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 顶部标题区 -->
|
||||||
|
<view class="header-section">
|
||||||
|
<view class="logo-area">
|
||||||
|
<image src="/static/logo.svg" class="logo" mode="aspectFit" />
|
||||||
|
<text class="app-title">朱子文化学习</text>
|
||||||
|
<text class="app-subtitle">传承千年智慧,弘扬文化精神</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录卡片 -->
|
||||||
|
<ZhuziCard class="login-card" title="微信快速登录">
|
||||||
|
<view class="login-content">
|
||||||
|
<view class="welcome-text">
|
||||||
|
<text class="main-text">欢迎回来!</text>
|
||||||
|
<text class="sub-text">继续您的朱子文化学习之旅</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 微信登录按钮 -->
|
||||||
|
<AncientButton
|
||||||
|
class="wx-login-btn"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="handleWxLogin"
|
||||||
|
>
|
||||||
|
<uni-icons type="weixin" size="24" color="#FFFFFF" class="wx-icon" />
|
||||||
|
<text>{{ isLoading ? '登录中...' : '微信授权登录' }}</text>
|
||||||
|
</AncientButton>
|
||||||
|
|
||||||
|
<!-- 登录说明 -->
|
||||||
|
<view class="login-tips">
|
||||||
|
<text class="tips-text">
|
||||||
|
首次登录需要绑定孩子信息
|
||||||
|
<text class="highlight">点击上方按钮开始</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<!-- 底部装饰 -->
|
||||||
|
<view class="footer-decoration">
|
||||||
|
<text class="ancient-text">「学而时习之,不亦说乎」</text>
|
||||||
|
<text class="author-text">—— 朱子语录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { mockLoginResponse } from '@/mocks/auth'
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// 微信登录处理
|
||||||
|
async function handleWxLogin() {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
// 模拟微信登录
|
||||||
|
await simulateWxLogin()
|
||||||
|
|
||||||
|
// 登录成功后跳转
|
||||||
|
const hasChild = mockLoginResponse.data.hasChild
|
||||||
|
if (hasChild) {
|
||||||
|
// 已绑定孩子,跳转首页(首页是tabbar页面,使用switchTab)
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 未绑定孩子,跳转绑定页面
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/auth/bind-child',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: '登录失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟微信登录
|
||||||
|
function simulateWxLogin(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 模拟网络请求
|
||||||
|
setTimeout(() => {
|
||||||
|
if (Math.random() > 0.1) { // 90% 成功率
|
||||||
|
// 保存用户信息到本地存储
|
||||||
|
uni.setStorageSync('userToken', mockLoginResponse.data.token)
|
||||||
|
uni.setStorageSync('userInfo', mockLoginResponse.data)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reject(new Error('登录失败'))
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查登录状态
|
||||||
|
onLoad(() => {
|
||||||
|
const token = uni.getStorageSync('userToken')
|
||||||
|
if (token) {
|
||||||
|
// 已登录,直接跳转首页(首页是tabbar页面,使用switchTab)
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1e3a8a 0%, #6366f1 100%);
|
||||||
|
position: relative;
|
||||||
|
padding: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.bg-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 0.1;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
.bamboo-bg-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.logo-area {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-subtitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
margin: 60rpx 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.main-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wx-login-btn {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.wx-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tips {
|
||||||
|
.tips-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: #daa520;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-decoration {
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: auto;
|
||||||
|
|
||||||
|
.ancient-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
692
src/pages/index/index.vue
Normal file
692
src/pages/index/index.vue
Normal file
@@ -0,0 +1,692 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="homepage-container">
|
||||||
|
<!-- 自定义导航栏 -->
|
||||||
|
<view class="custom-navbar">
|
||||||
|
<view class="navbar-content">
|
||||||
|
<view class="navbar-title">
|
||||||
|
<image src="/static/logo.svg" class="title-icon" mode="aspectFit" />
|
||||||
|
<text class="title-text">朱子文化</text>
|
||||||
|
</view>
|
||||||
|
<view class="navbar-subtitle">
|
||||||
|
传承千年智慧
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 页面内容 -->
|
||||||
|
<scroll-view class="page-content" scroll-y>
|
||||||
|
<!-- 轮播图区域 -->
|
||||||
|
<view class="carousel-section">
|
||||||
|
<swiper
|
||||||
|
class="carousel-swiper"
|
||||||
|
:indicator-dots="true"
|
||||||
|
:autoplay="true"
|
||||||
|
:interval="3000"
|
||||||
|
:duration="500"
|
||||||
|
:circular="true"
|
||||||
|
indicator-color="rgba(255, 255, 255, 0.5)"
|
||||||
|
indicator-active-color="#DAA520"
|
||||||
|
@change="onSwiperChange"
|
||||||
|
>
|
||||||
|
<swiper-item
|
||||||
|
v-for="(article, index) in carouselArticles"
|
||||||
|
:key="article.id"
|
||||||
|
@tap="goToArticleDetail(article.id)"
|
||||||
|
>
|
||||||
|
<view class="carousel-item">
|
||||||
|
<image
|
||||||
|
:src="article.coverImage || '/static/logo.svg'"
|
||||||
|
class="carousel-image"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="carousel-overlay">
|
||||||
|
<text class="carousel-title">{{ article.title }}</text>
|
||||||
|
<text class="carousel-author">{{ article.author }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 答题入口区域 -->
|
||||||
|
<view class="quiz-section">
|
||||||
|
<view class="section-title">
|
||||||
|
<uni-icons type="compose" size="24" color="#2D5E3E" />
|
||||||
|
<text class="title-text">开始学习</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="quiz-cards">
|
||||||
|
<!-- 家长答题卡片 -->
|
||||||
|
<ZhuziCard class="quiz-card parent-card ancient-card-enhanced ancient-float" @tap="goToParentQuiz">
|
||||||
|
<view class="card-header">
|
||||||
|
<view class="icon-container">
|
||||||
|
<image src="/static/logo.svg" class="quiz-icon" mode="aspectFit" />
|
||||||
|
<view class="icon-decoration" />
|
||||||
|
</view>
|
||||||
|
<view class="title-container">
|
||||||
|
<text class="card-title ancient-text calligraphy">家长答题</text>
|
||||||
|
<text class="card-subtitle">Parent Quiz</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<view class="quiz-details">
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-icon">◉</text>
|
||||||
|
<text class="quiz-desc">5道题目,每题30秒</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-icon">◈</text>
|
||||||
|
<text class="quiz-reward">积分将累积到孩子身上</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ancient-pattern">
|
||||||
|
<text class="pattern-text">※ 博学之,审问之,慎思之 ※</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-footer">
|
||||||
|
<AncientButton class="start-btn ancient-btn-enhanced">
|
||||||
|
<text class="btn-text">开始答题</text>
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<!-- 学生答题卡片 -->
|
||||||
|
<ZhuziCard class="quiz-card student-card ancient-card-enhanced ancient-float" @tap="goToStudentQuiz">
|
||||||
|
<view class="card-header">
|
||||||
|
<view class="icon-container">
|
||||||
|
<image src="/static/logo.svg" class="quiz-icon" mode="aspectFit" />
|
||||||
|
<view class="icon-decoration" />
|
||||||
|
</view>
|
||||||
|
<view class="title-container">
|
||||||
|
<text class="card-title ancient-text calligraphy">学生答题</text>
|
||||||
|
<text class="card-subtitle">Student Quiz</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<view class="quiz-details">
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-icon">◉</text>
|
||||||
|
<text class="quiz-desc">10道题目,每题30秒</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-icon">◈</text>
|
||||||
|
<text class="quiz-reward">答对获得相应积分奖励</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ancient-pattern">
|
||||||
|
<text class="pattern-text">※ 学而时习之,不亦说乎 ※</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-footer">
|
||||||
|
<AncientButton class="start-btn ancient-btn-enhanced secondary">
|
||||||
|
<text class="btn-text">开始答题</text>
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 朱子介绍区域 -->
|
||||||
|
<view class="intro-section">
|
||||||
|
<view class="section-title">
|
||||||
|
<uni-icons type="person-filled" size="24" color="#2D5E3E" />
|
||||||
|
<text class="title-text">朱子简介</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<ZhuziCard class="intro-card">
|
||||||
|
<view class="intro-content">
|
||||||
|
<view class="intro-header" @tap="toggleIntroExpand">
|
||||||
|
<text class="intro-name">朱熹(1130-1200)</text>
|
||||||
|
<uni-icons
|
||||||
|
:type="introExpanded ? 'up' : 'down'"
|
||||||
|
size="16"
|
||||||
|
color="#696969"
|
||||||
|
class="expand-icon"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="intro-summary">
|
||||||
|
字元晦、仲晦,号晦庵、晦翁,别号紫阳,世称朱文公。我国古代伟大的思想家、哲学家、教育家、文学家。
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 可展开的详细内容 -->
|
||||||
|
<view
|
||||||
|
v-if="introExpanded"
|
||||||
|
class="intro-detail"
|
||||||
|
:class="{ show: introExpanded }"
|
||||||
|
>
|
||||||
|
<text class="detail-text">{{ zhuziIntroduction }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="intro-quote">
|
||||||
|
<text class="quote-text">「学而时习之,不亦说乎」</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部装饰 -->
|
||||||
|
<view class="footer-decoration">
|
||||||
|
<view class="bamboo-decoration" />
|
||||||
|
<text class="footer-text">弘扬朱子文化,传承千年智慧</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { mockCarouselArticles, zhuziIntroduction } from '@/mocks/article'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const carouselArticles = ref(mockCarouselArticles)
|
||||||
|
const currentSwiperIndex = ref(0)
|
||||||
|
const introExpanded = ref(false)
|
||||||
|
|
||||||
|
// 轮播图切换
|
||||||
|
function onSwiperChange(e: any) {
|
||||||
|
currentSwiperIndex.value = e.detail.current
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到文章详情
|
||||||
|
function goToArticleDetail(articleId: string) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/article/detail?id=${articleId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到家长答题
|
||||||
|
function goToParentQuiz() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/quiz/parent-quiz',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到学生答题
|
||||||
|
function goToStudentQuiz() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/quiz/student-quiz',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换朱子介绍展开状态
|
||||||
|
function toggleIntroExpand() {
|
||||||
|
introExpanded.value = !introExpanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查登录状态
|
||||||
|
onLoad(() => {
|
||||||
|
// 模拟检查用户登录状态
|
||||||
|
const token = uni.getStorageSync('userToken')
|
||||||
|
if (!token) {
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/auth/login',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录,可以加载用户相关数据
|
||||||
|
console.log('用户已登录,加载首页数据')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.homepage-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8f5f0 0%, #d8d0c0 100%);
|
||||||
|
|
||||||
|
.custom-navbar {
|
||||||
|
background: linear-gradient(135deg, #8e3d3e 0%, #a76363 100%);
|
||||||
|
padding: calc(var(--status-bar-height, 0px) + 16rpx) 32rpx 16rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(45, 94, 62, 0.15);
|
||||||
|
|
||||||
|
.navbar-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.navbar-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
height: calc(100vh - 120rpx);
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #8e3d3e;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
|
||||||
|
// 古风标题装饰
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2rpx;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2rpx;
|
||||||
|
background: linear-gradient(135deg, #d9c8a3 0%, #b8824f 100%);
|
||||||
|
border-radius: 1rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮播图区域
|
||||||
|
.carousel-section {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.carousel-swiper {
|
||||||
|
height: 400rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(45, 94, 62, 0.2);
|
||||||
|
|
||||||
|
.carousel-item {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.carousel-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||||
|
padding: 48rpx 32rpx 32rpx;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.carousel-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-author {
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 答题区域
|
||||||
|
.quiz-section {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.quiz-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.quiz-card {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-8rpx);
|
||||||
|
box-shadow: 0 12rpx 32rpx rgba(45, 94, 62, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
|
||||||
|
.quiz-icon {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid rgba(142, 61, 62, 0.2);
|
||||||
|
padding: 8rpx;
|
||||||
|
background: rgba(217, 200, 163, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: -4rpx;
|
||||||
|
right: -4rpx;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
background: #d9c8a3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #8e3d3e;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999999;
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.quiz-details {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
.detail-icon {
|
||||||
|
font-size: 12rpx;
|
||||||
|
color: #d9c8a3;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
width: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-desc {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-reward {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ancient-pattern {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16rpx;
|
||||||
|
background: rgba(217, 200, 163, 0.08);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: 1rpx dashed rgba(217, 200, 163, 0.3);
|
||||||
|
|
||||||
|
.pattern-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #7a9e7e;
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
.start-btn {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 20rpx 32rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 古风卡片设计 - 移除所有边框
|
||||||
|
.parent-card {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(167, 99, 99, 0.06) 0%,
|
||||||
|
rgba(217, 200, 163, 0.08) 50%,
|
||||||
|
rgba(255, 255, 255, 0.95) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 32rpx rgba(167, 99, 99, 0.12),
|
||||||
|
0 4rpx 12rpx rgba(167, 99, 99, 0.08);
|
||||||
|
border: 1rpx solid rgba(167, 99, 99, 0.1);
|
||||||
|
|
||||||
|
.ancient-pattern {
|
||||||
|
border-color: rgba(167, 99, 99, 0.2);
|
||||||
|
background: rgba(167, 99, 99, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-card {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(74, 115, 84, 0.06) 0%,
|
||||||
|
rgba(122, 158, 126, 0.08) 50%,
|
||||||
|
rgba(255, 255, 255, 0.95) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 32rpx rgba(74, 115, 84, 0.12),
|
||||||
|
0 4rpx 12rpx rgba(74, 115, 84, 0.08);
|
||||||
|
border: 1rpx solid rgba(74, 115, 84, 0.1);
|
||||||
|
|
||||||
|
.ancient-pattern {
|
||||||
|
border-color: rgba(74, 115, 84, 0.2);
|
||||||
|
background: rgba(74, 115, 84, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 朱子介绍区域
|
||||||
|
.intro-section {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.intro-card {
|
||||||
|
.intro-content {
|
||||||
|
.intro-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.intro-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #8e3d3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-summary {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-detail {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
max-height: 1000rpx;
|
||||||
|
opacity: 1;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-quote {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
background: rgba(217, 200, 163, 0.15);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1rpx dashed #d9c8a3;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// 古风引用装饰
|
||||||
|
&::before {
|
||||||
|
content: '"';
|
||||||
|
position: absolute;
|
||||||
|
top: 8rpx;
|
||||||
|
left: 16rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #d9c8a3;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '"';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #d9c8a3;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #8e3d3e;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部装饰
|
||||||
|
.footer-decoration {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48rpx 0;
|
||||||
|
|
||||||
|
.bamboo-decoration {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
background: linear-gradient(90deg, #8e3d3e 0%, #d9c8a3 30%, #7a9e7e 60%, #8e3d3e 100%);
|
||||||
|
margin: 0 auto 24rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// 古风竹节装饰
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2rpx;
|
||||||
|
left: 30%;
|
||||||
|
width: 2rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
background: #7a9e7e;
|
||||||
|
border-radius: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2rpx;
|
||||||
|
right: 30%;
|
||||||
|
width: 2rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
background: #7a9e7e;
|
||||||
|
border-radius: 1rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666666;
|
||||||
|
font-style: italic;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// 古风装饰符
|
||||||
|
&::before {
|
||||||
|
content: '※';
|
||||||
|
margin-right: 8rpx;
|
||||||
|
color: #d9c8a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '※';
|
||||||
|
margin-left: 8rpx;
|
||||||
|
color: #d9c8a3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 适配不同屏幕尺寸
|
||||||
|
@media screen and (max-width: 750rpx) {
|
||||||
|
.quiz-cards {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.quiz-card {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
src/pages/login/README.md
Normal file
20
src/pages/login/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` 里面,默认会在登录后自动重定向到来源/配置的页面。
|
||||||
|
|
||||||
|
如果与您的业务不符,您可以自行修改。
|
||||||
102
src/pages/login/login.vue
Normal file
102
src/pages/login/login.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useTokenStore } from '@/store/token'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { tabbarList } from '@/tabbar/config'
|
||||||
|
import { isPageTabbar } from '@/tabbar/store'
|
||||||
|
import { ensureDecodeURIComponent } from '@/utils'
|
||||||
|
import { parseUrlToObj } from '@/utils/index'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '登录',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const redirectUrl = ref('')
|
||||||
|
onLoad((options) => {
|
||||||
|
console.log('login options: ', options)
|
||||||
|
if (options.redirect) {
|
||||||
|
redirectUrl.value = ensureDecodeURIComponent(options.redirect)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
redirectUrl.value = tabbarList[0].pagePath
|
||||||
|
}
|
||||||
|
console.log('redirectUrl.value: ', redirectUrl.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
async function doLogin() {
|
||||||
|
if (tokenStore.hasLogin) {
|
||||||
|
uni.navigateBack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口(各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
|
||||||
|
// 1/2 调用接口回来后设置token信息
|
||||||
|
// 这里用单token来模拟
|
||||||
|
tokenStore.setTokenInfo({
|
||||||
|
token: '123456',
|
||||||
|
expiresIn: 60 * 60 * 24 * 7,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2/2 调用接口回来后设置用户信息
|
||||||
|
// const res = await login({
|
||||||
|
// username: '菲鸽',
|
||||||
|
// password: '123456',
|
||||||
|
// })
|
||||||
|
// console.log('接口拿到的登录信息:', res)
|
||||||
|
userStore.setUserInfo({
|
||||||
|
userId: 123456,
|
||||||
|
username: 'abc123456',
|
||||||
|
nickname: '菲鸽',
|
||||||
|
avatar: 'https://oss.laf.run/ukw0y1-site/avatar.jpg',
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(redirectUrl.value)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log('登录失败', error)
|
||||||
|
}
|
||||||
|
let path = redirectUrl.value
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
path = `/${path}`
|
||||||
|
}
|
||||||
|
const { path: _path, query } = parseUrlToObj(path)
|
||||||
|
console.log('_path:', _path, 'query:', query, 'path:', path)
|
||||||
|
console.log('isPageTabbar(_path):', isPageTabbar(_path))
|
||||||
|
if (isPageTabbar(_path)) {
|
||||||
|
// 经过我的测试 switchTab 不能带 query 参数, 不管是放到 url 还是放到 query ,
|
||||||
|
// 最后跳转过去的时候都会丢失 query 信息
|
||||||
|
uni.switchTab({
|
||||||
|
url: path,
|
||||||
|
})
|
||||||
|
// uni.switchTab({
|
||||||
|
// url: _path,
|
||||||
|
// query,
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('redirectTo:', path)
|
||||||
|
uni.redirectTo({
|
||||||
|
url: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="login">
|
||||||
|
<!-- 本页面是非MP的登录页,主要用于 h5 和 APP -->
|
||||||
|
<view class="text-center">
|
||||||
|
登录页
|
||||||
|
</view>
|
||||||
|
<button class="mt-4 w-40 text-center" @click="doLogin">
|
||||||
|
点击模拟登录
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
//
|
||||||
|
</style>
|
||||||
34
src/pages/login/register.vue
Normal file
34
src/pages/login/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>
|
||||||
515
src/pages/me/me.vue
Normal file
515
src/pages/me/me.vue
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="me-container">
|
||||||
|
<!-- 用户信息卡片 -->
|
||||||
|
<view class="user-section">
|
||||||
|
<view class="user-card">
|
||||||
|
<image
|
||||||
|
:src="userInfo.avatar || '/static/images/default-avatar.png'"
|
||||||
|
class="user-avatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
@tap="previewAvatar"
|
||||||
|
/>
|
||||||
|
<view class="user-info">
|
||||||
|
<text class="user-name">{{ userInfo.child?.name || '小朋友' }}</text>
|
||||||
|
<text class="user-phone">{{ formatPhone(userInfo.phone) }}</text>
|
||||||
|
<view class="school-badge">
|
||||||
|
<uni-icons type="home" size="12" color="#FFFFFF" />
|
||||||
|
<text class="school-text">{{ userInfo.child?.schoolName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="class-info">
|
||||||
|
<text class="class-text">{{ userInfo.child?.gradeName }} {{ userInfo.child?.className }} {{ userInfo.child?.seatNumber }}号</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="edit-btn" @tap="editProfile">
|
||||||
|
<uni-icons type="compose" size="20" color="#DAA520" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 积分统计 -->
|
||||||
|
<view class="score-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ userInfo.child?.totalScore || 0 }}</text>
|
||||||
|
<text class="stat-label">总积分</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-divider" />
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ userInfo.child?.monthScore || 0 }}</text>
|
||||||
|
<text class="stat-label">本月积分</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-divider" />
|
||||||
|
<view class="stat-item" @tap="goToRanking">
|
||||||
|
<text class="stat-value">{{ currentRank }}</text>
|
||||||
|
<text class="stat-label">班级排名</text>
|
||||||
|
<uni-icons type="arrowright" size="12" color="#DAA520" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 功能菜单 -->
|
||||||
|
<view class="menu-section">
|
||||||
|
<ZhuziCard class="menu-card" title="学习中心">
|
||||||
|
<view class="menu-list">
|
||||||
|
<view class="menu-item" @tap="goToQuizRecords">
|
||||||
|
<view class="menu-icon quiz-icon">
|
||||||
|
<uni-icons type="compose" size="20" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
<text class="menu-text">答题记录</text>
|
||||||
|
<view v-if="quizRecordCount > 0" class="menu-badge">
|
||||||
|
<text class="badge-text">{{ quizRecordCount }}</text>
|
||||||
|
</view>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @tap="goToRanking">
|
||||||
|
<view class="menu-icon ranking-icon">
|
||||||
|
<uni-icons type="star" size="20" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
<text class="menu-text">排行榜</text>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @tap="goToHome">
|
||||||
|
<view class="menu-icon home-icon">
|
||||||
|
<uni-icons type="home" size="20" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
<text class="menu-text">返回首页</text>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<ZhuziCard class="menu-card" title="设置中心">
|
||||||
|
<view class="menu-list">
|
||||||
|
<view class="menu-item" @tap="shareApp">
|
||||||
|
<view class="menu-icon share-icon">
|
||||||
|
<uni-icons type="redo" size="20" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
<text class="menu-text">分享给朋友</text>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @tap="showAbout">
|
||||||
|
<view class="menu-icon about-icon">
|
||||||
|
<uni-icons type="info" size="20" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
<text class="menu-text">关于朱子文化</text>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @tap="contactUs">
|
||||||
|
<view class="menu-icon contact-icon">
|
||||||
|
<uni-icons type="phone" size="20" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
<text class="menu-text">联系我们</text>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 朱子文化介绍 -->
|
||||||
|
<view class="culture-section">
|
||||||
|
<ZhuziCard class="culture-card">
|
||||||
|
<view class="culture-content">
|
||||||
|
<view class="culture-header">
|
||||||
|
<image src="/static/logo.svg" class="culture-icon" mode="aspectFit" />
|
||||||
|
<text class="culture-title">朱子文化学习</text>
|
||||||
|
</view>
|
||||||
|
<text class="culture-desc">传承千年智慧,弘扬文化精神</text>
|
||||||
|
<view class="culture-quote">
|
||||||
|
<text class="quote-text">「学而时习之,不亦说乎」</text>
|
||||||
|
<text class="quote-author">—— 《论语》</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 版本信息 -->
|
||||||
|
<view class="version-section">
|
||||||
|
<text class="version-text">版本 v1.0.0</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { mockUserInfo } from '@/mocks/user'
|
||||||
|
|
||||||
|
const userInfo = ref(mockUserInfo)
|
||||||
|
const quizRecordCount = ref(0)
|
||||||
|
|
||||||
|
// 当前排名
|
||||||
|
const currentRank = computed(() => {
|
||||||
|
// 这里可以从真实的排行榜数据中获取
|
||||||
|
return '第5名'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化手机号
|
||||||
|
function formatPhone(phone?: string) {
|
||||||
|
if (!phone)
|
||||||
|
return '未绑定手机号'
|
||||||
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览头像
|
||||||
|
function previewAvatar() {
|
||||||
|
const avatarUrl = userInfo.value.avatar || '/static/images/default-avatar.png'
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [avatarUrl],
|
||||||
|
current: avatarUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑资料
|
||||||
|
function editProfile() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '编辑资料',
|
||||||
|
content: '此功能暂未开放',
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到答题记录
|
||||||
|
function goToQuizRecords() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/me/quiz-records',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到排行榜
|
||||||
|
function goToRanking() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/ranking/ranking',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
function goToHome() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享应用
|
||||||
|
function shareApp() {
|
||||||
|
uni.showShareMenu({
|
||||||
|
withShareTicket: true,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请点击右上角分享',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关于朱子文化
|
||||||
|
function showAbout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '朱子文化',
|
||||||
|
content: '朱熹(1130-1200),我国古代伟大的思想家、哲学家、教育家、文学家。素有"北孔南朱"之称,后世尊称为"朱子"。',
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 联系我们
|
||||||
|
function contactUs() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['客服电话', '官方邮箱', '意见反馈'],
|
||||||
|
success: (res) => {
|
||||||
|
const actions = [
|
||||||
|
() => uni.makePhoneCall({ phoneNumber: '400-123-4567' }),
|
||||||
|
() => uni.setClipboardData({ data: 'contact@zhuzi-culture.com' }),
|
||||||
|
() => uni.navigateTo({ url: '/pages/feedback/feedback' }),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (actions[res.tapIndex]) {
|
||||||
|
actions[res.tapIndex]()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取答题记录数量
|
||||||
|
const records = uni.getStorageSync('quizRecords') || []
|
||||||
|
quizRecordCount.value = records.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面分享配置
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: '朱子文化学习 - 传承千年智慧',
|
||||||
|
path: '/pages/index/index',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.me-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
|
||||||
|
.user-section {
|
||||||
|
padding: 32rpx 32rpx 16rpx;
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(45, 94, 62, 0.15);
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
border: 4rpx solid #daa520;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-phone {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
background: #2d5e3e;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
|
||||||
|
.school-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-info {
|
||||||
|
.class-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #daa520;
|
||||||
|
background: rgba(218, 165, 32, 0.1);
|
||||||
|
padding: 4rpx 8rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
right: 24rpx;
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(218, 165, 32, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-stats {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(45, 94, 62, 0.1);
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #daa520;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background: #e6e6e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-section {
|
||||||
|
padding: 16rpx 32rpx;
|
||||||
|
|
||||||
|
.menu-card {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
|
||||||
|
background: linear-gradient(135deg, #4ecdc4 0%, #7fdbda 100%);
|
||||||
|
}
|
||||||
|
&.ranking-icon {
|
||||||
|
background: linear-gradient(135deg, #ffd93d 0%, #ff6b6b 100%);
|
||||||
|
}
|
||||||
|
&.home-icon {
|
||||||
|
background: linear-gradient(135deg, #6c63ff 0%, #9c88ff 100%);
|
||||||
|
}
|
||||||
|
&.share-icon {
|
||||||
|
background: linear-gradient(135deg, #ff9500 0%, #ffad33 100%);
|
||||||
|
}
|
||||||
|
&.about-icon {
|
||||||
|
background: linear-gradient(135deg, #2d5e3e 0%, #4a7b5c 100%);
|
||||||
|
}
|
||||||
|
&.contact-icon {
|
||||||
|
background: linear-gradient(135deg, #ff6b9d 0%, #ff8cc8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2f4f4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-badge {
|
||||||
|
background: #ff4444;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 2rpx 8rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.culture-section {
|
||||||
|
padding: 0 32rpx 16rpx;
|
||||||
|
|
||||||
|
.culture-card {
|
||||||
|
.culture-content {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.culture-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
.culture-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.culture-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.culture-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.culture-quote {
|
||||||
|
background: rgba(218, 165, 32, 0.1);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
border: 1rpx dashed #daa520;
|
||||||
|
|
||||||
|
.quote-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #2d5e3e;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-author {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #daa520;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-section {
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.version-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
576
src/pages/me/quiz-records.vue
Normal file
576
src/pages/me/quiz-records.vue
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "答题记录",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="quiz-records-container">
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<view class="stats-section">
|
||||||
|
<ZhuziCard class="stats-card">
|
||||||
|
<view class="stats-content">
|
||||||
|
<view class="stats-grid">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ totalRecords }}</text>
|
||||||
|
<text class="stat-label">总答题次数</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ totalScore }}</text>
|
||||||
|
<text class="stat-label">累计得分</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ averageScore.toFixed(1) }}</text>
|
||||||
|
<text class="stat-label">平均得分</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-number">{{ accuracyRate }}%</text>
|
||||||
|
<text class="stat-label">平均正确率</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 筛选选项 -->
|
||||||
|
<view class="filter-section">
|
||||||
|
<view class="filter-tabs">
|
||||||
|
<view
|
||||||
|
v-for="filter in filterOptions"
|
||||||
|
:key="filter.key"
|
||||||
|
class="filter-item"
|
||||||
|
:class="{ active: currentFilter === filter.key }"
|
||||||
|
@tap="switchFilter(filter.key)"
|
||||||
|
>
|
||||||
|
<text class="filter-text">{{ filter.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 答题记录列表 -->
|
||||||
|
<view class="records-section">
|
||||||
|
<scroll-view
|
||||||
|
class="records-list"
|
||||||
|
scroll-y
|
||||||
|
enable-flex
|
||||||
|
@scrolltolower="loadMoreRecords"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-for="record in filteredRecords"
|
||||||
|
:key="record.id"
|
||||||
|
class="record-item"
|
||||||
|
:class="{ 'parent-record': record.type === 'parent' }"
|
||||||
|
@tap="viewRecordDetail(record)"
|
||||||
|
>
|
||||||
|
<view class="record-header">
|
||||||
|
<view class="record-type-badge" :class="record.type">
|
||||||
|
<uni-icons
|
||||||
|
:type="record.type === 'parent' ? 'person' : 'person-filled'"
|
||||||
|
size="12"
|
||||||
|
color="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<text class="type-text">{{ record.type === 'parent' ? '家长答题' : '学生答题' }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="record-date">{{ formatDate(record.date) }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="record-content">
|
||||||
|
<view class="score-display">
|
||||||
|
<text class="score-number">{{ record.score }}</text>
|
||||||
|
<text class="score-unit">分</text>
|
||||||
|
<view class="score-badge" :class="getScoreLevel(record.score, record.totalQuestions)">
|
||||||
|
<text class="badge-text">{{ getScoreLevelText(record.score, record.totalQuestions) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="record-details">
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">答题情况:</text>
|
||||||
|
<text class="detail-value">{{ record.correctAnswers }}/{{ record.totalQuestions }} 题正确</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">用时:</text>
|
||||||
|
<text class="detail-value">{{ formatTime(record.timeSpent) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">正确率:</text>
|
||||||
|
<text class="detail-value" :class="{ 'high-rate': getAccuracy(record) >= 80 }">
|
||||||
|
{{ getAccuracy(record) }}%
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="record-actions">
|
||||||
|
<uni-icons type="arrow-right" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 成绩进步标识 -->
|
||||||
|
<view v-if="isImprovement(record)" class="improvement-badge">
|
||||||
|
<uni-icons type="up" size="12" color="#32CD32" />
|
||||||
|
<text class="improvement-text">进步了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view v-if="hasMoreRecords" class="load-more" @tap="loadMoreRecords">
|
||||||
|
<text class="load-more-text">加载更多</text>
|
||||||
|
<uni-icons type="arrow-down" size="16" color="#CCCCCC" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-if="filteredRecords.length === 0" class="empty-state">
|
||||||
|
<uni-icons type="list" size="80" color="#CCCCCC" />
|
||||||
|
<text class="empty-title">暂无答题记录</text>
|
||||||
|
<text class="empty-desc">快去首页开始答题吧!</text>
|
||||||
|
<AncientButton class="go-quiz-btn" @click="goToHome">
|
||||||
|
开始答题
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { QuizRecord } from '@/mocks/user'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { mockQuizRecords } from '@/mocks/user'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const quizRecords = ref<QuizRecord[]>([])
|
||||||
|
const currentFilter = ref<'all' | 'parent' | 'student'>('all')
|
||||||
|
const hasMoreRecords = ref(false)
|
||||||
|
const pageSize = 10
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
// 筛选选项
|
||||||
|
const filterOptions = [
|
||||||
|
{ key: 'all' as const, name: '全部' },
|
||||||
|
{ key: 'parent' as const, name: '家长答题' },
|
||||||
|
{ key: 'student' as const, name: '学生答题' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const filteredRecords = computed(() => {
|
||||||
|
let records = quizRecords.value
|
||||||
|
|
||||||
|
if (currentFilter.value !== 'all') {
|
||||||
|
records = records.filter(record => record.type === currentFilter.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalRecords = computed(() => quizRecords.value.length)
|
||||||
|
|
||||||
|
const totalScore = computed(() => {
|
||||||
|
return quizRecords.value.reduce((sum, record) => sum + record.score, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const averageScore = computed(() => {
|
||||||
|
if (totalRecords.value === 0)
|
||||||
|
return 0
|
||||||
|
return totalScore.value / totalRecords.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const accuracyRate = computed(() => {
|
||||||
|
if (totalRecords.value === 0)
|
||||||
|
return 0
|
||||||
|
const totalCorrect = quizRecords.value.reduce((sum, record) => sum + record.correctAnswers, 0)
|
||||||
|
const totalQuestions = quizRecords.value.reduce((sum, record) => sum + record.totalQuestions, 0)
|
||||||
|
return Math.round((totalCorrect / totalQuestions) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
function formatDate(dateStr: string) {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now.getTime() - date.getTime()
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (days === 0) {
|
||||||
|
return `今天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
|
||||||
|
}
|
||||||
|
else if (days === 1) {
|
||||||
|
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
|
||||||
|
}
|
||||||
|
else if (days <= 7) {
|
||||||
|
return `${days}天前`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return date.toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds: number) {
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
return `${minutes}分${remainingSeconds}秒`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccuracy(record: QuizRecord) {
|
||||||
|
return Math.round((record.correctAnswers / record.totalQuestions) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScoreLevel(score: number, totalQuestions: number) {
|
||||||
|
const accuracy = (score / (totalQuestions * 20)) * 100 // 假设每题最高20分
|
||||||
|
|
||||||
|
if (accuracy >= 90)
|
||||||
|
return 'excellent'
|
||||||
|
if (accuracy >= 80)
|
||||||
|
return 'good'
|
||||||
|
if (accuracy >= 70)
|
||||||
|
return 'pass'
|
||||||
|
return 'need-improve'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScoreLevelText(score: number, totalQuestions: number) {
|
||||||
|
const accuracy = (score / (totalQuestions * 20)) * 100 // 假设每题最高20分
|
||||||
|
|
||||||
|
if (accuracy >= 90)
|
||||||
|
return '优秀'
|
||||||
|
if (accuracy >= 80)
|
||||||
|
return '良好'
|
||||||
|
if (accuracy >= 70)
|
||||||
|
return '合格'
|
||||||
|
return '需努力'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImprovement(record: QuizRecord) {
|
||||||
|
const currentIndex = filteredRecords.value.findIndex(r => r.id === record.id)
|
||||||
|
if (currentIndex >= filteredRecords.value.length - 1)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const previousRecord = filteredRecords.value[currentIndex + 1]
|
||||||
|
if (!previousRecord || previousRecord.type !== record.type)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return record.score > previousRecord.score
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchFilter(filter: 'all' | 'parent' | 'student') {
|
||||||
|
currentFilter.value = filter
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMoreRecords() {
|
||||||
|
if (!hasMoreRecords.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
// 模拟加载更多数据
|
||||||
|
currentPage.value++
|
||||||
|
// 这里可以调用API加载更多数据
|
||||||
|
console.log('加载更多记录...')
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewRecordDetail(record: QuizRecord) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '答题详情',
|
||||||
|
content: `类型:${record.type === 'parent' ? '家长答题' : '学生答题'}\n得分:${record.score}分\n正确:${record.correctAnswers}/${record.totalQuestions}题\n用时:${formatTime(record.timeSpent)}`,
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToHome() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
// 从本地存储获取答题记录
|
||||||
|
const storedRecords = uni.getStorageSync('quizRecords') || []
|
||||||
|
quizRecords.value = storedRecords.length > 0 ? storedRecords : mockQuizRecords
|
||||||
|
|
||||||
|
// 判断是否有更多数据
|
||||||
|
hasMoreRecords.value = quizRecords.value.length > pageSize
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.quiz-records-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
|
||||||
|
.stats-section {
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
.stats-content {
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 32rpx;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #daa520;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
padding: 0 32rpx 16rpx;
|
||||||
|
|
||||||
|
.filter-tabs {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #2d5e3e;
|
||||||
|
|
||||||
|
.filter-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-section {
|
||||||
|
padding: 0 32rpx 32rpx;
|
||||||
|
|
||||||
|
.records-list {
|
||||||
|
max-height: calc(100vh - 400rpx);
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(45, 94, 62, 0.1);
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.parent-record {
|
||||||
|
border-left: 4rpx solid #daa520;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.parent-record) {
|
||||||
|
border-left: 4rpx solid #32cd32;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
.record-type-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
|
||||||
|
&.parent {
|
||||||
|
background: #daa520;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.student {
|
||||||
|
background: #32cd32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-date {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.score-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.score-number {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-unit {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #696969;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -12rpx;
|
||||||
|
right: -32rpx;
|
||||||
|
padding: 2rpx 8rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 18rpx;
|
||||||
|
|
||||||
|
&.excellent {
|
||||||
|
background: #32cd32;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.good {
|
||||||
|
background: #4ecdc4;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pass {
|
||||||
|
background: #ffa726;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.need-improve {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-details {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #696969;
|
||||||
|
min-width: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #2f4f4f;
|
||||||
|
|
||||||
|
&.high-rate {
|
||||||
|
color: #32cd32;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-actions {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.improvement-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
background: rgba(50, 205, 50, 0.1);
|
||||||
|
border: 1rpx solid #32cd32;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 4rpx 8rpx;
|
||||||
|
|
||||||
|
.improvement-text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: #32cd32;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 120rpx 32rpx;
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #cccccc;
|
||||||
|
margin: 24rpx 0 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-quiz-btn {
|
||||||
|
width: 200rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
614
src/pages/quiz/parent-quiz.vue
Normal file
614
src/pages/quiz/parent-quiz.vue
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "家长答题",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="quiz-container">
|
||||||
|
<!-- 答题进度和倒计时 -->
|
||||||
|
<view class="quiz-header">
|
||||||
|
<view class="progress-info">
|
||||||
|
<text class="question-number">{{ currentQuestionIndex + 1 }}/{{ totalQuestions }}</text>
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: `${progressWidth}%` }"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="timer-container" :class="{ warning: timeLeft <= 10 }">
|
||||||
|
<uni-icons type="clock" size="16" color="#FFFFFF" />
|
||||||
|
<text class="timer-text">{{ timeLeft }}s</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 题目卡片 -->
|
||||||
|
<ZhuziCard v-if="currentQuestion" class="question-card">
|
||||||
|
<view class="question-content">
|
||||||
|
<!-- 题目类型和分值 -->
|
||||||
|
<view class="question-meta">
|
||||||
|
<text class="question-type">{{ currentQuestion.type === 'single' ? '单选题' : '多选题' }}</text>
|
||||||
|
<text class="question-score">{{ currentQuestion.score }}分</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 题目标题 -->
|
||||||
|
<view class="question-title">
|
||||||
|
<text>{{ currentQuestion.title }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 选项列表 -->
|
||||||
|
<view class="options-container">
|
||||||
|
<view
|
||||||
|
v-for="option in currentQuestion.options"
|
||||||
|
:key="option.id"
|
||||||
|
class="option-item"
|
||||||
|
:class="{
|
||||||
|
selected: selectedAnswers.includes(option.id),
|
||||||
|
correct: showAnswer && currentQuestion.correctAnswers.includes(option.id),
|
||||||
|
incorrect: showAnswer && selectedAnswers.includes(option.id) && !currentQuestion.correctAnswers.includes(option.id),
|
||||||
|
}"
|
||||||
|
@tap="selectOption(option.id)"
|
||||||
|
>
|
||||||
|
<view class="option-content">
|
||||||
|
<view class="option-icon">
|
||||||
|
<view
|
||||||
|
v-if="currentQuestion.type === 'single'"
|
||||||
|
class="radio-icon"
|
||||||
|
:class="{ checked: selectedAnswers.includes(option.id) }"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="checkbox-icon"
|
||||||
|
:class="{ checked: selectedAnswers.includes(option.id) }"
|
||||||
|
>
|
||||||
|
<uni-icons v-if="selectedAnswers.includes(option.id)" type="checkmarkempty" size="14" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="option-text">{{ option.text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 多选题确认按钮 -->
|
||||||
|
<AncientButton
|
||||||
|
v-if="currentQuestion.type === 'multiple' && !showAnswer"
|
||||||
|
class="confirm-btn"
|
||||||
|
:disabled="selectedAnswers.length === 0"
|
||||||
|
@click="confirmAnswer"
|
||||||
|
>
|
||||||
|
确认答案
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<!-- 答题反馈 -->
|
||||||
|
<view v-if="showFeedback" class="feedback-overlay" @tap="nextQuestion">
|
||||||
|
<view class="feedback-content">
|
||||||
|
<uni-icons
|
||||||
|
:type="isCorrect ? 'checkmarkempty' : 'closeempty'"
|
||||||
|
:size="80"
|
||||||
|
:color="isCorrect ? '#32CD32' : '#FF4444'"
|
||||||
|
class="feedback-icon"
|
||||||
|
/>
|
||||||
|
<text class="feedback-text">{{ isCorrect ? '回答正确!' : '很遗憾,答错了' }}</text>
|
||||||
|
<text v-if="isCorrect" class="score-text">+{{ currentQuestion?.score }}分</text>
|
||||||
|
<text class="next-hint">点击任意处继续</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 超时提示 -->
|
||||||
|
<view v-if="showTimeout" class="timeout-overlay" @tap="nextQuestion">
|
||||||
|
<view class="timeout-content">
|
||||||
|
<uni-icons type="clock" size="80" color="#FF6B6B" />
|
||||||
|
<text class="timeout-text">时间到了!</text>
|
||||||
|
<text class="timeout-hint">点击任意处继续</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { QuizQuestion } from '@/mocks/quiz'
|
||||||
|
import { computed, onUnmounted, ref } from 'vue'
|
||||||
|
import { mockParentQuestions } from '@/mocks/quiz'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const questions = ref<QuizQuestion[]>([...mockParentQuestions])
|
||||||
|
const currentQuestionIndex = ref(0)
|
||||||
|
const selectedAnswers = ref<string[]>([])
|
||||||
|
const showAnswer = ref(false)
|
||||||
|
const showFeedback = ref(false)
|
||||||
|
const showTimeout = ref(false)
|
||||||
|
const timeLeft = ref(30)
|
||||||
|
const totalScore = ref(0)
|
||||||
|
const correctCount = ref(0)
|
||||||
|
const timer = ref<number | null>(null)
|
||||||
|
const startTime = ref<number>(Date.now())
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const totalQuestions = computed(() => questions.value.length)
|
||||||
|
const currentQuestion = computed(() => questions.value[currentQuestionIndex.value])
|
||||||
|
const progressWidth = computed(() => ((currentQuestionIndex.value + 1) / totalQuestions.value) * 100)
|
||||||
|
const isCorrect = ref(false)
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
function startTimer() {
|
||||||
|
if (timer.value)
|
||||||
|
clearInterval(timer.value)
|
||||||
|
|
||||||
|
timeLeft.value = 30
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
timeLeft.value--
|
||||||
|
|
||||||
|
if (timeLeft.value <= 0) {
|
||||||
|
// 时间到了
|
||||||
|
handleTimeout()
|
||||||
|
}
|
||||||
|
}, 1000) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理超时
|
||||||
|
function handleTimeout() {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
showTimeout.value = true
|
||||||
|
|
||||||
|
// 2秒后自动进入下一题
|
||||||
|
setTimeout(() => {
|
||||||
|
nextQuestion()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择选项
|
||||||
|
function selectOption(optionId: string) {
|
||||||
|
if (showAnswer.value || showFeedback.value || showTimeout.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (currentQuestion.value?.type === 'single') {
|
||||||
|
selectedAnswers.value = [optionId]
|
||||||
|
// 单选题立即确认答案
|
||||||
|
setTimeout(() => {
|
||||||
|
confirmAnswer()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 多选题切换选择状态
|
||||||
|
const index = selectedAnswers.value.indexOf(optionId)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedAnswers.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedAnswers.value.push(optionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认答案
|
||||||
|
function confirmAnswer() {
|
||||||
|
if (showAnswer.value || !currentQuestion.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
showAnswer.value = true
|
||||||
|
|
||||||
|
// 检查答案是否正确
|
||||||
|
const correctAnswers = currentQuestion.value.correctAnswers
|
||||||
|
const isAnswerCorrect = selectedAnswers.value.length === correctAnswers.length
|
||||||
|
&& selectedAnswers.value.every(answer => correctAnswers.includes(answer))
|
||||||
|
|
||||||
|
isCorrect.value = isAnswerCorrect
|
||||||
|
|
||||||
|
if (isAnswerCorrect) {
|
||||||
|
totalScore.value += currentQuestion.value.score
|
||||||
|
correctCount.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示反馈
|
||||||
|
setTimeout(() => {
|
||||||
|
showAnswer.value = false
|
||||||
|
showFeedback.value = true
|
||||||
|
|
||||||
|
// 3秒后自动进入下一题
|
||||||
|
setTimeout(() => {
|
||||||
|
if (showFeedback.value) {
|
||||||
|
nextQuestion()
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一题
|
||||||
|
function nextQuestion() {
|
||||||
|
showFeedback.value = false
|
||||||
|
showTimeout.value = false
|
||||||
|
selectedAnswers.value = []
|
||||||
|
|
||||||
|
if (currentQuestionIndex.value < totalQuestions.value - 1) {
|
||||||
|
currentQuestionIndex.value++
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 答题完成,跳转到结果页
|
||||||
|
finishQuiz()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成答题
|
||||||
|
function finishQuiz() {
|
||||||
|
const endTime = Date.now()
|
||||||
|
const timeSpent = Math.floor((endTime - startTime.value) / 1000)
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
type: 'parent' as const,
|
||||||
|
totalScore: totalScore.value,
|
||||||
|
correctCount: correctCount.value,
|
||||||
|
totalQuestions: totalQuestions.value,
|
||||||
|
timeSpent,
|
||||||
|
questions: questions.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存结果到本地存储
|
||||||
|
uni.setStorageSync('quizResult', result)
|
||||||
|
|
||||||
|
// 跳转到结果页
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/quiz/result',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听页面返回
|
||||||
|
function handleBackPress() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认退出',
|
||||||
|
content: '答题尚未完成,确定要退出吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return true // 阻止默认返回行为
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时开始答题
|
||||||
|
onLoad(() => {
|
||||||
|
// 随机打乱题目顺序
|
||||||
|
questions.value = questions.value.sort(() => Math.random() - 0.5)
|
||||||
|
startTime.value = Date.now()
|
||||||
|
startTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听页面隐藏和显示
|
||||||
|
onHide(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (!showFeedback.value && !showTimeout.value && !showAnswer.value && timeLeft.value > 0) {
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听返回按钮
|
||||||
|
uni.onBackPress?.(handleBackPress)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.quiz-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(to bottom, #e6f3e6 0%, #d4f0d4 100%);
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.quiz-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
|
||||||
|
.question-number {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8rpx;
|
||||||
|
background: rgba(45, 94, 62, 0.2);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #2d5e3e 0%, #daa520 100%);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
background: #2d5e3e;
|
||||||
|
padding: 12rpx 20rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: #ff4444;
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-card {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.question-content {
|
||||||
|
.question-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.question-type {
|
||||||
|
background: rgba(45, 94, 62, 0.1);
|
||||||
|
color: #2d5e3e;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-score {
|
||||||
|
background: linear-gradient(135deg, #daa520 0%, #ffd700 100%);
|
||||||
|
color: white;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-container {
|
||||||
|
.option-item {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.option-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.option-icon {
|
||||||
|
margin-right: 24rpx;
|
||||||
|
|
||||||
|
.radio-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 3rpx solid #cccccc;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
border-color: #daa520;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
background: #daa520;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 3rpx solid #cccccc;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
background: #daa520;
|
||||||
|
border-color: #daa520;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2f4f4f;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected .option-content {
|
||||||
|
background: rgba(218, 165, 32, 0.1);
|
||||||
|
border-color: #daa520;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.correct .option-content {
|
||||||
|
background: rgba(50, 205, 50, 0.1);
|
||||||
|
border-color: #32cd32;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incorrect .option-content {
|
||||||
|
background: rgba(255, 68, 68, 0.1);
|
||||||
|
border-color: #ff4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反馈弹窗
|
||||||
|
.feedback-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
.feedback-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 64rpx 48rpx;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2);
|
||||||
|
animation: slideInUp 0.4s ease-out;
|
||||||
|
|
||||||
|
.feedback-icon {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #daa520;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-hint {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时弹窗
|
||||||
|
.timeout-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 68, 68, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
.timeout-content {
|
||||||
|
text-align: center;
|
||||||
|
animation: pulse 0.6s ease-in-out;
|
||||||
|
|
||||||
|
.timeout-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin: 32rpx 0 16rpx;
|
||||||
|
text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-hint {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100rpx);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
680
src/pages/quiz/result.vue
Normal file
680
src/pages/quiz/result.vue
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "答题结果",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="result-container">
|
||||||
|
<!-- 用户头像和信息 -->
|
||||||
|
<view class="user-section">
|
||||||
|
<image
|
||||||
|
:src="userInfo.avatar || '/static/images/default-avatar.png'"
|
||||||
|
class="user-avatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="user-info">
|
||||||
|
<text class="student-name">{{ userInfo.child?.name || '小朋友' }}</text>
|
||||||
|
<text class="parent-name">家长:{{ userInfo.phone || '未知' }}</text>
|
||||||
|
<view class="school-info">
|
||||||
|
<text class="school-text">{{ userInfo.child?.schoolName }} {{ userInfo.child?.gradeName }} {{ userInfo.child?.className }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 答题结果卡片 -->
|
||||||
|
<ZhuziCard class="result-card" :class="{ 'student-result': quizResult?.type === 'student' }">
|
||||||
|
<view class="result-content">
|
||||||
|
<!-- 成绩标题 -->
|
||||||
|
<view class="result-header">
|
||||||
|
<text class="result-title">{{ quizResult?.type === 'parent' ? '家长答题' : '学生答题' }}成绩</text>
|
||||||
|
<text class="result-date">{{ formatDate() }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核心成绩展示 -->
|
||||||
|
<view class="score-section">
|
||||||
|
<view class="main-score">
|
||||||
|
<text class="score-number">{{ quizResult?.totalScore || 0 }}</text>
|
||||||
|
<text class="score-unit">分</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="score-details">
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">正确题数</text>
|
||||||
|
<text class="detail-value">{{ quizResult?.correctCount }}/{{ quizResult?.totalQuestions }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">答题时间</text>
|
||||||
|
<text class="detail-value">{{ formatTime(quizResult?.timeSpent) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">正确率</text>
|
||||||
|
<text class="detail-value">{{ accuracy }}%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 成绩等级 -->
|
||||||
|
<view class="grade-section">
|
||||||
|
<view class="grade-badge" :class="gradeLevel.class">
|
||||||
|
<text class="grade-text">{{ gradeLevel.text }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="grade-desc">{{ gradeLevel.desc }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 激励文字 -->
|
||||||
|
<view class="encouragement">
|
||||||
|
<text class="encourage-text">{{ encouragementText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<!-- 分享区域 -->
|
||||||
|
<view class="share-section">
|
||||||
|
<view class="section-title">
|
||||||
|
<uni-icons type="gift" size="20" color="#DAA520" />
|
||||||
|
<text class="title-text">分享成绩</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view id="shareCanvas" class="share-preview">
|
||||||
|
<view class="share-content">
|
||||||
|
<!-- 背景装饰 -->
|
||||||
|
<view class="share-bg">
|
||||||
|
<view class="bamboo-pattern" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分享卡片内容 -->
|
||||||
|
<view class="share-card">
|
||||||
|
<view class="share-header">
|
||||||
|
<image src="/static/logo.svg" class="share-logo" mode="aspectFit" />
|
||||||
|
<text class="share-title">朱子文化学习</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="share-user">
|
||||||
|
<image
|
||||||
|
:src="userInfo.avatar || '/static/images/default-avatar.png'"
|
||||||
|
class="share-avatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<text class="share-name">{{ userInfo.child?.name || '小朋友' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="share-score">
|
||||||
|
<text class="share-score-num">{{ quizResult?.totalScore || 0 }}</text>
|
||||||
|
<text class="share-score-unit">分</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="share-details">
|
||||||
|
<text class="share-type">{{ quizResult?.type === 'parent' ? '家长答题' : '学生答题' }}</text>
|
||||||
|
<text class="share-accuracy">正确率 {{ accuracy }}%</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 小程序码占位 -->
|
||||||
|
<view class="qr-placeholder">
|
||||||
|
<uni-icons type="scan" size="40" color="#CCCCCC" />
|
||||||
|
<text class="qr-text">小程序码</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="share-actions">
|
||||||
|
<AncientButton class="share-btn" @click="shareToFriends">
|
||||||
|
<uni-icons type="redo" size="16" color="#FFFFFF" />
|
||||||
|
分享给朋友
|
||||||
|
</AncientButton>
|
||||||
|
|
||||||
|
<AncientButton class="generate-btn" @click="generateImage">
|
||||||
|
<uni-icons type="image" size="16" color="#FFFFFF" />
|
||||||
|
生成图片
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="action-section">
|
||||||
|
<AncientButton class="action-btn secondary" @click="viewRecord">
|
||||||
|
查看答题记录
|
||||||
|
</AncientButton>
|
||||||
|
|
||||||
|
<AncientButton class="action-btn primary" @click="backToHome">
|
||||||
|
返回首页
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { mockUserInfo } from '@/mocks/user'
|
||||||
|
|
||||||
|
interface QuizResult {
|
||||||
|
type: 'parent' | 'student'
|
||||||
|
totalScore: number
|
||||||
|
correctCount: number
|
||||||
|
totalQuestions: number
|
||||||
|
timeSpent: number
|
||||||
|
questions: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const quizResult = ref<QuizResult | null>(null)
|
||||||
|
const userInfo = ref(mockUserInfo)
|
||||||
|
|
||||||
|
// 计算正确率
|
||||||
|
const accuracy = computed(() => {
|
||||||
|
if (!quizResult.value)
|
||||||
|
return 0
|
||||||
|
return Math.round((quizResult.value.correctCount / quizResult.value.totalQuestions) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 成绩等级
|
||||||
|
const gradeLevel = computed(() => {
|
||||||
|
const score = accuracy.value
|
||||||
|
|
||||||
|
if (score >= 90) {
|
||||||
|
return { text: '优秀', class: 'excellent', desc: '表现出色,继续保持!' }
|
||||||
|
}
|
||||||
|
else if (score >= 80) {
|
||||||
|
return { text: '良好', class: 'good', desc: '很不错,再接再厉!' }
|
||||||
|
}
|
||||||
|
else if (score >= 70) {
|
||||||
|
return { text: '合格', class: 'pass', desc: '基础扎实,继续努力!' }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { text: '需努力', class: 'need-work', desc: '多多学习,相信你会进步!' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 激励文字
|
||||||
|
const encouragementText = computed(() => {
|
||||||
|
const texts = {
|
||||||
|
parent: [
|
||||||
|
'感谢您对朱子文化的学习和传承!',
|
||||||
|
'您的参与为孩子树立了良好的榜样!',
|
||||||
|
'家庭教育是最好的启蒙教育!',
|
||||||
|
],
|
||||||
|
student: [
|
||||||
|
'学而时习之,不亦说乎!',
|
||||||
|
'朱子的智慧伴您成长!',
|
||||||
|
'传承文化,从小做起!',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeTexts = texts[quizResult.value?.type || 'student']
|
||||||
|
return typeTexts[Math.floor(Math.random() * typeTexts.length)]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
function formatTime(seconds?: number) {
|
||||||
|
if (!seconds)
|
||||||
|
return '0分0秒'
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
return `${minutes}分${remainingSeconds}秒`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate() {
|
||||||
|
const now = new Date()
|
||||||
|
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享给朋友
|
||||||
|
function shareToFriends() {
|
||||||
|
uni.showShareMenu({
|
||||||
|
withShareTicket: true,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请点击右上角分享',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成分享图片
|
||||||
|
function generateImage() {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '生成中...',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟生成图片
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: '图片已保存到相册',
|
||||||
|
icon: 'success',
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看答题记录
|
||||||
|
function viewRecord() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/me/quiz-records',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
function backToHome() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取答题结果
|
||||||
|
onMounted(() => {
|
||||||
|
const result = uni.getStorageSync('quizResult')
|
||||||
|
if (result) {
|
||||||
|
quizResult.value = result
|
||||||
|
|
||||||
|
// 模拟保存答题记录到历史记录
|
||||||
|
const records = uni.getStorageSync('quizRecords') || []
|
||||||
|
records.unshift({
|
||||||
|
id: Date.now().toString(),
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
type: result.type,
|
||||||
|
score: result.totalScore,
|
||||||
|
totalQuestions: result.totalQuestions,
|
||||||
|
correctAnswers: result.correctCount,
|
||||||
|
timeSpent: result.timeSpent,
|
||||||
|
})
|
||||||
|
uni.setStorageSync('quizRecords', records)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 没有答题结果,返回首页
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无答题结果',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
backToHome()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面分享配置
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: `我在朱子文化学习中获得了${quizResult.value?.totalScore || 0}分!`,
|
||||||
|
path: '/pages/index/index',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShareTimeline(() => {
|
||||||
|
return {
|
||||||
|
title: `朱子文化学习 - 获得${quizResult.value?.totalScore || 0}分`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.result-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.user-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 32rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(45, 94, 62, 0.1);
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
border: 4rpx solid #daa520;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.student-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-info {
|
||||||
|
.school-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #daa520;
|
||||||
|
background: rgba(218, 165, 32, 0.1);
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
&.student-result {
|
||||||
|
border-left: 6rpx solid #32cd32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-date {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.main-score {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.score-number {
|
||||||
|
font-size: 120rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(135deg, #daa520 0%, #ffd700 100%);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
text-shadow: 2rpx 2rpx 4rpx rgba(218, 165, 32, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-unit {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.grade-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 16rpx 32rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&.excellent {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.good {
|
||||||
|
background: linear-gradient(135deg, #4ecdc4 0%, #7fdbda 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pass {
|
||||||
|
background: linear-gradient(135deg, #45b7d1 0%, #73c6e6 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.need-work {
|
||||||
|
background: linear-gradient(135deg, #ffa726 0%, #ffcc80 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.encouragement {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
background: rgba(218, 165, 32, 0.1);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1rpx dashed #daa520;
|
||||||
|
|
||||||
|
.encourage-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2d5e3e;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-section {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-preview {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(45, 94, 62, 0.1);
|
||||||
|
|
||||||
|
.share-content {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, #f5f5dc 0%, #fffaf0 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 48rpx 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.share-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 0.05;
|
||||||
|
|
||||||
|
.bamboo-pattern {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/* background: url('/static/images/bamboo-pattern.svg') repeat; */
|
||||||
|
/* 竹子图案暂时注释,后续添加实际图片资源 */
|
||||||
|
background:
|
||||||
|
linear-gradient(45deg, rgba(45, 94, 62, 0.02) 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, rgba(45, 94, 62, 0.02) 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, rgba(45, 94, 62, 0.02) 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, rgba(45, 94, 62, 0.02) 75%);
|
||||||
|
background-size: 30rpx 30rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.share-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.share-logo {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-user {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.share-avatar {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
border: 2rpx solid #daa520;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #2d5e3e;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-score {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.share-score-num {
|
||||||
|
font-size: 80rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #daa520;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-score-unit {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-details {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.share-type {
|
||||||
|
display: block;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #696969;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-accuracy {
|
||||||
|
display: block;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #daa520;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border: 2rpx dashed #cccccc;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.qr-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.share-btn,
|
||||||
|
.generate-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
color: #2d5e3e;
|
||||||
|
border-color: #2d5e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
614
src/pages/quiz/student-quiz.vue
Normal file
614
src/pages/quiz/student-quiz.vue
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "学生答题",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="quiz-container">
|
||||||
|
<!-- 答题进度和倒计时 -->
|
||||||
|
<view class="quiz-header">
|
||||||
|
<view class="progress-info">
|
||||||
|
<text class="question-number">{{ currentQuestionIndex + 1 }}/{{ totalQuestions }}</text>
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: `${progressWidth}%` }"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="timer-container" :class="{ warning: timeLeft <= 10 }">
|
||||||
|
<uni-icons type="clock" size="16" color="#FFFFFF" />
|
||||||
|
<text class="timer-text">{{ timeLeft }}s</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 题目卡片 -->
|
||||||
|
<ZhuziCard v-if="currentQuestion" class="question-card student-theme">
|
||||||
|
<view class="question-content">
|
||||||
|
<!-- 题目类型和分值 -->
|
||||||
|
<view class="question-meta">
|
||||||
|
<text class="question-type student-type">{{ currentQuestion.type === 'single' ? '单选题' : '多选题' }}</text>
|
||||||
|
<text class="question-score student-score">{{ currentQuestion.score }}分</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 题目标题 -->
|
||||||
|
<view class="question-title">
|
||||||
|
<text>{{ currentQuestion.title }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 选项列表 -->
|
||||||
|
<view class="options-container">
|
||||||
|
<view
|
||||||
|
v-for="option in currentQuestion.options"
|
||||||
|
:key="option.id"
|
||||||
|
class="option-item student-option"
|
||||||
|
:class="{
|
||||||
|
selected: selectedAnswers.includes(option.id),
|
||||||
|
correct: showAnswer && currentQuestion.correctAnswers.includes(option.id),
|
||||||
|
incorrect: showAnswer && selectedAnswers.includes(option.id) && !currentQuestion.correctAnswers.includes(option.id),
|
||||||
|
}"
|
||||||
|
@tap="selectOption(option.id)"
|
||||||
|
>
|
||||||
|
<view class="option-content">
|
||||||
|
<view class="option-icon">
|
||||||
|
<view
|
||||||
|
v-if="currentQuestion.type === 'single'"
|
||||||
|
class="radio-icon student-radio"
|
||||||
|
:class="{ checked: selectedAnswers.includes(option.id) }"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="checkbox-icon student-checkbox"
|
||||||
|
:class="{ checked: selectedAnswers.includes(option.id) }"
|
||||||
|
>
|
||||||
|
<uni-icons v-if="selectedAnswers.includes(option.id)" type="checkmarkempty" size="14" color="#FFFFFF" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="option-text">{{ option.text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 多选题确认按钮 -->
|
||||||
|
<AncientButton
|
||||||
|
v-if="currentQuestion.type === 'multiple' && !showAnswer"
|
||||||
|
class="confirm-btn student-btn"
|
||||||
|
:disabled="selectedAnswers.length === 0"
|
||||||
|
@click="confirmAnswer"
|
||||||
|
>
|
||||||
|
确认答案
|
||||||
|
</AncientButton>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
|
||||||
|
<!-- 答题反馈 -->
|
||||||
|
<view v-if="showFeedback" class="feedback-overlay" @tap="nextQuestion">
|
||||||
|
<view class="feedback-content student-feedback">
|
||||||
|
<uni-icons
|
||||||
|
:type="isCorrect ? 'checkmarkempty' : 'closeempty'"
|
||||||
|
:size="80"
|
||||||
|
:color="isCorrect ? '#32CD32' : '#FF4444'"
|
||||||
|
class="feedback-icon"
|
||||||
|
/>
|
||||||
|
<text class="feedback-text">{{ isCorrect ? '太棒了!答对了' : '加油!再想想' }}</text>
|
||||||
|
<text v-if="isCorrect" class="score-text student-score-text">+{{ currentQuestion?.score }}分</text>
|
||||||
|
<text class="next-hint">点击任意处继续</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 超时提示 -->
|
||||||
|
<view v-if="showTimeout" class="timeout-overlay" @tap="nextQuestion">
|
||||||
|
<view class="timeout-content">
|
||||||
|
<uni-icons type="clock" size="80" color="#FF6B6B" />
|
||||||
|
<text class="timeout-text">时间到了!</text>
|
||||||
|
<text class="timeout-hint">点击任意处继续</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { QuizQuestion } from '@/mocks/quiz'
|
||||||
|
import { computed, onUnmounted, ref } from 'vue'
|
||||||
|
import { mockStudentQuestions } from '@/mocks/quiz'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const questions = ref<QuizQuestion[]>([...mockStudentQuestions])
|
||||||
|
const currentQuestionIndex = ref(0)
|
||||||
|
const selectedAnswers = ref<string[]>([])
|
||||||
|
const showAnswer = ref(false)
|
||||||
|
const showFeedback = ref(false)
|
||||||
|
const showTimeout = ref(false)
|
||||||
|
const timeLeft = ref(30)
|
||||||
|
const totalScore = ref(0)
|
||||||
|
const correctCount = ref(0)
|
||||||
|
const timer = ref<number | null>(null)
|
||||||
|
const startTime = ref<number>(Date.now())
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const totalQuestions = computed(() => questions.value.length)
|
||||||
|
const currentQuestion = computed(() => questions.value[currentQuestionIndex.value])
|
||||||
|
const progressWidth = computed(() => ((currentQuestionIndex.value + 1) / totalQuestions.value) * 100)
|
||||||
|
const isCorrect = ref(false)
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
function startTimer() {
|
||||||
|
if (timer.value)
|
||||||
|
clearInterval(timer.value)
|
||||||
|
|
||||||
|
timeLeft.value = 30
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
timeLeft.value--
|
||||||
|
|
||||||
|
if (timeLeft.value <= 0) {
|
||||||
|
handleTimeout()
|
||||||
|
}
|
||||||
|
}, 1000) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理超时
|
||||||
|
function handleTimeout() {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
showTimeout.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
nextQuestion()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择选项
|
||||||
|
function selectOption(optionId: string) {
|
||||||
|
if (showAnswer.value || showFeedback.value || showTimeout.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (currentQuestion.value?.type === 'single') {
|
||||||
|
selectedAnswers.value = [optionId]
|
||||||
|
setTimeout(() => {
|
||||||
|
confirmAnswer()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const index = selectedAnswers.value.indexOf(optionId)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedAnswers.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedAnswers.value.push(optionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认答案
|
||||||
|
function confirmAnswer() {
|
||||||
|
if (showAnswer.value || !currentQuestion.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
showAnswer.value = true
|
||||||
|
|
||||||
|
// 检查答案是否正确
|
||||||
|
const correctAnswers = currentQuestion.value.correctAnswers
|
||||||
|
const isAnswerCorrect = selectedAnswers.value.length === correctAnswers.length
|
||||||
|
&& selectedAnswers.value.every(answer => correctAnswers.includes(answer))
|
||||||
|
|
||||||
|
isCorrect.value = isAnswerCorrect
|
||||||
|
|
||||||
|
if (isAnswerCorrect) {
|
||||||
|
totalScore.value += currentQuestion.value.score
|
||||||
|
correctCount.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示反馈
|
||||||
|
setTimeout(() => {
|
||||||
|
showAnswer.value = false
|
||||||
|
showFeedback.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (showFeedback.value) {
|
||||||
|
nextQuestion()
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一题
|
||||||
|
function nextQuestion() {
|
||||||
|
showFeedback.value = false
|
||||||
|
showTimeout.value = false
|
||||||
|
selectedAnswers.value = []
|
||||||
|
|
||||||
|
if (currentQuestionIndex.value < totalQuestions.value - 1) {
|
||||||
|
currentQuestionIndex.value++
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
finishQuiz()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成答题
|
||||||
|
function finishQuiz() {
|
||||||
|
const endTime = Date.now()
|
||||||
|
const timeSpent = Math.floor((endTime - startTime.value) / 1000)
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
type: 'student' as const,
|
||||||
|
totalScore: totalScore.value,
|
||||||
|
correctCount: correctCount.value,
|
||||||
|
totalQuestions: totalQuestions.value,
|
||||||
|
timeSpent,
|
||||||
|
questions: questions.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.setStorageSync('quizResult', result)
|
||||||
|
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/quiz/result',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听页面返回
|
||||||
|
function handleBackPress() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认退出',
|
||||||
|
content: '答题尚未完成,确定要退出吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时开始答题
|
||||||
|
onLoad(() => {
|
||||||
|
questions.value = questions.value.sort(() => Math.random() - 0.5)
|
||||||
|
startTime.value = Date.now()
|
||||||
|
startTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (!showFeedback.value && !showTimeout.value && !showAnswer.value && timeLeft.value > 0) {
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.onBackPress?.(handleBackPress)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.quiz-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
url('/static/images/ancient-paper-bg.jpg') no-repeat center center,
|
||||||
|
linear-gradient(135deg, #f8f5f0 0%, #d8d0c0 100%);
|
||||||
|
background-size: cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.quiz-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
|
||||||
|
.question-number {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8rpx;
|
||||||
|
background: rgba(50, 205, 50, 0.2);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #32cd32 0%, #98fb98 100%);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
background: #32cd32;
|
||||||
|
padding: 12rpx 20rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: #ff4444;
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-card {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
&.student-theme {
|
||||||
|
border-left: 6rpx solid #32cd32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-content {
|
||||||
|
.question-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.question-type.student-type {
|
||||||
|
background: rgba(50, 205, 50, 0.1);
|
||||||
|
color: #228b22;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-score.student-score {
|
||||||
|
background: linear-gradient(135deg, #32cd32 0%, #98fb98 100%);
|
||||||
|
color: white;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d5e3e;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-container {
|
||||||
|
.option-item.student-option {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.option-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.option-icon {
|
||||||
|
margin-right: 24rpx;
|
||||||
|
|
||||||
|
.radio-icon.student-radio {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 3rpx solid #cccccc;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
border-color: #32cd32;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
background: #32cd32;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-icon.student-checkbox {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 3rpx solid #cccccc;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
background: #32cd32;
|
||||||
|
border-color: #32cd32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2f4f4f;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected .option-content {
|
||||||
|
background: rgba(50, 205, 50, 0.1);
|
||||||
|
border-color: #32cd32;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.correct .option-content {
|
||||||
|
background: rgba(50, 205, 50, 0.15);
|
||||||
|
border-color: #32cd32;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incorrect .option-content {
|
||||||
|
background: rgba(255, 68, 68, 0.1);
|
||||||
|
border-color: #ff4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn.student-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
background: linear-gradient(135deg, #32cd32 0%, #98fb98 100%);
|
||||||
|
border-color: #32cd32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反馈弹窗
|
||||||
|
.feedback-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
.feedback-content.student-feedback {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 64rpx 48rpx;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2);
|
||||||
|
animation: slideInUp 0.4s ease-out;
|
||||||
|
border-left: 8rpx solid #32cd32;
|
||||||
|
|
||||||
|
.feedback-icon {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #228b22;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-text.student-score-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #32cd32;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-hint {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #696969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时弹窗
|
||||||
|
.timeout-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 68, 68, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
.timeout-content {
|
||||||
|
text-align: center;
|
||||||
|
animation: pulse 0.6s ease-in-out;
|
||||||
|
|
||||||
|
.timeout-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin: 32rpx 0 16rpx;
|
||||||
|
text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-hint {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100rpx);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
664
src/pages/ranking/ranking.vue
Normal file
664
src/pages/ranking/ranking.vue
Normal file
@@ -0,0 +1,664 @@
|
|||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "排行榜",
|
||||||
|
"navigationBarBackgroundColor": "#2D5E3E",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="ranking-container">
|
||||||
|
<!-- 用户个人排名信息 -->
|
||||||
|
<view class="user-rank-section">
|
||||||
|
<view class="user-card">
|
||||||
|
<image
|
||||||
|
:src="userRankInfo.avatar || '/static/images/default-avatar.png'"
|
||||||
|
class="user-avatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="user-info">
|
||||||
|
<text class="user-name">{{ userRankInfo.studentName }}</text>
|
||||||
|
<text class="user-school">{{ userRankInfo.schoolName }} {{ userRankInfo.gradeName }} {{ userRankInfo.className }}</text>
|
||||||
|
<view class="score-info">
|
||||||
|
<view class="score-item">
|
||||||
|
<text class="score-label">总积分</text>
|
||||||
|
<text class="score-value">{{ userRankInfo.totalScore }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="score-item">
|
||||||
|
<text class="score-label">本月积分</text>
|
||||||
|
<text class="score-value">{{ userRankInfo.monthScore }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 排行榜类型切换 -->
|
||||||
|
<view class="tab-section">
|
||||||
|
<view class="tab-container">
|
||||||
|
<view
|
||||||
|
v-for="tab in timeTabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentTimeTab === tab.key }"
|
||||||
|
@tap="switchTimeTab(tab.key)"
|
||||||
|
>
|
||||||
|
<text class="tab-text">{{ tab.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 排行榜范围选择 -->
|
||||||
|
<view class="scope-section">
|
||||||
|
<scroll-view class="scope-tabs" scroll-x enable-flex>
|
||||||
|
<view
|
||||||
|
v-for="scope in scopeTabs"
|
||||||
|
:key="scope.key"
|
||||||
|
class="scope-item"
|
||||||
|
:class="{ active: currentScope === scope.key }"
|
||||||
|
@tap="switchScope(scope.key)"
|
||||||
|
>
|
||||||
|
<uni-icons :type="scope.icon" size="16" :color="currentScope === scope.key ? '#FFFFFF' : '#2D5E3E'" />
|
||||||
|
<text class="scope-text">{{ scope.name }}</text>
|
||||||
|
<view v-if="getUserRankByScope(scope.key)" class="user-rank-badge">
|
||||||
|
<text class="rank-text">第{{ getUserRankByScope(scope.key) }}名</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 前三名金字塔展示 -->
|
||||||
|
<view class="podium-section">
|
||||||
|
<view class="podium-header">
|
||||||
|
<text class="podium-title">🏆 {{ getCurrentScopeTitle() }} 前三甲</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="podium-container">
|
||||||
|
<view
|
||||||
|
v-for="item in topThreeRanking"
|
||||||
|
:key="item.rank"
|
||||||
|
class="podium-item"
|
||||||
|
:class="{
|
||||||
|
first: item.rank === 1,
|
||||||
|
second: item.rank === 2,
|
||||||
|
third: item.rank === 3,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view class="user-info">
|
||||||
|
<image
|
||||||
|
:src="item.avatar || '/static/images/default-avatar.png'"
|
||||||
|
class="user-avatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<text class="user-name">{{ item.studentName }}</text>
|
||||||
|
<text class="user-score">{{ getCurrentScore(item) }}分</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="podium-base">
|
||||||
|
<text class="rank-number">{{ item.rank }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text v-if="item.rank === 1" class="rank-crown">👑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 完整排行榜列表 -->
|
||||||
|
<view class="ranking-list">
|
||||||
|
<view class="list-header">
|
||||||
|
<text class="header-title">完整排行榜</text>
|
||||||
|
<text class="header-subtitle">前20名</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="list-content" scroll-y enable-flex>
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in currentRankingList"
|
||||||
|
:key="item.studentName + index"
|
||||||
|
class="ranking-item"
|
||||||
|
:class="{
|
||||||
|
'top-three': item.rank <= 3,
|
||||||
|
'current-user': item.studentName === userRankInfo.studentName,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- 排名数字 -->
|
||||||
|
<view class="rank-number">
|
||||||
|
<text
|
||||||
|
class="rank-text"
|
||||||
|
:class="{
|
||||||
|
first: item.rank === 1,
|
||||||
|
second: item.rank === 2,
|
||||||
|
third: item.rank === 3,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.rank }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户头像 -->
|
||||||
|
<image
|
||||||
|
:src="item.avatar || '/static/images/default-avatar.png'"
|
||||||
|
class="item-avatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<view class="item-info">
|
||||||
|
<text class="item-name">{{ item.studentName }}</text>
|
||||||
|
<view class="item-details">
|
||||||
|
<text class="item-school">{{ item.schoolName }}</text>
|
||||||
|
<text class="item-class">{{ item.gradeName }} {{ item.className }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 积分信息 -->
|
||||||
|
<view class="item-score">
|
||||||
|
<text class="score-number">{{ getCurrentScore(item) }}</text>
|
||||||
|
<text class="score-unit">分</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 前三名奖杯图标 -->
|
||||||
|
<view v-if="item.rank <= 3" class="trophy-icon">
|
||||||
|
<uni-icons
|
||||||
|
type="medal-filled"
|
||||||
|
size="24"
|
||||||
|
:color="getTrophyColor(item.rank)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-if="currentRankingList.length === 0" class="empty-state">
|
||||||
|
<uni-icons type="list" size="60" color="#CCCCCC" />
|
||||||
|
<text class="empty-text">暂无排行数据</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 激励文字 -->
|
||||||
|
<view class="encouragement-section">
|
||||||
|
<ZhuziCard class="encouragement-card">
|
||||||
|
<view class="encouragement-content">
|
||||||
|
<uni-icons type="star" size="20" color="#DAA520" />
|
||||||
|
<text class="encouragement-text">{{ encouragementText }}</text>
|
||||||
|
</view>
|
||||||
|
</ZhuziCard>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RankingItem } from '@/mocks/ranking'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import {
|
||||||
|
mockCityRanking,
|
||||||
|
mockClassRanking,
|
||||||
|
mockDistrictRanking,
|
||||||
|
mockGradeRanking,
|
||||||
|
mockSchoolRanking,
|
||||||
|
mockUserRankInfo,
|
||||||
|
} from '@/mocks/ranking'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const userRankInfo = ref(mockUserRankInfo)
|
||||||
|
const currentTimeTab = ref<'total' | 'month'>('total')
|
||||||
|
const currentScope = ref<'class' | 'grade' | 'school' | 'district' | 'city'>('class')
|
||||||
|
|
||||||
|
// 时间维度选项卡
|
||||||
|
const timeTabs = [
|
||||||
|
{ key: 'total' as const, name: '总榜' },
|
||||||
|
{ key: 'month' as const, name: '月榜' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 范围维度选项卡
|
||||||
|
const scopeTabs = [
|
||||||
|
{ key: 'class' as const, name: '班级榜', icon: 'person' },
|
||||||
|
{ key: 'grade' as const, name: '年级榜', icon: 'persons' },
|
||||||
|
{ key: 'school' as const, name: '全校榜', icon: 'home' },
|
||||||
|
{ key: 'district' as const, name: '全区榜', icon: 'location' },
|
||||||
|
{ key: 'city' as const, name: '全市榜', icon: 'map' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 当前排行榜数据
|
||||||
|
const currentRankingList = computed(() => {
|
||||||
|
const rankingMap = {
|
||||||
|
class: mockClassRanking,
|
||||||
|
grade: mockGradeRanking,
|
||||||
|
school: mockSchoolRanking,
|
||||||
|
district: mockDistrictRanking,
|
||||||
|
city: mockCityRanking,
|
||||||
|
}
|
||||||
|
|
||||||
|
return rankingMap[currentScope.value] || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 前三名数据
|
||||||
|
const topThreeRanking = computed(() => {
|
||||||
|
return currentRankingList.value.filter(item => item.rank <= 3).sort((a, b) => a.rank - b.rank)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取当前显示的积分
|
||||||
|
function getCurrentScore(item: RankingItem) {
|
||||||
|
return currentTimeTab.value === 'total' ? item.totalScore : item.monthScore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前范围标题
|
||||||
|
function getCurrentScopeTitle() {
|
||||||
|
const titleMap = {
|
||||||
|
class: '班级排行榜',
|
||||||
|
grade: '年级排行榜',
|
||||||
|
school: '全校排行榜',
|
||||||
|
district: '全区排行榜',
|
||||||
|
city: '全市排行榜',
|
||||||
|
}
|
||||||
|
return titleMap[currentScope.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户在指定范围的排名
|
||||||
|
function getUserRankByScope(scope: string) {
|
||||||
|
const rankings = userRankInfo.value.rankings as any
|
||||||
|
return rankings[scope]?.rank
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取奖杯颜色
|
||||||
|
function getTrophyColor(rank: number) {
|
||||||
|
const colors = {
|
||||||
|
1: '#FFD700', // 金色
|
||||||
|
2: '#C0C0C0', // 银色
|
||||||
|
3: '#CD7F32', // 铜色
|
||||||
|
}
|
||||||
|
return colors[rank as keyof typeof colors] || '#CCCCCC'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激励文字
|
||||||
|
const encouragementText = computed(() => {
|
||||||
|
const userRank = getUserRankByScope(currentScope.value)
|
||||||
|
|
||||||
|
if (userRank <= 3) {
|
||||||
|
return '太棒了!您已进入前三名,继续保持!'
|
||||||
|
}
|
||||||
|
else if (userRank <= 10) {
|
||||||
|
return '表现优秀!向前三名发起冲击吧!'
|
||||||
|
}
|
||||||
|
else if (userRank <= 20) {
|
||||||
|
return '成绩不错!再接再厉进入前十名!'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '学而时习之,不亦说乎!坚持学习必有收获!'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换时间维度
|
||||||
|
function switchTimeTab(tab: 'total' | 'month') {
|
||||||
|
currentTimeTab.value = tab
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换排行范围
|
||||||
|
function switchScope(scope: 'class' | 'grade' | 'school' | 'district' | 'city') {
|
||||||
|
currentScope.value = scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时的数据初始化
|
||||||
|
onLoad(() => {
|
||||||
|
// 可以在这里加载用户的排名信息
|
||||||
|
console.log('排行榜页面加载完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
function onRefresh() {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '刷新中...',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟刷新数据
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新成功',
|
||||||
|
icon: 'success',
|
||||||
|
})
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面分享配置
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
const userRank = getUserRankByScope(currentScope.value)
|
||||||
|
return {
|
||||||
|
title: `我在朱子文化学习排行榜中排名第${userRank}名!`,
|
||||||
|
path: '/pages/ranking/ranking',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ranking-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||||
|
|
||||||
|
.user-rank-section {
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(30, 58, 138, 0.1);
|
||||||
|
border-left: 6rpx solid #1e3a8a;
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
border: 3rpx solid #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1e3a8a;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-school {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 32rpx;
|
||||||
|
|
||||||
|
.score-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.score-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-section {
|
||||||
|
padding: 0 32rpx 16rpx;
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #1e3a8a;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(30, 58, 138, 0.3);
|
||||||
|
|
||||||
|
.tab-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1e3a8a;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-section {
|
||||||
|
padding: 0 32rpx 16rpx;
|
||||||
|
|
||||||
|
.scope-tabs {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.scope-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 2rpx solid #e6e6e6;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #1e3a8a;
|
||||||
|
border-color: #1e3a8a;
|
||||||
|
|
||||||
|
.scope-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1e3a8a;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-rank-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
background: #ff6b6b;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 2rpx 8rpx;
|
||||||
|
|
||||||
|
.rank-text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-list {
|
||||||
|
padding: 16rpx 32rpx 32rpx;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16rpx 24rpx;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-content {
|
||||||
|
max-height: 800rpx;
|
||||||
|
|
||||||
|
.ranking-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(45, 94, 62, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.top-three {
|
||||||
|
background: linear-gradient(135deg, rgba(30, 58, 138, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%);
|
||||||
|
border: 2rpx solid #6366f1;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.current-user {
|
||||||
|
border: 3rpx solid #10b981;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-number {
|
||||||
|
width: 60rpx;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
|
||||||
|
.rank-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1e3a8a;
|
||||||
|
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
&.second {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
&.third {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-avatar {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
border: 2rpx solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1e3a8a;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.item-school,
|
||||||
|
.item-class {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-score {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
|
||||||
|
.score-number {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-unit {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trophy-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金字塔展示区域
|
||||||
|
.podium-section {
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.podium-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.podium-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1e3a8a;
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba(30, 58, 138, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.encouragement-section {
|
||||||
|
padding: 0 32rpx 32rpx;
|
||||||
|
|
||||||
|
.encouragement-card {
|
||||||
|
.encouragement-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.encouragement-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #2d5e3e;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
55
src/router/README.md
Normal file
55
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登录页` 的登录逻辑。
|
||||||
29
src/router/config.ts
Normal file
29
src/router/config.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { getAllPages } from '@/utils'
|
||||||
|
|
||||||
|
export const LOGIN_STRATEGY_MAP = {
|
||||||
|
DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP
|
||||||
|
DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录
|
||||||
|
}
|
||||||
|
// TODO: 1/3 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问
|
||||||
|
export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN
|
||||||
|
export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
|
||||||
|
|
||||||
|
export const LOGIN_PAGE = '/pages/login/login'
|
||||||
|
export const REGISTER_PAGE = '/pages/login/register'
|
||||||
|
|
||||||
|
export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE]
|
||||||
|
|
||||||
|
// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同
|
||||||
|
export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path)
|
||||||
|
|
||||||
|
// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表
|
||||||
|
// TODO: 2/3 在 definePage 配置 excludeLoginPath,或者在下面配置 EXCLUDE_LOGIN_PATH_LIST
|
||||||
|
export const EXCLUDE_LOGIN_PATH_LIST = [
|
||||||
|
'/pages/xxx/index',
|
||||||
|
...excludeLoginPathList, // 都是以 / 开头的 path
|
||||||
|
]
|
||||||
|
|
||||||
|
// 在小程序里面是否使用H5的登录页,默认为 false
|
||||||
|
// 如果为 true 则复用 h5 的登录逻辑
|
||||||
|
// TODO: 3/3 确定自己的登录页是否需要在小程序里面使用
|
||||||
|
export const LOGIN_PAGE_ENABLE_IN_MP = false
|
||||||
120
src/router/interceptor.ts
Normal file
120
src/router/interceptor.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { isMp } from '@uni-helper/uni-env'
|
||||||
|
/**
|
||||||
|
* by 菲鸽 on 2025-08-19
|
||||||
|
* 路由拦截,通常也是登录拦截
|
||||||
|
* 黑白名单的配置,请看 config.ts 文件, EXCLUDE_LOGIN_PATH_LIST
|
||||||
|
*/
|
||||||
|
import { useTokenStore } from '@/store/token'
|
||||||
|
import { isPageTabbar, tabbarStore } from '@/tabbar/store'
|
||||||
|
import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index'
|
||||||
|
import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP } from './config'
|
||||||
|
|
||||||
|
export const FG_LOG_ENABLE = false
|
||||||
|
export function judgeIsExcludePath(path: string) {
|
||||||
|
const isDev = import.meta.env.DEV
|
||||||
|
if (!isDev) {
|
||||||
|
return EXCLUDE_LOGIN_PATH_LIST.includes(path)
|
||||||
|
}
|
||||||
|
const allExcludeLoginPages = getAllPages('excludeLoginPath') // dev 环境下,需要每次都重新获取,否则新配置就不会生效
|
||||||
|
return EXCLUDE_LOGIN_PATH_LIST.includes(path) || (isDev && allExcludeLoginPages.some(page => page.path === path))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const navigateToInterceptor = {
|
||||||
|
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
|
||||||
|
// 增加对相对路径的处理,BY 网友 @ideal
|
||||||
|
invoke({ url, query }: { url: string, query?: Record<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}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理直接进入路由非首页时,tabbarIndex 不正确的问题
|
||||||
|
tabbarStore.setAutoCurIdx(path)
|
||||||
|
|
||||||
|
// 小程序里面使用平台自带的登录,则不走下面的逻辑
|
||||||
|
if (isMp && LOGIN_PAGE_ENABLE_IN_MP) {
|
||||||
|
return true // 明确表示允许路由继续执行
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
FG_LOG_ENABLE && console.log('tokenStore.hasLogin:', tokenStore.hasLogin)
|
||||||
|
|
||||||
|
// 不管黑白名单,登录了就直接去吧(但是当前不能是登录页)
|
||||||
|
if (tokenStore.hasLogin) {
|
||||||
|
if (path !== LOGIN_PAGE) {
|
||||||
|
return true // 明确表示允许路由继续执行
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('已经登录,但是还在登录页', myQuery.redirect)
|
||||||
|
const url = myQuery.redirect || HOME_PAGE
|
||||||
|
if (isPageTabbar(url)) {
|
||||||
|
uni.switchTab({ url })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
}
|
||||||
|
return false // 明确表示阻止原路由继续执行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fullPath = path
|
||||||
|
|
||||||
|
if (Object.keys(myQuery).length) {
|
||||||
|
fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}`
|
||||||
|
}
|
||||||
|
const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}`
|
||||||
|
|
||||||
|
// #region 1/2 默认需要登录的情况(白名单策略) ---------------------------
|
||||||
|
if (isNeedLoginMode) {
|
||||||
|
// 需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示白名单,可以直接通过
|
||||||
|
if (judgeIsExcludePath(path)) {
|
||||||
|
return true // 明确表示允许路由继续执行
|
||||||
|
}
|
||||||
|
// 否则需要重定向到登录页
|
||||||
|
else {
|
||||||
|
if (path === LOGIN_PAGE) {
|
||||||
|
return true // 明确表示允许路由继续执行
|
||||||
|
}
|
||||||
|
FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl)
|
||||||
|
uni.navigateTo({ url: redirectUrl })
|
||||||
|
return false // 明确表示阻止原路由继续执行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion 1/2 默认需要登录的情况(白名单策略) ---------------------------
|
||||||
|
|
||||||
|
// #region 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
|
||||||
|
else {
|
||||||
|
// 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页
|
||||||
|
if (judgeIsExcludePath(path)) {
|
||||||
|
FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl)
|
||||||
|
uni.navigateTo({ url: redirectUrl })
|
||||||
|
return false // 修改为false,阻止原路由继续执行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
|
||||||
|
return true // 明确表示允许路由继续执行
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeInterceptor = {
|
||||||
|
install() {
|
||||||
|
uni.addInterceptor('navigateTo', navigateToInterceptor)
|
||||||
|
uni.addInterceptor('reLaunch', navigateToInterceptor)
|
||||||
|
uni.addInterceptor('redirectTo', navigateToInterceptor)
|
||||||
|
uni.addInterceptor('switchTab', navigateToInterceptor)
|
||||||
|
},
|
||||||
|
}
|
||||||
13
src/service/displayEnumLabel.ts
Normal file
13
src/service/displayEnumLabel.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import * as API from './types';
|
||||||
|
|
||||||
|
export function displayStatusEnum(field: API.StatusEnum) {
|
||||||
|
return { available: 'available', pending: 'pending', sold: 'sold' }[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displayStatusEnum2(field: API.StatusEnum2) {
|
||||||
|
return { placed: 'placed', approved: 'approved', delivered: 'delivered' }[
|
||||||
|
field
|
||||||
|
];
|
||||||
|
}
|
||||||
11
src/service/index.ts
Normal file
11
src/service/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
export * from './types';
|
||||||
|
export * from './displayEnumLabel';
|
||||||
|
|
||||||
|
export * from './pet';
|
||||||
|
export * from './pet.vuequery';
|
||||||
|
export * from './store';
|
||||||
|
export * from './store.vuequery';
|
||||||
|
export * from './user';
|
||||||
|
export * from './user.vuequery';
|
||||||
185
src/service/pet.ts
Normal file
185
src/service/pet.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import request from '@/http/vue-query';
|
||||||
|
import type { CustomRequestOptions } from '@/http/types';
|
||||||
|
|
||||||
|
import * as API from './types';
|
||||||
|
|
||||||
|
/** Update an existing pet PUT /pet */
|
||||||
|
export async function petUsingPut({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: API.Pet;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<unknown>('/pet', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a new pet to the store POST /pet */
|
||||||
|
export async function petUsingPost({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: API.Pet;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<unknown>('/pet', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find pet by ID Returns a single pet GET /pet/${param0} */
|
||||||
|
export async function petPetIdUsingGet({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petPetIdUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { petId: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<API.Pet>(`/pet/${param0}`, {
|
||||||
|
method: 'GET',
|
||||||
|
params: { ...queryParams },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates a pet in the store with form data POST /pet/${param0} */
|
||||||
|
export async function petPetIdUsingPost({
|
||||||
|
params,
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petPetIdUsingPostParams;
|
||||||
|
body: API.PetPetIdUsingPostBody;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { petId: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<unknown>(`/pet/${param0}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
params: { ...queryParams },
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deletes a pet DELETE /pet/${param0} */
|
||||||
|
export async function petPetIdUsingDelete({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petPetIdUsingDeleteParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { petId: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<unknown>(`/pet/${param0}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
params: { ...queryParams },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** uploads an image POST /pet/${param0}/uploadImage */
|
||||||
|
export async function petPetIdUploadImageUsingPost({
|
||||||
|
params,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petPetIdUploadImageUsingPostParams;
|
||||||
|
body: API.PetPetIdUploadImageUsingPostBody;
|
||||||
|
file?: File;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { petId: param0, ...queryParams } = params;
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
formData.append('file', file);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(body).forEach((ele) => {
|
||||||
|
const item = (body as { [key: string]: any })[ele];
|
||||||
|
|
||||||
|
if (item !== undefined && item !== null) {
|
||||||
|
if (typeof item === 'object' && !(item instanceof File)) {
|
||||||
|
if (item instanceof Array) {
|
||||||
|
item.forEach((f) => formData.append(ele, f || ''));
|
||||||
|
} else {
|
||||||
|
formData.append(ele, JSON.stringify(item));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formData.append(ele, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return request<API.ApiResponse>(`/pet/${param0}/uploadImage`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
params: { ...queryParams },
|
||||||
|
data: formData,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
|
||||||
|
export async function petFindByStatusUsingGet({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petFindByStatusUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<API.Pet[]>('/pet/findByStatus', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */
|
||||||
|
export async function petFindByTagsUsingGet({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petFindByTagsUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<API.Pet[]>('/pet/findByTags', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
151
src/service/pet.vuequery.ts
Normal file
151
src/service/pet.vuequery.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import { queryOptions, useMutation } from '@tanstack/vue-query';
|
||||||
|
import type { DefaultError } from '@tanstack/vue-query';
|
||||||
|
import request from '@/http/vue-query';
|
||||||
|
import type { CustomRequestOptions } from '@/http/types';
|
||||||
|
|
||||||
|
import * as apis from './pet';
|
||||||
|
import * as API from './types';
|
||||||
|
|
||||||
|
/** Update an existing pet PUT /pet */
|
||||||
|
export function usePetUsingPutMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.petUsingPut,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a new pet to the store POST /pet */
|
||||||
|
export function usePetUsingPostMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.petUsingPost,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find pet by ID Returns a single pet GET /pet/${param0} */
|
||||||
|
export function petPetIdUsingGetQueryOptions(options: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petPetIdUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.petPetIdUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['petPetIdUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates a pet in the store with form data POST /pet/${param0} */
|
||||||
|
export function usePetPetIdUsingPostMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.petPetIdUsingPost,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deletes a pet DELETE /pet/${param0} */
|
||||||
|
export function usePetPetIdUsingDeleteMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.petPetIdUsingDelete,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** uploads an image POST /pet/${param0}/uploadImage */
|
||||||
|
export function usePetPetIdUploadImageUsingPostMutation(options?: {
|
||||||
|
onSuccess?: (value?: API.ApiResponse) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.petPetIdUploadImageUsingPost,
|
||||||
|
onSuccess(data: API.ApiResponse) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
|
||||||
|
export function petFindByStatusUsingGetQueryOptions(options: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petFindByStatusUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.petFindByStatusUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['petFindByStatusUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */
|
||||||
|
export function petFindByTagsUsingGetQueryOptions(options: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.petFindByTagsUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.petFindByTagsUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['petFindByTagsUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
72
src/service/store.ts
Normal file
72
src/service/store.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import request from '@/http/vue-query';
|
||||||
|
import type { CustomRequestOptions } from '@/http/types';
|
||||||
|
|
||||||
|
import * as API from './types';
|
||||||
|
|
||||||
|
/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
|
||||||
|
export async function storeInventoryUsingGet({
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<Record<string, number>>('/store/inventory', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Place an order for a pet POST /store/order */
|
||||||
|
export async function storeOrderUsingPost({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: API.Order;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<API.Order>('/store/order', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */
|
||||||
|
export async function storeOrderOrderIdUsingGet({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.storeOrderOrderIdUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { orderId: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<API.Order>(`/store/order/${param0}`, {
|
||||||
|
method: 'GET',
|
||||||
|
params: { ...queryParams },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
|
||||||
|
export async function storeOrderOrderIdUsingDelete({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.storeOrderOrderIdUsingDeleteParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { orderId: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<unknown>(`/store/order/${param0}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
params: { ...queryParams },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
75
src/service/store.vuequery.ts
Normal file
75
src/service/store.vuequery.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import { queryOptions, useMutation } from '@tanstack/vue-query';
|
||||||
|
import type { DefaultError } from '@tanstack/vue-query';
|
||||||
|
import request from '@/http/vue-query';
|
||||||
|
import type { CustomRequestOptions } from '@/http/types';
|
||||||
|
|
||||||
|
import * as apis from './store';
|
||||||
|
import * as API from './types';
|
||||||
|
|
||||||
|
/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
|
||||||
|
export function storeInventoryUsingGetQueryOptions(options: {
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.storeInventoryUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['storeInventoryUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Place an order for a pet POST /store/order */
|
||||||
|
export function useStoreOrderUsingPostMutation(options?: {
|
||||||
|
onSuccess?: (value?: API.Order) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.storeOrderUsingPost,
|
||||||
|
onSuccess(data: API.Order) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */
|
||||||
|
export function storeOrderOrderIdUsingGetQueryOptions(options: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.storeOrderOrderIdUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.storeOrderOrderIdUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['storeOrderOrderIdUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
|
||||||
|
export function useStoreOrderOrderIdUsingDeleteMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.storeOrderOrderIdUsingDelete,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
146
src/service/types.ts
Normal file
146
src/service/types.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
|
||||||
|
export type ApiResponse = {
|
||||||
|
code?: number;
|
||||||
|
type?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Category = {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Order = {
|
||||||
|
id?: number;
|
||||||
|
petId?: number;
|
||||||
|
quantity?: number;
|
||||||
|
shipDate?: string;
|
||||||
|
/** Order Status */
|
||||||
|
status?: 'placed' | 'approved' | 'delivered';
|
||||||
|
complete?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Pet = {
|
||||||
|
id?: number;
|
||||||
|
category?: Category;
|
||||||
|
name: string;
|
||||||
|
photoUrls: string[];
|
||||||
|
tags?: Tag[];
|
||||||
|
/** pet status in the store */
|
||||||
|
status?: 'available' | 'pending' | 'sold';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type petFindByStatusUsingGetParams = {
|
||||||
|
/** Status values that need to be considered for filter */
|
||||||
|
status: ('available' | 'pending' | 'sold')[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type petFindByTagsUsingGetParams = {
|
||||||
|
/** Tags to filter by */
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PetPetIdUploadImageUsingPostBody = {
|
||||||
|
/** Additional data to pass to server */
|
||||||
|
additionalMetadata?: string;
|
||||||
|
/** file to upload */
|
||||||
|
file?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type petPetIdUploadImageUsingPostParams = {
|
||||||
|
/** ID of pet to update */
|
||||||
|
petId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type petPetIdUsingDeleteParams = {
|
||||||
|
/** Pet id to delete */
|
||||||
|
petId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type petPetIdUsingGetParams = {
|
||||||
|
/** ID of pet to return */
|
||||||
|
petId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PetPetIdUsingPostBody = {
|
||||||
|
/** Updated name of the pet */
|
||||||
|
name?: string;
|
||||||
|
/** Updated status of the pet */
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type petPetIdUsingPostParams = {
|
||||||
|
/** ID of pet that needs to be updated */
|
||||||
|
petId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum StatusEnum {
|
||||||
|
'available' = 'available',
|
||||||
|
'pending' = 'pending',
|
||||||
|
'sold' = 'sold',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IStatusEnum = keyof typeof StatusEnum;
|
||||||
|
|
||||||
|
export enum StatusEnum2 {
|
||||||
|
'placed' = 'placed',
|
||||||
|
'approved' = 'approved',
|
||||||
|
'delivered' = 'delivered',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IStatusEnum2 = keyof typeof StatusEnum2;
|
||||||
|
|
||||||
|
export type storeOrderOrderIdUsingDeleteParams = {
|
||||||
|
/** ID of the order that needs to be deleted */
|
||||||
|
orderId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type storeOrderOrderIdUsingGetParams = {
|
||||||
|
/** ID of pet that needs to be fetched */
|
||||||
|
orderId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Tag = {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id?: number;
|
||||||
|
username?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
phone?: string;
|
||||||
|
/** User Status */
|
||||||
|
userStatus?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserCreateWithArrayUsingPostBody = User[];
|
||||||
|
|
||||||
|
export type UserCreateWithListUsingPostBody = User[];
|
||||||
|
|
||||||
|
export type userLoginUsingGetParams = {
|
||||||
|
/** The user name for login */
|
||||||
|
username: string;
|
||||||
|
/** The password for login in clear text */
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type userUsernameUsingDeleteParams = {
|
||||||
|
/** The name that needs to be deleted */
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type userUsernameUsingGetParams = {
|
||||||
|
/** The name that needs to be fetched. Use user1 for testing. */
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type userUsernameUsingPutParams = {
|
||||||
|
/** name that need to be updated */
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
150
src/service/user.ts
Normal file
150
src/service/user.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import request from '@/http/vue-query';
|
||||||
|
import type { CustomRequestOptions } from '@/http/types';
|
||||||
|
|
||||||
|
import * as API from './types';
|
||||||
|
|
||||||
|
/** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */
|
||||||
|
export async function userUsingPost({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: API.User;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<unknown>('/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get user by user name GET /user/${param0} */
|
||||||
|
export async function userUsernameUsingGet({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.userUsernameUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { username: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<API.User>(`/user/${param0}`, {
|
||||||
|
method: 'GET',
|
||||||
|
params: { ...queryParams },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updated user This can only be done by the logged in user. PUT /user/${param0} */
|
||||||
|
export async function userUsernameUsingPut({
|
||||||
|
params,
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.userUsernameUsingPutParams;
|
||||||
|
body: API.User;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { username: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<unknown>(`/user/${param0}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
params: { ...queryParams },
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
|
||||||
|
export async function userUsernameUsingDelete({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.userUsernameUsingDeleteParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
const { username: param0, ...queryParams } = params;
|
||||||
|
|
||||||
|
return request<unknown>(`/user/${param0}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
params: { ...queryParams },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */
|
||||||
|
export async function userCreateWithArrayUsingPost({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: API.UserCreateWithArrayUsingPostBody;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<unknown>('/user/createWithArray', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */
|
||||||
|
export async function userCreateWithListUsingPost({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: API.UserCreateWithListUsingPostBody;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<unknown>('/user/createWithList', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logs user into the system GET /auth/login */
|
||||||
|
export async function userLoginUsingGet({
|
||||||
|
params,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.userLoginUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<string>('/auth/login', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logs out current logged in user session 返回值: successful operation GET /user/logout */
|
||||||
|
export async function userLogoutUsingGet({
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<unknown>('/user/logout', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
149
src/service/user.vuequery.ts
Normal file
149
src/service/user.vuequery.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import { queryOptions, useMutation } from '@tanstack/vue-query';
|
||||||
|
import type { DefaultError } from '@tanstack/vue-query';
|
||||||
|
import request from '@/http/vue-query';
|
||||||
|
import type { CustomRequestOptions } from '@/http/types';
|
||||||
|
|
||||||
|
import * as apis from './user';
|
||||||
|
import * as API from './types';
|
||||||
|
|
||||||
|
/** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */
|
||||||
|
export function useUserUsingPostMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.userUsingPost,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get user by user name GET /user/${param0} */
|
||||||
|
export function userUsernameUsingGetQueryOptions(options: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.userUsernameUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.userUsernameUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['userUsernameUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updated user This can only be done by the logged in user. PUT /user/${param0} */
|
||||||
|
export function useUserUsernameUsingPutMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.userUsernameUsingPut,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
|
||||||
|
export function useUserUsernameUsingDeleteMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.userUsernameUsingDelete,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */
|
||||||
|
export function useUserCreateWithArrayUsingPostMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.userCreateWithArrayUsingPost,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */
|
||||||
|
export function useUserCreateWithListUsingPostMutation(options?: {
|
||||||
|
onSuccess?: (value?: unknown) => void;
|
||||||
|
onError?: (error?: DefaultError) => void;
|
||||||
|
}) {
|
||||||
|
const { onSuccess, onError } = options || {};
|
||||||
|
|
||||||
|
const response = useMutation({
|
||||||
|
mutationFn: apis.userCreateWithListUsingPost,
|
||||||
|
onSuccess(data: unknown) {
|
||||||
|
onSuccess?.(data);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
onError?.(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logs user into the system GET /auth/login */
|
||||||
|
export function userLoginUsingGetQueryOptions(options: {
|
||||||
|
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
|
||||||
|
params: API.userLoginUsingGetParams;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.userLoginUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['userLoginUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logs out current logged in user session 返回值: successful operation GET /user/logout */
|
||||||
|
export function userLogoutUsingGetQueryOptions(options: {
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
return apis.userLogoutUsingGet(queryKey[1] as typeof options);
|
||||||
|
},
|
||||||
|
queryKey: ['userLogoutUsingGet', options],
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
src/static/app/icons/1024x1024.png
Normal file
BIN
src/static/app/icons/1024x1024.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user