Files
FastapiAdmin/frontend/src/views/module_generator/gencode/components/CreateTableDialog.vue
T
zhangtao d2a863652e feat(docs): enhance README and backend documentation for new user onboarding and backend conventions
- Added a "Start Here" section in both English and Chinese README files to guide new users on running the project locally and exploring its features.
- Introduced backend conventions for date and serialization handling, clarifying the use of Pydantic v2 and PostgreSQL.
- Updated the "Quick Start" section with detailed steps for first-time local setup, including environment requirements and backend setup instructions.
- Improved overall structure and clarity of documentation to facilitate better understanding for developers and contributors.
2026-03-29 07:25:09 +08:00

353 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<EnhancedDialog
v-model="visible"
title="创建表"
width="min(920px, 96vw)"
append-to-body
@opened="onDialogOpened"
>
<div class="create-table-toolbar mb-3 flex flex-wrap items-center gap-2">
<span class="text-sm text-[var(--el-text-color-regular)] shrink-0">编辑方式</span>
<el-radio-group v-model="editMode" size="small">
<el-radio-button value="sql">SQL 语句</el-radio-button>
<el-radio-button value="visual">表结构</el-radio-button>
</el-radio-group>
<el-divider direction="vertical" class="!mx-1" />
<span class="text-xs text-[var(--el-text-color-secondary)]">
表结构模式通过表单生成 DDLSQL 模式可直接粘贴支持多条语句
</span>
</div>
<div v-show="editMode === 'sql'" class="sql-pane">
<div class="mb-2 flex flex-wrap items-center gap-2">
<span class="text-sm shrink-0">快速示例</span>
<el-button type="primary" size="small" @click="loadPresetSql('single', 'mysql')">
单表 · MySQL
</el-button>
<el-button type="primary" size="small" @click="loadPresetSql('single', 'postgres')">
单表 · PostgreSQL
</el-button>
<el-button type="success" size="small" @click="loadPresetSql('masterSub', 'mysql')">
主子表 · MySQL
</el-button>
<el-button type="success" size="small" @click="loadPresetSql('masterSub', 'postgres')">
主子表 · PostgreSQL
</el-button>
<el-dropdown trigger="click">
<el-button size="small" type="warning">
旧版示例含用户外键
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="sqlText = LEGACY_MYSQL_WITH_USER_FK">
MySQL sys_user
</el-dropdown-item>
<el-dropdown-item @click="sqlText = LEGACY_POSTGRES_WITH_USER_FK">
PostgreSQL sys_user
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-scrollbar max-height="72vh">
<div class="absolute z-36 right-5 top-2">
<el-link type="primary" @click="copySql">
<el-icon><CopyDocument /></el-icon>
复制
</el-link>
</div>
<Codemirror
ref="sqlRef"
v-model:value="sqlText"
:options="sqlOptions"
border
height="400px"
width="100%"
/>
</el-scrollbar>
</div>
<div v-show="editMode === 'visual'" class="visual-pane">
<el-form label-width="120px" class="max-w-3xl">
<el-form-item label="数据库方言">
<el-radio-group v-model="visual.dialect" size="small">
<el-radio-button value="mysql">MySQL</el-radio-button>
<el-radio-button value="postgres">PostgreSQL</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="模板">
<el-button @click="applyVisualPreset('single')">单表模板</el-button>
<el-button type="primary" @click="applyVisualPreset('masterSub')">主子表模板</el-button>
</el-form-item>
<el-divider content-position="left">主表</el-divider>
<el-form-item label="表名">
<el-input
v-model="visual.mainTableName"
placeholder="如 gen_demo_order_master"
clearable
/>
</el-form-item>
<el-form-item label="表注释">
<el-input v-model="visual.mainComment" placeholder="表注释" clearable />
</el-form-item>
<el-form-item label="启用子表">
<el-switch v-model="visual.subEnabled" @change="onSubToggle" />
</el-form-item>
<template v-if="visual.subEnabled">
<el-divider content-position="left">子表</el-divider>
<el-form-item label="表名">
<el-input
v-model="visual.subTableName"
placeholder="如 gen_demo_order_item"
clearable
/>
</el-form-item>
<el-form-item label="表注释">
<el-input v-model="visual.subComment" placeholder="表注释" clearable />
</el-form-item>
<el-form-item label="外键列名">
<el-input
v-model="visual.fkColumn"
placeholder="子表上指向主表的列,如 order_id"
clearable
/>
</el-form-item>
<el-form-item label="引用主表列">
<el-input v-model="visual.fkRefColumn" placeholder="一般为 id" clearable />
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="syncVisualToSql">生成 SQL 并切到 SQL 模式</el-button>
<el-button @click="previewSqlInPlace">仅预览 SQL下方</el-button>
</el-form-item>
</el-form>
<el-input
v-model="visualPreviewSql"
type="textarea"
:rows="12"
readonly
class="font-mono text-xs"
placeholder="点击「仅预览 SQL」或切换模式时生成"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="loading" @click="handleConfirm"> </el-button>
<el-button @click="handleCancel"> </el-button>
</div>
</template>
</EnhancedDialog>
</template>
<script setup lang="ts">
import "codemirror/mode/sql/sql.js";
import "codemirror/theme/dracula.css";
import { ref, watch, nextTick } from "vue";
import Codemirror from "codemirror-editor-vue3";
import type { EditorConfiguration } from "codemirror";
import type { CmComponentRef } from "codemirror-editor-vue3";
import { ElMessage } from "element-plus";
import { ArrowDown, CopyDocument } from "@element-plus/icons-vue";
import { useClipboard } from "@vueuse/core";
import EnhancedDialog from "@/components/CURD/EnhancedDialog.vue";
import { useSettingsStore } from "@/store";
import { ThemeMode } from "@/enums/settings/theme.enum";
import { buildSqlFromVisual } from "../utils/buildCreateTableSql";
import type { SqlDialect, VisualBuildState } from "../utils/buildCreateTableSql";
import {
applySubColumns,
mergeGenTableLinkIntoVisual,
visualPresetMasterSub,
visualPresetSingle,
type GenTableCreateLink,
} from "../utils/createTableVisualPresets";
import {
getExampleFromPresetMasterSub,
getExampleFromPresetSingle,
LEGACY_MYSQL_WITH_USER_FK,
LEGACY_POSTGRES_WITH_USER_FK,
} from "../utils/createTableSqlExamples";
defineOptions({ name: "CreateTableDialog" });
const visible = defineModel<boolean>({ default: false });
export interface CreateTableSubmitMeta {
fromVisual: boolean;
visualSnapshot?: VisualBuildState;
}
const props = withDefaults(
defineProps<{
loading?: boolean;
/** 代码生成抽屉第三步打开时传入,用于预填「表结构」主/子表名 */
linkFromGen?: GenTableCreateLink | null;
}>(),
{ loading: false, linkFromGen: null }
);
const emit = defineEmits<{
submit: [sql: string, meta?: CreateTableSubmitMeta];
}>();
const { copy } = useClipboard();
const settingsStore = useSettingsStore();
const editMode = ref<"sql" | "visual">("sql");
const sqlText = ref("");
const sqlRef = ref<CmComponentRef>();
const visual = ref<VisualBuildState>(visualPresetSingle("mysql"));
const visualPreviewSql = ref("");
const codeTheme = ref(settingsStore.theme === ThemeMode.DARK ? "dracula" : "default");
const sqlOptions: EditorConfiguration = {
mode: "text/x-sql",
lineNumbers: true,
smartIndent: true,
indentUnit: 2,
tabSize: 2,
readOnly: false,
theme: codeTheme.value,
lineWrapping: true,
autofocus: false,
};
watch(
() => settingsStore.theme,
(t) => {
const newTheme = t === ThemeMode.DARK ? "dracula" : "default";
codeTheme.value = newTheme;
sqlOptions.theme = newTheme;
if (sqlRef.value?.cminstance) {
sqlRef.value.cminstance.setOption("theme", newTheme);
}
}
);
function onDialogOpened() {
dialectWatchSkip = true;
editMode.value = "sql";
sqlText.value = "";
visual.value = visualPresetSingle("mysql");
visualPreviewSql.value = "";
void nextTick(() => {
dialectWatchSkip = false;
applyLinkFromGenIfAny();
});
}
/** 第三步已填主表/子表时:切到表结构模式并带入名称,减少重复输入 */
function applyLinkFromGenIfAny() {
const link = props.linkFromGen;
if (!link) return;
const touched =
(link.table_name || "").trim() ||
(link.table_comment || "").trim() ||
((link.sub_table_name || "").trim() && (link.sub_table_fk_name || "").trim());
if (!touched) return;
visual.value = mergeGenTableLinkIntoVisual(link, visual.value.dialect);
editMode.value = "visual";
visualPreviewSql.value = buildSqlFromVisual(applySubColumns(visual.value));
ElMessage.info({
message: "已从代码生成第三步带入主表/子表名,可在表结构模式中修改后执行创建",
duration: 3200,
});
}
watch(editMode, (m) => {
if (m === "sql") {
sqlText.value = buildSqlFromVisual(applySubColumns(visual.value));
void nextTick(() => sqlRef.value?.cminstance?.refresh());
} else {
visualPreviewSql.value = buildSqlFromVisual(applySubColumns(visual.value));
}
});
let dialectWatchSkip = false;
watch(
() => visual.value.dialect,
(_d, prev) => {
if (dialectWatchSkip) return;
if (prev === undefined) return;
const d = visual.value.dialect;
visual.value = visual.value.subEnabled
? applySubColumns(visualPresetMasterSub(d))
: visualPresetSingle(d);
visualPreviewSql.value = buildSqlFromVisual(applySubColumns(visual.value));
}
);
function onSubToggle() {
visual.value = applySubColumns(visual.value);
visualPreviewSql.value = buildSqlFromVisual(applySubColumns(visual.value));
}
function applyVisualPreset(kind: "single" | "masterSub") {
const d = visual.value.dialect;
visual.value =
kind === "single" ? visualPresetSingle(d) : applySubColumns(visualPresetMasterSub(d));
visualPreviewSql.value = buildSqlFromVisual(applySubColumns(visual.value));
}
function syncVisualToSql() {
sqlText.value = buildSqlFromVisual(applySubColumns(visual.value));
editMode.value = "sql";
void nextTick(() => {
sqlRef.value?.cminstance?.refresh();
});
ElMessage.success("已生成 SQL,可在 SQL 模式中继续编辑");
}
function previewSqlInPlace() {
visualPreviewSql.value = buildSqlFromVisual(applySubColumns(visual.value));
}
function loadPresetSql(kind: "single" | "masterSub", dialect: SqlDialect) {
sqlText.value =
kind === "single"
? getExampleFromPresetSingle(dialect)
: getExampleFromPresetMasterSub(dialect);
}
function copySql() {
if (!sqlText.value) {
ElMessage.warning("没有可复制的内容");
return;
}
copy(sqlText.value);
ElMessage.success("已复制");
}
function handleConfirm() {
const sql =
editMode.value === "sql"
? sqlText.value.trim()
: buildSqlFromVisual(applySubColumns(visual.value)).trim();
if (!sql) {
ElMessage.error("请填写 SQL,或在表结构中选择模板并生成");
return;
}
if (editMode.value === "visual") {
emit("submit", sql, {
fromVisual: true,
visualSnapshot: applySubColumns(visual.value),
});
} else {
emit("submit", sql);
}
}
function handleCancel() {
visible.value = false;
}
</script>
<style scoped lang="scss">
.visual-pane :deep(.el-textarea__inner) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
</style>