Compare commits

...

7 Commits

Author SHA1 Message Date
CrescentLeaf
b0c67da340 進一步完善 Server API 2025-09-06 01:53:09 +08:00
CrescentLeaf
c3c332017e ui: add LoginDialog 2025-09-06 01:52:38 +08:00
CrescentLeaf
9aba6ebc02 ui: public snackbar 2025-09-06 01:52:26 +08:00
CrescentLeaf
3e3609f25e chore: remove useless type declare 2025-09-06 01:52:14 +08:00
CrescentLeaf
d5e38a8167 feat: Client calling server API 2025-09-06 01:51:57 +08:00
CrescentLeaf
7d407d02ca ui: move ChatFragment implmention 2025-09-06 01:51:15 +08:00
CrescentLeaf
1cc0f57eb0 chore: remove useless .d.ts 2025-09-06 01:50:28 +08:00
16 changed files with 186 additions and 201 deletions

View File

@@ -1,4 +1,3 @@
import CryptoJS from "./types/CryptoJS.d.ts"
const dataIsEmpty = !localStorage.tws_data || localStorage.tws_data == ''
const aes = {
@@ -20,6 +19,7 @@ declare global {
interface Window {
data: {
apply: () => void
access_token?: string
}
}
}

View File

@@ -10,5 +10,6 @@ type ApiCallbackMessage = {
* 501: 伺服器端不支持請求的功能
*/
code: 200 | 400 | 401 | 403 | 404 | 500 | 501,
data?: { [key: string]: unknown },
}
export default ApiCallbackMessage

View File

@@ -1,3 +1,7 @@
export type CallMethod =
"User.auth" |
"User.register" |
"User.login"
export type ClientEvent =
"Client.onMessage"

View File

@@ -1,21 +1,41 @@
import { io, Socket } from 'https://unpkg.com/socket.io-client@4.8.1/dist/socket.io.esm.min.js'
import { CallMethod } from './ApiDeclare.ts'
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
import ApiCallbackMessage from './ApiCallbackMessage.ts'
type UnknownObject = { [key: string]: unknown }
class Client {
static socket: Socket
static socket?: Socket
static events: { [key: string]: (data: UnknownObject) => UnknownObject } = {}
static connect() {
this.socket && this.socket.disconnect()
this.socket?.disconnect()
this.socket && delete this.socket
this.socket = io()
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
try {
if (name == null || data == null) return
const re = this.events[name]?.(data)
re && callback(re)
} catch (e) {
console.error(e)
}
})
}
static call(method: CallMethod, args: {}, timeout: number = 5000) {
static invoke(method: CallMethod, args: UnknownObject = {}, timeout: number = 5000): Promise<ApiCallbackMessage> {
if (this.socket == null) throw new Error("客戶端未與伺服器端建立連接!")
return new Promise((resolve, reject) => {
this.socket.timeout().emit("The_White_Silk", (err, res: ApiCallbackMessage) => {
if (err) return reject(err)
this.socket!.timeout(timeout).emit("The_White_Silk", method, args, (err: string, res: ApiCallbackMessage) => {
if (err) return reject(err)
resolve(res)
})
})
}
static on(eventName: ClientEvent, func: (data: UnknownObject) => UnknownObject) {
this.events[eventName] = func
}
static off(eventName: ClientEvent){
delete this.events[eventName]
}
}
export default Client

View File

@@ -87,6 +87,7 @@
<body>
<div id="app"></div>
<mdui-snackbar close-on-outside-click id="public_snackbar"></mdui-snackbar>
<script nomodule>
alert('很抱歉, 此应用无法在较旧的浏览器运行, 请使用基于 Chromium 89+ 的浏览器(内核)使用 :(')

View File

@@ -1,113 +0,0 @@
// https://github.com/nozzlegear/crypto-js.d.ts/blob/master/crypto-js.d.ts
// Forked from https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/9143f1233f13692c14ae2afe7aee2d5014cba916/crypto-js/crypto-js.d.ts
declare namespace CryptoJS {
type Hash = (message: string, key?: string, ...options: any[]) => string;
interface Cipher {
encrypt(message: string, secretPassphrase: string, option?: CipherOption): WordArray;
decrypt(encryptedMessage: string | WordArray, secretPassphrase: string, option?: CipherOption): DecryptedMessage;
}
interface CipherAlgorythm {
createEncryptor(secretPassphrase: string, option?: CipherOption): Encriptor;
createDecryptor(secretPassphrase: string, option?: CipherOption): Decryptor;
}
interface Encriptor {
process(messagePart: string): string;
finalize(): string;
}
interface Decryptor {
process(messagePart: string): string;
finalize(): string;
}
interface WordArray {
iv: string;
salt: string;
ciphertext: string;
key?: string;
}
type DecryptedMessage = {
toString(encoder?: Encoder): string;
};
interface CipherOption {
iv?: string;
mode?: Mode;
padding?: Padding;
[option: string]: any;
}
interface Encoder {
parse(encodedMessage: string): any;
stringify(words: any): string;
}
interface Mode {}
interface Padding {}
interface Hashes {
MD5: Hash;
SHA1: Hash;
SHA256: Hash;
SHA224: Hash;
SHA512: Hash;
SHA384: Hash;
SHA3: Hash;
RIPEMD160: Hash;
HmacMD5: Hash;
HmacSHA1: Hash;
HmacSHA256: Hash;
HmacSHA224: Hash;
HmacSHA512: Hash;
HmacSHA384: Hash;
HmacSHA3: Hash;
HmacRIPEMD160: Hash;
PBKDF2: Hash;
AES: Cipher;
DES: Cipher;
TripleDES: Cipher;
RC4: Cipher;
RC4Drop: Cipher;
Rabbit: Cipher;
RabbitLegacy: Cipher;
EvpKDF: Cipher;
algo: {
AES: CipherAlgorythm;
DES: CipherAlgorythm;
TrippleDES: CipherAlgorythm;
RC4: CipherAlgorythm;
RC4Drop: CipherAlgorythm;
Rabbit: CipherAlgorythm;
RabbitLegacy: CipherAlgorythm;
EvpKDF: CipherAlgorythm;
};
format: {
OpenSSL: any;
Hex: any;
};
enc: {
Latin1: Encoder;
Utf8: Encoder;
Hex: Encoder;
Utf16: Encoder;
Utf16LE: Encoder;
Base64: Encoder;
};
mode: {
CFB: Mode;
CTR: Mode;
CTRGladman: Mode;
OFB: Mode;
ECB: Mode;
};
pad: {
Pkcs7: Padding;
AnsiX923: Padding;
Iso10126: Padding;
Iso97971: Padding;
ZeroPadding: Padding;
NoPadding: Padding;
};
}
}
declare let CryptoJS: CryptoJS.Hashes;
export default CryptoJS

View File

@@ -1,12 +1,15 @@
import Message from "./chat/Message.jsx"
import MessageContainer from "./chat/MessageContainer.jsx"
import Client from "../api/Client.ts";
import data from "../Data.ts";
import ChatFragment from "./chat/ChatFragment.jsx"
import LoginDialog from "./dialog/LoginDialog.jsx"
import ContactsListItem from "./main/ContactsListItem.jsx"
import RecentsListItem from "./main/RecentsListItem.jsx"
import snackbar from "./snackbar.js";
import useEventListener from './useEventListener.js'
export default function App() {
const [recentsList, setRecentsList] = React.useState([
{
/* {
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "麻油衣酱",
@@ -17,11 +20,11 @@ export default function App() {
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
content: "我是绫里真宵, 是一名灵媒师~"
},
}, */
])
const [contactsMap, setContactsMap] = React.useState({
所有: [
{
/* {
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "麻油衣酱",
@@ -30,7 +33,7 @@ export default function App() {
userId: 0,
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
nickName: "Maya Fey",
},
}, */
],
})
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
@@ -40,6 +43,25 @@ export default function App() {
setNavigationItemSelected(event.target.value)
})
const [
loginDialogRef,
inputAccountRef,
inputPasswordRef,
registerButtonRef,
loginButtonRef
] = [React.useRef(null), React.useRef(null), React.useRef(null), React.useRef(null), React.useRef(null)]
React.useEffect(async () => {
Client.connect()
const re = await Client.invoke("User.auth", {
access_token: data.access_token,
})
if (re.code == 401)
loginDialogRef.current.show()
else if (re.code != 200)
snackbar("驗證失敗: " + re.msg)
})
return (
<div style={{
display: "flex",
@@ -47,6 +69,12 @@ export default function App() {
width: 'calc(var(--whitesilk-window-width) - 80px)',
height: 'var(--whitesilk-window-height)',
}}>
<LoginDialog
ref={loginDialogRef}
inputAccountRef={inputAccountRef}
inputPasswordRef={inputPasswordRef}
registerButtonRef={registerButtonRef}
loginButtonRef={loginButtonRef} />
{
// 移动端用 页面调试
// 換個地方弄
@@ -91,7 +119,7 @@ export default function App() {
display: navigationItemSelected == "Contacts" ? null : 'none'
}}>
<mdui-collapse accordion value={Object.keys(contactsMap)[0]}>
{
{
Object.keys(contactsMap).map((v) =>
<mdui-collapse-item key={v} value={v}>
<mdui-list-subheader slot="header">{v}</mdui-list-subheader>
@@ -122,63 +150,7 @@ export default function App() {
{
// 聊天页面
}
<div style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
}}>
<mdui-top-app-bar style={{
position: 'sticky',
}}>
<mdui-button-icon icon="menu"></mdui-button-icon>
<mdui-top-app-bar-title>Title</mdui-top-app-bar-title>
<mdui-button-icon icon="more_vert"></mdui-button-icon>
</mdui-top-app-bar>
<div style={{
display: "flex",
flexDirection: "column",
height: "100%",
}}>
<div style={{
display: "flex",
justifyContent: "center",
}}>
<mdui-button variant="text">加載更多</mdui-button>
</div>
<MessageContainer>
<Message
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
</MessageContainer>
{
// 输入框
}
<div style={{
display: 'flex',
alignItems: 'center',
paddingBottom: '0.1rem',
paddingTop: '0.1rem',
height: '4rem',
position: 'sticky',
bottom: '0',
backgroundColor: 'rgb(var(--mdui-color-background))',
}}>
<mdui-text-field variant="outlined" placeholder="喵呜~" style={{
marginRight: '10px',
}}></mdui-text-field>
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
marginRight: '6px',
}}></mdui-button-icon>
<mdui-button-icon icon="send" style={{
marginRight: '7px',
}}></mdui-button-icon>
</div>
</div>
</div>
<ChatFragment />
</div>
)
}

View File

@@ -1,3 +1,64 @@
export default function ChatFragment() {
import Message from "./Message.jsx"
import MessageContainer from "./MessageContainer.jsx"
export default function ChatFragment() {
return (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
}}>
<mdui-top-app-bar style={{
position: 'sticky',
}}>
<mdui-button-icon icon="menu"></mdui-button-icon>
<mdui-top-app-bar-title>Title</mdui-top-app-bar-title>
<mdui-button-icon icon="more_vert"></mdui-button-icon>
</mdui-top-app-bar>
<div style={{
display: "flex",
flexDirection: "column",
height: "100%",
}}>
<div style={{
display: "flex",
justifyContent: "center",
}}>
<mdui-button variant="text">加載更多</mdui-button>
</div>
<MessageContainer>
<Message
nickName="Fey"
avatar="https://www.court-records.net/mugshot/aa6-004-maya.png">
Test
</Message>
</MessageContainer>
{
// 输入框
}
<div style={{
display: 'flex',
alignItems: 'center',
paddingBottom: '0.1rem',
paddingTop: '0.1rem',
height: '4rem',
position: 'sticky',
bottom: '0',
backgroundColor: 'rgb(var(--mdui-color-background))',
}}>
<mdui-text-field variant="outlined" placeholder="喵呜~" style={{
marginRight: '10px',
}}></mdui-text-field>
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
marginRight: '6px',
}}></mdui-button-icon>
<mdui-button-icon icon="send" style={{
marginRight: '7px',
}}></mdui-button-icon>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,21 @@
export default function LoginDialog({
inputAccountRef,
inputPasswordRef,
registerButtonRef,
loginButtonRef,
...prop
}) {
return (
<mdui-dialog headline="登录" {...prop}>
<mdui-text-field label="账号" ref={inputAccountRef}></mdui-text-field>
<div style={{
height: "10px",
}}></div>
<mdui-text-field label="密码" ref={inputPasswordRef}></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>
)
}

3
client/ui/snackbar.js Normal file
View File

@@ -0,0 +1,3 @@
export default function snackbar(text) {
$("#public_snackbar").text(text).get(0).open()
}

View File

@@ -10,5 +10,6 @@ type ApiCallbackMessage = {
* 501: 伺服器端不支持請求的功能
*/
code: 200 | 400 | 401 | 403 | 404 | 500 | 501,
data?: { [key: string]: unknown },
}
export default ApiCallbackMessage

7
server/api/ApiDeclare.ts Normal file
View File

@@ -0,0 +1,7 @@
export type CallMethod =
"User.auth" |
"User.register" |
"User.login"
export type ClientEvent =
"Client.onMessage"

View File

@@ -1,14 +1,15 @@
import HttpServerLike from '../types/HttpServerLike.ts'
import UserApi from "./UserApi.ts"
import * as SocketIo from "socket.io"
import ApiCallbackMessage from "../types/ApiCallbackMessage.ts"
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
import EventCallbackFunction from "../types/EventCallbackFunction.ts"
import BaseApi from "./BaseApi.ts"
export default class ApiManager {
static httpServer: HttpServerLike
static socketIoServer: SocketIo.Server
static event_listeners: { [key: string] : EventCallbackFunction } = {}
static apis_instance: {}
static event_listeners: { [key: string]: EventCallbackFunction } = {}
static apis_instance: { [key: string]: BaseApi } = {}
static initServer(httpServer: HttpServerLike, socketIoServer: SocketIo.Server) {
this.httpServer = httpServer
this.socketIoServer = socketIoServer
@@ -30,13 +31,17 @@ export default class ApiManager {
static initEvents() {
const io = this.socketIoServer
io.on('connection', (socket) => {
socket.on("The_White_Silk", (name: string, args: {}, callback: (ret: ApiCallbackMessage) => void) => {
if (name == null || args == null) return callback({
msg: "Invalid request.",
code: 400
})
socket.on("The_White_Silk", (name: string, args: { [key: string]: unknown }, callback: (ret: ApiCallbackMessage) => void) => {
try {
if (name == null || args == null) return callback({
msg: "Invalid request.",
code: 400
})
return callback(this.event_listeners[name]?.(args))
return callback(this.event_listeners[name]?.(args))
} catch (e) {
console.error(e)
}
})
})
}

View File

@@ -1,5 +1,6 @@
import EventCallbackFunction from "../types/EventCallbackFunction.ts"
import ApiManager from "./ApiManager.ts";
import ApiManager from "./ApiManager.ts"
import { CallMethod } from './ApiDeclare.ts'
export default abstract class BaseApi {
abstract getName(): string
@@ -7,7 +8,8 @@ export default abstract class BaseApi {
this.onInit()
}
abstract onInit(): void
registerEvent(name: string, func: EventCallbackFunction) {
ApiManager.addEventListener(this.getName() + "." + name, func)
registerEvent(name: CallMethod, func: EventCallbackFunction) {
if (!name.startsWith(this.getName() + ".")) throw Error("注冊的事件應該與接口集合命名空間相匹配: " + name)
ApiManager.addEventListener(name, func)
}
}

View File

@@ -1,11 +1,11 @@
import BaseApi from "./BaseApi.ts";
import BaseApi from "./BaseApi.ts"
export default class UserApi extends BaseApi {
override getName(): string {
return "User"
}
override onInit(): void {
this.registerEvent("", () => {
this.registerEvent("User.auth", () => {
return {
msg: "",
code: 200,

View File

@@ -1,4 +1,4 @@
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
import ApiCallbackMessage from "../api/ApiCallbackMessage.ts"
type EventCallbackFunction = (args: {}) => ApiCallbackMessage