feat: 界面优化

dev_na
puz 2026-07-02 09:30:48 +08:00
parent b0a3dedb99
commit 3337285135
24 changed files with 137 additions and 178 deletions

1
frontend/.gitignore vendored
View File

@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
*.tsbuildinfo
*.local
# Editor directories and files

View File

@ -27,6 +27,7 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^24.0.10",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
@ -1580,6 +1581,16 @@
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.13.2",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.2.tgz",
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/@types/pako/-/pako-2.0.4.tgz",
@ -4666,6 +4677,13 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz",

View File

@ -28,6 +28,7 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^24.0.10",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import { ConfigProvider, theme, App as AntdApp } from "antd";
import zhCN from "antd/locale/zh_CN";
import enUS from "antd/locale/en_US";
@ -6,10 +6,8 @@ import { useTranslation } from "react-i18next";
import AppRoutes from "./routes";
import { getOpenPlatformConfig } from "./api";
import { useThemeStore } from "./store/themeStore";
import type { SysPlatformConfig } from "./types";
export default function App() {
const [config, setConfig] = useState<SysPlatformConfig | null>(null);
const { colorPrimary, themeMode, initTheme } = useThemeStore();
const { i18n } = useTranslation();
const antdLocale = useMemo(() => (i18n.language === "en-US" ? enUS : zhCN), [i18n.language]);
@ -19,7 +17,6 @@ export default function App() {
const fetchConfig = async () => {
try {
const data = await getOpenPlatformConfig();
setConfig(data);
if (data.projectName) {
document.title = data.projectName;
}

View File

@ -4,7 +4,7 @@
width: 100%;
min-height: 52px;
padding: 8px 0;
background: #fff;
background: var(--app-surface-color, #fff);
box-sizing: border-box;
min-width: 0;
overflow: visible;
@ -19,7 +19,7 @@
.app-pagination-total {
flex: 0 0 auto;
min-width: 0;
color: #333;
color: var(--app-text-main, #333);
white-space: nowrap;
}
@ -97,7 +97,7 @@
align-items: stretch;
align-self: center;
gap: 6px;
color: #333;
color: var(--app-text-main, #333);
font-size: 14px;
line-height: 32px;
white-space: nowrap;
@ -119,31 +119,31 @@
width: 48px;
height: 32px;
padding: 0 8px;
color: #333;
color: var(--app-text-main, #333);
font-size: 14px;
line-height: 30px;
text-align: center;
background-color: #fff;
border: 1px solid #d9d9d9;
background-color: var(--app-surface-color, #fff);
border: 1px solid var(--app-border-color, #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;
border-color: var(--app-primary-color, #1677ff);
}
.app-pagination-quick-jumper input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
border-color: var(--app-primary-color, #1677ff);
box-shadow: 0 0 0 2px rgba(var(--app-primary-rgb, 22, 119, 255), 0.12);
}
.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;
border-color: var(--app-border-color, #d9d9d9);
box-shadow: none;
}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Pagination, PaginationProps } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Pagination, type PaginationProps } from 'antd';
import { useTranslation } from 'react-i18next';
import { getMinPageSize, getPageSizeOptions, type PaginationVariant } from '@/utils/pagination';
import './index.css';

View File

@ -8,11 +8,11 @@
flex: 1;
min-height: 0;
min-width: 0;
background: #fff;
background: var(--app-surface-color, #fff);
}
.list-table-container .ant-table-wrapper {
background: #fff;
background: var(--app-surface-color, #fff);
}
/* 行选中样式 */
@ -32,7 +32,7 @@
}
.selection-count {
color: #9095a1;
color: var(--app-text-secondary, #9095a1);
font-size: 14px;
}
@ -56,26 +56,26 @@
}
.list-table-container .ant-table {
color: #333;
color: var(--app-text-main, #333);
}
.list-table-container .ant-table-thead > tr > th {
background: #fafafa !important;
color: #333;
background: var(--app-bg-surface-soft, #fafafa) !important;
color: var(--app-text-main, #333);
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid var(--app-border-color, #f0f0f0);
}
.list-table-container .ant-table-tbody > tr > td {
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid var(--app-border-color, #f0f0f0);
}
.list-table-container .ant-table-tbody > tr:not(.row-selected):not(.ant-table-row-selected):hover > td {
background: #fff !important;
background: var(--app-surface-color, #fff) !important;
}
.list-table-container .ant-table-tbody > tr:not(.row-selected):not(.ant-table-row-selected) > td.ant-table-cell-row-hover {
background: #fff !important;
background: var(--app-surface-color, #fff) !important;
}
.list-table-container .ant-table-tbody > tr.row-selected > td.ant-table-cell-row-hover {
@ -153,7 +153,7 @@
align-items: center;
justify-content: center;
min-height: 0;
background: #fff;
background: var(--app-surface-color, #fff);
pointer-events: none;
}

View File

@ -5,7 +5,7 @@
min-height: 0;
padding: 16px;
gap: 0;
background: #fafafa;
background: var(--app-bg-layout, #f5f6fa);
}
.page-container.page-container {
@ -20,10 +20,10 @@
flex-wrap: wrap;
gap: 16px;
padding: 16px 16px 0;
border: 1px solid #e6e6e6;
border: 1px solid var(--app-border-color, #e6e6e6);
border-bottom: none;
border-radius: 4px 4px 0 0;
background: #fff;
background: var(--app-surface-color, #fff);
}
.page-container__header--actions-only {
@ -41,7 +41,7 @@
font-weight: 600;
font-size: 18px;
line-height: 28px;
color: #333333;
color: var(--app-text-main, #333333);
position: relative;
}
@ -52,7 +52,7 @@
top: 6px;
width: 4px;
height: 16px;
background: #3c70f5;
background: var(--app-primary-color, #3c70f5);
}
.page-container__subtitle.ant-typography {
@ -61,7 +61,7 @@
padding-bottom: 16px;
font-size: 14px;
line-height: 24px;
color: #9095a1;
color: var(--app-text-secondary, #9095a1);
}
.page-container__header-extra {
@ -78,9 +78,9 @@
justify-content: flex-end;
align-items: center;
padding: 0 16px 8px;
border-left: 1px solid #e6e6e6;
border-right: 1px solid #e6e6e6;
background: #fff;
border-left: 1px solid var(--app-border-color, #e6e6e6);
border-right: 1px solid var(--app-border-color, #e6e6e6);
background: var(--app-surface-color, #fff);
}
.page-container__body {
@ -89,10 +89,10 @@
display: flex;
flex-direction: column;
padding: 0 16px 12px;
border: 1px solid #e6e6e6;
border: 1px solid var(--app-border-color, #e6e6e6);
border-top: none;
border-radius: 0 0 4px 4px;
background: #fff;
background: var(--app-surface-color, #fff);
}
@media (max-width: 768px) {

View File

@ -9,9 +9,9 @@
box-sizing: border-box;
overflow: hidden;
padding: 16px;
border: 1px solid #e6e6e6;
border: 1px solid var(--app-border-color, #e6e6e6);
border-radius: 4px;
background-color: #fff;
background-color: var(--app-surface-color, #fff);
background-image: url("../../../assets/home/mask.png");
background-position: right top;
background-size: contain;
@ -43,7 +43,7 @@
.section-card__title {
margin: 0;
padding-bottom: 8px;
color: #333;
color: var(--app-text-main, #333);
font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 18px;
font-weight: 600;
@ -63,12 +63,12 @@
width: 4px;
height: 16px;
border-radius: 1px;
background: #3c70f5;
background: var(--app-primary-color, #3c70f5);
}
.section-card__description {
padding: 0 0 16px 12px;
color: #9095a1;
color: var(--app-text-secondary, #9095a1);
font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
font-weight: 400;
@ -138,11 +138,11 @@
.section-card__tabs .ant-tabs-tab.ant-tabs-tab-active:active {
border: none !important;
border-radius: 0 !important;
background-color: #e9eef8 !important;
background-color: var(--app-bg-surface-soft, #e9eef8) !important;
}
.section-card__tabs .ant-tabs-tab-btn {
color: #333;
color: var(--app-text-main, #333);
font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
line-height: 22px;
@ -151,7 +151,7 @@
}
.section-card__tabs .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: #1677ff !important;
color: var(--app-primary-color, #1677ff) !important;
font-weight: 600;
}
@ -167,7 +167,7 @@
overflow: hidden;
padding: 8px;
border-radius: 4px;
background-color: #f9fafe;
background-color: var(--app-bg-surface-soft, #f9fafe);
}
.section-card--auto .section-card__content {

View File

@ -1,9 +1,8 @@
import { useState, useEffect } from 'react';
import { fetchDictItemsByTypeCode } from '../api/dict';
import { SysDictItem } from '../types';
import type { SysDictItem } from '../types';
const dictCache: Record<string, SysDictItem[]> = {};
const pendingRequests: Record<string, Promise<SysDictItem[]>[]> = {};
export function useDict(typeCode: string) {
const [items, setItems] = useState<SysDictItem[]>(dictCache[typeCode] || []);

View File

@ -25,6 +25,12 @@
--item-hover-bg: rgba(22, 119, 255, 0.08);
--text-color-secondary: #66758f;
--link-color: #1677ff;
--list-table-scroll-y: 100%;
--meeting-status-border: rgba(22, 119, 255, 0.2);
--meeting-status-bg: rgba(22, 119, 255, 0.08);
--meeting-status-color: #1677ff;
--meeting-source-color: #3b82f6;
--meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px);
}
@ -51,6 +57,12 @@
--item-hover-bg: rgba(22, 119, 255, 0.08);
--text-color-secondary: #5b6474;
--link-color: #1677ff;
--list-table-scroll-y: 100%;
--meeting-status-border: rgba(22, 119, 255, 0.2);
--meeting-status-bg: rgba(22, 119, 255, 0.08);
--meeting-status-color: #1677ff;
--meeting-source-color: #3b82f6;
--meeting-progress-color: #1677ff;
}
:root[data-theme="tech"] {
@ -76,6 +88,12 @@
--item-hover-bg: rgba(88, 151, 255, 0.18);
--text-color-secondary: rgba(190, 206, 229, 0.74);
--link-color: #60a5fa;
--list-table-scroll-y: 100%;
--meeting-status-border: rgba(96, 165, 250, 0.28);
--meeting-status-bg: rgba(96, 165, 250, 0.14);
--meeting-status-color: #60a5fa;
--meeting-source-color: #60a5fa;
--meeting-progress-color: #60a5fa;
}
html {
@ -1325,6 +1343,12 @@ body::after {
--app-text-muted: #9095a1;
--text-color-secondary: #9095a1;
--link-color: #1677ff;
--list-table-scroll-y: 100%;
--meeting-status-border: rgba(22, 119, 255, 0.18);
--meeting-status-bg: rgba(22, 119, 255, 0.08);
--meeting-status-color: #1677ff;
--meeting-source-color: #3b82f6;
--meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px);
}

View File

@ -329,16 +329,6 @@ export default function AppLayout() {
}
];
let profile: { isPlatformAdmin?: boolean } = {};
try {
const stored = sessionStorage.getItem("userProfile");
if (stored) {
profile = JSON.parse(stored) || {};
}
} catch {
profile = {};
}
items.push({ type: "divider", key: "divider" });
items.push({
key: "logout",

View File

@ -38,7 +38,7 @@ import PageContainer from "@/components/shared/PageContainer";
import DataListPanel from "@/components/shared/DataListPanel";
import SectionCard from "@/components/shared/SectionCard";
import { getStandardPagination } from "@/utils/pagination";
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
import type { SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
import "./index.less";
const { Text, Title } = Typography;

View File

@ -12,7 +12,6 @@ import {
Select,
Space,
Switch,
Table,
Tag,
Tooltip,
TreeSelect,
@ -267,8 +266,6 @@ export default function Users() {
);
}, [data, searchText]);
const activeFilterLabel = filterTenantId ? tenantMap[filterTenantId] || `Tenant ${filterTenantId}` : "全部租户";
const openCreate = () => {
setEditing(null);
setRoleSelectOpen(false);

View File

@ -73,7 +73,6 @@ const AiModels: React.FC = () => {
const [fetchLoading, setFetchLoading] = useState(false);
const [connectivityLoading, setConnectivityLoading] = useState(false);
const [remoteModels, setRemoteModels] = useState<string[]>([]);
const [speakerModels, setSpeakerModels] = useState<string[]>([]);
const [createConfig, setCreateConfig] = useState<MeetingCreateConfig>(DEFAULT_CREATE_CONFIG);
const modelNameAutoFilledRef = useRef(false);
const localProfileLoadedRef = useRef(false);
@ -151,7 +150,6 @@ const AiModels: React.FC = () => {
const openDrawer = (record?: AiModelVO) => {
setRemoteModels([]);
setSpeakerModels([]);
modelNameAutoFilledRef.current = false;
localProfileLoadedRef.current = false;
@ -181,7 +179,6 @@ const AiModels: React.FC = () => {
setRemoteModels([record.modelCode]);
}
if (speakerModel) {
setSpeakerModels([String(speakerModel)]);
}
} else {
setEditingId(null);
@ -255,9 +252,7 @@ const AiModels: React.FC = () => {
const applyLocalProfile = (profile: AiLocalProfileVO, baseUrl: string) => {
const nextRemoteModels = Array.isArray(profile.asrModels) ? profile.asrModels : [];
const nextSpeakerModels = Array.isArray(profile.speakerModels) ? profile.speakerModels : [];
setRemoteModels(nextRemoteModels);
setSpeakerModels(nextSpeakerModels);
const nextValues: Record<string, unknown> = {};
if (profile.activeAsrModel) {

View File

@ -1,13 +1,13 @@
import { App, Button, Input, Space, Tag, Typography, Upload } from "antd";
import { App, Button, Input, Space, Tag, Typography } from "antd";
import type { ColumnsType } from "antd/es/table";
import { CheckCircleOutlined, ClockCircleOutlined, KeyOutlined, LinkOutlined, ReloadOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons";
import { CheckCircleOutlined, ClockCircleOutlined, KeyOutlined, LinkOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useState } from "react";
import PageContainer from "@/components/shared/PageContainer";
import AppPagination from "@/components/shared/AppPagination";
import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import SectionCard from "@/components/shared/SectionCard";
import { importLicenses, listLicenses, type LicenseImportResultVO, type LicenseVO } from "@/api/business/license";
import { listLicenses, type LicenseVO } from "@/api/business/license";
import "./LicenseManagement.css";
const { Text } = Typography;
@ -38,9 +38,8 @@ function formatDateTime(value?: string) {
}
export default function LicenseManagement() {
const { message } = App.useApp();
App.useApp();
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const [records, setRecords] = useState<LicenseVO[]>([]);
const [searchValue, setSearchValue] = useState("");
const [page, setPage] = useState(1);
@ -93,17 +92,6 @@ export default function LicenseManagement() {
available: records.filter((item) => item.licenseStatus === 1).length,
}), [records]);
const handleImport = async (file: File) => {
setUploading(true);
try {
const result: LicenseImportResultVO = await importLicenses(file);
message.success(`导入完成:${result.totalCount} 条,替换 ${result.replacedCount}`);
await loadData();
} finally {
setUploading(false);
}
};
const columns: ColumnsType<LicenseVO> = [
{
title: "授权标识",
@ -190,11 +178,6 @@ export default function LicenseManagement() {
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
</Button>
{/*<Upload showUploadList={false} beforeUpload={(file) => { void handleImport(file as File); return Upload.LIST_IGNORE; }}>*/}
{/* <Button type="primary" icon={<UploadOutlined />} loading={uploading}>*/}
{/* 导入正式授权*/}
{/* </Button>*/}
{/*</Upload>*/}
</Space>
}
footer={

View File

@ -14,10 +14,8 @@ import {
LinkOutlined,
LoadingOutlined,
PauseOutlined,
RobotOutlined,
SyncOutlined,
UserOutlined,
PlusOutlined,
CheckCircleFilled,
FilePdfOutlined,
FileWordOutlined,
@ -372,13 +370,6 @@ type MeetingProgressPhase = 'queued' | 'asr' | 'chapter' | 'summary' | 'terminal
const meetingProgressTerminalRefreshCache = new Map<number, string>();
const meetingProgressPhaseRefreshCache = new Map<number, MeetingProgressPhase>();
const DETAIL_STAGE_STEP_ITEMS = [
{ code: 'INITIALIZING', label: '数据初始化', hint: '完成会议数据准备' },
{ code: 'TRANSCRIBING', label: '转译音频', hint: '完成语音转写' },
{ code: 'SUMMARIZING', label: '生成总结', hint: '完成 AI 内容处理' },
{ code: 'COMPLETED', label: '处理完成', hint: '已全部完成' },
] as const;
const resolveProgressPhase = (progress: MeetingProgress | null | undefined): MeetingProgressPhase => {
const unifiedStatusCode = progress?.unifiedStatus?.statusCode;
if (unifiedStatusCode?.startsWith('FAILED_') || unifiedStatusCode === 'COMPLETED') {
@ -489,7 +480,6 @@ const MeetingProgressDisplay: React.FC<{
const isError = percent < 0;
const unifiedStatusText = progress?.unifiedStatus?.statusText;
const unifiedStatusMessage = progress?.unifiedStatus?.message;
const primaryStatusText = unifiedStatusText || (isError ? '处理失败' : '处理中');
const formatEta = (seconds?: number) => {
if (!seconds || seconds <= 0) return '计算中';
@ -701,8 +691,6 @@ const UnifiedMeetingProgressDisplay: React.FC<{
const isFailedStage = unifiedStatusCode?.startsWith('FAILED_') || isError;
const isCompletedStage = unifiedStatusCode === 'COMPLETED' || percent === 100;
const progressText = isError ? '--' : `${Math.max(percent, 0)}%`;
const currentStageLabel = DETAIL_STAGE_STEP_DISPLAY_ITEMS[currentStageIndex]?.label || primaryStatusText;
const currentStageDisplay = isFailedStage ? `${currentStageLabel}失败` : currentStageLabel;
const helperText = unifiedStatusMessage || progress?.message || (isError ? '当前阶段执行失败,请稍后重试。' : '阶段状态已与安卓端统一。');
const renderStageTimeline = (options?: {
@ -1223,8 +1211,6 @@ const MeetingDetail: React.FC = () => {
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | 'transcript' | null>(null);
const [isEditingSummary, setIsEditingSummary] = useState(false);
const [summaryDraft, setSummaryDraft] = useState('');
const [expandKeywords, setExpandKeywords] = useState(false);
const [expandSummary, setExpandSummary] = useState(false);
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
const [workspaceTab, setWorkspaceTab] = useState<WorkspaceTab>('transcript');
const [addingHotwords, setAddingHotwords] = useState(false);
@ -1286,14 +1272,7 @@ const MeetingDetail: React.FC = () => {
() => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ''),
[meeting?.analysis, meeting?.summaryContent, meeting?.tags],
);
const hasAnalysis = !!(
analysis.overview ||
analysis.keywords.length ||
analysis.chapters.length ||
analysis.speakerSummaries.length ||
analysis.keyPoints.length ||
analysis.todos.length
);
const expandKeywords = false;
const visibleKeywords = expandKeywords ? analysis.keywords : analysis.keywords.slice(0, 9);
const meetingTags = useMemo(
() => (meeting?.tags?.split(',').map((item) => item.trim()).filter(Boolean) || []),
@ -1386,10 +1365,6 @@ const MeetingDetail: React.FC = () => {
const meetingId = meeting?.id ?? (id ? Number(id) : NaN);
return buildMeetingPreviewUrl(meetingShareBaseUrl, meetingId);
}, [meetingShareBaseUrl, meeting?.id, id]);
const meetingPreviewUrl = useMemo(() => {
const meetingId = meeting?.id ?? (id ? Number(id) : NaN);
return buildMeetingPreviewUrl(meetingShareBaseUrl, meetingId, previewAccessPassword);
}, [meetingShareBaseUrl, meeting?.id, id, previewAccessPassword]);
const summaryModelDisplayName = useMemo(() => {
const matchedModel = llmModels.find((item) => item.id === meeting?.summaryModelId);
if (matchedModel?.modelName) {
@ -1825,23 +1800,6 @@ const MeetingDetail: React.FC = () => {
audioRef.current.play();
}, []);
const handleKeywordClick = useCallback((keyword: string) => {
const firstMatch = transcripts.find((item) =>
item.content.toLowerCase().includes(keyword.toLowerCase())
);
if (firstMatch) {
setWorkspaceTab('transcript');
setLinkedTranscriptIds([]);
setLinkedChapterKey(null);
setHighlightKeyword(keyword);
seekTo(firstMatch.startTime);
message.info(`已跳转至关键词 "${keyword}" 所在位置`);
} else {
message.warning(`在转录原文中未找到关键词 "${keyword}"`);
}
}, [transcripts, seekTo, message]);
const handleTranscriptRowPlay = useCallback((timeMs: number) => {
setLinkedTranscriptIds([]);
setLinkedChapterKey(null);

View File

@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Alert, Button, Empty, Input, Result, Skeleton, Tabs, message } from "antd";
import { useParams, useSearchParams } from "react-router-dom";
import {
AudioOutlined,
CalendarOutlined,
CaretRightFilled,
ClockCircleOutlined,
@ -32,7 +31,6 @@ import {
import { buildMeetingAnalysis } from "./meetingAnalysis";
import "./MeetingPreview.css";
type AnalysisTab = "chapters" | "speakers" | "actions" | "todos";
type PreviewPageTab = "summary" | "catalog" | "transcript";
const TEXT = {
@ -203,7 +201,6 @@ export default function MeetingPreview() {
const [meetingChapters, setMeetingChapters] = useState<MeetingChapterVO[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [analysisTab, setAnalysisTab] = useState<AnalysisTab>("speakers");
const [pageTab, setPageTab] = useState<PreviewPageTab>("summary");
const [activeTranscriptId, setActiveTranscriptId] = useState<number | null>(null);
const [passwordRequired, setPasswordRequired] = useState(false);

View File

@ -1,33 +1,24 @@
import {
AppstoreOutlined,
AudioOutlined,
CalendarOutlined,
CheckOutlined,
CloudUploadOutlined,
DeleteOutlined,
EditOutlined,
FilterOutlined,
InfoCircleOutlined,
PauseCircleOutlined,
PlusOutlined,
SearchOutlined,
SettingOutlined,
SyncOutlined,
TeamOutlined,
UnorderedListOutlined,
UserOutlined,
} from "@ant-design/icons";
import {
App,
Avatar,
Button,
Card,
Dropdown,
Empty,
Form,
Input,
List,
Modal,
Popconfirm,
Radio,
Select,
@ -39,7 +30,6 @@ import {
} from "antd";
import dayjs from "dayjs";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { listUsers } from "../../api";
@ -55,19 +45,16 @@ import {
type MeetingVO,
type RealtimeMeetingSessionStatus,
retryScheduleMeeting,
updateMeetingParticipants,
} from "../../api/business/meeting";
import { MeetingCreateDrawer, type MeetingCreateType } from "../../components/business/MeetingCreateDrawer";
import AppPagination from "../../components/shared/AppPagination";
import DataListPanel from "../../components/shared/DataListPanel";
import SectionCard from "../../components/shared/SectionCard";
import { usePermission } from "../../hooks/usePermission";
import type { SysUser } from "../../types";
import PageContainer from "../../components/shared/PageContainer";
import "./Meetings.css";
const { Title, Text } = Typography;
const { Option } = Select;
const { Search } = Input;
const CURRENT_PLATFORM = "WEB" as const;
@ -132,11 +119,6 @@ const isUnifiedTerminalProgress = (progress?: MeetingProgress | null) =>
|| progress.unifiedStatus?.statusCode?.startsWith("FAILED_")
);
const shouldPollMeetingCard = (item: MeetingVO) =>
shouldTrackGenerationProgress(item)
|| item.realtimeSessionStatus === "ACTIVE"
|| isPausedRealtimeSessionStatus(item.realtimeSessionStatus);
const getUnifiedStatusCode = (progress: MeetingProgress | null | undefined) =>
progress?.unifiedStatus?.statusCode;
@ -422,9 +404,7 @@ const MeetingCardItem: React.FC<{
const Meetings: React.FC = () => {
const { message } = App.useApp();
const { t } = useTranslation();
const navigate = useNavigate();
const { can } = usePermission();
const [searchParams, setSearchParams] = useSearchParams();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<MeetingVO[]>([]);
@ -445,7 +425,7 @@ const Meetings: React.FC = () => {
realtimeEnabled: false,
offlineAudioMaxSizeMb: 1024,
});
const [userList, setUserList] = useState<SysUser[]>([]);
const [, setUserList] = useState<SysUser[]>([]);
const progressTerminalRefreshRef = useRef<Map<number, string>>(new Map());
const [retryingMeetingIds, setRetryingMeetingIds] = useState<Record<number, boolean>>({});

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Alert, Avatar, Badge, Button, Card, Empty, Space, Typography, App } from 'antd';
import { Alert, Avatar, Badge, Button, Card, Empty, Space, App } from 'antd';
import {
AudioOutlined,
AudioMutedOutlined,
@ -26,7 +26,6 @@ import {
type RealtimeMeetingSessionStatus,
type RealtimeSocketSessionVO,
} from "../../api/business/meeting";
const { Text } = Typography;
const SAMPLE_RATE = 16000;
const CHUNK_SIZE = 1280;
const CURRENT_PLATFORM = "WEB" as const;
@ -252,7 +251,6 @@ export function RealtimeAsrSession() {
const sessionStartedRef = useRef(false);
const elapsedOffsetRef = useRef(0);
const finalTranscriptCount = transcripts.length;
const totalTranscriptChars = useMemo(
() => transcripts.reduce((sum, item) => sum + item.text.length, 0) + streamingText.length,
[streamingText, transcripts],

View File

@ -14,7 +14,7 @@
.dashboard-monitor-page__content {
gap: 12px;
padding: 8px 8px 0;
padding: 8px;
}
.dashboard-monitor-page__stats {
@ -48,6 +48,14 @@
margin-bottom: 8px;
}
.dashboard-monitor-page__task-panel .data-list-panel__table-area .app-page__table-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dashboard-monitor-page__task-list,
.dashboard-monitor-page__task-list .ant-list,
.dashboard-monitor-page__task-list .ant-list-items,
@ -57,16 +65,34 @@
.dashboard-monitor-page__task-list {
flex: 1;
height: 100%;
height: auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 0 6px 0 0;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
overscroll-behavior: contain;
}
.dashboard-monitor-page__task-item {
padding: 18px 0;
.dashboard-monitor-page__task-list .ant-spin-nested-loading,
.dashboard-monitor-page__task-list .ant-spin-container,
.dashboard-monitor-page__task-list .ant-list {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.dashboard-monitor-page__task-list .ant-list-items {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 6px;
}
.dashboard-monitor-page__task-list .dashboard-monitor-page__task-item {
padding: 6px 0;
border-bottom: 1px solid #f0f2f5;
}
@ -90,7 +116,7 @@
}
.dashboard-monitor-page__task-tags {
margin-top: 8px;
margin-top: 4px;
display: flex;
flex-wrap: wrap;
gap: 8px;

View File

@ -1,4 +1,3 @@
import React from "react";
import "./RightVisual.less";
export default function RightVisual() {

View File

@ -1,16 +1,9 @@
import { Button, Modal, Slider, Typography, App } from "antd";
import { Button, Modal, Slider, App } from "antd";
import { ScissorOutlined } from "@ant-design/icons";
import { useEffect, useMemo, useRef, useState } from "react";
import "./AvatarCropDialog.css";
const { Text } = Typography;
const VIEWPORT_SIZE = 300;
const ALLOWED_TYPES = new Map([
["image/jpeg", "jpg"],
["image/jpg", "jpg"],
["image/png", "png"],
]);
export type CropModalState = {
open: boolean;

View File

@ -1,9 +1,12 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2021",
"lib": ["ES2021"],
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true
"noEmit": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
}