Compare commits

..

No commits in common. "ba52e214373bafa68460fa0aae5124bc088dc1df" and "17dbeefdf895d15468552deda0ef21ae79c943f7" have entirely different histories.

22 changed files with 314 additions and 605 deletions

View File

@ -177,13 +177,6 @@
line-height: 22px; line-height: 22px;
} }
.data-list-panel__table-area .ant-table-thead > tr > th:last-child,
.data-list-panel__table-area .ant-table-tbody > tr > td:last-child,
.data-list-panel__table-area .ant-table-thead > tr > th.ant-table-cell-fix-right,
.data-list-panel__table-area .ant-table-tbody > tr > td.ant-table-cell-fix-right {
padding-right: 24px;
}
.data-list-panel__table-area .ant-table-content, .data-list-panel__table-area .ant-table-content,
.data-list-panel__table-area .ant-table-body { .data-list-panel__table-area .ant-table-body {
overflow-x: auto !important; overflow-x: auto !important;

View File

@ -70,24 +70,6 @@
border-bottom: 1px solid var(--app-border-color, #f0f0f0); border-bottom: 1px solid var(--app-border-color, #f0f0f0);
} }
.list-table-container .ant-table-thead > tr > th:last-child,
.list-table-container .ant-table-tbody > tr > td:last-child,
.list-table-container .ant-table-thead > tr > th.ant-table-cell-fix-right,
.list-table-container .ant-table-tbody > tr > td.ant-table-cell-fix-right {
padding-right: 24px;
}
.list-table-container .ant-table-thead > tr > th:last-child,
.list-table-container .ant-table-tbody > tr > td:last-child {
text-align: right;
}
.list-table-container .ant-table-tbody > tr > td:last-child .ant-space,
.list-table-container .ant-table-tbody > tr > td.ant-table-cell-fix-right .ant-space {
justify-content: flex-end;
width: 100%;
}
.list-table-container .ant-table-tbody > tr:not(.row-selected):not(.ant-table-row-selected):hover > td { .list-table-container .ant-table-tbody > tr:not(.row-selected):not(.ant-table-row-selected):hover > td {
background: var(--app-surface-color, #fff) !important; background: var(--app-surface-color, #fff) !important;
} }

View File

@ -1,38 +0,0 @@
.summary-stat-cards {
flex-shrink: 0;
min-width: 0;
}
.summary-stat-cards__card {
height: 100%;
min-height: 112px;
border: 1px solid #e6e6e6;
border-radius: 12px;
background: #fff;
box-shadow: none;
}
.summary-stat-cards__card .ant-card-body {
height: 100%;
padding: 26px 24px;
display: flex;
align-items: center;
}
.summary-stat-cards__label {
color: var(--app-text-secondary, #9095a1);
font-size: 13px;
}
.summary-stat-cards__card .ant-statistic-content {
display: flex;
align-items: center;
line-height: 1;
}
.summary-stat-cards__icon {
margin-right: 8px;
display: inline-flex;
align-items: center;
line-height: 1;
}

View File

@ -1,41 +0,0 @@
import type { CSSProperties, ReactNode } from "react";
import { Card, Col, Row, Statistic } from "antd";
import "./SummaryStatCards.css";
export interface SummaryStatCardItem {
key: string;
label: ReactNode;
value: string | number;
icon: ReactNode;
color: string;
}
interface SummaryStatCardsProps {
items: SummaryStatCardItem[];
ariaLabel?: string;
}
export default function SummaryStatCards({ items, ariaLabel }: SummaryStatCardsProps) {
return (
<div className="summary-stat-cards" aria-label={ariaLabel}>
<Row gutter={[16, 16]}>
{items.map((item) => (
<Col xs={24} sm={12} xl={6} key={item.key}>
<Card className="summary-stat-cards__card" variant="borderless">
<Statistic
title={<span className="summary-stat-cards__label">{item.label}</span>}
value={item.value}
valueStyle={{ color: item.color, fontWeight: 700 } as CSSProperties}
prefix={
<span className="summary-stat-cards__icon" style={{ color: item.color }}>
{item.icon}
</span>
}
/>
</Card>
</Col>
))}
</Row>
</div>
);
}

View File

@ -680,18 +680,8 @@ export default function Users() {
</Form.Item> </Form.Item>
) : null} ) : null}
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}><Form.Item label={t("common.status")} name="status" initialValue={1}><Select
<Form.Item options={statusDict.map((item) => ({label: item.itemLabel, value: Number(item.itemValue)}))}/></Form.Item></Col>
label={t("common.status")}
name="status"
initialValue={1}
valuePropName="checked"
getValueProps={(value) => ({ checked: value !== 0 })}
getValueFromEvent={(checked: boolean) => (checked ? 1 : 0)}
>
<Switch checkedChildren={t("usersExt.enabled")} unCheckedChildren={t("usersExt.disabled")} />
</Form.Item>
</Col>
{isPlatformMode && {isPlatformMode &&
<Col span={12}><Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked"><Switch/></Form.Item></Col>} <Col span={12}><Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked"><Switch/></Form.Item></Col>}
</Row> </Row>

View File

@ -12,7 +12,42 @@
background: transparent; background: transparent;
} }
.client-management-page__content { .client-summary-chips {
gap: 12px; display: flex;
padding: 8px; align-items: center;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
}
.client-summary-chip {
height: 32px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 10px;
border: 1px solid #e6e6e6;
border-radius: 4px;
background: #f9fafe;
color: #333;
font-size: 13px;
line-height: 20px;
white-space: nowrap;
}
.client-summary-chip .anticon {
color: #3c70f5;
}
.client-summary-chip strong {
color: #1677ff;
font-size: 16px;
font-weight: 600;
}
@media (max-width: 768px) {
.client-summary-chips,
.client-summary-chip {
width: 100%;
}
} }

View File

@ -23,7 +23,6 @@ import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable"; import ListTable from "@/components/shared/ListTable/ListTable";
import AppPagination from "@/components/shared/AppPagination"; import AppPagination from "@/components/shared/AppPagination";
import SectionCard from "@/components/shared/SectionCard"; import SectionCard from "@/components/shared/SectionCard";
import SummaryStatCards from "@/components/shared/SummaryStatCards";
import { createClientDownload, deleteClientDownload, listClientDownloads, type ClientDownloadDTO, type ClientDownloadVO, updateClientDownload, uploadClientPackage } from "@/api/business/client"; import { createClientDownload, deleteClientDownload, listClientDownloads, type ClientDownloadDTO, type ClientDownloadVO, updateClientDownload, uploadClientPackage } from "@/api/business/client";
import { fetchDictItemsByTypeCode } from "@/api/dict"; import { fetchDictItemsByTypeCode } from "@/api/dict";
import { useDict } from "@/hooks/useDict"; import { useDict } from "@/hooks/useDict";
@ -237,13 +236,6 @@ export default function ClientManagement() {
groups: platformGroups.length, groups: platformGroups.length,
}), [platformGroups.length, records]); }), [platformGroups.length, records]);
const statCards = useMemo(() => [
{ key: "total", label: "发布总数", value: stats.total, icon: <CloudUploadOutlined />, color: "#1890ff" },
{ key: "enabled", label: "已启用", value: stats.enabled, icon: <LaptopOutlined />, color: "#faad14" },
{ key: "latest", label: "最新版本", value: stats.latest, icon: <MobileOutlined />, color: "#52c41a" },
{ key: "groups", label: "平台分组", value: stats.groups, icon: <RocketOutlined />, color: "#13c2c2" },
], [stats]);
const openCreate = () => { const openCreate = () => {
const firstOption = platformGroups[0]?.options[0]; const firstOption = platformGroups[0]?.options[0];
if (!firstOption) { if (!firstOption) {
@ -418,14 +410,20 @@ export default function ClientManagement() {
<SectionCard <SectionCard
title="客户端管理" title="客户端管理"
description="发布平台由数据字典 client_platform 驱动,并按父子分组展示。" description="发布平台由数据字典 client_platform 驱动,并按父子分组展示。"
contentClassName="client-management-page__content"
> >
<SummaryStatCards items={statCards} ariaLabel="客户端发布统计" />
<DataListPanel <DataListPanel
leftActions={ leftActions={
<Space wrap>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}> <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button> </Button>
<div className="client-summary-chips" aria-label="客户端发布统计">
<span className="client-summary-chip"><CloudUploadOutlined aria-hidden="true" /><span></span><strong>{stats.total}</strong></span>
<span className="client-summary-chip"><LaptopOutlined aria-hidden="true" /><span></span><strong>{stats.enabled}</strong></span>
<span className="client-summary-chip"><MobileOutlined aria-hidden="true" /><span></span><strong>{stats.latest}</strong></span>
<span className="client-summary-chip"><RocketOutlined aria-hidden="true" /><span></span><strong>{stats.groups}</strong></span>
</div>
</Space>
} }
rightActions={ rightActions={
<Space wrap> <Space wrap>

View File

@ -12,9 +12,37 @@
background: transparent; background: transparent;
} }
.external-app-page__content { .external-app-summary-chips {
gap: 12px; display: flex;
padding: 8px; align-items: center;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
}
.external-app-summary-chip {
height: 32px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 10px;
border: 1px solid #e6e6e6;
border-radius: 4px;
background: #f9fafe;
color: #333;
font-size: 13px;
line-height: 20px;
white-space: nowrap;
}
.external-app-summary-chip .anticon {
color: #3c70f5;
}
.external-app-summary-chip strong {
color: #1677ff;
font-size: 16px;
font-weight: 600;
} }
.external-app-icon-form-item { .external-app-icon-form-item {
@ -105,6 +133,11 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.external-app-summary-chips,
.external-app-summary-chip {
width: 100%;
}
.external-app-icon-picker { .external-app-icon-picker {
width: 100%; width: 100%;
min-height: 116px; min-height: 116px;

View File

@ -7,13 +7,11 @@ import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable"; import ListTable from "@/components/shared/ListTable/ListTable";
import AppPagination from "@/components/shared/AppPagination"; import AppPagination from "@/components/shared/AppPagination";
import SectionCard from "@/components/shared/SectionCard"; import SectionCard from "@/components/shared/SectionCard";
import SummaryStatCards from "@/components/shared/SummaryStatCards";
import { createExternalApp, deleteExternalApp, listExternalApps, type ExternalAppDTO, type ExternalAppVO, updateExternalApp, uploadExternalAppApk, uploadExternalAppIcon } from "@/api/business/externalApp"; import { createExternalApp, deleteExternalApp, listExternalApps, type ExternalAppDTO, type ExternalAppVO, updateExternalApp, uploadExternalAppApk, uploadExternalAppIcon } from "@/api/business/externalApp";
import "./ExternalAppManagement.css"; import "./ExternalAppManagement.css";
const { Text } = Typography; const { Text } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
const ICON_UPLOAD_ACCEPT = ".png,.jpg,.jpeg,image/png,image/jpeg";
type ExternalAppFormValues = { type ExternalAppFormValues = {
appName: string; appName: string;
@ -123,13 +121,6 @@ export default function ExternalAppManagement() {
enabled: records.filter((item) => item.status === 1).length, enabled: records.filter((item) => item.status === 1).length,
}), [records]); }), [records]);
const statCards = useMemo(() => [
{ key: "total", label: "应用总数", value: stats.total, icon: <AppstoreOutlined />, color: "#1890ff" },
{ key: "native", label: "原生应用", value: stats.native, icon: <RobotOutlined />, color: "#faad14" },
{ key: "web", label: "Web 应用", value: stats.web, icon: <GlobalOutlined />, color: "#52c41a" },
{ key: "enabled", label: "已启用", value: stats.enabled, icon: <PictureOutlined />, color: "#13c2c2" },
], [stats]);
const openCreate = () => { const openCreate = () => {
setEditing(null); setEditing(null);
form.resetFields(); form.resetFields();
@ -227,20 +218,6 @@ export default function ExternalAppManagement() {
} }
}; };
const beforeUploadIcon = (file: File) => {
const isAllowedIcon = file.type
? file.type === "image/png" || file.type === "image/jpeg"
: /\.(png|jpe?g)$/i.test(file.name);
if (!isAllowedIcon) {
message.warning("应用图标仅支持上传 PNG / JPG 格式");
return Upload.LIST_IGNORE;
}
void handleUploadIcon(file);
return Upload.LIST_IGNORE;
};
const handleToggleStatus = async (record: ExternalAppVO, checked: boolean) => { const handleToggleStatus = async (record: ExternalAppVO, checked: boolean) => {
await updateExternalApp(record.id, { status: checked ? 1 : 0 }); await updateExternalApp(record.id, { status: checked ? 1 : 0 });
message.success(checked ? "已启用应用" : "已停用应用"); message.success(checked ? "已启用应用" : "已停用应用");
@ -322,14 +299,20 @@ export default function ExternalAppManagement() {
<SectionCard <SectionCard
title="外部应用管理" title="外部应用管理"
description="统一维护首页九宫格与抽屉入口中的原生应用、Web 服务和应用图标资源。" description="统一维护首页九宫格与抽屉入口中的原生应用、Web 服务和应用图标资源。"
contentClassName="external-app-page__content"
> >
<SummaryStatCards items={statCards} ariaLabel="外部应用统计" />
<DataListPanel <DataListPanel
leftActions={ leftActions={
<Space wrap>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}> <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button> </Button>
<div className="external-app-summary-chips" aria-label="外部应用统计">
<span className="external-app-summary-chip"><AppstoreOutlined aria-hidden="true" /><span></span><strong>{stats.total}</strong></span>
<span className="external-app-summary-chip"><RobotOutlined aria-hidden="true" /><span></span><strong>{stats.native}</strong></span>
<span className="external-app-summary-chip"><GlobalOutlined aria-hidden="true" /><span>Web </span><strong>{stats.web}</strong></span>
<span className="external-app-summary-chip"><PictureOutlined aria-hidden="true" /><span></span><strong>{stats.enabled}</strong></span>
</div>
</Space>
} }
rightActions={ rightActions={
<Space wrap> <Space wrap>
@ -377,9 +360,9 @@ export default function ExternalAppManagement() {
<Form.Item label="应用图标" className="external-app-icon-form-item"> <Form.Item label="应用图标" className="external-app-icon-form-item">
<Upload <Upload
accept={ICON_UPLOAD_ACCEPT} accept="image/png,image/jpeg,image/webp,image/svg+xml"
showUploadList={false} showUploadList={false}
beforeUpload={(file) => beforeUploadIcon(file as File)} beforeUpload={(file) => { void handleUploadIcon(file as File); return Upload.LIST_IGNORE; }}
> >
<button <button
type="button" type="button"
@ -397,7 +380,7 @@ export default function ExternalAppManagement() {
<span className="external-app-icon-picker__text"> <span className="external-app-icon-picker__text">
{uploadingIcon ? "上传中..." : watchedIconUrl ? "更换图标" : "点击上传图标"} {uploadingIcon ? "上传中..." : watchedIconUrl ? "更换图标" : "点击上传图标"}
</span> </span>
<span className="external-app-icon-picker__hint">PNG / JPG</span> <span className="external-app-icon-picker__hint">PNG / JPG / WebP / SVG</span>
</button> </button>
</Upload> </Upload>
</Form.Item> </Form.Item>

View File

@ -12,7 +12,42 @@
background: transparent; background: transparent;
} }
.license-management-page__content { .license-summary-chips {
gap: 12px; display: flex;
padding: 8px; align-items: center;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
}
.license-summary-chip {
height: 32px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 10px;
border: 1px solid #e6e6e6;
border-radius: 4px;
background: #f9fafe;
color: #333;
font-size: 13px;
line-height: 20px;
white-space: nowrap;
}
.license-summary-chip .anticon {
color: #3c70f5;
}
.license-summary-chip strong {
color: #1677ff;
font-size: 16px;
font-weight: 600;
}
@media (max-width: 768px) {
.license-summary-chips,
.license-summary-chip {
width: 100%;
}
} }

View File

@ -7,7 +7,6 @@ import AppPagination from "@/components/shared/AppPagination";
import DataListPanel from "@/components/shared/DataListPanel"; import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable"; import ListTable from "@/components/shared/ListTable/ListTable";
import SectionCard from "@/components/shared/SectionCard"; import SectionCard from "@/components/shared/SectionCard";
import SummaryStatCards from "@/components/shared/SummaryStatCards";
import { listLicenses, type LicenseVO } from "@/api/business/license"; import { listLicenses, type LicenseVO } from "@/api/business/license";
import "./LicenseManagement.css"; import "./LicenseManagement.css";
@ -93,13 +92,6 @@ export default function LicenseManagement() {
available: records.filter((item) => item.licenseStatus === 1).length, available: records.filter((item) => item.licenseStatus === 1).length,
}), [records]); }), [records]);
const statCards = useMemo(() => [
{ key: "total", label: "授权总数", value: stats.total, icon: <KeyOutlined />, color: "#1890ff" },
{ key: "using", label: "使用中", value: stats.using, icon: <LinkOutlined />, color: "#faad14" },
{ key: "formal", label: "正式授权", value: stats.formal, icon: <CheckCircleOutlined />, color: "#52c41a" },
{ key: "available", label: "可分配", value: stats.available, icon: <ClockCircleOutlined />, color: "#13c2c2" },
], [stats]);
const columns: ColumnsType<LicenseVO> = [ const columns: ColumnsType<LicenseVO> = [
{ {
title: "授权标识", title: "授权标识",
@ -147,10 +139,32 @@ export default function LicenseManagement() {
<SectionCard <SectionCard
title="授权码管理" title="授权码管理"
description="查看当前租户授权池" description="查看当前租户授权池"
contentClassName="license-management-page__content"
> >
<SummaryStatCards items={statCards} ariaLabel="授权码统计" />
<DataListPanel <DataListPanel
leftActions={
<div className="license-summary-chips" aria-label="授权码统计">
<span className="license-summary-chip">
<KeyOutlined aria-hidden="true" />
<span></span>
<strong className="tabular-nums">{stats.total}</strong>
</span>
<span className="license-summary-chip">
<LinkOutlined aria-hidden="true" />
<span>使</span>
<strong className="tabular-nums">{stats.using}</strong>
</span>
<span className="license-summary-chip">
<CheckCircleOutlined aria-hidden="true" />
<span></span>
<strong className="tabular-nums">{stats.formal}</strong>
</span>
<span className="license-summary-chip">
<ClockCircleOutlined aria-hidden="true" />
<span></span>
<strong className="tabular-nums">{stats.available}</strong>
</span>
</div>
}
rightActions={ rightActions={
<Space wrap> <Space wrap>
<Input <Input

View File

@ -16,91 +16,8 @@
min-width: 0; min-width: 0;
} }
.meeting-detail-section-card .section-card__header {
align-items: flex-start;
gap: 16px 20px;
}
.meeting-detail-section-card .section-card__title-wrap {
min-width: 0;
}
.meeting-detail-section-card .section-card__title { .meeting-detail-section-card .section-card__title {
min-width: 0; min-width: 0;
padding-bottom: 14px;
align-items: flex-start;
overflow: visible;
white-space: normal;
text-overflow: clip;
}
.meeting-detail-section-card .section-card__title::before {
display: none;
}
.meeting-detail-page-v2 .section-card__extra {
align-self: flex-start;
padding-top: 2px;
}
.meeting-detail-page-v2 .section-card__extra .ant-space {
justify-content: flex-end;
row-gap: 8px;
}
.meeting-detail-page-v2 .section-card__extra .ant-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
}
.meeting-detail-page-v2 .section-card__extra .ant-btn .anticon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.meeting-detail-page-v2 .meeting-detail-title-wrap {
display: flex;
align-items: flex-start;
gap: 16px;
min-width: 0;
max-width: 100%;
}
.meeting-detail-page-v2 .meeting-detail-title-icon {
width: 42px;
height: 42px;
flex: 0 0 42px;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
font-size: 20px;
}
.meeting-detail-page-v2 .meeting-detail-title-icon .anticon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.meeting-detail-page-v2 .meeting-detail-title-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 7px;
}
.meeting-detail-page-v2 .meeting-detail-title-row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
max-width: 100%;
} }
.meeting-detail-section-title { .meeting-detail-section-title {
@ -116,8 +33,6 @@
} }
.meeting-detail-page-v2 .meeting-detail-title-text { .meeting-detail-page-v2 .meeting-detail-title-text {
display: block;
flex: 0 1 auto;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
color: #333; color: #333;
@ -145,55 +60,12 @@
.meeting-detail-page-v2 .meeting-detail-meta-row { .meeting-detail-page-v2 .meeting-detail-meta-row {
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; gap: 10px 16px;
gap: 8px 16px;
color: #9095a1; color: #9095a1;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
} }
.meeting-detail-page-v2 .meeting-detail-meta-row span {
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 22px;
}
.meeting-detail-page-v2 .meeting-detail-meta-row .anticon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.meeting-detail-page-v2 .meeting-detail-config-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 2px;
}
.meeting-detail-page-v2 .meeting-detail-config-item {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 6px 12px;
border: 1px solid #d8e3ff;
border-radius: 999px;
background: #f3f7ff;
color: #58627f;
font-size: 13px;
line-height: 20px;
white-space: nowrap;
}
.meeting-detail-page-v2 .meeting-detail-config-item strong {
color: #333;
font-weight: 600;
}
.meeting-detail-section-content { .meeting-detail-section-content {
overflow: hidden; overflow: hidden;
} }
@ -550,16 +422,6 @@
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.meeting-detail-section-card .section-card__header {
flex-direction: column;
}
.meeting-detail-page-v2 .section-card__extra,
.meeting-detail-page-v2 .section-card__extra .ant-space {
width: 100%;
justify-content: flex-start;
}
.meeting-detail-section-content { .meeting-detail-section-content {
overflow: auto; overflow: auto;
} }
@ -576,190 +438,13 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.meeting-detail-page-v2 .meeting-detail-title-wrap { .meeting-detail-page-v2 .section-card__extra,
gap: 12px; .meeting-detail-page-v2 .section-card__extra .ant-space {
} width: 100%;
justify-content: flex-start;
.meeting-detail-page-v2 .meeting-detail-title-icon {
width: 38px;
height: 38px;
flex-basis: 38px;
font-size: 18px;
} }
.meeting-detail-page-v2 .meeting-detail-title-text { .meeting-detail-page-v2 .meeting-detail-title-text {
white-space: normal; white-space: normal;
} }
.meeting-detail-page-v2 .meeting-detail-title-row {
align-items: flex-start;
}
.meeting-detail-page-v2 .meeting-detail-config-item {
white-space: normal;
}
}
.meeting-detail-page-v2 .detail-right-column {
padding-right: 0;
}
.meeting-detail-page-v2 .transcript-player-anchor > .transcript-workspace-card,
.meeting-detail-page-v2 .transcript-player-anchor > .transcript-workspace-card.left-flow-card,
.meeting-detail-page-v2 .transcript-player-anchor > .transcript-workspace-card.ant-card {
overflow: hidden !important;
border: 1px solid #e5e7eb !important;
border-radius: 18px !important;
background: #ffffff !important;
box-shadow: none !important;
}
.meeting-detail-page-v2 .transcript-panel-header {
display: flex;
align-items: center;
justify-content: flex-start;
flex-shrink: 0;
min-height: 56px;
padding: 0 24px;
border-bottom: 1px solid #e5e7eb;
background: #ffffff;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs {
display: inline-flex !important;
align-items: center;
align-self: stretch;
gap: 24px !important;
min-height: 56px !important;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs button {
position: relative;
height: 56px !important;
min-width: 64px;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
color: #4b5563 !important;
font-size: 16px !important;
font-weight: 700 !important;
box-shadow: none !important;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs button + button {
margin-left: 0 !important;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs button:hover {
color: #3c70f5 !important;
background: transparent !important;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs button.active {
color: #2f5edb !important;
background: transparent !important;
box-shadow: none !important;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs button.active::after {
content: "" !important;
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 3px;
border-radius: 999px 999px 0 0;
background: #6258ff;
}
.meeting-detail-page-v2 .transcript-scroll-shell {
padding: 24px 20px 18px 18px !important;
background: #ffffff !important;
}
.meeting-detail-page-v2 .transcript-player.transcript-player--floating {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px !important;
border: 0 !important;
border-radius: 18px !important;
background: rgba(255, 255, 255, 0.96) !important;
box-shadow: 0 16px 40px rgba(20, 35, 70, 0.16) !important;
backdrop-filter: blur(12px) !important;
}
.meeting-detail-page-v2 .transcript-player .player-main-btn {
width: 46px !important;
height: 46px !important;
flex: 0 0 46px;
border: 0 !important;
border-radius: 50% !important;
background: linear-gradient(135deg, #3c70f5 0%, #6b8cff 100%) !important;
color: #ffffff !important;
box-shadow: 0 10px 24px rgba(60, 112, 245, 0.32) !important;
}
.meeting-detail-page-v2 .transcript-player .player-ghost-btn {
height: 36px !important;
border: 1px solid #dce6ff !important;
border-radius: 999px !important;
background: #f4f7ff !important;
color: #3c70f5 !important;
box-shadow: none !important;
}
.meeting-detail-page-v2 .transcript-player .player-progress-shell {
flex: 1;
min-width: 0;
}
.meeting-detail-page-v2 .transcript-player .player-range {
height: 6px !important;
border-radius: 999px !important;
background: linear-gradient(
90deg,
#3c70f5 0%,
#3c70f5 var(--player-progress, 0%),
#e6ebf5 var(--player-progress, 0%),
#e6ebf5 100%
) !important;
}
.meeting-detail-page-v2 .transcript-player .player-range::-webkit-slider-thumb {
width: 16px;
height: 16px;
border: 3px solid #ffffff !important;
border-radius: 50% !important;
background: #3c70f5 !important;
box-shadow: 0 4px 10px rgba(60, 112, 245, 0.28) !important;
}
.meeting-detail-page-v2 .transcript-player .player-range::-moz-range-thumb {
width: 16px;
height: 16px;
border: 3px solid #ffffff !important;
border-radius: 50% !important;
background: #3c70f5 !important;
box-shadow: 0 4px 10px rgba(60, 112, 245, 0.28) !important;
}
@media (max-width: 768px) {
.meeting-detail-page-v2 .transcript-panel-header {
align-items: flex-start;
flex-direction: column;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs {
width: 100%;
}
.meeting-detail-page-v2 .transcript-panel-header .transcript-stage-tabs button {
flex: 1;
}
} }

View File

@ -153,8 +153,6 @@ const resolveAudioExtension = (audioUrl?: string) => {
return normalizedUrl.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || 'mp3'; return normalizedUrl.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || 'mp3';
}; };
const normalizeTagDisplay = (tag: string) => tag.replace(/^#+\s*/, '').trim();
const getMeetingAudioDownloadName = (meeting?: Pick<MeetingVO, 'title' | 'audioUrl' | 'playbackAudioUrl'> | null) => { const getMeetingAudioDownloadName = (meeting?: Pick<MeetingVO, 'title' | 'audioUrl' | 'playbackAudioUrl'> | null) => {
const audioUrl = resolveMeetingPlaybackAudioUrl(meeting); const audioUrl = resolveMeetingPlaybackAudioUrl(meeting);
const extension = resolveAudioExtension(audioUrl); const extension = resolveAudioExtension(audioUrl);
@ -2360,10 +2358,6 @@ const MeetingDetail: React.FC = () => {
); );
} }
const playerProgressPercent = audioDuration > 0
? Math.min(100, Math.max(0, (audioCurrentTime / audioDuration) * 100))
: 0;
return ( return (
<PageContainer title={null} className="meeting-detail-page-v2"> <PageContainer title={null} className="meeting-detail-page-v2">
<SectionCard <SectionCard
@ -2387,9 +2381,7 @@ const MeetingDetail: React.FC = () => {
<span> <span>
<ClockCircleOutlined /> {dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')} <ClockCircleOutlined /> {dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')}
</span> </span>
<span> <span>{meeting.participants || '未指定'}</span>
<UserOutlined /> {meeting.participants || '未指定'}
</span>
</div> </div>
<div className="meeting-detail-config-row"> <div className="meeting-detail-config-row">
<span className="meeting-detail-config-item"> <span className="meeting-detail-config-item">
@ -2625,7 +2617,7 @@ const MeetingDetail: React.FC = () => {
} }
}} }}
> >
<span>{normalizeTagDisplay(tag)}</span> <span>#{tag}</span>
{isOwner && isSelected && <CheckCircleFilled style={{ fontSize: 12 }} />} {isOwner && isSelected && <CheckCircleFilled style={{ fontSize: 12 }} />}
</div> </div>
); );
@ -2782,13 +2774,10 @@ const MeetingDetail: React.FC = () => {
</audio> </audio>
)} )}
<div className="transcript-panel-header"> <div className="transcript-stage-tabs">
<div className="transcript-stage-tabs" role="tablist" aria-label="内容切换">
{aiCatalogEnabled && ( {aiCatalogEnabled && (
<button <button
type="button" type="button"
role="tab"
aria-selected={workspaceTab === 'catalog'}
className={workspaceTab === 'catalog' ? 'active' : ''} className={workspaceTab === 'catalog' ? 'active' : ''}
onClick={() => setWorkspaceTab('catalog')} onClick={() => setWorkspaceTab('catalog')}
> >
@ -2797,15 +2786,12 @@ const MeetingDetail: React.FC = () => {
)} )}
<button <button
type="button" type="button"
role="tab"
aria-selected={workspaceTab === 'transcript'}
className={workspaceTab === 'transcript' ? 'active' : ''} className={workspaceTab === 'transcript' ? 'active' : ''}
onClick={() => setWorkspaceTab('transcript')} onClick={() => setWorkspaceTab('transcript')}
> >
</button> </button>
</div> </div>
</div>
<div className="transcript-scroll-shell"> <div className="transcript-scroll-shell">
{aiCatalogEnabled && workspaceTab === 'catalog' ? ( {aiCatalogEnabled && workspaceTab === 'catalog' ? (
@ -2935,7 +2921,6 @@ const MeetingDetail: React.FC = () => {
max={audioDuration || 0} max={audioDuration || 0}
step={0.1} step={0.1}
value={Math.min(audioCurrentTime, audioDuration || 0)} value={Math.min(audioCurrentTime, audioDuration || 0)}
style={{ '--player-progress': `${playerProgressPercent}%` } as React.CSSProperties}
onChange={handleAudioProgressChange} onChange={handleAudioProgressChange}
/> />
</div> </div>
@ -2972,10 +2957,11 @@ const MeetingDetail: React.FC = () => {
} }
/* 当转录行处于活动状态时,调整高亮样式以保持可读性 */ /* 当转录行处于活动状态时,调整高亮样式以保持可读性 */
.ant-list-item.transcript-row.active .highlight-text { .ant-list-item.transcript-row.active .highlight-text {
background: linear-gradient(120deg, rgba(95, 81, 255, 0.15) 0%, rgba(108, 140, 255, 0.1) 100%); background: rgba(255, 255, 255, 0.2);
border-bottom-color: #5f51ff; border-bottom-color: #fff;
color: #4335eb; color: #fff;
text-shadow: none; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
animation: none; /* 活动行内不需要闪烁,避免视觉混乱 */
} }
.summary-keyword-link { .summary-keyword-link {
color: #5f51ff; color: #5f51ff;
@ -3920,17 +3906,16 @@ const MeetingDetail: React.FC = () => {
} }
.ant-list-item.transcript-row.linked .transcript-bubble, .ant-list-item.transcript-row.linked .transcript-bubble,
.ant-list-item.transcript-row.linked .transcript-bubble-editing { .ant-list-item.transcript-row.linked .transcript-bubble-editing {
background: #ffffff; background: linear-gradient(135deg, rgba(95, 81, 255, 0.08), rgba(108, 140, 255, 0.06));
border-color: rgba(60, 112, 245, 0.28); border-color: rgba(95, 81, 255, 0.2);
color: #2d3553; box-shadow: 0 10px 24px rgba(95, 81, 255, 0.06);
box-shadow: 0 10px 24px rgba(60, 112, 245, 0.08);
} }
.ant-list-item.transcript-row.active .transcript-bubble, .ant-list-item.transcript-row.active .transcript-bubble,
.ant-list-item.transcript-row.active .transcript-bubble-editing { .ant-list-item.transcript-row.active .transcript-bubble-editing {
border-color: rgba(60, 112, 245, 0.34); border-color: rgba(95, 81, 255, 0.16);
background: #ffffff; background: linear-gradient(135deg, #5b41ff, #6a5cff);
color: #2d3553; color: #ffffff;
box-shadow: 0 12px 28px rgba(60, 112, 245, 0.12); box-shadow: 0 12px 28px rgba(95, 81, 255, 0.2);
} }
.transcript-entry { .transcript-entry {
flex: 1; flex: 1;

View File

@ -92,7 +92,7 @@
min-height: 0; min-height: 0;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding: 6px 8px 12px; padding: 4px;
} }
.meetings-card-scroll .ant-list { .meetings-card-scroll .ant-list {
@ -122,33 +122,29 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
border: none !important; border: 1px solid #e6e6e6 !important;
border-radius: 16px !important; border-radius: 16px !important;
background: #fff !important; background: #fff !important;
box-shadow: box-shadow: 0 2px 8px rgba(24, 39, 75, 0.04) !important;
0 10px 26px rgba(24, 39, 75, 0.07),
0 2px 8px rgba(24, 39, 75, 0.05) !important;
cursor: pointer; cursor: pointer;
transform: translateY(0); transform: translateY(0);
will-change: transform, box-shadow; will-change: transform, box-shadow;
transition: transition:
border-color 0.18s ease,
background 0.18s ease, background 0.18s ease,
box-shadow 0.18s ease, box-shadow 0.18s ease,
transform 0.18s ease; transform 0.18s ease;
} }
.meeting-card-v2.ant-card:hover { .meeting-card-v2.ant-card:hover {
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%) !important; border-color: #8fb0fb !important;
box-shadow: background: #f9fafe !important;
0 16px 34px rgba(24, 39, 75, 0.12), box-shadow: 0 8px 20px rgba(60, 112, 245, 0.14) !important;
0 6px 14px rgba(60, 112, 245, 0.10) !important;
transform: translateY(-3px); transform: translateY(-3px);
} }
.meeting-card-v2.ant-card:active { .meeting-card-v2.ant-card:active {
box-shadow: box-shadow: 0 4px 12px rgba(60, 112, 245, 0.12) !important;
0 8px 18px rgba(24, 39, 75, 0.10),
0 2px 8px rgba(60, 112, 245, 0.10) !important;
transform: translateY(-1px); transform: translateY(-1px);
} }

View File

@ -249,13 +249,11 @@ const TableStatusCell: React.FC<{ meeting: MeetingVO; progress: MeetingProgress
}; };
const getMeetingTagList = (tags: unknown) => { const getMeetingTagList = (tags: unknown) => {
const normalizeMeetingTag = (tag: unknown) => String(tag).replace(/^#+\s*/, '').trim();
if (Array.isArray(tags)) { if (Array.isArray(tags)) {
return tags.map(normalizeMeetingTag).filter(Boolean); return tags.map((tag) => String(tag).trim()).filter(Boolean);
} }
if (typeof tags === "string") { if (typeof tags === "string") {
return tags.split(",").map(normalizeMeetingTag).filter(Boolean); return tags.split(",").map((tag) => tag.trim()).filter(Boolean);
} }
return []; return [];
}; };
@ -384,7 +382,7 @@ const MeetingCardItem: React.FC<{
{tags.length > 0 ? ( {tags.length > 0 ? (
tags.map(tag => ( tags.map(tag => (
<span key={tag} className="meeting-card-v2__tag"> <span key={tag} className="meeting-card-v2__tag">
{tag} #{tag}
</span> </span>
)) ))
) : ( ) : (

View File

@ -17,6 +17,27 @@
padding: 8px; padding: 8px;
} }
.dashboard-monitor-page__stats {
flex-shrink: 0;
min-width: 0;
}
.dashboard-monitor-page__stat-card {
height: 100%;
border: 1px solid #e6e6e6;
border-radius: 4px;
background: #fff;
box-shadow: none;
}
.dashboard-monitor-page__stat-card .ant-card-body {
padding: 18px 24px;
}
.dashboard-monitor-page__stat-label {
font-size: 13px;
}
.dashboard-monitor-page__task-panel { .dashboard-monitor-page__task-panel {
flex: 1; flex: 1;
min-height: 0; min-height: 0;

View File

@ -3,8 +3,7 @@ import PageContainer from "@/components/shared/PageContainer";
import AppPagination from '@/components/shared/AppPagination'; import AppPagination from '@/components/shared/AppPagination';
import DataListPanel from "@/components/shared/DataListPanel"; import DataListPanel from "@/components/shared/DataListPanel";
import SectionCard from "@/components/shared/SectionCard"; import SectionCard from "@/components/shared/SectionCard";
import SummaryStatCards from "@/components/shared/SummaryStatCards"; import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd';
import { Row, Col, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd';
import { import {
HistoryOutlined, HistoryOutlined,
CheckCircleOutlined, CheckCircleOutlined,
@ -160,16 +159,15 @@ export const Dashboard: React.FC = () => {
}; };
const statCards = [ const statCards = [
{ key: 'totalMeetings', label: '累计会议记录', value: stats?.totalMeetings ?? 0, icon: <HistoryOutlined />, color: '#1890ff' }, { label: '累计会议记录', value: stats?.totalMeetings, icon: <HistoryOutlined />, color: '#1890ff' },
{ {
key: 'processingTasks',
label: '当前分析中任务', label: '当前分析中任务',
value: stats?.processingTasks ?? 0, value: stats?.processingTasks,
icon: processingCount > 0 ? <LoadingOutlined spin /> : <ClockCircleOutlined />, icon: processingCount > 0 ? <LoadingOutlined spin /> : <ClockCircleOutlined />,
color: '#faad14' color: '#faad14'
}, },
{ key: 'todayNew', label: '今日新增分析', value: stats?.todayNew ?? 0, icon: <RiseOutlined />, color: '#52c41a' }, { label: '今日新增分析', value: stats?.todayNew, icon: <RiseOutlined />, color: '#52c41a' },
{ key: 'successRate', label: 'AI 处理成功率', value: `${stats?.successRate || 100}%`, icon: <CheckCircleOutlined />, color: '#13c2c2' }, { label: 'AI 处理成功率', value: `${stats?.successRate || 100}%`, icon: <CheckCircleOutlined />, color: '#13c2c2' },
]; ];
return ( return (
@ -182,7 +180,22 @@ export const Dashboard: React.FC = () => {
description="系统运行概览与最近任务动态。" description="系统运行概览与最近任务动态。"
contentClassName="dashboard-monitor-page__content" contentClassName="dashboard-monitor-page__content"
> >
<SummaryStatCards items={statCards} ariaLabel="任务监控统计" /> <div className="dashboard-monitor-page__stats">
<Row gutter={[16, 16]}>
{statCards.map((s, idx) => (
<Col xs={24} sm={12} xl={6} key={idx}>
<Card className="dashboard-monitor-page__stat-card" variant="borderless">
<Statistic
title={<Text type="secondary" className="dashboard-monitor-page__stat-label">{s.label}</Text>}
value={s.value || 0}
valueStyle={{ color: s.color, fontWeight: 700 }}
prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })}
/>
</Card>
</Col>
))}
</Row>
</div>
<DataListPanel <DataListPanel
className="dashboard-monitor-page__task-panel" className="dashboard-monitor-page__task-panel"

View File

@ -12,9 +12,37 @@
background: transparent; background: transparent;
} }
.devices-page__content { .devices-summary-chips {
gap: 12px; display: flex;
padding: 8px; align-items: center;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
}
.devices-summary-chip {
height: 32px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 10px;
border: 1px solid #e6e6e6;
border-radius: 4px;
background: #f9fafe;
color: #333;
font-size: 13px;
line-height: 20px;
white-space: nowrap;
}
.devices-summary-chip .anticon {
color: #3c70f5;
}
.devices-summary-chip strong {
color: #1677ff;
font-size: 16px;
font-weight: 600;
} }
.device-icon-placeholder { .device-icon-placeholder {
@ -87,4 +115,9 @@
.devices-page { .devices-page {
padding: 8px; padding: 8px;
} }
.devices-summary-chips,
.devices-summary-chip {
width: 100%;
}
} }

View File

@ -8,7 +8,6 @@ import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable"; import ListTable from "@/components/shared/ListTable/ListTable";
import AppPagination from "@/components/shared/AppPagination"; import AppPagination from "@/components/shared/AppPagination";
import SectionCard from "@/components/shared/SectionCard"; import SectionCard from "@/components/shared/SectionCard";
import SummaryStatCards from "@/components/shared/SummaryStatCards";
import { useDict } from "@/hooks/useDict"; import { useDict } from "@/hooks/useDict";
import { usePermission } from "@/hooks/usePermission"; import { usePermission } from "@/hooks/usePermission";
import { getDefaultPageSize } from "@/utils/pagination"; import { getDefaultPageSize } from "@/utils/pagination";
@ -94,12 +93,6 @@ export default function Devices() {
return { total, online, enabled }; return { total, online, enabled };
}, [devices]); }, [devices]);
const statCards = useMemo(() => [
{ key: "total", label: t("devicesExt.totalDevices"), value: stats.total, icon: <DesktopOutlined />, color: "#1890ff" },
{ key: "online", label: t("devicesExt.onlineDevices"), value: stats.online, icon: <ThunderboltOutlined />, color: "#faad14" },
{ key: "enabled", label: t("devicesExt.enabledDevices"), value: stats.enabled, icon: <CheckCircleOutlined />, color: "#1677ff" },
], [stats, t]);
const openEdit = (record: DeviceInfo) => { const openEdit = (record: DeviceInfo) => {
setEditing(record); setEditing(record);
form.setFieldsValue({ form.setFieldsValue({
@ -312,10 +305,27 @@ export default function Devices() {
<SectionCard <SectionCard
title={t("devices.title")} title={t("devices.title")}
description={t("devices.subtitle")} description={t("devices.subtitle")}
contentClassName="devices-page__content"
> >
<SummaryStatCards items={statCards} ariaLabel={t("devices.title")} />
<DataListPanel <DataListPanel
leftActions={
<div className="devices-summary-chips" aria-label={t("devices.title")}>
<span className="devices-summary-chip">
<DesktopOutlined aria-hidden="true" />
<span>{t("devicesExt.totalDevices")}</span>
<strong className="tabular-nums">{stats.total}</strong>
</span>
<span className="devices-summary-chip">
<ThunderboltOutlined aria-hidden="true" />
<span>{t("devicesExt.onlineDevices")}</span>
<strong className="tabular-nums">{stats.online}</strong>
</span>
<span className="devices-summary-chip">
<CheckCircleOutlined aria-hidden="true" />
<span>{t("devicesExt.enabledDevices")}</span>
<strong className="tabular-nums">{stats.enabled}</strong>
</span>
</div>
}
rightActions={ rightActions={
<Space wrap> <Space wrap>
<Input <Input

View File

@ -28,7 +28,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
padding: 8px 8px 12px; padding: 8px 4px 0;
} }
.tenants-grid { .tenants-grid {
@ -53,33 +53,29 @@
min-height: 230px; min-height: 230px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: none !important; border: 1px solid #e6e6e6 !important;
border-radius: 16px !important; border-radius: 16px !important;
background: #fff !important; background: #fff !important;
box-shadow: box-shadow: 0 2px 8px rgba(24, 39, 75, 0.04) !important;
0 10px 26px rgba(24, 39, 75, 0.07),
0 2px 8px rgba(24, 39, 75, 0.05) !important;
overflow: hidden; overflow: hidden;
transform: translateY(0); transform: translateY(0);
will-change: transform, box-shadow; will-change: transform, box-shadow;
transition: transition:
border-color 0.18s ease,
background 0.18s ease, background 0.18s ease,
box-shadow 0.18s ease, box-shadow 0.18s ease,
transform 0.18s ease; transform 0.18s ease;
} }
.tenant-card:hover { .tenant-card:hover {
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%) !important; border-color: #8fb0fb !important;
box-shadow: background: #f9fafe !important;
0 16px 34px rgba(24, 39, 75, 0.12), box-shadow: 0 8px 20px rgba(60, 112, 245, 0.14) !important;
0 6px 14px rgba(60, 112, 245, 0.10) !important;
transform: translateY(-3px); transform: translateY(-3px);
} }
.tenant-card:active { .tenant-card:active {
box-shadow: box-shadow: 0 4px 12px rgba(60, 112, 245, 0.12) !important;
0 8px 18px rgba(24, 39, 75, 0.10),
0 2px 8px rgba(60, 112, 245, 0.10) !important;
transform: translateY(-1px); transform: translateY(-1px);
} }

View File

@ -52,18 +52,6 @@
min-height: 0; /* Important for flex child scroll */ min-height: 0; /* Important for flex child scroll */
} }
.dict-type-search-row {
display: flex;
gap: 8px;
width: 100%;
margin-bottom: 12px;
}
.dict-type-search-input {
flex: 1;
min-width: 0;
}
.full-height { .full-height {
height: 100%; height: 100%;
} }

View File

@ -183,8 +183,8 @@ export default function Dictionaries() {
}} extra={can("sys_dict:type:create") && }} extra={can("sys_dict:type:create") &&
<Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true"/>} <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true"/>}
onClick={handleAddType}>{t("common.create")}</Button>}> onClick={handleAddType}>{t("common.create")}</Button>}>
<div className="dict-type-search-row flex-shrink-0"> <div style={{ marginBottom: 12 }} className="flex-shrink-0">
<Space.Compact className="dict-type-search-input"> <Space.Compact style={{ width: "100%" }}>
<Input <Input
placeholder={t("dictsExt.searchTypes")} placeholder={t("dictsExt.searchTypes")}
allowClear allowClear
@ -193,8 +193,8 @@ export default function Dictionaries() {
onPressEnter={() => handleTypeSearch(typeKeyword)} onPressEnter={() => handleTypeSearch(typeKeyword)}
/> />
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={() => handleTypeSearch(typeKeyword)}>{t("common.search")}</Button> <Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={() => handleTypeSearch(typeKeyword)}>{t("common.search")}</Button>
</Space.Compact>
<Button onClick={handleTypeReset}>{t("common.reset")}</Button> <Button onClick={handleTypeReset}>{t("common.reset")}</Button>
</Space.Compact>
</div> </div>
<div className="dict-type-table-wrap"> <div className="dict-type-table-wrap">
<Table <Table