客户在验收里点出来的问题,一次改完。两段改动:
一、平台运营与租户范围(后端为主)
- 新增请求头 X-Tenant-Id:平台运营是唯一跨租户身份,租户拦截器对它整体关掉了过滤,
于是「组织架构 / 账号管理 / 工作台」拿到的是所有租户混在一起的数据 —— 而只有一所
学校有数据时,它和学校管理员看到的完全一样,看不出错(这是最危险的一点)。
现在这些页面必须先选定学校,服务端再把本次请求的租户上下文换成目标租户:
只认平台身份(其它身份带头一律忽略)、目标租户必须存在且启用。
- OrgService.tree / UserService.list 补显式 tenant_id 条件(原来只靠拦截器,而拦截器对
平台不生效);TaskService.seesWholeTenant 改用新的 LoginUser.isPlatformStaff() ——
isPlatformAdmin 回答"当前租户是不是平台租户",代管时它为 false,会让平台退化成普通
用户(任务列表掉进"班主任只看自己带的班"分支,得到空列表);PLATFORM_ADMIN 补
TASK_VIEW(PRD §3.2「查看填报进度看板:平台运营 ✅」)。
- 操作日志对平台永远是空的:SQL 写死 tenant_id = 当前租户,而平台的 tenantId 是 0。
按契约 §13.4 补 tenantId 参数与 log:viewAll:平台默认看全量、可指定某所学校;
学校管理员恒为本租户,传别家返回 40301(不静默忽略,否则会让人以为"筛过了")。
- 开放接口"推送重试"从未生效(F-112):定时扫描缺 @InterceptorIgnore,异常被 log.warn
吞掉;同一方法里"接入方已停用"那条 update 也没有租户上下文,会每 10 秒重试到永远。
E2E 补了"真的重试了"的断言(原来只断言"安排了下次重试")。
二、PC 端走查体验
- 去掉 3 条无用菜单(名单管理 / 表单设计器 / 材料规则):功能都在「收集任务」行内入口,
菜单里那一条点进去只有指路;后端菜单键同步删除,entryAt 那套机制一并删掉。
- 班主任点工作台必弹 403:工作台无条件调了要 org:view / progress:viewAll 的两个接口。
403 走 HTTP 状态码,try/catch 拦不住它的弹窗 —— 改成按权限决定发不发请求。
- 操作日志:分页器被裁在可视区外(新增 .g-list-card 让表体自己滚、表头钉住、分页器留在
卡片底部)+ 去掉页面上那段免责声明;收集任务的状态切换改成单独一行 tab;去掉顶栏假数据
铃铛;配色按客户要求还原。
顺带修两处"检查器自己不严":check-wiring 新增第 ⑦ 项(PC 端引用的权限码必须存在于
PermissionDef —— 本轮我自己把 progress:viewAll 写成 progress:view:all,判断恒为 false,
而编译 / typecheck / lint / E2E 全都不红);verify-docs 的表格列数检查不再把 GFM 转义
竖线当成列分隔符。两个检查器都拿"故意写错的样例"试过,确认真的会红。
验证:typecheck / mvn package / check-wiring(未接线声明 0 条)/ verify-docs(22 项)全过;
e2e-m0 36、e2e-m1-orgs 48、e2e-m0-user 55、e2e-m5-logs 44、e2e-m5-notify-wiring 43、
e2e-m1-tenant-domain 6、e2e-m8-open 61 全绿;浏览器按角色逐个真点复验。
详见 docs/交接摘要.md §一·三十三 与 §一·三十四。
简介
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 的拖拽组件
推荐书籍
开源项目集合
工具
捐赠
