Files
template-10586/app/pages/console/tenant/index.vue
赵忠林 5e26fdc7fb feat(app): 初始化项目配置和页面结构
- 添加 .dockerignore 和 .env.example 配置文件
- 添加 .gitignore 忽略规则配置
- 创建服务端代理API路由(_file、_modules、_server)
- 集成 Ant Design Vue 组件库并配置SSR样式提取
- 定义API响应类型封装
- 创建基础布局组件(blank、console)
- 实现应用中心页面和组件(AppsCenter)
- 添加文章列表测试页面
- 配置控制台导航菜单结构
- 实现控制台头部组件
- 创建联系页面表单
2026-01-17 18:23:37 +08:00

337 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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