feat: 更新租户和订单相关功能,添加租户成员列表接口,优化数据处理和前端展示
This commit is contained in:
@@ -7,8 +7,41 @@ function normalizeItems(items) {
|
||||
}
|
||||
|
||||
export const TenantService = {
|
||||
async listTenants({ page, limit, name, code, status, sortField, sortOrder } = {}) {
|
||||
const query = { page, limit, name, code, status };
|
||||
async listTenants({
|
||||
page,
|
||||
limit,
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
code,
|
||||
status,
|
||||
expired_at_from,
|
||||
expired_at_to,
|
||||
created_at_from,
|
||||
created_at_to,
|
||||
sortField,
|
||||
sortOrder
|
||||
} = {}) {
|
||||
const iso = (d) => {
|
||||
if (!d) return undefined;
|
||||
const date = d instanceof Date ? d : new Date(d);
|
||||
if (Number.isNaN(date.getTime())) return undefined;
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const query = {
|
||||
page,
|
||||
limit,
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
code,
|
||||
status,
|
||||
expired_at_from: iso(expired_at_from),
|
||||
expired_at_to: iso(expired_at_to),
|
||||
created_at_from: iso(created_at_from),
|
||||
created_at_to: iso(created_at_to)
|
||||
};
|
||||
if (sortField && sortOrder) {
|
||||
if (sortOrder === 1) query.asc = sortField;
|
||||
if (sortOrder === -1) query.desc = sortField;
|
||||
@@ -33,6 +66,11 @@ export const TenantService = {
|
||||
}
|
||||
});
|
||||
},
|
||||
async listTenantUsers(tenantID, { page, limit, user_id, username, role, status } = {}) {
|
||||
return requestJson(`/super/v1/tenants/${tenantID}/users`, {
|
||||
query: { page, limit, user_id, username, role, status }
|
||||
});
|
||||
},
|
||||
async renewTenantExpire({ tenantID, duration }) {
|
||||
return requestJson(`/super/v1/tenants/${tenantID}`, {
|
||||
method: 'PATCH',
|
||||
|
||||
@@ -15,11 +15,30 @@ const totalRecords = ref(0);
|
||||
const page = ref(1);
|
||||
const rows = ref(10);
|
||||
|
||||
const keyword = ref('');
|
||||
const nameKeyword = ref('');
|
||||
const codeKeyword = ref('');
|
||||
const status = ref('');
|
||||
const tenantID = ref(null);
|
||||
const ownerUserID = ref(null);
|
||||
const expiredAtFrom = ref(null);
|
||||
const expiredAtTo = ref(null);
|
||||
const createdAtFrom = ref(null);
|
||||
const createdAtTo = ref(null);
|
||||
const sortField = ref('id');
|
||||
const sortOrder = ref(-1);
|
||||
|
||||
const tenantUsersDialogVisible = ref(false);
|
||||
const tenantUsersLoading = ref(false);
|
||||
const tenantUsersTenant = ref(null);
|
||||
const tenantUsers = ref([]);
|
||||
const tenantUsersTotal = ref(0);
|
||||
const tenantUsersPage = ref(1);
|
||||
const tenantUsersRows = ref(10);
|
||||
const tenantUsersUsername = ref('');
|
||||
const tenantUsersUserID = ref(null);
|
||||
const tenantUsersRole = ref('');
|
||||
const tenantUsersStatus = ref('');
|
||||
|
||||
const createDialogVisible = ref(false);
|
||||
const creating = ref(false);
|
||||
const createCode = ref('');
|
||||
@@ -38,6 +57,12 @@ function formatDate(value) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatCny(amountInCents) {
|
||||
const amount = Number(amountInCents) / 100;
|
||||
if (!Number.isFinite(amount)) return '-';
|
||||
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
||||
}
|
||||
|
||||
function getStatusSeverity(status) {
|
||||
switch (status) {
|
||||
case 'verified':
|
||||
@@ -76,9 +101,15 @@ async function loadTenants() {
|
||||
const result = await TenantService.listTenants({
|
||||
page: page.value,
|
||||
limit: rows.value,
|
||||
name: keyword.value,
|
||||
code: keyword.value,
|
||||
id: tenantID.value || undefined,
|
||||
user_id: ownerUserID.value || undefined,
|
||||
name: nameKeyword.value,
|
||||
code: codeKeyword.value,
|
||||
status: status.value,
|
||||
expired_at_from: expiredAtFrom.value || undefined,
|
||||
expired_at_to: expiredAtTo.value || undefined,
|
||||
created_at_from: createdAtFrom.value || undefined,
|
||||
created_at_to: createdAtTo.value || undefined,
|
||||
sortField: sortField.value,
|
||||
sortOrder: sortOrder.value
|
||||
});
|
||||
@@ -102,8 +133,15 @@ function onSearch() {
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
keyword.value = '';
|
||||
nameKeyword.value = '';
|
||||
codeKeyword.value = '';
|
||||
status.value = '';
|
||||
tenantID.value = null;
|
||||
ownerUserID.value = null;
|
||||
expiredAtFrom.value = null;
|
||||
expiredAtTo.value = null;
|
||||
createdAtFrom.value = null;
|
||||
createdAtTo.value = null;
|
||||
sortField.value = 'id';
|
||||
sortOrder.value = -1;
|
||||
page.value = 1;
|
||||
@@ -123,6 +161,70 @@ function onSort(event) {
|
||||
loadTenants();
|
||||
}
|
||||
|
||||
function openTenantUsersDialog(tenant) {
|
||||
tenantUsersTenant.value = tenant;
|
||||
tenantUsersDialogVisible.value = true;
|
||||
tenantUsersPage.value = 1;
|
||||
tenantUsersRows.value = 10;
|
||||
tenantUsersUsername.value = '';
|
||||
tenantUsersUserID.value = null;
|
||||
tenantUsersRole.value = '';
|
||||
tenantUsersStatus.value = '';
|
||||
loadTenantUsers();
|
||||
}
|
||||
|
||||
async function loadTenantUsers() {
|
||||
const tenant = tenantUsersTenant.value;
|
||||
const id = tenant?.id;
|
||||
if (!id) return;
|
||||
|
||||
tenantUsersLoading.value = true;
|
||||
try {
|
||||
const data = await TenantService.listTenantUsers(id, {
|
||||
page: tenantUsersPage.value,
|
||||
limit: tenantUsersRows.value,
|
||||
username: tenantUsersUsername.value,
|
||||
user_id: tenantUsersUserID.value || undefined,
|
||||
role: tenantUsersRole.value || undefined,
|
||||
status: tenantUsersStatus.value || undefined
|
||||
});
|
||||
|
||||
const normalizeItems = (items) => {
|
||||
if (Array.isArray(items)) return items;
|
||||
if (items && typeof items === 'object') return [items];
|
||||
return [];
|
||||
};
|
||||
|
||||
tenantUsers.value = normalizeItems(data?.items);
|
||||
tenantUsersTotal.value = data?.total ?? 0;
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载租户成员列表', life: 4000 });
|
||||
} finally {
|
||||
tenantUsersLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onTenantUsersSearch() {
|
||||
tenantUsersPage.value = 1;
|
||||
loadTenantUsers();
|
||||
}
|
||||
|
||||
function onTenantUsersReset() {
|
||||
tenantUsersUsername.value = '';
|
||||
tenantUsersUserID.value = null;
|
||||
tenantUsersRole.value = '';
|
||||
tenantUsersStatus.value = '';
|
||||
tenantUsersPage.value = 1;
|
||||
tenantUsersRows.value = 10;
|
||||
loadTenantUsers();
|
||||
}
|
||||
|
||||
function onTenantUsersPageChange(event) {
|
||||
tenantUsersPage.value = (event.page ?? 0) + 1;
|
||||
tenantUsersRows.value = event.rows ?? tenantUsersRows.value;
|
||||
loadTenantUsers();
|
||||
}
|
||||
|
||||
const renewDialogVisible = ref(false);
|
||||
const renewing = ref(false);
|
||||
const renewTenant = ref(null);
|
||||
@@ -282,17 +384,43 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||
<SearchField label="名称 / Code">
|
||||
<SearchField label="TenantID">
|
||||
<InputNumber v-model="tenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="Owner UserID">
|
||||
<InputNumber v-model="ownerUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="名称">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="keyword" placeholder="请输入" class="w-full" @keyup.enter="onSearch" />
|
||||
<InputText v-model="nameKeyword" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="Code">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="codeKeyword" placeholder="模糊匹配(小写)" class="w-full" @keyup.enter="onSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="状态">
|
||||
<Select v-model="status" :options="tenantStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" :loading="tenantStatusOptionsLoading" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="过期时间 From">
|
||||
<DatePicker v-model="expiredAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="过期时间 To">
|
||||
<DatePicker v-model="expiredAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 From">
|
||||
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 To">
|
||||
<DatePicker v-model="createdAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
@@ -319,13 +447,42 @@ onMounted(() => {
|
||||
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
||||
<Column field="code" header="Code" style="min-width: 10rem" />
|
||||
<Column field="name" header="名称" sortable style="min-width: 14rem" />
|
||||
<Column field="user_id" header="Owner" sortable style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<span v-if="data.owner?.username">{{ data.owner.username }}</span>
|
||||
<span v-else class="text-muted-color">{{ data.user_id ?? '-' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="管理员" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Tag v-for="u in data.admin_users || []" :key="u.id" :value="u.username || String(u.id)" severity="info" />
|
||||
<span v-if="!data.admin_users || data.admin_users.length === 0" class="text-muted-color">-</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status_description" header="状态" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" class="cursor-pointer" @click="openTenantStatusDialog(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="user_count" header="用户数" sortable style="min-width: 8rem" />
|
||||
<Column field="user_balance" header="余额" sortable style="min-width: 8rem" />
|
||||
<Column field="user_count" header="用户数" sortable style="min-width: 8rem">
|
||||
<template #body="{ data }">
|
||||
<Button
|
||||
:label="String(data.user_count ?? 0)"
|
||||
text
|
||||
size="small"
|
||||
icon="pi pi-users"
|
||||
class="p-0"
|
||||
@click="openTenantUsersDialog(data)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="income_amount_paid_sum" header="累计收入" sortable style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatCny(data.income_amount_paid_sum) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="expired_at" header="过期时间" sortable style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
<span v-if="data.expired_at" v-tooltip="getExpiryDaysInfo(data.expired_at).tooltipText" :class="getExpiryDaysInfo(data.expired_at).textClass">
|
||||
@@ -444,5 +601,96 @@ onMounted(() => {
|
||||
<Button label="确认创建" icon="pi pi-check" @click="confirmCreateTenant" :loading="creating" :disabled="!canCreateTenant" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="tenantUsersDialogVisible" :modal="true" :style="{ width: '980px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">租户成员</span>
|
||||
<span class="text-muted-color truncate max-w-[420px]">{{ tenantUsersTenant?.name ?? tenantUsersTenant?.code ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<SearchPanel :loading="tenantUsersLoading" @search="onTenantUsersSearch" @reset="onTenantUsersReset">
|
||||
<SearchField label="UserID">
|
||||
<InputNumber v-model="tenantUsersUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="用户名">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="tenantUsersUsername" placeholder="模糊匹配" class="w-full" @keyup.enter="onTenantUsersSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="角色">
|
||||
<Select
|
||||
v-model="tenantUsersRole"
|
||||
:options="[
|
||||
{ label: '全部', value: '' },
|
||||
{ label: 'member', value: 'member' },
|
||||
{ label: 'tenant_admin', value: 'tenant_admin' }
|
||||
]"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="请选择"
|
||||
class="w-full"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="状态">
|
||||
<Select
|
||||
v-model="tenantUsersStatus"
|
||||
:options="[
|
||||
{ label: '全部', value: '' },
|
||||
{ label: 'pending_verify', value: 'pending_verify' },
|
||||
{ label: 'verified', value: 'verified' },
|
||||
{ label: 'banned', value: 'banned' }
|
||||
]"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="请选择"
|
||||
class="w-full"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
:value="tenantUsers"
|
||||
dataKey="tenant_user.id"
|
||||
:loading="tenantUsersLoading"
|
||||
lazy
|
||||
:paginator="true"
|
||||
:rows="tenantUsersRows"
|
||||
:totalRecords="tenantUsersTotal"
|
||||
:first="(tenantUsersPage - 1) * tenantUsersRows"
|
||||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||
@page="onTenantUsersPageChange"
|
||||
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
scrollable
|
||||
scrollHeight="420px"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="tenant_user.user_id" header="UserID" style="min-width: 7rem" />
|
||||
<Column field="user.username" header="用户名" style="min-width: 14rem" />
|
||||
<Column field="tenant_user.role" header="角色" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Tag v-for="r in data?.tenant_user?.role || []" :key="r" :value="r" severity="secondary" />
|
||||
<span v-if="!data?.tenant_user?.role || data.tenant_user.role.length === 0" class="text-muted-color">-</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="user.status_description" header="状态" style="min-width: 10rem" />
|
||||
<Column field="tenant_user.created_at" header="加入时间" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data?.tenant_user?.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="关闭" icon="pi pi-times" text @click="tenantUsersDialogVisible = false" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user