feat: init

This commit is contained in:
Mu Yi 2025-11-14 15:48:15 +08:00
commit 81737d93a5
253 changed files with 21438 additions and 0 deletions

3
.commitlintrc.cjs Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
}

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

View 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: 开发工作流程和最佳实践指南
---

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

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

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

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

31
.dockerignore Normal file
View File

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

13
.editorconfig Normal file
View 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 # 关闭末尾空格修剪

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.hbuilderx
.stylelintcache
.eslintcache
docs/.vitepress/dist
docs/.vitepress/cache
src/types
# 单独把这个文件排除掉,用以解决部分电脑生成的 auto-import.d.ts 的API不完整导致类型提示报错问题
!src/types/auto-import.d.ts
src/manifest.json
src/pages.json
# 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/commit-msg Normal file
View File

@ -0,0 +1 @@
npx --no-install commitlint --edit "$1"

1
.husky/pre-commit Normal file
View File

@ -0,0 +1 @@
npx lint-staged --allow-empty

8
.npmrc Normal file
View 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

View 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 规范
- 注意页面栈和导航管理

17
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"recommendations": [
"vue.volar",
"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",
"christian-kohler.path-intellisense"
]
}

96
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,96 @@
{
// 配置语言的文件关联
"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",
"docker.md": "Dockerfile,docker*.md,nginx*,.dockerignore",
"pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts",
"package.json": "tsconfig.json,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
"eslint.config.mjs": ".commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*"
},
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
],
"cSpell.words": [
"alova",
"Aplipay",
"attributify",
"chooseavatar",
"climblee",
"commitlint",
"dcloudio",
"iconfont",
"oxlint",
"qrcode",
"refresherrefresh",
"scrolltolower",
"tabbar",
"Toutiao",
"uniapp",
"unibest",
"unocss",
"uview",
"uvui",
"Wechat",
"WechatMiniprogram",
"Weixin"
]
}

77
.vscode/vue3.code-snippets vendored Normal file
View 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"
],
},
}

38
Dockerfile Normal file
View File

@ -0,0 +1,38 @@
# 使用 node:24-alpine 作为基础镜像,固定版本+减少体积
FROM node:24-alpine AS builder
# 在容器中创建目录
WORKDIR /app
# 安装pnpm(使用 npm 的 --global-style 可以减少依赖安装体积)
RUN npm install -g pnpm@10.10.0 --global-style
# 设置pnpm镜像源
RUN pnpm config set registry https://registry.npmmirror.com
# 复制依赖文件
COPY package.json pnpm-lock.yaml ./
# 先复制scripts目录,因为prepare脚本需要用到其中的文件
COPY scripts ./scripts
# 安装依赖,但跳过prepare脚本(这一步会缓存,只有 package.json 或 pnpm-lock.yaml 变化时才会重新运行)
RUN pnpm install --ignore-scripts --frozen-lockfile
# 手动执行我们需要的docker:prepare脚本
RUN pnpm run docker:prepare
# 复制其余源代码
COPY . .
# 构建项目
RUN pnpm run build
# 使用nginx作为服务
FROM nginx:1.29.1-alpine3.22 AS production-stage
# 将构建好的项目复制到nginx下
COPY --from=builder /app/dist/build/h5 /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
EXPOSE 443
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

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

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

28
docker.md Normal file
View File

@ -0,0 +1,28 @@
## Docker
根据提供的 `Dockerfile`,可以通过以下步骤构建并运行镜像:
### 1. 构建Docker镜像
在项目根目录执行以下命令:
- `-t unibest:v1-2025091701`:为镜像指定名称和标签,YYYYMMDD+编号
- `.`:表示使用当前目录的Dockerfile
```bash
docker build -t unibest:v1-2025091701 .
docker build -t unibest:v1-2025091702 .
```
### 2. 运行Docker容器
使用以下命令运行容器:
```bash
docker run -d --name unibest-v1-2025091701 -p 80:80 unibest:v1-2025091701
docker run -d --name unibest-v1-2025091702 -p 80:80 unibest:v1-2025091702
```
- `-d`:表示在后台运行容器
- `-p 80:80`:将容器的80端口映射到主机的80端口
- `--name unibest-v1-2025091701`:为容器指定一个名称

32
env/.env vendored Normal file
View File

@ -0,0 +1,32 @@
VITE_APP_TITLE = '六玮中考'
VITE_APP_PORT = 9000
VITE_UNI_APPID = 'H57F2ACE4'
VITE_WX_APPID = 'wxc2399d3aa57174db'
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
# 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/
VITE_APP_PUBLIC_BASE=/
# 后台请求地址
# http://192.168.100.129:5066
# https://api.v3.ycymedu.com
VITE_SERVER_BASEURL = 'http://192.168.100.129:5066'
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
# 注意,如果是微信小程序,还有一套请求地址的配置,根据 develop、trial、release 分别设置上传地址,见 `src/utils/index.ts`。
# h5是否需要配置代理
VITE_APP_PROXY_ENABLE = false
# 下面的不用修改,只要不跟你后台的统一前缀冲突就行。如果修改了,记得修改 `nginx` 里面的配置
VITE_APP_PROXY_PREFIX = '/fg-api'
# 第二个请求地址 (目前alova中可以使用)
VITE_SERVER_BASEURL_SECONDARY = 'https://api.v3.ycymedu.com'
# 认证模式,'single' | 'double' ==> 单token | 双token
VITE_AUTH_MODE = 'single'
# 原生插件资源复制开关,控制是否启用 copy-native-resources 插件
VITE_COPY_NATIVE_RES_ENABLE = false

9
env/.env.development vendored Normal file
View 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
View 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
View 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'

58
eslint.config.mjs Normal file
View File

@ -0,0 +1,58 @@
import uniHelper from '@uni-helper/eslint-config'
export default uniHelper({
unocss: true,
vue: true,
markdown: false,
ignores: [
// 忽略uni_modules目录
'**/uni_modules/',
// 忽略原生插件目录
'**/nativeplugins/',
'dist',
// unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
'auto-import.d.ts',
// vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
'uni-pages.d.ts',
// 插件生成的文件
'src/pages.json',
'src/manifest.json',
// 忽略自动生成文件
'src/service/**',
],
// https://eslint-config.antfu.me/rules
rules: {
'no-useless-return': 'off',
'no-console': 'off',
'no-unused-vars': 'off',
'vue/no-unused-refs': 'off',
'unused-imports/no-unused-vars': 'off',
'eslint-comments/no-unlimited-disable': 'off',
'jsdoc/check-param-names': 'off',
'jsdoc/require-returns-description': 'off',
'ts/no-empty-object-type': 'off',
'no-extend-native': 'off',
'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

Binary file not shown.

26
index.html Normal file
View 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>

155
manifest.config.ts Normal file
View File

@ -0,0 +1,155 @@
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,
},
// 是否合并组件虚拟节点外层属性,uni-app 3.5.1+ 开始支持。目前仅支持 style、class 属性。
// 默认不开启(undefined),这里设置为开启。
mergeVirtualHostAttributes: true,
// styleIsolation: 'shared',
usingComponents: true,
// __usePrivacyCheck__: true,
},
'mp-alipay': {
usingComponents: true,
styleIsolation: 'shared',
optimization: {
subPackages: true,
},
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
'uniStatistics': {
enable: false,
},
'vueVersion': '3',
})

145
nginx.conf Normal file
View File

@ -0,0 +1,145 @@
# 配置工作进程数,通常设置为 CPU 核心数
worker_processes auto;
# 错误日志配置
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
# 开启多路复用
use epoll;
}
# 文件描述符限制 - 移到这里,在http块之前
worker_rlimit_nofile 65535;
http {
# 日志格式定义
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志配置
access_log /var/log/nginx/access.log main;
# 高效文件传输设置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# 连接超时设置
keepalive_timeout 65;
keepalive_requests 100;
# gzip 压缩优化
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_buffers 16 8k;
gzip_http_version 1.1;
# 增加更多文件类型
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
# 全局设置
# 合理限制请求体大小,根据实际需求调整
client_max_body_size 10m;
client_body_buffer_size 128k;
client_header_timeout 60s;
client_body_timeout 60s;
server {
listen 80;
server_name _;
gunzip on;
gzip_static always;
include /etc/nginx/mime.types;
absolute_redirect off;
root /usr/share/nginx/html;
# 安全相关响应头
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
# 根据实际情况调整 CSP
# add_header Content-Security-Policy "default-src 'self'";
# 处理 SPA 应用路由
location / {
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
# HTML 和 JSON 文件 - 短缓存策略
location ~ .*\.(html|json)$ {
add_header Cache-Control "public, max-age=300, must-revalidate";
}
# 静态资源 - 长缓存策略
location ~ .*\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|ttf|woff|woff2|eot|mp4|mp3|swf)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
expires 365d;
access_log off;
}
# JS 和 CSS - 带版本号的长缓存
location ~ .*\.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
expires 365d;
access_log off;
}
# 接口转发 - 替换为实际后端地址
# location ^~ /fg-api {
# proxy_http_version 1.1;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header Host $host;
# # 后端是HTTPS时的必要配置
# proxy_ssl_server_name on;
# proxy_ssl_protocols TLSv1.2 TLSv1.3;
# proxy_ssl_session_reuse on;
# # 对于生产环境,应该尽量使用有效的证书而不是依赖``proxy_ssl_verify off;`` ,因为这会带来安全风险
# proxy_ssl_verify off;
# # TODO:替换为实际后端服务地址
# # 注意在URL末尾添加了斜杠,这样Nginx会去掉 /fg-api 前缀
# # 前端请求 http://your-domain.com/fg-api/users 转发到 https://ukw0y1.laf.run/users
# proxy_pass https://ukw0y1.laf.run/;
# # 上面一行的效果与下面2行一样的效果,都是为了去掉 /fg-api 前缀
# # 显式移除/fg-api前缀
# # rewrite ^/fg-api(.*)$ $1 break;
# # 域名末尾不需要斜杠了
# # proxy_pass https://ukw0y1.laf.run;
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
# proxy_buffers 8 32k;
# proxy_buffer_size 64k;
# proxy_busy_buffers_size 128k;
# proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
# }
# 错误页面配置
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
}

View File

@ -0,0 +1,13 @@
import type { GenerateServiceProps } from 'openapi-ts-request'
export default [
{
schemaPath: 'https://ukw0y1.laf.run/unibest-opapi-test.json',
serversPath: './src/service',
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions } from '@/http/types';`,
requestOptionsType: 'CustomRequestOptions',
isGenReactQuery: false,
reactQueryMode: 'vue',
isGenJavaScript: false,
},
] as GenerateServiceProps[]

178
package.json Normal file
View File

@ -0,0 +1,178 @@
{
"name": "volunteer-secondary",
"type": "module",
"version": "3.18.6",
"unibest-version": "3.18.6",
"update-time": "2025-10-10",
"packageManager": "pnpm@10.10.0",
"generate-time": "用户创建项目时生成",
"license": "MIT",
"engines": {
"node": ">=20",
"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": "uni",
"dev:test": "uni --mode test",
"dev:prod": "uni --mode production",
"dev:h5": "uni",
"dev:h5:test": "uni --mode test",
"dev:h5:prod": "uni --mode production",
"dev:h5:ssr": "uni --ssr",
"dev:mp": "uni -p mp-weixin",
"dev:mp:test": "uni -p mp-weixin --mode test",
"dev:mp:prod": "uni -p mp-weixin --mode production",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-xhs": "uni -p mp-xhs",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app:test": "uni build -p app --mode test",
"build:app:prod": "uni build -p app --mode production",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:test": "uni build --mode test",
"build:h5:prod": "uni build --mode production",
"build": "uni build",
"build:test": "uni build --mode test",
"build:prod": "uni build --mode production",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp": "uni build -p mp-weixin",
"build:mp:test": "uni build -p mp-weixin --mode test",
"build:mp:prod": "uni build -p mp-weixin --mode production",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-xhs": "uni build -p mp-xhs",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit",
"openapi": "openapi-ts",
"prepare": "git init && husky && node ./scripts/create-base-files.js",
"docker:prepare": "node ./scripts/create-base-files.js",
"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",
"@z-cloud/virtual-uni": "^1.0.5",
"abortcontroller-polyfill": "^1.7.8",
"alova": "^3.3.3",
"dayjs": "1.11.10",
"htmlparser2": "^10.0.0",
"js-cookie": "^3.0.5",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"sard-uniapp": "^1.22.1",
"vue": "^3.4.21",
"z-paging": "^2.8.8"
},
"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.3",
"@uni-helper/vite-plugin-uni-layouts": "0.1.11",
"@uni-helper/vite-plugin-uni-manifest": "0.2.8",
"@uni-helper/vite-plugin-uni-pages": "0.3.19",
"@uni-helper/vite-plugin-uni-platform": "0.0.5",
"@uni-ku/bundle-optimizer": "v1.3.15-beta.2",
"@uni-ku/root": "1.4.1",
"@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",
"std-env": "^3.9.0",
"typescript": "~5.8.0",
"unocss": "66.0.0",
"unplugin-auto-import": "^20.0.0",
"vite": "5.2.8",
"vite-plugin-restart": "^1.0.0",
"vue-tsc": "^3.0.6"
},
"pnpm": {
"overrides": {
"unconfig": "7.3.2"
}
},
"overrides": {
"unconfig": "7.3.2"
},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china",
"unconfig": "7.3.2"
},
"lint-staged": {
"*": "eslint --fix"
}
}

29
pages.config.ts Normal file
View File

@ -0,0 +1,29 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
import { tabBar } from './src/tabbar/config'
export default defineUniPages({
globalStyle: {
navigationStyle: 'default',
navigationBarTitleText: '中考志愿',
navigationBarBackgroundColor: '#f8f8f8',
navigationBarTextStyle: 'black',
backgroundColor: '#FFFFFF',
},
easycom: {
autoscan: true,
custom: {
'^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
'^sar-(.*)': 'sard-uniapp/components/$1/$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: tabBar as any,
preloadRule: {
'pages/index/index': {
network: 'all',
packages: ['chart-sub'],
},
},
})

View File

@ -0,0 +1,12 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 2adcf3437a54b6ac130a3d018bdd5c4eab35bdf6..365844a2c06dc7c80d227978c5a4defee4aabc90 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -454,7 +454,7 @@ class PageContext {
const customPageMetaData = overrides || [];
const result = customPageMetaData.length ? mergePageMetaDataArray(generatedPageMetaData.concat(customPageMetaData)) : generatedPageMetaData;
const parseMeta = result.filter(
(page, index, self) => self.slice().reverse().findIndex((item) => page.path === item.path) === (self.length - 1 - index)
);
return type === "main" ? this.setHomePage(parseMeta) : parseMeta;
}

View File

@ -0,0 +1,52 @@
// 基础配置文件生成脚本
// 此脚本用于生成 src/manifest.json 和 src/pages.json 基础文件
// 由于这两个配置文件会被添加到 .gitignore 中,因此需要通过此脚本确保项目能正常运行
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
// 获取当前文件的目录路径(替代 CommonJS 中的 __dirname)
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// 最简可运行配置
const manifest = { }
const pages = {
pages: [
{
path: 'pages/index/index',
type: 'home',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '首页',
},
},
{
path: 'pages/me/me',
type: 'page',
style: {
navigationBarTitleText: '我的',
},
},
],
}
// 使用修复后的 __dirname 来解析文件路径
const manifestPath = path.resolve(__dirname, '../src/manifest.json')
const pagesPath = path.resolve(__dirname, '../src/pages.json')
// 确保 src 目录存在
const srcDir = path.resolve(__dirname, '../src')
if (!fs.existsSync(srcDir)) {
fs.mkdirSync(srcDir, { recursive: true })
}
// 如果 src/manifest.json 不存在,就创建它;存在就不处理,以免覆盖
if (!fs.existsSync(manifestPath)) {
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
}
// 如果 src/pages.json 不存在,就创建它;存在就不处理,以免覆盖
if (!fs.existsSync(pagesPath)) {
fs.writeFileSync(pagesPath, JSON.stringify(pages, null, 2))
}

83
scripts/open-dev-tools.js Normal file
View File

@ -0,0 +1,83 @@
import { exec } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
/**
* 打开开发者工具
*/
function _openDevTools() {
const platform = process.platform // darwin, win32, linux
const { UNI_PLATFORM } = process.env // mp-weixin, mp-alipay
const uniPlatformText = UNI_PLATFORM === 'mp-weixin' ? '微信小程序' : UNI_PLATFORM === 'mp-alipay' ? '支付宝小程序' : '小程序'
// 项目路径(构建输出目录)
const projectPath = path.resolve(process.cwd(), `dist/dev/${UNI_PLATFORM}`)
// 检查构建输出目录是否存在
if (!fs.existsSync(projectPath)) {
console.log(`❌ ${uniPlatformText}构建目录不存在:`, projectPath)
return
}
console.log(`🚀 正在打开${uniPlatformText}开发者工具...`)
// 根据不同操作系统执行不同命令
let command = ''
if (platform === 'darwin') {
// macOS
if (UNI_PLATFORM === 'mp-weixin') {
command = `/Applications/wechatwebdevtools.app/Contents/MacOS/cli -o "${projectPath}"`
}
else if (UNI_PLATFORM === 'mp-alipay') {
command = `/Applications/小程序开发者工具.app/Contents/MacOS/小程序开发者工具 --p "${projectPath}"`
}
}
else if (platform === 'win32' || platform === 'win64') {
// Windows
if (UNI_PLATFORM === 'mp-weixin') {
command = `"C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat" -o "${projectPath}"`
}
}
else {
// Linux 或其他系统
console.log('❌ 当前系统不支持自动打开微信开发者工具')
return
}
exec(command, (error, stdout, stderr) => {
if (error) {
console.log(`❌ 打开${uniPlatformText}开发者工具失败:`, error.message)
console.log(`💡 请确保${uniPlatformText}开发者工具服务端口已启用`)
console.log(`💡 可以手动打开${uniPlatformText}开发者工具并导入项目:`, projectPath)
return
}
if (stderr) {
console.log('⚠️ 警告:', stderr)
}
console.log(`✅ ${uniPlatformText}开发者工具已打开`)
if (stdout) {
console.log(stdout)
}
})
}
export default function openDevTools() {
// 首次构建标记
let isFirstBuild = true
return {
name: 'uni-devtools',
writeBundle() {
if (isFirstBuild && process.env.UNI_PLATFORM?.includes('mp')) {
isFirstBuild = false
_openDevTools()
}
},
}
}

101
scripts/postupgrade.js Normal file
View 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')
})

43
src/App.ku.vue Normal file
View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref } from 'vue'
import FgTabbar from '@/tabbar/index.vue'
import { isPageTabbar } from './tabbar/store'
import { currRoute } from './utils'
const isCurrentPageTabbar = ref(true)
onShow(() => {
const { path } = currRoute()
// “蜡笔小开心”提到本地是 '/pages/index/index',线上是 '/' 导致线上 tabbar 不见了
// 所以这里需要判断一下,如果是 '/' 就当做首页,也要显示 tabbar
if (path === '/') {
isCurrentPageTabbar.value = true
}
else {
isCurrentPageTabbar.value = isPageTabbar(path)
}
})
onLoad(() => {
uni.loadFontFace({family:"DinBold",source:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/din-bold.ttf"})
uni.loadFontFace({family:'JinBuFont',source:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/DingTalkJinBuTi.ttf"})
})
const exposeRef = ref('this is form app.Ku.vue')
defineExpose({
exposeRef,
})
</script>
<template>
<!-- 这个先隐藏了,知道这样用就行 -->
<view class="h-screen flex flex-col">
<KuRootView />
<FgTabbar v-if="isCurrentPageTabbar" />
</view>
</template>

38
src/App.vue Normal file
View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { navigateToInterceptor } from '@/router/interceptor'
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">
@import 'sard-uniapp/index.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
View File

@ -0,0 +1,17 @@
import { API_DOMAINS, http } from '@/http/alova'
export interface IFoo {
id: number
name: string
}
export function foo() {
return http.Get<IFoo>('/foo', {
params: {
name: '菲鸽',
page: 1,
pageSize: 10,
},
meta: { domain: API_DOMAINS.SECONDARY }, // 用于切换请求地址
})
}

43
src/api/foo.ts Normal file
View File

@ -0,0 +1,43 @@
import { http } from '@/http/http'
export interface IFoo {
id: number
name: string
}
export function foo() {
return http.Get<IFoo>('/foo', {
params: {
name: '菲鸽',
page: 1,
pageSize: 10,
},
})
}
export interface IFooItem {
id: string
name: string
}
/** GET 请求 */
export async function getFooAPI(name: string) {
return await http.get<IFooItem>('/foo', { name })
}
/** GET 请求;支持 传递 header 的范例 */
export function getFooAPI2(name: string) {
return http.get<IFooItem>('/foo', { name }, { 'Content-Type-100': '100' })
}
/** POST 请求 */
export function postFooAPI(name: string) {
return http.post<IFooItem>('/foo', { name })
}
/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */
export function postFooAPI2(name: string) {
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 })
}
/** POST 请求;支持 传递 header 的范例 */
export function postFooAPI3(name: string) {
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 }, { 'Content-Type-100': '100' })
}

85
src/api/login.ts Normal file
View File

@ -0,0 +1,85 @@
import type { IAuthLoginRes, ICaptcha, IDoubleTokenRes, IUpdateInfo, IUpdatePassword, IUserInfoRes } from './types/login'
import { http } from '@/http/http'
/**
* 登录表单
*/
export interface ILoginForm {
username: string
password: string
}
/**
* 获取验证码
* @returns ICaptcha 验证码
*/
export function getCode() {
return http.get<ICaptcha>('/user/getCode')
}
/**
* 用户登录
* @param loginForm 登录表单
*/
export function login(loginForm: ILoginForm) {
return http.post<IAuthLoginRes>('/auth/login', loginForm)
}
/**
* 刷新token
* @param refreshToken 刷新token
*/
export function refreshToken(refreshToken: string) {
return http.post<IDoubleTokenRes>('/auth/refreshToken', { refreshToken })
}
/**
* 获取用户信息
*/
export function getUserInfo() {
return http.get<IUserInfoRes>('/user/info')
}
/**
* 退出登录
*/
export function logout() {
return http.get<void>('/auth/logout')
}
/**
* 修改用户信息
*/
export function updateInfo(data: IUpdateInfo) {
return http.post('/user/updateInfo', data)
}
/**
* 修改用户密码
*/
export function updateUserPassword(data: IUpdatePassword) {
return http.post('/user/updatePassword', data)
}
/**
* 获取微信登录凭证
* @returns Promise 包含微信登录凭证(code)
*/
export function getWxCode() {
return new Promise<UniApp.LoginRes>((resolve, reject) => {
uni.login({
provider: 'weixin',
success: res => resolve(res),
fail: err => reject(new Error(err)),
})
})
}
/**
* 微信登录
* @param params 微信登录参数,包含code
* @returns Promise 包含登录结果
*/
export function wxLogin(data: { code: string }) {
return http.post<IAuthLoginRes>('/auth/wxLogin', data)
}

98
src/api/types/login.ts Normal file
View File

@ -0,0 +1,98 @@
// 认证模式类型
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 {
nickName?: string
avatar?: string
openId?: string
userExtend?: any
talentExtend?:any
[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
}

View File

@ -0,0 +1,135 @@
<template>
<view class="custom-checkbox">
<CheckboxGroup v-model="defValue" checked-color="#1580FF" @change="handleChange" v-bind="$attrs" :default-cols="4">
<Checkbox v-for="item in list" :key="item[valueKey]" :name="item[valueKey]" cell shape="button" :default-style="`
${checkboxStyle}
--checkbox-bg: #f7f8fa;
--checkbox-radius: 8rpx;
--checkbox-active-border: 2rpx solid #1580ff;
--checkbox-active-color: #1580ff;
height: var(--checkbox-height);
background-color: var(--checkbox-bg);
border-radius: var(--checkbox-radius);
display: flex;
align-items: center;
justify-content: center;
`">
{{ item[labelKey] }}
</Checkbox>
</CheckboxGroup>
</view>
</template>
<script lang="ts" setup>
import Checkbox from './Checkbox.vue'
import CheckboxGroup from './CheckboxGroup.vue'
const props = defineProps({
list: {
type: Array,
default: () => [],
},
labelKey: {
type: String,
default: 'name',
},
valueKey: {
type: String,
default: 'code',
},
defaultValue: {
type: Array<string>,
default: () => [],
},
width: {
type: [String, Number],
default: '216rpx',
},
height: {
type: [String, Number],
default: '60rpx',
},
defaultCols: {
type: Number,
default: 3,
},
})
defineOptions({
options: {
styleIsolation: 'shared',
},
})
const emits = defineEmits(['change'])
const defValue = ref<string[]>([])
// 只在初始化时设置默认值
onMounted(() => {
if (props.defaultValue?.length) {
defValue.value = [...props.defaultValue]
}
})
const handleChange = (val: unknown) => {
defValue.value = val as string[]
emits('change', val)
}
watch(
() => props.defaultValue,
(newVal) => {
defValue.value = [...newVal]
},
)
// 计算复选框样式
const checkboxStyle = computed(() => {
const width = typeof props.width === 'number' ? `${props.width}rpx` : props.width
const height = typeof props.height === 'number' ? `${props.height}rpx` : props.height
return `--checkbox-width: ${width};--checkbox-height: ${height};`
})
</script>
<style lang="scss" scoped>
.custom-checkbox {
// 定义默认变量
--checkbox-width: 216rpx;
--checkbox-height: 60rpx;
--checkbox-bg: #f7f8fa;
--checkbox-radius: 8rpx;
color: #333;
}
:deep(.ycym-checkbox) {
width: var(--checkbox-width);
height: var(--checkbox-height);
min-width: var(--checkbox-width);
background-color: var(--checkbox-bg);
border-radius: var(--checkbox-radius);
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid var(--checkbox-bg);
}
:deep(.checkbox__icon) {
display: none;
}
:deep(.checkbox-active) {
border-color: #1580ff !important;
.checkbox__label {
color: #1580ff !important;
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<view
:style="`${defaultStyle}`"
:class="`${isChecked?'checkbox-active':''} ${isDisabled?'checkbox-disabled':''} ${rootClass}`"
@click="handleClick"
>
<view class="checkbox__icon" :class="{ 'checkbox__icon--checked': isChecked }" v-if="showIcon">
<text v-if="isChecked" class="i-carbon-checkmark checkbox__icon-check"></text>
</view>
<view class="checkbox__label">
<slot>{{ label }}</slot>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue'
const props = defineProps({
name: {
type: [String, Number],
required: true,
},
label: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
showIcon:{
default:false
},
defaultStyle:{
type:String,
default: 'display: inline-flex;align-items: center;cursor: pointer;font-size: 28rpx;'
},
checkboxActive:{
default:'background-color: #0083ff;border-color: #0083ff;'
},
rootClass:{
type:String,
default: ""
}
})
const emit = defineEmits(['change'])
interface CheckboxGroupContext {
modelValue: ComputedRef<any[]>
disabled: ComputedRef<boolean>
max: ComputedRef<number>
selectedCount: ComputedRef<number>
toggleOption: (option: { value: string | number }) => void
}
// 注入checkbox group的上下文
const checkboxGroup = inject<CheckboxGroupContext>('checkboxGroup', {
modelValue: computed(() => []),
disabled: computed(() => false),
max: computed(() => 0),
selectedCount: computed(() => 0),
toggleOption: () => {},
})
// 是否选中
const isChecked = computed(() => {
const modelValue = checkboxGroup.modelValue.value
return modelValue.includes(props.name)
})
// 是否禁用
const isDisabled = computed(() => {
const max = checkboxGroup.max.value
const selectedCount = checkboxGroup.selectedCount.value
// 如果有原始禁用属性,或者group禁用,或者达到最大选择数且未选中,则禁用
return (
props.disabled ||
checkboxGroup.disabled.value ||
(max > 1 && !isChecked.value && selectedCount >= max)
)
})
// 处理点击事件
const handleClick = () => {
if (isDisabled.value) return
checkboxGroup.toggleOption({
value: props.name,
})
}
</script>
<style scoped>
.checkbox--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.checkbox__icon {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #dcdfe6;
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
transition: all 0.2s;
}
.checkbox__icon--checked {
border-color: #0083ff;
}
.checkbox__icon-check {
color: #fff;
font-size: 32rpx;
}
.checkbox__label {
line-height: 1;
}
.checkbox-active {
border:var(--checkbox-active-border);
color: var(--checkbox-active-color)
}
.checkbox-disabled {
opacity: 0.5;
cursor: not-allowed !important;
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<view class="checkbox-group" :style="`${checkgroupStyle}`">
<slot></slot>
</view>
</template>
<script lang="ts" setup>
import { provide, computed } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
max: {
type: Number,
default: 0,
},
disabled: {
type: Boolean,
default: false,
},
checkgroupStyle:{
type:String,
default:'grid-template-columns: repeat(3, 1fr);'
}
})
const emit = defineEmits(['update:modelValue', 'change'])
// 内部维护一个响应式的值
const innerValue = computed(() => props.modelValue)
// 切换选项
const toggleOption = (option: { label: string; value: string | number }) => {
const currentValue = innerValue.value
const index = currentValue.indexOf(option.value)
let newValue = [...currentValue]
if (index === -1) {
if (props.max === 1) {
newValue = [option.value]
} else if (props.max && currentValue.length >= props.max) {
uni.showToast({
title: `最多只能选择${props.max}项`,
icon: 'none',
})
return
} else {
newValue.push(option.value)
}
} else {
newValue.splice(index, 1)
}
emit('update:modelValue', newValue)
emit('change', newValue)
}
// 提供给checkbox的上下文
provide('checkboxGroup', {
modelValue: computed(() => props.modelValue),
disabled: computed(() => props.disabled),
max: computed(() => props.max),
selectedCount: computed(() => props.modelValue.length),
toggleOption,
})
</script>
<style scoped lang="scss">
.checkbox-group{
display: grid;
gap: 16rpx;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<Overlay :show="show">
<view class="bg-white wrapper flex flex-col rounded-[16rpx]" :style="{ width: defaultWidth }">
<view
class="relative flex items-center w-full justify-center py-[26rpx] title"
v-show="title"
>
<text class="text-[36rpx] text-[#303030] font-bold text-center">
{{ title }}
</text>
<view
class="i-carbon-close absolute right-[40rpx] text-[40rpx]"
@click="emits('update:show', false)"
v-if="showClose"
></view>
</view>
<view
class="min-h-[200rpx] h-max-content overflow-y-auto"
:class="{ 'px-[32rpx]': defaultPadding }"
>
<slot></slot>
</view>
</view>
</Overlay>
</template>
<script setup lang="ts">
import Overlay from '@/chart-sub/components/overlay/Overlay.vue'
defineProps({
show: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '提示',
},
showClose: {
type: Boolean,
default: true,
},
defaultPadding: {
type: Boolean,
default: true,
},
defaultWidth: {
type: String,
default: '90%',
},
})
const emits = defineEmits(['update:show'])
</script>
<style scoped lang="scss">
.wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: max-content;
.title {
border-bottom: 2rpx solid #f7f7f7;
}
}
</style>

View File

@ -0,0 +1,208 @@
<template>
<view class="navbar">
<!-- 状态栏占位 -->
<view
v-if="safeAreaInsetTop"
class="status-bar"
:style="{ height: statusBarHeight + 'px', backgroundColor: bgColor }"
></view>
<!-- 导航栏主体 -->
<view
class="navbar-content"
:class="[contentClass, fixed ? 'navbar-fixed' : '', bordered ? 'navbar-border' : '']"
:style="{
backgroundColor: bgColor,
height: `${navHeight}px`,
top: fixed ? (safeAreaInsetTop ? statusBarHeight : 0) + 'px' : '0',
}"
>
<view
:class="`navbar-left ${leftWidthMin ? 'min-w-[48rpx]' : ''} min-w-[48rpx]`"
@click="handleClickLeft"
>
<!-- #ifndef MP-ALIPAY -->
<view v-if="leftArrow" class="back-icon">
<view class="i-carbon-chevron-left text-[40rpx] text-[#333] font-semibold icon-class" />
</view>
<!-- #endif -->
<slot name="left"></slot>
</view>
<!-- 中间标题区域 -->
<view class="navbar-title">
<slot name="title">
<text class="title-text">{{ title }}</text>
</slot>
</view>
<!-- 右侧区域 -->
<view class="navbar-right">
<slot name="right"></slot>
</view>
</view>
<!-- 占位元素 -->
<view
v-if="placeholder && fixed"
:style="{
height: `${navHeight}px`,
backgroundColor: bgColor,
}"
></view>
<slot name="background"></slot>
</view>
</template>
<script lang="ts" setup>
import { getDeviceInfo, getWindowInfo } from '@/utils/tools'
defineProps({
title: {
type: String,
default: '',
},
leftArrow: {
type: Boolean,
default: false,
},
fixed: {
type: Boolean,
default: false,
},
placeholder: {
type: Boolean,
default: false,
},
bordered: {
type: Boolean,
default: true,
},
safeAreaInsetTop: {
type: Boolean,
default: true,
},
bgColor: {
type: String,
default: '#ffffff',
},
contentClass: {
type: String,
default: 'justify-between',
},
leftWidthMin: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['clickLeft'])
// 获取系统信息
const deviceInfo = getDeviceInfo()
const systemInfo = getWindowInfo()
const statusBarHeight = systemInfo.statusBarHeight || 0
// 动态计算导航栏高度
const navHeight = computed(() => {
// 获取屏幕信息
const { screenWidth } = systemInfo
const { platform } = deviceInfo
// 将px转换为rpx的比例
const ratio = 750 / screenWidth
// 根据平台设置不同的导航栏高度
if (platform === 'ios') {
return 88 / ratio // iOS 一般是 44pt,转换为px
} else if (platform === 'android') {
return 96 / ratio // Android 一般是 48dp,转换为px
} else {
return 88 / ratio // 默认值
}
})
const handleClickLeft = () => {
emit('clickLeft')
}
</script>
<style scoped>
.navbar {
width: 100%;
}
.status-bar {
width: 100%;
background-color: inherit;
}
.navbar-content {
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
/* justify-content: space-between; */
padding: 0 16rpx;
background-color: #fff;
}
.navbar-fixed {
position: fixed;
left: 0;
z-index: 99;
width: 100%;
}
.navbar-border {
border-bottom: 1rpx solid #eee;
}
.navbar-left {
display: flex;
align-items: center;
height: 100%;
}
.back-icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.navbar-title {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
overflow: hidden;
/* flex: 1; */
text-align: center;
}
.title-text {
overflow: hidden;
font-size: 34rpx;
font-weight: 500;
color: #333;
text-overflow: ellipsis;
white-space: nowrap;
}
.navbar-right {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 52rpx;
height: 100%;
}
.back-text {
font-size: 48rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<view
v-if="show"
class="overlay"
:class="{ 'overlay-show': show }"
@click="handleClick"
:style="{ zIndex }"
>
<slot></slot>
</view>
</template>
<script lang="ts" setup>
const props = defineProps({
show: {
type: Boolean,
default: false,
},
zIndex: {
type: Number,
default: 10,
},
closeOnClickOverlay: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['click', 'update:show'])
const handleClick = (event: Event) => {
emit('click', event)
if (props.closeOnClickOverlay) {
emit('update:show', false)
}
}
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.7);
transition: all 0.3s ease;
}
.overlay-show {
opacity: 1;
visibility: visible;
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<view class="flex flex-col h-screen relative custom-bg">
<Navbar safeAreaInsetTop :bordered="false" leftArrow @clickLeft="handleBack" bg-color="transparent">
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">能力测评报告</text>
</template>
</Navbar>
<view class="flex-1 overflow-auto relative mt-[40rpx] flex flex-col">
<view class="flex flex-col flex-1 overflow-auto pb-[20rpx]">
<!-- 顶部卡片 -->
<view class="flex flex-col pt-[32rpx] mx-[24rpx] bg-[#fff] px-[30rpx] pt-[30rpx] border-class">
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2 font-700">你的弱势能力为</text>
<text class="text-[#117CFC] text-[36rpx] z-2">
{{ studyRecord.title.replace('你的弱势能力:', '') }}
</text>
</view>
<!-- 雷达图占位 -->
<LineReport :echart-data="studyRecord.linChart" :description="studyRecord.description" />
<AbilityDimension :report-items="studyRecord.reportItems" />
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import Navbar from '@/chart-sub/components/navbar/Navbar.vue'
import LineReport from '../components/interestChart/LineReport.vue'
import AbilityDimension from '../components/AbilityDimension.vue'
import AiFooter from '../components/AiFooter.vue'
import { handleBack } from '../hooks/useEvaluateBack'
import { getAbilityDimension } from '@/service'
// #ifdef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
},
excludeLoginPath: false,
})
// #endif
// #ifndef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
transparentTitle: 'always',
navigationBarTitleText: ''
},
excludeLoginPath: false,
})
// #endif
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({
description: '',
title: '',
linChart: [],
reportItems: [],
hTag: '',
})
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
// getAbilityDimension({ ScaleId: pageId.value }).then((resp) => {
// if (resp.code === 200) {
// studyRecord.value = resp.result as {
// description: string
// title: string
// linChart: any[]
// reportItems: any[]
// hTag: string
// }
// }
// })
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.border-class {
border-radius: 20rpx 20rpx 0 0;
padding-bottom: 42rpx;
margin-bottom: -14rpx;
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<view class="flex flex-col h-screen relative custom-bg">
<Navbar
safeAreaInsetTop
:bordered="false"
leftArrow
@clickLeft="handleBack"
bg-color="transparent"
>
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">性格测评报告</text>
</template>
</Navbar>
<view class="flex-1 flex flex-col overflow-auto">
<view class="overflow-auto relative mt-[40rpx] flex-1 pb-[20rpx]">
<!-- 顶部卡片 -->
<view class="flex flex-col pt-[32rpx] px-[84rpx] h-[244rpx] mb-[-116rpx] font-700">
<view class="header-bg">
<image
src="https://api.static.ycymedu.com/src/images/evaluate/bg.png"
class="w-full h-full"
/>
</view>
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2">您的性格类型为</text>
<text class="text-[#117CFC] text-[36rpx] z-2">{{ studyRecord.title }}</text>
</view>
<!-- 雷达图占位 -->
<CharacterChart :linChart="studyRecord.linChart" :description="studyRecord.description" />
<DependenciesChart
:mainDomain="studyRecord.reportItem.mainDomain"
:major="studyRecord.reportItem.major"
:occupation="studyRecord.reportItem.occupation"
/>
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import Navbar from '@/chart-sub/components/navbar/Navbar.vue'
import CharacterChart from '../components/interestChart/CharacterChart.vue'
import DependenciesChart from '../components/interestChart/DependenciesChart.vue'
import AiFooter from '../components/AiFooter.vue'
import { getMBTIDimension } from '@/service'
import { handleBack } from '../hooks/useEvaluateBack'
// #ifdef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
},
excludeLoginPath: false,
})
// #endif
// #ifndef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
transparentTitle: 'always',
navigationBarTitleText: ''
},
excludeLoginPath: false,
})
// #endif
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({
description: '',
title: '',
linChart: {},
reportItem: { mainDomain: '', major: '', occupation: '' },
hTag: '',
})
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
// getMBTIDimension({ ScaleId: pageId.value }).then((resp) => {
// if (resp.code === 200) {
// studyRecord.value = resp.result as {
// description: string
// title: string
// linChart: any
// reportItem: { mainDomain: string; major: string; occupation: string }
// hTag: string
// }
// }
// })
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
</style>

View File

@ -0,0 +1,128 @@
<template>
<view class="flex flex-col h-screen relative custom-bg">
<Navbar safeAreaInsetTop :bordered="false" leftArrow @clickLeft="handleBack" bg-color="transparent">
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">性格测评报告</text>
</template>
</Navbar>
<view class="flex-1 overflow-auto relative mt-[40rpx]">
<view class="overflow-auto relative mt-[40rpx] flex-1 pb-[20rpx]">
<!-- 顶部卡片 -->
<view class="flex flex-col pt-[32rpx] px-[84rpx] h-[244rpx] mb-[-116rpx] font-700">
<view class="header-bg">
<image src="https://api.static.ycymedu.com/src/images/evaluate/bg.png" class="w-full h-full" />
</view>
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2">您的兴趣类型为</text>
<text class="text-[#117CFC] text-[36rpx] z-2">{{ studyRecord.title }}</text>
</view>
<!-- 雷达图占位 -->
<InterestRadar :picData="studyRecord.picCharts" />
<!-- 类型说明 -->
<TypeDetail :reportItems="studyRecord.reportItems" />
<!-- 适合职业 -->
<IntroMajor :tag="studyRecord.hTag" />
<!-- 兴趣分析与代表人物 -->
<InterestingThings :tag="studyRecord.hTag" :description="studyRecord.description" />
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import TypeDetail from '../components/TypeDetail.vue'
import InterestRadar from '../components/interestChart/InterestRadar.vue'
import IntroMajor from '../components/IntroMajor.vue'
import { getHollandDimensionInfo } from '@/service'
import InterestingThings from '../components/InterestingThings.vue'
import AiFooter from '../components/AiFooter.vue'
import { handleBack } from '../hooks/useEvaluateBack'
import Navbar from '@/chart-sub/components/navbar/Navbar.vue'
// #ifdef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
},
})
// #endif
// #ifndef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
transparentTitle: 'always',
navigationBarTitleText: '',
},
})
// #endif
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({ description: '', title: '', picCharts: {}, reportItems: [], hTag: '' })
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
getHollandDimensionInfo({ query: { ScaleId: pageId.value.toString() } }).then((resp) => {
if (resp.code === 200) {
studyRecord.value = resp.result as {
description: string
title: string
picCharts: any
reportItems: any[]
hTag: string
}
}
})
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<view :scroll-y="true" class="flex flex-col h-screen relative custom-bg">
<Navbar
safeAreaInsetTop
:bordered="false"
leftArrow
@clickLeft="handleBack"
bg-color="transparent"
>
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">职业锚测评报告</text>
</template>
</Navbar>
<view class="flex-1 overflow-auto relative mt-[40rpx]">
<view class="overflow-auto relative mt-[40rpx] flex-1 pb-[20rpx]">
<!-- 顶部卡片 -->
<view class="flex flex-col pt-[32rpx] px-[84rpx] h-[244rpx] mb-[-116rpx] font-700">
<view class="header-bg">
<image
src="https://api.static.ycymedu.com/src/images/evaluate/bg.png"
class="w-full h-full"
/>
</view>
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2">您的职业价值观</text>
<text class="text-[#117CFC] text-[40rpx] z-2">{{ studyRecord.tag }}</text>
</view>
<OpinionChart :pic-charts="studyRecord.picCharts" />
<AbilityDimension :report-items="studyRecord.reportItems" />
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import Navbar from '@/chart-sub/components/navbar/Navbar.vue'
import OpinionChart from '../components/interestChart/OpinionChart.vue'
import AbilityDimension from '../components/AbilityDimension.vue'
import AiFooter from '../components/AiFooter.vue'
import { handleBack } from '../hooks/useEvaluateBack'
import { getOpinionAbout } from '@/service'
// #ifdef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
},
excludeLoginPath: false,
})
// #endif
// #ifndef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
transparentTitle: 'always',
navigationBarTitleText: ''
},
excludeLoginPath: false,
})
// #endif
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({
description: '',
title: '',
picCharts: { indicator: [], radars: [] },
reportItems: [],
tag: '',
})
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
// getOpinionAbout({ ScaleId: pageId.value }).then((resp) => {
// if (resp.code === 200) {
// studyRecord.value = resp.result as {
// description: string
// title: string
// picCharts: { radars: any[]; indicator: any[] }
// reportItems: any[]
// tag: string
// }
// }
// })
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.border-class {
border-radius: 20rpx 20rpx 0 0;
padding-bottom: 42rpx;
margin-bottom: -14rpx;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<view class="mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar title="能力纬度详细介绍" />
<view class="flex flex-col gap-[40rpx]">
<view v-for="(item, index) in reportItems" :key="index">
<view class="text-[32rpx] text-[#000] font-700">{{ item.title }}</view>
<view class="text-[26rpx] text-[#666] mt-[6rpx]">
{{ item.resolving || item.description }}
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import TitleBar from './TitleBar.vue'
defineProps({
reportItems: {
type: Array<any>,
default: () => [],
},
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,57 @@
<template>
<!-- 底部AI智能顾问 -->
<view
class="pt-[16rpx] px-[32rpx] flex items-center justify-center bg-[#fff] pb-safe sticky bottom-0 z-999"
@click="toAiAssistant"
v-if="aiShow"
>
<view
class="rounded-[8rpx] border-[#1580FF] border-[2rpx] border-solid w-full py-[14rpx] flex items-center justify-center"
>
<view class="w-[52rpx] h-[52rpx] mr-[10rpx]">
<image
src="https://api.static.ycymedu.com/images/btn-bottom.png"
class="w-[52rpx] h-[52rpx]"
></image>
</view>
<text class="text-[#1580FF] text-[32rpx] font-700">智能AI顾问</text>
</view>
</view>
<view v-else class="pb-safe"></view>
</template>
<script setup lang="ts">
import { sysDictType } from '@/service'
const aiShow = ref(true)
// sysDictType({ id: 619330547859525 }).then((res) => {
// const { code, result } = res
// const { status } = result as { status: number }
// if (code === 200) {
// if (status === 1) {
// aiShow.value = false
// } else {
// aiShow.value = true
// }
// }
// })
const props = defineProps({
pageId: {
type: Number,
default: 0,
},
pageType: {
type: Number,
default: 0,
},
})
const toAiAssistant = () => {
uni.navigateTo({
url: `/pages-sub/ai-service/index?id=${props.pageId}&type=${props.pageType}`,
})
}
</script>

View File

@ -0,0 +1,37 @@
<template>
<view
class="text-[26rpx] text-[#000] mx-[24rpx] bg-[#fff] flex flex-col px-[20rpx] gap-[10rpx] pb-[20rpx] custom-class"
>
<view class="flex gap-[10rpx] w-full">
<view class="w-[94rpx] py-[12rpx] bg-color text-center">类型</view>
<view class="py-[12rpx] text-center bg-color flex-1">详解</view>
</view>
<view class="flex gap-[10rpx] w-full" v-for="(item, index) in reportItems" :key="index">
<view class="w-[94rpx] py-[12rpx] bg-color text-center">
<view>{{ item.tag }}</view>
<view>{{ item.title }}</view>
</view>
<view class="py-[22rpx] px-[14rpx] text-start bg-color flex-1">{{ item.resolving }}</view>
</view>
</view>
</template>
<script setup lang="ts">
const props = defineProps({
reportItems: {
type: Array<any>,
default: () => [],
},
})
</script>
<style lang="scss" scoped>
.bg-color {
background-color: #f5faff;
border-radius: 8rpx;
}
.custom-class {
border-radius: 0 0 20rpx 20rpx;
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<view class="interest-analysis mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar title="兴趣分析与代表人物" />
<text class="text-[26rpx] text-[#666] leading-[40rpx] mb-[30rpx] block">
{{ description }}
</text>
<view class="flex items-center gap-[10rpx] mb-[16rpx]">
<view class="w-[5rpx] h-[30rpx] bg-[#1580FF]"></view>
<text class="text-[#1580FF] text-[32rpx]">代表人物</text>
<text class="text-[#999] text-[24rpx]">以下代表人物仅作为参考</text>
</view>
<view class="avatars-list grid grid-cols-4 gap-x-[52rpx] gap-y-[18rpx]">
<view
class="avatar-item flex flex-col items-center"
v-for="(person, index) in personList"
:key="index"
>
<view class="w-[120rpx] h-[120rpx]">
<image
:src="person.avatarUrl"
class="w-[120rpx] h-[120rpx] rounded-full"
mode="aspectFill"
></image>
</view>
<text class="mt-[10rpx] text-[26rpx] text-[28rpx] font-normal text-center">
{{ person.nickName }}
</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { getTagMapPerson } from '@/service'
import TitleBar from './TitleBar.vue'
const props = defineProps({
tag: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
})
const personList = ref([])
watch(
() => props.tag,
(newV) => {
// getTagMapPerson({ tag: newV }).then((resp) => {
// if (resp.code === 200) {
// personList.value = resp.result as any[]
// }
// })
},
)
</script>

View File

@ -0,0 +1,53 @@
<template>
<view class="mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar title="推荐专业" />
<text class="text-[24rpx] text-[#999]">以下专业仅作为参考</text>
<view class="w-full">
<view class="flex py-[15rpx] px-[36rpx] bg-[#F5FAFF]">
<text class="w-[129rpx] text-center font-bold">专业大类</text>
<text class="flex-1 text-center font-bold">专业类</text>
<text class="flex-1 text-center font-bold">专业名称</text>
</view>
<view
:class="`flex py-[15rpx] text-[#333] text-[24rpx] px-[36rpx] ${index % 2 === 0 ? 'bg-[#fff]' : 'bg-[#F5FAFF]'}`"
v-for="(major, index) in majorList"
:key="index"
>
<text class="w-[129rpx] text-center">{{ major.tradeName }}</text>
<text class="flex-1 text-center">{{ major.categoryName }}</text>
<text class="flex-1 text-center">{{ major.name }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { getTagMapPro } from '@/service'
import TitleBar from './TitleBar.vue'
const props = defineProps({
tag: {
type: String,
default: '',
},
})
const majorList = ref([])
watch(
() => props.tag,
(newV) => {
// getTagMapPro({ tag: newV }).then((resp) => {
// if (resp.code === 200) {
// majorList.value = resp.result as any[]
// }
// })
},
)
</script>
<style lang="scss" scoped>
@import '@/chart-sub/styles/parallelogram.scss';
</style>

View File

@ -0,0 +1,38 @@
<template>
<view class="mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar :title="title" />
<view v-for="(item, index) in items" :key="index" class="suggestion-item">
<view class="text-[32rpx] font-700">{{ item.title }}</view>
<view class="text-[26rpx] font-400 mt-[10rpx] text-[#666]">
{{ item.description instanceof Array ? item.description.join(',') : item.description }}
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import TitleBar from './TitleBar.vue'
defineProps({
items: {
type: Array<{ title: string; description: string | [] }>,
default: () => [],
},
title: {
type: String,
default: '',
},
})
</script>
<style lang="scss" scoped>
.suggestion-item {
&:not(:last-child) {
margin-bottom: 50rpx;
}
&:first-child {
margin-top: 30rpx;
}
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<view class="mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar :title="title" />
<view class="text-[26rpx] text-[#666] mt-[10rpx] text-center">
<text
class="text-[22rpx] px-[12rpx] py-[4rpx] rounded-[20rpx] bg-[rgba(250,142,35,0.15)] text-[#FA8E23]"
>
{{ item.notes }}
</text>
<view class="mt-[20rpx] text-center">
<view class="text-[#000] text-[26rpx] font-700">学习风格表现</view>
<view class="mt-[10rpx]" v-for="(item, index) in item.learning_performance" :key="index">
{{ item }}
</view>
</view>
<view class="mt-[20rpx] text-center">
<view class="text-[#000] text-[26rpx] font-700">学习风格特点</view>
<view class="mt-[10rpx]" v-for="(item, index) in item.features" :key="index">
{{ item }}
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import TitleBar from './TitleBar.vue'
defineProps({
item: {
type: Object,
default: () => {},
},
title: {
type: String,
default: '',
},
})
</script>

View File

@ -0,0 +1,65 @@
<template>
<view class="relative pt-[40rpx]">
<Dashboard class="absolute right-[38rpx] top-0" :score="score" :color="rules[level].color" />
<view class="bg-[#fff] rounded-[24rpx] p-[22rpx]">
<view class="mt-[30rpx]">
<text class="text-[48rpx] text-[#000] font-600">{{ tagName }}</text>
<!-- <view class="flex items-center text-[#999] text-[24rpx] mt-[20rpx]">
<view class="i-carbon-warning w-[24rpx] h-[24rpx]"></view>
<text class="text-[#999] ml-[10rpx]">状态很好哦,继续保持,轻松迎接高考吧!</text>
</view> -->
<view class="mt-[58rpx]">
<ScoreCard :current-position="level" :rules="rules" />
</view>
<view class="relative mt-[68rpx] bg-[#F5FAFF]">
<view class="absolute top-[-9rpx] left-[20rpx] w-[180rpx] h-[52rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/test-icon.png"
mode="scaleToFill"
class="w-[180rpx] h-[52rpx]"
/>
</view>
<view class="px-[20rpx] pb-[20rpx] pt-[58rpx] text-[#333] text-[26rpx]">
{{ description }}
</view>
</view>
<view class="text-[#999] text-[24rpx] mt-[10rpx]">
{{ tip }}
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import Dashboard from '@/chart-sub/evaluate/components/psychologicalReportItem/Dashboard.vue'
import ScoreCard from './psychologicalReportItem/ScoreCard.vue'
defineProps({
score: {
type: Number,
default: 0,
},
rules: {
type: Array<{ label: string; range: string; color: string }>,
default: () => [],
},
tip: {
type: String,
default: '结果只做参考,不能准确判断是否有焦虑症。',
},
level: {
type: Number,
default: 0,
},
description: {
type: String,
default: '',
},
tagName: {
type: String,
default: '',
},
})
</script>

View File

@ -0,0 +1,52 @@
<template>
<view class="flex flex-col bg-white rounded-[24rpx] p-[30rpx] gap-[40rpx]">
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<view class="w-[38rpx] h-[38rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/life-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
</view>
<text class="text-[32rpx] text-[#000] font-700">生活建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息,早睡早起,避免熬夜。 每天适当运动(如散步、跑步),保持精力充沛。
避免过度放松,保持适度的学习节奏。
</view>
</view>
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<view class="w-[38rpx] h-[38rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/diet-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
</view>
<text class="text-[32rpx] text-[#000] font-700">饮食建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息,早睡早起,避免熬夜。 每天适当运动(如散步、跑步),保持精力充沛。
避免过度放松,保持适度的学习节奏。
</view>
</view>
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<view class="w-[38rpx] h-[38rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/learn-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
</view>
<text class="text-[32rpx] text-[#000] font-700">学习建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息,早睡早起,避免熬夜。 每天适当运动(如散步、跑步),保持精力充沛。
避免过度放松,保持适度的学习节奏。
</view>
</view>
</view>
</template>

View File

@ -0,0 +1,25 @@
<template>
<view class="text-center mb-[24rpx]">
<view class="text-[32rpx] font-bold relative inline-flex pb-[10rpx] items-center">
<view class="i-carbon-flash-filled h-[23rpx] w-[17rpx] text-[#1880FC]"></view>
<text class="text-[#1880FC] text-[36rpx] font-bold">{{ title }}</text>
<view class="i-carbon-flash-filled h-[23rpx] w-[17rpx] text-[#1880FC]"></view>
<view
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-full h-[10rpx] bg-[#cce3fc] rounded-full title-bar"
></view>
</view>
</view>
</template>
<script lang="ts" setup>
defineProps({
title: {
type: String,
default: '',
},
})
</script>
<style lang="scss" scoped>
@import '@/chart-sub/styles/parallelogram.scss';
</style>

View File

@ -0,0 +1,39 @@
<template>
<view
class="text-[26rpx] text-[#000] mx-[24rpx] bg-[#fff] flex flex-col px-[20rpx] gap-[10rpx] pb-[20rpx] custom-class"
>
<view class="flex gap-[10rpx] w-full">
<view class="w-[94rpx] py-[12rpx] bg-color text-center">类型</view>
<view class="py-[12rpx] text-center bg-color flex-1">性格特点</view>
</view>
<view class="flex gap-[10rpx] w-full" v-for="(item, index) in reportItems" :key="index">
<view
class="w-[94rpx] py-[12rpx] bg-color text-center flex flex-col justify-center items-center text-[#333] font-700"
>
<view>{{ item.tag }}</view>
<view>{{ item.title }}</view>
</view>
<view class="py-[22rpx] px-[14rpx] text-start bg-color flex-1">{{ item.resolving }}</view>
</view>
</view>
</template>
<script setup lang="ts">
const props = defineProps({
reportItems: {
type: Array<any>,
default: () => [],
},
})
</script>
<style lang="scss" scoped>
.bg-color {
background-color: #f5faff;
border-radius: 8rpx;
}
.custom-class {
border-radius: 0 0 20rpx 20rpx;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<view
class="radar-chart-placeholder mx-[24rpx] bg-white chart-class h-[630rpx] flex flex-col justify-center px-[24rpx] pt-[62rpx] pb-[20rpx]"
>
<view class="w-full z-1 flex flex-col gap-[40rpx]">
<view
class="w-full flex items-center gap-[16rpx] text-[30rpx] text-[#A8A8A8]"
v-for="(item, index) in characterData"
:style="`--start-color:${colors[index].startColor};--end-color:${colors[index].endColor};--active-color:${colors[index].activeColor}`"
:key="index"
>
<view
:class="`w-110rpx ${item.leftData.value > item.rightData.value ? 'left-active' : ''}`"
>
{{ item.leftData.name }}
</view>
<view class="flex-1 h-[28rpx] bg-[#F1F2F4] rounded-[16rpx]">
<view
class="w-[60%] rounded-[16rpx] bar-color h-[28rpx]"
:style="`transform:translateX(${(item.rightData.value / (item.rightData.value + item.leftData.value)) * 100}%);`"
></view>
</view>
<view
:class="`w-110rpx ${item.leftData.value > item.rightData.value ? '' : 'right-active'}`"
>
{{ item.rightData.name }}
</view>
</view>
</view>
<view class="px-[16rpx] py-[20rpx] flex flex-col bg-[#F5FAFF] mt-[32rpx]">
<text class="text-[30rpx] text-[#000] mb-[10rpx] font-700">性格介绍</text>
<text class="text-[26rpx] text-[#333]">{{ description }}</text>
</view>
</view>
</template>
<script lang="ts" setup>
const props = defineProps({
linChart: {
type: Object,
default: () => ({
name: [],
value: [],
}),
},
description: {
type: String,
default: '',
},
})
const colors = [
{ startColor: '#97FBCD', endColor: '#00BAAD', activeColor: '#00BAAD' },
{ startColor: '#FF5940', endColor: '#FAA896', activeColor: '#FF4117' },
{ startColor: '#FFBF5E', endColor: '#F59300', activeColor: '#EB8D00' },
{ startColor: '#96E4FA', endColor: '#117CFC', activeColor: '#117CFC' },
]
const transformData = (names: string[], values: number[]) => {
const result = []
const pairMap = new Map()
const pairData = { E: 'I', S: 'N', T: 'F', J: 'P' }
// 先建立名称和值的映射
names.forEach((name, index) => {
const match = name.match(/\((.*?)\)(\w+)/)
if (match) {
const letter = match[2]
pairMap.set(letter, { name, value: values[index] })
}
})
// 根据 pairData 构建配对数据
Object.entries(pairData).forEach(([left, right]) => {
const leftData = pairMap.get(left)
const rightData = pairMap.get(right)
if (leftData && rightData) {
result.push({
leftData,
rightData,
})
}
})
return result
}
const characterData = ref([])
watch(
() => props.linChart,
(newV) => {
let data = transformData(newV.name, newV.value)
characterData.value = data
},
)
</script>
<style lang="scss" scoped>
.chart-class {
border-radius: 20rpx;
}
.bar-color {
background: linear-gradient(90deg, var(--start-color) 0%, var(--end-color) 100%);
}
.left-active {
color: var(--active-color);
font-weight: 700;
}
.right-active {
color: var(--active-color);
font-weight: 700;
}
</style>

View File

@ -0,0 +1,181 @@
<template>
<view
class="suitable-positions mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx] flex flex-col"
>
<TitleBar title="适合的岗位领域" />
<view class="flex justify-center items-center">
<view class="h-[146px] w-[265px] z-1">
<LEchart ref="echart" :is-disable-scroll="true"></LEchart>
</view>
</view>
<view class="flex items-center gap-[10rpx] mb-[10rpx]">
<view class="w-[5rpx] h-[30rpx] bg-[#1580FF]"></view>
<text class="text-[#1580FF] text-[32rpx] font-bold">适合职业</text>
<text class="text-[#999] text-[24rpx]">以下数据仅作为参考</text>
</view>
<text class="text-[#666] text-[28rpx]">{{ major }}</text>
<view class="flex items-center gap-[10rpx] mb-[10rpx] mt-[40rpx]">
<view class="w-[5rpx] h-[30rpx] bg-[#1580FF]"></view>
<text class="text-[#1580FF] text-[32rpx] font-bold">适合专业</text>
<text class="text-[#999] text-[24rpx]">以下数据仅作为参考</text>
</view>
<text class="text-[#666] text-[28rpx]">{{ occupation }}</text>
</view>
</template>
<script lang="ts" setup>
import LEchart from '@/chart-sub/uni_modules/lime-echart/components/l-echart/l-echart.vue'
import TitleBar from '../TitleBar.vue'
const echarts = require('../../../uni_modules/lime-echart/static/echarts.min')
const echart = ref(null)
const props = defineProps({
mainDomain: {
type: String,
default: '',
},
major: {
type: String,
default: '',
},
occupation: {
type: String,
default: '',
},
})
// 定义固定的样式配置
const styleConfig = [
{ bg: '#FDF0F0', text: '#F58C8C', size: 45, x: 2, y: 38 },
{ bg: '#ECF5FF', text: '#3B9DFF', size: 77, x: 52, y: 64 },
{ bg: '#F6F4FD', text: '#7E5EFF', size: 63, x: 136, y: 48 },
{ bg: '#FFF8E5', text: '#FFC832', size: 62, x: 116, y: 104 },
{ bg: '#E6FBF0', text: '#37D480', size: 45, x: 0, y: 110 },
{ bg: '#FDF0F0', text: '#F58C8C', size: 45, x: 166, y: 118 },
]
const chartData = ref([])
// 根据文字长度选择合适的样式
const selectStylesByLength = (items: string[]) => {
const usedIndices = new Set<number>()
const result: Array<{ style: any; text: string }> = []
// 优先处理大尺寸的元素
const processItems = (items: string[], targetSize: number) => {
items.forEach((item) => {
// 查找匹配大小且未使用的样式
const availableIndices = styleConfig
.map((style, index) => ({ index, style }))
.filter(({ style, index }) => style.size === targetSize && !usedIndices.has(index))
if (availableIndices.length > 0) {
const { index, style } = availableIndices[0]
usedIndices.add(index)
result.push({ style, text: item })
}
})
}
// 按不同尺寸进行分类
const bigItems = items.filter((item) => item.length >= 4)
const mediumItems = items.filter((item) => item.length === 3)
const smallItems = items.filter((item) => item.length <= 2)
// 依次处理不同尺寸的元素
processItems(bigItems, 77)
processItems(mediumItems, 63)
processItems(smallItems, 45)
// 处理剩余的未分配元素(使用任何可用样式)
const remainingItems = items.filter((item) => !result.some((r) => r.text === item))
remainingItems.forEach((item) => {
// 尝试找到任何未使用的样式
for (let i = 0; i < styleConfig.length; i++) {
if (!usedIndices.has(i)) {
usedIndices.add(i)
result.push({ style: styleConfig[i], text: item })
break
}
}
})
return result
}
watch(
() => props.mainDomain,
(newV) => {
if (!newV) return
chartData.value = newV.split('、')
echart.value.init(echarts, (chart) => {
// 根据文字长度选择合适的样式
const selectedStyles = selectStylesByLength(chartData.value)
// 生成图表数据
const data = selectedStyles.map(({ style, text }) => {
return {
name: text,
x: style.x,
y: style.y,
symbolSize: style.size,
itemStyle: {
color: style.bg,
},
label: {
show: true,
color: style.text,
fontSize: 14,
formatter: (params: any) => {
const text = params.name
if (text.length >= 5) {
const lines = []
for (let i = 0; i < text.length; i += 3) {
lines.push(text.slice(i, i + 3))
}
return lines.join('\n\n')
}
return text
},
rich: {
lineHeight: 20,
},
align: 'center',
verticalAlign: 'middle',
position: 'inside',
},
}
})
let option = {
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
series: [
{
type: 'graph',
layout: 'none',
roam: true,
data,
links: [],
lineStyle: {
opacity: 0.9,
width: 2,
curveness: 0,
},
},
],
}
chart.setOption(option)
})
},
)
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,108 @@
<template>
<view class="mx-[24rpx] bg-white chart-class h-[500rpx] flex items-center justify-center">
<view class="h-[415rpx] w-full z-1">
<LEchart ref="echart"></LEchart>
</view>
</view>
</template>
<script lang="ts" setup>
import LEchart from '@/chart-sub/uni_modules/lime-echart/components/l-echart/l-echart.vue'
const echarts = require('../../../uni_modules/lime-echart/static/echarts.min')
const echart = ref(null)
const props = defineProps({
picData: {
type: Object,
default: () => ({
indicator: [],
radars: [],
}),
},
})
// 处理标签名称的映射
const nameMap: Record<string, string> = {
A: '艺术型(A)',
R: '现实型(R)',
I: '研究型(I)',
S: '社会型(S)',
E: '企业型(E)',
C: '常规型(C)',
}
// 转换 indicator 数据
const formatIndicator = computed(() => {
return props.picData.indicator.map((item) => ({
name: nameMap[item.name] || item.name,
max: item.max,
}))
})
watch(
() => props.picData,
(newV) => {
if (newV.radars.length > 0) {
echart.value.init(echarts, (chart) => {
let option = {
radar: {
center: ['50%', '50%'],
radius: '70%',
indicator: formatIndicator.value,
splitArea: {
show: false,
},
axisLine: {
lineStyle: {
color: '#e5e6eb',
},
},
splitLine: {
lineStyle: {
color: '#e5e6eb',
},
},
axisName: {
color: '#333',
},
},
series: [
{
name: '职业兴趣评测',
type: 'radar',
data: [
{
value: props.picData.radars,
name: '测评结果',
itemStyle: {
color: '#1580FF',
},
areaStyle: {
color: 'rgba(21,128,255,0.2)',
},
lineStyle: {
width: 2,
},
},
],
},
],
}
chart.setOption(option)
})
}
},
)
onBeforeMount(() => {
if (echart.value) {
echart.value.dispose()
}
})
</script>
<style lang="scss" scoped>
.chart-class {
border-radius: 20rpx 20rpx 0 0;
}
</style>

View File

@ -0,0 +1,183 @@
<template>
<view class="bg-white mx-[24rpx] rounded-[20rpx] pb-[20rpx]">
<view class="px-[24rpx] h-[368rpx] z-1">
<LEchart ref="echart" :customStyle="`z-index:1;`"></LEchart>
</view>
<view class="bg-[#F5FAFF] px-[16rpx] pt-[20rpx] mx-[34rpx] pb-[26rpx]">
<view class="text-[#000] text-[30rpx] mb-[10rpx] font-700">能力评测建议</view>
<view class="text-[26rpx] text-[#333]">{{ description }}</view>
</view>
</view>
</template>
<script lang="ts" setup>
import LEchart from '@/chart-sub/uni_modules/lime-echart/components/l-echart/l-echart.vue'
const echarts = require('../../../uni_modules/lime-echart/static/echarts.min')
const echart = ref(null)
const props = defineProps({
echartData: {
type: Array,
default: () => [
{
name: [],
value: [],
},
{
name: [],
value: [],
},
],
},
description: {
type: String,
default: '',
},
})
onMounted(() => {
updateChart()
})
watch(
() => props.echartData,
(newVal) => {
if (newVal && newVal.length > 0) {
updateChart()
}
},
{ deep: true },
)
const updateChart = () => {
echart.value.init(echarts, (chart) => {
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: function (params) {
let result = params[0].name.split('\n').join('') + '\n'
params.forEach((param) => {
result += `${param.marker} ${param.seriesName}: ${param.value}\n`
})
return result
},
textStyle: {
fontSize: 12,
},
position: function (point, params, dom, rect, size) {
const tooltipWidth = dom._rect.width
const canvasWidth = size.viewSize[0]
if (point[0] + tooltipWidth > canvasWidth) {
return [point[0] - tooltipWidth, '10%']
}
return [point[0], '10%']
},
},
grid: {
top: 40,
left: 10,
right: 20,
bottom: 10,
containLabel: true,
},
legend: {
data: ['我的数据', '平均水平'],
right: 'auto',
left: 'center',
top: 0,
itemWidth: 15,
itemHeight: 10,
textStyle: {
fontSize: 12,
},
},
xAxis: [
{
type: 'category',
data: (props.echartData[1] as { name: string[] })?.name.map((item) =>
item.replace('智能', '').replace(/(.{2})/g, '$1\n'),
),
axisLine: {
lineStyle: {
color: '#E0E0E0',
},
},
axisLabel: {
color: '#666',
fontSize: 12,
interval: 0,
},
axisTick: {
show: false,
},
},
],
yAxis: [
{
type: 'value',
name: '分数',
min: 0,
max: 50,
interval: 10,
axisLine: {
show: true,
lineStyle: {
color: '#E0E0E0',
},
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: '#E8E8E8',
type: 'solid',
},
},
axisLabel: {
color: '#999',
},
},
],
series: [
{
name: '我的数据',
type: 'bar',
barWidth: '20',
itemStyle: {
color: '#1580FF',
borderRadius: [4, 4, 0, 0],
},
data: (props.echartData[0] as { value: number[] })?.value,
},
{
name: '平均水平',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#F9AA5B',