refactor(pc): 从 Gi Admin Pro + Arco 重搭为纯 Vue 3 + Naive UI
旧模板的 193 个文件整体删除,换成纯 Vue 3.5 + Naive UI 2.42 的树。
- 去掉 auto-import(组件与 API 全显式 import)、v-hasPerm(改 user.has('码'),全库 8 处)、
Stylelint;n-tag 的 type 统一过 utils/tag.ts 转换(Arco 色词直接透传会静默变灰)
- 图标改为构建期抽取:scripts/extract-pc-icons.py -> icons/icon-data.ts(37 个),
内网离线部署不会白屏
- 重搭后修掉 6 类"编译 / lint / typecheck 全绿、界面上就是坏的"故障:
① NGrid 的 cols 写成像素值(不是断点名 -> 永远只 1 列,工作台因此错乱)
② n-tab-pane 用 tab 属性(只渲染空注释节点,整排 tab 一个字都不显示)
③ 漏 import 被当未知自定义元素(样式整块丢) ④ .n-card__content 类名不存在
(高度链第一环失效,表格行在 DOM 里但屏幕上没有) ⑤ 填报单状态映射错位一格
⑥ 有标题的卡片没有上内边距(naive 的 padding-top 只给"内容区是长子"的卡片)
- 卡片式版面提成全局基线:styles/index.scss 的 xt-* 令牌 + .page-card / .toolbar /
.section-title,页面只引用、不再各写一套
- 品牌:改名「深学在线」,文字标换成几何标(方 = 卷宗 / 环 = 在线,不用文字字形);
index.html 的页签图标从 404 的 /favicon.ico 改指真实存在的 /favicon.svg
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
import type { RoleVO } from '@/apis/xt/role'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NModal,
|
||||
NSelect,
|
||||
NSpace,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { createRole, ROLE_USER_TYPE_NAMES, updateRole } from '@/apis/xt/role'
|
||||
import { useActingTenantStore } from '@/stores/acting-tenant'
|
||||
|
||||
/**
|
||||
* 自定义角色的新增 / 编辑弹窗。
|
||||
*
|
||||
* ## 为什么"新建"要选一个模板角色
|
||||
*
|
||||
* 服务端对新建角色的校验是 **至少一项权限 + 一个菜单**(`@NotEmpty`)——
|
||||
* 一个新角色如果什么都不给,它的账号登录后什么都看不到,那本来也没有意义。
|
||||
* 而内置角色的权限矩阵在界面上**看得见、改不了**,本来就该当"参考模板"用
|
||||
* (这也是内置角色只读的设计理由之一)。
|
||||
*
|
||||
* 所以这里的做法是:选一个模板角色 → 复制它的权限码与菜单 → 建好之后
|
||||
* 到右侧的授权矩阵里增删。好处是新建这一步只需要填 4 个字段,
|
||||
* 而授权仍然只有**一个**编辑入口(`RoleGrantEditor`),不会出现两套勾选逻辑。
|
||||
*
|
||||
* ## 角色码建后不可改
|
||||
*
|
||||
* 它会进 JWT 的 `roles`、出现在日志与排查对话里。改了它,历史日志里的角色码
|
||||
* 就成了一个查不到的东西。所以编辑时禁用(后端接口里也没有这个字段)。
|
||||
*/
|
||||
defineOptions({ name: 'RoleFormModal' })
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 角色列表:模板下拉的候选(内置 + 已存在的自定义角色) */
|
||||
roles?: RoleVO[]
|
||||
}>(), {
|
||||
roles: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'save-success', id?: number): void
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
const acting = useActingTenantStore()
|
||||
|
||||
const visible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const detailId = ref<number | null>(null)
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
|
||||
const isEdit = computed(() => !!detailId.value)
|
||||
const title = computed(() => (isEdit.value ? '修改角色' : '新建角色'))
|
||||
|
||||
const current = computed(() => props.roles.find((r) => r.id === detailId.value) ?? null)
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
roleCode: '',
|
||||
roleName: '',
|
||||
userType: 3,
|
||||
remark: '',
|
||||
/** 新建时的模板角色码:复制它的权限与菜单 */
|
||||
templateCode: 'HEAD_TEACHER'
|
||||
}
|
||||
}
|
||||
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
const userTypeOptions = Object.entries(ROLE_USER_TYPE_NAMES).map(([code, name]) => ({
|
||||
label: name,
|
||||
value: Number(code)
|
||||
}))
|
||||
|
||||
/** 模板候选:内置角色优先(它们才是"官方口径"的模板) */
|
||||
const templateOptions = computed(() => props.roles
|
||||
.filter((r) => r.id !== detailId.value)
|
||||
.map((r) => ({
|
||||
label: `${r.roleName}(${r.roleType === 1 ? '内置' : '自定义'}|${r.permCodes?.length ?? 0} 项权限)`,
|
||||
value: r.roleCode
|
||||
})))
|
||||
|
||||
/**
|
||||
* ⚠️ `userType` 的 `type: 'number'` **不能省**。
|
||||
*
|
||||
* 只有 `required` 时,async-validator 会把 `rule.type` 默认成 `'string'`
|
||||
* (`Schema.validate` 里 `rule.type = this.getType(rule)`),随后按
|
||||
* `typeof value !== rule.type` 报错;而 `userType` 是数字(1/2/3)——
|
||||
* 结果是**「创建」永远失败**,且错误信息被 `NFormItem` 的 `#feedback` 插槽挡住
|
||||
* (naive 的渲染是 `children || feedback`),界面上只剩一个没有说明的红框。
|
||||
*/
|
||||
const rules: FormRules = {
|
||||
roleCode: [
|
||||
{ required: true, message: '请填写角色码', trigger: ['blur', 'input'] },
|
||||
{
|
||||
pattern: /^[A-Z][A-Z0-9_]{1,63}$/,
|
||||
message: '只能是大写字母、数字与下划线,且以字母开头',
|
||||
trigger: ['blur', 'input']
|
||||
}
|
||||
],
|
||||
roleName: [{ required: true, message: '请填写角色名称', trigger: ['blur', 'input'] }],
|
||||
userType: [{ type: 'number', required: true, message: '请选择用户类型', trigger: ['change'] }]
|
||||
}
|
||||
|
||||
function add(): void {
|
||||
detailId.value = null
|
||||
Object.assign(form, emptyForm())
|
||||
// 默认挑一个内置角色当模板:让"新建"这一步不用理解每一条权限码
|
||||
const preferred = props.roles.find((r) => r.roleCode === 'HEAD_TEACHER' && r.roleType === 1)
|
||||
if (preferred) {
|
||||
form.templateCode = preferred.roleCode
|
||||
form.userType = preferred.userType
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function edit(row: RoleVO): void {
|
||||
detailId.value = row.id
|
||||
Object.assign(form, emptyForm())
|
||||
form.roleCode = row.roleCode
|
||||
form.roleName = row.roleName
|
||||
form.userType = row.userType
|
||||
form.remark = row.remark ?? ''
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
async function confirm(): Promise<void> {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (isEdit.value && detailId.value) {
|
||||
await updateRole(detailId.value, {
|
||||
roleName: form.roleName.trim(),
|
||||
userType: form.userType,
|
||||
remark: form.remark.trim() || undefined
|
||||
})
|
||||
message.success('已保存')
|
||||
visible.value = false
|
||||
emit('save-success', detailId.value)
|
||||
return
|
||||
}
|
||||
|
||||
const tpl = props.roles.find((r) => r.roleCode === form.templateCode)
|
||||
if (!tpl) {
|
||||
message.warning('请选择作为模板的角色(新角色需要至少一项权限与一个菜单)')
|
||||
return
|
||||
}
|
||||
|
||||
const id = await createRole({
|
||||
roleCode: form.roleCode.trim().toUpperCase(),
|
||||
roleName: form.roleName.trim(),
|
||||
userType: form.userType,
|
||||
remark: form.remark.trim() || undefined,
|
||||
permCodes: [...(tpl.permCodes ?? [])],
|
||||
menus: [...(tpl.menus ?? [])]
|
||||
})
|
||||
message.success('已创建。下面可以在授权矩阵里增删权限与菜单')
|
||||
visible.value = false
|
||||
emit('save-success', id)
|
||||
} catch {
|
||||
// 提示已由 http 层给出(如角色码重复、平台级权限 40301、userType 与角色不符)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ add, edit })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal
|
||||
v-model:show="visible"
|
||||
preset="card"
|
||||
:title="title"
|
||||
:style="{ width: 'min(520px, calc(100vw - 32px))' }"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<NForm ref="formRef" :model="form" :rules="rules" label-placement="top">
|
||||
<NFormItem label="角色码" path="roleCode">
|
||||
<NInput
|
||||
v-model:value="form.roleCode"
|
||||
:disabled="isEdit"
|
||||
placeholder="如 CLASS_AUDITOR(大写字母 + 下划线)"
|
||||
/>
|
||||
<template #feedback>
|
||||
<span class="rf__hint">
|
||||
{{ isEdit
|
||||
? '角色码建后不可改:它已经进了 JWT 与历史日志,改了那些记录就查不到了'
|
||||
: '租户内唯一;它会出现在日志与排查对话里,所以用大写字母 + 下划线' }}
|
||||
</span>
|
||||
</template>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="角色名称" path="roleName">
|
||||
<NInput v-model:value="form.roleName" placeholder="如 年级审核员" :maxlength="64" clearable />
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="用户类型" path="userType">
|
||||
<NSelect v-model:value="form.userType" :options="userTypeOptions" />
|
||||
<template #feedback>
|
||||
<span class="rf__hint">
|
||||
账号的用户类型由角色推导;{{ isEdit && (current?.userCount ?? 0) > 0
|
||||
? '该角色还有账号在用,服务端不允许改这一项'
|
||||
: '所以它必须选对' }}
|
||||
</span>
|
||||
</template>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem v-if="!isEdit" label="以哪个角色为模板">
|
||||
<NSelect v-model:value="form.templateCode" :options="templateOptions" />
|
||||
<template #feedback>
|
||||
<span class="rf__hint">
|
||||
复制它的权限码与菜单;建好后在右侧授权矩阵里增删。新角色至少要有一项权限与一个菜单
|
||||
</span>
|
||||
</template>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="备注">
|
||||
<NInput
|
||||
v-model:value="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:maxlength="255"
|
||||
placeholder="这个角色是给谁用的(选填)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<NAlert type="info" :show-icon="false" class="rf__note">
|
||||
保存授权后<strong>立即生效</strong>,不需要重新登录。
|
||||
<template v-if="acting.tenantId">当前操作的是所选学校的角色。</template>
|
||||
</NAlert>
|
||||
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="visible = false">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton type="primary" :loading="submitting" @click="confirm">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rf__hint {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--xt-text-3);
|
||||
}
|
||||
|
||||
/* 表单与结论条之间留出间隔:不隔开的话,提示会像最后一个表单项的说明 */
|
||||
.rf__note {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user