feat: 重构README 新增引导功能

This commit is contained in:
Wxw-Gu
2026-07-31 11:26:23 +08:00
parent 77adc744e0
commit f0601cdc85
16 changed files with 1156 additions and 164 deletions
+53 -1
View File
@@ -20,6 +20,7 @@ import { AiModelConfig, useGroupReportGeneration } from './hooks/useGroupReportG
import { SummaryDateRange, SummaryMessageType } from './utils/group-report'
import { Contact, Message } from '../../shared/types'
import { DatabaseConnectionMode, DatabaseConnectionPage } from './components/DatabaseConnectionPage'
import { FirstUseWelcome } from './components/FirstUseWelcome'
import { ExportWorkspace } from './components/export/ExportWorkspace'
import { AISearchWorkspace } from './components/search/AISearchWorkspace'
import type { ExportJobProgress, ExportRequest, ExportTaskRecord } from '../../shared/export'
@@ -39,7 +40,8 @@ interface SelfInfo {
accountRoot: string
}
const MAC_KEY_FAQ_URL = 'https://github.com/hicccc77/WeFlow/blob/main/docs/MAC-KEY-FAQ.md'
const MAC_KEY_FAQ_URL = 'https://github.com/Wxw-Gu/WechatExplorer/blob/main/docs/mac-disable-sip.md'
const FIRST_USE_WELCOME_SEEN_KEY = 'wxe_first_use_welcome_seen'
const MESSAGE_MONITOR_DEBOUNCE_MS = 8000
const INITIAL_MESSAGE_COUNT = 20
const MESSAGE_PAGE_SIZE = 100
@@ -256,6 +258,7 @@ function App(): React.ReactElement {
const [bootState, setBootState] = useState<'loading' | 'connecting' | 'login'>('loading')
const [autoConnectSource, setAutoConnectSource] = useState<'env' | 'saved' | null>(null)
const [startupProgress, setStartupProgress] = useState<StartupProgress | null>(null)
const [showFirstUseWelcome, setShowFirstUseWelcome] = useState(false)
const [appearanceSettings, setAppearanceSettings] = React.useState<{
theme: 'system' | 'light' | 'dark'
compactMode: boolean
@@ -728,6 +731,7 @@ function App(): React.ReactElement {
})
setIsDatabaseConnected(true)
setBootState('login')
maybeShowFirstUseWelcome()
window.setTimeout(() => {
setStartupProgress(null)
}, 500)
@@ -1222,6 +1226,45 @@ function App(): React.ReactElement {
setActivePage('settings')
}
const dismissFirstUseWelcome = (): void => {
try {
localStorage.setItem(FIRST_USE_WELCOME_SEEN_KEY, '1')
} catch {
// The welcome prompt is optional and should not interrupt normal use.
}
setShowFirstUseWelcome(false)
}
const maybeShowFirstUseWelcome = (): void => {
try {
if (localStorage.getItem(FIRST_USE_WELCOME_SEEN_KEY) === '1') return
} catch {
// If localStorage is unavailable, still show the one-time prompt for this session.
}
setShowFirstUseWelcome(true)
}
const openFirstUseSearch = (): void => {
dismissFirstUseWelcome()
setActivePage('search')
}
const openFirstUseReport = (): void => {
dismissFirstUseWelcome()
setReportWorkspaceView('configure')
setSelectedReportId(null)
setActivePage('report')
}
const openFirstUseAISettings = (): void => {
dismissFirstUseWelcome()
openModelSettings()
}
const openFirstUseGuide = (): void => {
setShowFirstUseWelcome(true)
}
const openReport = (reportId: string): void => {
setSelectedReportId(reportId)
setReportWorkspaceView('result')
@@ -1651,10 +1694,19 @@ function App(): React.ReactElement {
dbReady={isDatabaseConnected}
onPageChange={handlePageChange}
onOpenSettings={openSettings}
onOpenGuide={openFirstUseGuide}
appearanceTheme={appearanceSettings.theme}
compactMode={appearanceSettings.compactMode}
>
{reportNotice && <div className="app-toast">{reportNotice}</div>}
{showFirstUseWelcome && (
<FirstUseWelcome
onDismiss={dismissFirstUseWelcome}
onOpenSearch={openFirstUseSearch}
onOpenReport={openFirstUseReport}
onOpenAISettings={openFirstUseAISettings}
/>
)}
{renderCurrentWorkspace()}
</AppShell>
)
@@ -1,5 +1,8 @@
import React from 'react'
const GUIDE_URL =
'https://github.com/Wxw-Gu/WechatExplorer/blob/main/docs/user-guide/getting-started.md'
export type DatabaseConnectionMode = 'automatic' | 'manual'
export type DatabaseConnectionStatusKind = 'normal' | 'success' | 'error'
@@ -116,9 +119,9 @@ export function DatabaseConnectionPage({
<LineIcon name="database" />
</div>
<h1>WechatExplorer</h1>
<p className="database-login-tagline"></p>
<p className="database-login-tagline"> AI </p>
<p className="database-login-description">
使 AI
</p>
<div className="database-login-promises">
<div>
@@ -131,7 +134,7 @@ export function DatabaseConnectionPage({
</div>
<div>
<LineIcon name="cloud" />
<span></span>
<span>AI </span>
</div>
</div>
</div>
@@ -140,6 +143,42 @@ export function DatabaseConnectionPage({
<section className="database-login-workspace" aria-label="数据库连接">
<div className="database-login-panel">
<div className="database-login-start">
<p className="database-login-eyebrow">使</p>
<h2></h2>
<p> 3 </p>
<ol>
<li>
<span>1</span>
<div>
<strong></strong>
<small></small>
</div>
</li>
<li>
<span>2</span>
<div>
<strong></strong>
<small></small>
</div>
</li>
<li>
<span>3</span>
<div>
<strong></strong>
<small></small>
</div>
</li>
</ol>
<a
className="database-login-guide-link"
href={GUIDE_URL}
target="_blank"
rel="noreferrer"
>
5
</a>
</div>
<div className="database-login-tabs" role="tablist" aria-label="连接方式">
<button
type="button"
@@ -148,16 +187,16 @@ export function DatabaseConnectionPage({
className={mode === 'automatic' ? 'active' : ''}
onClick={() => onModeChange('automatic')}
>
</button>
<button
type="button"
role="tab"
aria-selected={mode === 'manual'}
className={mode === 'manual' ? 'active' : ''}
className={`database-login-manual-tab ${mode === 'manual' ? 'active' : ''}`}
onClick={() => onModeChange('manual')}
>
</button>
</div>
@@ -219,20 +258,31 @@ export function DatabaseConnectionPage({
onClick={onAutoGetKey}
disabled={isFetching}
>
{isFetching
? '正在获取密钥…'
: statusKind === 'error'
? '重新检测'
: '自动获取密钥'}
{isFetching ? '正在获取密钥…' : statusKind === 'error' ? '重新检测' : '开始获取'}
</button>
{showMacKeyFaq && (
<p className="database-login-platform-note">
{isMac ? (
<>
macOS SIP{' '}
<a href={macKeyFaqUrl} target="_blank" rel="noreferrer">
</a>
</>
) : (
'Windows 已完整支持,不需要关闭 SIP。'
)}
</p>
{showMacKeyFaq && isMac && (
<a href={macKeyFaqUrl} target="_blank" rel="noreferrer">
·
</a>
)}
</div>
) : (
<div className="database-login-manual" role="tabpanel">
<p className="database-login-manual-note">
使
</p>
<div className="database-login-field">
<label htmlFor="database-login-key"></label>
<div className="database-login-key-input">
@@ -0,0 +1,78 @@
import React from 'react'
interface FirstUseWelcomeProps {
onDismiss: () => void
onOpenSearch: () => void
onOpenReport: () => void
onOpenAISettings: () => void
}
const GUIDE_URL =
'https://github.com/Wxw-Gu/WechatExplorer/blob/main/docs/user-guide/getting-started.md'
export function FirstUseWelcome({
onDismiss,
onOpenSearch,
onOpenReport,
onOpenAISettings
}: FirstUseWelcomeProps): React.ReactElement {
return (
<div className="first-use-welcome-overlay" role="presentation">
<section
className="first-use-welcome"
role="dialog"
aria-modal="true"
aria-labelledby="first-use-welcome-title"
>
<button
type="button"
className="first-use-welcome-close"
onClick={onDismiss}
aria-label="关闭欢迎提示"
>
×
</button>
<div className="first-use-welcome-mark" aria-hidden="true">
</div>
<p className="first-use-welcome-eyebrow"></p>
<h2 id="first-use-welcome-title"></h2>
<p className="first-use-welcome-lead">
AI
</p>
<button type="button" className="first-use-welcome-feature" onClick={onOpenReport}>
<span className="first-use-welcome-feature-icon" aria-hidden="true">
</span>
<span className="first-use-welcome-feature-copy">
<strong> AI </strong>
<small></small>
</span>
<span className="first-use-welcome-feature-arrow" aria-hidden="true">
</span>
</button>
<div className="first-use-welcome-secondary-actions">
<button type="button" onClick={onDismiss}>
</button>
<button type="button" onClick={onOpenSearch}>
</button>
</div>
<div className="first-use-welcome-footer">
<span> AI</span>
<button type="button" onClick={onOpenAISettings}>
AI
</button>
<a href={GUIDE_URL} target="_blank" rel="noreferrer">
使
</a>
</div>
</section>
</div>
)
}
@@ -17,6 +17,7 @@ interface AppShellProps {
dbReady: boolean
onPageChange: (page: AppPage) => void
onOpenSettings: () => void
onOpenGuide: () => void
appearanceTheme?: 'system' | 'light' | 'dark'
compactMode?: boolean
children: React.ReactNode
@@ -36,6 +37,7 @@ export function AppShell({
dbReady,
onPageChange,
onOpenSettings,
onOpenGuide,
appearanceTheme = 'system',
compactMode = false,
children
@@ -47,6 +49,19 @@ export function AppShell({
<aside className="app-primary-rail">
<BrandLogo />
<PrimaryNavigation activePage={activePage} onPageChange={onPageChange} />
<button
type="button"
className="app-guide-launcher"
onClick={onOpenGuide}
title="重新打开新手引导"
>
<span className="app-guide-launcher-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path d="M12 3 14.2 9.8 21 12l-6.8 2.2L12 21l-2.2-6.8L3 12l6.8-2.2L12 3Z" />
</svg>
</span>
<span className="app-guide-launcher-label"></span>
</button>
<div className="app-rail-account">
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} compact onClick={onOpenSettings} />
</div>
@@ -7,7 +7,7 @@ export interface NavigationItem {
export const PRIMARY_NAV_ITEMS: NavigationItem[] = [
{ id: 'archive', label: '档案' },
{ id: 'search', label: '检索' },
{ id: 'search', label: '问问微信' },
{ id: 'report', label: '日报' },
{ id: 'agent-hub', label: 'Agent' },
{ id: 'export', label: '导出' },
@@ -641,7 +641,7 @@ export function AISearchWorkspace({
<header className="ai-search-header">
<div>
<span className="ai-search-kicker">WechatExplorer · LOCAL INTELLIGENCE</span>
<h1>AI </h1>
<h1></h1>
<p></p>
</div>
<div className="ai-search-header-actions">
+129 -1
View File
@@ -1178,15 +1178,106 @@
}
.database-login-workspace {
box-sizing: border-box;
display: grid;
place-items: center;
align-items: start;
justify-items: center;
min-width: 0;
min-height: 0;
overflow-y: auto;
padding: 48px clamp(40px, 7vw, 96px);
background: #fff;
}
.database-login-panel {
width: min(520px, 100%);
margin: auto 0;
}
.database-login-start {
margin-bottom: 22px;
h2 {
margin: 0;
color: var(--login-text);
font-size: 22px;
line-height: 29px;
letter-spacing: -0.02em;
}
> p:not(.database-login-eyebrow) {
margin: 7px 0 18px;
color: var(--login-muted);
font-size: 12px;
line-height: 18px;
}
ol {
display: grid;
gap: 9px;
margin: 0;
padding: 0;
list-style: none;
}
li {
display: flex;
align-items: flex-start;
gap: 10px;
color: #35423d;
> span {
width: 20px;
height: 20px;
flex: 0 0 20px;
display: grid;
place-items: center;
border-radius: 50%;
color: var(--login-primary-dark);
background: #e2f0eb;
font-size: 11px;
font-weight: 700;
}
strong,
small {
display: block;
}
strong {
font-size: 12px;
line-height: 17px;
}
small {
margin-top: 1px;
color: var(--login-muted);
font-size: 11px;
line-height: 16px;
}
}
}
.database-login-eyebrow {
margin: 0 0 5px;
color: var(--login-primary);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.database-login-guide-link {
display: inline-block;
margin-top: 15px;
color: var(--login-primary);
font-size: 11px;
line-height: 17px;
text-decoration: none;
}
.database-login-guide-link:hover {
text-decoration: underline;
}
.database-login-tabs {
@@ -1217,6 +1308,16 @@
box-shadow: 0 1px 3px rgba(24, 28, 27, 0.08);
}
.database-login-tabs .database-login-manual-tab {
color: #7a8580;
font-size: 11px;
font-weight: 500;
}
.database-login-tabs .database-login-manual-tab.active {
color: #58645f;
}
.database-login-state-card {
padding: 18px;
border: 1px solid var(--login-border);
@@ -1396,6 +1497,33 @@
text-decoration: none;
}
.database-login-platform-note {
margin: 12px 0 0;
color: #77817d;
font-size: 11px;
line-height: 17px;
text-align: center;
}
.database-login-platform-note a {
color: var(--login-primary);
text-decoration: none;
}
.database-login-platform-note a:hover {
text-decoration: underline;
}
.database-login-manual-note {
margin: 0 0 20px;
padding: 10px 12px;
border-radius: 7px;
color: #65706b;
background: var(--login-surface-low);
font-size: 11px;
line-height: 17px;
}
.database-login-manual {
padding-top: 2px;
}
+50
View File
@@ -144,6 +144,56 @@ body {
-webkit-app-region: no-drag;
}
.app-guide-launcher {
box-sizing: border-box;
width: 100%;
min-height: 52px;
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
margin: 0 0 8px;
padding: 5px 0;
border: 0;
border-radius: var(--wxex-radius-md);
color: var(--wxex-text-secondary);
background: transparent;
cursor: pointer;
font-family: var(--wxex-font);
-webkit-app-region: no-drag;
&:hover {
color: var(--wxex-brand);
background: var(--wxex-brand-soft);
}
}
.app-guide-launcher-icon {
width: 22px;
height: 22px;
display: grid;
place-items: center;
svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
}
.app-guide-launcher-label {
font-size: 11px;
line-height: 15px;
font-weight: 600;
white-space: nowrap;
}
.account-summary {
box-sizing: border-box;
width: 100%;
+238
View File
@@ -137,6 +137,244 @@
transition: width 0.18s ease-out;
}
/* 首次连接成功后的下一步引导 */
.first-use-welcome-overlay {
position: fixed;
inset: 0;
z-index: 1200;
display: grid;
place-items: center;
padding: 24px;
background: rgba(16, 27, 23, 0.42);
backdrop-filter: blur(4px);
animation: settings-fade-in 0.16s ease-out;
}
.first-use-welcome {
position: relative;
width: min(520px, 100%);
padding: 34px;
border: 1px solid rgba(203, 222, 214, 0.9);
border-radius: 18px;
background: #fff;
box-shadow: 0 24px 70px rgba(16, 35, 28, 0.24);
animation: settings-pop-in 0.18s ease-out;
}
.first-use-welcome-close {
position: absolute;
top: 14px;
right: 16px;
width: 30px;
height: 30px;
border: 0;
border-radius: 50%;
color: #7c8882;
background: transparent;
cursor: pointer;
font-size: 23px;
line-height: 1;
}
.first-use-welcome-close:hover {
color: #26342d;
background: #f0f4f1;
}
.first-use-welcome-mark {
width: 42px;
height: 42px;
display: grid;
place-items: center;
margin-bottom: 18px;
border-radius: 13px;
color: #fff;
background: linear-gradient(135deg, #176b57, #42a27f);
box-shadow: 0 7px 16px rgba(23, 107, 87, 0.22);
font-size: 22px;
}
.first-use-welcome-eyebrow {
margin: 0 0 5px;
color: #247a63;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
}
.first-use-welcome h2 {
margin: 0;
color: #1f2c26;
font-size: 25px;
line-height: 34px;
letter-spacing: -0.025em;
}
.first-use-welcome-lead {
margin: 8px 0 24px;
color: #66756d;
font-size: 13px;
line-height: 21px;
}
.first-use-welcome-actions {
display: grid;
gap: 9px;
}
.first-use-welcome-feature {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
width: 100%;
padding: 17px 16px;
border: 1px solid #247a63;
border-radius: 12px;
color: #fff;
background: linear-gradient(135deg, #247a63, #176b57);
cursor: pointer;
text-align: left;
transition:
border-color 0.15s ease,
background 0.15s ease,
transform 0.15s ease;
&:hover {
border-color: #00604c;
background: linear-gradient(135deg, #176b57, #00604c);
transform: translateY(-1px);
}
}
.first-use-welcome-feature-icon {
width: 34px;
height: 34px;
flex: 0 0 34px;
display: grid;
place-items: center;
border-radius: 10px;
color: #176b57;
background: #fff;
font-size: 18px;
}
.first-use-welcome-feature-copy {
min-width: 0;
flex: 1;
}
.first-use-welcome-feature-arrow {
flex: 0 0 auto;
font-size: 11px;
font-weight: 650;
white-space: nowrap;
}
.first-use-welcome-feature-copy strong,
.first-use-welcome-feature-copy small {
display: block;
}
.first-use-welcome-feature-copy strong {
font-size: 14px;
line-height: 20px;
}
.first-use-welcome-feature-copy small {
margin-top: 2px;
color: rgba(255, 255, 255, 0.76);
font-size: 11px;
line-height: 16px;
}
.first-use-welcome-secondary-actions {
display: flex;
gap: 22px;
margin: 16px 0 22px;
}
.first-use-welcome-secondary-actions button {
padding: 0;
border: 0;
color: #53645b;
background: transparent;
cursor: pointer;
font-size: 12px;
}
.first-use-welcome-secondary-actions button:hover {
color: #247a63;
text-decoration: underline;
}
.first-use-welcome-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 23px;
padding-top: 16px;
border-top: 1px solid #edf1ef;
span,
button,
a {
color: #68766f;
background: transparent;
font-size: 11px;
text-decoration: none;
}
button {
padding: 0;
border: 0;
cursor: pointer;
}
button:hover,
a:hover {
color: #247a63;
text-decoration: underline;
}
}
@media (max-width: 640px), (max-height: 560px) {
.first-use-welcome-overlay {
display: block;
overflow: auto;
padding: 18px;
}
.first-use-welcome {
width: auto;
margin: 0 auto;
padding: 26px 22px 22px;
}
.first-use-welcome h2 {
font-size: 22px;
line-height: 29px;
}
.first-use-welcome-feature {
align-items: flex-start;
flex-wrap: wrap;
gap: 10px;
}
.first-use-welcome-feature-arrow {
width: 100%;
margin-left: 44px;
}
.first-use-welcome-footer {
align-items: flex-start;
flex-direction: column;
gap: 9px;
}
}
/* 设置面板 */
.settings-overlay {
position: fixed;