mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat(ui): custom HTML content in predefined locations
This commit is contained in:
parent
686edceda6
commit
aa80ca5bb2
|
|
@ -112,6 +112,14 @@
|
|||
"retryDelayDes": "Initial delay time (seconds) for task retries."
|
||||
},
|
||||
"settings": {
|
||||
"headlessFooter": "Landing page footer",
|
||||
"headlessFooterDes": "Custom HTML content displayed at the bottom of the login, sign up and callback result pages.",
|
||||
"headlessBottom": "Landing page bottom",
|
||||
"headlessBottomDes": "Custom HTML content displayed at the bottom of the login, sign up and callback result pages.",
|
||||
"customHTML": "Custom HTML",
|
||||
"customHTMLDes": "Insert custom HTML content at the preset position of the site.",
|
||||
"sidebarBottom": "Sidebar bottom",
|
||||
"sidebarBottomDes": "Custom HTML content displayed at the bottom of the sidebar.",
|
||||
"addNavItem": "Add navigation item",
|
||||
"customNavItems": "Custom sidebar items",
|
||||
"customNavItemsDes": "You can add custom items to the sidebar, and users will be redirected to the corresponding link when clicked.",
|
||||
|
|
|
|||
|
|
@ -112,6 +112,14 @@
|
|||
"retryDelayDes": "タスク再試行の初期遅延時間(秒)"
|
||||
},
|
||||
"settings": {
|
||||
"headlessFooter": "ログインセッションページの下部",
|
||||
"headlessFooterDes": "ユーザーがログイン、登録、コールバック結果などのページの下部に表示するカスタム HTML コンテンツ。",
|
||||
"headlessBottom": "ログインセッションページの主体下部",
|
||||
"headlessBottomDes": "ユーザーがログイン、登録、コールバック結果などのページの主体ボックスの下部に表示するカスタム HTML コンテンツ。",
|
||||
"customHTML": "カスタム HTML",
|
||||
"customHTMLDes": "サイトの既定の位置にカスタム HTML コンテンツを挿入します。",
|
||||
"sidebarBottom": "サイドバーの下部",
|
||||
"sidebarBottomDes": "サイドバーの下部に表示するカスタム HTML コンテンツ。",
|
||||
"addNavItem": "ナビゲーション項目を追加",
|
||||
"customNavItems": "サイドナビゲーションバーのカスタマイズ",
|
||||
"customNavItemsDes": "左側のナビゲーションバーにカスタム項目を追加できます。ユーザーがクリックすると、対応するリンクに移動します。",
|
||||
|
|
|
|||
|
|
@ -112,6 +112,14 @@
|
|||
"retryDelayDes": "任务重试的初始延迟时间(秒)。"
|
||||
},
|
||||
"settings": {
|
||||
"headlessFooter": "登录会话页面底部",
|
||||
"headlessFooterDes": "用户登录、注册、回调结果等页面底部展示的自定义 HTML 内容。",
|
||||
"headlessBottom": "登录会话页面主体底部",
|
||||
"headlessBottomDes": "用户登录、注册、回调结果等页面主体框底部展示的自定义 HTML 内容。",
|
||||
"customHTML": "自定义 HTML",
|
||||
"customHTMLDes": "在站点的预设位置插入展示自定义的 HTML 内容。",
|
||||
"sidebarBottom": "侧边栏底部",
|
||||
"sidebarBottomDes": "在侧边栏底部展示的自定义 HTML 内容。",
|
||||
"addNavItem": "添加导航条目",
|
||||
"customNavItems": "自定义侧边导航栏",
|
||||
"customNavItemsDes": "你可以在左侧导航栏中添加自定义的条目,用户点击后会跳转到对应的链接。",
|
||||
|
|
|
|||
|
|
@ -112,6 +112,14 @@
|
|||
"retryDelayDes": "任務重試的初始延遲時間(秒)。"
|
||||
},
|
||||
"settings": {
|
||||
"headlessFooter": "登入會話頁面底部",
|
||||
"headlessFooterDes": "使用者登入、註冊、回調結果等頁面底部展示的自訂 HTML 內容。",
|
||||
"headlessBottom": "登入會話頁面主體底部",
|
||||
"headlessBottomDes": "使用者登入、註冊、回調結果等頁面主體框底部展示的自訂 HTML 內容。",
|
||||
"customHTML": "自訂 HTML",
|
||||
"customHTMLDes": "在站點的預設位置插入展示自訂的 HTML 內容。",
|
||||
"sidebarBottom": "側邊欄底部",
|
||||
"sidebarBottomDes": "在側邊欄底部展示的自訂 HTML 內容。",
|
||||
"addNavItem": "新增導航條目",
|
||||
"customNavItems": "自定義側邊導航欄",
|
||||
"customNavItemsDes": "你可以在側邊導航欄中新增自定義的條目,用戶點擊後會跳轉到對應的鏈接。",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export interface SiteConfig {
|
|||
thumbnail_height?: number;
|
||||
custom_props?: CustomProps[];
|
||||
custom_nav_items?: CustomNavItem[];
|
||||
custom_html?: CustomHTML;
|
||||
}
|
||||
|
||||
export interface CaptchaResponse {
|
||||
|
|
@ -55,3 +56,9 @@ export interface CustomNavItem {
|
|||
url: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface CustomHTML {
|
||||
headless_footer?: string;
|
||||
headless_bottom?: string;
|
||||
sidebar_bottom?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useContext } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { SettingSection } from "../Settings";
|
||||
import { SettingContext } from "../SettingWrapper";
|
||||
import CustomHTML from "./CustomHTML";
|
||||
import CustomNavItems from "./CustomNavItems";
|
||||
import ThemeOptions from "./ThemeOptions";
|
||||
|
||||
|
|
@ -27,6 +28,9 @@ const Appearance = () => {
|
|||
onChange={(value: string) => setSettings({ custom_nav_items: value })}
|
||||
/>
|
||||
</SettingSection>
|
||||
<SettingSection>
|
||||
<CustomHTML />
|
||||
</SettingSection>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
import { LoadingButton } from "@mui/lab";
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Container,
|
||||
FormControl,
|
||||
Grid,
|
||||
Grid2,
|
||||
Paper,
|
||||
Stack,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Suspense, useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OutlineIconTextField } from "../../../Common/Form/OutlineIconTextField";
|
||||
import Logo from "../../../Common/Logo";
|
||||
import DrawerHeader from "../../../Frame/NavBar/DrawerHeader";
|
||||
import { SideNavItemComponent } from "../../../Frame/NavBar/PageNavigation";
|
||||
import StorageSummary from "../../../Frame/NavBar/StorageSummary";
|
||||
import PoweredBy from "../../../Frame/PoweredBy";
|
||||
import CloudDownload from "../../../Icons/CloudDownload";
|
||||
import CloudDownloadOutlined from "../../../Icons/CloudDownloadOutlined";
|
||||
import CubeSync from "../../../Icons/CubeSync";
|
||||
import CubeSyncFilled from "../../../Icons/CubeSyncFilled";
|
||||
import MailOutlined from "../../../Icons/MailOutlined";
|
||||
import PhoneLaptop from "../../../Icons/PhoneLaptop";
|
||||
import PhoneLaptopOutlined from "../../../Icons/PhoneLaptopOutlined";
|
||||
import SettingForm from "../../../Pages/Setting/SettingForm";
|
||||
import MonacoEditor from "../../../Viewers/CodeViewer/MonacoEditor";
|
||||
import { SettingContext } from "../SettingWrapper";
|
||||
import { NoMarginHelperText } from "../Settings";
|
||||
|
||||
export interface CustomHTMLProps {}
|
||||
|
||||
const HeadlessFooterPreview = ({ footer, bottom }: { footer?: string; bottom?: string }) => {
|
||||
const { t } = useTranslation("application");
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900],
|
||||
}}
|
||||
>
|
||||
<Container maxWidth={"xs"}>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{ minHeight: "100px" }}
|
||||
>
|
||||
<Box sx={{ width: "100%", py: 2 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
padding: (theme) => `${theme.spacing(2)} ${theme.spacing(3)} ${theme.spacing(3)}`,
|
||||
}}
|
||||
>
|
||||
<Logo
|
||||
sx={{
|
||||
maxWidth: "40%",
|
||||
maxHeight: "40px",
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Box
|
||||
sx={{
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
<Typography variant={"h6"}>{t("login.siginToYourAccount")}</Typography>
|
||||
<FormControl variant="standard" margin="normal" fullWidth>
|
||||
<OutlineIconTextField label={t("login.email")} variant={"outlined"} icon={<MailOutlined />} />
|
||||
</FormControl>
|
||||
<LoadingButton sx={{ mt: 2 }} fullWidth variant="contained" color="primary">
|
||||
<span>{t("login.continue")}</span>
|
||||
</LoadingButton>
|
||||
{bottom && (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: bottom }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</Paper>
|
||||
</Box>
|
||||
<PoweredBy />
|
||||
{footer && (
|
||||
<Box sx={{ mb: 2, width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: footer }} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarBottomPreview = ({ bottom }: { bottom?: string }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "300px",
|
||||
borderRadius: 1,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900],
|
||||
}}
|
||||
>
|
||||
<DrawerHeader disabled />
|
||||
<Stack
|
||||
direction={"column"}
|
||||
spacing={2}
|
||||
sx={{
|
||||
px: 1,
|
||||
pb: 1,
|
||||
flexGrow: 1,
|
||||
mx: 1,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<SideNavItemComponent
|
||||
item={{
|
||||
label: "navbar.remoteDownload",
|
||||
icon: [CloudDownload, CloudDownloadOutlined],
|
||||
path: "#1",
|
||||
}}
|
||||
/>
|
||||
<SideNavItemComponent
|
||||
item={{
|
||||
label: "navbar.connect",
|
||||
icon: [PhoneLaptop, PhoneLaptopOutlined],
|
||||
path: "#1",
|
||||
}}
|
||||
/>
|
||||
<SideNavItemComponent
|
||||
item={{
|
||||
label: "navbar.taskQueue",
|
||||
icon: [CubeSyncFilled, CubeSync],
|
||||
path: "#1",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<StorageSummary />
|
||||
{bottom && (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: bottom ?? "" }} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomHTML = ({}: CustomHTMLProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { formRef, setSettings, values } = useContext(SettingContext);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("settings.customHTML")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
{t("settings.customHTMLDes")}
|
||||
</Typography>
|
||||
<Stack spacing={3}>
|
||||
<SettingForm
|
||||
title={t("settings.headlessFooter")}
|
||||
lgWidth={5}
|
||||
spacing={3}
|
||||
secondary={
|
||||
<Grid2 size={{ md: 7, xs: 12 }}>
|
||||
<HeadlessFooterPreview footer={values.headless_footer_html ?? ""} />
|
||||
</Grid2>
|
||||
}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<MonacoEditor
|
||||
theme={theme.palette.mode === "dark" ? "vs-dark" : "vs"}
|
||||
value={values.headless_footer_html}
|
||||
height={"300px"}
|
||||
minHeight={"300px"}
|
||||
language={"html"}
|
||||
onChange={(e) => setSettings({ headless_footer_html: e as string })}
|
||||
/>
|
||||
</Suspense>
|
||||
<NoMarginHelperText>{t("settings.headlessFooterDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm
|
||||
title={t("settings.headlessBottom")}
|
||||
lgWidth={5}
|
||||
spacing={3}
|
||||
secondary={
|
||||
<Grid2 size={{ md: 7, xs: 12 }}>
|
||||
<HeadlessFooterPreview bottom={values.headless_bottom_html ?? ""} />
|
||||
</Grid2>
|
||||
}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<MonacoEditor
|
||||
theme={theme.palette.mode === "dark" ? "vs-dark" : "vs"}
|
||||
value={values.headless_bottom_html}
|
||||
height={"300px"}
|
||||
minHeight={"300px"}
|
||||
language={"html"}
|
||||
onChange={(e) => setSettings({ headless_bottom_html: e as string })}
|
||||
/>
|
||||
</Suspense>
|
||||
<NoMarginHelperText>{t("settings.headlessBottomDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm
|
||||
title={t("settings.sidebarBottom")}
|
||||
lgWidth={5}
|
||||
spacing={3}
|
||||
secondary={
|
||||
<Grid2 size={{ md: 7, xs: 12 }}>
|
||||
<SidebarBottomPreview bottom={values.sidebar_bottom_html ?? ""} />
|
||||
</Grid2>
|
||||
}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<MonacoEditor
|
||||
theme={theme.palette.mode === "dark" ? "vs-dark" : "vs"}
|
||||
value={values.sidebar_bottom_html}
|
||||
height={"300px"}
|
||||
minHeight={"300px"}
|
||||
language={"html"}
|
||||
onChange={(e) => setSettings({ sidebar_bottom_html: e as string })}
|
||||
/>
|
||||
</Suspense>
|
||||
<NoMarginHelperText>{t("settings.sidebarBottomDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomHTML;
|
||||
|
|
@ -300,7 +300,16 @@ const Settings = () => {
|
|||
</SettingsWrapper>
|
||||
)}
|
||||
{tab === SettingsPageTab.Appearance && (
|
||||
<SettingsWrapper settings={["theme_options", "defaultTheme", "custom_nav_items"]}>
|
||||
<SettingsWrapper
|
||||
settings={[
|
||||
"theme_options",
|
||||
"defaultTheme",
|
||||
"custom_nav_items",
|
||||
"headless_footer_html",
|
||||
"headless_bottom_html",
|
||||
"sidebar_bottom_html",
|
||||
]}
|
||||
>
|
||||
<Appearance />
|
||||
</SettingsWrapper>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ const Loading = () => {
|
|||
|
||||
const HeadlessFrame = () => {
|
||||
const loading = useAppSelector((state) => state.globalState.loading.headlessFrame);
|
||||
const { headless_footer, headless_bottom, sidebar_bottom } = useAppSelector(
|
||||
(state) => state.siteConfig.basic?.config?.custom_html ?? {},
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
let navigation = useNavigation();
|
||||
|
||||
|
|
@ -66,6 +69,11 @@ const HeadlessFrame = () => {
|
|||
}}
|
||||
>
|
||||
<Outlet />
|
||||
{headless_bottom && (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: headless_bottom }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{(loading || navigation.state !== "idle") && <Loading />}
|
||||
</div>
|
||||
|
|
@ -73,6 +81,11 @@ const HeadlessFrame = () => {
|
|||
</Paper>
|
||||
</Box>
|
||||
<PoweredBy />
|
||||
{headless_footer && (
|
||||
<Box sx={{ width: "100%", mb: 2 }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: headless_footer }} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { Box, Drawer, Popover, PopoverProps, Stack, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useContext, useRef } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import DrawerHeader from "./DrawerHeader.tsx";
|
||||
import SessionManager from "../../../session";
|
||||
import TreeNavigation from "../../FileManager/TreeView/TreeNavigation.tsx";
|
||||
import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx";
|
||||
import DrawerHeader from "./DrawerHeader.tsx";
|
||||
import PageNavigation, { AdminPageNavigation } from "./PageNavigation.tsx";
|
||||
import StorageSummary from "./StorageSummary.tsx";
|
||||
import { useContext, useRef } from "react";
|
||||
import SessionManager from "../../../session";
|
||||
import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx";
|
||||
|
||||
const DrawerContent = () => {
|
||||
const { sidebar_bottom } = useAppSelector((state) => state.siteConfig.basic?.config?.custom_html ?? {});
|
||||
const scrollRef = useRef<any>();
|
||||
const user = SessionManager.currentLoginOrNull();
|
||||
const theme = useTheme();
|
||||
|
|
@ -38,6 +39,11 @@ const DrawerContent = () => {
|
|||
</>
|
||||
)}
|
||||
{isDashboard && <AdminPageNavigation />}
|
||||
{sidebar_bottom && (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: sidebar_bottom }} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { ChevronLeft } from "@mui/icons-material";
|
||||
import { Box, Fade, IconButton, styled, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { setDrawerOpen } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { ChevronLeft } from "@mui/icons-material";
|
||||
import { useState } from "react";
|
||||
import Logo from "../../Common/Logo.tsx";
|
||||
|
||||
export const DrawerHeaderContainer = styled("div")(({ theme }) => ({
|
||||
|
|
@ -14,7 +14,7 @@ export const DrawerHeaderContainer = styled("div")(({ theme }) => ({
|
|||
justifyContent: "flex-end",
|
||||
}));
|
||||
|
||||
const DrawerHeader = () => {
|
||||
const DrawerHeader = ({ disabled }: { disabled?: boolean }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const dispatch = useAppDispatch();
|
||||
|
|
@ -22,7 +22,10 @@ const DrawerHeader = () => {
|
|||
const [showCollapse, setShowCollapse] = useState(false);
|
||||
|
||||
return (
|
||||
<DrawerHeaderContainer onMouseEnter={() => setShowCollapse(true)} onMouseLeave={() => setShowCollapse(false)}>
|
||||
<DrawerHeaderContainer
|
||||
onMouseEnter={() => setShowCollapse(disabled ? false : true)}
|
||||
onMouseLeave={() => setShowCollapse(false)}
|
||||
>
|
||||
<Box sx={{ width: "100%", pl: 2 }}>
|
||||
<Logo
|
||||
sx={{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Box, BoxProps, Typography, useTheme } from "@mui/material";
|
||||
import LogoIcon from "./assets/logo.svg";
|
||||
import LogoIconDark from "./assets/logo_light.svg";
|
||||
|
||||
export interface PoweredByProps extends BoxProps {}
|
||||
|
||||
|
|
@ -44,12 +46,7 @@ const PoweredBy = ({ ...rest }: PoweredByProps) => {
|
|||
sx={{
|
||||
height: 20,
|
||||
}}
|
||||
src={
|
||||
theme.palette.mode === "dark"
|
||||
? "https://docs.cloudreve.org/logo_light.svg"
|
||||
: "https://docs.cloudreve.org/logo.svg"
|
||||
}
|
||||
alt="Cloudreve"
|
||||
src={theme.palette.mode === "dark" ? LogoIconDark : LogoIcon}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 122 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 122 KiB |
Loading…
Reference in New Issue