初始化项目,自 v1.7.1 版本开始

This commit is contained in:
YunaiV
2023-02-11 00:44:00 +08:00
parent 11161afc1a
commit 56f3017baa
548 changed files with 52096 additions and 61 deletions

View File

@@ -0,0 +1,73 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '日志编号',
action: true,
actionWidth: '80px',
columns: [
{
title: '链路追踪',
field: 'traceId'
},
{
title: '用户编号',
field: 'userId',
isSearch: true
},
{
title: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
dictClass: 'number',
isSearch: true
},
{
title: '应用名',
field: 'applicationName',
isSearch: true
},
{
title: '请求方法名',
field: 'requestMethod'
},
{
title: '请求地址',
field: 'requestUrl',
isSearch: true
},
{
title: '请求时间',
field: 'beginTime',
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: '执行时长',
field: 'duration',
table: {
slots: {
default: 'duration_default'
}
}
},
{
title: '操作结果',
field: 'resultCode',
isSearch: true,
table: {
slots: {
default: 'resultCode_default'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,62 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #duration_default="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #resultCode_default="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:api-access-log:query']"
@click="handleDetail(row)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData">
<template #duration="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #resultCode="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="ApiAccessLog">
import { allSchemas } from './apiAccessLog.data'
import * as ApiAccessLogApi from '@/api/infra/apiAccessLog'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable] = useXTable({
allSchemas: allSchemas,
topActionSlots: false,
getListApi: ApiAccessLogApi.getApiAccessLogPageApi
})
// ========== 详情相关 ==========
const detailData = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
// 详情操作
const handleDetail = (row: ApiAccessLogApi.ApiAccessLogVO) => {
// 设置数据
detailData.value = row
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
</script>

View File

@@ -0,0 +1,76 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '日志编号',
action: true,
actionWidth: '300',
columns: [
{
title: '链路追踪',
field: 'traceId',
isTable: false
},
{
title: '用户编号',
field: 'userId',
isSearch: true
},
{
title: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
isSearch: true
},
{
title: '应用名',
field: 'applicationName',
isSearch: true
},
{
title: '请求方法名',
field: 'requestMethod'
},
{
title: '请求地址',
field: 'requestUrl',
isSearch: true
},
{
title: '异常发生时间',
field: 'exceptionTime',
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: '异常名',
field: 'exceptionName'
},
{
title: '处理状态',
field: 'processStatus',
dictType: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '处理人',
field: 'processUserId',
isTable: false
},
{
title: '处理时间',
field: 'processTime',
formatter: 'formatDate',
isTable: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,99 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作导出 -->
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
@click="exportList('错误数据.xls')"
/>
</template>
<template #duration_default="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #resultCode_default="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:api-access-log:query']"
@click="handleDetail(row)"
/>
<XTextButton
preIcon="ep:cpu"
title="已处理"
v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
v-hasPermi="['infra:api-error-log:update-status']"
@click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.DONE, '已处理')"
/>
<XTextButton
preIcon="ep:mute-notification"
title="已忽略"
v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
v-hasPermi="['infra:api-error-log:update-status']"
@click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.IGNORE, '已忽略')"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData" />
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="ApiErrorLog">
import { allSchemas } from './apiErrorLog.data'
import * as ApiErrorLogApi from '@/api/infra/apiErrorLog'
import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
const { t } = useI18n() // 国际化
const message = useMessage()
// ========== 列表相关 ==========
const [registerTable, { reload, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: ApiErrorLogApi.getApiErrorLogPageApi,
exportListApi: ApiErrorLogApi.exportApiErrorLogApi
})
// ========== 详情相关 ==========
const detailData = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
// 详情操作
const handleDetail = (row: ApiErrorLogApi.ApiErrorLogVO) => {
// 设置数据
detailData.value = row
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
// 异常处理操作
const handleProcessClick = (
row: ApiErrorLogApi.ApiErrorLogVO,
processSttatus: number,
type: string
) => {
message
.confirm('确认标记为' + type + '?', t('common.reminder'))
.then(async () => {
await ApiErrorLogApi.updateApiErrorLogPageApi(row.id, processSttatus)
message.success(t('common.updateSuccess'))
})
.finally(async () => {
// 刷新列表
await reload()
})
.catch(() => {})
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<ContentWrap>
<el-row>
<el-col>
<div class="mb-2 float-right">
<el-button size="small" @click="setJson"> 导入JSON</el-button>
<el-button size="small" @click="setOption"> 导入Options</el-button>
<el-button size="small" type="primary" @click="showJson">生成JSON</el-button>
<el-button size="small" type="success" @click="showOption">生成Options</el-button>
<el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
<el-button size="small" @click="changeLocale">中英切换</el-button>
</div>
</el-col>
<el-col>
<fc-designer ref="designer" height="780px" />
</el-col>
</el-row>
<Dialog :title="dialogTitle" v-model="dialogVisible" maxHeight="600">
<div ref="editor" v-if="dialogVisible">
<XTextButton style="float: right" :title="t('common.copy')" @click="copy(formValue)" />
<el-scrollbar height="580">
<pre>
{{ formValue }}
</pre>
</el-scrollbar>
</div>
<span style="color: red" v-if="err">输入内容格式有误!</span>
</Dialog>
</ContentWrap>
</template>
<script setup lang="ts" name="Build">
import formCreate from '@form-create/element-ui'
import { useClipboard } from '@vueuse/core'
const { t } = useI18n()
const message = useMessage()
const designer = ref()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const err = ref(false)
const type = ref(-1)
const formValue = ref('')
const openModel = (title: string) => {
dialogVisible.value = true
dialogTitle.value = title
}
const setJson = () => {
openModel('导入JSON--未实现')
}
const setOption = () => {
openModel('导入Options--未实现')
}
const showJson = () => {
openModel('生成JSON')
type.value = 0
formValue.value = designer.value.getRule()
}
const showOption = () => {
openModel('生成Options')
type.value = 1
formValue.value = designer.value.getOption()
}
const showTemplate = () => {
openModel('生成组件')
type.value = 2
formValue.value = makeTemplate()
}
const changeLocale = () => {
console.info('changeLocale')
}
/** 复制 **/
const copy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) {
message.error(t('common.copyError'))
} else {
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
}
const makeTemplate = () => {
const rule = designer.value.getRule()
const opt = designer.value.getOption()
return `<template>
<form-create
v-model="fapi"
:rule="rule"
:option="option"
@submit="onSubmit"
></form-create>
</template>
<script setup lang=ts>
import formCreate from "@form-create/element-ui";
const faps = ref(null)
const rule = ref('')
const option = ref('')
const init = () => {
rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}')
option.value = formCreate.parseJson('${JSON.stringify(opt)}')
}
const onSubmit = (formData) => {
//todo 提交表单
}
init()
<\/script>`
}
</script>

View File

@@ -0,0 +1,67 @@
<template>
<ContentWrap>
<ContentDetailWrap :title="title" @back="push('/infra/codegen')">
<el-tabs v-model="activeName">
<el-tab-pane label="基本信息" name="basicInfo">
<BasicInfoForm ref="basicInfoRef" :basicInfo="tableCurrentRow" />
</el-tab-pane>
<el-tab-pane label="字段信息" name="cloum">
<CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" />
</el-tab-pane>
</el-tabs>
<template #right>
<XButton
type="primary"
:title="t('action.save')"
:loading="loading"
@click="submitForm()"
/>
</template>
</ContentDetailWrap>
</ContentWrap>
</template>
<script setup lang="ts">
import { BasicInfoForm, CloumInfoForm } from './components'
import { getCodegenTableApi, updateCodegenTableApi } from '@/api/infra/codegen'
import { CodegenTableVO, CodegenColumnVO, CodegenUpdateReqVO } from '@/api/infra/codegen/types'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const { push } = useRouter()
const { query } = useRoute()
const loading = ref(false)
const title = ref('代码生成')
const activeName = ref('basicInfo')
const cloumInfoRef = ref(null)
const tableCurrentRow = ref<CodegenTableVO>()
const cloumCurrentRow = ref<CodegenColumnVO[]>([])
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
const getList = async () => {
const id = query.id as unknown as number
if (id) {
// 获取表详细信息
const res = await getCodegenTableApi(id)
title.value = '修改[ ' + res.table.tableName + ' ]生成配置'
tableCurrentRow.value = res.table
cloumCurrentRow.value = res.columns
}
}
const submitForm = async () => {
const basicInfo = unref(basicInfoRef)
const basicForm = await basicInfo?.elFormRef?.validate()?.catch(() => {})
if (basicForm) {
const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO
const genTable: CodegenUpdateReqVO = {
table: basicInfoData,
columns: cloumCurrentRow.value
}
await updateCodegenTableApi(genTable)
message.success(t('common.updateSuccess'))
push('/infra/codegen')
}
}
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,53 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
title: [required],
type: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
action: true,
actionWidth: '400px',
columns: [
{
title: '表名称',
field: 'tableName',
isSearch: true
},
{
title: '表描述',
field: 'tableComment',
isSearch: true
},
{
title: '实体',
field: 'className',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: t('common.updateTime'),
field: 'updateTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,183 @@
<template>
<Form :rules="rules" @register="register" />
</template>
<script setup lang="ts">
import { useForm } from '@/hooks/web/useForm'
import { FormSchema } from '@/types/form'
import { CodegenTableVO } from '@/api/infra/codegen/types'
import { getIntDictOptions } from '@/utils/dict'
import { listSimpleMenusApi } from '@/api/system/menu'
import { handleTree, defaultProps } from '@/utils/tree'
import { PropType } from 'vue'
const props = defineProps({
basicInfo: {
type: Object as PropType<Nullable<CodegenTableVO>>,
default: () => null
}
})
const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
const menuOptions = ref<any>([]) // 树形结构
const getTree = async () => {
const res = await listSimpleMenusApi()
menuOptions.value = handleTree(res)
}
const rules = reactive({
tableName: [required],
tableComment: [required],
className: [required],
author: [required],
templateType: [required],
scene: [required],
moduleName: [required],
businessName: [required],
businessPackage: [required],
classComment: [required]
})
const schema = reactive<FormSchema[]>([
{
label: '上级菜单',
field: 'parentMenuId',
component: 'TreeSelect',
componentProps: {
data: menuOptions,
props: defaultProps,
checkStrictly: true,
nodeKey: 'id'
},
labelMessage: '分配到指定菜单下,例如 系统管理',
colProps: {
span: 24
}
},
{
label: '表名称',
field: 'tableName',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '表描述',
field: 'tableComment',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '实体类名称',
field: 'className',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '类名称',
field: 'className',
component: 'Input',
labelMessage: '类名称首字母大写例如SysUser、SysMenu、SysDictData 等等',
colProps: {
span: 12
}
},
{
label: '生成模板',
field: 'templateType',
component: 'Select',
componentProps: {
options: templateTypeOptions
},
colProps: {
span: 12
}
},
{
label: '生成场景',
field: 'scene',
component: 'Select',
componentProps: {
options: sceneOptions
},
colProps: {
span: 12
}
},
{
label: '模块名',
field: 'moduleName',
component: 'Input',
labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
colProps: {
span: 12
}
},
{
label: '业务名',
field: 'businessName',
component: 'Input',
labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
colProps: {
span: 12
}
},
{
label: '类描述',
field: 'classComment',
component: 'Input',
labelMessage: '用作类描述,例如 用户',
colProps: {
span: 12
}
},
{
label: '作者',
field: 'author',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '备注',
field: 'remark',
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
])
const { register, methods, elFormRef } = useForm({
schema
})
watch(
() => props.basicInfo,
(basicInfo) => {
if (!basicInfo) return
const { setValues } = methods
setValues(basicInfo)
},
{
deep: true,
immediate: true
}
)
// ========== 初始化 ==========
onMounted(async () => {
await getTree()
})
defineExpose({
elFormRef,
getFormData: methods.getFormData
})
</script>

View File

@@ -0,0 +1,137 @@
<template>
<vxe-table
ref="dragTable"
border
:data="info"
max-height="600"
stripe
class="xtable-scrollbar"
:column-config="{ resizable: true }"
>
<vxe-column title="字段列名" field="columnName" fixed="left" width="10%" />
<vxe-colgroup title="基础属性">
<vxe-column title="字段描述" field="columnComment" width="10%">
<template #default="{ row }">
<vxe-input v-model="row.columnComment" placeholder="请输入字段描述" />
</template>
</vxe-column>
<vxe-column title="物理类型" field="dataType" width="10%" />
<vxe-column title="Java类型" width="10%" field="javaType">
<template #default="{ row }">
<vxe-select v-model="row.javaType" placeholder="请选择Java类型">
<vxe-option label="Long" value="Long" />
<vxe-option label="String" value="String" />
<vxe-option label="Integer" value="Integer" />
<vxe-option label="Double" value="Double" />
<vxe-option label="BigDecimal" value="BigDecimal" />
<vxe-option label="LocalDateTime" value="LocalDateTime" />
<vxe-option label="Boolean" value="Boolean" />
</vxe-select>
</template>
</vxe-column>
<vxe-column title="java属性" width="8%" field="javaField">
<template #default="{ row }">
<vxe-input v-model="row.javaField" placeholder="请输入java属性" />
</template>
</vxe-column>
</vxe-colgroup>
<vxe-colgroup title="增删改查">
<vxe-column title="插入" width="40px" field="createOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
</template>
</vxe-column>
<vxe-column title="编辑" width="40px" field="updateOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
</template>
</vxe-column>
<vxe-column title="列表" width="40px" field="listOperationResult">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
</template>
</vxe-column>
<vxe-column title="查询" width="40px" field="listOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
</template>
</vxe-column>
<vxe-column title="允许空" width="40px" field="nullable">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
</template>
</vxe-column>
<vxe-column title="查询方式" width="60px" field="listOperationCondition">
<template #default="{ row }">
<vxe-select v-model="row.listOperationCondition" placeholder="请选择查询方式">
<vxe-option label="=" value="=" />
<vxe-option label="!=" value="!=" />
<vxe-option label=">" value=">" />
<vxe-option label=">=" value=">=" />
<vxe-option label="<" value="<>" />
<vxe-option label="<=" value="<=" />
<vxe-option label="LIKE" value="LIKE" />
<vxe-option label="BETWEEN" value="BETWEEN" />
</vxe-select>
</template>
</vxe-column>
</vxe-colgroup>
<vxe-column title="显示类型" width="10%" field="htmlType">
<template #default="{ row }">
<vxe-select v-model="row.htmlType" placeholder="请选择显示类型">
<vxe-option label="文本框" value="input" />
<vxe-option label="文本域" value="textarea" />
<vxe-option label="下拉框" value="select" />
<vxe-option label="单选框" value="radio" />
<vxe-option label="复选框" value="checkbox" />
<vxe-option label="日期控件" value="datetime" />
<vxe-option label="图片上传" value="imageUpload" />
<vxe-option label="文件上传" value="fileUpload" />
<vxe-option label="富文本控件" value="editor" />
</vxe-select>
</template>
</vxe-column>
<vxe-column title="字典类型" width="10%" field="dictType">
<template #default="{ row }">
<vxe-select v-model="row.dictType" clearable filterable placeholder="请选择字典类型">
<vxe-option
v-for="dict in dictOptions"
:key="dict.id"
:label="dict.name"
:value="dict.type"
/>
</vxe-select>
</template>
</vxe-column>
<vxe-column title="示例" field="example">
<template #default="{ row }">
<vxe-input v-model="row.example" placeholder="请输入示例" />
</template>
</vxe-column>
</vxe-table>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { DictTypeVO } from '@/api/system/dict/types'
import { CodegenColumnVO } from '@/api/infra/codegen/types'
import { listSimpleDictTypeApi } from '@/api/system/dict/dict.type'
const props = defineProps({
info: {
type: Array as unknown as PropType<CodegenColumnVO[]>,
default: () => null
}
})
/** 查询字典下拉列表 */
const dictOptions = ref<DictTypeVO[]>()
const getDictOptions = async () => {
const res = await listSimpleDictTypeApi()
dictOptions.value = res
}
onMounted(async () => {
await getDictOptions()
})
defineExpose({
info: props.info
})
</script>

View File

@@ -0,0 +1,125 @@
<template>
<!-- 导入表 -->
<XModal title="导入表" v-model="visible">
<el-form :model="queryParams" ref="queryRef" :inline="true">
<el-form-item label="数据源" prop="dataSourceConfigId">
<el-select v-model="queryParams.dataSourceConfigId" placeholder="请选择数据源" clearable>
<el-option
v-for="config in dataSourceConfigs"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</el-select>
</el-form-item>
<el-form-item label="表名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入表名称" clearable />
</el-form-item>
<el-form-item label="表描述" prop="comment">
<el-input v-model="queryParams.comment" placeholder="请输入表描述" clearable />
</el-form-item>
<el-form-item>
<XButton
type="primary"
preIcon="ep:search"
:title="t('common.query')"
@click="handleQuery()"
/>
<XButton preIcon="ep:refresh-right" :title="t('common.reset')" @click="resetQuery()" />
</el-form-item>
</el-form>
<vxe-table
ref="xTable"
:data="dbTableList"
v-loading="dbLoading"
:checkbox-config="{ highlight: true, range: true }"
height="260px"
class="xtable-scrollbar"
>
<vxe-column type="checkbox" width="60" />
<vxe-column field="name" title="表名称" />
<vxe-column field="comment" title="表描述" />
</vxe-table>
<template #footer>
<div class="dialog-footer">
<XButton type="primary" :title="t('action.import')" @click="handleImportTable()" />
<XButton :title="t('dialog.close')" @click="handleClose()" />
</div>
</template>
</XModal>
</template>
<script setup lang="ts">
import { VxeTableInstance } from 'vxe-table'
import type { DatabaseTableVO } from '@/api/infra/codegen/types'
import { getSchemaTableListApi, createCodegenListApi } from '@/api/infra/codegen'
import { getDataSourceConfigListApi, DataSourceConfigVO } from '@/api/infra/dataSourceConfig'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const emit = defineEmits(['ok'])
// ======== 显示页面 ========
const visible = ref(false)
const dbLoading = ref(true)
const queryParams = reactive({
name: undefined,
comment: undefined,
dataSourceConfigId: 0
})
const dataSourceConfigs = ref<DataSourceConfigVO[]>([])
const show = async () => {
const res = await getDataSourceConfigListApi()
dataSourceConfigs.value = res
queryParams.dataSourceConfigId = dataSourceConfigs.value[0].id
visible.value = true
await getList()
}
/** 查询表数据 */
const dbTableList = ref<DatabaseTableVO[]>([])
/** 查询表数据 */
const getList = async () => {
dbLoading.value = true
const res = await getSchemaTableListApi(queryParams)
dbTableList.value = res
dbLoading.value = false
}
// 查询操作
const handleQuery = async () => {
await getList()
}
// 重置操作
const resetQuery = async () => {
queryParams.name = undefined
queryParams.comment = undefined
queryParams.dataSourceConfigId = 0
await getList()
}
const xTable = ref<VxeTableInstance>()
/** 多选框选中数据 */
const tables = ref<string[]>([])
/** 导入按钮操作 */
const handleImportTable = async () => {
if (xTable.value?.getCheckboxRecords().length === 0) {
message.error('请选择要导入的表')
return
}
xTable.value?.getCheckboxRecords().forEach((item) => {
tables.value.push(item.name)
})
await createCodegenListApi({
dataSourceConfigId: queryParams.dataSourceConfigId,
tableNames: tables.value
})
message.success('导入成功')
emit('ok')
handleClose()
}
const handleClose = () => {
visible.value = false
tables.value = []
}
defineExpose({
show
})
</script>

View File

@@ -0,0 +1,145 @@
<template>
<XModal title="预览" v-model="preview.open">
<div class="flex">
<el-card class="w-1/4" :gutter="12" shadow="hover">
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
<el-tree
ref="treeRef"
node-key="id"
:data="preview.fileTree"
:expand-on-click-node="false"
highlight-current
@node-click="handleNodeClick"
/>
</el-scrollbar>
</el-card>
<el-card class="w-3/4 ml-3" :gutter="12" shadow="hover">
<el-tabs v-model="preview.activeName">
<el-tab-pane
v-for="item in previewCodegen"
:label="item.filePath.substring(item.filePath.lastIndexOf('/') + 1)"
:name="item.filePath"
:key="item.filePath"
>
<XTextButton style="float: right" :title="t('common.copy')" @click="copy(item.code)" />
<pre>{{ item.code }}</pre>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</XModal>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import { handleTree2 } from '@/utils/tree'
import { previewCodegenApi } from '@/api/infra/codegen'
import { CodegenTableVO, CodegenPreviewVO } from '@/api/infra/codegen/types'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// ======== 显示页面 ========
const preview = reactive({
open: false,
titel: '代码预览',
fileTree: [],
activeName: ''
})
const previewCodegen = ref<CodegenPreviewVO[]>()
const show = async (row: CodegenTableVO) => {
const res = await previewCodegenApi(row.id)
let file = handleFiles(res)
previewCodegen.value = res
preview.fileTree = handleTree2(file, 'id', 'parentId', 'children', '/')
preview.activeName = res[0].filePath
preview.open = true
}
const handleNodeClick = async (data, node) => {
if (node && !node.isLeaf) {
return false
}
preview.activeName = data.id
}
/** 生成 files 目录 **/
interface filesType {
id: string
label: string
parentId: string
}
const handleFiles = (datas: CodegenPreviewVO[]) => {
let exists = {} // keyfile 的 idvaluetrue
let files: filesType[] = []
// 遍历每个元素
for (const data of datas) {
let paths = data.filePath.split('/')
let fullPath = '' // 从头开始的路径,用于生成 id
// 特殊处理 java 文件
if (paths[paths.length - 1].indexOf('.java') >= 0) {
let newPaths: string[] = []
for (let i = 0; i < paths.length; i++) {
let path = paths[i]
if (path !== 'java') {
newPaths.push(path)
continue
}
newPaths.push(path)
// 特殊处理中间的 package进行合并
let tmp = ''
while (i < paths.length) {
path = paths[i + 1]
if (
path === 'controller' ||
path === 'convert' ||
path === 'dal' ||
path === 'enums' ||
path === 'service' ||
path === 'vo' || // 下面三个,主要是兜底。可能考虑到有人改了包结构
path === 'mysql' ||
path === 'dataobject'
) {
break
}
tmp = tmp ? tmp + '.' + path : path
i++
}
if (tmp) {
newPaths.push(tmp)
}
}
paths = newPaths
}
// 遍历每个 path 拼接成树
for (let i = 0; i < paths.length; i++) {
// 已经添加到 files 中,则跳过
let oldFullPath = fullPath
// 下面的 replaceAll 的原因,是因为上面包处理了,导致和 tabs 不匹配,所以 replaceAll 下
fullPath = fullPath.length === 0 ? paths[i] : fullPath.replaceAll('.', '/') + '/' + paths[i]
if (exists[fullPath]) {
continue
}
// 添加到 files 中
exists[fullPath] = true
files.push({
id: fullPath,
label: paths[i],
parentId: oldFullPath || '/' // "/" 为根节点
})
}
}
return files
}
/** 复制 **/
const copy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) {
message.error(t('common.copyError'))
} else {
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
}
defineExpose({
show
})
</script>

View File

@@ -0,0 +1,5 @@
import BasicInfoForm from './BasicInfoForm.vue'
import CloumInfoForm from './CloumInfoForm.vue'
import ImportTable from './ImportTable.vue'
import Preview from './Preview.vue'
export { BasicInfoForm, CloumInfoForm, ImportTable, Preview }

View File

@@ -0,0 +1,107 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作导入 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.import')"
v-hasPermi="['infra:codegen:create']"
@click="openImportTable()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作预览 -->
<XTextButton
preIcon="ep:view"
:title="t('action.preview')"
v-hasPermi="['infra:codegen:query']"
@click="handlePreview(row)"
/>
<!-- 操作编辑 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['infra:codegen:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:codegen:delete']"
@click="deleteData(row.id)"
/>
<!-- 操作同步 -->
<XTextButton
preIcon="ep:refresh"
:title="t('action.sync')"
v-hasPermi="['infra:codegen:update']"
@click="handleSynchDb(row)"
/>
<!-- 操作生成 -->
<XTextButton
preIcon="ep:download"
:title="t('action.generate')"
v-hasPermi="['infra:codegen:download']"
@click="handleGenTable(row)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 弹窗导入表 -->
<ImportTable ref="importRef" @ok="reload()" />
<!-- 弹窗预览代码 -->
<Preview ref="previewRef" />
</template>
<script setup lang="ts" name="Codegen">
import download from '@/utils/download'
import * as CodegenApi from '@/api/infra/codegen'
import { CodegenTableVO } from '@/api/infra/codegen/types'
import { allSchemas } from './codegen.data'
import { ImportTable, Preview } from './components'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const { push } = useRouter() // 路由跳转
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: CodegenApi.getCodegenTablePageApi,
deleteApi: CodegenApi.deleteCodegenTableApi
})
// 导入操作
const importRef = ref()
const openImportTable = () => {
importRef.value.show()
}
// 预览操作
const previewRef = ref()
const handlePreview = (row: CodegenTableVO) => {
previewRef.value.show(row)
}
// 编辑操作
const handleUpdate = (rowId: number) => {
push('/codegen/edit?id=' + rowId)
}
// 同步操作
const handleSynchDb = (row: CodegenTableVO) => {
// 基于 DB 同步
const tableName = row.tableName
message
.confirm('确认要强制同步' + tableName + '表结构吗?', t('common.reminder'))
.then(async () => {
await CodegenApi.syncCodegenFromDBApi(row.id)
message.success('同步成功')
})
}
// 生成代码操作
const handleGenTable = async (row: CodegenTableVO) => {
const res = await CodegenApi.downloadCodegenApi(row.id)
download.zip(res, 'codegen-' + row.className + '.zip')
}
</script>

View File

@@ -0,0 +1,90 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
category: [required],
name: [required],
key: [required],
value: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
columns: [
{
title: '参数分类',
field: 'category'
},
{
title: '参数名称',
field: 'name',
isSearch: true
},
{
title: '参数键名',
field: 'key',
isSearch: true
},
{
title: '参数键值',
field: 'value'
},
{
title: '系统内置',
field: 'type',
dictType: DICT_TYPE.INFRA_CONFIG_TYPE,
dictClass: 'number',
isSearch: true
},
{
title: '是否可见',
field: 'visible',
table: {
slots: {
default: 'visible_default'
}
},
form: {
component: 'RadioButton',
componentProps: {
options: [
{ label: '是', value: true },
{ label: '否', value: false }
]
}
}
},
{
title: t('form.remark'),
field: 'remark',
isTable: false,
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,162 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['infra:config:create']"
@click="handleCreate()"
/>
<!-- 操作导出 -->
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['infra:config:export']"
@click="exportList('配置.xls')"
/>
</template>
<template #visible_default="{ row }">
<span>{{ row.visible ? '是' : '否' }} </span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['infra:config:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:config:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:config:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #visible="{ row }">
<span>{{ row.visible ? '是' : '否' }} </span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Config">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import * as ConfigApi from '@/api/infra/config'
import { rules, allSchemas } from './config.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: ConfigApi.getConfigPageApi,
deleteApi: ConfigApi.deleteConfigApi,
exportListApi: ConfigApi.exportConfigApi
})
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await ConfigApi.getConfigApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await ConfigApi.getConfigApi(rowId)
detailData.value = res
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as ConfigApi.ConfigVO
if (actionType.value === 'create') {
await ConfigApi.createConfigApi(data)
message.success(t('common.createSuccess'))
} else {
await ConfigApi.updateConfigApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,52 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
name: [required],
url: [required],
username: [required],
password: [required]
})
// 新增 + 修改
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
action: true,
columns: [
{
title: '数据源名称',
field: 'name'
},
{
title: '数据源连接',
field: 'url',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: '用户名',
field: 'username'
},
{
title: '密码',
field: 'password',
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,145 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['infra:data-source-config:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['infra:data-source-config:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:data-source-config:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:data-source-config:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="loading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="loading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="DataSourceConfig">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import * as DataSourceConfiggApi from '@/api/infra/dataSourceConfig'
import { rules, allSchemas } from './dataSourceConfig.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
isList: true,
getListApi: DataSourceConfiggApi.getDataSourceConfigListApi,
deleteApi: DataSourceConfiggApi.deleteDataSourceConfigApi
})
// ========== CRUD 相关 ==========
const loading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await DataSourceConfiggApi.getDataSourceConfigApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
// 设置数据
const res = await DataSourceConfiggApi.getDataSourceConfigApi(rowId)
detailData.value = res
setDialogTile('detail')
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as DataSourceConfiggApi.DataSourceConfigVO
if (actionType.value === 'create') {
await DataSourceConfiggApi.createDataSourceConfigApi(data)
message.success(t('common.createSuccess'))
} else {
await DataSourceConfiggApi.updateDataSourceConfigApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
loading.value = false
// 刷新列表
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,61 @@
<template>
<ContentWrap title="数据库文档">
<!-- 操作工具栏 -->
<div class="mb-10px">
<XButton
type="primary"
preIcon="ep:download"
:title="t('action.export') + ' HTML'"
@click="handleExport('HTML')"
/>
<XButton
type="primary"
preIcon="ep:download"
:title="t('action.export') + ' Word'"
@click="handleExport('Word')"
/>
<XButton
type="primary"
preIcon="ep:download"
:title="t('action.export') + ' Markdown'"
@click="handleExport('Markdown')"
/>
</div>
<IFrame v-if="!loding" v-loading="loding" :src="src" />
</ContentWrap>
</template>
<script setup lang="ts" name="DbDoc">
import download from '@/utils/download'
import * as DbDocApi from '@/api/infra/dbDoc'
const { t } = useI18n() // 国际化
const src = ref('')
const loding = ref(true)
/** 页面加载 */
const init = async () => {
const res = await DbDocApi.exportHtmlApi()
let blob = new Blob([res], { type: 'text/html' })
let blobUrl = window.URL.createObjectURL(blob)
src.value = blobUrl
loding.value = false
}
/** 处理导出 */
const handleExport = async (type: string) => {
if (type === 'HTML') {
const res = await DbDocApi.exportHtmlApi()
download.html(res, '数据库文档.html')
}
if (type === 'Word') {
const res = await DbDocApi.exportWordApi()
download.word(res, '数据库文档.doc')
}
if (type === 'Markdown') {
const res = await DbDocApi.exportMarkdownApi()
download.markdown(res, '数据库文档.md')
}
}
onMounted(async () => {
await init()
})
</script>

View File

@@ -0,0 +1,9 @@
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>
<script setup lang="ts" name="Druid">
const BASE_URL = import.meta.env.VITE_BASE_URL
const src = ref(BASE_URL + '/druid/index.html')
</script>

View File

@@ -0,0 +1,77 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
storage: [required],
config: {
basePath: [required],
host: [required],
port: [required],
username: [required],
password: [required],
mode: [required],
endpoint: [required],
bucket: [required],
accessKey: [required],
accessSecret: [required],
domain: [required]
}
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '配置编号',
action: true,
actionWidth: '400px',
columns: [
{
title: '配置名',
field: 'name',
isSearch: true
},
{
title: '存储器',
field: 'storage',
dictType: DICT_TYPE.INFRA_FILE_STORAGE,
dictClass: 'number',
isSearch: true
},
{
title: '主配置',
field: 'master',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean'
},
{
title: t('form.remark'),
field: 'remark',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,294 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['infra:file-config:create']"
@click="handleCreate(formRef)"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作编辑 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['infra:file-config:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:file-config:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作主配置 -->
<XTextButton
preIcon="ep:flag"
title="主配置"
v-hasPermi="['infra:file-config:update']"
@click="handleMaster(row)"
/>
<!-- 操作测试 -->
<XTextButton preIcon="ep:share" :title="t('action.test')" @click="handleTest(row.id)" />
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:file-config:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<el-form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:model="form"
:rules="rules"
label-width="120px"
>
<el-form-item label="配置名" prop="name">
<el-input v-model="form.name" placeholder="请输入配置名" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="存储器" prop="storage">
<el-select v-model="form.storage" placeholder="请选择存储器" :disabled="form.id !== 0">
<el-option
v-for="(dict, index) in getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- DB -->
<!-- Local / FTP / SFTP -->
<el-form-item
v-if="form.storage >= 10 && form.storage <= 12"
label="基础路径"
prop="config.basePath"
>
<el-input v-model="form.config.basePath" placeholder="请输入基础路径" />
</el-form-item>
<el-form-item
v-if="form.storage >= 11 && form.storage <= 12"
label="主机地址"
prop="config.host"
>
<el-input v-model="form.config.host" placeholder="请输入主机地址" />
</el-form-item>
<el-form-item
v-if="form.storage >= 11 && form.storage <= 12"
label="主机端口"
prop="config.port"
>
<el-input-number :min="0" v-model="form.config.port" placeholder="请输入主机端口" />
</el-form-item>
<el-form-item
v-if="form.storage >= 11 && form.storage <= 12"
label="用户名"
prop="config.username"
>
<el-input v-model="form.config.username" placeholder="请输入密码" />
</el-form-item>
<el-form-item
v-if="form.storage >= 11 && form.storage <= 12"
label="密码"
prop="config.password"
>
<el-input v-model="form.config.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item v-if="form.storage === 11" label="连接模式" prop="config.mode">
<el-radio-group v-model="form.config.mode">
<el-radio key="Active" label="Active">主动模式</el-radio>
<el-radio key="Passive" label="Passive">主动模式</el-radio>
</el-radio-group>
</el-form-item>
<!-- S3 -->
<el-form-item v-if="form.storage === 20" label="节点地址" prop="config.endpoint">
<el-input v-model="form.config.endpoint" placeholder="请输入节点地址" />
</el-form-item>
<el-form-item v-if="form.storage === 20" label="存储 bucket" prop="config.bucket">
<el-input v-model="form.config.bucket" placeholder="请输入 bucket" />
</el-form-item>
<el-form-item v-if="form.storage === 20" label="accessKey" prop="config.accessKey">
<el-input v-model="form.config.accessKey" placeholder="请输入 accessKey" />
</el-form-item>
<el-form-item v-if="form.storage === 20" label="accessSecret" prop="config.accessSecret">
<el-input v-model="form.config.accessSecret" placeholder="请输入 accessSecret" />
</el-form-item>
<!-- 通用 -->
<el-form-item v-if="form.storage === 20" label="自定义域名">
<!-- 无需参数校验所以去掉 prop -->
<el-input v-model="form.config.domain" placeholder="请输入自定义域名" />
</el-form-item>
<el-form-item v-else-if="form.storage" label="自定义域名" prop="config.domain">
<el-input v-model="form.config.domain" placeholder="请输入自定义域名" />
</el-form-item>
</el-form>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm(formRef)"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="FileConfig">
import type { FormInstance } from 'element-plus'
// 业务相关的 import
import * as FileConfigApi from '@/api/infra/fileConfig'
import { rules, allSchemas } from './fileConfig.data'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: FileConfigApi.getFileConfigPageApi,
deleteApi: FileConfigApi.deleteFileConfigApi
})
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormInstance>() // 表单 Ref
const detailData = ref() // 详情 Ref
const form = ref<FileConfigApi.FileConfigVO>({
id: 0,
name: '',
storage: 0,
master: false,
visible: false,
config: {
basePath: '',
host: '',
port: 0,
username: '',
password: '',
mode: '',
endpoint: '',
bucket: '',
accessKey: '',
accessSecret: '',
domain: ''
},
remark: '',
createTime: new Date()
})
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = (formEl: FormInstance | undefined) => {
setDialogTile('create')
formEl?.resetFields()
form.value = {
id: 0,
name: '',
storage: 0,
master: false,
visible: false,
config: {
basePath: '',
host: '',
port: 0,
username: '',
password: '',
mode: '',
endpoint: '',
bucket: '',
accessKey: '',
accessSecret: '',
domain: ''
},
remark: '',
createTime: new Date()
}
}
// 修改操作
const handleUpdate = async (rowId: number) => {
// 设置数据
const res = await FileConfigApi.getFileConfigApi(rowId)
form.value = res
setDialogTile('update')
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
// 设置数据
const res = await FileConfigApi.getFileConfigApi(rowId)
detailData.value = res
}
// 主配置操作
const handleMaster = (row: FileConfigApi.FileConfigVO) => {
message
.confirm('是否确认修改配置【 ' + row.name + ' 】为主配置?', t('common.reminder'))
.then(async () => {
await FileConfigApi.updateFileConfigMasterApi(row.id)
await reload()
})
}
const handleTest = async (rowId: number) => {
const res = await FileConfigApi.testFileConfigApi(rowId)
message.alert('测试通过,上传文件成功!访问地址:' + res)
}
// 提交按钮
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
if (actionType.value === 'create') {
await FileConfigApi.createFileConfigApi(form.value)
message.success(t('common.createSuccess'))
} else {
await FileConfigApi.updateFileConfigApi(form.value)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,52 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
action: true,
columns: [
{
title: '文件名',
field: 'name'
},
{
title: '文件路径',
field: 'path',
isSearch: true
},
{
title: 'URL',
field: 'url',
table: {
cellRender: {
name: 'XPreview'
}
}
},
{
title: '文件大小',
field: 'size',
formatter: 'formatSize'
},
{
title: '文件类型',
field: 'type',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,173 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:upload"
title="上传文件"
@click="uploadDialogVisible = true"
/>
</template>
<template #actionbtns_default="{ row }">
<XTextButton
preIcon="ep:copy-document"
:title="t('common.copy')"
@click="handleCopy(row.url)"
/>
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:file:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData">
<template #url="{ row }">
<el-image
v-if="row.type === 'jpg' || 'png' || 'gif'"
style="width: 100px; height: 100px"
:src="row.url"
:key="row.url"
lazy
/>
<span>{{ row.url }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
<XModal v-model="uploadDialogVisible" :title="uploadDialogTitle">
<el-upload
ref="uploadRef"
:action="updateUrl + '?updateSupport=' + updateSupport"
:headers="uploadHeaders"
:drag="true"
:limit="1"
:multiple="true"
:show-file-list="true"
:disabled="uploadDisabled"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:on-success="handleFileSuccess"
:on-error="excelUploadError"
:auto-upload="false"
accept=".jpg, .png, .gif"
>
<Icon icon="ep:upload-filled" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">请上传 .jpg, .png, .gif 标准格式文件</div>
</template>
</el-upload>
<template #footer>
<!-- 按钮保存 -->
<XButton
type="primary"
preIcon="ep:upload-filled"
:title="t('action.save')"
@click="submitFileForm()"
/>
<!-- 按钮关闭 -->
<XButton :title="t('dialog.close')" @click="uploadDialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="FileList">
import type { UploadInstance, UploadRawFile } from 'element-plus'
// 业务相关的 import
import { allSchemas } from './fileList.data'
import * as FileApi from '@/api/infra/fileList'
import { getAccessToken, getTenantId } from '@/utils/auth'
import { useClipboard } from '@vueuse/core'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: FileApi.getFilePageApi,
deleteApi: FileApi.deleteFileApi
})
const detailData = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
const uploadDialogVisible = ref(false)
const uploadDialogTitle = ref('上传')
const updateSupport = ref(0)
const uploadDisabled = ref(false)
const uploadRef = ref<UploadInstance>()
let updateUrl = import.meta.env.VITE_UPLOAD_URL
const uploadHeaders = ref()
// 文件上传之前判断
const beforeUpload = (file: UploadRawFile) => {
const isImg = file.type === 'image/jpeg' || 'image/gif' || 'image/png'
const isLt5M = file.size / 1024 / 1024 < 5
if (!isImg) message.error('上传文件只能是 jpeg / gif / png 格式!')
if (!isLt5M) message.error('上传文件大小不能超过 5MB!')
return isImg && isLt5M
}
// 处理上传的文件发生变化
// const handleFileChange = (uploadFile: UploadFile): void => {
// uploadRef.value.data.path = uploadFile.name
// }
// 文件上传
const submitFileForm = () => {
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
uploadDisabled.value = true
uploadRef.value!.submit()
}
// 文件上传成功
const handleFileSuccess = async (response: any): Promise<void> => {
if (response.code !== 0) {
message.error(response.msg)
return
}
message.success('上传成功')
uploadDialogVisible.value = false
uploadDisabled.value = false
await reload()
}
// 文件数超出提示
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
// 上传错误提示
const excelUploadError = (): void => {
message.error('导入数据失败,请您重新上传!')
}
// 详情操作
const handleDetail = (row: FileApi.FileVO) => {
// 设置数据
detailData.value = row
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
// ========== 复制相关 ==========
const handleCopy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) {
message.error(t('common.copyError'))
} else {
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
}
</script>

View File

@@ -0,0 +1,78 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['infra:job:export']"
@click="exportList('定时任务详情.xls')"
/>
</template>
<template #beginTime_default="{ row }">
<span>{{
dayjs(row.beginTime).format('YYYY-MM-DD HH:mm:ss') +
' ~ ' +
dayjs(row.endTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #duration_default="{ row }">
<span>{{ row.duration + ' 毫秒' }}</span>
</template>
<template #actionbtns_default="{ row }">
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:job:query']"
@click="handleDetail(row)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData">
<template #retryInterval="{ row }">
<span>{{ row.retryInterval + '毫秒' }} </span>
</template>
<template #monitorTimeout="{ row }">
<span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="JobLog">
import dayjs from 'dayjs'
import * as JobLogApi from '@/api/infra/jobLog'
import { allSchemas } from './jobLog.data'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: JobLogApi.getJobLogPageApi,
exportListApi: JobLogApi.exportJobLogApi
})
// ========== CRUD 相关 ==========
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
// ========== 详情相关 ==========
const detailData = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: JobLogApi.JobLogVO) => {
// 设置数据
const res = JobLogApi.getJobLogApi(row.id)
detailData.value = res
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
</script>

View File

@@ -0,0 +1,302 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['infra:job:create']"
@click="handleCreate()"
/>
<!-- 操作导出 -->
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['infra:job:export']"
@click="exportList('定时任务.xls')"
/>
<XButton
type="info"
preIcon="ep:zoom-in"
title="执行日志"
v-hasPermi="['infra:job:query']"
@click="handleJobLog()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['infra:job:update']"
@click="handleUpdate(row.id)"
/>
<XTextButton
preIcon="ep:edit"
:title="row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停'"
v-hasPermi="['infra:job:update']"
@click="handleChangeStatus(row)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:job:delete']"
@click="deleteData(row.id)"
/>
<el-dropdown class="p-0.5" v-hasPermi="['infra:job:trigger', 'infra:job:query']">
<XTextButton :title="t('action.more')" postIcon="ep:arrow-down" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<!-- 操作执行 -->
<XTextButton
preIcon="ep:view"
title="执行一次"
v-hasPermi="['infra:job:trigger']"
@click="handleRun(row)"
/>
</el-dropdown-item>
<el-dropdown-item>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:job:query']"
@click="handleDetail(row.id)"
/>
</el-dropdown-item>
<el-dropdown-item>
<!-- 操作日志 -->
<XTextButton
preIcon="ep:view"
title="调度日志"
v-hasPermi="['infra:job:query']"
@click="handleJobLog(row.id)"
/>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
>
<template #cronExpression="form">
<Crontab v-model="form['cronExpression']" :shortcuts="shortcuts" />
</template>
</Form>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #retryInterval="{ row }">
<span>{{ row.retryInterval + '毫秒' }} </span>
</template>
<template #monitorTimeout="{ row }">
<span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span>
</template>
<template #nextTimes>
<span>{{ Array.from(nextTimes, (x) => parseTime(x)).join('; ') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Job">
import type { FormExpose } from '@/components/Form'
import * as JobApi from '@/api/infra/job'
import { rules, allSchemas } from './job.data'
import { InfraJobStatusEnum } from '@/utils/constants'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const { push } = useRouter()
// 列表相关的变量
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: JobApi.getJobPageApi,
deleteApi: JobApi.deleteJobApi,
exportListApi: JobApi.exportJobApi
})
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
const nextTimes = ref([])
const shortcuts = ref([
{
text: '每天8点和12点 (自定义追加)',
value: '0 0 8,12 * * ?'
}
])
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await JobApi.getJobApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
// 设置数据
const res = await JobApi.getJobApi(rowId)
detailData.value = res
// 后续执行时长
const jobNextTime = await JobApi.getJobNextTimesApi(rowId)
nextTimes.value = jobNextTime
setDialogTile('detail')
}
const parseTime = (time) => {
if (!time) {
return null
}
const format = '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time
.replace(new RegExp(/-/gm), '/')
.replace('T', ' ')
.replace(new RegExp(/\.[\d]{3}/gm), '')
}
if (typeof time === 'number' && time.toString().length === 10) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value]
}
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
const handleChangeStatus = async (row: JobApi.JobVO) => {
const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
const status =
row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP
message
.confirm('确认要' + text + '定时任务编号为"' + row.id + '"的数据项?', t('common.reminder'))
.then(async () => {
row.status =
row.status === InfraJobStatusEnum.NORMAL
? InfraJobStatusEnum.NORMAL
: InfraJobStatusEnum.STOP
await JobApi.updateJobStatusApi(row.id, status)
message.success(text + '成功')
await reload()
})
.catch(() => {
row.status =
row.status === InfraJobStatusEnum.NORMAL
? InfraJobStatusEnum.STOP
: InfraJobStatusEnum.NORMAL
})
}
// 执行日志
const handleJobLog = (rowId?: number) => {
if (rowId) {
push('/job/job-log?id=' + rowId)
} else {
push('/job/job-log')
}
}
// 执行一次
const handleRun = (row: JobApi.JobVO) => {
message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => {
await JobApi.runJobApi(row.id)
message.success('执行成功')
await reload()
})
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as JobApi.JobVO
if (actionType.value === 'create') {
await JobApi.createJobApi(data)
message.success(t('common.createSuccess'))
} else {
await JobApi.updateJobApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,69 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
handlerName: [required],
cronExpression: [required],
retryCount: [required],
retryInterval: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '任务编号',
action: true,
actionWidth: '280px',
columns: [
{
title: '任务名称',
field: 'name',
isSearch: true
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.INFRA_JOB_STATUS,
dictClass: 'number',
isForm: false,
isSearch: true
},
{
title: '处理器的名字',
field: 'handlerName',
isSearch: true
},
{
title: '处理器的参数',
field: 'handlerParam',
isTable: false
},
{
title: 'CRON 表达式',
field: 'cronExpression'
},
{
title: '后续执行时间',
field: 'nextTimes',
isTable: false,
isForm: false
},
{
title: '重试次数',
field: 'retryCount',
isTable: false
},
{
title: '重试间隔',
field: 'retryInterval',
isTable: false
},
{
title: '监控超时时间',
field: 'monitorTimeout',
isTable: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,75 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '日志编号',
action: true,
columns: [
{
title: '任务编号',
field: 'jobId',
isSearch: true
},
{
title: '处理器的名字',
field: 'handlerName',
isSearch: true
},
{
title: '处理器的参数',
field: 'handlerParam'
},
{
title: '第几次执行',
field: 'executeIndex'
},
{
title: '开始执行时间',
field: 'beginTime',
formatter: 'formatDate',
table: {
slots: {
default: 'beginTime_default'
}
},
search: {
show: true,
itemRender: {
name: 'XDataPicker'
}
}
},
{
title: '结束执行时间',
field: 'endTime',
formatter: 'formatDate',
isTable: false,
search: {
show: true,
itemRender: {
name: 'XDataPicker'
}
}
},
{
title: '执行时长',
field: 'duration',
table: {
slots: {
default: 'duration_default'
}
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.INFRA_JOB_LOG_STATUS,
dictClass: 'number',
isSearch: true
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,277 @@
<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">
<div ref="commandStatsRef" class="h-88"></div>
</el-card>
</el-col>
<el-col :span="12" class="mt-3">
<el-card class="ml-3" :gutter="12" shadow="hover">
<div ref="usedmemory" class="h-88"></div>
</el-card>
</el-col>
</el-row>
<el-row class="mt-3">
<el-col :span="24" class="card-box" shadow="hover">
<el-card>
<el-table
v-loading="keyListLoad"
:data="keyList"
row-key="id"
@row-click="openKeyTemplate"
>
<el-table-column prop="keyTemplate" label="Key 模板" width="200" />
<el-table-column prop="keyType" label="Key 类型" width="100" />
<el-table-column prop="valueType" label="Value 类型" />
<el-table-column prop="timeoutType" label="超时时间" width="200">
<template #default="{ row }">
<DictTag :type="DICT_TYPE.INFRA_REDIS_TIMEOUT_TYPE" :value="row?.timeoutType" />
<span v-if="row?.timeout > 0">({{ row?.timeout / 1000 }} )</span>
</template>
</el-table-column>
<el-table-column prop="memo" label="备注" />
</el-table>
</el-card>
</el-col>
</el-row>
</el-scrollbar>
<XModal v-model="dialogVisible" :title="keyTemplate + ' 模板'">
<el-row>
<el-col :span="14" class="mt-3">
<el-card shadow="always">
<template #header>
<div class="card-header">
<span>键名列表</span>
</div>
</template>
<el-table :data="cacheKeys" style="width: 100%" @row-click="handleKeyValue">
<el-table-column label="缓存键名" align="center" :show-overflow-tooltip="true">
<template #default="{ row }">
{{ row }}
</template>
</el-table-column>
<el-table-column label="操作" align="right" width="60">
<template #default="{ row }">
<XTextButton preIcon="ep:delete" @click="handleDeleteKey(row)" />
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="10" class="mt-3">
<el-card shadow="always">
<template #header>
<div class="card-header">
<span>缓存内容</span>
<XTextButton
preIcon="ep:refresh"
title="清理全部"
@click="handleDeleteKeys(keyTemplate)"
class="float-right p-1"
/>
</div>
</template>
<el-descriptions :column="1">
<el-descriptions-item label="缓存键名:">{{ cacheForm.key }}</el-descriptions-item>
<el-descriptions-item label="缓存内容:">{{ cacheForm.value }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
</XModal>
</template>
<script setup lang="ts" name="Redis">
import * as echarts from 'echarts'
import { DICT_TYPE } from '@/utils/dict'
import * as RedisApi from '@/api/infra/redis'
import { RedisKeyInfo, RedisMonitorInfoVO } from '@/api/infra/redis/types'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const cache = ref<RedisMonitorInfoVO>()
const keyListLoad = ref(true)
const keyList = ref<RedisKeyInfo[]>([])
// 基本信息
const readRedisInfo = async () => {
const data = await RedisApi.getCacheApi()
cache.value = data
loadEchartOptions(data.commandStats)
const redisKeysInfo = await RedisApi.getKeyDefineListApi()
keyList.value = redisKeysInfo
keyListLoad.value = false //加载完成
}
// 图表
const commandStatsRef = ref<HTMLElement>()
const usedmemory = ref<HTMLDivElement>()
const loadEchartOptions = (stats) => {
const commandStats = [] as any[]
const nameList = [] as string[]
stats.forEach((row) => {
commandStats.push({
name: row.command,
value: row.calls
})
nameList.push(row.command)
})
const commandStatsInstance = echarts.init(commandStatsRef.value!, 'macarons')
commandStatsInstance.setOption({
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: nameList,
textStyle: {
color: '#a1a1a1'
}
},
series: [
{
name: '命令',
type: 'pie',
radius: [20, 120],
center: ['40%', '60%'],
data: commandStats,
roseType: 'radius',
label: {
show: true
},
emphasis: {
label: {
show: true
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
const usedMemoryInstance = echarts.init(usedmemory.value!, 'macarons')
usedMemoryInstance.setOption({
title: {
text: '内存使用情况',
left: 'center'
},
tooltip: {
formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
},
series: [
{
name: '峰值',
type: 'gauge',
min: 0,
max: 100,
progress: {
show: true
},
detail: {
formatter: cache.value!.info.used_memory_human
},
data: [
{
value: parseFloat(cache.value!.info.used_memory_human),
name: '内存消耗'
}
]
}
]
})
}
const dialogVisible = ref(false)
const keyTemplate = ref('')
const cacheKeys = ref()
const cacheForm = ref<{
key: string
value: string
}>({
key: '',
value: ''
})
const openKeyTemplate = async (row: RedisKeyInfo) => {
keyTemplate.value = row.keyTemplate
cacheKeys.value = await RedisApi.getKeyListApi(row.keyTemplate)
dialogVisible.value = true
}
const handleDeleteKey = async (row) => {
RedisApi.deleteKeyApi(row)
message.success(t('common.delSuccess'))
}
const handleDeleteKeys = async (row) => {
RedisApi.deleteKeysApi(row)
message.success(t('common.delSuccess'))
}
const handleKeyValue = async (row) => {
const res = await RedisApi.getKeyValueApi(row)
cacheForm.value = res
}
onBeforeMount(() => {
readRedisInfo()
})
</script>
<style scoped>
.redis {
height: 600px;
max-height: 860px;
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>
<script setup lang="ts" name="Server">
const BASE_URL = import.meta.env.VITE_BASE_URL
const src = ref(BASE_URL + '/admin/applications')
</script>

View File

@@ -0,0 +1,8 @@
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>
<script setup lang="ts" name="Skywalking">
const src = ref('http://skywalking.shop.iocoder.cn')
</script>

View File

@@ -0,0 +1,10 @@
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>
<script setup lang="ts" name="Swagger">
const BASE_URL = import.meta.env.VITE_BASE_URL
// const src = ref(BASE_URL + '/doc.html')
const src = ref(BASE_URL + '/swagger-ui')
</script>

View File

@@ -0,0 +1,4 @@
<template>
<div>index</div>
</template>
<script setup lang="ts" name="TestDome"></script>

View File

@@ -0,0 +1,116 @@
<template>
<div class="flex">
<el-card class="w-1/2" :gutter="12" shadow="always">
<template #header>
<div class="card-header">
<span>连接</span>
</div>
</template>
<div class="flex items-center">
<span class="text-lg font-medium mr-4"> 连接状态: </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="toggle">
{{ getIsOpen ? '关闭连接' : '开启连接' }}
</el-button>
</div>
<p class="text-lg font-medium mt-4">设置</p>
<hr class="my-4" />
<el-input
v-model="sendValue"
:autosize="{ minRows: 2, maxRows: 4 }"
type="textarea"
:disabled="!getIsOpen"
clearable
/>
<el-button type="primary" block class="mt-4" :disabled="!getIsOpen" @click="handlerSend">
发送
</el-button>
</el-card>
<el-card class="w-1/2" :gutter="12" shadow="always">
<template #header>
<div class="card-header">
<span>消息记录</span>
</div>
</template>
<div class="max-h-80 overflow-auto">
<ul>
<li v-for="item in getList" class="mt-2" :key="item.time">
<div class="flex items-center">
<span class="mr-2 text-primary font-medium">收到消息:</span>
<span>{{ dayjs(item.time).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
<div>
{{ item.res }}
</div>
</li>
</ul>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { useUserStore } from '@/store/modules/user'
import { useWebSocket } from '@vueuse/core'
const userStore = useUserStore()
const sendValue = ref('')
const server = ref(
(import.meta.env.VITE_BASE_URL + '/websocket/message').replace('http', 'ws') +
'?userId=' +
userStore.getUser.id
)
const state = reactive({
recordList: [] as { id: number; time: number; res: string }[]
})
const { status, data, send, close, open } = useWebSocket(server.value, {
autoReconnect: false,
heartbeat: true
})
watchEffect(() => {
if (data.value) {
try {
const res = JSON.parse(data.value)
state.recordList.push(res)
} catch (error) {
state.recordList.push({
res: data.value,
id: Math.ceil(Math.random() * 1000),
time: new Date().getTime()
})
}
}
})
const getIsOpen = computed(() => status.value === 'OPEN')
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red'))
const getList = computed(() => {
return [...state.recordList].reverse()
})
function handlerSend() {
send(sendValue.value)
sendValue.value = ''
}
function toggle() {
if (getIsOpen.value) {
close()
} else {
open()
}
}
</script>