93 lines
3.9 KiB
Vue
93 lines
3.9 KiB
Vue
<!-- SPEC: PJ2026-010405 云端控制台. -->
|
|
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
|
|
<!-- Responsibility: 以共享表格和实体卡片语义展示用户目录。 -->
|
|
|
|
<script setup lang="ts">
|
|
import { Users } from "lucide-vue-next";
|
|
import DataGrid, { type DataGridColumn } from "@/components/common/DataGrid.vue";
|
|
import SectionFrame from "@/components/layout/SectionFrame.vue";
|
|
import type { AdminBillingUserSummary } from "@/types";
|
|
|
|
defineProps<{ users: AdminBillingUserSummary[]; selectedId: string | null; view: "list" | "grid"; pendingId: string | null }>();
|
|
const emit = defineEmits<{ select: [userId: string]; status: [user: AdminBillingUserSummary] }>();
|
|
const columns: DataGridColumn[] = [{ key: "user", label: "User" }, { key: "status", label: "Status" }, { key: "credits", label: "Credits" }, { key: "usage", label: "Usage" }, { key: "keys", label: "Keys" }, { key: "actions", label: "Action", align: "right" }];
|
|
const userKey = (item: AdminBillingUserSummary): string => String(item.user?.id || "");
|
|
const formatNumber = (value: unknown): string => Number(value ?? 0).toLocaleString();
|
|
</script>
|
|
|
|
<template>
|
|
<SectionFrame
|
|
:title="view === 'grid' ? '用户实体网格' : '用户紧凑列表'"
|
|
description="选择动作与状态写操作使用同级控件,避免交互元素嵌套。"
|
|
:body-padding="false"
|
|
>
|
|
<div v-if="view === 'grid'" class="user-entity-grid">
|
|
<article
|
|
v-for="item in users"
|
|
:key="userKey(item)"
|
|
class="user-entity-card"
|
|
:data-selected="userKey(item) === selectedId"
|
|
>
|
|
<button
|
|
class="user-entity-select"
|
|
type="button"
|
|
@click="emit('select', item.user?.id || '')"
|
|
>
|
|
<Users :size="20" aria-hidden="true" />
|
|
<strong>{{ item.user?.displayName || item.user?.username }}</strong>
|
|
<small>{{ item.user?.email || item.user?.id }}</small>
|
|
<dl>
|
|
<div><dt>Available</dt><dd>{{ formatNumber(item.credits?.available) }}</dd></div>
|
|
<div><dt>Usage</dt><dd>{{ formatNumber(item.usage?.totalCredits) }}</dd></div>
|
|
<div><dt>Keys</dt><dd>{{ formatNumber(item.apiKeys?.activeCount) }}</dd></div>
|
|
</dl>
|
|
</button>
|
|
<button
|
|
class="table-action"
|
|
type="button"
|
|
:disabled="pendingId === item.user?.id"
|
|
@click="emit('status', item)"
|
|
>
|
|
{{ item.user?.status === "disabled" ? "启用" : "禁用" }}
|
|
</button>
|
|
</article>
|
|
</div>
|
|
<DataGrid
|
|
v-else
|
|
:columns="columns"
|
|
:rows="users"
|
|
:row-key="userKey"
|
|
:selected-id="selectedId"
|
|
aria-label="用户列表"
|
|
@select="emit('select', $event.user?.id || '')"
|
|
>
|
|
<template #cell-user="{ row }">
|
|
<strong>{{ row.user?.displayName || row.user?.username }}</strong>
|
|
<small>{{ row.user?.email || row.user?.id }} · {{ row.planId || "default" }}</small>
|
|
</template>
|
|
<template #cell-status="{ row }">
|
|
<span class="status-pill" :data-status="row.user?.status">
|
|
{{ row.user?.role }} / {{ row.user?.status }}
|
|
</span>
|
|
</template>
|
|
<template #cell-credits="{ row }">
|
|
{{ formatNumber(row.credits?.available) }} / {{ formatNumber(row.credits?.reserved) }}
|
|
</template>
|
|
<template #cell-usage="{ row }">{{ formatNumber(row.usage?.totalCredits) }}</template>
|
|
<template #cell-keys="{ row }">
|
|
{{ formatNumber(row.apiKeys?.activeCount) }} / {{ formatNumber(row.apiKeys?.totalCount) }}
|
|
</template>
|
|
<template #cell-actions="{ row }">
|
|
<button
|
|
class="table-action"
|
|
type="button"
|
|
:disabled="pendingId === row.user?.id"
|
|
@click.stop="emit('status', row)"
|
|
>
|
|
{{ row.user?.status === "disabled" ? "启用" : "禁用" }}
|
|
</button>
|
|
</template>
|
|
</DataGrid>
|
|
</SectionFrame>
|
|
</template>
|