后端:/tasks 建改删查 + publish/pause/resume/close/archive 六个动作, 按角色过滤可见范围(班主任只看自己带的班级的任务)。 前端:收集任务列表页 + 建/改任务弹窗(命名模板带实时预览与变量插入)。 验收:e2e-m1-task.py 71/71;e2e-m0.py 36/36、e2e-m0-user.py 55/55 回归通过; 前端 typecheck 0 错 / build 通过 / 浏览器肉眼确认。 三条硬约束(改动时当成规则): 1) 「暂停」与「截止」不是一回事。暂停:学生不能交、已提交的照常审、 且可恢复;截止不可逆。做成一个会让"临时冻一下"变成不可逆操作。 2) 发布前置 5 项全是阻断级,force 绕不过:名册非空(40016)、名册含姓名与 证件号(40017)、至少 1 项必传材料(40018)、已设置覆盖班级(40019)、 命名模板可用(40020)。只有 P1 级提醒(某班未指定初审人)可 force 跳过。 3) collectMode / certType 发布后不可改 —— 它们决定审核流与材料清单, 改了会让已填过的学生面对另一份表单。 本轮修掉的 3 个真问题: 1) collect_task_org 没有 deleted 列,而我让它继承了带 @TableLogic 的基类 —— MyBatis-Plus 会给每条查询自动加 deleted = 0,运行时报"列不存在", 编译期完全看不出来。改为不继承基类、显式声明字段; "移除"语义走 status = 0,不要对它调 deleteById(会变成物理删除)。 2) 任务编码查重被逻辑删除遮蔽:用 MP 的 selectCount 会自动过滤 deleted = 0, 但数据库的唯一约束不看软删标记 —— 软删掉的任务仍占着那个编码, 于是"查重说没有、插进去说重复",报 duplicate key(500)。 另外原用 COUNT+1 也错:删掉中间一个后计数会退回去,算出已占用的号。 改为 MAX(序号)+1,且查重语句刻意不加 deleted = 0(与唯一约束同口径); 编码一次用过就不复用(它同时是文件目录名,复用会把两批文件混进同一目录)。 3) Boot 4 把 Jackson 自动配置挪到了 spring-boot-starter-jackson,项目没引, 导致容器里没有 ObjectMapper bean(@Autowired 直接启动失败), 且 application.yml 的 spring.jackson.* 静默失效 —— 其中 time-zone: Asia/Shanghai 不生效最危险:本机时区恰好是上海时看不出来, 部署到 UTC 服务器后所有时间会整体偏 8 小时。 注意 spring-boot-starter-json 在 Boot 4 已无 4.x 版本(仓库里最高 3.5.6)。 补依赖,但任务配置的 JSON 读写仍不用 Jackson —— Boot 4 用 Jackson 3 而 jjwt-jackson 带来 Jackson 2,打包后两个 jackson-databind 并存 (2.21.5 + 3.1.5),import 哪个都是赌;改用项目已直接声明的 hutool-all。 已知缺口(都写进了代码注释与交接摘要): - 发布在前置数据齐备前不可达(名册属 M2、材料属 M1-04、覆盖班级属 M1-02), E2E 用 SQL 夹具造数据才走到发布之后的状态 —— M1-02/M1-04/M2 落地后 要回来把夹具换成真实接口。 - 命名规则试跑目前只做静态校验(变量名合法 + 必含姓名/证件号/材料名), 完整形态(拿真实名册跑一遍查非空与不重名)属 M1-09 / M3-01。 - stat 恒为 null(依赖名册与提交单)。故意不返回全 0 的对象 —— 那会让界面显示"完成率 0%",而真相是"还没有分母";界面显示「待导入名册」。 - 任务编码并发撞号由唯一约束兜底 + 可读提示(不再 500),未做分布式锁。 顺带: - 两个列表页的列宽按实测可用宽度重算(1366 屏:收集任务 824px、 账号管理因左侧组织树只有 574px),并把"操作列必须 fixed + 列宽口径" 写进注释 —— 之前把「状态」列整个遮住、把操作链接挤成竖排。 - 状态与最后登录并成一列,给窄屏省出 150px。
简介
Gi Admin Pro 是一个基于 Vue3、Vite、TypeScript、Arco Design Vue、Pinia、VueUse 等的免费中后台模版,它使用了最新的前端技术栈,内置丰富的主题配置,有着极高的代码规范,基于 mock 实现的动态数据展示,开箱即用的模板,也可用于学习参考。
Gi 前缀含义: G:代表全局 i:代表我的
Gi 用来定义全局组件前缀,如 GiTable、GiForm、GiPageLayout等
特性
- 最新技术栈:使用 Vue3 / Vite 等前端前沿技术开发,使用高效率的 npm 包管理器
- TypeScript:应用程序级 JavaScript 的语言
- 主题:丰富可配置的主题、暗黑模式
- 代码规范:丰富的规范插件及极高的代码规范
预览
| 平台 | 预览地址 |
|---|---|
| github | Gi Admin Pro 预览地址 |
| 账号 | 密码 | |
|---|---|---|
| 管理员 | admin | 123456 |
| 用户 | user | 123456 |
代码仓库
| 平台 | 预览地址 | 仓库地址 |
|---|---|---|
| gitee(码云) | -- | Gitee 仓库地址 |
| github | Gi Admin Pro 预览地址 | Github 仓库地址 |
项目示例图
安装使用
- 安装依赖
npm install
- 运行
npm run dev
- 打包
npm run build
开源作者
Lin
常见问题
为什么安装依赖不成功?
检查node版本,最好使用原生镜像npm
还原镜像
npm config set registry https://registry.npmjs.org/
为什么选择 Arco 组件库,而不是 Element Plus?
为什么全局组件使用前缀 Gi?
全局组件设置了按需引入,使用前缀,方便和局部组件做区分
为什么组件使用单词大写开头 (PascalCase)命名写法?
本项目.vue文件名以及在模板使用中,均采用大写开头 (PascalCase)命名方式
参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/
组件命名:单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)
其他优点:方便搜索(横线连接 (kebab-case)对搜索没那么方便)
为什么 css 类名推荐横线连接 (kebab-case)
参考大部分大网站,都是这个命名规则,不使用: .myClass这种
页面显示异常?
页面必须要保留一个根元素!!!
Vue3 权限管理对路由进行排序和格式化处理方式
使用 xe-utils 这个 js 库,简化数据处理
页面无法缓存?
请检查页面是否配置了name,且名称是否与数据一致
defineOptions({ name: 'AboutIndex' })
{
path: '/about/index',
name: 'AboutIndex', // 检查name是否一致
component: () => import('@/views/about/index.vue')
}
项目规范
.vue 文件行数规范
一般来说,一个 .vue 文件行数建议不超过 400 行,超过建议组件化拆分
vue 相关的命名
<script setup lang="ts">
const isEdit = ref(false)
// 不推荐
const title = computed(() => {
return isEdit.value ? '编辑' : '新增'
})
// 推荐 能一行就尽量写一行
const title = computed(() => (isEdit.value ? '编辑' : '新增'))
</script>
<script setup lang="ts">
// 表单建议使用 form 命名(简洁),不必要使用 formData, 同时使用 reactive
const form = reactive({
name: '',
phone: ''
})
</script>
<script setup lang="ts">
// 如果属性比较多
const getInitForm = () => ({
name: '',
phone: '',
email: '',
sex: 1,
age: ''
})
const form = reactive(getInitForm())
// 重置form
const resetForm = () => {
for (const key in form) {
delete form[key]
}
Object.assign(form, getInitForm())
}
</script>
<script setup lang="ts">
import { useAppStore, useUserStore } from '@/stores'
import { useLoading } from '@/hooks'
// stores 或 hooks 的使用命名规则定义
const appStore = useAppStore()
const userStore = useUserStore()
const { loading, setLoading } = useLoading()
</script>
写法技巧
尽量使用三元表达式
// 优化前
let isEdit = true
let title = ''
if (isEdit) {
title = '编辑'
} else {
title = '新增'
}
// 优化后
let title = isEdit ? '编辑' : '新增'
善用 includes 方法
// 优化前
if (type === 1 || type === 2 || type === 3) {
}
// 优化后,此种方式在vue模板也可使用
if ([1, 2, 3].includes(type)) {
}
使用箭头函数简化函数
// 优化前
function add(num1, num2) {
return num1 + num2
}
// 优化后
const add = (num1, num2) => num1 + num2
尽量减少 if else if
<script setup lang="ts">
// 比例进度条颜色 尽量减少 if else if
const getProportionColor = (proportion: number) => {
if (proportion < 30) return 'danger'
if (proportion < 60) return 'warning'
return 'success'
}
</script>
// 优化前
const status = 200
const message = ''
if (status === 200) {
message = '请求成功'
} else if (status === 404) {
message = '请求出错'
} else if (status === 500) {
message = '服务器错误'
}
// 优化后
const status = 200
const messageMap = {
200: '请求成功',
404: '请求出错',
500: '服务器错误'
}
const message = messageMap[status]
如果函数参数超过两个,建议优化
<script setup lang="ts">
function createUser(name, phone, age) {
console.log('姓名', name)
console.log('手机', phone)
console.log('年龄', age)
}
// 这种方式在使用的时候可读性很差,扩展性差,而且不易于维护
createUser('张三', '178****2828', 20)
function createUser2({ name, phone, age }) {
console.log('姓名', name)
console.log('手机', phone)
console.log('年龄', age)
}
// 以对象传参更直观,更好扩展和维护
createUser2({ name: '张三', phone: '178****2828', age: 20 })
</script>
接口调用书写
写法一
不需要 loading,不需要错误打印的 情况
<script setup lang="ts">
import { ref } from 'vue'
import { getUserList as getUserListApi, type UserItem } from '@/apis' // 同名可以使用别名
const userList = ref<UserItem[]>([])
const getUserList = async () => {
const res = await getUserListApi()
console.log('如果异步成功,则会打印这行文字,否则不会打印这行文字,也不会往下执行')
userList.value = res.data
}
</script>
// getUserListApi 是一个 Promise 异步函数,Promise 最后只有 成功 / 失败 两种状态 // getUserListApi 是基于 axios
封装的,在 axios 响应拦截器做了处理 // 当 res.success === false 的时候 Promise.reject(),
也就异步失败,异步失败不会往下执行
写法二
需要 loading,需要错误打印的情况
<script setup lang="ts">
import { ref } from 'vue'
import { getUserList as getUserListApi, type UserItem } from '@/apis' // 同名可以使用别名
const loading = ref(false)
const userList = ref<UserItem[]>([])
const getUserList = async () => {
try {
loading.value = true
const res = await getUserListApi()
console.log('如果异步成功,则会打印这行文字,否则不会打印这行文字,也不会往下执行')
userList.value = res.data
} catch (error) {
console.log('如果异步失败,会打印这行文字')
// 由于 axios 在封装的时候,已经在异步失败给了失败的提示弹窗处理
// 所以这里没必要再写 Message.error(error)
} finally {
console.log('如果异步或者失败,都会打印这行代码')
loading.value = false // 可以用来处理 loading
}
}
</script>
写法三
需要 loading, 不需要错误打印(不进行错误处理)的情况
<script setup lang="ts">
import { ref } from 'vue'
import { getUserList as getUserListApi, type UserItem } from '@/apis' // 同名可以使用别名
const loading = ref(false)
const userList = ref<UserItem[]>([])
const getUserList = async () => {
try {
loading.value = true
const res = await getUserListApi()
console.log('如果异步成功,则会打印这行文字,否则不会打印这行文字,也不会往下执行')
userList.value = res.data
} finally {
console.log('如果异步或者失败,都会打印这行代码')
loading.value = false // 可以用来处理 loading
}
}
</script>
// catch 可以省略
全局组件--命名规范
组件命名:单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)
可参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/
GiForm.vue
GiTable.vue
GiPageLayout.vue
局部组件--命名规范
组件命名:单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)
可参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/
Pane1.vue
Pane2.vue
PaneQuota1.vue
PaneQuota2.vue
Step1.vue
Step2.vue
AddModal.vue
EditDrawer.vue
DetailModal.vue
文件夹命名--命名规范 (采用中划线-)
1、文件名建议只使用小写字母,不使用大写字母
2、名称较长时采用半角连接符(-)分隔
home/index.vue
user/index.vue
user-detail/index.vue
弹窗组件 Modal、抽屉组件 Drawer 的一般封装
<template>
<a-modal v-model:visible="visible" :title="title" @ok="confirm">
<!-- 内容 -->
</a-modal>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
const visible = ref(false)
const detailId = ref('')
const isEdit = computed(() => !!detailId.value) // 判断是新增还是编辑模式
const title = computed(() => (isEdit.value ? '编辑' : '新增'))
const add = () => {
detailId.value = ''
visible.value = true
}
// 如果这里的参数超过两个,建议优化成对象形式
// const edit = ({ id, taskId }) = {
// console.log(id, taskId)
// }
const edit = (id: string) => {
detailId.value = id
visible.value = true
// getDetail() 回显操作
}
defineExpose({ add, edit })
const confirm = () => {
console.log('点击了确认按钮')
}
</script>
使用
模板里使用自定义组件:使用大写开头驼峰,双击好复制,对于搜索便利
<template>
<EditModal ref="EditModalRef"></EditModal>
</template>
<script setup lang="ts">
import EditModal from './EditModal.vue'
const EditModalRef = ref<InstanceType<typeof EditModal>>()
// 新增
const onAdd = () => {
EditModalRef.value?.add()
}
// 编辑
const onEdit = (item: PersonItem) => {
EditModalRef.value?.edit(item.id)
}
</script>
CSS 命名规范
建议采用全小写,多单词使用-连接符(参考大部分网站,包括掘金,码云等,都是采用这个规则)
或者采用BEM命名规范 BEM 命名规范
BEM 命名规范
<div class="article">
<div class="article__body">
<button class="article__button--primary"></button>
<button class="article__button--success"></button>
</div>
</div>
其他:
位置 1: 使用 var(--margin) 全局 css 变量
位置 2:使用 var(--padding) 全局 css 变量
建议尽量使用全局 css 变量来开发,可以有效提高效率和团队协作
其他规范
可参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/ , 其中一些规范也可借鉴
可参考 Gi Admin Pro 源码,如有更好的规范建议,可以联系作者本人
Vue 相关
插件推荐
Day.js 一个极简的 JavaScript 库,可以为现代浏览器解析、验证、操作和显示日期和时间 2K 大小
Lodash 一个一致性、模块化、高性能的 JavaScript 实用工具库
Xe-utils 一个 JavaScript 函数库、工具类
V-Viewer 基于 viewer.js 的 vue 图像查看器组件,支持旋转、缩放等操作
vue3-request 一个专为 Vue 3 设计的异步请求处理库,它通过统一的 useRequest Hook 和强大的插件生态,彻底简化了异步操作和 API 调用的复杂性
其他
VueDraggablePlus 支持 Vue2 和 Vue3 的拖拽组件
推荐书籍
开源项目集合
工具
捐赠
