feat: 代码生成器 模板仓库

This commit is contained in:
cuijiawang 2025-09-26 17:54:01 +08:00
parent 2fbdaedf21
commit 2bd36d1848
4 changed files with 1306 additions and 12 deletions

View File

@ -44,6 +44,56 @@ export interface CodegenResponse {
tableName: string;
}
// 用户模板
export interface UserTemplate {
id?: number;
userId?: number;
templateName: string;
templateGroup: string;
templateContent: string;
description?: string;
isPublic: number; // 0-私有 1-公开
version?: string;
useCount?: number;
status?: number; // 0-草稿 1-发布 2-禁用
createTime?: string;
updateTime?: string;
createBy?: string;
updateBy?: string;
isOwner?: boolean;
isPublicText?: string;
statusText?: string;
}
// 模板仓库配置
export interface TemplateRepository {
id?: number;
userId?: number;
templateSource: string; // system-系统模板 user-用户模板
templateId: string;
templateName: string;
templateGroup: string;
isEnabled: number; // 0-隐藏 1-显示
sortOrder?: number;
createTime?: string;
}
// 分页查询参数
export interface PageQuery {
pageNum: number;
pageSize: number;
}
// 分页响应MyBatis-Plus Page结构
export interface PageResult<T> {
records: T[];
total: number;
current: number;
size: number;
pages?: number;
searchCount?: boolean;
}
// 获取所有模板
export function getAllTemplatesApi() {
return http.request<ResponseData<TemplateGroup[]>>(
@ -62,3 +112,171 @@ export function generateCodeApi(data: CodegenRequest) {
}
);
}
// ============= 用户模板管理 API =============
// 分页查询用户模板列表
export function getUserTemplateListApi(
params: PageQuery & {
templateGroup?: string;
isPublic?: number;
}
) {
return http.request<ResponseData<PageResult<UserTemplate>>>(
"get",
"/codegen/user-template/list",
{ params }
);
}
// 查询公开模板列表
export function getPublicTemplatesApi(templateGroup?: string) {
return http.request<ResponseData<UserTemplate[]>>(
"get",
"/codegen/user-template/public",
{ params: { templateGroup } }
);
}
// 根据ID查询用户模板详情
export function getUserTemplateByIdApi(id: number) {
return http.request<ResponseData<UserTemplate>>(
"get",
`/codegen/user-template/${id}`
);
}
// 新增用户模板
export function addUserTemplateApi(data: UserTemplate) {
return http.request<ResponseData<void>>("post", "/codegen/user-template", {
data
});
}
// 修改用户模板
export function updateUserTemplateApi(data: UserTemplate) {
return http.request<ResponseData<void>>(
"post",
"/codegen/user-template/update",
{ data }
);
}
// 删除用户模板
export function deleteUserTemplateApi(id: number) {
return http.request<ResponseData<void>>(
"post",
"/codegen/user-template/delete",
{ data: { id } }
);
}
// 批量删除用户模板
export function deleteUserTemplatesBatchApi(ids: number[]) {
return http.request<ResponseData<void>>(
"post",
"/codegen/user-template/delete/batch",
{ data: { ids } }
);
}
// 复制模板
export function copyTemplateApi(templateId: number, newName: string) {
return http.request<ResponseData<void>>(
"post",
"/codegen/user-template/copy",
{ data: { templateId, newName } }
);
}
// 验证模板语法
export function validateTemplateApi(templateContent: string) {
return http.request<ResponseData<string>>(
"post",
"/codegen/user-template/validate",
{ data: templateContent }
);
}
// 检查模板名称是否存在
export function checkTemplateNameApi(templateName: string, excludeId?: number) {
return http.request<ResponseData<boolean>>(
"get",
"/codegen/user-template/check-name",
{ params: { templateName, excludeId } }
);
}
// ============= 模板仓库管理 API =============
// 获取用户的模板仓库列表
export function getUserRepositoryListApi() {
return http.request<ResponseData<TemplateRepository[]>>(
"get",
"/codegen/repository/list"
);
}
// 获取所有可用模板
export function getAvailableTemplatesApi() {
return http.request<ResponseData<TemplateRepository[]>>(
"get",
"/codegen/repository/available"
);
}
// 添加模板到用户仓库
export function addTemplateToRepositoryApi(data: TemplateRepository) {
return http.request<ResponseData<void>>("post", "/codegen/repository/add", {
data
});
}
// 从用户仓库移除模板
export function removeTemplateFromRepositoryApi(
templateSource: string,
templateId: string
) {
return http.request<ResponseData<void>>(
"post",
"/codegen/repository/remove",
{ data: { templateSource, templateId } }
);
}
// 切换模板启用状态
export function toggleTemplateStatusApi(
templateSource: string,
templateId: string,
isEnabled: number
) {
return http.request<ResponseData<void>>(
"post",
"/codegen/repository/toggle",
{ data: { templateSource, templateId, isEnabled } }
);
}
// 批量更新模板排序
export function updateTemplateSortApi(templates: TemplateRepository[]) {
return http.request<ResponseData<void>>("post", "/codegen/repository/sort", {
data: templates
});
}
// 初始化用户模板仓库
export function initUserRepositoryApi() {
return http.request<ResponseData<void>>("post", "/codegen/repository/init");
}
// 检查模板是否已添加到用户仓库
export function checkTemplateExistsApi(
templateSource: string,
templateId: string
) {
return http.request<ResponseData<boolean>>(
"get",
"/codegen/repository/check",
{ params: { templateSource, templateId } }
);
}

View File

@ -0,0 +1,638 @@
<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getUserTemplateListApi,
addUserTemplateApi,
updateUserTemplateApi,
deleteUserTemplateApi,
deleteUserTemplatesBatchApi,
copyTemplateApi,
validateTemplateApi,
checkTemplateNameApi,
type UserTemplate,
type PageQuery,
type PageResult
} from "@/api/system/codegen";
interface Props {
visible: boolean;
}
interface Emits {
(e: "update:visible", visible: boolean): void;
(e: "refresh"): void;
}
const props = defineProps<Props>();
const emits = defineEmits<Emits>();
//
const loading = ref(false);
const saveLoading = ref(false);
const activeTab = ref("list"); // list | edit
const tableData = ref<UserTemplate[]>([]);
const multipleSelection = ref<UserTemplate[]>([]);
const searchForm = reactive({
templateGroup: "",
isPublic: ""
});
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
});
//
const editForm = reactive<UserTemplate>({
id: undefined,
templateName: "",
templateGroup: "",
templateContent: "",
description: "",
isPublic: 0,
version: "1.0.0",
status: 1
});
const editFormRef = ref();
const isEditMode = ref(false);
//
const editRules = {
templateName: [
{ required: true, message: "请输入模板名称", trigger: "blur" },
{
min: 2,
max: 50,
message: "模板名称长度为 2 到 50 个字符",
trigger: "blur"
}
],
templateGroup: [
{ required: true, message: "请输入模板分组", trigger: "blur" },
{
min: 2,
max: 30,
message: "模板分组长度为 2 到 30 个字符",
trigger: "blur"
}
],
templateContent: [
{ required: true, message: "请输入模板内容", trigger: "blur" },
{ min: 10, message: "模板内容至少需要 10 个字符", trigger: "blur" }
]
};
//
const dialogVisible = computed({
get: () => props.visible,
set: val => emits("update:visible", val)
});
//
const groupOptions = computed(() => {
const groups = new Set(tableData.value.map(item => item.templateGroup));
return Array.from(groups).map(group => ({ label: group, value: group }));
});
//
const loadTableData = async () => {
loading.value = true;
try {
const params: PageQuery & typeof searchForm = {
pageNum: pagination.current,
pageSize: pagination.pageSize,
...searchForm
};
const res = await getUserTemplateListApi(params);
if (res.code === 200) {
const data = res.data as PageResult<UserTemplate>;
tableData.value = data.records;
pagination.total = data.total;
} else {
ElMessage.error(res.msg || "加载失败");
}
} catch (error) {
ElMessage.error("加载失败");
console.error(error);
} finally {
loading.value = false;
}
};
//
const handleSearch = () => {
pagination.current = 1;
loadTableData();
};
//
const handleReset = () => {
Object.assign(searchForm, {
templateGroup: "",
isPublic: ""
});
pagination.current = 1;
loadTableData();
};
//
const handlePageChange = (page: number) => {
pagination.current = page;
loadTableData();
};
const handleSizeChange = (size: number) => {
pagination.pageSize = size;
pagination.current = 1;
loadTableData();
};
//
const handleSelectionChange = (selection: UserTemplate[]) => {
multipleSelection.value = selection;
};
//
const handleAdd = () => {
isEditMode.value = false;
Object.assign(editForm, {
id: undefined,
templateName: "",
templateGroup: "",
templateContent: "",
description: "",
isPublic: 0,
version: "1.0.0",
status: 1
});
activeTab.value = "edit";
};
//
const handleEdit = (row: UserTemplate) => {
isEditMode.value = true;
Object.assign(editForm, row);
activeTab.value = "edit";
};
//
const handleCopy = async (row: UserTemplate) => {
try {
const { value: newName } = await ElMessageBox.prompt(
"请输入新的模板名称",
"复制模板",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
inputValue: `${row.templateName}_副本`,
inputPattern: /^.{2,50}$/,
inputErrorMessage: "模板名称长度为 2 到 50 个字符"
}
);
const res = await copyTemplateApi(row.id!, newName);
if (res.code === 200) {
ElMessage.success("复制成功");
loadTableData();
} else {
ElMessage.error(res.msg || "复制失败");
}
} catch (error) {
if (error !== "cancel") {
ElMessage.error("复制失败");
console.error(error);
}
}
};
//
const handleDelete = async (row: UserTemplate) => {
try {
await ElMessageBox.confirm(
`确定要删除模板 "${row.templateName}" 吗?`,
"确认删除",
{ type: "warning" }
);
const res = await deleteUserTemplateApi(row.id!);
if (res.code === 200) {
ElMessage.success("删除成功");
loadTableData();
emits("refresh");
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (error) {
if (error !== "cancel") {
ElMessage.error("删除失败");
console.error(error);
}
}
};
//
const handleBatchDelete = async () => {
if (multipleSelection.value.length === 0) {
ElMessage.warning("请选择要删除的模板");
return;
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${multipleSelection.value.length} 个模板吗?`,
"确认删除",
{ type: "warning" }
);
const ids = multipleSelection.value.map(item => item.id!);
const res = await deleteUserTemplatesBatchApi(ids);
if (res.code === 200) {
ElMessage.success("删除成功");
multipleSelection.value = [];
loadTableData();
emits("refresh");
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (error) {
if (error !== "cancel") {
ElMessage.error("删除失败");
console.error(error);
}
}
};
//
const handleSave = async () => {
try {
await editFormRef.value?.validate();
//
const nameExists = await checkTemplateNameApi(
editForm.templateName,
editForm.id
);
if (nameExists.data) {
ElMessage.error("模板名称已存在");
return;
}
saveLoading.value = true;
const res = isEditMode.value
? await updateUserTemplateApi(editForm)
: await addUserTemplateApi(editForm);
if (res.code === 200) {
ElMessage.success(isEditMode.value ? "修改成功" : "新增成功");
activeTab.value = "list";
loadTableData();
emits("refresh");
} else {
ElMessage.error(res.msg || "保存失败");
}
} catch (error) {
ElMessage.error("保存失败");
console.error(error);
} finally {
saveLoading.value = false;
}
};
//
const handleValidate = async () => {
if (!editForm.templateContent.trim()) {
ElMessage.warning("请先输入模板内容");
return;
}
try {
const res = await validateTemplateApi(editForm.templateContent);
if (res.code === 200) {
ElMessage.success("模板语法验证通过");
} else {
ElMessage.error(`模板语法错误:${res.msg}`);
}
} catch (error) {
ElMessage.error("验证失败");
console.error(error);
}
};
//
const handleBack = () => {
activeTab.value = "list";
};
//
const handleClose = () => {
dialogVisible.value = false;
activeTab.value = "list";
};
// visible
watch(
() => props.visible,
newVal => {
if (newVal) {
activeTab.value = "list";
loadTableData();
}
}
);
</script>
<template>
<el-dialog
v-model="dialogVisible"
title="自定义模板管理"
width="90%"
:before-close="handleClose"
destroy-on-close
>
<div class="custom-template-dialog">
<!-- 标签页 -->
<el-tabs v-model="activeTab" type="card">
<!-- 模板列表 -->
<el-tab-pane label="模板列表" name="list">
<div class="template-list">
<!-- 搜索栏 -->
<div class="search-form mb-4">
<el-row :gutter="16">
<el-col :span="6">
<el-select
v-model="searchForm.templateGroup"
placeholder="选择模板分组"
clearable
filterable
>
<el-option
v-for="group in groupOptions"
:key="group.value"
:label="group.label"
:value="group.value"
/>
</el-select>
</el-col>
<el-col :span="6">
<el-select
v-model="searchForm.isPublic"
placeholder="选择公开状态"
clearable
>
<el-option label="私有" :value="0" />
<el-option label="公开" :value="1" />
</el-select>
</el-col>
<el-col :span="12">
<div class="search-actions">
<el-button type="primary" @click="handleSearch"
>搜索</el-button
>
<el-button @click="handleReset">重置</el-button>
<el-button type="success" @click="handleAdd"
>新增模板</el-button
>
<el-button
type="danger"
:disabled="multipleSelection.length === 0"
@click="handleBatchDelete"
>
批量删除
</el-button>
</div>
</el-col>
</el-row>
</div>
<!-- 表格 -->
<el-table
v-loading="loading"
:data="tableData"
@selection-change="handleSelectionChange"
border
stripe
>
<el-table-column type="selection" width="55" />
<el-table-column
prop="templateName"
label="模板名称"
min-width="150"
/>
<el-table-column
prop="templateGroup"
label="模板分组"
width="120"
/>
<el-table-column
prop="description"
label="描述"
min-width="200"
show-overflow-tooltip
/>
<el-table-column prop="isPublic" label="公开状态" width="100">
<template #default="{ row }">
<el-tag :type="row.isPublic === 1 ? 'success' : 'info'">
{{ row.isPublic === 1 ? "公开" : "私有" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="useCount" label="使用次数" width="100" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
>编辑</el-button
>
<el-button
type="success"
size="small"
@click="handleCopy(row)"
>复制</el-button
>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper mt-4">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</el-tab-pane>
<!-- 编辑模板 -->
<el-tab-pane :label="isEditMode ? '编辑模板' : '新增模板'" name="edit">
<div class="template-edit">
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="120px"
>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="模板名称" prop="templateName">
<el-input
v-model="editForm.templateName"
placeholder="请输入模板名称"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模板分组" prop="templateGroup">
<el-input
v-model="editForm.templateGroup"
placeholder="请输入模板分组"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="公开状态">
<el-radio-group v-model="editForm.isPublic">
<el-radio :label="0">私有</el-radio>
<el-radio :label="1">公开</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="版本号">
<el-input
v-model="editForm.version"
placeholder="请输入版本号"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="模板描述">
<el-input
v-model="editForm.description"
type="textarea"
:rows="3"
placeholder="请输入模板描述"
/>
</el-form-item>
<el-form-item label="模板内容" prop="templateContent">
<div class="template-content-editor">
<div class="editor-toolbar">
<el-button
type="primary"
size="small"
@click="handleValidate"
>
验证语法
</el-button>
<span class="ml-2 text-gray-500"
>支持 FreeMarker 模板语法</span
>
</div>
<el-input
v-model="editForm.templateContent"
type="textarea"
:rows="15"
placeholder="请输入模板内容,支持 FreeMarker 语法..."
class="mt-2"
/>
</div>
</el-form-item>
</el-form>
<div class="form-actions">
<el-button @click="handleBack">返回</el-button>
<el-button
type="primary"
:loading="saveLoading"
@click="handleSave"
>
{{ isEditMode ? "更新" : "保存" }}
</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.custom-template-dialog {
.search-form {
.search-actions {
display: flex;
gap: 8px;
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
}
.template-edit {
.template-content-editor {
width: 100%;
.editor-toolbar {
display: flex;
align-items: center;
margin-bottom: 8px;
}
}
.form-actions {
display: flex;
gap: 16px;
justify-content: center;
padding-top: 24px;
margin-top: 24px;
border-top: 1px solid var(--el-border-color-light);
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
:deep(.el-tabs__content) {
padding: 0;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
</style>

View File

@ -0,0 +1,392 @@
<script setup lang="ts">
import { ref, watch, onMounted, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from "@element-plus/icons-vue";
import {
getAvailableTemplatesApi,
getUserRepositoryListApi,
addTemplateToRepositoryApi,
removeTemplateFromRepositoryApi,
toggleTemplateStatusApi,
type TemplateRepository
} from "@/api/system/codegen";
interface Props {
visible: boolean;
}
interface Emits {
(e: "update:visible", visible: boolean): void;
(e: "refresh"): void;
}
const props = defineProps<Props>();
const emits = defineEmits<Emits>();
//
const loading = ref(false);
const availableTemplates = ref<TemplateRepository[]>([]);
const userTemplates = ref<TemplateRepository[]>([]);
const searchKeyword = ref("");
//
const dialogVisible = computed({
get: () => props.visible,
set: val => emits("update:visible", val)
});
//
const filteredAvailableTemplates = computed(() => {
if (!searchKeyword.value) return availableTemplates.value;
return availableTemplates.value.filter(
template =>
template.templateName
.toLowerCase()
.includes(searchKeyword.value.toLowerCase()) ||
template.templateGroup
.toLowerCase()
.includes(searchKeyword.value.toLowerCase())
);
});
//
const groupedTemplates = computed(() => {
const groups: Record<string, TemplateRepository[]> = {};
filteredAvailableTemplates.value.forEach(template => {
if (!groups[template.templateGroup]) {
groups[template.templateGroup] = [];
}
groups[template.templateGroup].push(template);
});
return groups;
});
//
const loadData = async () => {
loading.value = true;
try {
const [availableRes, userRes] = await Promise.all([
getAvailableTemplatesApi(),
getUserRepositoryListApi()
]);
if (availableRes.code === 200) {
availableTemplates.value = availableRes.data;
}
if (userRes.code === 200) {
userTemplates.value = userRes.data;
}
} catch (error) {
ElMessage.error("加载模板数据失败");
console.error(error);
} finally {
loading.value = false;
}
};
//
const addTemplate = async (template: TemplateRepository) => {
try {
const res = await addTemplateToRepositoryApi(template);
if (res.code === 200) {
ElMessage.success("添加成功");
template.isEnabled = 1;
await loadData();
emits("refresh");
} else {
ElMessage.error(res.msg || "添加失败");
}
} catch (error) {
ElMessage.error("添加失败");
console.error(error);
}
};
//
const removeTemplate = async (template: TemplateRepository) => {
try {
await ElMessageBox.confirm(
`确定要从仓库中移除模板 "${template.templateName}" 吗?`,
"确认移除",
{ type: "warning" }
);
const res = await removeTemplateFromRepositoryApi(
template.templateSource,
template.templateId
);
if (res.code === 200) {
ElMessage.success("移除成功");
template.isEnabled = 0;
await loadData();
emits("refresh");
} else {
ElMessage.error(res.msg || "移除失败");
}
} catch (error) {
if (error !== "cancel") {
ElMessage.error("移除失败");
console.error(error);
}
}
};
//
const toggleTemplate = async (template: TemplateRepository) => {
const newStatus = template.isEnabled === 1 ? 0 : 1;
try {
const res = await toggleTemplateStatusApi(
template.templateSource,
template.templateId,
newStatus
);
if (res.code === 200) {
template.isEnabled = newStatus;
ElMessage.success(newStatus === 1 ? "已启用" : "已禁用");
emits("refresh");
} else {
ElMessage.error(res.msg || "操作失败");
}
} catch (error) {
ElMessage.error("操作失败");
console.error(error);
}
};
//
const handleClose = () => {
dialogVisible.value = false;
};
//
onMounted(() => {
if (props.visible) {
loadData();
}
});
// visible
watch(
() => props.visible,
newVal => {
if (newVal) {
loadData();
}
}
);
</script>
<template>
<el-dialog
v-model="dialogVisible"
title="模板仓库管理"
width="80%"
:before-close="handleClose"
destroy-on-close
>
<div class="repository-dialog">
<!-- 搜索栏 -->
<div class="search-bar mb-4">
<el-input
v-model="searchKeyword"
placeholder="搜索模板名称或分组"
clearable
class="w-64"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<!-- 加载状态 -->
<div v-loading="loading" class="min-h-64">
<!-- 模板列表 -->
<div
v-if="!loading && Object.keys(groupedTemplates).length > 0"
class="template-groups"
>
<div
v-for="(templates, groupName) in groupedTemplates"
:key="groupName"
class="template-group mb-6"
>
<div class="group-header mb-3">
<el-tag type="primary" size="large" effect="dark">
{{ groupName }}
</el-tag>
<span class="ml-2 text-gray-500"
>{{ templates.length }} 个模板</span
>
</div>
<div class="template-grid">
<div
v-for="template in templates"
:key="`${template.templateSource}-${template.templateId}`"
class="template-item"
:class="{ enabled: template.isEnabled === 1 }"
>
<div class="template-content">
<div class="template-header">
<h4 class="template-name">{{ template.templateName }}</h4>
<div class="template-badges">
<el-tag
:type="
template.templateSource === 'system'
? 'success'
: 'info'
"
size="small"
>
{{
template.templateSource === "system" ? "系统" : "用户"
}}
</el-tag>
<el-tag
v-if="template.isEnabled === 1"
type="primary"
size="small"
>
已启用
</el-tag>
</div>
</div>
<div class="template-actions">
<el-button
v-if="template.isEnabled === 0"
type="primary"
size="small"
@click="addTemplate(template)"
>
添加到仓库
</el-button>
<template v-else>
<el-button
type="warning"
size="small"
@click="toggleTemplate(template)"
>
{{ template.isEnabled === 1 ? "禁用" : "启用" }}
</el-button>
<el-button
type="danger"
size="small"
@click="removeTemplate(template)"
>
移除
</el-button>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="!loading" class="empty-state">
<el-empty description="暂无可用模板" />
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" @click="loadData">刷新</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.repository-dialog {
.search-bar {
display: flex;
justify-content: flex-start;
}
.template-groups {
.template-group {
.group-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
.template-item {
padding: 16px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
border-color: var(--el-color-primary-light-5);
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
&.enabled {
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-7);
}
.template-content {
.template-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
.template-name {
flex: 1;
margin: 0;
margin-right: 12px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.template-badges {
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-end;
}
}
.template-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
}
}
}
}
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
}
.dialog-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
</style>

View File

@ -5,6 +5,8 @@ import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import TemplateSelector from "./components/TemplateSelector.vue";
import ConfigForm from "./components/ConfigForm.vue";
import CodeEditor from "./components/CodeEditor.vue";
import TemplateRepositoryDialog from "./components/TemplateRepositoryDialog.vue";
import CustomTemplateDialog from "./components/CustomTemplateDialog.vue";
import Play from "@iconify-icons/ep/caret-right";
import CopyDocument from "@iconify-icons/ep/document-copy";
@ -45,6 +47,20 @@ const sqlCollapsed = ref(false);
const configCollapsed = ref(false);
const templateCollapsed = ref(false);
//
const templateRepositoryVisible = ref(false);
const customTemplateVisible = ref(false);
//
const openTemplateRepository = () => {
templateRepositoryVisible.value = true;
};
//
const openCustomTemplate = () => {
customTemplateVisible.value = true;
};
onMounted(() => {
loadAllTemplates();
});
@ -177,19 +193,37 @@ onMounted(() => {
<!-- 模板选择区域 -->
<el-card class="mb-4 collapsible-card" shadow="hover">
<template #header>
<div
class="collapsible-header"
@click="templateCollapsed = !templateCollapsed"
>
<h3 class="text-lg font-semibold">
模板选择
<el-icon
class="collapse-arrow"
:class="{ collapsed: templateCollapsed }"
<div class="flex justify-between items-center">
<div
class="collapsible-header"
@click="templateCollapsed = !templateCollapsed"
>
<h3 class="text-lg font-semibold">
模板选择
<el-icon
class="collapse-arrow"
:class="{ collapsed: templateCollapsed }"
>
<component :is="useRenderIcon(ArrowDown)" />
</el-icon>
</h3>
</div>
<div class="flex gap-2">
<el-button
type="default"
:icon="useRenderIcon(FolderOpened)"
@click="openTemplateRepository"
>
<component :is="useRenderIcon(ArrowDown)" />
</el-icon>
</h3>
模板仓库
</el-button>
<el-button
type="primary"
:icon="useRenderIcon(CopyDocument)"
@click="openCustomTemplate"
>
自定义模板
</el-button>
</div>
</div>
</template>
@ -248,6 +282,18 @@ onMounted(() => {
/>
</el-card>
</div>
<!-- 模板仓库弹窗 -->
<TemplateRepositoryDialog
v-model:visible="templateRepositoryVisible"
@refresh="loadAllTemplates"
/>
<!-- 自定义模板弹窗 -->
<CustomTemplateDialog
v-model:visible="customTemplateVisible"
@refresh="loadAllTemplates"
/>
</div>
</template>