feat(admin): 模板精简+请求层适配 Django JWT
- 克隆 art-design-pro v3.0.2(GitHub depth1),rm .git,pnpm clean:dev 精简演示页(92 文件) - .env.development/.env.production 均设 VITE_API_URL=/api,代理目标 127.0.0.1:8000(移除 Apifox Mock) - http 封装信封适配:code===200 解包 data;msg/message 字段兼容(拦截器/error/类型) - /api/admin/auth/token 登录适配(access->Bearer token, refresh->refreshToken);/api/admin/auth/me 映射 UserInfo(userName/roles/is_admin) - 登录页演示账号替换为真实 admin;logOut 按路由名判登录页修复 redirect 嵌套 - 根 .gitignore 补 !admin/.env(模板公共 env 非密钥,需入库) - 验证:Playwright 全链路(错误密码 401 留登页/正确登录进工作台/登出/受保护路由重定向/redirect 回跳)+ pnpm build(vue-tsc 通过,产物无 mock 域名) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
<!-- 菜单管理页面 -->
|
||||
<template>
|
||||
<div class="menu-page art-full-height">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model="formFilters"
|
||||
:items="formItems"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:showZebra="false"
|
||||
:loading="loading"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-auth="'add'" @click="handleAddMenu" v-ripple> 添加菜单 </ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="path"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="filteredTableData"
|
||||
:stripe="false"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
/>
|
||||
|
||||
<!-- 菜单弹窗 -->
|
||||
<MenuDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:type="dialogType"
|
||||
:editData="editData"
|
||||
:lockType="lockMenuType"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatMenuTitle } from '@/utils/router'
|
||||
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
|
||||
import { useTableColumns } from '@/hooks/core/useTableColumns'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import MenuDialog from './modules/menu-dialog.vue'
|
||||
import { fetchGetMenuList } from '@/api/system-manage'
|
||||
import { ElTag, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'Menus' })
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
// 弹窗相关
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'menu' | 'button'>('menu')
|
||||
const editData = ref<AppRouteRecord | any>(null)
|
||||
const lockMenuType = ref(false)
|
||||
|
||||
// 搜索相关
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
route: ''
|
||||
}
|
||||
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
const appliedFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
label: '菜单名称',
|
||||
key: 'name',
|
||||
type: 'input',
|
||||
props: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '路由地址',
|
||||
key: 'route',
|
||||
type: 'input',
|
||||
props: { clearable: true }
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getMenuList()
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取菜单列表数据
|
||||
*/
|
||||
const getMenuList = async (): Promise<void> => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const list = await fetchGetMenuList()
|
||||
tableData.value = list
|
||||
} catch (error) {
|
||||
throw error instanceof Error ? error : new Error('获取菜单失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单类型标签颜色
|
||||
* @param row 菜单行数据
|
||||
* @returns 标签颜色类型
|
||||
*/
|
||||
const getMenuTypeTag = (
|
||||
row: AppRouteRecord
|
||||
): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
|
||||
if (row.meta?.isAuthButton) return 'danger'
|
||||
if (row.children?.length) return 'info'
|
||||
if (row.meta?.link && row.meta?.isIframe) return 'success'
|
||||
if (row.path) return 'primary'
|
||||
if (row.meta?.link) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单类型文本
|
||||
* @param row 菜单行数据
|
||||
* @returns 菜单类型文本
|
||||
*/
|
||||
const getMenuTypeText = (row: AppRouteRecord): string => {
|
||||
if (row.meta?.isAuthButton) return '按钮'
|
||||
if (row.children?.length) return '目录'
|
||||
if (row.meta?.link && row.meta?.isIframe) return '内嵌'
|
||||
if (row.path) return '菜单'
|
||||
if (row.meta?.link) return '外链'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
// 表格列配置
|
||||
const { columnChecks, columns } = useTableColumns(() => [
|
||||
{
|
||||
prop: 'meta.title',
|
||||
label: '菜单名称',
|
||||
minWidth: 120,
|
||||
formatter: (row: AppRouteRecord) => formatMenuTitle(row.meta?.title)
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(ElTag, { type: getMenuTypeTag(row) }, () => getMenuTypeText(row))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
if (row.meta?.isAuthButton) return ''
|
||||
return row.meta?.link || row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'meta.authList',
|
||||
label: '权限标识',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
if (row.meta?.isAuthButton) {
|
||||
return row.meta?.authMark || ''
|
||||
}
|
||||
if (!row.meta?.authList?.length) return ''
|
||||
return `${row.meta.authList.length} 个权限标识`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'date',
|
||||
label: '编辑时间',
|
||||
formatter: () => '2022-3-12 12:00:00'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: () => h(ElTag, { type: 'success' }, () => '启用')
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
align: 'right',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
const buttonStyle = { style: 'text-align: right' }
|
||||
|
||||
if (row.meta?.isAuthButton) {
|
||||
return h('div', buttonStyle, [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => handleEditAuth(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => handleDeleteAuth()
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
return h('div', buttonStyle, [
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => handleAddAuth(),
|
||||
title: '新增权限'
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => handleEditMenu(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => handleDeleteMenu()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 数据相关
|
||||
const tableData = ref<AppRouteRecord[]>([])
|
||||
|
||||
/**
|
||||
* 重置搜索条件
|
||||
*/
|
||||
const handleReset = (): void => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
Object.assign(appliedFilters, { ...initialSearchState })
|
||||
getMenuList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
const handleSearch = (): void => {
|
||||
Object.assign(appliedFilters, { ...formFilters })
|
||||
getMenuList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新菜单列表
|
||||
*/
|
||||
const handleRefresh = (): void => {
|
||||
getMenuList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度克隆对象
|
||||
* @param obj 要克隆的对象
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
const deepClone = <T,>(obj: T): T => {
|
||||
if (obj === null || typeof obj !== 'object') return obj
|
||||
if (obj instanceof Date) return new Date(obj) as T
|
||||
if (Array.isArray(obj)) return obj.map((item) => deepClone(item)) as T
|
||||
|
||||
const cloned = {} as T
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
cloned[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* 将权限列表转换为子节点
|
||||
* @param items 菜单项数组
|
||||
* @returns 转换后的菜单项数组
|
||||
*/
|
||||
const convertAuthListToChildren = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
return items.map((item) => {
|
||||
const clonedItem = deepClone(item)
|
||||
|
||||
if (clonedItem.children?.length) {
|
||||
clonedItem.children = convertAuthListToChildren(clonedItem.children)
|
||||
}
|
||||
|
||||
if (item.meta?.authList?.length) {
|
||||
const authChildren: AppRouteRecord[] = item.meta.authList.map(
|
||||
(auth: { title: string; authMark: string }) => ({
|
||||
path: `${item.path}_auth_${auth.authMark}`,
|
||||
name: `${String(item.name)}_auth_${auth.authMark}`,
|
||||
meta: {
|
||||
title: auth.title,
|
||||
authMark: auth.authMark,
|
||||
isAuthButton: true,
|
||||
parentPath: item.path
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
clonedItem.children = clonedItem.children?.length
|
||||
? [...clonedItem.children, ...authChildren]
|
||||
: authChildren
|
||||
}
|
||||
|
||||
return clonedItem
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索菜单
|
||||
* @param items 菜单项数组
|
||||
* @returns 搜索结果数组
|
||||
*/
|
||||
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
const results: AppRouteRecord[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
|
||||
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
|
||||
const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
|
||||
const menuPath = (item.path || '').toLowerCase()
|
||||
const nameMatch = !searchName || menuTitle.includes(searchName)
|
||||
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
|
||||
|
||||
if (item.children?.length) {
|
||||
const matchedChildren = searchMenu(item.children)
|
||||
if (matchedChildren.length > 0) {
|
||||
const clonedItem = deepClone(item)
|
||||
clonedItem.children = matchedChildren
|
||||
results.push(clonedItem)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (nameMatch && routeMatch) {
|
||||
results.push(deepClone(item))
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// 过滤后的表格数据
|
||||
const filteredTableData = computed(() => {
|
||||
const searchedData = searchMenu(tableData.value)
|
||||
return convertAuthListToChildren(searchedData)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加菜单
|
||||
*/
|
||||
const handleAddMenu = (): void => {
|
||||
dialogType.value = 'menu'
|
||||
editData.value = null
|
||||
lockMenuType.value = true
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加权限按钮
|
||||
*/
|
||||
const handleAddAuth = (): void => {
|
||||
dialogType.value = 'menu'
|
||||
editData.value = null
|
||||
lockMenuType.value = false
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑菜单
|
||||
* @param row 菜单行数据
|
||||
*/
|
||||
const handleEditMenu = (row: AppRouteRecord): void => {
|
||||
dialogType.value = 'menu'
|
||||
editData.value = row
|
||||
lockMenuType.value = true
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑权限按钮
|
||||
* @param row 权限行数据
|
||||
*/
|
||||
const handleEditAuth = (row: AppRouteRecord): void => {
|
||||
dialogType.value = 'button'
|
||||
editData.value = {
|
||||
title: row.meta?.title,
|
||||
authMark: row.meta?.authMark
|
||||
}
|
||||
lockMenuType.value = false
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单表单数据类型
|
||||
*/
|
||||
interface MenuFormData {
|
||||
name: string
|
||||
path: string
|
||||
component?: string
|
||||
icon?: string
|
||||
roles?: string[]
|
||||
sort?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单数据
|
||||
* @param formData 表单数据
|
||||
*/
|
||||
const handleSubmit = (formData: MenuFormData): void => {
|
||||
console.log('提交数据:', formData)
|
||||
// TODO: 调用API保存数据
|
||||
getMenuList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*/
|
||||
const handleDeleteMenu = async (): Promise<void> => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
ElMessage.success('删除成功')
|
||||
getMenuList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除权限按钮
|
||||
*/
|
||||
const handleDeleteAuth = async (): Promise<void> => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
ElMessage.success('删除成功')
|
||||
getMenuList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换展开/收起所有菜单
|
||||
*/
|
||||
const toggleExpand = (): void => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value?.elTableRef && filteredTableData.value) {
|
||||
const processRows = (rows: AppRouteRecord[]) => {
|
||||
rows.forEach((row) => {
|
||||
if (row.children?.length) {
|
||||
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
processRows(filteredTableData.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:title="dialogTitle"
|
||||
:model-value="visible"
|
||||
@update:model-value="handleCancel"
|
||||
width="860px"
|
||||
align-center
|
||||
class="menu-dialog"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<ArtForm
|
||||
ref="formRef"
|
||||
v-model="form"
|
||||
:items="formItems"
|
||||
:rules="rules"
|
||||
:span="width > 640 ? 12 : 24"
|
||||
:gutter="20"
|
||||
label-width="100px"
|
||||
:show-reset="false"
|
||||
:show-submit="false"
|
||||
>
|
||||
<template #menuType>
|
||||
<ElRadioGroup v-model="form.menuType" :disabled="disableMenuType">
|
||||
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
|
||||
<ElRadioButton value="button" label="button">按钮</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</template>
|
||||
</ArtForm>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="handleCancel">取 消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">确 定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { ElIcon, ElTooltip } from 'element-plus'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { formatMenuTitle } from '@/utils/router'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import type { FormItem } from '@/components/core/forms/art-form/index.vue'
|
||||
import ArtForm from '@/components/core/forms/art-form/index.vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
/**
|
||||
* 创建带 tooltip 的表单标签
|
||||
* @param label 标签文本
|
||||
* @param tooltip 提示文本
|
||||
* @returns 渲染函数
|
||||
*/
|
||||
const createLabelTooltip = (label: string, tooltip: string) => {
|
||||
return () =>
|
||||
h('span', { class: 'flex items-center' }, [
|
||||
h('span', label),
|
||||
h(
|
||||
ElTooltip,
|
||||
{
|
||||
content: tooltip,
|
||||
placement: 'top'
|
||||
},
|
||||
() => h(ElIcon, { class: 'ml-0.5 cursor-help' }, () => h(QuestionFilled))
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
interface MenuFormData {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
label: string
|
||||
component: string
|
||||
icon: string
|
||||
isEnable: boolean
|
||||
sort: number
|
||||
isMenu: boolean
|
||||
keepAlive: boolean
|
||||
isHide: boolean
|
||||
isHideTab: boolean
|
||||
link: string
|
||||
isIframe: boolean
|
||||
showBadge: boolean
|
||||
showTextBadge: string
|
||||
fixedTab: boolean
|
||||
activePath: string
|
||||
roles: string[]
|
||||
isFullPage: boolean
|
||||
authName: string
|
||||
authLabel: string
|
||||
authIcon: string
|
||||
authSort: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
editData?: AppRouteRecord | any
|
||||
type?: 'menu' | 'button'
|
||||
lockType?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'submit', data: MenuFormData): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
type: 'menu',
|
||||
lockType: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const isEdit = ref(false)
|
||||
|
||||
const form = reactive<MenuFormData & { menuType: 'menu' | 'button' }>({
|
||||
menuType: 'menu',
|
||||
id: 0,
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
isEnable: true,
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHide: false,
|
||||
isHideTab: false,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
showBadge: false,
|
||||
showTextBadge: '',
|
||||
fixedTab: false,
|
||||
activePath: '',
|
||||
roles: [],
|
||||
isFullPage: false,
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
|
||||
label: [{ required: true, message: '输入权限标识', trigger: 'blur' }],
|
||||
authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
|
||||
authLabel: [{ required: true, message: '请输入权限标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单项配置
|
||||
*/
|
||||
const formItems = computed<FormItem[]>(() => {
|
||||
const baseItems: FormItem[] = [{ label: '菜单类型', key: 'menuType', span: 24 }]
|
||||
|
||||
// Switch 组件的 span:小屏幕 12,大屏幕 6
|
||||
const switchSpan = width.value < 640 ? 12 : 6
|
||||
|
||||
if (form.menuType === 'menu') {
|
||||
return [
|
||||
...baseItems,
|
||||
{ label: '菜单名称', key: 'name', type: 'input', props: { placeholder: '菜单名称' } },
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
'路由地址',
|
||||
'一级菜单:以 / 开头的绝对路径(如 /dashboard)\n二级及以下:相对路径(如 console、user)'
|
||||
),
|
||||
key: 'path',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:/dashboard 或 console' }
|
||||
},
|
||||
{ label: '权限标识', key: 'label', type: 'input', props: { placeholder: '如:User' } },
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
'组件路径',
|
||||
'一级父级菜单:填写 /index/index\n具体页面:填写组件路径(如 /system/user)\n目录菜单:留空'
|
||||
),
|
||||
key: 'component',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:/system/user 或留空' }
|
||||
},
|
||||
{ label: '图标', key: 'icon', type: 'input', props: { placeholder: '如:ri:user-line' } },
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
'角色权限',
|
||||
'仅用于前端权限模式:配置角色标识(如 R_SUPER、R_ADMIN)\n后端权限模式:无需配置'
|
||||
),
|
||||
key: 'roles',
|
||||
type: 'inputtag',
|
||||
props: { placeholder: '输入角色标识后按回车,如:R_SUPER' }
|
||||
},
|
||||
{
|
||||
label: '菜单排序',
|
||||
key: 'sort',
|
||||
type: 'number',
|
||||
props: { min: 1, controlsPosition: 'right', style: { width: '100%' } }
|
||||
},
|
||||
{
|
||||
label: '外部链接',
|
||||
key: 'link',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:https://www.example.com' }
|
||||
},
|
||||
{
|
||||
label: '文本徽章',
|
||||
key: 'showTextBadge',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:New、Hot' }
|
||||
},
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
'激活路径',
|
||||
'用于详情页等隐藏菜单,指定高亮显示的父级菜单路径\n例如:用户详情页高亮显示"用户管理"菜单'
|
||||
),
|
||||
key: 'activePath',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:/system/user' }
|
||||
},
|
||||
{ label: '是否启用', key: 'isEnable', type: 'switch', span: switchSpan },
|
||||
{ label: '页面缓存', key: 'keepAlive', type: 'switch', span: switchSpan },
|
||||
{ label: '隐藏菜单', key: 'isHide', type: 'switch', span: switchSpan },
|
||||
{ label: '是否内嵌', key: 'isIframe', type: 'switch', span: switchSpan },
|
||||
{ label: '显示徽章', key: 'showBadge', type: 'switch', span: switchSpan },
|
||||
{ label: '固定标签', key: 'fixedTab', type: 'switch', span: switchSpan },
|
||||
{ label: '标签隐藏', key: 'isHideTab', type: 'switch', span: switchSpan },
|
||||
{ label: '全屏页面', key: 'isFullPage', type: 'switch', span: switchSpan }
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
...baseItems,
|
||||
{
|
||||
label: '权限名称',
|
||||
key: 'authName',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:新增、编辑、删除' }
|
||||
},
|
||||
{
|
||||
label: '权限标识',
|
||||
key: 'authLabel',
|
||||
type: 'input',
|
||||
props: { placeholder: '如:add、edit、delete' }
|
||||
},
|
||||
{
|
||||
label: '权限排序',
|
||||
key: 'authSort',
|
||||
type: 'number',
|
||||
props: { min: 1, controlsPosition: 'right', style: { width: '100%' } }
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const type = form.menuType === 'menu' ? '菜单' : '按钮'
|
||||
return isEdit.value ? `编辑${type}` : `新建${type}`
|
||||
})
|
||||
|
||||
/**
|
||||
* 是否禁用菜单类型切换
|
||||
*/
|
||||
const disableMenuType = computed(() => {
|
||||
if (isEdit.value) return true
|
||||
if (!isEdit.value && form.menuType === 'menu' && props.lockType) return true
|
||||
return false
|
||||
})
|
||||
|
||||
/**
|
||||
* 重置表单数据
|
||||
*/
|
||||
const resetForm = (): void => {
|
||||
formRef.value?.reset()
|
||||
form.menuType = 'menu'
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载表单数据(编辑模式)
|
||||
*/
|
||||
const loadFormData = (): void => {
|
||||
if (!props.editData) return
|
||||
|
||||
isEdit.value = true
|
||||
|
||||
if (form.menuType === 'menu') {
|
||||
const row = props.editData
|
||||
form.id = row.id || 0
|
||||
form.name = formatMenuTitle(row.meta?.title || '')
|
||||
form.path = row.path || ''
|
||||
form.label = row.name || ''
|
||||
form.component = row.component || ''
|
||||
form.icon = row.meta?.icon || ''
|
||||
form.sort = row.meta?.sort || 1
|
||||
form.isMenu = row.meta?.isMenu ?? true
|
||||
form.keepAlive = row.meta?.keepAlive ?? false
|
||||
form.isHide = row.meta?.isHide ?? false
|
||||
form.isHideTab = row.meta?.isHideTab ?? false
|
||||
form.isEnable = row.meta?.isEnable ?? true
|
||||
form.link = row.meta?.link || ''
|
||||
form.isIframe = row.meta?.isIframe ?? false
|
||||
form.showBadge = row.meta?.showBadge ?? false
|
||||
form.showTextBadge = row.meta?.showTextBadge || ''
|
||||
form.fixedTab = row.meta?.fixedTab ?? false
|
||||
form.activePath = row.meta?.activePath || ''
|
||||
form.roles = row.meta?.roles || []
|
||||
form.isFullPage = row.meta?.isFullPage ?? false
|
||||
} else {
|
||||
const row = props.editData
|
||||
form.authName = row.title || ''
|
||||
form.authLabel = row.authMark || ''
|
||||
form.authIcon = row.icon || ''
|
||||
form.authSort = row.sort || 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
emit('submit', { ...form })
|
||||
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
|
||||
handleCancel()
|
||||
} catch {
|
||||
ElMessage.error('表单校验失败,请检查输入')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消操作
|
||||
*/
|
||||
const handleCancel = (): void => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框关闭后的回调
|
||||
*/
|
||||
const handleClosed = (): void => {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听对话框显示状态
|
||||
*/
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
form.menuType = props.type
|
||||
nextTick(() => {
|
||||
if (props.editData) {
|
||||
loadFormData()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 监听菜单类型变化
|
||||
*/
|
||||
watch(
|
||||
() => props.type,
|
||||
(newType) => {
|
||||
if (props.visible) {
|
||||
form.menuType = newType
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,240 @@
|
||||
<!-- 角色管理页面 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<RoleSearch
|
||||
v-show="showSearchBar"
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></RoleSearch>
|
||||
|
||||
<ElCard class="art-table-card" :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 '@/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
|
||||
type RoleSearchFormParams = Api.SystemManage.RoleSearchParams & {
|
||||
daterange?: string[]
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref<RoleSearchFormParams>({
|
||||
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,
|
||||
replaceSearchParams,
|
||||
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: RoleSearchFormParams) => {
|
||||
// 处理日期区间参数,把 daterange 转换为 startTime 和 endTime
|
||||
const { daterange, ...filtersParams } = params
|
||||
const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null]
|
||||
|
||||
replaceSearchParams({ ...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,125 @@
|
||||
<template>
|
||||
<ArtSearchBar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:items="formItems"
|
||||
:rules="rules"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
>
|
||||
</ArtSearchBar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type RoleSearchFormParams = Api.SystemManage.RoleSearchParams & {
|
||||
daterange?: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: RoleSearchFormParams
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: RoleSearchFormParams): void
|
||||
(e: 'search', params: RoleSearchFormParams): 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 (params: RoleSearchFormParams) => {
|
||||
await searchBarRef.value.validate()
|
||||
emit('search', params)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,247 @@
|
||||
<!-- 个人中心页面 -->
|
||||
<template>
|
||||
<div class="w-full h-full p-0 bg-transparent border-none shadow-none">
|
||||
<div class="relative flex-b mt-2.5 max-md:block max-md:mt-1">
|
||||
<div class="w-112 mr-5 max-md:w-full max-md:mr-0">
|
||||
<div class="art-card-sm relative p-9 pb-6 overflow-hidden text-center">
|
||||
<img class="absolute top-0 left-0 w-full h-50 object-cover" src="@imgs/user/bg.webp" />
|
||||
<img
|
||||
class="relative z-10 w-20 h-20 mt-30 mx-auto object-cover border-2 border-white rounded-full"
|
||||
src="@imgs/user/avatar.webp"
|
||||
/>
|
||||
<h2 class="mt-5 text-xl font-normal">{{ userInfo.userName }}</h2>
|
||||
<p class="mt-5 text-sm">专注于用户体验跟视觉设计</p>
|
||||
|
||||
<div class="w-75 mx-auto mt-7.5 text-left">
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:mail-line" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">jdkjjfnndf@mall.com</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:user-3-line" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">交互专家</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:map-pin-line" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">广东省深圳市</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:dribbble-fill" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">字节跳动-某某平台部-UED</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
<h3 class="text-sm font-medium">标签</h3>
|
||||
<div class="flex flex-wrap justify-center mt-3.5">
|
||||
<div
|
||||
v-for="item in lableList"
|
||||
:key="item"
|
||||
class="py-1 px-1.5 mr-2.5 mb-2.5 text-xs border border-g-300 rounded"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden max-md:w-full max-md:mt-3.5">
|
||||
<div class="art-card-sm">
|
||||
<h1 class="p-4 text-xl font-normal border-b border-g-300">基本设置</h1>
|
||||
|
||||
<ElForm
|
||||
:model="form"
|
||||
class="box-border p-5 [&>.el-row_.el-form-item]:w-[calc(50%-10px)] [&>.el-row_.el-input]:w-full [&>.el-row_.el-select]:w-full"
|
||||
ref="ruleFormRef"
|
||||
:rules="rules"
|
||||
label-width="86px"
|
||||
label-position="top"
|
||||
>
|
||||
<ElRow>
|
||||
<ElFormItem label="姓名" prop="realName">
|
||||
<ElInput v-model="form.realName" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="sex" class="ml-5">
|
||||
<ElSelect v-model="form.sex" placeholder="Select" :disabled="!isEdit">
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElRow>
|
||||
<ElFormItem label="昵称" prop="nikeName">
|
||||
<ElInput v-model="form.nikeName" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="邮箱" prop="email" class="ml-5">
|
||||
<ElInput v-model="form.email" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElRow>
|
||||
<ElFormItem label="手机" prop="mobile">
|
||||
<ElInput v-model="form.mobile" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="地址" prop="address" class="ml-5">
|
||||
<ElInput v-model="form.address" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElFormItem label="个人介绍" prop="des" class="h-32">
|
||||
<ElInput type="textarea" :rows="4" v-model="form.des" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
|
||||
<div class="flex-c justify-end [&_.el-button]:!w-27.5">
|
||||
<ElButton type="primary" class="w-22.5" v-ripple @click="edit">
|
||||
{{ isEdit ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
|
||||
<div class="art-card-sm my-5">
|
||||
<h1 class="p-4 text-xl font-normal border-b border-g-300">更改密码</h1>
|
||||
|
||||
<ElForm :model="pwdForm" class="box-border p-5" label-width="86px" label-position="top">
|
||||
<ElFormItem label="当前密码" prop="password">
|
||||
<ElInput
|
||||
v-model="pwdForm.password"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="新密码" prop="newPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.newPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="确认新密码" prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.confirmPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="flex-c justify-end [&_.el-button]:!w-27.5">
|
||||
<ElButton type="primary" class="w-22.5" v-ripple @click="editPwd">
|
||||
{{ isEditPwd ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'UserCenter' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.getUserInfo)
|
||||
|
||||
const isEdit = ref(false)
|
||||
const isEditPwd = ref(false)
|
||||
const date = ref('')
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 用户信息表单
|
||||
*/
|
||||
const form = reactive({
|
||||
realName: 'John Snow',
|
||||
nikeName: '皮卡丘',
|
||||
email: '59301283@mall.com',
|
||||
mobile: '18888888888',
|
||||
address: '广东省深圳市宝安区西乡街道101栋201',
|
||||
sex: '2',
|
||||
des: 'Art Design Pro 是一款兼具设计美学与高效开发的后台系统.'
|
||||
})
|
||||
|
||||
/**
|
||||
* 密码修改表单
|
||||
*/
|
||||
const pwdForm = reactive({
|
||||
password: '123456',
|
||||
newPassword: '123456',
|
||||
confirmPassword: '123456'
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
realName: [
|
||||
{ required: true, message: '请输入姓名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
nikeName: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
|
||||
mobile: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
|
||||
address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '请选择性别', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 性别选项
|
||||
*/
|
||||
const options = [
|
||||
{ value: '1', label: '男' },
|
||||
{ value: '2', label: '女' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 用户标签列表
|
||||
*/
|
||||
const lableList: Array<string> = ['专注设计', '很有想法', '辣~', '大长腿', '川妹子', '海纳百川']
|
||||
|
||||
onMounted(() => {
|
||||
getDate()
|
||||
})
|
||||
|
||||
/**
|
||||
* 根据当前时间获取问候语
|
||||
*/
|
||||
const getDate = () => {
|
||||
const h = new Date().getHours()
|
||||
|
||||
if (h >= 6 && h < 9) date.value = '早上好'
|
||||
else if (h >= 9 && h < 11) date.value = '上午好'
|
||||
else if (h >= 11 && h < 13) date.value = '中午好'
|
||||
else if (h >= 13 && h < 18) date.value = '下午好'
|
||||
else if (h >= 18 && h < 24) date.value = '晚上好'
|
||||
else date.value = '很晚了,早点睡'
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换用户信息编辑状态
|
||||
*/
|
||||
const edit = () => {
|
||||
isEdit.value = !isEdit.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换密码编辑状态
|
||||
*/
|
||||
const editPwd = () => {
|
||||
isEditPwd.value = !isEditPwd.value
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,259 @@
|
||||
<!-- 用户管理页面 -->
|
||||
<!-- art-full-height 自动计算出页面剩余高度 -->
|
||||
<!-- art-table-card 一个符合系统样式的 class,同时自动撑满剩余高度 -->
|
||||
<!-- 更多 useTable 使用示例请移步至 功能示例 下面的高级表格示例或者查看官方文档 -->
|
||||
<!-- useTable 文档:https://www.artd.pro/docs/zh/guide/hooks/use-table.html -->
|
||||
<template>
|
||||
<div class="user-page art-full-height">
|
||||
<!-- 搜索栏 -->
|
||||
<UserSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></UserSearch>
|
||||
|
||||
<ElCard class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :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"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 用户弹窗 -->
|
||||
<UserDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:type="dialogType"
|
||||
:user-data="currentUserData"
|
||||
@submit="handleDialogSubmit"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
|
||||
import { ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { fetchGetUserList } from '@/api/system-manage'
|
||||
import UserSearch from './modules/user-search.vue'
|
||||
import UserDialog from './modules/user-dialog.vue'
|
||||
import { ElTag, ElMessageBox, ElImage } from 'element-plus'
|
||||
import { DialogType } from '@/types'
|
||||
|
||||
defineOptions({ name: 'User' })
|
||||
|
||||
type UserListItem = Api.SystemManage.UserListItem
|
||||
|
||||
// 弹窗相关
|
||||
const dialogType = ref<DialogType>('add')
|
||||
const dialogVisible = ref(false)
|
||||
const currentUserData = ref<Partial<UserListItem>>({})
|
||||
|
||||
// 选中行
|
||||
const selectedRows = ref<UserListItem[]>([])
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
userName: undefined,
|
||||
userGender: undefined,
|
||||
userPhone: undefined,
|
||||
userEmail: undefined,
|
||||
status: '1'
|
||||
})
|
||||
|
||||
// 用户状态配置
|
||||
const USER_STATUS_CONFIG = {
|
||||
'1': { type: 'success' as const, text: '在线' },
|
||||
'2': { type: 'info' as const, text: '离线' },
|
||||
'3': { type: 'warning' as const, text: '异常' },
|
||||
'4': { type: 'danger' as const, text: '注销' }
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 获取用户状态配置
|
||||
*/
|
||||
const getUserStatusConfig = (status: string) => {
|
||||
return (
|
||||
USER_STATUS_CONFIG[status as keyof typeof USER_STATUS_CONFIG] || {
|
||||
type: 'info' as const,
|
||||
text: '未知'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
replaceSearchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
// 核心配置
|
||||
core: {
|
||||
apiFn: fetchGetUserList,
|
||||
apiParams: {
|
||||
current: 1,
|
||||
size: 20,
|
||||
...searchForm.value
|
||||
},
|
||||
// 自定义分页字段映射,未设置时将使用全局配置 tableConfig.ts 中的 paginationKey
|
||||
// paginationKey: {
|
||||
// current: 'pageNum',
|
||||
// size: 'pageSize'
|
||||
// },
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' }, // 勾选列
|
||||
{ type: 'index', width: 60, label: '序号' }, // 序号
|
||||
{
|
||||
prop: 'userInfo',
|
||||
label: '用户名',
|
||||
width: 280,
|
||||
// visible: false, // 默认是否显示列
|
||||
formatter: (row) => {
|
||||
return h('div', { class: 'user flex-c' }, [
|
||||
h(ElImage, {
|
||||
class: 'size-9.5 rounded-md',
|
||||
src: row.avatar,
|
||||
previewSrcList: [row.avatar],
|
||||
// 图片预览是否插入至 body 元素上,用于解决表格内部图片预览样式异常
|
||||
previewTeleported: true
|
||||
}),
|
||||
h('div', { class: 'ml-2' }, [
|
||||
h('p', { class: 'user-name' }, row.userName),
|
||||
h('p', { class: 'email' }, row.userEmail)
|
||||
])
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'userGender',
|
||||
label: '性别',
|
||||
sortable: true,
|
||||
formatter: (row) => row.userGender
|
||||
},
|
||||
{ prop: 'userPhone', label: '手机号' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: (row) => {
|
||||
const statusConfig = getUserStatusConfig(row.status)
|
||||
return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建日期',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
fixed: 'right', // 固定列
|
||||
formatter: (row) =>
|
||||
h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteUser(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
]
|
||||
},
|
||||
// 数据处理
|
||||
transform: {
|
||||
// 数据转换器 - 替换头像
|
||||
dataTransformer: (records) => {
|
||||
// 类型守卫检查
|
||||
if (!Array.isArray(records)) {
|
||||
console.warn('数据转换器: 期望数组类型,实际收到:', typeof records)
|
||||
return []
|
||||
}
|
||||
|
||||
// 使用本地头像替换接口返回的头像
|
||||
return records.map((item, index: number) => {
|
||||
return {
|
||||
...item,
|
||||
avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 搜索处理
|
||||
* @param params 参数
|
||||
*/
|
||||
const handleSearch = (params: Api.SystemManage.UserSearchParams) => {
|
||||
replaceSearchParams(params)
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示用户弹窗
|
||||
*/
|
||||
const showDialog = (type: DialogType, row?: UserListItem): void => {
|
||||
console.log('打开弹窗:', { type, row })
|
||||
dialogType.value = type
|
||||
currentUserData.value = row || {}
|
||||
nextTick(() => {
|
||||
dialogVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
const deleteUser = (row: UserListItem): void => {
|
||||
console.log('删除用户:', row)
|
||||
ElMessageBox.confirm(`确定要注销该用户吗?`, '注销用户', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('注销成功')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理弹窗提交事件
|
||||
*/
|
||||
const handleDialogSubmit = async () => {
|
||||
try {
|
||||
dialogVisible.value = false
|
||||
currentUserData.value = {}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理表格行选择变化
|
||||
*/
|
||||
const handleSelectionChange = (selection: UserListItem[]): void => {
|
||||
selectedRows.value = selection
|
||||
console.log('选中行数据:', selectedRows.value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '添加用户' : '编辑用户'"
|
||||
width="30%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||
<ElFormItem label="用户名" prop="username">
|
||||
<ElInput v-model="formData.username" placeholder="请输入用户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="gender">
|
||||
<ElSelect v-model="formData.gender">
|
||||
<ElOption label="男" value="男" />
|
||||
<ElOption label="女" value="女" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色" prop="role">
|
||||
<ElSelect v-model="formData.role" multiple>
|
||||
<ElOption
|
||||
v-for="role in roleList"
|
||||
:key="role.roleCode"
|
||||
:value="role.roleCode"
|
||||
:label="role.roleName"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ROLE_LIST_DATA } from '@/mock/temp/formData'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
type: string
|
||||
userData?: Partial<Api.SystemManage.UserListItem>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'submit'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 角色列表数据
|
||||
const roleList = ref(ROLE_LIST_DATA)
|
||||
|
||||
// 对话框显示控制
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const dialogType = computed(() => props.type)
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
phone: '',
|
||||
gender: '男',
|
||||
role: [] as string[]
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
* 根据对话框类型(新增/编辑)填充表单
|
||||
*/
|
||||
const initFormData = () => {
|
||||
const isEdit = props.type === 'edit' && props.userData
|
||||
const row = props.userData
|
||||
|
||||
Object.assign(formData, {
|
||||
username: isEdit && row ? row.userName || '' : '',
|
||||
phone: isEdit && row ? row.userPhone || '' : '',
|
||||
gender: isEdit && row ? row.userGender || '男' : '男',
|
||||
role: isEdit && row ? (Array.isArray(row.userRoles) ? row.userRoles : []) : []
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听对话框状态变化
|
||||
* 当对话框打开时初始化表单数据并清除验证状态
|
||||
*/
|
||||
watch(
|
||||
() => [props.visible, props.type, props.userData],
|
||||
([visible]) => {
|
||||
if (visible) {
|
||||
initFormData()
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
* 验证通过后触发提交事件
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
emit('submit')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<ArtSearchBar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:items="formItems"
|
||||
:rules="rules"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
>
|
||||
</ArtSearchBar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Api.SystemManage.UserSearchParams
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Api.SystemManage.UserSearchParams): void
|
||||
(e: 'search', params: Api.SystemManage.UserSearchParams): 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 = {
|
||||
// userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 动态 options
|
||||
const statusOptions = ref<{ label: string; value: string; disabled?: boolean }[]>([])
|
||||
|
||||
// 模拟接口返回状态数据
|
||||
function fetchStatusOptions(): Promise<typeof statusOptions.value> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{ label: '在线', value: '1' },
|
||||
{ label: '离线', value: '2' },
|
||||
{ label: '异常', value: '3' },
|
||||
{ label: '注销', value: '4' }
|
||||
])
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
statusOptions.value = await fetchStatusOptions()
|
||||
})
|
||||
|
||||
// 表单配置
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
label: '用户名',
|
||||
key: 'userName',
|
||||
type: 'input',
|
||||
placeholder: '请输入用户名',
|
||||
clearable: true
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
key: 'userPhone',
|
||||
type: 'input',
|
||||
props: { placeholder: '请输入手机号', maxlength: '11' }
|
||||
},
|
||||
{
|
||||
label: '邮箱',
|
||||
key: 'userEmail',
|
||||
type: 'input',
|
||||
props: { placeholder: '请输入邮箱' }
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
key: 'status',
|
||||
type: 'select',
|
||||
props: {
|
||||
placeholder: '请选择状态',
|
||||
options: statusOptions.value
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
key: 'userGender',
|
||||
type: 'radiogroup',
|
||||
props: {
|
||||
options: [
|
||||
{ label: '男', value: '1' },
|
||||
{ label: '女', value: '2' }
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 事件
|
||||
function handleReset() {
|
||||
console.log('重置表单')
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
async function handleSearch(params: Api.SystemManage.UserSearchParams) {
|
||||
await searchBarRef.value.validate()
|
||||
emit('search', params)
|
||||
console.log('表单数据', params)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user