feat: add superadmin user library detail

This commit is contained in:
2026-01-15 16:17:32 +08:00
parent 339fd4fb1d
commit c8ec0af07f
10 changed files with 1104 additions and 10 deletions

View File

@@ -186,6 +186,45 @@ export const UserService = {
items: normalizeItems(data?.items)
};
},
async listUserLibrary(userID, { page, limit, tenant_id, tenant_code, tenant_name, content_id, keyword, status, order_id, order_status, paid_at_from, paid_at_to, accessed_at_from, accessed_at_to, sortField, sortOrder } = {}) {
if (!userID) throw new Error('userID is required');
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,
tenant_id,
tenant_code,
tenant_name,
content_id,
keyword,
status,
order_id,
order_status,
paid_at_from: iso(paid_at_from),
paid_at_to: iso(paid_at_to),
accessed_at_from: iso(accessed_at_from),
accessed_at_to: iso(accessed_at_to)
};
if (sortField && sortOrder) {
if (sortOrder === 1) query.asc = sortField;
if (sortOrder === -1) query.desc = sortField;
}
const data = await requestJson(`/super/v1/users/${userID}/library`, { query });
return {
page: data?.page ?? page ?? 1,
limit: data?.limit ?? limit ?? 10,
total: data?.total ?? 0,
items: normalizeItems(data?.items)
};
},
async listUserFollowing(userID, { page, limit, tenant_id, code, name, status, created_at_from, created_at_to, sortField, sortOrder } = {}) {
if (!userID) throw new Error('userID is required');

View File

@@ -52,6 +52,12 @@ const followingTenantsTotal = ref(0);
const followingTenantsPage = ref(1);
const followingTenantsRows = ref(10);
const libraryItems = ref([]);
const libraryLoading = ref(false);
const libraryTotal = ref(0);
const libraryPage = ref(1);
const libraryRows = ref(10);
const rechargeOrders = ref([]);
const rechargeOrdersLoading = ref(false);
const rechargeOrdersTotal = ref(0);
@@ -60,6 +66,23 @@ const rechargeOrdersRows = ref(10);
const tabValue = ref('owned');
const accessStatusOptions = [
{ label: '全部', value: '' },
{ label: 'active', value: 'active' },
{ label: 'revoked', value: 'revoked' },
{ label: 'expired', value: 'expired' }
];
const orderStatusOptions = [
{ label: '全部', value: '' },
{ label: 'created', value: 'created' },
{ label: 'paid', value: 'paid' },
{ label: 'refunding', value: 'refunding' },
{ label: 'refunded', value: 'refunded' },
{ label: 'canceled', value: 'canceled' },
{ label: 'failed', value: 'failed' }
];
function formatDate(value) {
if (!value) return '-';
if (String(value).startsWith('0001-01-01')) return '-';
@@ -124,6 +147,19 @@ function getOrderStatusSeverity(value) {
}
}
function getAccessStatusSeverity(value) {
switch (value) {
case 'active':
return 'success';
case 'revoked':
return 'danger';
case 'expired':
return 'warn';
default:
return 'secondary';
}
}
function getNotificationReadSeverity(value) {
return value ? 'secondary' : 'warn';
}
@@ -275,6 +311,36 @@ async function loadLikes() {
}
}
async function loadLibrary() {
const id = userID.value;
if (!id || Number.isNaN(id)) return;
libraryLoading.value = true;
try {
const result = await UserService.listUserLibrary(id, {
page: libraryPage.value,
limit: libraryRows.value,
tenant_id: libraryTenantID.value || undefined,
tenant_code: libraryTenantCode.value || undefined,
tenant_name: libraryTenantName.value || undefined,
content_id: libraryContentID.value || undefined,
keyword: libraryKeyword.value || undefined,
status: libraryStatus.value || undefined,
order_id: libraryOrderID.value || undefined,
order_status: libraryOrderStatus.value || undefined,
paid_at_from: libraryPaidAtFrom.value || undefined,
paid_at_to: libraryPaidAtTo.value || undefined,
accessed_at_from: libraryAccessedAtFrom.value || undefined,
accessed_at_to: libraryAccessedAtTo.value || undefined
});
libraryItems.value = result.items;
libraryTotal.value = result.total;
} catch (error) {
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载内容消费明细', life: 4000 });
} finally {
libraryLoading.value = false;
}
}
async function loadFollowingTenants() {
const id = userID.value;
if (!id || Number.isNaN(id)) return;
@@ -453,6 +519,19 @@ const likesKeyword = ref('');
const likesCreatedAtFrom = ref(null);
const likesCreatedAtTo = ref(null);
const libraryTenantID = ref(null);
const libraryTenantCode = ref('');
const libraryTenantName = ref('');
const libraryContentID = ref(null);
const libraryKeyword = ref('');
const libraryStatus = ref('');
const libraryOrderID = ref(null);
const libraryOrderStatus = ref('');
const libraryPaidAtFrom = ref(null);
const libraryPaidAtTo = ref(null);
const libraryAccessedAtFrom = ref(null);
const libraryAccessedAtTo = ref(null);
const followingTenantID = ref(null);
const followingCode = ref('');
const followingName = ref('');
@@ -558,6 +637,35 @@ function onLikesPage(event) {
loadLikes();
}
function onLibrarySearch() {
libraryPage.value = 1;
loadLibrary();
}
function onLibraryReset() {
libraryTenantID.value = null;
libraryTenantCode.value = '';
libraryTenantName.value = '';
libraryContentID.value = null;
libraryKeyword.value = '';
libraryStatus.value = '';
libraryOrderID.value = null;
libraryOrderStatus.value = '';
libraryPaidAtFrom.value = null;
libraryPaidAtTo.value = null;
libraryAccessedAtFrom.value = null;
libraryAccessedAtTo.value = null;
libraryPage.value = 1;
libraryRows.value = 10;
loadLibrary();
}
function onLibraryPage(event) {
libraryPage.value = (event.page ?? 0) + 1;
libraryRows.value = event.rows ?? libraryRows.value;
loadLibrary();
}
function onFollowingSearch() {
followingTenantsPage.value = 1;
loadFollowingTenants();
@@ -609,6 +717,7 @@ watch(
couponsPage.value = 1;
favoritesPage.value = 1;
likesPage.value = 1;
libraryPage.value = 1;
rechargeOrdersPage.value = 1;
loadUser();
loadWallet();
@@ -620,6 +729,7 @@ watch(
loadCoupons();
loadFavorites();
loadLikes();
loadLibrary();
loadRechargeOrders();
},
{ immediate: true }
@@ -691,6 +801,7 @@ onMounted(() => {
<Tab value="following">关注</Tab>
<Tab value="favorites">收藏</Tab>
<Tab value="likes">点赞</Tab>
<Tab value="library">内容消费</Tab>
<Tab value="wallet">钱包</Tab>
<Tab value="recharge">充值记录</Tab>
<Tab value="coupons">优惠券</Tab>
@@ -1100,6 +1211,117 @@ onMounted(() => {
</DataTable>
</div>
</TabPanel>
<TabPanel value="library">
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="text-muted-color"> {{ libraryTotal }} </span>
</div>
<SearchPanel :loading="libraryLoading" @search="onLibrarySearch" @reset="onLibraryReset">
<SearchField label="TenantID">
<InputNumber v-model="libraryTenantID" :min="1" placeholder="精确匹配" class="w-full" />
</SearchField>
<SearchField label="TenantCode">
<InputText v-model="libraryTenantCode" placeholder="请输入" class="w-full" @keyup.enter="onLibrarySearch" />
</SearchField>
<SearchField label="TenantName">
<InputText v-model="libraryTenantName" placeholder="请输入" class="w-full" @keyup.enter="onLibrarySearch" />
</SearchField>
<SearchField label="ContentID">
<InputNumber v-model="libraryContentID" :min="1" placeholder="精确匹配" class="w-full" />
</SearchField>
<SearchField label="关键字">
<InputText v-model="libraryKeyword" placeholder="标题/摘要/描述" class="w-full" @keyup.enter="onLibrarySearch" />
</SearchField>
<SearchField label="访问状态">
<Select v-model="libraryStatus" :options="accessStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField>
<SearchField label="订单ID">
<InputNumber v-model="libraryOrderID" :min="1" placeholder="精确匹配" class="w-full" />
</SearchField>
<SearchField label="订单状态">
<Select v-model="libraryOrderStatus" :options="orderStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField>
<SearchField label="支付时间 From">
<DatePicker v-model="libraryPaidAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
</SearchField>
<SearchField label="支付时间 To">
<DatePicker v-model="libraryPaidAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
</SearchField>
<SearchField label="获取时间 From">
<DatePicker v-model="libraryAccessedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
</SearchField>
<SearchField label="获取时间 To">
<DatePicker v-model="libraryAccessedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
</SearchField>
</SearchPanel>
<DataTable
:value="libraryItems"
dataKey="access_id"
:loading="libraryLoading"
lazy
:paginator="true"
:rows="libraryRows"
:totalRecords="libraryTotal"
:first="(libraryPage - 1) * libraryRows"
:rowsPerPageOptions="[10, 20, 50, 100]"
@page="onLibraryPage"
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
scrollable
scrollHeight="420px"
responsiveLayout="scroll"
>
<Column field="access_id" header="记录ID" style="min-width: 7rem" />
<Column header="内容标题" style="min-width: 18rem">
<template #body="{ data }">
<span>{{ data?.content?.content?.title ?? data?.snapshot?.content_title ?? '-' }}</span>
</template>
</Column>
<Column field="content_id" header="ContentID" style="min-width: 8rem" />
<Column header="租户" style="min-width: 14rem">
<template #body="{ data }">
<div class="flex flex-col">
<span class="font-medium">{{ data?.content?.tenant?.name ?? '-' }}</span>
<span class="text-xs text-muted-color">ID: {{ data?.tenant_id ?? '-' }}</span>
</div>
</template>
</Column>
<Column header="作者" style="min-width: 12rem">
<template #body="{ data }">
<span>{{ data?.content?.owner?.username ?? data?.snapshot?.content_user_id ?? '-' }}</span>
</template>
</Column>
<Column field="order_id" header="订单ID" style="min-width: 9rem" />
<Column header="订单状态" style="min-width: 12rem">
<template #body="{ data }">
<Tag :value="data?.order_status_description || data?.order_status || '-'" :severity="getOrderStatusSeverity(data?.order_status)" />
</template>
</Column>
<Column header="实付金额" style="min-width: 10rem">
<template #body="{ data }">
{{ formatCny(data.amount_paid) }}
</template>
</Column>
<Column header="访问状态" style="min-width: 10rem">
<template #body="{ data }">
<Tag :value="data?.access_status_description || data?.access_status || '-'" :severity="getAccessStatusSeverity(data?.access_status)" />
</template>
</Column>
<Column header="支付时间" style="min-width: 14rem">
<template #body="{ data }">
{{ formatDate(data.paid_at) }}
</template>
</Column>
<Column header="获取时间" style="min-width: 14rem">
<template #body="{ data }">
{{ formatDate(data.accessed_at) }}
</template>
</Column>
</DataTable>
</div>
</TabPanel>
<TabPanel value="wallet">
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">