Merge branch 'develop'
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="menu-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||
<template #left>
|
||||
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
|
||||
<ElButton v-auth="'add'" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
|
||||
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton> -->
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="path"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredTableData"
|
||||
:marginTop="10"
|
||||
:stripe="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
|
||||
<ElFormItem label="菜单类型">
|
||||
<ElRadioGroup v-model="labelPosition" :disabled="disableMenuType">
|
||||
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
|
||||
<ElRadioButton value="button" label="button">权限</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<template v-if="labelPosition === 'menu'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="菜单名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="路由地址" prop="path">
|
||||
<ElInput v-model="form.path" placeholder="路由地址"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="label">
|
||||
<ElInput v-model="form.label" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="图标" prop="icon">
|
||||
<ArtIconSelector v-model="form.icon" :iconType="iconType" width="100%" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单排序" prop="sort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.sort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="外部链接" prop="link">
|
||||
<ElInput
|
||||
v-model="form.link"
|
||||
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
|
||||
></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否启用" prop="isEnable">
|
||||
<ElSwitch v-model="form.isEnable"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="页面缓存" prop="keepAlive">
|
||||
<ElSwitch v-model="form.keepAlive"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否显示" prop="isHidden">
|
||||
<ElSwitch v-model="form.isHidden"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否内嵌" prop="isMenu">
|
||||
<ElSwitch v-model="form.isIframe"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<template v-if="labelPosition === 'button'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限名称" prop="authName">
|
||||
<ElInput v-model="form.authName" placeholder="权限名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="authLabel">
|
||||
<ElInput v-model="form.authLabel" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限排序" prop="authSort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.authSort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { IconTypeEnum } from '@/enums/appEnum'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { ElPopover, ElButton } from 'element-plus'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'Menus' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
route: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 增加实际应用的搜索条件状态
|
||||
const appliedFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
Object.assign(appliedFilters, { ...initialSearchState })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 将当前输入的筛选条件应用到实际搜索
|
||||
Object.assign(appliedFilters, { ...formFilters })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '菜单名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '路由地址',
|
||||
prop: 'route',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 构建菜单类型标签
|
||||
const buildMenuTypeTag = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return 'info'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return 'success'
|
||||
} else if (row.path) {
|
||||
return 'primary'
|
||||
} else if (row.meta?.link) {
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单类型文本
|
||||
const buildMenuTypeText = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return '目录'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return '内嵌'
|
||||
} else if (row.path) {
|
||||
return '菜单'
|
||||
} else if (row.meta?.link) {
|
||||
return '外链'
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'meta.title',
|
||||
label: '菜单名称',
|
||||
minWidth: 120,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return formatMenuTitle(row.meta?.title)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return row.meta?.link || row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'meta.authList',
|
||||
label: '可操作权限',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
|
||||
return h(
|
||||
ElPopover,
|
||||
{
|
||||
placement: 'top-start',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
trigger: 'click',
|
||||
key: index
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h('div', { style: 'margin: 0; text-align: right' }, [
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => showModel('button', item)
|
||||
},
|
||||
{ default: () => '编辑' }
|
||||
),
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'danger',
|
||||
onClick: () => deleteAuth()
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
)
|
||||
]),
|
||||
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'date',
|
||||
label: '编辑时间',
|
||||
formatter: () => '2022-3-12 12:00:00'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '隐藏菜单',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
|
||||
row.meta.isHide ? '是' : '否'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h('div', [
|
||||
// 这里写两组权限标识判断是为了方便演示,在实际开发中可以删除其中一组
|
||||
// 前端模式权限标识
|
||||
hasAuth('B_CODE1') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => showModel('menu')
|
||||
}),
|
||||
hasAuth('B_CODE2') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
hasAuth('B_CODE3') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteMenu()
|
||||
}),
|
||||
// 后端模式权限标识
|
||||
hasAuth('add') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => showModel('menu')
|
||||
}),
|
||||
hasAuth('edit') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
hasAuth('delete') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteMenu()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const form = reactive({
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
isEnable: true,
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限 (修改这部分)
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
const iconType = ref(IconTypeEnum.UNICODE)
|
||||
|
||||
const labelPosition = ref('menu')
|
||||
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 tableData = ref<AppRouteRecord[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
const getTableData = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
tableData.value = menuList.value
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 过滤后的表格数据
|
||||
const filteredTableData = computed(() => {
|
||||
// 递归搜索函数
|
||||
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
return items.filter((item) => {
|
||||
// 获取搜索关键词,转换为小写并去除首尾空格
|
||||
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
|
||||
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
|
||||
|
||||
// 获取菜单标题和路径,确保它们存在
|
||||
const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
|
||||
const menuPath = (item.path || '').toLowerCase()
|
||||
|
||||
// 使用 includes 进行模糊匹配
|
||||
const nameMatch = !searchName || menuTitle.includes(searchName)
|
||||
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
|
||||
|
||||
// 如果有子菜单,递归搜索
|
||||
if (item.children && item.children.length > 0) {
|
||||
const matchedChildren = searchMenu(item.children)
|
||||
// 如果子菜单有匹配项,保留当前菜单
|
||||
if (matchedChildren.length > 0) {
|
||||
item.children = matchedChildren
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return nameMatch && routeMatch
|
||||
})
|
||||
}
|
||||
|
||||
return searchMenu(tableData.value)
|
||||
})
|
||||
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogTitle = computed(() => {
|
||||
const type = labelPosition.value === 'menu' ? '菜单' : '权限'
|
||||
return isEdit.value ? `编辑${type}` : `新建${type}`
|
||||
})
|
||||
|
||||
const showDialog = (type: string, row: AppRouteRecord) => {
|
||||
showModel('menu', row, true)
|
||||
}
|
||||
|
||||
const handleChange = () => {}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// const menuStore = useMenuStore()
|
||||
// const params =
|
||||
// labelPosition.value === 'menu'
|
||||
// ? {
|
||||
// title: form.name,
|
||||
// path: form.path,
|
||||
// name: form.label,
|
||||
// icon: form.icon,
|
||||
// sort: form.sort,
|
||||
// isEnable: form.isEnable,
|
||||
// isMenu: form.isMenu,
|
||||
// keepAlive: form.keepAlive,
|
||||
// isHidden: form.isHidden,
|
||||
// link: form.link
|
||||
// }
|
||||
// : {
|
||||
// title: form.authName,
|
||||
// name: form.authLabel,
|
||||
// icon: form.authIcon,
|
||||
// sort: form.authSort
|
||||
// }
|
||||
|
||||
if (isEdit.value) {
|
||||
// await menuStore.updateMenu(params)
|
||||
} else {
|
||||
// await menuStore.addMenu(params)
|
||||
}
|
||||
|
||||
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
|
||||
dialogVisible.value = false
|
||||
} catch {
|
||||
ElMessage.error(`${isEdit.value ? '编辑' : '新增'}失败`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showModel = (type: string, row?: any, lock: boolean = false) => {
|
||||
dialogVisible.value = true
|
||||
labelPosition.value = type
|
||||
isEdit.value = false
|
||||
lockMenuType.value = lock
|
||||
resetForm()
|
||||
|
||||
if (row) {
|
||||
isEdit.value = true
|
||||
nextTick(() => {
|
||||
// 回显数据
|
||||
if (type === 'menu') {
|
||||
// 菜单数据回显
|
||||
form.name = formatMenuTitle(row.meta.title)
|
||||
form.path = row.path
|
||||
form.label = row.name
|
||||
form.icon = row.meta.icon
|
||||
form.sort = row.meta.sort || 1
|
||||
form.isMenu = row.meta.isMenu
|
||||
form.keepAlive = row.meta.keepAlive
|
||||
form.isHidden = row.meta.isHidden || true
|
||||
form.isEnable = row.meta.isEnable || true
|
||||
form.link = row.meta.link
|
||||
form.isIframe = row.meta.isIframe || false
|
||||
} else {
|
||||
// 权限按钮数据回显
|
||||
form.authName = row.title
|
||||
form.authLabel = row.auth_mark
|
||||
form.authIcon = row.icon || ''
|
||||
form.authSort = row.sort || 1
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, {
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
}
|
||||
|
||||
const deleteMenu = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAuth = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改计算属性,增加锁定控制参数
|
||||
const disableMenuType = computed(() => {
|
||||
// 编辑权限时锁定为权限类型
|
||||
if (isEdit.value && labelPosition.value === 'button') return true
|
||||
// 编辑菜单时锁定为菜单类型
|
||||
if (isEdit.value && labelPosition.value === 'menu') return true
|
||||
// 顶部添加菜单按钮时锁定为菜单类型
|
||||
if (!isEdit.value && labelPosition.value === 'menu' && lockMenuType.value) return true
|
||||
return false
|
||||
})
|
||||
|
||||
// 添加一个控制变量
|
||||
const lockMenuType = ref(false)
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-page {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
:deep(.small-btn) {
|
||||
height: 30px !important;
|
||||
padding: 0 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-2-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-3-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-3-2-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,367 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="部门名称"></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple>搜索</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable ref="tableRef" row-key="id" :loading="loading" :data="roleList" :currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize" :total="pagination.total" @size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange">
|
||||
<template #default>
|
||||
<ElTableColumn label="角色名称" prop="name" />
|
||||
<ElTableColumn label="角色编码" prop="id" />
|
||||
<ElTableColumn label="描述" prop="desc" />
|
||||
<ElTableColumn label="启用" prop="enable">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at">
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showPermissionDialog()"> 菜单权限 </el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)"> 编辑 </el-button>
|
||||
<el-button link @click="deleteRole()"> 删除 </el-button>
|
||||
<!-- <ArtButtonMore
|
||||
:list="[
|
||||
{ key: 'permission', label: '菜单权限' },
|
||||
{ key: 'edit', label: '编辑角色' },
|
||||
{ key: 'delete', label: '删除角色' }
|
||||
]"
|
||||
@click="buttonMoreClick($event, scope.row)"
|
||||
/> -->
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||
width="30%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="角色名称" prop="roleName">
|
||||
<ElInput v-model="form.roleName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色编码" prop="roleCode">
|
||||
<ElInput v-model="form.roleCode" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="roleStatus">
|
||||
<ElInput v-model="form.des" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启用">
|
||||
<ElSwitch v-model="form.enable" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog
|
||||
v-model="permissionDialog"
|
||||
title="菜单权限"
|
||||
width="520px"
|
||||
align-center
|
||||
class="el-dialog-border"
|
||||
>
|
||||
<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>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
|
||||
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
|
||||
isSelectAll ? '取消全选' : '全部选择'
|
||||
}}</ElButton>
|
||||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
|
||||
import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
|
||||
const loading = ref(false)
|
||||
defineOptions({ name: 'Role' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const permissionDialog = ref(false)
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
const treeRef = ref()
|
||||
const isExpandAll = ref(true)
|
||||
const isSelectAll = ref(false)
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
// 处理菜单数据,将 authList 转换为子节点
|
||||
const processedMenuList = computed(() => {
|
||||
const processNode = (node: any) => {
|
||||
const processed = { ...node }
|
||||
|
||||
// 如果有 authList,将其转换为子节点
|
||||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||
id: `${node.id}_${auth.auth_mark}`,
|
||||
name: `${node.name}_${auth.auth_mark}`,
|
||||
label: auth.title,
|
||||
auth_mark: auth.auth_mark,
|
||||
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.map(processNode)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const form = reactive<Role>({
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
des: '',
|
||||
date: '',
|
||||
enable: true
|
||||
})
|
||||
|
||||
const roleList = ref<Role[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// const getTableData = () => {
|
||||
// roleList.value = ROLE_LIST_DATA
|
||||
// }
|
||||
// 获取列表数据
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { records, current, size, total } = await UserService.getRoleList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
roleList.value = records.map((item: any, index: number) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.roleName = row.roleName
|
||||
form.roleCode = row.roleCode
|
||||
form.des = row.des
|
||||
form.date = row.date
|
||||
form.enable = row.enable
|
||||
} else {
|
||||
form.roleName = ''
|
||||
form.roleCode = ''
|
||||
form.des = ''
|
||||
form.date = ''
|
||||
form.enable = true
|
||||
}
|
||||
}
|
||||
|
||||
const showPermissionDialog = () => {
|
||||
permissionDialog.value = true
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
|
||||
}
|
||||
|
||||
const deleteRole = () => {
|
||||
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
const message = dialogType.value === 'add' ? '新增成功' : '修改成功'
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const savePermission = () => {
|
||||
ElMessage.success('权限保存成功')
|
||||
permissionDialog.value = false
|
||||
}
|
||||
|
||||
const toggleExpandAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用store.nodesMap直接控制所有节点的展开状态
|
||||
const nodes = tree.store.nodesMap
|
||||
for (const node in nodes) {
|
||||
nodes[node].expanded = !isExpandAll.value
|
||||
}
|
||||
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
if (!isSelectAll.value) {
|
||||
// 全选:获取所有节点的key并设置为选中
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||
tree.setCheckedKeys(allKeys)
|
||||
} else {
|
||||
// 取消全选:清空所有选中
|
||||
tree.setCheckedKeys([])
|
||||
}
|
||||
|
||||
isSelectAll.value = !isSelectAll.value
|
||||
}
|
||||
|
||||
const getAllNodeKeys = (nodes: any[]): string[] => {
|
||||
const keys: string[] = []
|
||||
const traverse = (nodeList: any[]) => {
|
||||
nodeList.forEach((node) => {
|
||||
if (node.name) {
|
||||
keys.push(node.name)
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
return keys
|
||||
}
|
||||
|
||||
const handleTreeCheck = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||||
const checkedKeys = tree.getCheckedKeys()
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||
|
||||
// 判断是否全选:选中的节点数量等于总节点数量
|
||||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date)
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<div class="page-content user">
|
||||
<div class="content">
|
||||
<div class="left-wrap">
|
||||
<div class="user-wrap box-style">
|
||||
<img class="bg" src="@imgs/user/bg.webp" />
|
||||
<img class="avatar" src="@imgs/user/avatar.webp" />
|
||||
<h2 class="name">{{ userInfo.userName }}</h2>
|
||||
<p class="des">Art Design Pro 是一款漂亮的后台管理系统模版.</p>
|
||||
|
||||
<div class="outer-info">
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>jdkjjfnndf@mall.com</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>交互专家</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>广东省深圳市</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>字节跳动-某某平台部-UED</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lables">
|
||||
<h3>标签</h3>
|
||||
<div>
|
||||
<div v-for="item in lableList" :key="item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <el-carousel class="gallery" height="160px"
|
||||
:interval="5000"
|
||||
indicator-position="none"
|
||||
>
|
||||
<el-carousel-item class="item" v-for="item in galleryList" :key="item">
|
||||
<img :src="item"/>
|
||||
</el-carousel-item>
|
||||
</el-carousel> -->
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<div class="info box-style">
|
||||
<h1 class="title">基本设置</h1>
|
||||
|
||||
<ElForm
|
||||
:model="form"
|
||||
class="form"
|
||||
ref="ruleFormRef"
|
||||
:rules="rules"
|
||||
label-width="86px"
|
||||
label-position="top"
|
||||
>
|
||||
<ElRow>
|
||||
<ElFormItem label="姓名" prop="realName">
|
||||
<el-input v-model="form.realName" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="sex" class="right-input">
|
||||
<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="right-input">
|
||||
<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="right-input">
|
||||
<ElInput v-model="form.address" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElFormItem label="个人介绍" prop="des" :style="{ height: '130px' }">
|
||||
<ElInput type="textarea" :rows="4" v-model="form.des" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
|
||||
<div class="el-form-item-right">
|
||||
<ElButton type="primary" style="width: 90px" v-ripple @click="edit">
|
||||
{{ isEdit ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
|
||||
<div class="info box-style" style="margin-top: 20px">
|
||||
<h1 class="title">更改密码</h1>
|
||||
|
||||
<ElForm :model="pwdForm" class="form" 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="el-form-item-right">
|
||||
<ElButton type="primary" style="width: 90px" v-ripple @click="editPwd">
|
||||
{{ isEditPwd ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ElForm, 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 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 ruleFormRef = ref<FormInstance>()
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
realName: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
|
||||
],
|
||||
nikeName: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
|
||||
mobile: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
|
||||
address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
||||
sex: [{ type: 'array', required: true, message: '请选择性别', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: '1',
|
||||
label: '男'
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '女'
|
||||
}
|
||||
]
|
||||
|
||||
const lableList: Array<string> = ['专注设计', '很有想法', '辣~', '大长腿', '川妹子', '海纳百川']
|
||||
|
||||
onMounted(() => {
|
||||
getDate()
|
||||
})
|
||||
|
||||
const getDate = () => {
|
||||
const d = new Date()
|
||||
const h = d.getHours()
|
||||
let text = ''
|
||||
|
||||
if (h >= 6 && h < 9) {
|
||||
text = '早上好'
|
||||
} else if (h >= 9 && h < 11) {
|
||||
text = '上午好'
|
||||
} else if (h >= 11 && h < 13) {
|
||||
text = '中午好'
|
||||
} else if (h >= 13 && h < 18) {
|
||||
text = '下午好'
|
||||
} else if (h >= 18 && h < 24) {
|
||||
text = '晚上好'
|
||||
} else if (h >= 0 && h < 6) {
|
||||
text = '很晚了,早点睡'
|
||||
}
|
||||
|
||||
date.value = text
|
||||
}
|
||||
|
||||
const edit = () => {
|
||||
isEdit.value = !isEdit.value
|
||||
}
|
||||
|
||||
const editPwd = () => {
|
||||
isEditPwd.value = !isEditPwd.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.user {
|
||||
.icon {
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
$box-radius: calc(var(--custom-radius) + 4px);
|
||||
|
||||
.box-style {
|
||||
border: 1px solid var(--art-border-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
|
||||
.left-wrap {
|
||||
width: 450px;
|
||||
margin-right: 25px;
|
||||
|
||||
.user-wrap {
|
||||
position: relative;
|
||||
height: 600px;
|
||||
padding: 35px 40px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: $box-radius;
|
||||
|
||||
.bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-top: 120px;
|
||||
object-fit: cover;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.des {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.outer-info {
|
||||
width: 300px;
|
||||
margin: auto;
|
||||
margin-top: 30px;
|
||||
text-align: left;
|
||||
|
||||
> div {
|
||||
margin-top: 10px;
|
||||
|
||||
span {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lables {
|
||||
margin-top: 40px;
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
|
||||
> div {
|
||||
padding: 3px 6px;
|
||||
margin: 0 10px 10px 0;
|
||||
font-size: 12px;
|
||||
background: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery {
|
||||
margin-top: 25px;
|
||||
border-radius: 10px;
|
||||
|
||||
.item {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: $box-radius;
|
||||
|
||||
.info {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: $box-radius;
|
||||
|
||||
.title {
|
||||
padding: 15px 25px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: var(--art-text-gray-800);
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
}
|
||||
|
||||
.form {
|
||||
box-sizing: border-box;
|
||||
padding: 30px 25px;
|
||||
|
||||
> .el-row {
|
||||
.el-form-item {
|
||||
width: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.el-input,
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right-input {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.el-form-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
||||
.el-button {
|
||||
width: 110px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-vertical) {
|
||||
.page-content {
|
||||
.content {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
|
||||
.left-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="account-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增用户</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<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" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" />
|
||||
</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>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ROLE_LIST_DATA, ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
|
||||
|
||||
import { ElDialog, FormInstance, ElTag } from 'element-plus'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
const { width } = useWindowSize()
|
||||
|
||||
defineOptions({ name: 'User' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
level: '',
|
||||
email: '',
|
||||
date: '',
|
||||
daterange: '',
|
||||
status: '1'
|
||||
}
|
||||
|
||||
const roleList = ref<any[]>([])
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '用户名',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
|
||||
{
|
||||
label: '电话',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '用户等级',
|
||||
prop: 'level',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
options: () => [
|
||||
{ label: '普通用户', value: 'normal' },
|
||||
{ label: 'VIP用户', value: 'vip' },
|
||||
{ label: '高级VIP', value: 'svip' },
|
||||
{ label: '企业用户', value: 'enterprise', disabled: true }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '地址',
|
||||
prop: 'address',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '邮箱',
|
||||
prop: 'email',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
// 支持 9 种日期类型定义
|
||||
// 具体可参考 src/components/core/forms/art-search-bar/widget/art-search-date/README.md
|
||||
{
|
||||
prop: 'date',
|
||||
label: '日期',
|
||||
type: 'date',
|
||||
config: {
|
||||
type: 'date',
|
||||
placeholder: '请选择日期'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'daterange',
|
||||
label: '日期范围',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: '在线', value: '1' },
|
||||
{ label: '离线', value: '2' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 获取标签类型
|
||||
// 1: 在线 2: 离线 3: 异常 4: 注销
|
||||
const getTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'info'
|
||||
case '3':
|
||||
return 'warning'
|
||||
case '4':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建标签文本
|
||||
const buildTagText = (status: string) => {
|
||||
let text = ''
|
||||
if (status === '1') {
|
||||
text = '在线'
|
||||
} else if (status === '2') {
|
||||
text = '离线'
|
||||
} else if (status === '3') {
|
||||
text = '异常'
|
||||
} else if (status === '4') {
|
||||
text = '注销'
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
// 重置表单验证状态
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.username = row.username
|
||||
formData.phone = row.userPhone
|
||||
formData.gender = row.gender === 1 ? '男' : '女'
|
||||
|
||||
// 将用户角色代码数组直接赋值给formData.role
|
||||
formData.role = Array.isArray(row.userRoles) ? row.userRoles : []
|
||||
} else {
|
||||
formData.username = ''
|
||||
formData.phone = ''
|
||||
formData.gender = '男'
|
||||
formData.role = []
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = () => {
|
||||
ElMessageBox.confirm('确定要注销该用户吗?', '注销用户', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('注销成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' }, // 勾选列
|
||||
// { type: 'expand', label: '展开', width: 80 }, // 展开列
|
||||
// { type: 'index', label: '序号', width: 80 }, // 序号列
|
||||
{
|
||||
prop: 'avatar',
|
||||
label: '用户名',
|
||||
minWidth: width.value < 500 ? 220 : '',
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'user', style: 'display: flex; align-items: center' }, [
|
||||
h('img', { class: 'avatar', src: row.avatar }),
|
||||
h('div', {}, [
|
||||
h('p', { class: 'user-name' }, row.userName),
|
||||
h('p', { class: 'email' }, row.userEmail)
|
||||
])
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'userGender',
|
||||
label: '性别',
|
||||
sortable: true,
|
||||
formatter: (row) => (row.userGender === 1 ? '男' : '女')
|
||||
},
|
||||
{ prop: 'userPhone', label: '手机号' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getTagType(row.status) }, () => buildTagText(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建日期',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
// fixed: 'right', // 固定在右侧
|
||||
// disabled: true,
|
||||
formatter: (row: any) => {
|
||||
return h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteUser()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
phone: '',
|
||||
gender: '',
|
||||
role: [] as string[]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
getRoleList()
|
||||
})
|
||||
|
||||
// 获取用户列表数据
|
||||
const getUserList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { records, current, size, total } = await UserService.getUserList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
|
||||
// 使用本地头像替换接口返回的头像
|
||||
tableData.value = records.map((item: any, index: number) => ({
|
||||
...item,
|
||||
avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar
|
||||
}))
|
||||
|
||||
// 更新分页信息
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleList = () => {
|
||||
roleList.value = ROLE_LIST_DATA
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<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: 'change' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getUserList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getUserList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.account-page {
|
||||
:deep(.user) {
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-left: 10px;
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user