diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..6515971
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TheWhiteSilk
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/test.html b/client/test.html
new file mode 100644
index 0000000..379c99f
--- /dev/null
+++ b/client/test.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TheWhiteSilk Debugger
+
+
+
+ Send
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/ui/App.jsx b/client/ui/App.jsx
new file mode 100644
index 0000000..25feccb
--- /dev/null
+++ b/client/ui/App.jsx
@@ -0,0 +1,283 @@
+import Message from "./chat/Message.jsx"
+import MessageContainer from "./chat/MessageContainer.jsx"
+import ContactsListItem from "./main/ContactsListItem.jsx"
+import RecentsListItem from "./main/RecentsListItem.jsx"
+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: "麻油衣酱",
+ content: "成步堂君, 我又坐牢了("
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "Maya Fey",
+ content: "我是绫里真宵, 是一名灵媒师~"
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "麻油衣酱",
+ content: "成步堂君, 我又坐牢了("
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "Maya Fey",
+ content: "我是绫里真宵, 是一名灵媒师~"
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "麻油衣酱",
+ content: "成步堂君, 我又坐牢了("
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "Maya Fey",
+ content: "我是绫里真宵, 是一名灵媒师~"
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "麻油衣酱",
+ content: "成步堂君, 我又坐牢了("
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "Maya Fey",
+ content: "我是绫里真宵, 是一名灵媒师~"
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "麻油衣酱",
+ content: "成步堂君, 我又坐牢了("
+ },
+ {
+ userId: 0,
+ 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: "麻油衣酱",
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "Maya Fey",
+ }
+ ],
+ 测试分组114514: [
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "麻油衣酱",
+ },
+ {
+ userId: 0,
+ avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
+ nickName: "Maya Fey",
+ }
+ ],
+ })
+ const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
+
+ const navigationRailRef = React.useRef(null)
+ useEventListener(navigationRailRef, 'change', (event) => {
+ setNavigationItemSelected(event.target.value)
+ })
+
+ return (
+
+ {
+ // 移动端用 页面调试
+ // 換個地方弄
+ // (new URL(location.href).searchParams.get('debug') == 'true') &&
+ }
+
+
+
+
+
+
+
+
+ {
+ // 侧边列表
+ }
+ {
+ // 最近聊天
+ (navigationItemSelected == "Recents") &&
+
+ {
+ recentsList.map((v) =>
+
+ )
+ }
+
+ }
+ {
+ // 联系人列表
+ (navigationItemSelected == "Contacts") &&
+
+ {
+ Object.keys(contactsMap).map((v) =>
+ <>
+ {v}
+ {
+ contactsMap[v].map((v2) =>
+
+ )
+ }
+ >
+ )
+ }
+
+ }
+ {
+ // 分割线
+ }
+
+
+
+ {
+ // 聊天页面
+ }
+
+
+
+ Title
+
+
+
+
+
+ Test
+
+
+ Test
+
+
+ Test
+
+
+ Test
+
+
+ Test
+
+
+ Test
+
+
+ Test
+
+
+ Test
+
+
+ {
+ // 输入框
+ }
+
+
+
+
+
+ {/*
+
+ Title
+
+
+ */}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/client/ui/Avatar.jsx b/client/ui/Avatar.jsx
new file mode 100644
index 0000000..619410b
--- /dev/null
+++ b/client/ui/Avatar.jsx
@@ -0,0 +1,15 @@
+export default function Avatar({ src, text, icon = 'person', ...props } = {}) {
+ return (
+ src ?
+
+
+ : (
+ text ?
+ {
+ text.substring(0, 0)
+ }
+
+ :
+ )
+ )
+}
diff --git a/client/ui/chat/ChatFragment.jsx b/client/ui/chat/ChatFragment.jsx
new file mode 100644
index 0000000..e92ea04
--- /dev/null
+++ b/client/ui/chat/ChatFragment.jsx
@@ -0,0 +1,3 @@
+export default function ChatFragment() {
+
+}
diff --git a/client/ui/chat/Message.jsx b/client/ui/chat/Message.jsx
new file mode 100644
index 0000000..05f4615
--- /dev/null
+++ b/client/ui/chat/Message.jsx
@@ -0,0 +1,83 @@
+import Avatar from "../Avatar.jsx"
+
+/**
+ * 一条消息
+ * @param { Object } param
+ * @param { "left" | "right" } [param.direction="left"] 消息方向
+ * @param { String } [param.avatar] 头像链接
+ * @param { String } [param.nickName] 昵称
+ * @returns { React.JSX.Element }
+ */
+export default function Message({ direction = 'left', avatar, nickName, children, ...props } = {}) {
+ let isAtRight = direction == 'right'
+ return (
+
+
+ {
+ // 发送者昵称(左)
+ isAtRight &&
+ {nickName}
+
+ }
+ {
+ // 发送者头像
+ }
+
+ {
+ // 发送者昵称(右)
+ !isAtRight &&
+ {nickName}
+
+ }
+
+
+
+ {
+ // 消息内容
+ children
+ }
+
+
+
+ )
+}
diff --git a/client/ui/chat/MessageContainer.jsx b/client/ui/chat/MessageContainer.jsx
new file mode 100644
index 0000000..edcc26c
--- /dev/null
+++ b/client/ui/chat/MessageContainer.jsx
@@ -0,0 +1,18 @@
+/**
+ * 消息容器
+ * @returns { React.JSX.Element }
+ */
+export default function MessageContainer({ children, style, ...props } = {}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/client/ui/chat/SystemMessage.jsx b/client/ui/chat/SystemMessage.jsx
new file mode 100644
index 0000000..c9b9f4b
--- /dev/null
+++ b/client/ui/chat/SystemMessage.jsx
@@ -0,0 +1,27 @@
+/**
+ * 一条系统提示消息
+ * @returns { React.JSX.Element }
+ */
+export default function SystemMessage({ children } = {}) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/client/ui/main/ContactsListItem.jsx b/client/ui/main/ContactsListItem.jsx
new file mode 100644
index 0000000..3600ee2
--- /dev/null
+++ b/client/ui/main/ContactsListItem.jsx
@@ -0,0 +1,16 @@
+import Avatar from "../Avatar.jsx"
+
+export default function ContactsListItem({ nickName, avatar }) {
+ return (
+
+ {nickName}
+
+
+ )
+}
diff --git a/client/ui/main/RecentsListItem.jsx b/client/ui/main/RecentsListItem.jsx
new file mode 100644
index 0000000..e677948
--- /dev/null
+++ b/client/ui/main/RecentsListItem.jsx
@@ -0,0 +1,21 @@
+import Avatar from "../Avatar.jsx"
+
+export default function RecentsListItem({ nickName, avatar, content }) {
+ return (
+
+ {nickName}
+
+ {content}
+
+ )
+}
diff --git a/client/ui/useEventListener.js b/client/ui/useEventListener.js
new file mode 100644
index 0000000..1c0f67e
--- /dev/null
+++ b/client/ui/useEventListener.js
@@ -0,0 +1,17 @@
+/**
+ * @callback callback
+ * @param { Event } event
+ */
+
+/**
+ * 绑定事件
+ * @param { React.Ref } ref
+ * @param { String } eventName
+ * @param { callback } callback
+ */
+export default function useEventListener(ref, eventName, callback) {
+ React.useEffect(() => {
+ ref.current.addEventListener(eventName, callback)
+ return () => ref.current.removeEventListener(eventName, callback)
+ }, [ref, eventName, callback])
+}
diff --git a/deno.jsonc b/deno.jsonc
index 98215d1..339ccd0 100644
--- a/deno.jsonc
+++ b/deno.jsonc
@@ -6,8 +6,11 @@
"imports": {
"chalk": "npm:chalk@5.4.1",
"file-type": "npm:file-type@21.0.0",
- "webpack": "npm:webpack@5.101.2",
"express": "npm:express@5.1.0",
- "socket.io": "npm:socket.io@4.8.1"
+ "socket.io": "npm:socket.io@4.8.1",
+
+ "webpack": "npm:webpack@5.101.2",
+ "@babel/preset-env": "npm:@babel/preset-env@^7.26.9",
+ "@babel/preset-react": "npm:@babel/preset-react@^7.26.3"
}
}
\ No newline at end of file
diff --git a/server/api/ApiManager.ts b/server/api/ApiManager.ts
index 86517bd..82a31f0 100644
--- a/server/api/ApiManager.ts
+++ b/server/api/ApiManager.ts
@@ -1,6 +1,6 @@
import HttpServerLike from '../types/HttpServerLike.ts'
import UserApi from "./UserApi.ts"
-import SocketIo from "socket.io"
+import * as SocketIo from "socket.io"
import ApiCallbackMessage from "../types/ApiCallbackMessage.ts"
import EventCallbackFunction from "../types/EventCallbackFunction.ts"
diff --git a/server/main.ts b/server/main.ts
index 6ea0b62..63d8556 100644
--- a/server/main.ts
+++ b/server/main.ts
@@ -1,10 +1,11 @@
import ApiManager from "./api/ApiManager.ts"
import express from 'express'
-import SocketIo from 'socket.io'
+import * as SocketIo from 'socket.io'
import HttpServerLike from "./types/HttpServerLike.ts"
import config from './config.ts'
import http from 'node:http'
import https from 'node:https'
+import web_packer from './web_packer.ts'
const app = express()
const httpServer: HttpServerLike = (
@@ -21,3 +22,9 @@ ApiManager.initEvents()
ApiManager.initAllApis()
httpServer.listen(config.server.listen)
+
+web_packer?.run((err, status) => {
+ if (err) throw err
+ console.log("前端頁面已編譯完成")
+ web_packer?.close(() => {})
+})
diff --git a/server/web_packer.ts b/server/web_packer.ts
new file mode 100644
index 0000000..9b74ae3
--- /dev/null
+++ b/server/web_packer.ts
@@ -0,0 +1,29 @@
+import webpack from 'webpack'
+import config from "./config.ts"
+import path from 'node:path'
+
+export default webpack({
+ mode: 'production',
+ devtool: 'source-map',
+ entry: './client/ui/App.jsx',
+ output: {
+ filename: 'bundle.js',
+ path: path.resolve(import.meta.dirname as string, config.data_path, 'page_compiled'),
+ },
+ module: {
+ rules: [
+ {
+ test: /\.(j|t)sx?$/,
+ use: {
+ loader: "babel-loader",
+ options: {
+ presets: [
+ '@babel/preset-env',
+ '@babel/preset-react',
+ ],
+ },
+ },
+ },
+ ],
+ },
+})
diff --git a/webpack_config.ts b/webpack_config.ts
deleted file mode 100644
index c277a33..0000000
--- a/webpack_config.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import webpack from 'webpack'
-
-export default webpack({
-
-})