添加 react-router, 使 CallackError 获得更多成员, 导出, (WIP) 图片查看器修改, 修复遗忘的 data.apply()

This commit is contained in:
CrescentLeaf
2025-12-06 15:45:43 +08:00
parent 29ea0c5b84
commit b85b6833b6
10 changed files with 209 additions and 128 deletions

View File

@@ -1,7 +1,11 @@
import ApiCallbackMessage from "./ApiCallbackMessage.ts" import ApiCallbackMessage from "./ApiCallbackMessage.ts"
export default class CallbackError extends Error { export default class CallbackError extends Error {
declare code: number
declare data?: object
constructor(re: ApiCallbackMessage) { constructor(re: ApiCallbackMessage) {
super(`[${re.code}] ${re.msg}${re.data ? ` (data: ${JSON.stringify(re.data)})` : ''}`) super(`[${re.code}] ${re.msg}${re.data ? ` (data: ${JSON.stringify(re.data)})` : ''}`)
this.code = re.code
this.data = re.data
} }
} }

View File

@@ -7,10 +7,13 @@ import GroupSettingsBean from "./bean/GroupSettingsBean.ts"
import JoinRequestBean from "./bean/JoinRequestBean.ts" import JoinRequestBean from "./bean/JoinRequestBean.ts"
import MessageBean from "./bean/MessageBean.ts" import MessageBean from "./bean/MessageBean.ts"
import RecentChatBean from "./bean/RecentChatBean.ts" import RecentChatBean from "./bean/RecentChatBean.ts"
import LingChairClient from "./LingChairClient.ts" import LingChairClient from "./LingChairClient.ts"
import CallbackError from "./CallbackError.ts"
export { export {
LingChairClient, LingChairClient,
CallbackError,
Chat, Chat,
User, User,

View File

@@ -15,6 +15,7 @@
"pinch-zoom-element": "1.1.1", "pinch-zoom-element": "1.1.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-router": "7.10.1",
"socket.io-client": "4.8.1", "socket.io-client": "4.8.1",
"split.js": "1.3.2", "split.js": "1.3.2",
"ua-parser-js": "2.0.6" "ua-parser-js": "2.0.6"

View File

@@ -2,7 +2,9 @@ import data from "./Data.ts";
import getClient from "./getClient.ts" import getClient from "./getClient.ts"
/** /**
* 客户端上线 * 尝试进行验证
*
* 成功后自动保存到本地
* *
* 优先级: 账号密码 > 提供刷新令牌 > 储存的刷新令牌 * 优先级: 账号密码 > 提供刷新令牌 > 储存的刷新令牌
* *
@@ -23,4 +25,5 @@ export default async function performAuth(args: {
} }
data.refresh_token = getClient().getCachedRefreshToken() data.refresh_token = getClient().getCachedRefreshToken()
data.access_token = getClient().getCachedAccessToken() data.access_token = getClient().getCachedAccessToken()
data.apply()
} }

19
client/ui/ImageViewer.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { Dialog } from 'mdui'
import 'pinch-zoom-element'
import React from "react"
export default function ImageViewer() {
const dialogRef = React.useRef<Dialog>()
return <mdui-dialog ref={dialogRef} fullscreen="fullscreen">
<mdui-button-icon icon="open_in_new"
onclick="window.open(document.querySelector('#image-viewer-dialog-inner > *').src, '_blank')">
</mdui-button-icon>
<mdui-button-icon icon="close" onClick={() => dialogRef.current!.open = false}>
</mdui-button-icon>
{
// @ts-ignore 注册了这个元素
<pinch-zoom id="image-viewer-dialog-inner" style="width: var(--whitesilk-window-width); height: var(--whitesilk-window-height);"></pinch-zoom>
}
</mdui-dialog>
}

View File

@@ -1,26 +1,64 @@
import isMobileUI from "../utils/isMobileUI.ts" import isMobileUI from "../utils/isMobileUI.ts"
import useEventListener from "../utils/useEventListener.ts"
import AvatarMySelf from "./AvatarMySelf.tsx" import AvatarMySelf from "./AvatarMySelf.tsx"
import MainSharedContext from './MainSharedContext.ts' import MainSharedContext from './MainSharedContext.ts'
import React from "react" import React from "react"
import { BrowserRouter, Outlet, Route, Routes } from "react-router"
import LoginDialog from "./main-page/LoginDialog.tsx"
import useAsyncEffect from "../utils/useAsyncEffect.ts"
import performAuth from "../performAuth.ts"
import { CallbackError } from "lingchair-client-protocol"
export default function Main() { export default function Main() {
const [showLoginDialog, setShowLoginDialog] = React.useState(false)
// 多页面切换
const navigationRef = React.useRef<HTMLElement>()
const [currentShowPage, setCurrentShowPage] = React.useState('Recents')
type HTMLElementWithValue = HTMLElement & { value: string }
useEventListener(navigationRef, 'change', (event) => {
setCurrentShowPage((event.target as HTMLElementWithValue).value)
})
const sharedContext = { const sharedContext = {
openChatFragment: React.useRef() ui_functions: React.useRef({
}),
setShowLoginDialog,
} }
useAsyncEffect(async () => {
try {
await performAuth({})
} catch (e) {
if (e instanceof CallbackError)
if (e.code == 401 || e.code == 400)
setShowLoginDialog(true)
}
})
return ( return (
<MainSharedContext.Provider value={sharedContext}> <MainSharedContext.Provider value={sharedContext}>
<BrowserRouter>
<Routes>
<Route path="/" element={(
<div style={{ <div style={{
display: "flex", display: "flex",
position: 'relative', position: 'relative',
width: 'calc(var(--whitesilk-window-width) - 80px)', width: 'calc(var(--whitesilk-window-width) - 80px)',
height: 'var(--whitesilk-window-height)', height: 'var(--whitesilk-window-height)',
}}> }}>
{
// 将子路由渲染到此处
<Outlet />
}
<LoginDialog open={showLoginDialog} />
{ {
/** /**
* Default: 侧边列表提供列表切换 * Default: 侧边列表提供列表切换
*/ */
!isMobileUI() ? !isMobileUI() ?
<mdui-navigation-rail contained value="Recents"> <mdui-navigation-rail ref={navigationRef} contained value="Recents">
<mdui-button-icon slot="top"> <mdui-button-icon slot="top">
<AvatarMySelf /> <AvatarMySelf />
</mdui-button-icon> </mdui-button-icon>
@@ -89,7 +127,7 @@ export default function Main() {
* Mobile: 底部导航栏提供列表切换 * Mobile: 底部导航栏提供列表切换
* Default: 侧边列表提供列表切换 * Default: 侧边列表提供列表切换
*/ */
isMobileUI() && <mdui-navigation-bar label-visibility="selected" value="Recents" style={{ isMobileUI() && <mdui-navigation-bar ref={navigationRef} label-visibility="selected" value="Recents" style={{
position: 'sticky', position: 'sticky',
bottom: '0', bottom: '0',
}}> }}>
@@ -99,6 +137,10 @@ export default function Main() {
</mdui-navigation-bar> </mdui-navigation-bar>
} }
</div> </div>
)}>
</Route>
</Routes>
</BrowserRouter>
</MainSharedContext.Provider> </MainSharedContext.Provider>
) )
} }

View File

@@ -0,0 +1,44 @@
import * as React from 'react'
import { Button, Dialog, TextField } from "mdui"
import performAuth from '../../performAuth.ts'
import useEventListener from '../../utils/useEventListener.ts'
import showSnackbar from '../../utils/showSnackbar.ts'
export default function LoginDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
const loginDialogRef = React.useRef<Dialog>(null)
const loginButtonRef = React.useRef<Button>(null)
const registerButtonRef = React.useRef<Button>(null)
const loginInputAccountRef = React.useRef<TextField>(null)
const loginInputPasswordRef = React.useRef<TextField>(null)
useEventListener(loginButtonRef, 'click', async () => {
const account = loginInputAccountRef.current!.value
const password = loginInputPasswordRef.current!.value
try {
await performAuth({
account: account,
password: password,
})
location.reload()
} catch (e) {
if (e instanceof Error)
showSnackbar({ message: '登录失败: ' + e.message })
}
})
return (
<mdui-dialog {...props} headline="登录" ref={loginDialogRef}>
<mdui-text-field label="用户 ID / 用户名" ref={loginInputAccountRef}></mdui-text-field>
<div style={{
height: "10px",
}}></div>
<mdui-text-field label="密码" type="password" toggle-password ref={loginInputPasswordRef}></mdui-text-field>
<mdui-button slot="action" variant="text" ref={registerButtonRef}></mdui-button>
<mdui-button slot="action" variant="text" ref={loginButtonRef}></mdui-button>
</mdui-dialog>
)
}

View File

@@ -1,39 +1,4 @@
import { $ } from 'mdui/jq' import { $ } from 'mdui/jq'
import 'pinch-zoom-element'
document.body.appendChild(new DOMParser().parseFromString(`
<mdui-dialog id="image-viewer-dialog" fullscreen="fullscreen">
<style>
#image-viewer-dialog::part(panel) {
background: rgba(0, 0, 0, 0) !important;
padding: 0 !important;
}
#image-viewer-dialog>mdui-button-icon[icon=close] {
z-index: 114514;
position: fixed;
top: 15px;
right: 15px;
color: #ffffff
}
#image-viewer-dialog>mdui-button-icon[icon=open_in_new] {
z-index: 114514;
position: fixed;
top: 15px;
right: 65px;
color: #ffffff
}
</style>
<mdui-button-icon icon="open_in_new"
onclick="window.open(document.querySelector('#image-viewer-dialog-inner > *').src, '_blank')">
</mdui-button-icon>
<mdui-button-icon icon="close" onclick="this.parentNode.open = false">
</mdui-button-icon>
<pinch-zoom id="image-viewer-dialog-inner" style="width: var(--whitesilk-window-width); height: var(--whitesilk-window-height);">
</pinch-zoom>
</mdui-dialog>
`, 'text/html').body.firstChild as Node)
export default function openImageViewer(src: string) { export default function openImageViewer(src: string) {
$('#image-viewer-dialog-inner').empty() $('#image-viewer-dialog-inner').empty()

View File

@@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react'
export default function useEventListener<T extends HTMLElement | null>(ref: React.MutableRefObject<T>, eventName: string, callback: (event: Event) => void) { export default function useEventListener<T extends HTMLElement | undefined | null>(ref: React.MutableRefObject<T>, eventName: string, callback: (event: Event) => void) {
React.useEffect(() => { React.useEffect(() => {
ref.current!.addEventListener(eventName, callback) ref.current!.addEventListener(eventName, callback)
return () => ref.current?.removeEventListener(eventName, callback) return () => ref.current?.removeEventListener(eventName, callback)

View File

@@ -19,7 +19,7 @@
"express": "5.1.0", "express": "5.1.0",
"express-fileupload": "1.5.2", "express-fileupload": "1.5.2",
"file-type": "21.0.0", "file-type": "21.0.0",
"socket.io": "4.8.1", "lingchair-internal-shared": "*",
"lingchair-internal-shared": "*" "socket.io": "4.8.1"
} }
} }