From b847916a5bd4cad226cd71f571dc7f11f6f9a908 Mon Sep 17 00:00:00 2001 From: JustMe Date: Fri, 16 Oct 2026 17:54:31 +0800 Subject: [PATCH] feat(web): group space settings into basic, content and join tabs --- .../channel/ChannelSettingsSheet.module.css | 99 +++++++++++++--- .../features/channel/ChannelSettingsSheet.tsx | 110 ++++++++++-------- 2 files changed, 147 insertions(+), 62 deletions(-) diff --git a/web/src/features/channel/ChannelSettingsSheet.module.css b/web/src/features/channel/ChannelSettingsSheet.module.css index 4b25c41..ee763c2 100644 --- a/web/src/features/channel/ChannelSettingsSheet.module.css +++ b/web/src/features/channel/ChannelSettingsSheet.module.css @@ -15,10 +15,72 @@ color: var(--text); } -.form { +.layout { + display: grid; + grid-template-columns: 148px minmax(0, 1fr); + gap: var(--space-6); + height: min(360px, 70dvh); +} + +.tabs { display: flex; flex-direction: column; + align-self: start; + gap: var(--space-1); +} + +.tabs button { + display: flex; + align-items: center; + gap: var(--space-3); + min-height: 40px; + padding: 0 var(--space-3); + border: 0; + border-radius: var(--radius-md); + background: transparent; + color: var(--text-dim); + font: inherit; + font-size: 14px; + text-align: left; + cursor: pointer; +} + +.tabs button:hover { + background: var(--accent-soft); + color: var(--text); +} + +.tabs button[aria-selected='true'] { + background: var(--accent-soft); + color: var(--text); + font-weight: 600; +} + +.content { + display: flex; + flex-direction: column; + gap: var(--space-3); + min-width: 0; + min-height: 0; +} + +.pane { + display: flex; + flex: 1; + flex-direction: column; gap: var(--space-4); + overflow-y: auto; +} + +.check { + display: flex; + align-items: center; + gap: var(--space-2); + min-height: 36px; +} + +.check input { + accent-color: var(--accent); } .field { @@ -41,24 +103,35 @@ font: inherit; } -.check { - display: flex; - align-items: center; - gap: var(--space-2); - min-height: 36px; -} - -.check input { - accent-color: var(--accent); -} - .field input:disabled, .field select:disabled { opacity: 0.7; } +.footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-3); + padding-top: var(--space-3); + border-top: 1px solid var(--line); +} + +@media (width <= 720px) { + .layout { + grid-template-columns: minmax(0, 1fr); + gap: var(--space-4); + height: auto; + } + + .tabs { + flex-direction: row; + overflow-x: auto; + } +} + .note { - margin: 0; + margin: calc(-1 * var(--space-2)) 0 0; color: var(--text-dim); font-size: 12px; } diff --git a/web/src/features/channel/ChannelSettingsSheet.tsx b/web/src/features/channel/ChannelSettingsSheet.tsx index febc295..54c831b 100644 --- a/web/src/features/channel/ChannelSettingsSheet.tsx +++ b/web/src/features/channel/ChannelSettingsSheet.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import type { ChannelInfo, ChannelSettingsPatch } from '@/core-bridge/types'; import { Button } from '@/ui/Button'; +import { Icon, type IconName } from '@/ui/Icon'; import { Sheet } from '@/ui/Sheet'; import styles from './ChannelSettingsSheet.module.css'; @@ -8,6 +9,13 @@ import { useServerSettings } from '@/store/server'; import type { ServerInfoBody } from '@/core-bridge/generated/api'; import { ttlLabel, withCurrent } from '@/lib/settingOptions'; +type Tab = 'basic' | 'content' | 'join'; +const TABS: { id: Tab; label: string; icon: IconName }[] = [ + { id: 'basic', label: '基本', icon: 'edit' }, + { id: 'content', label: '内容', icon: 'history' }, + { id: 'join', label: '加入', icon: 'users' }, +]; + interface Props { channel: ChannelInfo; onClose: () => void; @@ -31,6 +39,7 @@ function SettingsForm({ channel, onClose, onSave, info }: Props & { info: Server const [joinPolicy, setJoinPolicy] = useState<'open' | 'invite'>(channel.joinPolicy!); const [joinPassword, setJoinPassword] = useState(''); const [clearPassword, setClearPassword] = useState(false); + const [tab, setTab] = useState('basic'); const [saving, setSaving] = useState(false); const [failed, setFailed] = useState(false); const ttlOptions = withCurrent(info.choices.ttl_seconds, ttlSeconds); @@ -51,56 +60,59 @@ function SettingsForm({ channel, onClose, onSave, info }: Props & { info: Server } } + const dirty = name.trim() !== (channel.name ?? '') || maxMembers !== channel.maxMembers || membersCanUpload !== channel.membersCanUpload + || ttlSeconds !== channel.ttlSeconds || fileMaxDownloads !== channel.maxDownloads || joinPolicy !== channel.joinPolicy || joinPassword !== '' || clearPassword; + const invalid = maxMembers < 1 || maxMembers > info.limits.max_members || ttlSeconds < info.limits.min_ttl_seconds || ttlSeconds > info.limits.max_ttl_seconds || Boolean(joinPassword && joinPassword.length < 8); + return ( - -
- - - - - - - - - - - - - {owner && channel.passwordRequired ? : null} -

成员与邀请请在右侧栏的「成员」页管理。

- {owner ? ( - <> - {failed ?

保存失败,请检查名称和设置后重试。

: null} - - - ) :

只有所有者可以修改空间设置。

} + +
+
+ {TABS.map((item) => )} +
+
+
item.id === tab)?.label}> + {tab === 'basic' ? <> + +

空间名称和设置对加入者及服务端可见。

+ +

成员与邀请请在右侧栏的「成员」页管理。

+ : null} + {tab === 'content' ? <> + + + + : null} + {tab === 'join' ? <> + + + {owner && channel.passwordRequired ? : null} +

空间主及受邀用户无需加入密码。修改规则不会移除已加入成员。

+ : null} +
+ {owner ? ( +
+ {failed ?

保存失败,请检查名称和设置后重试。

: null} + +
+ ) :

只有所有者可以修改空间设置。

} +
);