Merge remote-tracking branch '个人/dev_na' into dev_na
commit
76410071e7
|
|
@ -13,7 +13,10 @@
|
|||
}
|
||||
|
||||
.meeting-detail-section-card {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.meeting-detail-section-card .section-card__header {
|
||||
|
|
@ -65,23 +68,34 @@
|
|||
.meeting-detail-page-v2 .meeting-detail-title-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .meeting-detail-title-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex: 0 0 42px;
|
||||
.meeting-detail-page-v2 .meeting-detail-back-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex: 0 0 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 0;
|
||||
border: 1px solid #d8e3ff;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #3c70f5;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -92,13 +106,14 @@
|
|||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .meeting-detail-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 30px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
@ -195,6 +210,8 @@
|
|||
}
|
||||
|
||||
.meeting-detail-section-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
@ -202,11 +219,22 @@
|
|||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
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 {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
|
@ -214,6 +242,17 @@
|
|||
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 {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
|
|
@ -268,14 +307,6 @@
|
|||
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 {
|
||||
padding: 12px 14px !important;
|
||||
border: 1px solid #e6e6e6 !important;
|
||||
|
|
@ -348,7 +379,6 @@
|
|||
color: #3c70f5 !important;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .meeting-detail-title-icon,
|
||||
.meeting-detail-page-v2 .role-detail-icon,
|
||||
.meeting-detail-page-v2 .catalog-timeline-dot,
|
||||
.meeting-detail-page-v2 .chapter-time::after,
|
||||
|
|
@ -357,7 +387,6 @@
|
|||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .meeting-detail-title-icon,
|
||||
.meeting-detail-page-v2 .role-detail-icon {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
|
@ -381,12 +410,38 @@
|
|||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .summary-content-box,
|
||||
.meeting-detail-page-v2 .transcript-keyword-bar,
|
||||
.meeting-detail-page-v2 .empty-transcript-inline-note {
|
||||
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 {
|
||||
color: #333333 !important;
|
||||
font-size: 18px !important;
|
||||
|
|
@ -577,14 +632,14 @@
|
|||
|
||||
@media (max-width: 768px) {
|
||||
.meeting-detail-page-v2 .meeting-detail-title-wrap {
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .meeting-detail-title-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
flex-basis: 38px;
|
||||
font-size: 18px;
|
||||
.meeting-detail-page-v2 .meeting-detail-back-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-basis: 28px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .meeting-detail-title-text {
|
||||
|
|
@ -678,49 +733,77 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .transcript-player.transcript-player--floating {
|
||||
.meeting-detail-page-v2 .transcript-player.transcript-player--embedded {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px !important;
|
||||
flex: 0 0 auto;
|
||||
min-height: 72px;
|
||||
margin: 0;
|
||||
padding: 13px 18px !important;
|
||||
border: 0 !important;
|
||||
border-radius: 18px !important;
|
||||
background: rgba(255, 255, 255, 0.96) !important;
|
||||
box-shadow: 0 16px 40px rgba(20, 35, 70, 0.16) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
border-top: 1px solid #e5e7eb !important;
|
||||
border-radius: 0 !important;
|
||||
background: #ffffff !important;
|
||||
box-shadow: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .transcript-player .player-main-btn {
|
||||
width: 46px !important;
|
||||
height: 46px !important;
|
||||
flex: 0 0 46px;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
flex: 0 0 40px;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
background: linear-gradient(135deg, #3c70f5 0%, #6b8cff 100%) !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 10px 24px rgba(60, 112, 245, 0.32) !important;
|
||||
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 {
|
||||
height: 36px !important;
|
||||
align-self: center;
|
||||
padding: 0 14px !important;
|
||||
border: 1px solid #dce6ff !important;
|
||||
border-radius: 999px !important;
|
||||
background: #f4f7ff !important;
|
||||
color: #3c70f5 !important;
|
||||
line-height: 1;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .transcript-player .player-progress-shell {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .transcript-player .player-range {
|
||||
height: 6px !important;
|
||||
margin: 0;
|
||||
border-radius: 999px !important;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
FastForwardOutlined,
|
||||
FileTextOutlined,
|
||||
LeftOutlined,
|
||||
LinkOutlined,
|
||||
LoadingOutlined,
|
||||
|
|
@ -1242,10 +1241,6 @@ const MeetingDetail: React.FC = () => {
|
|||
|
||||
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | 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 autoOpenedCatalogAttemptRef = useRef<string | null>(null);
|
||||
|
||||
|
|
@ -1559,60 +1554,6 @@ const MeetingDetail: React.FC = () => {
|
|||
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(() => {
|
||||
if (!id) return;
|
||||
fetchData(Number(id));
|
||||
|
|
@ -2371,9 +2312,9 @@ const MeetingDetail: React.FC = () => {
|
|||
contentClassName="meeting-detail-section-content"
|
||||
title={(
|
||||
<div className="meeting-detail-title-wrap">
|
||||
<div className="meeting-detail-title-icon">
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
<button type="button" className="meeting-detail-back-button" onClick={() => navigate('/meetings')} aria-label="返回会议列表">
|
||||
<LeftOutlined />
|
||||
</button>
|
||||
<div className="meeting-detail-title-copy">
|
||||
<div className="meeting-detail-title-row">
|
||||
<span className="meeting-detail-title-text">{meeting.title}</span>
|
||||
|
|
@ -2410,9 +2351,6 @@ const MeetingDetail: React.FC = () => {
|
|||
)}
|
||||
extra={(
|
||||
<Space size={10} wrap>
|
||||
<Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}>
|
||||
返回
|
||||
</Button>
|
||||
{canRetrySchedule && (
|
||||
<Button icon={<SyncOutlined />} onClick={handleRetrySchedule} loading={actionLoading}>
|
||||
重新调度
|
||||
|
|
@ -2519,8 +2457,8 @@ const MeetingDetail: React.FC = () => {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<Row gutter={24} style={{ height: '100%' }}>
|
||||
<Col xs={24} xl={13} style={{ height: '100%' }}>
|
||||
<Row gutter={24} style={{ height: '100%', minHeight: 0 }}>
|
||||
<Col xs={24} xl={13} style={{ height: '100%', minHeight: 0 }}>
|
||||
<div className="detail-side-column detail-left-column">
|
||||
{(generationFailureNotice || emptyTranscriptFailureNotice || meeting.audioSaveStatus === 'FAILED') && (
|
||||
<Alert
|
||||
|
|
@ -2718,7 +2656,7 @@ const MeetingDetail: React.FC = () => {
|
|||
</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>}>
|
||||
{playbackAudioUrl && (
|
||||
<audio ref={audioRef} style={{ display: 'none' }} preload="auto">
|
||||
|
|
@ -2766,10 +2704,10 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} xl={11} style={{ height: '100%' }}>
|
||||
<div ref={leftColumnRef} className="detail-side-column detail-right-column">
|
||||
<Col xs={24} xl={11} style={{ height: '100%', minHeight: 0 }}>
|
||||
<div className="detail-side-column detail-right-column">
|
||||
{(
|
||||
<div ref={transcriptSectionRef} className="transcript-player-anchor">
|
||||
<div className="transcript-player-anchor">
|
||||
<Card
|
||||
className="left-flow-card transcript-workspace-card"
|
||||
variant="borderless"
|
||||
|
|
@ -2873,7 +2811,6 @@ const MeetingDetail: React.FC = () => {
|
|||
<List
|
||||
className="transcript-list"
|
||||
dataSource={transcripts}
|
||||
style={{ paddingBottom: playbackAudioUrl ? 156 : 0 }}
|
||||
renderItem={(item, index) => {
|
||||
const nextStartTime = transcripts[index + 1]?.startTime || Infinity;
|
||||
const isActive = (audioCurrentTime * 1000) >= item.startTime && (audioCurrentTime * 1000) < nextStartTime;
|
||||
|
|
@ -2904,6 +2841,33 @@ const MeetingDetail: React.FC = () => {
|
|||
</>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -2915,37 +2879,6 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
</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>{`
|
||||
@keyframes highlight-pulse {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
.meeting-detail-title-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
.meeting-detail-back-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex: 0 0 30px;
|
||||
margin-top: 0;
|
||||
border: 1px solid #d8e3ff;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
background: linear-gradient(135deg, #5f51ff, #6c8cff);
|
||||
box-shadow: 0 10px 24px rgba(95, 81, 255, 0.22);
|
||||
flex-shrink: 0;
|
||||
color: #3c70f5;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.meeting-detail-title-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.meeting-detail-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.meeting-detail-title-text {
|
||||
|
|
@ -3133,9 +3072,13 @@ const MeetingDetail: React.FC = () => {
|
|||
min-width: 0;
|
||||
}
|
||||
.summary-panel .ant-card-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 24px;
|
||||
}
|
||||
.summary-head {
|
||||
|
|
@ -3153,10 +3096,11 @@ const MeetingDetail: React.FC = () => {
|
|||
font-weight: 800;
|
||||
}
|
||||
.summary-content-box {
|
||||
background: #f8faff;
|
||||
border: 1px solid #eef1f9;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
min-height: 0;
|
||||
background: #ffffff;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.summary-section-title {
|
||||
color: #9aa0bd;
|
||||
|
|
@ -4070,11 +4014,10 @@ const MeetingDetail: React.FC = () => {
|
|||
box-shadow: 0 18px 44px rgba(72, 86, 128, 0.16);
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
.transcript-player--floating {
|
||||
position: fixed;
|
||||
bottom: 84px;
|
||||
z-index: 100;
|
||||
max-width: calc(100vw - 32px);
|
||||
.transcript-player--embedded {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
}
|
||||
.player-main-btn,
|
||||
.player-ghost-btn {
|
||||
|
|
@ -4233,9 +4176,9 @@ const MeetingDetail: React.FC = () => {
|
|||
.detail-left-column {
|
||||
overflow: visible;
|
||||
}
|
||||
.transcript-player--floating {
|
||||
bottom: 72px;
|
||||
max-width: calc(100vw - 24px);
|
||||
.transcript-player--embedded {
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
}
|
||||
.section-divider-text {
|
||||
white-space: normal;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
.profile-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -28,10 +28,10 @@
|
|||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 22px 24px;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid #dce6f8;
|
||||
border-radius: 8px;
|
||||
background:
|
||||
|
|
@ -59,7 +59,8 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.profile-hero__identity {
|
||||
.profile-hero__identity,
|
||||
.profile-hero__facts {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
@ -67,15 +68,15 @@
|
|||
.profile-hero__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-avatar-button {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
width: 74px;
|
||||
height: 74px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
|
@ -89,22 +90,22 @@
|
|||
}
|
||||
|
||||
.profile-hero__avatar.ant-avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border: 4px solid #ffffff;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border: 3px solid #ffffff;
|
||||
background: #3c70f5;
|
||||
box-shadow: 0 10px 28px rgba(60, 112, 245, 0.22);
|
||||
}
|
||||
|
||||
.profile-avatar-button__badge {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 5px;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
|
|
@ -127,10 +128,10 @@
|
|||
}
|
||||
|
||||
.profile-hero__title.ant-typography {
|
||||
margin: 4px 0 2px;
|
||||
margin: 2px 0 0;
|
||||
color: #1f2937;
|
||||
font-size: 26px;
|
||||
line-height: 34px;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.profile-hero__username.ant-typography {
|
||||
|
|
@ -139,23 +140,61 @@
|
|||
}
|
||||
|
||||
.profile-hero__tags {
|
||||
margin-top: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.profile-workbench {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(292px, 360px) minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-side-stack {
|
||||
.profile-hero__facts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
min-width: 0;
|
||||
border: 1px solid #e5e9f2;
|
||||
|
|
@ -164,14 +203,7 @@
|
|||
box-shadow: 0 8px 22px rgba(30, 41, 59, 0.04);
|
||||
}
|
||||
|
||||
.profile-overview-panel,
|
||||
.profile-security-panel {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.profile-panel__head,
|
||||
.profile-editor-panel__head,
|
||||
.profile-form-intro {
|
||||
.profile-editor-panel__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
|
@ -180,108 +212,11 @@
|
|||
}
|
||||
|
||||
.profile-panel__title.ant-typography,
|
||||
.profile-form-intro .ant-typography {
|
||||
.profile-tab-summary .ant-typography {
|
||||
margin: 2px 0 0;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -289,13 +224,13 @@
|
|||
}
|
||||
|
||||
.profile-editor-panel__head {
|
||||
padding: 18px 20px 14px;
|
||||
padding: 12px 18px 10px;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
}
|
||||
|
||||
.profile-tabs .ant-tabs-nav {
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
padding: 0 18px;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
|
@ -305,7 +240,7 @@
|
|||
}
|
||||
|
||||
.profile-tabs .ant-tabs-tab {
|
||||
padding: 14px 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.profile-tabs .ant-tabs-tab + .ant-tabs-tab {
|
||||
|
|
@ -313,29 +248,64 @@
|
|||
}
|
||||
|
||||
.profile-tabs .ant-tabs-content-holder {
|
||||
padding: 20px;
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 16px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 8px;
|
||||
background: #fafbfe;
|
||||
}
|
||||
|
||||
.profile-form-intro .ant-typography-secondary {
|
||||
.profile-tab-summary .ant-typography-secondary {
|
||||
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 {
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.profile-form .ant-input,
|
||||
|
|
@ -343,15 +313,22 @@
|
|||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.profile-readonly-input {
|
||||
color: #334155;
|
||||
background: #f8fafc !important;
|
||||
cursor: default;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.profile-avatar-control {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 18px;
|
||||
gap: 14px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-top: 2px;
|
||||
padding: 16px;
|
||||
margin-top: 0;
|
||||
padding: 12px;
|
||||
border: 1px dashed #9fbcfb;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(90deg, rgba(60, 112, 245, 0.07), rgba(25, 135, 84, 0.05));
|
||||
|
|
@ -359,19 +336,25 @@
|
|||
|
||||
.profile-avatar-control__preview {
|
||||
flex: 0 0 auto;
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dbe6fb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.profile-avatar-control__preview .ant-upload {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.profile-avatar-control__preview .ant-image,
|
||||
.profile-avatar-control__image {
|
||||
display: block;
|
||||
width: 132px !important;
|
||||
height: 132px !important;
|
||||
width: 104px !important;
|
||||
height: 104px !important;
|
||||
}
|
||||
|
||||
.profile-avatar-control__image {
|
||||
|
|
@ -383,7 +366,7 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
|
@ -391,16 +374,18 @@
|
|||
color: #2463eb;
|
||||
background: #f8fbff;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-avatar-control__empty .anticon {
|
||||
font-size: 26px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.profile-avatar-control__empty span {
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.profile-avatar-control__content {
|
||||
|
|
@ -410,12 +395,12 @@
|
|||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
gap: 14px;
|
||||
padding: 4px 0;
|
||||
gap: 10px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.profile-avatar-control__title {
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 4px;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
|
|
@ -426,34 +411,68 @@
|
|||
max-width: 520px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.profile-credential-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
background: #fafbfe;
|
||||
}
|
||||
|
||||
.profile-credential-panel__descriptions .ant-descriptions-view {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.profile-credential-item--wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.profile-credential-panel__descriptions .ant-descriptions-item-label {
|
||||
width: 180px;
|
||||
color: #475569;
|
||||
background: #f8fafc;
|
||||
.profile-credential-item > span,
|
||||
.profile-credential-item > strong {
|
||||
display: block;
|
||||
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 {
|
||||
margin: 16px 0 0;
|
||||
padding-top: 16px;
|
||||
margin: 10px 0 0;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #edf1f7;
|
||||
}
|
||||
|
||||
|
|
@ -462,21 +481,24 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile-workbench {
|
||||
grid-template-columns: 1fr;
|
||||
.profile-hero__facts {
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-shell {
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.profile-hero {
|
||||
padding: 18px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.profile-hero__identity {
|
||||
.profile-hero__identity,
|
||||
.profile-hero__facts {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
|
@ -485,26 +507,43 @@
|
|||
line-height: 30px;
|
||||
}
|
||||
|
||||
.profile-workbench {
|
||||
grid-template-columns: 1fr;
|
||||
.profile-hero__facts {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.profile-editor-panel__head,
|
||||
.profile-form-intro,
|
||||
.profile-tab-summary,
|
||||
.profile-avatar-control {
|
||||
flex-direction: column;
|
||||
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 {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
.profile-avatar-control__preview .ant-image,
|
||||
.profile-avatar-control__image {
|
||||
width: 112px !important;
|
||||
height: 112px !important;
|
||||
width: 104px !important;
|
||||
height: 104px !important;
|
||||
}
|
||||
|
||||
.profile-tabs .ant-tabs-nav,
|
||||
|
|
@ -527,9 +566,15 @@
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
.profile-overview-panel,
|
||||
.profile-security-panel,
|
||||
.profile-tabs .ant-tabs-content-holder {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.profile-credential-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.profile-credential-item--wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Button,
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
|
|
@ -17,17 +15,12 @@ import {
|
|||
message
|
||||
} from "antd";
|
||||
import {
|
||||
ApartmentOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloudUploadOutlined,
|
||||
IdcardOutlined,
|
||||
KeyOutlined,
|
||||
LockOutlined,
|
||||
MailOutlined,
|
||||
PhoneOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
SaveOutlined,
|
||||
SolutionOutlined,
|
||||
UploadOutlined,
|
||||
|
|
@ -196,32 +189,6 @@ export default function Profile() {
|
|||
{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 (
|
||||
<PageContainer title={null} className="profile-page">
|
||||
|
|
@ -262,73 +229,23 @@ export default function Profile() {
|
|||
</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>
|
||||
|
||||
<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">
|
||||
<div className="profile-editor-panel__head">
|
||||
<div>
|
||||
<div className="profile-panel__eyebrow">账户维护</div>
|
||||
<Title level={5} className="profile-panel__title">账户设置</Title>
|
||||
</div>
|
||||
<Text type="secondary">基础资料、安全密码与接口凭证统一维护</Text>
|
||||
</div>
|
||||
<Tabs
|
||||
defaultActiveKey="basic"
|
||||
|
|
@ -343,95 +260,104 @@ export default function Profile() {
|
|||
),
|
||||
children: (
|
||||
<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>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<div className="profile-avatar-control">
|
||||
<div className="profile-avatar-control__preview">
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt="头像预览"
|
||||
className="profile-avatar-control__image"
|
||||
preview={{ mask: "查看大图" }}
|
||||
/>
|
||||
) : (
|
||||
<div className="profile-tab-surface">
|
||||
<div className="profile-tab-summary">
|
||||
<div>
|
||||
<Title level={5}>基础资料</Title>
|
||||
<Text type="secondary">用于系统内展示和管理员识别,请保持联系方式准确。</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-basic-layout">
|
||||
<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
|
||||
accept={AVATAR_UPLOAD_ACCEPT}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleAvatarUpload}
|
||||
disabled={avatarUploading}
|
||||
>
|
||||
<button type="button" className="profile-avatar-control__empty">
|
||||
<CloudUploadOutlined />
|
||||
<span>上传头像</span>
|
||||
</button>
|
||||
<Button icon={<UploadOutlined/>} loading={avatarUploading}>
|
||||
{avatarUrl ? "更换头像" : t("profile.uploadAvatar")}
|
||||
</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
|
||||
accept={AVATAR_UPLOAD_ACCEPT}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleAvatarUpload}
|
||||
disabled={avatarUploading}
|
||||
>
|
||||
<Button icon={<UploadOutlined/>} loading={avatarUploading}>
|
||||
{avatarUrl ? "更换头像" : t("profile.uploadAvatar")}
|
||||
</Button>
|
||||
</Upload>
|
||||
</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 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>
|
||||
</Form>
|
||||
)
|
||||
|
|
@ -445,81 +371,86 @@ export default function Profile() {
|
|||
),
|
||||
children: (
|
||||
<Form form={pwdForm} layout="vertical" onFinish={handleUpdatePassword} className="profile-form">
|
||||
<div className="profile-form-intro">
|
||||
<div>
|
||||
<Title level={5}>登录安全</Title>
|
||||
<Text type="secondary">定期更新密码可以降低账号被误用的风险。</Text>
|
||||
<div className="profile-tab-surface">
|
||||
<div className="profile-tab-summary">
|
||||
<div>
|
||||
<Title level={5}>登录安全</Title>
|
||||
<Text type="secondary">定期更新密码可以降低账号被误用的风险。</Text>
|
||||
</div>
|
||||
<div className="profile-inline-note">密码更新后立即生效,建议不要复用其他系统密码。</div>
|
||||
</div>
|
||||
</div>
|
||||
<Alert
|
||||
showIcon
|
||||
type="info"
|
||||
message="密码更新后立即生效"
|
||||
description="建议使用与其他系统不同的新密码,避免复用。"
|
||||
className="profile-security-alert"
|
||||
/>
|
||||
|
||||
<Form.Item label={t("profile.currentPassword")} name="oldPassword" rules={[{ required: true }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space size={4}>
|
||||
<span>{t("profile.newPassword")}</span>
|
||||
<Tooltip
|
||||
title={
|
||||
policyHints.length > 0 ? (
|
||||
<ul style={{paddingLeft: 18, margin: 0, fontSize: 13, lineHeight: 1.6}}>
|
||||
{policyHints.map((hint) => (
|
||||
<li key={hint}>{hint}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
t("profile.passwordRules")
|
||||
)
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} lg={8}>
|
||||
<Form.Item label={t("profile.currentPassword")} name="oldPassword" rules={[{ required: true }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} lg={8}>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space size={4}>
|
||||
<span>{t("profile.newPassword")}</span>
|
||||
<Tooltip
|
||||
title={
|
||||
policyHints.length > 0 ? (
|
||||
<ul style={{paddingLeft: 18, margin: 0, fontSize: 13, lineHeight: 1.6}}>
|
||||
{policyHints.map((hint) => (
|
||||
<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"}}/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
name="newPassword"
|
||||
validateFirst
|
||||
rules={[
|
||||
{required: true},
|
||||
{
|
||||
validator: buildPasswordPolicyValidator(policy, () => user?.username)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("profile.confirmNewPassword")}
|
||||
name="confirmPassword"
|
||||
dependencies={["newPassword"]}
|
||||
rules={[
|
||||
{ required: true },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue("newPassword") === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t("profile.passwordsDoNotMatch")));
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</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>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} lg={8}>
|
||||
<Form.Item
|
||||
label={t("profile.confirmNewPassword")}
|
||||
name="confirmPassword"
|
||||
dependencies={["newPassword"]}
|
||||
rules={[
|
||||
{ required: true },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue("newPassword") === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t("profile.passwordsDoNotMatch")));
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<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 className="app-page__content-toolbar-filters" />
|
||||
</div>
|
||||
<div className="app-page__content-toolbar-filters" />
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
|
|
@ -533,80 +464,63 @@ export default function Profile() {
|
|||
),
|
||||
children: (
|
||||
<div className="profile-credential-panel">
|
||||
<div className="profile-form-intro">
|
||||
<div>
|
||||
<Title level={5}>Bot 接入凭证</Title>
|
||||
<Text type="secondary">为自动化工具或 Bot 调用保留独立凭证,便于轮换和审计。</Text>
|
||||
<div className="profile-tab-surface">
|
||||
<div className="profile-tab-summary">
|
||||
<div>
|
||||
<Title level={5}>Bot 接入凭证</Title>
|
||||
<Text type="secondary">为自动化工具或 Bot 调用保留独立凭证,便于轮换和审计。</Text>
|
||||
</div>
|
||||
<div className="profile-inline-note">重新生成后,旧密钥会立即失效。</div>
|
||||
</div>
|
||||
</div>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={t("profile.botCredentialHint")}
|
||||
description={t("profile.botCredentialHintDesc")}
|
||||
/>
|
||||
|
||||
<div className="profile-credential-card" aria-busy={credentialLoading}>
|
||||
<Descriptions
|
||||
bordered
|
||||
size="middle"
|
||||
column={1}
|
||||
className="profile-credential-panel__descriptions"
|
||||
items={[
|
||||
{
|
||||
key: "bind-status",
|
||||
label: t("profile.botBindStatus"),
|
||||
children: credential?.bound ? <Tag color="success">{t("profile.botBound")}</Tag> :
|
||||
<Tag>{t("profile.botUnbound")}</Tag>
|
||||
},
|
||||
{
|
||||
key: "bot-id",
|
||||
label: "X-Bot-Id",
|
||||
children: credential?.botId ? (
|
||||
<Paragraph copyable={{ text: credential.botId }} style={{ marginBottom: 0 }}>
|
||||
{credential.botId}
|
||||
</Paragraph>
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "bot-secret",
|
||||
label: "X-Bot-Secret",
|
||||
children: credential?.botSecret ? (
|
||||
<Paragraph copyable={{ text: credential.botSecret }} style={{ marginBottom: 0 }}>
|
||||
{credential.botSecret}
|
||||
</Paragraph>
|
||||
) : (
|
||||
t("profile.botSecretHidden")
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "last-access-time",
|
||||
label: t("profile.botLastAccessTime"),
|
||||
children: <span className="tabular-nums">{renderValue(credential?.lastAccessTime)}</span>
|
||||
},
|
||||
{
|
||||
key: "last-access-ip",
|
||||
label: t("profile.botLastAccessIp"),
|
||||
children: <span className="tabular-nums">{renderValue(credential?.lastAccessIp)}</span>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</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 className="profile-credential-grid" aria-busy={credentialLoading}>
|
||||
<div className="profile-credential-item">
|
||||
<span>{t("profile.botBindStatus")}</span>
|
||||
<strong>{credential?.bound ? <Tag color="success">{t("profile.botBound")}</Tag> : <Tag>{t("profile.botUnbound")}</Tag>}</strong>
|
||||
</div>
|
||||
<div className="profile-credential-item profile-credential-item--wide">
|
||||
<span>X-Bot-Id</span>
|
||||
{credential?.botId ? (
|
||||
<Paragraph copyable={{ text: credential.botId }} className="profile-copy-value">
|
||||
{credential.botId}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<strong>-</strong>
|
||||
)}
|
||||
</div>
|
||||
<div className="profile-credential-item profile-credential-item--wide">
|
||||
<span>X-Bot-Secret</span>
|
||||
{credential?.botSecret ? (
|
||||
<Paragraph copyable={{ text: credential.botSecret }} className="profile-copy-value">
|
||||
{credential.botSecret}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<strong>{t("profile.botSecretHidden")}</strong>
|
||||
)}
|
||||
</div>
|
||||
<div className="profile-credential-item">
|
||||
<span>{t("profile.botLastAccessTime")}</span>
|
||||
<strong className="tabular-nums">{renderValue(credential?.lastAccessTime)}</strong>
|
||||
</div>
|
||||
<div className="profile-credential-item">
|
||||
<span>{t("profile.botLastAccessIp")}</span>
|
||||
<strong className="tabular-nums">{renderValue(credential?.lastAccessIp)}</strong>
|
||||
</div>
|
||||
</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 className="app-page__content-toolbar-filters" />
|
||||
</div>
|
||||
<div className="app-page__content-toolbar-filters" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -614,7 +528,6 @@ export default function Profile() {
|
|||
]}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue