feat: 页面重构优化
parent
0d6acd1056
commit
37696b13a9
|
|
@ -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 (
|
||||
<ConfigProvider
|
||||
locale={antdLocale}
|
||||
theme={{
|
||||
algorithm: themeMode === 'tech' ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: colorPrimary,
|
||||
borderRadius: 4,
|
||||
colorBgLayout: '#f5f6fa',
|
||||
},
|
||||
components: {
|
||||
Card: {
|
||||
borderRadiusLG: 4
|
||||
},
|
||||
Button: {
|
||||
borderRadius: 4
|
||||
},
|
||||
Input: {
|
||||
borderRadius: 4
|
||||
},
|
||||
Select: {
|
||||
borderRadius: 4
|
||||
},
|
||||
Modal: {
|
||||
borderRadiusLG: 4
|
||||
}
|
||||
}
|
||||
}}
|
||||
theme={antdTheme}
|
||||
>
|
||||
<AntdApp>
|
||||
<AppRoutes />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="theme-selector-container">
|
||||
<Space align="center" style={{ cursor: 'pointer', padding: '0 8px' }} onClick={() => setOpen(true)}>
|
||||
<FormatPainterOutlined style={{ fontSize: '18px', color: 'var(--app-text-main)' }} title={t("layout.theme", "Theme")} />
|
||||
</Space>
|
||||
<button
|
||||
type="button"
|
||||
className="theme-selector-trigger"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label={t("theme.settings", "Theme Settings")}
|
||||
title={t("layout.theme", "Theme")}
|
||||
>
|
||||
<FormatPainterOutlined />
|
||||
</button>
|
||||
<Drawer
|
||||
title={t('theme.settings', 'Theme Settings')}
|
||||
placement="right"
|
||||
onClose={() => setOpen(false)}
|
||||
open={open}
|
||||
width={300}
|
||||
rootClassName="theme-settings-drawer-root"
|
||||
className="theme-settings-drawer"
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FormatPainterOutlined style={{ color: 'var(--app-primary-color)' }} />
|
||||
<Space direction="vertical" size="large" className="theme-settings-content">
|
||||
<section className="theme-settings-section">
|
||||
<div className="theme-settings-label">
|
||||
<FormatPainterOutlined />
|
||||
<Text strong>{t('theme.color', 'Theme Color')}</Text>
|
||||
</div>
|
||||
<ColorPicker
|
||||
className="theme-color-picker"
|
||||
value={colorPrimary}
|
||||
onChangeComplete={(color) => setColorPrimary(color.toHexString())}
|
||||
showText
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FormatPainterOutlined style={{ color: 'var(--app-primary-color)' }} />
|
||||
<section className="theme-settings-section">
|
||||
<div className="theme-settings-label">
|
||||
<FormatPainterOutlined />
|
||||
<Text strong>{t('theme.style', 'Style Mode')}</Text>
|
||||
</div>
|
||||
<Segmented
|
||||
className="theme-style-segmented"
|
||||
block
|
||||
options={themeOptions}
|
||||
value={themeMode}
|
||||
onChange={(value) => setThemeMode(value as ThemeMode)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</Space>
|
||||
</Drawer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -249,6 +249,10 @@
|
|||
"uploadIcon": "上传图标",
|
||||
"uploadBackground": "上传背景图",
|
||||
"previewHint": "上传后将在此处预览",
|
||||
"previewMask": "点击放大预览",
|
||||
"logoHint": "用于左上角品牌展示,建议使用透明底横版图",
|
||||
"iconHint": "用于浏览器标签和小尺寸识别,建议使用正方形图",
|
||||
"loginBgHint": "用于登录页背景展示,建议使用清晰横版大图",
|
||||
"icpPlaceholder": "例如:京 ICP 12345678 号",
|
||||
"copyrightPlaceholder": "例如:© 2026 UnisBase Team"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SectionCard
|
||||
title="当前租户"
|
||||
description="租户管理员可查看并切换当前租户的积分余额校验模式。"
|
||||
layout="auto"
|
||||
className="tenant-meeting-points__tenant-section"
|
||||
contentClassName="tenant-meeting-points__tenant-content"
|
||||
>
|
||||
<Space direction="vertical" size="large" className="tenant-meeting-points__tenant-card">
|
||||
<div className="tenant-meeting-points__tenant-card">
|
||||
<div className="tenant-meeting-points__tenant-heading">
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text strong style={{ fontSize: 18 }}>{currentTenantSetting.tenantName || "当前租户"}</Text>
|
||||
<Text type="secondary">租户编码:{currentTenantSetting.tenantCode || "-"}</Text>
|
||||
<div className="tenant-meeting-points__tenant-identity">
|
||||
<div className="tenant-meeting-points__tenant-icon">
|
||||
<SafetyCertificateOutlined />
|
||||
</div>
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text strong className="tenant-meeting-points__tenant-name">
|
||||
{currentTenantSetting.tenantName || "当前租户"}
|
||||
</Text>
|
||||
<Text type="secondary">租户编码:{currentTenantSetting.tenantCode || "-"}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
<Space className="tenant-meeting-points__tenant-actions" wrap>
|
||||
{renderStatusTag(isBalanceCheckEnabled)}
|
||||
{canUpdateBalanceCheck ? (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={switchingTenantId === currentTenantSetting.tenantId}
|
||||
onClick={() => confirmSwitch(currentTenantSetting, !isBalanceCheckEnabled)}
|
||||
>
|
||||
{isBalanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
{renderStatusTag(currentTenantSetting.balanceCheckEnabled)}
|
||||
</div>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
<div
|
||||
className={
|
||||
currentTenantSetting.balanceCheckEnabled
|
||||
? "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--enabled"
|
||||
: "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--unlimited"
|
||||
isBalanceCheckEnabled
|
||||
? "tenant-meeting-points__mode-panel tenant-meeting-points__mode-panel--enabled"
|
||||
: "tenant-meeting-points__mode-panel tenant-meeting-points__mode-panel--unlimited"
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text strong>
|
||||
{currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"}
|
||||
</Text>
|
||||
<div className="tenant-meeting-points__mode-icon">
|
||||
<CheckCircleOutlined />
|
||||
</div>
|
||||
<div className="tenant-meeting-points__mode-copy">
|
||||
<Text strong>{isBalanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"}</Text>
|
||||
<Text type="secondary">
|
||||
{currentTenantSetting.balanceCheckEnabled
|
||||
{isBalanceCheckEnabled
|
||||
? "后续会议提交将按当前账面余额执行拦截与扣减。"
|
||||
: "后续会议只记录消耗与流水,不扣减账面余额,积分分配也会被禁用。"}
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space size={40} wrap className="tenant-meeting-points__stats">
|
||||
<Statistic title="当前可用额度" value={currentTenantSetting.balanceCheckEnabled ? currentTenantSetting.publicBalance ?? 0 : "无限"} />
|
||||
<Statistic title="公共账户余额" value={currentTenantSetting.publicBalance ?? 0} />
|
||||
<Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} />
|
||||
</Space>
|
||||
<div className="tenant-meeting-points__stats">
|
||||
<div className="tenant-meeting-points__stat-card tenant-meeting-points__stat-card--primary">
|
||||
<Statistic
|
||||
title="当前可用额度"
|
||||
value={isBalanceCheckEnabled ? currentTenantSetting.publicBalance ?? 0 : "无限"}
|
||||
/>
|
||||
</div>
|
||||
<div className="tenant-meeting-points__stat-card">
|
||||
<Statistic
|
||||
title="公共账户余额"
|
||||
value={currentTenantSetting.publicBalance ?? 0}
|
||||
prefix={<DatabaseOutlined />}
|
||||
/>
|
||||
</div>
|
||||
<div className="tenant-meeting-points__stat-card">
|
||||
<Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text type="secondary">最近切换时间:{formatDateTime(currentTenantSetting.lastSwitchAt)}</Text>
|
||||
<Text type="secondary">最近切换人:{currentTenantSetting.lastSwitchByName || "-"}</Text>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
{canUpdateBalanceCheck ? (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={switchingTenantId === currentTenantSetting.tenantId}
|
||||
onClick={() => confirmSwitch(currentTenantSetting, !currentTenantSetting.balanceCheckEnabled)}
|
||||
>
|
||||
{currentTenantSetting.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
<div className="tenant-meeting-points__meta">
|
||||
<div className="tenant-meeting-points__meta-item">
|
||||
<ClockCircleOutlined />
|
||||
<span>最近切换时间:{formatDateTime(currentTenantSetting.lastSwitchAt)}</span>
|
||||
</div>
|
||||
<div className="tenant-meeting-points__meta-item">
|
||||
<SafetyCertificateOutlined />
|
||||
<span>最近切换人:{currentTenantSetting.lastSwitchByName || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ? <Tag color="red">禁用</Tag> : <Tag color="green">启用</Tag>) : "-";
|
||||
const displayName = user?.displayName || "-";
|
||||
const username = user?.username || "-";
|
||||
const organization = renderValue(user?.orgPath || user?.orgName);
|
||||
const roleTags = (
|
||||
<>
|
||||
{user?.isPlatformAdmin ? <Tag color="gold">{t("users.platformAdmin")}</Tag> : null}
|
||||
{user?.isTenantAdmin ? <Tag color="blue">租户管理员</Tag> : null}
|
||||
{user && !user.isPlatformAdmin && !user.isTenantAdmin ? <Tag color="blue">{t("profile.standardUser")}</Tag> : null}
|
||||
{user && !user.isPlatformAdmin && user.hasPlatformAdminPrivilege ? <Tag color="geekblue">可切换平台管理员</Tag> : null}
|
||||
</>
|
||||
);
|
||||
const profileMetaItems = [
|
||||
{
|
||||
key: "email",
|
||||
icon: <MailOutlined />,
|
||||
label: t("users.email"),
|
||||
value: renderValue(user?.email)
|
||||
},
|
||||
{
|
||||
key: "phone",
|
||||
icon: <PhoneOutlined />,
|
||||
label: t("users.phone"),
|
||||
value: renderValue(user?.phone)
|
||||
},
|
||||
{
|
||||
key: "organization",
|
||||
icon: <ApartmentOutlined />,
|
||||
label: t("users.org"),
|
||||
value: organization
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
icon: <CheckCircleOutlined />,
|
||||
label: t("common.status"),
|
||||
value: userStatus
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer title={null} className="profile-page">
|
||||
|
|
@ -180,95 +230,106 @@ export default function Profile() {
|
|||
description={t("profile.subtitle")}
|
||||
contentClassName="profile-section-content"
|
||||
>
|
||||
<Row gutter={[16, 16]} className="profile-layout">
|
||||
<Col xs={24} xl={8} className="profile-side-col">
|
||||
<Space direction="vertical" size={12} className="profile-side-stack">
|
||||
<Card className="profile-panel-card profile-summary-card" loading={loading}>
|
||||
<div className="profile-summary-card__header">
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}
|
||||
<div className="profile-shell">
|
||||
<section className="profile-hero" aria-busy={loading}>
|
||||
<div className="profile-hero__identity">
|
||||
<Upload accept={AVATAR_UPLOAD_ACCEPT} showUploadList={false} beforeUpload={handleAvatarUpload}
|
||||
disabled={avatarUploading}>
|
||||
<div className="profile-summary-card__avatar-trigger">
|
||||
<button type="button" className="profile-avatar-button" aria-label="上传头像">
|
||||
<Avatar
|
||||
size={92}
|
||||
size={88}
|
||||
src={avatarUrl}
|
||||
icon={avatarUrl ? undefined : <UserOutlined/>}
|
||||
className="profile-summary-card__avatar"
|
||||
className="profile-hero__avatar"
|
||||
/>
|
||||
</div>
|
||||
<span className="profile-avatar-button__badge">
|
||||
<CloudUploadOutlined />
|
||||
</span>
|
||||
</button>
|
||||
</Upload>
|
||||
|
||||
<div className="profile-summary-card__identity">
|
||||
<Title level={4} className="profile-summary-card__title">
|
||||
{user?.displayName || "-"}
|
||||
<div className="profile-hero__copy">
|
||||
<div className="profile-hero__eyebrow">个人资料</div>
|
||||
<Title level={3} className="profile-hero__title">
|
||||
{displayName}
|
||||
</Title>
|
||||
<Text type="secondary" className="tabular-nums">
|
||||
@{user?.username || "-"}
|
||||
<Text className="profile-hero__username tabular-nums">
|
||||
@{username}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Space wrap size={[8, 8]} className="profile-summary-card__tags">
|
||||
{user?.isPlatformAdmin ? <Tag color="gold">{t("users.platformAdmin")}</Tag> : null}
|
||||
{user?.isTenantAdmin ? <Tag color="blue">租户管理员</Tag> : null}
|
||||
{user && !user.isPlatformAdmin && !user.isTenantAdmin ?
|
||||
<Tag color="blue">{t("profile.standardUser")}</Tag> : null}
|
||||
{user && !user.isPlatformAdmin && user.hasPlatformAdminPrivilege ?
|
||||
<Tag color="geekblue">可切换平台管理员</Tag> : null}
|
||||
<Space wrap size={[8, 8]} className="profile-hero__tags">
|
||||
{roleTags}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Descriptions
|
||||
column={1}
|
||||
size="small"
|
||||
className="profile-summary-card__meta"
|
||||
items={[
|
||||
{
|
||||
key: "email",
|
||||
label: t("users.email"),
|
||||
children: <span className="tabular-nums">{renderValue(user?.email)}</span>
|
||||
},
|
||||
{
|
||||
key: "phone",
|
||||
label: t("users.phone"),
|
||||
children: <span className="tabular-nums">{renderValue(user?.phone)}</span>
|
||||
},
|
||||
{
|
||||
key: "organization",
|
||||
label: t("users.org"),
|
||||
children: renderValue(user?.orgPath || user?.orgName)
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: t("common.status"),
|
||||
children: userStatus
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Card className="profile-panel-card profile-help-card">
|
||||
<Title level={5} className="profile-help-card__title">
|
||||
安全提示
|
||||
</Title>
|
||||
<div className="profile-help-card__list">
|
||||
<div>头像上传后会立即填充到个人资料表单,保存后对外生效。</div>
|
||||
<div>密码修改成功后会刷新当前会话中的个人资料缓存。</div>
|
||||
<div>Bot 凭证重新生成后,旧密钥会立即失效。</div>
|
||||
<div className="profile-workbench">
|
||||
<aside className="profile-side-stack">
|
||||
<section className="profile-panel profile-overview-panel">
|
||||
<div className="profile-panel__head">
|
||||
<div>
|
||||
<div className="profile-panel__eyebrow">资料信息</div>
|
||||
<Title level={5} className="profile-panel__title">资料概览</Title>
|
||||
</div>
|
||||
<IdcardOutlined className="profile-panel__icon" />
|
||||
</div>
|
||||
|
||||
<div className="profile-meta-list">
|
||||
{profileMetaItems.map((item) => (
|
||||
<div className="profile-meta-list__item" key={item.key}>
|
||||
<div className="profile-meta-list__icon">{item.icon}</div>
|
||||
<div className="profile-meta-list__content">
|
||||
<div className="profile-meta-list__label">{item.label}</div>
|
||||
<div className="profile-meta-list__value tabular-nums">{item.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="profile-panel profile-security-panel">
|
||||
<div className="profile-panel__head">
|
||||
<div>
|
||||
<div className="profile-panel__eyebrow">安全状态</div>
|
||||
<Title level={5} className="profile-panel__title">安全提示</Title>
|
||||
</div>
|
||||
<SafetyCertificateOutlined className="profile-panel__icon" />
|
||||
</div>
|
||||
<div className="profile-security-list">
|
||||
<div>
|
||||
<strong>头像资料</strong>
|
||||
<span>上传裁剪后会先填入表单,保存资料后正式生效。</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>密码更新</strong>
|
||||
<span>修改成功后会刷新当前会话里的个人资料缓存。</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Bot 凭证</strong>
|
||||
<span>重新生成后,旧密钥会立即失效。</span>
|
||||
</div>
|
||||
</div>
|
||||
{user?.pwdResetRequired ? (
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
message="当前账号仍需要完成密码重置"
|
||||
description="请尽快在“安全设置”页签中更新密码。"
|
||||
className="profile-security-panel__alert"
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section className="profile-panel profile-editor-panel">
|
||||
<div className="profile-editor-panel__head">
|
||||
<div>
|
||||
<div className="profile-panel__eyebrow">账户维护</div>
|
||||
<Title level={5} className="profile-panel__title">账户设置</Title>
|
||||
</div>
|
||||
<Text type="secondary">基础资料、安全密码与接口凭证统一维护</Text>
|
||||
</div>
|
||||
{user?.pwdResetRequired ? (
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
message="当前账号仍需要完成密码重置"
|
||||
description="请尽快在“安全设置”页签中更新密码。"
|
||||
className="profile-help-card__alert"
|
||||
/>
|
||||
) : null}
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} xl={16} className="profile-main-col">
|
||||
<Card className="profile-panel-card profile-tabs-card">
|
||||
<Tabs
|
||||
defaultActiveKey="basic"
|
||||
className="profile-tabs"
|
||||
|
|
@ -282,6 +343,12 @@ export default function Profile() {
|
|||
),
|
||||
children: (
|
||||
<Form form={profileForm} layout="vertical" onFinish={handleUpdateProfile} className="profile-form">
|
||||
<div className="profile-form-intro">
|
||||
<div>
|
||||
<Title level={5}>基础资料</Title>
|
||||
<Text type="secondary">用于系统内展示和管理员识别,请保持联系方式准确。</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("users.displayName")} name="displayName" rules={[{required: true}]}>
|
||||
|
|
@ -305,24 +372,58 @@ export default function Profile() {
|
|||
<Input/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("profile.avatarUrl")} name="avatarUrl">
|
||||
<Input placeholder={t("profile.avatarUrlPlaceholder")}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="profile-upload-panel">
|
||||
<div>
|
||||
<Text strong>{t("profile.uploadAvatar")}</Text>
|
||||
<div className="profile-upload-panel__hint">建议上传方形图片,裁剪后会生成统一头像尺寸。</div>
|
||||
</div>
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}>
|
||||
<Button icon={<UploadOutlined/>} loading={avatarUploading}>
|
||||
{t("profile.uploadAvatar")}
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
<Form.Item name="avatarUrl" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<div className="profile-avatar-control">
|
||||
<div className="profile-avatar-control__preview">
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt="头像预览"
|
||||
className="profile-avatar-control__image"
|
||||
preview={{ mask: "查看大图" }}
|
||||
/>
|
||||
) : (
|
||||
<Upload
|
||||
accept={AVATAR_UPLOAD_ACCEPT}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleAvatarUpload}
|
||||
disabled={avatarUploading}
|
||||
>
|
||||
<button type="button" className="profile-avatar-control__empty">
|
||||
<CloudUploadOutlined />
|
||||
<span>上传头像</span>
|
||||
</button>
|
||||
</Upload>
|
||||
)}
|
||||
</div>
|
||||
<div className="profile-avatar-control__content">
|
||||
<div>
|
||||
<div className="profile-avatar-control__title">头像图片</div>
|
||||
<div className="profile-avatar-control__hint">
|
||||
{avatarUrl ? "当前头像已回显,点击图片可放大预览。" : "支持 PNG/JPG 图片,上传后会先裁剪成统一尺寸。"}
|
||||
</div>
|
||||
</div>
|
||||
<Upload
|
||||
accept={AVATAR_UPLOAD_ACCEPT}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleAvatarUpload}
|
||||
disabled={avatarUploading}
|
||||
>
|
||||
<Button icon={<UploadOutlined/>} loading={avatarUploading}>
|
||||
{avatarUrl ? "更换头像" : t("profile.uploadAvatar")}
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="app-page__content-toolbar profile-tab-actions">
|
||||
<div className="app-page__content-toolbar-actions">
|
||||
|
|
@ -344,6 +445,12 @@ export default function Profile() {
|
|||
),
|
||||
children: (
|
||||
<Form form={pwdForm} layout="vertical" onFinish={handleUpdatePassword} className="profile-form">
|
||||
<div className="profile-form-intro">
|
||||
<div>
|
||||
<Title level={5}>登录安全</Title>
|
||||
<Text type="secondary">定期更新密码可以降低账号被误用的风险。</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Alert
|
||||
showIcon
|
||||
type="info"
|
||||
|
|
@ -426,6 +533,12 @@ export default function Profile() {
|
|||
),
|
||||
children: (
|
||||
<div className="profile-credential-panel">
|
||||
<div className="profile-form-intro">
|
||||
<div>
|
||||
<Title level={5}>Bot 接入凭证</Title>
|
||||
<Text type="secondary">为自动化工具或 Bot 调用保留独立凭证,便于轮换和审计。</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
|
|
@ -433,7 +546,7 @@ export default function Profile() {
|
|||
description={t("profile.botCredentialHintDesc")}
|
||||
/>
|
||||
|
||||
<Card bordered={false} loading={credentialLoading} className="profile-credential-panel__card">
|
||||
<div className="profile-credential-card" aria-busy={credentialLoading}>
|
||||
<Descriptions
|
||||
bordered
|
||||
size="middle"
|
||||
|
|
@ -480,7 +593,7 @@ export default function Profile() {
|
|||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="app-page__content-toolbar profile-tab-actions">
|
||||
<div className="app-page__content-toolbar-actions">
|
||||
|
|
@ -500,9 +613,9 @@ export default function Profile() {
|
|||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<AvatarCropDialog
|
||||
|
|
|
|||
|
|
@ -21,29 +21,177 @@
|
|||
}
|
||||
|
||||
.platform-settings-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
margin: 0 4px 12px;
|
||||
padding: 8px 0;
|
||||
background: var(--app-bg-surface-soft, #f9fafe);
|
||||
}
|
||||
|
||||
.platform-settings-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.platform-settings-page .app-page__content-card {
|
||||
overflow: visible;
|
||||
.platform-settings-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.platform-settings-page .ant-card {
|
||||
.platform-settings-card.ant-card {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.platform-settings-card > .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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<keyof SysPlatformConfig, "logoUrl" | "iconUrl" | "loginBgUrl">;
|
||||
|
||||
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<boolean | string>;
|
||||
}
|
||||
|
||||
function AssetUploadCard({
|
||||
title,
|
||||
description,
|
||||
buttonText,
|
||||
previewLabel,
|
||||
previewHint,
|
||||
previewMask,
|
||||
url,
|
||||
fieldName,
|
||||
variant,
|
||||
uploading,
|
||||
onUpload,
|
||||
}: AssetUploadCardProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4 border border-dashed border-gray-300 rounded-lg bg-gray-50 h-32">
|
||||
{url ? (
|
||||
<img src={url} alt={label} className="max-h-full max-w-full object-contain" />
|
||||
) : (
|
||||
<div className="text-gray-400 text-center">
|
||||
<PictureOutlined style={{ fontSize: 24 }} />
|
||||
<div className="text-xs mt-1">{hint}</div>
|
||||
<div className={`platform-asset-card platform-asset-card--${variant}`}>
|
||||
<div className="platform-asset-card__header">
|
||||
<div>
|
||||
<div className="platform-asset-card__title">{title}</div>
|
||||
<div className="platform-asset-card__description">{description}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="platform-asset-card__preview" aria-label={previewLabel}>
|
||||
{url ? (
|
||||
<Image src={url} alt={previewLabel} preview={{ mask: previewMask }} />
|
||||
) : (
|
||||
<div className="platform-asset-card__empty">
|
||||
<PictureOutlined />
|
||||
<span>{previewHint}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={(file) => onUpload(file as File, fieldName)}>
|
||||
<Button icon={<UploadOutlined />} loading={uploading} block>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<AssetField | null>(null);
|
||||
const [form] = Form.useForm<PlatformFormValues>();
|
||||
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() {
|
|||
<div className="app-page__content-toolbar-filters" />
|
||||
</div>
|
||||
<Form form={form} layout="vertical" onFinish={onFinish} initialValues={{ projectName: "UnisBase" }} className="platform-settings-form">
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Card title={<Space size={8}><GlobalOutlined />{t("platformSettings.basicInfo")}</Space>} className="mb-6" loading={loading}>
|
||||
<Form.Item label={t("platformSettings.projectName")} name="projectName" rules={[
|
||||
{ required: true, message: t("platformSettings.projectNameRequired") },
|
||||
{
|
||||
max:50,
|
||||
message:t('common.maxLength',{length:50})
|
||||
}
|
||||
]}>
|
||||
<Input placeholder={t("platformSettings.projectNamePlaceholder")} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("platformSettings.desc")} name="systemDescription">
|
||||
<Input.TextArea rows={3} placeholder={t("platformSettings.systemDescriptionPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Col>
|
||||
<Form.Item name="logoUrl" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="iconUrl" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="loginBgUrl" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Col span={24}>
|
||||
<Card title={<Space size={8}><PictureOutlined />{t("platformSettings.brandAssets")}</Space>} className="app-page__content-card mb-6" loading={loading}>
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item label={t("platformSettings.logoUrl")} name="logoUrl">
|
||||
<Input placeholder={t("platformSettings.logoUrl")} className="mb-2" />
|
||||
</Form.Item>
|
||||
<ImagePreview url={logoUrl} label={t("platformSettings.logo")} hint={t("platformSettings.previewHint")} />
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={(file) => handleUpload(file, "logoUrl")}>
|
||||
<Button icon={<UploadOutlined />} block className="mt-2">{t("platformSettings.uploadLogo")}</Button>
|
||||
</Upload>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item label={t("platformSettings.iconUrl")} name="iconUrl">
|
||||
<Input placeholder={t("platformSettings.iconUrl")} className="mb-2" />
|
||||
</Form.Item>
|
||||
<ImagePreview url={iconUrl} label={t("platformSettings.icon")} hint={t("platformSettings.previewHint")} />
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={(file) => handleUpload(file, "iconUrl")}>
|
||||
<Button icon={<UploadOutlined />} block className="mt-2">{t("platformSettings.uploadIcon")}</Button>
|
||||
</Upload>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item label={t("platformSettings.loginBgUrl")} name="loginBgUrl">
|
||||
<Input placeholder={t("platformSettings.loginBgUrl")} className="mb-2" />
|
||||
</Form.Item>
|
||||
<ImagePreview url={loginBgUrl} label={t("platformSettings.loginBg")} hint={t("platformSettings.previewHint")} />
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={(file) => handleUpload(file, "loginBgUrl")}>
|
||||
<Button icon={<UploadOutlined />} block className="mt-2">{t("platformSettings.uploadBackground")}</Button>
|
||||
</Upload>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
<div className="platform-settings-layout">
|
||||
<Card title={<Space size={8}><GlobalOutlined />{t("platformSettings.basicInfo")}</Space>} className="platform-settings-card" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("platformSettings.projectName")} name="projectName" rules={[
|
||||
{ required: true, message: t("platformSettings.projectNameRequired") },
|
||||
{
|
||||
max: 50,
|
||||
message: t("common.maxLength", { length: 50 })
|
||||
}
|
||||
]}>
|
||||
<Input placeholder={t("platformSettings.projectNamePlaceholder")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("platformSettings.desc")} name="systemDescription">
|
||||
<Input.TextArea rows={4} placeholder={t("platformSettings.systemDescriptionPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Col span={24}>
|
||||
<Card title={<Space size={8}><FileTextOutlined />{t("platformSettings.complianceFooter")}</Space>} className="app-page__content-card" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("platformSettings.icp")} name="icpInfo">
|
||||
<Input placeholder={t("platformSettings.icpPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("platformSettings.copyright")} name="copyrightInfo">
|
||||
<Input placeholder={t("platformSettings.copyrightPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card title={<Space size={8}><PictureOutlined />{t("platformSettings.brandAssets")}</Space>} className="platform-settings-card platform-settings-card--assets" loading={loading}>
|
||||
<div className="platform-assets-grid">
|
||||
<AssetUploadCard
|
||||
title={t("platformSettings.logo")}
|
||||
description={t("platformSettings.logoHint")}
|
||||
buttonText={t("platformSettings.uploadLogo")}
|
||||
previewLabel={t("platformSettings.logo")}
|
||||
previewHint={t("platformSettings.previewHint")}
|
||||
previewMask={t("platformSettings.previewMask")}
|
||||
url={logoUrl}
|
||||
fieldName="logoUrl"
|
||||
variant="logo"
|
||||
uploading={uploadingField === "logoUrl"}
|
||||
onUpload={handleUpload}
|
||||
/>
|
||||
<AssetUploadCard
|
||||
title={t("platformSettings.icon")}
|
||||
description={t("platformSettings.iconHint")}
|
||||
buttonText={t("platformSettings.uploadIcon")}
|
||||
previewLabel={t("platformSettings.icon")}
|
||||
previewHint={t("platformSettings.previewHint")}
|
||||
previewMask={t("platformSettings.previewMask")}
|
||||
url={iconUrl}
|
||||
fieldName="iconUrl"
|
||||
variant="icon"
|
||||
uploading={uploadingField === "iconUrl"}
|
||||
onUpload={handleUpload}
|
||||
/>
|
||||
<AssetUploadCard
|
||||
title={t("platformSettings.loginBg")}
|
||||
description={t("platformSettings.loginBgHint")}
|
||||
buttonText={t("platformSettings.uploadBackground")}
|
||||
previewLabel={t("platformSettings.loginBg")}
|
||||
previewHint={t("platformSettings.previewHint")}
|
||||
previewMask={t("platformSettings.previewMask")}
|
||||
url={loginBgUrl}
|
||||
fieldName="loginBgUrl"
|
||||
variant="background"
|
||||
uploading={uploadingField === "loginBgUrl"}
|
||||
onUpload={handleUpload}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title={<Space size={8}><FileTextOutlined />{t("platformSettings.complianceFooter")}</Space>} className="platform-settings-card" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("platformSettings.icp")} name="icpInfo">
|
||||
<Input placeholder={t("platformSettings.icpPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label={t("platformSettings.copyright")} name="copyrightInfo">
|
||||
<Input placeholder={t("platformSettings.copyrightPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
|
|
|||
|
|
@ -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<ThemeState>((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<ThemeState>((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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue