feat(frontend): 管理端改用 Gi Admin Pro 框架 + 腾讯云主题
前端此前只有本地实现、从未入库,本次首次纳入版本管理。
框架切换
· 以 Gi Admin Pro(Vue3 + Vite + TS + Arco Design Vue + Pinia)为底
· 剥离 14 个演示页面目录 / 121 个文件(demo 28、file 20、form 19、system 18…)
· 依赖 dependencies 35 → 19,devDependencies 31 → 27
· 保留框架核心:layout、Gi* 组件库、hooks、权限指令、错误页、登录页
主题:腾讯云 / TDesign 官方色板
· 主色 #0052D9(腾讯蓝)、成功 #00A870、警告 #ED7B2F、危险 #E34D59
· 页面底色 #f3f3f3、文字 rgba(0,0,0,90/60/40/26%)
· 覆盖 Arco 底层色板 + 显式语义别名(primary/success/warning/danger/link/neutral)
· 三个改了不生效也不报错的坑已记入注释:
1) Arco 色值是逗号分隔 RGB 三元组,不是 #RRGGBB
2) Arco 把色板定义在 body 而非 :root,必须写 `:root, body`
3) setting.json 的 themeColor 会以内联样式写进 body,优先级高于样式表
对接后端(三处契约差异在 @/utils/http 统一适配)
· 响应信封 {code,msg,data} → {code,success,message,data}
· Token 头 token → Authorization: Bearer
· 分页 {list,total} → 补一份 records,list 原样保留
· 40101 → 登录页;登录密码 RSA 加密(公钥走 /auth/public-key,轮换无需重建前端)
· 路由:后端只下发菜单键,前端在本地路由表上过滤(有哪些页面归代码,谁能看归后端)
已实现页面(浏览器实测通过)
· 登录:RSA 加密 + 自适应验证码/租户码 + 账号锁定提示
· 工作台:真实数据(组织 6 个 / 角色 1 个 / 权限 25 项)
· 组织架构:三级树 + CRUD + 占用校验
· 租户管理:开通租户 + 初始密码只显示一次的强确认
· 待建页:未实现菜单显示所属里程碑
· 权限菜单:学校管理员看不到「租户管理」
视觉资产按腾讯云主题重做
· logo.svg(渐变圆角方 + 填)、favicon.svg(小尺寸做减法)、login-hero.svg(材料归集)
· 登录页点阵纹理走主题变量,换主色自动跟随
· 文案改为产品实际能力:自动改名 / 强制压缩 / 隐私保护
清理模板遗留(约 17MB + 三处外部依赖)
· 百度统计脚本(上报到 gi-demo 作者的统计账号)
· 用户菜单「项目地址」(跳转模板作者的 Gitee 仓库)
· 两份 8.8MB mp3、388KB favicon.ico、57KB 模板插画、未引用字体与 loading.gif
· 「个人中心」指向已删除的 /system/account(点了 404)
· 头部头像在无 avatarUrl 时渲染成裂图 → 改为姓名首字兜底
后端修复
· 新增 GET /auth/public-key(前端加密登录密码用)
· DataSeeder.ancestor_path 生成错误:子节点本该是 /16/17/ 却写成 /17/
该字段是「按院系统计」的索引,错了会漏人/多人且不报错 —— 改为从库里读父节点路径
· seedDemoOrgs 曾放在租户作用域外 + admin 早退分支之后,导致启动失败 / 永不执行
· 示范租户补一套三级组织树(1 学校 / 2 院系 / 3 班级),供界面演示
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="gi-table-setting" :class="{ 'gi-table-setting--fullscreen': isFullscreen }">
|
||||
<a-row justify="space-between" align="center" class="g-row-tool">
|
||||
<a-space wrap>
|
||||
<slot name="custom-title">
|
||||
<div class="gi-table-setting__title">{{ title }}</div>
|
||||
</slot>
|
||||
</a-space>
|
||||
|
||||
<a-space wrap>
|
||||
<slot name="custom-extra" />
|
||||
|
||||
<a-tooltip content="导出">
|
||||
<a-button class="gi-table-setting__btn" type="text" size="mini" @click="emit('export')">
|
||||
<template #icon><icon-download :size="18" /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip content="斑马纹">
|
||||
<a-button class="gi-table-setting__btn" type="text" size="mini" @click="stripe = !stripe">
|
||||
<template #icon><icon-brush :size="18" /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip content="刷新">
|
||||
<a-button class="gi-table-setting__btn" type="text" size="mini" @click="emit('refresh')">
|
||||
<template #icon><icon-refresh :size="18" /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip :content="isFullscreen ? '退出全屏' : '全屏'">
|
||||
<a-button class="gi-table-setting__btn" type="text" size="mini" @click="toggleFullscreen">
|
||||
<template #icon>
|
||||
<icon-fullscreen v-if="!isFullscreen" :size="18" />
|
||||
<icon-fullscreen-exit v-else :size="18" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip content="显示边框">
|
||||
<a-button class="gi-table-setting__btn" type="text" size="mini" @click="toggleBorder">
|
||||
<template #icon><icon-apps :size="18" /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-dropdown @select="handleSizeChange">
|
||||
<a-tooltip content="表格尺寸">
|
||||
<a-button class="gi-table-setting__btn" type="text" size="mini">
|
||||
<template #icon>
|
||||
<icon-font-colors :size="18" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<template #content>
|
||||
<a-doption v-for="item in TABLE_SIZE_OPTIONS" :key="item.value" :value="item.value"
|
||||
:active="item.value === size">
|
||||
{{ item.label }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-popover v-if="showSettingColumnBtn" trigger="click" position="br"
|
||||
:content-style="{ minWidth: '160px', padding: '6px 8px 10px' }">
|
||||
<a-button class="gi-table-setting__btn" type="text" size="mini">
|
||||
<template #icon><icon-settings :size="18" /></template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<div class="gi-table-setting__draggable">
|
||||
<VueDraggable :model-value="currentSettingColumns" :animation="150"
|
||||
@update:model-value="onSettingColumnsUpdate">
|
||||
<div v-for="item in currentSettingColumns" :key="item.key" class="gi-table-setting__draggable-item">
|
||||
<div class="gi-table-setting__draggable-item-move">
|
||||
<icon-drag-dot-vertical />
|
||||
</div>
|
||||
<a-checkbox v-model:model-value="item.show" :disabled="item.disabled">
|
||||
{{ item.title }}
|
||||
</a-checkbox>
|
||||
<div class="gi-table-setting__draggable-item-fixed">
|
||||
<span class="gi-table-setting__pin-btn" :class="{ 'is-active': item.fixedLeft }"
|
||||
@click.stop="toggleFixedLeft(item.key)">
|
||||
<icon-pushpin />
|
||||
</span>
|
||||
<span class="gi-table-setting__pin-btn gi-table-setting__pin-btn--right"
|
||||
:class="{ 'is-active': item.fixedRight }" @click.stop="toggleFixedRight(item.key)">
|
||||
<icon-pushpin />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
<a-divider :margin="6" />
|
||||
<a-row justify="center">
|
||||
<a-button type="primary" size="mini" long @click="resetSettingColumns">
|
||||
<template #icon><icon-refresh /></template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
</a-row>
|
||||
|
||||
<div class="gi-table-setting__body">
|
||||
<slot :setting-columns="settingColumns" :is-fullscreen="isFullscreen" :table-props="tableProps" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownInstance, TableColumnData, TableInstance } from '@arco-design/web-vue'
|
||||
import type { GiTableSettingProps, TableSettingColumnListItem } from './type'
|
||||
import { computed, ref } from 'vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { useTableColumnSetting } from './useTableColumnSetting'
|
||||
|
||||
defineOptions({ name: 'GiTableSetting' })
|
||||
|
||||
const props = withDefaults(defineProps<GiTableSettingProps>(), {
|
||||
title: '',
|
||||
columns: () => [],
|
||||
disabledColumnKeys: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
export: []
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
'custom-title'?: () => unknown
|
||||
'custom-extra'?: () => unknown
|
||||
'default': (props: {
|
||||
settingColumns: TableColumnData[]
|
||||
isFullscreen: boolean
|
||||
tableProps: {
|
||||
stripe: boolean
|
||||
bordered: { cell: boolean }
|
||||
size: TableInstance['size']
|
||||
}
|
||||
}) => unknown
|
||||
}>()
|
||||
|
||||
const stripe = ref(false)
|
||||
const size = ref<TableInstance['size']>('medium')
|
||||
const isBordered = ref(true)
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const TABLE_SIZE_OPTIONS = [
|
||||
{ label: '迷你', value: 'mini' },
|
||||
{ label: '小型', value: 'small' },
|
||||
{ label: '中等', value: 'medium' },
|
||||
{ label: '大型', value: 'large' }
|
||||
] as const
|
||||
|
||||
const tableProps = computed(() => ({
|
||||
stripe: stripe.value,
|
||||
bordered: { cell: isBordered.value },
|
||||
size: size.value
|
||||
}))
|
||||
|
||||
const {
|
||||
showSettingColumnBtn,
|
||||
currentSettingColumns,
|
||||
settingColumnList,
|
||||
settingColumns,
|
||||
resetSettingColumns,
|
||||
toggleFixedLeft,
|
||||
toggleFixedRight
|
||||
} = useTableColumnSetting({
|
||||
columns: () => props.columns,
|
||||
disabledColumnKeys: () => props.disabledColumnKeys
|
||||
})
|
||||
|
||||
function onSettingColumnsUpdate(val: TableSettingColumnListItem[]) {
|
||||
settingColumnList.value = val
|
||||
}
|
||||
|
||||
const handleSizeChange: DropdownInstance['onSelect'] = (value) => {
|
||||
if (value) {
|
||||
size.value = value as TableInstance['size']
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
function toggleBorder() {
|
||||
isBordered.value = !isBordered.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gi-table-setting {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-1);
|
||||
|
||||
&--fullscreen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1001;
|
||||
box-sizing: border-box;
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
&__btn {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__draggable {
|
||||
box-sizing: border-box;
|
||||
max-height: 250px;
|
||||
padding: 1px 0;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
&__draggable-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
&-move {
|
||||
padding: 0 2px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
&-fixed {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
:deep(.arco-checkbox) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
|
||||
.arco-checkbox-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__pin-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&--right {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import TableSetting from './GiTableSetting.vue'
|
||||
|
||||
export type * from './type'
|
||||
export type TableSettingInstance = InstanceType<typeof TableSetting>
|
||||
export { TableSetting as GiTableSetting }
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
|
||||
export interface GiTableSettingProps {
|
||||
/** 工具栏标题 */
|
||||
title?: string
|
||||
/** 列配置(用于列设置) */
|
||||
columns?: TableInstance['columns']
|
||||
/** 禁止控制显示隐藏的列 key */
|
||||
disabledColumnKeys?: string[]
|
||||
}
|
||||
|
||||
/** 列设置项类型 */
|
||||
export interface TableSettingColumnListItem {
|
||||
title: string
|
||||
key: string
|
||||
show: boolean
|
||||
disabled: boolean
|
||||
/** 固定左侧 */
|
||||
fixedLeft?: boolean
|
||||
/** 固定右侧 */
|
||||
fixedRight?: boolean
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { TableColumnData, TableInstance } from '@arco-design/web-vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import type { TableSettingColumnListItem } from './type'
|
||||
import { computed, ref, toValue, watch } from 'vue'
|
||||
|
||||
export interface UseTableColumnSettingOptions {
|
||||
columns: MaybeRefOrGetter<TableInstance['columns'] | undefined>
|
||||
disabledColumnKeys?: MaybeRefOrGetter<string[]>
|
||||
}
|
||||
|
||||
/** 获取列的 key */
|
||||
export function getTableColumnKey(column: TableColumnData, index?: number) {
|
||||
if (column.dataIndex) return String(column.dataIndex)
|
||||
if (typeof column.title === 'string' && column.title) return column.title
|
||||
return `__column_${index ?? Date.now()}__`
|
||||
}
|
||||
|
||||
/** 表格列设置:显隐、排序、固定 */
|
||||
export function useTableColumnSetting(options: UseTableColumnSettingOptions) {
|
||||
const showSettingColumnBtn = computed(() => {
|
||||
const columns = Array.isArray(toValue(options.columns)) ? toValue(options.columns) : undefined
|
||||
return Boolean(columns?.length)
|
||||
})
|
||||
|
||||
const initialSettingColumns = computed<TableSettingColumnListItem[]>(() => {
|
||||
const raw = toValue(options.columns)
|
||||
if (!raw) return []
|
||||
|
||||
const columns = Array.isArray(raw) ? raw : []
|
||||
return columns.map((column, index) => {
|
||||
const key = getTableColumnKey(column, index)
|
||||
const fixed = column.fixed
|
||||
const disabledKeys = toValue(options.disabledColumnKeys) ?? []
|
||||
return {
|
||||
key,
|
||||
title: typeof column.title === 'string' ? column.title : '',
|
||||
show: true,
|
||||
disabled: disabledKeys.includes(key),
|
||||
fixedLeft: fixed === 'left',
|
||||
fixedRight: fixed === 'right'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const settingColumnList = ref<TableSettingColumnListItem[]>([])
|
||||
|
||||
const isColumnStructureMatch = (
|
||||
userColumns: TableSettingColumnListItem[],
|
||||
initialColumns: TableSettingColumnListItem[]
|
||||
): boolean => {
|
||||
if (userColumns.length === 0 || userColumns.length !== initialColumns.length) return false
|
||||
const initialKeys = new Set(initialColumns.map((item) => item.key))
|
||||
const userKeys = new Set(userColumns.map((item) => item.key))
|
||||
return initialKeys.size === userKeys.size && [...initialKeys].every((key) => userKeys.has(key))
|
||||
}
|
||||
|
||||
const columnMap = computed(() => {
|
||||
const raw = toValue(options.columns)
|
||||
if (!raw) return new Map<string, TableColumnData>()
|
||||
const columns = Array.isArray(raw) ? raw : []
|
||||
return new Map(columns.map((col, index) => [getTableColumnKey(col, index), col]))
|
||||
})
|
||||
|
||||
const currentSettingColumns = computed(() => {
|
||||
const isValid = isColumnStructureMatch(settingColumnList.value, initialSettingColumns.value)
|
||||
return isValid ? settingColumnList.value : initialSettingColumns.value
|
||||
})
|
||||
|
||||
const resetSettingColumns = () => {
|
||||
settingColumnList.value = [...initialSettingColumns.value]
|
||||
}
|
||||
|
||||
const ensureSettingColumnList = () => {
|
||||
if (settingColumnList.value.length === 0 && initialSettingColumns.value.length > 0) {
|
||||
settingColumnList.value = initialSettingColumns.value.map((item) => ({ ...item }))
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFixedLeft = (key: string) => {
|
||||
ensureSettingColumnList()
|
||||
settingColumnList.value = settingColumnList.value.map((item) =>
|
||||
item.key === key
|
||||
? { ...item, fixedLeft: !item.fixedLeft, fixedRight: false }
|
||||
: item
|
||||
)
|
||||
}
|
||||
|
||||
const toggleFixedRight = (key: string) => {
|
||||
ensureSettingColumnList()
|
||||
settingColumnList.value = settingColumnList.value.map((item) =>
|
||||
item.key === key
|
||||
? { ...item, fixedRight: !item.fixedRight, fixedLeft: false }
|
||||
: item
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => toValue(options.columns),
|
||||
() => {
|
||||
if (!isColumnStructureMatch(settingColumnList.value, initialSettingColumns.value)) {
|
||||
resetSettingColumns()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const settingColumns = computed(() => {
|
||||
const raw = toValue(options.columns)
|
||||
if (!raw) return []
|
||||
|
||||
const shown = currentSettingColumns.value.filter((item) => item.show)
|
||||
const leftFixed: typeof shown = []
|
||||
const noFixed: typeof shown = []
|
||||
const rightFixed: typeof shown = []
|
||||
for (const item of shown) {
|
||||
if (item.fixedLeft) leftFixed.push(item)
|
||||
else if (item.fixedRight) rightFixed.push(item)
|
||||
else noFixed.push(item)
|
||||
}
|
||||
const ordered = [...leftFixed, ...noFixed, ...rightFixed]
|
||||
|
||||
return ordered
|
||||
.map((item) => {
|
||||
const col = columnMap.value.get(item.key)
|
||||
if (!col) return null
|
||||
const fixed = item.fixedRight ? 'right' : item.fixedLeft ? 'left' : undefined
|
||||
return { ...col, fixed }
|
||||
})
|
||||
.filter(Boolean) as TableColumnData[]
|
||||
})
|
||||
|
||||
return {
|
||||
showSettingColumnBtn,
|
||||
currentSettingColumns,
|
||||
settingColumnList,
|
||||
settingColumns,
|
||||
resetSettingColumns,
|
||||
toggleFixedLeft,
|
||||
toggleFixedRight
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user