feat: extend superadmin navigation

This commit is contained in:
2026-01-14 16:25:18 +08:00
parent 820ee5ba10
commit 788ba4c53a
21 changed files with 248 additions and 330 deletions

View File

@@ -1,148 +1,100 @@
# 超级管理员后台功能规划(按页面拆解 # 超级管理员后台功能规划(与当前系统功能对齐
> 目标:基于现有 `/super/v1/*` 能力,补齐平台级“管理 + 统计”闭环。以下按页面拆分,分别给出管理动作、统计指标与接口对照 > 目标:在不脱离现有后端能力的前提下,确保“系统已有功能超管可介入管理”,并通过统一入口、跨租户筛选与批量操作提升运营效率
## 0) 全局约定 ## 0) 设计原则
- **鉴权**`Authorization: Bearer <token>`;登录后本地持久化 token - **全量可管**:覆盖租户、用户、内容、订单、优惠券、钱包、提现、上传、通知等现有能力
- **路由基座**`/super/`前端API 基座 `/super/v1` - **便捷优先**:跨租户统一筛选、全局搜索、批量处理、审核队列,减少重复跳转
- **分页**:统一 `page/limit`,响应为 `requests.Pager` - **风险可控**:默认只读,危险操作需二次确认,动作全量记录
- **枚举**:优先 `/super/v1/tenants/statuses``/super/v1/users/statuses` - **接口对齐**:优先复用 `/super/v1/*` 现有能力,其它模块按“超管包装层”补齐
## 1) 登录 `/auth/login` ## 1) 当前系统能力地图(基于现有路由)
- 管理功能账号登录、token 写入、自动续期 - **租户与成员**:租户列表/详情、创建、状态变更、续期、健康检查、成员关注/加入/邀请
- 统计功能可选记录登录失败次数、IP、设备指纹审计 - **用户与认证**:登录/OTP、资料更新、实名、钱包/充值、订单、收藏/点赞、通知、优惠券
- 现有接口: - **内容与互动**:内容列表/详情、话题、评论、点赞/收藏。
- `POST /super/v1/auth/login`(需补齐实现) - **创作者与运营**:申请、内容 CRUD、优惠券 CRUD/发放、成员审核/邀请、订单与退款、报表、提现、结算账户、设置。
- `GET /super/v1/auth/token`token 校验/续期) - **交易与支付**:订单创建/支付/状态、支付回调。
- **资产与存储**:分片/直传上传、哈希检查、上传完成、媒体资产删除、存储签名上传/下载。
## 2) 概览 Dashboard `/` ## 2) 超管平台页面规划(按业务域)
- 管理功能:快捷入口(租户/用户/订单/内容)。 ### 2.1 登录与权限 `/auth/login`
- 统计指标(建议): - **功能**登录、token 续期、权限校验。
- 租户总数/活跃数/过期数 - **接口**`POST /super/v1/auth/login``GET /super/v1/auth/token`
- 用户总数/活跃数(按状态拆分)
- 订单数/成交额/退款额(按日、按状态)
- 内容总数/新增内容/被封禁内容
- 现有接口:
- `GET /super/v1/users/statistics`(需补齐实现)
- `GET /super/v1/orders/statistics`(需补齐实现)
- `GET /super/v1/tenants?limit=1&page=1`(可取 total
- `GET /super/v1/contents?limit=1&page=1`(可取 total
## 3) 租户管理 `/superadmin/tenants` ### 2.2 平台概览 `/`
- **功能**:核心指标、异常概览、快捷入口。
- **接口**`GET /super/v1/users/statistics``GET /super/v1/orders/statistics``GET /super/v1/tenants`count`GET /super/v1/contents`count
- **缺口**:平台级内容/订单趋势、退款率、提现统计。
- 管理功能: ### 2.3 租户管理 `/superadmin/tenants`
- 新建租户(绑定管理员) - **功能**:租户列表、创建、状态变更、续期、健康检查。
- 更新租户状态(正常/禁用) - **接口**`GET/POST /super/v1/tenants``PATCH /super/v1/tenants/:id/status``PATCH /super/v1/tenants/:id``GET /super/v1/tenants/health``GET /super/v1/tenants/statuses`
- 续期/变更过期时间
- 统计指标:
- 状态分布(待审核/正常/禁用)
- 即将过期租户数7/30 天)
- 租户 GMV Top N需补接口
- 现有接口:
- `POST /super/v1/tenants`
- `GET /super/v1/tenants`
- `PATCH /super/v1/tenants/{tenantID}/status`
- `PATCH /super/v1/tenants/{tenantID}`(续期)
- `GET /super/v1/tenants/statuses`
## 4) 租户详情 `/superadmin/tenants/:tenantID` ### 2.4 租户详情 `/superadmin/tenants/:tenantID`
- **功能**:租户信息、成员、内容、订单、资金与运营概览。
- **接口**`GET /super/v1/tenants/:id``GET /super/v1/tenants/:tenantID/users``GET /super/v1/tenants/:tenantID/contents``GET /super/v1/orders`(按 tenant_id 过滤)。
- **缺口**:租户成员邀请/审核、租户级财务/报表聚合。
- 管理功能(建议): ### 2.5 用户管理 `/superadmin/users`
- 基本信息/状态/过期时间编辑 - **功能**:用户列表、状态/角色、关联租户、账户概要(余额/冻结/实名认证)。
- 管理员与成员列表(角色管理) - **接口**`GET /super/v1/users``PATCH /super/v1/users/:id/status``PATCH /super/v1/users/:id/roles``GET /super/v1/users/statistics``GET /super/v1/users/statuses`
- 内容列表、订单列表、资金汇总 - **缺口**:实名信息、钱包明细、通知、优惠券、充值记录等超管视图接口。
- 统计指标(建议):
- 租户用户数、内容数、订单数、GMV
- 现有接口:
- `GET /super/v1/tenants/{tenantID}`(已有)
- 建议补充接口:
- `GET /super/v1/tenants/{tenantID}/users`
- `GET /super/v1/tenants/{tenantID}/contents`
- `GET /super/v1/tenants/{tenantID}/orders`
- `GET /super/v1/tenants/{tenantID}/statistics`
## 5) 用户管理 `/superadmin/users` ### 2.6 用户详情 `/superadmin/users/:userID`
- **功能**:资料、角色、加入/拥有租户、订单与内容消费、收藏/点赞/关注、优惠券与通知。
- **接口**`GET /super/v1/users/:id``GET /super/v1/users/:id/tenants``GET /super/v1/orders`(按 user_id 过滤)。
- **缺口**:用户钱包/通知/优惠券明细需要新接口。
- 管理功能: ### 2.7 内容治理 `/superadmin/contents`
- 用户列表筛选(用户名/状态/角色/所属租户) - **功能**:跨租户内容列表、审核、状态变更、违规处置。
- 状态变更、角色授予 - **接口**`GET /super/v1/contents``POST /super/v1/contents/:id/review``PATCH /super/v1/tenants/:tenantID/contents/:contentID/status`
- 统计指标: - **缺口**:评论治理、内容举报、内容资产明细(若需要)。
- 用户状态统计(已提供)
- 现有接口:
- `GET /super/v1/users`
- `PATCH /super/v1/users/{userID}/status`
- `PATCH /super/v1/users/{userID}/roles`
- `GET /super/v1/users/statistics`
- `GET /super/v1/users/statuses`
## 6) 用户详情 `/superadmin/users/:userID` ### 2.8 订单与退款 `/superadmin/orders`
- **功能**:订单列表、退款/强制退款、问题订单标记、支付状态核对。
- **接口**`GET /super/v1/orders``GET /super/v1/orders/:id``POST /super/v1/orders/:id/refund`
- 管理功能(建议): ### 2.9 创作者与成员审核 `/superadmin/creators`
- 用户资料、角色、状态 - **功能**:创作者申请审核、成员加入审核/邀请、创作者设置查看、提现与结算账户审核。
- 用户所属/拥有租户列表 - **接口**:租户级 `POST /t/:tenantCode/v1/creator/apply``POST /t/:tenantCode/v1/creator/members/:id/review``POST /t/:tenantCode/v1/creator/members/invite``GET /t/:tenantCode/v1/creator/payout-accounts``POST /t/:tenantCode/v1/creator/withdraw`
- 用户订单与内容购买记录 - **缺口**:超管跨租户管理接口(建议新增 `/super/v1/creators/*`)。
- 统计指标(建议):
- 用户消费总额、退款次数
- 现有接口:
- `GET /super/v1/users/{userID}`(已有)
- 建议补充接口:
- `GET /super/v1/users/{userID}/tenants`
- `GET /super/v1/users/{userID}/orders`
- `GET /super/v1/users/{userID}/contents`
## 7) 订单管理 `/superadmin/orders` ### 2.10 优惠券 `/superadmin/coupons`
- **功能**:跨租户优惠券列表、状态变更、发放记录、异常核查。
- **接口**:租户级 `GET/POST/PUT /t/:tenantCode/v1/creator/coupons``POST /t/:tenantCode/v1/creator/coupons/:id/grant`、用户侧 `/t/:tenantCode/v1/me/coupons*`
- **缺口**:超管聚合查询与冻结/归档接口。
- 管理功能: ### 2.11 财务与钱包 `/superadmin/finance`
- 订单列表(按租户/用户/状态/时间过滤) - **功能**:提现审核、用户钱包概况、异常充值/退款排查。
- 退款操作(平台侧) - **接口**:租户级 `POST /t/:tenantCode/v1/creator/withdraw`、用户侧 `GET/POST /t/:tenantCode/v1/me/wallet*`
- 统计指标: - **缺口**:超管提现列表与审批接口、钱包流水接口。
- 订单状态分布、GMV、退款额
- 现有接口:
- `GET /super/v1/orders`
- `POST /super/v1/orders/{orderID}/refund`(需补齐实现)
- `GET /super/v1/orders/statistics`(需补齐实现)
## 8) 订单详情 `/superadmin/orders/:orderID` ### 2.12 报表与导出 `/superadmin/reports`
- **功能**:跨租户报表、导出运营数据(订单、内容、退款、提现)。
- **接口**:租户级 `GET /t/:tenantCode/v1/creator/reports/overview``POST /t/:tenantCode/v1/creator/reports/export`
- **缺口**:超管聚合与导出接口。
- 管理功能: ### 2.13 资产与上传 `/superadmin/assets`
- 查看订单快照、支付信息、退款信息 - **功能**:上传记录、媒体资产清理、异常上传排查、存储用量。
- 退款/强制关闭 - **接口**:租户级 `POST /t/:tenantCode/v1/upload/*``DELETE /t/:tenantCode/v1/media-assets/:id``/t/:tenantCode/v1/storage/*`
- 现有接口: - **缺口**:资产列表/用量统计/跨租户查询接口。
- `GET /super/v1/orders/{orderID}`(需补齐实现)
## 9) 内容管理 `/superadmin/contents` ### 2.14 通知与消息 `/superadmin/notifications`
- **功能**:通知查看、批量触达、模板管理。
- **接口**:用户侧 `GET /t/:tenantCode/v1/me/notifications`
- **缺口**:超管发送与批量触达接口、通知模板管理接口。
- 管理功能: ## 3) 导航与便捷性设计(建议)
- 跨租户内容列表
- 内容状态更新(封禁/下架)
- 统计指标:
- 内容状态分布、热门内容 Top N
- 现有接口:
- `GET /super/v1/contents`
- `PATCH /super/v1/tenants/{tenantID}/contents/{contentID}/status`
## 10) 财务/提现(可选) - 统一筛选器:租户/用户/时间/状态为默认筛选维度。
- 快捷入口:租户详情可直接跳转到用户/内容/订单/提现。
- 审核队列:内容审核、退款、创作者申请、提现审核统一入口。
- 管理功能: ## 4) 推进顺序(与现有接口匹配)
- 提现订单审核(通过/驳回)
- 记录操作原因
- 统计指标:
- 提现订单数、金额、失败率
- 现有接口:无(服务层有 `ListWithdrawals/Approve/Reject`,需补 controller + route
## 11) 审计日志 / 操作记录(建议)
- 管理功能:
- 展示后台操作日志(操作人、对象、动作、时间)
- 支持导出
- 现有接口:无(可基于 `services.Audit` 扩展)
## 12) 系统配置 / 平台策略(建议)
- 管理功能:
- 平台佣金比例、内容审核策略、默认到期策略
- 现有接口:无(需新增配置表与接口)
- **P0已有接口即可落地**:登录、概览、租户管理、用户管理、内容治理、订单退款。
- **P1需补超管接口**:创作者审核、优惠券、提现/钱包、报表导出。
- **P2扩展增强**:资产/上传、通知中心、审计与系统配置。

View File

@@ -0,0 +1,36 @@
<script setup>
const props = defineProps({
title: { type: String, required: true },
description: { type: String, default: '' },
badge: { type: String, default: 'Pending' },
endpoints: { type: Array, default: () => [] },
notes: { type: Array, default: () => [] }
});
</script>
<template>
<div class="card">
<div class="flex items-center justify-between mb-4">
<h4 class="m-0">{{ props.title }}</h4>
<Tag severity="warning" :value="props.badge" />
</div>
<Message v-if="props.description" severity="warn" icon="pi pi-exclamation-triangle">
{{ props.description }}
</Message>
<div v-if="props.endpoints.length" class="mt-4">
<div class="font-medium mb-2">Suggested APIs</div>
<ul class="list-disc pl-5 text-sm text-muted-color">
<li v-for="item in props.endpoints" :key="item">{{ item }}</li>
</ul>
</div>
<div v-if="props.notes.length" class="mt-4">
<div class="font-medium mb-2">Notes</div>
<ul class="list-disc pl-5 text-sm text-muted-color">
<li v-for="item in props.notes" :key="item">{{ item }}</li>
</ul>
</div>
</div>
</template>

View File

@@ -14,7 +14,13 @@ const model = ref([
{ label: 'Tenants', icon: 'pi pi-fw pi-building', to: '/superadmin/tenants' }, { label: 'Tenants', icon: 'pi pi-fw pi-building', to: '/superadmin/tenants' },
{ label: 'Users', icon: 'pi pi-fw pi-users', to: '/superadmin/users' }, { label: 'Users', icon: 'pi pi-fw pi-users', to: '/superadmin/users' },
{ label: 'Orders', icon: 'pi pi-fw pi-shopping-cart', to: '/superadmin/orders' }, { label: 'Orders', icon: 'pi pi-fw pi-shopping-cart', to: '/superadmin/orders' },
{ label: 'Contents', icon: 'pi pi-fw pi-file', to: '/superadmin/contents' } { label: 'Contents', icon: 'pi pi-fw pi-file', to: '/superadmin/contents' },
{ label: 'Creators', icon: 'pi pi-fw pi-star', to: '/superadmin/creators' },
{ label: 'Coupons', icon: 'pi pi-fw pi-ticket', to: '/superadmin/coupons' },
{ label: 'Finance', icon: 'pi pi-fw pi-wallet', to: '/superadmin/finance' },
{ label: 'Reports', icon: 'pi pi-fw pi-chart-line', to: '/superadmin/reports' },
{ label: 'Assets', icon: 'pi pi-fw pi-folder', to: '/superadmin/assets' },
{ label: 'Notifications', icon: 'pi pi-fw pi-bell', to: '/superadmin/notifications' }
] ]
} }
]); ]);

View File

@@ -144,6 +144,36 @@ const router = createRouter({
name: 'superadmin-contents', name: 'superadmin-contents',
component: () => import('@/views/superadmin/Contents.vue') component: () => import('@/views/superadmin/Contents.vue')
}, },
{
path: '/superadmin/creators',
name: 'superadmin-creators',
component: () => import('@/views/superadmin/Creators.vue')
},
{
path: '/superadmin/coupons',
name: 'superadmin-coupons',
component: () => import('@/views/superadmin/Coupons.vue')
},
{
path: '/superadmin/finance',
name: 'superadmin-finance',
component: () => import('@/views/superadmin/Finance.vue')
},
{
path: '/superadmin/reports',
name: 'superadmin-reports',
component: () => import('@/views/superadmin/Reports.vue')
},
{
path: '/superadmin/assets',
name: 'superadmin-assets',
component: () => import('@/views/superadmin/Assets.vue')
},
{
path: '/superadmin/notifications',
name: 'superadmin-notifications',
component: () => import('@/views/superadmin/Notifications.vue')
},
{ {
path: '/superadmin/orders/:orderID', path: '/superadmin/orders/:orderID',
name: 'superadmin-order-detail', name: 'superadmin-order-detail',

View File

@@ -67,23 +67,7 @@ export const ContentService = {
items: normalizeItems(data?.items) items: normalizeItems(data?.items)
}; };
}, },
async listTenantContents( async listTenantContents(tenantID, { page, limit, keyword, status, visibility, user_id, published_at_from, published_at_to, created_at_from, created_at_to, sortField, sortOrder } = {}) {
tenantID,
{
page,
limit,
keyword,
status,
visibility,
user_id,
published_at_from,
published_at_to,
created_at_from,
created_at_to,
sortField,
sortOrder
} = {}
) {
if (!tenantID) throw new Error('tenantID is required'); if (!tenantID) throw new Error('tenantID is required');
const iso = (d) => { const iso = (d) => {
@@ -117,8 +101,7 @@ export const ContentService = {
total: data?.total ?? 0, total: data?.total ?? 0,
items: normalizeItems(data?.items) items: normalizeItems(data?.items)
}; };
} },
,
async updateTenantContentStatus(tenantID, contentID, { status } = {}) { async updateTenantContentStatus(tenantID, contentID, { status } = {}) {
if (!tenantID) throw new Error('tenantID is required'); if (!tenantID) throw new Error('tenantID is required');
if (!contentID) throw new Error('contentID is required'); if (!contentID) throw new Error('contentID is required');

View File

@@ -7,21 +7,7 @@ function normalizeItems(items) {
} }
export const TenantService = { export const TenantService = {
async listTenants({ async listTenants({ page, limit, id, user_id, name, code, status, expired_at_from, expired_at_to, created_at_from, created_at_to, sortField, sortOrder } = {}) {
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) => { const iso = (d) => {
if (!d) return undefined; if (!d) return undefined;
const date = d instanceof Date ? d : new Date(d); const date = d instanceof Date ? d : new Date(d);

View File

@@ -7,21 +7,7 @@ function normalizeItems(items) {
} }
export const UserService = { export const UserService = {
async listUsers({ async listUsers({ page, limit, id, tenant_id, username, status, role, created_at_from, created_at_to, verified_at_from, verified_at_to, sortField, sortOrder } = {}) {
page,
limit,
id,
tenant_id,
username,
status,
role,
created_at_from,
created_at_to,
verified_at_from,
verified_at_to,
sortField,
sortOrder
} = {}) {
const iso = (d) => { const iso = (d) => {
if (!d) return undefined; if (!d) return undefined;
const date = d instanceof Date ? d : new Date(d); const date = d instanceof Date ? d : new Date(d);
@@ -98,10 +84,7 @@ export const UserService = {
if (!userID) throw new Error('userID is required'); if (!userID) throw new Error('userID is required');
return requestJson(`/super/v1/users/${userID}`); return requestJson(`/super/v1/users/${userID}`);
}, },
async listUserTenants( async listUserTenants(userID, { page, limit, tenant_id, code, name, role, status, created_at_from, created_at_to } = {}) {
userID,
{ page, limit, tenant_id, code, name, role, status, created_at_from, created_at_to } = {}
) {
if (!userID) throw new Error('userID is required'); if (!userID) throw new Error('userID is required');
const iso = (d) => { const iso = (d) => {

View File

@@ -39,4 +39,3 @@ export async function refreshSuperToken() {
if (token) setSuperAuthToken(token); if (token) setSuperAuthToken(token);
return token; return token;
} }

View File

@@ -0,0 +1,11 @@
<script setup>
import PendingPanel from '@/components/PendingPanel.vue';
const endpoints = ['GET /super/v1/assets', 'DELETE /super/v1/assets/:id', 'GET /super/v1/assets/usage'];
const notes = ['Upload and storage endpoints are tenant-scoped today.', 'Add asset inventory before enabling cleanup actions.'];
</script>
<template>
<PendingPanel title="Assets" description="Asset governance requires a super admin inventory API." :endpoints="endpoints" :notes="notes" />
</template>

View File

@@ -292,11 +292,7 @@ onMounted(() => {
</Column> </Column>
<Column header="租户" sortable sortField="tenant_id" style="min-width: 18rem"> <Column header="租户" sortable sortField="tenant_id" style="min-width: 18rem">
<template #body="{ data }"> <template #body="{ data }">
<router-link <router-link v-if="data?.tenant?.id" class="inline-flex items-center gap-1 font-medium text-primary hover:underline" :to="`/superadmin/tenants/${data.tenant.id}`">
v-if="data?.tenant?.id"
class="inline-flex items-center gap-1 font-medium text-primary hover:underline"
:to="`/superadmin/tenants/${data.tenant.id}`"
>
<span class="truncate max-w-[220px]">{{ data?.tenant?.name ?? data?.tenant?.code ?? '-' }}</span> <span class="truncate max-w-[220px]">{{ data?.tenant?.name ?? data?.tenant?.code ?? '-' }}</span>
<i class="pi pi-external-link text-xs" /> <i class="pi pi-external-link text-xs" />
</router-link> </router-link>
@@ -309,11 +305,7 @@ onMounted(() => {
</Column> </Column>
<Column header="Owner" sortable sortField="user_id" style="min-width: 14rem"> <Column header="Owner" sortable sortField="user_id" style="min-width: 14rem">
<template #body="{ data }"> <template #body="{ data }">
<router-link <router-link v-if="(data?.owner?.id ?? data?.content?.user_id) > 0" class="inline-flex items-center gap-1 font-medium text-primary hover:underline" :to="`/superadmin/users/${data?.owner?.id ?? data?.content?.user_id}`">
v-if="(data?.owner?.id ?? data?.content?.user_id) > 0"
class="inline-flex items-center gap-1 font-medium text-primary hover:underline"
:to="`/superadmin/users/${data?.owner?.id ?? data?.content?.user_id}`"
>
<span class="truncate max-w-[200px]">{{ data?.owner?.username ?? `ID:${data?.content?.user_id ?? '-'}` }}</span> <span class="truncate max-w-[200px]">{{ data?.owner?.username ?? `ID:${data?.content?.user_id ?? '-'}` }}</span>
<i class="pi pi-external-link text-xs" /> <i class="pi pi-external-link text-xs" />
</router-link> </router-link>
@@ -353,16 +345,7 @@ onMounted(() => {
</Column> </Column>
<Column header="操作" style="min-width: 10rem"> <Column header="操作" style="min-width: 10rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button v-if="data?.content?.status === 'published'" label="下架" icon="pi pi-ban" severity="danger" text size="small" class="p-0" @click="openUnpublishDialog(data)" />
v-if="data?.content?.status === 'published'"
label="下架"
icon="pi pi-ban"
severity="danger"
text
size="small"
class="p-0"
@click="openUnpublishDialog(data)"
/>
<span v-else class="text-muted-color">-</span> <span v-else class="text-muted-color">-</span>
</template> </template>
</Column> </Column>

View File

@@ -0,0 +1,11 @@
<script setup>
import PendingPanel from '@/components/PendingPanel.vue';
const endpoints = ['GET /super/v1/coupons', 'PATCH /super/v1/coupons/:id/status', 'GET /super/v1/coupon-grants'];
const notes = ['Current coupon CRUD endpoints are tenant-scoped and tied to creator ownership.', 'Expose cross-tenant coupon listing before adding bulk actions.'];
</script>
<template>
<PendingPanel title="Coupons" description="Coupon management needs a super admin aggregation layer." :endpoints="endpoints" :notes="notes" />
</template>

View File

@@ -0,0 +1,18 @@
<script setup>
import PendingPanel from '@/components/PendingPanel.vue';
const endpoints = [
'GET /super/v1/creators',
'GET /super/v1/creator-applications',
'POST /super/v1/creator-applications/:id/review',
'GET /super/v1/creator-members',
'POST /super/v1/creator-members/:id/review',
'POST /super/v1/creator-members/invite'
];
const notes = ['Tenant-level creator endpoints require the tenant owner and are not usable from super admin today.', 'Keep creator approvals in the tenant admin portal until super admin APIs are added.'];
</script>
<template>
<PendingPanel title="Creators" description="Super admin creator operations require cross-tenant APIs." :endpoints="endpoints" :notes="notes" />
</template>

View File

@@ -0,0 +1,11 @@
<script setup>
import PendingPanel from '@/components/PendingPanel.vue';
const endpoints = ['GET /super/v1/withdrawals', 'POST /super/v1/withdrawals/:id/approve', 'POST /super/v1/withdrawals/:id/reject', 'GET /super/v1/wallet-ledgers'];
const notes = ['Withdrawals currently exist only in tenant creator APIs.', 'Add a super admin ledger view before exposing approvals.'];
</script>
<template>
<PendingPanel title="Finance" description="Withdrawals and wallet visibility require super admin endpoints." :endpoints="endpoints" :notes="notes" />
</template>

View File

@@ -0,0 +1,11 @@
<script setup>
import PendingPanel from '@/components/PendingPanel.vue';
const endpoints = ['GET /super/v1/notifications', 'POST /super/v1/notifications/broadcast', 'POST /super/v1/notifications/templates', 'GET /super/v1/notifications/templates'];
const notes = ['The current notification API is user-scoped only.', 'Add super admin send and template endpoints before enabling operations.'];
</script>
<template>
<PendingPanel title="Notifications" description="Notification management is pending super admin APIs." :endpoints="endpoints" :notes="notes" />
</template>

View File

@@ -176,9 +176,7 @@ watch(
</div> </div>
</template> </template>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="text-sm text-muted-color"> <div class="text-sm text-muted-color">该操作会将订单从 <span class="font-medium">paid</span> 推进到 <span class="font-medium">refunding</span> 并提交异步退款任务</div>
该操作会将订单从 <span class="font-medium">paid</span> 推进到 <span class="font-medium">refunding</span> 并提交异步退款任务
</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox v-model="refundForce" inputId="refundForce" binary /> <Checkbox v-model="refundForce" inputId="refundForce" binary />
<label for="refundForce" class="cursor-pointer">强制退款绕过默认时间窗</label> <label for="refundForce" class="cursor-pointer">强制退款绕过默认时间窗</label>
@@ -194,4 +192,3 @@ watch(
</template> </template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -328,15 +328,7 @@ loadOrders();
</Column> </Column>
<Column header="操作" style="min-width: 10rem"> <Column header="操作" style="min-width: 10rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button label="退款" icon="pi pi-replay" text size="small" class="p-0" :disabled="data?.status !== 'paid'" @click="openRefundDialog(data)" />
label="退款"
icon="pi pi-replay"
text
size="small"
class="p-0"
:disabled="data?.status !== 'paid'"
@click="openRefundDialog(data)"
/>
</template> </template>
</Column> </Column>
</DataTable> </DataTable>
@@ -352,9 +344,7 @@ loadOrders();
</div> </div>
</template> </template>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="text-sm text-muted-color"> <div class="text-sm text-muted-color">该操作会将订单从 <span class="font-medium">paid</span> 推进到 <span class="font-medium">refunding</span> 并提交异步退款任务</div>
该操作会将订单从 <span class="font-medium">paid</span> 推进到 <span class="font-medium">refunding</span> 并提交异步退款任务
</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox v-model="refundForce" inputId="refundForce" binary /> <Checkbox v-model="refundForce" inputId="refundForce" binary />
<label for="refundForce" class="cursor-pointer">强制退款绕过默认时间窗</label> <label for="refundForce" class="cursor-pointer">强制退款绕过默认时间窗</label>
@@ -366,14 +356,7 @@ loadOrders();
</div> </div>
<template #footer> <template #footer>
<Button label="取消" icon="pi pi-times" text @click="refundDialogVisible = false" :disabled="refundLoading" /> <Button label="取消" icon="pi pi-times" text @click="refundDialogVisible = false" :disabled="refundLoading" />
<Button <Button label="确认退款" icon="pi pi-check" severity="danger" @click="confirmRefund" :loading="refundLoading" :disabled="refundOrder?.status !== 'paid'" />
label="确认退款"
icon="pi pi-check"
severity="danger"
@click="confirmRefund"
:loading="refundLoading"
:disabled="refundOrder?.status !== 'paid'"
/>
</template> </template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -0,0 +1,11 @@
<script setup>
import PendingPanel from '@/components/PendingPanel.vue';
const endpoints = ['GET /super/v1/reports/overview', 'GET /super/v1/reports/series', 'POST /super/v1/reports/export'];
const notes = ['Current report APIs are scoped to creators in tenant context.', 'Add cross-tenant aggregation before wiring charts and exports.'];
</script>
<template>
<PendingPanel title="Reports" description="Platform reporting needs aggregated super admin APIs." :endpoints="endpoints" :notes="notes" />
</template>

View File

@@ -111,9 +111,7 @@ async function ensureStatusOptionsLoaded() {
statusOptionsLoading.value = true; statusOptionsLoading.value = true;
try { try {
const list = await TenantService.getTenantStatuses(); const list = await TenantService.getTenantStatuses();
statusOptions.value = (list || []) statusOptions.value = (list || []).map((kv) => ({ label: kv?.value ?? kv?.key ?? '-', value: kv?.key ?? '' })).filter((item) => item.value);
.map((kv) => ({ label: kv?.value ?? kv?.key ?? '-', value: kv?.key ?? '' }))
.filter((item) => item.value);
} finally { } finally {
statusOptionsLoading.value = false; statusOptionsLoading.value = false;
} }
@@ -642,14 +640,7 @@ onMounted(() => {
<Select v-model="contentsStatus" :options="contentStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" /> <Select v-model="contentsStatus" :options="contentStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField> </SearchField>
<SearchField label="可见性"> <SearchField label="可见性">
<Select <Select v-model="contentsVisibility" :options="contentVisibilityOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
v-model="contentsVisibility"
:options="contentVisibilityOptions"
optionLabel="label"
optionValue="value"
placeholder="请选择"
class="w-full"
/>
</SearchField> </SearchField>
<SearchField label="OwnerUserID"> <SearchField label="OwnerUserID">
<InputNumber v-model="contentsOwnerUserID" :min="1" placeholder="精确匹配" class="w-full" /> <InputNumber v-model="contentsOwnerUserID" :min="1" placeholder="精确匹配" class="w-full" />
@@ -717,10 +708,7 @@ onMounted(() => {
</Column> </Column>
<Column header="可见性" sortable sortField="visibility" style="min-width: 12rem"> <Column header="可见性" sortable sortField="visibility" style="min-width: 12rem">
<template #body="{ data }"> <template #body="{ data }">
<Tag <Tag :value="data?.visibility_description || data?.content?.visibility || '-'" :severity="getContentVisibilitySeverity(data?.content?.visibility)" />
:value="data?.visibility_description || data?.content?.visibility || '-'"
:severity="getContentVisibilitySeverity(data?.content?.visibility)"
/>
</template> </template>
</Column> </Column>
<Column header="价格" style="min-width: 10rem"> <Column header="价格" style="min-width: 10rem">
@@ -743,16 +731,7 @@ onMounted(() => {
</Column> </Column>
<Column header="操作" style="min-width: 10rem"> <Column header="操作" style="min-width: 10rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button v-if="data?.content?.status === 'published'" label="下架" icon="pi pi-ban" severity="danger" text size="small" class="p-0" @click="openUnpublishDialog(data)" />
v-if="data?.content?.status === 'published'"
label="下架"
icon="pi pi-ban"
severity="danger"
text
size="small"
class="p-0"
@click="openUnpublishDialog(data)"
/>
<span v-else class="text-muted-color">-</span> <span v-else class="text-muted-color">-</span>
</template> </template>
</Column> </Column>

View File

@@ -448,15 +448,7 @@ onMounted(() => {
<Column field="code" header="Code" style="min-width: 10rem" /> <Column field="code" header="Code" style="min-width: 10rem" />
<Column field="name" header="名称" sortable style="min-width: 16rem"> <Column field="name" header="名称" sortable style="min-width: 16rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="data.name || '-'" icon="pi pi-external-link" text size="small" class="p-0" as="router-link" :to="`/superadmin/tenants/${data.id}`" />
:label="data.name || '-'"
icon="pi pi-external-link"
text
size="small"
class="p-0"
as="router-link"
:to="`/superadmin/tenants/${data.id}`"
/>
</template> </template>
</Column> </Column>
<Column field="user_id" header="Owner" sortable style="min-width: 12rem"> <Column field="user_id" header="Owner" sortable style="min-width: 12rem">
@@ -480,14 +472,7 @@ onMounted(() => {
</Column> </Column>
<Column field="user_count" header="用户数" sortable style="min-width: 8rem"> <Column field="user_count" header="用户数" sortable style="min-width: 8rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="String(data.user_count ?? 0)" text size="small" icon="pi pi-users" class="p-0" @click="openTenantUsersDialog(data)" />
:label="String(data.user_count ?? 0)"
text
size="small"
icon="pi pi-users"
class="p-0"
@click="openTenantUsersDialog(data)"
/>
</template> </template>
</Column> </Column>
<Column field="income_amount_paid_sum" header="累计收入" sortable style="min-width: 10rem"> <Column field="income_amount_paid_sum" header="累计收入" sortable style="min-width: 10rem">

View File

@@ -78,9 +78,7 @@ async function ensureStatusOptionsLoaded() {
statusOptionsLoading.value = true; statusOptionsLoading.value = true;
try { try {
const list = await UserService.getUserStatuses(); const list = await UserService.getUserStatuses();
statusOptions.value = (list || []) statusOptions.value = (list || []).map((kv) => ({ label: kv?.value ?? kv?.key ?? '-', value: kv?.key ?? '' })).filter((item) => item.value);
.map((kv) => ({ label: kv?.value ?? kv?.key ?? '-', value: kv?.key ?? '' }))
.filter((item) => item.value);
} finally { } finally {
statusOptionsLoading.value = false; statusOptionsLoading.value = false;
} }
@@ -336,15 +334,7 @@ onMounted(() => {
<Column field="code" header="Code" style="min-width: 10rem" /> <Column field="code" header="Code" style="min-width: 10rem" />
<Column field="name" header="名称" style="min-width: 14rem"> <Column field="name" header="名称" style="min-width: 14rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="data.name || '-'" icon="pi pi-external-link" text size="small" class="p-0" as="router-link" :to="`/superadmin/tenants/${data.id}`" />
:label="data.name || '-'"
icon="pi pi-external-link"
text
size="small"
class="p-0"
as="router-link"
:to="`/superadmin/tenants/${data.id}`"
/>
</template> </template>
</Column> </Column>
<Column field="status_description" header="状态" style="min-width: 10rem" /> <Column field="status_description" header="状态" style="min-width: 10rem" />
@@ -393,15 +383,7 @@ onMounted(() => {
/> />
</SearchField> </SearchField>
<SearchField label="成员状态"> <SearchField label="成员状态">
<Select <Select v-model="joinedTenantsStatus" :options="statusFilterOptions" optionLabel="label" optionValue="value" placeholder="请选择" :loading="statusOptionsLoading" class="w-full" />
v-model="joinedTenantsStatus"
:options="statusFilterOptions"
optionLabel="label"
optionValue="value"
placeholder="请选择"
:loading="statusOptionsLoading"
class="w-full"
/>
</SearchField> </SearchField>
<SearchField label="加入时间 From"> <SearchField label="加入时间 From">
<DatePicker v-model="joinedTenantsJoinedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" /> <DatePicker v-model="joinedTenantsJoinedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
@@ -432,15 +414,7 @@ onMounted(() => {
<Column field="code" header="Code" style="min-width: 10rem" /> <Column field="code" header="Code" style="min-width: 10rem" />
<Column field="name" header="名称" style="min-width: 14rem"> <Column field="name" header="名称" style="min-width: 14rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="data.name || '-'" icon="pi pi-external-link" text size="small" class="p-0" as="router-link" :to="`/superadmin/tenants/${data.tenant_id}`" />
:label="data.name || '-'"
icon="pi pi-external-link"
text
size="small"
class="p-0"
as="router-link"
:to="`/superadmin/tenants/${data.tenant_id}`"
/>
</template> </template>
</Column> </Column>
<Column header="Owner" style="min-width: 12rem"> <Column header="Owner" style="min-width: 12rem">

View File

@@ -475,15 +475,7 @@ onMounted(() => {
<Column field="id" header="ID" sortable style="min-width: 6rem" /> <Column field="id" header="ID" sortable style="min-width: 6rem" />
<Column field="username" header="用户名" sortable style="min-width: 16rem"> <Column field="username" header="用户名" sortable style="min-width: 16rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="data.username || '-'" icon="pi pi-external-link" text size="small" class="p-0" as="router-link" :to="`/superadmin/users/${data.id}`" />
:label="data.username || '-'"
icon="pi pi-external-link"
text
size="small"
class="p-0"
as="router-link"
:to="`/superadmin/users/${data.id}`"
/>
</template> </template>
</Column> </Column>
<Column field="status" header="状态" sortable style="min-width: 10rem"> <Column field="status" header="状态" sortable style="min-width: 10rem">
@@ -501,14 +493,7 @@ onMounted(() => {
</Column> </Column>
<Column header="超管" style="min-width: 9rem"> <Column header="超管" style="min-width: 9rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="hasRole(data, 'super_admin') ? '是' : '否'" icon="pi pi-user-edit" text size="small" class="p-0" @click="openRolesDialog(data)" />
:label="hasRole(data, 'super_admin') ? '是' : '否'"
icon="pi pi-user-edit"
text
size="small"
class="p-0"
@click="openRolesDialog(data)"
/>
</template> </template>
</Column> </Column>
<Column field="balance" header="余额" sortable style="min-width: 10rem"> <Column field="balance" header="余额" sortable style="min-width: 10rem">
@@ -523,28 +508,12 @@ onMounted(() => {
</Column> </Column>
<Column header="拥有租户" style="min-width: 10rem"> <Column header="拥有租户" style="min-width: 10rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="String(data.owned_tenant_count ?? 0)" icon="pi pi-building" text size="small" class="p-0" :disabled="(data.owned_tenant_count ?? 0) === 0" @click="openOwnedTenantsDialog(data)" />
:label="String(data.owned_tenant_count ?? 0)"
icon="pi pi-building"
text
size="small"
class="p-0"
:disabled="(data.owned_tenant_count ?? 0) === 0"
@click="openOwnedTenantsDialog(data)"
/>
</template> </template>
</Column> </Column>
<Column header="加入租户" style="min-width: 10rem"> <Column header="加入租户" style="min-width: 10rem">
<template #body="{ data }"> <template #body="{ data }">
<Button <Button :label="String(data.joined_tenant_count ?? 0)" icon="pi pi-users" text size="small" class="p-0" :disabled="(data.joined_tenant_count ?? 0) === 0" @click="openJoinedTenantsDialog(data)" />
:label="String(data.joined_tenant_count ?? 0)"
icon="pi pi-users"
text
size="small"
class="p-0"
:disabled="(data.joined_tenant_count ?? 0) === 0"
@click="openJoinedTenantsDialog(data)"
/>
</template> </template>
</Column> </Column>
<Column field="verified_at" header="认证时间" sortable style="min-width: 14rem"> <Column field="verified_at" header="认证时间" sortable style="min-width: 14rem">