feat(admin-web): expose AI point balance and usage ledger
Surfaces the new AI point quota in the workspace shell and adds a Personal Center → AI Point Usage page that shows balance, base-char rule, and the paginated reservation/refund history. Updates shared types for the QuotaSummary and ledger payloads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,8 @@ const enUS = {
|
||||
kolManage: "Prompt Management",
|
||||
kolDashboard: "Dashboard",
|
||||
kolProfile: "Profile",
|
||||
personalCenter: "Personal Center",
|
||||
aiPointUsage: "AI Point Usage",
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: "Welcome back",
|
||||
@@ -117,10 +119,31 @@ const enUS = {
|
||||
quotaTitle: "Plan & Quota",
|
||||
quotaStatus: "Healthy",
|
||||
remainingQuota: "Article quota",
|
||||
aiPoints: "AI points",
|
||||
resetAt: "Reset at",
|
||||
userFallback: "Admin",
|
||||
logout: "Logout",
|
||||
planFallback: "Free Plan",
|
||||
aiUsageTitle: "AI Point Usage",
|
||||
aiUsageRecent: "Recent Usage",
|
||||
aiUsageEmpty: "No AI point usage yet",
|
||||
aiUsageBase: "Tiered every {base} chars",
|
||||
aiUsageRule: "{chars} chars / base {base}",
|
||||
aiUsagePendingHelp: "Points have been reserved. They will be confirmed once the AI task completes; if generation fails, the points are returned automatically.",
|
||||
aiUsageTypes: {
|
||||
article_selection_optimize: "Article Selection Optimize",
|
||||
template_analyze: "AI Analysis",
|
||||
template_title_generate: "AI Title Generation",
|
||||
template_outline_generate: "AI Outline Generation",
|
||||
kol_prompt_generate: "KOL Prompt Generation",
|
||||
kol_prompt_optimize: "KOL Prompt Optimize",
|
||||
},
|
||||
aiUsageStatus: {
|
||||
pending: "Processing",
|
||||
completed: "Charged",
|
||||
refunded: "Refunded",
|
||||
failed: "Failed",
|
||||
},
|
||||
},
|
||||
route: {
|
||||
login: {
|
||||
@@ -183,6 +206,22 @@ const enUS = {
|
||||
title: "Image Management",
|
||||
description: "Manage image assets, including folders, reference analysis, and cover library usage.",
|
||||
},
|
||||
aiPoints: {
|
||||
title: "AI Point Usage",
|
||||
description: "Review AI point balance, cycle rules, and recent usage records.",
|
||||
},
|
||||
},
|
||||
aiPoints: {
|
||||
balance: "Available Points",
|
||||
baseChars: "Base Characters",
|
||||
ledgerTitle: "Usage Ledger",
|
||||
ledgerLimit: "Latest {limit} records",
|
||||
ledgerTotal: "{total} records",
|
||||
table: {
|
||||
usageType: "Usage",
|
||||
points: "Points",
|
||||
requestChars: "Metered Chars",
|
||||
},
|
||||
},
|
||||
kol: {
|
||||
marketplace: {
|
||||
|
||||
@@ -86,6 +86,8 @@ const zhCN = {
|
||||
kolManage: "提示词管理",
|
||||
kolDashboard: "数据看板",
|
||||
kolProfile: "个人主页",
|
||||
personalCenter: "个人中心",
|
||||
aiPointUsage: "AI 点数明细",
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: "欢迎回来",
|
||||
@@ -117,10 +119,31 @@ const zhCN = {
|
||||
quotaTitle: "套餐与额度",
|
||||
quotaStatus: "状态正常",
|
||||
remainingQuota: "生成文章",
|
||||
aiPoints: "AI 点数",
|
||||
resetAt: "重置时间",
|
||||
userFallback: "管理员",
|
||||
logout: "退出",
|
||||
planFallback: "免费版",
|
||||
aiUsageTitle: "AI 点数明细",
|
||||
aiUsageRecent: "最近消耗",
|
||||
aiUsageEmpty: "暂无 AI 点数流水",
|
||||
aiUsageBase: "每 {base} 字进一档",
|
||||
aiUsageRule: "{chars} 字 / 基准 {base}",
|
||||
aiUsagePendingHelp: "点数已先行占用,AI 任务完成后会确认为扣除;如果生成失败,系统会自动退回点数。",
|
||||
aiUsageTypes: {
|
||||
article_selection_optimize: "文章片段优化",
|
||||
template_analyze: "AI 分析",
|
||||
template_title_generate: "AI 生成标题",
|
||||
template_outline_generate: "AI 生成大纲",
|
||||
kol_prompt_generate: "KOL 模版生成",
|
||||
kol_prompt_optimize: "KOL 模版优化",
|
||||
},
|
||||
aiUsageStatus: {
|
||||
pending: "处理中",
|
||||
completed: "已扣除",
|
||||
refunded: "已退回",
|
||||
failed: "失败",
|
||||
},
|
||||
},
|
||||
route: {
|
||||
login: {
|
||||
@@ -183,6 +206,22 @@ const zhCN = {
|
||||
title: "图片管理",
|
||||
description: "管理图片素材库,支持分组管理、引用分析与封面图库引用。",
|
||||
},
|
||||
aiPoints: {
|
||||
title: "AI 点数明细",
|
||||
description: "查看当前套餐 AI 点数余额、周期规则和最近消耗流水。",
|
||||
},
|
||||
},
|
||||
aiPoints: {
|
||||
balance: "可用点数",
|
||||
baseChars: "基准字数",
|
||||
ledgerTitle: "消耗流水",
|
||||
ledgerLimit: "最近 {limit} 条记录",
|
||||
ledgerTotal: "共 {total} 条记录",
|
||||
table: {
|
||||
usageType: "使用场景",
|
||||
points: "点数",
|
||||
requestChars: "计费字数",
|
||||
},
|
||||
},
|
||||
kol: {
|
||||
marketplace: {
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
AppstoreOutlined,
|
||||
BookOutlined,
|
||||
GlobalOutlined,
|
||||
HistoryOutlined,
|
||||
RadarChartOutlined,
|
||||
SearchOutlined,
|
||||
PictureOutlined,
|
||||
DownOutlined,
|
||||
LogoutOutlined,
|
||||
TranslationOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { computed, type Component } from "vue";
|
||||
@@ -30,6 +30,7 @@ const quotaQuery = useQuery({
|
||||
});
|
||||
|
||||
const userInitial = computed(() => authStore.user?.name?.slice(0, 1).toUpperCase() ?? "A");
|
||||
const quotaSummary = computed(() => quotaQuery.data.value);
|
||||
const selectedKeys = computed(() => {
|
||||
if (route.meta.navKey === null) {
|
||||
return [];
|
||||
@@ -125,6 +126,13 @@ const navSections = computed<NavSection[]>(() => {
|
||||
{ key: "/kol/marketplace", label: t("nav.kolMarketplace") },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "personalCenter",
|
||||
title: t("nav.personalCenter"),
|
||||
items: [
|
||||
{ key: "/account/ai-points", label: t("nav.aiPointUsage"), icon: HistoryOutlined },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (authStore.isActiveKol) {
|
||||
@@ -279,10 +287,13 @@ async function handleLogout(): Promise<void> {
|
||||
|
||||
<!-- Quota Indicator -->
|
||||
<div class="quota-pill">
|
||||
<a-tag color="blue" :bordered="false" class="quota-pill-tag">{{ quotaQuery.data.value?.plan_name || t("shell.planFallback") }}</a-tag>
|
||||
<a-tag color="blue" :bordered="false" class="quota-pill-tag">{{ quotaSummary?.plan_name || t("shell.planFallback") }}</a-tag>
|
||||
<span class="quota-pill-text">
|
||||
{{ t("shell.remainingQuota") }}: <strong>{{ quotaQuery.data.value?.balance ?? '--' }}</strong>
|
||||
<span class="quota-pill-total">/ {{ quotaQuery.data.value?.total_quota ?? '--' }}</span>
|
||||
{{ t("shell.remainingQuota") }}: <strong>{{ quotaSummary?.balance ?? '--' }}</strong>
|
||||
<span class="quota-pill-total">/ {{ quotaSummary?.total_quota ?? '--' }}</span>
|
||||
<span class="quota-pill-divider">·</span>
|
||||
{{ t("shell.aiPoints") }}: <strong>{{ quotaSummary?.ai_points_balance ?? '--' }}</strong>
|
||||
<span class="quota-pill-total">/ {{ quotaSummary?.ai_points_total ?? '--' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -485,6 +496,11 @@ async function handleLogout(): Promise<void> {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.quota-pill-divider {
|
||||
color: #bfbfbf;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.user-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ArticleListResponse,
|
||||
CreateArticleRequest,
|
||||
ArticleVersion,
|
||||
AIPointUsageListResponse,
|
||||
GenerateImitationRequest,
|
||||
GenerateImitationResponse,
|
||||
AuthTokens,
|
||||
@@ -293,6 +294,9 @@ export const workspaceApi = {
|
||||
quotaSummary() {
|
||||
return apiClient.get<QuotaSummary>("/api/tenant/workspace/quota-summary");
|
||||
},
|
||||
aiPointUsage(params?: { page?: number; page_size?: number; limit?: number; offset?: number }) {
|
||||
return apiClient.get<AIPointUsageListResponse>("/api/tenant/workspace/ai-point-usage", { params });
|
||||
},
|
||||
templateCards() {
|
||||
return apiClient.get<TemplateCard[]>("/api/tenant/workspace/template-cards");
|
||||
},
|
||||
|
||||
@@ -231,6 +231,16 @@ const router = createRouter({
|
||||
navKey: "/articles/templates",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "account/ai-points",
|
||||
name: "account-ai-points",
|
||||
component: () => import("@/views/AIPointUsageView.vue"),
|
||||
meta: {
|
||||
titleKey: "route.aiPoints.title",
|
||||
descriptionKey: "route.aiPoints.description",
|
||||
navKey: "/account/ai-points",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CalculatorOutlined,
|
||||
FieldTimeOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import type { AIPointUsageLog } from "@geo/shared-types";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { TableColumnsType } from "ant-design-vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import { workspaceApi } from "@/lib/api";
|
||||
import { formatDateTime } from "@/lib/display";
|
||||
|
||||
const { t } = useI18n();
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
const quotaQuery = useQuery({
|
||||
queryKey: ["workspace", "quota-summary", "ai-points"],
|
||||
queryFn: () => workspaceApi.quotaSummary(),
|
||||
});
|
||||
|
||||
const usageQuery = useQuery({
|
||||
queryKey: computed(() => ["workspace", "ai-point-usage", page.value, pageSize.value]),
|
||||
queryFn: () => workspaceApi.aiPointUsage({ page: page.value, page_size: pageSize.value }),
|
||||
});
|
||||
|
||||
const quotaSummary = computed(() => quotaQuery.data.value);
|
||||
const usageRows = computed(() => usageQuery.data.value?.items ?? []);
|
||||
const usageTotal = computed(() => usageQuery.data.value?.total ?? 0);
|
||||
const aiPointUsedPercent = computed(() => {
|
||||
const total = quotaSummary.value?.ai_points_total ?? 0;
|
||||
const used = quotaSummary.value?.ai_points_used ?? 0;
|
||||
if (total <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(100, Math.round((used / total) * 100));
|
||||
});
|
||||
const loading = computed(() => quotaQuery.isPending.value || usageQuery.isPending.value);
|
||||
const refreshing = computed(() => quotaQuery.isFetching.value || usageQuery.isFetching.value);
|
||||
|
||||
const columns = computed<TableColumnsType<AIPointUsageLog>>(() => [
|
||||
{
|
||||
title: t("aiPoints.table.usageType"),
|
||||
dataIndex: "usage_type",
|
||||
key: "usage_type",
|
||||
},
|
||||
{
|
||||
title: t("aiPoints.table.points"),
|
||||
dataIndex: "points",
|
||||
key: "points",
|
||||
width: 110,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
title: t("aiPoints.table.requestChars"),
|
||||
dataIndex: "request_chars",
|
||||
key: "request_chars",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: t("common.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 118,
|
||||
},
|
||||
{
|
||||
title: t("common.createdAt"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
width: 172,
|
||||
},
|
||||
]);
|
||||
|
||||
function refresh(): void {
|
||||
void quotaQuery.refetch();
|
||||
void usageQuery.refetch();
|
||||
}
|
||||
|
||||
function handleTableChange(nextPage: number, nextPageSize: number): void {
|
||||
page.value = nextPage;
|
||||
pageSize.value = nextPageSize;
|
||||
}
|
||||
|
||||
function getAIPointUsageTypeLabel(usageType: string): string {
|
||||
const key = `shell.aiUsageTypes.${usageType}`;
|
||||
const label = t(key);
|
||||
return label === key ? usageType : label;
|
||||
}
|
||||
|
||||
function getAIPointStatusMeta(status: string): { label: string; color: string } {
|
||||
const key = `shell.aiUsageStatus.${status}`;
|
||||
const label = t(key);
|
||||
const normalizedLabel = label === key ? status : label;
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return { label: normalizedLabel, color: "success" };
|
||||
case "pending":
|
||||
return { label: normalizedLabel, color: "processing" };
|
||||
case "refunded":
|
||||
return { label: normalizedLabel, color: "default" };
|
||||
default:
|
||||
return { label: normalizedLabel, color: "error" };
|
||||
}
|
||||
}
|
||||
|
||||
function formatAIPointDelta(item: AIPointUsageLog): string {
|
||||
return item.status === "refunded" ? `+${item.points}` : `-${item.points}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-points-page">
|
||||
<header class="ai-points-header">
|
||||
<div>
|
||||
<h1>{{ t("route.aiPoints.title") }}</h1>
|
||||
<p>{{ t("route.aiPoints.description") }}</p>
|
||||
</div>
|
||||
<a-button :loading="refreshing" @click="refresh">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
{{ t("common.refresh") }}
|
||||
</a-button>
|
||||
</header>
|
||||
|
||||
<section class="ai-points-summary">
|
||||
<div class="ai-points-balance">
|
||||
<span class="summary-label">{{ t("aiPoints.balance") }}</span>
|
||||
<strong>{{ quotaSummary?.ai_points_balance ?? "--" }}</strong>
|
||||
<a-progress :percent="aiPointUsedPercent" :show-info="false" stroke-color="#1677ff" />
|
||||
</div>
|
||||
|
||||
<div class="summary-metric">
|
||||
<ThunderboltOutlined />
|
||||
<span>{{ t("common.used") }}</span>
|
||||
<strong>{{ quotaSummary?.ai_points_used ?? "--" }} / {{ quotaSummary?.ai_points_total ?? "--" }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="summary-metric">
|
||||
<CalculatorOutlined />
|
||||
<span>{{ t("aiPoints.baseChars") }}</span>
|
||||
<strong>{{ quotaSummary?.ai_point_base_chars ?? "--" }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="summary-metric">
|
||||
<FieldTimeOutlined />
|
||||
<span>{{ t("shell.resetAt") }}</span>
|
||||
<strong>{{ formatDateTime(quotaSummary?.ai_points_reset_at) }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ai-points-ledger">
|
||||
<div class="ledger-head">
|
||||
<div>
|
||||
<h2>{{ t("aiPoints.ledgerTitle") }}</h2>
|
||||
<span>{{ t("aiPoints.ledgerTotal", { total: usageTotal }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="usageRows"
|
||||
:loading="loading"
|
||||
:locale="{ emptyText: t('shell.aiUsageEmpty') }"
|
||||
:pagination="{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: usageTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: handleTableChange,
|
||||
onShowSizeChange: handleTableChange,
|
||||
}"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'usage_type'">
|
||||
<div class="usage-type-cell">
|
||||
<strong>{{ getAIPointUsageTypeLabel(record.usage_type) }}</strong>
|
||||
<span v-if="record.model">{{ record.model }}</span>
|
||||
<span v-else-if="record.error_message" class="usage-error">{{ record.error_message }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'points'">
|
||||
<span class="points-delta" :class="{ 'points-delta--refund': record.status === 'refunded' }">
|
||||
{{ formatAIPointDelta(record) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'request_chars'">
|
||||
<span class="usage-rule">
|
||||
{{ t("shell.aiUsageRule", { chars: record.request_chars, base: record.base_chars }) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag class="status-tag" :color="getAIPointStatusMeta(record.status).color" :bordered="false">
|
||||
{{ getAIPointStatusMeta(record.status).label }}
|
||||
<a-tooltip v-if="record.status === 'pending'" :title="t('shell.aiUsagePendingHelp')">
|
||||
<QuestionCircleOutlined class="status-help-icon" />
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
{{ formatDateTime(record.created_at) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ai-points-page {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.ai-points-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ai-points-header h1,
|
||||
.ledger-head h2 {
|
||||
margin: 0;
|
||||
color: #141414;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.ai-points-header h1 {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.ai-points-header p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.ai-points-summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1.5fr) repeat(3, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ai-points-balance,
|
||||
.summary-metric,
|
||||
.ai-points-ledger {
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ai-points-balance {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.summary-label,
|
||||
.summary-metric span,
|
||||
.ledger-head span,
|
||||
.usage-type-cell span,
|
||||
.usage-rule {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ai-points-balance strong {
|
||||
display: block;
|
||||
margin: 6px 0 12px;
|
||||
color: #141414;
|
||||
font-size: 36px;
|
||||
font-weight: 750;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
.summary-metric {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
min-height: 112px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.summary-metric :deep(.anticon) {
|
||||
color: #1677ff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.summary-metric span,
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.summary-metric strong {
|
||||
color: #141414;
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.ai-points-ledger {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ledger-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.ledger-head h2 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.ledger-head span {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.usage-type-cell {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.usage-type-cell strong {
|
||||
color: #141414;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.usage-type-cell span,
|
||||
.usage-rule {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.usage-error {
|
||||
color: #cf1322 !important;
|
||||
}
|
||||
|
||||
.points-delta {
|
||||
color: #cf1322;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.points-delta--refund {
|
||||
color: #389e0d;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-help-icon {
|
||||
color: #1677ff;
|
||||
font-size: 12px;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.ai-points-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.ai-points-header {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.ai-points-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -48,6 +48,8 @@ export interface MembershipInfo {
|
||||
end_at: string | null;
|
||||
article_generation_limit: number;
|
||||
article_quota_cycle: string;
|
||||
ai_points_monthly: number;
|
||||
ai_point_base_chars: number;
|
||||
company_limit: number;
|
||||
image_storage_bytes: number;
|
||||
contact_admin_on_expiry: boolean;
|
||||
@@ -353,6 +355,34 @@ export interface QuotaSummary {
|
||||
used_quota: number;
|
||||
balance: number;
|
||||
reset_at: string | null;
|
||||
ai_points_total: number;
|
||||
ai_points_used: number;
|
||||
ai_points_balance: number;
|
||||
ai_point_base_chars: number;
|
||||
ai_points_reset_at: string | null;
|
||||
}
|
||||
|
||||
export interface AIPointUsageLog {
|
||||
id: number;
|
||||
usage_type: string;
|
||||
resource_type: string | null;
|
||||
resource_id: number | null;
|
||||
resource_uid: string | null;
|
||||
request_chars: number;
|
||||
base_chars: number;
|
||||
points: number;
|
||||
status: string;
|
||||
model: string | null;
|
||||
error_message: string | null;
|
||||
completed_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AIPointUsageListResponse {
|
||||
items: AIPointUsageLog[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface TemplateCard {
|
||||
|
||||
Reference in New Issue
Block a user