first commit
This commit is contained in:
50
src/component/Admin/Settings/SiteInformation/GeneralImagePreview.tsx
Executable file
50
src/component/Admin/Settings/SiteInformation/GeneralImagePreview.tsx
Executable file
@@ -0,0 +1,50 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export interface GeneralImagePreviewProps {
|
||||
src: string;
|
||||
debounce?: number; // (可选) 防抖时间
|
||||
}
|
||||
|
||||
const GeneralImagePreview = ({ src, debounce = 0 }: GeneralImagePreviewProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const [debouncedSrc, setDebouncedSrc] = useState(src);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSrc(src);
|
||||
}, debounce);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [src, debounce]);
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: isMobile ? 0 : 3 }}>
|
||||
<Box
|
||||
sx={{
|
||||
border: (t) => `1px solid ${t.palette.divider}`,
|
||||
p: 1,
|
||||
display: "inline-block",
|
||||
borderRadius: (theme) => `${theme.shape.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component={"img"}
|
||||
src={debouncedSrc}
|
||||
sx={{
|
||||
display: "block",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralImagePreview;
|
||||
65
src/component/Admin/Settings/SiteInformation/LogoPreview.tsx
Executable file
65
src/component/Admin/Settings/SiteInformation/LogoPreview.tsx
Executable file
@@ -0,0 +1,65 @@
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export interface LogoPreviewProps {
|
||||
logoLight: string;
|
||||
logoDark: string;
|
||||
}
|
||||
|
||||
const LogoPreview = ({ logoLight, logoDark }: LogoPreviewProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
return (
|
||||
<Stack spacing={1} direction={"row"} sx={{ mt: isMobile ? 0 : 3 }}>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: (theme) => theme.palette.grey[100],
|
||||
p: 1,
|
||||
borderRadius: (theme) => `${theme.shape.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component={"img"}
|
||||
src={logoDark}
|
||||
sx={{
|
||||
display: "block",
|
||||
height: "auto",
|
||||
maxWidth: 160,
|
||||
maxHeight: 35,
|
||||
width: "100%",
|
||||
objectPosition: "left",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: (theme) => theme.palette.grey[900],
|
||||
p: 1,
|
||||
borderRadius: (theme) => `${theme.shape.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component={"img"}
|
||||
src={logoLight}
|
||||
sx={{
|
||||
display: "block",
|
||||
height: "auto",
|
||||
maxWidth: 160,
|
||||
maxHeight: 35,
|
||||
width: "100%",
|
||||
objectPosition: "left",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoPreview;
|
||||
258
src/component/Admin/Settings/SiteInformation/SiteInformation.tsx
Executable file
258
src/component/Admin/Settings/SiteInformation/SiteInformation.tsx
Executable file
@@ -0,0 +1,258 @@
|
||||
import { Box, FormControl, FormControlLabel, Stack, Switch, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isTrueVal } from "../../../../session/utils.ts";
|
||||
import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx";
|
||||
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
|
||||
import { NoMarginHelperText, SettingSection, SettingSectionContent, StyledInputAdornment } from "../Settings.tsx";
|
||||
import { SettingContext } from "../SettingWrapper.tsx";
|
||||
import GeneralImagePreview from "./GeneralImagePreview.tsx";
|
||||
import LogoPreview from "./LogoPreview.tsx";
|
||||
import SiteURLInput from "./SiteURLInput.tsx";
|
||||
|
||||
const SiteInformation = () => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { formRef, setSettings, values } = useContext(SettingContext);
|
||||
|
||||
return (
|
||||
<Box component={"form"} ref={formRef} onSubmit={(e) => e.preventDefault()}>
|
||||
<Stack spacing={5}>
|
||||
<SettingSection>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("settings.basicInformation")}
|
||||
</Typography>
|
||||
<SettingSectionContent>
|
||||
<SettingForm title={t("settings.mainTitle")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ siteName: e.target.value })}
|
||||
value={values.siteName}
|
||||
required
|
||||
inputProps={{ maxLength: 255 }}
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.mainTitleDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.siteDescription")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ siteDes: e.target.value })}
|
||||
value={values.siteDes}
|
||||
multiline
|
||||
rows={4}
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.siteDescriptionDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.siteURL")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<SiteURLInput urls={values.siteURL} onChange={(v) => setSettings({ siteURL: v })} />
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.customFooterHTML")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ siteScript: e.target.value })}
|
||||
value={values.siteScript}
|
||||
multiline
|
||||
rows={4}
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.customFooterHTMLDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.announcement")} lgWidth={5} pro>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField inputProps={{ readOnly: true }} fullWidth multiline rows={4} />
|
||||
<NoMarginHelperText>{t("settings.announcementDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.tosUrl")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ tos_url: e.target.value })}
|
||||
value={values.tos_url}
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.tosUrlDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.privacyUrl")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ privacy_policy_url: e.target.value })}
|
||||
value={values.privacy_policy_url}
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.privacyUrlDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</SettingSectionContent>
|
||||
</SettingSection>
|
||||
<SettingSection>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("settings.branding")}
|
||||
</Typography>
|
||||
<SettingSectionContent>
|
||||
<SettingForm
|
||||
title={t("settings.logo")}
|
||||
lgWidth={5}
|
||||
spacing={3}
|
||||
secondary={
|
||||
<Grid item md={7} xs={12}>
|
||||
<LogoPreview logoDark={values.site_logo} logoLight={values.site_logo_light} />
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ site_logo: e.target.value })}
|
||||
value={values.site_logo}
|
||||
required
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<StyledInputAdornment disableTypography position="start">
|
||||
{t("settings.light")}
|
||||
</StyledInputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<DenseFilledTextField
|
||||
sx={{ mt: 1 }}
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ site_logo_light: e.target.value })}
|
||||
value={values.site_logo_light}
|
||||
required
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<StyledInputAdornment disableTypography position="start">
|
||||
{t("settings.dark")}
|
||||
</StyledInputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.logoDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm
|
||||
title={t("settings.smallIcon")}
|
||||
lgWidth={5}
|
||||
spacing={3}
|
||||
secondary={
|
||||
<Grid item md={7} xs={12}>
|
||||
<Box sx={{ maxWidth: 160 }}>
|
||||
<GeneralImagePreview src={values.pwa_small_icon} debounce={250} />
|
||||
</Box>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ pwa_small_icon: e.target.value })}
|
||||
value={values.pwa_small_icon}
|
||||
required
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.smallIconDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm
|
||||
title={t("settings.mediumIcon")}
|
||||
lgWidth={5}
|
||||
spacing={3}
|
||||
secondary={
|
||||
<Grid item md={7} xs={12}>
|
||||
<Box sx={{ maxWidth: 160 }}>
|
||||
<GeneralImagePreview src={values.pwa_medium_icon} debounce={250} />
|
||||
</Box>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ pwa_medium_icon: e.target.value })}
|
||||
value={values.pwa_medium_icon}
|
||||
required
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.mediumIconDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm
|
||||
title={t("settings.largeIcon")}
|
||||
lgWidth={5}
|
||||
spacing={3}
|
||||
secondary={
|
||||
<Grid item md={7} xs={12}>
|
||||
<Box sx={{ maxWidth: 160 }}>
|
||||
<GeneralImagePreview src={values.pwa_large_icon} debounce={250} />
|
||||
</Box>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={(e) => setSettings({ pwa_large_icon: e.target.value })}
|
||||
value={values.pwa_large_icon}
|
||||
required
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.largeIconDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</SettingSectionContent>
|
||||
</SettingSection>
|
||||
<SettingSection>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("vas.mobileApp")}
|
||||
</Typography>
|
||||
<SettingSectionContent>
|
||||
<SettingForm lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isTrueVal(values.show_app_promotion)}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
show_app_promotion: e.target.checked ? "1" : "0",
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t("vas.showAppPromotion")}
|
||||
/>
|
||||
<NoMarginHelperText>{t("vas.showAppPromotionDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("vas.appFeedback")} lgWidth={5} pro>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
slotProps={{
|
||||
input: {
|
||||
readOnly: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<NoMarginHelperText>{t("vas.appLinkDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("vas.appForum")} lgWidth={5} pro>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField fullWidth slotProps={{ input: { readOnly: true } }} />
|
||||
<NoMarginHelperText>{t("vas.appLinkDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</SettingSectionContent>
|
||||
</SettingSection>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteInformation;
|
||||
91
src/component/Admin/Settings/SiteInformation/SiteURLInput.tsx
Executable file
91
src/component/Admin/Settings/SiteInformation/SiteURLInput.tsx
Executable file
@@ -0,0 +1,91 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { Box, Collapse, Divider, IconButton, InputAdornment, Stack } from "@mui/material";
|
||||
import { DenseFilledTextField, SecondaryButton } from "../../../Common/StyledComponents.tsx";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Dismiss from "../../../Icons/Dismiss.tsx";
|
||||
import Add from "../../../Icons/Add.tsx";
|
||||
import { TransitionGroup } from "react-transition-group";
|
||||
import { NoMarginHelperText, StyledInputAdornment } from "../Settings.tsx";
|
||||
|
||||
export interface SiteURLInputProps {
|
||||
urls: string;
|
||||
onChange: (url: string) => void;
|
||||
}
|
||||
|
||||
const SiteURLInput = ({ urls, onChange }: SiteURLInputProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const urlSplit = useMemo(() => {
|
||||
return urls.split(",").map((url) => url);
|
||||
}, [urls]);
|
||||
|
||||
const onUrlChange = (index: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newUrls = [...urlSplit];
|
||||
newUrls[index] = e.target.value;
|
||||
onChange(newUrls.join(","));
|
||||
};
|
||||
|
||||
const removeUrl = (index: number) => () => {
|
||||
const newUrls = [...urlSplit];
|
||||
newUrls.splice(index, 1);
|
||||
onChange(newUrls.join(","));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={1}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={onUrlChange(0)}
|
||||
value={urlSplit[0]}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<StyledInputAdornment disableTypography position="start">
|
||||
{t("settings.primarySiteURL")}
|
||||
</StyledInputAdornment>
|
||||
),
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<NoMarginHelperText>{t("settings.primarySiteURLDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<NoMarginHelperText>{t("settings.secondaryDes")}</NoMarginHelperText>
|
||||
<TransitionGroup>
|
||||
{urlSplit.slice(1).map((url, index) => (
|
||||
<Collapse key={index}>
|
||||
<FormControl fullWidth sx={{ mb: 1 }}>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
onChange={onUrlChange(index + 1)}
|
||||
value={url}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<StyledInputAdornment disableTypography position="start">
|
||||
{t("settings.secondarySiteURL")}
|
||||
</StyledInputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton size={"small"} onClick={removeUrl(index + 1)}>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</FormControl>
|
||||
</Collapse>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
<Box sx={{ mt: "0!important" }}>
|
||||
<SecondaryButton variant={"contained"} startIcon={<Add />} onClick={() => onChange(`${urls},`)}>
|
||||
{t("settings.addSecondary")}
|
||||
</SecondaryButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteURLInput;
|
||||
Reference in New Issue
Block a user