feat: 代码生成器 下载功能

This commit is contained in:
cuijiawang 2025-09-26 12:42:24 +08:00
parent 944ecfa3bf
commit 2fbdaedf21
7 changed files with 7465 additions and 5041 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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; // CSSline-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

View File

@ -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>

View File

@ -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%
);
}
}
}

View 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")
);
}

View File

@ -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
};
}