StarrySkyWorld's picture
Fix frontend blank screen by using antd message hook
859f406
import { useEffect, useMemo, useState } from "react";
import {
App as AntdApp,
Button,
Form,
Input,
Layout,
message,
Modal,
Progress,
Space,
Table,
Tabs,
Tag,
Typography,
Upload,
} from "antd";
import { api, clearToken, getToken, setToken } from "../api/client";
const { Header, Content } = Layout;
const { Text } = Typography;
const taskColor = {
queued: "default",
running: "processing",
success: "success",
failed: "error",
};
export function App() {
const [token, setTokenState] = useState(getToken());
const [pwd, setPwd] = useState("");
const [currentPath, setCurrentPath] = useState(".");
const [entries, setEntries] = useState([]);
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(false);
const [downloadOpen, setDownloadOpen] = useState(false);
const [extractOpen, setExtractOpen] = useState(false);
const [apiMsg, contextHolder] = message.useMessage();
async function loadFiles(path = currentPath) {
setLoading(true);
try {
const data = await api(`/api/fs/list?path=${encodeURIComponent(path)}`);
setEntries(data.entries || []);
setCurrentPath(path);
} catch (err) {
apiMsg.error(String(err.message || err));
} finally {
setLoading(false);
}
}
async function loadTasks() {
try {
const data = await api("/api/tasks?limit=100");
setTasks(data.tasks || []);
} catch (err) {
if (String(err.message || "").includes("401")) {
clearToken();
setTokenState("");
}
}
}
useEffect(() => {
if (!token) return;
loadFiles(".");
loadTasks();
const timer = setInterval(loadTasks, 2000);
return () => clearInterval(timer);
}, [token]);
const columns = useMemo(
() => [
{
title: "Name",
dataIndex: "name",
render: (_, row) =>
row.isDir ? (
<a onClick={() => loadFiles(row.path)}>{row.name}</a>
) : (
<span>{row.name}</span>
),
},
{
title: "Type",
dataIndex: "isDir",
render: (v) => (v ? "Directory" : "File"),
},
{
title: "Size",
dataIndex: "size",
},
{
title: "Action",
render: (_, row) => (
<Space>
<Button
danger
size="small"
onClick={async () => {
await api("/api/fs/delete", {
method: "POST",
body: JSON.stringify({ path: row.path }),
});
apiMsg.success("Deleted");
loadFiles(currentPath);
}}
>
Delete
</Button>
{!row.isDir && (
<Button
size="small"
onClick={() =>
window.open(
`/api/fs/download?path=${encodeURIComponent(row.path)}`,
"_blank"
)
}
>
Download
</Button>
)}
</Space>
),
},
],
[currentPath]
);
if (!token) {
return (
<AntdApp>
{contextHolder}
<div className="center-card">
<h2>FastFileViewer Login</h2>
<Space.Compact style={{ width: "100%" }}>
<Input.Password
placeholder="Admin password"
value={pwd}
onChange={(e) => setPwd(e.target.value)}
onPressEnter={async () => {
try {
const data = await api("/api/login", {
method: "POST",
body: JSON.stringify({ password: pwd }),
});
setToken(data.token);
setTokenState(data.token);
setPwd("");
} catch (err) {
apiMsg.error("Login failed");
}
}}
/>
<Button
type="primary"
onClick={async () => {
try {
const data = await api("/api/login", {
method: "POST",
body: JSON.stringify({ password: pwd }),
});
setToken(data.token);
setTokenState(data.token);
setPwd("");
} catch (err) {
apiMsg.error("Login failed");
}
}}
>
Login
</Button>
</Space.Compact>
</div>
</AntdApp>
);
}
return (
<AntdApp>
{contextHolder}
<Layout style={{ minHeight: "100vh" }}>
<Header className="header">
<Text strong style={{ color: "#fff" }}>
FastFileViewer
</Text>
<Button
onClick={() => {
clearToken();
setTokenState("");
}}
>
Logout
</Button>
</Header>
<Content className="content">
<Tabs
items={[
{
key: "files",
label: "Files",
children: (
<>
<Space style={{ marginBottom: 12 }}>
<Text>Current: {currentPath}</Text>
<Button onClick={() => loadFiles(".")}>Root</Button>
<Button onClick={() => loadFiles(currentPath)}>Refresh</Button>
<Button onClick={() => setDownloadOpen(true)}>
URL Download
</Button>
<Button onClick={() => setExtractOpen(true)}>Extract</Button>
<Upload
showUploadList={false}
customRequest={async ({ file, onSuccess, onError }) => {
try {
const form = new FormData();
form.append("dir", currentPath);
form.append("file", file);
await api("/api/fs/upload", {
method: "POST",
body: form,
});
onSuccess?.("ok");
loadFiles(currentPath);
} catch (err) {
onError?.(err);
}
}}
>
<Button>Upload</Button>
</Upload>
</Space>
<Table
rowKey={(row) => row.path}
columns={columns}
dataSource={entries}
loading={loading}
pagination={false}
/>
</>
),
},
{
key: "tasks",
label: "Tasks",
children: (
<Table
rowKey={(row) => row.id}
dataSource={tasks}
pagination={false}
columns={[
{ title: "ID", dataIndex: "id" },
{ title: "Type", dataIndex: "type" },
{
title: "Status",
dataIndex: "status",
render: (v) => <Tag color={taskColor[v]}>{v}</Tag>,
},
{
title: "Progress",
dataIndex: "progress",
render: (v) => <Progress percent={v} size="small" />,
},
{ title: "Source", dataIndex: "source" },
{ title: "Target", dataIndex: "targetPath" },
{ title: "Error", dataIndex: "error" },
]}
/>
),
},
]}
/>
</Content>
</Layout>
<Modal
open={downloadOpen}
title="Create Download Task"
onCancel={() => setDownloadOpen(false)}
footer={null}
>
<Form
layout="vertical"
onFinish={async (values) => {
await api("/api/tasks/download", {
method: "POST",
body: JSON.stringify(values),
});
setDownloadOpen(false);
loadTasks();
}}
>
<Form.Item
label="URL"
name="url"
rules={[{ required: true, message: "Please input URL" }]}
>
<Input placeholder="https://example.com/file.zip" />
</Form.Item>
<Form.Item
label="Target Path"
name="targetPath"
initialValue={`${currentPath}/download.bin`}
rules={[{ required: true, message: "Please input target path" }]}
>
<Input />
</Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form>
</Modal>
<Modal
open={extractOpen}
title="Create Extract Task"
onCancel={() => setExtractOpen(false)}
footer={null}
>
<Form
layout="vertical"
onFinish={async (values) => {
await api("/api/tasks/extract", {
method: "POST",
body: JSON.stringify(values),
});
setExtractOpen(false);
loadTasks();
}}
>
<Form.Item
label="Archive Path"
name="sourcePath"
rules={[{ required: true, message: "Please input archive path" }]}
>
<Input placeholder="path/to/file.zip" />
</Form.Item>
<Form.Item
label="Target Dir"
name="targetPath"
initialValue={currentPath}
rules={[{ required: true, message: "Please input target dir" }]}
>
<Input />
</Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form>
</Modal>
</AntdApp>
);
}