From 0a33ff8e56862b7832f6a48f4a430941393f9a3e Mon Sep 17 00:00:00 2001 From: puz <13060209078@163.com> Date: Wed, 1 Jul 2026 22:00:19 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E9=A1=B5=E9=9D=A2=E8=B0=83?= =?UTF-8?q?=E6=95=B42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/shared/AppPagination/index.css | 207 +++++++++++++++++- .../components/shared/AppPagination/index.tsx | 127 +++++++++-- .../shared/DataListPanel/DataListPanel.css | 6 +- .../components/shared/ListTable/ListTable.css | 48 ++++ .../components/shared/ListTable/ListTable.tsx | 22 +- .../shared/PageContainer/PageContainer.css | 10 + frontend/src/index.css | 74 ++++++- frontend/src/pages/access/users/index.less | 76 +++++++ frontend/src/pages/access/users/index.tsx | 142 +++++++----- frontend/src/pages/business/AiModels.tsx | 4 +- .../pages/business/ExternalAppManagement.css | 92 ++++++++ .../pages/business/ExternalAppManagement.tsx | 37 +++- frontend/src/pages/business/HotWords.css | 42 +++- frontend/src/pages/business/HotWords.tsx | 51 +++-- frontend/src/pages/business/MeetingDetail.tsx | 2 +- frontend/src/pages/business/Meetings.css | 105 ++++++--- frontend/src/pages/business/Meetings.tsx | 69 +++--- .../src/pages/business/PromptTemplates.css | 21 +- .../src/pages/business/PromptTemplates.tsx | 26 ++- .../pages/business/ScreenSaverManagement.css | 21 ++ .../pages/business/ScreenSaverManagement.tsx | 10 +- frontend/src/pages/dashboard/index.css | 10 +- frontend/src/pages/devices/index.less | 11 + frontend/src/pages/devices/index.tsx | 33 ++- frontend/src/pages/home/index.less | 18 +- .../src/pages/organization/tenants/index.less | 51 ++++- .../src/pages/system/dictionaries/index.tsx | 2 +- frontend/src/pages/system/logs/index.tsx | 6 +- .../pages/system/platform-settings/index.tsx | 8 +- frontend/src/routes/routes.tsx | 2 +- frontend/src/utils/pagination.ts | 15 +- 31 files changed, 1128 insertions(+), 220 deletions(-) diff --git a/frontend/src/components/shared/AppPagination/index.css b/frontend/src/components/shared/AppPagination/index.css index 6808be1..bac975d 100644 --- a/frontend/src/components/shared/AppPagination/index.css +++ b/frontend/src/components/shared/AppPagination/index.css @@ -2,17 +2,18 @@ margin-top: auto; flex-shrink: 0; width: 100%; - height: 50px; - padding-top: 8px; + min-height: 52px; + padding: 8px 0; background: #fff; box-sizing: border-box; min-width: 0; - overflow: hidden; + overflow: visible; position: relative; display: flex; align-items: center; justify-content: space-between; - gap: 16px; + gap: 8px 16px; + flex-wrap: nowrap; } .app-pagination-total { @@ -22,15 +23,128 @@ white-space: nowrap; } -.app-pagination-container .ant-pagination { +.app-pagination-controls { flex: 1 1 auto; width: auto; min-width: 0; display: flex; align-items: center; justify-content: flex-end; + flex-wrap: nowrap; + gap: 8px; + overflow: visible; +} + +.app-pagination-container .ant-pagination { + flex: 0 1 auto; + min-width: 0; + min-height: 32px; + display: flex; + align-items: center; + justify-content: flex-end; margin: 0 !important; overflow: visible; + flex-wrap: nowrap; + row-gap: 8px; +} + +.app-pagination-container .app-global-pagination.ant-pagination { + width: auto; + padding: 0; + background: transparent; + border-top: 0; + border-radius: 0; +} + +.app-pagination-container .ant-pagination .ant-pagination-options { + height: 32px; + display: inline-flex; + flex: 0 0 auto; + align-items: center; + margin-inline-start: 8px !important; + row-gap: 8px; +} + +.app-pagination-container .ant-pagination .ant-pagination-options-quick-jumper { + height: 32px; + display: inline-flex; + align-items: center; + white-space: nowrap; +} + +.app-pagination-container .ant-select { + height: 32px; + display: inline-flex; + align-items: center; +} + +.app-pagination-container .ant-select-selector { + height: 32px !important; + display: inline-flex !important; + align-items: center !important; +} + +.app-pagination-container .ant-select-selection-item { + display: inline-flex !important; + align-items: center; + line-height: 30px !important; +} + +.app-pagination-quick-jumper { + height: 32px; + display: inline-flex; + flex: 0 0 auto; + align-items: stretch; + align-self: center; + gap: 6px; + color: #333; + font-size: 14px; + line-height: 32px; + white-space: nowrap; +} + +.app-pagination-quick-jumper__label { + box-sizing: border-box; + height: 32px; + display: inline-flex; + align-items: center; + line-height: 32px; + transform: translateY(-1px); +} + +.app-pagination-quick-jumper input { + box-sizing: border-box; + display: block; + flex: 0 0 48px; + width: 48px; + height: 32px; + padding: 0 8px; + color: #333; + font-size: 14px; + line-height: 30px; + text-align: center; + background-color: #fff; + border: 1px solid #d9d9d9; + border-radius: 4px; + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.app-pagination-quick-jumper input:hover { + border-color: #1677ff; +} + +.app-pagination-quick-jumper input:focus { + border-color: #1677ff; + box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1); +} + +.app-pagination-quick-jumper input:disabled { + color: rgba(0, 0, 0, 0.25); + cursor: not-allowed; + background-color: rgba(0, 0, 0, 0.04); + border-color: #d9d9d9; + box-shadow: none; } .app-pagination-container .ant-pagination-total-text { @@ -42,6 +156,68 @@ cursor: pointer; } +.app-pagination-container--compact { + align-items: flex-start; +} + +.app-pagination-container--compact .app-pagination-total { + padding-top: 5px; +} + +.app-pagination-container--compact .app-pagination-controls { + flex-wrap: wrap; +} + +.app-pagination-container--compact .ant-pagination { + flex-wrap: wrap; +} + +.app-pagination-container--narrow { + height: auto; + min-height: 50px; + align-items: flex-start; + flex-direction: column; + gap: 8px; +} + +.app-pagination-container--narrow .app-pagination-controls { + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + row-gap: 8px; +} + +.app-pagination-container--narrow .ant-pagination { + justify-content: flex-start; + flex-wrap: wrap; +} + +.app-pagination-container--simple { + min-height: 40px; + align-items: center; + flex-direction: row; + gap: 8px; + flex-wrap: nowrap; +} + +.app-pagination-container--simple .app-pagination-total { + flex: 1 1 auto; + padding-top: 0; +} + +.app-pagination-container--simple .app-pagination-controls { + flex: 0 0 auto; + width: auto; + min-width: max-content; + justify-content: flex-end; + flex-wrap: nowrap; +} + +.app-pagination-container--simple .ant-pagination { + justify-content: flex-end; + flex-wrap: nowrap; +} + @media (max-width: 768px) { .app-pagination-container { height: auto; @@ -53,9 +229,30 @@ } .app-pagination-container .ant-pagination { + width: auto; + justify-content: flex-start; + } + + .app-pagination-controls { width: 100%; justify-content: flex-start; flex-wrap: wrap; row-gap: 8px; } + + .app-pagination-container--simple { + flex-direction: row; + align-items: center; + } + + .app-pagination-container--simple .app-pagination-controls { + width: auto; + justify-content: flex-end; + flex-wrap: nowrap; + } + + .app-pagination-container--simple .ant-pagination { + justify-content: flex-end; + flex-wrap: nowrap; + } } diff --git a/frontend/src/components/shared/AppPagination/index.tsx b/frontend/src/components/shared/AppPagination/index.tsx index b0bd923..1ecaab0 100644 --- a/frontend/src/components/shared/AppPagination/index.tsx +++ b/frontend/src/components/shared/AppPagination/index.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Pagination, PaginationProps } from 'antd'; import { useTranslation } from 'react-i18next'; -import { getDefaultPageSize, getPageSizeOptions, type PaginationVariant } from '@/utils/pagination'; +import { getMinPageSize, getPageSizeOptions, type PaginationVariant } from '@/utils/pagination'; import './index.css'; export interface AppPaginationProps extends PaginationProps { @@ -11,33 +11,130 @@ export interface AppPaginationProps extends PaginationProps { export default function AppPagination(props: AppPaginationProps) { const { t } = useTranslation(); - const { className, showSizeChanger, showTotal, total, variant = 'table', pageSizeOptions, ...restProps } = props; + const { + className, + showQuickJumper, + showSizeChanger, + showTotal, + simple, + total, + variant = 'table', + pageSizeOptions, + onChange, + ...restProps + } = props; + const [jumperValue, setJumperValue] = useState(''); + const [layoutMode, setLayoutMode] = useState<'regular' | 'compact' | 'narrow'>('regular'); + const containerRef = useRef(null); const mergedClassName = ['app-global-pagination', className].filter(Boolean).join(' '); const mergedShowSizeChanger = showSizeChanger === undefined || showSizeChanger === true ? { showSearch: false } : showSizeChanger; - const defaultPageSize = getDefaultPageSize(variant); const mergedPageSizeOptions = pageSizeOptions ?? getPageSizeOptions(variant); + const defaultPageSize = getMinPageSize(mergedPageSizeOptions); const current = Number(restProps.current ?? restProps.defaultCurrent ?? 1); const pageSize = Number(restProps.pageSize ?? restProps.defaultPageSize ?? defaultPageSize); const rangeStart = total > 0 ? (current - 1) * pageSize + 1 : 0; const rangeEnd = total > 0 ? Math.min(current * pageSize, total) : 0; const totalContent = showTotal ? showTotal(total, [rangeStart, rangeEnd]) : t('common.total', { total }); + const pageCount = useMemo(() => Math.max(1, Math.ceil(total / pageSize)), [pageSize, total]); + const shouldShowQuickJumper = showQuickJumper !== false && !simple; + const isDisabled = Boolean(restProps.disabled); + const containerClassName = [ + 'app-pagination-container', + `app-pagination-container--${layoutMode}`, + simple && 'app-pagination-container--simple', + shouldShowQuickJumper && 'app-pagination-container--with-jumper', + ].filter(Boolean).join(' '); + + useEffect(() => { + setJumperValue(''); + }, [current, pageSize, total]); + + useEffect(() => { + if (simple) { + setLayoutMode('regular'); + return; + } + + const node = containerRef.current; + if (!node || typeof ResizeObserver === 'undefined') { + return; + } + + const updateLayoutMode = (width: number) => { + if (width < 520) { + setLayoutMode('narrow'); + } else if (width < 760) { + setLayoutMode('compact'); + } else { + setLayoutMode('regular'); + } + }; + + updateLayoutMode(node.getBoundingClientRect().width); + const observer = new ResizeObserver(([entry]) => { + updateLayoutMode(entry.contentRect.width); + }); + observer.observe(node); + return () => observer.disconnect(); + }, [simple]); + + const handleJump = () => { + if (isDisabled) { + return; + } + if (!jumperValue.trim()) { + return; + } + const targetPage = Number(jumperValue); + if (!Number.isFinite(targetPage)) { + setJumperValue(''); + return; + } + const nextPage = Math.min(Math.max(Math.trunc(targetPage), 1), pageCount); + if (nextPage !== current) { + onChange?.(nextPage, pageSize); + } + setJumperValue(''); + }; return ( -
+
{totalContent}
- +
+ + {shouldShowQuickJumper ? ( + + 跳至 + setJumperValue(event.target.value.replace(/[^\d]/g, ''))} + onBlur={handleJump} + onKeyDown={(event) => { + if (event.key === 'Enter') { + handleJump(); + } + }} + /> + + + ) : null} +
); } diff --git a/frontend/src/components/shared/DataListPanel/DataListPanel.css b/frontend/src/components/shared/DataListPanel/DataListPanel.css index e098f3f..399bed0 100644 --- a/frontend/src/components/shared/DataListPanel/DataListPanel.css +++ b/frontend/src/components/shared/DataListPanel/DataListPanel.css @@ -7,7 +7,7 @@ flex-direction: column; box-sizing: border-box; overflow: hidden; - padding: 8px; + padding: 12px 16px 10px; border-radius: 4px; background-color: #fff; } @@ -221,6 +221,10 @@ } @media (max-width: 768px) { + .data-list-panel { + padding: 12px; + } + .data-list-panel__toolbar { align-items: stretch; flex-direction: column; diff --git a/frontend/src/components/shared/ListTable/ListTable.css b/frontend/src/components/shared/ListTable/ListTable.css index a3b21fc..9662e53 100644 --- a/frontend/src/components/shared/ListTable/ListTable.css +++ b/frontend/src/components/shared/ListTable/ListTable.css @@ -1,5 +1,6 @@ /* 列表表格容器 */ .list-table-container { + position: relative; width: 100%; height: 100%; display: flex; @@ -113,6 +114,53 @@ overflow-y: auto !important; } +.list-table-container .list-table-table--y-scroll.ant-table-wrapper .ant-table-sticky-scroll { + display: none !important; + height: 0 !important; + overflow: hidden !important; +} + +.list-table-container .list-table-table--empty.list-table-table--y-scroll.ant-table-wrapper .ant-table-body { + position: relative; +} + +.list-table-container .list-table-table--empty.ant-table-wrapper .ant-table-placeholder { + display: none !important; + border: 0 !important; + box-shadow: none !important; +} + +.list-table-container .list-table-table--empty.ant-table-wrapper .ant-table-placeholder > td { + padding: 0 !important; + border: 0 !important; + box-shadow: none !important; + position: static !important; +} + +.list-table-container .list-table-table--empty.ant-table-wrapper .ant-table-placeholder .ant-table-cell { + border: 0 !important; + box-shadow: none !important; +} + +.list-table-empty-overlay { + position: absolute; + top: 47px; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + min-height: 0; + background: #fff; + pointer-events: none; +} + +.list-table-empty-overlay .ant-empty { + margin: 0; +} + .list-table-container .ant-table-wrapper, .list-table-container .ant-spin-nested-loading, .list-table-container .ant-spin-container, diff --git a/frontend/src/components/shared/ListTable/ListTable.tsx b/frontend/src/components/shared/ListTable/ListTable.tsx index 954d7f1..414bbbf 100644 --- a/frontend/src/components/shared/ListTable/ListTable.tsx +++ b/frontend/src/components/shared/ListTable/ListTable.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Table } from "antd"; +import { Empty, Table } from "antd"; import type { TablePaginationConfig, TableProps } from "antd"; import "./ListTable.css"; import i18n from "../../../i18n"; @@ -20,6 +20,7 @@ export type ListTableProps> = { selectedRow?: T | null; loading?: boolean; className?: string; + locale?: TableProps["locale"]; onChange?: TableProps["onChange"]; }; @@ -43,6 +44,7 @@ function ListTable>({ selectedRow, loading = false, className = "", + locale, onChange, }: ListTableProps) { const mergedScroll = React.useMemo(() => { @@ -53,6 +55,13 @@ function ListTable>({ }, [scroll]); const hasVerticalScroll = mergedScroll.y !== undefined; + const isEmptyTable = !loading && dataSource.length === 0; + const tableClassName = [ + hasVerticalScroll ? "list-table-table--y-scroll" : undefined, + isEmptyTable ? "list-table-table--empty" : undefined, + ] + .filter(Boolean) + .join(" "); const rowSelection: TableProps["rowSelection"] = onSelectionChange ? { selectedRowKeys, @@ -122,11 +131,18 @@ function ListTable>({ ["--list-table-scroll-y" as string]: typeof mergedScroll.y === "number" ? `${mergedScroll.y}px` : mergedScroll.y, } as React.CSSProperties) : undefined; + const emptyText = locale?.emptyText ?? ( + + ); + const tableLocale = { + ...locale, + emptyText: null, + }; return (
>({ pagination={mergedPagination} scroll={mergedScroll} loading={loading} + locale={tableLocale} onChange={onChange} onRow={(record) => ({ onClick: () => onRowClick?.(record), className: selectedRow?.[rowKey] === record[rowKey] ? "row-selected" : "", })} /> + {isEmptyTable ?
{emptyText}
: null} ); } diff --git a/frontend/src/components/shared/PageContainer/PageContainer.css b/frontend/src/components/shared/PageContainer/PageContainer.css index b03adee..830fd43 100644 --- a/frontend/src/components/shared/PageContainer/PageContainer.css +++ b/frontend/src/components/shared/PageContainer/PageContainer.css @@ -8,6 +8,10 @@ background: #fafafa; } +.page-container.page-container { + padding: 16px; +} + .page-container__header { position: relative; display: flex; @@ -90,3 +94,9 @@ border-radius: 0 0 4px 4px; background: #fff; } + +@media (max-width: 768px) { + .page-container.page-container { + padding: 12px; + } +} diff --git a/frontend/src/index.css b/frontend/src/index.css index d5684d1..ae10293 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -909,44 +909,55 @@ body::after { /* Global Pagination Style */ -.ant-table-wrapper { +.app-page__table-wrap .ant-table-wrapper, +.orgs-table-shell .ant-table-wrapper { height: 100%; } -.ant-table-wrapper .ant-spin-nested-loading { + +.app-page__table-wrap .ant-table-wrapper .ant-spin-nested-loading, +.orgs-table-shell .ant-table-wrapper .ant-spin-nested-loading { height: 100%; } -.ant-table-wrapper .ant-spin-container { + +.app-page__table-wrap .ant-table-wrapper .ant-spin-container, +.orgs-table-shell .ant-table-wrapper .ant-spin-container { height: 100%; display: flex; flex-direction: column; min-height: 0; } -.ant-table-wrapper .ant-table { +.app-page__table-wrap .ant-table-wrapper .ant-table, +.orgs-table-shell .ant-table-wrapper .ant-table { flex: 1; min-height: 0; display: flex; flex-direction: column; } -.ant-table-wrapper .ant-table-container { +.app-page__table-wrap .ant-table-wrapper .ant-table-container, +.orgs-table-shell .ant-table-wrapper .ant-table-container { flex: 1; min-height: 0; display: flex; flex-direction: column; } -.ant-table-wrapper .ant-table-content, -.ant-table-wrapper .ant-table-body { +.app-page__table-wrap .ant-table-wrapper .ant-table-content, +.app-page__table-wrap .ant-table-wrapper .ant-table-body, +.orgs-table-shell .ant-table-wrapper .ant-table-content, +.orgs-table-shell .ant-table-wrapper .ant-table-body { flex: 1; min-height: 0; } -.ant-table-wrapper .ant-table-header { +.app-page__table-wrap .ant-table-wrapper .ant-table-header, +.orgs-table-shell .ant-table-wrapper .ant-table-header { flex-shrink: 0; } -.ant-table-wrapper .ant-table-body { +.app-page__table-wrap .ant-table-wrapper .ant-table-body, +.orgs-table-shell .ant-table-wrapper .ant-table-body { overflow-y: auto !important; } @@ -982,11 +993,44 @@ body::after { .app-global-pagination.ant-pagination .ant-pagination-options, .ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options { margin-inline-start: 12px; + height: 32px; + display: inline-flex; + align-items: center; +} + +.app-global-pagination.ant-pagination .ant-pagination-item-active, +.app-global-pagination.ant-pagination .ant-pagination-item-1.ant-pagination-item-disabled, +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-active, +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-1.ant-pagination-item-disabled { + color: #1677ff !important; + background: #fff !important; + border: 1px solid #1677ff !important; +} + +.app-global-pagination.ant-pagination .ant-pagination-item-active a, +.app-global-pagination.ant-pagination .ant-pagination-item-1.ant-pagination-item-disabled a, +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-active a, +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-1.ant-pagination-item-disabled a { + color: #1677ff !important; + background: transparent !important; + border: 0 !important; } .app-global-pagination.ant-pagination .ant-pagination-options-quick-jumper, .ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options-quick-jumper { margin-inline-start: 12px; + height: 32px; + display: inline-flex; + align-items: center; + line-height: 32px; + white-space: nowrap; +} + +.app-global-pagination.ant-pagination .ant-pagination-options-quick-jumper input, +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options-quick-jumper input { + height: 32px; + line-height: 30px; + vertical-align: top; } .app-global-pagination.ant-pagination .ant-select-selection-search-input, @@ -1299,6 +1343,18 @@ body::after, backdrop-filter: none; } +.route-page-fallback { + width: 100%; + height: 100%; + min-height: 320px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 0; + box-shadow: none; +} + .ant-menu-light { background: transparent !important; color: #333 !important; diff --git a/frontend/src/pages/access/users/index.less b/frontend/src/pages/access/users/index.less index ae43e68..8f166ff 100644 --- a/frontend/src/pages/access/users/index.less +++ b/frontend/src/pages/access/users/index.less @@ -76,6 +76,63 @@ color: #8c8c8c; } +.user-affiliation-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + max-width: 100%; + min-width: 0; +} + +.user-affiliation-tag { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 100%; + height: 24px; + padding: 0 8px; + border: 1px solid #d6e4ff; + border-radius: 4px; + background: #f5f9ff; + color: #315efb; + font-size: 12px; + line-height: 22px; + vertical-align: top; +} + +.user-affiliation-tag--tenant { + border-color: #b7ebd5; + background: #f1fffa; + color: #08936a; +} + +.user-affiliation-tag--org { + border-color: #d6e4ff; + background: #f5f9ff; + color: #315efb; +} + +.user-affiliation-tag .anticon { + flex: 0 0 auto; + font-size: 12px; +} + +.user-affiliation-tag--tenant .anticon { + color: #13a878; +} + +.user-affiliation-tag--org .anticon { + color: #6b8cff; +} + +.user-affiliation-tag__text { + min-width: 0; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .user-phone { color: #8c8c8c; font-size: 13px; @@ -84,6 +141,7 @@ .user-drawer-title { display: flex; align-items: center; + gap: 8px; font-size: 16px; font-weight: 600; } @@ -99,6 +157,24 @@ margin-bottom: 20px; } +.user-membership-section { + margin-bottom: 20px; +} + +.user-membership-title { + margin-top: 8px !important; + margin-bottom: 16px !important; +} + +.user-membership-title--required::before { + content: "*"; + display: inline-block; + margin-inline-end: 4px; + color: #ff4d4f; + font-family: SimSun, sans-serif; + line-height: 1; +} + /* Custom alignment for Row/Col in Form */ .user-form .ant-row { margin-left: -8px !important; diff --git a/frontend/src/pages/access/users/index.tsx b/frontend/src/pages/access/users/index.tsx index 53bf2c7..823f6f0 100644 --- a/frontend/src/pages/access/users/index.tsx +++ b/frontend/src/pages/access/users/index.tsx @@ -66,9 +66,13 @@ const { Title, Text } = Typography; type OrgTreeNode = { value: number; title: string; children: OrgTreeNode[] }; -type Membership = { tenantId?: number; orgId?: number; orgName?: string }; +type Membership = { tenantId?: number; orgId?: number; orgName?: string; orgPath?: string; orgFullName?: string }; type ResetPasswordValues = { newPassword: string; confirmPassword: string }; +const getMembershipOrgName = (membership: Membership) => { + return membership.orgFullName || membership.orgPath || membership.orgName || ""; +}; + function buildOrgTree(list: SysOrg[]): OrgTreeNode[] { const map = new Map(); const roots: OrgTreeNode[] = []; @@ -400,16 +404,26 @@ export default function Users() { ? [{ title: t("users.tenant"), key: "tenant", + width: 260, render: (_: any, record: SysUser) => { if (record.memberships && record.memberships.length > 0) { + const tenantNames = Array.from( + new Set( + record.memberships + .map((membership: Membership) => tenantMap[membership.tenantId || 0] || (membership.tenantId ? `Tenant ${membership.tenantId}` : "")) + .filter(Boolean) + ) + ); return ( -
- {record.memberships.slice(0, 2).map((membership: any) => ( - - {tenantMap[membership.tenantId] || `Tenant ${membership.tenantId}`} - +
+ {tenantNames.map((name, index) => ( + + + + ))} - {record.memberships.length > 2 && +{record.memberships.length - 2} more}
); } @@ -420,17 +434,22 @@ export default function Users() { { title: t("users.orgNode"), key: "org", + width: 340, render: (_: any, record: SysUser) => { if (record.memberships && record.memberships.length > 0) { - const orgNames = record.memberships.map((membership: any) => membership.orgName).filter(Boolean); + const orgNames = Array.from( + new Set(record.memberships.map((membership: Membership) => getMembershipOrgName(membership).trim()).filter(Boolean)) + ); if (orgNames.length > 0) { return ( -
+
{orgNames.map((name: string, index: number) => ( - - - {name} - + + + + ))}
); @@ -531,8 +550,7 @@ export default function Users() { -
} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose footer={
+ {isPlatformMode && ( +
+ {t("usersExt.membershipsTitle")} + { + if (value?.some((membership) => membership?.tenantId)) { + return; + } + throw new Error(t("usersExt.membershipRequired")); + } + } + ]} + > + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + 1 &&
+ + setRoleSelectOpen(false)} + optionFilterProp={isPlatformMode ? "searchText" : "label"}/> {!editing ? ( ) : null} - ({label: item.itemLabel, value: Number(item.itemValue)}))}/> {isPlatformMode && } - {isPlatformMode && ( - <> - {t("usersExt.membershipsTitle")} - - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - 1 && - - - - { void handleUploadIcon(file as File); return Upload.LIST_IGNORE; }}> - } style={{ cursor: "pointer", border: "1px dashed var(--app-border-color)", background: "var(--app-bg-card)" }} /> + + + + { void handleUploadIcon(file as File); return Upload.LIST_IGNORE; }} + > + -
- -
-
+ prev.appType !== curr.appType}> {({ getFieldValue }) => getFieldValue("appType") === "native" ? ( diff --git a/frontend/src/pages/business/HotWords.css b/frontend/src/pages/business/HotWords.css index cc8a0f3..ff005d4 100644 --- a/frontend/src/pages/business/HotWords.css +++ b/frontend/src/pages/business/HotWords.css @@ -178,7 +178,7 @@ .hotwords-group-panel .app-pagination-container { flex-shrink: 0; height: auto; - min-height: 54px; + min-height: 48px; padding: 8px 12px; gap: 8px; overflow: hidden; @@ -193,6 +193,13 @@ font-size: 12px; } +.hotwords-group-panel .app-pagination-controls { + flex: 0 0 auto; + width: auto; + min-width: max-content; + justify-content: flex-end; +} + .hotwords-group-panel .app-pagination-container .ant-pagination { flex: 0 0 auto; width: auto; @@ -243,7 +250,11 @@ } .hotwords-pinyin-tag.ant-tag { + max-width: 132px; + overflow: hidden; border-radius: 4px; + text-overflow: ellipsis; + white-space: nowrap; } .hotwords-search__word { @@ -258,6 +269,27 @@ margin-top: 16px; } +.hotwords-drawer-form.hotwords-modal-form { + margin-top: 0; +} + +.hotwords-drawer-form .ant-form-item-label > label { + color: #333; + font-weight: 500; +} + +.hotwords-drawer-form .ant-input, +.hotwords-drawer-form .ant-input-number, +.hotwords-drawer-form .ant-select-selector, +.hotwords-drawer-form .ant-input-affix-wrapper, +.hotwords-drawer-form textarea.ant-input { + border-radius: 4px; +} + +.hotwords-drawer-form textarea.ant-input { + resize: none; +} + .hotwords-weight-input { width: 100%; } @@ -279,6 +311,14 @@ overflow-y: auto !important; } +.hotwords-table .ant-table-thead > tr > th { + white-space: nowrap; +} + +.hotwords-table .ant-table-tbody > tr > td { + height: 65px; +} + @media (max-width: 992px) { .hotwords-layout { flex-direction: column; diff --git a/frontend/src/pages/business/HotWords.tsx b/frontend/src/pages/business/HotWords.tsx index 2fe886b..aeb170a 100644 --- a/frontend/src/pages/business/HotWords.tsx +++ b/frontend/src/pages/business/HotWords.tsx @@ -20,12 +20,14 @@ import { } from "antd"; import PageContainer from "@/components/shared/PageContainer"; import DataListPanel from "@/components/shared/DataListPanel"; +import FormDrawer from "@/components/shared/FormDrawer"; import SectionCard from "@/components/shared/SectionCard"; import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, + SaveOutlined, SearchOutlined, } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; @@ -205,9 +207,9 @@ const HotWords: React.FC = () => { await loadGroupPage(); }; - const handleSubmit = async () => { + const handleSubmit = async (formValues?: HotWordFormValues) => { try { - const values = await form.validateFields(); + const values = formValues ?? await form.validateFields(); setSubmitLoading(true); const payload = { ...values, @@ -308,6 +310,7 @@ const HotWords: React.FC = () => { title: "热词原文", dataIndex: "word", key: "word", + width: 180, ellipsis: true, render: (text: string) => {text}, }, @@ -315,18 +318,21 @@ const HotWords: React.FC = () => { title: "拼音", dataIndex: "pinyinList", key: "pinyinList", + width: 160, render: (list: string[]) => list?.[0] ? {list[0]} : -, }, { title: "分类", dataIndex: "category", key: "category", + width: 140, render: (value: string) => categories.find((item) => item.itemValue === value)?.itemLabel || value || "-", }, { title: "热词组", dataIndex: "hotWordGroupId", key: "hotWordGroupId", + width: 160, render: (value?: number, record?: HotWordVO) => { const name = record?.hotWordGroupName || (value ? groupNameMap[value] : undefined); return name ? {name} : 未分组; @@ -336,17 +342,21 @@ const HotWords: React.FC = () => { title: "权重", dataIndex: "weight", key: "weight", + width: 90, render: (value: number) => {value}, }, { title: "状态", dataIndex: "status", key: "status", + width: 100, render: (value: number) => value === 1 ? : , }, { title: "操作", key: "action", + width: 140, + fixed: "right" as const, render: (_: unknown, record: HotWordVO) => ( + - + @@ -614,10 +633,10 @@ const HotWords: React.FC = () => { - + - + { )}
- 原文}> + 原文}> {playbackAudioUrl && (
+ {categories.map((i) => )} - + {dictTags.map((item) => )} - + { 提示词编辑器 (Markdown 实时预览) - - + + setPreviewContent(e.target.value)} @@ -571,7 +573,7 @@ const PromptTemplates: React.FC = () => { /> - +
{selectedHotWordGroupId ? ( @@ -582,7 +584,7 @@ const PromptTemplates: React.FC = () => { 未绑定热词组 )}
-
{previewContent}
+
{previewContent}
diff --git a/frontend/src/pages/business/ScreenSaverManagement.css b/frontend/src/pages/business/ScreenSaverManagement.css index a9e6d59..ea6554f 100644 --- a/frontend/src/pages/business/ScreenSaverManagement.css +++ b/frontend/src/pages/business/ScreenSaverManagement.css @@ -67,6 +67,9 @@ .screen-saver-page .screen-saver-table-thumb { width: 120px; aspect-ratio: 8 / 5; + display: flex; + align-items: center; + justify-content: center; overflow: hidden; border-radius: 4px; border: 1px solid #e6e6e6; @@ -74,11 +77,21 @@ } .screen-saver-page .screen-saver-table-thumb img { + display: block; width: 100%; height: 100%; object-fit: cover; } +.screen-saver-page .screen-saver-table-thumb__empty { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: #f5f6fa; +} + .screen-saver-drawer .screen-saver-drawer__footer { display: flex; justify-content: flex-end; @@ -121,11 +134,19 @@ } .screen-saver-drawer .screen-saver-preview-stage img { + display: block; width: 100%; height: 100%; object-fit: cover; } +.screen-saver-drawer .screen-saver-preview-stage__empty { + height: 100%; + display: grid; + place-items: center; + color: #8c9bb3; +} + .screen-saver-crop-modal .ant-modal-content { overflow: hidden; border-radius: 4px; diff --git a/frontend/src/pages/business/ScreenSaverManagement.tsx b/frontend/src/pages/business/ScreenSaverManagement.tsx index 2765a27..249bdc3 100644 --- a/frontend/src/pages/business/ScreenSaverManagement.tsx +++ b/frontend/src/pages/business/ScreenSaverManagement.tsx @@ -614,13 +614,13 @@ export default function ScreenSaverManagement() { { title: "预览", key: "thumb", - width: 90, + width: 140, render: (_, record) => (
{record.imageUrl ? ( - {record.name} + {record.name} ) : ( -
+
)} @@ -884,9 +884,9 @@ export default function ScreenSaverManagement() {
{currentImageUrl ? ( - 屏保预览 + 屏保预览 ) : ( -
+
请选择图片并完成裁剪后上传 diff --git a/frontend/src/pages/dashboard/index.css b/frontend/src/pages/dashboard/index.css index 369b36b..8d45df5 100644 --- a/frontend/src/pages/dashboard/index.css +++ b/frontend/src/pages/dashboard/index.css @@ -14,6 +14,7 @@ .dashboard-monitor-page__content { gap: 12px; + padding: 8px 8px 0; } .dashboard-monitor-page__stats { @@ -29,6 +30,10 @@ box-shadow: none; } +.dashboard-monitor-page__stat-card .ant-card-body { + padding: 18px 24px; +} + .dashboard-monitor-page__stat-label { font-size: 13px; } @@ -36,6 +41,7 @@ .dashboard-monitor-page__task-panel { flex: 1; min-height: 0; + padding: 12px 16px 10px; } .dashboard-monitor-page__task-panel .data-list-panel__toolbar { @@ -55,12 +61,12 @@ min-height: 0; overflow-y: auto; overflow-x: hidden; - padding-right: 4px; + padding: 0 6px 0 0; overscroll-behavior: contain; } .dashboard-monitor-page__task-item { - padding: 20px 0; + padding: 18px 0; border-bottom: 1px solid #f0f2f5; } diff --git a/frontend/src/pages/devices/index.less b/frontend/src/pages/devices/index.less index 2b7e21c..9315697 100644 --- a/frontend/src/pages/devices/index.less +++ b/frontend/src/pages/devices/index.less @@ -60,29 +60,40 @@ .device-cell { width: 100%; min-width: 0; + display: flex; align-items: center; + gap: 10px; } .device-cell__content { flex: 1; min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; } .device-name { display: block; font-weight: 600; color: #262626; + max-width: 100%; } .device-code { display: block; font-size: 12px; color: #8c8c8c; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .device-drawer-title { display: flex; align-items: center; + gap: 8px; font-size: 16px; font-weight: 600; } diff --git a/frontend/src/pages/devices/index.tsx b/frontend/src/pages/devices/index.tsx index d2e0bb6..9919d77 100644 --- a/frontend/src/pages/devices/index.tsx +++ b/frontend/src/pages/devices/index.tsx @@ -1,4 +1,4 @@ -import { Button, Drawer, Form, Input, Popconfirm, Select, Space, Tag, Typography, message } from "antd"; +import { Button, Drawer, Form, Input, Popconfirm, Select, Space, Tag, Tooltip, Typography, message } from "antd"; import { CheckCircleOutlined, DeleteOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, ReloadOutlined, SearchOutlined, ThunderboltOutlined, UndoOutlined, UserOutlined } from "@ant-design/icons"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -10,10 +10,12 @@ import AppPagination from "@/components/shared/AppPagination"; import SectionCard from "@/components/shared/SectionCard"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; +import { getDefaultPageSize } from "@/utils/pagination"; import type { DeviceInfo } from "@/types"; import "./index.less"; const { Text } = Typography; +const DEFAULT_TABLE_PAGE_SIZE = getDefaultPageSize("table"); type DeviceFormValues = { deviceName?: string; @@ -21,6 +23,16 @@ type DeviceFormValues = { weatherCityName?: string; }; +const formatDeviceCode = (code?: string) => { + if (!code) { + return "-"; + } + if (code.length <= 16) { + return code; + } + return `${code.slice(0, 8)}...${code.slice(-6)}`; +}; + export default function Devices() { const { t } = useTranslation(); const { can } = usePermission(); @@ -32,7 +44,7 @@ export default function Devices() { const [open, setOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); - const [pagination, setPagination] = useState({ current: 1, pageSize: 20 }); + const [pagination, setPagination] = useState({ current: 1, pageSize: DEFAULT_TABLE_PAGE_SIZE }); const loadData = async () => { setLoading(true); @@ -141,10 +153,9 @@ export default function Devices() { { title: t("devicesExt.device"), key: "device", - width: 280, - ellipsis: {showTitle: true}, + width: 320, render: (_value: unknown, record: DeviceInfo) => ( - +
@@ -152,11 +163,11 @@ export default function Devices() { {record.deviceName || t("devicesExt.unnamedDevice")} - - {record.deviceCode} - + + 设备编码:{formatDeviceCode(record.deviceCode)} +
-
+
) }, { @@ -351,8 +362,8 @@ export default function Devices() { -
} open={open} diff --git a/frontend/src/pages/home/index.less b/frontend/src/pages/home/index.less index 83face9..2354728 100644 --- a/frontend/src/pages/home/index.less +++ b/frontend/src/pages/home/index.less @@ -17,8 +17,18 @@ background: transparent; } +.home-shell-card { + padding: 0; +} + .home-shell-card .section-card__content { - padding: 8px; + padding: 0; + background: transparent; +} + +.home-data-panel { + padding: 0; + background: transparent; } .home-data-panel .data-list-panel__table-area, @@ -27,6 +37,12 @@ min-height: 0; } +.home-data-panel .data-list-panel__table-container, +.home-data-panel .data-list-panel__table-area, +.home-data-panel .app-page__table-wrap { + overflow: hidden; +} + .home-container { position: relative; flex: 1; diff --git a/frontend/src/pages/organization/tenants/index.less b/frontend/src/pages/organization/tenants/index.less index 8e4337a..25a9d74 100644 --- a/frontend/src/pages/organization/tenants/index.less +++ b/frontend/src/pages/organization/tenants/index.less @@ -54,19 +54,46 @@ display: flex; flex-direction: column; border: 1px solid #e6e6e6 !important; - border-radius: 4px !important; - box-shadow: none !important; + border-radius: 16px !important; + background: #fff !important; + box-shadow: 0 2px 8px rgba(24, 39, 75, 0.04) !important; overflow: hidden; + transform: translateY(0); + will-change: transform, box-shadow; + transition: + border-color 0.18s ease, + background 0.18s ease, + box-shadow 0.18s ease, + transform 0.18s ease; +} + +.tenant-card:hover { + border-color: #8fb0fb !important; + background: #f9fafe !important; + box-shadow: 0 8px 20px rgba(60, 112, 245, 0.14) !important; + transform: translateY(-3px); +} + +.tenant-card:active { + box-shadow: 0 4px 12px rgba(60, 112, 245, 0.12) !important; + transform: translateY(-1px); } .tenant-card .ant-card-body { flex: 1; + min-height: 0; display: flex; flex-direction: column; + padding: 24px !important; } .tenant-card .ant-card-actions { border-top-color: #f0f0f0; + background: rgba(255, 255, 255, 0.82); +} + +.tenant-card:hover .ant-card-actions { + background: #f9fafe; } .tenant-card__header { @@ -77,7 +104,7 @@ .tenant-card__avatar { margin-right: 12px; - border-radius: 8px; + border-radius: 10px; } .tenant-card__avatar--enabled { @@ -112,7 +139,7 @@ .tenant-card__status { flex: 0 0 auto; margin: 0; - border-radius: 4px; + border-radius: 10px; } .tenant-card__code { @@ -161,6 +188,22 @@ color: #ff4d4f; } +.tenant-card__action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 10px; + transition: + color 0.18s ease, + background 0.18s ease; +} + +.tenant-card__action:hover { + background: rgba(60, 112, 245, 0.08); +} + .tenants-search-form { justify-content: flex-end; gap: 8px 0; diff --git a/frontend/src/pages/system/dictionaries/index.tsx b/frontend/src/pages/system/dictionaries/index.tsx index fb7d3ee..0e95231 100644 --- a/frontend/src/pages/system/dictionaries/index.tsx +++ b/frontend/src/pages/system/dictionaries/index.tsx @@ -234,7 +234,7 @@ export default function Dictionaries() {
-
{ const resetParams = { current: 1, - size: 20, + size: DEFAULT_TABLE_PAGE_SIZE, username: "", moduleName: "", status: undefined, diff --git a/frontend/src/pages/system/platform-settings/index.tsx b/frontend/src/pages/system/platform-settings/index.tsx index bcae9cc..19590ff 100644 --- a/frontend/src/pages/system/platform-settings/index.tsx +++ b/frontend/src/pages/system/platform-settings/index.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Col, Form, Input, Row, Upload, message } from "antd"; +import { Button, Card, Col, Form, 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"; @@ -91,7 +91,7 @@ export default function PlatformSettings() { - {t("platformSettings.basicInfo")}} className="mb-6" loading={loading}> + {t("platformSettings.basicInfo")}} className="mb-6" loading={loading}> - {t("platformSettings.brandAssets")}} className="app-page__content-card mb-6" loading={loading}> + {t("platformSettings.brandAssets")}} className="app-page__content-card mb-6" loading={loading}> @@ -142,7 +142,7 @@ export default function PlatformSettings() { - {t("platformSettings.complianceFooter")}} className="app-page__content-card" loading={loading}> + {t("platformSettings.complianceFooter")}} className="app-page__content-card" loading={loading}> diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx index 30dd6e8..1b737bb 100644 --- a/frontend/src/routes/routes.tsx +++ b/frontend/src/routes/routes.tsx @@ -37,7 +37,7 @@ const MeetingDetail = lazy(() => import("../pages/business/MeetingDetail")); function RouteFallback() { return ( -
+
); diff --git a/frontend/src/utils/pagination.ts b/frontend/src/utils/pagination.ts index 54e38ad..ff1e26a 100644 --- a/frontend/src/utils/pagination.ts +++ b/frontend/src/utils/pagination.ts @@ -6,11 +6,19 @@ export type PaginationVariant = 'table' | 'card'; export const TABLE_PAGE_SIZE_OPTIONS = ['10', '20', '50', '100']; export const CARD_PAGE_SIZE_OPTIONS = ['8', '16', '24', '40']; -export const getDefaultPageSize = (variant: PaginationVariant = 'table') => (variant === 'card' ? 8 : 10); - export const getPageSizeOptions = (variant: PaginationVariant = 'table') => variant === 'card' ? CARD_PAGE_SIZE_OPTIONS : TABLE_PAGE_SIZE_OPTIONS; +export const getMinPageSize = (pageSizeOptions: Array) => { + const sizes = pageSizeOptions + .map((item) => Number(item)) + .filter((item) => Number.isFinite(item) && item > 0); + return sizes.length > 0 ? Math.min(...sizes) : 10; +}; + +export const getDefaultPageSize = (variant: PaginationVariant = 'table') => + getMinPageSize(getPageSizeOptions(variant)); + export interface StandardPaginationConfig extends TablePaginationConfig { variant?: PaginationVariant; } @@ -26,6 +34,7 @@ export const getStandardPagination = ( overrides: StandardPaginationConfig = {} ): TablePaginationConfig => { const { variant = 'table', pageSizeOptions, className, showSizeChanger, ...restOverrides } = overrides; + const mergedPageSizeOptions = pageSizeOptions ?? getPageSizeOptions(variant); const mergedClassName = ['app-global-pagination', className].filter(Boolean).join(' '); const mergedShowSizeChanger = showSizeChanger === undefined || showSizeChanger === true @@ -39,7 +48,7 @@ export const getStandardPagination = ( onChange, showQuickJumper: true, showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }), - pageSizeOptions: pageSizeOptions ?? getPageSizeOptions(variant), + pageSizeOptions: mergedPageSizeOptions, size: 'default', position: ['bottomRight'], ...restOverrides,