feat(admin,backend): 新增平台总后台超级管理员最高权限体系与会员列表、续费列表、订阅管理中心
这个提交包含在:
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
Tooltip,
|
||||
Input,
|
||||
@@ -6,47 +6,36 @@ import {
|
||||
Select,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Divider,
|
||||
Message,
|
||||
Button,
|
||||
Tag,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconLanguage,
|
||||
IconNotification,
|
||||
IconSunFill,
|
||||
IconMoonFill,
|
||||
IconUser,
|
||||
IconSettings,
|
||||
IconPoweroff,
|
||||
IconExperiment,
|
||||
IconDashboard,
|
||||
IconInteraction,
|
||||
IconTag,
|
||||
IconLoading,
|
||||
IconSafe,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { GlobalState } from '@/store';
|
||||
import { GlobalContext } from '@/context';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import Logo from '@/assets/logo.svg';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
import IconButton from './IconButton';
|
||||
import Settings from '../Settings';
|
||||
import styles from './style/index.module.less';
|
||||
import defaultLocale from '@/locale';
|
||||
import useStorage from '@/utils/useStorage';
|
||||
import { generatePermission } from '@/routes';
|
||||
import { clearAdminTokens } from '@/utils/auth-token';
|
||||
import axios from 'axios';
|
||||
|
||||
function Navbar({ show }: { show: boolean }) {
|
||||
const t = useLocale();
|
||||
const { userInfo, userLoading } = useSelector((state: GlobalState) => state);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [_, setUserStatus] = useStorage('userStatus');
|
||||
const [role, setRole] = useStorage('userRole', 'admin');
|
||||
|
||||
const [, setUserStatus] = useStorage('userStatus');
|
||||
const { setLang, lang, theme, setTheme } = useContext(GlobalContext);
|
||||
|
||||
function logout() {
|
||||
@@ -60,26 +49,6 @@ function Navbar({ show }: { show: boolean }) {
|
||||
});
|
||||
}
|
||||
|
||||
function onMenuItemClick(key) {
|
||||
if (key === 'logout') {
|
||||
logout();
|
||||
} else {
|
||||
Message.info(`You clicked ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'update-userInfo',
|
||||
payload: {
|
||||
userInfo: {
|
||||
...userInfo,
|
||||
permissions: generatePermission(role),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [role]);
|
||||
|
||||
if (!show) {
|
||||
return (
|
||||
<div className={styles['fixed-settings']}>
|
||||
@@ -92,55 +61,26 @@ function Navbar({ show }: { show: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangeRole = () => {
|
||||
const newRole = role === 'admin' ? 'user' : 'admin';
|
||||
setRole(newRole);
|
||||
};
|
||||
const isSuperAdmin = true; // 当前登录账号为系统最高权限账号
|
||||
const roleTitle = lang === 'zh-CN' ? '超级管理员 [最高权限]' : 'Platform Super Admin';
|
||||
const roleScope = lang === 'zh-CN' ? '全平台运营总控' : 'Global Platform';
|
||||
|
||||
const droplist = (
|
||||
<Menu onClickMenuItem={onMenuItemClick}>
|
||||
<Menu.SubMenu
|
||||
key="role"
|
||||
title={
|
||||
<>
|
||||
<IconUser className={styles['dropdown-icon']} />
|
||||
<span className={styles['user-role']}>
|
||||
{role === 'admin'
|
||||
? t['menu.user.role.admin']
|
||||
: t['menu.user.role.user']}
|
||||
</span>
|
||||
</>
|
||||
<Menu
|
||||
onClickMenuItem={(key) => {
|
||||
if (key === 'logout') {
|
||||
logout();
|
||||
}
|
||||
>
|
||||
<Menu.Item onClick={handleChangeRole} key="switch role">
|
||||
<IconTag className={styles['dropdown-icon']} />
|
||||
{t['menu.user.switchRoles']}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Item key="setting">
|
||||
<IconSettings className={styles['dropdown-icon']} />
|
||||
{t['menu.user.setting']}
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="more"
|
||||
title={
|
||||
<div style={{ width: 80 }}>
|
||||
<IconExperiment className={styles['dropdown-icon']} />
|
||||
{t['message.seeMore']}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="workplace">
|
||||
<IconDashboard className={styles['dropdown-icon']} />
|
||||
{t['menu.dashboard.workplace']}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="card list">
|
||||
<IconInteraction className={styles['dropdown-icon']} />
|
||||
{t['menu.list.cardList']}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
|
||||
<Divider style={{ margin: '4px 0' }} />
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '8px 16px', borderBottom: '1px solid var(--color-border-2)' }}>
|
||||
<div style={{ fontWeight: 600, color: 'var(--color-text-1)', fontSize: 13 }}>
|
||||
{userInfo?.name || 'Administrator'}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 2 }}>
|
||||
{roleTitle} · {roleScope}
|
||||
</div>
|
||||
</div>
|
||||
<Menu.Item key="logout">
|
||||
<IconPoweroff className={styles['dropdown-icon']} />
|
||||
{t['navbar.logout']}
|
||||
@@ -148,6 +88,8 @@ function Navbar({ show }: { show: boolean }) {
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const avatarText = userInfo?.name?.trim()?.slice(0, 1) || 'A';
|
||||
|
||||
return (
|
||||
<div className={styles.navbar}>
|
||||
<div className={styles.left}>
|
||||
@@ -184,11 +126,6 @@ function Navbar({ show }: { show: boolean }) {
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<MessageBox>
|
||||
<IconButton icon={<IconNotification />} />
|
||||
</MessageBox>
|
||||
</li>
|
||||
<li>
|
||||
<Tooltip
|
||||
content={
|
||||
@@ -205,13 +142,28 @@ function Navbar({ show }: { show: boolean }) {
|
||||
</li>
|
||||
<Settings />
|
||||
{userInfo && (
|
||||
<li>
|
||||
<li style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag
|
||||
color="arcoblue"
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
fontWeight: 500,
|
||||
fontSize: 12,
|
||||
padding: '2px 8px',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<IconSafe style={{ marginRight: 4 }} />
|
||||
{roleTitle}
|
||||
</Tag>
|
||||
<Dropdown droplist={droplist} position="br" disabled={userLoading}>
|
||||
<Avatar size={32} style={{ cursor: 'pointer' }}>
|
||||
{userLoading ? (
|
||||
<IconLoading />
|
||||
) : userInfo.avatar ? (
|
||||
<img alt="" src={userInfo.avatar} />
|
||||
) : (
|
||||
<img alt="avatar" src={userInfo.avatar} />
|
||||
avatarText
|
||||
)}
|
||||
</Avatar>
|
||||
</Dropdown>
|
||||
|
||||
@@ -4,13 +4,8 @@ import { Layout, Menu, Breadcrumb, Spin } from '@arco-design/web-react';
|
||||
import cs from 'classnames';
|
||||
import {
|
||||
IconDashboard,
|
||||
IconList,
|
||||
IconSettings,
|
||||
IconFile,
|
||||
IconApps,
|
||||
IconCheckCircle,
|
||||
IconExclamationCircle,
|
||||
IconUser,
|
||||
IconMenuFold,
|
||||
IconMenuUnfold,
|
||||
IconIdcard,
|
||||
@@ -18,6 +13,7 @@ import {
|
||||
IconRobot,
|
||||
IconScan,
|
||||
IconUserGroup,
|
||||
IconSafe,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import { useSelector } from 'react-redux';
|
||||
import qs from 'query-string';
|
||||
@@ -54,45 +50,44 @@ function getIconFromKey(key) {
|
||||
return <IconUserGroup className={styles.icon} />;
|
||||
case 'settings-biz':
|
||||
return <IconSettings className={styles.icon} />;
|
||||
case 'list':
|
||||
return <IconList className={styles.icon} />;
|
||||
case 'form':
|
||||
return <IconSettings className={styles.icon} />;
|
||||
case 'profile':
|
||||
return <IconFile className={styles.icon} />;
|
||||
case 'visualization':
|
||||
return <IconApps className={styles.icon} />;
|
||||
case 'result':
|
||||
return <IconCheckCircle className={styles.icon} />;
|
||||
case 'platform':
|
||||
return <IconSafe className={styles.icon} />;
|
||||
case 'exception':
|
||||
return <IconExclamationCircle className={styles.icon} />;
|
||||
case 'user':
|
||||
return <IconUser className={styles.icon} />;
|
||||
default:
|
||||
return <div className={styles['icon-empty']} />;
|
||||
}
|
||||
}
|
||||
|
||||
const KEEP_IGNORED_ROUTES = new Set([
|
||||
'crm/lead-profile',
|
||||
'exception/403',
|
||||
'exception/404',
|
||||
'exception/500',
|
||||
]);
|
||||
|
||||
function getFlattenRoutes(routes) {
|
||||
const mod = import.meta.glob('./pages/**/[a-z[]*.tsx');
|
||||
const res = [];
|
||||
function travel(_routes) {
|
||||
function travel(_routes, parentIgnored = false) {
|
||||
_routes.forEach((route) => {
|
||||
const visibleChildren = (route.children || []).filter(
|
||||
(child) => !child.ignore
|
||||
);
|
||||
if (route.key && (!route.children || !visibleChildren.length)) {
|
||||
try {
|
||||
route.component = lazyload(mod[`./pages/${route.key}/index.tsx`]);
|
||||
res.push(route);
|
||||
} catch (e) {
|
||||
console.log(route.key);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const ignored = parentIgnored || !!route.ignore;
|
||||
if (isArray(route.children) && route.children.length) {
|
||||
travel(route.children);
|
||||
travel(route.children, ignored);
|
||||
return;
|
||||
}
|
||||
if (!route.key) {
|
||||
return;
|
||||
}
|
||||
if (ignored && !KEEP_IGNORED_ROUTES.has(route.key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
route.component = lazyload(mod[`./pages/${route.key}/index.tsx`]);
|
||||
res.push(route);
|
||||
} catch (e) {
|
||||
console.log(route.key);
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -297,7 +292,7 @@ function PageLayout() {
|
||||
</Route>
|
||||
<Route
|
||||
path="*"
|
||||
component={lazyload(() => import('./pages/exception/403'))}
|
||||
component={lazyload(() => import('./pages/exception/404'))}
|
||||
/>
|
||||
</Switch>
|
||||
</Content>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { omniLocales } from './omni.ts';
|
||||
import { omniLocales } from './omni';
|
||||
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
...omniLocales['en-US'].messages,
|
||||
'menu.welcome': 'Welcome',
|
||||
'menu.dashboard': 'Dashboard',
|
||||
'menu.card': 'Digital Cards',
|
||||
'menu.card.list': 'Card Library',
|
||||
@@ -31,31 +30,14 @@ const i18n = {
|
||||
'menu.settings.tenant': 'Domain & SSL',
|
||||
'menu.settings.billing': 'Billing',
|
||||
'menu.settings.auditLogs': 'Audit Logs',
|
||||
'menu.list': 'List',
|
||||
'menu.result': 'Result',
|
||||
'menu.platform': 'Platform Operations',
|
||||
'menu.platform.subscribers': 'Subscribers',
|
||||
'menu.platform.renewals': 'Renewals',
|
||||
'menu.platform.subscriptions': 'Subscription Plans',
|
||||
'menu.exception': 'Exception',
|
||||
'menu.form': 'Form',
|
||||
'menu.profile': 'Profile',
|
||||
'menu.visualization': 'Data Visualization',
|
||||
'menu.user': 'User Center',
|
||||
'menu.exception.403': '403',
|
||||
'menu.exception.404': '404',
|
||||
'menu.exception.500': '500',
|
||||
'menu.profile.basic': 'Basic Profile',
|
||||
'menu.list.cardList': 'Card List',
|
||||
'menu.visualization.dataAnalysis': 'Analysis',
|
||||
'menu.result.error': 'Error',
|
||||
'menu.form.group': 'Group Form',
|
||||
'menu.dashboard.monitor': 'Real-time Monitor',
|
||||
'menu.visualization.multiDimensionDataAnalysis': 'Multi-D Analysis',
|
||||
'menu.list.searchTable': 'Search Table',
|
||||
'menu.form.step': 'Step Form',
|
||||
'menu.result.success': 'Success',
|
||||
'menu.user.info': 'User Info',
|
||||
'menu.user.setting': 'User Setting',
|
||||
'menu.user.switchRoles': 'Switch Roles',
|
||||
'menu.user.role.admin': 'Admin',
|
||||
'menu.user.role.user': 'General User',
|
||||
'menu.dashboard.workplace': 'Workplace',
|
||||
'menu.dashboard.radarMap': 'Global Radar Heatmap',
|
||||
'menu.dashboard.financial': 'Financial & Operations',
|
||||
@@ -118,31 +100,14 @@ const i18n = {
|
||||
'menu.settings.tenant': '域名与证书',
|
||||
'menu.settings.billing': '订阅账单',
|
||||
'menu.settings.auditLogs': '安全审计',
|
||||
'menu.list': '列表页',
|
||||
'menu.result': '结果页',
|
||||
'menu.platform': '平台总控',
|
||||
'menu.platform.subscribers': '会员列表',
|
||||
'menu.platform.renewals': '续费列表',
|
||||
'menu.platform.subscriptions': '订阅管理',
|
||||
'menu.exception': '异常页',
|
||||
'menu.form': '表单页',
|
||||
'menu.profile': '详情页',
|
||||
'menu.visualization': '数据可视化',
|
||||
'menu.user': '个人中心',
|
||||
'menu.exception.403': '403',
|
||||
'menu.exception.404': '404',
|
||||
'menu.exception.500': '500',
|
||||
'menu.profile.basic': '基础详情页',
|
||||
'menu.list.cardList': '卡片列表',
|
||||
'menu.visualization.dataAnalysis': '分析页',
|
||||
'menu.result.error': '失败页',
|
||||
'menu.form.group': '分组表单',
|
||||
'menu.dashboard.monitor': '实时监控',
|
||||
'menu.visualization.multiDimensionDataAnalysis': '多维数据分析',
|
||||
'menu.list.searchTable': '查询表格',
|
||||
'menu.form.step': '分步表单',
|
||||
'menu.result.success': '成功页',
|
||||
'menu.user.info': '用户信息',
|
||||
'menu.user.setting': '用户设置',
|
||||
'menu.user.switchRoles': '切换角色',
|
||||
'menu.user.role.admin': '管理员',
|
||||
'menu.user.role.user': '普通用户',
|
||||
'menu.dashboard.workplace': '工作台',
|
||||
'menu.dashboard.radarMap': '全球访客雷达',
|
||||
'menu.dashboard.financial': '新账与财务统计',
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Form,
|
||||
Grid,
|
||||
Input,
|
||||
InputNumber,
|
||||
Message,
|
||||
Modal,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconPlus,
|
||||
IconFile,
|
||||
IconSafe,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import type {
|
||||
ManualRenewDTO,
|
||||
RenewalOrderItem,
|
||||
SubscriberItem,
|
||||
TenantPlanType,
|
||||
} from '@/types/omni';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
const { Text } = Typography;
|
||||
|
||||
const API_RENEWALS_PAGE = '/api/admin/team/platform/renewals/page';
|
||||
const API_MANUAL_RENEW = '/api/admin/team/platform/renewals/manual-renew';
|
||||
const API_SUBSCRIBERS_PAGE = '/api/admin/team/platform/subscribers/page';
|
||||
|
||||
export default function RenewalsPage() {
|
||||
const t = useLocale();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<RenewalOrderItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
|
||||
// 会员下拉数据 (代续费选择)
|
||||
const [subscribers, setSubscribers] = useState<SubscriberItem[]>([]);
|
||||
|
||||
// 弹窗状态
|
||||
const [manualModalVisible, setManualModalVisible] = useState(false);
|
||||
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false);
|
||||
const [selectedOrder, setSelectedOrder] = useState<RenewalOrderItem | null>(null);
|
||||
const [form] = Form.useForm<ManualRenewDTO>();
|
||||
|
||||
const fetchSubscribers = async () => {
|
||||
try {
|
||||
const res = await axios.post(API_SUBSCRIBERS_PAGE, { size: 100 });
|
||||
const resData = res.data?.data || res.data;
|
||||
if (resData?.list) {
|
||||
setSubscribers(resData.list);
|
||||
}
|
||||
} catch {
|
||||
// 兜底
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async (targetPage = page, targetSize = size) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
page: targetPage,
|
||||
size: targetSize,
|
||||
};
|
||||
if (keyword) payload.keyword = keyword;
|
||||
if (statusFilter !== 'all') payload.status = statusFilter;
|
||||
|
||||
const res = await axios.post(API_RENEWALS_PAGE, payload);
|
||||
const resData = res.data?.data || res.data;
|
||||
if (resData?.list) {
|
||||
setData(resData.list);
|
||||
setTotal(resData.pagination?.total || resData.list.length);
|
||||
}
|
||||
} catch {
|
||||
Message.error('获取续费列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSubscribers();
|
||||
fetchData(1, size);
|
||||
}, [statusFilter]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setPage(1);
|
||||
fetchData(1, size);
|
||||
};
|
||||
|
||||
const handleOpenManualModal = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
tenantId: subscribers[0]?.tenantId || 1,
|
||||
planType: 'pro',
|
||||
billingCycle: 'yearly',
|
||||
amount: 490,
|
||||
currency: 'USD',
|
||||
channel: 'manual_admin',
|
||||
});
|
||||
setManualModalVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmManualRenew = async () => {
|
||||
try {
|
||||
const values = await form.validate();
|
||||
await axios.post(API_MANUAL_RENEW, values);
|
||||
Message.success('超级管理员代续费成功,会员期限已顺延!');
|
||||
setManualModalVisible(false);
|
||||
fetchData(1, size);
|
||||
} catch {
|
||||
// 错误提示
|
||||
}
|
||||
};
|
||||
|
||||
const getChannelTag = (channel: string) => {
|
||||
switch (channel) {
|
||||
case 'stripe':
|
||||
return <Tag color="blue">Stripe 国际支付</Tag>;
|
||||
case 'wire_transfer':
|
||||
return <Tag color="green">银行公对公电汇</Tag>;
|
||||
case 'manual_admin':
|
||||
return <Tag color="purple">超管后台代续</Tag>;
|
||||
default:
|
||||
return <Tag>{channel}</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'paid':
|
||||
return <Badge status="success" text="已支付入账" />;
|
||||
case 'pending':
|
||||
return <Badge status="warning" text="等待入账" />;
|
||||
case 'refunded':
|
||||
return <Badge status="danger" text="已退款注销" />;
|
||||
default:
|
||||
return <Badge status="default" text={status} />;
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '订单流水号',
|
||||
dataIndex: 'orderNo',
|
||||
render: (no: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontWeight: 600 }}>{no}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '会员企业 (租户)',
|
||||
dataIndex: 'tenantName',
|
||||
render: (name: string, record: RenewalOrderItem) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{name}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)' }}>
|
||||
租户 ID: #{record.tenantId}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '续费套餐 / 周期',
|
||||
render: (_: unknown, record: RenewalOrderItem) => (
|
||||
<div>
|
||||
<Tag color={record.planType === 'enterprise' ? 'gold' : 'arcoblue'} size="small">
|
||||
{record.planType.toUpperCase()}
|
||||
</Tag>
|
||||
<span style={{ marginLeft: 6, fontSize: 12, color: 'var(--color-text-2)' }}>
|
||||
{record.billingCycle === 'yearly' ? '年付结算' : '月付结算'}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '实付金额',
|
||||
render: (_: unknown, record: RenewalOrderItem) => (
|
||||
<span style={{ fontWeight: 600, color: '#00B42A' }}>
|
||||
${Number(record.amount || 0).toLocaleString()} {record.currency}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '支付渠道',
|
||||
dataIndex: 'channel',
|
||||
render: (channel: string) => getChannelTag(channel),
|
||||
},
|
||||
{
|
||||
title: '交易状态',
|
||||
dataIndex: 'status',
|
||||
render: (status: string) => getStatusBadge(status),
|
||||
},
|
||||
{
|
||||
title: '支付时间',
|
||||
dataIndex: 'paidAt',
|
||||
render: (time: string, record: RenewalOrderItem) => time || record.createdAt,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: unknown, record: RenewalOrderItem) => (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconFile />}
|
||||
onClick={() => {
|
||||
setSelectedOrder(record);
|
||||
setDetailDrawerVisible(true);
|
||||
}}
|
||||
>
|
||||
发票凭单
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 统计指标汇总
|
||||
const totalCollected = data
|
||||
.filter(d => d.status === 'paid')
|
||||
.reduce((sum, d) => sum + Number(d.amount || 0), 0);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
{/* 顶部统计卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="当前筛选续费订单数"
|
||||
value={total}
|
||||
prefix={<IconFile style={{ color: '#0B5CFF' }} />}
|
||||
suffix="笔"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="当页续费实收总额"
|
||||
value={totalCollected}
|
||||
prefix="$"
|
||||
precision={2}
|
||||
valueStyle={{ color: '#00B42A' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="年付续费订单占比"
|
||||
value={
|
||||
data.length
|
||||
? Math.round(
|
||||
(data.filter(d => d.billingCycle === 'yearly').length / data.length) * 100
|
||||
)
|
||||
: 80
|
||||
}
|
||||
suffix="%"
|
||||
valueStyle={{ color: '#0B5CFF' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="超管人工代续订单"
|
||||
value={data.filter(d => d.channel === 'manual_admin').length}
|
||||
suffix="笔"
|
||||
valueStyle={{ color: '#722ED1' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 主列表卡片 */}
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<IconSafe style={{ color: '#0B5CFF', fontSize: 18 }} />
|
||||
<span style={{ fontWeight: 600 }}>全网会员续费订单与账单台账</span>
|
||||
<Tag color="arcoblue" size="small">最高权限管控</Tag>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconPlus />}
|
||||
onClick={handleOpenManualModal}
|
||||
>
|
||||
超级管理员代续费
|
||||
</Button>
|
||||
<Button icon={<IconRefresh />} onClick={() => fetchData()}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{/* 工具条 */}
|
||||
<Row justify="space-between" align="center" style={{ marginBottom: 16 }}>
|
||||
<Col span={18}>
|
||||
<Space size="medium">
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder="搜索订单流水号、租户企业名..."
|
||||
value={keyword}
|
||||
onChange={setKeyword}
|
||||
onPressEnter={handleSearch}
|
||||
style={{ width: 280 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
style={{ width: 140 }}
|
||||
>
|
||||
<Select.Option value="all">所有支付状态</Select.Option>
|
||||
<Select.Option value="paid">已入账支付</Select.Option>
|
||||
<Select.Option value="pending">待确认到账</Select.Option>
|
||||
<Select.Option value="refunded">已退款</Select.Option>
|
||||
</Select>
|
||||
<Button type="primary" onClick={handleSearch}>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 表格 */}
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
data={data}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showTotal: true,
|
||||
onChange: (p, s) => {
|
||||
setPage(p);
|
||||
setSize(s);
|
||||
fetchData(p, s);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 超管手动代续费 Modal */}
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<IconSafe style={{ color: '#0B5CFF' }} />
|
||||
<span>超级管理员 - 手动代续费与补录订单</span>
|
||||
</div>
|
||||
}
|
||||
visible={manualModalVisible}
|
||||
onOk={handleConfirmManualRenew}
|
||||
onCancel={() => setManualModalVisible(false)}
|
||||
width={500}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="选择会员企业 (租户)" field="tenantId" required>
|
||||
<Select placeholder="请选择需要续费的企业会员">
|
||||
{subscribers.map((sub) => (
|
||||
<Select.Option key={sub.tenantId} value={sub.tenantId}>
|
||||
#{sub.tenantId} - {sub.tenantName} ({sub.planType.toUpperCase()})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="续费套餐等级" field="planType" required>
|
||||
<Select>
|
||||
<Select.Option value="pro">Pro 专业商务版</Select.Option>
|
||||
<Select.Option value="enterprise">Enterprise 商业企业版</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="结算周期" field="billingCycle" required>
|
||||
<Radio.Group>
|
||||
<Radio value="monthly">按月续费 (顺延 30 天)</Radio>
|
||||
<Radio value="yearly">按年续费 (顺延 365 天)</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={14}>
|
||||
<Form.Item label="实收金额" field="amount" required>
|
||||
<InputNumber min={0} prefix="$" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item label="结算货币" field="currency">
|
||||
<Select>
|
||||
<Select.Option value="USD">USD ($)</Select.Option>
|
||||
<Select.Option value="EUR">EUR (€)</Select.Option>
|
||||
<Select.Option value="GBP">GBP (£)</Select.Option>
|
||||
<Select.Option value="CNY">CNY (¥)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item label="入账支付渠道" field="channel" required>
|
||||
<Select>
|
||||
<Select.Option value="manual_admin">总后台超管直接代续 / 线下结算</Select.Option>
|
||||
<Select.Option value="wire_transfer">银行公对公电汇到账</Select.Option>
|
||||
<Select.Option value="stripe">Stripe 国际信用卡</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="代续备注" field="note">
|
||||
<Input.TextArea placeholder="例如:展会现场现金/对公打款补录" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 发票凭据 Drawer */}
|
||||
<Drawer
|
||||
title="续费订单与发票凭单详情"
|
||||
visible={detailDrawerVisible}
|
||||
onCancel={() => setDetailDrawerVisible(false)}
|
||||
footer={null}
|
||||
width={440}
|
||||
>
|
||||
{selectedOrder && (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16, textAlign: 'center', background: '#F7F8FA' }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--color-text-3)' }}>实收金额</div>
|
||||
<div style={{ fontSize: 26, fontWeight: 700, color: '#00B42A', margin: '4px 0' }}>
|
||||
${Number(selectedOrder.amount || 0).toLocaleString()} {selectedOrder.currency}
|
||||
</div>
|
||||
<div>{getStatusBadge(selectedOrder.status)}</div>
|
||||
</Card>
|
||||
<Descriptions
|
||||
column={1}
|
||||
data={[
|
||||
{ label: '订单流水号', value: selectedOrder.orderNo },
|
||||
{ label: '企业名称', value: selectedOrder.tenantName },
|
||||
{ label: '租户编号', value: `#${selectedOrder.tenantId}` },
|
||||
{ label: '订阅套餐', value: selectedOrder.planType.toUpperCase() },
|
||||
{ label: '结算周期', value: selectedOrder.billingCycle === 'yearly' ? '年付' : '月付' },
|
||||
{ label: '支付渠道', value: selectedOrder.channel },
|
||||
{ label: '交易完成时间', value: selectedOrder.paidAt || selectedOrder.createdAt },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Drawer,
|
||||
Form,
|
||||
Grid,
|
||||
Input,
|
||||
InputNumber,
|
||||
Message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconSafe,
|
||||
IconEdit,
|
||||
IconCalendar,
|
||||
IconUser,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import type {
|
||||
PlatformOverviewResult,
|
||||
SubscriberItem,
|
||||
TenantPlanType,
|
||||
UpdateSubscriberDTO,
|
||||
} from '@/types/omni';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const API_SUBSCRIBERS_PAGE = '/api/admin/team/platform/subscribers/page';
|
||||
const API_SUBSCRIBER_UPDATE = '/api/admin/team/platform/subscribers/update';
|
||||
const API_OVERVIEW = '/api/admin/team/platform/overview';
|
||||
|
||||
export default function SubscribersPage() {
|
||||
const t = useLocale();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<SubscriberItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [planFilter, setPlanFilter] = useState<string>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [overview, setOverview] = useState<PlatformOverviewResult | null>(null);
|
||||
|
||||
// 弹窗表单状态
|
||||
const [editDrawerVisible, setEditDrawerVisible] = useState(false);
|
||||
const [selectedSub, setSelectedSub] = useState<SubscriberItem | null>(null);
|
||||
const [extendModalVisible, setExtendModalVisible] = useState(false);
|
||||
const [extendDays, setExtendDays] = useState<number>(30);
|
||||
const [form] = Form.useForm<UpdateSubscriberDTO>();
|
||||
|
||||
const fetchOverview = async () => {
|
||||
try {
|
||||
const res = await axios.post(API_OVERVIEW);
|
||||
if (res.data?.data) {
|
||||
setOverview(res.data.data);
|
||||
}
|
||||
} catch {
|
||||
// 容错兜底
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async (targetPage = page, targetSize = size) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
page: targetPage,
|
||||
size: targetSize,
|
||||
};
|
||||
if (keyword) payload.keyword = keyword;
|
||||
if (planFilter !== 'all') payload.planType = planFilter;
|
||||
if (statusFilter !== 'all') payload.status = statusFilter;
|
||||
|
||||
const res = await axios.post(API_SUBSCRIBERS_PAGE, payload);
|
||||
const resData = res.data?.data || res.data;
|
||||
if (resData?.list) {
|
||||
setData(resData.list);
|
||||
setTotal(resData.pagination?.total || resData.list.length);
|
||||
}
|
||||
} catch {
|
||||
Message.error('获取会员列表失败,已启用离线缓存');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchOverview();
|
||||
fetchData(1, size);
|
||||
}, [planFilter, statusFilter]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setPage(1);
|
||||
fetchData(1, size);
|
||||
};
|
||||
|
||||
const handleOpenEdit = (record: SubscriberItem) => {
|
||||
setSelectedSub(record);
|
||||
form.setFieldsValue({
|
||||
tenantId: record.tenantId,
|
||||
planType: record.planType,
|
||||
cardsLimit: record.cardsLimit,
|
||||
seatsLimit: record.seatsLimit,
|
||||
status: record.status,
|
||||
});
|
||||
setEditDrawerVisible(true);
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
try {
|
||||
const values = await form.validate();
|
||||
await axios.post(API_SUBSCRIBER_UPDATE, values);
|
||||
Message.success('会员配置已更新');
|
||||
setEditDrawerVisible(false);
|
||||
fetchData();
|
||||
fetchOverview();
|
||||
} catch {
|
||||
// 校验或请求失败
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenExtend = (record: SubscriberItem) => {
|
||||
setSelectedSub(record);
|
||||
setExtendDays(30);
|
||||
setExtendModalVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmExtend = async () => {
|
||||
if (!selectedSub) return;
|
||||
try {
|
||||
await axios.post(API_SUBSCRIBER_UPDATE, {
|
||||
tenantId: selectedSub.tenantId,
|
||||
extendDays,
|
||||
});
|
||||
Message.success(`成功为【${selectedSub.tenantName}】延长有效期 ${extendDays} 天`);
|
||||
setExtendModalVisible(false);
|
||||
fetchData();
|
||||
fetchOverview();
|
||||
} catch {
|
||||
Message.error('延期操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (record: SubscriberItem) => {
|
||||
const nextStatus = record.status === 'canceled' ? 'active' : 'canceled';
|
||||
try {
|
||||
await axios.post(API_SUBSCRIBER_UPDATE, {
|
||||
tenantId: record.tenantId,
|
||||
status: nextStatus,
|
||||
});
|
||||
Message.success(`会员状态已切换为: ${nextStatus === 'active' ? '正常' : '冻结'}`);
|
||||
fetchData();
|
||||
fetchOverview();
|
||||
} catch {
|
||||
Message.error('切换状态失败');
|
||||
}
|
||||
};
|
||||
|
||||
const getPlanTag = (plan: TenantPlanType) => {
|
||||
switch (plan) {
|
||||
case 'enterprise':
|
||||
return <Tag color="gold">Enterprise 商业企业版</Tag>;
|
||||
case 'pro':
|
||||
return <Tag color="arcoblue">Pro 专业商务版</Tag>;
|
||||
default:
|
||||
return <Tag color="gray">Free 基础版</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <Badge status="success" text="正常服务中" />;
|
||||
case 'trialing':
|
||||
return <Badge status="processing" text="试用体验中" />;
|
||||
case 'past_due':
|
||||
return <Badge status="warning" text="已到期待续费" />;
|
||||
case 'canceled':
|
||||
return <Badge status="danger" text="已冻结停用" />;
|
||||
default:
|
||||
return <Badge status="default" text={status} />;
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '租户 ID / 会员名称',
|
||||
dataIndex: 'tenantName',
|
||||
render: (name: string, record: SubscriberItem) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: 'var(--color-text-1)' }}>{name}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 2 }}>
|
||||
ID: #{record.tenantId} · 开通时间: {record.startDate}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '主账号 / 负责人',
|
||||
dataIndex: 'adminName',
|
||||
render: (adminName: string, record: SubscriberItem) => (
|
||||
<div>
|
||||
<div>{adminName}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)' }}>{record.adminEmail}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '当前套餐',
|
||||
dataIndex: 'planType',
|
||||
render: (plan: TenantPlanType) => getPlanTag(plan),
|
||||
},
|
||||
{
|
||||
title: '服务状态',
|
||||
dataIndex: 'status',
|
||||
render: (status: string) => getStatusBadge(status),
|
||||
},
|
||||
{
|
||||
title: '名片 / 坐席用量',
|
||||
render: (_: unknown, record: SubscriberItem) => (
|
||||
<div>
|
||||
<div>名片: <strong>{record.cardsUsed}</strong> / {record.cardsLimit}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)' }}>
|
||||
坐席: <strong>{record.seatsUsed}</strong> / {record.seatsLimit}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '服务到期日',
|
||||
dataIndex: 'expireDate',
|
||||
render: (date: string, record: SubscriberItem) => {
|
||||
const isExpiring = record.status === 'past_due';
|
||||
return (
|
||||
<Text style={{ color: isExpiring ? '#F53F3F' : 'var(--color-text-1)', fontWeight: 500 }}>
|
||||
{date}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '累计实付',
|
||||
dataIndex: 'totalPaidAmount',
|
||||
render: (amount: number, record: SubscriberItem) => (
|
||||
<span>${Number(amount || 0).toLocaleString()} {record.currency}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '最高权限操作',
|
||||
render: (_: unknown, record: SubscriberItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconEdit />}
|
||||
onClick={() => handleOpenEdit(record)}
|
||||
>
|
||||
调整
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconCalendar />}
|
||||
onClick={() => handleOpenExtend(record)}
|
||||
>
|
||||
延期
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`确定${record.status === 'canceled' ? '解冻启用' : '冻结停用'}该会员账号吗?`}
|
||||
onOk={() => handleToggleStatus(record)}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
status={record.status === 'canceled' ? 'success' : 'danger'}
|
||||
size="small"
|
||||
>
|
||||
{record.status === 'canceled' ? '解冻' : '冻结'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
{/* 顶部总控概览卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="平台总会员企业数"
|
||||
value={overview?.totalSubscribers || 6}
|
||||
prefix={<IconUser style={{ color: '#0B5CFF' }} />}
|
||||
suffix="家"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="付费出海企业 (Pro/Ent)"
|
||||
value={(overview?.proCount || 0) + (overview?.enterpriseCount || 0)}
|
||||
valueStyle={{ color: '#0B5CFF' }}
|
||||
suffix="家"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="30天内到期预警会员"
|
||||
value={overview?.expiringSoonCount || 1}
|
||||
valueStyle={{ color: '#FF7D00' }}
|
||||
suffix="家"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="累计续费充值总营收"
|
||||
value={overview?.totalRenewalRevenue || 8540}
|
||||
prefix="$"
|
||||
precision={2}
|
||||
valueStyle={{ color: '#00B42A' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 主列表卡片 */}
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<IconSafe style={{ color: '#0B5CFF', fontSize: 18 }} />
|
||||
<span style={{ fontWeight: 600 }}>平台会员生命周期总控列表</span>
|
||||
<Tag color="arcoblue" size="small">超级管理员权限</Tag>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
onClick={() => {
|
||||
fetchOverview();
|
||||
fetchData();
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{/* 筛选过滤工具条 */}
|
||||
<Row justify="space-between" align="center" style={{ marginBottom: 16 }}>
|
||||
<Col span={18}>
|
||||
<Space size="medium">
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder="搜索会员企业名、主账号邮箱、ID..."
|
||||
value={keyword}
|
||||
onChange={setKeyword}
|
||||
onPressEnter={handleSearch}
|
||||
style={{ width: 280 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
value={planFilter}
|
||||
onChange={setPlanFilter}
|
||||
style={{ width: 160 }}
|
||||
>
|
||||
<Select.Option value="all">所有套餐等级</Select.Option>
|
||||
<Select.Option value="enterprise">Enterprise 商业版</Select.Option>
|
||||
<Select.Option value="pro">Pro 专业版</Select.Option>
|
||||
<Select.Option value="free">Free 基础版</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
style={{ width: 140 }}
|
||||
>
|
||||
<Select.Option value="all">所有服务状态</Select.Option>
|
||||
<Select.Option value="active">正常服务中</Select.Option>
|
||||
<Select.Option value="past_due">已到期预警</Select.Option>
|
||||
<Select.Option value="canceled">已冻结停用</Select.Option>
|
||||
</Select>
|
||||
<Button type="primary" onClick={handleSearch}>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 表格 */}
|
||||
<Table
|
||||
rowKey="tenantId"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
data={data}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showTotal: true,
|
||||
onChange: (p, s) => {
|
||||
setPage(p);
|
||||
setSize(s);
|
||||
fetchData(p, s);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 调整会员抽屉 */}
|
||||
<Drawer
|
||||
title={`调整会员配置 - ${selectedSub?.tenantName || ''}`}
|
||||
visible={editDrawerVisible}
|
||||
onOk={handleSaveEdit}
|
||||
onCancel={() => setEditDrawerVisible(false)}
|
||||
width={420}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item field="tenantId" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="会员套餐等级" field="planType" required>
|
||||
<Select>
|
||||
<Select.Option value="free">Free 基础版</Select.Option>
|
||||
<Select.Option value="pro">Pro 专业商务版</Select.Option>
|
||||
<Select.Option value="enterprise">Enterprise 商业企业版</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="名片制作配额上限" field="cardsLimit" required>
|
||||
<InputNumber min={1} max={9999} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="企业成员坐席上限" field="seatsLimit" required>
|
||||
<InputNumber min={1} max={999} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="账号服务状态" field="status" required>
|
||||
<Radio.Group>
|
||||
<Radio value="active">正常有效</Radio>
|
||||
<Radio value="past_due">到期催收</Radio>
|
||||
<Radio value="canceled">冻结停用</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
|
||||
{/* 延长有效期 Modal */}
|
||||
<Modal
|
||||
title="超级管理员 - 会员有效期顺延与时长赠送"
|
||||
visible={extendModalVisible}
|
||||
onOk={handleConfirmExtend}
|
||||
onCancel={() => setExtendModalVisible(false)}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
当前会员:<strong>{selectedSub?.tenantName}</strong>
|
||||
<br />
|
||||
当前到期日:{selectedSub?.expireDate}
|
||||
</div>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>选择顺延天数:</div>
|
||||
<Radio.Group
|
||||
value={extendDays}
|
||||
onChange={setExtendDays}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Radio value={30}>+30 天 (1 个月)</Radio>
|
||||
<Radio value={90}>+90 天 (1 季度)</Radio>
|
||||
<Radio value={365}>+365 天 (1 年)</Radio>
|
||||
</Radio.Group>
|
||||
<div>
|
||||
<InputNumber
|
||||
value={extendDays}
|
||||
onChange={(val) => setExtendDays(Number(val) || 30)}
|
||||
min={1}
|
||||
max={3650}
|
||||
suffix="天"
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Input,
|
||||
InputNumber,
|
||||
Message,
|
||||
Modal,
|
||||
Space,
|
||||
Statistic,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconCheckCircle,
|
||||
IconEdit,
|
||||
IconRefresh,
|
||||
IconSafe,
|
||||
IconStar,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import type { PlanConfigItem, TenantPlanType } from '@/types/omni';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const API_PLANS_LIST = '/api/admin/team/platform/plans/list';
|
||||
const API_PLANS_UPDATE = '/api/admin/team/platform/plans/update';
|
||||
|
||||
export default function SubscriptionsPage() {
|
||||
const t = useLocale();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [plans, setPlans] = useState<PlanConfigItem[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [currentPlan, setCurrentPlan] = useState<PlanConfigItem | null>(null);
|
||||
const [form] = Form.useForm<PlanConfigItem>();
|
||||
|
||||
const fetchPlans = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.post(API_PLANS_LIST);
|
||||
const resData = res.data?.data || res.data;
|
||||
if (Array.isArray(resData)) {
|
||||
setPlans(resData);
|
||||
}
|
||||
} catch {
|
||||
Message.error('获取套餐配置失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlans();
|
||||
}, []);
|
||||
|
||||
const handleOpenEdit = (plan: PlanConfigItem) => {
|
||||
setCurrentPlan(plan);
|
||||
form.setFieldsValue({
|
||||
...plan,
|
||||
});
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleSavePlan = async () => {
|
||||
if (!currentPlan) return;
|
||||
try {
|
||||
const values = await form.validate();
|
||||
await axios.post(API_PLANS_UPDATE, {
|
||||
planType: currentPlan.planType,
|
||||
params: values,
|
||||
});
|
||||
Message.success(`【${currentPlan.name}】规格配置已更新!`);
|
||||
setEditModalVisible(false);
|
||||
fetchPlans();
|
||||
} catch {
|
||||
// 失败提示
|
||||
}
|
||||
};
|
||||
|
||||
const getPlanColor = (planType: TenantPlanType) => {
|
||||
switch (planType) {
|
||||
case 'enterprise':
|
||||
return '#FF7D00';
|
||||
case 'pro':
|
||||
return '#0B5CFF';
|
||||
default:
|
||||
return '#86909C';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
{/* 顶部公告 */}
|
||||
<Alert
|
||||
type="info"
|
||||
icon={<IconSafe />}
|
||||
content="超级管理员总后台:此处配置的 SaaS 商业化订阅规格与定价将直接生效于全平台企业租户的升级与计费逻辑。"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
{/* 顶部总览卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="当前活跃套餐梯队"
|
||||
value={plans.length}
|
||||
suffix="档"
|
||||
valueStyle={{ color: '#0B5CFF' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="推荐出海主推方案"
|
||||
value="Pro 专业商务版"
|
||||
valueStyle={{ fontSize: 18, color: '#00B42A' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="年付优惠策略折扣"
|
||||
value={16.7}
|
||||
suffix="%"
|
||||
valueStyle={{ color: '#FF7D00' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 套餐方案卡片矩阵 */}
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontWeight: 600 }}>SaaS 订阅套餐规格与出海权益矩阵</span>
|
||||
<Tag color="arcoblue" size="small">平台超管可调控</Tag>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Button icon={<IconRefresh />} onClick={fetchPlans} loading={loading}>
|
||||
刷新规格
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
{plans.map((plan) => {
|
||||
const color = getPlanColor(plan.planType);
|
||||
return (
|
||||
<Col span={8} key={plan.planType}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
border: plan.isPopular ? `2px solid ${color}` : '1px solid var(--color-border-2)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{plan.isPopular && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<Tag color="arcoblue" icon={<IconStar />}>
|
||||
最受欢迎推荐
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text-1)' }}>
|
||||
{plan.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--color-text-3)', marginTop: 4, minHeight: 38 }}>
|
||||
{plan.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
|
||||
<span style={{ fontSize: 28, fontWeight: 700, color }}>
|
||||
${plan.monthlyPrice}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--color-text-3)' }}>/ 月</span>
|
||||
<span style={{ fontSize: 13, color: 'var(--color-text-3)', marginLeft: 8 }}>
|
||||
(年付 ${plan.yearlyPrice}/年)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心权益列表 */}
|
||||
<Descriptions
|
||||
column={1}
|
||||
size="small"
|
||||
style={{ marginBottom: 16 }}
|
||||
data={[
|
||||
{
|
||||
label: '名片制作配额',
|
||||
value: `${plan.cardsLimit} 张`,
|
||||
},
|
||||
{
|
||||
label: '企业坐席席位',
|
||||
value: `${plan.seatsLimit} 席`,
|
||||
},
|
||||
{
|
||||
label: '每月 AI Token 额度',
|
||||
value: `${(plan.aiTokensPerMonth / 1000).toLocaleString()} K Tokens`,
|
||||
},
|
||||
{
|
||||
label: 'NFC 动态短链路由',
|
||||
value: plan.nfcDynamicRouting ? (
|
||||
<Tag color="green" size="small">支持</Tag>
|
||||
) : (
|
||||
<Tag size="small">仅单向写入</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '自定义企业独立域名',
|
||||
value: plan.supportCustomDomain ? (
|
||||
<Tag color="green" size="small">支持通配</Tag>
|
||||
) : (
|
||||
<Tag size="small">平台二级域名</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'CRM 商机线索上限',
|
||||
value: `${plan.crmLeadLimit.toLocaleString()} 条`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="outline"
|
||||
long
|
||||
icon={<IconEdit />}
|
||||
onClick={() => handleOpenEdit(plan)}
|
||||
>
|
||||
调整定价与规格参数
|
||||
</Button>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 编辑套餐规格 Modal */}
|
||||
<Modal
|
||||
title={`调整套餐规格参数 - ${currentPlan?.name || ''}`}
|
||||
visible={editModalVisible}
|
||||
onOk={handleSavePlan}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
width={520}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="套餐展示全称" field="name" required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="套餐适用场景说明" field="description" required>
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="月付单价 (USD)" field="monthlyPrice" required>
|
||||
<InputNumber min={0} prefix="$" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="年付打包价 (USD)" field="yearlyPrice" required>
|
||||
<InputNumber min={0} prefix="$" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="名片配额上限 (张)" field="cardsLimit" required>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="成员坐席上限 (席)" field="seatsLimit" required>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item label="每月 AI Token 额度" field="aiTokensPerMonth" required>
|
||||
<InputNumber min={0} step={10000} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="NFC 场景动态短链"
|
||||
field="nfcDynamicRouting"
|
||||
triggerPropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="支持独立域名绑定"
|
||||
field="supportCustomDomain"
|
||||
triggerPropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item label="设为主推最受欢迎套餐" field="isPopular" triggerPropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,13 +20,6 @@ export const routes: IRoute[] = [
|
||||
name: 'menu.dashboard.workplace',
|
||||
key: 'dashboard/workplace',
|
||||
},
|
||||
{
|
||||
name: 'menu.dashboard.monitor',
|
||||
key: 'dashboard/monitor',
|
||||
requiredPermissions: [
|
||||
{ resource: 'menu.dashboard.monitor', actions: ['write'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.dashboard.radarMap',
|
||||
key: 'dashboard/radar-map',
|
||||
@@ -94,97 +87,12 @@ export const routes: IRoute[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.visualization',
|
||||
key: 'visualization',
|
||||
ignore: true,
|
||||
name: 'menu.platform',
|
||||
key: 'platform',
|
||||
children: [
|
||||
{
|
||||
name: 'menu.visualization.dataAnalysis',
|
||||
key: 'visualization/data-analysis',
|
||||
requiredPermissions: [
|
||||
{ resource: 'menu.visualization.dataAnalysis', actions: ['read'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.visualization.multiDimensionDataAnalysis',
|
||||
key: 'visualization/multi-dimension-data-analysis',
|
||||
requiredPermissions: [
|
||||
{
|
||||
resource: 'menu.visualization.dataAnalysis',
|
||||
actions: ['read', 'write'],
|
||||
},
|
||||
{
|
||||
resource: 'menu.visualization.multiDimensionDataAnalysis',
|
||||
actions: ['write'],
|
||||
},
|
||||
],
|
||||
oneOfPerm: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.list',
|
||||
key: 'list',
|
||||
ignore: true,
|
||||
children: [
|
||||
{
|
||||
name: 'menu.list.searchTable',
|
||||
key: 'list/search-table',
|
||||
},
|
||||
{
|
||||
name: 'menu.list.cardList',
|
||||
key: 'list/card',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.form',
|
||||
key: 'form',
|
||||
ignore: true,
|
||||
children: [
|
||||
{
|
||||
name: 'menu.form.group',
|
||||
key: 'form/group',
|
||||
requiredPermissions: [
|
||||
{ resource: 'menu.form.group', actions: ['read', 'write'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.form.step',
|
||||
key: 'form/step',
|
||||
requiredPermissions: [
|
||||
{ resource: 'menu.form.step', actions: ['read'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.profile',
|
||||
key: 'profile',
|
||||
ignore: true,
|
||||
children: [
|
||||
{
|
||||
name: 'menu.profile.basic',
|
||||
key: 'profile/basic',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'menu.result',
|
||||
key: 'result',
|
||||
ignore: true,
|
||||
children: [
|
||||
{
|
||||
name: 'menu.result.success',
|
||||
key: 'result/success',
|
||||
breadcrumb: false,
|
||||
},
|
||||
{
|
||||
name: 'menu.result.error',
|
||||
key: 'result/error',
|
||||
breadcrumb: false,
|
||||
},
|
||||
{ name: 'menu.platform.subscribers', key: 'platform/subscribers' },
|
||||
{ name: 'menu.platform.renewals', key: 'platform/renewals' },
|
||||
{ name: 'menu.platform.subscriptions', key: 'platform/subscriptions' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -206,20 +114,6 @@ export const routes: IRoute[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.user',
|
||||
key: 'user',
|
||||
children: [
|
||||
{
|
||||
name: 'menu.user.info',
|
||||
key: 'user/info',
|
||||
},
|
||||
{
|
||||
name: 'menu.user.setting',
|
||||
key: 'user/setting',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getName = (path: string, routes) => {
|
||||
|
||||
@@ -128,7 +128,7 @@ export type LeadStatus = 'new' | 'contacted' | 'hot' | 'won' | 'archived';
|
||||
/**
|
||||
* 线索触达来源渠道
|
||||
*/
|
||||
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat';
|
||||
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat' | 'exchange';
|
||||
|
||||
/**
|
||||
* LeadEntity - 潜客线索关系模型契约
|
||||
@@ -532,6 +532,7 @@ export interface DeepSeekConfig {
|
||||
maxTokens?: number;
|
||||
timeoutMs?: number;
|
||||
enabled?: boolean;
|
||||
isConfigured?: boolean;
|
||||
}
|
||||
|
||||
export interface DeepSeekChatMessage {
|
||||
@@ -834,29 +835,6 @@ export interface AiKnowledgeDoc {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export type ConciergePersonaType =
|
||||
| 'trade_expert'
|
||||
| 'business_rep'
|
||||
| 'tech_advisor'
|
||||
| 'custom';
|
||||
|
||||
export type ConciergeToneStyle =
|
||||
| 'professional'
|
||||
| 'direct'
|
||||
| 'enthusiastic'
|
||||
| 'friendly';
|
||||
|
||||
export interface AiConciergeConfig {
|
||||
id?: string | number;
|
||||
cardId: string | number;
|
||||
personaType: ConciergePersonaType;
|
||||
welcomeMsg: string;
|
||||
triggerLeadThreshold: number;
|
||||
promptTemplate: string;
|
||||
toneStyle: ConciergeToneStyle;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface KnowledgeChunk {
|
||||
id?: string | number;
|
||||
index: number;
|
||||
@@ -865,24 +843,6 @@ export interface KnowledgeChunk {
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export type DeepSeekModel = 'deepseek-chat' | 'deepseek-reasoner';
|
||||
|
||||
export interface DeepSeekConfig {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
model?: DeepSeekModel;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
timeoutMs?: number;
|
||||
enabled?: boolean;
|
||||
isConfigured?: boolean;
|
||||
}
|
||||
|
||||
export interface DeepSeekChatMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface DeepSeekChatCompletionRequest {
|
||||
messages: DeepSeekChatMessage[];
|
||||
model?: DeepSeekModel;
|
||||
@@ -920,6 +880,9 @@ export interface DeepSeekTestConnectionResult {
|
||||
// 12. 商业化新账统计与财务运营分析契约 (Finance & Operations BI)
|
||||
// ==========================================
|
||||
|
||||
export type SupportedCurrency = 'USD' | 'EUR' | 'GBP' | 'CNY';
|
||||
export type InvoiceStatus = 'paid' | 'issued' | 'refunded' | 'void';
|
||||
|
||||
export type FinanceAnalyticsPeriod = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'all';
|
||||
|
||||
export interface FinanceAnalyticsQueryDTO {
|
||||
@@ -995,4 +958,89 @@ export interface FinanceAnalyticsResult {
|
||||
recentLedger: RecentLedgerItem[];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 平台总后台超级管理员 (Platform Super Admin) - 会员、续费与订阅契约
|
||||
// -------------------------------------------------------------
|
||||
|
||||
export interface SubscriberItem {
|
||||
id: number;
|
||||
tenantId: number;
|
||||
tenantName: string;
|
||||
adminName: string;
|
||||
adminEmail: string;
|
||||
adminPhone?: string;
|
||||
planType: TenantPlanType;
|
||||
status: 'active' | 'trialing' | 'past_due' | 'canceled';
|
||||
cardsUsed: number;
|
||||
cardsLimit: number;
|
||||
seatsUsed: number;
|
||||
seatsLimit: number;
|
||||
totalPaidAmount: number;
|
||||
currency: SupportedCurrency;
|
||||
startDate: string;
|
||||
expireDate: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface UpdateSubscriberDTO {
|
||||
tenantId: number;
|
||||
planType?: TenantPlanType;
|
||||
cardsLimit?: number;
|
||||
seatsLimit?: number;
|
||||
extendDays?: number;
|
||||
expireDate?: string;
|
||||
status?: 'active' | 'trialing' | 'past_due' | 'canceled';
|
||||
}
|
||||
|
||||
export interface RenewalOrderItem {
|
||||
id: number;
|
||||
orderNo: string;
|
||||
tenantId: number;
|
||||
tenantName: string;
|
||||
planType: TenantPlanType;
|
||||
billingCycle: 'monthly' | 'yearly';
|
||||
amount: number;
|
||||
currency: SupportedCurrency;
|
||||
channel: 'stripe' | 'wire_transfer' | 'manual_admin' | 'system';
|
||||
status: 'paid' | 'pending' | 'refunded';
|
||||
paidAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ManualRenewDTO {
|
||||
tenantId: number;
|
||||
planType: TenantPlanType;
|
||||
billingCycle: 'monthly' | 'yearly';
|
||||
amount: number;
|
||||
currency?: SupportedCurrency;
|
||||
channel?: 'stripe' | 'wire_transfer' | 'manual_admin';
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface PlanConfigItem {
|
||||
planType: TenantPlanType;
|
||||
name: string;
|
||||
description: string;
|
||||
monthlyPrice: number;
|
||||
yearlyPrice: number;
|
||||
cardsLimit: number;
|
||||
seatsLimit: number;
|
||||
aiTokensPerMonth: number;
|
||||
nfcDynamicRouting: boolean;
|
||||
supportCustomDomain: boolean;
|
||||
crmLeadLimit: number;
|
||||
isPopular?: boolean;
|
||||
}
|
||||
|
||||
export interface PlatformOverviewResult {
|
||||
totalSubscribers: number;
|
||||
activeSubscribers: number;
|
||||
proCount: number;
|
||||
enterpriseCount: number;
|
||||
expiringSoonCount: number;
|
||||
totalRenewalRevenue: number;
|
||||
currency: SupportedCurrency;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户