first commit

This commit is contained in:
zc
2026-02-26 17:31:18 +08:00
commit f1b87df6ca
803 changed files with 297148 additions and 0 deletions

View File

@@ -0,0 +1,297 @@
<template>
<a-drawer
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 1350 ? 1350 : '100%'"
@before-ok="save"
@close="reset"
>
<a-tabs v-model:active-key="activeKey">
<a-tab-pane key="1" title="生成配置">
<GiForm ref="formRef" v-model="form" :columns="formColumns" />
</a-tab-pane>
<a-tab-pane key="2" title="字段配置">
<GiTable
row-key="tableName"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: 800, minWidth: 900 }"
:pagination="false"
:draggable="{ type: 'handle', width: 40 }"
:disabled-tools="['setting', 'refresh']"
:disabled-column-keys="['tableName']"
@change="handleChangeSort"
>
<template #toolbar-left>
<a-popconfirm
content="是否确定同步最新数据表结构?同步后只要不点击确定保存,则不影响原有配置数据。"
type="warning"
@ok="handleRefresh(form.tableName)"
>
<a-tooltip content="同步最新数据表结构">
<a-button
type="primary"
status="success"
size="small"
title="同步"
:disabled="dataList.length !== 0 && dataList[0].createTime == null"
>
<template #icon><icon-sync /></template>同步
</a-button>
</a-tooltip>
</a-popconfirm>
</template>
<template #fieldName="{ record }">
<a-input v-model="record.fieldName" />
</template>
<template #fieldType="{ record }">
<a-select
v-model="record.fieldType"
placeholder="请选择字段类型"
allow-search
allow-create
:error="!record.fieldType"
>
<a-option value="String">String</a-option>
<a-option value="Integer">Integer</a-option>
<a-option value="Long">Long</a-option>
<a-option value="Float">Float</a-option>
<a-option value="Double">Double</a-option>
<a-option value="Boolean">Boolean</a-option>
<a-option value="BigDecimal">BigDecimal</a-option>
<a-option value="LocalDate">LocalDate</a-option>
<a-option value="LocalTime">LocalTime</a-option>
<a-option value="LocalDateTime">LocalDateTime</a-option>
</a-select>
</template>
<template #comment="{ record }">
<a-input v-model="record.comment" />
</template>
<template #showInList="{ record }">
<a-checkbox v-model="record.showInList" value="true" />
</template>
<template #showInForm="{ record }">
<a-checkbox v-model="record.showInForm" value="true" />
</template>
<template #isRequired="{ record }">
<a-checkbox v-if="record.showInForm" v-model="record.isRequired" value="true" />
<a-checkbox v-else disabled />
</template>
<template #showInQuery="{ record }">
<a-checkbox v-model="record.showInQuery" value="true" />
</template>
<template #formType="{ record }">
<a-select
v-if="record.showInForm || record.showInQuery"
v-model="record.formType"
:options="form_type_enum"
:default-value="1"
placeholder="请选择表单类型"
/>
<span v-else>无需设置</span>
</template>
<template #queryType="{ record }">
<a-select
v-if="record.showInQuery"
v-model="record.queryType"
:options="query_type_enum"
:default-value="1"
placeholder="请选择查询方式"
/>
<span v-else>无需设置</span>
</template>
<template #dictCode="{ record }">
<a-select
v-model="record.dictCode"
:options="dictList"
placeholder="请选择字典类型"
allow-search
allow-clear
/>
</template>
</GiTable>
</a-tab-pane>
</a-tabs>
</a-drawer>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { type FieldConfigResp, type GeneratorConfigResp, getGenConfig, listFieldConfig, listFieldConfigDict, saveGenConfig } from '@/apis/code/generator'
import type { LabelValueState } from '@/types/global'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const title = ref('')
const visible = ref(false)
const activeKey = ref('1')
const formRef = ref<InstanceType<typeof GiForm>>()
const { form_type_enum, query_type_enum } = useDict('form_type_enum', 'query_type_enum')
const dictList = ref<LabelValueState[]>([])
const [form, resetForm] = useResetReactive({
isOverride: false,
})
const formColumns: ColumnItem[] = reactive([
{
label: '作者名称',
field: 'author',
type: 'input',
required: true,
props: {
maxLength: 100,
},
},
{
label: '业务名称',
field: 'businessName',
type: 'input',
props: {
placeholder: '自定义业务名称,例如:用户',
maxLength: 50,
},
rules: [{ required: true, message: '请输入业务名称' }],
},
{
label: '所属模块',
field: 'moduleName',
type: 'input',
props: {
placeholder: '项目模块名称例如continew-system',
maxLength: 60,
showWordLimit: true,
},
rules: [{ required: true, message: '请输入所属模块' }],
},
{
label: '模块包名',
field: 'packageName',
type: 'input',
props: {
placeholder: '项目模块包名例如top.continew.admin.system',
maxLength: 60,
},
rules: [{ required: true, message: '请输入模块包名' }],
},
{
label: '去表前缀',
field: 'tablePrefix',
type: 'input',
props: {
placeholder: '数据库表前缀例如sys_',
maxLength: 20,
},
},
{
label: '是否覆盖',
field: 'isOverride',
type: 'switch',
props: {
type: 'round',
checkedValue: true,
uncheckedValue: false,
checkedText: '是',
uncheckedText: '否',
},
},
])
const dataList = ref<FieldConfigResp[]>([])
const loading = ref(false)
// 查询列表数据
const getDataList = async (tableName: string, requireSync: boolean) => {
try {
loading.value = true
const { data } = await listFieldConfig(tableName, requireSync)
dataList.value = data
} finally {
loading.value = false
}
}
// Table 字段配置
const columns: TableInstanceColumns[] = [
{ title: '名称', slotName: 'fieldName' },
{ title: '类型', slotName: 'fieldType' },
{ title: '描述', slotName: 'comment', width: 170 },
{ title: '列表', slotName: 'showInList', width: 60, align: 'center' },
{ title: '表单', slotName: 'showInForm', width: 60, align: 'center' },
{ title: '必填', slotName: 'isRequired', width: 60, align: 'center' },
{ title: '查询', slotName: 'showInQuery', width: 60, align: 'center' },
{ title: '表单类型', slotName: 'formType' },
{ title: '查询方式', slotName: 'queryType' },
{ title: '关联字典', slotName: 'dictCode' },
]
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
}
// 同步
const handleRefresh = async (tableName: string) => {
await getDataList(tableName, true)
}
// 拖拽排序
const handleChangeSort = (newDataList: FieldConfigResp[]) => {
dataList.value = newDataList
}
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) {
activeKey.value = '1'
return false
}
await saveGenConfig(form.tableName, {
genConfig: form,
fieldConfigs: dataList.value,
} as GeneratorConfigResp)
Message.success('保存成功')
emit('save-success')
return true
} catch (error) {
return false
}
}
// 打开
const onOpen = async (tableName: string, comment: string) => {
reset()
comment = comment ? `${comment}` : ' '
title.value = `${tableName}${comment}配置`
// 查询生成配置
const { data } = await getGenConfig(tableName)
Object.assign(form, data)
form.isOverride = form.isOverride || false
visible.value = true
// 查询字段配置
await getDataList(tableName, false)
const res = await listFieldConfigDict()
dictList.value = res.data
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
:deep(.gen-config.arco-form) {
width: 50%;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<a-modal v-model:visible="visible" width="90%" :footer="false">
<template #title>
{{ previewTableNames.length === 1 ? `生成 ${previewTableNames[0]} 表预览` : '批量生成预览' }}
<a-link v-permission="['code:generator:generate']" style="margin-left: 10px" icon @click="onDownload">下载源码</a-link>
<a-link v-permission="['code:generator:generate']" style="margin-left: 10px" icon @click="onGenerator">生成源码</a-link>
</template>
<div class="preview-content">
<a-layout :has-sider="true">
<a-layout-sider theme="dark" style="max-width:600px; height: 700px" :resize-directions="['right']" :width="370">
<a-tree
ref="treeRef"
:data="treeData"
show-line
block-node
:selected-keys="selectedKeys"
class="selectPreview"
@select="onSelectPreview"
>
<template #switcher-icon="node, { isLeaf }">
<icon-caret-down v-if="!isLeaf" />
</template>
<template #icon="node">
<GiSvgIcon v-if="!node.isLeaf && !node.expanded" :size="16" name="directory-blue" />
<GiSvgIcon v-if="!node.isLeaf && node.expanded" :size="16" name="directory-open-blue" />
<GiSvgIcon v-if="node.isLeaf && checkFileType(node.node.title, '.java')" :size="16" name="file-java" />
<GiSvgIcon v-if="node.isLeaf && checkFileType(node.node.title, '.vue')" :size="16" name="file-vue" />
<GiSvgIcon
v-if="node.isLeaf && checkFileType(node.node.title, '.ts')" :size="16"
name="file-typescript"
/>
<GiSvgIcon
v-if="node.isLeaf && checkFileType(node.node.title, '.js')" :size="16"
name="file-javascript"
/>
<GiSvgIcon v-if="node.isLeaf && checkFileType(node.node.title, '.json')" :size="16" name="file-json" />
<GiSvgIcon v-if="node.isLeaf && checkFileType(node.node.title, 'pom.xml')" :size="16" name="file-maven" />
<GiSvgIcon
v-if="node.isLeaf && checkFileType(node.node.title, '.xml') && !checkFileType(node.node.title, 'pom.xml')"
:size="16" name="file-xml"
/>
<GiSvgIcon v-if="node.isLeaf && checkFileType(node.node.title, '.sql')" :size="16" name="file-sql" />
</template>
</a-tree>
</a-layout-sider>
<a-layout-content>
<a-card>
<template #title>
<a-typography-title :heading="6" ellipsis>
{{ currentPreview?.path }}{{ currentPreview?.path.indexOf('/') !== -1 ? '/' : '\\' }}{{ currentPreview?.fileName }}
</a-typography-title>
</template>
<a-scrollbar style="height: 650px; overflow: auto">
<a-link style="position: absolute; right: 20px; z-index: 999" title="复制" @click="onCopy">
<template #icon><icon-copy size="large" /></template>
<template #default>复制</template>
</a-link>
<GiCodeView
:type="'vue' === currentPreview?.fileName.split('.')[1] ? 'vue' : 'javascript'"
:code-json="currentPreview?.content"
/>
</a-scrollbar>
</a-card>
</a-layout-content>
</a-layout>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { Message, type TreeNodeData } from '@arco-design/web-vue'
import { useClipboard } from '@vueuse/core'
import { type GeneratePreviewResp, genPreview } from '@/apis/code/generator'
const emit = defineEmits(['download', 'generate'])
const { copy, copied } = useClipboard()
const genPreviewList = ref<GeneratePreviewResp[]>([])
const currentPreview = ref<GeneratePreviewResp>()
const visible = ref(false)
const previewTableNames = ref<string[]>([])
const treeRef = ref()
const treeData = ref<TreeNodeData[]>([])
// 合并目录
const mergeDir = (parent: TreeNodeData) => {
// 合并目录
if (parent.children?.length === 1 && typeof parent.children[0].key === 'number') {
const mergeTitle = mergeDir(parent.children[0])
if (mergeTitle !== '') {
parent.title = `${parent.title}/${mergeTitle}`
}
parent.children = parent.children[0].children
return parent.title
}
// 合并子目录
if (parent?.children) {
for (const child of parent.children) {
mergeDir(child)
}
}
return parent.title
}
const pushDir = (children: TreeNodeData[] | undefined, treeNode: TreeNodeData) => {
if (children) {
for (const child of children) {
if (child.title === treeNode.title) {
return child.children
}
}
}
children?.push(treeNode)
return treeNode.children
}
// 自增的一个key 因为key相同的节点会出现一些问题
let autoIncrementKey = 0
// 将生成的目录组装成树结构
const assembleTree = (genPreview: GeneratePreviewResp) => {
const separator = genPreview.path.includes('/') ? '/' : '\\'
const paths: string[] = genPreview.path.split(separator)
let tempChildren: TreeNodeData[] | undefined = treeData.value
for (const path of paths) {
// 向treeData中推送目录,如果该级目录有那么不推送进行下级的合并
tempChildren = pushDir(tempChildren, { title: path, key: autoIncrementKey++, children: new Array<TreeNodeData>() })
}
tempChildren?.push({ title: genPreview.fileName, key: genPreview.fileName, children: new Array<TreeNodeData>() })
}
// 下载
const onDownload = () => {
emit('download', [previewTableNames.value])
}
// 下载
const onGenerator = () => {
emit('generate', [previewTableNames.value])
}
// 校验文件类型
const checkFileType = (title: string, type: string) => {
return title.endsWith(type)
}
// 复制
const onCopy = () => {
if (currentPreview.value) {
copy(currentPreview.value?.content)
}
}
watch(copied, () => {
if (copied.value) {
Message.success('复制成功')
}
})
const selectedKeys = ref()
// 选择文件预览
const onSelectPreview = (keys: (string | number)[]) => {
if (typeof keys[0] === 'string') {
currentPreview.value = genPreviewList.value.filter((p) => p.fileName === keys[0])[0]
selectedKeys.value = keys
} else {
const expandedKeys = treeRef.value.getExpandedNodes().map((node) => node.key)
treeRef.value.expandNode(keys[0], !expandedKeys.includes(keys[0]))
}
}
// 打开
const onOpen = async (tableNames: Array<string>) => {
treeData.value = []
previewTableNames.value = tableNames
const { data } = await genPreview(tableNames)
genPreviewList.value = data
for (const genPreview of genPreviewList.value) {
assembleTree(genPreview)
}
for (const valueElement of treeData.value) {
mergeDir(valueElement)
}
selectedKeys.value = [genPreviewList.value[0].fileName]
currentPreview.value = genPreviewList.value[0]
await nextTick(() => {
treeRef.value.expandAll(true)
})
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
:deep(.arco-tree-show-line .arco-tree-node-is-leaf:not(.arco-tree-node-is-tail) .arco-tree-node-indent::after) {
content: none;
}
.preview-content :deep(.arco-layout-sider) {
min-width: 200px;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<div class="gi_table_page">
<GiTable
v-model:selectedKeys="selectedKeys"
title=""
row-key="tableName"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size', 'setting']"
:disabled-column-keys="['tableName']"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@select="select"
@select-all="selectAll"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.tableName" placeholder="搜索表名称" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button type="primary" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onPreview(selectedKeys)">
<template #icon><icon-code-sandbox /></template>
<template #default>批量生成</template>
</a-button>
</template>
<template #toolbar-bottom>
<a-alert>
<template v-if="selectedKeys.length > 0">
已选中 {{ selectedKeys.length }} 条记录(可跨页)
</template>
<template v-else>未选中任何记录</template>
<template v-if="selectedKeys.length > 0" #action>
<a-link @click="onClearSelected">清空</a-link>
</template>
</a-alert>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['code:generator:config']" title="配置" @click="onConfig(record.tableName, record.comment)">配置</a-link>
<a-link
v-permission="['code:generator:preview']"
:disabled="!record.createTime"
:title="record.createTime ? '生成' : '请先进行生成配置'"
@click="onPreview([record.tableName])"
>
生成
</a-link>
</a-space>
</template>
</GiTable>
<GenConfigDrawer ref="GenConfigDrawerRef" @save-success="search" />
<GenPreviewModal ref="GenPreviewModalRef" @generate="onGenerate" @download="onDownload" />
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import GenConfigDrawer from './GenConfigDrawer.vue'
import { downloadCode, generateCode, listGenConfig } from '@/apis/code/generator'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { isMobile } from '@/utils'
defineOptions({ name: 'CodeGenerator' })
const GenPreviewModal = defineAsyncComponent(() => import('./GenPreviewModal.vue'))
const queryForm = reactive({
tableName: undefined,
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
} = useTable((page) => listGenConfig({ ...queryForm, ...page }), { immediate: true, formatResult: (data) => data.map((i) => ({ ...i, disabled: !i.createTime })) })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '表名称', dataIndex: 'tableName', minWidth: 225, ellipsis: true, tooltip: true },
{ title: '描述', dataIndex: 'comment', ellipsis: true, tooltip: true },
{ title: '类名前缀', dataIndex: 'classNamePrefix', ellipsis: true, tooltip: true },
{ title: '作者名称', dataIndex: 'author' },
{ title: '所属模块', dataIndex: 'moduleName', ellipsis: true, tooltip: true },
{ title: '模块包名', dataIndex: 'packageName', ellipsis: true, tooltip: true },
{ title: '配置时间', dataIndex: 'createTime', width: 180 },
{ title: '修改时间', dataIndex: 'updateTime', width: 180 },
{ title: '操作', dataIndex: 'action', slotName: 'action', width: 160, align: 'center', fixed: !isMobile() ? 'right' : undefined },
]
// 重置
const reset = () => {
queryForm.tableName = undefined
search()
}
// 清空所有选中数据
const onClearSelected = () => {
selectedKeys.value = []
}
const GenConfigDrawerRef = ref<InstanceType<typeof GenConfigDrawer>>()
// 配置
const onConfig = (tableName: string, comment: string) => {
GenConfigDrawerRef.value?.onOpen(tableName, comment)
}
const GenPreviewModalRef = ref<InstanceType<typeof GenPreviewModal>>()
// 预览
const onPreview = (tableNames: Array<string>) => {
GenPreviewModalRef.value?.onOpen(tableNames)
}
// 生成
const onDownload = async (tableNames: Array<string>) => {
const res = await downloadCode(tableNames)
const contentDisposition = res.headers['content-disposition']
const pattern = /filename=([^;]+\.[^.;]+);*/
const result = pattern.exec(contentDisposition) || ''
// 对名字进行解码
const fileName = window.decodeURI(result[1])
// 创建下载的链接
const blob = new Blob([res.data])
const downloadElement = document.createElement('a')
const href = window.URL.createObjectURL(blob)
downloadElement.style.display = 'none'
downloadElement.href = href
// 下载后文件名
downloadElement.download = fileName
document.body.appendChild(downloadElement)
// 点击下载
downloadElement.click()
// 下载完成,移除元素
document.body.removeChild(downloadElement)
// 释放掉 blob 对象
window.URL.revokeObjectURL(href)
}
// 生成
const onGenerate = async (tableNames: Array<string>) => {
const res = await generateCode(tableNames)
if (res.code === 0) {
Message.success('代码生成成功')
}
}
</script>
<style scoped lang="scss"></style>