Files
xiaotutongxue/frontend-pc
lovife 7523ebe789 feat(task): M1-02 覆盖班级与班主任配置
补上发布前置校验的最后一环(40019),同时确立任务的数据隔离边界 ——
班主任能看到哪些任务、能审哪些单子,全由 collect_task_org 决定。

后端 GET/PUT /tasks/{id}/orgs(整体替换语义 + 软移除 + 历史行复用);
前端「配置班级」抽屉:组织树只允许勾选班级,主班主任留空时显式提示
「将由该班负责人初审」而非留白。发布被拦时给出「去哪里补」的指引
(BLOCKING_HINTS),而不是只说"未设置覆盖班级"。

三条守住的规则:
1) 班级与初审人都必须在本租户内。跨租户的 id 一律当作"不存在或已删除",
   不透露"它存在但不属于你" —— 漏了的后果是 A 校能把 B 校的班挂到自己
   的任务上,B 校的班主任就出现在 A 校的审批流里。
2) 初审人必须持有班主任/副班主任角色。指定初审人本质上是一次授权,
   那个人将能看到该班学生的身份证号与学籍材料;不校验角色就等于把
   "谁能看学生隐私"变成任意授权。报错要给出下一步,不能只说"校验失败"。
3) 移除是软移除(status=0),且「移除再加回」必须复用原行。
   后者不是优化而是必须:uk_task_org UNIQUE (task_id, org_id) 不带 status
   条件,一个班在同一任务下只能有一行,直接 insert 会违反唯一约束。

本轮修掉的 3 个真问题:
1) 我此前误判 uk_task_org 不存在 —— grep 只匹配含表名的行,而
   CONSTRAINT uk_task_org UNIQUE (task_id, org_id) 那行没有表名。
   基于这个错误前提,我写了一个多余的部分唯一索引 uk_task_org_active
   和一段基于错误前提的代码,已全部撤掉并更正注释。
   教训:查约束要看 CREATE TABLE 整块,不能只 grep 表名。
2) collect_task_org.expected_count 是 NOT NULL DEFAULT 0,与接口契约矛盾:
   DDL 注释说它是"由名册汇总"(系统算的),但《03-接口设计》§7.6 把它放在
   PUT 请求体里、说"用于与名册实际人数比对" —— 只有"管理员填的期望值 vs
   名册实际值"才需要比对。而 0 是有意义的业务值("预期 0 人"),拿它兼职
   表示"没填"会让每个没填的班都触发"应填 0 人、实际 31 人"的假告警;告警
   变噪音就没人看了,真正"应填 28、名册却有 31"(名册导错)反而被漏掉,
   而这条告警的全部价值就在那一次。已由 V4 迁移改为可空 + 去掉默认值,
   并同步更正 docs/db/schema-postgresql.sql 与 PRD 里的内联 DDL。
3) 删掉迁移文件后必须 mvn clean,否则它会"复活":我只跑 mvn package,
   target/classes 里的旧副本被打进 jar,Flyway 发现历史里没有 v4 就又执行
   了一遍 —— 索引回来了、历史行填回去了。之后又建了同名版本号的新 V4,
   于是"两个 v4"→"checksum 冲突",排查绕了好几圈。

顺带修掉:
· 任务列表操作列从 3 个链接涨到 4 个,160px 会挤成竖排 —— 把「进度」并进
  「状态」列腾出 40px,操作列给到 200px,合计 820px(可用约 824px)。
· defineEmits 必须用函数调用签名(frontend-pc/.agents/skills/code-vue)。
  我此前写的 5 个组件都是被禁的 tuple 形式,一并改正:
  OrgFormModal / TaskFormModal / TaskOrgDrawer / UserFormModal / UserImportModal。
· e2e-m1-orgs.py 的清理抽成幂等的 cleanup_all(),并在 __main__ 的 finally
  里兜一次 —— 该脚本首次运行时崩在准备阶段、清理段没执行,把两个测试账号
  留在了库里(是在浏览器里看到下拉框里有个「E2E班主任」才发现的)。
  其余三条 E2E 脚本仍是老样子,下一轮一并改。

验收:e2e-m1-orgs.py 48/48(含跨租户隔离);e2e-m1-task.py 71/71、
e2e-m0-user.py 55/55、e2e-m0.py 36/36 回归通过,共 210 项断言;
前端 typecheck / eslint / lint:style 0 错、build 通过;
浏览器实测抽屉的开合、树的勾选规则与保存落库。
2026-09-29 09:32:48 +08:00
..

Gi Admin Pro

star
[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)

简介

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?

Element Plus 对比 Arco design

为什么全局组件使用前缀 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 相关

Vue3 官网

Vue-Router

Vite

Pinia

插件推荐

Arco Design 组件库

Day.js 一个极简的 JavaScript 库,可以为现代浏览器解析、验证、操作和显示日期和时间 2K 大小

Lodash 一个一致性、模块化、高性能的 JavaScript 实用工具库

Xe-utils 一个 JavaScript 函数库、工具类

VueUse 一个 Vue3 Hooks 库

VueRequest 一个 Vue 请求库

V-Viewer 基于 viewer.js 的 vue 图像查看器组件,支持旋转、缩放等操作

Vue-Color-Kit 一个 vue3 颜色选择器组件

Vxe-Table

vue3-request 一个专为 Vue 3 设计的异步请求处理库,它通过统一的 useRequest Hook 和强大的插件生态,彻底简化了异步操作和 API 调用的复杂性

其他

VCalendar 日历组件

Vue Cal 日历组件

VueDraggablePlus 支持 Vue2 和 Vue3 的拖拽组件

推荐书籍

Vue 入门指南与实战案例

深入理解 TypeScript

前端宝典

Web 开发人员需要知道的 CSS 技巧

阮一峰 ES6

阮一峰 flex 布局

开源项目集合

Vue3 开源项目集合

React 开源项目集合

工具

菜鸟工具

代码生成图片

JSON-TO-ANY 对象转 ts 类型

在线三角形样式生成器

iTab

捐赠