first commit

This commit is contained in:
2025-10-19 13:31:11 +00:00
commit 8bfc183b66
1248 changed files with 195992 additions and 0 deletions

View 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;

View 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;

View 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;

View 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;