diff --git a/src/web/pages/Accounts.tsx b/src/web/pages/Accounts.tsx index b5220cd..0073e46 100644 --- a/src/web/pages/Accounts.tsx +++ b/src/web/pages/Accounts.tsx @@ -4,6 +4,8 @@ import { api } from '../api.js'; import CenteredModal from '../components/CenteredModal.js'; import { useToast } from '../components/Toast.js'; import ModernSelect from '../components/ModernSelect.js'; +import { MobileCard, MobileField } from '../components/MobileCard.js'; +import { useIsMobile } from '../components/useIsMobile.js'; import { buildAddAccountPrereqHint, buildVerifyFailureHint, @@ -76,6 +78,8 @@ export default function Accounts() { const [loaded, setLoaded] = useState(false); const [sortMode, setSortMode] = useState('custom'); const [highlightAccountId, setHighlightAccountId] = useState(null); + const [expandedAccountIds, setExpandedAccountIds] = useState([]); + const isMobile = useIsMobile(768); const [showAdd, setShowAdd] = useState(false); const [addMode, setAddMode] = useState<'token' | 'login'>('token'); const [loginForm, setLoginForm] = useState(createLoginForm); @@ -538,6 +542,14 @@ export default function Accounts() { setSelectedAccountIds(visibleAccounts.map((account) => account.id)); }; + const toggleAccountDetails = (accountId: number) => { + setExpandedAccountIds((current) => ( + current.includes(accountId) + ? current.filter((id) => id !== accountId) + : [...current, accountId] + )); + }; + const runBatchAccountAction = async (action: 'enable' | 'disable' | 'delete' | 'refreshBalance') => { if (selectedAccountIds.length === 0) return; if (action === 'delete' && typeof globalThis.confirm === 'function' && !globalThis.confirm(`确认删除选中的 ${selectedAccountIds.length} 个账号?`)) return; @@ -1303,7 +1315,192 @@ export default function Accounts() {
{visibleAccounts.length > 0 ? ( - + isMobile ? ( +
+ {visibleAccounts.map((a: any) => { + const capabilities = resolveAccountCapabilities(a); + const connectionMode = resolveAccountCredentialMode(a); + const health = resolveRuntimeHealth(a); + const isExpanded = expandedAccountIds.includes(a.id); + return ( + + toggleAccountSelection(a.id, event.target.checked)} + /> + + {connectionMode === 'apikey' ? 'API Key' : 'Session'} + +
+ )} + > + + + + {health.label} + + + {health.reason} + + + )} + /> + +
${(a.balance || 0).toFixed(2)}
+
0 ? 'var(--color-success)' : 'var(--color-text-muted)', fontWeight: 500 }}> + +{(a.todayReward || 0).toFixed(2)} +
+ + )} + /> + +
${(a.balanceUsed || 0).toFixed(2)}
+
0 ? 'var(--color-danger)' : 'var(--color-text-muted)', fontWeight: 500 }}> + -{(a.todaySpend || 0).toFixed(2)} +
+ + )} + /> + {isExpanded ? ( +
+ + + {a.site?.name || '-'} + + + ) : ( + + {a.site?.name || '-'} + + )} + /> + handleToggleCheckin(a)} + disabled={!!actionLoading[`checkin-toggle-${a.id}`]} + data-tooltip={a.checkinEnabled ? '点击关闭签到,全部签到会忽略此账号' : '点击开启签到'} + aria-label={a.checkinEnabled ? '点击关闭签到,全部签到会忽略此账号' : '点击开启签到'} + > + {actionLoading[`checkin-toggle-${a.id}`] + ? + : (a.checkinEnabled ? '开启' : '关闭')} + + ) : ( + + 不支持 + + )} + /> + +
+ ) : null} +
+ + + {sortMode === 'custom' && ( + <> + + + + )} + {capabilities.canRefreshBalance && ( + + )} + + {capabilities.canCheckin && ( + + )} + {a.status === 'expired' && !capabilities.proxyOnly && ( + + )} + + +
+ + ); + })} + + ) : ( +
@@ -1490,6 +1687,7 @@ export default function Accounts() { })}
+ ) ) : (
diff --git a/src/web/pages/Sites.tsx b/src/web/pages/Sites.tsx index e8e67cc..b40b160 100644 --- a/src/web/pages/Sites.tsx +++ b/src/web/pages/Sites.tsx @@ -3,7 +3,9 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { api } from '../api.js'; import { useToast } from '../components/Toast.js'; import ModernSelect from '../components/ModernSelect.js'; +import { MobileCard, MobileField } from '../components/MobileCard.js'; import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js'; +import { useIsMobile } from '../components/useIsMobile.js'; import { formatDateTimeLocal } from './helpers/checkinLogTime.js'; import { clearFocusParams, readFocusSiteId } from './helpers/navigationFocus.js'; import { tr } from '../i18n.js'; @@ -78,6 +80,8 @@ export default function Sites() { const [orderingSiteId, setOrderingSiteId] = useState(null); const [pinningSiteId, setPinningSiteId] = useState(null); const [selectedSiteIds, setSelectedSiteIds] = useState([]); + const [expandedSiteIds, setExpandedSiteIds] = useState([]); + const isMobile = useIsMobile(768); const [batchActionLoading, setBatchActionLoading] = useState(false); const editorPresence = useAnimatedVisibility(Boolean(editor), 220); const lastEditorRef = useRef(null); @@ -327,6 +331,14 @@ export default function Sites() { setSelectedSiteIds(sortedSites.map((site) => site.id)); }; + const toggleSiteDetails = (siteId: number) => { + setExpandedSiteIds((current) => ( + current.includes(siteId) + ? current.filter((id) => id !== siteId) + : [...current, siteId] + )); + }; + const runBatchAction = async (action: 'enable' | 'disable' | 'delete' | 'enableSystemProxy' | 'disableSystemProxy') => { if (selectedSiteIds.length === 0) return; if (action === 'delete' && typeof globalThis.confirm === 'function' && !globalThis.confirm(`确认删除选中的 ${selectedSiteIds.length} 个站点?`)) return; @@ -555,7 +567,138 @@ export default function Sites() {
{sites.length > 0 ? ( - + isMobile ? ( +
+ {sortedSites.map((site) => { + const isExpanded = expandedSiteIds.includes(site.id); + return ( + toggleSiteSelection(site.id, event.target.checked)} + /> + )} + > + + {site.status === 'disabled' ? '禁用' : '启用'} + + )} + /> + + {site.platform || '-'} + + )} + /> + + + {isExpanded ? ( +
+ + {site.useSystemProxy ? '已开启' : '未开启'} + + )} + /> + + {site.externalCheckinUrl} + + ) : '-'} + /> + +
+ ) : null} +
+ + + {sortMode === 'custom' && ( + <> + + + + )} + + + +
+
+ ); + })} +
+ ) : ( +
@@ -720,6 +863,7 @@ export default function Sites() { ))}
+ ) ) : (
diff --git a/src/web/pages/Tokens.tsx b/src/web/pages/Tokens.tsx index 7c699b4..46aca1d 100644 --- a/src/web/pages/Tokens.tsx +++ b/src/web/pages/Tokens.tsx @@ -5,6 +5,8 @@ import CenteredModal from '../components/CenteredModal.js'; import { useToast } from '../components/Toast.js'; import { formatDateTimeLocal } from './helpers/checkinLogTime.js'; import ModernSelect from '../components/ModernSelect.js'; +import { MobileCard, MobileField } from '../components/MobileCard.js'; +import { useIsMobile } from '../components/useIsMobile.js'; import { clearFocusParams, readFocusTokenId } from './helpers/navigationFocus.js'; import { shouldIgnoreRowSelectionClick } from './helpers/rowSelection.js'; import { tr } from '../i18n.js'; @@ -105,6 +107,7 @@ function isTruthyFlag(input: string | null): boolean { export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: TokensPanelProps) { const location = useLocation(); const navigate = useNavigate(); + const isMobile = useIsMobile(768); const initialCreateForm = { accountId: 0, name: '', @@ -868,7 +871,117 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
) : tokens.length > 0 ? ( - + isMobile ? ( +
+ {tokens.map((token: any) => { + const loadingPrefix = `token-${token.id}`; + return ( + toggleTokenSelection(token.id, event.target.checked)} + /> + )} + > + {token.tokenMasked || '***'}} + /> + + + {token.site?.name || 'unknown'} + + + ) : ( + + {token.site?.name || 'unknown'} + + )} + /> + + + {token.enabled ? '启用' : '禁用'} + + )} + /> + 默认 : '-'} + /> + +
+ {!token.isDefault && ( + + )} + + + + +
+
+ ); + })} +
+ ) : ( +
@@ -998,6 +1111,7 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token })}
+ ) ) : (
diff --git a/src/web/pages/sites.mobile.test.tsx b/src/web/pages/sites.mobile.test.tsx new file mode 100644 index 0000000..3e957c3 --- /dev/null +++ b/src/web/pages/sites.mobile.test.tsx @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +describe('Sites mobile layout', () => { + it('includes mobile-card usage in Sites page', () => { + const source = readFileSync(resolve(process.cwd(), 'src/web/pages/Sites.tsx'), 'utf8'); + expect(source).toContain('mobile-card'); + }); +});