| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- <template>
- <div class="dict-tab-panel sync-panel">
- <el-row :gutter="16" class="mb16">
- <el-col :span="8">
- <div class="sync-mode-card merge">
- <div class="sync-mode-title">MERGE · 合并</div>
- <div class="sync-mode-desc">推荐。新增缺失项,更新平台管控字段,保留租户扩展</div>
- </div>
- </el-col>
- <el-col :span="8">
- <div class="sync-mode-card append">
- <div class="sync-mode-title">APPEND · 追加</div>
- <div class="sync-mode-desc">仅追加模板中不存在的数据,不修改已有项</div>
- </div>
- </el-col>
- <el-col :span="8">
- <div class="sync-mode-card overwrite">
- <div class="sync-mode-title">OVERWRITE · 覆盖</div>
- <div class="sync-mode-desc">覆盖各租户中「平台管控」的字典项,请谨慎使用</div>
- </div>
- </el-col>
- </el-row>
- <el-card shadow="never" class="mb16 sync-config-card">
- <div slot="header" class="card-header">
- <span><i class="el-icon-setting" /> 同步配置</span>
- </div>
- <el-form ref="syncForm" :model="syncForm" label-width="100px" size="small">
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="同步模式" required>
- <el-select v-model="syncForm.syncMode" style="width: 100%">
- <el-option label="MERGE - 合并(推荐)" value="MERGE" />
- <el-option label="APPEND - 仅追加缺失项" value="APPEND" />
- <el-option label="OVERWRITE - 覆盖平台管控项" value="OVERWRITE" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="租户范围" required>
- <el-select v-model="syncForm.scopeType" style="width: 100%">
- <el-option label="全部启用租户" value="ALL" />
- <el-option label="仅指定租户" value="INCLUDE" />
- <el-option label="排除指定租户" value="EXCLUDE" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="并行执行">
- <div class="parallel-row">
- <el-switch v-model="syncForm.parallel" />
- <el-input-number v-if="syncForm.parallel" v-model="syncForm.threads" :min="1" :max="16" size="small" controls-position="right" />
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="指定租户" v-if="syncForm.scopeType !== 'ALL'">
- <el-select v-model="syncForm.tenantIds" multiple filterable placeholder="选择租户" style="width: 100%">
- <el-option v-for="t in tenantOptions" :key="t.id" :label="t.tenantName + ' (' + t.tenantCode + ')'" :value="t.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="字典类型">
- <el-select v-model="syncForm.dictTypes" multiple filterable clearable placeholder="空 = 全部模板类型" style="width: 100%">
- <el-option v-for="t in templateTypeOptions" :key="t.dictType" :label="t.dictName + ' / ' + t.dictType" :value="t.dictType" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-alert
- v-if="syncForm.syncMode === 'OVERWRITE'"
- type="warning"
- :closable="false"
- show-icon
- class="mb12"
- title="OVERWRITE 将覆盖各租户中「平台管控」的字典项,租户自有扩展项不受影响"
- />
- <el-form-item v-if="syncForm.syncMode === 'OVERWRITE'" class="overwrite-check">
- <el-checkbox v-model="syncForm.overwriteConfirm">我已了解上述风险,确认执行覆盖同步</el-checkbox>
- </el-form-item>
- <el-form-item class="action-row">
- <el-button type="info" plain icon="el-icon-view" @click="handleSync(true)" v-hasPermi="['tenant:dict:sync']">全量预览</el-button>
- <el-button type="warning" icon="el-icon-upload2" @click="handleSync(false)" v-hasPermi="['tenant:dict:sync']">开始同步</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- <el-card shadow="never" v-if="previewList.length" class="mb16 result-card">
- <div slot="header" class="card-header">
- <span><i class="el-icon-data-analysis" /> 预览结果</span>
- <el-tag size="mini" type="info" effect="plain">未写入数据库</el-tag>
- </div>
- <el-table :data="previewList" border size="small" class="dict-table">
- <el-table-column label="租户" prop="tenantName" min-width="120" />
- <el-table-column label="编码" prop="tenantCode" width="110" />
- <el-table-column label="新增类型" prop="typeAdded" width="88" align="center">
- <template slot-scope="s"><span class="stat-num add">{{ s.row.typeAdded || 0 }}</span></template>
- </el-table-column>
- <el-table-column label="更新类型" prop="typeUpdated" width="88" align="center">
- <template slot-scope="s"><span class="stat-num update">{{ s.row.typeUpdated || 0 }}</span></template>
- </el-table-column>
- <el-table-column label="新增数据" prop="dataAdded" width="88" align="center">
- <template slot-scope="s"><span class="stat-num add">{{ s.row.dataAdded || 0 }}</span></template>
- </el-table-column>
- <el-table-column label="更新数据" prop="dataUpdated" width="88" align="center">
- <template slot-scope="s"><span class="stat-num update">{{ s.row.dataUpdated || 0 }}</span></template>
- </el-table-column>
- <el-table-column label="跳过" prop="dataSkipped" width="72" align="center" />
- <el-table-column label="移除" prop="dataRemoved" width="72" align="center" />
- </el-table>
- </el-card>
- <el-card shadow="never" v-if="taskNo" class="result-card">
- <div slot="header" class="card-header">
- <span><i class="el-icon-loading" v-if="taskInfo && taskInfo.status === 'RUNNING'" /><i class="el-icon-finished" v-else /> 同步任务 {{ taskNo }}</span>
- <el-button type="text" icon="el-icon-refresh" @click="pollTask">刷新状态</el-button>
- </div>
- <el-descriptions :column="4" border size="small" v-if="taskInfo" class="task-summary">
- <el-descriptions-item label="状态"><el-tag :type="taskStatusType" size="small">{{ taskInfo.status }}</el-tag></el-descriptions-item>
- <el-descriptions-item label="模式">{{ taskInfo.syncMode }}</el-descriptions-item>
- <el-descriptions-item label="成功 / 总数">{{ taskInfo.successTenants }} / {{ taskInfo.totalTenants }}</el-descriptions-item>
- <el-descriptions-item label="失败">
- <span :class="{ 'stat-fail': taskInfo.failedTenants > 0 }">{{ taskInfo.failedTenants }}</span>
- </el-descriptions-item>
- </el-descriptions>
- <el-table v-if="taskInfo && taskInfo.details && taskInfo.details.length" :data="taskInfo.details" border size="small" class="dict-table mt12">
- <el-table-column label="租户" prop="tenantName" min-width="120" />
- <el-table-column label="状态" prop="status" width="90" align="center">
- <template slot-scope="s">
- <el-tag size="mini" :type="s.row.status === 'SUCCESS' ? 'success' : (s.row.status === 'FAILED' ? 'danger' : 'warning')">{{ s.row.status }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="新增类型" prop="typeAdded" width="88" align="center" />
- <el-table-column label="更新类型" prop="typeUpdated" width="88" align="center" />
- <el-table-column label="新增数据" prop="dataAdded" width="88" align="center" />
- <el-table-column label="更新数据" prop="dataUpdated" width="88" align="center" />
- <el-table-column label="跳过" prop="dataSkipped" width="72" align="center" />
- <el-table-column label="错误" prop="errorMsg" min-width="160" :show-overflow-tooltip="true" />
- </el-table>
- </el-card>
- <el-empty v-if="!previewList.length && !taskNo" description="配置同步参数后,可先「全量预览」查看差异,再「开始同步」" :image-size="100" />
- </div>
- </template>
- <script>
- import { listCompanyOptions } from '@/api/admin/sysCompany'
- import { runDictSync, getDictSyncTask, previewDictSync, templateDictTypeOptions } from '@/api/tenant/dict'
- export default {
- name: 'AdminTenantDictSync',
- data() {
- return {
- tenantOptions: [],
- templateTypeOptions: [],
- previewList: [],
- taskNo: '',
- taskInfo: null,
- pollTimer: null,
- syncForm: {
- syncMode: 'MERGE',
- scopeType: 'ALL',
- tenantIds: [],
- dictTypes: [],
- parallel: false,
- threads: 4,
- overwriteConfirm: false,
- dryRun: false
- }
- }
- },
- computed: {
- taskStatusType() {
- const s = this.taskInfo && this.taskInfo.status
- if (s === 'SUCCESS') return 'success'
- if (s === 'FAILED') return 'danger'
- if (s === 'PARTIAL') return 'warning'
- return 'info'
- }
- },
- created() {
- listCompanyOptions().then(r => { this.tenantOptions = r.data || [] })
- templateDictTypeOptions().then(r => {
- this.templateTypeOptions = r.data || []
- if (!this.templateTypeOptions.length) {
- this.$message.warning('平台模板为空,请先在「平台模板」Tab 维护字典模板后再同步')
- }
- })
- },
- beforeDestroy() {
- if (this.pollTimer) clearInterval(this.pollTimer)
- },
- methods: {
- buildPayload(dryRun) {
- return {
- syncMode: this.syncForm.syncMode,
- scopeType: this.syncForm.scopeType,
- tenantIds: this.syncForm.scopeType === 'ALL' ? [] : this.syncForm.tenantIds,
- dictTypes: this.syncForm.dictTypes,
- parallel: this.syncForm.parallel,
- threads: this.syncForm.threads,
- overwriteConfirm: this.syncForm.overwriteConfirm,
- dryRun: dryRun
- }
- },
- handlePreview() {
- if (this.syncForm.scopeType === 'INCLUDE' && (!this.syncForm.tenantIds || !this.syncForm.tenantIds.length)) {
- this.$message.warning('请选择要预览的租户')
- return
- }
- const tenantId = this.syncForm.scopeType === 'INCLUDE' ? this.syncForm.tenantIds[0] : (this.tenantOptions[0] && this.tenantOptions[0].id)
- if (!tenantId) {
- this.$message.warning('无可用租户')
- return
- }
- previewDictSync(tenantId, this.syncForm.dictTypes).then(r => {
- this.previewList = r.data || []
- this.$message.success('预览完成(仅首个租户示例,完整预览请用 dryRun 同步)')
- })
- },
- handleSync(dryRun) {
- if (this.syncForm.syncMode === 'OVERWRITE' && !this.syncForm.overwriteConfirm) {
- this.$message.error('OVERWRITE 模式请先勾选确认')
- return
- }
- if (this.syncForm.scopeType !== 'ALL' && (!this.syncForm.tenantIds || !this.syncForm.tenantIds.length)) {
- this.$message.warning('请选择租户')
- return
- }
- this.$confirm(dryRun ? '确认预览同步差异?' : '确认开始字典同步任务?', '提示', { type: 'warning' }).then(() => {
- return runDictSync(this.buildPayload(dryRun))
- }).then(r => {
- const data = r.data || {}
- if (data.dryRun) {
- this.previewList = data.preview || []
- if (!this.previewList.length) {
- this.$message.warning('预览完成:无差异(请确认平台模板中已有字典类型和数据)')
- } else {
- this.$message.success('全量预览完成,共 ' + this.previewList.length + ' 个租户')
- }
- return
- }
- if (!data.taskNo) {
- this.$message.error('同步任务创建失败,请检查后端日志')
- return
- }
- this.taskNo = data.taskNo
- this.taskInfo = { status: 'RUNNING', syncMode: data.syncMode, totalTenants: data.totalTenants, successTenants: 0, failedTenants: 0, details: [] }
- this.startPoll()
- this.$message.success('同步任务已启动:' + data.taskNo)
- }).catch(err => {
- const msg = (err && err.message) || (typeof err === 'string' ? err : '同步失败')
- if (msg && msg !== 'cancel') this.$message.error(msg)
- })
- },
- startPoll() {
- if (this.pollTimer) clearInterval(this.pollTimer)
- this.pollTask()
- this.pollTimer = setInterval(() => this.pollTask(), 3000)
- },
- pollTask() {
- if (!this.taskNo) return
- getDictSyncTask(this.taskNo).then(r => {
- this.taskInfo = r.data
- if (this.taskInfo && ['SUCCESS', 'FAILED', 'PARTIAL'].includes(this.taskInfo.status)) {
- clearInterval(this.pollTimer)
- this.pollTimer = null
- }
- })
- }
- }
- }
- </script>
- <style scoped>
- .sync-panel { padding-top: 4px; }
- .mb12 { margin-bottom: 12px; }
- .mb16 { margin-bottom: 16px; }
- .mt12 { margin-top: 12px; }
- .dict-table { width: 100%; }
- .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-weight: 600;
- color: #303133;
- }
- .card-header i { margin-right: 6px; color: #409eff; }
- .sync-mode-card {
- padding: 14px 16px;
- border-radius: 8px;
- border: 1px solid #ebeef5;
- background: #fff;
- height: 100%;
- transition: box-shadow 0.2s;
- }
- .sync-mode-card:hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); }
- .sync-mode-title { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
- .sync-mode-desc { font-size: 12px; color: #909399; line-height: 1.5; }
- .sync-mode-card.merge { border-left: 3px solid #67c23a; }
- .sync-mode-card.merge .sync-mode-title { color: #67c23a; }
- .sync-mode-card.append { border-left: 3px solid #409eff; }
- .sync-mode-card.append .sync-mode-title { color: #409eff; }
- .sync-mode-card.overwrite { border-left: 3px solid #e6a23c; }
- .sync-mode-card.overwrite .sync-mode-title { color: #e6a23c; }
- .sync-config-card >>> .el-card__body { padding-top: 8px; }
- .parallel-row { display: flex; align-items: center; gap: 12px; }
- .overwrite-check { margin-bottom: 0; }
- .action-row { margin-bottom: 0; margin-top: 4px; }
- .stat-num { font-weight: 600; }
- .stat-num.add { color: #67c23a; }
- .stat-num.update { color: #409eff; }
- .stat-fail { color: #f56c6c; font-weight: 600; }
- .task-summary { margin-bottom: 0; }
- </style>
|