diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index feba21d..7fdcb80 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,46 @@ export default function App() { const { colorPrimary, themeMode, initTheme } = useThemeStore(); const { i18n } = useTranslation(); const antdLocale = useMemo(() => (i18n.language === "en-US" ? enUS : zhCN), [i18n.language]); + const isTechTheme = themeMode === "tech"; + + const antdTheme = useMemo(() => ({ + algorithm: theme.defaultAlgorithm, + token: { + colorPrimary, + borderRadius: 4, + colorBgLayout: isTechTheme ? "#edf6ff" : "#f5f6fa", + colorBgContainer: isTechTheme ? "#ffffff" : "#ffffff", + colorBgElevated: isTechTheme ? "#ffffff" : "#ffffff", + colorBorder: isTechTheme ? "rgba(32, 111, 218, 0.18)" : "#e6e6e6", + colorText: isTechTheme ? "#102033" : "#333333", + colorTextSecondary: isTechTheme ? "#5f728f" : "#9095a1", + colorFillSecondary: isTechTheme ? "rgba(32, 111, 218, 0.08)" : "rgba(0, 0, 0, 0.04)", + }, + components: { + Card: { + borderRadiusLG: 4 + }, + Button: { + borderRadius: 4 + }, + Input: { + borderRadius: 4 + }, + Select: { + borderRadius: 4 + }, + Modal: { + borderRadiusLG: 4 + }, + Drawer: { + colorBgElevated: "#ffffff" + }, + Segmented: { + itemSelectedBg: isTechTheme ? "#ffffff" : "#ffffff", + itemSelectedColor: isTechTheme ? colorPrimary : "#333333" + } + } + }), [colorPrimary, isTechTheme]); useEffect(() => { initTheme(); @@ -41,31 +81,7 @@ export default function App() { return ( diff --git a/frontend/src/components/ThemeSelector/ThemeSelector.css b/frontend/src/components/ThemeSelector/ThemeSelector.css new file mode 100644 index 0000000..4b7fe4c --- /dev/null +++ b/frontend/src/components/ThemeSelector/ThemeSelector.css @@ -0,0 +1,144 @@ +.theme-selector-container { + display: inline-flex; + align-items: center; +} + +.theme-selector-trigger { + width: 34px; + height: 34px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--app-text-main); + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + transition: + color 0.18s ease, + background-color 0.18s ease, + border-color 0.18s ease; +} + +.theme-selector-trigger:hover, +.theme-selector-trigger:focus-visible { + color: var(--app-primary-color); + background: rgba(var(--app-primary-rgb), 0.08); + border-color: rgba(var(--app-primary-rgb), 0.16); + outline: none; +} + +.theme-selector-trigger .anticon { + font-size: 18px; + line-height: 1; +} + +.theme-settings-drawer-root .ant-drawer-mask { + background: rgba(15, 23, 42, 0.42); +} + +.theme-settings-drawer .ant-drawer-header { + min-height: 56px; + padding: 0 24px; + border-bottom: 1px solid var(--app-border-color); + background: var(--app-surface-color); +} + +.theme-settings-drawer .ant-drawer-title, +.theme-settings-drawer .ant-typography { + color: var(--app-text-main); +} + +.theme-settings-drawer .ant-drawer-close { + color: var(--app-text-secondary); +} + +.theme-settings-drawer .ant-drawer-close:hover { + color: var(--app-primary-color); +} + +.theme-settings-drawer .ant-drawer-body { + padding: 24px; + background: var(--app-bg-surface); +} + +.theme-settings-content { + width: 100%; +} + +.theme-settings-section { + min-width: 0; +} + +.theme-settings-label { + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 8px; + color: var(--app-text-main); +} + +.theme-settings-label .anticon { + color: var(--app-primary-color); +} + +.theme-color-picker { + width: 100%; +} + +.theme-color-picker .ant-color-picker-trigger { + width: 100%; + justify-content: center; + border-color: var(--app-border-color); + background: var(--app-surface-color); +} + +.theme-style-segmented.ant-segmented { + padding: 4px; + background: var(--app-bg-surface-soft); + border: 1px solid var(--app-border-color); +} + +.theme-style-segmented .ant-segmented-item { + min-width: 0; + color: var(--app-text-secondary); +} + +.theme-style-segmented .ant-segmented-item-label { + padding-inline: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.theme-style-segmented .ant-segmented-item-selected { + color: var(--app-text-main); +} + +:root[data-theme="tech"] .theme-selector-trigger:hover, +:root[data-theme="tech"] .theme-selector-trigger:focus-visible { + background: rgba(var(--app-primary-rgb), 0.14); + border-color: rgba(var(--app-primary-rgb), 0.28); +} + +:root[data-theme="tech"] .theme-settings-drawer-root .ant-drawer-mask { + background: rgba(15, 23, 42, 0.34); + backdrop-filter: blur(2px); +} + +:root[data-theme="tech"] .theme-color-picker .ant-color-picker-trigger { + background: #ffffff; +} + +:root[data-theme="tech"] .theme-settings-drawer .ant-drawer-header { + background: linear-gradient(90deg, #ffffff, #f0f7ff); +} + +:root[data-theme="tech"] .theme-settings-drawer .ant-drawer-body { + background: + linear-gradient(rgba(255, 255, 255, 0.72) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.72) 1px, transparent 1px), + linear-gradient(180deg, #f8fbff 0%, #eef6ff 100%); + background-size: 28px 28px, 28px 28px, auto; +} diff --git a/frontend/src/components/ThemeSelector/ThemeSelector.tsx b/frontend/src/components/ThemeSelector/ThemeSelector.tsx index 3ce4228..cf8e52c 100644 --- a/frontend/src/components/ThemeSelector/ThemeSelector.tsx +++ b/frontend/src/components/ThemeSelector/ThemeSelector.tsx @@ -3,6 +3,7 @@ import { FormatPainterOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { useState } from 'react'; import { useThemeStore, type ThemeMode } from '@/store/themeStore'; +import './ThemeSelector.css'; const { Text } = Typography; @@ -11,50 +12,58 @@ export default function ThemeSelector() { const [open, setOpen] = useState(false); const { colorPrimary, themeMode, setColorPrimary, setThemeMode } = useThemeStore(); - const themeOptions = [ - { label: t('theme.default', 'Default'), value: 'default' }, + const themeOptions: Array<{ label: string; value: ThemeMode }> = [ { label: t('theme.minimal', 'Minimal'), value: 'minimal' }, { label: t('theme.tech', 'Tech'), value: 'tech' }, ]; return (
- setOpen(true)}> - - + setOpen(false)} open={open} width={300} + rootClassName="theme-settings-drawer-root" + className="theme-settings-drawer" > - -
-
- + +
+
+ {t('theme.color', 'Theme Color')}
setColorPrimary(color.toHexString())} showText - style={{ width: '100%' }} /> -
+ -
-
- +
+
+ {t('theme.style', 'Style Mode')}
setThemeMode(value as ThemeMode)} /> -
+
diff --git a/frontend/src/components/shared/DataListPanel/DataListPanel.css b/frontend/src/components/shared/DataListPanel/DataListPanel.css index cc35036..8d675d1 100644 --- a/frontend/src/components/shared/DataListPanel/DataListPanel.css +++ b/frontend/src/components/shared/DataListPanel/DataListPanel.css @@ -9,7 +9,7 @@ overflow: hidden; padding: 12px 16px 10px; border-radius: 4px; - background-color: #fff; + background-color: var(--app-surface-color, #fff); } .data-list-panel--auto { @@ -155,22 +155,22 @@ .data-list-panel__table-area .ant-table-thead > tr > th { height: 45px; - color: #000; + color: var(--app-text-main, #000); font-size: 14px; font-weight: 600; - background: #fafafa !important; + background: var(--app-bg-surface-soft, #fafafa) !important; } .data-list-panel__table-area .ant-table-tbody > tr > td { height: 47px; - color: #000; + color: var(--app-text-main, #000); font-size: 14px; font-weight: 400; } .data-list-panel__table-area .ant-table-tbody > tr:not(.row-selected):not(.ant-table-row-selected):not(.dict-type-row-selected):hover > td, .data-list-panel__table-area .ant-table-tbody > tr:not(.row-selected):not(.ant-table-row-selected):not(.dict-type-row-selected) > td.ant-table-cell-row-hover { - background: #fff !important; + background: var(--app-surface-color, #fff) !important; } .data-list-panel__table-area .ant-table-cell { @@ -205,7 +205,7 @@ min-width: 0; overflow: visible; border-radius: 0; - background: #fff; + background: var(--app-surface-color, #fff); box-sizing: border-box; min-height: 50px; } @@ -222,11 +222,24 @@ .data-list-panel__footer .app-pagination-container, .data-list-panel__footer .app-pagination-container .ant-pagination, .data-list-panel__footer .app-pagination-total { - color: #333; + color: var(--app-text-main, #333); font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 14px; } +:root[data-theme="tech"] .data-list-panel { + border: 1px solid rgba(22, 119, 255, 0.14); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(246, 251, 255, 0.92)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +:root[data-theme="tech"] .data-list-panel__table-area .ant-table-thead > tr > th { + background: + linear-gradient(180deg, rgba(var(--app-primary-rgb), 0.12), rgba(47, 211, 255, 0.08)) !important; + border-bottom-color: rgba(22, 119, 255, 0.18) !important; +} + @media (max-width: 768px) { .data-list-panel { padding: 12px; diff --git a/frontend/src/components/shared/SectionCard/SectionCard.css b/frontend/src/components/shared/SectionCard/SectionCard.css index f8bc7e2..d7f936f 100644 --- a/frontend/src/components/shared/SectionCard/SectionCard.css +++ b/frontend/src/components/shared/SectionCard/SectionCard.css @@ -175,6 +175,23 @@ overflow: visible; } +:root[data-theme="tech"] .section-card { + background-image: + radial-gradient(circle at 90% 0%, rgba(47, 211, 255, 0.18), transparent 28%), + linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(232, 244, 255, 0.88)), + linear-gradient(rgba(22, 119, 255, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(22, 119, 255, 0.08) 1px, transparent 1px); + background-size: auto, auto, 28px 28px, 28px 28px; + box-shadow: var(--app-shadow); + border-color: rgba(22, 119, 255, 0.24); +} + +:root[data-theme="tech"] .section-card__content { + background: + linear-gradient(180deg, rgba(246, 251, 255, 0.94), rgba(232, 244, 255, 0.9)); + border: 1px solid rgba(22, 119, 255, 0.12); +} + @media (max-width: 768px) { .section-card { padding: 12px; diff --git a/frontend/src/index.css b/frontend/src/index.css index d2f97e6..1755f96 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1325,8 +1325,8 @@ body::after { /* web-fe baseline overrides */ :root, -:root[data-theme="minimal"], -:root[data-theme="tech"] { +:root[data-theme="default"], +:root[data-theme="minimal"] { --app-bg-main: #f5f6fa; --app-bg-overlay: none; --app-bg-card: #fff; @@ -1353,9 +1353,48 @@ body::after { --app-form-drawer-max-width: calc(100vw - 48px); } +:root[data-theme="tech"] { + --app-bg-main: + radial-gradient(circle at 16% 8%, rgba(22, 119, 255, 0.2), transparent 26%), + radial-gradient(circle at 88% 18%, rgba(47, 211, 255, 0.22), transparent 24%), + linear-gradient(rgba(255, 255, 255, 0.62) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.62) 1px, transparent 1px), + linear-gradient(135deg, #f7fbff 0%, #e6f3ff 45%, #f5fbff 100%); + --app-bg-overlay: none; + --app-bg-overlay-size: 32px 32px; + --app-bg-card: rgba(255, 255, 255, 0.9); + --app-bg-layout: #e6f3ff; + --app-text-main: #0e2f56; + --app-text-secondary: #4f6f98; + --app-border-color: rgba(22, 119, 255, 0.22); + --app-shadow: 0 14px 34px rgba(22, 119, 255, 0.12); + --app-bg-page: #e6f3ff; + --app-bg-surface: #f6fbff; + --app-bg-surface-soft: #e7f3ff; + --app-bg-surface-strong: #ffffff; + --app-surface-color: rgba(255, 255, 255, 0.92); + --app-text-muted: #5f7da3; + --item-hover-bg: rgba(22, 119, 255, 0.12); + --text-color-secondary: #4f6f98; + --link-color: #1677ff; + --list-table-scroll-y: 100%; + --meeting-status-border: rgba(22, 119, 255, 0.22); + --meeting-status-bg: rgba(22, 119, 255, 0.08); + --meeting-status-color: #1677ff; + --meeting-source-color: #1677ff; + --meeting-progress-color: #1677ff; + --app-form-drawer-width: 600px; + --app-form-drawer-max-width: calc(100vw - 48px); +} + html, body { - background: #f5f6fa; + background: var(--app-bg-layout); +} + +:root[data-theme="tech"] body { + background: var(--app-bg-main); + background-size: auto, auto, var(--app-bg-overlay-size), var(--app-bg-overlay-size), auto; } body { @@ -1370,8 +1409,8 @@ body::after, } .ant-layout-sider { - background: #fff !important; - border-right: 1px solid #f5f6fa; + background: var(--app-surface-color) !important; + border-right: 1px solid var(--app-bg-layout); backdrop-filter: none; } @@ -1389,11 +1428,11 @@ body::after, .ant-menu-light { background: transparent !important; - color: #333 !important; + color: var(--app-text-main) !important; } .ant-menu-light .ant-menu-item a { - color: #333 !important; + color: var(--app-text-main) !important; } .app-page__filter-card, @@ -1402,7 +1441,7 @@ body::after, border: none; border-radius: 4px !important; box-shadow: none; - background: #fff; + background: var(--app-surface-color); backdrop-filter: none; } @@ -1425,16 +1464,16 @@ body::after, } .app-page__empty-state { - background: #fff; + background: var(--app-surface-color); border-radius: 4px; - border: 1px dashed #d9d9d9; + border: 1px dashed var(--app-border-color); backdrop-filter: none; } .ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination, .app-global-pagination.ant-pagination { padding: 8px 0 0; - background: #fff; + background: var(--app-surface-color); border-top: none; border-radius: 0; } diff --git a/frontend/src/layouts/AppLayout.css b/frontend/src/layouts/AppLayout.css index 6444236..9789502 100644 --- a/frontend/src/layouts/AppLayout.css +++ b/frontend/src/layouts/AppLayout.css @@ -2,19 +2,19 @@ min-height: 100vh; height: 100vh; overflow: hidden; - background: #f5f6fa; + background: var(--app-bg-layout); } .main-shell { min-height: 0; flex: 1; - background: #f5f6fa; + background: var(--app-bg-layout); } .main-header { height: 64px; padding: 0 24px; - background: #fff; + background: var(--app-surface-color); display: flex; align-items: center; justify-content: space-between; @@ -38,7 +38,7 @@ } .app-header-logo-title { - color: #1f2a37; + color: var(--app-text-main); font-size: 18px; font-weight: 600; line-height: 1; @@ -58,9 +58,9 @@ position: relative; height: calc(100vh - 64px); overflow: visible; - border-right: 1px solid #f5f6fa; - border-top: 2px solid #f5f6fa; - background: #fff !important; + border-right: 1px solid var(--app-bg-layout); + border-top: 2px solid var(--app-bg-layout); + background: var(--app-surface-color) !important; transition: flex-basis 0.24s ease, max-width 0.24s ease, @@ -81,18 +81,18 @@ .main-sider .ant-layout-sider-children::-webkit-scrollbar-track, .main-menu::-webkit-scrollbar-track { - background: #f8f8f8 !important; + background: var(--app-bg-surface-soft) !important; } .main-sider .ant-layout-sider-children::-webkit-scrollbar-thumb, .main-menu::-webkit-scrollbar-thumb { - background: #f8f8f8 !important; + background: rgba(144, 149, 161, 0.34) !important; border-radius: 3px !important; } .main-sider .ant-layout-sider-children::-webkit-scrollbar-thumb:hover, .main-menu::-webkit-scrollbar-thumb:hover { - background: #e0e0e0 !important; + background: rgba(144, 149, 161, 0.58) !important; } .main-sider .ant-layout-sider-trigger { @@ -181,9 +181,9 @@ border-radius: 50%; text-align: center; line-height: 35px; - border: 1px solid #f0f0f0; + border: 1px solid var(--app-border-color); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); - background: #fff; + background: var(--app-surface-color); transition: border-color 0.18s ease, box-shadow 0.18s ease, @@ -204,8 +204,8 @@ height: 100%; min-width: 0; padding: 0; - background: #fff; - color: #666; + background: var(--app-surface-color); + color: var(--app-text-secondary); border: none; box-shadow: none; display: flex; @@ -243,8 +243,8 @@ } .main-sider .collapsedCenter .trigger:hover { - color: #1890ff; - background: #fff; + color: var(--app-primary-color); + background: var(--app-bg-surface-soft); } .main-menu { @@ -253,13 +253,13 @@ overflow-x: hidden; border-inline-end: none !important; padding-top: 8px; - background: #fff; + background: var(--app-surface-color); } .main-content-layout { min-width: 0; min-height: 0; - background: #f5f6fa; + background: var(--app-bg-layout); } .main-content { @@ -267,5 +267,73 @@ height: calc(100vh - 64px); min-height: 0; overflow: hidden; - background: #f5f6fa; + background: var(--app-bg-layout); +} + +:root[data-theme="tech"] .main-header, +:root[data-theme="tech"] .main-sider, +:root[data-theme="tech"] .main-menu, +:root[data-theme="tech"] .collapsedCenter { + box-shadow: inset 0 -1px 0 rgba(22, 119, 255, 0.08), 0 8px 24px rgba(37, 99, 235, 0.06); +} + +:root[data-theme="tech"] .main-layout, +:root[data-theme="tech"] .main-shell, +:root[data-theme="tech"] .main-content-layout, +:root[data-theme="tech"] .main-content { + background: + radial-gradient(circle at 12% 8%, rgba(22, 119, 255, 0.18), transparent 28%), + radial-gradient(circle at 92% 22%, rgba(47, 211, 255, 0.18), transparent 24%), + linear-gradient(rgba(255, 255, 255, 0.62) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.62) 1px, transparent 1px), + linear-gradient(135deg, #f7fbff 0%, #e6f3ff 48%, #f5fbff 100%); + background-size: auto, auto, 32px 32px, 32px 32px, auto; +} + +:root[data-theme="tech"] .main-header { + background: + linear-gradient(90deg, rgba(var(--app-primary-rgb), 0.1), rgba(47, 211, 255, 0.08)), + linear-gradient(90deg, rgba(255, 255, 255, 0.98), rgba(230, 243, 255, 0.96)); + border-bottom: 1px solid rgba(22, 119, 255, 0.16); +} + +:root[data-theme="tech"] .main-sider, +:root[data-theme="tech"] .main-menu { + background: + linear-gradient(180deg, rgba(var(--app-primary-rgb), 0.1), rgba(47, 211, 255, 0.08)), + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(224, 240, 255, 0.94)) !important; +} + +:root[data-theme="tech"] .main-sider::before { + content: ""; + position: absolute; + inset: 0 0 auto; + height: 3px; + background: linear-gradient(90deg, var(--app-primary-color), rgba(47, 211, 255, 0.9)); +} + +:root[data-theme="tech"] .collapsedCenter, +:root[data-theme="tech"] .main-sider .collapsedCenter .trigger { + background: #ffffff; +} + +:root[data-theme="tech"] .main-sider .ant-menu-light, +:root[data-theme="tech"] .main-sider .ant-menu-light .ant-menu-submenu-title, +:root[data-theme="tech"] .main-sider .ant-menu-light .ant-menu-item { + color: var(--app-text-main) !important; + background: transparent !important; +} + +:root[data-theme="tech"] .main-sider .ant-menu-light .ant-menu-item-selected, +:root[data-theme="tech"] .main-sider .ant-menu-light .ant-menu-submenu-selected > .ant-menu-submenu-title { + color: var(--app-primary-color) !important; + background: + linear-gradient(90deg, rgba(var(--app-primary-rgb), 0.18), rgba(47, 211, 255, 0.12)) !important; + box-shadow: inset 3px 0 0 var(--app-primary-color); +} + +:root[data-theme="tech"] .main-sider .ant-menu-light .ant-menu-item:hover, +:root[data-theme="tech"] .main-sider .ant-menu-light .ant-menu-submenu-title:hover { + color: var(--app-primary-color) !important; + background: rgba(var(--app-primary-rgb), 0.08) !important; } diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index 7fe0daf..57f4432 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -249,6 +249,10 @@ "uploadIcon": "Upload Icon", "uploadBackground": "Upload Background", "previewHint": "Preview appears here after upload", + "previewMask": "Click to enlarge", + "logoHint": "Used for the top-left brand area. A transparent horizontal image is recommended.", + "iconHint": "Used for browser tabs and small-size recognition. A square image is recommended.", + "loginBgHint": "Used for the login page background. A clear wide image is recommended.", "icpPlaceholder": "Example: ICP 12345678", "copyrightPlaceholder": "Example: © 2026 UnisBase Team" }, diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index 1f9afcd..6e51cfd 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -249,6 +249,10 @@ "uploadIcon": "上传图标", "uploadBackground": "上传背景图", "previewHint": "上传后将在此处预览", + "previewMask": "点击放大预览", + "logoHint": "用于左上角品牌展示,建议使用透明底横版图", + "iconHint": "用于浏览器标签和小尺寸识别,建议使用正方形图", + "loginBgHint": "用于登录页背景展示,建议使用清晰横版大图", "icpPlaceholder": "例如:京 ICP 12345678 号", "copyrightPlaceholder": "例如:© 2026 UnisBase Team" }, diff --git a/frontend/src/pages/business/MeetingPointsManagement.css b/frontend/src/pages/business/MeetingPointsManagement.css index 7cff186..a06ddb8 100644 --- a/frontend/src/pages/business/MeetingPointsManagement.css +++ b/frontend/src/pages/business/MeetingPointsManagement.css @@ -1,7 +1,7 @@ .meeting-points-page { padding: 8px; min-width: 0; - background: #f5f6fa; + background: var(--app-bg-layout, #f5f6fa); } .meeting-points-page > .page-container__body { diff --git a/frontend/src/pages/business/TenantMeetingPointsSettings.css b/frontend/src/pages/business/TenantMeetingPointsSettings.css index 659ad0f..b194033 100644 --- a/frontend/src/pages/business/TenantMeetingPointsSettings.css +++ b/frontend/src/pages/business/TenantMeetingPointsSettings.css @@ -1,7 +1,6 @@ .tenant-meeting-points { - padding: 8px; min-width: 0; - background: #f5f6fa; + background: var(--app-bg-layout, #f5f6fa); } .tenant-meeting-points > .page-container__body { @@ -12,33 +11,216 @@ background: transparent; } +.tenant-meeting-points__tenant-section { + max-width: 100%; +} + +.tenant-meeting-points__tenant-content { + padding: 12px; +} + .tenant-meeting-points__tenant-card { + display: flex; + flex-direction: column; + gap: 16px; width: 100%; - padding: 8px 12px; + padding: 18px; + box-sizing: border-box; + border: 1px solid #edf0f5; border-radius: 4px; background: #fff; + box-shadow: 0 8px 24px rgba(24, 38, 73, 0.04); } .tenant-meeting-points__tenant-heading { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; } -.tenant-meeting-points__mode-card { +.tenant-meeting-points__tenant-identity { + display: flex; + align-items: center; + min-width: 0; + gap: 12px; +} + +.tenant-meeting-points__tenant-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 44px; + width: 44px; + height: 44px; + border-radius: 4px; + color: #2f6cf6; + font-size: 22px; + background: #edf4ff; +} + +.tenant-meeting-points__tenant-name { + display: block; + color: var(--app-text-main, #202733); + font-size: 18px; + line-height: 26px; +} + +.tenant-meeting-points__tenant-actions { + justify-content: flex-end; +} + +.tenant-meeting-points__mode-panel { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 16px; + border: 1px solid transparent; border-radius: 4px; } -.tenant-meeting-points__mode-card--enabled { - background: #f6ffed; +.tenant-meeting-points__mode-panel--enabled { + border-color: #b7eb8f; + background: linear-gradient(90deg, #f6ffed 0%, #fbfffa 100%); } -.tenant-meeting-points__mode-card--unlimited { - background: #fff7e6; +.tenant-meeting-points__mode-panel--unlimited { + border-color: #ffd591; + background: linear-gradient(90deg, #fff7e6 0%, #fffdf7 100%); +} + +.tenant-meeting-points__mode-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 30px; + width: 30px; + height: 30px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.76); + font-size: 16px; +} + +.tenant-meeting-points__mode-panel--enabled .tenant-meeting-points__mode-icon { + color: #389e0d; +} + +.tenant-meeting-points__mode-panel--unlimited .tenant-meeting-points__mode-icon { + color: #d46b08; +} + +.tenant-meeting-points__mode-copy { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; } .tenant-meeting-points__stats { + display: grid; + grid-template-columns: repeat(3, minmax(180px, 1fr)); + gap: 12px; width: 100%; } + +.tenant-meeting-points__stat-card { + position: relative; + min-width: 0; + padding: 16px; + overflow: hidden; + border: 1px solid #edf0f5; + border-radius: 4px; + background: #fafbfe; +} + +.tenant-meeting-points__stat-card::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 3px; + background: #d5dce8; +} + +.tenant-meeting-points__stat-card--primary { + background: #f4f8ff; +} + +.tenant-meeting-points__stat-card--primary::before { + background: #2f6cf6; +} + +.tenant-meeting-points__stat-card .ant-statistic-title { + margin-bottom: 8px; + color: var(--app-text-secondary, #778195); + font-size: 13px; + line-height: 20px; +} + +.tenant-meeting-points__stat-card .ant-statistic-content { + color: var(--app-text-main, #202733); + font-size: 24px; + line-height: 32px; +} + +.tenant-meeting-points__meta { + display: grid; + grid-template-columns: repeat(2, minmax(220px, 1fr)); + gap: 10px; + padding-top: 2px; +} + +.tenant-meeting-points__meta-item { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + padding: 10px 12px; + border: 1px solid #edf0f5; + border-radius: 4px; + color: var(--app-text-secondary, #778195); + background: #fbfcff; + font-size: 14px; + line-height: 22px; +} + +.tenant-meeting-points__meta-item span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 992px) { + .tenant-meeting-points__stats, + .tenant-meeting-points__meta { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .tenant-meeting-points__tenant-content { + padding: 8px; + } + + .tenant-meeting-points__tenant-card { + padding: 14px; + } + + .tenant-meeting-points__tenant-heading { + flex-direction: column; + } + + .tenant-meeting-points__tenant-actions, + .tenant-meeting-points__tenant-actions .ant-btn { + width: 100%; + } + + .tenant-meeting-points__tenant-actions .ant-space-item { + width: 100%; + } + + .tenant-meeting-points__mode-panel { + padding: 12px; + } +} diff --git a/frontend/src/pages/business/TenantMeetingPointsSettings.tsx b/frontend/src/pages/business/TenantMeetingPointsSettings.tsx index f28a896..0200aa7 100644 --- a/frontend/src/pages/business/TenantMeetingPointsSettings.tsx +++ b/frontend/src/pages/business/TenantMeetingPointsSettings.tsx @@ -1,4 +1,11 @@ -import { ReloadOutlined, SearchOutlined } from "@ant-design/icons"; +import { + CheckCircleOutlined, + ClockCircleOutlined, + DatabaseOutlined, + ReloadOutlined, + SearchOutlined, + SafetyCertificateOutlined, +} from "@ant-design/icons"; import { getCurrentUser } from "@/api"; import AppPagination from "@/components/shared/AppPagination"; import DataListPanel from "@/components/shared/DataListPanel"; @@ -13,7 +20,7 @@ import { updateTenantMeetingPointsBalanceCheck, } from "@/api/business/meetingPoints"; import type { UserProfile } from "@/types"; -import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd"; +import { Button, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd"; import { useEffect, useState } from "react"; import "./TenantMeetingPointsSettings.css"; @@ -207,67 +214,95 @@ export default function TenantMeetingPointsSettings() { if (!currentTenantSetting) { return null; } + const isBalanceCheckEnabled = currentTenantSetting.balanceCheckEnabled; return ( - +
- - {currentTenantSetting.tenantName || "当前租户"} - 租户编码:{currentTenantSetting.tenantCode || "-"} +
+
+ +
+ + + {currentTenantSetting.tenantName || "当前租户"} + + 租户编码:{currentTenantSetting.tenantCode || "-"} + +
+ + {renderStatusTag(isBalanceCheckEnabled)} + {canUpdateBalanceCheck ? ( + + ) : null} + - {renderStatusTag(currentTenantSetting.balanceCheckEnabled)}
- - - - {currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"} - +
+ +
+
+ {isBalanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"} - {currentTenantSetting.balanceCheckEnabled + {isBalanceCheckEnabled ? "后续会议提交将按当前账面余额执行拦截与扣减。" : "后续会议只记录消耗与流水,不扣减账面余额,积分分配也会被禁用。"} - - +
+
- - - - - +
+
+ +
+
+ } + /> +
+
+ +
+
- - 最近切换时间:{formatDateTime(currentTenantSetting.lastSwitchAt)} - 最近切换人:{currentTenantSetting.lastSwitchByName || "-"} - - - - {canUpdateBalanceCheck ? ( - - ) : null} - - -
+
+
+ + 最近切换时间:{formatDateTime(currentTenantSetting.lastSwitchAt)} +
+
+ + 最近切换人:{currentTenantSetting.lastSwitchByName || "-"} +
+
+
); }; diff --git a/frontend/src/pages/profile/index.less b/frontend/src/pages/profile/index.less index 23b9df1..3b06bfa 100644 --- a/frontend/src/pages/profile/index.less +++ b/frontend/src/pages/profile/index.less @@ -16,180 +16,520 @@ overflow: auto; } -.profile-layout { +.profile-shell { + display: flex; + flex-direction: column; + gap: 14px; min-width: 0; } -.profile-side-col, -.profile-main-col { +.profile-hero { + position: relative; + display: flex; + align-items: stretch; + justify-content: space-between; + gap: 18px; + min-width: 0; + overflow: hidden; + padding: 22px 24px; + border: 1px solid #dce6f8; + border-radius: 8px; + background: + linear-gradient(112deg, rgba(60, 112, 245, 0.11), rgba(255, 255, 255, 0.68) 42%, rgba(25, 135, 84, 0.08)), + #ffffff; +} + +.profile-hero::after { + content: ""; + position: absolute; + right: -54px; + top: -70px; + width: 280px; + height: 180px; + border: 1px solid rgba(60, 112, 245, 0.14); + border-radius: 8px; + background: repeating-linear-gradient( + -35deg, + rgba(60, 112, 245, 0.08) 0, + rgba(60, 112, 245, 0.08) 1px, + transparent 1px, + transparent 10px + ); + transform: rotate(8deg); + pointer-events: none; +} + +.profile-hero__identity { + position: relative; + z-index: 1; +} + +.profile-hero__identity { + display: flex; + align-items: center; + gap: 18px; + min-width: 0; +} + +.profile-avatar-button { + position: relative; + flex: 0 0 auto; + width: 96px; + height: 96px; + padding: 0; + border: none; + border-radius: 50%; + background: transparent; + cursor: pointer; +} + +.profile-avatar-button:focus-visible { + outline: 3px solid rgba(60, 112, 245, 0.28); + outline-offset: 4px; +} + +.profile-hero__avatar.ant-avatar { + width: 88px; + height: 88px; + border: 4px solid #ffffff; + background: #3c70f5; + box-shadow: 0 10px 28px rgba(60, 112, 245, 0.22); +} + +.profile-avatar-button__badge { + position: absolute; + right: 4px; + bottom: 5px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 2px solid #ffffff; + border-radius: 50%; + color: #ffffff; + background: #198754; + box-shadow: 0 8px 18px rgba(25, 135, 84, 0.2); +} + +.profile-hero__copy { + min-width: 0; +} + +.profile-hero__eyebrow, +.profile-panel__eyebrow { + color: #64748b; + font-size: 12px; + font-weight: 600; + line-height: 18px; + letter-spacing: 0; + text-transform: uppercase; +} + +.profile-hero__title.ant-typography { + margin: 4px 0 2px; + color: #1f2937; + font-size: 26px; + line-height: 34px; +} + +.profile-hero__username.ant-typography { + display: block; + color: #516070; +} + +.profile-hero__tags { + margin-top: 12px; +} + +.profile-workbench { + display: grid; + grid-template-columns: minmax(292px, 360px) minmax(0, 1fr); + gap: 14px; min-width: 0; } .profile-side-stack { - width: 100%; + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; } -.profile-panel-card.ant-card { - border: 1px solid #e6e6e6; - border-radius: 4px; - box-shadow: none; +.profile-panel { + min-width: 0; + border: 1px solid #e5e9f2; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 8px 22px rgba(30, 41, 59, 0.04); } -.profile-panel-card.ant-card .ant-card-body { +.profile-overview-panel, +.profile-security-panel { padding: 18px; } -.profile-summary-card, -.profile-help-card, -.profile-tabs-card { - overflow: hidden; -} - -.profile-summary-card__header { +.profile-panel__head, +.profile-editor-panel__head, +.profile-form-intro { display: flex; - flex-direction: column; - align-items: center; + align-items: flex-start; + justify-content: space-between; gap: 16px; - padding-bottom: 20px; + min-width: 0; } -.profile-summary-card__avatar-trigger { - width: fit-content; - cursor: pointer; +.profile-panel__title.ant-typography, +.profile-form-intro .ant-typography { + margin: 2px 0 0; + color: #1f2937; } -.profile-summary-card__avatar.ant-avatar { - background: #3c70f5; - box-shadow: none; -} - -.profile-summary-card__identity { - text-align: center; -} - -.profile-summary-card__title.ant-typography { - margin: 0 0 4px; -} - -.profile-summary-card__tags { +.profile-panel__icon { + display: inline-flex; + align-items: center; justify-content: center; - width: 100%; - margin: 0; + flex: 0 0 auto; + width: 36px; + height: 36px; + border-radius: 8px; + color: #3c70f5; + background: #eef4ff; + font-size: 18px; } -.profile-summary-card__meta { - margin-top: 16px; - padding-top: 20px; - border-top: 1px solid #f0f0f0; -} - -.profile-summary-card__meta .ant-descriptions-view { - border-radius: 4px; - overflow: hidden; -} - -.profile-summary-card__meta .ant-descriptions-item-label { - width: 96px; - color: #64748b; -} - -.profile-help-card__title.ant-typography { - margin-bottom: 12px; -} - -.profile-help-card__list { +.profile-meta-list { display: flex; flex-direction: column; gap: 10px; - color: #475569; - line-height: 1.7; + margin-top: 18px; } -.profile-help-card__alert { +.profile-meta-list__item { + display: flex; + align-items: flex-start; + gap: 12px; + min-width: 0; + padding: 13px; + border: 1px solid #edf1f7; + border-radius: 8px; + background: #fafbfe; +} + +.profile-meta-list__icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 32px; + height: 32px; + border-radius: 8px; + color: #2463eb; + background: #eef4ff; +} + +.profile-meta-list__content { + min-width: 0; +} + +.profile-meta-list__label { + margin-bottom: 4px; + color: #64748b; + font-size: 12px; + line-height: 18px; +} + +.profile-meta-list__value { + min-width: 0; + color: #1f2937; + font-weight: 600; + line-height: 22px; + overflow-wrap: anywhere; +} + +.profile-security-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 18px; +} + +.profile-security-list > div { + padding-left: 12px; + border-left: 3px solid #d8e4fb; +} + +.profile-security-list strong, +.profile-security-list span { + display: block; +} + +.profile-security-list strong { + margin-bottom: 3px; + color: #1f2937; + font-size: 14px; + line-height: 22px; +} + +.profile-security-list span { + color: #64748b; + line-height: 22px; +} + +.profile-security-panel__alert, +.profile-security-alert { margin-top: 16px; - border-radius: 4px; + border-radius: 8px; } -.profile-tabs-card .ant-card-body { - padding: 0; +.profile-editor-panel { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.profile-editor-panel__head { + padding: 18px 20px 14px; + border-bottom: 1px solid #edf1f7; } .profile-tabs .ant-tabs-nav { margin: 0; - padding: 0 18px; - border-bottom: 1px solid #f0f0f0; - background: #fff; + padding: 0 20px; + border-bottom: 1px solid #edf1f7; + background: #ffffff; +} + +.profile-tabs .ant-tabs-nav::before { + border-bottom: none; +} + +.profile-tabs .ant-tabs-tab { + padding: 14px 0; +} + +.profile-tabs .ant-tabs-tab + .ant-tabs-tab { + margin-left: 28px; } .profile-tabs .ant-tabs-content-holder { - padding: 18px; + padding: 20px; +} + +.profile-form { + max-width: 980px; +} + +.profile-form-intro { + align-items: center; + margin-bottom: 18px; + padding: 14px 16px; + border: 1px solid #edf1f7; + border-radius: 8px; + background: #fafbfe; +} + +.profile-form-intro .ant-typography-secondary { + display: block; + margin-top: 4px; } .profile-form .ant-form-item { - margin-bottom: 20px; + margin-bottom: 18px; } -.profile-upload-panel { +.profile-form .ant-input, +.profile-form .ant-input-affix-wrapper { + border-radius: 6px; +} + +.profile-avatar-control { display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 16px 18px; - border-radius: 4px; - border: 1px dashed #b7cdfd; - background: #f9fafe; + align-items: stretch; + gap: 18px; + box-sizing: border-box; + width: 100%; + min-width: 0; + margin-top: 2px; + padding: 16px; + border: 1px dashed #9fbcfb; + border-radius: 8px; + background: linear-gradient(90deg, rgba(60, 112, 245, 0.07), rgba(25, 135, 84, 0.05)); } -.profile-upload-panel__hint { - margin-top: 4px; +.profile-avatar-control__preview { + flex: 0 0 auto; + width: 132px; + height: 132px; + overflow: hidden; + border: 1px solid #dbe6fb; + border-radius: 8px; + background: #ffffff; +} + +.profile-avatar-control__preview .ant-image, +.profile-avatar-control__image { + display: block; + width: 132px !important; + height: 132px !important; +} + +.profile-avatar-control__image { + object-fit: cover; +} + +.profile-avatar-control__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + height: 100%; + padding: 0; + border: none; + color: #2463eb; + background: #f8fbff; + cursor: pointer; +} + +.profile-avatar-control__empty .anticon { + font-size: 26px; +} + +.profile-avatar-control__empty span { + color: #475569; + font-weight: 600; + line-height: 22px; +} + +.profile-avatar-control__content { + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + min-width: 0; + gap: 14px; + padding: 4px 0; +} + +.profile-avatar-control__title { + margin-bottom: 6px; + color: #1f2937; + font-size: 15px; + font-weight: 600; + line-height: 22px; +} + +.profile-avatar-control__hint { + max-width: 520px; color: #64748b; font-size: 13px; -} - -.profile-security-alert, -.profile-credential-panel__card { - margin-bottom: 20px; -} - -.profile-security-alert { - border-radius: 4px; + line-height: 22px; } .profile-credential-panel { display: flex; flex-direction: column; + gap: 18px; } -.profile-credential-panel__card { - padding: 0; - background: transparent; - box-shadow: none; -} - -.profile-credential-panel__card .ant-card-body { - padding: 0; -} - -.profile-credential-panel__descriptions .ant-descriptions-view { - border-radius: 4px; +.profile-credential-card { + border-radius: 8px; overflow: hidden; } +.profile-credential-panel__descriptions .ant-descriptions-view { + border-radius: 8px; + overflow: hidden; +} + +.profile-credential-panel__descriptions .ant-descriptions-item-label { + width: 180px; + color: #475569; + background: #f8fafc; +} + .profile-tab-actions { - margin: 8px 0 0; + margin: 16px 0 0; + padding-top: 16px; + border-top: 1px solid #edf1f7; +} + +@media (max-width: 1200px) { + .profile-hero { + flex-direction: column; + } + + .profile-workbench { + grid-template-columns: 1fr; + } } @media (max-width: 768px) { - .profile-tabs .ant-tabs-nav { - padding: 0 16px; + .profile-shell { + gap: 12px; } + .profile-hero { + padding: 18px; + } + + .profile-hero__identity { + align-items: flex-start; + } + + .profile-hero__title.ant-typography { + font-size: 22px; + line-height: 30px; + } + + .profile-workbench { + grid-template-columns: 1fr; + } + + .profile-editor-panel__head, + .profile-form-intro, + .profile-avatar-control { + flex-direction: column; + align-items: flex-start; + } + + .profile-avatar-control__preview { + width: 112px; + height: 112px; + } + + .profile-avatar-control__preview .ant-image, + .profile-avatar-control__image { + width: 112px !important; + height: 112px !important; + } + + .profile-tabs .ant-tabs-nav, .profile-tabs .ant-tabs-content-holder { + padding-right: 16px; + padding-left: 16px; + } + + .profile-tabs .ant-tabs-tab + .ant-tabs-tab { + margin-left: 18px; + } +} + +@media (max-width: 520px) { + .profile-hero__identity { + flex-direction: column; + } + + .profile-editor-panel__head { padding: 16px; } - .profile-upload-panel { - align-items: flex-start; - flex-direction: column; + .profile-overview-panel, + .profile-security-panel, + .profile-tabs .ant-tabs-content-holder { + padding: 16px; } } diff --git a/frontend/src/pages/profile/index.tsx b/frontend/src/pages/profile/index.tsx index 14b18dc..7aa6cc3 100644 --- a/frontend/src/pages/profile/index.tsx +++ b/frontend/src/pages/profile/index.tsx @@ -2,10 +2,10 @@ import { Alert, Avatar, Button, - Card, Col, Descriptions, Form, + Image, Input, Row, Space, @@ -17,10 +17,17 @@ import { message } from "antd"; import { + ApartmentOutlined, + CheckCircleOutlined, + CloudUploadOutlined, + IdcardOutlined, KeyOutlined, LockOutlined, + MailOutlined, + PhoneOutlined, QuestionCircleOutlined, ReloadOutlined, + SafetyCertificateOutlined, SaveOutlined, SolutionOutlined, UploadOutlined, @@ -38,6 +45,7 @@ import AvatarCropDialog, { type CropModalState } from "./AvatarCropDialog"; import "./index.less"; const {Paragraph, Text, Title} = Typography; +const AVATAR_UPLOAD_ACCEPT = "image/png,image/jpeg"; export default function Profile() { const { t } = useTranslation(); @@ -109,6 +117,11 @@ export default function Profile() { }; const handleAvatarUpload = (file: File) => { + if (file.type !== "image/png" && file.type !== "image/jpeg") { + message.error("仅支持上传 PNG 或 JPG 图片"); + return Upload.LIST_IGNORE; + } + const reader = new FileReader(); reader.onload = () => { setCropState({ @@ -172,6 +185,43 @@ export default function Profile() { const avatarUrlValue = Form.useWatch("avatarUrl", profileForm) as string | undefined; const avatarUrl = avatarUrlValue?.trim() || undefined; const userStatus = user ? (user.status === 0 ? 禁用 : 启用) : "-"; + const displayName = user?.displayName || "-"; + const username = user?.username || "-"; + const organization = renderValue(user?.orgPath || user?.orgName); + const roleTags = ( + <> + {user?.isPlatformAdmin ? {t("users.platformAdmin")} : null} + {user?.isTenantAdmin ? 租户管理员 : null} + {user && !user.isPlatformAdmin && !user.isTenantAdmin ? {t("profile.standardUser")} : null} + {user && !user.isPlatformAdmin && user.hasPlatformAdminPrivilege ? 可切换平台管理员 : null} + + ); + const profileMetaItems = [ + { + key: "email", + icon: , + label: t("users.email"), + value: renderValue(user?.email) + }, + { + key: "phone", + icon: , + label: t("users.phone"), + value: renderValue(user?.phone) + }, + { + key: "organization", + icon: , + label: t("users.org"), + value: organization + }, + { + key: "status", + icon: , + label: t("common.status"), + value: userStatus + } + ]; return ( @@ -180,95 +230,106 @@ export default function Profile() { description={t("profile.subtitle")} contentClassName="profile-section-content" > - - - - -
- +
+
+ -
+
+ + + +
-
- - {user?.displayName || "-"} + <div className="profile-hero__copy"> + <div className="profile-hero__eyebrow">个人资料</div> + <Title level={3} className="profile-hero__title"> + {displayName} - - @{user?.username || "-"} + + @{username} -
- - - {user?.isPlatformAdmin ? {t("users.platformAdmin")} : null} - {user?.isTenantAdmin ? 租户管理员 : null} - {user && !user.isPlatformAdmin && !user.isTenantAdmin ? - {t("profile.standardUser")} : null} - {user && !user.isPlatformAdmin && user.hasPlatformAdminPrivilege ? - 可切换平台管理员 : null} + + {roleTags}
+
- {renderValue(user?.email)} - }, - { - key: "phone", - label: t("users.phone"), - children: {renderValue(user?.phone)} - }, - { - key: "organization", - label: t("users.org"), - children: renderValue(user?.orgPath || user?.orgName) - }, - { - key: "status", - label: t("common.status"), - children: userStatus - } - ]} - /> -
+ - - - 安全提示 - -
-
头像上传后会立即填充到个人资料表单,保存后对外生效。
-
密码修改成功后会刷新当前会话中的个人资料缓存。
-
Bot 凭证重新生成后,旧密钥会立即失效。
+
+ + +
+
+
+
账户维护
+ 账户设置 +
+ 基础资料、安全密码与接口凭证统一维护
- {user?.pwdResetRequired ? ( - - ) : null} - - - - - - +
+
+ 基础资料 + 用于系统内展示和管理员识别,请保持联系方式准确。 +
+
@@ -305,24 +372,58 @@ export default function Profile() { - - - - - -
-
- {t("profile.uploadAvatar")} -
建议上传方形图片,裁剪后会生成统一头像尺寸。
-
- - - -
+ + + + +
+
+ {avatarUrl ? ( + 头像预览 + ) : ( + + + + )} +
+
+
+
头像图片
+
+ {avatarUrl ? "当前头像已回显,点击图片可放大预览。" : "支持 PNG/JPG 图片,上传后会先裁剪成统一尺寸。"} +
+
+ + + +
+
+ +
@@ -344,6 +445,12 @@ export default function Profile() { ), children: (
+
+
+ 登录安全 + 定期更新密码可以降低账号被误用的风险。 +
+
+
+
+ Bot 接入凭证 + 为自动化工具或 Bot 调用保留独立凭证,便于轮换和审计。 +
+
- +
- +
@@ -500,9 +613,9 @@ export default function Profile() { } ]} /> - - - +
+
+
.ant-card-head { + min-height: 54px; + border-bottom-color: #edf0f5; +} + +.platform-settings-card > .ant-card-body { + padding: 22px 24px 24px; +} + +.platform-settings-card .ant-form-item { + margin-bottom: 0; } .platform-settings-page .ant-row { row-gap: 24px; } +.platform-assets-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.platform-asset-card { + min-width: 0; + display: flex; + flex-direction: column; + gap: 14px; + padding: 16px; + border: 1px solid #edf0f5; + border-radius: 4px; + background: #fbfcff; +} + +.platform-asset-card__header { + min-height: 52px; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.platform-asset-card__title { + color: var(--app-text-main, #333); + font-size: 14px; + font-weight: 600; + line-height: 22px; +} + +.platform-asset-card__description { + margin-top: 4px; + color: var(--app-text-secondary, #9095a1); + font-size: 12px; + line-height: 20px; +} + +.platform-asset-card__preview { + position: relative; + height: 168px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + border: 1px dashed #d8dde8; + border-radius: 4px; + background: #fff; +} + +.platform-asset-card--icon .platform-asset-card__preview { + background-image: + linear-gradient(45deg, #f3f5f9 25%, transparent 25%), + linear-gradient(-45deg, #f3f5f9 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #f3f5f9 75%), + linear-gradient(-45deg, transparent 75%, #f3f5f9 75%); + background-position: 0 0, 0 10px, 10px -10px, -10px 0; + background-size: 20px 20px; +} + +.platform-asset-card--background .platform-asset-card__preview { + height: 188px; +} + +.platform-asset-card__preview .ant-image { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.platform-asset-card__preview .ant-image-img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.platform-asset-card--background .platform-asset-card__preview .ant-image-img { + object-fit: cover; +} + +.platform-asset-card__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + color: #8f96a3; + font-size: 12px; + line-height: 20px; + text-align: center; +} + +.platform-asset-card__empty .anticon { + color: #b4bac6; + font-size: 28px; +} + +.platform-asset-card .ant-upload-wrapper, +.platform-asset-card .ant-upload { + width: 100%; +} + @media (max-height: 1080px) { .platform-settings-scroll { padding-bottom: 48px; } } + +@media (max-width: 1200px) { + .platform-assets-grid { + grid-template-columns: 1fr; + } + + .platform-asset-card__preview, + .platform-asset-card--background .platform-asset-card__preview { + height: 180px; + } +} + +@media (max-width: 768px) { + .platform-settings-card > .ant-card-body { + padding: 16px; + } + + .platform-settings-toolbar { + margin-bottom: 8px; + } +} diff --git a/frontend/src/pages/system/platform-settings/index.tsx b/frontend/src/pages/system/platform-settings/index.tsx index 19590ff..2b5a342 100644 --- a/frontend/src/pages/system/platform-settings/index.tsx +++ b/frontend/src/pages/system/platform-settings/index.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Col, Form, Input, Row, Space, Upload, message } from "antd"; +import { Button, Card, Col, Form, Image, Input, Row, Space, Upload, message } from "antd"; import { FileTextOutlined, GlobalOutlined, PictureOutlined, SaveOutlined, UploadOutlined } from "@ant-design/icons"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -9,18 +9,60 @@ import type { SysPlatformConfig } from "@/types"; import "./index.less"; type PlatformFormValues = SysPlatformConfig; +type AssetField = Extract; -function ImagePreview({ url, label, hint }: { url?: string; label: string; hint: string }) { +interface AssetUploadCardProps { + title: string; + description: string; + buttonText: string; + previewLabel: string; + previewHint: string; + previewMask: string; + url?: string; + fieldName: AssetField; + variant: "logo" | "icon" | "background"; + uploading: boolean; + onUpload: (file: File, fieldName: AssetField) => Promise; +} + +function AssetUploadCard({ + title, + description, + buttonText, + previewLabel, + previewHint, + previewMask, + url, + fieldName, + variant, + uploading, + onUpload, +}: AssetUploadCardProps) { return ( -
- {url ? ( - {label} - ) : ( -
- -
{hint}
+
+
+
+
{title}
+
{description}
- )} +
+ +
+ {url ? ( + {previewLabel} + ) : ( +
+ + {previewHint} +
+ )} +
+ + onUpload(file as File, fieldName)}> + +
); } @@ -29,6 +71,7 @@ export default function PlatformSettings() { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); + const [uploadingField, setUploadingField] = useState(null); const [form] = Form.useForm(); const logoUrl = Form.useWatch("logoUrl", form); const iconUrl = Form.useWatch("iconUrl", form); @@ -48,7 +91,8 @@ export default function PlatformSettings() { loadConfig(); }, []); - const handleUpload = async (file: File, fieldName: keyof SysPlatformConfig) => { + const handleUpload = async (file: File, fieldName: AssetField) => { + setUploadingField(fieldName); try { const url = await uploadPlatformAsset(file); form.setFieldValue(fieldName, url); @@ -56,6 +100,8 @@ export default function PlatformSettings() { } catch (error) { message.error(error instanceof Error ? error.message : t("common.error")); return Upload.LIST_IGNORE; + } finally { + setUploadingField(null); } return false; }; @@ -89,75 +135,97 @@ export default function PlatformSettings() {
- - - {t("platformSettings.basicInfo")}} className="mb-6" loading={loading}> - - - - - - - - + + + - - {t("platformSettings.brandAssets")}} className="app-page__content-card mb-6" loading={loading}> - - - - - - - handleUpload(file, "logoUrl")}> - - - - - - - - - handleUpload(file, "iconUrl")}> - - - - - - - - - handleUpload(file, "loginBgUrl")}> - - - - - - +
+ {t("platformSettings.basicInfo")}} className="platform-settings-card" loading={loading}> + + + + + + + + + + + + + - - {t("platformSettings.complianceFooter")}} className="app-page__content-card" loading={loading}> - - - - - - - - - - - - - - - + {t("platformSettings.brandAssets")}} className="platform-settings-card platform-settings-card--assets" loading={loading}> +
+ + + +
+
+ + {t("platformSettings.complianceFooter")}} className="platform-settings-card" loading={loading}> + + + + + + + + + + + + + +
diff --git a/frontend/src/store/themeStore.ts b/frontend/src/store/themeStore.ts index d1784b6..7b1b335 100644 --- a/frontend/src/store/themeStore.ts +++ b/frontend/src/store/themeStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -export type ThemeMode = 'minimal' | 'tech' | 'default'; +export type ThemeMode = 'minimal' | 'tech'; export type LayoutMode = 'side' | 'top'; interface ThemeState { @@ -14,7 +14,7 @@ interface ThemeState { } const DEFAULT_COLOR = '#1677ff'; -const DEFAULT_MODE: ThemeMode = 'default'; +const DEFAULT_MODE: ThemeMode = 'minimal'; const DEFAULT_LAYOUT: LayoutMode = 'side'; function hexToRgbChannels(color: string): string { @@ -37,6 +37,10 @@ function applyThemeColor(color: string) { document.documentElement.style.setProperty('--app-primary-rgb', hexToRgbChannels(color)); } +function normalizeThemeMode(mode: string | null): ThemeMode { + return mode === 'tech' ? 'tech' : DEFAULT_MODE; +} + const getColorStorageKey = () => { const username = localStorage.getItem("username") || "default"; return `unis_theme_color_${username}`; @@ -63,10 +67,11 @@ export const useThemeStore = create((set) => ({ applyThemeColor(color); }, setThemeMode: (mode: ThemeMode) => { - set({ themeMode: mode }); + const nextMode = normalizeThemeMode(mode); + set({ themeMode: nextMode }); const key = getModeStorageKey(); - localStorage.setItem(key, mode); - document.documentElement.setAttribute('data-theme', mode); + localStorage.setItem(key, nextMode); + document.documentElement.setAttribute('data-theme', nextMode); }, setLayoutMode: (mode: LayoutMode) => { set({ layoutMode: mode }); @@ -81,8 +86,11 @@ export const useThemeStore = create((set) => ({ const storedColor = localStorage.getItem(colorKey); const color = storedColor || DEFAULT_COLOR; - const storedMode = localStorage.getItem(modeKey) as ThemeMode; - const mode = storedMode || DEFAULT_MODE; + const storedMode = localStorage.getItem(modeKey); + const mode = normalizeThemeMode(storedMode); + if (storedMode !== mode) { + localStorage.setItem(modeKey, mode); + } const storedLayout = localStorage.getItem(layoutKey) as LayoutMode; const layout = storedLayout || DEFAULT_LAYOUT;