Compare commits
11 Commits
b0c67da340
...
32369ec3bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32369ec3bd | ||
|
|
2d92fffb55 | ||
|
|
e8f97d9131 | ||
|
|
8a1ff9ac23 | ||
|
|
7553c5b281 | ||
|
|
7c616a2dac | ||
|
|
28a8eaf337 | ||
|
|
60fcb19769 | ||
|
|
e50a90a770 | ||
|
|
adbe6b193b | ||
|
|
c5d1f11017 |
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -5,8 +5,8 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"command": "deno task main",
|
"command": "deno task debug",
|
||||||
"name": "Run deno task main",
|
"name": "Run deno task debug",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { CryptoES } from './Imports.ts'
|
||||||
|
|
||||||
const dataIsEmpty = !localStorage.tws_data || localStorage.tws_data == ''
|
const dataIsEmpty = !localStorage.tws_data || localStorage.tws_data == ''
|
||||||
|
|
||||||
const aes = {
|
const aes = {
|
||||||
enc: (m: string, k: string) => CryptoJS.AES.encrypt(m, k).toString(),
|
enc: (m: string, k: string) => CryptoES.AES.encrypt(m, k).toString(CryptoES.enc.Utf8),
|
||||||
dec: (m: string, k: string) => CryptoJS.AES.decrypt(m, k).toString(CryptoJS.enc.Utf8),
|
dec: (m: string, k: string) => CryptoES.AES.decrypt(m, k).toString(CryptoES.enc.Utf8),
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = location.host + '_TWS_姐姐'
|
const key = location.host + '_TWS_姐姐'
|
||||||
|
|||||||
29
client/Imports.ts
Normal file
29
client/Imports.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from 'https://esm.sh/react@18.3.1'
|
||||||
|
import * as ReactDOM from 'https://esm.sh/react-dom@18.3.1'
|
||||||
|
import CryptoES from 'https://unpkg.com/crypto-es@3.0.4/dist/index.mjs'
|
||||||
|
|
||||||
|
import type { Dialog } from 'https://unpkg.com/mdui@2.1.4/components/dialog/index.d.ts'
|
||||||
|
import type { TextField } from 'https://unpkg.com/mdui@2.1.4/components/text-field/index.d.ts'
|
||||||
|
import type { Button } from 'https://unpkg.com/mdui@2.1.4/components/button/index.d.ts'
|
||||||
|
import type { NavigationRail } from 'https://unpkg.com/mdui@2.1.4/components/navigation-rail/navigation-rail.d.ts'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace React {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicAttributes {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
React,
|
||||||
|
ReactDOM,
|
||||||
|
CryptoES,
|
||||||
|
|
||||||
|
Dialog as MduiDialog,
|
||||||
|
TextField as MduiTextField,
|
||||||
|
Button as MduiButton,
|
||||||
|
NavigationRail as MduiNavigationRail,
|
||||||
|
}
|
||||||
6
client/api/client_data/RecentChat.ts
Normal file
6
client/api/client_data/RecentChat.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default class RecentChat {
|
||||||
|
declare id: string
|
||||||
|
declare title: string
|
||||||
|
declare avatar: string | null
|
||||||
|
declare content: string
|
||||||
|
}
|
||||||
7
client/api/client_data/User.ts
Normal file
7
client/api/client_data/User.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default class User {
|
||||||
|
declare id: string
|
||||||
|
declare count: number
|
||||||
|
declare username: string | null
|
||||||
|
declare nickname: string
|
||||||
|
declare avatar: string | null
|
||||||
|
}
|
||||||
@@ -11,14 +11,10 @@
|
|||||||
</script>
|
</script>
|
||||||
<link rel="icon" href="icon.ico" />
|
<link rel="icon" href="icon.ico" />
|
||||||
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
|
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link rel="stylesheet" href="./static/material_icons.css" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet" />
|
|
||||||
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
|
||||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/split.js@1.6.5/dist/split.min.js"></script>
|
||||||
|
|
||||||
<title>TheWhiteSilk</title>
|
<title>TheWhiteSilk</title>
|
||||||
|
|
||||||
@@ -82,6 +78,16 @@
|
|||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
-ms-interpolation-mode: nearest-neighbor;
|
-ms-interpolation-mode: nearest-neighbor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
background-color: rgb(var(--mdui-color-surface-variant));;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter.gutter-horizontal {
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -93,7 +99,8 @@
|
|||||||
alert('很抱歉, 此应用无法在较旧的浏览器运行, 请使用基于 Chromium 89+ 的浏览器(内核)使用 :(')
|
alert('很抱歉, 此应用无法在较旧的浏览器运行, 请使用基于 Chromium 89+ 的浏览器(内核)使用 :(')
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import App from './ui/App.jsx'
|
import App from './ui/App.tsx'
|
||||||
|
import { React, ReactDOM } from './Imports.ts'
|
||||||
ReactDOM.createRoot(document.getElementById('app')).render(React.createElement(App, null))
|
ReactDOM.createRoot(document.getElementById('app')).render(React.createElement(App, null))
|
||||||
|
|
||||||
let onResize = () => {
|
let onResize = () => {
|
||||||
@@ -103,6 +110,14 @@
|
|||||||
}
|
}
|
||||||
window.addEventListener('resize', onResize)
|
window.addEventListener('resize', onResize)
|
||||||
onResize()
|
onResize()
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
Split(['#SideBar', '#ChatFragment'], {
|
||||||
|
sizes: [25, 75],
|
||||||
|
minSize: [200, 400],
|
||||||
|
gutterSize: 2,
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
45
client/static/material_icons.css
Normal file
45
client/static/material_icons.css
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/* fallback */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(material_icons.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fallback */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Icons Outlined';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(material_icons_outlined.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons-outlined {
|
||||||
|
font-family: 'Material Icons Outlined';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
BIN
client/static/material_icons.woff2
Normal file
BIN
client/static/material_icons.woff2
Normal file
Binary file not shown.
BIN
client/static/material_icons_outlined.woff2
Normal file
BIN
client/static/material_icons_outlined.woff2
Normal file
Binary file not shown.
3350
client/typedef/mdui-jsx.d.ts
vendored
Normal file
3350
client/typedef/mdui-jsx.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,156 +0,0 @@
|
|||||||
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: "麻油衣酱",
|
|
||||||
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",
|
|
||||||
}, */
|
|
||||||
],
|
|
||||||
})
|
|
||||||
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
|
|
||||||
|
|
||||||
const navigationRailRef = React.useRef(null)
|
|
||||||
useEventListener(navigationRailRef, 'change', (event) => {
|
|
||||||
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",
|
|
||||||
position: 'relative',
|
|
||||||
width: 'calc(var(--whitesilk-window-width) - 80px)',
|
|
||||||
height: 'var(--whitesilk-window-height)',
|
|
||||||
}}>
|
|
||||||
<LoginDialog
|
|
||||||
ref={loginDialogRef}
|
|
||||||
inputAccountRef={inputAccountRef}
|
|
||||||
inputPasswordRef={inputPasswordRef}
|
|
||||||
registerButtonRef={registerButtonRef}
|
|
||||||
loginButtonRef={loginButtonRef} />
|
|
||||||
{
|
|
||||||
// 移动端用 页面调试
|
|
||||||
// 換個地方弄
|
|
||||||
// (new URL(location.href).searchParams.get('debug') == 'true') && <script src="https://unpkg.com/eruda/eruda.js"></script>
|
|
||||||
}
|
|
||||||
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
|
|
||||||
<mdui-button-icon icon="menu" slot="top"></mdui-button-icon>
|
|
||||||
|
|
||||||
<mdui-navigation-rail-item icon="watch_later--outlined" value="Recents"></mdui-navigation-rail-item>
|
|
||||||
<mdui-navigation-rail-item icon="contacts--outlined" value="Contacts"></mdui-navigation-rail-item>
|
|
||||||
|
|
||||||
<mdui-button-icon icon="settings" slot="bottom"></mdui-button-icon>
|
|
||||||
</mdui-navigation-rail>
|
|
||||||
{
|
|
||||||
// 侧边列表
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// 最近聊天
|
|
||||||
<mdui-list style={{
|
|
||||||
width: "35%",
|
|
||||||
overflowY: 'auto',
|
|
||||||
paddingRight: '10px',
|
|
||||||
display: navigationItemSelected == "Recents" ? null : 'none'
|
|
||||||
}}>
|
|
||||||
{
|
|
||||||
recentsList.map((v) =>
|
|
||||||
<RecentsListItem
|
|
||||||
key={v.userId}
|
|
||||||
nickName={v.nickName}
|
|
||||||
avatar={v.avatar}
|
|
||||||
content={v.content} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</mdui-list>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// 联系人列表
|
|
||||||
<mdui-list style={{
|
|
||||||
width: "35%",
|
|
||||||
overflowY: 'auto',
|
|
||||||
paddingRight: '10px',
|
|
||||||
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>
|
|
||||||
{
|
|
||||||
contactsMap[v].map((v2) =>
|
|
||||||
<ContactsListItem
|
|
||||||
key={v2.userId}
|
|
||||||
nickName={v2.nickName}
|
|
||||||
avatar={v2.avatar} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</mdui-collapse-item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</mdui-collapse>
|
|
||||||
</mdui-list>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// 分割线
|
|
||||||
}
|
|
||||||
<div style={{
|
|
||||||
// 我们删除了 body 的padding 因此不需要再 calc 了
|
|
||||||
height: 'var(--whitesilk-window-height)',
|
|
||||||
marginRight: '10px',
|
|
||||||
}}>
|
|
||||||
<mdui-divider vertical></mdui-divider>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
// 聊天页面
|
|
||||||
}
|
|
||||||
<ChatFragment />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
151
client/ui/App.tsx
Normal file
151
client/ui/App.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import Client from "../api/Client.ts"
|
||||||
|
import data from "../Data.ts"
|
||||||
|
import ChatFragment from "./chat/ChatFragment.jsx"
|
||||||
|
import LoginDialog from "./dialog/LoginDialog.tsx"
|
||||||
|
import ContactsListItem from "./main/ContactsListItem.jsx"
|
||||||
|
import RecentsListItem from "./main/RecentsListItem.jsx"
|
||||||
|
import snackbar from "./snackbar.js"
|
||||||
|
import useEventListener from './useEventListener.js'
|
||||||
|
|
||||||
|
import { MduiDialog, React, MduiNavigationRail, MduiTextField, MduiButton } from '../Imports.ts'
|
||||||
|
import User from "../api/client_data/User.ts"
|
||||||
|
import RecentChat from "../api/client_data/RecentChat.ts"
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [recentsList, setRecentsList] = React.useState([
|
||||||
|
{
|
||||||
|
id: '0',
|
||||||
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
|
title: "麻油衣酱",
|
||||||
|
content: "成步堂君, 我又坐牢了("
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '0',
|
||||||
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
|
title: "Maya Fey",
|
||||||
|
content: "我是绫里真宵, 是一名灵媒师~"
|
||||||
|
},
|
||||||
|
] as RecentChat[])
|
||||||
|
const [contactsMap, setContactsMap] = React.useState({
|
||||||
|
所有: [
|
||||||
|
{
|
||||||
|
id: '0',
|
||||||
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
|
nickname: "麻油衣酱",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '0',
|
||||||
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
|
nickname: "Maya Fey",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as { [key: string]: User[] })
|
||||||
|
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
|
||||||
|
|
||||||
|
const navigationRailRef = React.useRef(null)
|
||||||
|
useEventListener(navigationRailRef, 'change', (event) => {
|
||||||
|
setNavigationItemSelected((event.target as HTMLElement as MduiNavigationRail).value as string)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loginDialogRef: React.MutableRefObject<MduiDialog | null> = React.useRef(null)
|
||||||
|
const inputAccountRef: React.MutableRefObject<MduiTextField | null> = React.useRef(null)
|
||||||
|
const inputPasswordRef: React.MutableRefObject<MduiTextField | null> = React.useRef(null)
|
||||||
|
const registerButtonRef: React.MutableRefObject<MduiButton | null> = React.useRef(null)
|
||||||
|
const loginButtonRef: React.MutableRefObject<MduiButton | null> = React.useRef(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// deno-lint-ignore no-window-prefix no-window
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
Client.connect()
|
||||||
|
const re = await Client.invoke("User.auth", {
|
||||||
|
access_token: data.access_token,
|
||||||
|
})
|
||||||
|
if (re.code == 401)
|
||||||
|
loginDialogRef.current!.open = true
|
||||||
|
else if (re.code != 200)
|
||||||
|
snackbar("驗證失敗: " + re.msg)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
position: 'relative',
|
||||||
|
width: 'calc(var(--whitesilk-window-width) - 80px)',
|
||||||
|
height: 'var(--whitesilk-window-height)',
|
||||||
|
}}>
|
||||||
|
<LoginDialog
|
||||||
|
loginDialogRef={loginDialogRef}
|
||||||
|
inputAccountRef={inputAccountRef}
|
||||||
|
inputPasswordRef={inputPasswordRef}
|
||||||
|
registerButtonRef={registerButtonRef}
|
||||||
|
loginButtonRef={loginButtonRef} />
|
||||||
|
{
|
||||||
|
// 移动端用 页面调试
|
||||||
|
// 換個地方弄
|
||||||
|
// (new URL(location.href).searchParams.get('debug') == 'true') && <script src="https://unpkg.com/eruda/eruda.js"></script>
|
||||||
|
}
|
||||||
|
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
|
||||||
|
<mdui-button-icon icon="menu" slot="top"></mdui-button-icon>
|
||||||
|
|
||||||
|
<mdui-navigation-rail-item icon="watch_later--outlined" value="Recents"></mdui-navigation-rail-item>
|
||||||
|
<mdui-navigation-rail-item icon="contacts--outlined" value="Contacts"></mdui-navigation-rail-item>
|
||||||
|
|
||||||
|
<mdui-button-icon icon="settings" slot="bottom"></mdui-button-icon>
|
||||||
|
</mdui-navigation-rail>
|
||||||
|
{
|
||||||
|
// 侧边列表
|
||||||
|
}
|
||||||
|
<div id="SideBar">
|
||||||
|
{
|
||||||
|
// 最近聊天
|
||||||
|
<mdui-list style={{
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: '10px',
|
||||||
|
display: navigationItemSelected == "Recents" ? null : 'none'
|
||||||
|
}}>
|
||||||
|
{
|
||||||
|
recentsList.map((v) =>
|
||||||
|
<RecentsListItem
|
||||||
|
key={v.id}
|
||||||
|
nickName={v.title}
|
||||||
|
avatar={v.avatar}
|
||||||
|
content={v.content} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</mdui-list>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// 联系人列表
|
||||||
|
<mdui-list style={{
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: '10px',
|
||||||
|
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>
|
||||||
|
{
|
||||||
|
contactsMap[v].map((v2) =>
|
||||||
|
<ContactsListItem
|
||||||
|
key={v2.id}
|
||||||
|
nickName={v2.nickname}
|
||||||
|
avatar={v2.avatar} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</mdui-collapse-item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</mdui-collapse>
|
||||||
|
</mdui-list>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
// 聊天页面
|
||||||
|
}
|
||||||
|
<ChatFragment id="ChatFragment" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { React } from '../Imports.ts'
|
||||||
|
|
||||||
export default function Avatar({ src, text, icon = 'person', ...props } = {}) {
|
export default function Avatar({ src, text, icon = 'person', ...props } = {}) {
|
||||||
return (
|
return (
|
||||||
src ? <mdui-avatar {...props}>
|
src ? <mdui-avatar {...props}>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import Message from "./Message.jsx"
|
import Message from "./Message.jsx"
|
||||||
import MessageContainer from "./MessageContainer.jsx"
|
import MessageContainer from "./MessageContainer.jsx"
|
||||||
|
|
||||||
export default function ChatFragment() {
|
import { React } from '../../Imports.ts'
|
||||||
|
|
||||||
|
export default function ChatFragment({ ...props } = {}) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -9,7 +11,7 @@ export default function ChatFragment() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}}>
|
}} {...props}>
|
||||||
<mdui-top-app-bar style={{
|
<mdui-top-app-bar style={{
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
}}>
|
}}>
|
||||||
@@ -45,10 +47,12 @@ export default function ChatFragment() {
|
|||||||
paddingTop: '0.1rem',
|
paddingTop: '0.1rem',
|
||||||
height: '4rem',
|
height: '4rem',
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
bottom: '0',
|
bottom: '2px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
marginRight: '4px',
|
||||||
backgroundColor: 'rgb(var(--mdui-color-background))',
|
backgroundColor: 'rgb(var(--mdui-color-background))',
|
||||||
}}>
|
}}>
|
||||||
<mdui-text-field variant="outlined" placeholder="喵呜~" style={{
|
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize max-rows="1" style={{
|
||||||
marginRight: '10px',
|
marginRight: '10px',
|
||||||
}}></mdui-text-field>
|
}}></mdui-text-field>
|
||||||
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
|
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
|
||||||
@@ -59,6 +63,6 @@ export default function ChatFragment() {
|
|||||||
}}></mdui-button-icon>
|
}}></mdui-button-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Avatar from "../Avatar.jsx"
|
import Avatar from "../Avatar.jsx"
|
||||||
|
|
||||||
|
import { React } from '../../Imports.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一条消息
|
* 一条消息
|
||||||
* @param { Object } param
|
* @param { Object } param
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { React } from '../../Imports.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息容器
|
* 消息容器
|
||||||
* @returns { React.JSX.Element }
|
* @returns { React.JSX.Element }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { React } from '../../Imports.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一条系统提示消息
|
* 一条系统提示消息
|
||||||
* @returns { React.JSX.Element }
|
* @returns { React.JSX.Element }
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
|
import { React, MduiDialog, MduiTextField, MduiButton } from '../../Imports.ts'
|
||||||
|
import '../../mdui-jsx.d.ts'
|
||||||
|
|
||||||
|
interface Refs {
|
||||||
|
inputAccountRef: React.MutableRefObject<MduiTextField | null>
|
||||||
|
inputPasswordRef: React.MutableRefObject<MduiTextField | null>
|
||||||
|
registerButtonRef: React.MutableRefObject<MduiButton | null>
|
||||||
|
loginButtonRef: React.MutableRefObject<MduiButton | null>
|
||||||
|
loginDialogRef: React.MutableRefObject<MduiDialog | null>
|
||||||
|
}
|
||||||
|
|
||||||
export default function LoginDialog({
|
export default function LoginDialog({
|
||||||
inputAccountRef,
|
inputAccountRef,
|
||||||
inputPasswordRef,
|
inputPasswordRef,
|
||||||
registerButtonRef,
|
registerButtonRef,
|
||||||
loginButtonRef,
|
loginButtonRef,
|
||||||
...prop
|
loginDialogRef
|
||||||
}) {
|
}: Refs) {
|
||||||
return (
|
return (
|
||||||
<mdui-dialog headline="登录" {...prop}>
|
<mdui-dialog headline="登录" ref={loginDialogRef}>
|
||||||
|
|
||||||
<mdui-text-field label="账号" ref={inputAccountRef}></mdui-text-field>
|
<mdui-text-field label="账号" ref={inputAccountRef}></mdui-text-field>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import Avatar from "../Avatar.jsx"
|
import Avatar from "../Avatar.jsx"
|
||||||
|
|
||||||
|
import { React } from '../../Imports.ts'
|
||||||
|
|
||||||
export default function ContactsListItem({ nickName, avatar }) {
|
export default function ContactsListItem({ nickName, avatar }) {
|
||||||
return (
|
return (
|
||||||
<mdui-list-item rounded style={{
|
<mdui-list-item rounded style={{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Avatar from "../Avatar.jsx"
|
import Avatar from "../Avatar.jsx"
|
||||||
|
|
||||||
|
import { React } from '../../Imports.ts'
|
||||||
|
|
||||||
export default function RecentsListItem({ nickName, avatar, content }) {
|
export default function RecentsListItem({ nickName, avatar, content }) {
|
||||||
return (
|
return (
|
||||||
<mdui-list-item rounded style={{
|
<mdui-list-item rounded style={{
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function snackbar(text) {
|
export default function snackbar(text) {
|
||||||
$("#public_snackbar").text(text).get(0).open()
|
$("#public_snackbar").text(text).get(0).open()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* @param { Event } event
|
* @param { Event } event
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { React } from "../Imports.ts"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绑定事件
|
* 绑定事件
|
||||||
* @param { React.Ref } ref
|
* @param { React.Ref } ref
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"main": "deno run --allow-read --allow-write --allow-env --allow-net --allow-sys ./server/main.ts",
|
"main": "deno run --allow-read --allow-write --allow-env --allow-net --allow-sys ./server/main.ts",
|
||||||
|
"debug": "deno run --watch --allow-read --allow-write --allow-env --allow-net --allow-sys ./server/main.ts",
|
||||||
"test": "deno run --allow-read --allow-write --allow-env --allow-net ./server/main_test.ts"
|
"test": "deno run --allow-read --allow-write --allow-env --allow-net ./server/main_test.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import HttpServerLike from '../types/HttpServerLike.ts'
|
import HttpServerLike from '../typedef/HttpServerLike.ts'
|
||||||
import UserApi from "./UserApi.ts"
|
import UserApi from "./UserApi.ts"
|
||||||
import * as SocketIo from "socket.io"
|
import * as SocketIo from "socket.io"
|
||||||
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
|
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
|
||||||
import EventCallbackFunction from "../types/EventCallbackFunction.ts"
|
import EventCallbackFunction from "../typedef/EventCallbackFunction.ts"
|
||||||
import BaseApi from "./BaseApi.ts"
|
import BaseApi from "./BaseApi.ts"
|
||||||
|
|
||||||
export default class ApiManager {
|
export default class ApiManager {
|
||||||
@@ -41,6 +41,12 @@ export default class ApiManager {
|
|||||||
return callback(this.event_listeners[name]?.(args))
|
return callback(this.event_listeners[name]?.(args))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
try {
|
||||||
|
callback({
|
||||||
|
code: 500,
|
||||||
|
msg: "錯誤: " + e
|
||||||
|
})
|
||||||
|
} catch(_e) {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import EventCallbackFunction from "../types/EventCallbackFunction.ts"
|
import EventCallbackFunction from "../typedef/EventCallbackFunction.ts"
|
||||||
import ApiManager from "./ApiManager.ts"
|
import ApiManager from "./ApiManager.ts"
|
||||||
import { CallMethod } from './ApiDeclare.ts'
|
import { CallMethod } from './ApiDeclare.ts'
|
||||||
|
|
||||||
@@ -8,6 +8,12 @@ export default abstract class BaseApi {
|
|||||||
this.onInit()
|
this.onInit()
|
||||||
}
|
}
|
||||||
abstract onInit(): void
|
abstract onInit(): void
|
||||||
|
checkArgsMissing(args: { [key: string]: unknown }, names: []) {
|
||||||
|
for (const k of names)
|
||||||
|
if (!(k in args))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
registerEvent(name: CallMethod, func: EventCallbackFunction) {
|
registerEvent(name: CallMethod, func: EventCallbackFunction) {
|
||||||
if (!name.startsWith(this.getName() + ".")) throw Error("注冊的事件應該與接口集合命名空間相匹配: " + name)
|
if (!name.startsWith(this.getName() + ".")) throw Error("注冊的事件應該與接口集合命名空間相匹配: " + name)
|
||||||
ApiManager.addEventListener(name, func)
|
ApiManager.addEventListener(name, func)
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ export default class UserApi extends BaseApi {
|
|||||||
return "User"
|
return "User"
|
||||||
}
|
}
|
||||||
override onInit(): void {
|
override onInit(): void {
|
||||||
this.registerEvent("User.auth", () => {
|
this.registerEvent("User.auth", (args) => {
|
||||||
return {
|
return {
|
||||||
msg: "",
|
msg: "",
|
||||||
code: 200,
|
code: 401,
|
||||||
data: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ async function compileJs(path: string) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@babel/preset-react",
|
"@babel/preset-react",
|
||||||
"@babel/preset-typescript",
|
[
|
||||||
|
"@babel/preset-typescript", {
|
||||||
|
allowDeclareFields: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
],
|
],
|
||||||
targets: {
|
targets: {
|
||||||
chrome: "53",
|
chrome: "53",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ApiManager from "./api/ApiManager.ts"
|
|||||||
// @ts-types="npm:@types/express"
|
// @ts-types="npm:@types/express"
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import * as SocketIo from 'socket.io'
|
import * as SocketIo from 'socket.io'
|
||||||
import HttpServerLike from "./types/HttpServerLike.ts"
|
import HttpServerLike from "./typedef/HttpServerLike.ts"
|
||||||
import config from './config.ts'
|
import config from './config.ts'
|
||||||
import http from 'node:http'
|
import http from 'node:http'
|
||||||
import https from 'node:https'
|
import https from 'node:https'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import ApiCallbackMessage from "../api/ApiCallbackMessage.ts"
|
import ApiCallbackMessage from "../api/ApiCallbackMessage.ts"
|
||||||
|
|
||||||
type EventCallbackFunction = (args: {}) => ApiCallbackMessage
|
type EventCallbackFunction = (args: { [key: string]: unknown }) => ApiCallbackMessage
|
||||||
|
|
||||||
export default EventCallbackFunction
|
export default EventCallbackFunction
|
||||||
Reference in New Issue
Block a user