初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
@@ -0,0 +1,242 @@
<!-- 角色管理页面 -->
<template>
<div class="art-full-height">
<RoleSearch
v-show="showSearchBar"
v-model="searchForm"
@search="handleSearch"
@reset="resetSearchParams"
></RoleSearch>
<ElCard
class="art-table-card"
shadow="never"
:style="{ 'margin-top': showSearchBar ? '12px' : '0' }"
>
<ArtTableHeader
v-model:columns="columnChecks"
v-model:showSearchBar="showSearchBar"
:loading="loading"
@refresh="refreshData"
>
<template #left>
<ElSpace wrap>
<ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
</ElSpace>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
</ArtTable>
</ElCard>
<!-- 角色编辑弹窗 -->
<RoleEditDialog
v-model="dialogVisible"
:dialog-type="dialogType"
:role-data="currentRoleData"
@success="refreshData"
/>
<!-- 菜单权限弹窗 -->
<RolePermissionDialog
v-model="permissionDialog"
:role-data="currentRoleData"
@success="refreshData"
/>
</div>
</template>
<script setup lang="ts">
import { ButtonMoreItem } from '@/components/core/forms/art-button-more/index.vue'
import { useTable } from '@/hooks/core/useTable'
import { fetchGetRoleList } from '@/apps/demo/api/system-manage'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import RoleSearch from './modules/role-search.vue'
import RoleEditDialog from './modules/role-edit-dialog.vue'
import RolePermissionDialog from './modules/role-permission-dialog.vue'
import { ElTag, ElMessageBox } from 'element-plus'
defineOptions({ name: 'Role' })
type RoleListItem = Api.SystemManage.RoleListItem
// 搜索表单
const searchForm = ref({
roleName: undefined,
roleCode: undefined,
description: undefined,
enabled: undefined,
daterange: undefined
})
const showSearchBar = ref(false)
const dialogVisible = ref(false)
const permissionDialog = ref(false)
const currentRoleData = ref<RoleListItem | undefined>(undefined)
const {
columns,
columnChecks,
data,
loading,
pagination,
getData,
searchParams,
resetSearchParams,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
// 核心配置
core: {
apiFn: fetchGetRoleList,
apiParams: {
current: 1,
size: 20
},
// 排除 apiParams 中的属性
excludeParams: ['daterange'],
columnsFactory: () => [
{
prop: 'roleId',
label: '角色ID',
width: 100
},
{
prop: 'roleName',
label: '角色名称',
minWidth: 120
},
{
prop: 'roleCode',
label: '角色编码',
minWidth: 120
},
{
prop: 'description',
label: '角色描述',
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'enabled',
label: '角色状态',
width: 100,
formatter: (row) => {
const statusConfig = row.enabled
? { type: 'success', text: '启用' }
: { type: 'warning', text: '禁用' }
return h(
ElTag,
{ type: statusConfig.type as 'success' | 'warning' },
() => statusConfig.text
)
}
},
{
prop: 'createTime',
label: '创建日期',
width: 180,
sortable: true
},
{
prop: 'operation',
label: '操作',
width: 80,
fixed: 'right',
formatter: (row) =>
h('div', [
h(ArtButtonMore, {
list: [
{
key: 'permission',
label: '菜单权限',
icon: 'ri:user-3-line'
},
{
key: 'edit',
label: '编辑角色',
icon: 'ri:edit-2-line'
},
{
key: 'delete',
label: '删除角色',
icon: 'ri:delete-bin-4-line',
color: '#f56c6c'
}
],
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row)
})
])
}
]
}
})
const dialogType = ref<'add' | 'edit'>('add')
const showDialog = (type: 'add' | 'edit', row?: RoleListItem) => {
dialogVisible.value = true
dialogType.value = type
currentRoleData.value = row
}
/**
* 搜索处理
* @param params 搜索参数
*/
const handleSearch = (params: Record<string, any>) => {
// 处理日期区间参数,把 daterange 转换为 startTime 和 endTime
const { daterange, ...filtersParams } = params
const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null]
// 搜索参数赋值
Object.assign(searchParams, { ...filtersParams, startTime, endTime })
getData()
}
const buttonMoreClick = (item: ButtonMoreItem, row: RoleListItem) => {
switch (item.key) {
case 'permission':
showPermissionDialog(row)
break
case 'edit':
showDialog('edit', row)
break
case 'delete':
deleteRole(row)
break
}
}
const showPermissionDialog = (row?: RoleListItem) => {
permissionDialog.value = true
currentRoleData.value = row
}
const deleteRole = (row: RoleListItem) => {
ElMessageBox.confirm(`确定删除角色"${row.roleName}"吗?此操作不可恢复!`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// TODO: 调用删除接口
ElMessage.success('删除成功')
refreshData()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
</script>
@@ -0,0 +1,162 @@
<template>
<ElDialog
v-model="visible"
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
width="30%"
align-center
@close="handleClose"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="角色名称" prop="roleName">
<ElInput v-model="form.roleName" placeholder="请输入角色名称" />
</ElFormItem>
<ElFormItem label="角色编码" prop="roleCode">
<ElInput v-model="form.roleCode" placeholder="请输入角色编码" />
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入角色描述"
/>
</ElFormItem>
<ElFormItem label="启用">
<ElSwitch v-model="form.enabled" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="handleClose">取消</ElButton>
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
type RoleListItem = Api.SystemManage.RoleListItem
interface Props {
modelValue: boolean
dialogType: 'add' | 'edit'
roleData?: RoleListItem
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
dialogType: 'add',
roleData: undefined
})
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
/**
* 弹窗显示状态双向绑定
*/
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
/**
* 表单验证规则
*/
const rules = reactive<FormRules>({
roleName: [
{ required: true, message: '请输入角色名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
roleCode: [
{ required: true, message: '请输入角色编码', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
description: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
})
/**
* 表单数据
*/
const form = reactive<RoleListItem>({
roleId: 0,
roleName: '',
roleCode: '',
description: '',
createTime: '',
enabled: true
})
/**
* 监听弹窗打开,初始化表单数据
*/
watch(
() => props.modelValue,
(newVal) => {
if (newVal) initForm()
}
)
/**
* 监听角色数据变化,更新表单
*/
watch(
() => props.roleData,
(newData) => {
if (newData && props.modelValue) initForm()
},
{ deep: true }
)
/**
* 初始化表单数据
* 根据弹窗类型填充表单或重置表单
*/
const initForm = () => {
if (props.dialogType === 'edit' && props.roleData) {
Object.assign(form, props.roleData)
} else {
Object.assign(form, {
roleId: 0,
roleName: '',
roleCode: '',
description: '',
createTime: '',
enabled: true
})
}
}
/**
* 关闭弹窗并重置表单
*/
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
/**
* 提交表单
* 验证通过后调用接口保存数据
*/
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
// TODO: 调用新增/编辑接口
const message = props.dialogType === 'add' ? '新增成功' : '修改成功'
ElMessage.success(message)
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
}
}
</script>
@@ -0,0 +1,254 @@
<template>
<ElDialog
v-model="visible"
title="菜单权限"
width="520px"
align-center
class="el-dialog-border"
@close="handleClose"
>
<ElScrollbar height="70vh">
<ElTree
ref="treeRef"
:data="processedMenuList"
show-checkbox
node-key="name"
:default-expand-all="isExpandAll"
:default-checked-keys="[1, 2, 3]"
:props="defaultProps"
@check="handleTreeCheck"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</ElScrollbar>
<template #footer>
<ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton>
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
isSelectAll ? '取消全选' : '全部选择'
}}</ElButton>
<ElButton type="primary" @click="savePermission">保存</ElButton>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { useMenuStore } from '@/store/modules/menu'
import { formatMenuTitle } from '@/utils/router'
type RoleListItem = Api.SystemManage.RoleListItem
interface Props {
modelValue: boolean
roleData?: RoleListItem
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
roleData: undefined
})
const emit = defineEmits<Emits>()
const { menuList } = storeToRefs(useMenuStore())
const treeRef = ref()
const isExpandAll = ref(true)
const isSelectAll = ref(false)
/**
* 弹窗显示状态双向绑定
*/
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
/**
* 菜单节点类型
*/
interface MenuNode {
id?: string | number
name?: string
label?: string
meta?: {
title?: string
authList?: Array<{
authMark: string
title: string
checked?: boolean
}>
}
children?: MenuNode[]
[key: string]: any
}
/**
* 处理菜单数据,将 authList 转换为树形子节点
* 递归处理菜单树,将权限列表展开为可选择的子节点
*/
const processedMenuList = computed(() => {
const processNode = (node: MenuNode): MenuNode => {
const processed = { ...node }
// 如果有 authList,将其转换为子节点
if (node.meta?.authList?.length) {
const authNodes = node.meta.authList.map((auth) => ({
id: `${node.id}_${auth.authMark}`,
name: `${node.name}_${auth.authMark}`,
label: auth.title,
authMark: auth.authMark,
isAuth: true,
checked: auth.checked || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
// 递归处理子节点
if (processed.children) {
processed.children = processed.children.map(processNode)
}
return processed
}
return (menuList.value as any[]).map(processNode)
})
/**
* 树形组件配置
*/
const defaultProps = {
children: 'children',
label: (data: any) => formatMenuTitle(data.meta?.title) || data.label || ''
}
/**
* 监听弹窗打开,初始化权限数据
*/
watch(
() => props.modelValue,
(newVal) => {
if (newVal && props.roleData) {
// TODO: 根据角色加载对应的权限数据
console.log('设置权限:', props.roleData)
}
}
)
/**
* 关闭弹窗并清空选中状态
*/
const handleClose = () => {
visible.value = false
treeRef.value?.setCheckedKeys([])
}
/**
* 保存权限配置
*/
const savePermission = () => {
// TODO: 调用保存权限接口
ElMessage.success('权限保存成功')
emit('success')
handleClose()
}
/**
* 切换全部展开/收起状态
*/
const toggleExpandAll = () => {
const tree = treeRef.value
if (!tree) return
const nodes = tree.store.nodesMap
// 这里保留 any,因为 Element Plus 的内部节点类型较复杂
Object.values(nodes).forEach((node: any) => {
node.expanded = !isExpandAll.value
})
isExpandAll.value = !isExpandAll.value
}
/**
* 切换全选/取消全选状态
*/
const toggleSelectAll = () => {
const tree = treeRef.value
if (!tree) return
if (!isSelectAll.value) {
const allKeys = getAllNodeKeys(processedMenuList.value)
tree.setCheckedKeys(allKeys)
} else {
tree.setCheckedKeys([])
}
isSelectAll.value = !isSelectAll.value
}
/**
* 递归获取所有节点的 key
* @param nodes 节点列表
* @returns 所有节点的 key 数组
*/
const getAllNodeKeys = (nodes: MenuNode[]): string[] => {
const keys: string[] = []
const traverse = (nodeList: MenuNode[]): void => {
nodeList.forEach((node) => {
if (node.name) keys.push(node.name)
if (node.children?.length) traverse(node.children)
})
}
traverse(nodes)
return keys
}
/**
* 处理树节点选中状态变化
* 同步更新全选按钮状态
*/
const handleTreeCheck = () => {
const tree = treeRef.value
if (!tree) return
const checkedKeys = tree.getCheckedKeys()
const allKeys = getAllNodeKeys(processedMenuList.value)
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
}
/**
* 输出选中的权限数据到控制台
* 用于调试和查看当前选中的权限配置
*/
const outputSelectedData = () => {
const tree = treeRef.value
if (!tree) return
const selectedData = {
checkedKeys: tree.getCheckedKeys(),
halfCheckedKeys: tree.getHalfCheckedKeys(),
checkedNodes: tree.getCheckedNodes(),
halfCheckedNodes: tree.getHalfCheckedNodes(),
totalChecked: tree.getCheckedKeys().length,
totalHalfChecked: tree.getHalfCheckedKeys().length
}
console.log('=== 选中的权限数据 ===', selectedData)
ElMessage.success(`已输出选中数据到控制台,共选中 ${selectedData.totalChecked} 个节点`)
}
</script>
@@ -0,0 +1,121 @@
<template>
<ArtSearchBar
ref="searchBarRef"
v-model="formData"
:items="formItems"
:rules="rules"
@reset="handleReset"
@search="handleSearch"
>
</ArtSearchBar>
</template>
<script setup lang="ts">
interface Props {
modelValue: Record<string, any>
}
interface Emits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'search', params: Record<string, any>): void
(e: 'reset'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const searchBarRef = ref()
/**
* 表单数据双向绑定
*/
const formData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
/**
* 表单校验规则
*/
const rules = {}
/**
* 角色状态选项
*/
const statusOptions = ref([
{ label: '启用', value: true },
{ label: '禁用', value: false }
])
/**
* 搜索表单配置项
*/
const formItems = computed(() => [
{
label: '角色名称',
key: 'roleName',
type: 'input',
placeholder: '请输入角色名称',
clearable: true
},
{
label: '角色编码',
key: 'roleCode',
type: 'input',
placeholder: '请输入角色编码',
clearable: true
},
{
label: '角色描述',
key: 'description',
type: 'input',
placeholder: '请输入角色描述',
clearable: true
},
{
label: '角色状态',
key: 'enabled',
type: 'select',
props: {
placeholder: '请选择状态',
options: statusOptions.value,
clearable: true
}
},
{
label: '创建日期',
key: 'daterange',
type: 'datetime',
props: {
style: { width: '100%' },
placeholder: '请选择日期范围',
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD',
shortcuts: [
{ text: '今日', value: [new Date(), new Date()] },
{ text: '最近一周', value: [new Date(Date.now() - 604800000), new Date()] },
{ text: '最近一个月', value: [new Date(Date.now() - 2592000000), new Date()] }
]
}
}
])
/**
* 处理重置事件
*/
const handleReset = () => {
emit('reset')
}
/**
* 处理搜索事件
* 验证表单后触发搜索
*/
const handleSearch = async () => {
await searchBarRef.value.validate()
emit('search', formData.value)
}
</script>