diff --git a/frontend/src/components/shared/DataListPanel/DataListPanel.css b/frontend/src/components/shared/DataListPanel/DataListPanel.css index 399bed0..cc35036 100644 --- a/frontend/src/components/shared/DataListPanel/DataListPanel.css +++ b/frontend/src/components/shared/DataListPanel/DataListPanel.css @@ -177,6 +177,13 @@ 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-body { overflow-x: auto !important; diff --git a/frontend/src/components/shared/ListTable/ListTable.css b/frontend/src/components/shared/ListTable/ListTable.css index 1f2d1d9..95c8d2c 100644 --- a/frontend/src/components/shared/ListTable/ListTable.css +++ b/frontend/src/components/shared/ListTable/ListTable.css @@ -70,6 +70,24 @@ 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 { background: var(--app-surface-color, #fff) !important; } diff --git a/frontend/src/components/shared/SummaryStatCards/SummaryStatCards.css b/frontend/src/components/shared/SummaryStatCards/SummaryStatCards.css new file mode 100644 index 0000000..3f0453f --- /dev/null +++ b/frontend/src/components/shared/SummaryStatCards/SummaryStatCards.css @@ -0,0 +1,38 @@ +.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; +} diff --git a/frontend/src/components/shared/SummaryStatCards/index.tsx b/frontend/src/components/shared/SummaryStatCards/index.tsx new file mode 100644 index 0000000..b1c80af --- /dev/null +++ b/frontend/src/components/shared/SummaryStatCards/index.tsx @@ -0,0 +1,41 @@ +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 ( +
+ + {items.map((item) => ( + + + {item.label}} + value={item.value} + valueStyle={{ color: item.color, fontWeight: 700 } as CSSProperties} + prefix={ + + {item.icon} + + } + /> + + + ))} + +
+ ); +} diff --git a/frontend/src/pages/access/users/index.tsx b/frontend/src/pages/access/users/index.tsx index 6d13325..9484866 100644 --- a/frontend/src/pages/access/users/index.tsx +++ b/frontend/src/pages/access/users/index.tsx @@ -680,8 +680,18 @@ export default function Users() { ) : null} - .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; + } } diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index 046fae0..cfd5b52 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -153,6 +153,8 @@ const resolveAudioExtension = (audioUrl?: string) => { return normalizedUrl.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || 'mp3'; }; +const normalizeTagDisplay = (tag: string) => tag.replace(/^#+\s*/, '').trim(); + const getMeetingAudioDownloadName = (meeting?: Pick | null) => { const audioUrl = resolveMeetingPlaybackAudioUrl(meeting); const extension = resolveAudioExtension(audioUrl); @@ -2357,6 +2359,10 @@ const MeetingDetail: React.FC = () => { ); } + const playerProgressPercent = audioDuration > 0 + ? Math.min(100, Math.max(0, (audioCurrentTime / audioDuration) * 100)) + : 0; + return ( { {dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')} - {meeting.participants || '未指定'} + + {meeting.participants || '未指定'} +
@@ -2616,7 +2624,7 @@ const MeetingDetail: React.FC = () => { } }} > - #{tag} + {normalizeTagDisplay(tag)} {isOwner && isSelected && }
); @@ -2773,23 +2781,29 @@ const MeetingDetail: React.FC = () => { )} -
- {aiCatalogEnabled && ( +
+
+ {aiCatalogEnabled && ( + + )} - )} - +
@@ -2920,6 +2934,7 @@ const MeetingDetail: React.FC = () => { max={audioDuration || 0} step={0.1} value={Math.min(audioCurrentTime, audioDuration || 0)} + style={{ '--player-progress': `${playerProgressPercent}%` } as React.CSSProperties} onChange={handleAudioProgressChange} />
@@ -2956,11 +2971,10 @@ const MeetingDetail: React.FC = () => { } /* 当转录行处于活动状态时,调整高亮样式以保持可读性 */ .ant-list-item.transcript-row.active .highlight-text { - background: rgba(255, 255, 255, 0.2); - border-bottom-color: #fff; - color: #fff; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - animation: none; /* 活动行内不需要闪烁,避免视觉混乱 */ + background: linear-gradient(120deg, rgba(95, 81, 255, 0.15) 0%, rgba(108, 140, 255, 0.1) 100%); + border-bottom-color: #5f51ff; + color: #4335eb; + text-shadow: none; } .summary-keyword-link { color: #5f51ff; @@ -3905,16 +3919,17 @@ const MeetingDetail: React.FC = () => { } .ant-list-item.transcript-row.linked .transcript-bubble, .ant-list-item.transcript-row.linked .transcript-bubble-editing { - background: linear-gradient(135deg, rgba(95, 81, 255, 0.08), rgba(108, 140, 255, 0.06)); - border-color: rgba(95, 81, 255, 0.2); - box-shadow: 0 10px 24px rgba(95, 81, 255, 0.06); + background: #ffffff; + border-color: rgba(60, 112, 245, 0.28); + color: #2d3553; + 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-editing { - border-color: rgba(95, 81, 255, 0.16); - background: linear-gradient(135deg, #5b41ff, #6a5cff); - color: #ffffff; - box-shadow: 0 12px 28px rgba(95, 81, 255, 0.2); + border-color: rgba(60, 112, 245, 0.34); + background: #ffffff; + color: #2d3553; + box-shadow: 0 12px 28px rgba(60, 112, 245, 0.12); } .transcript-entry { flex: 1; diff --git a/frontend/src/pages/business/Meetings.css b/frontend/src/pages/business/Meetings.css index 84580fb..6f1173e 100644 --- a/frontend/src/pages/business/Meetings.css +++ b/frontend/src/pages/business/Meetings.css @@ -92,7 +92,7 @@ min-height: 0; overflow-x: hidden; overflow-y: auto; - padding: 4px; + padding: 6px 8px 12px; } .meetings-card-scroll .ant-list { @@ -122,29 +122,33 @@ display: flex; flex-direction: column; overflow: hidden; - border: 1px solid #e6e6e6 !important; + border: none !important; border-radius: 16px !important; background: #fff !important; - box-shadow: 0 2px 8px rgba(24, 39, 75, 0.04) !important; + box-shadow: + 0 10px 26px rgba(24, 39, 75, 0.07), + 0 2px 8px rgba(24, 39, 75, 0.05) !important; cursor: pointer; 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; } .meeting-card-v2.ant-card:hover { - border-color: #8fb0fb !important; - background: #f9fafe !important; - box-shadow: 0 8px 20px rgba(60, 112, 245, 0.14) !important; + background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%) !important; + box-shadow: + 0 16px 34px rgba(24, 39, 75, 0.12), + 0 6px 14px rgba(60, 112, 245, 0.10) !important; transform: translateY(-3px); } .meeting-card-v2.ant-card:active { - box-shadow: 0 4px 12px rgba(60, 112, 245, 0.12) !important; + box-shadow: + 0 8px 18px rgba(24, 39, 75, 0.10), + 0 2px 8px rgba(60, 112, 245, 0.10) !important; transform: translateY(-1px); } diff --git a/frontend/src/pages/business/Meetings.tsx b/frontend/src/pages/business/Meetings.tsx index ff04ef4..fb9755f 100644 --- a/frontend/src/pages/business/Meetings.tsx +++ b/frontend/src/pages/business/Meetings.tsx @@ -249,11 +249,13 @@ const TableStatusCell: React.FC<{ meeting: MeetingVO; progress: MeetingProgress }; const getMeetingTagList = (tags: unknown) => { + const normalizeMeetingTag = (tag: unknown) => String(tag).replace(/^#+\s*/, '').trim(); + if (Array.isArray(tags)) { - return tags.map((tag) => String(tag).trim()).filter(Boolean); + return tags.map(normalizeMeetingTag).filter(Boolean); } if (typeof tags === "string") { - return tags.split(",").map((tag) => tag.trim()).filter(Boolean); + return tags.split(",").map(normalizeMeetingTag).filter(Boolean); } return []; }; @@ -382,7 +384,7 @@ const MeetingCardItem: React.FC<{ {tags.length > 0 ? ( tags.map(tag => ( - #{tag} + {tag} )) ) : ( diff --git a/frontend/src/pages/dashboard/index.css b/frontend/src/pages/dashboard/index.css index 2f2374f..387a328 100644 --- a/frontend/src/pages/dashboard/index.css +++ b/frontend/src/pages/dashboard/index.css @@ -17,27 +17,6 @@ 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 { flex: 1; min-height: 0; diff --git a/frontend/src/pages/dashboard/index.tsx b/frontend/src/pages/dashboard/index.tsx index 42246c9..b53ff8e 100644 --- a/frontend/src/pages/dashboard/index.tsx +++ b/frontend/src/pages/dashboard/index.tsx @@ -3,7 +3,8 @@ import PageContainer from "@/components/shared/PageContainer"; import AppPagination from '@/components/shared/AppPagination'; import DataListPanel from "@/components/shared/DataListPanel"; import SectionCard from "@/components/shared/SectionCard"; -import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd'; +import SummaryStatCards from "@/components/shared/SummaryStatCards"; +import { Row, Col, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd'; import { HistoryOutlined, CheckCircleOutlined, @@ -159,15 +160,16 @@ export const Dashboard: React.FC = () => { }; const statCards = [ - { label: '累计会议记录', value: stats?.totalMeetings, icon: , color: '#1890ff' }, + { key: 'totalMeetings', label: '累计会议记录', value: stats?.totalMeetings ?? 0, icon: , color: '#1890ff' }, { + key: 'processingTasks', label: '当前分析中任务', - value: stats?.processingTasks, + value: stats?.processingTasks ?? 0, icon: processingCount > 0 ? : , color: '#faad14' }, - { label: '今日新增分析', value: stats?.todayNew, icon: , color: '#52c41a' }, - { label: 'AI 处理成功率', value: `${stats?.successRate || 100}%`, icon: , color: '#13c2c2' }, + { key: 'todayNew', label: '今日新增分析', value: stats?.todayNew ?? 0, icon: , color: '#52c41a' }, + { key: 'successRate', label: 'AI 处理成功率', value: `${stats?.successRate || 100}%`, icon: , color: '#13c2c2' }, ]; return ( @@ -180,22 +182,7 @@ export const Dashboard: React.FC = () => { description="系统运行概览与最近任务动态。" contentClassName="dashboard-monitor-page__content" > -
- - {statCards.map((s, idx) => ( - - - {s.label}} - value={s.value || 0} - valueStyle={{ color: s.color, fontWeight: 700 }} - prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })} - /> - - - ))} - -
+ [ + { key: "total", label: t("devicesExt.totalDevices"), value: stats.total, icon: , color: "#1890ff" }, + { key: "online", label: t("devicesExt.onlineDevices"), value: stats.online, icon: , color: "#faad14" }, + { key: "enabled", label: t("devicesExt.enabledDevices"), value: stats.enabled, icon: , color: "#1677ff" }, + ], [stats, t]); + const openEdit = (record: DeviceInfo) => { setEditing(record); form.setFieldsValue({ @@ -305,27 +312,10 @@ export default function Devices() { + - - - - - - -
- } rightActions={ } onClick={handleAddType}>{t("common.create")}}> -
- +
+ handleTypeSearch(typeKeyword)} /> - +