- 添加 .dockerignore 和 .env.example 配置文件 - 添加 .gitignore 忽略规则配置 - 创建服务端代理API路由(_file、_modules、_server) - 集成 Ant Design Vue 组件库并配置SSR样式提取 - 定义API响应类型封装 - 创建基础布局组件(blank、console) - 实现应用中心页面和组件(AppsCenter) - 添加文章列表测试页面 - 配置控制台导航菜单结构 - 实现控制台头部组件 - 创建联系页面表单
337 lines
10 KiB
Vue
337 lines
10 KiB
Vue
<template>
|
||
<div class="space-y-4">
|
||
<a-page-header title="租户管理" sub-title="租户创建、查询与维护" :ghost="false" class="page-header">
|
||
<template #extra>
|
||
<a-space>
|
||
<a-input
|
||
v-model:value="keywords"
|
||
allow-clear
|
||
placeholder="搜索租户名称/租户ID"
|
||
class="w-64"
|
||
@press-enter="doSearch"
|
||
/>
|
||
<a-button :loading="loading" @click="reload">刷新</a-button>
|
||
<a-button type="primary" @click="openCreate">新增租户</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-page-header>
|
||
|
||
<a-card :bordered="false" class="card">
|
||
<a-alert v-if="error" class="mb-4" show-icon type="error" :message="error" />
|
||
|
||
<a-table
|
||
:data-source="list"
|
||
:loading="loading"
|
||
:pagination="false"
|
||
size="middle"
|
||
:row-key="(r: any) => r.tenantId ?? r.appId ?? r.tenantName"
|
||
>
|
||
<a-table-column title="租户ID" data-index="tenantId" width="90" />
|
||
<a-table-column title="租户名称" key="tenantName" width="220">
|
||
<template #default="{ record }">
|
||
<div class="flex items-center gap-2 min-w-0">
|
||
<a-avatar :src="record.logo" :size="22" shape="square">
|
||
<template #icon>
|
||
<UserOutlined />
|
||
</template>
|
||
</a-avatar>
|
||
<span class="truncate">{{ record.tenantName || '-' }}</span>
|
||
</div>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="状态" key="status" width="120">
|
||
<template #default="{ record }">
|
||
<a-tag v-if="record.status === 0 || record.status === undefined" color="green">正常</a-tag>
|
||
<a-tag v-else color="default">禁用</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="创建时间" data-index="createTime" width="180" />
|
||
<a-table-column title="操作" key="actions" width="260" fixed="right">
|
||
<template #default="{ record }">
|
||
<a-space>
|
||
<a-button size="small" @click="openEdit(record)">编辑</a-button>
|
||
<a-button size="small" @click="openReset(record)" :disabled="!record.tenantId">重置密码</a-button>
|
||
<a-popconfirm
|
||
title="确定删除该租户?"
|
||
ok-text="删除"
|
||
cancel-text="取消"
|
||
@confirm="remove(record)"
|
||
>
|
||
<a-button size="small" danger :loading="busyTenantId === record.tenantId" :disabled="!record.tenantId">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
|
||
<div class="mt-4 flex items-center justify-end">
|
||
<a-pagination
|
||
:current="page"
|
||
:page-size="limit"
|
||
:total="total"
|
||
show-size-changer
|
||
:page-size-options="['10', '20', '50', '100']"
|
||
@change="onPageChange"
|
||
@show-size-change="onPageSizeChange"
|
||
/>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-modal
|
||
v-model:open="editOpen"
|
||
:title="editTitle"
|
||
ok-text="保存"
|
||
cancel-text="取消"
|
||
:confirm-loading="saving"
|
||
@ok="submitEdit"
|
||
>
|
||
<a-form ref="editFormRef" layout="vertical" :model="editForm" :rules="editRules">
|
||
<a-form-item v-if="editForm.tenantId" label="租户ID">
|
||
<a-input :value="String(editForm.tenantId ?? '')" disabled />
|
||
</a-form-item>
|
||
<a-form-item label="租户名称" name="tenantName">
|
||
<a-input v-model:value="editForm.tenantName" placeholder="例如:某某科技有限公司" />
|
||
</a-form-item>
|
||
<a-form-item label="企业名称" name="companyName">
|
||
<a-input v-model:value="editForm.companyName" placeholder="例如:某某科技有限公司" />
|
||
</a-form-item>
|
||
<a-form-item label="Logo" name="logo">
|
||
<a-input v-model:value="editForm.logo" placeholder="https://..." />
|
||
</a-form-item>
|
||
<a-form-item label="应用秘钥" name="appSecret">
|
||
<a-input-password v-model:value="editForm.appSecret" placeholder="appSecret(可选)" />
|
||
</a-form-item>
|
||
<a-form-item label="状态" name="status">
|
||
<a-select
|
||
v-model:value="editForm.status"
|
||
placeholder="请选择"
|
||
:options="[
|
||
{ label: '正常', value: 0 },
|
||
{ label: '禁用', value: 1 }
|
||
]"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="备注" name="comments">
|
||
<a-textarea v-model:value="editForm.comments" :rows="3" placeholder="备注(可选)" />
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:open="resetOpen"
|
||
title="重置租户密码"
|
||
ok-text="确认重置"
|
||
cancel-text="取消"
|
||
:confirm-loading="resetting"
|
||
@ok="submitReset"
|
||
>
|
||
<a-form layout="vertical">
|
||
<a-form-item label="租户">
|
||
<a-input :value="selectedTenant?.tenantName || ''" disabled />
|
||
</a-form-item>
|
||
<a-form-item label="新密码">
|
||
<a-input-password v-model:value="resetPassword" placeholder="请输入新密码" />
|
||
</a-form-item>
|
||
</a-form>
|
||
<a-alert class="mt-2" type="warning" show-icon message="重置后请尽快通知租户管理员修改密码。" />
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { message, type FormInstance } from 'ant-design-vue'
|
||
import { UserOutlined } from '@ant-design/icons-vue'
|
||
import { addTenant, pageTenant, removeTenant, updateTenant, updateTenantPassword } from '@/api/system/tenant'
|
||
import type { Tenant } from '@/api/system/tenant/model'
|
||
import { TEMPLATE_ID } from '@/config/setting'
|
||
|
||
definePageMeta({ layout: 'console' })
|
||
|
||
const loading = ref(false)
|
||
const error = ref<string>('')
|
||
|
||
const list = ref<Tenant[]>([])
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const limit = ref(10)
|
||
|
||
const keywords = ref('')
|
||
const tenantCode = ref('')
|
||
|
||
const adminHeaders = { TenantId: TEMPLATE_ID }
|
||
|
||
async function loadTenants() {
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
const res = await pageTenant({
|
||
page: page.value,
|
||
limit: limit.value,
|
||
keywords: keywords.value || undefined,
|
||
tenantCode: tenantCode.value || undefined
|
||
}, { headers: adminHeaders })
|
||
list.value = res?.list ?? []
|
||
total.value = res?.count ?? 0
|
||
} catch (e) {
|
||
error.value = e instanceof Error ? e.message : '租户列表加载失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function reload() {
|
||
await loadTenants()
|
||
}
|
||
|
||
function doSearch() {
|
||
page.value = 1
|
||
loadTenants()
|
||
}
|
||
|
||
function onPageChange(nextPage: number) {
|
||
page.value = nextPage
|
||
loadTenants()
|
||
}
|
||
|
||
function onPageSizeChange(_current: number, nextSize: number) {
|
||
limit.value = nextSize
|
||
page.value = 1
|
||
loadTenants()
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadTenants()
|
||
})
|
||
|
||
const editOpen = ref(false)
|
||
const saving = ref(false)
|
||
const editFormRef = ref<FormInstance>()
|
||
const editForm = reactive<Tenant>({
|
||
tenantId: undefined,
|
||
tenantName: '',
|
||
companyName: '',
|
||
appId: '',
|
||
appSecret: '',
|
||
logo: '',
|
||
comments: '',
|
||
status: 0
|
||
})
|
||
const editTitle = computed(() => (editForm.tenantId ? '编辑租户' : '新增租户'))
|
||
const editRules = reactive({
|
||
tenantName: [{ required: true, type: 'string', message: '请输入租户名称' }]
|
||
})
|
||
|
||
function openCreate() {
|
||
editForm.tenantId = undefined
|
||
editForm.tenantName = ''
|
||
editForm.companyName = ''
|
||
editForm.appId = ''
|
||
editForm.appSecret = ''
|
||
editForm.logo = ''
|
||
editForm.comments = ''
|
||
editForm.status = 0
|
||
editOpen.value = true
|
||
}
|
||
|
||
function openEdit(row: Tenant) {
|
||
editForm.tenantId = row.tenantId
|
||
editForm.tenantName = row.tenantName ?? ''
|
||
editForm.companyName = row.companyName ?? ''
|
||
editForm.appId = row.appId ?? ''
|
||
editForm.appSecret = row.appSecret ?? ''
|
||
editForm.logo = row.logo ?? ''
|
||
editForm.comments = row.comments ?? ''
|
||
editForm.status = row.status ?? 0
|
||
editOpen.value = true
|
||
}
|
||
|
||
async function submitEdit() {
|
||
try {
|
||
await editFormRef.value?.validate()
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
const payload: Tenant = {
|
||
...editForm,
|
||
tenantName: editForm.tenantName?.trim(),
|
||
companyName: editForm.companyName?.trim() || undefined,
|
||
appId: editForm.appId?.trim(),
|
||
appSecret: editForm.appSecret?.trim() || undefined,
|
||
logo: editForm.logo?.trim() || undefined,
|
||
comments: editForm.comments?.trim() || undefined
|
||
}
|
||
if (payload.tenantId) {
|
||
await updateTenant(payload, { headers: adminHeaders })
|
||
message.success('租户已更新')
|
||
} else {
|
||
await addTenant(payload, { headers: adminHeaders })
|
||
message.success('租户已创建')
|
||
}
|
||
editOpen.value = false
|
||
await loadTenants()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const busyTenantId = ref<number | null>(null)
|
||
async function remove(row: Tenant) {
|
||
if (!row.tenantId) return
|
||
busyTenantId.value = row.tenantId
|
||
try {
|
||
await removeTenant(row.tenantId, { headers: adminHeaders })
|
||
message.success('已删除')
|
||
if (list.value.length <= 1 && page.value > 1) page.value -= 1
|
||
await loadTenants()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '删除失败')
|
||
} finally {
|
||
busyTenantId.value = null
|
||
}
|
||
}
|
||
|
||
const resetOpen = ref(false)
|
||
const resetting = ref(false)
|
||
const resetPassword = ref('')
|
||
const selectedTenant = ref<Tenant | null>(null)
|
||
function openReset(row: Tenant) {
|
||
selectedTenant.value = row
|
||
resetPassword.value = ''
|
||
resetOpen.value = true
|
||
}
|
||
|
||
async function submitReset() {
|
||
if (!selectedTenant.value?.tenantId) return
|
||
const pwd = resetPassword.value.trim()
|
||
if (!pwd) {
|
||
message.error('请输入新密码')
|
||
return
|
||
}
|
||
resetting.value = true
|
||
try {
|
||
await updateTenantPassword(selectedTenant.value.tenantId, pwd, { headers: adminHeaders })
|
||
message.success('密码已重置')
|
||
resetOpen.value = false
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '重置失败')
|
||
} finally {
|
||
resetting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-header {
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.card {
|
||
border-radius: 12px;
|
||
}
|
||
</style>
|