diff --git a/src/api/system/index.js b/src/api/system/index.js
index 69638dd..ea3bd94 100644
--- a/src/api/system/index.js
+++ b/src/api/system/index.js
@@ -18,6 +18,7 @@ export { default as modelFieldServer } from './modelFieldServer'
export { default as modelServer } from './modelServer'
export { default as paramSetupServer } from './paramSetupServer'
export { default as sysControlTypeServer } from './sysControlTypeServer'
+export { default as sysTenantServer } from './sysTenantServer'
diff --git a/src/api/system/sysTenantServer.js b/src/api/system/sysTenantServer.js
new file mode 100644
index 0000000..32d1ead
--- /dev/null
+++ b/src/api/system/sysTenantServer.js
@@ -0,0 +1,22 @@
+import http from '@/utils/http'
+
+class SysTenantServer {
+ async list() {
+ return http.get('/sys_tenant/index', {})
+ }
+
+ async add(row) {
+ return http.post('/sys_tenant/add', row)
+ }
+
+ async edit(row) {
+ return http.post('/sys_tenant/edit', row)
+ }
+
+ async del(row) {
+ return http.post('/sys_tenant/del', row)
+ }
+}
+
+const sysTenantServer = new SysTenantServer()
+export default sysTenantServer
diff --git a/src/assets/css/ivewExpand.less b/src/assets/css/ivewExpand.less
index 1f1a1f1..0f144ff 100644
--- a/src/assets/css/ivewExpand.less
+++ b/src/assets/css/ivewExpand.less
@@ -186,20 +186,21 @@
text-overflow: ellipsis;
}
-/* 自适应表格宽度 */
+/* 与 View Design 一致:横向/纵向滚动由 .ivu-table-body 上的 overflow 类控制,避免 wrapper 与整表同高导致横向条在页面最底 */
.ivu-table-wrapper {
width: 100%;
height: 100%;
- overflow-x: auto;
- overflow-y: auto;
+ overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
+ min-height: 0;
}
.ivu-table {
width: 100% !important;
flex: 1;
+ min-height: 0;
}
/* 移除 table-layout: auto,使用 iView 默认的 fixed 布局以确保列对齐 */
@@ -208,8 +209,8 @@
} */
.ivu-table-body {
- overflow-y: auto;
flex: 1;
+ min-height: 0;
}
/* 按钮风格一致 */
diff --git a/src/components/login-form/login-form.vue b/src/components/login-form/login-form.vue
index e0399a6..2784779 100644
--- a/src/components/login-form/login-form.vue
+++ b/src/components/login-form/login-form.vue
@@ -14,6 +14,13 @@
+
+
+
+
+
+
+
@@ -40,7 +47,8 @@ export default {
return {
form: {
userName: '',
- password: ''
+ password: '',
+ tenantCode: 'default'
}
}
},
@@ -48,7 +56,8 @@ export default {
rules() {
return {
userName: this.userNameRules,
- password: this.passwordRules
+ password: this.passwordRules,
+ tenantCode: [{ required: false }]
}
}
},
@@ -58,7 +67,8 @@ export default {
if (valid) {
this.$emit('on-success-valid', {
userName: this.form.userName,
- password: this.form.password
+ password: this.form.password,
+ tenantCode: (this.form.tenantCode || '').trim() || 'default'
})
}
})
diff --git a/src/components/main/main.vue b/src/components/main/main.vue
index 56eaffb..73dd524 100644
--- a/src/components/main/main.vue
+++ b/src/components/main/main.vue
@@ -17,6 +17,11 @@
@@ -42,7 +47,7 @@ import User from './components/user'
import ABackTop from './components/a-back-top'
import Fullscreen from './components/fullscreen'
import Language from './components/language'
-import { mapGetters } from 'vuex'
+import { mapGetters, mapState } from 'vuex'
import headerBar from './components/header-bar'
import loadFlower from '../load-flower/index'
import Terminal from './components/terminal'
@@ -77,6 +82,7 @@ export default {
userName: 'user/userName',
userAvator: 'user/avatorImgPath'
}),
+ ...mapState('user', ['currentTenant']),
cacheList() {
return ['ParentView']
}
@@ -149,6 +155,21 @@ export default {
z-index: 999999;
}
+.main-tenant-tag {
+ margin-right: 16px;
+ padding: 0 10px;
+ height: 28px;
+ line-height: 28px;
+ font-size: 13px;
+ color: #2d8cf0;
+ background: rgba(45, 140, 240, 0.08);
+ border-radius: 4px;
+ max-width: 160px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.sidebar-brand {
display: flex;
justify-content: center;
diff --git a/src/components/tables/index.vue b/src/components/tables/index.vue
index 776dcfb..cf60791 100644
--- a/src/components/tables/index.vue
+++ b/src/components/tables/index.vue
@@ -13,11 +13,24 @@
下载
-
+
@@ -81,6 +94,9 @@ export default {
},
// 动态计算的偏移量
calculatedOffset: null,
+ /** 传给 iView Table 的 max-height(px),使表体在可视区内滚动,横向条始终在视口底部 */
+ bodyViewportMaxHeight: null,
+ _tableContentResizeObserver: null,
}
},
@@ -145,6 +161,13 @@ export default {
// 使用手动指定的偏移量
return `calc(100vh - ${this.maxHeightOffset}px)`
},
+
+ /** 未传 height 时由容器测量得到,避免宽表横向滚动条落在整表最底部 */
+ effectiveTableMaxHeight() {
+ if (this.height) return undefined
+ if (this.bodyViewportMaxHeight == null) return undefined
+ return this.bodyViewportMaxHeight
+ },
},
methods: {
@@ -174,6 +197,7 @@ export default {
// 触发表格重新渲染
this.$refs.tablesMain.$forceUpdate()
}
+ this.syncTableBodyMaxHeight()
})
},
onChangePage(page) {
@@ -184,6 +208,43 @@ export default {
this.$emit('on-select', selection, row)
},
+ syncTableBodyMaxHeight() {
+ if (this.height) {
+ this.bodyViewportMaxHeight = null
+ return
+ }
+ this.$nextTick(() => {
+ const wrap = this.$refs.table_content
+ if (!wrap) return
+ const titleEl = wrap.querySelector('.table-title')
+ const titleH = (this.title || this.isDown) && titleEl ? titleEl.offsetHeight : 0
+ const avail = wrap.clientHeight - titleH
+ const next = Math.max(160, Math.floor(avail))
+ if (this.bodyViewportMaxHeight !== next) {
+ this.bodyViewportMaxHeight = next
+ }
+ this.$nextTick(() => {
+ const inst = this.$refs.tablesMain
+ if (inst && typeof inst.handleResize === 'function') {
+ inst.handleResize()
+ }
+ })
+ })
+ },
+
+ setupTableContentResizeObserver() {
+ const el = this.$refs.table_content
+ if (!el || typeof ResizeObserver === 'undefined') return
+ if (this._tableContentResizeObserver) {
+ this._tableContentResizeObserver.disconnect()
+ }
+ const ro = new ResizeObserver(() => {
+ this.syncTableBodyMaxHeight()
+ })
+ ro.observe(el)
+ this._tableContentResizeObserver = ro
+ },
+
// 动态计算表格容器上方所有元素的高度总和
calculateOffset() {
this.$nextTick(() => {
@@ -203,6 +264,9 @@ export default {
// 计算总偏移量
this.calculatedOffset = topOffset + pageBoxHeight + bottomPadding
+ this.$nextTick(() => {
+ this.syncTableBodyMaxHeight()
+ })
})
},
},
@@ -243,6 +307,7 @@ export default {
// 动态计算偏移量
this.calculateOffset()
+ this.setupTableContentResizeObserver()
// 如果数据或列配置为空,输出警告
if (!this.insideTableData || this.insideTableData.length === 0) {
@@ -255,11 +320,17 @@ export default {
// 监听窗口大小变化,重新计算偏移量
window.addEventListener('resize', this.calculateOffset)
+ window.addEventListener('resize', this.syncTableBodyMaxHeight)
},
beforeDestroy() {
// 移除窗口大小监听
window.removeEventListener('resize', this.calculateOffset)
+ window.removeEventListener('resize', this.syncTableBodyMaxHeight)
+ if (this._tableContentResizeObserver) {
+ this._tableContentResizeObserver.disconnect()
+ this._tableContentResizeObserver = null
+ }
},
}
@@ -268,8 +339,12 @@ export default {
width: 100%;
.table-content {
- overflow-y: auto;
+ /* 纵向由 Table 内部表体滚动;避免与外层双滚动导致横向条在整页最底 */
+ overflow: hidden;
width: 100%;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
.table-title {
font-weight: bold;
@@ -279,10 +354,17 @@ export default {
padding: 0rem 0.15rem;
height: 50px;
line-height: 50px;
+ flex-shrink: 0;
.right-link {
float: right;
}
}
+
+ .table-main {
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ }
}
.page-box {
diff --git a/src/config/menuConfig.js b/src/config/menuConfig.js
index 9af152c..2441481 100644
--- a/src/config/menuConfig.js
+++ b/src/config/menuConfig.js
@@ -115,6 +115,17 @@ export const defaultMenus = [
is_show_menu: 1,
icon: 'md-text',
sort: 3
+ },
+ {
+ id: 125,
+ name: '租户管理',
+ path: '/system/tenant',
+ component: 'system/sys_tenant',
+ parent_id: 120,
+ type: '页面',
+ is_show_menu: 1,
+ icon: 'md-git-branch',
+ sort: 4
}
]
}
diff --git a/src/store/user.js b/src/store/user.js
index 2bbdd42..35e83a7 100644
--- a/src/store/user.js
+++ b/src/store/user.js
@@ -3,6 +3,16 @@ import uiTool from '../utils/uiTool'
import { defaultMenus, filterMenusByIds } from '../config/menuConfig'
import userServer from '../api/system/userServer'
+function readTenantLs() {
+ try {
+ const s = localStorage.getItem('currentTenant')
+ if (!s || s === 'undefined') return null
+ return JSON.parse(s)
+ } catch {
+ return null
+ }
+}
+
export default {
namespaced: true,
state: {
@@ -10,7 +20,9 @@ export default {
avatorImgPath: '',
token: getToken(),
authorityMenus: [],
- menuList: localStorage.getItem('menuList') ? JSON.parse(localStorage.getItem('menuList')) : []
+ menuList: localStorage.getItem('menuList') ? JSON.parse(localStorage.getItem('menuList')) : [],
+ /** 登录返回的租户信息:{ id, name, code, is_platform } */
+ currentTenant: readTenantLs()
},
mutations: {
setAvator(state, avatorPath) {
@@ -31,6 +43,14 @@ export default {
setMenuList(state, menus) {
state.menuList = menus
localStorage.setItem('menuList', JSON.stringify(menus))
+ },
+ setCurrentTenant(state, tenant) {
+ state.currentTenant = tenant && typeof tenant === 'object' ? tenant : null
+ if (state.currentTenant) {
+ localStorage.setItem('currentTenant', JSON.stringify(state.currentTenant))
+ } else {
+ localStorage.removeItem('currentTenant')
+ }
}
},
getters: {
@@ -162,6 +182,11 @@ export default {
commit('setUserName', name)
commit('setToken', token)
+ if (res.data.tenant) {
+ commit('setCurrentTenant', res.data.tenant)
+ } else {
+ commit('setCurrentTenant', null)
+ }
// 登录接口返回的 authorityMenus 是菜单 ID 数组字符串
console.log('登录返回的菜单 IDs:', authorityMenusIds)
@@ -205,6 +230,7 @@ export default {
commit('setToken', '')
commit('setAuthorityMenus', '[]')
commit('setMenuList', [])
+ commit('setCurrentTenant', null)
localStorage.removeItem('menuList')
window.location.reload()
}
diff --git a/src/views/index.js b/src/views/index.js
index df359a9..e551f17 100644
--- a/src/views/index.js
+++ b/src/views/index.js
@@ -4,6 +4,7 @@ import SysLog from './system/sys_log.vue'
import SysParamSetup from './system/sys_param_setup.vue'
import SysRole from './system/sys_role.vue'
import SysUser from './system/sys_user.vue'
+import SysTenant from './system/sys_tenant.vue'
import SysLogOperate from './system/sys_log_operate.vue'
@@ -36,6 +37,7 @@ export function setupComponentMap(customMap = {}, uiTool) {
'system/sys_param_setup': SysParamSetup,
'system/sys_role': SysRole,
'system/sys_user': SysUser,
+ 'system/sys_tenant': SysTenant,
'system/sys_control': SysControl,
'system/sys_menu': SysMenu,
'system/sys_title': SysTitle,
@@ -60,6 +62,7 @@ export default {
SysParamSetup,
SysRole,
SysUser,
+ SysTenant,
SysControl,
SysMenu,
SysTitle,
diff --git a/src/views/login/login.vue b/src/views/login/login.vue
index 3d7ed73..76c7e80 100644
--- a/src/views/login/login.vue
+++ b/src/views/login/login.vue
@@ -43,9 +43,13 @@ export default {
},
methods: {
...mapActions('user', ['handleLogin']),
- async handleSubmit({ userName, password }) {
+ async handleSubmit({ userName, password, tenantCode }) {
try {
- let userFrom = { name: userName, password: password }
+ let userFrom = {
+ name: userName,
+ password: password,
+ tenant_code: tenantCode || 'default'
+ }
await this.handleLogin({
userFrom,
Main,
diff --git a/src/views/system/sys_role.vue b/src/views/system/sys_role.vue
index d982b6f..3213613 100644
--- a/src/views/system/sys_role.vue
+++ b/src/views/system/sys_role.vue
@@ -1,5 +1,8 @@
+
+ 角色为全库共用,不按租户隔离;各租户用户可在「用户管理」中绑定同一角色,菜单权限以角色配置为准。
+
@@ -73,6 +76,23 @@ export default {
this.init()
},
methods: {
+ parseRoleMenuIds(menus) {
+ if (menus == null || menus === '') {
+ return []
+ }
+ if (Array.isArray(menus)) {
+ return menus.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
+ }
+ if (typeof menus === 'string') {
+ try {
+ const parsed = JSON.parse(menus)
+ return this.parseRoleMenuIds(parsed)
+ } catch {
+ return []
+ }
+ }
+ return []
+ },
async init() {
let res = await roleServer.list()
this.gridOption.data = res.data
@@ -120,12 +140,8 @@ export default {
let res = await menuServer.list()
let tree = uiTool.transformTree(res.data)
- if (row.menus) {
- this.expandTreeAll = JSON.parse(row.menus)
- this.treeData = this.mapTree(tree)
- } else {
- this.treeData = this.mapTree(tree)
- }
+ this.expandTreeAll = this.parseRoleMenuIds(row.menus)
+ this.treeData = this.mapTree(tree)
this.isShowPermission = true
},
@@ -136,7 +152,7 @@ export default {
p.children = this.mapTree(p.children)
}
- let row = this.expandTreeAll.find((p2) => p2 === p.id)
+ let row = this.expandTreeAll.find((p2) => Number(p2) === Number(p.id))
if (row) {
p.checked = true
}
diff --git a/src/views/system/sys_tenant.vue b/src/views/system/sys_tenant.vue
new file mode 100644
index 0000000..ca9ea2d
--- /dev/null
+++ b/src/views/system/sys_tenant.vue
@@ -0,0 +1,127 @@
+
+
+
+ 仅平台租户(is_platform=1)可维护租户列表;普通租户登录后本页仅能看到自身租户信息。
+ 需在数据库执行迁移脚本创建 sys_tenant 表及默认数据。
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/sys_user.vue b/src/views/system/sys_user.vue
index 403b361..efa71db 100644
--- a/src/views/system/sys_user.vue
+++ b/src/views/system/sys_user.vue
@@ -1,5 +1,10 @@
+
+ 当前租户:{{ currentTenant.name }}({{ currentTenant.code }})。用户按租户隔离;
+ 角色全库共用,下拉中的角色对所有租户一致。
+ 平台租户可为他人指定「目标租户」。
+
@@ -16,9 +21,10 @@