Files
xiaotutongxue/frontend-pc
lovife ef903a55c3 feat(task): 表单设计器(M1-08)—— 填报表单在界面上也能配了
"能动态设置填报表单"是本产品的核心承诺之一,而在这之前表单定义只能用 SQL 改。
更要紧的是它有一个**不可逆的方向**:内置字段删不掉(40026),正确用法是
"用条件显示隐藏"—— 而只有界面上给这条路留了位置,用户才会照着做。

新增:
  · apis/xt/form-schema.ts —— 表单定义接口 + 字段类型 + 条件选项 + 扩展字段 key 生成
  · views/task/TaskFormSchemaDrawer.vue —— 21 项内置字段(必填/条件/排序/提示语就地改)
    + 扩展字段增删改 + 保存为新版本 + 版本冲突处理
  · task/index.vue 操作列加「表单设计」,240→300px

浏览器实测:
  · 版本标签「未保存过(当前为官方默认表单)」——服务端对没存过的任务返回内置默认定义,
    让设计器从一份完整的官方表单开始,而不是从空白开始
  · **21 项全在,且「条件显示」反查正确**:补考三科→仅补考时、
    在读学校/专业/入学时间→仅在校生、本职业工作年限必填开关是关的。
    把服务端任意 visibleWhen 对象映射回人话标签,是本界面最容易做错的一处
  · 保存→库里 v1 current=1 fields=21;三次保存得到 v1/v2/v3,
    旧的 is_current=0 全部保留(**版本只增不改**实测)
  · 「不收集」= {field:"_never_",...}(内置字段删不掉,用一条永远不成立的条件隐藏);
    保存后原有条件(is_retake/is_student)都保住了

⭐ 端到端:学生端 字段 21→17(已获证书编号消失、补考三科消失、在校生三项保留)、
   材料 4→3(工作证明消失)

⚠️ 顺带修掉一个真 bug:**材料侧的条件没求值**
   上面这次验证暴露的:表单字段的条件前端有求值(21→17),材料的没有 ——
   「工作证明(仅非在校生)」照样显示给在校生;而后端校验是排除它的,
   于是表现成"系统让我传,我不传也不报错",学生只能自己猜。
   修法:把 visible() 抽成 whenVisible(),**字段与材料共用同一份**。
   教训:同一套规则出现在两处时必须共用同一个函数 ——
   抄一份的代价不是多几行,而是两边行为不一致,而两边各自看都没错。

修掉一条过期的 E2E 断言:e2e-m1-form.py 断言「联系电话不锁定」,
   与实现刻意的只读相反(BuiltinFields L88-93:collect_submission 没有 phone 列,
   所以一期只读;反过来做"前端允许改、后端悄悄丢掉"最糟 ——
   学生以为改成功,实际提交的还是老号码)。改断言并写明理由,48/48 恢复。

frontend-mp 类型检查修干净:原先 fill.vue 还有 3 条(输入框事件、日期选择、
   FormData 缺 submissionStatus),现在只剩 config/index.ts 的 2 条 ——
   那是 uni-app 的 #ifdef H5/#ifndef H5 声明同一变量,vue-tsc 看不懂条件编译,设计如此。

验收:e2e-m1-form 48/48、materials 68/68、task 71/71、orgs 48/48、
   m3-submit 31/31、file 29/29、draft 27/27、gate 29/29(合计 351 条断言全绿)
2026-09-29 13:48:11 +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

捐赠