wnm168 commited on
Commit
1ef7ce3
·
verified ·
1 Parent(s): 538fec7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1223 -1
index.html CHANGED
@@ -1 +1,1223 @@
1
- <!DOCTYPE html><meta name="viewport" content="width=device-width, initial-scale=1" /><html><head> <meta charset="UTF-8" /> <title>Task</title> <link rel="stylesheet" type="text/css" href="https://www.unpkg.com/[email protected]/dist/css/bootstrap.min.css" /> <link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/[email protected]/css/fonts.min.css" rel="stylesheet" /></head><body> <div id="root"></div> <a id="back-to-top" href="#" class="btn btn-success btn-lg back-to-top" role="button"><i class="mdi mdi-arrow-up"></i></a> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://www.unpkg.com/[email protected]/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script> <script src="https://www.unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/react-bootstrap.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/redux.min.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-router-dom.min.js"></script> <script src="https://unpkg.com/[email protected]/babel.min.js"></script> <script src="https://unpkg.com/[email protected]/runtime.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/babel-polyfill/7.12.1/polyfill.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script> <script src="layer/layer.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ajv/8.17.1/ajv7.min.js"></script> <script src="https://unpkg.com/@tanstack/react-query@4/build/umd/index.production.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/mitt.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js"></script> <style> .bi { display: inline-block; width: 1rem; height: 1rem; } @media (min-width: 768px) { .sidebar { width: 100%; } .sidebar .offcanvas-lg { position: -webkit-sticky; position: sticky; top: 48px; } .navbar-search { display: block; } } .sidebar .nav-link { font-size: 0.875rem; font-weight: 500; } .sidebar .nav-link.active { color: #2470dc; } .sidebar-heading { font-size: 0.75rem; } .navbar { background-color: teal; } .navbar-brand { padding-top: 0.75rem; padding-bottom: 0.75rem; } .navbar .form-control { padding: 0.75rem 1rem; } .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; -webkit-user-select: none; -moz-user-select: none; user-select: none; } @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } .b-example-divider { width: 100%; height: 3rem; background-color: rgba(0, 0, 0, 0.1); border: solid rgba(0, 0, 0, 0.15); border-width: 1px 0; box-shadow: inset 0 0.5em 1.5em rgba(0, 0, 0, 0.1), inset 0 0.125em 0.5em rgba(0, 0, 0, 0.15); } .b-example-vr { flex-shrink: 0; width: 1.5rem; height: 100vh; } .bi { vertical-align: -0.125em; fill: currentColor; } .nav-scroller { position: relative; z-index: 2; height: 2.75rem; overflow-y: hidden; } .nav-scroller .nav { display: flex; flex-wrap: nowrap; padding-bottom: 1rem; margin-top: -1px; overflow-x: auto; text-align: center; white-space: nowrap; -webkit-overflow-scrolling: touch; } .btn-bd-primary { --bd-violet-bg: #712cf9; --bd-violet-rgb: 112.520718, 44.062154, 249.437846; --bs-btn-font-weight: 600; --bs-btn-color: var(--bs-white); --bs-btn-bg: var(--bd-violet-bg); --bs-btn-border-color: var(--bd-violet-bg); --bs-btn-hover-color: var(--bs-white); --bs-btn-hover-bg: #6528e0; --bs-btn-hover-border-color: #6528e0; --bs-btn-focus-shadow-rgb: var(--bd-violet-rgb); --bs-btn-active-color: var(--bs-btn-hover-color); --bs-btn-active-bg: #5a23c8; --bs-btn-active-border-color: #5a23c8; } .bd-mode-toggle { z-index: 1500; } .bd-mode-toggle .dropdown-menu .active .bi { display: block !important; } .back-to-top { position: fixed; bottom: 25px; right: 25px; display: none; } .leftsidebar { height: 100%; box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1); } @media (min-width: 768px) { .leftsidebar { min-width: 15%; } } @media (max-width: 768px) { .leftsidebar { max-width: 50%; } } .bg-teal { background-color: teal; } </style> <script type="text/babel" data-presets="react" data-type="module"> //事件监听开始 通过修改localstorage实现跨页面事件监听 const emitter = mitt(); // 监听 localStorage 变化 window.addEventListener("storage", (event) => { if (event.key === "event") { const { type, data } = JSON.parse(event.newValue); emitter.emit(type, data); } }); // 封装 emit 方法 const emitEvent = (type, data) => { // 触发本地事件 emitter.emit(type, data); const randomString = Math.random() .toString(36) .substring(2, 10); // 生成一个随机字符串确保event每次的值不一样,如果一样会不触发事件 const identity = `${Date.now()}-${randomString}`; // 存储到 localStorage,以便其他页面能够接收到 localStorage.setItem( "event", JSON.stringify({ type, data, identity }) ); }; // 封装 on 方法 const onEvent = (type, callback) => { emitter.on(type, callback); }; // 封装 off 方法 const offEvent = (type, callback) => { emitter.off(type, callback); }; //事件监听结束 var settingStorage = localforage.createInstance({ name: "setting", driver: localforage.LOCALSTORAGE }); // settingStorage.setItem("category", { name: 'test', id: 1 }); // settingStorage.getItem('category').then(function (value) { // console.log(value); // }).catch(function (err) { // console.log(err); // }); // settingStorage.getItem('category', function (err, value) { // console.log(value.name); // }); const { createStore, combineReducers } = Redux; // 从 localStorage 加载初始状态 const loadStateFromLocalStorage = () => { try { const serializedState = localStorage.getItem('settings'); if (serializedState === null) { return {}; // 默认值 } return JSON.parse(serializedState); } catch (e) { console.error("Could not load state from localStorage:", e); return {}; // 默认值 } }; // 保存状态到 localStorage const saveStateToLocalStorage = (state) => { try { const serializedState = JSON.stringify(state); localStorage.setItem('settings', serializedState); } catch (e) { console.error("Could not save state to localStorage:", e); } }; // 定义初始状态 const initialSettingsState = loadStateFromLocalStorage(); // 创建 settings Reducer function settingsReducer(state = initialSettingsState, action) { switch (action.type) { case 'SAVE_SETTING': return { ...state, ...action.payload }; default: return state; } } // 合并 Reducer(如果有多个) const rootReducer = combineReducers({ settings: settingsReducer, }); // 创建 Redux Store const STORE = createStore(rootReducer); // 订阅 Store 的变化,并将状态保存到 localStorage STORE.subscribe(() => { saveStateToLocalStorage(STORE.getState().settings); }); //数据校验 // var ajv = new ajv7.default() // const schema = { // type: "object", // properties: { // foo: { type: "integer" }, // bar: { type: "string" } // }, // required: ["foo"], // additionalProperties: false // } // const validate = ajv.compile(schema) // const data = { // foo: 1, // bar: "abc" // } // const valid = validate(data) // if (!valid) console.log(validate.errors) const bytesToSize = (bytes) => { if (bytes === 0) return '0 B'; var k = 1024; sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; }; const formatDate = (date) => { var d = new Date(date); var year = d.getFullYear(); var month = d.getMonth() + 1; var day = d.getDate()< 10 ? '0' + d.getDate() : '' + d.getDate(); var hour = d.getHours(); var minutes = d.getMinutes(); var seconds = d.getSeconds(); return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds; }; let layerLoading = null; const showLoading = () => { const loadindex = layer.load(1); layerLoading = loadindex; } const hideLoading = () => { layer.close(layerLoading); } const { useState, useEffect, useRef } = React; const { HashRouter, Route, Link, Switch, useLocation, useParams } = ReactRouterDOM; const { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } = ReactQuery; const queryClient = new QueryClient() const { Alert, Badge, Button, ButtonGroup, ButtonToolbar, Collapse, Col, Container, Form, Image, InputGroup, ListGroup, Modal, Nav, Navbar, NavDropdown, Offcanvas, Pagination, Row, Table, } = ReactBootstrap; //注意修改js文件后需要直接访问js以更新浏览器缓存 // 表格组件 const DataTable = ({ data, columns }) => { return ( <Table responsive bordered> <thead> <tr className="text-center"> {columns.map((column, index) => ( <th key={index}>{column.title}</th> ))} </tr> </thead> <tbody> {data.map((row, rowIndex) => ( <tr key={rowIndex} className="text-center"> {columns.map((column, colIndex) => ( <td key={colIndex}> {} {column.render ? column.render(row) : row[column.dataIndex]} </td> ))} </tr> ))} </tbody> </Table> ); }; //分页组件 const Paginate = (props) => { const page = props.page; const pageCount = Math.ceil( props.totalCount / props.itemsPerPage ); const SelectItems = () => { const pageNumbers = Array.from( { length: pageCount }, (_, i) => i + 1 ); return ( <select className="form-select me-2" style={{ width: "auto" }} onChange={(e) => { props.onClick(parseInt(e.target.value)); }} > {pageNumbers.map((number) => { const selected = number === page ? true : false; return ( <option key={number} value={number} selected={selected} > {number} </option> ); })} </select> ); }; return ( <div className="d-flex justify-content-center align-items-baseline"> <SelectItems /> <span className="text-info me-2"> {page}/{pageCount} </span> <Pagination> {pageCount > 1 && page > 1 && ( <Pagination.First onClick={() => { props.onClick(1); }} /> )} {pageCount > 1 && page > 1 && ( <Pagination.Prev onClick={() => { props.onClick(page - 1); }} /> )} {pageCount > 1 && page< pageCount && ( <Pagination.Next onClick={() => { props.onClick(page + 1); }} /> )} {pageCount > 1 && page< pageCount && ( <Pagination.Last onClick={() => { props.onClick(pageCount); }} /> )} </Pagination> </div> ); }; //图标组件 const Icon = (props) => { return ( <span onClick={props.onClick} className={`mdi mdi-${props.icon} fs-${props.size} ${props.className}`} ></span> ); }; //按钮图标组件 const IconButton = (props) => { return ( <Button variant="success" onClick={props.onClick} className={props.className} > <span className={`mdi mdi-${props.icon} fs-${props.iconSize} ${props.iconClassName}`} ></span> {props.text} </Button> ); }; //设置框 const SettingModal = (props) => { const settings = [ { "alist": [{ "label": "Alist地址", "key": "alist_host", "show": true }, { "label": "Alist令牌", "key": "alist_token", "show": false }] }, { "github": [{ "label": "Actions地址", "key": "github_host", "show": true }, { "label": "Github令牌", "key": "github_token", "show": false }] }, { "directus": [{ "label": "Directus地址", "key": "directus_host", "show": true }, { "label": "Directus令牌", "key": "directus_token", "show": false }] } ] const [setting, setSetting] = useState({}); // useEffect(() => { // localStorage.setItem('settings', JSON.stringify(setting)); // }, [setting]); const loadSetting = () => { const storedSettings = STORE.getState().settings; if (storedSettings) { setSetting(storedSettings); } } const saveSetting = () => { STORE.dispatch({ type: 'SAVE_SETTING', payload: setting }) //localStorage.setItem('settings', JSON.stringify(setting)); } return ( <Modal show={props.show} onHide={props.onHide} onShow={loadSetting}> <Modal.Header closeButton onHide={props.onHide}> <Modal.Title>设置</Modal.Title> </Modal.Header> <Modal.Body> <Form> <ListGroup> {settings.map((value, index) => { const key = Object.keys(value)[0]; const items = value[key]; return (<ListGroup.Item> {items.map((setting_item) => { return ( <Form.Group as={Row} className="mb-3"> <Form.Label column sm="3"> {setting_item.label} </Form.Label> <Col sm="9"> <Form.Control type={setting_item.show ? "input" : "password"} value={setting[setting_item.key]} name={setting_item.key} placeholder={setting_item.label} onChange={(e) => { setSetting({ ...setting, [setting_item.key]: e.target.value }) }} /> </Col> </Form.Group> ) })} </ListGroup.Item>) })} </ListGroup> </Form> </Modal.Body> <Modal.Footer className="justify-content-between"> <Button variant="secondary" onClick={() => { props.onHide(); }} > 关闭 </Button> <Button variant="primary" onClick={() => { saveSetting(); props.onHide(); //props.onSave(); }} > 保存 </Button> </Modal.Footer> </Modal> ); }; //axios封装开始 const useAxios = () => { const [response, setResponse] = useState(null); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); // Create an Axios instance const axiosInstance = axios.create({}); // Set up request and response interceptors axiosInstance.interceptors.request.use( (config) => { // Log or modify request here //console.log("Sending request to:", config.url); return config; }, (error) => { // Handle request error here return Promise.reject(error); } ); axiosInstance.interceptors.response.use( (response) => { // Log or modify response here //console.log("Received response from:", response.config.url); return response; }, (error) => { // Handle response error here return Promise.reject(error); } ); useEffect(() => { const source = axios.CancelToken.source(); return () => { // Cancel the request when the component unmounts source.cancel( "组件被卸载: 请求取消." ); }; }, []); // Making the API call with cancellation support const fetchData = async ({ url, method, data, headers }) => { setLoading(true); try { const result = await axiosInstance({ url, method, headers: headers ? headers : {}, data: method.toLowerCase() === "get" ? undefined : data, params: method.toLowerCase() === "get" ? data : undefined, cancelToken: axios.CancelToken.source().token, }); setResponse(result.data); } catch (error) { if (axios.isCancel(error)) { console.log("Request cancelled", error.message); } else { setError( error.response ? error.response.data : error.message ); } } finally { setLoading(false); } }; return [response, error, loading, fetchData]; }; //axios封闭结束 //API定义开始 const getFiles = () => { const [response, error, loading, fetchData] = useAxios(); const fetchDataByPage = async (setting, query) => { var host = setting.alist_host; if (!host.endsWith("/")) { host = host + '/' } fetchData({ url: host + 'api/fs/list', method: "POST", data: query, headers: { 'Authorization': setting.alist_token, 'Content-Type': 'application/json' }, }); }; return [response, error, loading, fetchDataByPage]; }; //API定义结束 const Layout = ({ children }) => { useEffect(() => { // 组件挂载时执行的代码(相当于 componentDidMount) }, []); // 空数组表示只在挂载和卸载时执行 const [showSideBar, setShowSideBar] = useState(false); const handleSidebarClose = () => setShowSideBar(false); const handleSidebarShow = () => setShowSideBar(true); const toggleSidebarShow = () => { setShowSideBar(!showSideBar); }; const [setting, setSetting] = useState(false); return ( <div className="pb-5"> <header className="sticky-top"> <Navbar expand="md"> <Container fluid> <div> <Navbar.Toggle className="shadow-none border-0" onClick={handleSidebarShow} children={ <Icon icon="menu" size="3" className="text-white" /> } /> <Navbar.Brand as={Link} to="/" className="text-white" > 离线管理 </Navbar.Brand> </div> <div className="d-flex"> <LocalTasks /> <Button style={{ backgroundColor: "transparent", }} className="nav-link btn" onClick={() => { setSetting(true) }} children={ <Icon icon="dots-vertical" size="3" className="text-white" /> } ></Button> <SettingModal show={setting} onHide={() => { setSetting(false); }} /> </div> </Container> </Navbar> </header> <Container fluid> <Row style={{ minHeight: "100vh" }}> <Col md="2" lg="2" xl="2" className="ps-0 d-none d-md-block" > <Offcanvas className="leftsidebar h-100 bg-light" show={showSideBar} onHide={handleSidebarClose} placement="start" responsive="md" > <Offcanvas.Header className="py-2 border-bottom" closeButton > <Offcanvas.Title> 离线任务 </Offcanvas.Title> </Offcanvas.Header> <Offcanvas.Body className="p-0"> <Container fluid className="p-0"> <Nav activeKey="1" className="flex-column" > <Nav.Link as={Link} className="nav-link text-dark" to="/" onClick={ handleSidebarClose } > <Icon icon="plus" size="6" className="me-2" /> 离线管理 </Nav.Link> </Nav> </Container> </Offcanvas.Body> </Offcanvas> </Col> <Col xs="12" sm="12" md="10" lg="10" xl="10"> <main> <Container fluid className="pt-2 px-0"> {children} </Container> </main> </Col> </Row> </Container> </div> ); }; const Home = () => { const location = useLocation(); const { id } = useParams(); return ( <div> <div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light"> <label className="fs-3">Home</label> <ButtonToolbar aria-label="文件列表" className="bg-teal rounded" > <ButtonGroup className="bg-teal"> <IconButton onClick={() => { alert("test") }} text="刷新" className="bg-teal border-0" icon="reload" iconClassName="me-1 text-white" iconSize="6" /> <IconButton onClick={() => { alert("hello"); }} text="删除" className="bg-teal border-0" icon="delete-outline" iconClassName="me-1 text-white" iconSize="6" /> </ButtonGroup> </ButtonToolbar> </div> <Container fluid className="p-2"></Container> </div> ); }; const LocalTasks = () => { const [show, setShow] = useState(false); const handleClose = () => setShow(false); const handleShow = () => setShow(true); const [downloads, setDownloads] = useState([]) const [addDownloadObject, setAddDownloadObject] = useState({}) const setting = STORE.getState().settings; const columns = [ { title: "文件名称", dataIndex: "name" }, { title: "大小", dataIndex: "size", render: (row) => (bytesToSize(row.size)) }, { title: "日期", dataIndex: "created", render: (row) => (formatDate(row.created)) }, { title: "操作", dataIndex: "name", render: (row) => ( <div> <Icon icon="delete-outline" size="6" className="me-2" onClick={() => { layer.confirm('确定删除?该操作无法撤销!!!', { icon: 3 }, function (index) { setDownloads( downloads.filter(a => a.name !== row.name ) ); layer.close(index); }, function () { }); }} /> <Icon icon="pencil-outline" size="6" className="me-2" onClick={() => { layer.prompt({ title: '输入文件名称,并确认', formType: 0, value: row.name, success: function (layero, index) { $("div[aria-modal]").eq(0).removeAttr("tabindex");//解决弹出窗的input无法获取焦点的问题 }, end: function (layero, index) { $("div[aria-modal]").eq(0).attr("tabindex", -1).focus();//再把焦点还回去 } }, function (value, index) { const newDownloads = downloads.map(downloadItem => { if (downloadItem.name === row.name) { return { ...downloadItem, name: value }; } return downloadItem; }); setDownloads(newDownloads); layer.close(index); }); }} /> </div> ), }, ]; const { mutateAsync: localTaskdMutation } = useMutation({ mutationKey: ["get-download"], mutationFn: async () => { showLoading(); var host = setting.directus_host; if (!host.endsWith("/")) { host = host + '/' } var url = host + 'items/task'; const tasks = downloads.map(task => { return { url: task.url + '##' + task.name } }) return await axios.post(url, tasks, { headers: { 'Authorization': "Bearer " + setting.directus_token, 'Content-Type': 'application/json' }, }) }, onSuccess: async (data, variables, context) => { hideLoading(); layer.msg('任务添加成功', { time: 2000, icon: 6 }); }, onError: () => { hideLoading(); layer.msg('任务添加失败', { time: 2000, icon: 5 }); } }) const addDowload = (fileinfo) => { const file = fileinfo.data.data; const download = { name: file.name, size: file.size, url: file.raw_url, created: file.created } setAddDownloadObject(download) } useEffect(() => { if (addDownloadObject && ('name' in addDownloadObject)) { setDownloads([...downloads, addDownloadObject]) setAddDownloadObject({}) } }, [addDownloadObject]); useEffect(() => { onEvent("addDownload", addDowload) settingStorage.getItem('downloads').then(function (value) { if (value) { setDownloads(value) } }).catch(function (err) { console.log(err) }); }, []); useEffect(() => { settingStorage.setItem('downloads', downloads) }, [downloads]); if (downloads.length > 0) { return ( <div> <Button style={{ backgroundColor: "transparent", }} className="nav-link btn" onClick={handleShow} children={ <span> <Icon icon="download" size="3" className="text-white" /> <Badge bg="danger" style={{ top: '-15px', left: '-10px' }}>{downloads.length}</Badge> </span> } ></Button> <Modal show={show} onHide={handleClose}> <Modal.Header closeButton> <Modal.Title>本地下载任务</Modal.Title> </Modal.Header> <Modal.Body> {downloads && ( <DataTable data={downloads ? downloads : []} columns={columns} /> )} </Modal.Body> <Modal.Footer className="justify-content-between"> <ButtonGroup> <Button variant="primary" onClick={async () => { await localTaskdMutation() }}> 添加转存 </Button> </ButtonGroup> <ButtonGroup> <Button variant="danger" onClick={() => { layer.confirm('确定删除?该操作无法撤销!!!', { icon: 3 }, function (index) { setDownloads([]); layer.close(index); }, function () { }); }}> 清空 </Button> <Button variant="primary" onClick={handleClose}> 关闭 </Button> </ButtonGroup> </Modal.Footer> </Modal> </div > ); } } App = () => { const [open, setOpen] = useState(false); const [reload, setReload] = useState(false); const [response, error, loading, fetchDataByPage] = getFiles(); const { folder } = useParams(); const location = useLocation(); const [path, setPath] = useState(location.pathname); const [page, setPage] = useState(1); const [query, setQuery] = useState({ "path": path, "password": "", "page": page, "per_page": 0, "refresh": true }); const setting = STORE.getState().settings; //const queryClient = useQueryClient() // Queries //const { data, error, isLoading, refetch } = useQuery({ // queryKey: ['test'], queryFn: () => axios.get("") //}) const { data: fileData, mutateAsync: downloadMutation } = useMutation({ mutationKey: ["get-download"], mutationFn: async (fileinfo) => { showLoading(); var host = setting.alist_host; if (!host.endsWith("/")) { host = host + '/' } var url = host + 'api/fs/get'; return await axios.post(url, fileinfo, { headers: { 'Authorization': setting.alist_token, 'Content-Type': 'application/json' }, }) }, onSuccess: async (data, variables, context) => { hideLoading(); }, onError: () => { hideLoading(); } }) useEffect(() => { if (fileData) { emitEvent("addDownload", fileData) } }, [fileData]); const columns = [ { title: "文件名称", dataIndex: "name" }, { title: "大小", dataIndex: "size", render: (row) => (bytesToSize(row.size)) }, { title: "日期", dataIndex: "created", render: (row) => (formatDate(row.created)) }, { title: "操作", dataIndex: "name", render: (row) => ( row.is_dir ?<Nav.Link as={Link} className="nav-link text-dark" to={path + row.name + '/'} target="_blank" > <Icon icon="open-in-new" size="6" className="me-2" /> </Nav.Link> : <Icon icon="download-outline" size="6" className="me-2" onClick={async () => { let data = { "path": decodeURIComponent(path + row.name), "password": "" } await downloadMutation(data); }} /> ), }, ]; useEffect(() => { if (!setting.alist_token || setting.alist_token.length< 5) { layer.alert("请先正确配置Alsit的令牌", { icon: 5 }); return } fetchDataByPage(setting, query); return () => { } }, [reload, query]); const forceUpdate = () => { setReload((pre) => !pre); }; return ( <div> <div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light"> <label className="fs-3">文件列表</label> <ButtonToolbar aria-label="功能区" className="bg-teal rounded" > <ButtonGroup className="bg-teal"> <IconButton onClick={() => { emitEvent("test", { a: 'b' }) }} text="刷新" className="bg-teal border-0" icon="reload" iconClassName="me-1 text-white" iconSize="6" /> </ButtonGroup> </ButtonToolbar> </div> <Container fluid className="p-2"> {error && ( <div className="text-center text-danger"> {error} </div> )} {(loading) && ( <div className="text-center text-success"> 正在努力加载中...... </div> )} {response && ( <DataTable data={response.data.content ? response.data.content : []} columns={columns} /> )} </Container> </div> ); }; const container = document.getElementById("root"); const root = ReactDOM.createRoot(container); root.render( <QueryClientProvider client={queryClient}> <HashRouter> <Route path="/:path?"> <Layout> <Switch> <Route path="/" exact component={App} /> <Route path="/:folder?" component={App} /> </Switch> </Layout> </Route> </HashRouter> </QueryClientProvider> ); $(document).ready(function () { $(window).scroll(function () { if ($(this).scrollTop() > 50) { $("#back-to-top").fadeIn(); } else { $("#back-to-top").fadeOut(); } }); // scroll body to 0px on click $("#back-to-top").click(function () { $("body,html").animate( { scrollTop: 0, }, 400 ); return false; }); }); </script></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3
+ <html>
4
+
5
+ <head>
6
+ <meta charset="UTF-8" />
7
+ <title>Task</title>
8
+ <link rel="stylesheet" type="text/css" href="https://www.unpkg.com/[email protected]/dist/css/bootstrap.min.css" />
9
+ <link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet" />
10
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/css/fonts.min.css" rel="stylesheet" />
11
+ </head>
12
+
13
+ <body>
14
+ <div id="root"></div>
15
+ <a id="back-to-top" href="#" class="btn btn-success btn-lg back-to-top" role="button"><i
16
+ class="mdi mdi-arrow-up"></i></a>
17
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
18
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
19
+ <script src="https://www.unpkg.com/[email protected]/dist/jquery.min.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
21
+ <script src="https://www.unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
22
+ <script src="https://unpkg.com/[email protected]/dist/react-bootstrap.min.js"></script>
23
+ <script src="https://unpkg.com/[email protected]/dist/redux.min.js"></script>
24
+ <script src="https://unpkg.com/[email protected]/umd/react-router-dom.min.js"></script>
25
+ <script src="https://unpkg.com/[email protected]/babel.min.js"></script>
26
+ <script src="https://unpkg.com/[email protected]/runtime.js"></script>
27
+ <script src="https://cdn.bootcdn.net/ajax/libs/babel-polyfill/7.12.1/polyfill.min.js"></script>
28
+ <script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
29
+ <script src="layer/layer.js"></script>
30
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/ajv/8.17.1/ajv7.min.js"></script>
31
+ <script src="https://unpkg.com/@tanstack/react-query@4/build/umd/index.production.js"></script>
32
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/mitt.umd.min.js"></script>
33
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js"></script>
34
+
35
+ <style>
36
+ .bi {
37
+ display: inline-block;
38
+ width: 1rem;
39
+ height: 1rem;
40
+ }
41
+
42
+ /*
43
+ * Sidebar
44
+ */
45
+ @media (min-width: 768px) {
46
+ .sidebar {
47
+ width: 100%;
48
+ }
49
+
50
+ .sidebar .offcanvas-lg {
51
+ position: -webkit-sticky;
52
+ position: sticky;
53
+ top: 48px;
54
+ }
55
+
56
+ .navbar-search {
57
+ display: block;
58
+ }
59
+ }
60
+
61
+ .sidebar .nav-link {
62
+ font-size: 0.875rem;
63
+ font-weight: 500;
64
+ }
65
+
66
+ .sidebar .nav-link.active {
67
+ color: #2470dc;
68
+ }
69
+
70
+ .sidebar-heading {
71
+ font-size: 0.75rem;
72
+ }
73
+
74
+ /*
75
+ * Navbar
76
+ */
77
+ .navbar {
78
+ background-color: teal;
79
+ }
80
+
81
+ .navbar-brand {
82
+ padding-top: 0.75rem;
83
+ padding-bottom: 0.75rem;
84
+ /* background-color: rgba(0, 0, 0, .25);
85
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); */
86
+ }
87
+
88
+ .navbar .form-control {
89
+ padding: 0.75rem 1rem;
90
+ }
91
+
92
+ .bd-placeholder-img {
93
+ font-size: 1.125rem;
94
+ text-anchor: middle;
95
+ -webkit-user-select: none;
96
+ -moz-user-select: none;
97
+ user-select: none;
98
+ }
99
+
100
+ @media (min-width: 768px) {
101
+ .bd-placeholder-img-lg {
102
+ font-size: 3.5rem;
103
+ }
104
+ }
105
+
106
+ .b-example-divider {
107
+ width: 100%;
108
+ height: 3rem;
109
+ background-color: rgba(0, 0, 0, 0.1);
110
+ border: solid rgba(0, 0, 0, 0.15);
111
+ border-width: 1px 0;
112
+ box-shadow: inset 0 0.5em 1.5em rgba(0, 0, 0, 0.1),
113
+ inset 0 0.125em 0.5em rgba(0, 0, 0, 0.15);
114
+ }
115
+
116
+ .b-example-vr {
117
+ flex-shrink: 0;
118
+ width: 1.5rem;
119
+ height: 100vh;
120
+ }
121
+
122
+ .bi {
123
+ vertical-align: -0.125em;
124
+ fill: currentColor;
125
+ }
126
+
127
+ .nav-scroller {
128
+ position: relative;
129
+ z-index: 2;
130
+ height: 2.75rem;
131
+ overflow-y: hidden;
132
+ }
133
+
134
+ .nav-scroller .nav {
135
+ display: flex;
136
+ flex-wrap: nowrap;
137
+ padding-bottom: 1rem;
138
+ margin-top: -1px;
139
+ overflow-x: auto;
140
+ text-align: center;
141
+ white-space: nowrap;
142
+ -webkit-overflow-scrolling: touch;
143
+ }
144
+
145
+ .btn-bd-primary {
146
+ --bd-violet-bg: #712cf9;
147
+ --bd-violet-rgb: 112.520718, 44.062154, 249.437846;
148
+
149
+ --bs-btn-font-weight: 600;
150
+ --bs-btn-color: var(--bs-white);
151
+ --bs-btn-bg: var(--bd-violet-bg);
152
+ --bs-btn-border-color: var(--bd-violet-bg);
153
+ --bs-btn-hover-color: var(--bs-white);
154
+ --bs-btn-hover-bg: #6528e0;
155
+ --bs-btn-hover-border-color: #6528e0;
156
+ --bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
157
+ --bs-btn-active-color: var(--bs-btn-hover-color);
158
+ --bs-btn-active-bg: #5a23c8;
159
+ --bs-btn-active-border-color: #5a23c8;
160
+ }
161
+
162
+ .bd-mode-toggle {
163
+ z-index: 1500;
164
+ }
165
+
166
+ .bd-mode-toggle .dropdown-menu .active .bi {
167
+ display: block !important;
168
+ }
169
+
170
+ .back-to-top {
171
+ position: fixed;
172
+ bottom: 25px;
173
+ right: 25px;
174
+ display: none;
175
+ }
176
+
177
+ .leftsidebar {
178
+ height: 100%;
179
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
180
+ }
181
+
182
+ @media (min-width: 768px) {
183
+ .leftsidebar {
184
+ min-width: 15%;
185
+ }
186
+ }
187
+
188
+ @media (max-width: 768px) {
189
+ .leftsidebar {
190
+ max-width: 50%;
191
+ }
192
+ }
193
+
194
+ .bg-teal {
195
+ background-color: teal;
196
+ }
197
+ </style>
198
+
199
+ <script type="text/babel" data-presets="react" data-type="module">
200
+ //事件监听开始 通过修改localstorage实现跨页面事件监听
201
+ const emitter = mitt();
202
+ // 监听 localStorage 变化
203
+ window.addEventListener("storage", (event) => {
204
+ if (event.key === "event") {
205
+ const { type, data } = JSON.parse(event.newValue);
206
+ emitter.emit(type, data);
207
+ }
208
+ });
209
+ // 封装 emit 方法
210
+ const emitEvent = (type, data) => {
211
+ // 触发本地事件
212
+ emitter.emit(type, data);
213
+ const randomString = Math.random()
214
+ .toString(36)
215
+ .substring(2, 10); // 生成一个随机字符串确保event每次的值不一样,如果一样会不触发事件
216
+ const identity = `${Date.now()}-${randomString}`;
217
+ // 存储到 localStorage,以便其他页面能够接收到
218
+ localStorage.setItem(
219
+ "event",
220
+ JSON.stringify({ type, data, identity })
221
+ );
222
+ };
223
+
224
+ // 封装 on 方法
225
+ const onEvent = (type, callback) => {
226
+ emitter.on(type, callback);
227
+ };
228
+
229
+ // 封装 off 方法
230
+ const offEvent = (type, callback) => {
231
+ emitter.off(type, callback);
232
+ };
233
+ //事件监听结束
234
+
235
+
236
+ var settingStorage = localforage.createInstance({
237
+ name: "setting",
238
+ driver: localforage.LOCALSTORAGE
239
+ });
240
+ // settingStorage.setItem("category", { name: 'test', id: 1 });
241
+ // settingStorage.getItem('category').then(function (value) {
242
+ // console.log(value);
243
+ // }).catch(function (err) {
244
+ // console.log(err);
245
+ // });
246
+ // settingStorage.getItem('category', function (err, value) {
247
+ // console.log(value.name);
248
+ // });
249
+
250
+
251
+ const { createStore, combineReducers } = Redux;
252
+ // 从 localStorage 加载初始状态
253
+ const loadStateFromLocalStorage = () => {
254
+ try {
255
+ const serializedState = localStorage.getItem('settings');
256
+ if (serializedState === null) {
257
+ return {}; // 默认值
258
+ }
259
+ return JSON.parse(serializedState);
260
+ } catch (e) {
261
+ console.error("Could not load state from localStorage:", e);
262
+ return {}; // 默认值
263
+ }
264
+ };
265
+ // 保存状态到 localStorage
266
+ const saveStateToLocalStorage = (state) => {
267
+ try {
268
+ const serializedState = JSON.stringify(state);
269
+ localStorage.setItem('settings', serializedState);
270
+ } catch (e) {
271
+ console.error("Could not save state to localStorage:", e);
272
+ }
273
+ };
274
+
275
+ // 定义初始状态
276
+ const initialSettingsState = loadStateFromLocalStorage();
277
+ // 创建 settings Reducer
278
+ function settingsReducer(state = initialSettingsState, action) {
279
+ switch (action.type) {
280
+ case 'SAVE_SETTING':
281
+ return { ...state, ...action.payload };
282
+ default:
283
+ return state;
284
+ }
285
+ }
286
+
287
+ // 合并 Reducer(如果有多个)
288
+ const rootReducer = combineReducers({
289
+ settings: settingsReducer,
290
+ });
291
+
292
+ // 创建 Redux Store
293
+ const STORE = createStore(rootReducer);
294
+
295
+ // 订阅 Store 的变化,并将状态保存到 localStorage
296
+ STORE.subscribe(() => {
297
+ saveStateToLocalStorage(STORE.getState().settings);
298
+ });
299
+
300
+
301
+ //数据校验
302
+ // var ajv = new ajv7.default()
303
+ // const schema = {
304
+ // type: "object",
305
+ // properties: {
306
+ // foo: { type: "integer" },
307
+ // bar: { type: "string" }
308
+ // },
309
+ // required: ["foo"],
310
+ // additionalProperties: false
311
+ // }
312
+
313
+ // const validate = ajv.compile(schema)
314
+
315
+ // const data = {
316
+ // foo: 1,
317
+ // bar: "abc"
318
+ // }
319
+ // const valid = validate(data)
320
+ // if (!valid) console.log(validate.errors)
321
+
322
+
323
+ const bytesToSize = (bytes) => {
324
+ if (bytes === 0) return '0 B';
325
+ var k = 1024;
326
+ sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
327
+ i = Math.floor(Math.log(bytes) / Math.log(k));
328
+ return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
329
+ };
330
+ const formatDate = (date) => {
331
+ var d = new Date(date);
332
+ var year = d.getFullYear();
333
+ var month = d.getMonth() + 1;
334
+ var day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate();
335
+ var hour = d.getHours();
336
+ var minutes = d.getMinutes();
337
+ var seconds = d.getSeconds();
338
+ return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds;
339
+ };
340
+
341
+ let layerLoading = null;
342
+
343
+ const showLoading = () => {
344
+ const loadindex = layer.load(1);
345
+ layerLoading = loadindex;
346
+ }
347
+
348
+ const hideLoading = () => {
349
+ layer.close(layerLoading);
350
+ }
351
+
352
+
353
+ const { useState, useEffect, useRef } = React;
354
+ const { HashRouter, Route, Link, Switch, useLocation, useParams } = ReactRouterDOM;
355
+ const { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } = ReactQuery;
356
+ const queryClient = new QueryClient()
357
+ const {
358
+ Alert,
359
+ Badge,
360
+ Button,
361
+ ButtonGroup,
362
+ ButtonToolbar,
363
+ Collapse,
364
+ Col,
365
+ Container,
366
+ Form,
367
+ Image,
368
+ InputGroup,
369
+ ListGroup,
370
+ Modal,
371
+ Nav,
372
+ Navbar,
373
+ NavDropdown,
374
+ Offcanvas,
375
+ Pagination,
376
+ Row,
377
+ Table,
378
+ } = ReactBootstrap;
379
+ //注意修改js文件后需要直接访问js以更新浏览器缓存
380
+
381
+ // 表格组件
382
+ const DataTable = ({ data, columns }) => {
383
+ return (
384
+ <Table responsive bordered>
385
+ <thead>
386
+ <tr className="text-center">
387
+ {columns.map((column, index) => (
388
+ <th key={index}>{column.title}</th>
389
+ ))}
390
+ </tr>
391
+ </thead>
392
+ <tbody>
393
+ {data.map((row, rowIndex) => (
394
+ <tr key={rowIndex} className="text-center">
395
+ {columns.map((column, colIndex) => (
396
+ <td key={colIndex}>
397
+ {/* 调用渲染方法,如果没有定义,则直接显示数据 */}
398
+ {column.render
399
+ ? column.render(row)
400
+ : row[column.dataIndex]}
401
+ </td>
402
+ ))}
403
+ </tr>
404
+ ))}
405
+ </tbody>
406
+ </Table>
407
+ );
408
+ };
409
+ //分页组件
410
+ const Paginate = (props) => {
411
+ const page = props.page;
412
+ const pageCount = Math.ceil(
413
+ props.totalCount / props.itemsPerPage
414
+ );
415
+
416
+ const SelectItems = () => {
417
+ const pageNumbers = Array.from(
418
+ { length: pageCount },
419
+ (_, i) => i + 1
420
+ );
421
+ return (
422
+ <select
423
+ className="form-select me-2"
424
+ style={{ width: "auto" }}
425
+ onChange={(e) => {
426
+ props.onClick(parseInt(e.target.value));
427
+ }}
428
+ >
429
+ {pageNumbers.map((number) => {
430
+ const selected = number === page ? true : false;
431
+ return (
432
+ <option
433
+ key={number}
434
+ value={number}
435
+ selected={selected}
436
+ >
437
+ {number}
438
+ </option>
439
+ );
440
+ })}
441
+ </select>
442
+ );
443
+ };
444
+ return (
445
+ <div className="d-flex justify-content-center align-items-baseline">
446
+ <SelectItems />
447
+ <span className="text-info me-2">
448
+ {page}/{pageCount}
449
+ </span>
450
+ <Pagination>
451
+ {pageCount > 1 && page > 1 && (
452
+ <Pagination.First
453
+ onClick={() => {
454
+ props.onClick(1);
455
+ }}
456
+ />
457
+ )}
458
+ {pageCount > 1 && page > 1 && (
459
+ <Pagination.Prev
460
+ onClick={() => {
461
+ props.onClick(page - 1);
462
+ }}
463
+ />
464
+ )}
465
+ {pageCount > 1 && page < pageCount && (
466
+ <Pagination.Next
467
+ onClick={() => {
468
+ props.onClick(page + 1);
469
+ }}
470
+ />
471
+ )}
472
+ {pageCount > 1 && page < pageCount && (
473
+ <Pagination.Last
474
+ onClick={() => {
475
+ props.onClick(pageCount);
476
+ }}
477
+ />
478
+ )}
479
+ </Pagination>
480
+ </div>
481
+ );
482
+ };
483
+ //图标组件
484
+ const Icon = (props) => {
485
+ return (
486
+ <span
487
+ onClick={props.onClick}
488
+ className={`mdi mdi-${props.icon} fs-${props.size} ${props.className}`}
489
+ ></span>
490
+ );
491
+ };
492
+ //按钮图标组件
493
+ const IconButton = (props) => {
494
+ return (
495
+ <Button
496
+ variant="success"
497
+ onClick={props.onClick}
498
+ className={props.className}
499
+ >
500
+ <span
501
+ className={`mdi mdi-${props.icon} fs-${props.iconSize} ${props.iconClassName}`}
502
+ ></span>
503
+ {props.text}
504
+ </Button>
505
+ );
506
+ };
507
+ //设置框
508
+ const SettingModal = (props) => {
509
+ const settings = [
510
+ { "alist": [{ "label": "Alist地址", "key": "alist_host", "show": true }, { "label": "Alist令牌", "key": "alist_token", "show": false }] },
511
+ { "github": [{ "label": "Actions地址", "key": "github_host", "show": true }, { "label": "Github令牌", "key": "github_token", "show": false }] },
512
+ { "directus": [{ "label": "Directus地址", "key": "directus_host", "show": true }, { "label": "Directus令牌", "key": "directus_token", "show": false }] }
513
+ ]
514
+ const [setting, setSetting] = useState({});
515
+ // useEffect(() => {
516
+ // localStorage.setItem('settings', JSON.stringify(setting));
517
+ // }, [setting]);
518
+
519
+ const loadSetting = () => {
520
+ const storedSettings = STORE.getState().settings;
521
+ if (storedSettings) {
522
+ setSetting(storedSettings);
523
+ }
524
+ }
525
+ const saveSetting = () => {
526
+ STORE.dispatch({ type: 'SAVE_SETTING', payload: setting })
527
+ //localStorage.setItem('settings', JSON.stringify(setting));
528
+ }
529
+ return (
530
+ <Modal show={props.show} onHide={props.onHide} onShow={loadSetting}>
531
+ <Modal.Header closeButton onHide={props.onHide}>
532
+ <Modal.Title>设置</Modal.Title>
533
+ </Modal.Header>
534
+ <Modal.Body>
535
+ <Form>
536
+ <ListGroup>
537
+ {settings.map((value, index) => {
538
+ const key = Object.keys(value)[0];
539
+ const items = value[key];
540
+ return (<ListGroup.Item>
541
+ {items.map((setting_item) => {
542
+ return (
543
+ <Form.Group as={Row} className="mb-3">
544
+ <Form.Label column sm="3">
545
+ {setting_item.label}
546
+ </Form.Label>
547
+ <Col sm="9">
548
+ <Form.Control type={setting_item.show ? "input" : "password"} value={setting[setting_item.key]} name={setting_item.key} placeholder={setting_item.label} onChange={(e) => { setSetting({ ...setting, [setting_item.key]: e.target.value }) }} />
549
+ </Col>
550
+ </Form.Group>
551
+ )
552
+ })}
553
+ </ListGroup.Item>)
554
+ })}
555
+ </ListGroup>
556
+ </Form>
557
+ </Modal.Body>
558
+ <Modal.Footer className="justify-content-between">
559
+ <Button
560
+ variant="secondary"
561
+ onClick={() => {
562
+ props.onHide();
563
+ }}
564
+ >
565
+ 关闭
566
+ </Button>
567
+ <Button
568
+ variant="primary"
569
+ onClick={() => {
570
+ saveSetting();
571
+ props.onHide();
572
+ //props.onSave();
573
+ }}
574
+ >
575
+ 保存
576
+ </Button>
577
+ </Modal.Footer>
578
+ </Modal>
579
+ );
580
+ };
581
+
582
+ //axios封装开始
583
+ const useAxios = () => {
584
+ const [response, setResponse] = useState(null);
585
+ const [error, setError] = useState("");
586
+ const [loading, setLoading] = useState(false);
587
+
588
+ // Create an Axios instance
589
+ const axiosInstance = axios.create({});
590
+
591
+ // Set up request and response interceptors
592
+ axiosInstance.interceptors.request.use(
593
+ (config) => {
594
+ // Log or modify request here
595
+ //console.log("Sending request to:", config.url);
596
+ return config;
597
+ },
598
+ (error) => {
599
+ // Handle request error here
600
+ return Promise.reject(error);
601
+ }
602
+ );
603
+
604
+ axiosInstance.interceptors.response.use(
605
+ (response) => {
606
+ // Log or modify response here
607
+ //console.log("Received response from:", response.config.url);
608
+ return response;
609
+ },
610
+ (error) => {
611
+ // Handle response error here
612
+ return Promise.reject(error);
613
+ }
614
+ );
615
+
616
+ useEffect(() => {
617
+ const source = axios.CancelToken.source();
618
+ return () => {
619
+ // Cancel the request when the component unmounts
620
+ source.cancel(
621
+ "组件被卸载: 请求取消."
622
+ );
623
+ };
624
+ }, []);
625
+
626
+ // Making the API call with cancellation support
627
+ const fetchData = async ({ url, method, data, headers }) => {
628
+ setLoading(true);
629
+ try {
630
+ const result = await axiosInstance({
631
+ url,
632
+ method,
633
+ headers: headers ? headers : {},
634
+ data:
635
+ method.toLowerCase() === "get"
636
+ ? undefined
637
+ : data,
638
+ params:
639
+ method.toLowerCase() === "get"
640
+ ? data
641
+ : undefined,
642
+ cancelToken: axios.CancelToken.source().token,
643
+ });
644
+ setResponse(result.data);
645
+ } catch (error) {
646
+ if (axios.isCancel(error)) {
647
+ console.log("Request cancelled", error.message);
648
+ } else {
649
+ setError(
650
+ error.response
651
+ ? error.response.data
652
+ : error.message
653
+ );
654
+ }
655
+ } finally {
656
+ setLoading(false);
657
+ }
658
+ };
659
+ return [response, error, loading, fetchData];
660
+ };
661
+ //axios封闭结束
662
+
663
+ //API定义开始
664
+ const getFiles = () => {
665
+ const [response, error, loading, fetchData] = useAxios();
666
+
667
+ const fetchDataByPage = async (setting, query) => {
668
+ var host = setting.alist_host;
669
+ if (!host.endsWith("/")) {
670
+ host = host + '/'
671
+ }
672
+ fetchData({
673
+ url: host + 'api/fs/list',
674
+ method: "POST",
675
+ data: query,
676
+ headers: {
677
+ 'Authorization': setting.alist_token,
678
+ 'Content-Type': 'application/json'
679
+ },
680
+ });
681
+ };
682
+ return [response, error, loading, fetchDataByPage];
683
+ };
684
+ //API定义结束
685
+
686
+ const Layout = ({ children }) => {
687
+ useEffect(() => {
688
+ // 组件挂载时执行的代码(相当于 componentDidMount)
689
+ }, []); // 空数组表示只在挂载和卸载时执行
690
+
691
+ const [showSideBar, setShowSideBar] = useState(false);
692
+ const handleSidebarClose = () => setShowSideBar(false);
693
+ const handleSidebarShow = () => setShowSideBar(true);
694
+ const toggleSidebarShow = () => {
695
+ setShowSideBar(!showSideBar);
696
+ };
697
+
698
+ const [setting, setSetting] = useState(false);
699
+
700
+ return (
701
+ <div className="pb-5">
702
+ <header className="sticky-top">
703
+ <Navbar expand="md">
704
+ <Container fluid>
705
+ <div>
706
+ <Navbar.Toggle
707
+ className="shadow-none border-0"
708
+ onClick={handleSidebarShow}
709
+ children={
710
+ <Icon
711
+ icon="menu"
712
+ size="3"
713
+ className="text-white"
714
+ />
715
+ }
716
+ />
717
+ <Navbar.Brand
718
+ as={Link}
719
+ to="/"
720
+ className="text-white"
721
+ >
722
+ 离线管理
723
+ </Navbar.Brand>
724
+ </div>
725
+ <div className="d-flex">
726
+ <LocalTasks />
727
+ <Button
728
+ style={{
729
+ backgroundColor: "transparent",
730
+ }}
731
+ className="nav-link btn"
732
+ onClick={() => {
733
+ setSetting(true)
734
+ }}
735
+ children={
736
+ <Icon
737
+ icon="dots-vertical"
738
+ size="3"
739
+ className="text-white"
740
+ />
741
+ }
742
+ ></Button>
743
+ <SettingModal
744
+ show={setting}
745
+ onHide={() => {
746
+ setSetting(false);
747
+ }}
748
+ />
749
+ </div>
750
+ </Container>
751
+ </Navbar>
752
+ </header>
753
+ <Container fluid>
754
+ <Row style={{ minHeight: "100vh" }}>
755
+ <Col
756
+ md="2"
757
+ lg="2"
758
+ xl="2"
759
+ className="ps-0 d-none d-md-block"
760
+ >
761
+ <Offcanvas
762
+ className="leftsidebar h-100 bg-light"
763
+ show={showSideBar}
764
+ onHide={handleSidebarClose}
765
+ placement="start"
766
+ responsive="md"
767
+ >
768
+ <Offcanvas.Header
769
+ className="py-2 border-bottom"
770
+ closeButton
771
+ >
772
+ <Offcanvas.Title>
773
+ 离线任务
774
+ </Offcanvas.Title>
775
+ </Offcanvas.Header>
776
+ <Offcanvas.Body className="p-0">
777
+ <Container fluid className="p-0">
778
+ <Nav
779
+ activeKey="1"
780
+ className="flex-column"
781
+ >
782
+ <Nav.Link
783
+ as={Link}
784
+ className="nav-link text-dark"
785
+ to="/"
786
+ onClick={
787
+ handleSidebarClose
788
+ }
789
+ >
790
+ <Icon
791
+ icon="plus"
792
+ size="6"
793
+ className="me-2"
794
+ />
795
+ 离线管理
796
+ </Nav.Link>
797
+ </Nav>
798
+ </Container>
799
+ </Offcanvas.Body>
800
+ </Offcanvas>
801
+ </Col>
802
+
803
+ <Col xs="12" sm="12" md="10" lg="10" xl="10">
804
+ <main>
805
+ <Container fluid className="pt-2 px-0">
806
+ {children}
807
+ </Container>
808
+ </main>
809
+ </Col>
810
+ </Row>
811
+ </Container>
812
+ </div>
813
+ );
814
+ };
815
+ const Home = () => {
816
+ const location = useLocation();
817
+ const { id } = useParams();
818
+ return (
819
+ <div>
820
+ <div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
821
+ <label className="fs-3">Home</label>
822
+ <ButtonToolbar
823
+ aria-label="文件列表"
824
+ className="bg-teal rounded"
825
+ >
826
+ <ButtonGroup className="bg-teal">
827
+ <IconButton
828
+ onClick={() => {
829
+ alert("test")
830
+ }}
831
+ text="刷新"
832
+ className="bg-teal border-0"
833
+ icon="reload"
834
+ iconClassName="me-1 text-white"
835
+ iconSize="6"
836
+ />
837
+ <IconButton
838
+ onClick={() => {
839
+ alert("hello");
840
+ }}
841
+ text="删除"
842
+ className="bg-teal border-0"
843
+ icon="delete-outline"
844
+ iconClassName="me-1 text-white"
845
+ iconSize="6"
846
+ />
847
+ </ButtonGroup>
848
+ </ButtonToolbar>
849
+ </div>
850
+ <Container fluid className="p-2"></Container>
851
+ </div>
852
+ );
853
+ };
854
+
855
+
856
+
857
+ const LocalTasks = () => {
858
+ const [show, setShow] = useState(false);
859
+ const handleClose = () => setShow(false);
860
+ const handleShow = () => setShow(true);
861
+ const [downloads, setDownloads] = useState([])
862
+ const [addDownloadObject, setAddDownloadObject] = useState({})
863
+ const setting = STORE.getState().settings;
864
+ const columns = [
865
+ { title: "文件名称", dataIndex: "name" },
866
+ { title: "大小", dataIndex: "size", render: (row) => (bytesToSize(row.size)) },
867
+ { title: "日期", dataIndex: "created", render: (row) => (formatDate(row.created)) },
868
+ {
869
+ title: "操作",
870
+ dataIndex: "name",
871
+ render: (row) => (
872
+ <div>
873
+ <Icon
874
+ icon="delete-outline"
875
+ size="6"
876
+ className="me-2"
877
+ onClick={() => {
878
+ layer.confirm('确定删除?该操作无法撤销!!!', { icon: 3 }, function (index) {
879
+ setDownloads(
880
+ downloads.filter(a =>
881
+ a.name !== row.name
882
+ )
883
+ );
884
+ layer.close(index);
885
+ }, function () {
886
+
887
+ });
888
+ }}
889
+ />
890
+ <Icon
891
+ icon="pencil-outline"
892
+ size="6"
893
+ className="me-2"
894
+ onClick={() => {
895
+ layer.prompt({
896
+ title: '输入文件名称,并确认',
897
+ formType: 0,
898
+ value: row.name,
899
+ success: function (layero, index) {
900
+ $("div[aria-modal]").eq(0).removeAttr("tabindex");//解决弹出窗的input无法获取焦点的问题
901
+ },
902
+ end: function (layero, index) {
903
+ $("div[aria-modal]").eq(0).attr("tabindex", -1).focus();//再把焦点还回去
904
+ }
905
+ }, function (value, index) {
906
+ const newDownloads = downloads.map(downloadItem => {
907
+ if (downloadItem.name === row.name) {
908
+ return {
909
+ ...downloadItem,
910
+ name: value
911
+ };
912
+ }
913
+ return downloadItem;
914
+ });
915
+ setDownloads(newDownloads);
916
+ layer.close(index);
917
+ });
918
+ }}
919
+ />
920
+ </div>
921
+ ),
922
+ },
923
+ ];
924
+
925
+
926
+ const { mutateAsync: localTaskdMutation } = useMutation({
927
+ mutationKey: ["get-download"],
928
+ mutationFn: async () => {
929
+ showLoading();
930
+ var host = setting.directus_host;
931
+ if (!host.endsWith("/")) {
932
+ host = host + '/'
933
+ }
934
+ var url = host + 'items/task';
935
+ const tasks = downloads.map(task => {
936
+ return { url: task.url + '##' + task.name }
937
+ })
938
+ return await axios.post(url, tasks, {
939
+ headers: {
940
+ 'Authorization': "Bearer " + setting.directus_token,
941
+ 'Content-Type': 'application/json'
942
+ },
943
+ })
944
+ },
945
+ onSuccess: async (data, variables, context) => {
946
+ hideLoading();
947
+ layer.msg('任务添加成功', { time: 2000, icon: 6 });
948
+ },
949
+ onError: () => {
950
+ hideLoading();
951
+ layer.msg('任务添加失败', { time: 2000, icon: 5 });
952
+ }
953
+ })
954
+
955
+
956
+
957
+ const addDowload = (fileinfo) => {
958
+ const file = fileinfo.data.data;
959
+ const download = { name: file.name, size: file.size, url: file.raw_url, created: file.created }
960
+ setAddDownloadObject(download)
961
+ }
962
+ useEffect(() => {
963
+ if (addDownloadObject && ('name' in addDownloadObject)) {
964
+ setDownloads([...downloads, addDownloadObject])
965
+ setAddDownloadObject({})
966
+ }
967
+ }, [addDownloadObject]);
968
+ useEffect(() => {
969
+ onEvent("addDownload", addDowload)
970
+ settingStorage.getItem('downloads').then(function (value) {
971
+ if (value) {
972
+ setDownloads(value)
973
+ }
974
+ }).catch(function (err) {
975
+ console.log(err)
976
+ });
977
+ }, []);
978
+ useEffect(() => {
979
+ settingStorage.setItem('downloads', downloads)
980
+ }, [downloads]);
981
+
982
+
983
+ if (downloads.length > 0) {
984
+ return (
985
+ <div>
986
+ <Button
987
+ style={{
988
+ backgroundColor: "transparent",
989
+ }}
990
+ className="nav-link btn"
991
+ onClick={handleShow}
992
+ children={
993
+ <span>
994
+ <Icon
995
+ icon="download"
996
+ size="3"
997
+ className="text-white"
998
+ />
999
+ <Badge bg="danger" style={{ top: '-15px', left: '-10px' }}>{downloads.length}</Badge>
1000
+ </span>
1001
+ }
1002
+ ></Button>
1003
+
1004
+
1005
+ <Modal show={show} onHide={handleClose}>
1006
+ <Modal.Header closeButton>
1007
+ <Modal.Title>本地下载任务</Modal.Title>
1008
+ </Modal.Header>
1009
+ <Modal.Body>
1010
+ {downloads && (
1011
+ <DataTable data={downloads ? downloads : []} columns={columns} />
1012
+ )}
1013
+ </Modal.Body>
1014
+ <Modal.Footer className="justify-content-between">
1015
+
1016
+ <ButtonGroup>
1017
+ <Button variant="primary" onClick={async () => { await localTaskdMutation() }}>
1018
+ 添加转存
1019
+ </Button>
1020
+ </ButtonGroup>
1021
+
1022
+ <ButtonGroup>
1023
+ <Button variant="danger" onClick={() => {
1024
+ layer.confirm('确定删除?该操作无法撤销!!!', { icon: 3 }, function (index) {
1025
+ setDownloads([]);
1026
+ layer.close(index);
1027
+ }, function () {
1028
+
1029
+ });
1030
+ }}>
1031
+ 清空
1032
+ </Button>
1033
+ <Button variant="primary" onClick={handleClose}>
1034
+ 关闭
1035
+ </Button>
1036
+ </ButtonGroup>
1037
+
1038
+
1039
+ </Modal.Footer>
1040
+ </Modal>
1041
+
1042
+ </div >
1043
+ );
1044
+ }
1045
+ }
1046
+
1047
+
1048
+ App = () => {
1049
+ const [open, setOpen] = useState(false);
1050
+ const [reload, setReload] = useState(false);
1051
+ const [response, error, loading, fetchDataByPage] = getFiles();
1052
+ const { folder } = useParams();
1053
+ const location = useLocation();
1054
+ const [path, setPath] = useState(location.pathname);
1055
+ const [page, setPage] = useState(1);
1056
+ const [query, setQuery] = useState({ "path": path, "password": "", "page": page, "per_page": 0, "refresh": true });
1057
+ const setting = STORE.getState().settings;
1058
+
1059
+
1060
+ //const queryClient = useQueryClient()
1061
+ // Queries
1062
+ //const { data, error, isLoading, refetch } = useQuery({
1063
+ // queryKey: ['test'], queryFn: () => axios.get("")
1064
+ //})
1065
+
1066
+ const { data: fileData, mutateAsync: downloadMutation } = useMutation({
1067
+ mutationKey: ["get-download"],
1068
+ mutationFn: async (fileinfo) => {
1069
+ showLoading();
1070
+ var host = setting.alist_host;
1071
+ if (!host.endsWith("/")) {
1072
+ host = host + '/'
1073
+ }
1074
+ var url = host + 'api/fs/get';
1075
+ return await axios.post(url, fileinfo, {
1076
+ headers: {
1077
+ 'Authorization': setting.alist_token,
1078
+ 'Content-Type': 'application/json'
1079
+ },
1080
+ })
1081
+ },
1082
+ onSuccess: async (data, variables, context) => {
1083
+ hideLoading();
1084
+ },
1085
+ onError: () => {
1086
+ hideLoading();
1087
+ }
1088
+ })
1089
+
1090
+ useEffect(() => {
1091
+ if (fileData) {
1092
+ emitEvent("addDownload", fileData)
1093
+ }
1094
+ }, [fileData]);
1095
+
1096
+
1097
+ const columns = [
1098
+ { title: "文件名称", dataIndex: "name" },
1099
+ { title: "大小", dataIndex: "size", render: (row) => (bytesToSize(row.size)) },
1100
+ { title: "日期", dataIndex: "created", render: (row) => (formatDate(row.created)) },
1101
+ {
1102
+ title: "操作",
1103
+ dataIndex: "name",
1104
+ render: (row) => (
1105
+ row.is_dir ? <Nav.Link
1106
+ as={Link}
1107
+ className="nav-link text-dark"
1108
+ to={path + row.name + '/'}
1109
+ target="_blank"
1110
+ >
1111
+ <Icon
1112
+ icon="open-in-new"
1113
+ size="6"
1114
+ className="me-2"
1115
+ />
1116
+ </Nav.Link> :
1117
+ <Icon
1118
+ icon="download-outline"
1119
+ size="6"
1120
+ className="me-2"
1121
+ onClick={async () => {
1122
+ let data = { "path": decodeURIComponent(path + row.name), "password": "" }
1123
+ await downloadMutation(data);
1124
+ }}
1125
+ />
1126
+ ),
1127
+ },
1128
+ ];
1129
+ useEffect(() => {
1130
+ if (!setting.alist_token || setting.alist_token.length < 5) {
1131
+ layer.alert("请先正确配置Alsit的令牌", { icon: 5 });
1132
+ return
1133
+ }
1134
+ fetchDataByPage(setting, query);
1135
+ return () => { }
1136
+ }, [reload, query]);
1137
+
1138
+
1139
+ const forceUpdate = () => {
1140
+ setReload((pre) => !pre);
1141
+ };
1142
+
1143
+ return (
1144
+ <div>
1145
+ <div className="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
1146
+ <label className="fs-3">文件列表</label>
1147
+ <ButtonToolbar
1148
+ aria-label="功能区"
1149
+ className="bg-teal rounded"
1150
+ >
1151
+ <ButtonGroup className="bg-teal">
1152
+ <IconButton
1153
+ onClick={() => {
1154
+ emitEvent("test", { a: 'b' })
1155
+ }}
1156
+ text="刷新"
1157
+ className="bg-teal border-0"
1158
+ icon="reload"
1159
+ iconClassName="me-1 text-white"
1160
+ iconSize="6"
1161
+ />
1162
+ </ButtonGroup>
1163
+ </ButtonToolbar>
1164
+ </div>
1165
+ <Container fluid className="p-2">
1166
+ {error && (
1167
+ <div className="text-center text-danger">
1168
+ {error}
1169
+ </div>
1170
+ )}
1171
+ {(loading) && (
1172
+ <div className="text-center text-success">
1173
+ 正在努力加载中......
1174
+ </div>
1175
+ )}
1176
+ {response && (
1177
+ <DataTable data={response.data.content ? response.data.content : []} columns={columns} />
1178
+ )}
1179
+ </Container>
1180
+ </div>
1181
+ );
1182
+ };
1183
+
1184
+ const container = document.getElementById("root");
1185
+ const root = ReactDOM.createRoot(container);
1186
+ root.render(
1187
+ <QueryClientProvider client={queryClient}>
1188
+ <HashRouter>
1189
+ <Route path="/:path?">
1190
+ <Layout>
1191
+ <Switch>
1192
+ <Route path="/" exact component={App} />
1193
+ <Route path="/:folder?" component={App} />
1194
+ </Switch>
1195
+ </Layout>
1196
+ </Route>
1197
+ </HashRouter>
1198
+ </QueryClientProvider>
1199
+ );
1200
+
1201
+ $(document).ready(function () {
1202
+ $(window).scroll(function () {
1203
+ if ($(this).scrollTop() > 50) {
1204
+ $("#back-to-top").fadeIn();
1205
+ } else {
1206
+ $("#back-to-top").fadeOut();
1207
+ }
1208
+ });
1209
+ // scroll body to 0px on click
1210
+ $("#back-to-top").click(function () {
1211
+ $("body,html").animate(
1212
+ {
1213
+ scrollTop: 0,
1214
+ },
1215
+ 400
1216
+ );
1217
+ return false;
1218
+ });
1219
+ });
1220
+ </script>
1221
+ </body>
1222
+
1223
+ </html>