feat: token stats and feature gating
parent
e3997c661e
commit
b2df36647c
|
|
@ -11,7 +11,6 @@ import MarketplacePage from './pages/MarketplacePage';
|
||||||
import TeamsPage from './pages/TeamsPage';
|
import TeamsPage from './pages/TeamsPage';
|
||||||
import PromptLibraryPage from './pages/PromptLibraryPage';
|
import PromptLibraryPage from './pages/PromptLibraryPage';
|
||||||
import StatsPage from './pages/StatsPage';
|
import StatsPage from './pages/StatsPage';
|
||||||
import LLMProvidersPage from './pages/LLMProvidersPage';
|
|
||||||
import SharedSessionPage from './pages/SharedSessionPage';
|
import SharedSessionPage from './pages/SharedSessionPage';
|
||||||
import WorkflowsPage from './pages/WorkflowsPage';
|
import WorkflowsPage from './pages/WorkflowsPage';
|
||||||
import { useAuth } from './store/auth';
|
import { useAuth } from './store/auth';
|
||||||
|
|
@ -52,7 +51,6 @@ export default function App() {
|
||||||
<Route path="/teams" element={<TeamsPage />} />
|
<Route path="/teams" element={<TeamsPage />} />
|
||||||
<Route path="/prompts" element={<PromptLibraryPage />} />
|
<Route path="/prompts" element={<PromptLibraryPage />} />
|
||||||
<Route path="/stats" element={<StatsPage />} />
|
<Route path="/stats" element={<StatsPage />} />
|
||||||
<Route path="/llm-providers" element={<LLMProvidersPage />} />
|
|
||||||
<Route path="/workflows" element={<WorkflowsPage />} />
|
<Route path="/workflows" element={<WorkflowsPage />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
|
||||||
38
src/api.ts
38
src/api.ts
|
|
@ -486,9 +486,45 @@ export interface AgentStats {
|
||||||
daily: { day: string; count: number }[];
|
daily: { day: string; count: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentTokenStatsByModel {
|
||||||
|
providerKind: string;
|
||||||
|
model: string;
|
||||||
|
calls: number;
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
costUSD: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentTokenStatsDaily {
|
||||||
|
day: string;
|
||||||
|
calls: number;
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
costUSD: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentTokenStats {
|
||||||
|
agentId: string;
|
||||||
|
days: number;
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
costUSD: number;
|
||||||
|
byModel: AgentTokenStatsByModel[];
|
||||||
|
daily: AgentTokenStatsDaily[];
|
||||||
|
}
|
||||||
|
|
||||||
export const StatsAPI = {
|
export const StatsAPI = {
|
||||||
overview: () => api.get<StatsOverview>('/stats/overview').then((r) => r.data),
|
overview: () => api.get<StatsOverview>('/stats/overview').then((r) => r.data),
|
||||||
agent: (id: string) => api.get<AgentStats>(`/stats/agents/${id}`).then((r) => r.data)
|
agent: (id: string) => api.get<AgentStats>(`/stats/agents/${id}`).then((r) => r.data),
|
||||||
|
agentTokens: (id: string, opts: { days?: number; limit?: number } = {}) =>
|
||||||
|
api
|
||||||
|
.get<AgentTokenStats>(`/stats/agents/${id}/tokens`, {
|
||||||
|
params: { days: opts.days ?? 30, limit: opts.limit ?? 10 }
|
||||||
|
})
|
||||||
|
.then((r) => r.data)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============== 流式(手写 SSE,不用 EventSource,因为它不能 POST) ==============
|
// ============== 流式(手写 SSE,不用 EventSource,因为它不能 POST) ==============
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
ApartmentOutlined,
|
ApartmentOutlined,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
ApiOutlined,
|
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
SunOutlined,
|
SunOutlined,
|
||||||
MoonOutlined,
|
MoonOutlined,
|
||||||
|
|
@ -44,7 +43,6 @@ const NAV_GROUPS: Array<{
|
||||||
label: '管理',
|
label: '管理',
|
||||||
items: [
|
items: [
|
||||||
{ to: '/stats', icon: <BarChartOutlined />, label: '调用统计' },
|
{ to: '/stats', icon: <BarChartOutlined />, label: '调用统计' },
|
||||||
{ to: '/llm-providers', icon: <ApiOutlined />, label: 'LLM 提供商' },
|
|
||||||
{ to: '/teams', icon: <TeamOutlined />, label: '团队' }
|
{ to: '/teams', icon: <TeamOutlined />, label: '团队' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,10 @@ export default function ChatPage() {
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
const autoScrollRef = useRef(true);
|
const autoScrollRef = useRef(true);
|
||||||
const initialScrollDoneRef = useRef(false);
|
const initialScrollDoneRef = useRef(false);
|
||||||
|
const scrollRafRef = useRef<number | null>(null);
|
||||||
|
const scrollProgrammaticRef = useRef(false);
|
||||||
|
const lastScrollTopRef = useRef(0);
|
||||||
|
const userScrollLockRef = useRef(false);
|
||||||
|
|
||||||
// URL 参数 ?session=xxx&msg=yyy
|
// URL 参数 ?session=xxx&msg=yyy
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -91,9 +95,21 @@ export default function ChatPage() {
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const scrollBottom = (force = false) => {
|
const scrollBottom = (force = false) => {
|
||||||
if (!force && !autoScrollRef.current) return;
|
if (force) {
|
||||||
requestAnimationFrame(() => {
|
userScrollLockRef.current = false;
|
||||||
bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight, behavior: 'smooth' });
|
autoScrollRef.current = true;
|
||||||
|
}
|
||||||
|
if (!force && (!autoScrollRef.current || userScrollLockRef.current)) return;
|
||||||
|
if (scrollRafRef.current) {
|
||||||
|
cancelAnimationFrame(scrollRafRef.current);
|
||||||
|
scrollRafRef.current = null;
|
||||||
|
}
|
||||||
|
scrollRafRef.current = requestAnimationFrame(() => {
|
||||||
|
scrollRafRef.current = null;
|
||||||
|
const el = bodyRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
scrollProgrammaticRef.current = true;
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -101,8 +117,24 @@ export default function ChatPage() {
|
||||||
const el = bodyRef.current;
|
const el = bodyRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
|
if (scrollProgrammaticRef.current) {
|
||||||
|
scrollProgrammaticRef.current = false;
|
||||||
|
lastScrollTopRef.current = el.scrollTop;
|
||||||
|
} else {
|
||||||
|
const nextTop = el.scrollTop;
|
||||||
|
const lastTop = lastScrollTopRef.current;
|
||||||
|
if (nextTop < lastTop - 2) {
|
||||||
|
userScrollLockRef.current = true;
|
||||||
|
}
|
||||||
|
lastScrollTopRef.current = nextTop;
|
||||||
|
}
|
||||||
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
|
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||||
autoScrollRef.current = distance < 32;
|
if (distance < 8) userScrollLockRef.current = false;
|
||||||
|
autoScrollRef.current = distance < 32 && !userScrollLockRef.current;
|
||||||
|
if ((!autoScrollRef.current || userScrollLockRef.current) && scrollRafRef.current) {
|
||||||
|
cancelAnimationFrame(scrollRafRef.current);
|
||||||
|
scrollRafRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
el.addEventListener('scroll', onScroll, { passive: true });
|
el.addEventListener('scroll', onScroll, { passive: true });
|
||||||
onScroll();
|
onScroll();
|
||||||
|
|
@ -294,7 +326,7 @@ export default function ChatPage() {
|
||||||
setSessionRefresh((t) => t + 1);
|
setSessionRefresh((t) => t + 1);
|
||||||
setAttachments([]); // 用完即清
|
setAttachments([]); // 用完即清
|
||||||
setImageUrls([]);
|
setImageUrls([]);
|
||||||
scrollBottom(true);
|
scrollBottom();
|
||||||
},
|
},
|
||||||
onAborted: (data) => {
|
onAborted: (data) => {
|
||||||
// 已停止:保留已生成内容;user 消息也留下(用 tempUser 占位)
|
// 已停止:保留已生成内容;user 消息也留下(用 tempUser 占位)
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,8 @@ export default function PromptLibraryPage({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="feature-cover-container">
|
||||||
|
<div className="page-container">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
|
|
@ -489,6 +490,10 @@ export default function PromptLibraryPage({
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
|
<div className="feature-cover">
|
||||||
|
<Empty description="功能规划中,本期不支持" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,43 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BarChartOutlined, LineChartOutlined, MessageOutlined, RobotOutlined } from '@ant-design/icons';
|
import { BarChartOutlined, LineChartOutlined, MessageOutlined, RobotOutlined } from '@ant-design/icons';
|
||||||
import { Card, Empty, App as AntApp, Spin, Tag } from 'antd';
|
import { Card, Empty, App as AntApp, Spin, Tag, Select, Space, Table } from 'antd';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { StatsAPI, StatsOverview } from '../api';
|
import { AgentTokenStats, StatsAPI, StatsOverview } from '../api';
|
||||||
|
|
||||||
export default function StatsPage() {
|
export default function StatsPage() {
|
||||||
const { message } = AntApp.useApp();
|
const { message } = AntApp.useApp();
|
||||||
const [data, setData] = useState<StatsOverview | null>(null);
|
const [data, setData] = useState<StatsOverview | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [tokenLoading, setTokenLoading] = useState(false);
|
||||||
|
const [tokenData, setTokenData] = useState<AgentTokenStats | null>(null);
|
||||||
|
const [tokenAgentId, setTokenAgentId] = useState<string>('');
|
||||||
|
const [tokenDays, setTokenDays] = useState<number>(30);
|
||||||
|
const [tokenLimit, setTokenLimit] = useState<number>(10);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
StatsAPI.overview()
|
StatsAPI.overview()
|
||||||
.then(setData)
|
.then((res) => {
|
||||||
|
setData(res);
|
||||||
|
if (!tokenAgentId && res.topAgents?.[0]?.id) {
|
||||||
|
setTokenAgentId(res.topAgents[0].id);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((e) => message.error('加载失败:' + (e?.message ?? e)))
|
.catch((e) => message.error('加载失败:' + (e?.message ?? e)))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tokenAgentId) return;
|
||||||
|
setTokenLoading(true);
|
||||||
|
StatsAPI.agentTokens(tokenAgentId, { days: tokenDays, limit: tokenLimit })
|
||||||
|
.then(setTokenData)
|
||||||
|
.catch((e) => message.error('Token 统计加载失败:' + (e?.message ?? e)))
|
||||||
|
.finally(() => setTokenLoading(false));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [tokenAgentId, tokenDays, tokenLimit]);
|
||||||
|
|
||||||
if (loading) return <Spin style={{ marginTop: 80, display: 'block' }} />;
|
if (loading) return <Spin style={{ marginTop: 80, display: 'block' }} />;
|
||||||
if (!data) return <Empty description="暂无数据" style={{ marginTop: 80 }} />;
|
if (!data) return <Empty description="暂无数据" style={{ marginTop: 80 }} />;
|
||||||
|
|
||||||
|
|
@ -25,6 +45,8 @@ export default function StatsPage() {
|
||||||
const maxAgent = Math.max(1, ...data.topAgents.map((a) => a.messageCount));
|
const maxAgent = Math.max(1, ...data.topAgents.map((a) => a.messageCount));
|
||||||
const last7Days = data.daily.slice(-7).reduce((sum, item) => sum + item.total, 0);
|
const last7Days = data.daily.slice(-7).reduce((sum, item) => sum + item.total, 0);
|
||||||
const avgSessionMessages = data.sessionCount > 0 ? (data.messageCount / data.sessionCount).toFixed(1) : '0.0';
|
const avgSessionMessages = data.sessionCount > 0 ? (data.messageCount / data.sessionCount).toFixed(1) : '0.0';
|
||||||
|
const formatUSD = (v?: number | null) =>
|
||||||
|
`$${Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 })}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
|
|
@ -250,6 +272,176 @@ export default function StatsPage() {
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 18 }}>
|
||||||
|
<Card
|
||||||
|
title="Token 使用量"
|
||||||
|
extra={
|
||||||
|
<Space size={10} wrap>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={tokenAgentId || undefined}
|
||||||
|
style={{ minWidth: 220 }}
|
||||||
|
placeholder="选择智能体"
|
||||||
|
onChange={setTokenAgentId}
|
||||||
|
options={data.topAgents.map((a) => ({ value: a.id, label: a.name }))}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={tokenDays}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onChange={setTokenDays}
|
||||||
|
options={[
|
||||||
|
{ value: 7, label: '近 7 天' },
|
||||||
|
{ value: 30, label: '近 30 天' },
|
||||||
|
{ value: 90, label: '近 90 天' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={tokenLimit}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onChange={setTokenLimit}
|
||||||
|
options={[
|
||||||
|
{ value: 5, label: 'Top 5' },
|
||||||
|
{ value: 10, label: 'Top 10' },
|
||||||
|
{ value: 20, label: 'Top 20' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
style={{ borderRadius: 22, boxShadow: '0 12px 28px rgba(15, 23, 42, 0.045)' }}
|
||||||
|
>
|
||||||
|
{tokenLoading ? (
|
||||||
|
<Spin style={{ marginTop: 20, display: 'block' }} />
|
||||||
|
) : !tokenAgentId ? (
|
||||||
|
<Empty description="暂无可统计的智能体" style={{ marginTop: 20 }} />
|
||||||
|
) : !tokenData ? (
|
||||||
|
<Empty description="暂无 Token 统计数据" style={{ marginTop: 20 }} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 14, marginBottom: 16 }}>
|
||||||
|
{[
|
||||||
|
{ label: '总 Token', value: tokenData.totalTokens, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' },
|
||||||
|
{ label: '输入 Token', value: tokenData.promptTokens, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' },
|
||||||
|
{ label: '输出 Token', value: tokenData.completionTokens, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' },
|
||||||
|
{ label: '费用 (USD)', value: formatUSD(tokenData.costUSD), tone: 'rgba(249, 115, 22, 0.10)', color: 'var(--color-warning)' }
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
style={{
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: '16px 18px',
|
||||||
|
background: 'rgba(255,255,255,0.72)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.7)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 10 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 999,
|
||||||
|
background: item.color,
|
||||||
|
marginRight: 6,
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 26, fontWeight: 700, color: 'var(--color-text)' }}>
|
||||||
|
{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: 18 }}>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title="近 N 天趋势(总 Token)"
|
||||||
|
extra={
|
||||||
|
<Tag bordered={false} style={{ borderRadius: 999, background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)' }}>
|
||||||
|
days={tokenData.days}
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
style={{ borderRadius: 18, boxShadow: 'none' }}
|
||||||
|
>
|
||||||
|
{tokenData.daily.length === 0 ? (
|
||||||
|
<Empty description="暂无" />
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: 220, padding: '18px 12px 10px' }}>
|
||||||
|
{(() => {
|
||||||
|
const maxTokenDaily = Math.max(1, ...tokenData.daily.map((d) => d.totalTokens));
|
||||||
|
return tokenData.daily.map((d) => {
|
||||||
|
const h = (d.totalTokens / maxTokenDaily) * 170;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={d.day}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
minWidth: 0
|
||||||
|
}}
|
||||||
|
title={`${d.day}\nCalls ${d.calls}\nTotalTokens ${d.totalTokens}\nCost ${formatUSD(d.costUSD)}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 12,
|
||||||
|
height: h,
|
||||||
|
background: 'var(--gradient-brand)',
|
||||||
|
borderRadius: '6px 6px 0 0'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ fontSize: 10, color: 'var(--color-text-tertiary)', marginTop: 6, transform: 'rotate(-45deg)' }}>
|
||||||
|
{d.day.slice(5)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title="模型消耗排行"
|
||||||
|
style={{ borderRadius: 18, boxShadow: 'none' }}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
rowKey={(r) => `${r.providerKind}:${r.model}`}
|
||||||
|
dataSource={tokenData.byModel || []}
|
||||||
|
columns={[
|
||||||
|
{ title: 'Provider', dataIndex: 'providerKind', width: 110 },
|
||||||
|
{ title: 'Model', dataIndex: 'model' },
|
||||||
|
{ title: 'Calls', dataIndex: 'calls', width: 80 },
|
||||||
|
{
|
||||||
|
title: 'Tokens',
|
||||||
|
dataIndex: 'totalTokens',
|
||||||
|
width: 110,
|
||||||
|
render: (v) => Number(v || 0).toLocaleString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Cost',
|
||||||
|
dataIndex: 'costUSD',
|
||||||
|
width: 110,
|
||||||
|
render: (v) => formatUSD(v)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ export default function TeamsPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container" style={{ maxWidth: 1080 }}>
|
<div className="feature-cover-container">
|
||||||
|
<div className="page-container" style={{ maxWidth: 1080 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
|
|
@ -390,6 +391,10 @@ export default function TeamsPage() {
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="feature-cover">
|
||||||
|
<Empty description="功能规划中,本期不支持" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,8 @@ export default function WorkflowsPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container" style={{ maxWidth: 1400 }}>
|
<div className="feature-cover-container">
|
||||||
|
<div className="page-container" style={{ maxWidth: 1400 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
|
|
@ -358,6 +359,10 @@ export default function WorkflowsPage() {
|
||||||
onClose={() => setRunsOpen(false)}
|
onClose={() => setRunsOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="feature-cover">
|
||||||
|
<Empty description="功能规划中,本期不支持" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1004,6 +1004,29 @@ body {
|
||||||
padding: 40px 32px 60px;
|
padding: 40px 32px 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feature-cover-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-cover {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.74);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-cover .ant-empty {
|
||||||
|
padding: 26px 30px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue