feat: M0-11 membership + quota + cost admin web page
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 10s

This commit is contained in:
WangDL 2026-05-23 20:15:24 +08:00
parent 2769f108f6
commit bc9ad19426

79
src/pages/Membership.tsx Normal file
View File

@ -0,0 +1,79 @@
import { useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Table, Button, Typography, App, Modal, Input, InputNumber, Tag, Tabs, Space } from 'antd'
import { ReloadOutlined, PlusOutlined, DollarOutlined } from '@ant-design/icons'
import { api } from '@/services/http-client'
import dayjs from 'dayjs'
const { Title } = Typography
function MembershipPage() {
const { message } = App.useApp()
const qc = useQueryClient()
const [addOpen, setAddOpen] = useState(false)
const [newPlan, setNewPlan] = useState({ name: '', code: '', priceMonthly: 0, monthlyChatCount: 10 })
const { data: plans } = useQuery({ queryKey: ['quota', 'plans'], queryFn: (): Promise<any> => api.get('/admin-api/quota/plans') })
const { data: memberships } = useQuery({ queryKey: ['quota', 'memberships'], queryFn: (): Promise<any> => api.get('/admin-api/quota/memberships') })
const { data: costs } = useQuery({ queryKey: ['quota', 'costs'], queryFn: (): Promise<any> => api.get('/admin-api/quota/costs') })
const addPlan = async () => {
await api.post('/admin-api/quota/plans', newPlan)
message.success('已添加')
setAddOpen(false)
qc.invalidateQueries({ queryKey: ['quota'] })
}
const planCols = [
{ title: '名称', dataIndex: 'name', width: 120 },
{ title: '代码', dataIndex: 'code', width: 100 },
{ title: '月费', dataIndex: 'priceMonthly', width: 80, render: (v: number) => `¥${v}` },
{ title: '年费', dataIndex: 'priceYearly', width: 80, render: (v: number) => `¥${v}` },
{ title: '月聊天', dataIndex: 'monthlyChatCount', width: 80 },
{ title: '月OCR', dataIndex: 'monthlyOcrPages', width: 80 },
{ title: '状态', dataIndex: 'isActive', width: 70, render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag> },
]
const memberCols = [
{ title: '用户', dataIndex: ['user', 'email'], width: 200 },
{ title: '计划', dataIndex: ['plan', 'name'], width: 120 },
{ title: '开始', dataIndex: 'startedAt', width: 100, render: (d: string) => dayjs(d).format('MM-DD') },
{ title: '状态', dataIndex: 'active', width: 70, render: (v: boolean) => <Tag color={v ? 'green' : 'red'}>{v ? '有效' : '失效'}</Tag> },
]
const costCols = [
{ title: '日期', dataIndex: 'date', width: 100, render: (d: string) => dayjs(d).format('MM-DD') },
{ title: '服务商', dataIndex: 'provider', width: 80 },
{ title: '模型', dataIndex: 'model', width: 120 },
{ title: '调用', dataIndex: 'calls', width: 70, align: 'center' as const },
{ title: 'Token', dataIndex: 'tokens', width: 90, align: 'center' as const },
{ title: '费用', dataIndex: 'cost', width: 80, render: (v: number) => `¥${v.toFixed(4)}` },
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Title level={5} style={{ margin: 0 }}><DollarOutlined /> </Title>
<Space>
<Button icon={<PlusOutlined />} type="primary" onClick={() => setAddOpen(true)}></Button>
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['quota'] })}></Button>
</Space>
</div>
<Tabs items={[
{ key: 'plans', label: '会员计划', children: <Table dataSource={plans || []} columns={planCols} rowKey="id" pagination={false} size="small" /> },
{ key: 'members', label: '用户会员', children: <Table dataSource={memberships || []} columns={memberCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" /> },
{ key: 'costs', label: '成本汇总', children: <Table dataSource={costs || []} columns={costCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" /> },
]} />
<Modal title="新增会员计划" open={addOpen} onOk={addPlan} onCancel={() => setAddOpen(false)} okText="添加">
<Input placeholder="名称" value={newPlan.name} onChange={e => setNewPlan({ ...newPlan, name: e.target.value })} style={{ marginBottom: 12 }} />
<Input placeholder="代码" value={newPlan.code} onChange={e => setNewPlan({ ...newPlan, code: e.target.value })} style={{ marginBottom: 12 }} />
<InputNumber placeholder="月费" value={newPlan.priceMonthly} onChange={v => setNewPlan({ ...newPlan, priceMonthly: v || 0 })} style={{ width: '100%', marginBottom: 12 }} prefix="¥" />
<InputNumber placeholder="月聊天次数" value={newPlan.monthlyChatCount} onChange={v => setNewPlan({ ...newPlan, monthlyChatCount: v || 0 })} style={{ width: '100%' }} />
</Modal>
</div>
)
}
export default MembershipPage