feat: M0-11 membership + quota + cost admin web page
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 10s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 10s
This commit is contained in:
parent
2769f108f6
commit
bc9ad19426
79
src/pages/Membership.tsx
Normal file
79
src/pages/Membership.tsx
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user