fix(external-admin): refine authenticated branding

This commit is contained in:
zhx 2026-07-15 19:57:01 +08:00
parent 7bc1b6a423
commit c67d2e2d4b
6 changed files with 60 additions and 33 deletions

View File

@ -22,7 +22,7 @@ const displayScopeOptions = [
]; ];
export function BannerCreateDialog({ loading, onClose, onSubmit, open }) { export function BannerCreateDialog({ loading, onClose, onSubmit, open }) {
const { locale, t } = useExternalI18n(); const { direction, locale, t } = useExternalI18n();
const [form, setForm] = useState(initialForm); const [form, setForm] = useState(initialForm);
useEffect(() => setForm(initialForm), [open]); useEffect(() => setForm(initialForm), [open]);
@ -79,6 +79,7 @@ export function BannerCreateDialog({ loading, onClose, onSubmit, open }) {
<TextField disabled={loading} label={t("banners.sort")} slotProps={{ htmlInput: { min: 0 } }} type="number" value={form.sortOrder} onChange={(event) => setForm({ ...form, sortOrder: event.target.value })} /> <TextField disabled={loading} label={t("banners.sort")} slotProps={{ htmlInput: { min: 0 } }} type="number" value={form.sortOrder} onChange={(event) => setForm({ ...form, sortOrder: event.target.value })} />
</Stack> </Stack>
<TimeRangeFilter <TimeRangeFilter
direction={direction}
disabled={loading} disabled={loading}
label={t("banners.schedule")} label={t("banners.schedule")}
locale={locale} locale={locale}

View File

@ -138,7 +138,18 @@ export function ExternalAdminLayout() {
}} }}
> >
<Toolbar> <Toolbar>
<Typography className="external-header-title" component="h2" noWrap>{mobile && !current ? t("app.shortTitle") : t(current?.labelKey || "app.shortTitle")}</Typography> {mobile ? (
<AppIdentity
appCode={session?.appCode}
appName={session?.appName || session?.appCode}
className="external-mobile-brand"
logoUrl={session?.logoUrl}
showLogoFallback={false}
size="small"
/>
) : (
<Typography className="external-header-title" component="h2" noWrap>{t(current?.labelKey || "app.shortTitle")}</Typography>
)}
<Box flex={1} /> <Box flex={1} />
<Stack direction="row" spacing={1.5} sx={{ alignItems: "center" }}> <Stack direction="row" spacing={1.5} sx={{ alignItems: "center" }}>
<LanguageSwitcher compact={mobile} /> <LanguageSwitcher compact={mobile} />
@ -193,7 +204,7 @@ export function ExternalAdminLayout() {
})} })}
</Box> </Box>
<Divider sx={{ my: 1.5 }} /> <Divider sx={{ my: 1.5 }} />
<Stack direction="row" spacing={1.5}> <Stack direction={{ xs: "column", sm: "row" }} spacing={1.5}>
<Button color="inherit" fullWidth startIcon={<KeyOutlined />} onClick={() => { setSheetOpen(false); navigate("/change-password"); }}>{t("auth.changePassword")}</Button> <Button color="inherit" fullWidth startIcon={<KeyOutlined />} onClick={() => { setSheetOpen(false); navigate("/change-password"); }}>{t("auth.changePassword")}</Button>
<Button fullWidth startIcon={<LogoutOutlined />} variant="outlined" onClick={handleLogout}>{t("auth.logout")}</Button> <Button fullWidth startIcon={<LogoutOutlined />} variant="outlined" onClick={handleLogout}>{t("auth.logout")}</Button>
</Stack> </Stack>

View File

@ -55,20 +55,9 @@ const CAPABILITY_ICONS = {
export default function OverviewPage() { export default function OverviewPage() {
const { session } = useExternalAuth(); const { session } = useExternalAuth();
const { t } = useExternalI18n(); const { t } = useExternalI18n();
const enabledCount = EXTERNAL_CAPABILITIES.filter((item) => hasExternalCapability(session, item.code)).length;
return ( return (
<ExternalPage title={t("nav.overview")}> <ExternalPage title={t("nav.overview")}>
<Box className="external-hero">
<Typography className="external-hero-eyebrow">{t("overview.welcome")}</Typography>
<Typography className="external-hero-title" component="h2">{session.appName || session.appCode}</Typography>
<Box className="external-hero-meta">
<Box><Typography color="text.secondary" variant="caption">{t("overview.currentApp")}</Typography><Typography fontWeight={700}>{session.appName || session.appCode} ({session.appCode})</Typography></Box>
<Box><Typography color="text.secondary" variant="caption">{t("overview.account")}</Typography><Typography fontWeight={700}>{session.account}</Typography></Box>
<Box><Typography color="text.secondary" variant="caption">{t("overview.permissions")}</Typography><Typography fontWeight={700}>{enabledCount} / {EXTERNAL_CAPABILITIES.length}</Typography></Box>
</Box>
</Box>
<Typography className="external-section-title" component="h3">{t("overview.features")}</Typography>
<Box className="external-capability-grid"> <Box className="external-capability-grid">
{EXTERNAL_CAPABILITIES.map((item) => { {EXTERNAL_CAPABILITIES.map((item) => {
const enabled = hasExternalCapability(session, item.code); const enabled = hasExternalCapability(session, item.code);

View File

@ -224,6 +224,22 @@ body {
letter-spacing: 0.03em; letter-spacing: 0.03em;
} }
.external-mobile-brand {
max-width: min(48vw, 180px);
color: var(--primary-strong);
font-family: var(--font-display);
font-size: 15px;
font-weight: 750;
letter-spacing: 0.04em;
}
.external-mobile-brand img,
.external-sidebar-brand img,
.external-change-password-brand img {
border-color: var(--primary-border);
box-shadow: 0 0 0 1px rgba(214, 181, 110, 0.08), 0 8px 24px rgba(0, 0, 0, 0.38);
}
.external-user-avatar { .external-user-avatar {
width: 34px !important; width: 34px !important;
height: 34px !important; height: 34px !important;

View File

@ -49,7 +49,7 @@ const defaultMessages = {
// withTime=false / // withTime=false /
// / 00:00:00.000 23:59:59.999 // / 00:00:00.000 23:59:59.999
export function TimeRangeFilter({ className = "", disabled = false, label = "时间", locale = "zh-CN", messages, onChange, value, withTime = true }) { export function TimeRangeFilter({ className = "", direction = "ltr", disabled = false, label = "时间", locale = "zh-CN", messages, onChange, value, withTime = true }) {
const copy = useMemo(() => ({ ...defaultMessages, ...messages }), [messages]); const copy = useMemo(() => ({ ...defaultMessages, ...messages }), [messages]);
const range = useMemo(() => normalizeRange(value), [value]); const range = useMemo(() => normalizeRange(value), [value]);
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
@ -69,6 +69,12 @@ export function TimeRangeFilter({ className = "", disabled = false, label = "时
const rangeDistance = formatLocalizedRangeDistance(draft.startMs, draft.endMs, withTime, copy); const rangeDistance = formatLocalizedRangeDistance(draft.startMs, draft.endMs, withTime, copy);
const weekLabels = useMemo(() => localizedWeekLabels(locale), [locale]); const weekLabels = useMemo(() => localizedWeekLabels(locale), [locale]);
const hasRange = Boolean(range.startMs || range.endMs); const hasRange = Boolean(range.startMs || range.endMs);
// CSS Modules do not pass through the external-admin Emotion RTL cache. Keep
// popover anchoring and directional glyphs explicit so Arabic mirrors as a unit.
const rtl = direction === "rtl";
const horizontalOrigin = rtl ? "right" : "left";
const PreviousMonthIcon = rtl ? ChevronRightOutlined : ChevronLeftOutlined;
const NextMonthIcon = rtl ? ChevronLeftOutlined : ChevronRightOutlined;
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
@ -183,9 +189,9 @@ export function TimeRangeFilter({ className = "", disabled = false, label = "时
<Popover <Popover
anchorEl={anchorEl} anchorEl={anchorEl}
open={open} open={open}
anchorOrigin={{ horizontal: "left", vertical: "bottom" }} anchorOrigin={{ horizontal: horizontalOrigin, vertical: "bottom" }}
slotProps={{ paper: { className: styles.paper } }} slotProps={{ paper: { className: styles.paper, dir: direction } }}
transformOrigin={{ horizontal: "left", vertical: "top" }} transformOrigin={{ horizontal: horizontalOrigin, vertical: "top" }}
onClose={closePopover} onClose={closePopover}
> >
<form className={styles.panel} onSubmit={submitDraft}> <form className={styles.panel} onSubmit={submitDraft}>
@ -233,11 +239,11 @@ export function TimeRangeFilter({ className = "", disabled = false, label = "时
</div> </div>
<div className={styles.calendarHeader}> <div className={styles.calendarHeader}>
<IconButton aria-label={copy.previousMonth} size="small" onClick={() => changeMonth(-1)}> <IconButton aria-label={copy.previousMonth} size="small" onClick={() => changeMonth(-1)}>
<ChevronLeftOutlined fontSize="small" /> <PreviousMonthIcon fontSize="small" />
</IconButton> </IconButton>
<span className={styles.calendarTitle}>{monthTitle(viewDate, locale)}</span> <span className={styles.calendarTitle}>{monthTitle(viewDate, locale)}</span>
<IconButton aria-label={copy.nextMonth} size="small" onClick={() => changeMonth(1)}> <IconButton aria-label={copy.nextMonth} size="small" onClick={() => changeMonth(1)}>
<ChevronRightOutlined fontSize="small" /> <NextMonthIcon fontSize="small" />
</IconButton> </IconButton>
</div> </div>
<div className={`${styles.calendarGrid} ${styles.rangeCalendarGrid}`}> <div className={`${styles.calendarGrid} ${styles.rangeCalendarGrid}`}>

View File

@ -9,7 +9,8 @@
width: auto; width: auto;
min-width: 132px !important; min-width: 132px !important;
max-width: min(420px, 100%); max-width: min(420px, 100%);
padding: 7px 28px 3px var(--control-padding-x) !important; padding-block: 7px 3px !important;
padding-inline: var(--control-padding-x) 28px !important;
border-color: var(--border) !important; border-color: var(--border) !important;
background: var(--bg-input-strong) !important; background: var(--bg-input-strong) !important;
box-shadow: none !important; box-shadow: none !important;
@ -29,8 +30,8 @@
.triggerMeta { .triggerMeta {
position: absolute; position: absolute;
top: -7px; inset-block-start: -7px;
left: var(--space-2); inset-inline-start: var(--space-2);
display: inline-flex; display: inline-flex;
max-width: calc(100% - var(--space-4)); max-width: calc(100% - var(--space-4));
align-items: center; align-items: center;
@ -58,15 +59,15 @@
color: var(--text-primary); color: var(--text-primary);
font-size: var(--control-font-size); font-size: var(--control-font-size);
line-height: var(--control-line-height); line-height: var(--control-line-height);
text-align: left; text-align: start;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.triggerChevron { .triggerChevron {
position: absolute; position: absolute;
top: 50%; inset-block-start: 50%;
right: var(--space-2); inset-inline-end: var(--space-2);
color: var(--text-tertiary); color: var(--text-tertiary);
transform: translateY(-50%); transform: translateY(-50%);
} }
@ -94,8 +95,7 @@
.quickButton { .quickButton {
min-width: 0 !important; min-width: 0 !important;
padding-right: var(--space-2) !important; padding-inline: var(--space-2) !important;
padding-left: var(--space-2) !important;
} }
.rangeFields { .rangeFields {
@ -115,7 +115,7 @@
border-radius: var(--radius-control); border-radius: var(--radius-control);
background: var(--bg-input-strong); background: var(--bg-input-strong);
color: var(--text-primary); color: var(--text-primary);
text-align: left; text-align: start;
transition: transition:
border-color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard),
@ -275,11 +275,15 @@
} }
.dayRangeStart { .dayRangeStart {
border-radius: var(--radius-control) 0 0 var(--radius-control); border-radius: 0;
border-start-start-radius: var(--radius-control);
border-end-start-radius: var(--radius-control);
} }
.dayRangeEnd { .dayRangeEnd {
border-radius: 0 var(--radius-control) var(--radius-control) 0; border-radius: 0;
border-start-end-radius: var(--radius-control);
border-end-end-radius: var(--radius-control);
} }
.dayRangeStart.dayRangeEnd { .dayRangeStart.dayRangeEnd {
@ -304,8 +308,8 @@
.daySelected::after { .daySelected::after {
position: absolute; position: absolute;
top: 1px; inset-block-start: 1px;
right: 3px; inset-inline-end: 3px;
color: currentcolor; color: currentcolor;
content: attr(data-range-edge); content: attr(data-range-edge);
font-size: 8px; font-size: 8px;