Day5: 机构管理模块优化 — 隐藏内部租户+后端搜索+快捷操作+新建引导
- 后端过滤系统内部租户(super/public/school等),列表只展示真实机构 - 搜索改为后端分页查询(keyword+tenantType参数),去掉前端过滤 - 表格新增登录地址列,一键复制完整URL - 新增停用/启用快捷按钮(PATCH /tenants/:id/status) - 新建机构成功后弹出引导,可直接跳转创建管理员账号 - 修复编辑弹窗因模板访问window导致的渲染崩溃 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f246b38fc1
commit
83f007d20e
@ -35,10 +35,17 @@ export class TenantsController {
|
||||
@RequirePermission('tenant:read')
|
||||
findAll(
|
||||
@Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
|
||||
@Query('pageSize', new ParseIntPipe({ optional: true }))
|
||||
pageSize: number = 10,
|
||||
@Query('pageSize', new ParseIntPipe({ optional: true })) pageSize: number = 10,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('tenantType') tenantType?: string,
|
||||
) {
|
||||
return this.tenantsService.findAll(page, pageSize);
|
||||
return this.tenantsService.findAll({ page, pageSize, keyword, tenantType });
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@RequirePermission('tenant:update')
|
||||
toggleStatus(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
||||
return this.tenantsService.toggleStatus(id, req.user?.tenantId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
@ -84,18 +84,33 @@ export class TenantsService {
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(page: number = 1, pageSize: number = 10) {
|
||||
// 系统内部租户编码(不在机构列表中展示)
|
||||
private readonly INTERNAL_TENANT_CODES = ['super', 'public', 'school', 'teacher', 'student', 'judge'];
|
||||
|
||||
async findAll(params: { page?: number; pageSize?: number; keyword?: string; tenantType?: string } = {}) {
|
||||
const { page = 1, pageSize = 10, keyword, tenantType } = params;
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: any = {
|
||||
code: { notIn: this.INTERNAL_TENANT_CODES },
|
||||
validState: { not: undefined },
|
||||
};
|
||||
if (keyword) {
|
||||
where.OR = [
|
||||
{ name: { contains: keyword } },
|
||||
{ code: { contains: keyword } },
|
||||
];
|
||||
}
|
||||
if (tenantType) {
|
||||
where.tenantType = tenantType;
|
||||
}
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
this.prisma.tenant.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: pageSize,
|
||||
include: {
|
||||
menus: {
|
||||
include: {
|
||||
menu: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
users: true,
|
||||
@ -107,15 +122,23 @@ export class TenantsService {
|
||||
createTime: 'desc',
|
||||
},
|
||||
}),
|
||||
this.prisma.tenant.count(),
|
||||
this.prisma.tenant.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
return { list, total, page, pageSize };
|
||||
}
|
||||
|
||||
/** 切换租户启用/停用状态 */
|
||||
async toggleStatus(id: number, currentTenantId?: number) {
|
||||
await this.checkSuperTenant(currentTenantId);
|
||||
const tenant = await this.prisma.tenant.findUnique({ where: { id } });
|
||||
if (!tenant) throw new NotFoundException('租户不存在');
|
||||
if (tenant.isSuper === 1) throw new BadRequestException('不能停用超级租户');
|
||||
|
||||
return this.prisma.tenant.update({
|
||||
where: { id },
|
||||
data: { validState: tenant.validState === 1 ? 2 : 1 },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
| [评审进度优化](./super-admin/review-progress-optimization.md) | 活动监管 | 已实现(待验收) | 2026-03-27 |
|
||||
| [成果发布优化](./super-admin/results-publish-optimization.md) | 活动监管 | 已实现(待验收) | 2026-03-27 |
|
||||
| [内容管理模块](./super-admin/content-management.md) | 内容管理 | P0 已实现并优化 | 2026-03-31 |
|
||||
| [机构管理优化](./super-admin/org-management.md) | 机构管理 | 已优化 | 2026-03-31 |
|
||||
|
||||
## 机构管理端
|
||||
|
||||
|
||||
42
docs/design/super-admin/org-management.md
Normal file
42
docs/design/super-admin/org-management.md
Normal file
@ -0,0 +1,42 @@
|
||||
# 超管端机构管理 — 优化记录
|
||||
|
||||
> 所属端:超管端
|
||||
> 状态:已优化
|
||||
> 创建日期:2026-03-31
|
||||
> 最后更新:2026-03-31
|
||||
|
||||
---
|
||||
|
||||
## 模块说明
|
||||
|
||||
超管端「机构管理」菜单,管理平台接入的外部机构(图书馆、学校、幼儿园等)。每个机构对应一个租户,拥有独立的用户、角色、菜单权限。
|
||||
|
||||
## Day5 (2026-03-31) — 优化内容
|
||||
|
||||
### 1. 隐藏系统内部租户
|
||||
- 后端列表查询过滤 super/public/school/teacher/student/judge 等系统内部编码
|
||||
- 列表只展示真实外部机构
|
||||
|
||||
### 2. 搜索改为后端分页
|
||||
- keyword(名称/编码)和 tenantType 参数传后端查询
|
||||
- 去掉前端 computed 过滤,支持大数据量
|
||||
- 类型下拉选择后自动触发查询
|
||||
|
||||
### 3. 新增登录地址列
|
||||
- 表格新增「登录地址」列,显示 `/:code/login`
|
||||
- 旁边有复制按钮,一键复制完整 URL,方便运营发给机构管理员
|
||||
|
||||
### 4. 停用/启用快捷操作
|
||||
- 操作列新增停用/启用按钮(二次确认提示影响)
|
||||
- 后端新增 `PATCH /tenants/:id/status` 接口
|
||||
|
||||
### 5. 新建后引导
|
||||
- 创建机构成功后弹出引导弹窗
|
||||
- 提供「为该机构创建管理员账号」按钮,跳转用户管理页
|
||||
- 避免创建机构后不知道下一步做什么
|
||||
|
||||
### 新增 API
|
||||
```
|
||||
PATCH /api/tenants/:id/status — 切换租户启用/停用状态
|
||||
GET /api/tenants (新增参数) — keyword 关键词搜索 + tenantType 类型筛选
|
||||
```
|
||||
@ -87,6 +87,11 @@ export async function getTenantMenus(id: number): Promise<Menu[]> {
|
||||
return response;
|
||||
}
|
||||
|
||||
// 切换租户启用/停用
|
||||
export async function toggleTenantStatus(id: number): Promise<Tenant> {
|
||||
return await request.patch<any, Tenant>(`/tenants/${id}/status`);
|
||||
}
|
||||
|
||||
// 兼容性导出:保留 tenantsApi 对象
|
||||
export const tenantsApi = {
|
||||
getList: getTenantsList,
|
||||
@ -95,4 +100,5 @@ export const tenantsApi = {
|
||||
update: updateTenant,
|
||||
delete: deleteTenant,
|
||||
getTenantMenus: getTenantMenus,
|
||||
toggleStatus: toggleTenantStatus,
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="tenants-page">
|
||||
<a-card class="mb-4">
|
||||
<a-card class="title-card">
|
||||
<template #title>机构管理</template>
|
||||
<template #extra>
|
||||
<a-button v-permission="'tenant:create'" type="primary" @click="handleAdd">
|
||||
@ -11,13 +11,14 @@
|
||||
</a-card>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<a-form layout="inline" class="search-form" @finish="handleSearch">
|
||||
<div class="filter-bar">
|
||||
<a-form layout="inline" @finish="handleSearch">
|
||||
<a-form-item label="机构名称">
|
||||
<a-input v-model:value="searchKeyword" placeholder="搜索机构名称或编码" allow-clear style="width: 200px"
|
||||
@press-enter="handleSearch" />
|
||||
<a-input v-model:value="searchKeyword" placeholder="搜索机构名称或编码" allow-clear style="width: 200px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机构类型">
|
||||
<a-select v-model:value="searchType" placeholder="全部类型" allow-clear style="width: 130px">
|
||||
<a-select v-model:value="searchType" style="width: 130px" @change="handleSearch">
|
||||
<a-select-option value="">全部类型</a-select-option>
|
||||
<a-select-option value="library">图书馆</a-select-option>
|
||||
<a-select-option value="kindergarten">幼儿园</a-select-option>
|
||||
<a-select-option value="school">学校</a-select-option>
|
||||
@ -27,15 +28,20 @@
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit">搜索</a-button>
|
||||
<a-button @click="handleResetSearch">重置</a-button>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><search-outlined /></template>搜索
|
||||
</a-button>
|
||||
<a-button @click="handleResetSearch">
|
||||
<template #icon><reload-outlined /></template>重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 机构列表 -->
|
||||
<a-table :columns="columns" :data-source="filteredData" :loading="loading" :pagination="pagination"
|
||||
row-key="id" @change="handleTableChange">
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination"
|
||||
row-key="id" @change="handleTableChange" class="data-table">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'nameInfo'">
|
||||
<div class="org-cell">
|
||||
@ -50,16 +56,22 @@
|
||||
{{ tenantTypeLabel(record.tenantType) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'loginUrl'">
|
||||
<div class="login-url-cell">
|
||||
<span class="url-text">/{{ record.code }}/login</span>
|
||||
<a-button type="link" size="small" class="copy-btn" @click.stop="copyLoginUrl(record.code)">
|
||||
<copy-outlined />
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'statistics'">
|
||||
<a-space :size="12">
|
||||
<span class="stat-item">
|
||||
<user-outlined />
|
||||
{{ record._count?.users || 0 }}
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<safety-outlined />
|
||||
{{ record._count?.roles || 0 }}
|
||||
</span>
|
||||
<a-tooltip title="用户数">
|
||||
<span class="stat-item"><user-outlined /> {{ record._count?.users || 0 }}</span>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="角色数">
|
||||
<span class="stat-item"><safety-outlined /> {{ record._count?.roles || 0 }}</span>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'validState'">
|
||||
@ -75,7 +87,12 @@
|
||||
@click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button v-permission="'tenant:delete'" v-if="record.isSuper !== 1 && record.code !== 'public'"
|
||||
<a-button v-permission="'tenant:update'" type="link" size="small"
|
||||
:style="{ color: record.validState === 1 ? '#f59e0b' : '#10b981' }"
|
||||
@click="handleToggleStatus(record)">
|
||||
{{ record.validState === 1 ? '停用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button v-permission="'tenant:delete'" v-if="record.isSuper !== 1"
|
||||
type="link" size="small" danger @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
@ -155,14 +172,27 @@
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
|
||||
<!-- 新建成功引导弹窗 -->
|
||||
<a-modal v-model:open="guideVisible" title="机构创建成功" :footer="null" width="460px">
|
||||
<a-result status="success" :title="`「${lastCreatedName}」创建成功`" sub-title="接下来你可以:">
|
||||
<template #extra>
|
||||
<div style="display: flex; flex-direction: column; gap: 12px; align-items: center">
|
||||
<a-button type="primary" @click="goCreateAdmin">为该机构创建管理员账号</a-button>
|
||||
<a-button @click="guideVisible = false">稍后再说</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-result>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableColumnsType, FormInstance } from 'ant-design-vue'
|
||||
import { PlusOutlined, UserOutlined, SafetyOutlined } from '@ant-design/icons-vue'
|
||||
import { PlusOutlined, UserOutlined, SafetyOutlined, CopyOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import {
|
||||
tenantsApi,
|
||||
type Tenant,
|
||||
@ -170,11 +200,15 @@ import {
|
||||
type UpdateTenantForm,
|
||||
} from '@/api/tenants'
|
||||
import { menusApi, type Menu } from '@/api/menus'
|
||||
import { useListRequest } from '@/composables/useListRequest'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Tenant[]>([])
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, showTotal: (t: number) => `共 ${t} 条` })
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const menusLoading = ref(false)
|
||||
@ -184,44 +218,19 @@ const formRef = ref<FormInstance>()
|
||||
const editingId = ref<number | null>(null)
|
||||
const activeTab = ref('basic')
|
||||
|
||||
// 搜索
|
||||
// 搜索(#2 #6 后端分页搜索)
|
||||
const searchKeyword = ref('')
|
||||
const searchType = ref<string | undefined>(undefined)
|
||||
const searchType = ref('')
|
||||
|
||||
// 新建成功引导(#6)
|
||||
const guideVisible = ref(false)
|
||||
const lastCreatedName = ref('')
|
||||
const lastCreatedId = ref<number | null>(null)
|
||||
|
||||
// 菜单相关
|
||||
const allMenus = ref<Menu[]>([])
|
||||
const topLevelMenus = computed(() => buildMenuTree(allMenus.value))
|
||||
|
||||
const {
|
||||
loading,
|
||||
dataSource,
|
||||
pagination,
|
||||
handleTableChange,
|
||||
refresh: refreshList,
|
||||
} = useListRequest<Tenant>({
|
||||
requestFn: tenantsApi.getList,
|
||||
errorMessage: '获取机构列表失败',
|
||||
})
|
||||
|
||||
// 前端过滤(搜索 + 类型筛选)
|
||||
const filteredData = computed(() => {
|
||||
let result = dataSource.value
|
||||
if (searchKeyword.value) {
|
||||
const kw = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(t) =>
|
||||
t.name.toLowerCase().includes(kw) ||
|
||||
t.code.toLowerCase().includes(kw),
|
||||
)
|
||||
}
|
||||
if (searchType.value) {
|
||||
result = result.filter((t: any) => t.tenantType === searchType.value)
|
||||
}
|
||||
// 隐藏公众用户租户(系统内部使用,不应在机构列表展示)
|
||||
result = result.filter((t) => t.code !== 'public')
|
||||
return result
|
||||
})
|
||||
|
||||
const form = reactive<CreateTenantForm & { validState?: number; menuIds?: number[]; tenantType?: string }>({
|
||||
name: '',
|
||||
code: '',
|
||||
@ -248,13 +257,15 @@ const rules = {
|
||||
],
|
||||
}
|
||||
|
||||
// #3 登录地址列 + #6 描述预览
|
||||
const columns: TableColumnsType = [
|
||||
{ title: '机构信息', key: 'nameInfo', width: 260 },
|
||||
{ title: '类型', key: 'tenantType', width: 100 },
|
||||
{ title: '用户/角色', key: 'statistics', width: 120 },
|
||||
{ title: '机构信息', key: 'nameInfo', width: 220 },
|
||||
{ title: '类型', key: 'tenantType', width: 90 },
|
||||
{ title: '登录地址', key: 'loginUrl', width: 160 },
|
||||
{ title: '用户/角色', key: 'statistics', width: 110 },
|
||||
{ title: '状态', key: 'validState', width: 80 },
|
||||
{ title: '创建时间', key: 'createTime', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' },
|
||||
{ title: '创建时间', key: 'createTime', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right' },
|
||||
]
|
||||
|
||||
const tenantTypeLabel = (type: string) => {
|
||||
@ -275,18 +286,63 @@ const tenantTypeColor = (type: string) => {
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
return new Date(dateStr).toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
const handleSearch = () => { /* 前端过滤,computed 自动触发 */ }
|
||||
|
||||
const handleResetSearch = () => {
|
||||
searchKeyword.value = ''
|
||||
searchType.value = undefined
|
||||
// #3 复制登录地址
|
||||
const copyLoginUrl = (code: string) => {
|
||||
const url = `${window.location.origin}/${code}/login`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
message.success('登录地址已复制')
|
||||
}).catch(() => {
|
||||
message.info(`登录地址:${url}`)
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 菜单相关逻辑(保持不变) ==========
|
||||
// #2 后端搜索
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await tenantsApi.getList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
tenantType: searchType.value || undefined,
|
||||
} as any)
|
||||
dataSource.value = res.list
|
||||
pagination.total = res.total
|
||||
} catch {
|
||||
message.error('获取机构列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => { pagination.current = 1; fetchList() }
|
||||
const handleResetSearch = () => { searchKeyword.value = ''; searchType.value = ''; pagination.current = 1; fetchList() }
|
||||
const handleTableChange = (pag: any) => { pagination.current = pag.current; pagination.pageSize = pag.pageSize; fetchList() }
|
||||
|
||||
// #4 快捷停用/启用
|
||||
const handleToggleStatus = (record: Tenant) => {
|
||||
const action = record.validState === 1 ? '停用' : '启用'
|
||||
Modal.confirm({
|
||||
title: `确定${action}?`,
|
||||
content: record.validState === 1
|
||||
? `停用后「${record.name}」的所有用户将无法登录`
|
||||
: `启用后「${record.name}」的用户将恢复登录`,
|
||||
okText: `确定${action}`,
|
||||
okType: record.validState === 1 ? 'danger' : 'primary',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await tenantsApi.toggleStatus(record.id)
|
||||
message.success(`已${action}`)
|
||||
fetchList()
|
||||
} catch { message.error('操作失败') }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 菜单相关逻辑 ==========
|
||||
|
||||
const fetchAllMenus = async () => {
|
||||
menusLoading.value = true
|
||||
@ -303,13 +359,11 @@ const getTenantMenuIds = (): Set<number> => {
|
||||
const tenantMenuIds = new Set<number>()
|
||||
const tenantMenu = findMenuInTree(allMenus.value, '租户管理')
|
||||
if (tenantMenu) collectMenuIds(tenantMenu, tenantMenuIds)
|
||||
// 也过滤掉"机构管理"相关菜单
|
||||
const orgMenu = findMenuInTree(allMenus.value, '机构管理')
|
||||
if (orgMenu) collectMenuIds(orgMenu, tenantMenuIds)
|
||||
return tenantMenuIds
|
||||
}
|
||||
|
||||
// 保存编辑时被排除但原本已分配的菜单 ID,保存时合并回去
|
||||
const preservedExcludedMenuIds = ref<number[]>([])
|
||||
|
||||
const fetchTenantMenus = async (tenantId: number) => {
|
||||
@ -325,7 +379,6 @@ const fetchTenantMenus = async (tenantId: number) => {
|
||||
}
|
||||
const allMenuIds = extractMenuIds(tenantMenus)
|
||||
const excludeIds = getTenantMenuIds()
|
||||
// 记录被排除但原本已分配的菜单(如机构管理、用户中心等),保存时需要保留
|
||||
preservedExcludedMenuIds.value = allMenuIds.filter((id) => excludeIds.has(id))
|
||||
form.menuIds = allMenuIds.filter((id) => !excludeIds.has(id))
|
||||
} catch {
|
||||
@ -365,7 +418,6 @@ const collectMenuIds = (menu: Menu, ids: Set<number>) => {
|
||||
const buildMenuTree = (menus: Menu[]): Menu[] => {
|
||||
const flatMenus = flattenMenus(menus)
|
||||
const excludeIds = getTenantMenuIds()
|
||||
// 也排除用户中心的菜单(超管专属)
|
||||
const userCenterMenu = findMenuInTree(menus, '用户中心')
|
||||
if (userCenterMenu) collectMenuIds(userCenterMenu, excludeIds)
|
||||
|
||||
@ -477,12 +529,11 @@ const handleDelete = (record: Tenant) => {
|
||||
content: `确定要删除机构「${record.name}」吗?此操作不可恢复。`,
|
||||
okText: '确定删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await tenantsApi.delete(record.id)
|
||||
message.success('删除成功')
|
||||
refreshList()
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || '删除失败')
|
||||
}
|
||||
@ -497,7 +548,6 @@ const handleSubmit = async () => {
|
||||
|
||||
const excludeIds = getTenantMenuIds()
|
||||
const visibleMenuIds = (form.menuIds || []).filter((id) => !excludeIds.has(id))
|
||||
// 编辑时:把被排除但原本已分配的菜单 ID 保留回去,避免保存时丢失
|
||||
const menuIds = editingId.value
|
||||
? [...visibleMenuIds, ...preservedExcludedMenuIds.value]
|
||||
: visibleMenuIds
|
||||
@ -511,19 +561,23 @@ const handleSubmit = async () => {
|
||||
menuIds,
|
||||
} as UpdateTenantForm)
|
||||
message.success('保存成功')
|
||||
modalVisible.value = false
|
||||
} else {
|
||||
await tenantsApi.create({
|
||||
const created = await tenantsApi.create({
|
||||
name: form.name, code: form.code,
|
||||
domain: form.domain || undefined,
|
||||
description: form.description || undefined,
|
||||
tenantType: form.tenantType,
|
||||
menuIds,
|
||||
} as CreateTenantForm)
|
||||
message.success('添加成功')
|
||||
modalVisible.value = false
|
||||
// #6 新建成功引导
|
||||
lastCreatedName.value = form.name
|
||||
lastCreatedId.value = created.id
|
||||
guideVisible.value = true
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
refreshList()
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
if (error?.errorFields) return
|
||||
message.error(error?.response?.data?.message || '操作失败')
|
||||
@ -532,6 +586,14 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// #6 引导跳转创建管理员
|
||||
const goCreateAdmin = () => {
|
||||
guideVisible.value = false
|
||||
// 跳转到用户管理页面(带上租户信息)
|
||||
const tenantCode = authStore.tenantCode || 'super'
|
||||
router.push(`/${tenantCode}/system/users`)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
formRef.value?.resetFields()
|
||||
@ -539,67 +601,53 @@ const handleCancel = () => {
|
||||
form.menuIds = []
|
||||
}
|
||||
|
||||
onMounted(() => { fetchAllMenus() })
|
||||
onMounted(() => { fetchList(); fetchAllMenus() })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$primary: #6366f1;
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
.title-card { margin-bottom: 16px; border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
:deep(.ant-card-head) { border-bottom: none; .ant-card-head-title { font-size: 18px; font-weight: 600; } }
|
||||
:deep(.ant-card-body) { padding: 0; }
|
||||
}
|
||||
|
||||
.filter-bar { padding: 20px 24px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 16px; }
|
||||
|
||||
.data-table { :deep(.ant-table-wrapper) { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden;
|
||||
.ant-table-thead > tr > th { background: #fafafa; font-weight: 600; }
|
||||
.ant-table-tbody > tr:hover > td { background: rgba($primary, 0.03); }
|
||||
.ant-table-pagination { padding: 16px; margin: 0; }
|
||||
} }
|
||||
|
||||
.org-cell {
|
||||
.org-name {
|
||||
font-weight: 600;
|
||||
color: #1e1b4b;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
.org-name { font-weight: 600; color: #1e1b4b; font-size: 14px; margin-bottom: 4px; }
|
||||
.org-code { :deep(.ant-tag) { font-size: 11px; } }
|
||||
}
|
||||
|
||||
.org-code {
|
||||
:deep(.ant-tag) {
|
||||
font-size: 11px;
|
||||
}
|
||||
// #3 登录地址列
|
||||
.login-url-cell {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
.url-text { font-size: 12px; color: #6b7280; font-family: monospace; }
|
||||
.copy-btn { padding: 0 4px; font-size: 12px; color: #9ca3af;
|
||||
&:hover { color: $primary; }
|
||||
}
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px; color: #6b7280;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.form-hint { font-size: 12px; color: #9ca3af; }
|
||||
|
||||
// 菜单配置区域
|
||||
.menu-config {
|
||||
.menu-config-hint {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.menu-config-hint { font-size: 13px; color: #6b7280; margin-bottom: 16px; }
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #faf9fe;
|
||||
border-radius: 12px;
|
||||
|
||||
.menu-group-header {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0ecf9;
|
||||
}
|
||||
|
||||
.menu-items {
|
||||
padding-left: 24px;
|
||||
}
|
||||
margin-bottom: 16px; padding: 16px; background: #faf9fe; border-radius: 12px;
|
||||
.menu-group-header { margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #f0ecf9; }
|
||||
.menu-items { padding-left: 24px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user