功能模块:添加系统管理模块
- 添加用户管理(src/views/system/user/) - 用户列表查询 - 用户新增编辑 - 用户角色分配 - 用户状态管理 - 添加角色管理(src/views/system/role/) - 角色列表管理 - 角色权限分配 - 菜单权限配置 - 添加菜单管理(src/views/system/menu/) - 菜单树形展示 - 菜单层级管理 - 菜单图标配置 - 添加字典管理(src/views/system/dict/) - 字典类型管理 - 字典数据管理 - 字典缓存刷新 - 添加文件管理(src/views/system/file/) - 文件上传下载 - 文件分类管理 - 文件预览功能 - 添加图片管理(src/views/system/picture/) - 图片上传管理 - 图片预览浏览 - 添加日志管理(src/views/system/log/) - 登录日志查询 - 操作日志记录
This commit is contained in:
parent
e8a78fa8b6
commit
b64d303570
1090
src/views/system/dict/data.vue
Normal file
1090
src/views/system/dict/data.vue
Normal file
File diff suppressed because it is too large
Load Diff
907
src/views/system/dict/index.vue
Normal file
907
src/views/system/dict/index.vue
Normal file
@ -0,0 +1,907 @@
|
||||
<template>
|
||||
<div class="dict-management p-1 bg-gray-50 h-screen flex flex-col">
|
||||
<!-- 搜索表单和字典表格 -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 搜索表单 -->
|
||||
<div class="px-4 py-2 border-b border-gray-100">
|
||||
<n-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
class="search-form"
|
||||
>
|
||||
<n-grid :cols="4" :x-gap="8" :y-gap="4">
|
||||
<n-grid-item>
|
||||
<n-form-item label="字典名称" path="dictName">
|
||||
<n-input
|
||||
v-model:value="searchForm.dictName"
|
||||
placeholder="请输入字典名称"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="字典类型" path="dictType">
|
||||
<n-input
|
||||
v-model:value="searchForm.dictType"
|
||||
placeholder="请输入字典类型"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="状态" path="dictStatus">
|
||||
<n-select
|
||||
v-model:value="searchForm.dictStatus"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="" path="">
|
||||
<NSpace>
|
||||
<NButton type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline:search /></NIcon>
|
||||
</template>
|
||||
搜索
|
||||
</NButton>
|
||||
<NButton @click="handleReset">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline:refresh /></NIcon>
|
||||
</template>
|
||||
重置
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 表格头部操作栏 -->
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
||||
<div class="flex items-center gap-4">
|
||||
<NButton
|
||||
v-permission="'system:dict:add'"
|
||||
type="primary"
|
||||
class="px-3 flex items-center"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||
<icon-park-outline:plus />
|
||||
</NIcon>
|
||||
</template>
|
||||
新增
|
||||
</NButton>
|
||||
|
||||
<NButton
|
||||
v-permission="'system:dict:delete'"
|
||||
type="error"
|
||||
:disabled="selectedRows.length === 0"
|
||||
class="px-3 flex items-center"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||
<icon-park-outline:delete />
|
||||
</NIcon>
|
||||
</template>
|
||||
批量删除
|
||||
</NButton>
|
||||
|
||||
<NButton
|
||||
v-permission="'system:dict:update'"
|
||||
type="info"
|
||||
class="px-3 flex items-center"
|
||||
@click="handleSyncCache"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||
<icon-park-outline:refresh />
|
||||
</NIcon>
|
||||
</template>
|
||||
同步缓存
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<div class="table-wrapper flex-1 overflow-auto pt-4">
|
||||
<!-- 数据表格 -->
|
||||
<n-data-table
|
||||
v-if="tableData.length > 0 || loading"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:row-key="rowKey"
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
:scroll-x="1450"
|
||||
size="medium"
|
||||
class="custom-table"
|
||||
@update:checked-row-keys="handleRowSelectionChange"
|
||||
/>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<CoiEmpty
|
||||
v-else
|
||||
type="nodata"
|
||||
title="暂无数据"
|
||||
description="当前没有字典类型数据"
|
||||
:show-action="true"
|
||||
>
|
||||
<template #action>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="medium"
|
||||
round
|
||||
@click="handleAdd"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<icon-park-outline:plus />
|
||||
</NIcon>
|
||||
</template>
|
||||
新增字典类型
|
||||
</NButton>
|
||||
</template>
|
||||
</CoiEmpty>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div v-if="tableData.length > 0" class="border-t border-gray-100">
|
||||
<CoiPagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:show-size-picker="true"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹框 -->
|
||||
<CoiDialog
|
||||
ref="formDialogRef"
|
||||
:title="modalTitle"
|
||||
:width="800"
|
||||
height="auto"
|
||||
confirm-text="确定"
|
||||
cancel-text="取消"
|
||||
@coi-confirm="handleSubmit"
|
||||
@coi-cancel="handleCancel"
|
||||
>
|
||||
<template #content>
|
||||
<div class="px-3 py-2">
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="90px"
|
||||
require-mark-placement="right-hanging"
|
||||
class="compact-form"
|
||||
>
|
||||
<n-grid :cols="2" :x-gap="10">
|
||||
<n-grid-item>
|
||||
<n-form-item label="字典名称" path="dictName" class="mb-2">
|
||||
<n-input
|
||||
v-model:value="formData.dictName"
|
||||
placeholder="请输入字典名称"
|
||||
maxlength="50"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form-item label="字典类型" path="dictType" class="mb-2">
|
||||
<n-input
|
||||
v-model:value="formData.dictType"
|
||||
placeholder="请输入字典类型"
|
||||
:disabled="isEdit"
|
||||
maxlength="50"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form-item label="状态" path="dictStatus" class="mb-2">
|
||||
<n-select
|
||||
v-model:value="formData.dictStatus"
|
||||
placeholder="请选择状态"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<n-form-item label="备注" path="remark" class="mb-2">
|
||||
<n-input
|
||||
v-model:value="formData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
</CoiDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
|
||||
import { NButton, NIcon, NPopconfirm, NSpace, NSwitch } from 'naive-ui'
|
||||
import CoiDialog from '@/components/common/CoiDialog.vue'
|
||||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||
import CoiPagination from '@/components/common/CoiPagination.vue'
|
||||
import { useDict, usePermission } from '@/hooks'
|
||||
import { useDictStore } from '@/store'
|
||||
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||
import {
|
||||
addDictType,
|
||||
batchDeleteDictType,
|
||||
deleteDictType,
|
||||
DictStatus,
|
||||
getDictTypeList,
|
||||
syncDictCache,
|
||||
updateDictType,
|
||||
updateDictTypeStatus,
|
||||
} from '@/service/api/system/dict'
|
||||
import type {
|
||||
DictTypeForm,
|
||||
DictTypeSearchForm,
|
||||
DictTypeVo,
|
||||
} from '@/service/api/system/dict'
|
||||
|
||||
// 图标导入
|
||||
import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
|
||||
import IconParkOutlineEdit from '~icons/icon-park-outline/edit'
|
||||
|
||||
// 路由和权限验证
|
||||
const router = useRouter()
|
||||
const { hasPermission } = usePermission()
|
||||
const { getSelectOptions, getDictLabel } = useDict(['sys_switch_status'])
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const tableData = ref<DictTypeVo[]>([])
|
||||
const selectedRows = ref<DictTypeVo[]>([])
|
||||
const checkedRowKeys = ref<string[]>([])
|
||||
const searchFormRef = ref<FormInst>()
|
||||
const formRef = ref<FormInst>()
|
||||
const formDialogRef = ref()
|
||||
const isEdit = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const dictStore = useDictStore()
|
||||
|
||||
// 搜索表单数据
|
||||
const searchForm = reactive<DictTypeSearchForm>({
|
||||
dictName: '',
|
||||
dictType: '',
|
||||
dictStatus: '',
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<DictTypeForm>({
|
||||
dictId: '',
|
||||
dictType: '',
|
||||
dictName: '',
|
||||
dictStatus: DictStatus.NORMAL,
|
||||
remark: '',
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
})
|
||||
|
||||
// 分页处理函数
|
||||
function handlePageChange(page: number) {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = computed(() => getSelectOptions('sys_switch_status'))
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
dictName: [
|
||||
{ required: true, message: '请输入字典名称', trigger: ['blur', 'input'] },
|
||||
{ min: 1, max: 50, message: '字典名称长度在1到50个字符', trigger: ['blur', 'input'] },
|
||||
],
|
||||
dictType: [
|
||||
{ required: true, message: '请输入字典类型', trigger: ['blur', 'input'] },
|
||||
{ min: 1, max: 50, message: '字典类型长度在1到50个字符', trigger: ['blur', 'input'] },
|
||||
{ pattern: /^[a-z_]\w*$/i, message: '字典类型只能包含字母、数字、下划线,且不能以数字开头', trigger: ['blur', 'input'] },
|
||||
],
|
||||
dictStatus: [
|
||||
{ required: true, message: '请选择状态', trigger: ['blur', 'change'] },
|
||||
],
|
||||
}
|
||||
|
||||
// 表格行键
|
||||
const rowKey = (row: DictTypeVo) => row.dictId
|
||||
|
||||
// 表格列配置
|
||||
const columns: DataTableColumns<DictTypeVo> = [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 70,
|
||||
align: 'center',
|
||||
render: (_, index) => {
|
||||
return (pagination.page - 1) * pagination.pageSize + index + 1
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '字典名称',
|
||||
key: 'dictName',
|
||||
width: 150,
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{
|
||||
title: '字典类型',
|
||||
key: 'dictType',
|
||||
width: 150,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('a', {
|
||||
class: 'dict-type-clickable-link',
|
||||
style: {
|
||||
color: '#326C72',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'inline-block',
|
||||
},
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
handleViewDictData(row)
|
||||
},
|
||||
onMouseenter: (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
target.style.color = '#4A9BA1'
|
||||
target.style.textDecoration = 'underline'
|
||||
},
|
||||
onMouseleave: (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
target.style.color = '#326C72'
|
||||
target.style.textDecoration = 'none'
|
||||
},
|
||||
}, row.dictType)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '字典状态',
|
||||
key: 'dictStatus',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('div', { class: 'flex items-center justify-center' }, [
|
||||
h(NPopconfirm, {
|
||||
onPositiveClick: () => handleStatusChange(row),
|
||||
negativeText: '取消',
|
||||
positiveText: '确定',
|
||||
}, {
|
||||
default: () => `确定要将字典类型「${row.dictName}」状态切换为「${getDictLabel('sys_switch_status', row.dictStatus === DictStatus.NORMAL ? '1' : '0')}」吗?`,
|
||||
trigger: () => h(NSwitch, {
|
||||
value: row.dictStatus === DictStatus.NORMAL,
|
||||
size: 'small',
|
||||
checkedChildren: getDictLabel('sys_switch_status', '0', '启用'),
|
||||
uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'),
|
||||
}),
|
||||
}),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'remark',
|
||||
width: 150,
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
width: 170,
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
key: 'updateTime',
|
||||
width: 170,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500 text-sm' }, row.updateTime || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '修改人',
|
||||
key: 'updateBy',
|
||||
width: 120,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-600' }, row.updateBy || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render: (row) => {
|
||||
const buttons = []
|
||||
|
||||
// 编辑按钮
|
||||
if (hasPermission('system:dict:update')) {
|
||||
buttons.push(h(NButton, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
class: 'action-btn-primary',
|
||||
onClick: () => handleEdit(row),
|
||||
}, {
|
||||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||
default: () => h(IconParkOutlineEdit),
|
||||
}),
|
||||
default: () => '编辑',
|
||||
}))
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
if (hasPermission('system:dict:delete')) {
|
||||
buttons.push(h(NPopconfirm, {
|
||||
onPositiveClick: () => handleDelete(row),
|
||||
negativeText: '取消',
|
||||
positiveText: '确定',
|
||||
}, {
|
||||
default: () => '确定删除此字典类型吗?删除后将同时删除相关的字典数据!',
|
||||
trigger: () => h(NButton, {
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
size: 'small',
|
||||
class: 'action-btn-secondary action-btn-danger',
|
||||
}, {
|
||||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||
default: () => h(IconParkOutlineDelete),
|
||||
}),
|
||||
default: () => '删除',
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const _hasSearchConditions = computed(() => {
|
||||
return !!(searchForm.dictName || searchForm.dictType || searchForm.dictStatus)
|
||||
})
|
||||
|
||||
// 获取表格数据
|
||||
async function getTableData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
pageNo: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
dictName: searchForm.dictName || undefined,
|
||||
dictType: searchForm.dictType || undefined,
|
||||
dictStatus: searchForm.dictStatus || undefined,
|
||||
}
|
||||
|
||||
const { data, isSuccess } = await getDictTypeList(params)
|
||||
|
||||
if (isSuccess && data) {
|
||||
tableData.value = data.records
|
||||
pagination.itemCount = data.total
|
||||
}
|
||||
else {
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('获取数据失败')
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
function handleReset() {
|
||||
searchForm.dictName = ''
|
||||
searchForm.dictType = ''
|
||||
searchForm.dictStatus = ''
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
modalTitle.value = '新增字典类型'
|
||||
|
||||
// 重置表单数据
|
||||
Object.assign(formData, {
|
||||
dictId: '',
|
||||
dictType: '',
|
||||
dictName: '',
|
||||
dictStatus: DictStatus.NORMAL,
|
||||
remark: '',
|
||||
})
|
||||
|
||||
formDialogRef.value?.coiOpen()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(row: DictTypeVo) {
|
||||
isEdit.value = true
|
||||
modalTitle.value = '编辑字典类型'
|
||||
|
||||
// 填充表单数据
|
||||
Object.assign(formData, {
|
||||
dictId: row.dictId,
|
||||
dictType: row.dictType,
|
||||
dictName: row.dictName,
|
||||
dictStatus: row.dictStatus,
|
||||
remark: row.remark || '',
|
||||
})
|
||||
|
||||
formDialogRef.value?.coiOpen()
|
||||
}
|
||||
|
||||
// 删除
|
||||
async function handleDelete(row: DictTypeVo) {
|
||||
try {
|
||||
const { isSuccess } = await deleteDictType(row.dictId)
|
||||
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('删除成功')
|
||||
dictStore.invalidate()
|
||||
getTableData()
|
||||
}
|
||||
else {
|
||||
coiMsgError('删除失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('删除失败,请检查网络连接')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
async function handleBatchDelete() {
|
||||
if (checkedRowKeys.value.length === 0) {
|
||||
coiMsgWarning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await coiMsgBox(`确定删除选中的${checkedRowKeys.value.length}条记录吗?删除后将同时删除相关的字典数据!`, '批量删除确认')
|
||||
|
||||
const { isSuccess } = await batchDeleteDictType(checkedRowKeys.value)
|
||||
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('批量删除成功')
|
||||
checkedRowKeys.value = []
|
||||
selectedRows.value = []
|
||||
dictStore.invalidate()
|
||||
getTableData()
|
||||
}
|
||||
else {
|
||||
coiMsgError('批量删除失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// 用户取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
async function handleStatusChange(row: DictTypeVo) {
|
||||
const newStatus = row.dictStatus === DictStatus.NORMAL ? DictStatus.DISABLED : DictStatus.NORMAL
|
||||
|
||||
try {
|
||||
const { isSuccess } = await updateDictTypeStatus(row.dictId, newStatus)
|
||||
|
||||
if (isSuccess) {
|
||||
const statusLabel = getDictLabel('sys_switch_status', newStatus, newStatus === DictStatus.NORMAL ? '启用' : '停用')
|
||||
coiMsgSuccess(`状态已更新为「${statusLabel}」`)
|
||||
dictStore.invalidate()
|
||||
getTableData()
|
||||
}
|
||||
else {
|
||||
coiMsgError('状态修改失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('状态修改失败,请检查网络连接')
|
||||
}
|
||||
}
|
||||
|
||||
// 同步缓存
|
||||
async function handleSyncCache() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { isSuccess } = await syncDictCache()
|
||||
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('缓存同步成功')
|
||||
dictStore.invalidate()
|
||||
}
|
||||
else {
|
||||
coiMsgError('缓存同步失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('缓存同步失败,请检查网络连接')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 行选择变化
|
||||
function handleRowSelectionChange(rowKeys: string[]) {
|
||||
checkedRowKeys.value = rowKeys
|
||||
selectedRows.value = tableData.value.filter(row => rowKeys.includes(row.dictId))
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value)
|
||||
return
|
||||
|
||||
try {
|
||||
// 先进行表单验证
|
||||
await formRef.value.validate()
|
||||
}
|
||||
catch {
|
||||
// 表单验证失败,提示用户检查填写内容
|
||||
coiMsgWarning('验证失败,请检查填写内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 表单验证通过,执行API调用
|
||||
try {
|
||||
const submitData = { ...formData }
|
||||
|
||||
const { isSuccess } = isEdit.value ? await updateDictType(submitData) : await addDictType(submitData)
|
||||
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess(isEdit.value ? '修改成功' : '新增成功')
|
||||
formDialogRef.value?.coiClose()
|
||||
dictStore.invalidate()
|
||||
getTableData()
|
||||
}
|
||||
else {
|
||||
coiMsgError(isEdit.value ? '修改失败,请稍后重试' : '新增失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
coiMsgError(error.message || (isEdit.value ? '修改失败,请检查网络连接' : '新增失败,请检查网络连接'))
|
||||
}
|
||||
else {
|
||||
coiMsgError(isEdit.value ? '修改失败,请检查网络连接' : '新增失败,请检查网络连接')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查看字典数据
|
||||
function handleViewDictData(row: DictTypeVo) {
|
||||
router.push({
|
||||
path: '/system/dict/data',
|
||||
query: {
|
||||
dictType: row.dictType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
function handleCancel() {
|
||||
formDialogRef.value?.coiClose()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dict-management {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.custom-table :deep(.n-data-table-td) {
|
||||
padding: 8px 16px !important;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-th) {
|
||||
padding: 12px 16px !important;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-tr:hover .n-data-table-td) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 操作按钮样式 */
|
||||
.action-btn-primary {
|
||||
background-color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn-secondary {
|
||||
background-color: transparent;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-btn-secondary.action-btn-danger {
|
||||
color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.action-btn-secondary.action-btn-danger:hover {
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.custom-table :deep(.n-data-table-base-table) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 紧凑表单样式 */
|
||||
.compact-form :deep(.n-form-item) {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.compact-form :deep(.n-form-item .n-form-item-feedback-wrapper) {
|
||||
min-height: 0 !important;
|
||||
padding-top: 2px !important;
|
||||
}
|
||||
|
||||
.compact-form :deep(.n-form-item .n-form-item-label) {
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.compact-form :deep(.n-input),
|
||||
.compact-form :deep(.n-input-number),
|
||||
.compact-form :deep(.n-select),
|
||||
.compact-form :deep(.n-cascader),
|
||||
.compact-form :deep(.n-radio-group) {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.compact-form :deep(.n-input .n-input__input-el),
|
||||
.compact-form :deep(.n-input-number .n-input__input-el) {
|
||||
padding: 2px 1px !important;
|
||||
min-height: 32px !important;
|
||||
}
|
||||
|
||||
.compact-form :deep(.n-select .n-base-selection) {
|
||||
min-height: 32px !important;
|
||||
padding: 2px 1px !important;
|
||||
}
|
||||
|
||||
.compact-form :deep(.n-select .n-base-selection .n-base-selection-label) {
|
||||
padding: 0 6px !important;
|
||||
min-height: 30px !important;
|
||||
line-height: 30px !important;
|
||||
}
|
||||
|
||||
.compact-form :deep(.n-cascader .n-cascader-trigger) {
|
||||
min-height: 32px !important;
|
||||
padding: 2px 1px !important;
|
||||
}
|
||||
|
||||
/* 字典类型可点击链接样式 - 使用青绿色系配色 */
|
||||
.custom-table :deep(.dict-type-clickable-link) {
|
||||
color: #326C72 !important;
|
||||
cursor: pointer !important;
|
||||
text-decoration: none !important;
|
||||
font-weight: 500 !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.custom-table :deep(.dict-type-clickable-link:hover) {
|
||||
color: #4A9BA1 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.custom-table :deep(.dict-type-clickable-link:active) {
|
||||
color: #275A5F !important;
|
||||
}
|
||||
|
||||
/* 针对表格单元格内容的强制样式 */
|
||||
.custom-table :deep(.n-data-table-td .dict-type-clickable-link) {
|
||||
color: #326C72 !important;
|
||||
cursor: pointer !important;
|
||||
text-decoration: none !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* 更加具体的选择器以确保样式生效 */
|
||||
.dict-management .custom-table :deep(.n-data-table-tbody .n-data-table-tr .n-data-table-td .dict-type-clickable-link) {
|
||||
color: #326C72 !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
</style>
|
||||
1065
src/views/system/file/index.vue
Normal file
1065
src/views/system/file/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
737
src/views/system/loginlog/index.vue
Normal file
737
src/views/system/loginlog/index.vue
Normal file
@ -0,0 +1,737 @@
|
||||
<template>
|
||||
<div class="loginlog-management p-1 bg-gray-50 h-screen flex flex-col">
|
||||
<!-- 搜索表单和登录日志表格 -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 搜索表单 -->
|
||||
<div class="px-4 py-2 border-b border-gray-100">
|
||||
<n-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
class="search-form"
|
||||
>
|
||||
<n-grid :cols="6" :x-gap="8" :y-gap="4">
|
||||
<n-grid-item>
|
||||
<n-form-item label="用户名称" path="loginName">
|
||||
<n-input
|
||||
v-model:value="searchForm.loginName"
|
||||
placeholder="请输入用户名称"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="登录IP" path="loginIp">
|
||||
<n-input
|
||||
v-model:value="searchForm.loginIp"
|
||||
placeholder="请输入登录IP"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="设备名称" path="deviceName">
|
||||
<n-input
|
||||
v-model:value="searchForm.deviceName"
|
||||
placeholder="请输入设备名称"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="登录状态" path="loginStatus">
|
||||
<n-select
|
||||
v-model:value="searchForm.loginStatus"
|
||||
placeholder="请选择登录状态"
|
||||
clearable
|
||||
:options="loginStatusOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="登录时间" path="timeRange">
|
||||
<n-date-picker
|
||||
v-model:value="searchForm.timeRange"
|
||||
type="datetimerange"
|
||||
clearable
|
||||
placeholder="选择时间范围"
|
||||
:shortcuts="{
|
||||
今天: () => [new Date().setHours(0, 0, 0, 0), new Date().setHours(23, 59, 59, 999)],
|
||||
昨天: () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return [yesterday.setHours(0, 0, 0, 0), yesterday.setHours(23, 59, 59, 999)];
|
||||
},
|
||||
最近7天: () => [Date.now() - 7 * 24 * 60 * 60 * 1000, Date.now()],
|
||||
最近30天: () => [Date.now() - 30 * 24 * 60 * 60 * 1000, Date.now()],
|
||||
}"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="" path="">
|
||||
<NSpace>
|
||||
<NButton type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline-search /></NIcon>
|
||||
</template>
|
||||
搜索
|
||||
</NButton>
|
||||
<NButton @click="handleReset">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline-refresh /></NIcon>
|
||||
</template>
|
||||
重置
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 表格头部操作栏 -->
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
||||
<div class="flex items-center gap-4">
|
||||
<NButton
|
||||
v-permission="PERMISSIONS.LOGIN_LOG.DELETE"
|
||||
type="error"
|
||||
:disabled="selectedRows.length === 0"
|
||||
class="px-3 flex items-center"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||
<IconParkOutlineDelete />
|
||||
</NIcon>
|
||||
</template>
|
||||
删除
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>共 {{ pagination.itemCount }} 条</span>
|
||||
<NButton text @click="getLoginLogList">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline-refresh /></NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<div class="table-wrapper flex-1 overflow-auto pt-4">
|
||||
<!-- 数据表格 -->
|
||||
<n-data-table
|
||||
v-if="tableData.length > 0 || loading"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(row: LoginLogVo) => row.loginLogId"
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
:scroll-x="1400"
|
||||
size="medium"
|
||||
class="custom-table"
|
||||
@update:checked-row-keys="handleRowSelectionChange"
|
||||
/>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<CoiEmpty
|
||||
v-else
|
||||
:type="getEmptyType()"
|
||||
:title="getEmptyTitle()"
|
||||
:description="getEmptyDescription()"
|
||||
:show-action="true"
|
||||
:action-text="getEmptyActionText()"
|
||||
size="medium"
|
||||
@action="handleEmptyAction"
|
||||
>
|
||||
<template #action>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="medium"
|
||||
round
|
||||
@click="handleEmptyAction"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<icon-park-outline-refresh />
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ getEmptyActionText() }}
|
||||
</NButton>
|
||||
</template>
|
||||
</CoiEmpty>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div v-if="tableData.length > 0" class="border-t border-gray-100">
|
||||
<CoiPagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:show-size-picker="true"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:show-quick-jumper="true"
|
||||
:show-current-info="true"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, nextTick, onMounted, ref } from 'vue'
|
||||
import type { DataTableColumns, FormInst } from 'naive-ui'
|
||||
import { NButton, NIcon, NPopconfirm, NSpace } from 'naive-ui'
|
||||
import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
|
||||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||
import CoiPagination from '@/components/common/CoiPagination.vue'
|
||||
import DictTag from '@/components/common/DictTag.vue'
|
||||
import {
|
||||
batchDeleteLoginLog,
|
||||
deleteLoginLog,
|
||||
getLoginLogListPage,
|
||||
} from '@/service/api/system/loginlog'
|
||||
import type { LoginLogVo } from '@/service/api/system/loginlog'
|
||||
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||
import { PERMISSIONS } from '@/constants/permissions'
|
||||
import { useDict, usePermission } from '@/hooks'
|
||||
|
||||
// 权限相关
|
||||
const { hasPermission } = usePermission()
|
||||
const { dictOptions } = useDict(['sys_common_status'])
|
||||
|
||||
const loginStatusOptions = computed(() => {
|
||||
const options = dictOptions.sys_common_status || []
|
||||
return options
|
||||
.filter(option => option.dictValue === '1' || option.dictValue === '2')
|
||||
.map(option => ({
|
||||
label: option.dictLabel,
|
||||
value: option.dictValue === '1' ? '0' : '1',
|
||||
}))
|
||||
})
|
||||
|
||||
// 搜索表单数据
|
||||
interface LoginLogSearchForm {
|
||||
loginName?: string
|
||||
loginIp?: string
|
||||
deviceName?: string
|
||||
loginStatus?: string | null
|
||||
timeRange?: [number, number] | null
|
||||
beginTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const tableData = ref<LoginLogVo[]>([])
|
||||
const searchFormRef = ref<FormInst | null>(null)
|
||||
const selectedRows = ref<LoginLogVo[]>([])
|
||||
|
||||
// 分页数据
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
})
|
||||
|
||||
// 搜索表单数据
|
||||
const searchForm = ref<LoginLogSearchForm>({})
|
||||
|
||||
// 表格列定义
|
||||
const columns: DataTableColumns<LoginLogVo> = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
render: (_, index) => {
|
||||
return (pagination.value.page - 1) * pagination.value.pageSize + index + 1
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录用户',
|
||||
key: 'loginName',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-600 font-medium' }, row.loginName)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录IP',
|
||||
key: 'loginIp',
|
||||
width: 140,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-600' }, row.loginIp)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录地址',
|
||||
key: 'loginAddress',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500' }, row.loginAddress || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '浏览器',
|
||||
key: 'loginBrowser',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500' }, row.browser || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作系统',
|
||||
key: 'loginOs',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500' }, row.os || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '设备名称',
|
||||
key: 'deviceName',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500' }, row.deviceName || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录状态',
|
||||
key: 'loginStatus',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: row => h(DictTag, { dictType: 'sys_common_status', value: row.loginStatus === '0' ? '1' : '2' }),
|
||||
},
|
||||
{
|
||||
title: '登录信息',
|
||||
key: 'loginMsg',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500' }, row.message || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录时间',
|
||||
key: 'loginTime',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500 text-sm' }, row.loginTime)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render: (row) => {
|
||||
const buttons = []
|
||||
|
||||
// 删除按钮
|
||||
if (hasPermission(PERMISSIONS.LOGIN_LOG.DELETE)) {
|
||||
buttons.push(h(NPopconfirm, {
|
||||
onPositiveClick: () => handleDelete(row.loginLogId),
|
||||
negativeText: '取消',
|
||||
positiveText: '确定',
|
||||
}, {
|
||||
default: () => '确定删除此登录记录吗?',
|
||||
trigger: () => h(NButton, {
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
size: 'small',
|
||||
class: 'action-btn-secondary action-btn-danger',
|
||||
}, {
|
||||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||
default: () => h(IconParkOutlineDelete),
|
||||
}),
|
||||
default: () => '删除',
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 获取登录日志列表
|
||||
async function getLoginLogList() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 构建请求参数,处理时间范围
|
||||
const { timeRange, ...otherParams } = searchForm.value
|
||||
|
||||
// 过滤掉空值和null值,只保留有效的搜索条件
|
||||
const filteredParams = Object.entries(otherParams).reduce((acc, [key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
acc[key] = value
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
const params: Record<string, any> = {
|
||||
pageNo: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
...filteredParams,
|
||||
}
|
||||
|
||||
// 处理时间范围,转换为beginTime和endTime
|
||||
if (timeRange && timeRange.length === 2) {
|
||||
params.beginTime = new Date(timeRange[0]).toISOString().slice(0, 19).replace('T', ' ')
|
||||
params.endTime = new Date(timeRange[1]).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
|
||||
const { isSuccess, data } = await getLoginLogListPage(params)
|
||||
|
||||
if (isSuccess && data) {
|
||||
tableData.value = data.records || []
|
||||
pagination.value.itemCount = data.total || 0
|
||||
}
|
||||
else {
|
||||
console.warn('获取登录日志列表失败,可能是权限或网络问题')
|
||||
coiMsgError('获取登录日志列表失败,请检查网络连接或联系管理员')
|
||||
tableData.value = []
|
||||
pagination.value.itemCount = 0
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取登录日志列表失败:', error)
|
||||
coiMsgError('获取登录日志列表失败,请检查网络连接')
|
||||
tableData.value = []
|
||||
pagination.value.itemCount = 0
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化处理
|
||||
function handlePageChange(page: number) {
|
||||
pagination.value.page = page
|
||||
getLoginLogList()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
getLoginLogList()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.value.page = 1
|
||||
getLoginLogList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
async function handleReset() {
|
||||
// 重置搜索表单的所有字段
|
||||
searchForm.value = {
|
||||
loginName: '',
|
||||
loginIp: '',
|
||||
deviceName: '',
|
||||
loginStatus: null,
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
timeRange: null,
|
||||
}
|
||||
|
||||
// 使用 nextTick 确保 DOM 更新后再重置表单状态
|
||||
await nextTick()
|
||||
|
||||
// 重置表单验证状态
|
||||
if (searchFormRef.value) {
|
||||
searchFormRef.value.restoreValidation()
|
||||
}
|
||||
|
||||
// 重置分页到第一页
|
||||
pagination.value.page = 1
|
||||
// 重新获取数据
|
||||
getLoginLogList()
|
||||
}
|
||||
|
||||
// 行选择变化
|
||||
function handleRowSelectionChange(rowKeys: number[]) {
|
||||
selectedRows.value = tableData.value.filter(row => rowKeys.includes(row.loginLogId))
|
||||
}
|
||||
|
||||
// 删除登录日志
|
||||
async function handleDelete(loginLogId: number) {
|
||||
try {
|
||||
const { isSuccess } = await deleteLoginLog(loginLogId)
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('删除成功')
|
||||
await getLoginLogList()
|
||||
}
|
||||
else {
|
||||
coiMsgError('删除失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
coiMsgError('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除登录日志
|
||||
async function handleBatchDelete() {
|
||||
if (selectedRows.value.length === 0) {
|
||||
coiMsgWarning('请先选择要删除的登录记录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 确认删除操作
|
||||
await coiMsgBox(`确定要删除选中的 ${selectedRows.value.length} 条登录记录吗?`, '批量删除确认')
|
||||
}
|
||||
catch {
|
||||
// 用户取消删除操作,直接返回,不显示任何提示
|
||||
return
|
||||
}
|
||||
|
||||
// 用户确认删除,执行删除操作
|
||||
try {
|
||||
const logIds = selectedRows.value.map(log => log.loginLogId)
|
||||
const { isSuccess } = await batchDeleteLoginLog(logIds)
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('批量删除成功')
|
||||
selectedRows.value = []
|
||||
await getLoginLogList()
|
||||
}
|
||||
else {
|
||||
coiMsgError('批量删除失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
coiMsgError('批量删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否有搜索条件
|
||||
function hasSearchConditions() {
|
||||
return Object.values(searchForm.value).some((value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 获取空状态类型
|
||||
function getEmptyType() {
|
||||
if (hasSearchConditions()) {
|
||||
return 'search'
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
// 获取空状态标题
|
||||
function getEmptyTitle() {
|
||||
if (hasSearchConditions()) {
|
||||
return '搜索无结果'
|
||||
}
|
||||
return '暂无登录日志'
|
||||
}
|
||||
|
||||
// 获取空状态描述
|
||||
function getEmptyDescription() {
|
||||
if (hasSearchConditions()) {
|
||||
return '未找到符合搜索条件的登录记录,请尝试调整搜索条件或重置筛选'
|
||||
}
|
||||
return '当前还没有登录日志数据'
|
||||
}
|
||||
|
||||
// 获取空状态操作文字
|
||||
function getEmptyActionText() {
|
||||
if (hasSearchConditions()) {
|
||||
return '重置筛选'
|
||||
}
|
||||
return '刷新数据'
|
||||
}
|
||||
|
||||
// 处理空状态操作
|
||||
function handleEmptyAction() {
|
||||
if (hasSearchConditions()) {
|
||||
handleReset()
|
||||
}
|
||||
else {
|
||||
getLoginLogList()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
getLoginLogList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loginlog-management {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-td) {
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-th) {
|
||||
padding: 8px 10px;
|
||||
background-color: #fafafa;
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-tr:hover .n-data-table-td) {
|
||||
background-color: #f8faff;
|
||||
}
|
||||
|
||||
.search-form :deep(.n-form-item-label) {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* CoiEmpty按钮样式 */
|
||||
|
||||
/* 操作按钮样式优化 */
|
||||
.custom-table :deep(.n-button--small-type) {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* 标签尺寸优化 */
|
||||
.custom-table :deep(.n-tag--small-type) {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
/* 统一按钮样式系统 - 支持动态主题色彩 */
|
||||
.action-btn-secondary {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.action-btn-secondary:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
.action-btn-secondary:active {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
.action-btn-danger {
|
||||
border-color: var(--error-color) !important;
|
||||
color: var(--error-color) !important;
|
||||
}
|
||||
|
||||
.action-btn-danger:hover {
|
||||
border-color: var(--error-color-hover) !important;
|
||||
color: var(--error-color-hover) !important;
|
||||
background: var(--error-color-suppl) !important;
|
||||
}
|
||||
|
||||
/* 登录日志详情样式 */
|
||||
.login-log-detail :deep(.n-descriptions-item-label) {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.login-log-detail :deep(.n-descriptions-item-content) {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.table-wrapper {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-corner {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
/* 表格滚动条样式 */
|
||||
.custom-table :deep(.n-data-table-base-table) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-track {
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb {
|
||||
background: #dee2e6;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb:hover {
|
||||
background: #adb5bd;
|
||||
}
|
||||
</style>
|
||||
1847
src/views/system/menu/index.vue
Normal file
1847
src/views/system/menu/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
975
src/views/system/operlog/index.vue
Normal file
975
src/views/system/operlog/index.vue
Normal file
@ -0,0 +1,975 @@
|
||||
<template>
|
||||
<div class="operlog-management p-1 bg-gray-50 h-screen flex flex-col">
|
||||
<!-- 搜索表单和操作日志表格 -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 搜索表单 -->
|
||||
<div class="px-4 py-2 border-b border-gray-100">
|
||||
<n-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
class="search-form"
|
||||
>
|
||||
<n-grid :cols="7" :x-gap="8" :y-gap="4">
|
||||
<n-grid-item>
|
||||
<n-form-item label="操作名称" path="operName">
|
||||
<n-input
|
||||
v-model:value="searchForm.operName"
|
||||
placeholder="请输入操作名称"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="操作人员" path="operMan">
|
||||
<n-input
|
||||
v-model:value="searchForm.operMan"
|
||||
placeholder="请输入操作人员"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="操作状态" path="operStatus">
|
||||
<n-select
|
||||
v-model:value="searchForm.operStatus"
|
||||
placeholder="请选择操作状态"
|
||||
clearable
|
||||
:options="operStatusOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="操作IP" path="operIp">
|
||||
<n-input
|
||||
v-model:value="searchForm.operIp"
|
||||
placeholder="请输入操作IP"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="操作时间" path="timeRange">
|
||||
<n-date-picker
|
||||
v-model:value="searchForm.timeRange"
|
||||
type="datetimerange"
|
||||
clearable
|
||||
placeholder="选择时间范围"
|
||||
:shortcuts="{
|
||||
今天: () => [new Date().setHours(0, 0, 0, 0), new Date().setHours(23, 59, 59, 999)],
|
||||
昨天: () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return [yesterday.setHours(0, 0, 0, 0), yesterday.setHours(23, 59, 59, 999)];
|
||||
},
|
||||
最近7天: () => [Date.now() - 7 * 24 * 60 * 60 * 1000, Date.now()],
|
||||
最近30天: () => [Date.now() - 30 * 24 * 60 * 60 * 1000, Date.now()],
|
||||
}"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-form-item label="" path="">
|
||||
<NSpace>
|
||||
<NButton type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline-search /></NIcon>
|
||||
</template>
|
||||
搜索
|
||||
</NButton>
|
||||
<NButton @click="handleReset">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline-refresh /></NIcon>
|
||||
</template>
|
||||
重置
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 表格头部操作栏 -->
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
||||
<div class="flex items-center gap-4">
|
||||
<NButton
|
||||
v-button="PERMISSIONS.OPER_LOG.DELETE"
|
||||
type="error"
|
||||
:disabled="selectedRows.length === 0"
|
||||
class="px-3 flex items-center"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||
<IconParkOutlineDelete />
|
||||
</NIcon>
|
||||
</template>
|
||||
删除
|
||||
</NButton>
|
||||
|
||||
<NButton
|
||||
v-button="PERMISSIONS.OPER_LOG.DELETE"
|
||||
type="warning"
|
||||
class="px-3 flex items-center"
|
||||
@click="handleClearAll"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||
<IconParkOutlineDelete />
|
||||
</NIcon>
|
||||
</template>
|
||||
清空
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>共 {{ pagination.itemCount }} 条</span>
|
||||
<NButton text @click="getOperLogList">
|
||||
<template #icon>
|
||||
<NIcon><icon-park-outline-refresh /></NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<div class="table-wrapper flex-1 overflow-auto pt-4">
|
||||
<!-- 数据表格 -->
|
||||
<n-data-table
|
||||
v-if="tableData.length > 0 || loading"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(row: OperLogVo) => row.operId"
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
:scroll-x="1700"
|
||||
size="medium"
|
||||
class="custom-table"
|
||||
@update:checked-row-keys="handleRowSelectionChange"
|
||||
/>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<CoiEmpty
|
||||
v-else
|
||||
:type="getEmptyType()"
|
||||
:title="getEmptyTitle()"
|
||||
:description="getEmptyDescription()"
|
||||
:show-action="true"
|
||||
:action-text="getEmptyActionText()"
|
||||
size="medium"
|
||||
@action="handleEmptyAction"
|
||||
>
|
||||
<template #action>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="medium"
|
||||
round
|
||||
@click="handleEmptyAction"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<icon-park-outline-refresh />
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ getEmptyActionText() }}
|
||||
</NButton>
|
||||
</template>
|
||||
</CoiEmpty>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div v-if="tableData.length > 0" class="border-t border-gray-100">
|
||||
<CoiPagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:show-size-picker="true"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:show-quick-jumper="true"
|
||||
:show-current-info="true"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志详情弹框 -->
|
||||
<CoiDialog
|
||||
ref="detailDialogRef"
|
||||
title="操作日志详情"
|
||||
:width="900"
|
||||
height="auto"
|
||||
cancel-text="关闭"
|
||||
:show-confirm="false"
|
||||
@coi-cancel="handleCloseDetail"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-3">
|
||||
<n-descriptions
|
||||
v-if="currentLogDetail"
|
||||
:column="2"
|
||||
bordered
|
||||
label-placement="left"
|
||||
class="oper-log-detail"
|
||||
>
|
||||
<n-descriptions-item label="方法名称" :span="2">
|
||||
<span class="method-name">{{ currentLogDetail.methodName }}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="消耗时间[毫秒]">
|
||||
{{ currentLogDetail.costTime || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="操作状态">
|
||||
<DictTag dict-type="sys_common_status" :value="currentLogDetail.operStatus === '0' ? '1' : '2'" />
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item v-if="currentLogDetail.operParam" label="请求参数" :span="2">
|
||||
<div class="json-container">
|
||||
<pre class="json-content">{{ formatJson(currentLogDetail.operParam) }}</pre>
|
||||
</div>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item v-if="currentLogDetail.jsonResult" label="返回数据" :span="2">
|
||||
<div class="json-container">
|
||||
<pre class="json-content">{{ formatJson(currentLogDetail.jsonResult) }}</pre>
|
||||
</div>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
</CoiDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, nextTick, onMounted, ref } from 'vue'
|
||||
import type { DataTableColumns, FormInst } from 'naive-ui'
|
||||
import { NButton, NIcon, NPopconfirm, NSpace, NTag } from 'naive-ui'
|
||||
import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
|
||||
import IconParkOutlinePreviewOpen from '~icons/icon-park-outline/preview-open'
|
||||
import CoiDialog from '@/components/common/CoiDialog.vue'
|
||||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||
import CoiPagination from '@/components/common/CoiPagination.vue'
|
||||
import DictTag from '@/components/common/DictTag.vue'
|
||||
import {
|
||||
batchDeleteOperLog,
|
||||
clearOperLog,
|
||||
deleteOperLog,
|
||||
getOperLogDetailById,
|
||||
getOperLogListPage,
|
||||
} from '@/service/api/system/operlog'
|
||||
import type { OperLogSearchForm, OperLogVo } from '@/service/api/system/operlog'
|
||||
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||
import { PERMISSIONS } from '@/constants/permissions'
|
||||
import { useDict, usePermission } from '@/hooks'
|
||||
|
||||
// 权限相关
|
||||
const { hasButton } = usePermission()
|
||||
const { dictOptions } = useDict(['sys_oper_type', 'sys_common_status'])
|
||||
|
||||
const operStatusOptions = computed(() => {
|
||||
const options = dictOptions.sys_common_status || []
|
||||
return options
|
||||
.filter(option => option.dictValue === '1' || option.dictValue === '2')
|
||||
.map(option => ({
|
||||
label: option.dictLabel,
|
||||
value: option.dictValue === '1' ? '0' : '1',
|
||||
}))
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const tableData = ref<OperLogVo[]>([])
|
||||
const searchFormRef = ref<FormInst | null>(null)
|
||||
const selectedRows = ref<OperLogVo[]>([])
|
||||
const currentLogDetail = ref<OperLogVo | null>(null)
|
||||
|
||||
// 弹框引用
|
||||
const detailDialogRef = ref()
|
||||
|
||||
// 分页数据
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
})
|
||||
|
||||
// 搜索表单数据
|
||||
const searchForm = ref<OperLogSearchForm>({})
|
||||
|
||||
// JSON格式化
|
||||
function formatJson(jsonStr: string) {
|
||||
try {
|
||||
const obj = JSON.parse(jsonStr)
|
||||
return JSON.stringify(obj, null, 2)
|
||||
}
|
||||
catch {
|
||||
return jsonStr
|
||||
}
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: DataTableColumns<OperLogVo> = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
render: (_, index) => {
|
||||
return (pagination.value.page - 1) * pagination.value.pageSize + index + 1
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作名称',
|
||||
key: 'operName',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-600' }, row.operName)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作类型',
|
||||
key: 'operType',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: row => h(DictTag, { dictType: 'sys_oper_type', value: row.operType }),
|
||||
},
|
||||
{
|
||||
title: '操作人员[登录名/用户名]',
|
||||
key: 'operMan',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-600' }, row.operMan)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '系统类型',
|
||||
key: 'systemType',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-600' }, row.systemType || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '请求方式',
|
||||
key: 'requestMethod',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(NTag, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
}, {
|
||||
default: () => row.requestMethod || '-',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '请求URL',
|
||||
key: 'operUrl',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', {
|
||||
class: 'text-sm',
|
||||
title: row.operUrl,
|
||||
}, row.operUrl || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作IP',
|
||||
key: 'operIp',
|
||||
width: 130,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-600' }, row.operIp)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作地点',
|
||||
key: 'operLocation',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
return h('span', row.operLocation || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作状态',
|
||||
key: 'operStatus',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: row => h(DictTag, { dictType: 'sys_common_status', value: row.operStatus === '0' ? '1' : '2' }),
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
key: 'operTime',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500 text-sm' }, row.operTime)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '消耗时间',
|
||||
key: 'costTime',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('span', { class: 'text-gray-500 text-sm' }, `${row.costTime || '0'}ms`)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render: (row) => {
|
||||
const buttons = []
|
||||
|
||||
// 查看详情按钮
|
||||
buttons.push(h(NButton, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
class: 'action-btn-primary',
|
||||
onClick: () => handleViewDetail(row),
|
||||
}, {
|
||||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||
default: () => h(IconParkOutlinePreviewOpen),
|
||||
}),
|
||||
default: () => '查看',
|
||||
}))
|
||||
|
||||
// 删除按钮
|
||||
if (hasButton(PERMISSIONS.OPER_LOG.DELETE)) {
|
||||
buttons.push(h(NPopconfirm, {
|
||||
onPositiveClick: () => handleDelete(row.operId),
|
||||
negativeText: '取消',
|
||||
positiveText: '确定',
|
||||
}, {
|
||||
default: () => '确定删除此操作记录吗?',
|
||||
trigger: () => h(NButton, {
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
size: 'small',
|
||||
class: 'action-btn-secondary action-btn-danger',
|
||||
}, {
|
||||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||
default: () => h(IconParkOutlineDelete),
|
||||
}),
|
||||
default: () => '删除',
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 获取操作日志列表
|
||||
async function getOperLogList() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 构建请求参数,处理时间范围
|
||||
const { timeRange, ...otherParams } = searchForm.value
|
||||
|
||||
// 过滤掉空值和null值,只保留有效的搜索条件
|
||||
const filteredParams = Object.entries(otherParams).reduce((acc, [key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
acc[key] = value
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
const params = {
|
||||
pageNo: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
...filteredParams,
|
||||
}
|
||||
|
||||
// 处理时间范围,转换为beginTime和endTime
|
||||
if (timeRange && timeRange.length === 2) {
|
||||
params.beginTime = new Date(timeRange[0]).toISOString().slice(0, 19).replace('T', ' ')
|
||||
params.endTime = new Date(timeRange[1]).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
|
||||
const { isSuccess, data } = await getOperLogListPage(params)
|
||||
|
||||
if (isSuccess && data) {
|
||||
tableData.value = data.records || []
|
||||
pagination.value.itemCount = data.total || 0
|
||||
}
|
||||
else {
|
||||
console.warn('获取操作日志列表失败,可能是权限或网络问题')
|
||||
coiMsgError('获取操作日志列表失败,请检查网络连接或联系管理员')
|
||||
tableData.value = []
|
||||
pagination.value.itemCount = 0
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取操作日志列表失败:', error)
|
||||
coiMsgError('获取操作日志列表失败,请检查网络连接')
|
||||
tableData.value = []
|
||||
pagination.value.itemCount = 0
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化处理
|
||||
function handlePageChange(page: number) {
|
||||
pagination.value.page = page
|
||||
getOperLogList()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
getOperLogList()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.value.page = 1
|
||||
getOperLogList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
async function handleReset() {
|
||||
// 重置搜索表单的所有字段
|
||||
searchForm.value = {
|
||||
operName: '',
|
||||
operMan: '',
|
||||
operType: null,
|
||||
operStatus: null,
|
||||
operIp: '',
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
timeRange: null,
|
||||
}
|
||||
|
||||
// 使用 nextTick 确保 DOM 更新后再重置表单状态
|
||||
await nextTick()
|
||||
|
||||
// 重置表单验证状态
|
||||
if (searchFormRef.value) {
|
||||
searchFormRef.value.restoreValidation()
|
||||
}
|
||||
|
||||
// 重置分页到第一页
|
||||
pagination.value.page = 1
|
||||
// 重新获取数据
|
||||
getOperLogList()
|
||||
}
|
||||
|
||||
// 行选择变化
|
||||
function handleRowSelectionChange(rowKeys: number[]) {
|
||||
selectedRows.value = tableData.value.filter(row => rowKeys.includes(row.operId))
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
async function handleViewDetail(log: OperLogVo) {
|
||||
try {
|
||||
const { isSuccess, data } = await getOperLogDetailById(log.operId)
|
||||
if (isSuccess && data) {
|
||||
currentLogDetail.value = data
|
||||
detailDialogRef.value?.coiOpen()
|
||||
}
|
||||
else {
|
||||
coiMsgError('获取操作日志详情失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取操作日志详情失败:', error)
|
||||
coiMsgError('获取操作日志详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭详情弹框
|
||||
function handleCloseDetail() {
|
||||
detailDialogRef.value?.coiClose()
|
||||
currentLogDetail.value = null
|
||||
}
|
||||
|
||||
// 删除操作日志
|
||||
async function handleDelete(operId: number) {
|
||||
try {
|
||||
const { isSuccess } = await deleteOperLog(operId)
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('删除成功')
|
||||
await getOperLogList()
|
||||
}
|
||||
else {
|
||||
coiMsgError('删除失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
coiMsgError('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除操作日志
|
||||
async function handleBatchDelete() {
|
||||
if (selectedRows.value.length === 0) {
|
||||
coiMsgWarning('请先选择要删除的操作记录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 确认删除操作
|
||||
await coiMsgBox(`确定要删除选中的 ${selectedRows.value.length} 条操作记录吗?`, '批量删除确认')
|
||||
}
|
||||
catch {
|
||||
// 用户取消删除操作,直接返回,不显示任何提示
|
||||
return
|
||||
}
|
||||
|
||||
// 用户确认删除,执行删除操作
|
||||
try {
|
||||
const operIds = selectedRows.value.map(log => log.operId)
|
||||
const { isSuccess } = await batchDeleteOperLog(operIds)
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('批量删除成功')
|
||||
selectedRows.value = []
|
||||
await getOperLogList()
|
||||
}
|
||||
else {
|
||||
coiMsgError('批量删除失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
coiMsgError('批量删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 清空操作日志
|
||||
async function handleClearAll() {
|
||||
try {
|
||||
// 确认清空操作
|
||||
await coiMsgBox('确定要清空所有操作日志吗?此操作不可恢复!', '清空确认')
|
||||
}
|
||||
catch {
|
||||
// 用户取消清空操作,直接返回,不显示任何提示
|
||||
return
|
||||
}
|
||||
|
||||
// 用户确认清空,执行清空操作
|
||||
try {
|
||||
const { isSuccess } = await clearOperLog()
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('清空成功')
|
||||
selectedRows.value = []
|
||||
await getOperLogList()
|
||||
}
|
||||
else {
|
||||
coiMsgError('清空失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('清空失败:', error)
|
||||
coiMsgError('清空失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否有搜索条件
|
||||
function hasSearchConditions() {
|
||||
return Object.values(searchForm.value).some((value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 获取空状态类型
|
||||
function getEmptyType() {
|
||||
if (hasSearchConditions()) {
|
||||
return 'search'
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
// 获取空状态标题
|
||||
function getEmptyTitle() {
|
||||
if (hasSearchConditions()) {
|
||||
return '搜索无结果'
|
||||
}
|
||||
return '暂无操作日志'
|
||||
}
|
||||
|
||||
// 获取空状态描述
|
||||
function getEmptyDescription() {
|
||||
if (hasSearchConditions()) {
|
||||
return '未找到符合搜索条件的操作记录,请尝试调整搜索条件或重置筛选'
|
||||
}
|
||||
return '当前还没有操作日志数据'
|
||||
}
|
||||
|
||||
// 获取空状态操作文字
|
||||
function getEmptyActionText() {
|
||||
if (hasSearchConditions()) {
|
||||
return '重置筛选'
|
||||
}
|
||||
return '刷新数据'
|
||||
}
|
||||
|
||||
// 处理空状态操作
|
||||
function handleEmptyAction() {
|
||||
if (hasSearchConditions()) {
|
||||
handleReset()
|
||||
}
|
||||
else {
|
||||
getOperLogList()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
getOperLogList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operlog-management {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-td) {
|
||||
padding: 7px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-th) {
|
||||
padding: 9px 12px;
|
||||
background-color: #fafafa;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-tr:hover .n-data-table-td) {
|
||||
background-color: #f8faff;
|
||||
}
|
||||
|
||||
.search-form :deep(.n-form-item-label) {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* CoiEmpty按钮样式 */
|
||||
|
||||
/* 操作按钮样式优化 */
|
||||
.custom-table :deep(.n-button--small-type) {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* 标签尺寸优化 */
|
||||
.custom-table :deep(.n-tag--small-type) {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
/* 统一按钮样式系统 - 支持动态主题色彩 */
|
||||
.action-btn-primary {
|
||||
background: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
color: #ffffff !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: 0 2px 4px var(--primary-color-suppl) !important;
|
||||
}
|
||||
|
||||
.action-btn-primary:hover {
|
||||
background: var(--primary-color-hover) !important;
|
||||
border-color: var(--primary-color-hover) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 4px 8px var(--primary-color-suppl) !important;
|
||||
}
|
||||
|
||||
.action-btn-primary:active {
|
||||
background: var(--primary-color-pressed) !important;
|
||||
border-color: var(--primary-color-pressed) !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
.action-btn-secondary {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.action-btn-secondary:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
.action-btn-secondary:active {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
.action-btn-danger {
|
||||
border-color: var(--error-color) !important;
|
||||
color: var(--error-color) !important;
|
||||
}
|
||||
|
||||
.action-btn-danger:hover {
|
||||
border-color: var(--error-color-hover) !important;
|
||||
color: var(--error-color-hover) !important;
|
||||
background: var(--error-color-suppl) !important;
|
||||
}
|
||||
|
||||
/* 操作日志详情样式 */
|
||||
.oper-log-detail :deep(.n-descriptions-item-label) {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
background-color: #f9fafb;
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.oper-log-detail :deep(.n-descriptions-item-content) {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 方法名称样式 */
|
||||
.method-name {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
color: #7c3aed;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* URL 和方法名样式 */
|
||||
.url-text {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
background-color: #f3f4f6;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: #059669;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.method-text {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
background-color: #f3f4f6;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: #7c3aed;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* JSON 容器样式 */
|
||||
.json-container {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
background-color: #1f2937;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.json-content {
|
||||
color: #f9fafb;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 错误消息样式 */
|
||||
.error-msg {
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.table-wrapper {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-corner {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
/* 表格滚动条样式 */
|
||||
.custom-table :deep(.n-data-table-base-table) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-track {
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb {
|
||||
background: #dee2e6;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb:hover {
|
||||
background: #adb5bd;
|
||||
}
|
||||
</style>
|
||||
1224
src/views/system/picture/index.vue
Normal file
1224
src/views/system/picture/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1659
src/views/system/role/index.vue
Normal file
1659
src/views/system/role/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
2314
src/views/system/user/index.vue
Normal file
2314
src/views/system/user/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user