feat(web): split the home page into quick share and spaces zones

Two side-by-side zones (stacked on narrow screens) with an icon and a
one-line positioning each: quick share is temporary, read-only and needs
no account; spaces are long-lived, multi-user and need a login. Shorter
copy, my shares listed under the share input, and an empty state for
spaces.
This commit is contained in:
justme committed 2026-10-14 22:42:06 +08:00
1 parent 0d31e3d058
commit d0aefb3d6e
5 files changed
+46 -64

No files matched your search

@@ -3,6 +3,14 @@
font-size: 13px;
}
.quiet {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.list {
margin: 0;
padding: 0;
+4 -2
View File
@@ -11,9 +11,11 @@ interface Props {
kind: 'channel' | 'share';
title: string;
onOpen: (c: ChannelInfo | Share) => void;
/** 标题只给屏幕阅读器(所在分区已有标题) */
quietTitle?: boolean;
}
export function ChannelList({ kind, title, onOpen }: Props) {
export function ChannelList({ kind, title, onOpen, quietTitle = false }: Props) {
const channels = useChannels((s) => s.list);
const forgetChannel = useChannels((s) => s.forget);
const shares = useShares((s) => s.list);
@@ -24,7 +26,7 @@ export function ChannelList({ kind, title, onOpen }: Props) {
return (
<section aria-label={title}>
<h2 className={styles.h}>{title}</h2>
<h2 className={`${styles.h} ${quietTitle ? styles.quiet : ''}`}>{title}</h2>
<ul className={styles.list}>
{list.map((c) => (
<li key={c.id} className={styles.row}>
+31 -21
View File
@@ -4,10 +4,12 @@ import { buildInfo } from '@/lib/buildInfo';
import type { ChannelInfo, Share } from '@/core-bridge/types';
import type { ClipboardPayload } from '@/lib/clipboard';
import { useGlobalPaste } from '@/lib/useGlobalPaste';
import { useChannels } from '@/store/channels';
import { useShares } from '@/store/shares';
import { solveCreationCaptcha } from '@/core-bridge/captcha';
import { errorMessage } from '@/core-bridge/errors';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { ThemeMenu } from '@/ui/ThemeMenu';
import { ChannelList } from './ChannelList';
import { CreateSheet } from './CreateSheet';
@@ -16,10 +18,10 @@ import styles from './Home.module.css';
const ScanSheet = lazy(() => import('@/features/scan/ScanSheet').then((m) => ({ default: m.ScanSheet })));
/** 初始状态:什么都没创建时看到的页面。核心是「快速分享」 */
/** 首页:左边「便捷分享」(临时、只读、对方无需账号),右边「空间」(长期、多人、登录使用) */
export function Home() {
const createDraft = useShares((s) => s.createDraft);
const shares = useShares((s) => s.list);
const channels = useChannels((s) => s.list);
const [createOpen, setCreateOpen] = useState(false);
const [scanOpen, setScanOpen] = useState(false);
const [busy, setBusy] = useState(false);
@@ -59,13 +61,21 @@ export function Home() {
return (
<div className={styles.page}>
<div className={styles.appearance}><ThemeMenu /></div>
<main className={`${styles.main} ${styles.dashboard} ${shares.length ? styles.hasShares : ''}`}>
<main className={`${styles.main} ${styles.dashboard}`}>
<header className={`${styles.head} ${styles.dashboardHead}`}>
<h1 className={styles.logo}>WebDrop</h1>
<p className={styles.sub}>在自己的设备之间快速传文件和文本,也可以分享给别人。</p>
<p className={styles.sub}>设备之间传文件,也能分享给别人。</p>
</header>
<div className={styles.primaryColumn}>
<section className={styles.zone} aria-labelledby="share-title">
<div className={styles.zoneHead}>
<span className={styles.zoneIcon}><Icon name="share" size={20} /></span>
<div>
<h2 id="share-title" className={styles.zoneTitle}>便捷分享</h2>
<p className={styles.zoneHint}>临时 · 只读 · 对方无需账号</p>
</div>
<span className={styles.zoneBadge}><Icon name="lock" size={13} /> 端到端加密</span>
</div>
<QuickShare
busy={busy}
text={shareText}
@@ -75,24 +85,24 @@ export function Home() {
onShare={share}
/>
{failedMessage ? <p role="alert" className={styles.error}>创建分享失败:{failedMessage}</p> : null}
<ChannelList kind="share" title="我的分享" onOpen={open} />
</section>
<section className={styles.channels} aria-labelledby="channels-title">
<div className={styles.sectionHead}>
<h2 id="channels-title">空间工作区</h2>
<p>同一账号跨设备使用,也可邀请其他账号共同使用频道与文件。</p>
<section className={styles.zone} aria-labelledby="space-title">
<div className={styles.zoneHead}>
<span className={styles.zoneIcon}><Icon name="folder" size={20} /></span>
<div>
<h2 id="space-title" className={styles.zoneTitle}>空间</h2>
<p className={styles.zoneHint}>长期 · 多人 · 登录使用</p>
</div>
<div className={styles.actions}>
<Button icon="edit" onClick={() => setCreateOpen(true)}>新建空间</Button>
<Button icon="scan" onClick={() => setScanOpen(true)}>加入空间</Button>
</div>
<ChannelList kind="channel" title="我的空间" onOpen={open} />
<p className={styles.note}>空间密钥按账号加密分发,登录其他设备后可自动恢复。已授权成员可读取空间历史。</p>
</section>
</div>
{shares.length ? <aside className={styles.secondaryColumn} aria-label="我的分享"><ChannelList kind="share" title="我的分享" onOpen={open} /></aside> : null}
</div>
<ChannelList kind="channel" title="我的空间" onOpen={open} quietTitle />
{channels.length === 0 ? <p className={styles.empty}>还没有空间。</p> : null}
<div className={styles.actions}>
<Button icon="plus" onClick={() => setCreateOpen(true)}>新建空间</Button>
<Button icon="scan" onClick={() => setScanOpen(true)}>加入空间</Button>
</div>
</section>
</main>
<aside className={styles.buildInfo} aria-label="构建信息">
@@ -14,36 +14,6 @@
background: var(--busy-soft);
}
.head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
}
.title {
margin: 0;
font-size: 18px;
}
.hint {
margin: var(--space-1) 0 0;
color: var(--text-dim);
font-size: 13px;
}
.tag {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
background: var(--ok-soft);
color: var(--ok);
font-size: 12px;
}
.editor {
width: 100%;
min-height: 112px;
+3 -11
View File
@@ -14,7 +14,7 @@ interface Props {
onShare: (payload: ClipboardPayload) => Promise<boolean>;
}
/** 临时分享入口:先收集文本和文件,确认后再建立草稿。 */
/** 便捷分享的输入区:先收集文本和文件,确认后再建立草稿。标题与说明由 Home 的分区头提供 */
export function QuickShare({ busy, text, files, onTextChange, onFilesChange, onShare }: Props) {
const touch = useMediaQuery('(pointer: coarse)');
const fileRef = useRef<HTMLInputElement>(null);
@@ -59,18 +59,10 @@ export function QuickShare({ busy, text, files, onTextChange, onFilesChange, onS
<input ref={galleryRef} type="file" accept="image/*,video/*" multiple hidden onChange={(event) => { pick(event.target.files); event.target.value = ''; }} />
<input ref={cameraRef} type="file" accept="image/*" capture="environment" hidden onChange={(event) => { pick(event.target.files); event.target.value = ''; }} />
<header className={styles.head}>
<div>
<h2 className={styles.title}>快速分享</h2>
<p className={styles.hint}>临时文本直接写在这里,也可以拖入文件。内容封存后只读。</p>
</div>
<span className={styles.tag}><Icon name="lock" size={13} /> 端到端加密</span>
</header>
<textarea
className={styles.editor}
aria-label="临时分享文本"
placeholder="输入或粘贴要分享的文本…"
placeholder="输入文字,或拖入文件"
value={text}
onChange={(event) => onTextChange(event.target.value)}
rows={4}
@@ -101,7 +93,7 @@ export function QuickShare({ busy, text, files, onTextChange, onFilesChange, onS
))}
</div>
<button type="button" className={styles.submit} disabled={busy || !canShare} onClick={() => void onShare({ files, text })}>
{busy ? '正在创建分享…' : '创建临时分享'}
{busy ? '创建中…' : '创建分享'}
</button>
</footer>
</section>