|
|
@@ -0,0 +1,132 @@
|
|
|
+<template>
|
|
|
+ <el-form-item class="tenant-select-item">
|
|
|
+ <el-select
|
|
|
+ v-if="options.length > 0"
|
|
|
+ v-model="value"
|
|
|
+ class="tenant-select"
|
|
|
+ placeholder="请选择租户"
|
|
|
+ :loading="loading"
|
|
|
+ @change="persist"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in options"
|
|
|
+ :key="item.tenantId"
|
|
|
+ :label="item.tenantName"
|
|
|
+ :value="item.tenantId"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <el-input
|
|
|
+ v-else
|
|
|
+ v-model="manualValue"
|
|
|
+ class="tenant-select"
|
|
|
+ clearable
|
|
|
+ placeholder="租户 ID"
|
|
|
+ @change="persist"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import request from '@/utils/request';
|
|
|
+import { getTenantId, setTenantId, removeTenantId } from '@/utils/tenant-util';
|
|
|
+
|
|
|
+/**
|
|
|
+ * 登录页租户选择器(多租户)。
|
|
|
+ *
|
|
|
+ * 为什么必须在登录页加:库隔离下账号只存在于各租户库中,服务端要在认证之前
|
|
|
+ * 就知道连哪个库,因此登录请求必须携带 X-Tenant-ID。
|
|
|
+ * token 签发之后租户写入 claim,后续请求由服务端以 claim 为准,本组件不再参与。
|
|
|
+ *
|
|
|
+ * 行为约定:
|
|
|
+ * - 挂载时拉取 /main/tenant/list(匿名接口,只返回启用且当前实例已生效的租户);
|
|
|
+ * - 只有一个租户时自动选中 —— 单租户部署的体验与改造前完全一致(零点击);
|
|
|
+ * - 多个租户时不预设默认值,强制用户显式选择,避免"默认登进某个租户"的误操作;
|
|
|
+ * - 清单接口不可用时退化为手填租户 ID,保证异常情况下运维仍能进系统;
|
|
|
+ * - 不提供"平台运营端"入口:该凭证只能访问 /sys/tenant/**,本前端没有对应界面,
|
|
|
+ * 登录后所有业务接口都会被租户隔离切面拒绝,故登录页只允许选择真实租户。
|
|
|
+ */
|
|
|
+export default {
|
|
|
+ name: 'TenantSelect',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ options: [],
|
|
|
+ value: '',
|
|
|
+ manualValue: ''
|
|
|
+ };
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.restore();
|
|
|
+ this.loadOptions();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /* 回填上次选择 */
|
|
|
+ restore() {
|
|
|
+ const stored = getTenantId();
|
|
|
+ if (stored === '') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.manualValue = stored;
|
|
|
+ // 与选项值保持同一类型:后端把 Long 序列化成字符串,平台项也是字符串 '0'。
|
|
|
+ // 一旦 Number 化,applyDefault 的全等比较就会失配,刷新后下拉框被清空而请求仍带着旧租户。
|
|
|
+ this.value = stored;
|
|
|
+ },
|
|
|
+ /* 拉取租户清单 */
|
|
|
+ loadOptions() {
|
|
|
+ this.loading = true;
|
|
|
+ request
|
|
|
+ .get('/main/tenant/list')
|
|
|
+ .then((res) => {
|
|
|
+ const data = res && res.data ? res.data.data : null;
|
|
|
+ this.options = Array.isArray(data)
|
|
|
+ ? data.filter((item) => item && item.tenantId !== null && item.tenantId !== undefined)
|
|
|
+ : [];
|
|
|
+ this.applyDefault();
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 拉不到清单时退化为手填:接口异常不该把登录整个堵死
|
|
|
+ this.options = [];
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ this.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /* 决定默认选中项 */
|
|
|
+ applyDefault() {
|
|
|
+ if (this.options.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const hit = this.options.some(
|
|
|
+ (item) => String(item.tenantId) === String(this.value)
|
|
|
+ );
|
|
|
+ if (hit) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.options.length === 1) {
|
|
|
+ // 单租户部署:自动选中,用户无需感知多租户
|
|
|
+ this.value = this.options[0].tenantId;
|
|
|
+ this.persist(this.value);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 多租户:不预设默认值。上次选择的租户已不在清单里(停用/到期/本实例无数据源)时,
|
|
|
+ // 必须把落盘的值一并清掉,否则界面显示"请选择租户"、请求却仍带着失效租户 ID 被服务端拒绝。
|
|
|
+ this.value = '';
|
|
|
+ removeTenantId();
|
|
|
+ },
|
|
|
+ /* 落盘选择结果 */
|
|
|
+ persist(val) {
|
|
|
+ if (val === null || val === undefined || String(val).trim() === '') {
|
|
|
+ removeTenantId();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setTenantId(val);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.tenant-select {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+</style>
|