项目创建

This commit is contained in:
浅念
2025-07-04 17:16:38 +08:00
parent ce47b39fa5
commit 719911918f
372 changed files with 105035 additions and 32 deletions
+662
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div class="page-content">
<h1>菜单-1</h1>
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div class="page-content">
<h1>菜单-2-1</h1>
</div>
</template>
+5
View File
@@ -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>
+344
View File
@@ -0,0 +1,344 @@
<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 :data="roleList" index>
<template #default>
<ElTableColumn label="角色名称" prop="roleName" />
<ElTableColumn label="角色编码" prop="roleCode" />
<ElTableColumn label="描述" prop="des" />
<ElTableColumn label="启用" prop="enable">
<template #default="scope">
<ElTag :type="scope.row.enable ? 'primary' : 'info'">
{{ scope.row.enable ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="date">
<template #default="scope">
{{ formatDate(scope.row.date) }}
</template>
</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 { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
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)
// 处理菜单数据,将 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 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 buttonMoreClick = (item: ButtonMoreItem, row: any) => {
// if (item.key === 'permission') {
// showPermissionDialog()
// } else if (item.key === 'edit') {
// showDialog('edit', row)
// } else if (item.key === 'delete') {
// deleteRole()
// }
// }
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>
+444
View File
@@ -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">&#xe72e;</i>
<span>jdkjjfnndf@mall.com</span>
</div>
<div>
<i class="iconfont-sys">&#xe608;</i>
<span>交互专家</span>
</div>
<div>
<i class="iconfont-sys">&#xe736;</i>
<span>广东省深圳市</span>
</div>
<div>
<i class="iconfont-sys">&#xe811;</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>
+477
View File
@@ -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>