Enable dual-device login and mobile APK update checks
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChevronRight,
|
||||
Folder,
|
||||
@@ -13,7 +14,8 @@ import {
|
||||
Edit2,
|
||||
Trash2,
|
||||
FolderPlus,
|
||||
ChevronLeft
|
||||
ChevronLeft,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { NetdiskPathPickerModal } from '@/src/components/ui/NetdiskPathPickerModal';
|
||||
@@ -66,6 +68,7 @@ import {
|
||||
import {
|
||||
toDirectoryPath,
|
||||
} from '@/src/pages/files-tree';
|
||||
import { RECYCLE_BIN_RETENTION_DAYS, RECYCLE_BIN_ROUTE } from '@/src/pages/recycle-bin-state';
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -119,6 +122,7 @@ interface UiFile {
|
||||
type NetdiskTargetAction = 'move' | 'copy';
|
||||
|
||||
export default function MobileFiles() {
|
||||
const navigate = useNavigate();
|
||||
const initialPath = readCachedValue<string[]>(getFilesLastPathCacheKey()) ?? [];
|
||||
const initialCachedFiles = readCachedValue<FileMetadata[]>(getFilesListCacheKey(toBackendPath(initialPath))) ?? [];
|
||||
|
||||
@@ -445,19 +449,29 @@ export default function MobileFiles() {
|
||||
|
||||
{/* Top Header - Path navigation */}
|
||||
<div className="flex-none px-4 py-3 bg-[#0f172a]/80 border-b border-white/5 sticky top-0 z-20 shadow-md backdrop-blur-xl">
|
||||
<div className="flex flex-nowrap items-center text-sm overflow-x-auto custom-scrollbar whitespace-nowrap">
|
||||
{currentPath.length > 0 && (
|
||||
<button className="mr-3 p-1.5 rounded-full bg-white/5 text-slate-300 active:bg-white/10" onClick={handleBackClick}>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button className="text-slate-400 hover:text-white" onClick={() => handleBreadcrumbClick(-1)}>根目录</button>
|
||||
{currentPath.map((pathItem, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<ChevronRight className="w-3 h-3 mx-1 text-slate-600 shrink-0" />
|
||||
<button onClick={() => handleBreadcrumbClick(index)} className={cn(index === currentPath.length - 1 ? 'text-white font-medium' : 'text-slate-400', 'shrink-0')}>{pathItem}</button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex min-w-0 flex-1 flex-nowrap items-center text-sm overflow-x-auto custom-scrollbar whitespace-nowrap">
|
||||
{currentPath.length > 0 && (
|
||||
<button className="mr-3 p-1.5 rounded-full bg-white/5 text-slate-300 active:bg-white/10" onClick={handleBackClick}>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button className="text-slate-400 hover:text-white" onClick={() => handleBreadcrumbClick(-1)}>根目录</button>
|
||||
{currentPath.map((pathItem, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<ChevronRight className="w-3 h-3 mx-1 text-slate-600 shrink-0" />
|
||||
<button onClick={() => handleBreadcrumbClick(index)} className={cn(index === currentPath.length - 1 ? 'text-white font-medium' : 'text-slate-400', 'shrink-0')}>{pathItem}</button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(RECYCLE_BIN_ROUTE)}
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-slate-200"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
回收站
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -584,10 +598,10 @@ export default function MobileFiles() {
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setDeleteModalOpen(false)} />
|
||||
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="relative w-full max-w-sm glass-panel bg-[#0f172a] border border-white/10 rounded-2xl p-5 z-10 shadow-2xl">
|
||||
<h3 className="text-lg font-bold text-white mb-2 flex items-center gap-2"><Trash2 className="text-red-400 w-5 h-5"/>确认删除</h3>
|
||||
<p className="text-sm text-slate-300 mb-6 mt-3">你确实要彻底删除 <span className="text-white font-medium break-all">{fileToDelete?.name}</span> 吗?</p>
|
||||
<p className="text-sm text-slate-300 mb-6 mt-3">确定要将 <span className="text-white font-medium break-all">{fileToDelete?.name}</span> 移入回收站吗?文件会保留 {RECYCLE_BIN_RETENTION_DAYS} 天,期间可以恢复。</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" className="flex-1 bg-white/5 border-white/10 text-white" onClick={() => setDeleteModalOpen(false)}>取消</Button>
|
||||
<Button className="flex-1 bg-red-500 text-white hover:bg-red-600" onClick={handleDelete}>删除</Button>
|
||||
<Button className="flex-1 bg-red-500 text-white hover:bg-red-600" onClick={handleDelete}>移入回收站</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FolderPlus,
|
||||
Mail,
|
||||
Send,
|
||||
Smartphone,
|
||||
Upload,
|
||||
User,
|
||||
Zap,
|
||||
@@ -25,7 +26,14 @@ import { getOverviewCacheKey } from '@/src/lib/page-cache';
|
||||
import { clearPostLoginPending, hasPostLoginPending, readStoredSession } from '@/src/lib/session';
|
||||
import type { FileMetadata, PageResponse, UserProfile } from '@/src/lib/types';
|
||||
|
||||
import { getOverviewLoadErrorMessage } from '@/src/pages/overview-state';
|
||||
import {
|
||||
APK_DOWNLOAD_PUBLIC_URL,
|
||||
APK_DOWNLOAD_PATH,
|
||||
getMobileOverviewApkEntryMode,
|
||||
getOverviewLoadErrorMessage,
|
||||
getOverviewStorageQuotaLabel,
|
||||
shouldShowOverviewApkDownload,
|
||||
} from '@/src/pages/overview-state';
|
||||
|
||||
function formatFileSize(size: number) {
|
||||
if (size <= 0) return '0 B';
|
||||
@@ -56,6 +64,8 @@ export default function MobileOverview() {
|
||||
const [loadingError, setLoadingError] = useState('');
|
||||
const [retryToken, setRetryToken] = useState(0);
|
||||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
||||
const [apkActionMessage, setApkActionMessage] = useState('');
|
||||
const [checkingApkUpdate, setCheckingApkUpdate] = useState(false);
|
||||
|
||||
const currentHour = new Date().getHours();
|
||||
let greeting = '晚上好';
|
||||
@@ -72,6 +82,46 @@ export default function MobileOverview() {
|
||||
const latestFile = recentFiles[0] ?? null;
|
||||
const profileDisplayName = profile?.displayName || profile?.username || '未登录';
|
||||
const profileAvatarFallback = profileDisplayName.charAt(0).toUpperCase();
|
||||
const showApkDownload = shouldShowOverviewApkDownload();
|
||||
const apkEntryMode = getMobileOverviewApkEntryMode();
|
||||
|
||||
const handleCheckApkUpdate = async () => {
|
||||
setCheckingApkUpdate(true);
|
||||
setApkActionMessage('');
|
||||
|
||||
try {
|
||||
const response = await fetch(APK_DOWNLOAD_PUBLIC_URL, {
|
||||
method: 'HEAD',
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`检查更新失败 (${response.status})`);
|
||||
}
|
||||
|
||||
const lastModified = response.headers.get('last-modified');
|
||||
setApkActionMessage(
|
||||
lastModified
|
||||
? `发现最新安装包,更新时间 ${new Intl.DateTimeFormat('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(lastModified))},正在打开下载链接。`
|
||||
: '发现最新安装包,正在打开下载链接。'
|
||||
);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const openedWindow = window.open(APK_DOWNLOAD_PUBLIC_URL, '_blank', 'noopener,noreferrer');
|
||||
if (!openedWindow) {
|
||||
window.location.href = APK_DOWNLOAD_PUBLIC_URL;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setApkActionMessage(error instanceof Error ? error.message : '检查更新失败,请稍后重试');
|
||||
} finally {
|
||||
setCheckingApkUpdate(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -166,7 +216,7 @@ export default function MobileOverview() {
|
||||
<MobileMetricCard title="文件总数" value={`${rootFiles.length}`} icon={FileText} delay={0.1} color="text-amber-400" bg="bg-amber-500/20" />
|
||||
<MobileMetricCard title="近期上传" value={`${recentWeekUploads}`} icon={Upload} delay={0.15} color="text-emerald-400" bg="bg-emerald-500/20" />
|
||||
<MobileMetricCard title="快传就绪" value={latestFile ? '使用中' : '待命'} icon={Send} delay={0.2} color="text-[#336EFF]" bg="bg-[#336EFF]/20" />
|
||||
<MobileMetricCard title="存储占用" value={`${storagePercent.toFixed(1)}%`} icon={Database} delay={0.25} color="text-purple-400" bg="bg-purple-500/20" subtitle={`${formatFileSize(usedBytes)}`} />
|
||||
<MobileMetricCard title="存储占用" value={`${storagePercent.toFixed(1)}%`} icon={Database} delay={0.25} color="text-purple-400" bg="bg-purple-500/20" subtitle={getOverviewStorageQuotaLabel(storageQuotaBytes)} />
|
||||
</div>
|
||||
|
||||
{/* 快捷操作区 */}
|
||||
@@ -182,6 +232,48 @@ export default function MobileOverview() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showApkDownload || apkEntryMode === 'update' ? (
|
||||
<Card className="glass-panel overflow-hidden border-[#336EFF]/20 relative">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(51,110,255,0.2),transparent_45%),linear-gradient(180deg,rgba(16,24,40,0.94),rgba(15,23,42,0.88))]" />
|
||||
<CardContent className="relative z-10 p-4 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-[#336EFF]/15 text-[#7ea4ff]">
|
||||
<Smartphone className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-semibold text-white">Android 客户端</p>
|
||||
<p className="text-[11px] leading-5 text-slate-300">
|
||||
{apkEntryMode === 'update'
|
||||
? '在 App 内检查 OSS 上的最新安装包,并跳转到更新下载链接。'
|
||||
: '总览页可直接下载最新 APK,安装包与前端站点一起托管在 OSS。'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{apkEntryMode === 'update' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleCheckApkUpdate()}
|
||||
disabled={checkingApkUpdate}
|
||||
className="inline-flex h-10 w-full items-center justify-center rounded-xl bg-[#336EFF] px-4 text-sm font-medium text-white shadow-md shadow-[#336EFF]/20 transition-colors hover:bg-[#2958cc] disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{checkingApkUpdate ? '检查中...' : '检查更新'}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={APK_DOWNLOAD_PATH}
|
||||
download="yoyuzh-portal.apk"
|
||||
className="inline-flex h-10 w-full items-center justify-center rounded-xl bg-[#336EFF] px-4 text-sm font-medium text-white shadow-md shadow-[#336EFF]/20 transition-colors hover:bg-[#2958cc]"
|
||||
>
|
||||
下载 APK
|
||||
</a>
|
||||
)}
|
||||
{apkActionMessage ? (
|
||||
<p className="text-[11px] leading-5 text-slate-300">{apkActionMessage}</p>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{/* 近期文件 (精简版) */}
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="flex flex-row items-center justify-between py-3 px-4 pb-2 border-b border-white/5">
|
||||
|
||||
Reference in New Issue
Block a user