mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 05:27:48 +08:00
feat: 统一手动发送入口为文字转语音
- 移除普通文本、图片和本地语音的手动发送入口 - 保留文字转语音的生成、试听和发送能力
This commit is contained in:
@@ -124,6 +124,25 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
return typeMatch && contentMatch
|
||||
})
|
||||
}, [messages, contentFilter])
|
||||
|
||||
const handleOpenPersonalWechatSend = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const status = await window.api.getPersonalWechatSenderStatus()
|
||||
if (!status.canSendVoice) {
|
||||
if (onOpenPersonalWechatSettings) {
|
||||
onOpenPersonalWechatSettings()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (onOpenPersonalWechatSettings) {
|
||||
onOpenPersonalWechatSettings()
|
||||
return
|
||||
}
|
||||
}
|
||||
setSendDialogOpen(true)
|
||||
}, [onOpenPersonalWechatSettings])
|
||||
|
||||
if (!contact) return <EmptyConversationState />
|
||||
|
||||
return (
|
||||
@@ -138,7 +157,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
onContentFilterChange={onContentFilterChange || (() => undefined)}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshData={onRefreshData}
|
||||
onTestSend={() => setSendDialogOpen(true)}
|
||||
onTestSend={() => void handleOpenPersonalWechatSend()}
|
||||
onOpenAiSettings={onCreateGroupReport || (() => undefined)}
|
||||
/>
|
||||
<DataTrustBar messageCount={messages.length} />
|
||||
|
||||
@@ -106,11 +106,12 @@ export function ChatHeader({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="chat-header-text-action"
|
||||
aria-label="发送消息"
|
||||
aria-label="文字转语音"
|
||||
title="文字转语音"
|
||||
onClick={onTestSend}
|
||||
>
|
||||
<SendIcon />
|
||||
<span>发送消息</span>
|
||||
<span>文字转语音</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip>
|
||||
@@ -120,12 +121,13 @@ export function ChatHeader({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="chat-header-text-action"
|
||||
aria-label="发送消息"
|
||||
aria-label="文字转语音"
|
||||
title="文字转语音"
|
||||
onClick={onTestSend}
|
||||
disabled
|
||||
>
|
||||
<SendIcon />
|
||||
<span>发送消息</span>
|
||||
<span>文字转语音</span>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type {
|
||||
PersonalWechatSendRequest,
|
||||
PersonalWechatSenderStatus
|
||||
} from '../../../../shared/personal-wechat'
|
||||
import type { PersonalWechatSenderStatus } from '../../../../shared/personal-wechat'
|
||||
import type { TextToSpeechSettings, TextToSpeechVoice } from '../../../../shared/text-to-speech'
|
||||
import { Button, SegmentedControl, SegmentedControlItem, Textarea } from '../ui'
|
||||
import { Button, Textarea } from '../ui'
|
||||
|
||||
export type PersonalWechatComposerMode = 'text' | 'image' | 'voice'
|
||||
type VoiceSource = 'generated' | 'file'
|
||||
type SelectedLocalFile = { path: string; name: string }
|
||||
type GeneratedVoice = { filePath: string; audioDataUrl: string }
|
||||
|
||||
export type ChatMessage = {
|
||||
id: string
|
||||
type: PersonalWechatComposerMode | 'system'
|
||||
type: 'voice' | 'system'
|
||||
text?: string
|
||||
fileName?: string
|
||||
outgoing: boolean
|
||||
@@ -23,15 +17,9 @@ interface PersonalWechatChatComposerProps {
|
||||
className?: string
|
||||
status: PersonalWechatSenderStatus
|
||||
targetId: string
|
||||
isGroupChat: boolean
|
||||
initialMode?: PersonalWechatComposerMode
|
||||
initialImage?: SelectedLocalFile | null
|
||||
onOpenTextToSpeechSettings?: () => void
|
||||
onCancel: () => void
|
||||
onSend: (
|
||||
request: PersonalWechatSendRequest,
|
||||
displayText: string
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
onSend: (filePath: string, displayText: string) => Promise<{ success: boolean; error?: string }>
|
||||
onMessage: (message: ChatMessage) => void
|
||||
busy: boolean
|
||||
}
|
||||
@@ -46,20 +34,12 @@ export function PersonalWechatChatComposer({
|
||||
className,
|
||||
status,
|
||||
targetId,
|
||||
isGroupChat,
|
||||
initialMode = 'text',
|
||||
initialImage = null,
|
||||
onOpenTextToSpeechSettings,
|
||||
onCancel,
|
||||
onSend,
|
||||
onMessage,
|
||||
busy
|
||||
}: PersonalWechatChatComposerProps): React.ReactElement {
|
||||
const [mode, setMode] = useState<PersonalWechatComposerMode>(initialMode)
|
||||
const [text, setText] = useState('')
|
||||
const [image, setImage] = useState<SelectedLocalFile | null>(initialImage)
|
||||
const [voice, setVoice] = useState<SelectedLocalFile | null>(null)
|
||||
const [voiceSource, setVoiceSource] = useState<VoiceSource>('generated')
|
||||
const [voiceText, setVoiceText] = useState('')
|
||||
const [ttsSettings, setTtsSettings] = useState<TextToSpeechSettings | null>(null)
|
||||
const [ttsVoices, setTtsVoices] = useState<TextToSpeechVoice[]>([])
|
||||
@@ -71,12 +51,6 @@ export function PersonalWechatChatComposer({
|
||||
const [feedback, setFeedback] = useState<{ success: boolean; message: string } | null>(null)
|
||||
const generatedVoiceRef = useRef<GeneratedVoice | null>(null)
|
||||
const generatedAudioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const selectedTypeReady =
|
||||
mode === 'text'
|
||||
? status.canSendText
|
||||
: mode === 'image'
|
||||
? status.canSendImage
|
||||
: status.canSendVoice
|
||||
const isBusy = busy || isGenerating
|
||||
const selectedTtsVoice = ttsVoices.find((item) => item.id === ttsSettings?.selectedVoiceId)
|
||||
|
||||
@@ -108,61 +82,17 @@ export function PersonalWechatChatComposer({
|
||||
setIsPreviewPlaying(false)
|
||||
setPreviewCurrentTime(0)
|
||||
setPreviewDuration(0)
|
||||
if (current?.filePath)
|
||||
if (current?.filePath) {
|
||||
void window.api.removeGeneratedTextToSpeechAudio(current.filePath).catch(() => undefined)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => () => clearGeneratedVoice(), [clearGeneratedVoice])
|
||||
|
||||
const selectImage = async (): Promise<void> => {
|
||||
if (isBusy) return
|
||||
const selection = await window.api.selectPersonalWechatImage()
|
||||
if (!selection.canceled && selection.path) {
|
||||
setImage({
|
||||
path: selection.path,
|
||||
name: selection.name || selection.path.split(/[\\/]/).pop() || '图片'
|
||||
})
|
||||
setFeedback(null)
|
||||
}
|
||||
}
|
||||
|
||||
const selectVoice = async (): Promise<void> => {
|
||||
if (isBusy) return
|
||||
const selection = await window.api.selectPersonalWechatVoice()
|
||||
if (!selection.canceled && selection.path) {
|
||||
setVoice({
|
||||
path: selection.path,
|
||||
name: selection.name || selection.path.split(/[\\/]/).pop() || '语音'
|
||||
})
|
||||
setFeedback(null)
|
||||
}
|
||||
}
|
||||
|
||||
const send = async (
|
||||
request: PersonalWechatSendRequest,
|
||||
displayText: string
|
||||
): Promise<boolean> => {
|
||||
if (isBusy || !selectedTypeReady) return false
|
||||
setFeedback(null)
|
||||
const result = await onSend(request, displayText)
|
||||
setFeedback({
|
||||
success: result.success,
|
||||
message: result.success ? '已发送' : result.error || '发送失败'
|
||||
})
|
||||
if (result.success)
|
||||
onMessage({
|
||||
id: `${Date.now()}-${Math.random()}`,
|
||||
type: request.type,
|
||||
text: request.type === 'text' ? request.text : displayText,
|
||||
fileName: request.type === 'text' ? undefined : displayText,
|
||||
outgoing: true
|
||||
})
|
||||
return result.success
|
||||
}
|
||||
|
||||
const generateVoice = async (): Promise<void> => {
|
||||
if (isBusy || !ttsSettings?.hasApiKey || !ttsSettings.selectedVoiceId || !voiceText.trim())
|
||||
if (isBusy || !ttsSettings?.hasApiKey || !ttsSettings.selectedVoiceId || !voiceText.trim()) {
|
||||
return
|
||||
}
|
||||
clearGeneratedVoice()
|
||||
setIsGenerating(true)
|
||||
setFeedback(null)
|
||||
@@ -188,244 +118,122 @@ export function PersonalWechatChatComposer({
|
||||
}
|
||||
}
|
||||
|
||||
const sendCurrent = async (): Promise<void> => {
|
||||
if (mode === 'text') {
|
||||
const value = text.trim()
|
||||
if (value)
|
||||
await send({ type: 'text', to: targetId, text: value, isGroup: isGroupChat }, value)
|
||||
} else if (mode === 'image' && image) {
|
||||
await send(
|
||||
{ type: 'image', to: targetId, filePath: image.path, isGroup: isGroupChat },
|
||||
image.name
|
||||
)
|
||||
} else if (mode === 'voice' && voiceSource === 'file' && voice) {
|
||||
await send(
|
||||
{ type: 'voice', to: targetId, filePath: voice.path, isGroup: isGroupChat },
|
||||
voice.name
|
||||
)
|
||||
const sendGenerated = async (): Promise<void> => {
|
||||
const current = generatedVoiceRef.current
|
||||
if (!current || isBusy || !status.canSendVoice) return
|
||||
setFeedback(null)
|
||||
const displayText = voiceText.trim() || '语音消息'
|
||||
const result = await onSend(current.filePath, displayText)
|
||||
setFeedback({
|
||||
success: result.success,
|
||||
message: result.success
|
||||
? '发送成功, 可使用另一设备查看发送的消息'
|
||||
: result.error || '发送失败'
|
||||
})
|
||||
if (result.success) {
|
||||
onMessage({
|
||||
id: `${Date.now()}-${Math.random()}`,
|
||||
type: 'voice',
|
||||
text: displayText,
|
||||
fileName: displayText,
|
||||
outgoing: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const sendGenerated = async (): Promise<void> => {
|
||||
if (!generatedVoiceRef.current) return
|
||||
await send(
|
||||
{
|
||||
type: 'voice',
|
||||
to: targetId,
|
||||
filePath: generatedVoiceRef.current.filePath,
|
||||
isGroup: isGroupChat
|
||||
},
|
||||
voiceText.trim() || '语音消息'
|
||||
)
|
||||
}
|
||||
|
||||
const sendGeneratedAlternative = async (): Promise<void> => {
|
||||
if (!generatedVoiceRef.current) return
|
||||
await send(
|
||||
{
|
||||
type: 'voice',
|
||||
to: targetId,
|
||||
filePath: generatedVoiceRef.current.filePath,
|
||||
isGroup: isGroupChat,
|
||||
voiceSendMode: 'legacy'
|
||||
},
|
||||
voiceText.trim() || '语音消息'
|
||||
)
|
||||
}
|
||||
|
||||
const canSend = Boolean(
|
||||
selectedTypeReady &&
|
||||
!isBusy &&
|
||||
((mode === 'text' && text.trim()) ||
|
||||
(mode === 'image' && image) ||
|
||||
(mode === 'voice' &&
|
||||
((voiceSource === 'file' && voice) || (voiceSource === 'generated' && generatedVoice))))
|
||||
)
|
||||
const generatedReady = Boolean(
|
||||
ttsSettings?.hasApiKey && ttsSettings.selectedVoiceId && voiceText.trim()
|
||||
)
|
||||
const canSend = Boolean(targetId && status.canSendVoice && !isBusy && generatedVoice)
|
||||
const previewProgress = previewDuration > 0 ? (previewCurrentTime / previewDuration) * 100 : 0
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`personal-wechat-composer${className ? ` ${className}` : ''}`}
|
||||
aria-label="发送消息"
|
||||
aria-label="文字转语音"
|
||||
>
|
||||
<SegmentedControl
|
||||
className="personal-wechat-composer-mode"
|
||||
aria-label="消息类型"
|
||||
value={mode}
|
||||
onValueChange={(value) => {
|
||||
clearGeneratedVoice()
|
||||
setMode(value as PersonalWechatComposerMode)
|
||||
setFeedback(null)
|
||||
}}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<SegmentedControlItem value="text">文字</SegmentedControlItem>
|
||||
<SegmentedControlItem value="image">图片</SegmentedControlItem>
|
||||
<SegmentedControlItem value="voice">语音</SegmentedControlItem>
|
||||
</SegmentedControl>
|
||||
|
||||
<div className="personal-wechat-composer-editor">
|
||||
{mode === 'text' && (
|
||||
<div className="personal-wechat-text-editor">
|
||||
<Textarea
|
||||
aria-label="消息内容"
|
||||
placeholder="输入消息"
|
||||
value={text}
|
||||
maxLength={2000}
|
||||
rows={3}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
setText(event.target.value)
|
||||
setFeedback(null)
|
||||
<div className="personal-wechat-voice-editor">
|
||||
<div className="personal-wechat-voice-heading">
|
||||
<span>{selectedTtsVoice?.name || '尚未选择音色'}</span>
|
||||
{onOpenTextToSpeechSettings && (
|
||||
<Button variant="link" size="sm" onClick={onOpenTextToSpeechSettings} disabled={isBusy}>
|
||||
语音设置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label="语音文字"
|
||||
placeholder="输入要生成的文字"
|
||||
value={voiceText}
|
||||
maxLength={1000}
|
||||
rows={4}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
clearGeneratedVoice()
|
||||
setVoiceText(event.target.value)
|
||||
setFeedback(null)
|
||||
}}
|
||||
/>
|
||||
{generatedVoice ? (
|
||||
<div className="personal-wechat-generated-result">
|
||||
<audio
|
||||
ref={generatedAudioRef}
|
||||
src={generatedVoice.audioDataUrl}
|
||||
preload="metadata"
|
||||
onLoadedMetadata={(event) => setPreviewDuration(event.currentTarget.duration)}
|
||||
onTimeUpdate={(event) => setPreviewCurrentTime(event.currentTarget.currentTime)}
|
||||
onPause={() => setIsPreviewPlaying(false)}
|
||||
onPlay={() => setIsPreviewPlaying(true)}
|
||||
onEnded={() => {
|
||||
setIsPreviewPlaying(false)
|
||||
setPreviewCurrentTime(0)
|
||||
}}
|
||||
/>
|
||||
<span>{text.length} / 2000</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'image' && (
|
||||
<div className="personal-wechat-file-editor">
|
||||
<div>
|
||||
<strong>{image?.name || '还没有选择图片'}</strong>
|
||||
<small>{image ? image.path : '支持 PNG、JPG、GIF 和 WebP,最大 20 MB'}</small>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void selectImage()}
|
||||
disabled={isBusy}
|
||||
>
|
||||
{image ? '重新选择' : '选择图片'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'voice' && (
|
||||
<div className="personal-wechat-voice-editor">
|
||||
<SegmentedControl
|
||||
aria-label="语音来源"
|
||||
value={voiceSource}
|
||||
onValueChange={(value) => {
|
||||
clearGeneratedVoice()
|
||||
setVoiceSource(value as VoiceSource)
|
||||
setFeedback(null)
|
||||
}}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<SegmentedControlItem value="generated">输入文字生成</SegmentedControlItem>
|
||||
<SegmentedControlItem value="file">选择本地文件</SegmentedControlItem>
|
||||
</SegmentedControl>
|
||||
{voiceSource === 'generated' ? (
|
||||
<>
|
||||
<div className="personal-wechat-voice-heading">
|
||||
<span>{selectedTtsVoice?.name || '尚未选择音色'}</span>
|
||||
{onOpenTextToSpeechSettings && (
|
||||
<Button variant="link" size="sm" onClick={onOpenTextToSpeechSettings}>
|
||||
语音设置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label="语音文字"
|
||||
placeholder="输入要生成的文字"
|
||||
value={voiceText}
|
||||
maxLength={1000}
|
||||
rows={2}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
clearGeneratedVoice()
|
||||
setVoiceText(event.target.value)
|
||||
setFeedback(null)
|
||||
}}
|
||||
/>
|
||||
{generatedVoice ? (
|
||||
<div className="personal-wechat-generated-result">
|
||||
<audio
|
||||
ref={generatedAudioRef}
|
||||
src={generatedVoice.audioDataUrl}
|
||||
preload="metadata"
|
||||
onLoadedMetadata={(event) => setPreviewDuration(event.currentTarget.duration)}
|
||||
onTimeUpdate={(event) =>
|
||||
setPreviewCurrentTime(event.currentTarget.currentTime)
|
||||
}
|
||||
onPause={() => setIsPreviewPlaying(false)}
|
||||
onPlay={() => setIsPreviewPlaying(true)}
|
||||
onEnded={() => {
|
||||
setIsPreviewPlaying(false)
|
||||
setPreviewCurrentTime(0)
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<strong>语音已生成</strong>
|
||||
<span>
|
||||
{formatAudioTime(previewCurrentTime)} / {formatAudioTime(previewDuration)}
|
||||
</span>
|
||||
<div className="personal-wechat-preview-track">
|
||||
<span style={{ width: `${Math.min(100, previewProgress)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="personal-wechat-generated-result-actions">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const audio = generatedAudioRef.current
|
||||
if (!audio) return
|
||||
if (audio.paused) {
|
||||
await audio.play()
|
||||
setIsPreviewPlaying(true)
|
||||
} else {
|
||||
audio.pause()
|
||||
}
|
||||
}}
|
||||
disabled={busy}
|
||||
>
|
||||
{isPreviewPlaying ? '暂停' : '试听'}
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => void sendGenerated()} disabled={!canSend}>
|
||||
{busy ? '发送中…' : '发送'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void sendGeneratedAlternative()}
|
||||
disabled={!canSend}
|
||||
title="使用另一条语音发送路径"
|
||||
>
|
||||
{busy ? '发送中…' : '空白语音?重新处理'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void generateVoice()}
|
||||
disabled={!generatedReady || isBusy}
|
||||
>
|
||||
{isGenerating ? '正在生成…' : '生成语音'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="personal-wechat-file-editor">
|
||||
<div>
|
||||
<strong>{voice?.name || '还没有选择语音文件'}</strong>
|
||||
<small>{voice ? voice.path : '支持 SILK、MP3、WAV、M4A、AAC、OGG 和 FLAC'}</small>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void selectVoice()}
|
||||
disabled={isBusy}
|
||||
>
|
||||
{voice ? '重新选择' : '选择语音'}
|
||||
</Button>
|
||||
<strong>语音已生成</strong>
|
||||
<span>
|
||||
{formatAudioTime(previewCurrentTime)} / {formatAudioTime(previewDuration)}
|
||||
</span>
|
||||
<div className="personal-wechat-preview-track">
|
||||
<span style={{ width: `${Math.min(100, previewProgress)}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="personal-wechat-generated-result-actions">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const audio = generatedAudioRef.current
|
||||
if (!audio) return
|
||||
if (audio.paused) {
|
||||
await audio.play()
|
||||
setIsPreviewPlaying(true)
|
||||
} else {
|
||||
audio.pause()
|
||||
}
|
||||
}}
|
||||
disabled={busy}
|
||||
>
|
||||
{isPreviewPlaying ? '暂停' : '试听'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void sendGenerated()}
|
||||
disabled={!canSend}
|
||||
aria-label="发送到微信"
|
||||
>
|
||||
{busy ? '发送中…' : '发送到微信'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void generateVoice()}
|
||||
disabled={!generatedReady || isBusy}
|
||||
>
|
||||
{isGenerating ? '正在生成…' : '生成语音'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -436,11 +244,6 @@ export function PersonalWechatChatComposer({
|
||||
<Button variant="outline" size="sm" onClick={onCancel} disabled={isBusy}>
|
||||
取消
|
||||
</Button>
|
||||
{!(mode === 'voice' && voiceSource === 'generated' && generatedVoice) && (
|
||||
<Button size="sm" onClick={() => void sendCurrent()} disabled={!canSend}>
|
||||
{busy ? '正在发送…' : '发送消息'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -21,8 +21,7 @@ import {
|
||||
import { isMac, isWindows } from '../../utils/runtime-environment'
|
||||
import {
|
||||
PersonalWechatChatComposer,
|
||||
type ChatMessage,
|
||||
type PersonalWechatComposerMode
|
||||
type ChatMessage
|
||||
} from './PersonalWechatChatComposer'
|
||||
import { PersonalWechatSetupGuide } from './PersonalWechatSetupGuide'
|
||||
import { PersonalWechatVoiceDiagnosticDialog } from './PersonalWechatVoiceDiagnosticDialog'
|
||||
@@ -36,7 +35,7 @@ export interface PersonalWechatSendDialogProps {
|
||||
onClose: () => void
|
||||
onOpenTextToSpeechSettings?: () => void
|
||||
onOpenPersonalWechatSettings?: () => void
|
||||
initialMode?: PersonalWechatComposerMode
|
||||
initialMode?: 'text' | 'image' | 'voice'
|
||||
initialImage?: SelectedLocalFile | null
|
||||
}
|
||||
|
||||
@@ -76,7 +75,6 @@ function PersonalWechatMacSendDialog({
|
||||
isGroupChat,
|
||||
onClose,
|
||||
onOpenTextToSpeechSettings,
|
||||
initialMode = 'text',
|
||||
initialImage = null
|
||||
}: PersonalWechatSendDialogProps): React.ReactElement {
|
||||
const [senderStatus, setSenderStatus] = useState<PersonalWechatSenderStatus | null>(null)
|
||||
@@ -89,28 +87,20 @@ function PersonalWechatMacSendDialog({
|
||||
const [runtimeBusy, setRuntimeBusy] = useState(false)
|
||||
const [sendBusy, setSendBusy] = useState(false)
|
||||
const [detectionAttempted, setDetectionAttempted] = useState(false)
|
||||
// Status can be reconstructed from a previous OneBot process/log. These
|
||||
// session gates ensure the user explicitly binds and detects after opening
|
||||
// the flow instead of inheriting stale readiness.
|
||||
// 状态可能来自之前的 OneBot 进程或日志;发送入口只信任当前语音能力状态。
|
||||
const [sessionBound, setSessionBound] = useState(false)
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
const [sendError, setSendError] = useState<string | null>(null)
|
||||
const [voiceDiagnostic, setVoiceDiagnostic] = useState<PersonalWechatVoiceDiagnostic | null>(null)
|
||||
const [voiceDiagnosticOpen, setVoiceDiagnosticOpen] = useState(false)
|
||||
const [keepOneBotProcess, setKeepOneBotProcess] = useState(false)
|
||||
const [composerStarted, setComposerStarted] = useState(Boolean(initialImage))
|
||||
const requestIdRef = useRef(0)
|
||||
const restoreFocusRef = useRef<HTMLElement | null>(null)
|
||||
const closingRef = useRef(false)
|
||||
const displayName = contact.m_nsNickName || contact.m_nsUsrName || '未命名会话'
|
||||
const targetId = contact.m_nsUsrName
|
||||
const isBusy = binding || runtimeBusy || sendBusy
|
||||
const setupReady = Boolean(
|
||||
sessionBound &&
|
||||
detectionAttempted &&
|
||||
senderStatus?.canSendText &&
|
||||
(senderStatus?.canSendImage || senderStatus?.canSendVoice)
|
||||
)
|
||||
const setupReady = Boolean(initialImage ? senderStatus?.canSendImage : senderStatus?.canSendVoice)
|
||||
|
||||
const refreshStatus = useCallback(async (): Promise<void> => {
|
||||
const requestId = ++requestIdRef.current
|
||||
@@ -246,15 +236,25 @@ function PersonalWechatMacSendDialog({
|
||||
}
|
||||
|
||||
const handleSend = async (
|
||||
request: PersonalWechatSendRequest
|
||||
filePath: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
setSendBusy(true)
|
||||
setSendError(null)
|
||||
try {
|
||||
const response = await window.api.sendPersonalWechatMessage({ ...request, to: targetId })
|
||||
const response = await window.api.sendGeneratedTtsVoice({
|
||||
to: targetId,
|
||||
isGroup: isGroupChat,
|
||||
filePath
|
||||
})
|
||||
setSenderStatus(response.status)
|
||||
if (!response.success) setSendError(response.error || '发送失败')
|
||||
return { success: response.success, error: response.error }
|
||||
const success = response.action.status === 'sent'
|
||||
const error = success
|
||||
? undefined
|
||||
: response.action.errorCode === 'SEND_CAPABILITY_UNAVAILABLE'
|
||||
? '当前微信发送能力不可用'
|
||||
: response.action.reason || '发送失败,请重试'
|
||||
if (error) setSendError(error)
|
||||
return { success, error }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
setSendError(message)
|
||||
@@ -264,6 +264,39 @@ function PersonalWechatMacSendDialog({
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendReportImage = async (): Promise<void> => {
|
||||
if (!initialImage || !senderStatus?.canSendImage || sendBusy) return
|
||||
setSendBusy(true)
|
||||
setSendError(null)
|
||||
try {
|
||||
const response = await window.api.sendPersonalWechatMessage({
|
||||
type: 'image',
|
||||
to: targetId,
|
||||
isGroup: isGroupChat,
|
||||
filePath: initialImage.path
|
||||
} satisfies PersonalWechatSendRequest)
|
||||
setSenderStatus(response.status)
|
||||
if (!response.success) {
|
||||
setSendError(response.error || '日报图片发送失败')
|
||||
return
|
||||
}
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
id: `${Date.now()}-${Math.random()}`,
|
||||
type: 'system',
|
||||
text: initialImage.name,
|
||||
fileName: initialImage.name,
|
||||
outgoing: true
|
||||
}
|
||||
])
|
||||
} catch (error) {
|
||||
setSendError(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
setSendBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenVoiceDiagnostic = async (): Promise<void> => {
|
||||
if (!isMac) return
|
||||
const diagnostic = await window.api.getPersonalWechatVoiceDiagnostic()
|
||||
@@ -288,9 +321,9 @@ function PersonalWechatMacSendDialog({
|
||||
{displayName.slice(0, 1)}
|
||||
</div>
|
||||
<div className="personal-wechat-chat-heading">
|
||||
<DialogTitle>{displayName}</DialogTitle>
|
||||
<DialogTitle>文字转语音</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isGroupChat ? '群聊' : '联系人'} · {setupReady ? '微信已连接' : '配置微信消息发送'}
|
||||
发送给 {displayName} · {setupReady ? '微信已连接' : '配置微信发送能力'}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<span
|
||||
@@ -300,7 +333,7 @@ function PersonalWechatMacSendDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="personal-wechat-chat-body">
|
||||
{setupReady && composerStarted && (
|
||||
{setupReady && !initialImage && (
|
||||
<div className="personal-wechat-message-list" aria-label="消息列表">
|
||||
{messages.length === 0 ? (
|
||||
<div className="personal-wechat-empty-message">还没有发送消息。</div>
|
||||
@@ -310,13 +343,7 @@ function PersonalWechatMacSendDialog({
|
||||
key={message.id}
|
||||
className={`personal-wechat-message-bubble ${message.outgoing ? 'is-outgoing' : ''}`}
|
||||
>
|
||||
<span className="personal-wechat-message-kind">
|
||||
{message.type === 'text'
|
||||
? '文字'
|
||||
: message.type === 'image'
|
||||
? '图片'
|
||||
: '语音'}
|
||||
</span>
|
||||
<span className="personal-wechat-message-kind">语音</span>
|
||||
<span>{message.text || message.fileName}</span>
|
||||
</div>
|
||||
))
|
||||
@@ -324,7 +351,7 @@ function PersonalWechatMacSendDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!setupReady || !composerStarted) && senderStatus && (
|
||||
{!setupReady && senderStatus && (
|
||||
<PersonalWechatSetupGuide
|
||||
runtimeStatus={runtimeStatus}
|
||||
senderStatus={senderStatus}
|
||||
@@ -337,18 +364,24 @@ function PersonalWechatMacSendDialog({
|
||||
onBind={() => void handleBind()}
|
||||
detectionAttempted={detectionAttempted}
|
||||
onDetect={() => void handleDetect()}
|
||||
onStartSending={() => setComposerStarted(true)}
|
||||
onStartSending={() => undefined}
|
||||
onOpenTextToSpeechSettings={handleOpenSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{setupReady && composerStarted && (
|
||||
{setupReady && initialImage && (
|
||||
<section className="personal-wechat-composer" aria-label="日报图片发送">
|
||||
<p>已准备日报图片:{initialImage.name}</p>
|
||||
<Button size="sm" onClick={() => void handleSendReportImage()} disabled={sendBusy}>
|
||||
{sendBusy ? '发送中…' : '发送日报图片'}
|
||||
</Button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{setupReady && !initialImage && (
|
||||
<PersonalWechatChatComposer
|
||||
status={senderStatus!}
|
||||
targetId={targetId}
|
||||
isGroupChat={isGroupChat}
|
||||
initialMode={initialMode}
|
||||
initialImage={initialImage}
|
||||
onOpenTextToSpeechSettings={handleOpenSettings}
|
||||
onCancel={handleClose}
|
||||
onSend={handleSend}
|
||||
@@ -362,7 +395,7 @@ function PersonalWechatMacSendDialog({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{setupReady && composerStarted && isMac && (
|
||||
{setupReady && isMac && (
|
||||
<div className="personal-wechat-chat-footer flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>保留 OneBot 进程</span>
|
||||
@@ -377,7 +410,7 @@ function PersonalWechatMacSendDialog({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{(!setupReady || !composerStarted) && (
|
||||
{!setupReady && (
|
||||
<div
|
||||
className={`personal-wechat-chat-footer flex items-center ${isMac ? 'justify-between' : 'justify-end'}`}
|
||||
>
|
||||
|
||||
@@ -77,13 +77,9 @@ export function PersonalWechatSetupGuide({
|
||||
const runtimeReady = runtimeStatus?.state === 'ready' || senderStatus?.runtimeReady === true
|
||||
const runtimeDownloading = runtimeBusy || runtimeStatus?.state === 'downloading'
|
||||
const connected = sessionBound && (senderStatus ? isWechatBound(senderStatus) : false)
|
||||
const canSendText = Boolean(senderStatus?.canSendText)
|
||||
const canSendImage = Boolean(senderStatus?.canSendImage)
|
||||
const canSendVoice = Boolean(senderStatus?.canSendVoice)
|
||||
const mediaReady = canSendImage || canSendVoice
|
||||
const detectedTextReady = detectionAttempted && canSendText
|
||||
const detectedMediaReady = detectionAttempted && mediaReady
|
||||
const verificationComplete = connected && detectedTextReady && detectedMediaReady
|
||||
const detectedVoiceReady = detectionAttempted && canSendVoice
|
||||
const verificationComplete = connected && detectedVoiceReady
|
||||
const allReady = verificationComplete
|
||||
const progress = runtimeProgress || runtimeStatus
|
||||
const progressPercent = Math.max(0, Math.min(100, Math.round((progress?.progress || 0) * 100)))
|
||||
@@ -94,7 +90,7 @@ export function PersonalWechatSetupGuide({
|
||||
<div>
|
||||
<span className="personal-wechat-eyebrow">首次使用</span>
|
||||
<h2>先准备好微信消息功能</h2>
|
||||
<p>只需几步,TraceMemo 就能像普通聊天窗口一样发送文字、图片和语音。</p>
|
||||
<p>只需几步,TraceMemo 就能在当前会话中生成并发送语音。</p>
|
||||
</div>
|
||||
{allReady && <span className="personal-wechat-setup-complete">已完成</span>}
|
||||
</div>
|
||||
@@ -191,14 +187,8 @@ export function PersonalWechatSetupGuide({
|
||||
<span className="personal-wechat-step-number">{verificationComplete ? '✓' : '3'}</span>
|
||||
<div className="personal-wechat-step-content">
|
||||
<strong>验证消息能力</strong>
|
||||
<p>
|
||||
请打开手机微信,给任意好友手动发送:
|
||||
<br />① 一条文字消息
|
||||
<br />② 一张图片
|
||||
</p>
|
||||
<p className="personal-wechat-step-hint">
|
||||
TraceMemo 需要通过你主动发送的消息初始化微信消息能力。完成后点击“重新检测”。
|
||||
</p>
|
||||
<p>请打开微信,可以给任意好友发送一张图片,完成一次能力初始化。</p>
|
||||
<p className="personal-wechat-step-hint">完成后点击“重新检测”。</p>
|
||||
{connected && !verificationComplete && (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -211,7 +201,7 @@ export function PersonalWechatSetupGuide({
|
||||
)}
|
||||
{detectionAttempted && !detecting && !verificationComplete && (
|
||||
<p className="personal-wechat-step-hint" role="status">
|
||||
暂未检测到新的消息,请确认已在手机微信中发送文字和图片,然后再次检测。
|
||||
暂未检测到语音发送能力,请确认微信已完成初始化后再次检测。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -224,10 +214,7 @@ export function PersonalWechatSetupGuide({
|
||||
<div className="personal-wechat-step-content">
|
||||
<strong>能力检测</strong>
|
||||
<div className="personal-wechat-capabilities" aria-label="微信消息能力">
|
||||
{[
|
||||
['文字消息', detectedTextReady],
|
||||
['图片和语音消息', detectedMediaReady]
|
||||
].map(([label, ready]) => (
|
||||
{[['语音消息', detectedVoiceReady]].map(([label, ready]) => (
|
||||
<span key={String(label)} className={ready ? 'is-ready' : ''}>
|
||||
<b aria-hidden>{ready ? '✓' : '−'}</b>
|
||||
{label}
|
||||
|
||||
@@ -59,7 +59,6 @@ export function PersonalWechatWindowsSendDialog({
|
||||
onClose,
|
||||
onOpenTextToSpeechSettings,
|
||||
onOpenPersonalWechatSettings,
|
||||
initialMode = 'text',
|
||||
initialImage = null
|
||||
}: PersonalWechatSendDialogProps): React.ReactElement {
|
||||
const [status, setStatus] = useState<PersonalWechatSenderStatus | null>(null)
|
||||
@@ -105,13 +104,23 @@ export function PersonalWechatWindowsSendDialog({
|
||||
}
|
||||
|
||||
const handleSend = async (
|
||||
request: PersonalWechatSendRequest
|
||||
filePath: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
setSendBusy(true)
|
||||
try {
|
||||
const response = await window.api.sendPersonalWechatMessage({ ...request, to: targetId })
|
||||
const response = await window.api.sendGeneratedTtsVoice({
|
||||
to: targetId,
|
||||
isGroup: isGroupChat,
|
||||
filePath
|
||||
})
|
||||
setStatus(response.status)
|
||||
return { success: response.success, error: response.error }
|
||||
const success = response.action.status === 'sent'
|
||||
const error = success
|
||||
? undefined
|
||||
: response.action.errorCode === 'SEND_CAPABILITY_UNAVAILABLE'
|
||||
? '当前微信发送能力不可用'
|
||||
: response.action.reason || '发送失败,请重试'
|
||||
return { success, error }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
setStatus((current) => ({
|
||||
@@ -125,6 +134,33 @@ export function PersonalWechatWindowsSendDialog({
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendReportImage = async (): Promise<void> => {
|
||||
if (!initialImage || !status?.canSendImage || sendBusy) return
|
||||
setSendBusy(true)
|
||||
try {
|
||||
const response = await window.api.sendPersonalWechatMessage({
|
||||
type: 'image',
|
||||
to: targetId,
|
||||
isGroup: isGroupChat,
|
||||
filePath: initialImage.path
|
||||
} satisfies PersonalWechatSendRequest)
|
||||
setStatus(response.status)
|
||||
if (!response.success) return
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
id: `${Date.now()}-${Math.random()}`,
|
||||
type: 'system',
|
||||
text: initialImage.name,
|
||||
fileName: initialImage.name,
|
||||
outgoing: true
|
||||
}
|
||||
])
|
||||
} finally {
|
||||
setSendBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const statusTone = useMemo(() => {
|
||||
if (status?.state === 'online' && status.canSend) return 'ready'
|
||||
if (!status || status.state === 'checking' || status.state === 'starting') return 'checking'
|
||||
@@ -145,12 +181,12 @@ export function PersonalWechatWindowsSendDialog({
|
||||
<DialogHeader className="flex-row items-center justify-between space-y-0 pr-10">
|
||||
<div>
|
||||
<span className="text-[11px] font-bold tracking-normal text-primary">实验性功能</span>
|
||||
<DialogTitle className="mt-0.5 text-[19px] leading-[26px] tracking-normal">
|
||||
个人微信发送
|
||||
<DialogTitle className="mt-0.5 text-[19px] leading-[26px] tracking-normal">
|
||||
文字转语音
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="sr-only">
|
||||
向当前微信联系人或群聊发送文字、图片和语音。
|
||||
向当前微信联系人或群聊发送文字转语音。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -158,12 +194,12 @@ export function PersonalWechatWindowsSendDialog({
|
||||
<span aria-hidden>i</span>
|
||||
<p>
|
||||
<strong>请在其他设备确认发送结果</strong>
|
||||
语音、图片等消息请切换到手机、平板等其他设备确认是否送达。
|
||||
语音消息请切换到手机、平板等其他设备确认是否送达。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="personal-wechat-send-target">
|
||||
<span>{isGroupChat ? '发送到群聊' : '发送给联系人'}</span>
|
||||
<span>{isGroupChat ? '发送到群聊' : '发送给联系人'}</span>
|
||||
<strong>{displayName}</strong>
|
||||
<code>{targetId}</code>
|
||||
</div>
|
||||
@@ -208,31 +244,31 @@ export function PersonalWechatWindowsSendDialog({
|
||||
key={message.id}
|
||||
className={`personal-wechat-message-bubble ${message.outgoing ? 'is-outgoing' : ''}`}
|
||||
>
|
||||
<span className="personal-wechat-message-kind">
|
||||
{message.type === 'text'
|
||||
? '文字'
|
||||
: message.type === 'image'
|
||||
? '图片'
|
||||
: '语音'}
|
||||
</span>
|
||||
<span className="personal-wechat-message-kind">语音</span>
|
||||
<span>{message.text || message.fileName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<PersonalWechatChatComposer
|
||||
status={status}
|
||||
targetId={targetId}
|
||||
isGroupChat={isGroupChat}
|
||||
initialMode={initialMode}
|
||||
initialImage={initialImage}
|
||||
className="personal-wechat-windows-composer"
|
||||
onOpenTextToSpeechSettings={onOpenTextToSpeechSettings}
|
||||
onCancel={handleClose}
|
||||
onSend={handleSend}
|
||||
onMessage={(message) => setMessages((current) => [...current, message])}
|
||||
busy={sendBusy}
|
||||
/>
|
||||
{initialImage && status.canSendImage ? (
|
||||
<section className="personal-wechat-composer" aria-label="日报图片发送">
|
||||
<p>已准备日报图片:{initialImage.name}</p>
|
||||
<Button size="sm" onClick={() => void handleSendReportImage()} disabled={sendBusy}>
|
||||
{sendBusy ? '发送中…' : '发送日报图片'}
|
||||
</Button>
|
||||
</section>
|
||||
) : !initialImage ? (
|
||||
<PersonalWechatChatComposer
|
||||
status={status}
|
||||
targetId={targetId}
|
||||
className="personal-wechat-windows-composer"
|
||||
onOpenTextToSpeechSettings={onOpenTextToSpeechSettings}
|
||||
onCancel={handleClose}
|
||||
onSend={handleSend}
|
||||
onMessage={(message) => setMessages((current) => [...current, message])}
|
||||
busy={sendBusy}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
span + span::before {
|
||||
span+span::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
@@ -112,7 +112,7 @@
|
||||
}
|
||||
|
||||
.personal-wechat-send-dialog {
|
||||
> footer {
|
||||
>footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@@ -130,7 +130,7 @@
|
||||
background: rgba(36, 122, 99, 0.065);
|
||||
padding: 10px 12px;
|
||||
|
||||
> span {
|
||||
>span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: grid;
|
||||
@@ -251,7 +251,7 @@
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-border);
|
||||
|
||||
> div {
|
||||
>div {
|
||||
min-width: 0;
|
||||
background: var(--wxex-bg-main);
|
||||
padding: 9px 11px;
|
||||
@@ -281,7 +281,7 @@
|
||||
background: var(--wxex-bg-sidebar);
|
||||
padding: 3px;
|
||||
|
||||
> [role='radio'] {
|
||||
>[role='radio'] {
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
border-radius: var(--wxex-radius-sm);
|
||||
@@ -324,7 +324,7 @@
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
|
||||
> span {
|
||||
>span {
|
||||
color: var(--wxex-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -334,7 +334,7 @@
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
> small {
|
||||
>small {
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
@@ -347,18 +347,18 @@
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
|
||||
> span {
|
||||
>span {
|
||||
color: var(--wxex-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
> div,
|
||||
> small {
|
||||
>div,
|
||||
>small {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
> div {
|
||||
>div {
|
||||
min-width: 0;
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-main);
|
||||
@@ -384,7 +384,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
> small {
|
||||
>small {
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -458,7 +458,7 @@
|
||||
background: rgba(36, 122, 99, 0.045);
|
||||
padding: 11px 12px;
|
||||
|
||||
> div:first-child {
|
||||
>div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -484,7 +484,7 @@
|
||||
border-radius: 999px;
|
||||
background: rgba(36, 122, 99, 0.12);
|
||||
|
||||
> span {
|
||||
>span {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
left: 0;
|
||||
@@ -493,7 +493,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.personal-wechat-generation-track > span {
|
||||
.personal-wechat-generation-track>span {
|
||||
width: 38%;
|
||||
animation: personal-wechat-generation-slide 1.15s ease-in-out infinite;
|
||||
}
|
||||
@@ -538,7 +538,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> span {
|
||||
>span {
|
||||
color: var(--wxex-text-muted);
|
||||
font: 10px/16px var(--wxex-font);
|
||||
white-space: nowrap;
|
||||
@@ -622,6 +622,7 @@
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
color: var(--wxex-text-primary);
|
||||
font-size: 14px;
|
||||
@@ -754,7 +755,7 @@
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
> li {
|
||||
>li {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
@@ -787,14 +788,14 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.personal-wechat-steps > li.is-current .personal-wechat-step-number,
|
||||
.personal-wechat-steps > li.is-active .personal-wechat-step-number {
|
||||
.personal-wechat-steps>li.is-current .personal-wechat-step-number,
|
||||
.personal-wechat-steps>li.is-active .personal-wechat-step-number {
|
||||
border-color: var(--wxex-brand);
|
||||
background: var(--wxex-brand-soft);
|
||||
color: var(--wxex-brand);
|
||||
}
|
||||
|
||||
.personal-wechat-steps > li.is-complete .personal-wechat-step-number {
|
||||
.personal-wechat-steps>li.is-complete .personal-wechat-step-number {
|
||||
border-color: color-mix(in srgb, var(--wxex-success) 35%, transparent);
|
||||
background: color-mix(in srgb, var(--wxex-success) 12%, var(--wxex-bg-elevated));
|
||||
color: var(--wxex-success);
|
||||
@@ -805,12 +806,12 @@
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
|
||||
> strong {
|
||||
>strong {
|
||||
color: var(--wxex-text-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
> p {
|
||||
>p {
|
||||
margin: 0;
|
||||
color: var(--wxex-text-secondary);
|
||||
font-size: 11px;
|
||||
@@ -842,7 +843,7 @@
|
||||
gap: 6px;
|
||||
max-width: 360px;
|
||||
|
||||
> div:first-child {
|
||||
>div:first-child {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--wxex-text-muted);
|
||||
@@ -858,7 +859,7 @@
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--wxex-brand) 14%, transparent);
|
||||
|
||||
> span {
|
||||
>span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
@@ -871,7 +872,7 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
|
||||
> span {
|
||||
>span {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
@@ -918,7 +919,7 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
> p {
|
||||
>p {
|
||||
margin: 8px 0;
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 10px;
|
||||
@@ -935,7 +936,7 @@
|
||||
background: var(--wxex-border);
|
||||
}
|
||||
|
||||
dl > div {
|
||||
dl>div {
|
||||
min-width: 0;
|
||||
background: var(--wxex-bg-main);
|
||||
padding: 7px 9px;
|
||||
@@ -978,9 +979,9 @@
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.personal-wechat-composer-editor > .personal-wechat-text-editor,
|
||||
.personal-wechat-composer-editor > .personal-wechat-voice-editor,
|
||||
.personal-wechat-composer-editor > .personal-wechat-file-editor {
|
||||
.personal-wechat-composer-editor>.personal-wechat-text-editor,
|
||||
.personal-wechat-composer-editor>.personal-wechat-voice-editor,
|
||||
.personal-wechat-composer-editor>.personal-wechat-file-editor {
|
||||
box-sizing: border-box;
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -992,7 +993,7 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.personal-wechat-composer-editor > .personal-wechat-text-editor {
|
||||
.personal-wechat-composer-editor>.personal-wechat-text-editor {
|
||||
height: 100%;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
}
|
||||
@@ -1002,7 +1003,7 @@
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.personal-wechat-text-editor > span {
|
||||
.personal-wechat-text-editor>span {
|
||||
justify-self: end;
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 10px;
|
||||
@@ -1014,16 +1015,16 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.personal-wechat-composer-editor > .personal-wechat-file-editor {
|
||||
.personal-wechat-composer-editor>.personal-wechat-file-editor {
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.personal-wechat-composer-editor > .personal-wechat-voice-editor {
|
||||
.personal-wechat-composer-editor>.personal-wechat-voice-editor {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.personal-wechat-file-editor > div {
|
||||
.personal-wechat-file-editor>div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1054,7 +1055,7 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.personal-wechat-voice-heading > span {
|
||||
.personal-wechat-voice-heading>span {
|
||||
color: var(--wxex-text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -1089,13 +1090,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.personal-wechat-composer-footer > span {
|
||||
.personal-wechat-composer-footer>span {
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
|
||||
&.is-success {
|
||||
color: var(--wxex-success);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
color: var(--wxex-danger);
|
||||
}
|
||||
@@ -1118,15 +1120,18 @@
|
||||
.personal-wechat-capabilities {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.personal-wechat-chat-header,
|
||||
.personal-wechat-message-list,
|
||||
.personal-wechat-setup,
|
||||
.personal-wechat-composer {
|
||||
padding-inline: 14px;
|
||||
}
|
||||
|
||||
.personal-wechat-generated-result {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.personal-wechat-composer-editor {
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
@@ -1550,6 +1555,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-bubble .image-content {
|
||||
display: block;
|
||||
max-width: min(260px, 46vw);
|
||||
@@ -1608,6 +1614,7 @@
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
@@ -1846,8 +1853,9 @@
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
.chat-content-search-count,
|
||||
.chat-header-text-action > span {
|
||||
.chat-header-text-action>span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1956,7 +1964,7 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.database-login-promises > div {
|
||||
.database-login-promises>div {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2014,7 +2022,7 @@
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
> p:not(.database-login-eyebrow) {
|
||||
>p:not(.database-login-eyebrow) {
|
||||
margin: 7px 0 18px;
|
||||
color: var(--login-muted);
|
||||
font-size: 12px;
|
||||
@@ -2035,7 +2043,7 @@
|
||||
gap: 10px;
|
||||
color: #35423d;
|
||||
|
||||
> span {
|
||||
>span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: 0 0 20px;
|
||||
@@ -2154,7 +2162,7 @@
|
||||
border-top: 1px solid #e2e7e4;
|
||||
}
|
||||
|
||||
.database-login-diagnostics > div {
|
||||
.database-login-diagnostics>div {
|
||||
min-height: 43px;
|
||||
display: grid;
|
||||
grid-template-columns: 112px 1fr;
|
||||
@@ -2162,7 +2170,7 @@
|
||||
border-bottom: 1px solid #e2e7e4;
|
||||
}
|
||||
|
||||
.database-login-diagnostics > div:last-child {
|
||||
.database-login-diagnostics>div:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -2261,7 +2269,7 @@
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.database-login-auto > a {
|
||||
.database-login-auto>a {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
color: var(--login-primary);
|
||||
@@ -2305,7 +2313,7 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.database-login-field > label {
|
||||
.database-login-field>label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
@@ -2432,7 +2440,8 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px), (max-height: 620px) {
|
||||
@media (max-width: 900px),
|
||||
(max-height: 620px) {
|
||||
.database-login-brand {
|
||||
padding: 30px;
|
||||
}
|
||||
@@ -2503,6 +2512,7 @@
|
||||
code {
|
||||
color: var(--text-secondary, #68766f);
|
||||
}
|
||||
|
||||
code {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -2512,4 +2522,4 @@
|
||||
|
||||
.database-account-status {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user