mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
fix: 增加手动复制退群信息 以及设置页面文案
This commit is contained in:
@@ -22,7 +22,9 @@ import type { Contact } from '../../../../shared/types'
|
||||
import {
|
||||
GROUP_EXIT_NOTIFICATION_TEMPLATE,
|
||||
GROUP_EXIT_NOTIFICATION_TEMPLATE_MAX_LENGTH,
|
||||
renderGroupExitMonitorNotification,
|
||||
validateGroupExitNotificationTemplate,
|
||||
type GroupExitMonitorEvent,
|
||||
type GroupExitMonitorState
|
||||
} from '../../../../shared/group-exit-monitor'
|
||||
import type { PersonalWechatSendCapability } from '../../../../shared/personal-wechat'
|
||||
@@ -465,6 +467,8 @@ export function GroupExitMonitorWorkspace({
|
||||
const [selectedRoomId, setSelectedRoomId] = React.useState('all')
|
||||
const [view, setView] = React.useState<'events' | 'manage'>('events')
|
||||
const [resendingEventId, setResendingEventId] = React.useState<string | null>(null)
|
||||
const [copiedEventId, setCopiedEventId] = React.useState('')
|
||||
const copyResetTimer = React.useRef<number | null>(null)
|
||||
const [manageKeyword, setManageKeyword] = React.useState('')
|
||||
const [manageFilter, setManageFilter] = React.useState<GroupSelectionFilter>('all')
|
||||
const [selectedManageRoomIds, setSelectedManageRoomIds] = React.useState<Set<string>>(
|
||||
@@ -522,6 +526,13 @@ export function GroupExitMonitorWorkspace({
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (copyResetTimer.current !== null) window.clearTimeout(copyResetTimer.current)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const groupOptions = React.useMemo(() => {
|
||||
const optionMap = new Map<string, { roomId: string; groupName: string; count: number }>()
|
||||
for (const event of state.events) {
|
||||
@@ -617,6 +628,28 @@ export function GroupExitMonitorWorkspace({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动复制退群信息:按“管理群聊”里保存的模板拼接当前事件,只写系统剪贴板,
|
||||
* 不触发任何微信发送,用户自行决定粘贴到哪里。
|
||||
*/
|
||||
const copyEventNotification = async (event: GroupExitMonitorEvent): Promise<void> => {
|
||||
const api = typeof window !== 'undefined' ? window.api : undefined
|
||||
const text = renderGroupExitMonitorNotification(event, notificationTemplate)
|
||||
setError('')
|
||||
try {
|
||||
const result = api && typeof api.copyText === 'function' ? await api.copyText(text) : null
|
||||
if (!result?.success) throw new Error(result?.error || '复制失败,请重试')
|
||||
setCopiedEventId(event.id)
|
||||
if (copyResetTimer.current !== null) window.clearTimeout(copyResetTimer.current)
|
||||
copyResetTimer.current = window.setTimeout(() => {
|
||||
copyResetTimer.current = null
|
||||
setCopiedEventId('')
|
||||
}, 2_000)
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason))
|
||||
}
|
||||
}
|
||||
|
||||
const saveGroups = async (): Promise<void> => {
|
||||
const api = window.api
|
||||
if (saving || !dbReady || typeof api.setGroupExitMonitorGroups !== 'function') return
|
||||
@@ -868,9 +901,20 @@ export function GroupExitMonitorWorkspace({
|
||||
<div className="exit-monitor-event-body">
|
||||
<div className="exit-monitor-event-heading">
|
||||
<strong>{event.message}</strong>
|
||||
<time dateTime={new Date(event.detectedAt).toISOString()}>
|
||||
{formatDetectedAt(event.detectedAt)}
|
||||
</time>
|
||||
<div className="exit-monitor-event-side">
|
||||
<time dateTime={new Date(event.detectedAt).toISOString()}>
|
||||
{formatDetectedAt(event.detectedAt)}
|
||||
</time>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="exit-monitor-event-copy"
|
||||
onClick={() => void copyEventNotification(event)}
|
||||
aria-label={`复制退群信息:${event.message}`}
|
||||
>
|
||||
{copiedEventId === event.id ? '已复制' : '复制退群信息'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{event.previousCount > event.currentCount ? (
|
||||
<p className="exit-monitor-event-count">
|
||||
|
||||
@@ -36,6 +36,10 @@ function formatBytes(value: number): string {
|
||||
return `${(value / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
const REPOSITORY_URL = 'https://github.com/Wxw-Gu/TraceMemo'
|
||||
// README 就是仓库首页,跳转后需要的正是「交流与反馈」这一节(含群二维码)。
|
||||
const GROUP_README_URL = `${REPOSITORY_URL}#-交流与反馈`
|
||||
|
||||
const capabilityLabel: Record<PersonalWechatSendCapability['status'], string> = {
|
||||
unsupported: '暂不支持',
|
||||
unconfigured: '尚未配置',
|
||||
@@ -507,6 +511,23 @@ export function PersonalWechatSendPage({
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 className="settings-section-heading">发送能力授权</h2>
|
||||
<section className="settings-card grid gap-2">
|
||||
<p>发送能力属授权制,需要联系群主。请先加入交流群,然后在群内添加群主申请授权。</p>
|
||||
<p className="settings-footnote">
|
||||
进群请点击{' '}
|
||||
<a
|
||||
className="text-primary hover:underline"
|
||||
href={GROUP_README_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
这里
|
||||
</a>{' '}
|
||||
跳转。
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
) : isMac ? (
|
||||
<>
|
||||
|
||||
@@ -337,6 +337,19 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.exit-monitor-event-side {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.exit-monitor-event-side .exit-monitor-event-copy {
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.exit-monitor-event-count {
|
||||
display: block;
|
||||
margin: 5px 0 0;
|
||||
@@ -707,6 +720,13 @@
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.exit-monitor-event-side {
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.exit-monitor-event-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ReactElement } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { GroupExitMonitorWorkspace } from '../../src/renderer/src/features/group-exit-monitor/GroupExitMonitorWorkspace'
|
||||
import type { GroupExitMonitorState } from '../../src/shared/group-exit-monitor'
|
||||
import { formatGroupExitMonitorTime } from '../../src/shared/group-exit-monitor'
|
||||
import type { Contact } from '../../src/shared/types'
|
||||
import { TooltipProvider } from '../../src/renderer/src/components/ui'
|
||||
|
||||
@@ -327,6 +328,67 @@ describe('GroupExitMonitorWorkspace', () => {
|
||||
expect(screen.queryByRole('heading', { name: '退群监测模板' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('copies the exit notice built from the template saved on the management page', async () => {
|
||||
const user = userEvent.setup()
|
||||
const template = [
|
||||
'[退群监测]',
|
||||
'用户: {user}',
|
||||
'群备注: {groupRemark}',
|
||||
'微信号: {wxid}',
|
||||
'人数: {previousCount}->{currentCount}',
|
||||
'退群时间: {time}'
|
||||
].join('\n')
|
||||
const copyText = vi.fn().mockResolvedValue({ success: true })
|
||||
window.api = {
|
||||
getGroupExitMonitorState: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ...state, notificationTemplate: template }),
|
||||
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
||||
copyText,
|
||||
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
||||
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(state)
|
||||
} as typeof window.api
|
||||
|
||||
renderWorkspace(<GroupExitMonitorWorkspace dbReady />)
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '复制退群信息:小艾退出了研发群' })
|
||||
)
|
||||
|
||||
await waitFor(() => expect(copyText).toHaveBeenCalledTimes(1))
|
||||
expect(copyText).toHaveBeenCalledWith(
|
||||
[
|
||||
'[退群监测]',
|
||||
'用户: 小艾微信名',
|
||||
'群备注: 小艾群备注',
|
||||
'微信号: wxid_alice',
|
||||
'人数: 240->239',
|
||||
`退群时间: ${formatGroupExitMonitorTime(state.events[0].detectedAt)}`
|
||||
].join('\n')
|
||||
)
|
||||
expect(await screen.findByText('已复制')).toBeVisible()
|
||||
})
|
||||
|
||||
it('reports a clipboard failure and keeps the event list intact', async () => {
|
||||
const user = userEvent.setup()
|
||||
const copyText = vi.fn().mockResolvedValue({ success: false, error: '剪贴板不可用' })
|
||||
window.api = {
|
||||
getGroupExitMonitorState: vi.fn().mockResolvedValue(state),
|
||||
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
||||
copyText,
|
||||
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
||||
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(state)
|
||||
} as typeof window.api
|
||||
|
||||
renderWorkspace(<GroupExitMonitorWorkspace dbReady />)
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '复制退群信息:小艾退出了研发群' })
|
||||
)
|
||||
|
||||
expect(await screen.findByText('剪贴板不可用')).toBeVisible()
|
||||
expect(screen.getByText('小艾退出了研发群')).toBeVisible()
|
||||
expect(screen.queryByText('已复制')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('leaves every monitoring checkbox off for a new installation', async () => {
|
||||
const user = userEvent.setup()
|
||||
const newInstallState: GroupExitMonitorState = {
|
||||
|
||||
@@ -198,4 +198,14 @@ describe('PersonalWechatSendPage on Windows', () => {
|
||||
expect(await screen.findByRole('spinbutton', { name: '微信发送能力端口' })).toBeVisible()
|
||||
expect(screen.queryByRole('switch', { name: '保留 OneBot 进程' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the authorization notice with a README link on the Windows WeChat send page', async () => {
|
||||
render(<PersonalWechatSendPage onNotice={vi.fn()} />)
|
||||
|
||||
expect(await screen.findByText('发送能力授权')).toBeVisible()
|
||||
expect(screen.getByText('发送能力属授权制,需要联系群主。请先加入交流群,然后在群内添加群主申请授权。')).toBeVisible()
|
||||
const readmeLink = screen.getByRole('link', { name: '这里' })
|
||||
expect(readmeLink).toHaveAttribute('href', 'https://github.com/Wxw-Gu/TraceMemo#-交流与反馈')
|
||||
expect(readmeLink).toHaveAttribute('target', '_blank')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user