feat: 界面全部改为中文

- 侧边栏导航:仪表盘、登录、内容浏览、发布、互动、API 测试、设置
- 7 个页面所有按钮、标签、提示、错误信息改为中文
- API 端点列表分类改为中文(登录、内容、发布、互动)
- 组件内文本:展开/收起、复制、点赞、收藏、评论等
- 页面标题改为 Social MCP - 管理后台
This commit is contained in:
2026-03-01 16:52:57 +08:00
parent 0e693842a6
commit 69a0f7b24c
16 changed files with 233 additions and 233 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Social MCP - Admin Dashboard</title>
<title>Social MCP - 管理后台</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>" />
</head>
<body class="bg-dark-bg text-dark-text">
+1 -1
View File
@@ -25,7 +25,7 @@ export function CommentTree({ comments, depth = 0 }: Props) {
</div>
<p className="text-sm text-dark-text/90">{comment.content}</p>
{comment.likeCount > 0 && (
<span className="text-xs text-dark-muted mt-1 block">{comment.likeCount} likes</span>
<span className="text-xs text-dark-muted mt-1 block">{comment.likeCount} </span>
)}
</div>
</div>
+4 -4
View File
@@ -23,15 +23,15 @@ export function FeedCard({ feed, onClick }: Props) {
/>
) : (
<div className="w-full h-full flex items-center justify-center text-dark-muted text-sm">
No Cover
</div>
)}
{feed.type === 'video' && (
<Badge variant="info" className="absolute top-2 right-2">Video</Badge>
<Badge variant="info" className="absolute top-2 right-2"></Badge>
)}
</div>
<div className="p-3">
<h3 className="text-sm font-medium line-clamp-2 mb-2">{feed.title || feed.description || 'Untitled'}</h3>
<h3 className="text-sm font-medium line-clamp-2 mb-2">{feed.title || feed.description || '无标题'}</h3>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
{feed.user.avatar && (
@@ -39,7 +39,7 @@ export function FeedCard({ feed, onClick }: Props) {
)}
<span className="text-xs text-dark-muted truncate">{feed.user.nickname}</span>
</div>
<span className="text-xs text-dark-muted shrink-0">{formatNumber(feed.likeCount)} likes</span>
<span className="text-xs text-dark-muted shrink-0">{formatNumber(feed.likeCount)} </span>
</div>
</div>
</div>
+9 -9
View File
@@ -40,7 +40,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div className="relative ml-auto w-full max-w-2xl bg-dark-card border-l border-dark-border overflow-y-auto">
<div className="sticky top-0 bg-dark-card border-b border-dark-border px-5 py-3 flex items-center justify-between z-10">
<h3 className="font-semibold truncate">Feed Detail</h3>
<h3 className="font-semibold truncate"></h3>
<button onClick={onClose} className="text-dark-muted hover:text-dark-text text-xl">&times;</button>
</div>
@@ -85,7 +85,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
{/* Video */}
{detail.videoUrl && (
<div className="rounded-xl overflow-hidden bg-dark-bg p-4">
<Badge variant="info">Video Note</Badge>
<Badge variant="info"></Badge>
<p className="text-xs text-dark-muted mt-2 break-all">{detail.videoUrl}</p>
</div>
)}
@@ -108,10 +108,10 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Likes', value: detail.likeCount },
{ label: 'Collects', value: detail.collectCount },
{ label: 'Comments', value: detail.commentCount },
{ label: 'Shares', value: detail.shareCount },
{ label: '点赞', value: detail.likeCount },
{ label: '收藏', value: detail.collectCount },
{ label: '评论', value: detail.commentCount },
{ label: '分享', value: detail.shareCount },
].map((s) => (
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
@@ -145,14 +145,14 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
variant="ghost"
onClick={() => void navigator.clipboard.writeText(detail.id)}
>
Copy Feed ID
Feed ID
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => void navigator.clipboard.writeText(detail.xsecToken)}
>
Copy Token
Token
</Button>
</div>
</div>
@@ -161,7 +161,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
{detail.comments.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
Comments ({detail.comments.length})
({detail.comments.length})
</h3>
<CommentTree comments={detail.comments} />
</div>
+1 -1
View File
@@ -9,7 +9,7 @@ interface Props {
emptyText?: string;
}
export function FeedGrid({ feeds, loading, onSelect, emptyText = 'No feeds found' }: Props) {
export function FeedGrid({ feeds, loading, onSelect, emptyText = '暂无内容' }: Props) {
if (loading) {
return (
<div className="flex justify-center py-12">
+5 -5
View File
@@ -56,10 +56,10 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Follows', value: profile.follows },
{ label: 'Fans', value: profile.fans },
{ label: 'Interactions', value: profile.interaction },
{ label: 'Notes', value: profile.feedCount },
{ label: '关注', value: profile.follows },
{ label: '粉丝', value: profile.fans },
{ label: '获赞与收藏', value: profile.interaction },
{ label: '笔记', value: profile.feedCount },
].map((s) => (
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
@@ -72,7 +72,7 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
{profile.feeds.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
Recent Notes ({profile.feeds.length})
({profile.feeds.length})
</h3>
<FeedGrid feeds={profile.feeds} loading={false} onSelect={onFeedSelect} />
</div>
+3 -3
View File
@@ -20,17 +20,17 @@ export function Header() {
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
Token Settings
Token
</button>
)}
</div>
<div className="flex items-center gap-3">
{health && (
<Badge variant={health.healthy ? 'success' : 'danger'}>
{health.healthy ? 'Healthy' : 'Unhealthy'}
{health.healthy ? '正常' : '异常'}
</Badge>
)}
{!health && <Badge variant="warning">Connecting...</Badge>}
{!health && <Badge variant="warning">...</Badge>}
</div>
</header>
);
+2 -2
View File
@@ -17,13 +17,13 @@ export function JsonViewer({ data, collapsed = false, maxHeight = '400px' }: Pro
onClick={() => setIsCollapsed(!isCollapsed)}
className="text-xs text-dark-muted hover:text-dark-text"
>
{isCollapsed ? 'Expand' : 'Collapse'}
{isCollapsed ? '展开' : '收起'}
</button>
<button
onClick={() => void navigator.clipboard.writeText(json)}
className="text-xs text-dark-muted hover:text-dark-accent"
>
Copy
</button>
</div>
{!isCollapsed && (
+20 -20
View File
@@ -1,25 +1,25 @@
export const NAV_ITEMS = [
{ path: '/', label: 'Dashboard', icon: 'dashboard' },
{ path: '/login', label: 'Login', icon: 'login' },
{ path: '/browser', label: 'Browser', icon: 'browser' },
{ path: '/publish', label: 'Publish', icon: 'publish' },
{ path: '/interactions', label: 'Interactions', icon: 'interactions' },
{ path: '/api-tester', label: 'API Tester', icon: 'api' },
{ path: '/settings', label: 'Settings', icon: 'settings' },
{ path: '/', label: '仪表盘', icon: 'dashboard' },
{ path: '/login', label: '登录', icon: 'login' },
{ path: '/browser', label: '内容浏览', icon: 'browser' },
{ path: '/publish', label: '发布', icon: 'publish' },
{ path: '/interactions', label: '互动', icon: 'interactions' },
{ path: '/api-tester', label: 'API 测试', icon: 'api' },
{ path: '/settings', label: '设置', icon: 'settings' },
] as const;
export const API_ENDPOINTS = [
{ key: 'login_status', method: 'GET', path: '/api/xhs/login/status', label: 'Check Login Status', category: 'Login' },
{ key: 'login_qrcode', method: 'GET', path: '/api/xhs/login/qrcode', label: 'Get Login QR Code', category: 'Login' },
{ key: 'login_delete', method: 'DELETE', path: '/api/xhs/login/cookies', label: 'Delete Cookies (Logout)', category: 'Login' },
{ key: 'feeds', method: 'GET', path: '/api/xhs/feeds', label: 'List Feeds', category: 'Content' },
{ key: 'search', method: 'POST', path: '/api/xhs/search', label: 'Search', category: 'Content', body: { keyword: '', filters: { sort: 'general', type: 'all', time: 'all' } } },
{ key: 'feed_detail', method: 'POST', path: '/api/xhs/feeds/detail', label: 'Feed Detail', category: 'Content', body: { feed_id: '', xsec_token: '', load_all_comments: false } },
{ key: 'user_profile', method: 'POST', path: '/api/xhs/user/profile', label: 'User Profile', category: 'Content', body: { user_id: '', xsec_token: '' } },
{ key: 'publish_image', method: 'POST', path: '/api/xhs/publish/image', label: 'Publish Image Note', category: 'Publish', body: { title: '', content: '', images: [], tags: [], is_original: false, visibility: 'public' } },
{ key: 'publish_video', method: 'POST', path: '/api/xhs/publish/video', label: 'Publish Video Note', category: 'Publish', body: { title: '', content: '', video: '', tags: [], visibility: 'public' } },
{ key: 'comment', method: 'POST', path: '/api/xhs/comment', label: 'Post Comment', category: 'Interaction', body: { feed_id: '', xsec_token: '', content: '' } },
{ key: 'comment_reply', method: 'POST', path: '/api/xhs/comment/reply', label: 'Reply Comment', category: 'Interaction', body: { feed_id: '', xsec_token: '', content: '', comment_id: '', user_id: '' } },
{ key: 'like', method: 'POST', path: '/api/xhs/like', label: 'Like/Unlike', category: 'Interaction', body: { feed_id: '', xsec_token: '', unlike: false } },
{ key: 'favorite', method: 'POST', path: '/api/xhs/favorite', label: 'Favorite/Unfavorite', category: 'Interaction', body: { feed_id: '', xsec_token: '', unfavorite: false } },
{ key: 'login_status', method: 'GET', path: '/api/xhs/login/status', label: '检查登录状态', category: '登录' },
{ key: 'login_qrcode', method: 'GET', path: '/api/xhs/login/qrcode', label: '获取登录二维码', category: '登录' },
{ key: 'login_delete', method: 'DELETE', path: '/api/xhs/login/cookies', label: '删除 Cookie(登出)', category: '登录' },
{ key: 'feeds', method: 'GET', path: '/api/xhs/feeds', label: '获取推荐列表', category: '内容' },
{ key: 'search', method: 'POST', path: '/api/xhs/search', label: '搜索', category: '内容', body: { keyword: '', filters: { sort: 'general', type: 'all', time: 'all' } } },
{ key: 'feed_detail', method: 'POST', path: '/api/xhs/feeds/detail', label: '笔记详情', category: '内容', body: { feed_id: '', xsec_token: '', load_all_comments: false } },
{ key: 'user_profile', method: 'POST', path: '/api/xhs/user/profile', label: '用户主页', category: '内容', body: { user_id: '', xsec_token: '' } },
{ key: 'publish_image', method: 'POST', path: '/api/xhs/publish/image', label: '发布图文笔记', category: '发布', body: { title: '', content: '', images: [], tags: [], is_original: false, visibility: 'public' } },
{ key: 'publish_video', method: 'POST', path: '/api/xhs/publish/video', label: '发布视频笔记', category: '发布', body: { title: '', content: '', video: '', tags: [], visibility: 'public' } },
{ key: 'comment', method: 'POST', path: '/api/xhs/comment', label: '发表评论', category: '互动', body: { feed_id: '', xsec_token: '', content: '' } },
{ key: 'comment_reply', method: 'POST', path: '/api/xhs/comment/reply', label: '回复评论', category: '互动', body: { feed_id: '', xsec_token: '', content: '', comment_id: '', user_id: '' } },
{ key: 'like', method: 'POST', path: '/api/xhs/like', label: '点赞/取消', category: '互动', body: { feed_id: '', xsec_token: '', unlike: false } },
{ key: 'favorite', method: 'POST', path: '/api/xhs/favorite', label: '收藏/取消', category: '互动', body: { feed_id: '', xsec_token: '', unfavorite: false } },
] as const;
+26 -26
View File
@@ -52,12 +52,12 @@ export function ApiTesterPage() {
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">API Tester</h1>
<h1 className="text-2xl font-bold">API </h1>
<Tabs
tabs={[
{ key: 'rest', label: 'REST API' },
{ key: 'mcp', label: 'MCP Tools' },
{ key: 'mcp', label: 'MCP 工具' },
]}
active={mode}
onChange={(k) => setMode(k as 'rest' | 'mcp')}
@@ -87,11 +87,11 @@ export function ApiTesterPage() {
{response !== null && (
<Card>
<div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">Response</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider"></h2>
<div className="flex items-center gap-2">
{responseStatus && (
<Badge variant={responseStatus === 'success' ? 'success' : 'danger'}>
{responseStatus}
{responseStatus === 'success' ? '成功' : '失败'}
</Badge>
)}
{duration !== null && (
@@ -108,11 +108,11 @@ export function ApiTesterPage() {
<div>
<Card>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">History</h2>
<Button variant="ghost" size="sm" onClick={() => setHistory([])}>Clear</Button>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider"></h2>
<Button variant="ghost" size="sm" onClick={() => setHistory([])}></Button>
</div>
{history.length === 0 ? (
<p className="text-sm text-dark-muted">No requests yet</p>
<p className="text-sm text-dark-muted"></p>
) : (
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{history.map((entry) => (
@@ -136,7 +136,7 @@ export function ApiTesterPage() {
</div>
<div className="flex items-center gap-2 mt-1 text-dark-muted">
<Badge variant={entry.status === 'success' ? 'success' : 'danger'} className="text-[10px]">
{entry.status}
{entry.status === 'success' ? '成功' : '失败'}
</Badge>
<span>{entry.time}</span>
<span>{entry.duration}ms</span>
@@ -227,10 +227,10 @@ function RestPanel({
return (
<>
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">REST Request</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">REST </h2>
<div className="space-y-3">
<Select
label="Endpoint"
label="端点"
options={categories.flatMap((cat) => [
{ value: `__cat_${cat}`, label: `── ${cat} ──` },
...API_ENDPOINTS.filter((e) => e.category === cat).map((e) => ({
@@ -253,7 +253,7 @@ function RestPanel({
</div>
{endpoint.method !== 'GET' && (
<div className="flex flex-col gap-1.5">
<label className="text-sm text-dark-muted">Request Body (JSON)</label>
<label className="text-sm text-dark-muted"> (JSON)</label>
<textarea
value={bodyText}
onChange={(e) => setBodyText(e.target.value)}
@@ -263,8 +263,8 @@ function RestPanel({
</div>
)}
<div className="flex gap-2">
<Button onClick={() => void handleSend()} loading={loading}>Send Request</Button>
<Button variant="ghost" size="sm" onClick={() => void navigator.clipboard.writeText(curl)}>Copy cURL</Button>
<Button onClick={() => void handleSend()} loading={loading}></Button>
<Button variant="ghost" size="sm" onClick={() => void navigator.clipboard.writeText(curl)}> cURL</Button>
</div>
</div>
</Card>
@@ -375,20 +375,20 @@ function McpPanel({
<>
{/* Connection */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP Connection</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP </h2>
<div className="flex items-center gap-3">
<Badge variant={connState === 'connected' ? 'success' : connState === 'connecting' ? 'warning' : 'default'}>
{connState}
{connState === 'connected' ? '已连接' : connState === 'connecting' ? '连接中' : '未连接'}
</Badge>
{connState === 'disconnected' && (
<Button size="sm" onClick={() => void handleConnect()} loading={connecting}>
Connect
</Button>
)}
{connState === 'connected' && (
<>
<span className="text-xs text-dark-muted">{tools.length} tools available</span>
<Button size="sm" variant="ghost" onClick={handleDisconnect}>Disconnect</Button>
<span className="text-xs text-dark-muted">{tools.length} </span>
<Button size="sm" variant="ghost" onClick={handleDisconnect}></Button>
</>
)}
{connState === 'connecting' && <Spinner size="sm" />}
@@ -398,10 +398,10 @@ function McpPanel({
{/* Tool Call */}
{connState === 'connected' && tools.length > 0 && (
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP Tool Call</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP </h2>
<div className="space-y-3">
<Select
label="Tool"
label="工具"
options={tools.map((t) => ({ value: t.name, label: t.name }))}
value={selectedTool}
onChange={(e) => handleToolChange(e.target.value)}
@@ -415,13 +415,13 @@ function McpPanel({
)}
{currentTool.inputSchema?.properties && (
<div>
<p className="text-dark-muted font-semibold mb-1">Parameters:</p>
<p className="text-dark-muted font-semibold mb-1"></p>
{Object.entries(currentTool.inputSchema.properties).map(([key, prop]) => (
<div key={key} className="flex gap-2 ml-2">
<code className="text-dark-accent">{key}</code>
<span className="text-dark-muted">
{prop.type || ''}
{currentTool.inputSchema?.required?.includes(key) ? ' (required)' : ' (optional)'}
{currentTool.inputSchema?.required?.includes(key) ? ' (必填)' : ' (可选)'}
{prop.description ? `${prop.description}` : ''}
</span>
</div>
@@ -433,7 +433,7 @@ function McpPanel({
{/* Arguments editor */}
<div className="flex flex-col gap-1.5">
<label className="text-sm text-dark-muted">Arguments (JSON)</label>
<label className="text-sm text-dark-muted"> (JSON)</label>
<textarea
value={argsText}
onChange={(e) => setArgsText(e.target.value)}
@@ -443,7 +443,7 @@ function McpPanel({
</div>
<Button onClick={() => void handleCall()} loading={loading}>
Call Tool
</Button>
</div>
</Card>
@@ -452,8 +452,8 @@ function McpPanel({
{connState === 'disconnected' && (
<Card>
<div className="text-center py-8 text-dark-muted">
<p>Connect to the MCP server to discover and test tools.</p>
<p className="text-xs mt-2">This uses the same SSE + JSON-RPC protocol that AI clients use.</p>
<p> MCP </p>
<p className="text-xs mt-2">使 AI SSE + JSON-RPC </p>
</div>
</Card>
)}
+33 -33
View File
@@ -43,10 +43,10 @@ export function BrowserPage() {
if (res.success && res.data) {
setFeeds(res.data);
} else {
toast('error', res.error?.message || 'Failed to load feeds');
toast('error', res.error?.message || '加载推荐失败');
}
} catch (err) {
toast('error', err instanceof Error ? err.message : 'Failed to load feeds');
toast('error', err instanceof Error ? err.message : '加载推荐失败');
} finally {
setFeedsLoading(false);
}
@@ -54,7 +54,7 @@ export function BrowserPage() {
const handleSearch = useCallback(async () => {
if (!keyword.trim()) {
toast('warning', 'Enter a keyword');
toast('warning', '请输入关键词');
return;
}
setSearchLoading(true);
@@ -68,10 +68,10 @@ export function BrowserPage() {
if (res.success && res.data) {
setSearchResults(res.data);
} else {
toast('error', res.error?.message || 'Search failed');
toast('error', res.error?.message || '搜索失败');
}
} catch (err) {
toast('error', err instanceof Error ? err.message : 'Search failed');
toast('error', err instanceof Error ? err.message : '搜索失败');
} finally {
setSearchLoading(false);
}
@@ -89,13 +89,13 @@ export function BrowserPage() {
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Content Browser</h1>
<h1 className="text-2xl font-bold"></h1>
<Tabs
tabs={[
{ key: 'explore', label: 'Explore' },
{ key: 'search', label: 'Search' },
{ key: 'user', label: 'User Profile' },
{ key: 'explore', label: '探索' },
{ key: 'search', label: '搜索' },
{ key: 'user', label: '用户主页' },
]}
active={tab}
onChange={(k) => setTab(k as TabKey)}
@@ -105,13 +105,13 @@ export function BrowserPage() {
{tab === 'explore' && (
<div className="space-y-4">
<Button onClick={() => void handleExplore()} loading={feedsLoading}>
Load Feed
</Button>
<FeedGrid
feeds={feeds}
loading={feedsLoading}
onSelect={handleFeedSelect}
emptyText="Click 'Load Feed' to get recommended content"
emptyText="点击「加载推荐」获取推荐内容"
/>
</div>
)}
@@ -122,53 +122,53 @@ export function BrowserPage() {
<div className="flex gap-3 items-end flex-wrap">
<div className="flex-1 min-w-[200px]">
<Input
label="Keyword"
placeholder="Search xiaohongshu..."
label="关键词"
placeholder="搜索小红书..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && void handleSearch()}
/>
</div>
<Select
label="Sort"
label="排序"
options={[
{ value: 'general', label: 'Default' },
{ value: 'time_descending', label: 'Latest' },
{ value: 'popularity_descending', label: 'Popular' },
{ value: 'general', label: '默认' },
{ value: 'time_descending', label: '最新' },
{ value: 'popularity_descending', label: '热门' },
]}
value={sortFilter}
onChange={(e) => setSortFilter(e.target.value)}
/>
<Select
label="Type"
label="类型"
options={[
{ value: 'all', label: 'All' },
{ value: 'note', label: 'Notes' },
{ value: 'video', label: 'Videos' },
{ value: 'all', label: '全部' },
{ value: 'note', label: '图文' },
{ value: 'video', label: '视频' },
]}
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
/>
<Select
label="Time"
label="时间"
options={[
{ value: 'all', label: 'Any time' },
{ value: 'day', label: 'Past day' },
{ value: 'week', label: 'Past week' },
{ value: 'half_year', label: 'Past 6 months' },
{ value: 'all', label: '不限' },
{ value: 'day', label: '一天内' },
{ value: 'week', label: '一周内' },
{ value: 'half_year', label: '半年内' },
]}
value={timeFilter}
onChange={(e) => setTimeFilter(e.target.value)}
/>
<Button onClick={() => void handleSearch()} loading={searchLoading}>
Search
</Button>
</div>
<FeedGrid
feeds={searchResults}
loading={searchLoading}
onSelect={handleFeedSelect}
emptyText="Enter a keyword and search"
emptyText="输入关键词进行搜索"
/>
</div>
)}
@@ -178,11 +178,11 @@ export function BrowserPage() {
<div className="space-y-4">
{!userView && (
<div className="text-center py-12 text-dark-muted">
<p>Click on a user in a feed detail to view their profile</p>
<p className="text-xs mt-2">Or enter user details manually:</p>
<p></p>
<p className="text-xs mt-2"></p>
<div className="flex gap-3 items-end justify-center mt-4">
<Input
placeholder="User ID"
placeholder="用户 ID"
value={manualUserId}
onChange={(e) => setManualUserId(e.target.value)}
/>
@@ -199,7 +199,7 @@ export function BrowserPage() {
}
}}
>
Load
</Button>
</div>
</div>
@@ -207,7 +207,7 @@ export function BrowserPage() {
{userView && userView.userId && userView.xsecToken && (
<div>
<Button variant="ghost" size="sm" onClick={() => setUserView(null)} className="mb-4">
&larr; Back
&larr;
</Button>
<UserCard
userId={userView.userId}
+19 -19
View File
@@ -15,9 +15,9 @@ export function DashboardPage() {
return (
<div className="space-y-6 max-w-5xl">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Dashboard</h1>
<h1 className="text-2xl font-bold"></h1>
<Button variant="ghost" size="sm" onClick={() => void refreshHealth()}>
Refresh
</Button>
</div>
@@ -25,24 +25,24 @@ export function DashboardPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Server Status */}
<Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Server</div>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{healthLoading ? (
<Spinner size="sm" />
) : health ? (
<div className="space-y-1">
<Badge variant={health.healthy ? 'success' : 'danger'}>
{health.healthy ? 'Healthy' : 'Unhealthy'}
{health.healthy ? '正常' : '异常'}
</Badge>
<p className="text-sm text-dark-muted">v{health.version}</p>
</div>
) : (
<Badge variant="danger">Offline</Badge>
<Badge variant="danger">线</Badge>
)}
</Card>
{/* Uptime */}
<Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Uptime</div>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{health ? (
<p className="text-xl font-mono font-bold text-dark-text">{formatUptime(health.uptime)}</p>
) : (
@@ -52,13 +52,13 @@ export function DashboardPage() {
{/* Login Status */}
<Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Xiaohongshu Login</div>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{loginLoading ? (
<Spinner size="sm" />
) : loginStatus ? (
<div className="space-y-1">
<Badge variant={loginStatus.loggedIn ? 'success' : 'warning'}>
{loginStatus.loggedIn ? 'Logged In' : 'Not Logged In'}
{loginStatus.loggedIn ? '已登录' : '未登录'}
</Badge>
{loginStatus.username && (
<p className="text-sm text-dark-muted">{loginStatus.username}</p>
@@ -69,18 +69,18 @@ export function DashboardPage() {
onClick={() => void refreshLogin()}
className="text-xs text-dark-accent hover:underline"
>
Click to check
</button>
)}
</Card>
{/* Memory */}
<Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Memory</div>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{health ? (
<div className="space-y-1">
<p className="text-xl font-mono font-bold text-dark-text">{health.memory.heapUsed} MB</p>
<p className="text-xs text-dark-muted">of {health.memory.heapTotal} MB heap</p>
<p className="text-xs text-dark-muted"> {health.memory.heapTotal} MB </p>
</div>
) : (
<p className="text-dark-muted">-</p>
@@ -91,13 +91,13 @@ export function DashboardPage() {
{/* Plugin Health */}
{health && Object.keys(health.plugins).length > 0 && (
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Plugins</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="space-y-2">
{Object.entries(health.plugins).map(([name, info]) => (
<div key={name} className="flex items-center justify-between py-1">
<span className="text-sm">{name}</span>
<Badge variant={info.healthy ? 'success' : 'danger'}>
{info.healthy ? 'Healthy' : info.message || 'Unhealthy'}
{info.healthy ? '正常' : info.message || '异常'}
</Badge>
</div>
))}
@@ -107,19 +107,19 @@ export function DashboardPage() {
{/* Quick actions */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Quick Actions</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="flex flex-wrap gap-3">
<Button variant="secondary" size="sm" onClick={() => navigate('/login')}>
Manage Login
</Button>
<Button variant="secondary" size="sm" onClick={() => navigate('/browser')}>
Browse Content
</Button>
<Button variant="secondary" size="sm" onClick={() => navigate('/publish')}>
Publish Note
</Button>
<Button variant="secondary" size="sm" onClick={() => navigate('/api-tester')}>
API Tester
API
</Button>
</div>
</Card>
@@ -128,7 +128,7 @@ export function DashboardPage() {
{health && (
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
Raw Health Data
</h2>
<pre className="bg-dark-bg border border-dark-border rounded-lg p-4 text-xs text-dark-text overflow-auto font-mono max-h-64">
{JSON.stringify(health, null, 2)}
+36 -36
View File
@@ -37,7 +37,7 @@ export function InteractionsPage() {
const checkIds = () => {
if (!feedId.trim() || !xsecToken.trim()) {
toast('warning', 'Feed ID and xsec_token are required');
toast('warning', 'Feed ID xsec_token 为必填项');
return false;
}
return true;
@@ -48,11 +48,11 @@ export function InteractionsPage() {
setLoading(unlike ? 'unlike' : 'like');
try {
const res = await toggleLike(feedId, xsecToken, unlike);
addLog(unlike ? 'Unlike' : 'Like', res);
toast('success', unlike ? 'Unliked' : 'Liked');
addLog(unlike ? '取消点赞' : '点赞', res);
toast('success', unlike ? '已取消点赞' : '已点赞');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed';
addLog(unlike ? 'Unlike' : 'Like', { error: msg });
const msg = err instanceof Error ? err.message : '操作失败';
addLog(unlike ? '取消点赞' : '点赞', { error: msg });
toast('error', msg);
} finally {
setLoading(null);
@@ -64,11 +64,11 @@ export function InteractionsPage() {
setLoading(unfavorite ? 'unfavorite' : 'favorite');
try {
const res = await toggleFavorite(feedId, xsecToken, unfavorite);
addLog(unfavorite ? 'Unfavorite' : 'Favorite', res);
toast('success', unfavorite ? 'Unfavorited' : 'Favorited');
addLog(unfavorite ? '取消收藏' : '收藏', res);
toast('success', unfavorite ? '已取消收藏' : '已收藏');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed';
addLog(unfavorite ? 'Unfavorite' : 'Favorite', { error: msg });
const msg = err instanceof Error ? err.message : '操作失败';
addLog(unfavorite ? '取消收藏' : '收藏', { error: msg });
toast('error', msg);
} finally {
setLoading(null);
@@ -77,18 +77,18 @@ export function InteractionsPage() {
const handleComment = useCallback(async () => {
if (!checkIds() || !commentText.trim()) {
toast('warning', 'Comment text is required');
toast('warning', '评论内容为必填项');
return;
}
setLoading('comment');
try {
const res = await postComment(feedId, xsecToken, commentText);
addLog('Comment', res);
toast('success', 'Comment posted');
addLog('评论', res);
toast('success', '评论已发布');
setCommentText('');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed';
addLog('Comment', { error: msg });
const msg = err instanceof Error ? err.message : '操作失败';
addLog('评论', { error: msg });
toast('error', msg);
} finally {
setLoading(null);
@@ -97,7 +97,7 @@ export function InteractionsPage() {
const handleReply = useCallback(async () => {
if (!checkIds() || !replyText.trim()) {
toast('warning', 'Reply text is required');
toast('warning', '回复内容为必填项');
return;
}
setLoading('reply');
@@ -109,12 +109,12 @@ export function InteractionsPage() {
comment_id: replyCommentId || undefined,
user_id: replyUserId || undefined,
});
addLog('Reply', res);
toast('success', 'Reply posted');
addLog('回复', res);
toast('success', '回复已发布');
setReplyText('');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed';
addLog('Reply', { error: msg });
const msg = err instanceof Error ? err.message : '操作失败';
addLog('回复', { error: msg });
toast('error', msg);
} finally {
setLoading(null);
@@ -123,11 +123,11 @@ export function InteractionsPage() {
return (
<div className="max-w-3xl space-y-6">
<h1 className="text-2xl font-bold">Interactions</h1>
<h1 className="text-2xl font-bold"></h1>
{/* Target */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Target Note</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="grid grid-cols-2 gap-4">
<Input label="Feed ID" value={feedId} onChange={(e) => setFeedId(e.target.value)} placeholder="Feed ID" />
<Input label="xsec_token" value={xsecToken} onChange={(e) => setXsecToken(e.target.value)} placeholder="xsec_token" />
@@ -136,45 +136,45 @@ export function InteractionsPage() {
{/* Like / Favorite */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Quick Actions</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="flex flex-wrap gap-3">
<Button onClick={() => void handleLike(false)} loading={loading === 'like'} size="sm">
Like
</Button>
<Button onClick={() => void handleLike(true)} loading={loading === 'unlike'} variant="secondary" size="sm">
Unlike
</Button>
<Button onClick={() => void handleFavorite(false)} loading={loading === 'favorite'} size="sm">
Favorite
</Button>
<Button onClick={() => void handleFavorite(true)} loading={loading === 'unfavorite'} variant="secondary" size="sm">
Unfavorite
</Button>
</div>
</Card>
{/* Comment */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Post Comment</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="space-y-3">
<Textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder="Write a comment..." />
<Textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder="写评论..." />
<Button onClick={() => void handleComment()} loading={loading === 'comment'} size="sm">
Post Comment
</Button>
</div>
</Card>
{/* Reply */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Reply to Comment</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<Input label="Comment ID" value={replyCommentId} onChange={(e) => setReplyCommentId(e.target.value)} placeholder="Optional" />
<Input label="User ID" value={replyUserId} onChange={(e) => setReplyUserId(e.target.value)} placeholder="Optional" />
<Input label="评论 ID" value={replyCommentId} onChange={(e) => setReplyCommentId(e.target.value)} placeholder="可选" />
<Input label="用户 ID" value={replyUserId} onChange={(e) => setReplyUserId(e.target.value)} placeholder="可选" />
</div>
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="Write a reply..." />
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="写回复..." />
<Button onClick={() => void handleReply()} loading={loading === 'reply'} size="sm">
Post Reply
</Button>
</div>
</Card>
@@ -183,8 +183,8 @@ export function InteractionsPage() {
{log.length > 0 && (
<Card>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">Action Log</h2>
<Button variant="ghost" size="sm" onClick={() => setLog([])}>Clear</Button>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider"></h2>
<Button variant="ghost" size="sm" onClick={() => setLog([])}></Button>
</div>
<div className="space-y-3 max-h-96 overflow-y-auto">
{log.map((entry) => (
+25 -25
View File
@@ -48,7 +48,7 @@ export function LoginPage() {
const res = await getLoginQRCode();
if (res.success && res.data) {
if (res.data.alreadyLoggedIn) {
toast('info', 'Already logged in!');
toast('info', '已经登录!');
void refreshStatus();
return;
}
@@ -62,7 +62,7 @@ export function LoginPage() {
if (cookieRes.success && cookieRes.data?.hasCookies) {
stopPolling();
setQrData(null);
toast('success', 'Login successful!');
toast('success', '登录成功!');
void refreshStatus();
}
} catch {
@@ -74,17 +74,17 @@ export function LoginPage() {
if (prev <= 1) {
stopPolling();
setQrData(null);
toast('warning', 'QR code expired');
toast('warning', '二维码已过期');
return 0;
}
return prev - 1;
});
}, 1000);
} else {
toast('error', res.error?.message || 'Failed to get QR code');
toast('error', res.error?.message || '获取二维码失败');
}
} catch (err) {
toast('error', err instanceof Error ? err.message : 'Failed to get QR code');
toast('error', err instanceof Error ? err.message : '获取二维码失败');
} finally {
setQrLoading(false);
}
@@ -95,13 +95,13 @@ export function LoginPage() {
try {
const res = await deleteCookies();
if (res.success) {
toast('success', 'Logged out successfully');
toast('success', '已成功登出');
void refreshStatus();
} else {
toast('error', res.error?.message || 'Failed to logout');
toast('error', res.error?.message || '登出失败');
}
} catch (err) {
toast('error', err instanceof Error ? err.message : 'Failed to logout');
toast('error', err instanceof Error ? err.message : '登出失败');
} finally {
setLogoutLoading(false);
}
@@ -119,16 +119,16 @@ export function LoginPage() {
return (
<div className="max-w-2xl space-y-6">
<h1 className="text-2xl font-bold">Xiaohongshu Login</h1>
<h1 className="text-2xl font-bold"></h1>
{!token && (
<Card className="border-dark-warning/30 bg-dark-warning/10">
<div className="flex items-center justify-between">
<p className="text-sm text-dark-warning">
Bearer Token API 401 Settings Token
Bearer Token API 401 Token
</p>
<Button size="sm" variant="secondary" onClick={() => navigate('/settings')}>
Settings
</Button>
</div>
</Card>
@@ -139,28 +139,28 @@ export function LoginPage() {
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-2">
Current Status
</h2>
{statusLoading ? (
<Spinner size="sm" />
) : status ? (
<div className="flex items-center gap-3">
<Badge variant={status.loggedIn ? 'success' : 'warning'}>
{status.loggedIn ? 'Logged In' : 'Not Logged In'}
{status.loggedIn ? '已登录' : '未登录'}
</Badge>
{status.username && <span className="text-sm">{status.username}</span>}
</div>
) : (
<span className="text-sm text-dark-muted">Click Refresh to check login status</span>
<span className="text-sm text-dark-muted"></span>
)}
</div>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => void refreshStatus()}>
Refresh
</Button>
{status?.loggedIn && (
<Button variant="danger" size="sm" onClick={() => void handleLogout()} loading={logoutLoading}>
Logout
</Button>
)}
</div>
@@ -170,14 +170,14 @@ export function LoginPage() {
{/* QR Code Login */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">
QR Code Login
</h2>
{!qrData && !qrLoading && (
<div className="text-center py-8">
<p className="text-dark-muted mb-4">Click the button to generate a QR code for login</p>
<p className="text-dark-muted mb-4"></p>
<Button onClick={() => void handleGetQR()} disabled={status?.loggedIn}>
Get QR Code
</Button>
</div>
)}
@@ -185,30 +185,30 @@ export function LoginPage() {
{qrLoading && (
<div className="flex flex-col items-center py-8 gap-3">
<Spinner size="lg" />
<p className="text-sm text-dark-muted">Generating QR code...</p>
<p className="text-sm text-dark-muted">...</p>
</div>
)}
{qrData && (
<div className="flex flex-col items-center gap-4">
<div className="bg-white rounded-xl p-4">
<img src={qrData} alt="Login QR Code" className="w-64 h-64" />
<img src={qrData} alt="登录二维码" className="w-64 h-64" />
</div>
<p className="text-sm text-dark-muted">Scan with Xiaohongshu app to login</p>
<p className="text-sm text-dark-muted">使 App </p>
{polling && (
<div className="flex items-center gap-3">
<Spinner size="sm" />
<span className="text-sm text-dark-accent">
Waiting for scan... {formatCountdown(countdown)}
... {formatCountdown(countdown)}
</span>
</div>
)}
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => void handleGetQR()}>
Refresh QR
</Button>
<Button variant="ghost" size="sm" onClick={stopPolling}>
Cancel
</Button>
</div>
</div>
+30 -30
View File
@@ -34,7 +34,7 @@ export function PublishPage() {
const handlePublishImage = useCallback(async () => {
if (!imgTitle.trim() || !imgPaths.trim()) {
toast('warning', 'Title and images are required');
toast('warning', '标题和图片为必填项');
return;
}
setImgLoading(true);
@@ -52,12 +52,12 @@ export function PublishPage() {
});
setImgResult(res);
if (res.success) {
toast('success', 'Image note published!');
toast('success', '图文笔记发布成功!');
} else {
toast('error', res.error?.message || 'Publish failed');
toast('error', res.error?.message || '发布失败');
}
} catch (err) {
const msg = err instanceof Error ? err.message : 'Publish failed';
const msg = err instanceof Error ? err.message : '发布失败';
toast('error', msg);
setImgResult({ error: msg });
} finally {
@@ -67,7 +67,7 @@ export function PublishPage() {
const handlePublishVideo = useCallback(async () => {
if (!vidTitle.trim() || !vidPath.trim()) {
toast('warning', 'Title and video path are required');
toast('warning', '标题和视频路径为必填项');
return;
}
setVidLoading(true);
@@ -83,12 +83,12 @@ export function PublishPage() {
});
setVidResult(res);
if (res.success) {
toast('success', 'Video note published!');
toast('success', '视频笔记发布成功!');
} else {
toast('error', res.error?.message || 'Publish failed');
toast('error', res.error?.message || '发布失败');
}
} catch (err) {
const msg = err instanceof Error ? err.message : 'Publish failed';
const msg = err instanceof Error ? err.message : '发布失败';
toast('error', msg);
setVidResult({ error: msg });
} finally {
@@ -98,12 +98,12 @@ export function PublishPage() {
return (
<div className="max-w-3xl space-y-4">
<h1 className="text-2xl font-bold">Publish Note</h1>
<h1 className="text-2xl font-bold"></h1>
<Tabs
tabs={[
{ key: 'image', label: 'Image Note' },
{ key: 'video', label: 'Video Note' },
{ key: 'image', label: '图文笔记' },
{ key: 'video', label: '视频笔记' },
]}
active={tab}
onChange={setTab}
@@ -112,28 +112,28 @@ export function PublishPage() {
{tab === 'image' && (
<Card>
<div className="space-y-4">
<Input label="Title" value={imgTitle} onChange={(e) => setImgTitle(e.target.value)} placeholder="Note title" />
<Textarea label="Content" value={imgContent} onChange={(e) => setImgContent(e.target.value)} placeholder="Note body text" />
<Textarea label="Image Paths (one per line)" value={imgPaths} onChange={(e) => setImgPaths(e.target.value)} placeholder="/path/to/image1.jpg&#10;/path/to/image2.jpg" />
<Input label="Tags (comma separated)" value={imgTags} onChange={(e) => setImgTags(e.target.value)} placeholder="travel, food" />
<Input label="标题" value={imgTitle} onChange={(e) => setImgTitle(e.target.value)} placeholder="笔记标题" />
<Textarea label="正文" value={imgContent} onChange={(e) => setImgContent(e.target.value)} placeholder="笔记正文" />
<Textarea label="图片路径(每行一个)" value={imgPaths} onChange={(e) => setImgPaths(e.target.value)} placeholder="/path/to/image1.jpg&#10;/path/to/image2.jpg" />
<Input label="标签(逗号分隔)" value={imgTags} onChange={(e) => setImgTags(e.target.value)} placeholder="旅行, 美食" />
<div className="flex gap-4 items-end">
<Select
label="Visibility"
label="可见性"
options={[
{ value: 'public', label: 'Public' },
{ value: 'private', label: 'Private' },
{ value: 'friends', label: 'Friends' },
{ value: 'public', label: '公开' },
{ value: 'private', label: '私密' },
{ value: 'friends', label: '仅好友' },
]}
value={imgVisibility}
onChange={(e) => setImgVisibility(e.target.value)}
/>
<label className="flex items-center gap-2 pb-2 cursor-pointer">
<input type="checkbox" checked={imgOriginal} onChange={(e) => setImgOriginal(e.target.checked)} className="rounded" />
<span className="text-sm text-dark-muted">Original content</span>
<span className="text-sm text-dark-muted"></span>
</label>
</div>
<Button onClick={() => void handlePublishImage()} loading={imgLoading}>
Publish Image Note
</Button>
{imgResult !== null && <JsonViewer data={imgResult} />}
</div>
@@ -143,22 +143,22 @@ export function PublishPage() {
{tab === 'video' && (
<Card>
<div className="space-y-4">
<Input label="Title" value={vidTitle} onChange={(e) => setVidTitle(e.target.value)} placeholder="Note title" />
<Textarea label="Content" value={vidContent} onChange={(e) => setVidContent(e.target.value)} placeholder="Note body text" />
<Input label="Video Path" value={vidPath} onChange={(e) => setVidPath(e.target.value)} placeholder="/path/to/video.mp4" />
<Input label="Tags (comma separated)" value={vidTags} onChange={(e) => setVidTags(e.target.value)} placeholder="travel, vlog" />
<Input label="标题" value={vidTitle} onChange={(e) => setVidTitle(e.target.value)} placeholder="笔记标题" />
<Textarea label="正文" value={vidContent} onChange={(e) => setVidContent(e.target.value)} placeholder="笔记正文" />
<Input label="视频路径" value={vidPath} onChange={(e) => setVidPath(e.target.value)} placeholder="/path/to/video.mp4" />
<Input label="标签(逗号分隔)" value={vidTags} onChange={(e) => setVidTags(e.target.value)} placeholder="旅行, vlog" />
<Select
label="Visibility"
label="可见性"
options={[
{ value: 'public', label: 'Public' },
{ value: 'private', label: 'Private' },
{ value: 'friends', label: 'Friends' },
{ value: 'public', label: '公开' },
{ value: 'private', label: '私密' },
{ value: 'friends', label: '仅好友' },
]}
value={vidVisibility}
onChange={(e) => setVidVisibility(e.target.value)}
/>
<Button onClick={() => void handlePublishVideo()} loading={vidLoading}>
Publish Video Note
</Button>
{vidResult !== null && <JsonViewer data={vidResult} />}
</div>
+18 -18
View File
@@ -10,49 +10,49 @@ export function SettingsPage() {
return (
<div className="max-w-2xl space-y-6">
<h1 className="text-2xl font-bold">Settings</h1>
<h1 className="text-2xl font-bold"></h1>
{/* Server Connection */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">Server Connection</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4"></h2>
<div className="space-y-4">
<Input
label="Server URL"
label="服务器地址"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
placeholder="Leave empty for same-origin (default)"
placeholder="留空则使用同源地址(默认)"
/>
<p className="text-xs text-dark-muted">
Leave empty when the dashboard is served by the same Express server.
Set to e.g. <code className="text-dark-accent">http://192.168.1.100:3000</code> for remote servers.
Dashboard Express {' '}
<code className="text-dark-accent">http://192.168.1.100:3000</code> 用于远程服务器。
</p>
</div>
</Card>
{/* Authentication */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">Authentication</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4"></h2>
<div className="space-y-4">
<Input
label="Bearer Token"
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Enter your API token"
placeholder="输入 API Token"
/>
<p className="text-xs text-dark-muted">
Token from <code className="text-dark-accent">BEARER_TOKEN</code> environment variable or{' '}
<code className="text-dark-accent">.social-mcp/bearer-token</code> file.
Token <code className="text-dark-accent">BEARER_TOKEN</code> {' '}
<code className="text-dark-accent">.social-mcp/bearer-token</code>
</p>
<div className="flex gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
toast('success', 'Settings saved');
toast('success', '设置已保存');
}}
>
Save
</Button>
<Button
variant="ghost"
@@ -60,10 +60,10 @@ export function SettingsPage() {
onClick={() => {
setToken('');
setServerUrl('');
toast('info', 'Settings cleared');
toast('info', '设置已清除');
}}
>
Clear All
</Button>
</div>
</div>
@@ -71,11 +71,11 @@ export function SettingsPage() {
{/* About */}
<Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">About</h2>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4"></h2>
<div className="space-y-2 text-sm text-dark-muted">
<p><span className="text-dark-text">Social MCP</span> Multi-platform social media automation</p>
<p>Version: 0.1.0</p>
<p>Stack: React 19 + TypeScript + Tailwind CSS</p>
<p><span className="text-dark-text">Social MCP</span> </p>
<p>0.1.0</p>
<p>React 19 + TypeScript + Tailwind CSS</p>
</div>
</Card>
</div>