优化更新

This commit is contained in:
2026-01-23 22:05:09 +08:00
parent 8283555457
commit 0608f0febb
14 changed files with 667 additions and 622 deletions

View File

@@ -16,6 +16,7 @@
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@ckeditor/ckeditor5-vue": "^7.3.0", "@ckeditor/ckeditor5-vue": "^7.3.0",
"@element-plus/icons-vue": "^2.3.2",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.13.2", "axios": "^1.13.2",
"ckeditor5": "^47.4.0", "ckeditor5": "^47.4.0",

View File

@@ -2,171 +2,148 @@ import request from '@/utils/request'
export default { export default {
login: { login: {
url: `auth/login`,
name: '用户登录', name: '用户登录',
post: async function (params) { post: async function (params) {
return await request.post(this.url, params) return await request.post('auth/login', params)
}, },
}, },
logout: { logout: {
url: `auth/logout`,
name: '用户登出', name: '用户登出',
get: async function () { get: async function () {
return await request.get(this.url) return await request.get('auth/logout')
}, },
}, },
user: { user: {
url: `auth/user`,
name: '获取用户信息', name: '获取用户信息',
get: async function () { get: async function () {
return await request.get(this.url) return await request.get('auth/user')
}, },
}, },
users: { users: {
list: { list: {
url: `auth/users/index`,
name: "获得用户列表", name: "获得用户列表",
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }); return await request.get('auth/users/index', { params });
}, },
}, },
add: { add: {
url: `auth/users/add`,
name: "添加用户", name: "添加用户",
post: async function (params) { post: async function (params) {
return await request.post(this.url, params); return await request.post('auth/users/add', params);
}, },
}, },
edit: { edit: {
url: `auth/users/edit`,
name: "编辑用户", name: "编辑用户",
post: async function (params) { post: async function (params) {
return await request.put(this.url, params); return await request.put('auth/users/edit', params);
}, },
}, },
uppasswd: { uppasswd: {
url: `auth/users/passwd`,
name: "修改密码", name: "修改密码",
post: async function (params) { post: async function (params) {
return await request.put(this.url, params); return await request.put('auth/users/passwd', params);
}, },
}, },
uprole: { uprole: {
url: `auth/users/uprole`,
name: "设置角色", name: "设置角色",
post: async function (params) { post: async function (params) {
return await request.put(this.url, params); return await request.put('auth/users/uprole', params);
}, },
}, },
delete: { delete: {
url: `auth/users/delete`,
name: "删除用户", name: "删除用户",
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params); return await request.delete('auth/users/delete', params);
}, },
}, },
}, },
role: { role: {
list: { list: {
url: `auth/role/index`,
name: "获得角色列表", name: "获得角色列表",
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }); return await request.get('auth/role/index', { params });
}, },
}, },
add: { add: {
url: `auth/role/add`,
name: "添加角色", name: "添加角色",
post: async function (params) { post: async function (params) {
return await request.post(this.url, params); return await request.post('auth/role/add', params);
}, },
}, },
edit: { edit: {
url: `auth/role/edit`,
name: "编辑角色", name: "编辑角色",
post: async function (params) { post: async function (params) {
return await request.put(this.url, params); return await request.put('auth/role/edit', params);
}, },
}, },
auth: { auth: {
url: `auth/role/auth`,
name: "角色授权", name: "角色授权",
post: async function (params) { post: async function (params) {
return await request.put(this.url, params); return await request.put('auth/role/auth', params);
}, },
}, },
delete: { delete: {
url: `auth/role/delete`,
name: "删除角色", name: "删除角色",
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params); return await request.delete('auth/role/delete', params);
}, },
}, },
}, },
department: { department: {
list: { list: {
url: `auth/department/index`,
name: "获得部门列表", name: "获得部门列表",
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }); return await request.get('auth/department/index', { params });
}, },
}, },
add: { add: {
url: `auth/department/add`,
name: "添加部门", name: "添加部门",
post: async function (params) { post: async function (params) {
return await request.post(this.url, params); return await request.post('auth/department/add', params);
}, },
}, },
edit: { edit: {
url: `auth/department/edit`,
name: "编辑部门", name: "编辑部门",
post: async function (params) { post: async function (params) {
return await request.put(this.url, params); return await request.put('auth/department/edit', params);
}, },
}, },
delete: { delete: {
url: `auth/department/delete`,
name: "删除部门", name: "删除部门",
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params); return await request.delete('auth/department/delete', params);
}, },
}, },
}, },
menu: { menu: {
my: { my: {
url: `auth/menu/my`,
name: '获取我的菜单', name: '获取我的菜单',
get: async function () { get: async function () {
return await request.get(this.url) return await request.get('auth/menu/my')
}, },
}, },
list: { list: {
url: `auth/menu/index`,
name: "获取菜单", name: "获取菜单",
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }); return await request.get('auth/menu/index', { params });
}, },
}, },
add: { add: {
url: `auth/menu/add`,
name: "添加菜单", name: "添加菜单",
post: async function (params) { post: async function (params) {
return await request.post(this.url, params); return await request.post('auth/menu/add', params);
}, },
}, },
edit: { edit: {
url: `auth/menu/edit`,
name: "编辑菜单", name: "编辑菜单",
post: async function (params) { post: async function (params) {
return await request.put(this.url, params); return await request.put('auth/menu/edit', params);
}, },
}, },
delete: { delete: {
url: `auth/menu/delete`,
name: "删除菜单", name: "删除菜单",
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params); return await request.delete('auth/menu/delete', params);
}, },
}, },
}, },

View File

@@ -2,347 +2,301 @@ import request from '@/utils/request'
export default { export default {
version: { version: {
url: `system/index/version`,
name: '获取最新版本号', name: '获取最新版本号',
get: async function () { get: async function () {
return await request.get(this.url) return await request.get('system/index/version')
}, },
}, },
clearcache: { clearcache: {
url: `system/index/clearcache`,
name: '清除缓存', name: '清除缓存',
post: async function () { post: async function () {
return await request.post(this.url) return await request.post('system/index/clearcache')
}, },
}, },
info: { info: {
url: `system/index/info`,
name: '系统信息', name: '系统信息',
get: function (params) { get: function (params) {
return request.get(this.url, { params }) return request.get('system/index/info', { params })
}, },
}, },
setting: { setting: {
list: { list: {
url: `system/setting/index`,
name: '获取配置信息', name: '获取配置信息',
get: function (params) { get: function (params) {
return request.get(this.url, { params }) return request.get('system/setting/index', { params })
}, },
}, },
fields: { fields: {
url: `system/setting/fields`,
name: '获取配置字段', name: '获取配置字段',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/setting/fields', { params })
}, },
}, },
add: { add: {
url: `system/setting/add`,
name: '保存配置信息', name: '保存配置信息',
post: function (data) { post: function (data) {
return request.post(this.url, data) return request.post('system/setting/add', data)
}, },
}, },
edit: { edit: {
url: `system/setting/edit`,
name: '编辑配置信息', name: '编辑配置信息',
post: function (data) { post: function (data) {
return request.put(this.url, data) return request.put('system/setting/edit', data)
}, },
}, },
save: { save: {
url: `system/setting/save`,
name: '保存配置信息', name: '保存配置信息',
post: function (data) { post: function (data) {
return request.put(this.url, data) return request.put('system/setting/save', data)
}, },
}, },
}, },
dictionary: { dictionary: {
category: { category: {
url: `system/dict/category`,
name: '获取字典树', name: '获取字典树',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/dict/category', { params })
}, },
}, },
editcate: { editcate: {
url: `system/dict/editcate`,
name: '编辑字典树', name: '编辑字典树',
post: async function (data = {}) { post: async function (data = {}) {
return await request.put(this.url, data) return await request.put('system/dict/editcate', data)
}, },
}, },
addcate: { addcate: {
url: `system/dict/addcate`,
name: '添加字典树', name: '添加字典树',
post: async function (data = {}) { post: async function (data = {}) {
return await request.post(this.url, data) return await request.post('system/dict/addcate', data)
}, },
}, },
delCate: { delCate: {
url: `system/dict/deletecate`,
name: '删除字典树', name: '删除字典树',
post: async function (data = {}) { post: async function (data = {}) {
return await request.delete(this.url, data) return await request.delete('system/dict/deletecate', data)
}, },
}, },
list: { list: {
url: `system/dict/lists`,
name: '字典明细', name: '字典明细',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/dict/lists', { params })
}, },
}, },
get: { get: {
url: `system/dict/detail`,
name: '获取字典数据', name: '获取字典数据',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/dict/detail', { params })
}, },
}, },
edit: { edit: {
url: `system/dict/edit`,
name: '编辑字典明细', name: '编辑字典明细',
post: async function (data = {}) { post: async function (data = {}) {
return await request.put(this.url, data) return await request.put('system/dict/edit', data)
}, },
}, },
add: { add: {
url: `system/dict/add`,
name: '添加字典明细', name: '添加字典明细',
post: async function (data = {}) { post: async function (data = {}) {
return await request.post(this.url, data) return await request.post('system/dict/add', data)
}, },
}, },
delete: { delete: {
url: `system/dict/delete`,
name: '删除字典明细', name: '删除字典明细',
post: async function (data = {}) { post: async function (data = {}) {
return await request.delete(this.url, data) return await request.delete('system/dict/delete', data)
}, },
}, },
detail: { detail: {
url: `system/dict/detail`,
name: '字典明细', name: '字典明细',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/dict/detail', { params })
}, },
}, },
alldic: { alldic: {
url: `system/dict/all`,
name: '全部字典', name: '全部字典',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/dict/all', { params })
}, },
}, },
}, },
area: { area: {
list: { list: {
url: `system/area/index`,
name: '地区列表', name: '地区列表',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/area/index', { params })
}, },
}, },
add: { add: {
url: `system/area/add`,
name: '地区添加', name: '地区添加',
post: async function (params) { post: async function (params) {
return await request.post(this.url, params) return await request.post('system/area/add', params)
}, },
}, },
edit: { edit: {
url: `system/area/edit`,
name: '地区编辑', name: '地区编辑',
post: async function (params) { post: async function (params) {
return await request.put(this.url, params) return await request.put('system/area/edit', params)
}, },
}, },
}, },
app: { app: {
list: { list: {
url: `system/app/list`,
name: '应用列表', name: '应用列表',
get: async function () { get: async function () {
return await request.get(this.url) return await request.get('system/app/list')
}, },
}, },
}, },
client: { client: {
list: { list: {
url: `system/client/index`,
name: '客户端列表', name: '客户端列表',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/client/index', { params })
}, },
}, },
add: { add: {
url: `system/client/add`,
name: '客户端添加', name: '客户端添加',
post: async function (params) { post: async function (params) {
return await request.post(this.url, params) return await request.post('system/client/add', params)
}, },
}, },
edit: { edit: {
url: `system/client/edit`,
name: '客户端编辑', name: '客户端编辑',
post: async function (params) { post: async function (params) {
return await request.put(this.url, params) return await request.put('system/client/edit', params)
}, },
}, },
delete: { delete: {
url: `system/client/delete`,
name: '客户端删除', name: '客户端删除',
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params) return await request.delete('system/client/delete', params)
}, },
}, },
menu: { menu: {
list: { list: {
url: `system/menu/index`,
name: '客户端菜单列表', name: '客户端菜单列表',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/menu/index', { params })
}, },
}, },
add: { add: {
url: `system/menu/add`,
name: '客户端菜单添加', name: '客户端菜单添加',
post: async function (params) { post: async function (params) {
return await request.post(this.url, params) return await request.post('system/menu/add', params)
}, },
}, },
edit: { edit: {
url: `system/menu/edit`,
name: '客户端菜单编辑', name: '客户端菜单编辑',
post: async function (params) { post: async function (params) {
return await request.put(this.url, params) return await request.put('system/menu/edit', params)
}, },
}, },
delete: { delete: {
url: `system/menu/delete`,
name: '客户端菜单删除', name: '客户端菜单删除',
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params) return await request.delete('system/menu/delete', params)
}, },
}, },
}, },
}, },
log: { log: {
list: { list: {
url: `system/log/index`,
name: '日志列表', name: '日志列表',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/log/index', { params })
}, },
}, },
my: { my: {
url: `system/log/my`,
name: '我的日志', name: '我的日志',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/log/my', { params })
}, },
}, },
delete: { delete: {
url: `system/log/delete`,
name: '日志删除', name: '日志删除',
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params) return await request.delete('system/log/delete', params)
}, },
}, },
}, },
tasks: { tasks: {
list: { list: {
url: `system/tasks/index`,
name: '任务列表', name: '任务列表',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/tasks/index', { params })
}, },
}, },
delete: { delete: {
url: `system/tasks/delete`,
name: '任务删除', name: '任务删除',
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params) return await request.delete('system/tasks/delete', params)
}, },
}, },
}, },
crontab: { crontab: {
list: { list: {
url: `system/crontab/index`,
name: '定时任务列表', name: '定时任务列表',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/crontab/index', { params })
}, },
}, },
add: { add: {
url: `system/crontab/add`,
name: '定时任务添加', name: '定时任务添加',
post: async function (params) { post: async function (params) {
return await request.post(this.url, params) return await request.post('system/crontab/add', params)
}, },
}, },
edit: { edit: {
url: `system/crontab/edit`,
name: '定时任务编辑', name: '定时任务编辑',
post: async function (params) { post: async function (params) {
return await request.put(this.url, params) return await request.put('system/crontab/edit', params)
}, },
}, },
delete: { delete: {
url: `system/crontab/delete`,
name: '定时任务删除', name: '定时任务删除',
post: async function (params) { post: async function (params) {
return await request.delete(this.url, params) return await request.delete('system/crontab/delete', params)
}, },
}, },
log: { log: {
url: `system/crontab/log`,
name: '定时任务日志', name: '定时任务日志',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/crontab/log', { params })
}, },
}, },
reload: { reload: {
url: `system/crontab/reload`,
name: '定时任务重载', name: '定时任务重载',
post: async function (params) { post: async function (params) {
return await request.put(this.url, params) return await request.put('system/crontab/reload', params)
}, },
}, },
}, },
modules: { modules: {
list: { list: {
url: `system/modules/index`,
name: '模块列表', name: '模块列表',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/modules/index', { params })
}, },
}, },
update: { update: {
url: `system/modules/update`,
name: '更新模块', name: '更新模块',
post: async function (params) { post: async function (params) {
return await request.post(this.url, params) return await request.post('system/modules/update', params)
}, },
}, },
}, },
sms: { sms: {
count: { count: {
url: `system/sms/count`,
name: '短信发送统计', name: '短信发送统计',
get: async function (params) { get: async function (params) {
return await request.get(this.url, { params }) return await request.get('system/sms/count', { params })
}, },
}, },
}, },
upload: { upload: {
url: `system/file/upload`,
name: '文件上传', name: '文件上传',
post: async function (params = {}) { post: async function (params = {}) {
return await request.post(this.url, params) return await request.post('system/file/upload', params)
}, },
}, },
} }

View File

@@ -1,4 +1,5 @@
import * as AIcons from '@ant-design/icons-vue' import * as AIcons from '@ant-design/icons-vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
export default { export default {
install(app) { install(app) {
@@ -6,5 +7,9 @@ export default {
for (let icon in AIcons) { for (let icon in AIcons) {
app.component(`${icon}`, AIcons[icon]) app.component(`${icon}`, AIcons[icon])
} }
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(`El${key}`, component)
}
} }
} }

239
src/hooks/useTable.js Normal file
View File

@@ -0,0 +1,239 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
/**
* 表格通用hooks
* @param {Object} options 配置选项
* @param {Function} options.api 获取列表数据的API函数必须返回包含data和total的响应
* @param {Object} options.searchForm 搜索表单的初始值
* @param {Array} options.columns 表格列配置
* @param {String} options.rowKey 行的唯一标识,默认为'id'
* @param {Boolean} options.needPagination 是否需要分页默认为true
* @param {Object} options.paginationConfig 分页配置,可选
* @param {Boolean} options.needSelection 是否需要行选择默认为false
* @param {Boolean} options.immediateLoad 是否在组件挂载时自动加载数据默认为true
* @returns {Object} 返回表格相关的状态和方法
*/
export function useTable(options = {}) {
const {
api,
searchForm: initialSearchForm = {},
columns = [],
rowKey = 'id',
needPagination = true,
paginationConfig = {},
needSelection = false,
immediateLoad = true
} = options
// 表格引用
const tableRef = ref(null)
// 搜索表单
const searchForm = reactive({ ...initialSearchForm })
// 表格数据
const tableData = ref([])
// 加载状态
const loading = ref(false)
// 选中的行数据
const selectedRows = ref([])
// 选中的行keys
const selectedRowKeys = computed(() => selectedRows.value.map(item => item[rowKey]))
// 分页配置
const defaultPaginationConfig = {
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['20', '50', '100', '200']
}
const pagination = reactive({
...defaultPaginationConfig,
...paginationConfig
})
// 行选择配置
const rowSelection = computed(() => {
if (!needSelection) return null
return {
selectedRowKeys: selectedRowKeys.value,
onChange: (keys, rows) => {
selectedRows.value = rows
}
}
})
// 行选择事件处理用于scTable的@select事件
const handleSelectChange = (record, selected, selectedRows) => {
if (!needSelection) return
if (selected) {
selectedRows.value.push(record)
} else {
const index = selectedRows.value.findIndex(item => item[rowKey] === record[rowKey])
if (index > -1) {
selectedRows.value.splice(index, 1)
}
}
}
// 全选/取消全选处理用于scTable的@selectAll事件
const handleSelectAll = (selected, selectedRows, changeRows) => {
if (!needSelection) return
if (selected) {
changeRows.forEach(record => {
if (!selectedRows.value.find(item => item[rowKey] === record[rowKey])) {
selectedRows.value.push(record)
}
})
} else {
changeRows.forEach(record => {
const index = selectedRows.value.findIndex(item => item[rowKey] === record[rowKey])
if (index > -1) {
selectedRows.value.splice(index, 1)
}
})
}
}
// 加载数据
const loadData = async (params = {}) => {
if (!api) {
console.warn('useTable: 未提供api函数无法加载数据')
return
}
loading.value = true
try {
const requestParams = {
...searchForm,
...params
}
// 如果需要分页,添加分页参数
if (needPagination) {
requestParams.page = pagination.current
requestParams.limit = pagination.pageSize
}
// 调用API函数确保this上下文正确
const res = await api(requestParams)
if (res.code === 1) {
// 如果是分页数据
if (needPagination) {
tableData.value = res.data?.data || []
pagination.total = res.data?.total || 0
} else {
// 非分页数据(如树形数据)
tableData.value = res.data || []
}
} else {
message.error(res.message || '加载数据失败')
}
} catch (error) {
console.error('加载数据失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
// 分页变化处理
const handlePaginationChange = ({ page, pageSize }) => {
if (!needPagination) return
pagination.current = page
pagination.pageSize = pageSize
loadData()
}
// 搜索
const handleSearch = () => {
if (needPagination) {
pagination.current = 1
}
loadData()
}
// 重置
const handleReset = () => {
// 重置搜索表单为初始值
Object.keys(searchForm).forEach(key => {
searchForm[key] = initialSearchForm[key]
})
// 清空选择
selectedRows.value = []
// 重置分页
if (needPagination) {
pagination.current = 1
}
// 重新加载数据
loadData()
}
// 刷新表格
const refreshTable = () => {
loadData()
}
// 清空选择
const clearSelection = () => {
selectedRows.value = []
}
// 设置选中行
const setSelectedRows = (rows) => {
selectedRows.value = rows
}
// 更新搜索表单
const setSearchForm = (data) => {
Object.assign(searchForm, data)
}
// 直接设置表格数据(用于特殊场景)
const setTableData = (data) => {
tableData.value = data
}
// 组件挂载时自动加载数据
if (immediateLoad) {
onMounted(() => {
loadData()
})
}
return {
// ref
tableRef,
// 响应式数据
searchForm,
tableData,
loading,
pagination,
selectedRows,
selectedRowKeys,
// 配置
columns,
rowKey,
rowSelection,
// 方法
loadData,
handleSearch,
handleReset,
handlePaginationChange,
handleSelectChange,
handleSelectAll,
refreshTable,
clearSelection,
setSelectedRows,
setSearchForm,
setTableData
}
}

View File

@@ -1,36 +1,23 @@
<template> <template>
<div v-show="showTags" class="tags-view"> <div v-show="showTags" class="tags-view">
<a-dropdown :trigger="['contextmenu']"> <div class="tags-wrapper" @contextmenu.prevent>
<div class="tags-wrapper"> <a-space :size="4">
<a-space :size="4"> <a-tag
<a-tag v-for="tag in visitedViews" :key="tag.fullPath" :closable="!tag.meta?.affix" class="tag-item" v-for="tag in visitedViews"
:class="{ active: isActive(tag) }" @click="clickTag(tag)" @close="closeSelectedTag(tag)" :key="tag.fullPath"
@contextmenu.prevent="handleContextMenu($event, tag)"> :closable="!tag.meta?.affix"
{{ tag.meta?.title || tag.name }} class="tag-item"
</a-tag> :class="{ active: isActive(tag), 'tag-affix': tag.meta?.affix }"
</a-space> @click="clickTag(tag)"
</div> @close="closeSelectedTag(tag)"
<template #overlay> @contextmenu.prevent="handleContextMenu($event, tag)">
<a-menu @click="handleMenuClick"> <template #icon v-if="tag.meta?.affix">
<a-menu-item key="refresh"> <PushpinFilled />
<ReloadOutlined /> </template>
<span>刷新</span> {{ tag.meta?.title || tag.name }}
</a-menu-item> </a-tag>
<a-menu-item v-if="!selectedTag.meta?.affix" key="close"> </a-space>
<CloseOutlined /> </div>
<span>关闭</span>
</a-menu-item>
<a-menu-item key="closeOthers">
<ColumnWidthOutlined />
<span>关闭其他</span>
</a-menu-item>
<a-menu-item key="closeAll">
<CloseCircleOutlined />
<span>关闭所有</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<div class="tags-actions"> <div class="tags-actions">
<a-tooltip title="刷新当前页"> <a-tooltip title="刷新当前页">
@@ -49,17 +36,55 @@
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</div> </div>
<!-- 右键菜单 -->
<teleport to="body">
<div
v-if="contextMenu.visible"
:style="{
position: 'fixed',
left: contextMenu.x + 'px',
top: contextMenu.y + 'px',
zIndex: 9999
}"
class="context-menu"
@click="closeContextMenu">
<a-menu @click="handleMenuClick">
<a-menu-item key="refresh">
<ReloadOutlined />
<span>刷新</span>
</a-menu-item>
<a-menu-item v-if="selectedTag && !selectedTag.meta?.affix" key="close">
<CloseOutlined />
<span>关闭</span>
</a-menu-item>
<a-menu-item key="closeOthers">
<ColumnWidthOutlined />
<span>关闭其他</span>
</a-menu-item>
<a-menu-item key="closeAll">
<CloseCircleOutlined />
<span>关闭所有</span>
</a-menu-item>
</a-menu>
</div>
</teleport>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onMounted } from 'vue' import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useLayoutStore } from '@/stores/modules/layout' import { useLayoutStore } from '@/stores/modules/layout'
import { ReloadOutlined, CloseOutlined, ColumnWidthOutlined, CloseCircleOutlined } from '@ant-design/icons-vue' import {
ReloadOutlined,
CloseOutlined,
ColumnWidthOutlined,
CloseCircleOutlined,
PushpinFilled
} from '@ant-design/icons-vue'
import config from '@/config' import config from '@/config'
// 定义组件名称(多词命名)
defineOptions({ defineOptions({
name: 'TagsView', name: 'TagsView',
}) })
@@ -69,8 +94,15 @@ const router = useRouter()
const layoutStore = useLayoutStore() const layoutStore = useLayoutStore()
const showTags = ref(true) const showTags = ref(true)
const selectedTag = ref({}) const selectedTag = ref(null)
const visitedViews = computed(() => layoutStore.viewTags) const visitedViews = computed(() => layoutStore.viewTags)
console.log(visitedViews)
// 右键菜单状态
const contextMenu = ref({
visible: false,
x: 0,
y: 0
})
// 判断是否是当前激活的标签 // 判断是否是当前激活的标签
const isActive = (tag) => { const isActive = (tag) => {
@@ -87,7 +119,7 @@ const addTags = () => {
name: name, name: name,
query: route.query, query: route.query,
params: route.params, params: route.params,
meta: route.meta, meta: route.meta
}) })
} }
} }
@@ -120,7 +152,9 @@ const closeOthersTags = () => {
} }
// 保留固定标签和当前选中的标签 // 保留固定标签和当前选中的标签
const tagsToKeep = visitedViews.value.filter((tag) => tag.meta?.affix || tag.fullPath === selectedTag.value.fullPath) const tagsToKeep = visitedViews.value.filter(
(tag) => tag.meta?.affix || tag.fullPath === selectedTag.value.fullPath
)
// 更新标签列表 // 更新标签列表
layoutStore.viewTags = tagsToKeep layoutStore.viewTags = tagsToKeep
@@ -142,7 +176,7 @@ const closeAllTags = () => {
router.push(affixTags[0].fullPath) router.push(affixTags[0].fullPath)
} else { } else {
// 如果没有固定标签,跳转到首页 // 如果没有固定标签,跳转到首页
router.push('/home') router.push(config.DASHBOARD_URL)
} }
} }
@@ -153,23 +187,52 @@ const clickTag = (tag) => {
} }
} }
// 刷新当前标签 // 刷新指定标签
const refreshTag = (tag) => {
// 如果刷新的是当前激活的标签
if (isActive(tag)) {
// 调用 store 的刷新方法,触发组件重新渲染
layoutStore.refreshTag()
} else {
// 如果刷新的是其他标签,先跳转到该标签
router.push(tag.fullPath)
}
}
// 刷新当前选中的标签(用于顶部操作按钮)
const refreshSelectedTag = () => { const refreshSelectedTag = () => {
// 使用 router.go(0) 刷新页面 // 找到当前激活的标签
router.go(0) const currentTag = visitedViews.value.find((tag) => isActive(tag))
if (currentTag) {
refreshTag(currentTag)
}
} }
// 右键菜单处理 // 右键菜单处理
const handleContextMenu = (event, tag) => { const handleContextMenu = (event, tag) => {
event.preventDefault() event.preventDefault()
event.stopPropagation()
selectedTag.value = tag selectedTag.value = tag
contextMenu.value = {
visible: true,
x: event.clientX,
y: event.clientY
}
}
// 关闭右键菜单
const closeContextMenu = () => {
contextMenu.value.visible = false
} }
// 菜单点击处理 // 菜单点击处理
const handleMenuClick = ({ key }) => { const handleMenuClick = ({ key }) => {
switch (key) { switch (key) {
case 'refresh': case 'refresh':
refreshSelectedTag() if (selectedTag.value) {
refreshTag(selectedTag.value)
}
break break
case 'close': case 'close':
if (selectedTag.value && !selectedTag.value.meta?.affix) { if (selectedTag.value && !selectedTag.value.meta?.affix) {
@@ -183,6 +246,17 @@ const handleMenuClick = ({ key }) => {
closeAllTags() closeAllTags()
break break
} }
closeContextMenu()
}
// 点击其他地方关闭右键菜单
const handleClickOutside = (event) => {
if (contextMenu.value.visible) {
const menuElement = document.querySelector('.context-menu')
if (menuElement && !menuElement.contains(event.target)) {
closeContextMenu()
}
}
} }
// 监听路由变化,自动添加标签 // 监听路由变化,自动添加标签
@@ -191,15 +265,22 @@ watch(
() => { () => {
addTags() addTags()
// 更新当前选中的标签 // 更新当前选中的标签
selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || {} selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || null
}, },
{ immediate: true }, { immediate: true }
) )
onMounted(() => { onMounted(() => {
addTags() addTags()
// 初始化选中的标签 // 初始化选中的标签
selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || {} selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || null
// 添加点击事件监听器
document.addEventListener('click', handleClickOutside)
})
onBeforeUnmount(() => {
// 移除点击事件监听器
document.removeEventListener('click', handleClickOutside)
}) })
</script> </script>
@@ -236,6 +317,7 @@ onMounted(() => {
.tag-item { .tag-item {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px;
height: 28px; height: 28px;
line-height: 28px; line-height: 28px;
padding: 0 12px; padding: 0 12px;
@@ -245,6 +327,7 @@ onMounted(() => {
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
user-select: none;
&:hover { &:hover {
color: #1890ff; color: #1890ff;
@@ -262,6 +345,28 @@ onMounted(() => {
} }
} }
&.tag-affix {
background-color: #fff7e6;
border-color: #ffd591;
color: #fa8c16;
&.active {
background-color: #fa8c16;
border-color: #fa8c16;
color: #ffffff;
}
&:hover {
background-color: #ffe7ba;
border-color: #ffa940;
}
&.active:hover {
background-color: #d46b08;
border-color: #d46b08;
}
}
:deep(.ant-tag-close-icon) { :deep(.ant-tag-close-icon) {
color: inherit; color: inherit;
@@ -289,4 +394,32 @@ onMounted(() => {
} }
} }
} }
.context-menu {
background: #ffffff;
border-radius: 2px;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f0f0;
padding: 4px 0;
min-width: 160px;
:deep(.ant-menu) {
background: transparent;
border: none;
box-shadow: none;
padding: 0;
.ant-menu-item {
margin: 0;
padding: 8px 12px;
height: auto;
line-height: normal;
&:hover {
background-color: #f5f5f5;
}
}
}
}
</style> </style>

View File

@@ -43,7 +43,7 @@
<a-layout-content class="app-main"> <a-layout-content class="app-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive :include="cachedViews"> <keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" /> <component :is="Component" :key="refreshKey" />
</keep-alive> </keep-alive>
</router-view> </router-view>
</a-layout-content> </a-layout-content>
@@ -100,7 +100,7 @@
<a-layout-content class="app-main top-content"> <a-layout-content class="app-main top-content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive :include="cachedViews"> <keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" /> <component :is="Component" :key="refreshKey" />
</keep-alive> </keep-alive>
</router-view> </router-view>
</a-layout-content> </a-layout-content>
@@ -163,6 +163,9 @@ const layoutClass = computed(() => {
} }
}) })
// 获取刷新 key
const refreshKey = computed(() => layoutStore.refreshKey)
const openKeys = ref([]) const openKeys = ref([])
const selectedKeys = ref([]) const selectedKeys = ref([])
const menuList = computed(() => { const menuList = computed(() => {

View File

@@ -22,14 +22,14 @@
<a-button type="primary" @click="handleAdd"> <a-button type="primary" @click="handleAdd">
<template #icon><plus-outlined /></template> <template #icon><plus-outlined /></template>
</a-button> </a-button>
<a-button danger :disabled="selection.length === 0" @click="handleBatchDelete"> <a-button danger :disabled="selectedRows.length === 0" @click="handleBatchDelete">
<template #icon><delete-outlined /></template> <template #icon><delete-outlined /></template>
</a-button> </a-button>
</div> </div>
</div> </div>
<div class="table-content"> <div class="table-content">
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" :pagination="false" <scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" :pagination="false"
:row-key="rowKey" :row-selection="rowSelection" @refresh="loadData" @select="handleSelectChange" :row-key="rowKey" :row-selection="rowSelection" @refresh="refreshTable" @select="handleSelectChange"
@selectAll="handleSelectAll"> @selectAll="handleSelectAll">
<template #action="{ record }"> <template #action="{ record }">
<a-space> <a-space>
@@ -49,18 +49,40 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import scTable from '@/components/scTable/index.vue' import scTable from '@/components/scTable/index.vue'
import saveDialog from './save.vue' import saveDialog from './save.vue'
import authApi from '@/api/auth' import authApi from '@/api/auth'
import { useTable } from '@/hooks/useTable'
defineOptions({ defineOptions({
name: 'authDepartment' name: 'authDepartment'
}) })
// 表格引用 // 使用useTable hooks
const tableRef = ref(null) const {
tableRef,
searchForm,
tableData,
loading,
selectedRows,
rowSelection,
handleSearch,
handleReset,
handleSelectChange,
handleSelectAll,
refreshTable
} = useTable({
api: authApi.department.list.get,
searchForm: {
keyword: ''
},
columns: [],
needPagination: false,
needSelection: true,
immediateLoad: false
})
// 对话框状态 // 对话框状态
const dialog = reactive({ const dialog = reactive({
@@ -70,29 +92,9 @@ const dialog = reactive({
// 弹窗引用 // 弹窗引用
const saveDialogRef = ref(null) const saveDialogRef = ref(null)
// 选中的行
const selection = ref([])
// 搜索表单
const searchForm = reactive({
keyword: ''
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
// 行key // 行key
const rowKey = 'id' const rowKey = 'id'
// 行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selection.value.map(item => item.id),
onChange: (selectedRowKeys, selectedRows) => {
selection.value = selectedRows
}
}))
// 表格列配置 // 表格列配置
const columns = [ const columns = [
{ title: '#', dataIndex: '_index', key: '_index', width: 60, align: 'center' }, { title: '#', dataIndex: '_index', key: '_index', width: 60, align: 'center' },
@@ -102,69 +104,6 @@ const columns = [
{ title: '操作', dataIndex: 'action', key: 'action', width: 220, align: 'center', slot: 'action', fixed: 'right' } { title: '操作', dataIndex: 'action', key: 'action', width: 220, align: 'center', slot: 'action', fixed: 'right' }
] ]
// 加载部门列表数据
const loadData = async () => {
loading.value = true
try {
const params = {
is_tree: 1,
...searchForm
}
const res = await authApi.department.list.get(params)
if (res.code === 1) {
tableData.value = res.data || []
} else {
message.error(res.message || '加载数据失败')
}
} catch (error) {
console.error('加载部门列表失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
// 选择变化处理
const handleSelectChange = (record, selected, selectedRows) => {
if (selected) {
selection.value.push(record)
} else {
const index = selection.value.findIndex(item => item.id === record.id)
if (index > -1) {
selection.value.splice(index, 1)
}
}
}
// 全选/取消全选处理
const handleSelectAll = (selected, selectedRows, changeRows) => {
if (selected) {
changeRows.forEach(record => {
if (!selection.value.find(item => item.id === record.id)) {
selection.value.push(record)
}
})
} else {
changeRows.forEach(record => {
const index = selection.value.findIndex(item => item.id === record.id)
if (index > -1) {
selection.value.splice(index, 1)
}
})
}
}
// 搜索
const handleSearch = () => {
loadData()
}
// 重置
const handleReset = () => {
searchForm.keyword = ''
selection.value = []
loadData()
}
// 新增部门 // 新增部门
const handleAdd = () => { const handleAdd = () => {
@@ -196,7 +135,7 @@ const handleDelete = async (record) => {
const res = await authApi.department.delete.post({ id: record.id }) const res = await authApi.department.delete.post({ id: record.id })
if (res.code === 1) { if (res.code === 1) {
message.success('删除成功') message.success('删除成功')
loadData() refreshTable()
} else { } else {
message.error(res.message || '删除失败') message.error(res.message || '删除失败')
} }
@@ -208,25 +147,25 @@ const handleDelete = async (record) => {
// 批量删除 // 批量删除
const handleBatchDelete = () => { const handleBatchDelete = () => {
if (selection.value.length === 0) { if (selectedRows.value.length === 0) {
message.warning('请选择要删除的部门') message.warning('请选择要删除的部门')
return return
} }
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: `确定删除选中的 ${selection.value.length} 个部门吗?如果删除项中含有子集将会被一并删除`, content: `确定删除选中的 ${selectedRows.value.length} 个部门吗?如果删除项中含有子集将会被一并删除`,
okText: '确定', okText: '确定',
cancelText: '取消', cancelText: '取消',
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { try {
const ids = selection.value.map(item => item.id) const ids = selectedRows.value.map(item => item.id)
const res = await authApi.department.delete.post({ ids }) const res = await authApi.department.delete.post({ ids })
if (res.code === 1) { if (res.code === 1) {
message.success('删除成功') message.success('删除成功')
selection.value = [] selectedRows.value = []
loadData() refreshTable()
} else { } else {
message.error(res.message || '删除失败') message.error(res.message || '删除失败')
} }
@@ -238,32 +177,14 @@ const handleBatchDelete = () => {
}) })
} }
// 根据ID获取树节点
const filterTree = (id) => {
let target = null
function filter(tree) {
for (const item of tree) {
if (item.id === id) {
target = item
return
}
if (item.children) {
filter(item.children)
}
}
}
filter(tableData.value)
return target
}
// 保存成功回调 // 保存成功回调
const handleSaveSuccess = (data, mode) => { const handleSaveSuccess = () => {
loadData() refreshTable()
} }
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
loadData() refreshTable()
}) })
</script> </script>

View File

@@ -23,17 +23,17 @@
<a-button type="primary" @click="handleAdd"> <a-button type="primary" @click="handleAdd">
<template #icon><plus-outlined /></template> <template #icon><plus-outlined /></template>
</a-button> </a-button>
<a-button danger :disabled="selection.length === 0" @click="handleBatchDelete"> <a-button danger :disabled="selectedRows.length === 0" @click="handleBatchDelete">
<template #icon><delete-outlined /></template> <template #icon><delete-outlined /></template>
</a-button> </a-button>
<a-button :disabled="selection.length !== 1" @click="handlePermission"> <a-button :disabled="selectedRows.length !== 1" @click="handlePermission">
<template #icon><key-outlined /></template> <template #icon><key-outlined /></template>
</a-button> </a-button>
</div> </div>
</div> </div>
<div class="table-content"> <div class="table-content">
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" <scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
:pagination="pagination" :row-key="rowKey" :row-selection="rowSelection" @refresh="loadData" :pagination="pagination" :row-key="rowKey" :row-selection="rowSelection" @refresh="refreshTable"
@paginationChange="handlePaginationChange" @select="handleSelectChange" @selectAll="handleSelectAll"> @paginationChange="handlePaginationChange" @select="handleSelectChange" @selectAll="handleSelectAll">
<template #status="{ record }"> <template #status="{ record }">
<a-tag :color="record.status === 1 ? 'success' : 'error'"> <a-tag :color="record.status === 1 ? 'success' : 'error'">
@@ -62,19 +62,42 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive } from 'vue'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import scTable from '@/components/scTable/index.vue' import scTable from '@/components/scTable/index.vue'
import saveDialog from './save.vue' import saveDialog from './save.vue'
import permissionDialog from './permission.vue' import permissionDialog from './permission.vue'
import authApi from '@/api/auth' import authApi from '@/api/auth'
import { useTable } from '@/hooks/useTable'
defineOptions({ defineOptions({
name: 'authRole' name: 'authRole'
}) })
// 表格引用 // 使用useTable hooks
const tableRef = ref(null) const {
tableRef,
searchForm,
tableData,
loading,
pagination,
selectedRows,
rowSelection,
handleSearch,
handleReset,
handlePaginationChange,
handleSelectChange,
handleSelectAll,
refreshTable
} = useTable({
api: authApi.role.list.get,
searchForm: {
keyword: ''
},
columns: [],
needPagination: true,
needSelection: true
})
// 对话框状态 // 对话框状态
const dialog = reactive({ const dialog = reactive({
@@ -86,39 +109,9 @@ const dialog = reactive({
const saveDialogRef = ref(null) const saveDialogRef = ref(null)
const permissionDialogRef = ref(null) const permissionDialogRef = ref(null)
// 选中的行
const selection = ref([])
// 搜索表单
const searchForm = reactive({
keyword: ''
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['20', '50', '100', '200']
})
// 行key // 行key
const rowKey = 'id' const rowKey = 'id'
// 行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selection.value.map(item => item.id),
onChange: (selectedRowKeys, selectedRows) => {
selection.value = selectedRows
}
}))
// 表格列配置 // 表格列配置
const columns = [ const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' }, { title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' },
@@ -129,80 +122,6 @@ const columns = [
{ title: '操作', dataIndex: 'action', key: 'action', width: 200, align: 'center', slot: 'action', fixed: 'right' } { title: '操作', dataIndex: 'action', key: 'action', width: 200, align: 'center', slot: 'action', fixed: 'right' }
] ]
// 分页变化处理
const handlePaginationChange = ({ page, pageSize }) => {
pagination.current = page
pagination.pageSize = pageSize
loadData()
}
// 加载角色列表数据
const loadData = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
limit: pagination.pageSize,
...searchForm
}
const res = await authApi.role.list.get(params)
if (res.code === 1) {
tableData.value = res.data?.data || []
pagination.total = res.data?.total || 0
} else {
message.error(res.message || '加载数据失败')
}
} catch (error) {
console.error('加载角色列表失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
// 选择变化处理
const handleSelectChange = (record, selected, selectedRows) => {
if (selected) {
selection.value.push(record)
} else {
const index = selection.value.findIndex(item => item.id === record.id)
if (index > -1) {
selection.value.splice(index, 1)
}
}
}
// 全选/取消全选处理
const handleSelectAll = (selected, selectedRows, changeRows) => {
if (selected) {
changeRows.forEach(record => {
if (!selection.value.find(item => item.id === record.id)) {
selection.value.push(record)
}
})
} else {
changeRows.forEach(record => {
const index = selection.value.findIndex(item => item.id === record.id)
if (index > -1) {
selection.value.splice(index, 1)
}
})
}
}
// 搜索
const handleSearch = () => {
pagination.current = 1
loadData()
}
// 重置
const handleReset = () => {
searchForm.keyword = ''
selection.value = []
pagination.current = 1
loadData()
}
// 新增角色 // 新增角色
const handleAdd = () => { const handleAdd = () => {
@@ -234,7 +153,7 @@ const handleDelete = async (record) => {
const res = await authApi.role.delete.post({ id: record.id }) const res = await authApi.role.delete.post({ id: record.id })
if (res.code === 1) { if (res.code === 1) {
message.success('删除成功') message.success('删除成功')
loadData() refreshTable()
} else { } else {
message.error(res.message || '删除失败') message.error(res.message || '删除失败')
} }
@@ -246,25 +165,25 @@ const handleDelete = async (record) => {
// 批量删除 // 批量删除
const handleBatchDelete = () => { const handleBatchDelete = () => {
if (selection.value.length === 0) { if (selectedRows.value.length === 0) {
message.warning('请选择要删除的角色') message.warning('请选择要删除的角色')
return return
} }
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: `确定删除选中的 ${selection.value.length} 个角色吗?`, content: `确定删除选中的 ${selectedRows.value.length} 个角色吗?`,
okText: '确定', okText: '确定',
cancelText: '取消', cancelText: '取消',
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { try {
const ids = selection.value.map(item => item.id) const ids = selectedRows.value.map(item => item.id)
const res = await authApi.role.delete.post({ ids }) const res = await authApi.role.delete.post({ ids })
if (res.code === 1) { if (res.code === 1) {
message.success('删除成功') message.success('删除成功')
selection.value = [] selectedRows.value = []
loadData() refreshTable()
} else { } else {
message.error(res.message || '删除失败') message.error(res.message || '删除失败')
} }
@@ -278,34 +197,25 @@ const handleBatchDelete = () => {
// 权限设置 // 权限设置
const handlePermission = () => { const handlePermission = () => {
if (selection.value.length !== 1) { if (selectedRows.value.length !== 1) {
message.error('请选择一个角色进行权限设置') message.error('请选择一个角色进行权限设置')
return return
} }
dialog.permission = true dialog.permission = true
setTimeout(() => { setTimeout(() => {
permissionDialogRef.value?.open().setData(selection.value[0]) permissionDialogRef.value?.open().setData(selectedRows.value[0])
}, 0) }, 0)
} }
// 保存成功回调 // 保存成功回调
const handleSaveSuccess = (data, mode) => { const handleSaveSuccess = () => {
if (mode === 'add') { refreshTable()
loadData()
} else if (mode === 'edit') {
loadData()
}
} }
// 权限设置成功回调 // 权限设置成功回调
const permissionSuccess = () => { const permissionSuccess = () => {
loadData() refreshTable()
} }
// 初始化
onMounted(() => {
loadData()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -35,9 +35,9 @@
<a-button type="primary" @click="handleSearch"> <a-button type="primary" @click="handleSearch">
<template #icon><search-outlined /></template> <template #icon><search-outlined /></template>
</a-button> </a-button>
<a-button @click="handleReset"> <a-button @click="handleUserReset">
<template #icon><redo-outlined /></template> <template #icon><redo-outlined /></template>
</a-button> </a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -52,9 +52,9 @@
</div> </div>
</div> </div>
<div class="table-content"> <div class="table-content">
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" <scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
:pagination="pagination" :row-key="rowKey" @refresh="loadData" :pagination="pagination" :row-key="rowKey" @refresh="refreshTable"
@paginationChange="handlePaginationChange"> @paginationChange="handlePaginationChange">
<template #avatar="{ record }"> <template #avatar="{ record }">
<a-avatar :src="record.avatar" :size="32"> <a-avatar :src="record.avatar" :size="32">
<template #icon><user-outlined /></template> <template #icon><user-outlined /></template>
@@ -102,13 +102,33 @@ import scTable from '@/components/scTable/index.vue'
import saveDialog from './save.vue' import saveDialog from './save.vue'
import roleDialog from './role.vue' import roleDialog from './role.vue'
import authApi from '@/api/auth' import authApi from '@/api/auth'
import { useTable } from '@/hooks/useTable'
defineOptions({ defineOptions({
name: 'authUser' name: 'authUser'
}) })
// 表格引用 // 使用useTable hooks
const tableRef = ref(null) const {
tableRef,
searchForm,
tableData,
loading,
pagination,
handleSearch,
handleReset,
handlePaginationChange,
refreshTable
} = useTable({
api: authApi.users.list.get,
searchForm: {
username: '',
nickname: '',
department_id: null
},
columns: [],
needPagination: true
})
// 对话框状态 // 对话框状态
const dialog = reactive({ const dialog = reactive({
@@ -126,37 +146,9 @@ const filteredDepartmentTree = ref([])
const selectedDeptKeys = ref([]) const selectedDeptKeys = ref([])
const departmentKeyword = ref('') const departmentKeyword = ref('')
// 搜索表单
const searchForm = reactive({
username: '',
nickname: '',
department_id: null
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['20', '50', '100', '200']
})
// 行key // 行key
const rowKey = 'id' const rowKey = 'id'
// 分页变化处理
const handlePaginationChange = ({ page, pageSize }) => {
pagination.current = page
pagination.pageSize = pageSize
loadData()
}
// 表格列配置 // 表格列配置
const columns = [ const columns = [
{ title: '头像', dataIndex: 'avatar', key: 'avatar', width: 80, align: 'center', slot: 'avatar' }, { title: '头像', dataIndex: 'avatar', key: 'avatar', width: 80, align: 'center', slot: 'avatar' },
@@ -210,34 +202,15 @@ const handleDeptSearch = (e) => {
filteredDepartmentTree.value = filterTree(departmentTree.value) filteredDepartmentTree.value = filterTree(departmentTree.value)
} }
// 加载用户列表数据 // 重置 - 覆盖useTable的handleReset以添加额外逻辑
const loadData = async () => { const handleUserReset = () => {
loading.value = true searchForm.username = ''
try { searchForm.nickname = ''
const params = { searchForm.department_id = null
page: pagination.current, selectedDeptKeys.value = []
limit: pagination.pageSize, departmentKeyword.value = ''
...searchForm filteredDepartmentTree.value = departmentTree.value
} handleReset()
const res = await authApi.users.list.get(params)
if (res.code === 1) {
tableData.value = res.data?.data || []
pagination.total = res.data?.total || 0
} else {
message.error(res.message || '加载数据失败')
}
} catch (error) {
console.error('加载用户列表失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
// 表格变化处理(排序、筛选)
const handleTableChange = (pagination, filters, sorter) => {
// 如果需要处理排序或筛选,可以在这里添加逻辑
console.log('表格变化:', { pagination, filters, sorter })
} }
// 部门选择事件 // 部门选择事件
@@ -247,31 +220,7 @@ const onDeptSelect = (selectedKeys) => {
} else { } else {
searchForm.department_id = null searchForm.department_id = null
} }
pagination.current = 1 handleSearch()
loadData()
}
// 搜索
const handleSearch = () => {
pagination.current = 1
loadData()
}
// 重置
const handleReset = () => {
searchForm.username = ''
searchForm.nickname = ''
searchForm.department_id = null
selectedDeptKeys.value = []
departmentKeyword.value = ''
filteredDepartmentTree.value = departmentTree.value
pagination.current = 1
loadData()
}
// 刷新表格
const refreshTable = () => {
loadData()
} }
// 导出数据 // 导出数据
@@ -320,7 +269,7 @@ const handleDelete = async (record) => {
const res = await authApi.users.delete.post({ id: record.id }) const res = await authApi.users.delete.post({ id: record.id })
if (res.code === 1) { if (res.code === 1) {
message.success('删除成功') message.success('删除成功')
loadData() refreshTable()
} else { } else {
message.error(res.message || '删除失败') message.error(res.message || '删除失败')
} }
@@ -331,23 +280,18 @@ const handleDelete = async (record) => {
} }
// 保存成功回调 // 保存成功回调
const handleSaveSuccess = (data, mode) => { const handleSaveSuccess = () => {
if (mode === 'add') { refreshTable()
loadData()
} else if (mode === 'edit') {
loadData()
}
} }
// 角色设置成功回调 // 角色设置成功回调
const handleRoleSuccess = () => { const handleRoleSuccess = () => {
loadData() refreshTable()
} }
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
loadDepartmentTree() loadDepartmentTree()
loadData()
}) })
</script> </script>

View File

@@ -28,7 +28,7 @@
<a-button type="primary" @click="handleSearch"> <a-button type="primary" @click="handleSearch">
<template #icon><search-outlined /></template> <template #icon><search-outlined /></template>
</a-button> </a-button>
<a-button @click="handleReset"> <a-button @click="handleLogReset">
<template #icon><redo-outlined /></template> <template #icon><redo-outlined /></template>
</a-button> </a-button>
</a-space> </a-space>
@@ -44,7 +44,7 @@
</div> </div>
<div class="table-content"> <div class="table-content">
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" <scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
:pagination="pagination" :row-key="rowKey" @refresh="loadData" @paginationChange="handlePaginationChange" :pagination="pagination" :row-key="rowKey" @refresh="refreshTable" @paginationChange="handlePaginationChange"
:row-selection="rowSelection"> :row-selection="rowSelection">
<template #method="{ record }"> <template #method="{ record }">
<a-tag :color="getMethodColor(record.method)">{{ record.method }}</a-tag> <a-tag :color="getMethodColor(record.method)">{{ record.method }}</a-tag>
@@ -67,19 +67,44 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import { SearchOutlined, RedoOutlined, DeleteOutlined, FolderOutlined, FileOutlined } from '@ant-design/icons-vue' import { SearchOutlined, RedoOutlined, DeleteOutlined, FolderOutlined, FileOutlined } from '@ant-design/icons-vue'
import scTable from '@/components/scTable/index.vue' import scTable from '@/components/scTable/index.vue'
import logInfoDialog from './info.vue' import logInfoDialog from './info.vue'
import systemApi from '@/api/system' import systemApi from '@/api/system'
import { useTable } from '@/hooks/useTable'
defineOptions({ defineOptions({
name: 'systemLog' name: 'systemLog'
}) })
// 表格引用 // 使用useTable hooks
const tableRef = ref(null) const {
tableRef,
searchForm,
tableData,
loading,
pagination,
selectedRowKeys,
rowSelection,
handleSearch,
handleReset,
handlePaginationChange,
refreshTable
} = useTable({
api: systemApi.log.list.get,
searchForm: {
title: '',
method: null,
status: null,
created_at: []
},
columns: [],
needPagination: true,
needSelection: true,
immediateLoad: false
})
// 对话框状态 // 对话框状态
const dialog = reactive({ const dialog = reactive({
@@ -89,6 +114,7 @@ const dialog = reactive({
// 弹窗引用 // 弹窗引用
const infoDialogRef = ref(null) const infoDialogRef = ref(null)
// 树数据 // 树数据
const treeData = ref([ const treeData = ref([
{ {
@@ -116,39 +142,6 @@ const treeData = ref([
// 选中的树节点 // 选中的树节点
const selectedKeys = ref([]) const selectedKeys = ref([])
// 选中的行
const selectedRowKeys = ref([])
// 行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (selectedKeys) => {
selectedRowKeys.value = selectedKeys
},
}))
// 搜索表单
const searchForm = reactive({
title: '',
method: null,
status: null,
created_at: []
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['20', '50', '100', '200']
})
// 行key // 行key
const rowKey = 'id' const rowKey = 'id'
@@ -175,40 +168,6 @@ const columns = [
{ title: '操作', dataIndex: 'action', key: 'action', width: 150, align: 'center', slot: 'action', fixed: 'right' } { title: '操作', dataIndex: 'action', key: 'action', width: 150, align: 'center', slot: 'action', fixed: 'right' }
] ]
// 加载日志列表数据
const loadData = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
limit: pagination.pageSize,
title: searchForm.title,
method: searchForm.method,
status: searchForm.status,
created_at: searchForm.created_at
}
const res = await systemApi.log.list.get(params)
if (res.code === 1) {
tableData.value = res.data?.data || []
pagination.total = res.data?.total || 0
} else {
message.error(res.message || '加载数据失败')
}
} catch (error) {
console.error('加载日志列表失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
// 分页变化处理
const handlePaginationChange = ({ page, pageSize }) => {
pagination.current = page
pagination.pageSize = pageSize
loadData()
}
// 树节点选择事件 // 树节点选择事件
const onSelect = (selectedKeys, info) => { const onSelect = (selectedKeys, info) => {
@@ -230,27 +189,10 @@ const onSelect = (selectedKeys, info) => {
searchForm.status = null searchForm.status = null
} }
pagination.current = 1 pagination.current = 1
loadData() refreshTable()
} }
} }
// 搜索
const handleSearch = () => {
pagination.current = 1
loadData()
}
// 重置
const handleReset = () => {
searchForm.title = ''
searchForm.method = null
searchForm.status = null
searchForm.timeRange = []
selectedKeys.value = []
pagination.current = 1
loadData()
}
// 查看日志详情 // 查看日志详情
const handleView = (record) => { const handleView = (record) => {
dialog.info = true dialog.info = true
@@ -265,7 +207,7 @@ const handleDelete = async (record) => {
const res = await systemApi.log.delete.post({ id: record.id }) const res = await systemApi.log.delete.post({ id: record.id })
if (res.code === 1) { if (res.code === 1) {
message.success('删除成功') message.success('删除成功')
loadData() refreshTable()
selectedRowKeys.value = [] selectedRowKeys.value = []
} else { } else {
message.error(res.message || '删除失败') message.error(res.message || '删除失败')
@@ -295,7 +237,7 @@ const handleClearLog = () => {
await Promise.all(promises) await Promise.all(promises)
message.success('清空成功') message.success('清空成功')
selectedRowKeys.value = [] selectedRowKeys.value = []
loadData() refreshTable()
} catch (error) { } catch (error) {
console.error('清空日志失败:', error) console.error('清空日志失败:', error)
message.error('清空失败') message.error('清空失败')
@@ -306,7 +248,7 @@ const handleClearLog = () => {
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
loadData() refreshTable()
}) })
</script> </script>

View File

@@ -73,6 +73,7 @@ function transformMenusToRoutes(menus) {
icon: menu.meta?.icon || menu.icon, icon: menu.meta?.icon || menu.icon,
hidden: menu.hidden || menu.meta?.hidden, hidden: menu.hidden || menu.meta?.hidden,
keepAlive: menu.meta?.keepAlive || false, keepAlive: menu.meta?.keepAlive || false,
affix: menu.meta?.affix || 0,
role: menu.meta?.role || [] role: menu.meta?.role || []
} }
} }

View File

@@ -26,6 +26,9 @@ export const useLayoutStore = defineStore(
// 视图标签页(用于记录页面滚动位置) // 视图标签页(用于记录页面滚动位置)
const viewTags = ref([]) const viewTags = ref([])
// 刷新标签的 key用于触发组件刷新
const refreshKey = ref(0)
// 切换侧边栏折叠 // 切换侧边栏折叠
const toggleSidebar = () => { const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value sidebarCollapsed.value = !sidebarCollapsed.value
@@ -81,6 +84,11 @@ export const useLayoutStore = defineStore(
showBreadcrumb.value = show showBreadcrumb.value = show
} }
// 刷新标签
const refreshTag = () => {
refreshKey.value++
}
// 重置主题设置 // 重置主题设置
const resetTheme = () => { const resetTheme = () => {
themeColor.value = '#1890ff' themeColor.value = '#1890ff'
@@ -98,6 +106,7 @@ export const useLayoutStore = defineStore(
themeColor, themeColor,
showTags, showTags,
showBreadcrumb, showBreadcrumb,
refreshKey,
toggleSidebar, toggleSidebar,
setLayoutMode, setLayoutMode,
setSelectedParentMenu, setSelectedParentMenu,
@@ -108,13 +117,14 @@ export const useLayoutStore = defineStore(
setShowTags, setShowTags,
setShowBreadcrumb, setShowBreadcrumb,
resetTheme, resetTheme,
refreshTag,
} }
}, },
{ {
persist: { persist: {
key: 'layout-store', key: 'layout-store',
storage: customStorage, storage: customStorage,
pick: ['layoutMode', 'sidebarCollapsed', 'themeColor', 'showTags', 'showBreadcrumb'], pick: ['layoutMode', 'sidebarCollapsed', 'themeColor', 'showTags', 'showBreadcrumb', 'viewTags'],
}, },
}, },
) )

View File

@@ -1048,6 +1048,11 @@
resolved "https://mirrors.huaweicloud.com/repository/npm/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31" resolved "https://mirrors.huaweicloud.com/repository/npm/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31"
integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA== integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
"@element-plus/icons-vue@^2.3.2":
version "2.3.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz#7e9cb231fb738b2056f33e22c3a29e214b538dcf"
integrity sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==
"@emotion/hash@^0.9.0": "@emotion/hash@^0.9.0":
version "0.9.2" version "0.9.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" resolved "https://mirrors.huaweicloud.com/repository/npm/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b"