feat: 代码生成器 下载功能
This commit is contained in:
parent
944ecfa3bf
commit
2fbdaedf21
@ -36,20 +36,20 @@
|
||||
"@vueuse/motion": "^2.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.4.0",
|
||||
"cropperjs": "^1.5.13",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.8",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "2.3.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jszip": "^3.10.1",
|
||||
"mitt": "^3.0.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path": "^0.12.7",
|
||||
"pinia": "^2.1.4",
|
||||
"pinyin-pro": "^3.15.2",
|
||||
"cropperjs": "^1.5.13",
|
||||
"vue-tippy": "^6.2.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.2",
|
||||
"responsive-storage": "^2.2.0",
|
||||
@ -57,6 +57,7 @@
|
||||
"typeit": "^8.7.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2",
|
||||
"vue-tippy": "^6.2.0",
|
||||
"vue-types": "^5.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
|
||||
11706
pnpm-lock.yaml
generated
11706
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -32,13 +32,15 @@ const updateValue = (value: string) => {
|
||||
|
||||
const updateLineNumbers = (content: string) => {
|
||||
const lines = content.split("\n");
|
||||
lineNumbers.value = lines.map((_, index) => String(index + 1).padStart(3, " "));
|
||||
lineNumbers.value = lines.map((_, index) =>
|
||||
String(index + 1).padStart(3, " ")
|
||||
);
|
||||
};
|
||||
|
||||
// 监听内容变化,更新行号
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
newValue => {
|
||||
updateLineNumbers(newValue);
|
||||
},
|
||||
{ immediate: true }
|
||||
@ -47,7 +49,9 @@ watch(
|
||||
// 同步滚动
|
||||
const handleScroll = (event: Event) => {
|
||||
const textarea = event.target as HTMLTextAreaElement;
|
||||
const lineNumbersEl = textarea.parentElement?.querySelector(".line-numbers") as HTMLElement;
|
||||
const lineNumbersEl = textarea.parentElement?.querySelector(
|
||||
".line-numbers"
|
||||
) as HTMLElement;
|
||||
if (lineNumbersEl) {
|
||||
lineNumbersEl.scrollTop = textarea.scrollTop;
|
||||
}
|
||||
@ -74,12 +78,12 @@ const handleKeydown = (event: KeyboardEvent) => {
|
||||
// 计算动态高度
|
||||
const dynamicHeight = computed(() => {
|
||||
if (props.readonly && props.modelValue) {
|
||||
const lines = props.modelValue.split('\n').length;
|
||||
const lines = props.modelValue.split("\n").length;
|
||||
const lineHeight = 19.5; // 匹配CSS中的line-height
|
||||
const padding = 16; // top + bottom padding
|
||||
const minHeight = 200;
|
||||
const calculatedHeight = lines * lineHeight + padding;
|
||||
return Math.max(calculatedHeight, minHeight) + 'px';
|
||||
return Math.max(calculatedHeight, minHeight) + "px";
|
||||
}
|
||||
return props.height;
|
||||
});
|
||||
@ -118,31 +122,31 @@ const dynamicHeight = computed(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.code-editor {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
|
||||
&.readonly {
|
||||
background: var(--el-disabled-bg-color);
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
.line-numbers {
|
||||
background: var(--el-bg-color-page);
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
min-width: 40px;
|
||||
padding: 8px 4px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
overflow: hidden;
|
||||
font-family: Consolas, Monaco, "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
background: var(--el-bg-color-page);
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
// 隐藏滚动条
|
||||
scrollbar-width: none; // Firefox
|
||||
@ -153,31 +157,31 @@ const dynamicHeight = computed(() => {
|
||||
}
|
||||
|
||||
.line-number {
|
||||
height: 19.5px; // 匹配textarea行高
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 19.5px; // 匹配textarea行高
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.code-textarea {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-family: Consolas, Monaco, "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-primary);
|
||||
resize: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
// 只读模式下自动调整高度
|
||||
&[readonly] {
|
||||
overflow: hidden;
|
||||
min-height: 200px;
|
||||
height: auto !important;
|
||||
min-height: 200px;
|
||||
overflow: hidden;
|
||||
scrollbar-width: none; // Firefox
|
||||
-ms-overflow-style: none; // IE/Edge
|
||||
|
||||
|
||||
@ -25,7 +25,12 @@ const updateValue = (key: keyof CodegenOptions, value: any) => {
|
||||
|
||||
// 配置分组
|
||||
const basicConfig = [
|
||||
{ key: "dataType", label: "解析引擎", type: "select", options: "dataTypeOptions" },
|
||||
{
|
||||
key: "dataType",
|
||||
label: "解析引擎",
|
||||
type: "select",
|
||||
options: "dataTypeOptions"
|
||||
},
|
||||
{ key: "authorName", label: "作者", type: "input" },
|
||||
{ key: "packageName", label: "包名", type: "input" }
|
||||
];
|
||||
@ -37,9 +42,24 @@ const returnConfig = [
|
||||
];
|
||||
|
||||
const typeConfig = [
|
||||
{ key: "tinyintTransType", label: "TinyInt转换", type: "select", options: "tinyintTransTypeOptions" },
|
||||
{ key: "timeTransType", label: "时间类型", type: "select", options: "timeTransTypeOptions" },
|
||||
{ key: "nameCaseType", label: "命名类型", type: "select", options: "nameCaseTypeOptions" }
|
||||
{
|
||||
key: "tinyintTransType",
|
||||
label: "TinyInt转换",
|
||||
type: "select",
|
||||
options: "tinyintTransTypeOptions"
|
||||
},
|
||||
{
|
||||
key: "timeTransType",
|
||||
label: "时间类型",
|
||||
type: "select",
|
||||
options: "timeTransTypeOptions"
|
||||
},
|
||||
{
|
||||
key: "nameCaseType",
|
||||
label: "命名类型",
|
||||
type: "select",
|
||||
options: "nameCaseTypeOptions"
|
||||
}
|
||||
];
|
||||
|
||||
const switchConfig = [
|
||||
@ -67,7 +87,7 @@ const switchConfig = [
|
||||
v-if="config.type === 'select'"
|
||||
:model-value="modelValue[config.key]"
|
||||
class="w-full"
|
||||
@update:model-value="(val) => updateValue(config.key, val)"
|
||||
@update:model-value="val => updateValue(config.key, val)"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in props[config.options]"
|
||||
@ -79,7 +99,7 @@ const switchConfig = [
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="modelValue[config.key]"
|
||||
@update:model-value="(val) => updateValue(config.key, val)"
|
||||
@update:model-value="val => updateValue(config.key, val)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@ -96,7 +116,7 @@ const switchConfig = [
|
||||
>
|
||||
<el-input
|
||||
:model-value="modelValue[config.key]"
|
||||
@update:model-value="(val) => updateValue(config.key, val)"
|
||||
@update:model-value="val => updateValue(config.key, val)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@ -114,7 +134,7 @@ const switchConfig = [
|
||||
<el-select
|
||||
:model-value="modelValue[config.key]"
|
||||
class="w-full"
|
||||
@update:model-value="(val) => updateValue(config.key, val)"
|
||||
@update:model-value="val => updateValue(config.key, val)"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in props[config.options]"
|
||||
@ -139,7 +159,7 @@ const switchConfig = [
|
||||
>
|
||||
<el-switch
|
||||
:model-value="modelValue[config.key]"
|
||||
@update:model-value="(val) => updateValue(config.key, val)"
|
||||
@update:model-value="val => updateValue(config.key, val)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@ -148,6 +168,31 @@ const switchConfig = [
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 响应式设计
|
||||
@media (width <= 768px) {
|
||||
.config-form {
|
||||
.config-section {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
|
||||
.switch-item {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-form {
|
||||
.config-section {
|
||||
margin-bottom: 24px;
|
||||
@ -158,22 +203,26 @@ const switchConfig = [
|
||||
|
||||
.section-title {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
margin-bottom: 16px;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
content: "";
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary) 0%,
|
||||
var(--el-color-primary-light-3) 100%
|
||||
);
|
||||
border-radius: 2px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,27 +232,28 @@ const switchConfig = [
|
||||
gap: 16px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
padding: 16px;
|
||||
margin-bottom: 0;
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 2px 12px rgb(0 0 0 / 8%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.el-input, .el-select {
|
||||
.el-input,
|
||||
.el-select {
|
||||
.el-input__wrapper {
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
@ -230,33 +280,41 @@ const switchConfig = [
|
||||
gap: 12px;
|
||||
|
||||
.switch-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, var(--el-bg-color) 0%, var(--el-bg-color-page) 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-bg-color) 0%,
|
||||
var(--el-bg-color-page) 100%
|
||||
);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--el-color-primary-light-8) 0%, var(--el-color-primary-light-6) 100%);
|
||||
content: "";
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--el-color-primary-light-8) 0%,
|
||||
var(--el-color-primary-light-6) 100%
|
||||
);
|
||||
transition: transform 0.3s ease;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
@ -266,9 +324,9 @@ const switchConfig = [
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
@ -293,29 +351,4 @@ const switchConfig = [
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.config-form {
|
||||
.config-section {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
|
||||
.switch-item {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -8,8 +8,9 @@ import CodeEditor from "./components/CodeEditor.vue";
|
||||
|
||||
import Play from "@iconify-icons/ep/caret-right";
|
||||
import CopyDocument from "@iconify-icons/ep/document-copy";
|
||||
import Download from "@iconify-icons/ep/download";
|
||||
import FolderOpened from "@iconify-icons/ep/folder-opened";
|
||||
import ArrowDown from "@iconify-icons/ep/arrow-down";
|
||||
import ArrowUp from "@iconify-icons/ep/arrow-up";
|
||||
|
||||
defineOptions({
|
||||
name: "Codegen"
|
||||
@ -33,6 +34,8 @@ const {
|
||||
setOutputModel,
|
||||
generateCode,
|
||||
copyCode,
|
||||
downloadCurrentCode,
|
||||
downloadAllCode,
|
||||
switchHistoricalData
|
||||
} = useCodegen();
|
||||
|
||||
@ -52,8 +55,12 @@ onMounted(() => {
|
||||
<div class="p-4">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white flex items-center">
|
||||
<el-icon class="mr-2"><component :is="useRenderIcon('ep:code')" /></el-icon>
|
||||
<h1
|
||||
class="text-2xl font-bold text-gray-800 dark:text-white flex items-center"
|
||||
>
|
||||
<el-icon class="mr-2"
|
||||
><component :is="useRenderIcon('ep:code')"
|
||||
/></el-icon>
|
||||
代码生成器
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300 mt-2">
|
||||
@ -64,10 +71,16 @@ onMounted(() => {
|
||||
<!-- 历史记录区域 -->
|
||||
<el-card v-if="hasHistory" class="mb-4 collapsible-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="collapsible-header" @click="historyCollapsed = !historyCollapsed">
|
||||
<div
|
||||
class="collapsible-header"
|
||||
@click="historyCollapsed = !historyCollapsed"
|
||||
>
|
||||
<h3 class="text-lg font-semibold">
|
||||
历史记录
|
||||
<el-icon class="collapse-arrow" :class="{ 'collapsed': historyCollapsed }">
|
||||
<el-icon
|
||||
class="collapse-arrow"
|
||||
:class="{ collapsed: historyCollapsed }"
|
||||
>
|
||||
<component :is="useRenderIcon(ArrowDown)" />
|
||||
</el-icon>
|
||||
</h3>
|
||||
@ -97,7 +110,10 @@ onMounted(() => {
|
||||
<div class="collapsible-header" @click="sqlCollapsed = !sqlCollapsed">
|
||||
<h3 class="text-lg font-semibold">
|
||||
输入SQL
|
||||
<el-icon class="collapse-arrow" :class="{ 'collapsed': sqlCollapsed }">
|
||||
<el-icon
|
||||
class="collapse-arrow"
|
||||
:class="{ collapsed: sqlCollapsed }"
|
||||
>
|
||||
<component :is="useRenderIcon(ArrowDown)" />
|
||||
</el-icon>
|
||||
</h3>
|
||||
@ -120,10 +136,16 @@ onMounted(() => {
|
||||
<el-card class="mb-4 collapsible-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="collapsible-header" @click="configCollapsed = !configCollapsed">
|
||||
<div
|
||||
class="collapsible-header"
|
||||
@click="configCollapsed = !configCollapsed"
|
||||
>
|
||||
<h3 class="text-lg font-semibold">
|
||||
生成设置
|
||||
<el-icon class="collapse-arrow" :class="{ 'collapsed': configCollapsed }">
|
||||
<el-icon
|
||||
class="collapse-arrow"
|
||||
:class="{ collapsed: configCollapsed }"
|
||||
>
|
||||
<component :is="useRenderIcon(ArrowDown)" />
|
||||
</el-icon>
|
||||
</h3>
|
||||
@ -155,10 +177,16 @@ onMounted(() => {
|
||||
<!-- 模板选择区域 -->
|
||||
<el-card class="mb-4 collapsible-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="collapsible-header" @click="templateCollapsed = !templateCollapsed">
|
||||
<div
|
||||
class="collapsible-header"
|
||||
@click="templateCollapsed = !templateCollapsed"
|
||||
>
|
||||
<h3 class="text-lg font-semibold">
|
||||
模板选择
|
||||
<el-icon class="collapse-arrow" :class="{ 'collapsed': templateCollapsed }">
|
||||
<el-icon
|
||||
class="collapse-arrow"
|
||||
:class="{ collapsed: templateCollapsed }"
|
||||
>
|
||||
<component :is="useRenderIcon(ArrowDown)" />
|
||||
</el-icon>
|
||||
</h3>
|
||||
@ -182,14 +210,32 @@ onMounted(() => {
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">输出代码</h3>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="useRenderIcon(CopyDocument)"
|
||||
:disabled="!hasOutput"
|
||||
@click="copyCode"
|
||||
>
|
||||
复制代码
|
||||
</el-button>
|
||||
<div class="flex gap-2">
|
||||
<el-button
|
||||
type="success"
|
||||
:icon="useRenderIcon(Download)"
|
||||
:disabled="!hasOutput"
|
||||
@click="downloadCurrentCode"
|
||||
>
|
||||
下载当前文件
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
:icon="useRenderIcon(FolderOpened)"
|
||||
:disabled="!hasOutput"
|
||||
@click="downloadAllCode"
|
||||
>
|
||||
下载所有文件
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="useRenderIcon(CopyDocument)"
|
||||
:disabled="!hasOutput"
|
||||
@click="copyCode"
|
||||
>
|
||||
复制代码
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -206,6 +252,19 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 响应式设计
|
||||
@media (width <= 768px) {
|
||||
.main {
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: calc(100vh - 200px);
|
||||
background: var(--el-bg-color-page);
|
||||
@ -213,21 +272,25 @@ onMounted(() => {
|
||||
|
||||
:deep(.el-card) {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 4px 20px 0 rgb(0 0 0 / 15%);
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-bg-color) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary-light-9) 0%,
|
||||
var(--el-bg-color) 100%
|
||||
);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,8 +312,8 @@ onMounted(() => {
|
||||
|
||||
.collapse-arrow {
|
||||
margin-left: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.collapsed {
|
||||
@ -305,15 +368,15 @@ onMounted(() => {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮样式优化
|
||||
:deep(.el-button) {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
@ -321,24 +384,19 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&.el-button--primary {
|
||||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-dark-2) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary) 0%,
|
||||
var(--el-color-primary-dark-2) 100%
|
||||
);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-2) 0%, var(--el-color-primary) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.main {
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary-light-2) 0%,
|
||||
var(--el-color-primary) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
420
src/views/system/codegen/utils/download.ts
Normal file
420
src/views/system/codegen/utils/download.ts
Normal file
@ -0,0 +1,420 @@
|
||||
/**
|
||||
* 代码下载工具函数
|
||||
* 支持根据代码内容自动识别文件类型和生成文件名
|
||||
*/
|
||||
import JSZip from "jszip";
|
||||
|
||||
// 文件类型映射
|
||||
const FILE_TYPE_PATTERNS = {
|
||||
java: [
|
||||
/public\s+class\s+\w+/,
|
||||
/public\s+interface\s+\w+/,
|
||||
/public\s+enum\s+\w+/,
|
||||
/@Entity/,
|
||||
/@Service/,
|
||||
/@Controller/,
|
||||
/@Repository/,
|
||||
/@Component/,
|
||||
/package\s+[\w.]+;/,
|
||||
/import\s+[\w.]+;/
|
||||
],
|
||||
xml: [
|
||||
/<\?xml/,
|
||||
/<mapper/,
|
||||
/<select/,
|
||||
/<insert/,
|
||||
/<update/,
|
||||
/<delete/,
|
||||
/<configuration>/,
|
||||
/<beans>/
|
||||
],
|
||||
sql: [
|
||||
/CREATE\s+TABLE/i,
|
||||
/ALTER\s+TABLE/i,
|
||||
/INSERT\s+INTO/i,
|
||||
/SELECT\s+.*\s+FROM/i,
|
||||
/UPDATE\s+.*\s+SET/i,
|
||||
/DELETE\s+FROM/i,
|
||||
/DROP\s+TABLE/i
|
||||
],
|
||||
js: [
|
||||
/function\s+\w+/,
|
||||
/const\s+\w+\s*=/,
|
||||
/let\s+\w+\s*=/,
|
||||
/var\s+\w+\s*=/,
|
||||
/export\s+/,
|
||||
/import\s+.*\s+from/,
|
||||
/module\.exports/,
|
||||
/require\(/
|
||||
],
|
||||
ts: [
|
||||
/interface\s+\w+/,
|
||||
/type\s+\w+/,
|
||||
/export\s+interface/,
|
||||
/export\s+type/,
|
||||
/:\s*\w+(\[\])?/,
|
||||
/import\s+.*\s+from\s+['"].*\.ts['"]/
|
||||
],
|
||||
vue: [
|
||||
/<template>/,
|
||||
/<script.*>/,
|
||||
/<style.*>/,
|
||||
/export\s+default\s*{/,
|
||||
/defineComponent/
|
||||
],
|
||||
json: [/^\s*{/, /^\s*\[/, /"[\w-]+"\s*:/],
|
||||
yml: [/^[\w-]+:/, /^\s+-\s/, /^---/],
|
||||
properties: [/^[\w.-]+\s*=/, /^#/, /spring\./, /server\./],
|
||||
html: [/<!DOCTYPE\s+html>/i, /<html/i, /<head>/i, /<body>/i, /<div/i, /<p>/i],
|
||||
css: [
|
||||
/\.[a-zA-Z-]+\s*{/,
|
||||
/#[a-zA-Z-]+\s*{/,
|
||||
/@media/,
|
||||
/@import/,
|
||||
/:\s*[\w#.-]+;/
|
||||
],
|
||||
scss: [/\$[\w-]+:/, /@mixin/, /@include/, /@extend/, /&:/]
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据代码内容识别文件类型
|
||||
* @param content 代码内容
|
||||
* @returns 文件扩展名
|
||||
*/
|
||||
export function detectFileType(content: string): string {
|
||||
if (!content || content.trim().length === 0) {
|
||||
return "txt";
|
||||
}
|
||||
|
||||
const trimmedContent = content.trim();
|
||||
|
||||
// 遍历所有文件类型模式
|
||||
for (const [fileType, patterns] of Object.entries(FILE_TYPE_PATTERNS)) {
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.test(trimmedContent)) {
|
||||
return fileType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回txt
|
||||
return "txt";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从代码内容中提取有意义的文件名
|
||||
* @param content 代码内容
|
||||
* @param fileType 文件类型
|
||||
* @returns 文件名(不含扩展名)
|
||||
*/
|
||||
export function extractFileName(content: string, fileType: string): string {
|
||||
if (!content || content.trim().length === 0) {
|
||||
return "untitled";
|
||||
}
|
||||
|
||||
const trimmedContent = content.trim();
|
||||
|
||||
switch (fileType) {
|
||||
case "java":
|
||||
return extractJavaFileName(trimmedContent);
|
||||
case "xml":
|
||||
return extractXmlFileName(trimmedContent);
|
||||
case "sql":
|
||||
return extractSqlFileName(trimmedContent);
|
||||
case "js":
|
||||
case "ts":
|
||||
return extractJsFileName(trimmedContent);
|
||||
case "vue":
|
||||
return extractVueFileName(trimmedContent);
|
||||
default:
|
||||
return generateGenericFileName(trimmedContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取Java文件名
|
||||
*/
|
||||
function extractJavaFileName(content: string): string {
|
||||
// 优先提取类名
|
||||
const classMatch = content.match(/public\s+class\s+(\w+)/);
|
||||
if (classMatch) {
|
||||
return classMatch[1];
|
||||
}
|
||||
|
||||
// 提取接口名
|
||||
const interfaceMatch = content.match(/public\s+interface\s+(\w+)/);
|
||||
if (interfaceMatch) {
|
||||
return interfaceMatch[1];
|
||||
}
|
||||
|
||||
// 提取枚举名
|
||||
const enumMatch = content.match(/public\s+enum\s+(\w+)/);
|
||||
if (enumMatch) {
|
||||
return enumMatch[1];
|
||||
}
|
||||
|
||||
// 从注解中推断
|
||||
if (content.includes("@Entity")) {
|
||||
const entityMatch = content.match(/@Table\s*\(\s*name\s*=\s*["'](\w+)["']/);
|
||||
if (entityMatch) {
|
||||
return toCamelCase(entityMatch[1]) + "Entity";
|
||||
}
|
||||
}
|
||||
|
||||
if (content.includes("@Service")) {
|
||||
return "Service";
|
||||
}
|
||||
|
||||
if (content.includes("@Controller")) {
|
||||
return "Controller";
|
||||
}
|
||||
|
||||
if (content.includes("@Repository")) {
|
||||
return "Repository";
|
||||
}
|
||||
|
||||
return "JavaClass";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取XML文件名
|
||||
*/
|
||||
function extractXmlFileName(content: string): string {
|
||||
// MyBatis Mapper
|
||||
const mapperMatch = content.match(
|
||||
/<mapper\s+namespace\s*=\s*["']([\w.]+)["']/
|
||||
);
|
||||
if (mapperMatch) {
|
||||
const namespace = mapperMatch[1];
|
||||
const parts = namespace.split(".");
|
||||
return parts[parts.length - 1] + "Mapper";
|
||||
}
|
||||
|
||||
// Spring配置
|
||||
if (content.includes("<beans")) {
|
||||
return "applicationContext";
|
||||
}
|
||||
|
||||
// MyBatis配置
|
||||
if (content.includes("<configuration>")) {
|
||||
return "mybatis-config";
|
||||
}
|
||||
|
||||
return "config";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取SQL文件名
|
||||
*/
|
||||
function extractSqlFileName(content: string): string {
|
||||
// 提取表名
|
||||
const createTableMatch = content.match(/CREATE\s+TABLE\s+[`'"]*(\w+)[`'"]*/i);
|
||||
if (createTableMatch) {
|
||||
return createTableMatch[1];
|
||||
}
|
||||
|
||||
const alterTableMatch = content.match(/ALTER\s+TABLE\s+[`'"]*(\w+)[`'"]*/i);
|
||||
if (alterTableMatch) {
|
||||
return alterTableMatch[1] + "_alter";
|
||||
}
|
||||
|
||||
const insertMatch = content.match(/INSERT\s+INTO\s+[`'"]*(\w+)[`'"]*/i);
|
||||
if (insertMatch) {
|
||||
return insertMatch[1] + "_data";
|
||||
}
|
||||
|
||||
return "database";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取JS/TS文件名
|
||||
*/
|
||||
function extractJsFileName(content: string): string {
|
||||
// 提取导出的类名或函数名
|
||||
const exportClassMatch = content.match(/export\s+class\s+(\w+)/);
|
||||
if (exportClassMatch) {
|
||||
return exportClassMatch[1];
|
||||
}
|
||||
|
||||
const exportFunctionMatch = content.match(/export\s+function\s+(\w+)/);
|
||||
if (exportFunctionMatch) {
|
||||
return exportFunctionMatch[1];
|
||||
}
|
||||
|
||||
const exportConstMatch = content.match(/export\s+const\s+(\w+)/);
|
||||
if (exportConstMatch) {
|
||||
return exportConstMatch[1];
|
||||
}
|
||||
|
||||
// 提取接口或类型名
|
||||
const interfaceMatch = content.match(/interface\s+(\w+)/);
|
||||
if (interfaceMatch) {
|
||||
return interfaceMatch[1];
|
||||
}
|
||||
|
||||
const typeMatch = content.match(/type\s+(\w+)/);
|
||||
if (typeMatch) {
|
||||
return typeMatch[1];
|
||||
}
|
||||
|
||||
return "script";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取Vue文件名
|
||||
*/
|
||||
function extractVueFileName(content: string): string {
|
||||
// 从defineOptions中提取name
|
||||
const defineOptionsMatch = content.match(
|
||||
/defineOptions\s*\(\s*{\s*name:\s*["'](\w+)["']/
|
||||
);
|
||||
if (defineOptionsMatch) {
|
||||
return defineOptionsMatch[1];
|
||||
}
|
||||
|
||||
// 从export default中的name属性提取
|
||||
const exportNameMatch = content.match(
|
||||
/export\s+default\s*{\s*name:\s*["'](\w+)["']/
|
||||
);
|
||||
if (exportNameMatch) {
|
||||
return exportNameMatch[1];
|
||||
}
|
||||
|
||||
return "Component";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成通用文件名
|
||||
*/
|
||||
function generateGenericFileName(content: string): string {
|
||||
// 提取第一行作为文件名(去除特殊字符)
|
||||
const firstLine = content.split("\n")[0].trim();
|
||||
if (firstLine.length > 0) {
|
||||
const cleanName = firstLine
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/\s+/g, "_")
|
||||
.substring(0, 20);
|
||||
if (cleanName.length > 0) {
|
||||
return cleanName;
|
||||
}
|
||||
}
|
||||
|
||||
return "untitled";
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为驼峰命名
|
||||
*/
|
||||
function toCamelCase(str: string): string {
|
||||
return str
|
||||
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
.replace(/^[a-z]/, letter => letter.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单个文件
|
||||
* @param content 文件内容
|
||||
* @param originalFileName 原始文件名(可选)
|
||||
*/
|
||||
export function downloadSingleFile(
|
||||
content: string,
|
||||
originalFileName?: string
|
||||
): void {
|
||||
if (!content || content.trim().length === 0) {
|
||||
console.warn("文件内容为空,无法下载");
|
||||
return;
|
||||
}
|
||||
|
||||
let fileName = originalFileName;
|
||||
|
||||
if (!fileName) {
|
||||
const fileType = detectFileType(content);
|
||||
const baseName = extractFileName(content, fileType);
|
||||
fileName = `${baseName}.${fileType}`;
|
||||
}
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载文件(需要jszip库支持)
|
||||
* @param files 文件映射 { fileName: content }
|
||||
* @param zipFileName zip文件名
|
||||
*/
|
||||
export async function downloadMultipleFiles(
|
||||
files: Record<string, string>,
|
||||
zipFileName = "generated-code.zip"
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 创建JSZip实例
|
||||
const zip = new JSZip();
|
||||
|
||||
// 添加文件到zip
|
||||
Object.entries(files).forEach(([_templateName, content]) => {
|
||||
if (content && content.trim().length > 0) {
|
||||
const fileType = detectFileType(content);
|
||||
const baseName = extractFileName(content, fileType);
|
||||
const fileName = `${baseName}.${fileType}`;
|
||||
|
||||
zip.file(fileName, content);
|
||||
}
|
||||
});
|
||||
|
||||
// 生成zip文件
|
||||
const zipContent = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(zipContent);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = zipFileName;
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("批量下载失败:", error);
|
||||
// 如果jszip不可用,回退到单个文件下载
|
||||
console.warn("JSZip不可用,回退到单个文件下载");
|
||||
Object.entries(files).forEach(([_templateName, content]) => {
|
||||
if (content && content.trim().length > 0) {
|
||||
downloadSingleFile(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳作为文件名后缀
|
||||
*/
|
||||
export function getTimestamp(): string {
|
||||
const now = new Date();
|
||||
return (
|
||||
now.getFullYear().toString() +
|
||||
(now.getMonth() + 1).toString().padStart(2, "0") +
|
||||
now.getDate().toString().padStart(2, "0") +
|
||||
"_" +
|
||||
now.getHours().toString().padStart(2, "0") +
|
||||
now.getMinutes().toString().padStart(2, "0") +
|
||||
now.getSeconds().toString().padStart(2, "0")
|
||||
);
|
||||
}
|
||||
@ -7,6 +7,11 @@ import {
|
||||
TemplateInfo,
|
||||
CodegenRequest
|
||||
} from "@/api/system/codegen";
|
||||
import {
|
||||
downloadSingleFile,
|
||||
downloadMultipleFiles,
|
||||
getTimestamp
|
||||
} from "./download";
|
||||
|
||||
export function useCodegen() {
|
||||
// 表单数据
|
||||
@ -148,6 +153,41 @@ export function useCodegen() {
|
||||
});
|
||||
}
|
||||
|
||||
// 下载当前代码文件
|
||||
function downloadCurrentCode() {
|
||||
if (!outputStr.value) {
|
||||
ElMessage.warning("没有可下载的内容");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
downloadSingleFile(outputStr.value.trim());
|
||||
ElMessage.success("下载成功");
|
||||
} catch (error) {
|
||||
console.error("下载失败:", error);
|
||||
ElMessage.error("下载失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 批量下载所有生成的代码文件
|
||||
async function downloadAllCode() {
|
||||
if (!outputJson.value || Object.keys(outputJson.value).length === 0) {
|
||||
ElMessage.warning("没有可下载的内容");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timestamp = getTimestamp();
|
||||
const zipFileName = `generated-code-${timestamp}.zip`;
|
||||
|
||||
await downloadMultipleFiles(outputJson.value, zipFileName);
|
||||
ElMessage.success("批量下载成功");
|
||||
} catch (error) {
|
||||
console.error("批量下载失败:", error);
|
||||
ElMessage.error("批量下载失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 设置历史记录
|
||||
function setHistoricalData(tableName: string) {
|
||||
// 添加新表名(如果不存在)
|
||||
@ -215,6 +255,8 @@ export function useCodegen() {
|
||||
setOutputModel,
|
||||
generateCode,
|
||||
copyCode,
|
||||
downloadCurrentCode,
|
||||
downloadAllCode,
|
||||
switchHistoricalData
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user