Bladeren bron

1、租户管理菜单维护页面
2、租户销售端菜单维护页面

yys 1 dag geleden
bovenliggende
commit
f93f03b69b

+ 2 - 0
src/views/admin/menu.js

@@ -15,6 +15,8 @@ const adminRoutes = {
     // 2. 租户管理
     { path: 'company', component: () => import('@/views/admin/sysCompany/index'), name: 'SysCompanyAdmin', meta: { title: '租户管理' } },
     { path: 'moduleUsage', component: () => import('@/views/admin/moduleUsage/index'), name: 'AdminModuleUsage', meta: { title: '租户模块使用统计' } },
+    { path: 'tenantMenu', component: () => import('@/views/admin/tenantMenu/index'), name: 'AdminTenantSysMenu', meta: { title: '租户管理端菜单' } },
+    { path: 'tenantCompany', component: () => import('@/views/admin/tenantCompany/index'), name: 'AdminTenantComMenu', meta: { title: '租户销售端菜单' } },
 
     // 3. 代理管理
     { path: 'proxy', component: () => import('@/views/admin/proxy/index'), name: 'AdminProxy', meta: { title: '代理管理' } },

+ 470 - 0
src/views/admin/tenantCompany/index.vue

@@ -0,0 +1,470 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="菜单名称" prop="menuName">
+        <el-input
+          v-model="queryParams.menuName"
+          placeholder="请输入菜单名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="菜单状态" clearable size="small">
+          <el-option
+            v-for="dict in statusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['system:menu:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          icon="el-icon-sort"
+          size="mini"
+          @click="toggleExpandAll"
+        >展开/折叠</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table
+      v-if="refreshTable"
+      v-loading="loading"
+      :data="menuList"
+      row-key="menuId"
+      :default-expand-all="isExpandAll"
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+    >
+      <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
+      <el-table-column prop="icon" label="图标" align="center" width="100">
+        <template slot-scope="scope">
+          <svg-icon :icon-class="scope.row.icon" />
+        </template>
+      </el-table-column>
+      <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
+      <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
+      <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
+      <el-table-column prop="status" label="状态" width="80">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini"
+                     type="text"
+                     icon="el-icon-edit"
+                     @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:menu:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-plus"
+            @click="handleAdd(scope.row)"
+            v-hasPermi="['system:menu:add']"
+          >新增</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['system:menu:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 添加或修改菜单对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="上级菜单">
+              <treeselect
+                v-model="form.parentId"
+                :options="menuOptions"
+                :normalizer="normalizer"
+                :show-count="true"
+                placeholder="选择上级菜单"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="菜单类型" prop="menuType">
+              <el-radio-group v-model="form.menuType">
+                <el-radio label="M">目录</el-radio>
+                <el-radio label="C">菜单</el-radio>
+                <el-radio label="F">按钮</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item v-if="form.menuType != 'F'" label="菜单图标">
+              <el-popover
+                placement="bottom-start"
+                width="460"
+                trigger="click"
+                @show="$refs['iconSelect'].reset()"
+              >
+                <IconSelect ref="iconSelect" @selected="selected" />
+                <el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
+                  <svg-icon
+                    v-if="form.icon"
+                    slot="prefix"
+                    :icon-class="form.icon"
+                    class="el-input__icon"
+                    style="height: 32px;width: 16px;"
+                  />
+                  <i v-else slot="prefix" class="el-icon-search el-input__icon" />
+                </el-input>
+              </el-popover>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="菜单名称" prop="menuName">
+              <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="显示排序" prop="orderNum">
+              <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType != 'F'">
+              <span slot="label">
+                <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                是否外链
+              </span>
+              <el-radio-group v-model="form.isFrame">
+                <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 v-if="form.menuType != 'F'" prop="path">
+              <span slot="label">
+                <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                路由地址
+              </span>
+              <el-input v-model="form.path" placeholder="请输入路由地址" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType == 'C'">
+            <el-form-item prop="component">
+              <span slot="label">
+                <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                组件路径
+              </span>
+              <el-input v-model="form.component" placeholder="请输入组件路径" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType != 'M'">
+              <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
+              <span slot="label">
+                <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                权限字符
+              </span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType == 'C'">
+              <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
+              <span slot="label">
+                <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                路由参数
+              </span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType == 'C'">
+              <span slot="label">
+                <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                是否缓存
+              </span>
+              <el-radio-group v-model="form.isCache">
+                <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 v-if="form.menuType != 'F'">
+              <span slot="label">
+                <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                显示状态
+              </span>
+              <el-radio-group v-model="form.visible">
+                <el-radio
+                  v-for="dict in visibleOptions"
+                  :key="dict.dictValue"
+                  :label="dict.dictValue"
+                >{{dict.dictLabel}}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType != 'F'">
+              <span slot="label">
+                <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                菜单状态
+              </span>
+              <el-radio-group v-model="form.status">
+                <el-radio
+                  v-for="dict in statusOptions"
+                  :key="dict.dictValue"
+                  :label="dict.dictValue"
+                >{{dict.dictLabel}}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { delTenantComMenu,tenantComMenu,addTenantComMenu,updateTenantComMenu,getTenantComMenu } from "@/api/system/menu";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import IconSelect from "@/components/IconSelect";
+
+/**
+ * 租户销售端菜单模板维护(主库 tenant_company_menu)
+ * 新建租户时从此模板复制到租户库 company_menu
+ */
+export default {
+  name: "AdminTenantComMenu",
+  components: { Treeselect, IconSelect },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 菜单表格树数据
+      menuList: [],
+      // 菜单树选项
+      menuOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 是否展开,默认全部折叠
+      isExpandAll: false,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 显示状态数据字典
+      visibleOptions: [],
+      // 菜单状态数据字典
+      statusOptions: [],
+      // 查询参数
+      queryParams: {
+        menuName: undefined,
+        visible: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        menuName: [
+          { required: true, message: "菜单名称不能为空", trigger: "blur" }
+        ],
+        orderNum: [
+          { required: true, message: "菜单顺序不能为空", trigger: "blur" }
+        ],
+        path: [
+          { required: true, message: "路由地址不能为空", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+    this.getDicts("sys_show_hide").then(response => {
+      this.visibleOptions = response.data;
+    });
+    this.getDicts("sys_normal_disable").then(response => {
+      this.statusOptions = response.data;
+    });
+  },
+  methods: {
+    // 选择图标
+    selected(name) {
+      this.form.icon = name;
+    },
+    /** 查询菜单列表 */
+    getList() {
+      this.loading = true;
+      tenantComMenu(this.queryParams).then(response => {
+        this.menuList = this.handleTree(response.data, "menuId");
+        this.loading = false;
+      });
+    },
+    /** 转换菜单数据结构 */
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children;
+      }
+      return {
+        id: node.menuId,
+        label: node.menuName,
+        children: node.children
+      };
+    },
+    /** 查询菜单下拉树结构 */
+    getTreeselect() {
+      tenantComMenu().then(response => {
+        this.menuOptions = [];
+        const menu = { menuId: 0, menuName: '主类目', children: [] };
+        menu.children = this.handleTree(response.data, "menuId");
+        this.menuOptions.push(menu);
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        menuId: undefined,
+        parentId: 0,
+        menuName: undefined,
+        icon: undefined,
+        menuType: "M",
+        orderNum: undefined,
+        isFrame: "1",
+        isCache: "0",
+        visible: "0",
+        status: "0"
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd(row) {
+      this.reset();
+      this.getTreeselect();
+      if (row != null && row.menuId) {
+        this.form.parentId = row.menuId;
+      } else {
+        this.form.parentId = 0;
+      }
+      this.open = true;
+      this.title = "添加菜单";
+    },
+    /** 展开/折叠操作 */
+    toggleExpandAll() {
+      this.refreshTable = false;
+      this.isExpandAll = !this.isExpandAll;
+      this.$nextTick(() => {
+        this.refreshTable = true;
+      });
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.getTreeselect();
+      getTenantComMenu(row.menuId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改菜单";
+      });
+    },
+    /** 提交按钮 */
+    submitForm: function() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.menuId != undefined) {
+            updateTenantComMenu(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addTenantComMenu(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      this.$confirm('是否确认删除名称为"' + row.menuName + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delTenantComMenu(row.menuId);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
+    }
+  }
+};
+</script>

+ 637 - 319
src/views/admin/tenantDict/sync/index.vue

@@ -1,319 +1,637 @@
-<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>

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

+ 152 - 152
src/views/admin/tenantDict/template/TemplateDataPanel.vue

@@ -1,152 +1,152 @@
-<template>
-  <div class="template-data-panel">
-    <el-row :gutter="10" class="mb8 toolbar-row">
-      <el-col :span="1.5">
-        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['tenant:dict:template:add']">新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
-      </el-col>
-    </el-row>
-
-    <el-table v-loading="loading" :data="dataList" border size="small" @selection-change="handleSelectionChange" class="dict-table">
-      <el-table-column type="selection" width="45" align="center" />
-      <el-table-column label="标签" prop="dictLabel" align="center" min-width="120" :show-overflow-tooltip="true" />
-      <el-table-column label="键值" prop="dictValue" align="center" min-width="100" />
-      <el-table-column label="排序" prop="dictSort" width="70" align="center" />
-      <el-table-column label="平台管控" width="88" align="center">
-        <template slot-scope="scope">
-          <el-tag size="mini" :type="scope.row.isManaged === 1 ? 'danger' : 'info'">{{ scope.row.isManaged === 1 ? '是' : '否' }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" width="76" align="center">
-        <template slot-scope="scope"><dict-tag :options="statusOptions" :value="scope.row.status"/></template>
-      </el-table-column>
-      <el-table-column label="操作" width="130" align="center" fixed="right" class-name="small-padding fixed-width">
-        <template slot-scope="scope">
-          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
-          <el-button size="mini" type="text" icon="el-icon-delete" style="color:#f5222d" @click="handleDelete(scope.row)" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
-
-    <el-dialog :title="title" :visible.sync="open" width="520px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
-        <el-form-item label="字典类型"><el-input v-model="form.dictType" disabled /></el-form-item>
-        <el-form-item label="数据标签" prop="dictLabel"><el-input v-model="form.dictLabel" /></el-form-item>
-        <el-form-item label="数据键值" prop="dictValue"><el-input v-model="form.dictValue" :disabled="form.dictCode != null" /></el-form-item>
-        <el-form-item label="显示排序" prop="dictSort"><el-input-number v-model="form.dictSort" :min="0" controls-position="right" style="width: 100%" /></el-form-item>
-        <el-form-item label="平台管控">
-          <el-radio-group v-model="form.isManaged">
-            <el-radio :label="1">是</el-radio><el-radio :label="0">否</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-radio-group v-model="form.status">
-            <el-radio v-for="d in statusOptions" :key="d.dictValue" :label="d.dictValue">{{ d.dictLabel }}</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import {
-  listTemplateDictData, getTemplateDictData, addTemplateDictData,
-  updateTemplateDictData, delTemplateDictData
-} from '@/api/tenant/dict'
-
-export default {
-  name: 'TemplateDataPanel',
-  props: {
-    dictType: { type: String, required: true }
-  },
-  data() {
-    return {
-      loading: false,
-      ids: [],
-      single: true,
-      multiple: true,
-      total: 0,
-      dataList: [],
-      title: '',
-      open: false,
-      statusOptions: [],
-      queryParams: { pageNum: 1, pageSize: 10, dictType: undefined },
-      form: {},
-      rules: {
-        dictLabel: [{ required: true, message: '标签不能为空', trigger: 'blur' }],
-        dictValue: [{ required: true, message: '键值不能为空', trigger: 'blur' }],
-        dictSort: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
-      }
-    }
-  },
-  watch: {
-    dictType: {
-      immediate: true,
-      handler(val) {
-        if (val) {
-          this.queryParams.dictType = val
-          this.getList()
-        }
-      }
-    }
-  },
-  created() {
-    this.getDicts('sys_normal_disable').then(r => { this.statusOptions = r.data })
-  },
-  methods: {
-    getList() {
-      if (!this.queryParams.dictType) return
-      this.loading = true
-      listTemplateDictData(this.queryParams).then(r => {
-        this.dataList = r.rows
-        this.total = r.total
-        this.loading = false
-      }).catch(() => { this.loading = false })
-    },
-    cancel() { this.open = false; this.reset() },
-    reset() {
-      this.form = { dictCode: undefined, dictLabel: undefined, dictValue: undefined, dictSort: 0, status: '0', isManaged: 0, remark: undefined }
-      this.resetForm('form')
-    },
-    handleAdd() { this.reset(); this.form.dictType = this.queryParams.dictType; this.open = true; this.title = '添加模板数据' },
-    handleSelectionChange(s) { this.ids = s.map(i => i.dictCode); this.single = s.length !== 1; this.multiple = !s.length },
-    handleUpdate(row) {
-      this.reset()
-      getTemplateDictData(row.dictCode || this.ids[0]).then(r => { this.form = r.data; this.open = true; this.title = '修改模板数据' })
-    },
-    submitForm() {
-      this.$refs['form'].validate(valid => {
-        if (!valid) return
-        const req = this.form.dictCode != null ? updateTemplateDictData(this.form) : addTemplateDictData(this.form)
-        req.then(() => { this.msgSuccess('保存成功'); this.open = false; this.getList() })
-      })
-    },
-    handleDelete(row) {
-      const ids = row.dictCode || this.ids.join(',')
-      this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => delTemplateDictData(ids))
-        .then(() => { this.getList(); this.msgSuccess('删除成功') }).catch(() => {})
-    }
-  }
-}
-</script>
-
-<style scoped>
-.template-data-panel { min-height: 200px; }
-.mb8 { margin-bottom: 8px; }
-.toolbar-row { display: flex; flex-wrap: wrap; align-items: center; }
-.dict-table { width: 100%; }
-</style>
-
+<template>
+  <div class="template-data-panel">
+    <el-row :gutter="10" class="mb8 toolbar-row">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['tenant:dict:template:add']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table v-loading="loading" :data="dataList" border size="small" @selection-change="handleSelectionChange" class="dict-table">
+      <el-table-column type="selection" width="45" align="center" />
+      <el-table-column label="标签" prop="dictLabel" align="center" min-width="120" :show-overflow-tooltip="true" />
+      <el-table-column label="键值" prop="dictValue" align="center" min-width="100" />
+      <el-table-column label="排序" prop="dictSort" width="70" align="center" />
+      <el-table-column label="平台管控" width="88" align="center">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.isManaged === 1 ? 'danger' : 'info'">{{ scope.row.isManaged === 1 ? '是' : '否' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="76" align="center">
+        <template slot-scope="scope"><dict-tag :options="statusOptions" :value="scope.row.status"/></template>
+      </el-table-column>
+      <el-table-column label="操作" width="130" align="center" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" style="color:#f5222d" @click="handleDelete(scope.row)" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <el-dialog :title="title" :visible.sync="open" width="520px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="字典类型"><el-input v-model="form.dictType" disabled /></el-form-item>
+        <el-form-item label="数据标签" prop="dictLabel"><el-input v-model="form.dictLabel" /></el-form-item>
+        <el-form-item label="数据键值" prop="dictValue"><el-input v-model="form.dictValue" :disabled="form.dictCode != null" /></el-form-item>
+        <el-form-item label="显示排序" prop="dictSort"><el-input-number v-model="form.dictSort" :min="0" controls-position="right" style="width: 100%" /></el-form-item>
+        <el-form-item label="平台管控">
+          <el-radio-group v-model="form.isManaged">
+            <el-radio :label="1">是</el-radio><el-radio :label="0">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="d in statusOptions" :key="d.dictValue" :label="d.dictValue">{{ d.dictLabel }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listTemplateDictData, getTemplateDictData, addTemplateDictData,
+  updateTemplateDictData, delTemplateDictData
+} from '@/api/tenant/dict'
+
+export default {
+  name: 'TemplateDataPanel',
+  props: {
+    dictType: { type: String, required: true }
+  },
+  data() {
+    return {
+      loading: false,
+      ids: [],
+      single: true,
+      multiple: true,
+      total: 0,
+      dataList: [],
+      title: '',
+      open: false,
+      statusOptions: [],
+      queryParams: { pageNum: 1, pageSize: 10, dictType: undefined },
+      form: {},
+      rules: {
+        dictLabel: [{ required: true, message: '标签不能为空', trigger: 'blur' }],
+        dictValue: [{ required: true, message: '键值不能为空', trigger: 'blur' }],
+        dictSort: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
+      }
+    }
+  },
+  watch: {
+    dictType: {
+      immediate: true,
+      handler(val) {
+        if (val) {
+          this.queryParams.dictType = val
+          this.getList()
+        }
+      }
+    }
+  },
+  created() {
+    this.getDicts('sys_normal_disable').then(r => { this.statusOptions = r.data })
+  },
+  methods: {
+    getList() {
+      if (!this.queryParams.dictType) return
+      this.loading = true
+      listTemplateDictData(this.queryParams).then(r => {
+        this.dataList = r.rows
+        this.total = r.total
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    cancel() { this.open = false; this.reset() },
+    reset() {
+      this.form = { dictCode: undefined, dictLabel: undefined, dictValue: undefined, dictSort: 0, status: '0', isManaged: 0, remark: undefined }
+      this.resetForm('form')
+    },
+    handleAdd() { this.reset(); this.form.dictType = this.queryParams.dictType; this.open = true; this.title = '添加模板数据' },
+    handleSelectionChange(s) { this.ids = s.map(i => i.dictCode); this.single = s.length !== 1; this.multiple = !s.length },
+    handleUpdate(row) {
+      this.reset()
+      getTemplateDictData(row.dictCode || this.ids[0]).then(r => { this.form = r.data; this.open = true; this.title = '修改模板数据' })
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) return
+        const req = this.form.dictCode != null ? updateTemplateDictData(this.form) : addTemplateDictData(this.form)
+        req.then(() => { this.msgSuccess('保存成功'); this.open = false; this.getList() })
+      })
+    },
+    handleDelete(row) {
+      const ids = row.dictCode || this.ids.join(',')
+      this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => delTemplateDictData(ids))
+        .then(() => { this.getList(); this.msgSuccess('删除成功') }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.template-data-panel { min-height: 200px; }
+.mb8 { margin-bottom: 8px; }
+.toolbar-row { display: flex; flex-wrap: wrap; align-items: center; }
+.dict-table { width: 100%; }
+</style>
+

+ 342 - 342
src/views/admin/tenantDict/template/index.vue

@@ -1,342 +1,342 @@
-<template>
-  <div class="dict-tab-panel">
-    <div class="tip-banner">
-      <i class="el-icon-info" />
-      <span>可从「字典维护」勾选字典后点「导入到平台模板」,或在此从平台主库 / 租户库批量选取导入</span>
-    </div>
-
-    <el-card shadow="never" class="mb16 filter-card" v-show="showSearch">
-      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="72px">
-        <el-form-item label="字典名称" prop="dictName">
-          <el-input v-model="queryParams.dictName" placeholder="请输入字典名称" clearable @keyup.enter.native="handleQuery" />
-        </el-form-item>
-        <el-form-item label="字典类型" prop="dictType">
-          <el-input v-model="queryParams.dictType" placeholder="请输入字典类型" clearable @keyup.enter.native="handleQuery" />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <el-row :gutter="10" class="mb8 toolbar-row">
-      <el-col :span="1.5">
-        <el-button type="primary" plain icon="el-icon-download" size="mini" @click="openImportDialog" v-hasPermi="['tenant:dict:template:add']">从字典库导入</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['tenant:dict:template:add']">手动新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
-      </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
-    </el-row>
-
-    <el-table v-loading="loading" :data="typeList" border size="small" @selection-change="handleSelectionChange" class="dict-table">
-      <el-table-column type="selection" width="48" align="center" />
-      <el-table-column label="编号" align="center" prop="dictId" width="76" />
-      <el-table-column label="字典名称" align="center" prop="dictName" min-width="120" :show-overflow-tooltip="true" />
-      <el-table-column label="字典类型" align="center" min-width="140">
-        <template slot-scope="scope">
-          <el-link type="primary" :underline="false" @click="openDataDrawer(scope.row)">{{ scope.row.dictType }}</el-link>
-        </template>
-      </el-table-column>
-      <el-table-column label="平台管控" align="center" width="88">
-        <template slot-scope="scope">
-          <el-tag size="mini" :type="scope.row.isManaged === 1 ? 'danger' : 'info'">{{ scope.row.isManaged === 1 ? '是' : '否' }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="默认同步模式" align="center" prop="syncMode" width="120">
-        <template slot-scope="scope">
-          <el-tag size="mini" type="info" effect="plain">{{ scope.row.syncMode }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" align="center" width="76">
-        <template slot-scope="scope">
-          <dict-tag :options="statusOptions" :value="scope.row.status"/>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" width="200" fixed="right" class-name="small-padding fixed-width">
-        <template slot-scope="scope">
-          <el-button size="mini" type="text" icon="el-icon-view" @click="openDataDrawer(scope.row)">字典数据</el-button>
-          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
-          <el-button size="mini" type="text" icon="el-icon-delete" style="color:#f5222d" @click="handleDelete(scope.row)" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
-
-    <el-drawer
-      :title="'模板字典数据 · ' + (currentType.dictName || currentType.dictType || '')"
-      :visible.sync="dataDrawerVisible"
-      size="62%"
-      append-to-body
-      destroy-on-close
-      custom-class="dict-data-drawer"
-    >
-      <div class="drawer-body-wrap" v-if="dataDrawerVisible && currentType.dictType">
-        <div class="drawer-type-meta">
-          <el-tag size="small" type="primary" effect="plain">{{ currentType.dictType }}</el-tag>
-        </div>
-        <template-data-panel :dict-type="currentType.dictType" />
-      </div>
-    </el-drawer>
-
-    <el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
-        <el-form-item label="字典名称" prop="dictName"><el-input v-model="form.dictName" /></el-form-item>
-        <el-form-item label="字典类型" prop="dictType"><el-input v-model="form.dictType" :disabled="form.dictId != null" /></el-form-item>
-        <el-form-item label="平台管控" prop="isManaged">
-          <el-radio-group v-model="form.isManaged">
-            <el-radio :label="1">是(同步时可覆盖)</el-radio>
-            <el-radio :label="0">否(同步时仅追加)</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="默认同步模式" prop="syncMode">
-          <el-select v-model="form.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-form-item label="状态" prop="status">
-          <el-radio-group v-model="form.status">
-            <el-radio v-for="dict in statusOptions" :key="dict.dictValue" :label="dict.dictValue">{{ dict.dictLabel }}</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" /></el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog title="从字典库导入到平台模板" :visible.sync="importOpen" width="800px" append-to-body class="import-dialog">
-      <el-tabs v-model="importTab" class="import-tabs">
-        <el-tab-pane label="平台主库字典" name="platform">
-          <el-table v-loading="importLoading" :data="platformDictList" @selection-change="s => importPlatformSelection = s" max-height="360" size="small" border>
-            <el-table-column type="selection" width="45" />
-            <el-table-column label="字典名称" prop="dictName" min-width="140" />
-            <el-table-column label="字典类型" prop="dictType" min-width="160" />
-            <el-table-column label="状态" prop="status" width="80" align="center" />
-          </el-table>
-        </el-tab-pane>
-        <el-tab-pane label="租户字典" name="tenant">
-          <el-card shadow="never" class="mb8 import-filter">
-            <el-form inline size="small">
-              <el-form-item label="租户">
-                <inline-tenant-selector mode="admin" @change="onImportTenantChange" />
-              </el-form-item>
-              <el-form-item>
-                <el-button type="primary" size="mini" icon="el-icon-search" @click="loadTenantDictList" :disabled="!importTenantId">加载字典</el-button>
-              </el-form-item>
-            </el-form>
-          </el-card>
-          <el-table v-loading="importLoading" :data="tenantDictList" @selection-change="s => importTenantSelection = s" max-height="300" size="small" border>
-            <el-table-column type="selection" width="45" />
-            <el-table-column label="字典名称" prop="dictName" min-width="140" />
-            <el-table-column label="字典类型" prop="dictType" min-width="160" />
-            <el-table-column label="来源" prop="dictSource" width="80" align="center" />
-          </el-table>
-        </el-tab-pane>
-      </el-tabs>
-      <div class="import-options">
-        <el-checkbox v-model="importOverwrite">已存在同类型时覆盖更新(否则仅追加缺失项)</el-checkbox>
-      </div>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" :loading="importSubmitting" icon="el-icon-upload2" @click="submitImport">导入选中</el-button>
-        <el-button @click="importOpen = false">取 消</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import InlineTenantSelector from '@/components/InlineTenantSelector'
-import TemplateDataPanel from './TemplateDataPanel'
-import { listType } from '@/api/system/dict/type'
-import { listTenantDictType } from '@/api/tenant/dict'
-import {
-  listTemplateDictType, getTemplateDictType, addTemplateDictType,
-  updateTemplateDictType, delTemplateDictType,
-  importTemplateFromPlatform, importTemplateFromTenant
-} from '@/api/tenant/dict'
-
-export default {
-  name: 'TenantDictTemplate',
-  components: { InlineTenantSelector, TemplateDataPanel },
-  data() {
-    return {
-      loading: true, ids: [], single: true, multiple: true, showSearch: true, total: 0, typeList: [],
-      title: '', open: false, statusOptions: [],
-      queryParams: { pageNum: 1, pageSize: 10, dictName: undefined, dictType: undefined },
-      form: {},
-      rules: {
-        dictName: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
-        dictType: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
-        syncMode: [{ required: true, message: '请选择同步模式', trigger: 'change' }]
-      },
-      dataDrawerVisible: false,
-      currentType: {},
-      importOpen: false,
-      importTab: 'platform',
-      importLoading: false,
-      importSubmitting: false,
-      importOverwrite: false,
-      platformDictList: [],
-      importPlatformSelection: [],
-      importTenantId: null,
-      tenantDictList: [],
-      importTenantSelection: []
-    }
-  },
-  created() {
-    this.getList()
-    this.getDicts('sys_normal_disable').then(r => { this.statusOptions = r.data })
-  },
-  methods: {
-    getList() {
-      this.loading = true
-      listTemplateDictType(this.queryParams).then(r => {
-        this.typeList = r.rows; this.total = r.total; this.loading = false
-      }).catch(() => { this.loading = false })
-    },
-    openDataDrawer(row) {
-      this.currentType = { dictId: row.dictId, dictType: row.dictType, dictName: row.dictName }
-      this.dataDrawerVisible = true
-    },
-    openImportDialog() {
-      this.importOpen = true
-      this.importTab = 'platform'
-      this.loadPlatformDictList()
-    },
-    loadPlatformDictList() {
-      this.importLoading = true
-      listType({ pageNum: 1, pageSize: 500 }).then(r => {
-        this.platformDictList = r.rows || []
-        this.importLoading = false
-      }).catch(() => { this.importLoading = false })
-    },
-    onImportTenantChange(val) {
-      this.importTenantId = val || null
-      this.tenantDictList = []
-      this.importTenantSelection = []
-    },
-    loadTenantDictList() {
-      if (!this.importTenantId) return
-      this.importLoading = true
-      listTenantDictType(this.importTenantId, { pageNum: 1, pageSize: 500 }).then(r => {
-        this.tenantDictList = r.rows || []
-        this.importLoading = false
-      }).catch(() => { this.importLoading = false })
-    },
-    submitImport() {
-      const overwriteExisting = this.importOverwrite
-      if (this.importTab === 'platform') {
-        if (!this.importPlatformSelection.length) {
-          this.$message.warning('请选择要导入的平台字典')
-          return
-        }
-        this.importSubmitting = true
-        importTemplateFromPlatform({
-          dictIds: this.importPlatformSelection.map(i => i.dictId),
-          overwriteExisting
-        }).then(r => {
-          this.$message.success(r.data.message || '导入成功')
-          this.importOpen = false
-          this.getList()
-        }).finally(() => { this.importSubmitting = false })
-      } else {
-        if (!this.importTenantId || !this.importTenantSelection.length) {
-          this.$message.warning('请选择租户并勾选字典')
-          return
-        }
-        this.importSubmitting = true
-        importTemplateFromTenant({
-          tenantId: this.importTenantId,
-          dictTypes: this.importTenantSelection.map(i => i.dictType),
-          overwriteExisting
-        }).then(r => {
-          this.$message.success(r.data.message || '导入成功')
-          this.importOpen = false
-          this.getList()
-        }).finally(() => { this.importSubmitting = false })
-      }
-    },
-    cancel() { this.open = false; this.reset() },
-    reset() {
-      this.form = { dictId: undefined, dictName: undefined, dictType: undefined, status: '0', isManaged: 0, syncMode: 'MERGE', remark: undefined }
-      this.resetForm('form')
-    },
-    handleQuery() { this.queryParams.pageNum = 1; this.getList() },
-    resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
-    handleAdd() { this.reset(); this.open = true; this.title = '添加模板字典类型' },
-    handleSelectionChange(s) { this.ids = s.map(i => i.dictId); this.single = s.length !== 1; this.multiple = !s.length },
-    handleUpdate(row) {
-      this.reset()
-      getTemplateDictType(row.dictId || this.ids[0]).then(r => { this.form = r.data; this.open = true; this.title = '修改模板字典类型' })
-    },
-    submitForm() {
-      this.$refs['form'].validate(valid => {
-        if (!valid) return
-        const req = this.form.dictId != null ? updateTemplateDictType(this.form) : addTemplateDictType(this.form)
-        req.then(() => { this.msgSuccess('保存成功'); this.open = false; this.getList() })
-      })
-    },
-    handleDelete(row) {
-      const ids = row.dictId || this.ids.join(',')
-      this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => delTemplateDictType(ids))
-        .then(() => { this.getList(); this.msgSuccess('删除成功') }).catch(() => {})
-    }
-  }
-}
-</script>
-
-<style scoped>
-.tip-banner {
-  display: flex;
-  align-items: flex-start;
-  gap: 8px;
-  padding: 10px 14px;
-  margin-bottom: 16px;
-  background: #fdf6ec;
-  border: 1px solid #faecd8;
-  border-radius: 8px;
-  font-size: 13px;
-  color: #e6a23c;
-  line-height: 1.5;
-}
-.tip-banner i { font-size: 16px; margin-top: 1px; }
-.mb8 { margin-bottom: 8px; }
-.mb16 { margin-bottom: 16px; }
-.filter-card { padding-bottom: 0; }
-.toolbar-row { display: flex; flex-wrap: wrap; align-items: center; }
-.dict-table { width: 100%; }
-.drawer-body-wrap { padding: 0 20px 20px; }
-.drawer-type-meta { margin-bottom: 12px; }
-.import-filter { padding-bottom: 0; background: #fafafa; }
-.import-options {
-  margin-top: 16px;
-  padding: 12px 14px;
-  background: #f5f7fa;
-  border-radius: 6px;
-}
-</style>
-
-<style>
-.dict-data-drawer .el-drawer__header {
-  margin-bottom: 8px;
-  padding-bottom: 16px;
-  border-bottom: 1px solid #ebeef5;
-  font-weight: 600;
-  color: #303133;
-}
-.dict-data-drawer .el-drawer__body { padding-top: 0; }
-</style>
-
+<template>
+  <div class="dict-tab-panel">
+    <div class="tip-banner">
+      <i class="el-icon-info" />
+      <span>可从「字典维护」勾选字典后点「导入到平台模板」,或在此从平台主库 / 租户库批量选取导入</span>
+    </div>
+
+    <el-card shadow="never" class="mb16 filter-card" v-show="showSearch">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="72px">
+        <el-form-item label="字典名称" prop="dictName">
+          <el-input v-model="queryParams.dictName" placeholder="请输入字典名称" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="字典类型" prop="dictType">
+          <el-input v-model="queryParams.dictType" placeholder="请输入字典类型" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-row :gutter="10" class="mb8 toolbar-row">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-download" size="mini" @click="openImportDialog" v-hasPermi="['tenant:dict:template:add']">从字典库导入</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['tenant:dict:template:add']">手动新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <el-table v-loading="loading" :data="typeList" border size="small" @selection-change="handleSelectionChange" class="dict-table">
+      <el-table-column type="selection" width="48" align="center" />
+      <el-table-column label="编号" align="center" prop="dictId" width="76" />
+      <el-table-column label="字典名称" align="center" prop="dictName" min-width="120" :show-overflow-tooltip="true" />
+      <el-table-column label="字典类型" align="center" min-width="140">
+        <template slot-scope="scope">
+          <el-link type="primary" :underline="false" @click="openDataDrawer(scope.row)">{{ scope.row.dictType }}</el-link>
+        </template>
+      </el-table-column>
+      <el-table-column label="平台管控" align="center" width="88">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.isManaged === 1 ? 'danger' : 'info'">{{ scope.row.isManaged === 1 ? '是' : '否' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="默认同步模式" align="center" prop="syncMode" width="120">
+        <template slot-scope="scope">
+          <el-tag size="mini" type="info" effect="plain">{{ scope.row.syncMode }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" width="76">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="200" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-view" @click="openDataDrawer(scope.row)">字典数据</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['tenant:dict:template:edit']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" style="color:#f5222d" @click="handleDelete(scope.row)" v-hasPermi="['tenant:dict:template:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <el-drawer
+      :title="'模板字典数据 · ' + (currentType.dictName || currentType.dictType || '')"
+      :visible.sync="dataDrawerVisible"
+      size="62%"
+      append-to-body
+      destroy-on-close
+      custom-class="dict-data-drawer"
+    >
+      <div class="drawer-body-wrap" v-if="dataDrawerVisible && currentType.dictType">
+        <div class="drawer-type-meta">
+          <el-tag size="small" type="primary" effect="plain">{{ currentType.dictType }}</el-tag>
+        </div>
+        <template-data-panel :dict-type="currentType.dictType" />
+      </div>
+    </el-drawer>
+
+    <el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
+        <el-form-item label="字典名称" prop="dictName"><el-input v-model="form.dictName" /></el-form-item>
+        <el-form-item label="字典类型" prop="dictType"><el-input v-model="form.dictType" :disabled="form.dictId != null" /></el-form-item>
+        <el-form-item label="平台管控" prop="isManaged">
+          <el-radio-group v-model="form.isManaged">
+            <el-radio :label="1">是(同步时可覆盖)</el-radio>
+            <el-radio :label="0">否(同步时仅追加)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="默认同步模式" prop="syncMode">
+          <el-select v-model="form.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-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in statusOptions" :key="dict.dictValue" :label="dict.dictValue">{{ dict.dictLabel }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" /></el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="从字典库导入到平台模板" :visible.sync="importOpen" width="800px" append-to-body class="import-dialog">
+      <el-tabs v-model="importTab" class="import-tabs">
+        <el-tab-pane label="平台主库字典" name="platform">
+          <el-table v-loading="importLoading" :data="platformDictList" @selection-change="s => importPlatformSelection = s" max-height="360" size="small" border>
+            <el-table-column type="selection" width="45" />
+            <el-table-column label="字典名称" prop="dictName" min-width="140" />
+            <el-table-column label="字典类型" prop="dictType" min-width="160" />
+            <el-table-column label="状态" prop="status" width="80" align="center" />
+          </el-table>
+        </el-tab-pane>
+        <el-tab-pane label="租户字典" name="tenant">
+          <el-card shadow="never" class="mb8 import-filter">
+            <el-form inline size="small">
+              <el-form-item label="租户">
+                <inline-tenant-selector mode="admin" @change="onImportTenantChange" />
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" size="mini" icon="el-icon-search" @click="loadTenantDictList" :disabled="!importTenantId">加载字典</el-button>
+              </el-form-item>
+            </el-form>
+          </el-card>
+          <el-table v-loading="importLoading" :data="tenantDictList" @selection-change="s => importTenantSelection = s" max-height="300" size="small" border>
+            <el-table-column type="selection" width="45" />
+            <el-table-column label="字典名称" prop="dictName" min-width="140" />
+            <el-table-column label="字典类型" prop="dictType" min-width="160" />
+            <el-table-column label="来源" prop="dictSource" width="80" align="center" />
+          </el-table>
+        </el-tab-pane>
+      </el-tabs>
+      <div class="import-options">
+        <el-checkbox v-model="importOverwrite">已存在同类型时覆盖更新(否则仅追加缺失项)</el-checkbox>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="importSubmitting" icon="el-icon-upload2" @click="submitImport">导入选中</el-button>
+        <el-button @click="importOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import InlineTenantSelector from '@/components/InlineTenantSelector'
+import TemplateDataPanel from './TemplateDataPanel'
+import { listType } from '@/api/system/dict/type'
+import { listTenantDictType } from '@/api/tenant/dict'
+import {
+  listTemplateDictType, getTemplateDictType, addTemplateDictType,
+  updateTemplateDictType, delTemplateDictType,
+  importTemplateFromPlatform, importTemplateFromTenant
+} from '@/api/tenant/dict'
+
+export default {
+  name: 'TenantDictTemplate',
+  components: { InlineTenantSelector, TemplateDataPanel },
+  data() {
+    return {
+      loading: true, ids: [], single: true, multiple: true, showSearch: true, total: 0, typeList: [],
+      title: '', open: false, statusOptions: [],
+      queryParams: { pageNum: 1, pageSize: 10, dictName: undefined, dictType: undefined },
+      form: {},
+      rules: {
+        dictName: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
+        dictType: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
+        syncMode: [{ required: true, message: '请选择同步模式', trigger: 'change' }]
+      },
+      dataDrawerVisible: false,
+      currentType: {},
+      importOpen: false,
+      importTab: 'platform',
+      importLoading: false,
+      importSubmitting: false,
+      importOverwrite: false,
+      platformDictList: [],
+      importPlatformSelection: [],
+      importTenantId: null,
+      tenantDictList: [],
+      importTenantSelection: []
+    }
+  },
+  created() {
+    this.getList()
+    this.getDicts('sys_normal_disable').then(r => { this.statusOptions = r.data })
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listTemplateDictType(this.queryParams).then(r => {
+        this.typeList = r.rows; this.total = r.total; this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    openDataDrawer(row) {
+      this.currentType = { dictId: row.dictId, dictType: row.dictType, dictName: row.dictName }
+      this.dataDrawerVisible = true
+    },
+    openImportDialog() {
+      this.importOpen = true
+      this.importTab = 'platform'
+      this.loadPlatformDictList()
+    },
+    loadPlatformDictList() {
+      this.importLoading = true
+      listType({ pageNum: 1, pageSize: 500 }).then(r => {
+        this.platformDictList = r.rows || []
+        this.importLoading = false
+      }).catch(() => { this.importLoading = false })
+    },
+    onImportTenantChange(val) {
+      this.importTenantId = val || null
+      this.tenantDictList = []
+      this.importTenantSelection = []
+    },
+    loadTenantDictList() {
+      if (!this.importTenantId) return
+      this.importLoading = true
+      listTenantDictType(this.importTenantId, { pageNum: 1, pageSize: 500 }).then(r => {
+        this.tenantDictList = r.rows || []
+        this.importLoading = false
+      }).catch(() => { this.importLoading = false })
+    },
+    submitImport() {
+      const overwriteExisting = this.importOverwrite
+      if (this.importTab === 'platform') {
+        if (!this.importPlatformSelection.length) {
+          this.$message.warning('请选择要导入的平台字典')
+          return
+        }
+        this.importSubmitting = true
+        importTemplateFromPlatform({
+          dictIds: this.importPlatformSelection.map(i => i.dictId),
+          overwriteExisting
+        }).then(r => {
+          this.$message.success(r.data.message || '导入成功')
+          this.importOpen = false
+          this.getList()
+        }).finally(() => { this.importSubmitting = false })
+      } else {
+        if (!this.importTenantId || !this.importTenantSelection.length) {
+          this.$message.warning('请选择租户并勾选字典')
+          return
+        }
+        this.importSubmitting = true
+        importTemplateFromTenant({
+          tenantId: this.importTenantId,
+          dictTypes: this.importTenantSelection.map(i => i.dictType),
+          overwriteExisting
+        }).then(r => {
+          this.$message.success(r.data.message || '导入成功')
+          this.importOpen = false
+          this.getList()
+        }).finally(() => { this.importSubmitting = false })
+      }
+    },
+    cancel() { this.open = false; this.reset() },
+    reset() {
+      this.form = { dictId: undefined, dictName: undefined, dictType: undefined, status: '0', isManaged: 0, syncMode: 'MERGE', remark: undefined }
+      this.resetForm('form')
+    },
+    handleQuery() { this.queryParams.pageNum = 1; this.getList() },
+    resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
+    handleAdd() { this.reset(); this.open = true; this.title = '添加模板字典类型' },
+    handleSelectionChange(s) { this.ids = s.map(i => i.dictId); this.single = s.length !== 1; this.multiple = !s.length },
+    handleUpdate(row) {
+      this.reset()
+      getTemplateDictType(row.dictId || this.ids[0]).then(r => { this.form = r.data; this.open = true; this.title = '修改模板字典类型' })
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) return
+        const req = this.form.dictId != null ? updateTemplateDictType(this.form) : addTemplateDictType(this.form)
+        req.then(() => { this.msgSuccess('保存成功'); this.open = false; this.getList() })
+      })
+    },
+    handleDelete(row) {
+      const ids = row.dictId || this.ids.join(',')
+      this.$confirm('确认删除?', '警告', { type: 'warning' }).then(() => delTemplateDictType(ids))
+        .then(() => { this.getList(); this.msgSuccess('删除成功') }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.tip-banner {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  padding: 10px 14px;
+  margin-bottom: 16px;
+  background: #fdf6ec;
+  border: 1px solid #faecd8;
+  border-radius: 8px;
+  font-size: 13px;
+  color: #e6a23c;
+  line-height: 1.5;
+}
+.tip-banner i { font-size: 16px; margin-top: 1px; }
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+.toolbar-row { display: flex; flex-wrap: wrap; align-items: center; }
+.dict-table { width: 100%; }
+.drawer-body-wrap { padding: 0 20px 20px; }
+.drawer-type-meta { margin-bottom: 12px; }
+.import-filter { padding-bottom: 0; background: #fafafa; }
+.import-options {
+  margin-top: 16px;
+  padding: 12px 14px;
+  background: #f5f7fa;
+  border-radius: 6px;
+}
+</style>
+
+<style>
+.dict-data-drawer .el-drawer__header {
+  margin-bottom: 8px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #ebeef5;
+  font-weight: 600;
+  color: #303133;
+}
+.dict-data-drawer .el-drawer__body { padding-top: 0; }
+</style>
+

+ 470 - 0
src/views/admin/tenantMenu/index.vue

@@ -0,0 +1,470 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="菜单名称" prop="menuName">
+        <el-input
+          v-model="queryParams.menuName"
+          placeholder="请输入菜单名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="菜单状态" clearable size="small">
+          <el-option
+            v-for="dict in statusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['system:menu:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          icon="el-icon-sort"
+          size="mini"
+          @click="toggleExpandAll"
+        >展开/折叠</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table
+      v-if="refreshTable"
+      v-loading="loading"
+      :data="menuList"
+      row-key="menuId"
+      :default-expand-all="isExpandAll"
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+    >
+      <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
+      <el-table-column prop="icon" label="图标" align="center" width="100">
+        <template slot-scope="scope">
+          <svg-icon :icon-class="scope.row.icon" />
+        </template>
+      </el-table-column>
+      <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
+      <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
+      <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
+      <el-table-column prop="status" label="状态" width="80">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini"
+                     type="text"
+                     icon="el-icon-edit"
+                     @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:menu:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-plus"
+            @click="handleAdd(scope.row)"
+            v-hasPermi="['system:menu:add']"
+          >新增</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['system:menu:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 添加或修改菜单对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="上级菜单">
+              <treeselect
+                v-model="form.parentId"
+                :options="menuOptions"
+                :normalizer="normalizer"
+                :show-count="true"
+                placeholder="选择上级菜单"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="菜单类型" prop="menuType">
+              <el-radio-group v-model="form.menuType">
+                <el-radio label="M">目录</el-radio>
+                <el-radio label="C">菜单</el-radio>
+                <el-radio label="F">按钮</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item v-if="form.menuType != 'F'" label="菜单图标">
+              <el-popover
+                placement="bottom-start"
+                width="460"
+                trigger="click"
+                @show="$refs['iconSelect'].reset()"
+              >
+                <IconSelect ref="iconSelect" @selected="selected" />
+                <el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
+                  <svg-icon
+                    v-if="form.icon"
+                    slot="prefix"
+                    :icon-class="form.icon"
+                    class="el-input__icon"
+                    style="height: 32px;width: 16px;"
+                  />
+                  <i v-else slot="prefix" class="el-icon-search el-input__icon" />
+                </el-input>
+              </el-popover>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="菜单名称" prop="menuName">
+              <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="显示排序" prop="orderNum">
+              <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType != 'F'">
+              <span slot="label">
+                <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                是否外链
+              </span>
+              <el-radio-group v-model="form.isFrame">
+                <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 v-if="form.menuType != 'F'" prop="path">
+              <span slot="label">
+                <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                路由地址
+              </span>
+              <el-input v-model="form.path" placeholder="请输入路由地址" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType == 'C'">
+            <el-form-item prop="component">
+              <span slot="label">
+                <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                组件路径
+              </span>
+              <el-input v-model="form.component" placeholder="请输入组件路径" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType != 'M'">
+              <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
+              <span slot="label">
+                <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                权限字符
+              </span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType == 'C'">
+              <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
+              <span slot="label">
+                <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                路由参数
+              </span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType == 'C'">
+              <span slot="label">
+                <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                是否缓存
+              </span>
+              <el-radio-group v-model="form.isCache">
+                <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 v-if="form.menuType != 'F'">
+              <span slot="label">
+                <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                显示状态
+              </span>
+              <el-radio-group v-model="form.visible">
+                <el-radio
+                  v-for="dict in visibleOptions"
+                  :key="dict.dictValue"
+                  :label="dict.dictValue"
+                >{{dict.dictLabel}}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.menuType != 'F'">
+              <span slot="label">
+                <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                菜单状态
+              </span>
+              <el-radio-group v-model="form.status">
+                <el-radio
+                  v-for="dict in statusOptions"
+                  :key="dict.dictValue"
+                  :label="dict.dictValue"
+                >{{dict.dictLabel}}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { tenantMenu,addTenantMenu,updateTenantMenu,getTenantMenu,delTenantMenu } from "@/api/system/menu";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import IconSelect from "@/components/IconSelect";
+
+/**
+ * 租户管理端菜单模板维护(主库 tenant_sys_menu)
+ * 新建租户时从此模板复制到租户库 sys_menu
+ */
+export default {
+  name: "AdminTenantSysMenu",
+  components: { Treeselect, IconSelect },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 菜单表格树数据
+      menuList: [],
+      // 菜单树选项
+      menuOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 是否展开,默认全部折叠
+      isExpandAll: false,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 显示状态数据字典
+      visibleOptions: [],
+      // 菜单状态数据字典
+      statusOptions: [],
+      // 查询参数
+      queryParams: {
+        menuName: undefined,
+        visible: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        menuName: [
+          { required: true, message: "菜单名称不能为空", trigger: "blur" }
+        ],
+        orderNum: [
+          { required: true, message: "菜单顺序不能为空", trigger: "blur" }
+        ],
+        path: [
+          { required: true, message: "路由地址不能为空", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+    this.getDicts("sys_show_hide").then(response => {
+      this.visibleOptions = response.data;
+    });
+    this.getDicts("sys_normal_disable").then(response => {
+      this.statusOptions = response.data;
+    });
+  },
+  methods: {
+    // 选择图标
+    selected(name) {
+      this.form.icon = name;
+    },
+    /** 查询菜单列表 */
+    getList() {
+      this.loading = true;
+      tenantMenu(this.queryParams).then(response => {
+        this.menuList = this.handleTree(response.data, "menuId");
+        this.loading = false;
+      });
+    },
+    /** 转换菜单数据结构 */
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children;
+      }
+      return {
+        id: node.menuId,
+        label: node.menuName,
+        children: node.children
+      };
+    },
+    /** 查询菜单下拉树结构 */
+    getTreeselect() {
+      tenantMenu().then(response => {
+        this.menuOptions = [];
+        const menu = { menuId: 0, menuName: '主类目', children: [] };
+        menu.children = this.handleTree(response.data, "menuId");
+        this.menuOptions.push(menu);
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        menuId: undefined,
+        parentId: 0,
+        menuName: undefined,
+        icon: undefined,
+        menuType: "M",
+        orderNum: undefined,
+        isFrame: "1",
+        isCache: "0",
+        visible: "0",
+        status: "0"
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd(row) {
+      this.reset();
+      this.getTreeselect();
+      if (row != null && row.menuId) {
+        this.form.parentId = row.menuId;
+      } else {
+        this.form.parentId = 0;
+      }
+      this.open = true;
+      this.title = "添加菜单";
+    },
+    /** 展开/折叠操作 */
+    toggleExpandAll() {
+      this.refreshTable = false;
+      this.isExpandAll = !this.isExpandAll;
+      this.$nextTick(() => {
+        this.refreshTable = true;
+      });
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.getTreeselect();
+      getTenantMenu(row.menuId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改菜单";
+      });
+    },
+    /** 提交按钮 */
+    submitForm: function() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.menuId != undefined) {
+            updateTenantMenu(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addTenantMenu(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      this.$confirm('是否确认删除名称为"' + row.menuName + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delTenantMenu(row.menuId);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
+    }
+  }
+};
+</script>