初始化
This commit is contained in:
321
saiadmin-artd/src/views/safeguard/attachment/index.vue
Normal file
321
saiadmin-artd/src/views/safeguard/attachment/index.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="flex-shrink-0 w-64 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="tree-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<b>附件分类</b>
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="primary"
|
||||
@click="categoryShowDialog('add')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<ElScrollbar>
|
||||
<ElTree
|
||||
:data="treeData"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="flex items-center justify-between w-full" v-if="data.id > 1">
|
||||
<span>{{ node.label }}</span>
|
||||
<div class="tree-node-actions">
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="secondary"
|
||||
@click="categoryShowDialog('edit', data)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="error"
|
||||
@click="categoryDeleteRow(data, categoryApi.delete, getCategoryList)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-grow min-w-0">
|
||||
<ElCard class="art-table-card !mt-0" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<ElSpace wrap>
|
||||
<ElUpload
|
||||
v-permission="'core:system:uploadImage'"
|
||||
class="upload-btn"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:before-upload="beforeUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<ElButton :icon="UploadFilled">上传图片</ElButton>
|
||||
</ElUpload>
|
||||
<ElButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="moveDialogVisible = true"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:swap-box-line" />
|
||||
</template>
|
||||
移动
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
<ElSpace wrap>
|
||||
<SaSelect
|
||||
v-model="searchForm.storage_mode"
|
||||
placeholder="请选择存储模式"
|
||||
dict="upload_mode"
|
||||
@change="handleSearch"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
/>
|
||||
<ElInput
|
||||
v-model="searchForm.origin_name"
|
||||
placeholder="请输入文件名称"
|
||||
:suffix-icon="Search"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类弹窗 -->
|
||||
<CategoryDialog
|
||||
v-model="categoryDialogVisible"
|
||||
:dialog-type="categoryDialogType"
|
||||
:data="categoryDialogData"
|
||||
@success="getCategoryList"
|
||||
/>
|
||||
|
||||
<!-- 移动弹窗 -->
|
||||
<MoveDialog v-model="moveDialogVisible" :selected-rows="selectedRows" @success="refreshData" />
|
||||
|
||||
<!-- 表单弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/attachment'
|
||||
import categoryApi from '@/api/safeguard/category'
|
||||
import { uploadImage } from '@/api/auth'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { Search, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { UploadRequestOptions, UploadProps } from 'element-plus'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import CategoryDialog from './modules/category-dialog.vue'
|
||||
import MoveDialog from './modules/move-dialog.vue'
|
||||
|
||||
/** 附件分类数据 */
|
||||
const treeData = ref([])
|
||||
|
||||
/** 获取附件分类数据 */
|
||||
const getCategoryList = () => {
|
||||
categoryApi.list({ tree: true }).then((data: any) => {
|
||||
treeData.value = data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换附件分类
|
||||
* @param data
|
||||
*/
|
||||
const handleNodeClick = (data: any) => {
|
||||
if (data.id === 1) {
|
||||
searchParams.category_id = undefined
|
||||
} else {
|
||||
searchParams.category_id = data.id
|
||||
}
|
||||
getData()
|
||||
}
|
||||
|
||||
/** 附件分类弹窗相关 */
|
||||
const {
|
||||
dialogType: categoryDialogType,
|
||||
dialogVisible: categoryDialogVisible,
|
||||
dialogData: categoryDialogData,
|
||||
showDialog: categoryShowDialog,
|
||||
deleteRow: categoryDeleteRow
|
||||
} = useSaiAdmin()
|
||||
|
||||
/** 移动弹窗相关 */
|
||||
const moveDialogVisible = ref(false)
|
||||
|
||||
/** 附件弹窗相关 */
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
selectedRows,
|
||||
handleSelectionChange,
|
||||
deleteRow,
|
||||
deleteSelectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
/** 附件搜索表单 */
|
||||
const searchForm = ref({
|
||||
origin_name: undefined,
|
||||
storage_mode: undefined,
|
||||
category_id: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
/** 附件表格相关 */
|
||||
const {
|
||||
columns,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'url', label: '预览', saiType: 'image', width: 80 },
|
||||
{ prop: 'origin_name', label: '文件名称', minWidth: 160, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'storage_mode',
|
||||
label: '存储模式',
|
||||
width: 100,
|
||||
saiType: 'dict',
|
||||
saiDict: 'upload_mode'
|
||||
},
|
||||
{ prop: 'mime_type', label: '文件类型', width: 160, showOverflowTooltip: true },
|
||||
{ prop: 'size_info', label: '文件大小', width: 100 },
|
||||
{ prop: 'create_time', label: '上传时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/** 附件搜索 */
|
||||
const handleSearch = () => {
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
/** 附件上传前验证 */
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 附件处理上传 */
|
||||
const handleUpload = async (options: UploadRequestOptions) => {
|
||||
const { file } = options
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await uploadImage(formData)
|
||||
ElMessage.success('上传成功')
|
||||
refreshData()
|
||||
} catch (error: any) {
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error(error.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化附件分类数据 */
|
||||
onMounted(() => {
|
||||
getCategoryList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tree-node-actions {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.el-tree-node__content:hover .tree-node-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增分类' : '编辑分类'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="上级分类" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="formData.parent_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="category_name">
|
||||
<el-input v-model="formData.category_name" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/category'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
parent_id: [{ required: true, message: '请选择上级分类', trigger: 'change' }],
|
||||
category_name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
parent_id: null,
|
||||
level: '',
|
||||
category_name: '',
|
||||
sort: 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await api.list({ tree: true })
|
||||
optionData.treeData = data
|
||||
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增文件' : '编辑文件'"
|
||||
width="800px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="文件名称" prop="origin_name">
|
||||
<el-input v-model="formData.origin_name" placeholder="请输入文件名称" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import api from '@/api/safeguard/attachment'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
origin_name: [{ required: true, message: '请输入文件名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 初始表单数据
|
||||
const initialFormData = {
|
||||
id: '',
|
||||
origin_name: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'edit') {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="移动到分类"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item>
|
||||
<div class="text-gray-600 mb-2">
|
||||
已选择 <span class="text-primary font-medium">{{ selectedCount }}</span> 个文件
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标分类" prop="category_id">
|
||||
<el-tree-select
|
||||
v-model="formData.category_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="请选择目标分类"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定移动</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/attachment'
|
||||
import categoryApi from '@/api/safeguard/category'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
selectedRows: any[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
selectedRows: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 选中数量
|
||||
*/
|
||||
const selectedCount = computed(() => props.selectedRows.length)
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
category_id: [{ required: true, message: '请选择目标分类', trigger: 'change' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
category_id: null
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await categoryApi.list({ tree: true })
|
||||
optionData.treeData = data
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
const ids = props.selectedRows.map((row) => row.id)
|
||||
await api.move({
|
||||
ids: ids,
|
||||
category_id: formData.category_id
|
||||
})
|
||||
|
||||
ElMessage.success(`成功移动 ${ids.length} 个文件`)
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:label-width="'70px'"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="formData.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
249
saiadmin-artd/src/views/safeguard/cache/index.vue
vendored
Normal file
249
saiadmin-artd/src/views/safeguard/cache/index.vue
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div class="page-content mb-5">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 字典缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">数据字典-缓存信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.dict_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.dict_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理缓存
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.dict_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 配置缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">系统配置-缓存信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.config_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.config_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理缓存
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.config_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="缓存前缀">
|
||||
{{ cacheInfo.config_cache?.prefix }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 菜单缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">菜单数据-缓存信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.menu_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.menu_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.menu_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="缓存前缀">
|
||||
{{ cacheInfo.menu_cache?.prefix }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 权限缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">权限按钮-缓存信息</span>
|
||||
<span class="text-sm text-gray-500"> 缓存权限按钮的数据 </span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.button_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.button_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.button_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="缓存前缀">
|
||||
{{ cacheInfo.button_cache?.prefix }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色前缀">
|
||||
{{ cacheInfo.button_cache?.role }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 反射文件缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">反射文件-缓存信息</span>
|
||||
<span class="text-sm text-gray-500"> 缓存反射文件的反射属性的方法名称和权限参数 </span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.reflection_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.reflection_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理缓存
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.reflection_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="非验证方法缓存前缀">
|
||||
{{ cacheInfo.reflection_cache?.no_need }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="方法名称和权限缓存参数">
|
||||
{{ cacheInfo.reflection_cache?.attr }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/server'
|
||||
import { onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const cacheInfo = reactive({
|
||||
menu_cache: {} as any,
|
||||
button_cache: {} as any,
|
||||
config_cache: {} as any,
|
||||
dict_cache: {} as any,
|
||||
reflection_cache: {} as any
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新缓存信息
|
||||
*/
|
||||
const updateCacheInfo = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.cache({})
|
||||
cacheInfo.menu_cache = data.menu_cache
|
||||
cacheInfo.button_cache = data.button_cache
|
||||
cacheInfo.config_cache = data.config_cache
|
||||
cacheInfo.dict_cache = data.dict_cache
|
||||
cacheInfo.reflection_cache = data.reflection_cache
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
const handleClearCache = (tag: string): void => {
|
||||
if (!tag) {
|
||||
ElMessage.warning('请选择要清理的缓存')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要清理标签:【${tag}】的缓存吗?`, '清理选中缓存', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
api.clear({ tag }).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
updateCacheInfo()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCacheInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 200px;
|
||||
}
|
||||
:deep(.el-descriptions__content) {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
214
saiadmin-artd/src/views/safeguard/database/index.vue
Normal file
214
saiadmin-artd/src/views/safeguard/database/index.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:database:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleOptimizeRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:tools-fill" />
|
||||
</template>
|
||||
优化表
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:database:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleFragmentRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:wrench-line" />
|
||||
</template>
|
||||
清理碎片
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="name"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:database:index'"
|
||||
type="primary"
|
||||
icon="ri:node-tree"
|
||||
tool-tip="表结构"
|
||||
@click="handleTableDialog(row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:recycle:index'"
|
||||
type="success"
|
||||
icon="ri:recycle-line"
|
||||
tool-tip="回收站"
|
||||
@click="handleRecycleDialog(row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 表结构信息 -->
|
||||
<TableDialog v-model="dialogVisible" :data="dialogData" />
|
||||
|
||||
<!-- 回收站 -->
|
||||
<RecycleList v-model="recycleVisible" :data="recycleData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import api from '@/api/safeguard/database'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import TableDialog from './modules/table-dialog.vue'
|
||||
import RecycleList from './modules/recycle-list.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'name', label: '表名称', minWidth: 200 },
|
||||
{ prop: 'comment', label: '表注释', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'engine', label: '表引擎', width: 120 },
|
||||
{ prop: 'update_time', label: '更新时间', width: 180, sortable: true },
|
||||
{ prop: 'rows', label: '总行数', width: 120 },
|
||||
{ prop: 'data_free', label: '碎片大小', width: 120 },
|
||||
{ prop: 'data_length', label: '数据大小', width: 120 },
|
||||
{ prop: 'collation', label: '字符集', width: 180 },
|
||||
{ prop: 'create_time', label: '创建时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogVisible, dialogData, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
const recycleVisible = ref(false)
|
||||
const recycleData = ref({})
|
||||
|
||||
/**
|
||||
* 表结构
|
||||
* @param row
|
||||
*/
|
||||
const handleTableDialog = (row: Record<string, any>): void => {
|
||||
dialogVisible.value = true
|
||||
dialogData.value = row
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收站
|
||||
* @param row
|
||||
*/
|
||||
const handleRecycleDialog = (row: Record<string, any>): void => {
|
||||
recycleVisible.value = true
|
||||
recycleData.value = row
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化表
|
||||
*/
|
||||
const handleOptimizeRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要优化的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要优化选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'优化选中数据',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api.optimize({ tables: selectedRows.value.map((row) => row.name) }).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理表碎片
|
||||
*/
|
||||
const handleFragmentRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要清理碎片的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要清理选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'清理碎片操作',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api.fragment({ tables: selectedRows.value.map((row) => row.name) }).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:title="`回收站 - ${props.data?.name}`"
|
||||
size="70%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="art-full-height">
|
||||
<!-- 表格头部 -->
|
||||
<div>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:recycle:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleDestroyRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
销毁
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:recycle:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleRestoreRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:restart-line" />
|
||||
</template>
|
||||
恢复
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 数据详情插槽 -->
|
||||
<template #json_data="{ row }">
|
||||
{{ JSON.stringify(row) }}
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/database'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
searchForm.value.table = props.data?.name
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
const searchForm = ref({
|
||||
table: null
|
||||
})
|
||||
|
||||
const {
|
||||
loading,
|
||||
data: tableData,
|
||||
columns,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.getRecycle,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'delete_time', label: '删除时间', width: 180 },
|
||||
{ prop: 'json_data', label: '数据详情', useSlot: true, showOverflowTooltip: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { handleSelectionChange, selectedRows } = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁选中数据
|
||||
*/
|
||||
const handleDestroyRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要销毁的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要销毁选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'销毁选中数据',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api
|
||||
.delete({ table: searchForm.value.table, ids: selectedRows.value.map((row) => row.id) })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复选中数据
|
||||
*/
|
||||
const handleRestoreRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要恢复的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要恢复选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'恢复选中数据',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api
|
||||
.recovery({ table: searchForm.value.table, ids: selectedRows.value.map((row) => row.id) })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="表结构信息" width="800px" align-center @close="handleClose">
|
||||
<div>
|
||||
<el-table :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="column_name" label="字段名称" width="180"> </el-table-column>
|
||||
<el-table-column prop="column_type" label="字段类型" width="120"> </el-table-column>
|
||||
<el-table-column prop="column_key" label="字段索引" width="100"> </el-table-column>
|
||||
<el-table-column prop="column_default" label="默认值" width="100"> </el-table-column>
|
||||
<el-table-column prop="column_comment" label="字段注释" min-width="200" showOverflowTooltip>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/database'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const tableData = ref<Api.Common.ApiData[]>([])
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
if (props.data.name) {
|
||||
const data = await api.getDetailed({ table: props.data.name })
|
||||
tableData.value = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="表名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入表名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
402
saiadmin-artd/src/views/safeguard/dict/index.vue
Normal file
402
saiadmin-artd/src/views/safeguard/dict/index.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<!-- 左右页面 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="flex-shrink-0 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="left-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<b>数据字典</b>
|
||||
</template>
|
||||
<ElSpace wrap>
|
||||
<SaButton type="primary" icon="ri:refresh-line" @click="refreshTypeData" />
|
||||
<SaButton
|
||||
v-permission="'core:dict:edit'"
|
||||
type="primary"
|
||||
@click="typeShowDialog('add')"
|
||||
/>
|
||||
<SaButton v-permission="'core:dict:edit'" type="secondary" @click="updateTypeDialog" />
|
||||
<SaButton v-permission="'core:dict:edit'" type="error" @click="deleteTypeDialog" />
|
||||
</ElSpace>
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="typeData"
|
||||
:columns="typeColumns"
|
||||
:pagination="typePagination"
|
||||
highlight-current-row
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 基础列 -->
|
||||
<template #name-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="typeSearch.name"
|
||||
placeholder="搜索字典名称"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleTypeSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #code-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="typeSearch.code"
|
||||
placeholder="搜索字典标识"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleTypeSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #id="{ row }">
|
||||
<ElRadio
|
||||
v-model="selectedId"
|
||||
:value="row.id"
|
||||
@update:modelValue="handleTypeChange(row.id, row)"
|
||||
/>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0" v-if="selectedId === 0">
|
||||
<ElCard class="flex flex-col flex-5 min-h-0 !mt-0" shadow="never">
|
||||
<el-empty description="请先选择左侧字典类型配置" />
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0" v-if="selectedId > 0">
|
||||
<DictSearch v-model="searchForm" @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<ElCard class="flex flex-col flex-5 min-h-0 art-table-card" shadow="never">
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:dict:edit'"
|
||||
@click="showDataDialog('add', { type_id: selectedId })"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:dict:edit'"
|
||||
@click="deleteSelectedRows(api.dataDelete, getDictData)"
|
||||
:disabled="selectedRows.length === 0"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="dictData"
|
||||
:columns="dictColumns"
|
||||
:pagination="dictPagination"
|
||||
highlight-current-row
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 基础列 -->
|
||||
<template #label="{ row }">
|
||||
<ElTag
|
||||
:style="{
|
||||
backgroundColor: getColor(row.color, 'bg'),
|
||||
borderColor: getColor(row.color, 'border'),
|
||||
color: getColor(row.color, 'text')
|
||||
}"
|
||||
>
|
||||
{{ row.label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:dict:edit'"
|
||||
type="secondary"
|
||||
@click="showDataDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:dict:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.dataDelete, getDictData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字典编辑弹窗 -->
|
||||
<TypeEditDialog
|
||||
v-model="typeVisible"
|
||||
:dialog-type="typeDialogType"
|
||||
:data="currentTypeData"
|
||||
@success="getTypeData()"
|
||||
/>
|
||||
|
||||
<!-- 字典项编辑弹窗 -->
|
||||
<DictEditDialog
|
||||
v-model="dictVisible"
|
||||
:dialog-type="dictDialogType"
|
||||
:data="currentDictData"
|
||||
@success="getDictData()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api/safeguard/dict'
|
||||
import DictSearch from '@/views/safeguard/dict/modules/dict-search.vue'
|
||||
import DictEditDialog from './modules/dict-edit-dialog.vue'
|
||||
import TypeEditDialog from './modules/type-edit-dialog.vue'
|
||||
|
||||
// 字典类型数据
|
||||
const {
|
||||
dialogType: typeDialogType,
|
||||
dialogVisible: typeVisible,
|
||||
dialogData: currentTypeData,
|
||||
showDialog: typeShowDialog,
|
||||
deleteRow: typeDeleteRow
|
||||
} = useSaiAdmin()
|
||||
|
||||
// 字典类型
|
||||
const selectedId = ref(0)
|
||||
const selectedRow = ref({})
|
||||
const typeSearch = ref({
|
||||
name: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
/** 修改字典类型 */
|
||||
const updateTypeDialog = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要修改的数据')
|
||||
return
|
||||
}
|
||||
typeShowDialog('edit', { ...selectedRow.value })
|
||||
}
|
||||
|
||||
/** 删除字典类型 */
|
||||
const deleteTypeDialog = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
typeDeleteRow({ ...selectedRow.value }, api.delete, refreshTypeData)
|
||||
}
|
||||
|
||||
/** 字典类型搜索 */
|
||||
const handleTypeSearch = () => {
|
||||
Object.assign(searchTypeParams, typeSearch.value)
|
||||
getTypeData()
|
||||
}
|
||||
|
||||
/** 字典类型切换 */
|
||||
const handleTypeChange = (val: any, row?: any) => {
|
||||
selectedId.value = val
|
||||
selectedRow.value = row
|
||||
searchForm.value.type_id = val
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getDictData()
|
||||
}
|
||||
|
||||
/** 刷新数据 */
|
||||
const refreshTypeData = () => {
|
||||
selectedId.value = 0
|
||||
selectedRow.value = {}
|
||||
getTypeData()
|
||||
getDictData()
|
||||
}
|
||||
|
||||
// 字典类型数据
|
||||
const {
|
||||
data: typeData,
|
||||
columns: typeColumns,
|
||||
getData: getTypeData,
|
||||
searchParams: searchTypeParams,
|
||||
loading,
|
||||
pagination: typePagination,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.typeList,
|
||||
apiParams: {
|
||||
...typeSearch.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: '选中', width: 80, align: 'center', useSlot: true },
|
||||
{ prop: 'name', label: '字典名称', useHeaderSlot: true, width: 150 },
|
||||
{ prop: 'code', label: '字典标识', useHeaderSlot: true, width: 150 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 字典项数据
|
||||
const {
|
||||
dialogType: dictDialogType,
|
||||
dialogVisible: dictVisible,
|
||||
dialogData: currentDictData,
|
||||
showDialog: showDataDialog,
|
||||
deleteRow,
|
||||
handleSelectionChange,
|
||||
selectedRows,
|
||||
deleteSelectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
/** 字典项搜索 */
|
||||
const searchForm = ref({
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
type_id: null
|
||||
})
|
||||
|
||||
// 字典项数据
|
||||
const {
|
||||
data: dictData,
|
||||
columns: dictColumns,
|
||||
getData: getDictData,
|
||||
pagination: dictPagination,
|
||||
searchParams
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.dataList,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'label', label: '字典标签', useSlot: true },
|
||||
{ prop: 'value', label: '字典键值' },
|
||||
{ prop: 'color', label: '颜色' },
|
||||
{ prop: 'sort', label: '排序' },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status' },
|
||||
{ prop: 'operation', label: '操作', useSlot: true, width: 120 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 字典项搜索
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
if (selectedId.value) {
|
||||
Object.assign(searchParams, params)
|
||||
getDictData()
|
||||
}
|
||||
}
|
||||
|
||||
// 字典项重置搜索
|
||||
const handleReset = () => {
|
||||
if (!selectedId.value) {
|
||||
ElMessage.warning('请选择字典类型')
|
||||
return
|
||||
}
|
||||
Object.assign(searchParams, {
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
type_id: selectedId.value
|
||||
})
|
||||
getDictData()
|
||||
}
|
||||
|
||||
const getColor = (color: string | undefined, type: 'bg' | 'border' | 'text') => {
|
||||
// 如果没有指定颜色,使用默认主色调
|
||||
if (!color) {
|
||||
const colors = {
|
||||
bg: 'var(--el-color-primary-light-9)',
|
||||
border: 'var(--el-color-primary-light-8)',
|
||||
text: 'var(--el-color-primary)'
|
||||
}
|
||||
return colors[type]
|
||||
}
|
||||
|
||||
// 如果是 hex 颜色,转换为 RGB
|
||||
let r, g, b
|
||||
if (color.startsWith('#')) {
|
||||
const hex = color.slice(1)
|
||||
r = parseInt(hex.slice(0, 2), 16)
|
||||
g = parseInt(hex.slice(2, 4), 16)
|
||||
b = parseInt(hex.slice(4, 6), 16)
|
||||
} else if (color.startsWith('rgb')) {
|
||||
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
|
||||
if (match) {
|
||||
r = parseInt(match[1])
|
||||
g = parseInt(match[2])
|
||||
b = parseInt(match[3])
|
||||
} else {
|
||||
return color
|
||||
}
|
||||
} else {
|
||||
return color
|
||||
}
|
||||
|
||||
// 根据类型返回不同的颜色变体
|
||||
switch (type) {
|
||||
case 'bg':
|
||||
// 背景色 - 更浅的版本
|
||||
return `rgba(${r}, ${g}, ${b}, 0.1)`
|
||||
case 'border':
|
||||
// 边框色 - 中等亮度
|
||||
return `rgba(${r}, ${g}, ${b}, 0.3)`
|
||||
case 'text':
|
||||
// 文字色 - 原始颜色
|
||||
return `rgb(${r}, ${g}, ${b})`
|
||||
default:
|
||||
return color
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 10px 2px 10px 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增字典项数据' : '编辑字典项数据'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="字典标签" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入字典标签" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典键值" prop="value">
|
||||
<el-input v-model="formData.value" placeholder="请输入字典键值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色选择" prop="color">
|
||||
<el-color-picker v-model="formData.color" color-format="hex" :predefine="predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<SaiRadio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/dict'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import SaiRadio from '@/components/sai/sa-radio/index.vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const predefineColors = ref([
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#5d87ff',
|
||||
'#b48df3',
|
||||
'#1d84ff',
|
||||
'#60c041',
|
||||
'#38c0fc',
|
||||
'#f9901f',
|
||||
'#ff80c8',
|
||||
'#909399'
|
||||
])
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
label: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
value: [{ required: true, message: '请输入字典键值', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
type_id: '',
|
||||
code: '',
|
||||
label: '',
|
||||
color: '#5d87ff',
|
||||
value: '',
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.dataSave(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.dataUpdate(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dictStore.refresh()
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="80px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典标签" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入字典标签" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典键值" prop="value">
|
||||
<el-input v-model="formData.value" placeholder="请输入字典键值" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增字典数据' : '编辑字典数据'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="字典名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标识" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入字典标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<SaiRadio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/dict'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import SaiRadio from '@/components/sai/sa-radio/index.vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入字典标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
121
saiadmin-artd/src/views/safeguard/email-log/index.vue
Normal file
121
saiadmin-artd/src/views/safeguard/email-log/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:email:destroy'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #status="{ row }">
|
||||
<ElTag v-if="row.status == 'success'" type="success">成功</ElTag>
|
||||
<ElTag v-else type="danger">失败</ElTag>
|
||||
</template>
|
||||
<template #operation="{ row }">
|
||||
<div class="flex">
|
||||
<SaButton
|
||||
v-permission="'core:email:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/safeguard/emailLog'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
from: undefined,
|
||||
email: undefined,
|
||||
status: undefined,
|
||||
create_time: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'gateway', label: '服务Host' },
|
||||
{ prop: 'from', label: '发件人', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'email', label: '收件人', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'code', label: '验证码' },
|
||||
{ prop: 'status', label: '发送状态', useSlot: true },
|
||||
{ prop: 'response', label: '发送结果', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'create_time', label: '发送时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 80, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { deleteRow, deleteSelectedRows, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="true"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="发件人" prop="from">
|
||||
<el-input v-model="formData.from" placeholder="请输入发件人" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="收件人" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入收件人" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="发送状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择发送状态" clearable>
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failure" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(12)" v-show="isExpanded">
|
||||
<el-form-item label="发送时间" prop="create_time">
|
||||
<el-date-picker
|
||||
v-model="formData.create_time"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
122
saiadmin-artd/src/views/safeguard/login-log/index.vue
Normal file
122
saiadmin-artd/src/views/safeguard/login-log/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:logs:deleteLogin'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #status="{ row }">
|
||||
<ElTag v-if="row.status == 1" type="success">成功</ElTag>
|
||||
<ElTag v-else type="danger">失败</ElTag>
|
||||
</template>
|
||||
<template #operation="{ row }">
|
||||
<div class="flex">
|
||||
<SaButton
|
||||
v-permission="'core:logs:deleteLogin'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/safeguard/loginLog'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
username: undefined,
|
||||
ip: undefined,
|
||||
status: undefined,
|
||||
login_time: undefined,
|
||||
orderField: 'login_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'username', label: '登录用户' },
|
||||
{ prop: 'status', label: '登录状态', useSlot: true },
|
||||
{ prop: 'ip', label: '登录IP' },
|
||||
{ prop: 'ip_location', label: '登录地点' },
|
||||
{ prop: 'os', label: '操作系统' },
|
||||
{ prop: 'browser', label: '浏览器' },
|
||||
{ prop: 'message', label: '登录信息', showOverflowTooltip: true },
|
||||
{ prop: 'login_time', label: '登录时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 80, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { deleteRow, deleteSelectedRows, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="true"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="登录用户" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入登录用户" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="登录IP" prop="ip">
|
||||
<el-input v-model="formData.ip" placeholder="请输入登录IP" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="登录状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择登录状态" clearable>
|
||||
<el-option label="成功" value="1" />
|
||||
<el-option label="失败" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(12)" v-show="isExpanded">
|
||||
<el-form-item label="登录时间" prop="login_time">
|
||||
<el-date-picker
|
||||
v-model="formData.login_time"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
170
saiadmin-artd/src/views/safeguard/oper-log/index.vue
Normal file
170
saiadmin-artd/src/views/safeguard/oper-log/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:logs:deleteOper'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton type="success" @click="handleParams(row)" />
|
||||
<SaButton
|
||||
v-permission="'core:logs:deleteOper'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import api from '@/api/safeguard/operLog'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
username: undefined,
|
||||
ip: undefined,
|
||||
service_name: undefined,
|
||||
router: undefined,
|
||||
create_time: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'username', label: '操作用户' },
|
||||
{ prop: 'service_name', label: '业务名称' },
|
||||
{ prop: 'router', label: '路由', minWidth: 180, showOverflowTooltip: true },
|
||||
{ prop: 'ip', label: '操作IP' },
|
||||
{ prop: 'ip_location', label: '操作地点' },
|
||||
{ prop: 'create_time', label: '操作时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { deleteRow, deleteSelectedRows, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
|
||||
// 预览参数
|
||||
const handleParams = (row: any) => {
|
||||
let formattedData = row.request_data
|
||||
// 尝试格式化JSON数据
|
||||
if (row.request_data) {
|
||||
try {
|
||||
// 如果已经是对象,直接格式化;如果是字符串,先解析再格式化
|
||||
const parsedData =
|
||||
typeof row.request_data === 'string' ? JSON.parse(row.request_data) : row.request_data
|
||||
formattedData = JSON.stringify(parsedData, null, 2)
|
||||
} catch (error) {
|
||||
// 如果解析失败,保持原样显示
|
||||
formattedData = row.request_data
|
||||
console.log('Error parsing JSON:', error)
|
||||
}
|
||||
}
|
||||
|
||||
ElMessageBox({
|
||||
title: '请求参数',
|
||||
message: h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
maxHeight: '400px',
|
||||
minWidth: '380px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
[
|
||||
h(
|
||||
'pre',
|
||||
{
|
||||
style: {
|
||||
margin: 0,
|
||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
formattedData
|
||||
)
|
||||
]
|
||||
),
|
||||
callback: () => {}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="true"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="操作用户" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入操作用户" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="操作路由" prop="router">
|
||||
<el-input v-model="formData.router" placeholder="请输入操作路由" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="操作IP" prop="ip">
|
||||
<el-input v-model="formData.ip" placeholder="请输入操作IP" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(12)" v-show="isExpanded">
|
||||
<el-form-item label="操作时间" prop="create_time">
|
||||
<el-date-picker
|
||||
v-model="formData.create_time"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
213
saiadmin-artd/src/views/safeguard/server/index.vue
Normal file
213
saiadmin-artd/src/views/safeguard/server/index.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="page-content mb-5">
|
||||
<el-row :gutter="20">
|
||||
<!-- 内存 信息 -->
|
||||
<el-col :span="24" class="mb-4">
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">内存信息</span>
|
||||
</template>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex-1">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="总内存">
|
||||
{{ serverInfo.memory.total }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已使用内存">
|
||||
{{ serverInfo.memory.used }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="PHP使用内存">
|
||||
{{ serverInfo.memory.php }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="空闲内存">
|
||||
{{ serverInfo.memory.free }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="使用率">
|
||||
{{ serverInfo.memory.rate }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="w-80 p-4 text-center">
|
||||
<div class="pb-3.5">
|
||||
<span class="text-base font-medium">内存使用率</span>
|
||||
</div>
|
||||
<el-progress
|
||||
type="dashboard"
|
||||
:percentage="Number.parseFloat(serverInfo.memory.rate)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- PHP 信息 -->
|
||||
<el-col :span="24" class="mb-4">
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">PHP及环境信息</span>
|
||||
</template>
|
||||
<div class="py-2">
|
||||
<el-descriptions :column="2" border class="php-config" v-if="serverInfo.phpEnv">
|
||||
<el-descriptions-item
|
||||
label="PHP版本"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.php_version }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="操作系统"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.os }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="项目路径"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
<div class="project-path">{{ serverInfo.phpEnv?.project_path }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="内存限制"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.memory_limit }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="最大执行时间"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{
|
||||
serverInfo.phpEnv?.max_execution_time === '0'
|
||||
? '无限制'
|
||||
: `${serverInfo.phpEnv?.max_execution_time}秒`
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="错误报告"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.error_reporting }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="显示错误"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.display_errors }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="上传限制"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.upload_max_filesize }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="POST大小"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.post_max_size }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="扩展目录"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.extension_dir }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="扩展目录"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.loaded_extensions }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 磁盘 信息 -->
|
||||
<el-col :span="24" class="mb-4">
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span><i class="el-icon-disk"></i> 磁盘监控</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="serverInfo.disk" style="width: 100%">
|
||||
<el-table-column prop="filesystem" label="文件系统" />
|
||||
<el-table-column prop="size" label="总大小" />
|
||||
<el-table-column prop="used" label="已用空间" />
|
||||
<el-table-column prop="available" label="可用空间" />
|
||||
<el-table-column prop="use_percentage" label="使用率">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="parseInt(row.use_percentage.replace('%', ''))"
|
||||
:stroke-width="12"
|
||||
:show-text="true"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mounted_on" label="挂载点" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/server'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const serverInfo = reactive({
|
||||
memory: {
|
||||
total: '',
|
||||
used: '',
|
||||
rate: '',
|
||||
php: '',
|
||||
free: ''
|
||||
},
|
||||
disk: [] as any[],
|
||||
phpEnv: {} as any
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新服务器信息
|
||||
*/
|
||||
const updateServer = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.monitor({})
|
||||
serverInfo.memory = data.memory
|
||||
serverInfo.phpEnv = data.phpEnv
|
||||
serverInfo.disk = data.disk
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateServer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 200px;
|
||||
}
|
||||
:deep(.el-descriptions__content) {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user