Compare commits
34 Commits
921570f229
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84cbf588e0 | ||
|
|
6d3cb4f4b3 | ||
|
|
b8447640df | ||
|
|
6f155f8238 | ||
|
|
e77b0d7978 | ||
|
|
59d39fbc11 | ||
|
|
18d98a25f9 | ||
|
|
15992bd1a8 | ||
|
|
d05cde8068 | ||
|
|
96aeebeee5 | ||
|
|
a0e791f2eb | ||
|
|
ea27514eac | ||
|
|
76671ed625 | ||
|
|
7519a81c88 | ||
|
|
1ffe6a8a52 | ||
|
|
ac2d49a400 | ||
|
|
2f4930b39a | ||
|
|
f1c5f86553 | ||
|
|
64e7385a24 | ||
|
|
c40a899d15 | ||
|
|
abd4aaac7c | ||
|
|
b808feb1fd | ||
|
|
f7ccd807a1 | ||
|
|
3a21802d76 | ||
|
|
6dc3943b70 | ||
|
|
d3c6876b22 | ||
|
|
93c15be7c4 | ||
|
|
c72540fe3a | ||
|
|
651fc084fa | ||
|
|
80718dfc8b | ||
|
|
d18177452b | ||
|
|
7b9c686558 | ||
|
|
a353222f16 | ||
|
|
c1f1fc25ee |
63
index.html
63
index.html
@@ -1,6 +1,12 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
Using LeonPan open source project.
|
||||
Powered by LeonCloud
|
||||
awa
|
||||
-->
|
||||
<link href="https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Latin/MiSansLatin-Medium.min.css" rel="stylesheet">
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="{pwa_small_icon}" sizes="64x64" />
|
||||
<meta
|
||||
@@ -40,43 +46,39 @@
|
||||
transform: scale(0.8);
|
||||
animation: fadeIn 0.6s ease-out 0.3s forwards;
|
||||
}
|
||||
#app-loader .spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
#app-loader .progress-container {
|
||||
width: 200px;
|
||||
height: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
animation: fadeIn 0.6s ease-out 0.3s forwards;
|
||||
}
|
||||
#app-loader .spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
#app-loader .spinner svg {
|
||||
display: block;
|
||||
}
|
||||
#app-loader .spinner .stroke {
|
||||
stroke: var(--defaultThemeColor);
|
||||
stroke-linecap: round;
|
||||
animation: spinDash 1.4s ease-in-out infinite;
|
||||
}
|
||||
#app-loader .spinner .background {
|
||||
stroke: rgba(0, 0, 0, 0.1)
|
||||
#app-loader .progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--defaultThemeColor);
|
||||
border-radius: 2px;
|
||||
animation: linearProgress 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes spinDash {
|
||||
@keyframes linearProgress {
|
||||
0% {
|
||||
stroke-dasharray: 1px, 200px;
|
||||
stroke-dashoffset: 0;
|
||||
transform: translateX(-100%);
|
||||
width: 50%;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 100px, 200px;
|
||||
stroke-dashoffset: -15px;
|
||||
transform: translateX(100%);
|
||||
width: 50%;
|
||||
}
|
||||
51% {
|
||||
transform: translateX(100%);
|
||||
width: 50%;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 1px, 200px;
|
||||
stroke-dashoffset: -126px;
|
||||
transform: translateX(-100%);
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
@@ -91,11 +93,8 @@
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="app-loader">
|
||||
<div class="logo"></div>
|
||||
<div class="spinner">
|
||||
<svg viewBox="22 22 44 44">
|
||||
<circle class="background" cx="44" cy="44" r="20" fill="none" stroke-width="4"></circle>
|
||||
<circle class="stroke" cx="44" cy="44" r="20" fill="none" stroke-width="4"></circle>
|
||||
</svg>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script async type="module" src="/src/main.tsx"></script>
|
||||
|
||||
@@ -404,6 +404,7 @@
|
||||
"deleteViewSetting": "Delete view setting"
|
||||
},
|
||||
"modals": {
|
||||
"scanQRCodeToAccess": "Scan QR Code to Access",
|
||||
"includePasswordInShareLink": "Include password in share link",
|
||||
"includePasswordInShareLinkDes": "If selected, password will be included in the share link, and no password is required when accessing the share link.",
|
||||
"showFileName": "Show file name",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"totalFilesAndFolders": "Files and Folders",
|
||||
"shareLinks": "Share links",
|
||||
"totalBlobs": "Blobs",
|
||||
"storagePolicies": "Storage Policies Usage",
|
||||
"noStoragePolicies": "No storage policies available",
|
||||
"homepage": "Homepage",
|
||||
"github": "GitHub",
|
||||
"documents": "Documents",
|
||||
|
||||
@@ -404,6 +404,7 @@
|
||||
"deleteViewSetting": "删除视图设置"
|
||||
},
|
||||
"modals": {
|
||||
"scanQRCodeToAccess": "扫描二维码访问分享链接",
|
||||
"includePasswordInShareLink": "在链接中包含密码",
|
||||
"includePasswordInShareLinkDes": "勾选后,分享链接中会包含密码,通过此链接访问时不需要再输入密码。",
|
||||
"showFileName": "显示文件名",
|
||||
|
||||
@@ -102,5 +102,7 @@
|
||||
"50005": "内部错误 ({{message}})",
|
||||
"50010": "目标节点不可用",
|
||||
"50011": "文件元信息查询失败"
|
||||
}
|
||||
},
|
||||
"announcement": "公告",
|
||||
"dontShowAgain": "不再显示"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"totalFilesAndFolders": "文件与目录",
|
||||
"shareLinks": "分享链接",
|
||||
"totalBlobs": "文件 Blob",
|
||||
"storagePolicies": "存储策略空间使用",
|
||||
"noStoragePolicies": "暂无存储策略",
|
||||
"homepage": "主页",
|
||||
"github": "GitHub",
|
||||
"documents": "文档",
|
||||
@@ -113,6 +115,8 @@
|
||||
"retryDelayDes": "任务重试的初始延迟时间(秒)。"
|
||||
},
|
||||
"settings": {
|
||||
"announcementEnabled": "启用公告",
|
||||
"announcementEnabledDes": "启用后,用户登录时会看到公告弹窗",
|
||||
"headlessFooter": "登录会话页面底部",
|
||||
"headlessFooterDes": "用户登录、注册、回调结果等页面底部展示的自定义 HTML 内容。",
|
||||
"headlessBottom": "登录会话页面主体底部",
|
||||
|
||||
BIN
public/static/img/logo.png
Normal file
BIN
public/static/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
BIN
public/static/img/logolong.png
Normal file
BIN
public/static/img/logolong.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
46
src/App.tsx
46
src/App.tsx
@@ -15,6 +15,7 @@ import { GrowDialogTransition } from "./component/FileManager/Search/SearchPopup
|
||||
import Warning from "./component/Icons/Warning.tsx";
|
||||
import { useAppSelector } from "./redux/hooks.ts";
|
||||
import { changeThemeColor } from "./util";
|
||||
import AnnouncementDialog from "./component/Common/AnnouncementDialog.tsx";
|
||||
|
||||
export const applyThemeWithOverrides = (themeConfig: ThemeOptions): ThemeOptions => {
|
||||
return {
|
||||
@@ -23,11 +24,20 @@ export const applyThemeWithOverrides = (themeConfig: ThemeOptions): ThemeOptions
|
||||
...themeConfig.shape,
|
||||
borderRadius: 4,
|
||||
},
|
||||
typography: {
|
||||
...themeConfig.typography,
|
||||
fontFamily:
|
||||
'"MiSans", -apple-system, BlinkMacSystemFont, "ZCOOL QingKe HuangYou", "Inter", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
overscrollBehavior: "none",
|
||||
fontFamily: '"MiSans", sans-serif',
|
||||
},
|
||||
html: {
|
||||
fontFamily: '"MiSans", sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -52,6 +62,35 @@ export const applyThemeWithOverrides = (themeConfig: ThemeOptions): ThemeOptions
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
borderRadius: 4,
|
||||
border: "1px solid transparent",
|
||||
transition: "all 0.2s ease",
|
||||
minWidth: 48,
|
||||
minHeight: 32,
|
||||
padding: "4px 12px",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.04)",
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "rgba(25, 118, 210, 0.1)",
|
||||
color: "rgb(25, 118, 210)",
|
||||
borderColor: "rgba(25, 118, 210, 0.3)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(25, 118, 210, 0.15)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiToggleButtonGroup: {
|
||||
styleOverrides: {
|
||||
grouped: {
|
||||
margin: 2,
|
||||
border: 0,
|
||||
borderRadius: 4,
|
||||
"&.Mui-disabled": {
|
||||
border: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -63,6 +102,12 @@ export const applyThemeWithOverrides = (themeConfig: ThemeOptions): ThemeOptions
|
||||
},
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
MuiButtonBase: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
@@ -374,6 +419,7 @@ const AppContent = () => {
|
||||
}}
|
||||
>
|
||||
<GlobalDialogs />
|
||||
<AnnouncementDialog />
|
||||
<Outlet />
|
||||
</SnackbarProvider>
|
||||
</>
|
||||
|
||||
@@ -20,10 +20,19 @@ export interface Version {
|
||||
commit: string;
|
||||
}
|
||||
|
||||
export interface StoragePolicySpace {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
used: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface HomepageSummary {
|
||||
metrics_summary?: MetricsSummary;
|
||||
site_urls: string[];
|
||||
version: Version;
|
||||
storage_policies?: StoragePolicySpace[];
|
||||
}
|
||||
|
||||
export interface ManualRefreshLicenseService {
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { CustomProps, ViewerGroup } from "./explorer.ts";
|
||||
import { User } from "./user.ts";
|
||||
|
||||
export interface CustomNavItem {
|
||||
title?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
url: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
export interface CustomHTML {
|
||||
headlessFooter?: string;
|
||||
headlessBottom?: string;
|
||||
sidebarBottom?: string;
|
||||
}
|
||||
|
||||
export enum CaptchaType {
|
||||
NORMAL = "normal",
|
||||
RECAPTCHA = "recaptcha",
|
||||
@@ -46,21 +60,11 @@ export interface SiteConfig {
|
||||
custom_nav_items?: CustomNavItem[];
|
||||
custom_html?: CustomHTML;
|
||||
thumb_exts?: string[];
|
||||
announcement?: string;
|
||||
announcement_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CaptchaResponse {
|
||||
ticket: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface CustomNavItem {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface CustomHTML {
|
||||
headless_footer?: string;
|
||||
headless_bottom?: string;
|
||||
sidebar_bottom?: string;
|
||||
}
|
||||
|
||||
@@ -72,35 +72,34 @@ const ProDialog = ({ open, onClose }: ProDialogProps) => {
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t("pro.description")}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" fontWeight={600} sx={{ mt: 2 }}>
|
||||
{t("pro.proInclude")}
|
||||
Cloudreve的sb pro玩意
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
{features.map((feature) => (
|
||||
<ListItem key={feature}>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "36px",
|
||||
}}
|
||||
>
|
||||
<CheckmarkCircleFilled color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
slotProps={{
|
||||
primary: {
|
||||
sx: {},
|
||||
variant: "body1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t(`pro.${feature}`)}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
))}
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
slotProps={{
|
||||
primary: {
|
||||
sx: {},
|
||||
variant: "body1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
谁用Pro我骂谁
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
slotProps={{
|
||||
primary: {
|
||||
sx: {},
|
||||
variant: "body1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
写代码程序猿万岁!
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
{showPromotion && (
|
||||
<Alert
|
||||
@@ -128,11 +127,8 @@ const ProDialog = ({ open, onClose }: ProDialogProps) => {
|
||||
}}
|
||||
>
|
||||
<Button variant="outlined" color="primary" onClick={onClose}>
|
||||
{t("pro.later")}
|
||||
好的,我现在就写代码
|
||||
</Button>
|
||||
<StyledButton onClick={openMore} variant="contained" color="primary">
|
||||
{t("pro.learnMore")}
|
||||
</StyledButton>
|
||||
</StyledDialogActions>
|
||||
</DraggableDialog>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ const SharesInput = (props: SharesInputProps) => {
|
||||
},
|
||||
mt: 0,
|
||||
}}
|
||||
variant="outlined"
|
||||
variant="filled"
|
||||
margin="dense"
|
||||
placeholder={t("dashboard:settings.searchShare")}
|
||||
type="text"
|
||||
|
||||
@@ -43,9 +43,10 @@ import SparkleFilled from "../../Icons/SparkleFilled.tsx";
|
||||
import Telegram from "../../Icons/Telegram.tsx";
|
||||
import PageContainer from "../../Pages/PageContainer.tsx";
|
||||
import PageHeader from "../../Pages/PageHeader.tsx";
|
||||
import ProDialog from "../Common/ProDialog.tsx";
|
||||
import ProDialog from "../../Admin/Common/ProDialog.tsx";
|
||||
import SiteUrlWarning from "./SiteUrlWarning.tsx";
|
||||
import CommentMultiple from "../../Icons/CommentMultiple.tsx";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
|
||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
@@ -306,6 +307,67 @@ const Home = () => {
|
||||
</SwitchTransition>
|
||||
</StyledPaper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<StyledPaper>
|
||||
<Typography variant="subtitle1" fontWeight={500}>
|
||||
{t("summary.storagePolicies")}
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2, mt: 1 }} />
|
||||
<Box>
|
||||
{summary?.storage_policies && summary.storage_policies.length > 0 ? (
|
||||
<Box sx={{ gap: 3, display: "flex", flexDirection: "column" }}>
|
||||
{summary.storage_policies.map((policy) => {
|
||||
const percentage = policy.total > 0 ? (policy.used / policy.total) * 100 : 0;
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<Box key={policy.id}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5 }}>
|
||||
<Typography variant="subtitle2">{policy.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatSize(policy.used)} / {formatSize(policy.total)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{policy.type}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{percentage.toFixed(1)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={percentage}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.divider,
|
||||
"& .MuiLinearProgress-bar": {
|
||||
borderRadius: 4,
|
||||
backgroundColor:
|
||||
percentage > 90 ? red[500] : percentage > 70 ? yellow[500] : green[500],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ py: 2, textAlign: "center", color: "text.secondary" }}>
|
||||
{t("summary.noStoragePolicies")}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</StyledPaper>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={5} lg={4}>
|
||||
<StyledPaper sx={{ p: 0 }}>
|
||||
<Box sx={{ p: 3, display: "flex", alignItems: "center" }}>
|
||||
|
||||
@@ -173,6 +173,8 @@ const Settings = () => {
|
||||
"tos_url",
|
||||
"privacy_policy_url",
|
||||
"show_app_promotion",
|
||||
"announcement",
|
||||
"announcement_enabled",
|
||||
]}
|
||||
>
|
||||
<SiteInformation />
|
||||
|
||||
@@ -64,10 +64,32 @@ const SiteInformation = () => {
|
||||
<NoMarginHelperText>{t("settings.customFooterHTMLDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.announcement")} lgWidth={5} pro>
|
||||
<SettingForm title={t("settings.announcementEnabled", "启用公告")} lgWidth={5} pro={false}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField inputProps={{ readOnly: true }} fullWidth multiline rows={4} />
|
||||
<NoMarginHelperText>{t("settings.announcementDes")}</NoMarginHelperText>
|
||||
<Switch
|
||||
checked={isTrueVal(values.announcement_enabled)}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
announcement_enabled: e.target.checked ? "1" : "0",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NoMarginHelperText>
|
||||
{t("settings.announcementEnabledDes", "启用后,用户登录时会看到公告弹窗")}
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.announcement", "公告内容")} lgWidth={5} pro={false}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
value={values.announcement || ""}
|
||||
onChange={(e) => setSettings({ announcement: e.target.value })}
|
||||
disabled={false}
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.announcementDes", "设置用户登录后看到的公告内容")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.tosUrl")} lgWidth={5}>
|
||||
|
||||
165
src/component/Common/AnnouncementDialog.tsx
Normal file
165
src/component/Common/AnnouncementDialog.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Button,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import { getAnnouncement } from "../../redux/thunks/site.ts";
|
||||
import SessionManager from "../../session";
|
||||
|
||||
interface AnnouncementDialogProps {
|
||||
forceOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
// 为公告内容创建一个带样式的容器组件
|
||||
const AnnouncementContent = styled("div")`
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
a {
|
||||
color: #1976d2;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 8px;
|
||||
}
|
||||
th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
`;
|
||||
|
||||
const AnnouncementDialog = ({ forceOpen = false, onClose }: AnnouncementDialogProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
const dispatch = useAppDispatch();
|
||||
const announcement = useAppSelector((state) => state.siteConfig.basic.config.announcement || "");
|
||||
const announcementEnabled = useAppSelector((state) => state.siteConfig.basic.config.announcement_enabled || false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 加载公告
|
||||
dispatch(getAnnouncement());
|
||||
}, [dispatch]);
|
||||
|
||||
// 保存上次显示的公告内容,用于检测公告是否更新
|
||||
const [lastAnnouncement, setLastAnnouncement] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// 检查公告是否更新
|
||||
const hasAnnouncementChanged = announcement !== lastAnnouncement && lastAnnouncement !== "";
|
||||
|
||||
// 如果公告已更新,清除不再显示状态并强制显示
|
||||
if (hasAnnouncementChanged) {
|
||||
// 使用特殊标记来覆盖之前的设置
|
||||
SessionManager.set("announcement_dismissed", "false");
|
||||
}
|
||||
|
||||
// 更新上次显示的公告内容
|
||||
if (announcement) {
|
||||
setLastAnnouncement(announcement);
|
||||
}
|
||||
|
||||
// 检查是否应该显示公告
|
||||
// 更健壮的检测逻辑:只有当设置为"true"时才视为不再显示
|
||||
const dismissed = SessionManager.get("announcement_dismissed") === "true";
|
||||
const shouldShow = announcementEnabled && announcement && announcement.trim() !== "" && !dismissed;
|
||||
|
||||
if (shouldShow) {
|
||||
// 延迟显示,让页面加载完成
|
||||
const timer = setTimeout(() => setOpen(true), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
// 确保当不应该显示且没有强制打开时关闭弹窗
|
||||
if (!forceOpen) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
}, [announcement, announcementEnabled, lastAnnouncement]);
|
||||
|
||||
// 处理外部强制打开
|
||||
useEffect(() => {
|
||||
if (forceOpen && announcement && announcement.trim() !== "") {
|
||||
setOpen(true);
|
||||
// 重置不再显示选项
|
||||
setDontShowAgain(false);
|
||||
}
|
||||
}, [announcement, announcementEnabled, lastAnnouncement, forceOpen]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (dontShowAgain) {
|
||||
// 保存用户选择不再显示
|
||||
SessionManager.set("announcement_dismissed", "true");
|
||||
}
|
||||
setOpen(false);
|
||||
// 通知父组件弹窗已关闭
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ borderBottom: 1, borderColor: "divider", paddingBottom: 1 }}>
|
||||
{t("announcement", "公告")}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ padding: 2, maxHeight: "80vh" }}>
|
||||
<Paper elevation={0} sx={{ p: 2 }}>
|
||||
<AnnouncementContent dangerouslySetInnerHTML={{ __html: announcement }} />
|
||||
</Paper>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={dontShowAgain} onChange={(e) => setDontShowAgain(e.target.checked)} />}
|
||||
label={t("dontShowAgain", "不再显示")}
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<Button onClick={handleClose} variant="contained" color="primary" sx={{ m: 2 }}>
|
||||
{t("ok", "确定")}
|
||||
</Button>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// 导出一个可以被外部组件控制的版本
|
||||
export { AnnouncementDialog };
|
||||
export default AnnouncementDialog;
|
||||
@@ -1,39 +1,27 @@
|
||||
import { Box, CircularProgress, circularProgressClasses, CircularProgressProps } from "@mui/material";
|
||||
import { Box } from "@mui/material";
|
||||
import { forwardRef } from "react";
|
||||
import LinearProgressComponent from "./LinearProgress";
|
||||
|
||||
export interface FacebookCircularProgressProps extends CircularProgressProps {
|
||||
bgColor?: string;
|
||||
export interface FacebookCircularProgressProps {
|
||||
sx?: any;
|
||||
color?: string;
|
||||
size?: number;
|
||||
thickness?: number;
|
||||
fgColor?: string;
|
||||
}
|
||||
|
||||
const FacebookCircularProgress = forwardRef(({ sx, bgColor, fgColor, ...rest }: FacebookCircularProgressProps, ref) => {
|
||||
const FacebookCircularProgress = forwardRef(({ sx, color, fgColor }: FacebookCircularProgressProps, ref) => {
|
||||
return (
|
||||
<Box sx={{ position: "relative", ...sx }} ref={ref}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
sx={{
|
||||
color: (theme) => bgColor ?? theme.palette.grey[theme.palette.mode === "light" ? 200 : 800],
|
||||
}}
|
||||
size={40}
|
||||
thickness={4}
|
||||
{...rest}
|
||||
value={100}
|
||||
/>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: (theme) => fgColor ?? theme.palette.primary.main,
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
[`& .${circularProgressClasses.circle}`]: {
|
||||
strokeLinecap: "round",
|
||||
},
|
||||
}}
|
||||
size={40}
|
||||
thickness={4}
|
||||
{...rest}
|
||||
/>
|
||||
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", ...sx }} ref={ref}>
|
||||
<LinearProgressComponent color={color || fgColor} height={8} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default FacebookCircularProgress;
|
||||
|
||||
// === 耻辱榜 ===
|
||||
// csc禁止狗叫
|
||||
// 要不然你的传奇事迹将在这几行注释里留存一辈子
|
||||
// 2025/10/26
|
||||
// === 永久留存 禁止删除 ===
|
||||
|
||||
@@ -8,7 +8,11 @@ function ErrorBoundary() {
|
||||
// Uncaught ReferenceError: path is not defined
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<h1 style={{ color: "#a4a4a4", margin: "5px 0px" }}>:(</h1>
|
||||
<h1 style={{ color: "#a4a4a4", margin: "5px 0px" }}>:) OHH FUCKING YES!!!</h1>
|
||||
<p>LeonPan发生了一些意想不到的错误</p>
|
||||
<p>
|
||||
请反馈到<a href="http://leonmmcoset.jjxmm.win:2000/leonmmcoset/leonpan-assets/issues">LeonGit Issues</a>
|
||||
</p>
|
||||
<h2 style={{ margin: "15px 0px" }}>{t("common:renderError")}</h2>
|
||||
{!!error && (
|
||||
<details>
|
||||
@@ -23,6 +27,8 @@ function ErrorBoundary() {
|
||||
)}
|
||||
</details>
|
||||
)}
|
||||
<hr />
|
||||
<p>前端又要忙活了(</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const OutlineIconTextField = ({ icon, ...rest }: OutlineIconTextFieldProp
|
||||
return (
|
||||
<TextField
|
||||
{...rest}
|
||||
variant="standard"
|
||||
variant="filled"
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: !isMobile && <InputAdornment position="start">{icon}</InputAdornment>,
|
||||
|
||||
30
src/component/Common/LinearProgress.tsx
Normal file
30
src/component/Common/LinearProgress.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Box, LinearProgress, linearProgressClasses } from "@mui/material";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
export interface LinearProgressProps {
|
||||
color?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const LinearProgressComponent = forwardRef(({ color, height = 8, ...rest }: LinearProgressProps, ref) => {
|
||||
return (
|
||||
<Box sx={{ width: "100%", maxWidth: 200, ...rest }} ref={ref}>
|
||||
<LinearProgress
|
||||
variant="indeterminate"
|
||||
sx={{
|
||||
height: height,
|
||||
borderRadius: height / 2,
|
||||
[`&.${linearProgressClasses.colorPrimary}`]: {
|
||||
backgroundColor: (theme) => theme.palette.grey[theme.palette.mode === "light" ? 200 : 800],
|
||||
},
|
||||
[`& .${linearProgressClasses.bar}`]: {
|
||||
borderRadius: height / 2,
|
||||
backgroundColor: color || ((theme) => theme.palette.primary.main),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default LinearProgressComponent;
|
||||
@@ -152,6 +152,7 @@ const ExtractArchive = () => {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
variant="filled"
|
||||
placeholder={t("application:modals.passwordDescription")}
|
||||
label={t("modals.password")}
|
||||
value={password}
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { Box, Checkbox, Collapse, DialogContent, IconButton, Stack, Tooltip, useTheme } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import dayjs from "dayjs";
|
||||
import { TFunction } from "i18next";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -216,6 +228,17 @@ const ShareDialog = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Paper
|
||||
sx={{ mt: 2, p: 3, display: "flex", flexDirection: "column", alignItems: "center" }}
|
||||
elevation={1}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2 }}>
|
||||
{t("application:modals.scanQRCodeToAccess")}
|
||||
</Typography>
|
||||
<Box sx={{ bgcolor: "white", p: 2 }}>
|
||||
<QRCodeSVG value={finalShareLink} size={200} level="M" includeMargin />
|
||||
</Box>
|
||||
</Paper>
|
||||
{shareLinkPassword.password && (
|
||||
<>
|
||||
<Collapse in={!includePassword}>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { ConfigLoadState } from "../../../redux/siteConfigSlice.ts";
|
||||
import { openEmptyContextMenu } from "../../../redux/thunks/filemanager.ts";
|
||||
import { loadSiteConfig } from "../../../redux/thunks/site.ts";
|
||||
import CircularProgress from "../../Common/CircularProgress.tsx";
|
||||
import FacebookCircularProgress from "../../Common/CircularProgress.tsx";
|
||||
import "../../Common/FadeTransition.css";
|
||||
import { RadiusFrame } from "../../Frame/RadiusFrame.tsx";
|
||||
import ExplorerError from "./ExplorerError.tsx";
|
||||
@@ -137,7 +137,7 @@ const Explorer = () => {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
<FacebookCircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{index == ExplorerPage.GridView && <GridView />}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Box, Container, Grid, Paper, AppBar, Toolbar, IconButton, Typography, Button } from "@mui/material";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import { Box, Container, Grid, Paper, Typography, Button } from "@mui/material";
|
||||
import { Outlet, useNavigation } from "react-router-dom";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import AutoHeight from "../Common/AutoHeight.tsx";
|
||||
import CircularProgress from "../Common/CircularProgress.tsx";
|
||||
import Logo from "../Common/Logo.tsx";
|
||||
import LanguageSwitcher from "../Common/LanguageSwitcher.tsx";
|
||||
import PoweredBy from "./PoweredBy.tsx";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
|
||||
import AutoHeight from "../Common/AutoHeight";
|
||||
import CircularProgress from "../Common/CircularProgress";
|
||||
import Logo from "../Common/Logo";
|
||||
import LanguageSwitcher from "../Common/LanguageSwitcher";
|
||||
import PoweredBy from "./PoweredBy";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
@@ -25,7 +24,7 @@ const Loading = () => {
|
||||
|
||||
const HeadlessFrame = () => {
|
||||
const loading = useAppSelector((state) => state.globalState.loading.headlessFrame);
|
||||
const { headless_footer, headless_bottom, sidebar_bottom } = useAppSelector(
|
||||
const { headlessFooter, headlessBottom } = useAppSelector(
|
||||
(state) => state.siteConfig.basic?.config?.custom_html ?? {},
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -41,35 +40,6 @@ const HeadlessFrame = () => {
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<AppBar
|
||||
sx={{
|
||||
backgroundColor: "#f5f5f5",
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
{/* <IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton> */}
|
||||
{/* <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
News
|
||||
</Typography> */}
|
||||
<Logo
|
||||
sx={{
|
||||
maxWidth: "40%",
|
||||
maxHeight: "40px",
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</Box>
|
||||
<Container maxWidth={"xs"}>
|
||||
<Grid
|
||||
container
|
||||
@@ -110,9 +80,9 @@ const HeadlessFrame = () => {
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
{headless_bottom && (
|
||||
{headlessBottom && (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: headless_bottom }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: headlessBottom }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
@@ -122,9 +92,9 @@ const HeadlessFrame = () => {
|
||||
</Paper>
|
||||
</Box>
|
||||
<PoweredBy />
|
||||
{headless_footer && (
|
||||
{headlessFooter && (
|
||||
<Box sx={{ width: "100%", mb: 2 }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: headless_footer }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: headlessFooter }} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
@@ -46,6 +46,7 @@ import WrenchSettings from "../../Icons/WrenchSettings.tsx";
|
||||
import { ProChip } from "../../Pages/Setting/SettingForm.tsx";
|
||||
import NavIconTransition from "./NavIconTransition.tsx";
|
||||
import SideNavItem from "./SideNavItem.tsx";
|
||||
import { AnnouncementDialog } from "../../Common/AnnouncementDialog.tsx";
|
||||
|
||||
export interface NavigationItem {
|
||||
label: string;
|
||||
@@ -246,8 +247,12 @@ export const AdminPageNavigation = memo(() => {
|
||||
});
|
||||
|
||||
const PageNavigation = () => {
|
||||
const shopNavEnabled = useAppSelector((state) => state.siteConfig.basic.config.shop_nav_enabled);
|
||||
const { t } = useTranslation("application");
|
||||
// 移除不存在的shop_nav_enabled属性引用
|
||||
const appPromotionEnabled = useAppSelector((state) => state.siteConfig.basic.config.app_promotion);
|
||||
const announcementEnabled = useAppSelector((state) => state.siteConfig.basic.config.announcement_enabled || false);
|
||||
const announcement = useAppSelector((state) => state.siteConfig.basic.config.announcement || "");
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||
const user = SessionManager.currentLoginOrNull();
|
||||
const isAdmin = useMemo(() => {
|
||||
return GroupBS(user?.user).enabled(GroupPermission.is_admin);
|
||||
@@ -272,6 +277,21 @@ const PageNavigation = () => {
|
||||
{connectEnabled && <SideNavItemComponent item={ConnectNavigationItem} />}
|
||||
<SideNavItemComponent item={TaskNavigationItem} />
|
||||
{remoteDownloadEnabled && <SideNavItemComponent item={RemoteDownloadNavigationItem} />}
|
||||
{/* 公告导航项 - 只有当公告启用且有内容时显示 */}
|
||||
{announcementEnabled && announcement && announcement.trim() !== "" && (
|
||||
<SideNavItem
|
||||
onClick={() => setShowAnnouncement(true)}
|
||||
label={t("navbar.announcement", { defaultValue: "公告" })}
|
||||
icon={
|
||||
<NavIconTransition
|
||||
sx={{ px: 0, py: 0, pr: "14px", height: "20px" }}
|
||||
iconProps={{ fontSize: "small", color: "action" }}
|
||||
fileIcon={[Warning, WarningOutlined]}
|
||||
active={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
@@ -279,9 +299,9 @@ const PageNavigation = () => {
|
||||
<Box>
|
||||
{customNavItems.map((item) => (
|
||||
<SideNavItemComponent
|
||||
key={item.name}
|
||||
key={item.title || item.name || Math.random().toString(36)}
|
||||
item={{
|
||||
label: item.name,
|
||||
label: item.title || item.name || "",
|
||||
iconifyName: item.icon,
|
||||
path: item.url,
|
||||
}}
|
||||
@@ -298,6 +318,8 @@ const PageNavigation = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* 公告对话框 */}
|
||||
<AnnouncementDialog forceOpen={showAnnouncement} onClose={() => setShowAnnouncement(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,9 @@ const PoweredBy = ({ ...rest }: PoweredByProps) => {
|
||||
}}
|
||||
fontWeight={500}
|
||||
>
|
||||
Powered by Miaostars
|
||||
Powered by LeonCloud with ❤️
|
||||
<br />
|
||||
Copyright {new Date().getFullYear()} LeonMMcoset
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.side-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.side-enter-active {
|
||||
opacity: 1;
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
.side-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.side-enter-active,
|
||||
.side-exit-active {
|
||||
|
||||
@@ -27,3 +27,10 @@ export function updateSiteConfig(): AppThunk {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getAnnouncement(): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
// 获取基本配置,其中包含公告信息
|
||||
await dispatch(loadSiteConfig("basic"));
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user