feat:调整会议详情
parent
37696b13a9
commit
2bc0b55e60
|
|
@ -13,7 +13,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-section-card {
|
.meeting-detail-section-card {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-section-card .section-card__header {
|
.meeting-detail-section-card .section-card__header {
|
||||||
|
|
@ -65,23 +68,34 @@
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-wrap {
|
.meeting-detail-page-v2 .meeting-detail-title-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-icon {
|
.meeting-detail-page-v2 .meeting-detail-back-button {
|
||||||
width: 42px;
|
width: 30px;
|
||||||
height: 42px;
|
height: 30px;
|
||||||
flex: 0 0 42px;
|
flex: 0 0 30px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-top: 0;
|
||||||
|
border: 1px solid #d8e3ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #3c70f5;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: 20px;
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-icon .anticon {
|
.meeting-detail-page-v2 .meeting-detail-back-button:hover {
|
||||||
|
border-color: #9cb8ff;
|
||||||
|
background: #f3f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .meeting-detail-back-button .anticon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -92,13 +106,14 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 7px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-row {
|
.meeting-detail-page-v2 .meeting-detail-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
min-height: 30px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +210,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-section-content {
|
.meeting-detail-section-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,11 +219,22 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .meeting-detail-workspace > .ant-row {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .meeting-detail-workspace > .ant-row > .ant-col {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .detail-side-column {
|
.meeting-detail-page-v2 .detail-side-column {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
@ -214,6 +242,17 @@
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .detail-left-column {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 100%;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .detail-left-column .summary-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .left-flow-card.ant-card {
|
.meeting-detail-page-v2 .left-flow-card.ant-card {
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
@ -268,14 +307,6 @@
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-player--floating {
|
|
||||||
z-index: 20;
|
|
||||||
border: 1px solid #e6e6e6;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-player {
|
.meeting-detail-page-v2 .transcript-player {
|
||||||
padding: 12px 14px !important;
|
padding: 12px 14px !important;
|
||||||
border: 1px solid #e6e6e6 !important;
|
border: 1px solid #e6e6e6 !important;
|
||||||
|
|
@ -348,7 +379,6 @@
|
||||||
color: #3c70f5 !important;
|
color: #3c70f5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-icon,
|
|
||||||
.meeting-detail-page-v2 .role-detail-icon,
|
.meeting-detail-page-v2 .role-detail-icon,
|
||||||
.meeting-detail-page-v2 .catalog-timeline-dot,
|
.meeting-detail-page-v2 .catalog-timeline-dot,
|
||||||
.meeting-detail-page-v2 .chapter-time::after,
|
.meeting-detail-page-v2 .chapter-time::after,
|
||||||
|
|
@ -357,7 +387,6 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-icon,
|
|
||||||
.meeting-detail-page-v2 .role-detail-icon {
|
.meeting-detail-page-v2 .role-detail-icon {
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
@ -381,12 +410,38 @@
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .summary-content-box,
|
|
||||||
.meeting-detail-page-v2 .transcript-keyword-bar,
|
.meeting-detail-page-v2 .transcript-keyword-bar,
|
||||||
.meeting-detail-page-v2 .empty-transcript-inline-note {
|
.meeting-detail-page-v2 .empty-transcript-inline-note {
|
||||||
background: #f9fafe !important;
|
background: #f9fafe !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .summary-panel.left-flow-card,
|
||||||
|
.meeting-detail-page-v2 .summary-panel.left-flow-card.ant-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: #e5e7eb !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .summary-panel .ant-card-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 24px !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .summary-content-box {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-text {
|
.meeting-detail-page-v2 .meeting-detail-title-text {
|
||||||
color: #333333 !important;
|
color: #333333 !important;
|
||||||
font-size: 18px !important;
|
font-size: 18px !important;
|
||||||
|
|
@ -577,14 +632,14 @@
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-wrap {
|
.meeting-detail-page-v2 .meeting-detail-title-wrap {
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-icon {
|
.meeting-detail-page-v2 .meeting-detail-back-button {
|
||||||
width: 38px;
|
width: 28px;
|
||||||
height: 38px;
|
height: 28px;
|
||||||
flex-basis: 38px;
|
flex-basis: 28px;
|
||||||
font-size: 18px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .meeting-detail-title-text {
|
.meeting-detail-page-v2 .meeting-detail-title-text {
|
||||||
|
|
@ -678,49 +733,77 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-scroll-shell {
|
.meeting-detail-page-v2 .transcript-scroll-shell {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
padding: 24px 20px 18px 18px !important;
|
padding: 24px 20px 18px 18px !important;
|
||||||
background: #ffffff !important;
|
background: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-player.transcript-player--floating {
|
.meeting-detail-page-v2 .transcript-player.transcript-player--embedded {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 14px 16px !important;
|
flex: 0 0 auto;
|
||||||
|
min-height: 72px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 13px 18px !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 18px !important;
|
border-top: 1px solid #e5e7eb !important;
|
||||||
background: rgba(255, 255, 255, 0.96) !important;
|
border-radius: 0 !important;
|
||||||
box-shadow: 0 16px 40px rgba(20, 35, 70, 0.16) !important;
|
background: #ffffff !important;
|
||||||
backdrop-filter: blur(12px) !important;
|
box-shadow: none !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-player .player-main-btn {
|
.meeting-detail-page-v2 .transcript-player .player-main-btn {
|
||||||
width: 46px !important;
|
width: 40px !important;
|
||||||
height: 46px !important;
|
height: 40px !important;
|
||||||
flex: 0 0 46px;
|
flex: 0 0 40px;
|
||||||
|
padding: 0 !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 50% !important;
|
border-radius: 50% !important;
|
||||||
background: linear-gradient(135deg, #3c70f5 0%, #6b8cff 100%) !important;
|
background: linear-gradient(135deg, #3c70f5 0%, #6b8cff 100%) !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
box-shadow: 0 10px 24px rgba(60, 112, 245, 0.32) !important;
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 0 8px 20px rgba(60, 112, 245, 0.28) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .transcript-player .player-main-btn .anticon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
transform: translate(1px, 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-detail-page-v2 .transcript-player .player-main-btn .anticon-pause {
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-player .player-ghost-btn {
|
.meeting-detail-page-v2 .transcript-player .player-ghost-btn {
|
||||||
height: 36px !important;
|
height: 36px !important;
|
||||||
|
align-self: center;
|
||||||
|
padding: 0 14px !important;
|
||||||
border: 1px solid #dce6ff !important;
|
border: 1px solid #dce6ff !important;
|
||||||
border-radius: 999px !important;
|
border-radius: 999px !important;
|
||||||
background: #f4f7ff !important;
|
background: #f4f7ff !important;
|
||||||
color: #3c70f5 !important;
|
color: #3c70f5 !important;
|
||||||
|
line-height: 1;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-player .player-progress-shell {
|
.meeting-detail-page-v2 .transcript-player .player-progress-shell {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-detail-page-v2 .transcript-player .player-range {
|
.meeting-detail-page-v2 .transcript-player .player-range {
|
||||||
height: 6px !important;
|
height: 6px !important;
|
||||||
|
margin: 0;
|
||||||
border-radius: 999px !important;
|
border-radius: 999px !important;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FastForwardOutlined,
|
FastForwardOutlined,
|
||||||
FileTextOutlined,
|
|
||||||
LeftOutlined,
|
LeftOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
|
|
@ -1242,10 +1241,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
|
|
||||||
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||||
const pendingTranscriptScrollIdRef = useRef<number | null>(null);
|
const pendingTranscriptScrollIdRef = useRef<number | null>(null);
|
||||||
const leftColumnRef = useRef<HTMLDivElement>(null);
|
|
||||||
const transcriptSectionRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [showFloatingTranscriptPlayer, setShowFloatingTranscriptPlayer] = useState(false);
|
|
||||||
const [floatingTranscriptPlayerLayout, setFloatingTranscriptPlayerLayout] = useState<{ left: number; width: number } | null>(null);
|
|
||||||
const [generationProgress, setGenerationProgress] = useState<MeetingProgress | null>(null);
|
const [generationProgress, setGenerationProgress] = useState<MeetingProgress | null>(null);
|
||||||
const autoOpenedCatalogAttemptRef = useRef<string | null>(null);
|
const autoOpenedCatalogAttemptRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -1559,60 +1554,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
meeting?.status,
|
meeting?.status,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!playbackAudioUrl) {
|
|
||||||
setShowFloatingTranscriptPlayer(false);
|
|
||||||
setFloatingTranscriptPlayerLayout(null);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateFloatingPlayerState = () => {
|
|
||||||
const target = transcriptSectionRef.current;
|
|
||||||
if (!target) {
|
|
||||||
setShowFloatingTranscriptPlayer(false);
|
|
||||||
setFloatingTranscriptPlayerLayout(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = target.getBoundingClientRect();
|
|
||||||
const rootRect = leftColumnRef.current?.getBoundingClientRect();
|
|
||||||
|
|
||||||
setFloatingTranscriptPlayerLayout({
|
|
||||||
left: rect.left,
|
|
||||||
width: rect.width,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rootRect) {
|
|
||||||
const isVisible = rect.bottom > rootRect.top + 80 && rect.top < rootRect.bottom - 40;
|
|
||||||
setShowFloatingTranscriptPlayer(isVisible);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isVisible = rect.bottom > 120 && rect.top < window.innerHeight - 80;
|
|
||||||
setShowFloatingTranscriptPlayer(isVisible);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateFloatingPlayerState();
|
|
||||||
|
|
||||||
const target = transcriptSectionRef.current;
|
|
||||||
const root = leftColumnRef.current;
|
|
||||||
const resizeObserver = target ? new ResizeObserver(() => updateFloatingPlayerState()) : null;
|
|
||||||
if (target && resizeObserver) {
|
|
||||||
resizeObserver.observe(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', updateFloatingPlayerState);
|
|
||||||
window.addEventListener('scroll', updateFloatingPlayerState, { passive: true });
|
|
||||||
root?.addEventListener('scroll', updateFloatingPlayerState, { passive: true });
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', updateFloatingPlayerState);
|
|
||||||
window.removeEventListener('scroll', updateFloatingPlayerState);
|
|
||||||
root?.removeEventListener('scroll', updateFloatingPlayerState);
|
|
||||||
resizeObserver?.disconnect();
|
|
||||||
};
|
|
||||||
}, [meeting?.status, playbackAudioUrl]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
fetchData(Number(id));
|
fetchData(Number(id));
|
||||||
|
|
@ -2371,9 +2312,9 @@ const MeetingDetail: React.FC = () => {
|
||||||
contentClassName="meeting-detail-section-content"
|
contentClassName="meeting-detail-section-content"
|
||||||
title={(
|
title={(
|
||||||
<div className="meeting-detail-title-wrap">
|
<div className="meeting-detail-title-wrap">
|
||||||
<div className="meeting-detail-title-icon">
|
<button type="button" className="meeting-detail-back-button" onClick={() => navigate('/meetings')} aria-label="返回会议列表">
|
||||||
<FileTextOutlined />
|
<LeftOutlined />
|
||||||
</div>
|
</button>
|
||||||
<div className="meeting-detail-title-copy">
|
<div className="meeting-detail-title-copy">
|
||||||
<div className="meeting-detail-title-row">
|
<div className="meeting-detail-title-row">
|
||||||
<span className="meeting-detail-title-text">{meeting.title}</span>
|
<span className="meeting-detail-title-text">{meeting.title}</span>
|
||||||
|
|
@ -2410,9 +2351,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
extra={(
|
extra={(
|
||||||
<Space size={10} wrap>
|
<Space size={10} wrap>
|
||||||
<Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}>
|
|
||||||
返回
|
|
||||||
</Button>
|
|
||||||
{canRetrySchedule && (
|
{canRetrySchedule && (
|
||||||
<Button icon={<SyncOutlined />} onClick={handleRetrySchedule} loading={actionLoading}>
|
<Button icon={<SyncOutlined />} onClick={handleRetrySchedule} loading={actionLoading}>
|
||||||
重新调度
|
重新调度
|
||||||
|
|
@ -2519,8 +2457,8 @@ const MeetingDetail: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Row gutter={24} style={{ height: '100%' }}>
|
<Row gutter={24} style={{ height: '100%', minHeight: 0 }}>
|
||||||
<Col xs={24} xl={13} style={{ height: '100%' }}>
|
<Col xs={24} xl={13} style={{ height: '100%', minHeight: 0 }}>
|
||||||
<div className="detail-side-column detail-left-column">
|
<div className="detail-side-column detail-left-column">
|
||||||
{(generationFailureNotice || emptyTranscriptFailureNotice || meeting.audioSaveStatus === 'FAILED') && (
|
{(generationFailureNotice || emptyTranscriptFailureNotice || meeting.audioSaveStatus === 'FAILED') && (
|
||||||
<Alert
|
<Alert
|
||||||
|
|
@ -2718,7 +2656,7 @@ const MeetingDetail: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={transcriptSectionRef} className="transcript-player-anchor">
|
<div className="transcript-player-anchor">
|
||||||
<Card className="left-flow-card" variant="borderless" title={<Space size={8}><AudioOutlined />原文</Space>}>
|
<Card className="left-flow-card" variant="borderless" title={<Space size={8}><AudioOutlined />原文</Space>}>
|
||||||
{playbackAudioUrl && (
|
{playbackAudioUrl && (
|
||||||
<audio ref={audioRef} style={{ display: 'none' }} preload="auto">
|
<audio ref={audioRef} style={{ display: 'none' }} preload="auto">
|
||||||
|
|
@ -2766,10 +2704,10 @@ const MeetingDetail: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} xl={11} style={{ height: '100%' }}>
|
<Col xs={24} xl={11} style={{ height: '100%', minHeight: 0 }}>
|
||||||
<div ref={leftColumnRef} className="detail-side-column detail-right-column">
|
<div className="detail-side-column detail-right-column">
|
||||||
{(
|
{(
|
||||||
<div ref={transcriptSectionRef} className="transcript-player-anchor">
|
<div className="transcript-player-anchor">
|
||||||
<Card
|
<Card
|
||||||
className="left-flow-card transcript-workspace-card"
|
className="left-flow-card transcript-workspace-card"
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
|
|
@ -2873,7 +2811,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
<List
|
<List
|
||||||
className="transcript-list"
|
className="transcript-list"
|
||||||
dataSource={transcripts}
|
dataSource={transcripts}
|
||||||
style={{ paddingBottom: playbackAudioUrl ? 156 : 0 }}
|
|
||||||
renderItem={(item, index) => {
|
renderItem={(item, index) => {
|
||||||
const nextStartTime = transcripts[index + 1]?.startTime || Infinity;
|
const nextStartTime = transcripts[index + 1]?.startTime || Infinity;
|
||||||
const isActive = (audioCurrentTime * 1000) >= item.startTime && (audioCurrentTime * 1000) < nextStartTime;
|
const isActive = (audioCurrentTime * 1000) >= item.startTime && (audioCurrentTime * 1000) < nextStartTime;
|
||||||
|
|
@ -2904,6 +2841,33 @@ const MeetingDetail: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{playbackAudioUrl && (
|
||||||
|
<div className="transcript-player transcript-player--embedded">
|
||||||
|
<button type="button" className="player-main-btn" onClick={toggleAudioPlayback} aria-label="toggle-audio">
|
||||||
|
{audioPlaying ? <PauseOutlined /> : <CaretRightFilled />}
|
||||||
|
</button>
|
||||||
|
<div className="player-progress-shell">
|
||||||
|
<div className="player-time-row">
|
||||||
|
<span>{formatPlayerTime(audioCurrentTime)}</span>
|
||||||
|
<span>{formatPlayerTime(audioDuration)}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="player-range"
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={audioDuration || 0}
|
||||||
|
step={0.1}
|
||||||
|
value={Math.min(audioCurrentTime, audioDuration || 0)}
|
||||||
|
style={{ '--player-progress': `${playerProgressPercent}%` } as React.CSSProperties}
|
||||||
|
onChange={handleAudioProgressChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="player-ghost-btn" onClick={cyclePlaybackRate}>
|
||||||
|
<FastForwardOutlined />
|
||||||
|
{audioPlaybackRate}x
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -2915,37 +2879,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
{playbackAudioUrl && showFloatingTranscriptPlayer && floatingTranscriptPlayerLayout && (
|
|
||||||
<div
|
|
||||||
className="transcript-player transcript-player--floating"
|
|
||||||
style={{ left: floatingTranscriptPlayerLayout.left, width: floatingTranscriptPlayerLayout.width }}
|
|
||||||
>
|
|
||||||
<button type="button" className="player-main-btn" onClick={toggleAudioPlayback} aria-label="toggle-audio">
|
|
||||||
{audioPlaying ? <PauseOutlined /> : <CaretRightFilled />}
|
|
||||||
</button>
|
|
||||||
<div className="player-progress-shell">
|
|
||||||
<div className="player-time-row">
|
|
||||||
<span>{formatPlayerTime(audioCurrentTime)}</span>
|
|
||||||
<span>{formatPlayerTime(audioDuration)}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="player-range"
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={audioDuration || 0}
|
|
||||||
step={0.1}
|
|
||||||
value={Math.min(audioCurrentTime, audioDuration || 0)}
|
|
||||||
style={{ '--player-progress': `${playerProgressPercent}%` } as React.CSSProperties}
|
|
||||||
onChange={handleAudioProgressChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button type="button" className="player-ghost-btn" onClick={cyclePlaybackRate}>
|
|
||||||
<FastForwardOutlined />
|
|
||||||
{audioPlaybackRate}x
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes highlight-pulse {
|
@keyframes highlight-pulse {
|
||||||
0% { box-shadow: 0 0 0 rgba(95, 81, 255, 0); transform: scale(1); }
|
0% { box-shadow: 0 0 0 rgba(95, 81, 255, 0); transform: scale(1); }
|
||||||
|
|
@ -3021,29 +2954,35 @@ const MeetingDetail: React.FC = () => {
|
||||||
.meeting-detail-title-wrap {
|
.meeting-detail-title-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.meeting-detail-title-icon {
|
.meeting-detail-back-button {
|
||||||
width: 42px;
|
width: 30px;
|
||||||
height: 42px;
|
height: 30px;
|
||||||
border-radius: 14px;
|
flex: 0 0 30px;
|
||||||
|
margin-top: 0;
|
||||||
|
border: 1px solid #d8e3ff;
|
||||||
|
border-radius: 6px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff;
|
color: #3c70f5;
|
||||||
font-size: 20px;
|
font-size: 14px;
|
||||||
background: linear-gradient(135deg, #5f51ff, #6c8cff);
|
line-height: 1;
|
||||||
box-shadow: 0 10px 24px rgba(95, 81, 255, 0.22);
|
background: #ffffff;
|
||||||
flex-shrink: 0;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.meeting-detail-title-copy {
|
.meeting-detail-title-copy {
|
||||||
display: grid;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.meeting-detail-title-row {
|
.meeting-detail-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
min-height: 30px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.meeting-detail-title-text {
|
.meeting-detail-title-text {
|
||||||
|
|
@ -3133,9 +3072,13 @@ const MeetingDetail: React.FC = () => {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.summary-panel .ant-card-body {
|
.summary-panel .ant-card-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
.summary-head {
|
.summary-head {
|
||||||
|
|
@ -3153,10 +3096,11 @@ const MeetingDetail: React.FC = () => {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
.summary-content-box {
|
.summary-content-box {
|
||||||
background: #f8faff;
|
min-height: 0;
|
||||||
border: 1px solid #eef1f9;
|
background: #ffffff;
|
||||||
border-radius: 16px;
|
border: 0;
|
||||||
padding: 24px;
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.summary-section-title {
|
.summary-section-title {
|
||||||
color: #9aa0bd;
|
color: #9aa0bd;
|
||||||
|
|
@ -4070,11 +4014,10 @@ const MeetingDetail: React.FC = () => {
|
||||||
box-shadow: 0 18px 44px rgba(72, 86, 128, 0.16);
|
box-shadow: 0 18px 44px rgba(72, 86, 128, 0.16);
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(24px);
|
||||||
}
|
}
|
||||||
.transcript-player--floating {
|
.transcript-player--embedded {
|
||||||
position: fixed;
|
flex: 0 0 auto;
|
||||||
bottom: 84px;
|
margin: 0;
|
||||||
z-index: 100;
|
max-width: none;
|
||||||
max-width: calc(100vw - 32px);
|
|
||||||
}
|
}
|
||||||
.player-main-btn,
|
.player-main-btn,
|
||||||
.player-ghost-btn {
|
.player-ghost-btn {
|
||||||
|
|
@ -4233,9 +4176,9 @@ const MeetingDetail: React.FC = () => {
|
||||||
.detail-left-column {
|
.detail-left-column {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.transcript-player--floating {
|
.transcript-player--embedded {
|
||||||
bottom: 72px;
|
margin: 0;
|
||||||
max-width: calc(100vw - 24px);
|
max-width: none;
|
||||||
}
|
}
|
||||||
.section-divider-text {
|
.section-divider-text {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
.profile-shell {
|
.profile-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,10 +28,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 18px;
|
gap: 14px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 22px 24px;
|
padding: 14px 18px;
|
||||||
border: 1px solid #dce6f8;
|
border: 1px solid #dce6f8;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background:
|
background:
|
||||||
|
|
@ -59,7 +59,8 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero__identity {
|
.profile-hero__identity,
|
||||||
|
.profile-hero__facts {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -67,15 +68,15 @@
|
||||||
.profile-hero__identity {
|
.profile-hero__identity {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 18px;
|
gap: 14px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-button {
|
.profile-avatar-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 96px;
|
width: 74px;
|
||||||
height: 96px;
|
height: 74px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -89,22 +90,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero__avatar.ant-avatar {
|
.profile-hero__avatar.ant-avatar {
|
||||||
width: 88px;
|
width: 68px;
|
||||||
height: 88px;
|
height: 68px;
|
||||||
border: 4px solid #ffffff;
|
border: 3px solid #ffffff;
|
||||||
background: #3c70f5;
|
background: #3c70f5;
|
||||||
box-shadow: 0 10px 28px rgba(60, 112, 245, 0.22);
|
box-shadow: 0 10px 28px rgba(60, 112, 245, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-button__badge {
|
.profile-avatar-button__badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 0;
|
||||||
bottom: 5px;
|
bottom: 2px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 28px;
|
width: 24px;
|
||||||
height: 28px;
|
height: 24px;
|
||||||
border: 2px solid #ffffff;
|
border: 2px solid #ffffff;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|
@ -127,10 +128,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero__title.ant-typography {
|
.profile-hero__title.ant-typography {
|
||||||
margin: 4px 0 2px;
|
margin: 2px 0 0;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
font-size: 26px;
|
font-size: 22px;
|
||||||
line-height: 34px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero__username.ant-typography {
|
.profile-hero__username.ant-typography {
|
||||||
|
|
@ -139,23 +140,61 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero__tags {
|
.profile-hero__tags {
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-workbench {
|
.profile-hero__facts {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(292px, 360px) minmax(0, 1fr);
|
|
||||||
gap: 14px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-side-stack {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: stretch;
|
||||||
gap: 14px;
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 280px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hero-fact {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 240px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border: 1px solid rgba(220, 230, 248, 0.9);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hero-fact > .anticon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #2463eb;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hero-fact div {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-hero-fact span,
|
||||||
|
.profile-hero-fact strong {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hero-fact span {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hero-fact strong {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-panel {
|
.profile-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid #e5e9f2;
|
border: 1px solid #e5e9f2;
|
||||||
|
|
@ -164,14 +203,7 @@
|
||||||
box-shadow: 0 8px 22px rgba(30, 41, 59, 0.04);
|
box-shadow: 0 8px 22px rgba(30, 41, 59, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-overview-panel,
|
.profile-editor-panel__head {
|
||||||
.profile-security-panel {
|
|
||||||
padding: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-panel__head,
|
|
||||||
.profile-editor-panel__head,
|
|
||||||
.profile-form-intro {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -180,108 +212,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-panel__title.ant-typography,
|
.profile-panel__title.ant-typography,
|
||||||
.profile-form-intro .ant-typography {
|
.profile-tab-summary .ant-typography {
|
||||||
margin: 2px 0 0;
|
margin: 2px 0 0;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-panel__icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #3c70f5;
|
|
||||||
background: #eef4ff;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-meta-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-meta-list__item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 13px;
|
|
||||||
border: 1px solid #edf1f7;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fafbfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-meta-list__icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #2463eb;
|
|
||||||
background: #eef4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-meta-list__content {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-meta-list__label {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-meta-list__value {
|
|
||||||
min-width: 0;
|
|
||||||
color: #1f2937;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 22px;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-security-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-security-list > div {
|
|
||||||
padding-left: 12px;
|
|
||||||
border-left: 3px solid #d8e4fb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-security-list strong,
|
|
||||||
.profile-security-list span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-security-list strong {
|
|
||||||
margin-bottom: 3px;
|
|
||||||
color: #1f2937;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-security-list span {
|
|
||||||
color: #64748b;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-security-panel__alert,
|
|
||||||
.profile-security-alert {
|
|
||||||
margin-top: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-editor-panel {
|
.profile-editor-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -289,13 +224,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-editor-panel__head {
|
.profile-editor-panel__head {
|
||||||
padding: 18px 20px 14px;
|
padding: 12px 18px 10px;
|
||||||
border-bottom: 1px solid #edf1f7;
|
border-bottom: 1px solid #edf1f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tabs .ant-tabs-nav {
|
.profile-tabs .ant-tabs-nav {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 20px;
|
padding: 0 18px;
|
||||||
border-bottom: 1px solid #edf1f7;
|
border-bottom: 1px solid #edf1f7;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +240,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tabs .ant-tabs-tab {
|
.profile-tabs .ant-tabs-tab {
|
||||||
padding: 14px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tabs .ant-tabs-tab + .ant-tabs-tab {
|
.profile-tabs .ant-tabs-tab + .ant-tabs-tab {
|
||||||
|
|
@ -313,29 +248,64 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tabs .ant-tabs-content-holder {
|
.profile-tabs .ant-tabs-content-holder {
|
||||||
padding: 20px;
|
padding: 14px 18px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-form {
|
.profile-form {
|
||||||
max-width: 980px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-form-intro {
|
.profile-tab-surface {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab-summary {
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 18px;
|
justify-content: space-between;
|
||||||
padding: 14px 16px;
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
border: 1px solid #edf1f7;
|
border: 1px solid #edf1f7;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fafbfe;
|
background: #fafbfe;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-form-intro .ant-typography-secondary {
|
.profile-tab-summary .ant-typography-secondary {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-inline-note {
|
||||||
|
flex: 0 1 360px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid #dbe7ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #41607d;
|
||||||
|
background: #f5f9ff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-basic-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(320px, 388px);
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-basic-main,
|
||||||
|
.profile-basic-media {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-form .ant-form-item {
|
.profile-form .ant-form-item {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-form .ant-input,
|
.profile-form .ant-input,
|
||||||
|
|
@ -343,15 +313,22 @@
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-readonly-input {
|
||||||
|
color: #334155;
|
||||||
|
background: #f8fafc !important;
|
||||||
|
cursor: default;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-avatar-control {
|
.profile-avatar-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 18px;
|
gap: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
border: 1px dashed #9fbcfb;
|
border: 1px dashed #9fbcfb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: linear-gradient(90deg, rgba(60, 112, 245, 0.07), rgba(25, 135, 84, 0.05));
|
background: linear-gradient(90deg, rgba(60, 112, 245, 0.07), rgba(25, 135, 84, 0.05));
|
||||||
|
|
@ -359,19 +336,25 @@
|
||||||
|
|
||||||
.profile-avatar-control__preview {
|
.profile-avatar-control__preview {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 132px;
|
width: 104px;
|
||||||
height: 132px;
|
height: 104px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #dbe6fb;
|
border: 1px solid #dbe6fb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-avatar-control__preview .ant-upload {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-avatar-control__preview .ant-image,
|
.profile-avatar-control__preview .ant-image,
|
||||||
.profile-avatar-control__image {
|
.profile-avatar-control__image {
|
||||||
display: block;
|
display: block;
|
||||||
width: 132px !important;
|
width: 104px !important;
|
||||||
height: 132px !important;
|
height: 104px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-control__image {
|
.profile-avatar-control__image {
|
||||||
|
|
@ -383,7 +366,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -391,16 +374,18 @@
|
||||||
color: #2463eb;
|
color: #2463eb;
|
||||||
background: #f8fbff;
|
background: #f8fbff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-control__empty .anticon {
|
.profile-avatar-control__empty .anticon {
|
||||||
font-size: 26px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-control__empty span {
|
.profile-avatar-control__empty span {
|
||||||
color: #475569;
|
color: #64748b;
|
||||||
font-weight: 600;
|
font-size: 12px;
|
||||||
line-height: 22px;
|
font-weight: 500;
|
||||||
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-control__content {
|
.profile-avatar-control__content {
|
||||||
|
|
@ -410,12 +395,12 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 14px;
|
gap: 10px;
|
||||||
padding: 4px 0;
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-control__title {
|
.profile-avatar-control__title {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -426,34 +411,68 @@
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 22px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-credential-panel {
|
.profile-credential-panel {
|
||||||
display: flex;
|
min-width: 0;
|
||||||
flex-direction: column;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-credential-card {
|
.profile-credential-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-credential-item {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
background: #fafbfe;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-credential-panel__descriptions .ant-descriptions-view {
|
.profile-credential-item--wide {
|
||||||
border-radius: 8px;
|
grid-column: span 2;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-credential-panel__descriptions .ant-descriptions-item-label {
|
.profile-credential-item > span,
|
||||||
width: 180px;
|
.profile-credential-item > strong {
|
||||||
color: #475569;
|
display: block;
|
||||||
background: #f8fafc;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-credential-item > span {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-credential-item > strong {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-copy-value.ant-typography {
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tab-actions {
|
.profile-tab-actions {
|
||||||
margin: 16px 0 0;
|
margin: 10px 0 0;
|
||||||
padding-top: 16px;
|
padding-top: 10px;
|
||||||
border-top: 1px solid #edf1f7;
|
border-top: 1px solid #edf1f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,21 +481,24 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-workbench {
|
.profile-hero__facts {
|
||||||
grid-template-columns: 1fr;
|
justify-content: flex-start;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.profile-shell {
|
.profile-shell {
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero {
|
.profile-hero {
|
||||||
padding: 18px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero__identity {
|
.profile-hero__identity,
|
||||||
|
.profile-hero__facts {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -485,26 +507,43 @@
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-workbench {
|
.profile-hero__facts {
|
||||||
grid-template-columns: 1fr;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-editor-panel__head,
|
.profile-editor-panel__head,
|
||||||
.profile-form-intro,
|
.profile-tab-summary,
|
||||||
.profile-avatar-control {
|
.profile-avatar-control {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-basic-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-inline-note {
|
||||||
|
width: 100%;
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-credential-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-credential-item--wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-avatar-control__preview {
|
.profile-avatar-control__preview {
|
||||||
width: 112px;
|
width: 104px;
|
||||||
height: 112px;
|
height: 104px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-control__preview .ant-image,
|
.profile-avatar-control__preview .ant-image,
|
||||||
.profile-avatar-control__image {
|
.profile-avatar-control__image {
|
||||||
width: 112px !important;
|
width: 104px !important;
|
||||||
height: 112px !important;
|
height: 104px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tabs .ant-tabs-nav,
|
.profile-tabs .ant-tabs-nav,
|
||||||
|
|
@ -527,9 +566,15 @@
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-overview-panel,
|
|
||||||
.profile-security-panel,
|
|
||||||
.profile-tabs .ant-tabs-content-holder {
|
.profile-tabs .ant-tabs-content-holder {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-credential-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-credential-item--wide {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import {
|
import {
|
||||||
Alert,
|
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
Descriptions,
|
|
||||||
Form,
|
Form,
|
||||||
Image,
|
Image,
|
||||||
Input,
|
Input,
|
||||||
|
|
@ -17,17 +15,12 @@ import {
|
||||||
message
|
message
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
ApartmentOutlined,
|
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CloudUploadOutlined,
|
CloudUploadOutlined,
|
||||||
IdcardOutlined,
|
|
||||||
KeyOutlined,
|
KeyOutlined,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
MailOutlined,
|
|
||||||
PhoneOutlined,
|
|
||||||
QuestionCircleOutlined,
|
QuestionCircleOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
SafetyCertificateOutlined,
|
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
SolutionOutlined,
|
SolutionOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
|
|
@ -196,32 +189,6 @@ export default function Profile() {
|
||||||
{user && !user.isPlatformAdmin && user.hasPlatformAdminPrivilege ? <Tag color="geekblue">可切换平台管理员</Tag> : null}
|
{user && !user.isPlatformAdmin && user.hasPlatformAdminPrivilege ? <Tag color="geekblue">可切换平台管理员</Tag> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
const profileMetaItems = [
|
|
||||||
{
|
|
||||||
key: "email",
|
|
||||||
icon: <MailOutlined />,
|
|
||||||
label: t("users.email"),
|
|
||||||
value: renderValue(user?.email)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "phone",
|
|
||||||
icon: <PhoneOutlined />,
|
|
||||||
label: t("users.phone"),
|
|
||||||
value: renderValue(user?.phone)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "organization",
|
|
||||||
icon: <ApartmentOutlined />,
|
|
||||||
label: t("users.org"),
|
|
||||||
value: organization
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "status",
|
|
||||||
icon: <CheckCircleOutlined />,
|
|
||||||
label: t("common.status"),
|
|
||||||
value: userStatus
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer title={null} className="profile-page">
|
<PageContainer title={null} className="profile-page">
|
||||||
|
|
@ -262,73 +229,23 @@ export default function Profile() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-hero__facts">
|
||||||
|
<div className="profile-hero-fact">
|
||||||
|
<CheckCircleOutlined />
|
||||||
|
<div>
|
||||||
|
<span>{t("common.status")}</span>
|
||||||
|
<strong>{userStatus}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="profile-workbench">
|
|
||||||
<aside className="profile-side-stack">
|
|
||||||
<section className="profile-panel profile-overview-panel">
|
|
||||||
<div className="profile-panel__head">
|
|
||||||
<div>
|
|
||||||
<div className="profile-panel__eyebrow">资料信息</div>
|
|
||||||
<Title level={5} className="profile-panel__title">资料概览</Title>
|
|
||||||
</div>
|
|
||||||
<IdcardOutlined className="profile-panel__icon" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-meta-list">
|
|
||||||
{profileMetaItems.map((item) => (
|
|
||||||
<div className="profile-meta-list__item" key={item.key}>
|
|
||||||
<div className="profile-meta-list__icon">{item.icon}</div>
|
|
||||||
<div className="profile-meta-list__content">
|
|
||||||
<div className="profile-meta-list__label">{item.label}</div>
|
|
||||||
<div className="profile-meta-list__value tabular-nums">{item.value}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="profile-panel profile-security-panel">
|
|
||||||
<div className="profile-panel__head">
|
|
||||||
<div>
|
|
||||||
<div className="profile-panel__eyebrow">安全状态</div>
|
|
||||||
<Title level={5} className="profile-panel__title">安全提示</Title>
|
|
||||||
</div>
|
|
||||||
<SafetyCertificateOutlined className="profile-panel__icon" />
|
|
||||||
</div>
|
|
||||||
<div className="profile-security-list">
|
|
||||||
<div>
|
|
||||||
<strong>头像资料</strong>
|
|
||||||
<span>上传裁剪后会先填入表单,保存资料后正式生效。</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>密码更新</strong>
|
|
||||||
<span>修改成功后会刷新当前会话里的个人资料缓存。</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Bot 凭证</strong>
|
|
||||||
<span>重新生成后,旧密钥会立即失效。</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{user?.pwdResetRequired ? (
|
|
||||||
<Alert
|
|
||||||
showIcon
|
|
||||||
type="warning"
|
|
||||||
message="当前账号仍需要完成密码重置"
|
|
||||||
description="请尽快在“安全设置”页签中更新密码。"
|
|
||||||
className="profile-security-panel__alert"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section className="profile-panel profile-editor-panel">
|
<section className="profile-panel profile-editor-panel">
|
||||||
<div className="profile-editor-panel__head">
|
<div className="profile-editor-panel__head">
|
||||||
<div>
|
<div>
|
||||||
<div className="profile-panel__eyebrow">账户维护</div>
|
<div className="profile-panel__eyebrow">账户维护</div>
|
||||||
<Title level={5} className="profile-panel__title">账户设置</Title>
|
<Title level={5} className="profile-panel__title">账户设置</Title>
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary">基础资料、安全密码与接口凭证统一维护</Text>
|
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="basic"
|
defaultActiveKey="basic"
|
||||||
|
|
@ -343,95 +260,104 @@ export default function Profile() {
|
||||||
),
|
),
|
||||||
children: (
|
children: (
|
||||||
<Form form={profileForm} layout="vertical" onFinish={handleUpdateProfile} className="profile-form">
|
<Form form={profileForm} layout="vertical" onFinish={handleUpdateProfile} className="profile-form">
|
||||||
<div className="profile-form-intro">
|
|
||||||
<div>
|
|
||||||
<Title level={5}>基础资料</Title>
|
|
||||||
<Text type="secondary">用于系统内展示和管理员识别,请保持联系方式准确。</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col xs={24} md={12}>
|
|
||||||
<Form.Item label={t("users.displayName")} name="displayName" rules={[{required: true}]}>
|
|
||||||
<Input/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} md={12}>
|
|
||||||
<Form.Item label={t("users.email")} name="email"
|
|
||||||
rules={[{required: true, message: "请输入邮箱地址"}, {
|
|
||||||
type: "email",
|
|
||||||
message: "请输入正确的邮箱格式"
|
|
||||||
}]}>
|
|
||||||
<Input/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col xs={24} md={12}>
|
|
||||||
<Form.Item label={t("users.phone")} name="phone">
|
|
||||||
<Input/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Form.Item name="avatarUrl" hidden>
|
<Form.Item name="avatarUrl" hidden>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Row gutter={16}>
|
<div className="profile-tab-surface">
|
||||||
<Col xs={24} md={12}>
|
<div className="profile-tab-summary">
|
||||||
<div className="profile-avatar-control">
|
<div>
|
||||||
<div className="profile-avatar-control__preview">
|
<Title level={5}>基础资料</Title>
|
||||||
{avatarUrl ? (
|
<Text type="secondary">用于系统内展示和管理员识别,请保持联系方式准确。</Text>
|
||||||
<Image
|
</div>
|
||||||
src={avatarUrl}
|
</div>
|
||||||
alt="头像预览"
|
|
||||||
className="profile-avatar-control__image"
|
<div className="profile-basic-layout">
|
||||||
preview={{ mask: "查看大图" }}
|
<div className="profile-basic-main">
|
||||||
/>
|
<Row gutter={16}>
|
||||||
) : (
|
<Col xs={24} lg={12}>
|
||||||
|
<Form.Item label={t("users.displayName")} name="displayName" rules={[{required: true}]}>
|
||||||
|
<Input/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Form.Item label={t("users.email")} name="email"
|
||||||
|
rules={[{required: true, message: "请输入邮箱地址"}, {
|
||||||
|
type: "email",
|
||||||
|
message: "请输入正确的邮箱格式"
|
||||||
|
}]}>
|
||||||
|
<Input/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Form.Item label={t("users.phone")} name="phone">
|
||||||
|
<Input/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Form.Item label={t("users.org")}>
|
||||||
|
<Tooltip title={organization === "-" ? undefined : organization}>
|
||||||
|
<Input value={organization} readOnly className="profile-readonly-input" />
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-basic-media">
|
||||||
|
<div className="profile-avatar-control">
|
||||||
|
<div className="profile-avatar-control__preview">
|
||||||
|
{avatarUrl ? (
|
||||||
|
<Image
|
||||||
|
src={avatarUrl}
|
||||||
|
alt="头像预览"
|
||||||
|
className="profile-avatar-control__image"
|
||||||
|
preview={{ mask: "查看大图" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Upload
|
||||||
|
accept={AVATAR_UPLOAD_ACCEPT}
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={handleAvatarUpload}
|
||||||
|
disabled={avatarUploading}
|
||||||
|
>
|
||||||
|
<button type="button" className="profile-avatar-control__empty" aria-label="选择头像图片">
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
<span>PNG / JPG</span>
|
||||||
|
</button>
|
||||||
|
</Upload>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="profile-avatar-control__content">
|
||||||
|
<div>
|
||||||
|
<div className="profile-avatar-control__title">头像图片</div>
|
||||||
|
<div className="profile-avatar-control__hint">
|
||||||
|
{avatarUrl ? "点击图片可放大预览。" : "支持 PNG/JPG 图片,上传后会先裁剪成统一尺寸。"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Upload
|
<Upload
|
||||||
accept={AVATAR_UPLOAD_ACCEPT}
|
accept={AVATAR_UPLOAD_ACCEPT}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
beforeUpload={handleAvatarUpload}
|
beforeUpload={handleAvatarUpload}
|
||||||
disabled={avatarUploading}
|
disabled={avatarUploading}
|
||||||
>
|
>
|
||||||
<button type="button" className="profile-avatar-control__empty">
|
<Button icon={<UploadOutlined/>} loading={avatarUploading}>
|
||||||
<CloudUploadOutlined />
|
{avatarUrl ? "更换头像" : t("profile.uploadAvatar")}
|
||||||
<span>上传头像</span>
|
</Button>
|
||||||
</button>
|
|
||||||
</Upload>
|
</Upload>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="profile-avatar-control__content">
|
|
||||||
<div>
|
|
||||||
<div className="profile-avatar-control__title">头像图片</div>
|
|
||||||
<div className="profile-avatar-control__hint">
|
|
||||||
{avatarUrl ? "当前头像已回显,点击图片可放大预览。" : "支持 PNG/JPG 图片,上传后会先裁剪成统一尺寸。"}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Upload
|
|
||||||
accept={AVATAR_UPLOAD_ACCEPT}
|
|
||||||
showUploadList={false}
|
|
||||||
beforeUpload={handleAvatarUpload}
|
|
||||||
disabled={avatarUploading}
|
|
||||||
>
|
|
||||||
<Button icon={<UploadOutlined/>} loading={avatarUploading}>
|
|
||||||
{avatarUrl ? "更换头像" : t("profile.uploadAvatar")}
|
|
||||||
</Button>
|
|
||||||
</Upload>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<div className="app-page__content-toolbar profile-tab-actions">
|
|
||||||
<div className="app-page__content-toolbar-actions">
|
|
||||||
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => profileForm.submit()}>
|
|
||||||
{t("profile.saveChanges")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="app-page__content-toolbar-filters" />
|
|
||||||
|
<div className="app-page__content-toolbar profile-tab-actions">
|
||||||
|
<div className="app-page__content-toolbar-actions">
|
||||||
|
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => profileForm.submit()}>
|
||||||
|
{t("profile.saveChanges")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="app-page__content-toolbar-filters" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|
@ -445,81 +371,86 @@ export default function Profile() {
|
||||||
),
|
),
|
||||||
children: (
|
children: (
|
||||||
<Form form={pwdForm} layout="vertical" onFinish={handleUpdatePassword} className="profile-form">
|
<Form form={pwdForm} layout="vertical" onFinish={handleUpdatePassword} className="profile-form">
|
||||||
<div className="profile-form-intro">
|
<div className="profile-tab-surface">
|
||||||
<div>
|
<div className="profile-tab-summary">
|
||||||
<Title level={5}>登录安全</Title>
|
<div>
|
||||||
<Text type="secondary">定期更新密码可以降低账号被误用的风险。</Text>
|
<Title level={5}>登录安全</Title>
|
||||||
|
<Text type="secondary">定期更新密码可以降低账号被误用的风险。</Text>
|
||||||
|
</div>
|
||||||
|
<div className="profile-inline-note">密码更新后立即生效,建议不要复用其他系统密码。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<Alert
|
|
||||||
showIcon
|
|
||||||
type="info"
|
|
||||||
message="密码更新后立即生效"
|
|
||||||
description="建议使用与其他系统不同的新密码,避免复用。"
|
|
||||||
className="profile-security-alert"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Item label={t("profile.currentPassword")} name="oldPassword" rules={[{ required: true }]}>
|
<Row gutter={16}>
|
||||||
<Input.Password />
|
<Col xs={24} lg={8}>
|
||||||
</Form.Item>
|
<Form.Item label={t("profile.currentPassword")} name="oldPassword" rules={[{ required: true }]}>
|
||||||
<Form.Item
|
<Input.Password />
|
||||||
label={
|
</Form.Item>
|
||||||
<Space size={4}>
|
</Col>
|
||||||
<span>{t("profile.newPassword")}</span>
|
<Col xs={24} lg={8}>
|
||||||
<Tooltip
|
<Form.Item
|
||||||
title={
|
label={
|
||||||
policyHints.length > 0 ? (
|
<Space size={4}>
|
||||||
<ul style={{paddingLeft: 18, margin: 0, fontSize: 13, lineHeight: 1.6}}>
|
<span>{t("profile.newPassword")}</span>
|
||||||
{policyHints.map((hint) => (
|
<Tooltip
|
||||||
<li key={hint}>{hint}</li>
|
title={
|
||||||
))}
|
policyHints.length > 0 ? (
|
||||||
</ul>
|
<ul style={{paddingLeft: 18, margin: 0, fontSize: 13, lineHeight: 1.6}}>
|
||||||
) : (
|
{policyHints.map((hint) => (
|
||||||
t("profile.passwordRules")
|
<li key={hint}>{hint}</li>
|
||||||
)
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
t("profile.passwordRules")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
overlayStyle={{maxWidth: 300}}
|
||||||
|
>
|
||||||
|
<QuestionCircleOutlined style={{color: "rgba(0,0,0,0.45)", cursor: "help"}}/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
overlayStyle={{maxWidth: 300}}
|
name="newPassword"
|
||||||
|
validateFirst
|
||||||
|
rules={[
|
||||||
|
{required: true},
|
||||||
|
{
|
||||||
|
validator: buildPasswordPolicyValidator(policy, () => user?.username)
|
||||||
|
}
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<QuestionCircleOutlined style={{color: "rgba(0,0,0,0.45)", cursor: "help"}}/>
|
<Input.Password />
|
||||||
</Tooltip>
|
</Form.Item>
|
||||||
</Space>
|
</Col>
|
||||||
}
|
<Col xs={24} lg={8}>
|
||||||
name="newPassword"
|
<Form.Item
|
||||||
validateFirst
|
label={t("profile.confirmNewPassword")}
|
||||||
rules={[
|
name="confirmPassword"
|
||||||
{required: true},
|
dependencies={["newPassword"]}
|
||||||
{
|
rules={[
|
||||||
validator: buildPasswordPolicyValidator(policy, () => user?.username)
|
{ required: true },
|
||||||
}
|
({ getFieldValue }) => ({
|
||||||
]}
|
validator(_, value) {
|
||||||
>
|
if (!value || getFieldValue("newPassword") === value) {
|
||||||
<Input.Password />
|
return Promise.resolve();
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
return Promise.reject(new Error(t("profile.passwordsDoNotMatch")));
|
||||||
label={t("profile.confirmNewPassword")}
|
}
|
||||||
name="confirmPassword"
|
})
|
||||||
dependencies={["newPassword"]}
|
]}
|
||||||
rules={[
|
>
|
||||||
{ required: true },
|
<Input.Password />
|
||||||
({ getFieldValue }) => ({
|
</Form.Item>
|
||||||
validator(_, value) {
|
</Col>
|
||||||
if (!value || getFieldValue("newPassword") === value) {
|
</Row>
|
||||||
return Promise.resolve();
|
|
||||||
}
|
<div className="app-page__content-toolbar profile-tab-actions">
|
||||||
return Promise.reject(new Error(t("profile.passwordsDoNotMatch")));
|
<div className="app-page__content-toolbar-actions">
|
||||||
}
|
<Button type="primary" danger loading={saving} onClick={() => pwdForm.submit()}>
|
||||||
})
|
{t("profile.updatePassword")}
|
||||||
]}
|
</Button>
|
||||||
>
|
</div>
|
||||||
<Input.Password />
|
<div className="app-page__content-toolbar-filters" />
|
||||||
</Form.Item>
|
|
||||||
<div className="app-page__content-toolbar profile-tab-actions">
|
|
||||||
<div className="app-page__content-toolbar-actions">
|
|
||||||
<Button type="primary" danger loading={saving} onClick={() => pwdForm.submit()}>
|
|
||||||
{t("profile.updatePassword")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="app-page__content-toolbar-filters" />
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|
@ -533,80 +464,63 @@ export default function Profile() {
|
||||||
),
|
),
|
||||||
children: (
|
children: (
|
||||||
<div className="profile-credential-panel">
|
<div className="profile-credential-panel">
|
||||||
<div className="profile-form-intro">
|
<div className="profile-tab-surface">
|
||||||
<div>
|
<div className="profile-tab-summary">
|
||||||
<Title level={5}>Bot 接入凭证</Title>
|
<div>
|
||||||
<Text type="secondary">为自动化工具或 Bot 调用保留独立凭证,便于轮换和审计。</Text>
|
<Title level={5}>Bot 接入凭证</Title>
|
||||||
|
<Text type="secondary">为自动化工具或 Bot 调用保留独立凭证,便于轮换和审计。</Text>
|
||||||
|
</div>
|
||||||
|
<div className="profile-inline-note">重新生成后,旧密钥会立即失效。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<Alert
|
|
||||||
type="info"
|
|
||||||
showIcon
|
|
||||||
message={t("profile.botCredentialHint")}
|
|
||||||
description={t("profile.botCredentialHintDesc")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="profile-credential-card" aria-busy={credentialLoading}>
|
<div className="profile-credential-grid" aria-busy={credentialLoading}>
|
||||||
<Descriptions
|
<div className="profile-credential-item">
|
||||||
bordered
|
<span>{t("profile.botBindStatus")}</span>
|
||||||
size="middle"
|
<strong>{credential?.bound ? <Tag color="success">{t("profile.botBound")}</Tag> : <Tag>{t("profile.botUnbound")}</Tag>}</strong>
|
||||||
column={1}
|
</div>
|
||||||
className="profile-credential-panel__descriptions"
|
<div className="profile-credential-item profile-credential-item--wide">
|
||||||
items={[
|
<span>X-Bot-Id</span>
|
||||||
{
|
{credential?.botId ? (
|
||||||
key: "bind-status",
|
<Paragraph copyable={{ text: credential.botId }} className="profile-copy-value">
|
||||||
label: t("profile.botBindStatus"),
|
{credential.botId}
|
||||||
children: credential?.bound ? <Tag color="success">{t("profile.botBound")}</Tag> :
|
</Paragraph>
|
||||||
<Tag>{t("profile.botUnbound")}</Tag>
|
) : (
|
||||||
},
|
<strong>-</strong>
|
||||||
{
|
)}
|
||||||
key: "bot-id",
|
</div>
|
||||||
label: "X-Bot-Id",
|
<div className="profile-credential-item profile-credential-item--wide">
|
||||||
children: credential?.botId ? (
|
<span>X-Bot-Secret</span>
|
||||||
<Paragraph copyable={{ text: credential.botId }} style={{ marginBottom: 0 }}>
|
{credential?.botSecret ? (
|
||||||
{credential.botId}
|
<Paragraph copyable={{ text: credential.botSecret }} className="profile-copy-value">
|
||||||
</Paragraph>
|
{credential.botSecret}
|
||||||
) : (
|
</Paragraph>
|
||||||
"-"
|
) : (
|
||||||
)
|
<strong>{t("profile.botSecretHidden")}</strong>
|
||||||
},
|
)}
|
||||||
{
|
</div>
|
||||||
key: "bot-secret",
|
<div className="profile-credential-item">
|
||||||
label: "X-Bot-Secret",
|
<span>{t("profile.botLastAccessTime")}</span>
|
||||||
children: credential?.botSecret ? (
|
<strong className="tabular-nums">{renderValue(credential?.lastAccessTime)}</strong>
|
||||||
<Paragraph copyable={{ text: credential.botSecret }} style={{ marginBottom: 0 }}>
|
</div>
|
||||||
{credential.botSecret}
|
<div className="profile-credential-item">
|
||||||
</Paragraph>
|
<span>{t("profile.botLastAccessIp")}</span>
|
||||||
) : (
|
<strong className="tabular-nums">{renderValue(credential?.lastAccessIp)}</strong>
|
||||||
t("profile.botSecretHidden")
|
</div>
|
||||||
)
|
</div>
|
||||||
},
|
|
||||||
{
|
<div className="app-page__content-toolbar profile-tab-actions">
|
||||||
key: "last-access-time",
|
<div className="app-page__content-toolbar-actions">
|
||||||
label: t("profile.botLastAccessTime"),
|
<Button
|
||||||
children: <span className="tabular-nums">{renderValue(credential?.lastAccessTime)}</span>
|
type="primary"
|
||||||
},
|
icon={credential?.bound ? <ReloadOutlined/> : <KeyOutlined/>}
|
||||||
{
|
loading={credentialSaving}
|
||||||
key: "last-access-ip",
|
onClick={handleGenerateCredential}
|
||||||
label: t("profile.botLastAccessIp"),
|
>
|
||||||
children: <span className="tabular-nums">{renderValue(credential?.lastAccessIp)}</span>
|
{credential?.bound ? t("profile.regenerateBotCredential") : t("profile.generateBotCredential")}
|
||||||
}
|
</Button>
|
||||||
]}
|
</div>
|
||||||
/>
|
<div className="app-page__content-toolbar-filters" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="app-page__content-toolbar profile-tab-actions">
|
|
||||||
<div className="app-page__content-toolbar-actions">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={credential?.bound ? <ReloadOutlined/> : <KeyOutlined/>}
|
|
||||||
loading={credentialSaving}
|
|
||||||
onClick={handleGenerateCredential}
|
|
||||||
>
|
|
||||||
{credential?.bound ? t("profile.regenerateBotCredential") : t("profile.generateBotCredential")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="app-page__content-toolbar-filters" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -614,7 +528,6 @@ export default function Profile() {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue