From cfb5aeff7143747c13e59fdf54a7b3f3fe306e68 Mon Sep 17 00:00:00 2001 From: puz <13060209078@163.com> Date: Wed, 1 Jul 2026 11:01:38 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=B0=83=E6=95=B4=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared/FormDrawer/FormDrawer.css | 133 +++++++++++++++ .../components/shared/FormDrawer/index.tsx | 156 ++++++++++++++++++ frontend/src/index.css | 16 ++ frontend/src/layouts/AppLayout.tsx | 15 +- .../business/MeetingPointsManagement.tsx | 19 +-- .../src/pages/business/PromptTemplates.tsx | 44 +++-- .../src/pages/system/sys-params/index.less | 50 +++++- .../src/pages/system/sys-params/index.tsx | 1 - frontend/src/routes/routes.tsx | 12 +- frontend/vite.config.ts | 25 ++- 10 files changed, 418 insertions(+), 53 deletions(-) create mode 100644 frontend/src/components/shared/FormDrawer/FormDrawer.css create mode 100644 frontend/src/components/shared/FormDrawer/index.tsx diff --git a/frontend/src/components/shared/FormDrawer/FormDrawer.css b/frontend/src/components/shared/FormDrawer/FormDrawer.css new file mode 100644 index 0000000..8fab1ae --- /dev/null +++ b/frontend/src/components/shared/FormDrawer/FormDrawer.css @@ -0,0 +1,133 @@ +.form-drawer-root .ant-drawer-content-wrapper { + max-width: var(--app-form-drawer-max-width, calc(100vw - 48px)); +} + +.form-drawer .ant-drawer-header { + min-height: 64px; + padding: 16px 24px; + border-bottom: 1px solid var(--app-border-color, #f0f0f0); +} + +.form-drawer .ant-drawer-header-title { + min-width: 0; +} + +.form-drawer .ant-drawer-title { + min-width: 0; +} + +.form-drawer .ant-drawer-body { + background: var(--app-surface-color, #ffffff); +} + +.form-drawer .ant-drawer-footer { + border-top: 1px solid var(--app-border-color, #f0f0f0); + background: var(--app-surface-color, #ffffff); +} + +.form-drawer__title { + display: flex; + min-width: 0; + align-items: center; + gap: 8px; +} + +.form-drawer__title-icon { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + color: var(--app-primary-color, #1677ff); + font-size: 16px; +} + +.form-drawer__title-main { + min-width: 0; +} + +.form-drawer__title-text { + display: block; + overflow: hidden; + color: var(--app-text-main, rgba(0, 0, 0, 0.88)); + font-size: 16px; + font-weight: 600; + line-height: 24px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.form-drawer__subtitle { + display: block; + overflow: hidden; + margin-top: 2px; + color: var(--app-text-secondary, rgba(0, 0, 0, 0.45)); + font-size: 12px; + font-weight: 400; + line-height: 20px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.form-drawer__body { + min-height: 100%; + padding: 24px; +} + +.form-drawer__body--compact { + padding: 20px 24px; +} + +.form-drawer__body--spacious { + padding: 24px 32px; +} + +.form-drawer__body .ant-form-vertical .ant-form-item { + margin-bottom: 18px; +} + +.form-drawer__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 24px; +} + +.form-drawer__footer-extra { + min-width: 0; + color: var(--app-text-secondary, rgba(0, 0, 0, 0.45)); + font-size: 13px; +} + +.form-drawer__footer-actions { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: flex-end; + gap: 12px; + margin-left: auto; +} + +.form-drawer__footer-actions .ant-btn { + min-width: 72px; +} + +@media (max-width: 768px) { + .form-drawer-root .ant-drawer-content-wrapper { + width: 100vw !important; + max-width: 100vw; + } + + .form-drawer .ant-drawer-header { + padding: 14px 16px; + } + + .form-drawer__body, + .form-drawer__body--compact, + .form-drawer__body--spacious { + padding: 16px; + } + + .form-drawer__footer { + padding: 12px 16px; + } +} diff --git a/frontend/src/components/shared/FormDrawer/index.tsx b/frontend/src/components/shared/FormDrawer/index.tsx new file mode 100644 index 0000000..a0530b6 --- /dev/null +++ b/frontend/src/components/shared/FormDrawer/index.tsx @@ -0,0 +1,156 @@ +import type { CSSProperties, ReactNode } from "react"; +import { Button, Drawer } from "antd"; +import type { ButtonProps, DrawerProps } from "antd"; +import "./FormDrawer.css"; + +type FormDrawerSize = "sm" | "md" | "lg" | "xl" | "wide"; +type FormDrawerBodyDensity = "compact" | "default" | "spacious"; + +const FORM_DRAWER_WIDTH = "var(--app-form-drawer-width, 600px)"; + +const drawerWidthMap: Record = { + sm: FORM_DRAWER_WIDTH, + md: FORM_DRAWER_WIDTH, + lg: FORM_DRAWER_WIDTH, + xl: FORM_DRAWER_WIDTH, + wide: FORM_DRAWER_WIDTH, +}; + +interface FormDrawerProps + extends Omit { + open: boolean; + onClose: () => void; + title: ReactNode; + subtitle?: ReactNode; + icon?: ReactNode; + children: ReactNode; + size?: FormDrawerSize; + width?: number | string; + bodyDensity?: FormDrawerBodyDensity; + bodyClassName?: string; + bodyStyle?: CSSProperties; + footer?: ReactNode; + footerExtra?: ReactNode; + hideFooter?: boolean; + cancelText?: ReactNode; + okText?: ReactNode; + okIcon?: ReactNode; + okLoading?: boolean; + okDisabled?: boolean; + onOk?: () => void; + cancelButtonProps?: ButtonProps; + okButtonProps?: ButtonProps; +} + +function joinClassNames(...classes: Array) { + return classes.filter(Boolean).join(" "); +} + +export default function FormDrawer({ + open, + onClose, + title, + subtitle, + icon, + children, + size = "md", + width, + bodyDensity = "default", + bodyClassName, + bodyStyle, + footer, + footerExtra, + hideFooter = false, + cancelText = "取消", + okText = "保存", + okIcon, + okLoading = false, + okDisabled = false, + onOk, + cancelButtonProps, + okButtonProps, + rootClassName, + className, + styles, + placement = "right", + maskClosable = false, + destroyOnHidden = true, + ...drawerProps +}: FormDrawerProps) { + const { children: okButtonChildren, onClick: okButtonOnClick, ...restOkButtonProps } = okButtonProps ?? {}; + const { body, footer: footerStyle, header, ...restStyles } = styles ?? {}; + const resolvedWidth = width ?? drawerWidthMap[size]; + const resolvedTitle = ( +
+ {icon ? {icon} : null} + + {title} + {subtitle ? {subtitle} : null} + +
+ ); + const defaultFooter = ( +
+ {footerExtra ?
{footerExtra}
: null} +
+ + {onOk || okButtonProps ? ( + + ) : null} +
+
+ ); + const resolvedFooter = hideFooter ? null : footer ?? defaultFooter; + const bodyClasses = joinClassNames( + "form-drawer__body", + bodyDensity !== "default" && `form-drawer__body--${bodyDensity}`, + bodyClassName, + ); + + return ( + +
+ {children} +
+
+ ); +} + +export type { FormDrawerProps, FormDrawerSize, FormDrawerBodyDensity }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 222b644..d5684d1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -23,6 +23,8 @@ --item-hover-bg: rgba(22, 119, 255, 0.08); --text-color-secondary: #66758f; --link-color: #1677ff; + --app-form-drawer-width: 600px; + --app-form-drawer-max-width: calc(100vw - 48px); } :root[data-theme="minimal"] { @@ -339,6 +341,13 @@ body::after { padding: 8px 4px 4px; } +.ant-drawer:has(.app-page__drawer-footer) .ant-drawer-content-wrapper, +.ant-drawer:has(.screen-saver-drawer__footer) .ant-drawer-content-wrapper, +.meeting-create-drawer-root .ant-drawer-content-wrapper { + width: min(var(--app-form-drawer-width), var(--app-form-drawer-max-width)) !important; + max-width: var(--app-form-drawer-max-width); +} + .app-page__split { flex: 1; min-height: 0; @@ -856,6 +865,11 @@ body::after { } @media (max-width: 768px) { + :root { + --app-form-drawer-width: 100vw; + --app-form-drawer-max-width: 100vw; + } + body::after { inset: 10px; border-radius: 18px; @@ -1259,6 +1273,8 @@ body::after { --app-text-muted: #9095a1; --text-color-secondary: #9095a1; --link-color: #1677ff; + --app-form-drawer-width: 600px; + --app-form-drawer-max-width: calc(100vw - 48px); } html, diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index 0b70611..1e2bc29 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -1,4 +1,3 @@ -import * as AntIcons from "@ant-design/icons"; import { BellOutlined, ApartmentOutlined, @@ -31,6 +30,16 @@ import "./AppLayout.css"; const { Header, Sider, Content } = Layout; const iconMap: Record = { + ApartmentOutlined: , + BookOutlined: , + DashboardOutlined: , + DesktopOutlined: , + SafetyCertificateOutlined: , + SettingOutlined: , + ShopOutlined: , + TeamOutlined: , + UserOutlined: , + VideoCameraOutlined: , dashboard: , meeting: , user: , @@ -46,9 +55,7 @@ const iconMap: Record = { function resolveMenuIcon(icon?: string): ReactNode { if (!icon) return ; const aliasIcon = iconMap[icon]; - if (aliasIcon) return aliasIcon; - const IconComponent = (AntIcons as unknown as Record)[icon]; - return IconComponent ? : ; + return aliasIcon ?? ; } type PermissionMenuNode = SysPermission & { diff --git a/frontend/src/pages/business/MeetingPointsManagement.tsx b/frontend/src/pages/business/MeetingPointsManagement.tsx index 0cdd6c6..e06f160 100644 --- a/frontend/src/pages/business/MeetingPointsManagement.tsx +++ b/frontend/src/pages/business/MeetingPointsManagement.tsx @@ -386,11 +386,16 @@ export default function MeetingPointsManagement() { > } onClick={() => void handleOpenTransfer()}> - 分配积分 + + - ) : null + {isLookupTab(activeTabKey) && showTransferButton ? ( + + ) : null} + } rightActions={ @@ -416,12 +421,6 @@ export default function MeetingPointsManagement() { ) : null} - - - - } + okIcon={} + okLoading={submitLoading} + okButtonProps={{ htmlType: "submit", form: "prompt-template-form" }} >
{ } }} > - - + + {(isPlatformAdmin || isTenantAdmin) && ( - + {categories.map((i) => )} - + {dictTags.map((item) => )} - + { 提示词编辑器 (Markdown 实时预览) - - + + setPreviewContent(e.target.value)} @@ -573,7 +571,7 @@ const PromptTemplates: React.FC = () => { /> - +
{selectedHotWordGroupId ? ( @@ -588,7 +586,7 @@ const PromptTemplates: React.FC = () => { - + ); }; diff --git a/frontend/src/pages/system/sys-params/index.less b/frontend/src/pages/system/sys-params/index.less index 0b49bf1..bd1c202 100644 --- a/frontend/src/pages/system/sys-params/index.less +++ b/frontend/src/pages/system/sys-params/index.less @@ -33,21 +33,55 @@ } .param-boolean-segmented { - .ant-segmented-item-selected { - background: var(--app-primary-color) !important; - color: #fff !important; - box-shadow: none; + width: 176px; + padding: 2px; + border-radius: 999px; + background: #eaf1fb; + + .ant-segmented-group { + width: 100%; + overflow: hidden; + border-radius: 999px; } .ant-segmented-item { - color: var(--app-text-secondary, rgba(0, 0, 0, 0.65)); - transition: all 0.2s; + flex: 1; + min-width: 0; + height: 26px; + color: #5d6f86; + border-radius: 0; + transition: color 0.2s, background 0.2s; &:hover:not(.ant-segmented-item-selected) { - color: var(--app-primary-color); - background: rgba(22, 119, 255, 0.06); + color: #1d5fbf; + background: #dbe8fb; } } + + .ant-segmented-item:first-child { + border-radius: 999px 0 0 999px; + } + + .ant-segmented-item:last-child { + border-radius: 0 999px 999px 0; + } + + .ant-segmented-thumb { + border-radius: 0; + } + + .ant-segmented-item-selected { + background: #2f7ff0 !important; + color: #fff !important; + box-shadow: 0 1px 4px rgba(47, 127, 240, 0.22); + } + + .ant-segmented-item-label { + min-height: 26px; + line-height: 26px; + font-size: 13px; + font-weight: 500; + } } @media (max-width: 768px) { diff --git a/frontend/src/pages/system/sys-params/index.tsx b/frontend/src/pages/system/sys-params/index.tsx index c5a63e2..01b6f86 100644 --- a/frontend/src/pages/system/sys-params/index.tsx +++ b/frontend/src/pages/system/sys-params/index.tsx @@ -359,7 +359,6 @@ export default function SysParams() { ) : isType(paramType, "Boolean") ? ( import("@/pages/business/MeetingPoint const TenantMeetingPointsSettings = lazy(() => import("@/pages/business/TenantMeetingPointsSettings")); const LicenseManagement = lazy(() => import("@/pages/business/LicenseManagement")); -import SpeakerReg from "../pages/business/SpeakerReg"; +const SpeakerReg = lazy(() => import("../pages/business/SpeakerReg")); const RealtimeAsrSession = lazy(async () => { const mod = await import("../pages/business/RealtimeAsrSession"); return { default: mod.default ?? mod.RealtimeAsrSession }; }); -import HotWords from "../pages/business/HotWords"; -import PromptTemplates from "../pages/business/PromptTemplates"; -import AiModels from "../pages/business/AiModels"; -import Meetings from "../pages/business/Meetings"; -import MeetingDetail from "../pages/business/MeetingDetail"; +const HotWords = lazy(() => import("../pages/business/HotWords")); +const PromptTemplates = lazy(() => import("../pages/business/PromptTemplates")); +const AiModels = lazy(() => import("../pages/business/AiModels")); +const Meetings = lazy(() => import("../pages/business/Meetings")); +const MeetingDetail = lazy(() => import("../pages/business/MeetingDetail")); function RouteFallback() { return ( diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e1d0028..5f03e45 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,7 +9,30 @@ export default defineConfig({ } }, build: { - chunkSizeWarningLimit: 700 + chunkSizeWarningLimit: 700, + rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes("node_modules")) { + return undefined; + } + + if (id.includes("node_modules/react/") || id.includes("node_modules/react-dom/") || id.includes("node_modules/scheduler/")) { + return "vendor-react"; + } + + if (id.includes("node_modules/jspdf/") || id.includes("node_modules/react-markdown/") || id.includes("node_modules/remark-") || id.includes("node_modules/rehype-") || id.includes("node_modules/unified/")) { + return "vendor-docs"; + } + + if (id.includes("node_modules/@dnd-kit/")) { + return "vendor-dnd"; + } + + return undefined; + } + } + } }, server: { port: 5174,