Commit d57928c4 authored by wangteng's avatar wangteng

代码提交

parent 6d58b2e9
...@@ -5,3 +5,6 @@ ...@@ -5,3 +5,6 @@
# 总体要求 # 总体要求
- 每做一个任务就在工作记录中记录一次 - 每做一个任务就在工作记录中记录一次
# 服务器发版本
- deploy_server.py
...@@ -308,53 +308,12 @@ def list_operation_records( ...@@ -308,53 +308,12 @@ def list_operation_records(
operation: Optional[Literal["backup", "restore", "clear", "encrypt", "decrypt"]] = None, operation: Optional[Literal["backup", "restore", "clear", "encrypt", "decrypt"]] = None,
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ):
if operation in ("backup", "clear"):
_import_file_backed_history(db, operation)
query = db.query(OperationRecord) query = db.query(OperationRecord)
if operation: if operation:
query = query.filter(OperationRecord.operation == operation) query = query.filter(OperationRecord.operation == operation)
return [row.to_dict() for row in query.order_by(OperationRecord.created_at.desc(), OperationRecord.id.desc()).all()] return [row.to_dict() for row in query.order_by(OperationRecord.created_at.desc(), OperationRecord.id.desc()).all()]
def _import_file_backed_history(db: Session, operation: str) -> None:
"""将旧版已存在的备份文件补登记到历史表,仅首次扫描未登记文件。"""
known = {
row[0]
for row in db.query(OperationRecord.filename)
.filter(OperationRecord.operation == operation, OperationRecord.filename != "")
.all()
}
added = 0
for path in get_backup_dir().glob("*.json"):
if path.name in known:
continue
try:
package = json.loads(path.read_text(encoding="utf-8"))
file_operation = package.get("operation") or "backup"
if file_operation != operation:
continue
source = package.get("source") or {}
source_id = source.get("id")
if not isinstance(source_id, int) or not db.get(DataSource, source_id):
source_id = None
db.add(OperationRecord(
operation=operation,
source_id=source_id,
source_name=str(source.get("name") or ""),
database_name=str(source.get("database") or source.get("schema") or ""),
scope=str(source.get("scope") or ""),
tables_json=json.dumps([item.get("table_name") for item in package.get("tables", []) if item.get("table_name")], ensure_ascii=False),
filename=path.name,
execute_setting="由已有备份文件补登记",
created_at=datetime.fromtimestamp(path.stat().st_mtime),
))
added += 1
except (OSError, ValueError, TypeError, json.JSONDecodeError):
continue
if added:
db.commit()
@router.post("/records/import") @router.post("/records/import")
def import_legacy_operation_records( def import_legacy_operation_records(
operation: Literal["backup", "restore", "clear", "encrypt", "decrypt"], operation: Literal["backup", "restore", "clear", "encrypt", "decrypt"],
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
<div class="clear-page"> <div class="clear-page">
<div class="config-toolbar clear-toolbar"> <div class="config-toolbar clear-toolbar">
<div class="left"> <div class="left">
<input v-model="keyword" class="text-input clear-search" placeholder="备份文件名/数据库/数据表" @keyup.enter="loadVersions" /> <input v-model="keyword" class="text-input clear-search" placeholder="备份文件名/数据库/数据表" @keyup.enter="onQuery" />
<button class="btn btn-primary" @click="loadVersions">查询</button> <button class="btn btn-primary" @click="onQuery">查询</button><button class="btn" @click="onReset">重置</button>
</div> </div>
<div class="right"><button class="btn btn-danger" @click="openCreate">清空数据表</button></div> <div class="right"><button class="btn btn-danger" @click="openCreate">清空数据表</button></div>
</div> </div>
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
<div class="q-card"> <div class="q-card">
<div class="table-scroll"> <div class="table-scroll">
<table class="config-list clear-list"> <table class="config-list clear-list">
<thead><tr><th>备份文件</th><th>数据源</th><th>数据库 / Schema</th><th>已清空数据表</th><th>备份时间</th><th>操作</th></tr></thead> <thead><tr><th>备份文件</th><th>数据源</th><th>数据库</th><th>已清空数据表</th><th>备份时间</th><th>操作</th></tr></thead>
<tbody> <tbody>
<tr v-if="!records.length"><td colspan="6" class="empty-cell">暂无清表记录</td></tr> <tr v-if="!recordsLoading && !pagedRecords.length"><td colspan="6" class="empty-cell">暂无清表记录</td></tr>
<tr v-for="record in records" :key="record.id"> <tr v-for="record in pagedRecords" :key="record.id">
<td class="filename">{{ record.filename }}</td><td>{{ record.source?.name || '—' }}</td> <td class="filename">{{ record.filename }}</td><td>{{ record.source?.name || '—' }}</td>
<td>{{ record.source?.database || record.source?.schema || '—' }}</td> <td>{{ record.source?.database || record.source?.schema || '—' }}</td>
<td class="tables">{{ (record.tables || []).join('、') || '—' }}</td> <td class="tables">{{ (record.tables || []).join('、') || '—' }}</td>
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div><div class="pagination-wrap" v-if="filteredRecords.length"><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 15, 20, 30]" :total="filteredRecords.length" layout="total, sizes, prev, pager, next" background small /></div>
</div> </div>
</div> </div>
...@@ -39,15 +39,20 @@ ...@@ -39,15 +39,20 @@
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { listDataSources } from '@/api/dataSources' import { listDataSources } from '@/api/dataSources'
import { listSchemas, listTables } from '@/api/connections' import { listSchemas, listTables } from '@/api/connections'
import { clearTables, listOperationRecords, restoreClearedTables } from '@/api/backup' import { clearTables, listOperationRecords, restoreClearedTables } from '@/api/backup'
const sources = ref([]), schemas = ref([]), tables = ref([]), records = ref([]) const sources = ref([]), schemas = ref([]), tables = ref([]), records = ref([])
const keyword = ref(''), createOpen = ref(false), loading = ref(false), clearing = ref(false), restoring = ref('') const keyword = ref(''), appliedKeyword = ref(''), currentPage = ref(1), pageSize = ref(10), createOpen = ref(false), loading = ref(false), recordsLoading = ref(true), clearing = ref(false), restoring = ref('')
const form = reactive({ source: null, database: '', scope: 'database', tables: [] }) const form = reactive({ source: null, database: '', scope: 'database', tables: [] })
const filteredRecords = computed(() => {
const q = appliedKeyword.value.trim().toLowerCase()
return q ? records.value.filter(item => [item.filename, item.source_name, item.database, ...(item.tables || [])].join(' ').toLowerCase().includes(q)) : records.value
})
const pagedRecords = computed(() => filteredRecords.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value))
function sourceForm(source, database = '') { function sourceForm(source, database = '') {
return { dbType: source.db_type, host: source.host, port: source.port, user: source.user, password: source.password || '', db: database || source.database || '', schema: database || source.schema || source.database || '' } return { dbType: source.db_type, host: source.host, port: source.port, user: source.user, password: source.password || '', db: database || source.database || '', schema: database || source.schema || source.database || '' }
...@@ -56,13 +61,12 @@ function formatDate(value) { return value ? new Date(value).toLocaleString() : ' ...@@ -56,13 +61,12 @@ function formatDate(value) { return value ? new Date(value).toLocaleString() : '
function resetForm() { Object.assign(form, { source: null, database: '', scope: 'database', tables: [] }); schemas.value = []; tables.value = [] } function resetForm() { Object.assign(form, { source: null, database: '', scope: 'database', tables: [] }); schemas.value = []; tables.value = [] }
function openCreate() { resetForm(); createOpen.value = true } function openCreate() { resetForm(); createOpen.value = true }
async function loadVersions() { async function loadVersions() {
recordsLoading.value = true
try { try {
const versions = await listOperationRecords('clear') const versions = await listOperationRecords('clear')
const q = keyword.value.trim().toLowerCase() records.value = versions.map(item => ({ ...item, source: { name: item.source_name, database: item.database } }))
records.value = versions currentPage.value = 1
.filter(item => !q || [item.filename, item.source_name, item.database, ...(item.tables || [])].join(' ').toLowerCase().includes(q)) } catch (error) { ElMessage.error(`加载清表记录失败:${error.message}`) } finally { recordsLoading.value = false }
.map(item => ({ ...item, source: { name: item.source_name, database: item.database } }))
} catch (error) { ElMessage.error(`加载清表记录失败:${error.message}`) }
} }
async function onSourceChange() { async function onSourceChange() {
form.database = ''; form.tables = []; schemas.value = []; tables.value = [] form.database = ''; form.tables = []; schemas.value = []; tables.value = []
...@@ -97,7 +101,13 @@ async function restore(record) { ...@@ -97,7 +101,13 @@ async function restore(record) {
restoring.value = record.filename restoring.value = record.filename
try { const result = await restoreClearedTables({ filename: record.filename, source_id: source.id, database, schema: database }); ElMessage.success(result.message) } catch (error) { ElMessage.error(error.message) } finally { restoring.value = '' } try { const result = await restoreClearedTables({ filename: record.filename, source_id: source.id, database, schema: database }); ElMessage.success(result.message) } catch (error) { ElMessage.error(error.message) } finally { restoring.value = '' }
} }
onMounted(async () => { try { sources.value = await listDataSources() } catch (_) { sources.value = [] } await loadVersions() }) function onQuery() { appliedKeyword.value = keyword.value; currentPage.value = 1 }
function onReset() { keyword.value = ''; appliedKeyword.value = ''; currentPage.value = 1 }
watch([filteredRecords, () => Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value))], () => {
const pageCount = Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value))
if (currentPage.value > pageCount) currentPage.value = pageCount
})
onMounted(async () => { await loadVersions(); try { sources.value = await listDataSources() } catch (_) { sources.value = [] } })
</script> </script>
<style scoped> <style scoped>
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div class="right"><button class="btn btn-success" @click="openAdd">+ 新增数据源</button></div> <div class="right"><button class="btn btn-success" @click="openAdd">+ 新增数据源</button></div>
</div> </div>
<div class="q-card"> <div class="q-card">
<div class="table-scroll"><table class="config-list"><thead><tr><th>名称</th><th>类型</th><th>地址</th><th>数据库 / Schema</th><th>状态</th><th>最近测试</th><th>操作</th></tr></thead> <div class="table-scroll"><table class="config-list"><thead><tr><th>名称</th><th>类型</th><th>地址</th><th>数据库</th><th>状态</th><th>最近测试</th><th>操作</th></tr></thead>
<tbody><tr v-if="!loading && !pagedRows.length"><td colspan="7" class="empty-cell">{{ filteredRows.length ? '当前页暂无数据' : '暂无数据源,请先新增' }}</td></tr> <tbody><tr v-if="!loading && !pagedRows.length"><td colspan="7" class="empty-cell">{{ filteredRows.length ? '当前页暂无数据' : '暂无数据源,请先新增' }}</td></tr>
<tr v-for="row in pagedRows" :key="row.id"><td><b>{{ row.name }}</b></td><td>{{ row.db_type }}</td><td>{{ row.host }}:{{ row.port }}</td><td>{{ row.database || '—' }}<span v-if="row.schema"> / {{ row.schema }}</span></td><td><span class="tag" :class="row.status === '启用' ? 'tag-green' : 'tag-gray'">{{ row.status }}</span></td><td><span v-if="row.last_tested_at" :class="row.last_test_ok ? 'ok-text' : 'error-text'">{{ row.last_test_ok ? '成功' : '失败' }}</span><span v-else>—</span></td><td><button class="btn-text" @click="onTest(row)">测试</button><button class="btn-text" @click="openEdit(row)">编辑</button><button class="btn-text danger" @click="onDelete(row)">删除</button></td></tr></tbody> <tr v-for="row in pagedRows" :key="row.id"><td><b>{{ row.name }}</b></td><td>{{ row.db_type }}</td><td>{{ row.host }}:{{ row.port }}</td><td>{{ row.database || '—' }}<span v-if="row.schema"> / {{ row.schema }}</span></td><td><span class="tag" :class="row.status === '启用' ? 'tag-green' : 'tag-gray'">{{ row.status }}</span></td><td><span v-if="row.last_tested_at" :class="row.last_test_ok ? 'ok-text' : 'error-text'">{{ row.last_test_ok ? '成功' : '失败' }}</span><span v-else>—</span></td><td><button class="btn-text" @click="onTest(row)">测试</button><button class="btn-text" @click="openEdit(row)">编辑</button><button class="btn-text danger" @click="onDelete(row)">删除</button></td></tr></tbody>
</table></div> </table></div>
......
<template> <template>
<div class="design-page"> <div class="design-page">
<div class="config-toolbar"><div class="left"><input v-model="keyword" class="text-input design-search" placeholder="数据源/数据库/表名" @keyup.enter="loadConfigs" /><button class="btn btn-primary" @click="loadConfigs">查询</button><button class="btn" @click="keyword = ''">重置</button></div><div class="right"><button class="btn btn-success" @click="openAdd">+ 新增校验</button></div></div> <div class="config-toolbar"><div class="left"><input v-model="keyword" class="text-input design-search" placeholder="数据源/数据库/表名" @keyup.enter="onQuery" /><button class="btn btn-primary" @click="onQuery">查询</button><button class="btn" @click="onReset">重置</button></div><div class="right"><button class="btn btn-success" @click="openAdd">+ 新增校验</button></div></div>
<div class="q-card"><div class="table-scroll"><table class="config-list"><thead><tr><th>校验名称</th><th>数据源</th><th>数据库 / Schema</th><th>校验范围</th><th>校验规则</th><th>最近扫描</th><th>操作</th></tr></thead><tbody><tr v-if="pagedConfigs.length === 0"><td colspan="7" class="empty-cell">暂无表设计校验配置,点击右上角新增</td></tr><tr v-for="item in pagedConfigs" :key="item.id"><td>{{ item.name }}</td><td>{{ sourceName(item.sourceId) }}</td><td>{{ item.schema || '—' }}</td><td>{{ item.scope === 'database' ? '整个库' : `指定 ${item.tables.length} 张表` }}</td><td>{{ ruleText(item) }}</td><td>{{ item.checkedAt || '—' }}</td><td><button class="btn-text" @click="scan(item)">扫描</button><button class="btn-text" @click="openEdit(item)">编辑</button><button class="btn-text danger" @click="remove(item)">删除</button></td></tr></tbody></table></div><div class="pagination-wrap" v-if="filteredConfigs.length"><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 15, 20, 30]" :total="filteredConfigs.length" layout="total, sizes, prev, pager, next" background small /></div></div> <div class="q-card"><div class="table-scroll"><table class="config-list"><thead><tr><th>校验名称</th><th>数据源</th><th>数据库</th><th>校验范围</th><th>校验规则</th><th>最近扫描</th><th>操作</th></tr></thead><tbody><tr v-if="configsLoading"><td colspan="7" class="empty-cell">加载中...</td></tr><tr v-else-if="pagedConfigs.length === 0"><td colspan="7" class="empty-cell">暂无表设计校验配置,点击右上角新增</td></tr><tr v-for="item in pagedConfigs" :key="item.id"><td>{{ item.name }}</td><td>{{ sourceName(item.sourceId) }}</td><td>{{ item.schema || '—' }}</td><td>{{ item.scope === 'database' ? '整个库' : `指定 ${item.tables.length} 张表` }}</td><td>{{ ruleText(item) }}</td><td>{{ formatCheckedAt(item.checkedAt) }}</td><td><button class="btn-text" @click="scan(item)">扫描</button><button class="btn-text" @click="openEdit(item)">编辑</button><button class="btn-text danger" @click="remove(item)">删除</button></td></tr></tbody></table></div><div class="pagination-wrap" v-if="filteredConfigs.length"><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 15, 20, 30]" :total="filteredConfigs.length" layout="total, sizes, prev, pager, next" background small /></div></div>
<Teleport to="body"><div class="modal-mask" :class="{ show: dialogOpen }" @click="closeEditor"><div class="modal design-modal" @click.stop><div class="modal-header"><span class="modal-title">{{ editingId ? '编辑表设计校验' : '新增表设计校验' }}</span><button class="modal-close" aria-label="关闭" @click="closeEditor">×</button></div><div class="modal-body design-modal__body"> <Teleport to="body"><div class="modal-mask" :class="{ show: dialogOpen }" @click="closeEditor"><div class="modal design-modal" @click.stop><div class="modal-header"><span class="modal-title">{{ editingId ? '编辑表设计校验' : '新增表设计校验' }}</span><button class="modal-close" aria-label="关闭" @click="closeEditor">×</button></div><div class="modal-body design-modal__body">
<div class="step"><span class="step-no">1</span><div class="step-content"><b>校验名称</b><input v-model="form.name" class="text-input design-name-input" placeholder="如:生产库表注释完整性" /></div></div> <div class="step"><span class="step-no">1</span><div class="step-content"><b>校验名称</b><input v-model="form.name" class="text-input design-name-input" placeholder="如:生产库表注释完整性" /></div></div>
<div class="step"><span class="step-no">2</span><div class="step-content"><b>选择数据源</b><el-select v-model="form.sourceId" filterable placeholder="请选择数据源" class="source-select" @change="loadObjects"><el-option v-for="source in sources" :key="source.id" :label="source.name" :value="source.id" /></el-select></div></div> <div class="step"><span class="step-no">2</span><div class="step-content"><b>选择数据源</b><el-select v-model="form.sourceId" filterable placeholder="请选择数据源" class="source-select" @change="loadObjects"><el-option v-for="source in sources" :key="source.id" :label="source.name" :value="source.id" /></el-select></div></div>
...@@ -11,16 +11,16 @@ ...@@ -11,16 +11,16 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { listDataSources } from '@/api/dataSources' import { listDataSources } from '@/api/dataSources'
import { createTableDesignConfig, deleteTableDesignConfig, getTableDesignObjects, checkTableDesign, importTableDesignConfigs, listTableDesignConfigs, updateTableDesignConfig } from '@/api/tableDesign' import { createTableDesignConfig, deleteTableDesignConfig, getTableDesignObjects, checkTableDesign, listTableDesignConfigs, updateTableDesignConfig } from '@/api/tableDesign'
const router = useRouter(); const sources = ref([]); const schemas = ref([]); const tables = ref([]); const configs = ref([]); const keyword = ref(''); const currentPage = ref(1); const pageSize = ref(10); const dialogOpen = ref(false); const editingId = ref(null); const saving = ref(false) const router = useRouter(); const sources = ref([]); const schemas = ref([]); const tables = ref([]); const configs = ref([]); const keyword = ref(''); const appliedKeyword = ref(''); const currentPage = ref(1); const pageSize = ref(10); const dialogOpen = ref(false); const editingId = ref(null); const saving = ref(false); const configsLoading = ref(true)
const form = reactive({ name: '', sourceId: null, schema: '', scope: 'database', tables: [], checkTableComment: true, checkColumnComment: true }) const form = reactive({ name: '', sourceId: null, schema: '', scope: 'database', tables: [], checkTableComment: true, checkColumnComment: true })
const filteredConfigs = computed(() => { const k = keyword.value.trim().toLowerCase(); return !k ? configs.value : configs.value.filter((x) => `${x.name} ${sourceName(x.sourceId)} ${x.schema} ${x.tables.join(' ')}`.toLowerCase().includes(k)) }); const pagedConfigs = computed(() => filteredConfigs.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)); const sourceName = (id) => sources.value.find((s) => s.id === id)?.name || '—'; const ruleText = (x) => [x.checkTableComment && '表注释', x.checkColumnComment && '字段注释'].filter(Boolean).join('、') || '—' const filteredConfigs = computed(() => { const k = appliedKeyword.value.trim().toLowerCase(); return !k ? configs.value : configs.value.filter((x) => `${x.name} ${sourceName(x.sourceId)} ${x.schema} ${x.tables.join(' ')}`.toLowerCase().includes(k)) }); const pagedConfigs = computed(() => filteredConfigs.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)); const sourceName = (id) => sources.value.find((s) => s.id === id)?.name || '—'; const ruleText = (x) => [x.checkTableComment && '表注释', x.checkColumnComment && '字段注释'].filter(Boolean).join('、') || '—'
function formatCheckedAt(value) { if (!value) return '—'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const pad = (number) => String(number).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` }
function configPayload() { return { name: form.name, source_id: form.sourceId, schema: form.schema, scope: form.scope, tables: form.tables, check_table_comment: form.checkTableComment, check_column_comment: form.checkColumnComment } } function configPayload() { return { name: form.name, source_id: form.sourceId, schema: form.schema, scope: form.scope, tables: form.tables, check_table_comment: form.checkTableComment, check_column_comment: form.checkColumnComment } }
async function migrateLegacyConfigs() { const migrationKey = 'table-design-configs-database-migrated'; if (localStorage.getItem(migrationKey)) return; let legacy; try { legacy = JSON.parse(localStorage.getItem('table-design-configs') || '[]') || [] } catch (_) { legacy = [] } if (legacy.length) { const configs = legacy.map(item => ({ name: item.name || '', source_id: item.sourceId, schema: item.schema || '', scope: item.scope || 'database', tables: item.tables || [], check_table_comment: !!item.checkTableComment, check_column_comment: !!item.checkColumnComment, checked_at: item.checkedAt && !Number.isNaN(new Date(item.checkedAt).getTime()) ? new Date(item.checkedAt).toISOString() : null, last_result: item.lastResult || null })); await importTableDesignConfigs(configs) } localStorage.setItem(migrationKey, 'v1') }
async function loadObjects() { schemas.value = []; tables.value = []; form.schema = ''; form.tables = []; if (!form.sourceId) return; try { const r = await getTableDesignObjects(form.sourceId); if (!r.ok) throw new Error(r.message); schemas.value = r.schemas || []; form.schema = r.schema || schemas.value[0] || ''; tables.value = r.tables || [] } catch (e) { ElMessage.error(e.message) } } async function loadObjects() { schemas.value = []; tables.value = []; form.schema = ''; form.tables = []; if (!form.sourceId) return; try { const r = await getTableDesignObjects(form.sourceId); if (!r.ok) throw new Error(r.message); schemas.value = r.schemas || []; form.schema = r.schema || schemas.value[0] || ''; tables.value = r.tables || [] } catch (e) { ElMessage.error(e.message) } }
async function loadTables() { if (!form.sourceId || !form.schema) return; try { const r = await getTableDesignObjects(form.sourceId, form.schema); if (!r.ok) throw new Error(r.message); tables.value = r.tables || [] } catch (e) { ElMessage.error(e.message) } } async function loadTables() { if (!form.sourceId || !form.schema) return; try { const r = await getTableDesignObjects(form.sourceId, form.schema); if (!r.ok) throw new Error(r.message); tables.value = r.tables || [] } catch (e) { ElMessage.error(e.message) } }
function resetEditor() { Object.assign(form, { name: '', sourceId: null, schema: '', scope: 'database', tables: [], checkTableComment: true, checkColumnComment: true }); schemas.value = []; tables.value = [] } function closeEditor() { dialogOpen.value = false; resetEditor() } function openAdd() { editingId.value = null; resetEditor(); dialogOpen.value = true } function resetEditor() { Object.assign(form, { name: '', sourceId: null, schema: '', scope: 'database', tables: [], checkTableComment: true, checkColumnComment: true }); schemas.value = []; tables.value = [] } function closeEditor() { dialogOpen.value = false; resetEditor() } function openAdd() { editingId.value = null; resetEditor(); dialogOpen.value = true }
...@@ -28,7 +28,11 @@ function openEdit(item) { editingId.value = item.id; Object.assign(form, { name: ...@@ -28,7 +28,11 @@ function openEdit(item) { editingId.value = item.id; Object.assign(form, { name:
async function save() { if (!form.name.trim() || !form.sourceId || !form.schema) { ElMessage.warning('请填写校验名称、数据源和数据库'); return } if (form.scope === 'tables' && !form.tables.length) { ElMessage.warning('请至少选择一张表'); return } if (!form.checkTableComment && !form.checkColumnComment) { ElMessage.warning('至少选择一条校验规则'); return } saving.value = true; try { const item = editingId.value ? await updateTableDesignConfig(editingId.value, configPayload()) : await createTableDesignConfig(configPayload()); const i = configs.value.findIndex(x => x.id === item.id); if (i >= 0) configs.value[i] = item; else configs.value.unshift(item); closeEditor(); ElMessage.success('配置已保存') } catch (e) { ElMessage.error(`保存失败:${e.message}`) } finally { saving.value = false } } async function save() { if (!form.name.trim() || !form.sourceId || !form.schema) { ElMessage.warning('请填写校验名称、数据源和数据库'); return } if (form.scope === 'tables' && !form.tables.length) { ElMessage.warning('请至少选择一张表'); return } if (!form.checkTableComment && !form.checkColumnComment) { ElMessage.warning('至少选择一条校验规则'); return } saving.value = true; try { const item = editingId.value ? await updateTableDesignConfig(editingId.value, configPayload()) : await createTableDesignConfig(configPayload()); const i = configs.value.findIndex(x => x.id === item.id); if (i >= 0) configs.value[i] = item; else configs.value.unshift(item); closeEditor(); ElMessage.success('配置已保存') } catch (e) { ElMessage.error(`保存失败:${e.message}`) } finally { saving.value = false } }
async function scan(item) { try { const r = await checkTableDesign({ config_id: item.id, source_id: item.sourceId, schema: item.schema, scope: item.scope, tables: item.tables, check_table_comment: item.checkTableComment, check_column_comment: item.checkColumnComment }); if (!r.ok) throw new Error(r.message); item.checkedAt = r.checked_at; item.lastResult = r; router.push({ name: 'TableDesignResults', query: { config: item.id } }) } catch (e) { ElMessage.error(`扫描失败:${e.message}`) } } async function scan(item) { try { const r = await checkTableDesign({ config_id: item.id, source_id: item.sourceId, schema: item.schema, scope: item.scope, tables: item.tables, check_table_comment: item.checkTableComment, check_column_comment: item.checkColumnComment }); if (!r.ok) throw new Error(r.message); item.checkedAt = r.checked_at; item.lastResult = r; router.push({ name: 'TableDesignResults', query: { config: item.id } }) } catch (e) { ElMessage.error(`扫描失败:${e.message}`) } }
async function remove(item) { try { await ElMessageBox.confirm(`确定删除校验配置「${item.name}」?`, '提示', { type: 'warning' }); await deleteTableDesignConfig(item.id); configs.value = configs.value.filter(x => x.id !== item.id) } catch {} } async function remove(item) { try { await ElMessageBox.confirm(`确定删除校验配置「${item.name}」?`, '提示', { type: 'warning' }); await deleteTableDesignConfig(item.id); configs.value = configs.value.filter(x => x.id !== item.id) } catch {} }
async function loadConfigs() { try { await migrateLegacyConfigs(); configs.value = await listTableDesignConfigs() } catch (e) { configs.value = []; ElMessage.error(`加载表设计配置失败:${e.message}`) } currentPage.value = 1 } onMounted(async () => { try { sources.value = await listDataSources() } catch (e) { ElMessage.error(`加载数据源失败:${e.message}`) } await loadConfigs() }) async function loadConfigs() { configsLoading.value = true; try { configs.value = await listTableDesignConfigs() } catch (e) { configs.value = []; ElMessage.error(`加载表设计配置失败:${e.message}`) } finally { configsLoading.value = false } currentPage.value = 1 }
function onQuery() { appliedKeyword.value = keyword.value; currentPage.value = 1 }
function onReset() { keyword.value = ''; appliedKeyword.value = ''; currentPage.value = 1 }
watch([filteredConfigs, () => Math.max(1, Math.ceil(filteredConfigs.value.length / pageSize.value))], () => { const pageCount = Math.max(1, Math.ceil(filteredConfigs.value.length / pageSize.value)); if (currentPage.value > pageCount) currentPage.value = pageCount })
onMounted(async () => { await loadConfigs(); try { sources.value = await listDataSources() } catch (e) { ElMessage.error(`加载数据源失败:${e.message}`) } })
</script> </script>
<style scoped> <style scoped>
.design-page{min-width:900px}.design-search{min-width:200px}.design-modal{width:1000px;max-width:94vw}.design-modal__body{padding:0 20px;max-height:64vh;overflow-y:auto}.step{display:flex;gap:14px;padding:22px 0;border-bottom:1px solid #ebeef5;align-items:flex-start}.step:last-child{border-bottom:0}.step-no{width:28px;height:28px;border-radius:50%;background:#409eff;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-weight:600;flex:0 0 auto}.step-content{flex:1;min-width:0}.step-content b{display:inline-block;line-height:40px;color:var(--app-text)}.source-select,.design-name-input{width:380px;margin-left:20px;vertical-align:middle}.scope-choice{margin-top:12px}.table-picker{margin-top:14px;max-height:220px;overflow:auto;padding:10px 14px;border:1px solid var(--app-border)}.table-picker :deep(.el-checkbox){display:block;margin:7px 0}.table-picker small,.muted{color:var(--app-text-muted)}.step-content :deep(.el-checkbox){margin-left:20px}.design-modal .modal-footer{min-height:68px;padding:16px 20px;box-sizing:border-box}.empty-cell{text-align:center;color:#909399;padding:24px} .design-page{min-width:900px}.design-search{min-width:200px}.design-modal{width:1000px;max-width:94vw}.design-modal__body{padding:0 20px;max-height:64vh;overflow-y:auto}.step{display:flex;gap:14px;padding:22px 0;border-bottom:1px solid #ebeef5;align-items:flex-start}.step:last-child{border-bottom:0}.step-no{width:28px;height:28px;border-radius:50%;background:#409eff;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-weight:600;flex:0 0 auto}.step-content{flex:1;min-width:0}.step-content b{display:inline-block;line-height:40px;color:var(--app-text)}.source-select,.design-name-input{width:380px;margin-left:20px;vertical-align:middle}.scope-choice{margin-top:12px}.table-picker{margin-top:14px;max-height:220px;overflow:auto;padding:10px 14px;border:1px solid var(--app-border)}.table-picker :deep(.el-checkbox){display:block;margin:7px 0}.table-picker small,.muted{color:var(--app-text-muted)}.step-content :deep(.el-checkbox){margin-left:20px}.design-modal .modal-footer{min-height:68px;padding:16px 20px;box-sizing:border-box}.empty-cell{text-align:center;color:#909399;padding:24px}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment