This commit is contained in:
2025-10-17 10:31:13 +08:00
commit e6e86f2ce0
1043 changed files with 1031839 additions and 0 deletions

View File

@@ -0,0 +1,262 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:config:export']"
>导出</el-button
>
</template>
<template #accountCount="scope">
<el-tag>{{ scope.row.accountCount }}</el-tag>
</template>
<template #visible="scope">
<dict-tag
v-if="scope.row.visible !== undefined"
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="scope.row.visible"
/>
</template>
<template #type="scope">
<dict-tag
v-if="scope.row.type !== undefined"
:type="DICT_TYPE.INFRA_CONFIG_TYPE"
:value="scope.row.type"
/>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraConfig' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
dialogWidth: '50%',
menuWidth: 180,
column: {
category: {
label: '参数分类',
width: 90,
rules: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }]
},
name: {
label: '参数名称',
search: true,
minWidth: 120,
rules: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
},
key: {
label: '参数键名',
search: true,
minWidth: 120,
rules: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }]
},
value: {
label: '参数键值',
rules: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }]
},
visible: {
label: '是否可见',
type: 'radio',
width: 85,
dicData: getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING),
rules: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
value: true
},
type: {
label: '系统内置',
search: true,
width: 95,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE),
display: false
},
remark: {
label: '备注',
type: 'textarea',
minRows: 2,
span: 24,
maxRows: 4,
overHidden: true
},
createTime: {
label: '创建时间',
search: true,
searchRange: true,
display: false,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 160,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:config'])
const exportLoading = ref(false) // 导出的加载中
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await ConfigApi.getConfigPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await ConfigApi.getConfig(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await ConfigApi.createConfig(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await ConfigApi.updateConfig(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ConfigApi.deleteConfig(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
let searchObj = { ...tableSearch.value }
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await ConfigApi.exportConfig(searchObj)
download.excel(data, '配置管理列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,268 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<!-- 自定义操作栏 -->
<template #menu="{ row, index }">
<el-button
link
type="primary"
@click="crudRef.rowEdit(row, index)"
v-hasPermi="['infra:data-source-config:update']"
v-if="row.id !== 0"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="crudRef.rowDel(row, index)"
v-hasPermi="['infra:data-source-config:delete']"
v-if="row.id !== 0"
>
删除
</el-button>
</template>
<template #dbCode="{ row }">
<span> {{ row.dbCode }} </span>
<el-tag
size="small"
:type="row.isConnect == 'Y' ? 'primary' : 'danger'"
effect="dark"
class="pos-absolute right-2px top-2px"
>
{{ row.isConnect == 'Y' ? '已连接' : '已断开' }}
</el-tag>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
defineOptions({ name: 'InfraDataSourceConfig' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const DBList = [
{
label: 'MySQL 5.7+',
value: 'MySQL',
driverClass: 'com.mysql.cj.jdbc.Driver',
url: 'jdbc:mysql://127.0.0.1:3306/jeelowcode?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai'
},
{
label: 'Oracle',
value: 'Oracle',
driverClass: 'oracle.jdbc.OracleDriver',
url: 'jdbc:oracle:thin:@127.0.0.1:1521:ORCL'
},
{
label: 'postgresql',
value: 'postgresql',
driverClass: 'org.postgresql.Driver',
url: 'jdbc:postgresql://127.0.0.1:5432/jeelowcode'
},
{
label: '达梦',
value: 'DM',
driverClass: 'dm.jdbc.driver.DmDriver',
url: 'jdbc:dm://127.0.0.1:5236/?jeelowcode&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8'
}
]
const tableOption = reactive({
editBtn: false,
delBtn: false,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
column: {
dbCode: {
label: '数据源编码',
minWidth: 120,
rules: [{ required: true, message: '数据源编码不能为空', trigger: 'blur' }],
editDisabled: true
},
name: {
label: '数据源名称',
minWidth: 120,
rules: [{ required: true, message: '数据源名称不能为空', trigger: 'blur' }]
},
dbType: {
label: '数据库类型',
type: 'select',
dicData: DBList,
width: 120,
rules: [{ required: true, message: '数据库类型不能为空', trigger: 'change' }],
change: ({ item }) => {
if (item) {
tableForm.value.driverClass = item.driverClass
tableForm.value.url = item.url
}
}
},
driverClass: {
label: '驱动类',
hide: true,
rules: [{ required: true, message: '驱动类不能为空', trigger: 'blur' }]
},
url: {
label: '数据源连接',
type: 'textarea',
minRows: 1,
maxRows: 3,
overHidden: true,
minWidth: 120,
rules: [{ required: true, message: '数据源连接不能为空', trigger: 'blur' }]
},
username: {
label: '用户名',
width: 120,
rules: [{ required: true, message: '用户名不能为空', trigger: 'blur' }]
},
password: {
label: '密码',
rules: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
hide: true
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 160,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:data-source-config'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await DataSourceConfigApi.getDataSourceConfigList()
tableData.value = data
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await DataSourceConfigApi.getDataSourceConfig(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await DataSourceConfigApi.createDataSourceConfig(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await DataSourceConfigApi.updateDataSourceConfig(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DataSourceConfigApi.deleteDataSourceConfig(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,57 @@
<template>
<ContentWrap title="数据库文档">
<div class="mb-10px">
<el-button type="primary" plain @click="handleExport('HTML')">
<Icon icon="ep:download" /> 导出 HTML
</el-button>
<el-button type="primary" plain @click="handleExport('Word')">
<Icon icon="ep:download" /> 导出 Word
</el-button>
<el-button type="primary" plain @click="handleExport('Markdown')">
<Icon icon="ep:download" /> 导出 Markdown
</el-button>
</div>
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import download from '@/utils/download'
import * as DbDocApi from '@/api/infra/dbDoc'
defineOptions({ name: 'InfraDBDoc' })
const loading = ref(true) // 是否加载中
const src = ref('') // HTML 的地址
/** 页面加载 */
const init = async () => {
try {
const data = await DbDocApi.exportHtml()
const blob = new Blob([data], { type: 'text/html' })
src.value = window.URL.createObjectURL(blob)
} finally {
loading.value = false
}
}
/** 处理导出 */
const handleExport = async (type: string) => {
if (type === 'HTML') {
const res = await DbDocApi.exportHtml()
download.html(res, '数据库文档.html')
}
if (type === 'Word') {
const res = await DbDocApi.exportWord()
download.word(res, '数据库文档.doc')
}
if (type === 'Markdown') {
const res = await DbDocApi.exportMarkdown()
download.markdown(res, '数据库文档.md')
}
}
/** 初始化 */
onMounted(async () => {
await init()
})
</script>

View File

@@ -0,0 +1,25 @@
<template>
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame v-if="!loading" v-loading="loading" :src="url" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraDruid' })
const loading = ref(true) // 是否加载中
const url = ref(import.meta.env.VITE_BASE_URL + '/druid/index.html')
/** 初始化 */
onMounted(async () => {
try {
const data = await ConfigApi.getConfigKey('url.druid')
if (data && data.length > 0) {
url.value = data
}
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,103 @@
<template>
<DesignPopup v-model="dialogVisible" title="上传文件" width="40%" :is-footer="true">
<div class="p-20px">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="uploadUrl"
:auto-upload="false"
:data="data"
:disabled="formLoading"
:limit="1"
:on-change="handleFileChange"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
:http-request="httpRequest"
accept=".jpg, .png, .gif"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text"> 将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示仅允许导入 jpgpnggif 格式文件
</div>
</template>
</el-upload>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitFileForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'InfraFileForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const fileList = ref([]) // 文件列表
const data = ref({ path: '' })
const uploadRef = ref()
const { uploadUrl, httpRequest } = useUpload()
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 处理上传的文件发生变化 */
const handleFileChange = (file) => {
data.value.path = file.name
}
/** 提交表单 */
const submitFileForm = () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
unref(uploadRef)?.submit()
}
/** 文件上传成功处理 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitFormSuccess = () => {
// 清理
dialogVisible.value = false
formLoading.value = false
unref(uploadRef)?.clearFiles()
// 提示成功,并刷新
setTimeout(() => {
message.success(t('common.createSuccess'))
emit('success')
}, 1000)
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
// 重置上传状态和文件
formLoading.value = false
uploadRef.value?.clearFiles()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>

View File

@@ -0,0 +1,210 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
@search-change="searchChange"
@search-reset="resetChange"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left>
<el-button type="primary" plain @click="openForm">
<Icon icon="ep:upload" class="mr-5px" /> 上传文件
</el-button>
</template>
<template #urlValue="scope">
{{ scope.row.url }}
</template>
<template #url="{ row }">
<el-image
v-if="row.type.includes('image')"
class="h-80px w-80px"
lazy
:src="row.url"
:preview-src-list="[row.url]"
preview-teleported
fit="cover"
/>
<el-link
v-else-if="row.type.includes('pdf')"
type="primary"
:href="row.url"
:underline="false"
target="_blank"
>预览</el-link
>
<el-link v-else type="primary" download :href="row.url" :underline="false" target="_blank"
>下载</el-link
>
</template>
</avue-crud>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="formRef" @success="getTableData" />
</template>
<script lang="ts" setup>
import { fileSizeFormatter } from '@/utils'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as FileApi from '@/api/infra/file'
import FileForm from './FileForm.vue'
defineOptions({ name: 'InfraFile' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
name: {
label: '文件名',
minWidth: 120,
overHidden: true
},
path: {
label: '文件路径',
minWidth: 120,
search: true,
overHidden: true
},
urlValue: {
label: 'URL',
minWidth: 100,
overHidden: true
},
size: {
label: '文件大小',
minWidth: 100,
formatter: fileSizeFormatter,
width: '120px'
},
type: {
label: '文件类型',
minWidth: 120,
search: true,
overHidden: true
},
url: {
label: '文件内容',
width: 120
},
createTime: {
label: '上传时间',
searchRange: true,
search: true,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 160,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:file'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await FileApi.getFilePage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await FileApi.deleteFile(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = () => {
formRef.value.open()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,452 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<!-- 表格 -->
<template #storage="scope">
<dict-tag
:type="DICT_TYPE.INFRA_FILE_STORAGE"
:value="scope.row.storage ? scope.row.storage : ''"
/>
</template>
<template #master="scope">
<dict-tag
v-if="scope.row.master !== undefined"
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="scope.row.master"
/>
</template>
<!-- 表单 -->
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
link
type="primary"
class="is-text"
icon="el-icon-operation"
:disabled="row.master"
@click="handleMaster(row.id)"
v-hasPermi="['infra:file-config:update']"
>
主配置
</el-button>
<el-button
link
class="is-text"
icon="el-icon-tickets"
type="primary"
@click="handleTest(row.id)"
>
测试
</el-button>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as FileConfigApi from '@/api/infra/fileConfig'
defineOptions({ name: 'InfraFileConfig' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
menuWidth: 300,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 100,
span: 24,
dialogWidth: '50%',
column: {
id: {
label: '编号',
width: 80,
display: false
},
name: {
label: '配置名',
search: true,
minWidth: 90,
rules: [{ required: true, message: '配置名不能为空', trigger: 'blur' }]
},
remark: {
label: '备注'
},
storage: {
label: '存储器',
type: 'select',
disabled: false,
search: true,
span: 8,
minWidth: 90,
dicData: getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE),
rules: [{ required: true, message: '存储器不能为空', trigger: 'blur' }],
change: ({ value, column }) => {
let {
basePath,
host,
port,
username,
password,
mode,
endpoint,
bucket,
accessKey,
accessSecret,
domain
} = tableOption.column
if (value) {
domain.display = true
if (value === 20) {
domain.rules = []
} else {
domain.rules = [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
}
} else {
domain.display = false
}
if (value >= 10 && value <= 12) {
basePath.display = true
} else {
basePath.display = false
}
if (value >= 11 && value <= 12) {
host.display = true
port.display = true
username.display = true
password.display = true
} else {
host.display = false
port.display = false
username.display = false
password.display = false
}
if (value === 11) {
mode.display = true
} else {
mode.display = false
}
if (value === 20) {
endpoint.display = true
bucket.display = true
accessKey.display = true
accessSecret.display = true
} else {
endpoint.display = false
bucket.display = false
accessKey.display = false
accessSecret.display = false
}
}
},
master: {
label: '主配置',
type: 'select',
display: false,
span: 8,
minWidth: 90,
dicData: getIntDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)
},
basePath: {
label: '基础路径',
hide: true,
display: false,
rules: [{ required: true, message: '基础路径不能为空', trigger: 'blur' }]
},
host: {
label: '主机地址',
hide: true,
display: false,
rules: [{ required: true, message: '主机地址不能为空', trigger: 'blur' }]
},
port: {
label: '主机端口',
type: 'number',
span: 8,
hide: true,
display: false,
rules: [{ required: true, message: '主机端口不能为空', trigger: 'blur' }]
},
username: {
label: '用户名',
hide: true,
display: false,
rules: [{ required: true, message: '用户名不能为空', trigger: 'blur' }]
},
password: {
label: '密码',
hide: true,
display: false,
rules: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
},
mode: {
label: '连接模式',
type: 'radio',
hide: true,
display: false,
dicData: [
{ label: '主动模式', value: 'Active' },
{ label: '被动模式', value: 'Passive' }
],
rules: [{ required: true, message: '连接模式不能为空', trigger: 'blur' }]
},
endpoint: {
label: '节点地址',
hide: true,
display: false,
rules: [{ required: true, message: '节点地址不能为空', trigger: 'blur' }]
},
bucket: {
label: '存储 bucket',
hide: true,
display: false,
rules: [{ required: true, message: '存储 bucket不能为空', trigger: 'blur' }]
},
accessKey: {
label: 'accessKey',
hide: true,
display: false,
rules: [{ required: true, message: 'accessKey不能为空', trigger: 'blur' }]
},
accessSecret: {
label: 'accessSecret',
hide: true,
display: false,
rules: [{ required: true, message: 'accessSecret不能为空', trigger: 'blur' }]
},
domain: {
label: '自定义域名',
hide: true,
display: false,
rules: [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
},
createTime: {
label: '创建时间',
searchRange: true,
search: true,
display: false,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
let tableChildForm: string[] = reactive([]) // 保存参数,方便提交
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:file-config'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await FileConfigApi.getFileConfigPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableOption.column.storage.disabled = true
let data = await FileConfigApi.getFileConfig(tableForm.value.id)
tableChildForm = Object.keys(data.config)
// 数据处理
data = {
storage: data.storage,
remark: data.remark,
name: data.name,
master: data.master,
id: data.id,
createTime: data.createTime,
...data.config
}
tableForm.value = data
} else {
tableOption.column.storage.disabled = false
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
// 删除多余表单
Object.keys(tableOption.column).forEach((item) => {
if (tableOption.column[item].display != undefined && !tableOption.column[item].display)
delete form[item]
})
// 添加config参数
let config = {}
Object.keys(form).forEach((item) => {
if (item !== 'storage' && item !== 'remark' && item !== 'name') {
config[item] = form[item]
delete form[item]
}
})
form.config = config
// 发送请求
let bool = await FileConfigApi.createFileConfig(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
// 添加config参数
let config = {}
tableChildForm.forEach((item) => {
if (form[item]) {
config[item] = form[item]
}
})
Object.keys(form).forEach((item) => {
if (
item !== 'storage' &&
item !== 'remark' &&
item !== 'name' &&
item !== 'master' &&
item !== 'id' &&
item !== 'createTime'
)
delete form[item] //清除多余参数(已经放在config中)
})
form.config = config
let bool = await FileConfigApi.updateFileConfig(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await FileConfigApi.deleteFileConfig(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 主配置按钮操作 */
const handleMaster = async (id) => {
try {
await message.confirm('是否确认修改配置编号为"' + id + '"的数据项为主配置?')
await FileConfigApi.updateFileConfigMaster(id)
message.success(t('common.updateSuccess'))
await getTableData()
} catch {}
}
/** 测试按钮操作 */
const handleTest = async (id) => {
loading.value = true
try {
const response = await FileConfigApi.testFileConfig(id)
message.alert(
`<div>测试通过,上传文件成功!访问地址:</div>
<div style="word-break: break-word;">${response}</div>
`,
'',
{
dangerouslyUseHTMLString: true
}
)
} finally {
loading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,398 @@
<template>
<ContentWrap>
<avue-crud
v-model="tableForm"
ref="crudRef"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:job:export']"
>导出</el-button
>
<el-button type="info" plain @click="handleJobLog()" v-hasPermi="['infra:job:query']">
<Icon icon="ep:zoom-in" class="mr-5px" /> 执行日志
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
link
class="is-text"
:icon="
scope.row.status === InfraJobStatusEnum.STOP
? 'el-icon-video-play'
: 'el-icon-video-pause'
"
@click="handleChangeStatus(scope.row)"
v-hasPermi="['infra:job:update']"
>
{{ scope.row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停' }}
</el-button>
<el-dropdown
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['infra:job:trigger', 'infra:job:query']"
>
<div class="pt-3px pr-4px pb-3px pl-4px cursor-pointer">
<el-text type="primary">
<span>更多</span>
<Icon :size="16" icon="iconamoon:arrow-down-2-light" />
</el-text>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="handleRun" v-if="checkPermi(['infra:job:trigger'])">
执行一次
</el-dropdown-item>
<el-dropdown-item
@click="crudRef.rowView(scope.row, scope.index)"
v-if="checkPermi(['infra:job:query'])"
>
任务详细
</el-dropdown-item>
<el-dropdown-item command="handleJobLog" v-if="checkPermi(['infra:job:query'])">
调度日志
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #accountCount="scope">
<el-tag>{{ scope.row.accountCount }}</el-tag>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.INFRA_JOB_STATUS"
:value="scope.row.status"
/>
</template>
<template #cronExpression-form="{ type }">
<div v-if="type == 'view'">{{ tableForm.cronExpression }}</div>
<Crontab v-else v-model="tableForm.cronExpression" />
</template>
<template #stayus-form="{ value }">
<dict-tag v-if="value" :type="DICT_TYPE.INFRA_JOB_STATUS" :value="value" />
</template>
<template #executionTime-form>
<div class="pt-10px">
<el-timeline>
<el-timeline-item
v-for="(nextTime, index) in nextTimes"
:key="index"
:timestamp="formatDate(nextTime)"
>
{{ index + 1 }}
</el-timeline-item>
</el-timeline>
</div>
</template>
</avue-crud>
</ContentWrap>
<DesignPopup v-model="logPopup.show" :title="logPopup.title" width="80%" controlType="drawer">
<div class="p-20px">
<InfraJobLog ref="logRef" :jobId="logPopup.jobId"></InfraJobLog>
</div>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { checkPermi } from '@/utils/permission'
import { formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as JobApi from '@/api/infra/job'
import { InfraJobStatusEnum } from '@/utils/constants'
import InfraJobLog from './logger/index.vue'
defineOptions({ name: 'SystemTenant' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const nextTimes = ref([]) // 下一轮执行时间的数组
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchLabelWidth: 120,
menuWidth: 300,
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
column: {
name: {
label: '任务名称',
minWidth: 110,
search: true,
rules: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }]
},
status: {
label: '任务状态',
width: 90,
search: true,
type: 'select',
display: false,
dicData: getIntDictOptions(DICT_TYPE.INFRA_JOB_STATUS)
},
handlerName: {
label: '处理器的名字',
minWidth: 100,
search: true,
editDisabled: true,
rules: [{ required: true, message: '处理器的名字不能为空', trigger: 'blur' }]
},
handlerParam: {
label: '处理器的参数',
minWidth: 110
},
cronExpression: {
label: 'CRON 表达式',
minWidth: 110,
rules: [{ required: true, message: 'CRON 表达式不能为空', trigger: 'blur' }]
},
retryCount: {
label: '重试次数',
hide: true,
rules: [{ required: true, message: '重试次数不能为空', trigger: 'blur' }],
placeholder: '请输入重试次数。设置为 0 时,不进行重试'
},
retryInterval: {
label: '重试间隔',
hide: true,
rules: [{ required: true, message: '重试间隔不能为空', trigger: 'blur' }],
placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔'
},
monitorTimeout: {
label: '监控超时时间',
placeholder: '请输入监控超时时间,单位:毫秒',
hide: true
},
executionTime: {
label: '后续执行时间',
display: false,
hide: true
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const logPopup = ref({ show: false, title: '', jobId: 0 })
const permission = getCurrPermi(['infra:job'])
const exportLoading = ref(false) // 导出的加载中
const crudRef = ref()
const logRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await JobApi.getJobPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
loading.value = true
tableForm.value = await JobApi.getJob(tableForm.value.id)
if (type == 'view') nextTimes.value = await JobApi.getJobNextTimes(tableForm.value.id!)
loading.value = false
}
if (type === 'view') {
tableOption.column.status.display = true
tableOption.column.executionTime.display = true
} else {
tableOption.column.status.display = false
tableOption.column.executionTime.display = false
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await JobApi.createJob(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await JobApi.updateJob(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await JobApi.deleteJob(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 修改状态操作 */
const handleChangeStatus = async (row: JobApi.JobVO) => {
try {
// 修改状态的二次确认
const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
await message.confirm(
'确认要' + text + '定时任务编号为"' + row.id + '"的数据项?',
t('common.reminder')
)
const status =
row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP
await JobApi.updateJobStatus(row.id, status)
message.success(text + '成功')
// 刷新列表
await getTableData()
} catch {
// 取消后,进行恢复按钮
row.status =
row.status === InfraJobStatusEnum.NORMAL ? InfraJobStatusEnum.STOP : InfraJobStatusEnum.NORMAL
}
}
/** '更多'操作按钮 */
const handleCommand = (command, row) => {
switch (command) {
case 'handleRun':
handleRun(row)
break
case 'handleJobLog':
handleJobLog(row)
break
default:
break
}
}
/** 执行一次 */
const handleRun = async (row: JobApi.JobVO) => {
try {
// 二次确认
await message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder'))
// 提交执行
await JobApi.runJob(row.id)
message.success('执行成功')
// 刷新列表
await getTableData()
} catch {}
}
/** 跳转执行日志 */
const handleJobLog = (row?) => {
logPopup.value = {
show: true,
title: row ? `${row.name} 调度日志` : '所有的调度日志',
jobId: row?.id || 0
}
setTimeout(() => {
if (logRef.value) logRef.value.resetChange()
}, 30)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
let searchObj = { ...tableSearch.value }
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await JobApi.exportJob(searchObj)
download.excel(data, '定时任务列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss" scoped>
.el-dropdown {
padding: 4px 2px;
}
</style>

View File

@@ -0,0 +1,230 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
@search-change="searchChange"
@search-reset="resetChange"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:tenant:export']"
>导出</el-button
>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.INFRA_JOB_LOG_STATUS"
:value="scope.row.status"
/>
</template>
<template #beginTimeText-form>
{{ formatDate(tableForm.beginTime) + ' ~ ' + formatDate(tableForm.endTime) }}
</template>
<template #duration-form="{ value }">
{{ value + ' 毫秒' }}
</template>
<template #status-form="{ value }">
<dict-tag
v-if="value !== undefined"
:type="DICT_TYPE.INFRA_JOB_LOG_STATUS"
:value="value"
/>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDate, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as JobLogApi from '@/api/infra/jobLog'
defineOptions({ name: 'InfraJobLog' })
interface Props {
jobId?: number
}
const props = defineProps<Props>()
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchLabelWidth: 100,
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
jobId: {
label: '任务编号',
width: 90
},
handlerName: {
label: '处理器的名字',
search: true,
searchSpan: 5
},
beginTime: {
label: '执行时间',
hide: true,
display: false,
search: true,
type: 'date',
searchRange: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始执行时间',
endPlaceholder: '结束执行时间',
searchSpan: 8,
},
handlerParam: {
label: '处理器的参数'
},
executeIndex: {
label: '第几次执行',
width: 100
},
beginTimeText: {
label: '执行时间',
html: true,
width: 160,
formatter: (row) => {
return `<div>
<div>${formatDate(row.beginTime)}</div>
<div>${formatDate(row.endTime)}</div>
</div>`
}
},
duration: {
label: '执行时长',
width: 100,
formatter: (row) => {
return row.duration + '毫秒'
}
},
status: {
label: '任务状态',
searchSpan: 5,
search: true,
width: 90,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS)
},
result: {
label: '执行结果',
hide: true
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj: any = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.beginTime?.length) {
const dateArr = getSearchDate(searchObj.beginTime)
searchObj.beginTime = dateArr[0]
searchObj.endTime = dateArr[1]
} else delete searchObj.beginTime
if (props.jobId !== 0) searchObj['jobId'] = props.jobId
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await JobLogApi.getJobLogPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
let searchObj: any = { ...tableSearch.value }
if (props.jobId) searchObj.jobId = props.jobId
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await JobLogApi.exportJobLog(searchObj)
download.excel(data, '调度日志列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
defineExpose({ resetChange })
</script>

View File

@@ -0,0 +1,265 @@
<template>
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
<el-row>
<!-- 基本信息 -->
<el-col :span="24" class="card-box" shadow="hover">
<el-card>
<el-descriptions title="基本信息" :column="6" border>
<el-descriptions-item label="Redis版本 :">
{{ cache?.info?.redis_version }}
</el-descriptions-item>
<el-descriptions-item label="运行模式 :">
{{ cache?.info?.redis_mode == 'standalone' ? '单机' : '集群' }}
</el-descriptions-item>
<el-descriptions-item label="端口 :">
{{ cache?.info?.tcp_port }}
</el-descriptions-item>
<el-descriptions-item label="客户端数 :">
{{ cache?.info?.connected_clients }}
</el-descriptions-item>
<el-descriptions-item label="运行时间(天) :">
{{ cache?.info?.uptime_in_days }}
</el-descriptions-item>
<el-descriptions-item label="使用内存 :">
{{ cache?.info?.used_memory_human }}
</el-descriptions-item>
<el-descriptions-item label="使用CPU :">
{{ cache?.info ? parseFloat(cache?.info?.used_cpu_user_children).toFixed(2) : '' }}
</el-descriptions-item>
<el-descriptions-item label="内存配置 :">
{{ cache?.info?.maxmemory_human }}
</el-descriptions-item>
<el-descriptions-item label="AOF是否开启 :">
{{ cache?.info?.aof_enabled == '0' ? '否' : '是' }}
</el-descriptions-item>
<el-descriptions-item label="RDB是否成功 :">
{{ cache?.info?.rdb_last_bgsave_status }}
</el-descriptions-item>
<el-descriptions-item label="Key数量 :">
{{ cache?.dbSize }}
</el-descriptions-item>
<el-descriptions-item label="网络入口/出口 :">
{{ cache?.info?.instantaneous_input_kbps }}kps/
{{ cache?.info?.instantaneous_output_kbps }}kps
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<!-- 命令统计 -->
<el-col :span="12" class="mt-3">
<el-card :gutter="12" shadow="hover">
<Echart :options="commandStatsRefChika" :height="420" />
</el-card>
</el-col>
<!-- 内存使用量统计 -->
<el-col :span="12" class="mt-3">
<el-card class="ml-3" :gutter="12" shadow="hover">
<Echart :options="usedmemoryEchartChika" :height="420" />
</el-card>
</el-col>
</el-row>
</el-scrollbar>
</template>
<script lang="ts" setup>
import * as RedisApi from '@/api/infra/redis'
import { RedisMonitorInfoVO } from '@/api/infra/redis/types'
const cache = ref<RedisMonitorInfoVO>()
// 基本信息
const readRedisInfo = async () => {
const data = await RedisApi.getCache()
cache.value = data
}
// 内存使用情况
const usedmemoryEchartChika = reactive<any>({
title: {
// 仪表盘标题。
text: '内存使用情况',
left: 'center',
show: true, // 是否显示标题,默认 true。
offsetCenter: [0, '20%'], //相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
color: 'yellow', // 文字的颜色,默认 #333。
fontSize: 20 // 文字的字体大小,默认 15。
},
toolbox: {
show: false,
feature: {
restore: { show: true },
saveAsImage: { show: true }
}
},
series: [
{
name: '峰值',
type: 'gauge',
min: 0,
max: 50,
splitNumber: 10,
//这是指针的颜色
color: '#F5C74E',
radius: '85%',
center: ['50%', '50%'],
startAngle: 225,
endAngle: -45,
axisLine: {
// 坐标轴线
lineStyle: {
// 属性lineStyle控制线条样式
color: [
[0.2, '#7FFF00'],
[0.8, '#00FFFF'],
[1, '#FF0000']
],
//width: 6 外框的大小(环的宽度)
width: 10
}
},
axisTick: {
// 坐标轴小标记
//里面的线长是5短线
length: 5, // 属性length控制线长
lineStyle: {
// 属性lineStyle控制线条样式
color: '#76D9D7'
}
},
splitLine: {
// 分隔线
length: 20, // 属性length控制线长
lineStyle: {
// 属性lineStyle详见lineStyle控制线条样式
color: '#76D9D7'
}
},
axisLabel: {
color: '#76D9D7',
distance: 15,
fontSize: 15
},
pointer: {
// 指针的大小
width: 7,
show: true
},
detail: {
textStyle: {
fontWeight: 'normal',
// 里面文字下的数值大小50
fontSize: 15,
color: '#FFFFFF'
},
valueAnimation: true
},
progress: {
show: true
}
}
]
})
// 指令使用情况
const commandStatsRefChika = reactive({
title: {
text: '命令统计',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 30,
top: 10,
bottom: 20,
data: [] as any[],
textStyle: {
color: '#a1a1a1'
}
},
series: [
{
name: '命令',
type: 'pie',
radius: [20, 120],
center: ['40%', '60%'],
data: [] as any[],
roseType: 'radius',
label: {
show: true
},
emphasis: {
label: {
show: true
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
/** 加载数据 */
const getSummary = () => {
// 初始化命令图表
initCommandStatsChart()
usedMemoryInstance()
}
/** 命令使用情况 */
const initCommandStatsChart = async () => {
usedmemoryEchartChika.series[0].data = []
// 发起请求
try {
const data = await RedisApi.getCache()
cache.value = data
// 处理数据
const commandStats = [] as any[]
const nameList = [] as string[]
data.commandStats.forEach((row) => {
commandStats.push({
name: row.command,
value: row.calls
})
nameList.push(row.command)
})
commandStatsRefChika.legend.data = nameList
commandStatsRefChika.series[0].data = commandStats
} catch {}
}
const usedMemoryInstance = async () => {
try {
const data = await RedisApi.getCache()
cache.value = data
// 仪表盘详情,用于显示数据。
usedmemoryEchartChika.series[0].detail = {
show: true, // 是否显示详情,默认 true。
offsetCenter: [0, '50%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
color: 'auto', // 文字的颜色,默认 auto。
fontSize: 30, // 文字的字体大小,默认 15。
formatter: cache.value!.info.used_memory_human // 格式化函数或者字符串
}
usedmemoryEchartChika.series[0].data[0] = {
value: cache.value!.info.used_memory_human,
name: '内存消耗'
}
usedmemoryEchartChika.tooltip = {
formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
}
} catch {}
}
/** 初始化 **/
onMounted(() => {
// 读取 redis 信息
readRedisInfo()
// 加载数据
getSummary()
})
</script>

View File

@@ -0,0 +1,27 @@
<template>
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraAdminServer' })
const loading = ref(true) // 是否加载中
const src = ref(import.meta.env.VITE_BASE_URL + '/admin/applications')
/** 初始化 */
onMounted(async () => {
try {
// 友情提示:如果访问出现 404 问题:
const data = await ConfigApi.getConfigKey('url.spring-boot-admin')
if (data && data.length > 0) {
src.value = data
}
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,25 @@
<template>
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraSkyWalking' })
const loading = ref(true) // 是否加载中
const src = ref('')
/** 初始化 */
onMounted(async () => {
try {
const data = await ConfigApi.getConfigKey('url.skywalking')
if (data && data.length > 0) {
src.value = data
}
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,26 @@
<template>
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraSwagger' })
const loading = ref(true) // 是否加载中
const src = ref(import.meta.env.VITE_BASE_URL + '/doc.html') // Knife4j UI
// const src = ref(import.meta.env.VITE_BASE_URL + '/swagger-ui') // Swagger UI
/** 初始化 */
onMounted(async () => {
try {
const data = await ConfigApi.getConfigKey('url.swagger')
if (data && data.length > 0) {
src.value = data
}
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,184 @@
<template>
<div class="flex">
<!-- 左侧建立连接发送消息 -->
<el-card :gutter="12" class="w-1/2" shadow="always">
<template #header>
<div class="card-header">
<span>连接</span>
</div>
</template>
<div class="flex items-center">
<span class="mr-4 text-lg font-medium"> 连接状态: </span>
<el-tag :color="getTagColor">{{ status }}</el-tag>
</div>
<hr class="my-4" />
<div class="flex">
<el-input v-model="server" disabled>
<template #prepend>服务地址</template>
</el-input>
<el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggleConnectStatus">
{{ getIsOpen ? '关闭连接' : '开启连接' }}
</el-button>
</div>
<p class="mt-4 text-lg font-medium">消息输入框</p>
<hr class="my-4" />
<el-input
v-model="sendText"
:autosize="{ minRows: 2, maxRows: 4 }"
:disabled="!getIsOpen"
clearable
placeholder="请输入你要发送的消息"
type="textarea"
/>
<el-select v-model="sendUserId" class="mt-4" placeholder="请选择发送人">
<el-option key="" label="所有人" value="" />
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
</el-select>
<el-button :disabled="!getIsOpen" block class="ml-2 mt-4" type="primary" @click="handlerSend">
发送
</el-button>
</el-card>
<!-- 右侧消息记录 -->
<el-card :gutter="12" class="w-1/2" shadow="always">
<template #header>
<div class="card-header">
<span>消息记录</span>
</div>
</template>
<div class="max-h-80 overflow-auto">
<ul>
<li v-for="msg in messageReverseList" :key="msg.time" class="mt-2">
<div class="flex items-center">
<span class="text-primary mr-2 font-medium">收到消息:</span>
<span>{{ formatDate(new Date(msg.time)) }}</span>
</div>
<div>
{{ msg.text }}
</div>
</li>
</ul>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { useWebSocket } from '@vueuse/core'
import { getRefreshToken } from '@/utils/auth'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'InfraWebSocket' })
const message = useMessage() // 消息弹窗
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因WebSocket 无法方便的刷新访问令牌
) // WebSocket 服务地址
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色
/** 发起 WebSocket 连接 */
const { status, data, send, close, open } = useWebSocket(server.value, {
autoReconnect: true,
heartbeat: true
})
/** 监听接收到的数据 */
const messageList = ref([] as { time: number; text: string }[]) // 消息列表
const messageReverseList = computed(() => messageList.value.slice().reverse())
watchEffect(() => {
if (!data.value) {
return
}
try {
// 1. 收到心跳
if (data.value === 'pong') {
// state.recordList.push({
// text: '【心跳】',
// time: new Date().getTime()
// })
return
}
// 2.1 解析 type 消息类型
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
const content = JSON.parse(jsonMessage.content)
if (!type) {
message.error('未知的消息类型:' + data.value)
return
}
// 2.2 消息类型demo-message-receive
if (type === 'demo-message-receive') {
const single = content.single
if (single) {
messageList.value.push({
text: `【单发】用户编号(${content.fromUserId})${content.text}`,
time: new Date().getTime()
})
} else {
messageList.value.push({
text: `【群发】用户编号(${content.fromUserId})${content.text}`,
time: new Date().getTime()
})
}
return
}
// 2.3 消息类型notice-push
if (type === 'notice-push') {
messageList.value.push({
text: `【系统通知】:${content.title}`,
time: new Date().getTime()
})
return
}
message.error('未处理消息:' + data.value)
} catch (error) {
message.error('处理消息发生异常:' + data.value)
console.error(error)
}
})
/** 发送消息 */
const sendText = ref('') // 发送内容
const sendUserId = ref('') // 发送人
const handlerSend = () => {
// 1.1 先 JSON 化 message 消息内容
const messageContent = JSON.stringify({
text: sendText.value,
toUserId: sendUserId.value
})
// 1.2 再 JSON 化整个消息
const jsonMessage = JSON.stringify({
type: 'demo-message-send',
content: messageContent
})
// 2. 最后发送消息
send(jsonMessage)
sendText.value = ''
}
/** 切换 websocket 连接状态 */
const toggleConnectStatus = () => {
if (getIsOpen.value) {
close()
} else {
open()
}
}
/** 初始化 **/
const userList = ref<any[]>([]) // 用户列表
onMounted(async () => {
// 获取用户列表
userList.value = await UserApi.getSimpleUserList()
})
</script>