Compare commits

...

7 Commits

Author SHA1 Message Date
CrescentLeaf
fabdd192dd rename: default(Value -> State)
* 我没有想到这是已有的属性定义
2025-10-07 23:07:27 +08:00
CrescentLeaf
8d7ddd46be chore: make Preferences' lint happy 2025-10-07 23:05:34 +08:00
CrescentLeaf
4b91bc9dbb feat(wip): 群组设定 2025-10-07 22:32:16 +08:00
CrescentLeaf
80c6f0b7a7 chore: 移除调试代码 2025-10-07 22:32:11 +08:00
CrescentLeaf
4eff829a30 feat: 配置页面组件 2025-10-07 22:31:34 +08:00
CrescentLeaf
96ca578c70 ui: 调整 mdui-menu 圆角大小 2025-10-07 14:53:01 +08:00
CrescentLeaf
7a0110180d ui: 加大 mdui-menu 圆角大小 2025-10-07 14:23:29 +08:00
10 changed files with 227 additions and 4 deletions

View File

@@ -44,7 +44,6 @@ class Client {
}
static invoke(method: CallMethod, args: unknown = {}, timeout: number = 5000, refreshAndRetryLimit: number = 3, forceRefreshAndRetry: boolean = false): Promise<ApiCallbackMessage> {
// 在 未初始化 / 未建立连接且调用非可调用接口 的时候进行延迟
console.log(this.connected, method)
if (this.socket == null || (!this.connected && !CallableMethodBeforeAuth.includes(method))) {
return new Promise((reslove) => {
setTimeout(async () => reslove(await this.invoke(method, args, timeout)), 500)
@@ -95,6 +94,7 @@ class Client {
access_token: token
}, timeout, 1, true)
if (re.code == 200) {
// 灵车: 你应该先 connected = true 再调用
await this.updateCachedProfile()
document.cookie = 'token=' + token
document.cookie = 'device_id=' + data.device_id

View File

@@ -1,2 +1,2 @@
import { css } from 'lit';
export const menuStyle = css `:host{--shape-corner:var(--mdui-shape-corner-extra-small);position:relative;display:block;border-radius:var(--shape-corner);background-color:rgb(var(--mdui-color-surface-container));box-shadow:var(--mdui-elevation-level2);min-width:7rem;max-width:17.5rem;padding-top:.5rem;padding-bottom:.5rem;--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface)}::slotted(mdui-divider){margin-top:.5rem;margin-bottom:.5rem}`;
export const menuStyle = css `:host{--shape-corner:var(--mdui-shape-corner-small);position:relative;display:block;border-radius:var(--shape-corner);background-color:rgb(var(--mdui-color-surface-container));box-shadow:var(--mdui-elevation-level2);min-width:7rem;max-width:17.5rem;padding-top:.5rem;padding-bottom:.5rem;--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-surface)}::slotted(mdui-divider){margin-top:.5rem;margin-bottom:.5rem}`;

View File

@@ -17,6 +17,14 @@ import randomUUID from "../../randomUUID.ts"
import EventBus from "../../EventBus.ts"
import User from "../../api/client_data/User.ts"
import PreferenceLayout from '../preference/PreferenceLayout.tsx'
import PreferenceHeader from '../preference/PreferenceHeader.tsx'
import PreferenceStore from '../preference/PreferenceStore.ts'
import SwitchPreference from '../preference/SwitchPreference.tsx'
import SelectPreference from '../preference/SelectPreference.tsx'
import TextFieldPreference from '../preference/TextFieldPreference.tsx'
import Preference from '../preference/Preference.tsx'
interface Args extends React.HTMLAttributes<HTMLElement> {
target: string
showReturnButton?: boolean
@@ -202,7 +210,9 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
addFile(file.type, file.name, file)
}
})
const groupPreferenceStore = new PreferenceStore()
return (
<div style={{
width: '100%',
@@ -424,7 +434,59 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
flexDirection: "column",
height: "100%",
}}>
Work in progress...
{
chatInfo.type == 'group' && <PreferenceLayout>
<PreferenceHeader
title="群组管理" />
<Preference
title="群组成员列表"
icon="group"
disabled={true}
description="别看了, 还没做" />
<PreferenceHeader
title="入群设定" />
<SwitchPreference
title="允许入群"
icon="person_add"
defaultState={false}
updater={groupPreferenceStore.updater('allow_new_member_join')} />
<SwitchPreference
title="允许成员邀请"
description="目前压根没有这项功能, 甚至还不能查看成员列表, 以后再说吧"
icon="_"
disabled={true}
defaultState={false}
updater={groupPreferenceStore.updater('allow_new_member_from_invitation')} />
<SelectPreference
title="入群验证方式"
icon="_"
selections={{
disabled: "无需验证",
allowed_by_admin: "只需要管理员批准 (WIP)",
answered_and_allowed_by_admin: "需要回答问题并获得管理员批准 (WIP)",
}}
disabled={!groupPreferenceStore.value.allow_new_member_join}
updater={groupPreferenceStore.updater('new_member_join_method')}
defaultCheckedId="disabled" />
{
groupPreferenceStore.value.new_member_join_method == 'answered_and_allowed_by_admin'
&& <TextFieldPreference
title="设置问题"
icon="_"
description="WIP"
defaultState=""
disabled={true}
updater={groupPreferenceStore.updater('answered_and_allowed_by_admin_question')} />
}
</PreferenceLayout>
}
{
chatInfo.type == 'private' && (
<div>
</div>
)
}
</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="None">
</mdui-tab-panel>

View File

@@ -0,0 +1,16 @@
import { ListItem } from "mdui"
interface Args extends React.HTMLAttributes<ListItem> {
title: string
description?: string
icon: string
disabled?: boolean
}
export default function Preference({ title, icon, disabled, description, ...props }: Args) {
// @ts-ignore: 为什么 ...props 要说参数不兼容呢?
return <mdui-list-item disabled={disabled ? true : undefined} rounded icon={icon} {...props}>
{title}
{description && <span slot="description">{description}</span>}
</mdui-list-item>
}

View File

@@ -0,0 +1,5 @@
export default function PreferenceHeader({ title }: {
title: string
}) {
return <mdui-list-subheader>{title}</mdui-list-subheader>
}

View File

@@ -0,0 +1,8 @@
export default function PreferenceLayout({ children, ...props }: React.HTMLAttributes<HTMLElement>) {
return <mdui-list style={{
marginLeft: '15px',
marginRight: '15px',
}} {...props}>
{children}
</mdui-list>
}

View File

@@ -0,0 +1,26 @@
import React from 'react'
export default class PreferenceStore {
declare value: { [key: string]: unknown }
declare setter: React.Dispatch<React.SetStateAction<{ [key: string]: unknown }>>
declare onUpdate: (value: unknown) => void
constructor() {
const _ = React.useState<{ [key: string]: unknown }>({})
this.value = _[0]
this.setter = _[1]
}
// 创建一个用于子选项的更新函数
updater(key: string) {
return (value: unknown) => {
const newValue = JSON.parse(JSON.stringify({
...this.value,
[key]: value,
}))
this.setter(newValue)
this.onUpdate?.(newValue)
}
}
setOnUpdate(onUpdate: (value: unknown) => void) {
this.onUpdate = onUpdate
}
}

View File

@@ -0,0 +1,43 @@
import React from 'react'
import { Dropdown } from 'mdui'
import useEventListener from '../useEventListener.ts'
interface Args extends React.HTMLAttributes<HTMLElement> {
title: string
icon: string
disabled?: boolean
updater: (value: unknown) => void
selections: { [id: string]: string }
defaultCheckedId: string
}
export default function SelectPreference({ title, icon, updater, selections, defaultCheckedId, disabled }: Args) {
const [checkedId, setCheckedId] = React.useState(defaultCheckedId)
const dropDownRef = React.useRef<Dropdown>(null)
const [isDropDownOpen, setDropDownOpen] = React.useState(false)
useEventListener(dropDownRef, 'closed', () => {
setDropDownOpen(false)
})
return <mdui-list-item icon={icon} rounded disabled={disabled ? true : undefined} onClick={() => setDropDownOpen(!isDropDownOpen)}>
<mdui-dropdown ref={dropDownRef} trigger="manual" open={isDropDownOpen}>
<span slot="trigger">{title}</span>
<mdui-menu onClick={(e) => {
e.stopPropagation()
setDropDownOpen(false)
}}>
{
Object.keys(selections).map((id) =>
// @ts-ignore: selected 确实存在, 但是并不对外公开使用
<mdui-menu-item key={id} selected={checkedId == id ? true : undefined} onClick={() => {
setCheckedId(id)
updater(id)
}}>{selections[id]}</mdui-menu-item>
)
}
</mdui-menu>
</mdui-dropdown>
<span slot="description">{selections[checkedId]}</span>
</mdui-list-item>
}

View File

@@ -0,0 +1,28 @@
import { Switch } from 'mdui'
import React from 'react'
interface Args extends React.HTMLAttributes<HTMLElement> {
title: string
description?: string
icon: string
updater: (value: unknown) => void
defaultState: boolean
disabled?: boolean
}
export default function SwitchPreference({ title, icon, updater, disabled, description, defaultState }: Args) {
const switchRef = React.useRef<Switch>(null)
React.useEffect(() => {
switchRef.current!.checked = defaultState
}, [defaultState])
return <mdui-list-item disabled={disabled ? true : undefined} rounded icon={icon} onClick={() => {
switchRef.current!.checked = !switchRef.current!.checked
updater(switchRef.current!.checked)
}}>
{title}
{description && <span slot="description">{description}</span>}
<mdui-switch slot="end-icon" checked-icon="" ref={switchRef} onClick={(e) => e.preventDefault()}></mdui-switch>
</mdui-list-item>
}

View File

@@ -0,0 +1,35 @@
import React from 'react'
import { prompt } from 'mdui'
interface Args extends React.HTMLAttributes<HTMLElement> {
title: string
description?: string
icon: string
updater: (value: unknown) => void
defaultState: string
disabled?: boolean
}
export default function TextFieldPreference({ title, icon, description, updater, defaultState, disabled }: Args) {
const [ text, setText ] = React.useState(defaultState)
return <mdui-list-item icon={icon} rounded disabled={disabled ? true : undefined} onClick={() => {
prompt({
headline: title,
confirmText: "确定",
cancelText: "取消",
onConfirm: (value) => {
setText(value)
updater(value)
},
onCancel: () => {},
textFieldOptions: {
label: description,
value: text,
},
})
}}>
{title}
{description && <span slot="description">{description}</span>}
</mdui-list-item>
}