Compare commits
15 Commits
1a69b521e6
...
3617292409
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3617292409 | ||
|
|
a3fc61494e | ||
|
|
fa62180667 | ||
|
|
e60c1cf1c4 | ||
|
|
7e60e4a4be | ||
|
|
f3a9cb8641 | ||
|
|
c577797e57 | ||
|
|
3a7e4970d4 | ||
|
|
0e14bb9a45 | ||
|
|
2869a77abd | ||
|
|
913d1f395f | ||
|
|
abf06c71af | ||
|
|
afeab61468 | ||
|
|
f06e93ef06 | ||
|
|
71b368a5ac |
@@ -1,4 +1,4 @@
|
|||||||
import { io, Socket } from 'https://unpkg.com/socket.io-client@4.8.1/dist/socket.io.esm.min.js'
|
import { io, Socket } from 'socket.io-client'
|
||||||
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
|
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
|
||||||
import ApiCallbackMessage from './ApiCallbackMessage.ts'
|
import ApiCallbackMessage from './ApiCallbackMessage.ts'
|
||||||
|
|
||||||
@@ -10,7 +10,9 @@ class Client {
|
|||||||
static connect() {
|
static connect() {
|
||||||
this.socket?.disconnect()
|
this.socket?.disconnect()
|
||||||
this.socket && delete this.socket
|
this.socket && delete this.socket
|
||||||
this.socket = io()
|
this.socket = io({
|
||||||
|
transports: ['websocket']
|
||||||
|
})
|
||||||
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
|
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
|
||||||
try {
|
try {
|
||||||
if (name == null || data == null) return
|
if (name == null || data == null) return
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
"mdui": "npm:mdui@2.1.4",
|
"mdui": "npm:mdui@2.1.4",
|
||||||
"split.js": "npm:split.js@1.3.2",
|
"split.js": "npm:split.js@1.3.2",
|
||||||
"crypto-es": "npm:crypto-es@3.1.0"
|
"crypto-es": "npm:crypto-es@3.1.0",
|
||||||
|
"socket.io-client": "npm:socket.io-client@4.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import User from "../api/client_data/User.ts"
|
|||||||
import RecentChat from "../api/client_data/RecentChat.ts"
|
import RecentChat from "../api/client_data/RecentChat.ts"
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as CryptoES from 'crypto-es'
|
import { Button, Dialog, NavigationRail, TextField } from "mdui"
|
||||||
import { Button, Dialog, NavigationRail, snackbar, TextField } from "mdui"
|
|
||||||
import Split from 'split.js'
|
import Split from 'split.js'
|
||||||
import 'mdui/jsx.zh-cn.d.ts'
|
import 'mdui/jsx.zh-cn.d.ts'
|
||||||
|
import { checkApiSuccessOrSncakbar } from "./snackbar.ts";
|
||||||
|
import RegisterDialog from "./dialog/RegisterDialog.tsx";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace React {
|
namespace React {
|
||||||
@@ -26,7 +27,7 @@ declare global {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [recentsList, setRecentsList] = React.useState([
|
const [recentsList, setRecentsList] = React.useState([
|
||||||
{
|
{
|
||||||
id: '0',
|
id: '0',
|
||||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
title: "麻油衣酱",
|
title: "麻油衣酱",
|
||||||
@@ -37,11 +38,11 @@ export default function App() {
|
|||||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
title: "Maya Fey",
|
title: "Maya Fey",
|
||||||
content: "我是绫里真宵, 是一名灵媒师~"
|
content: "我是绫里真宵, 是一名灵媒师~"
|
||||||
},
|
},
|
||||||
] as RecentChat[])
|
] as RecentChat[])
|
||||||
const [contactsMap, setContactsMap] = React.useState({
|
const [contactsMap, setContactsMap] = React.useState({
|
||||||
所有: [
|
所有: [
|
||||||
{
|
{
|
||||||
id: '0',
|
id: '0',
|
||||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
nickname: "麻油衣酱",
|
nickname: "麻油衣酱",
|
||||||
@@ -50,7 +51,7 @@ export default function App() {
|
|||||||
id: '0',
|
id: '0',
|
||||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||||
nickname: "Maya Fey",
|
nickname: "Maya Fey",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as unknown as { [key: string]: User[] })
|
} as unknown as { [key: string]: User[] })
|
||||||
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
|
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
|
||||||
@@ -61,27 +62,16 @@ export default function App() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loginDialogRef: React.MutableRefObject<Dialog | null> = React.useRef(null)
|
const loginDialogRef: React.MutableRefObject<Dialog | null> = React.useRef(null)
|
||||||
const inputAccountRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
const loginInputAccountRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||||
const inputPasswordRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
const loginInputPasswordRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||||
const registerButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
|
||||||
const loginButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
|
||||||
|
|
||||||
useEventListener(loginButtonRef as React.MutableRefObject<Button>, 'click', async () => {
|
const registerDialogRef: React.MutableRefObject<Dialog | null> = React.useRef(null)
|
||||||
const account = inputAccountRef.current!.value
|
const registerInputUserNameRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||||
const password = inputPasswordRef.current!.value
|
const registerInputNickNameRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||||
|
const registerInputPasswordRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||||
const re = await Client.invoke("User.login", {
|
|
||||||
account: account,
|
|
||||||
password: CryptoES.SHA256(password),
|
|
||||||
})
|
|
||||||
if (re.code != 200)
|
|
||||||
snackbar({
|
|
||||||
message: "登錄失敗: " + re.msg
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
;(async () => {
|
; (async () => {
|
||||||
Split(['#SideBar', '#ChatFragment'], {
|
Split(['#SideBar', '#ChatFragment'], {
|
||||||
sizes: [25, 75],
|
sizes: [25, 75],
|
||||||
minSize: [200, 400],
|
minSize: [200, 400],
|
||||||
@@ -90,14 +80,12 @@ export default function App() {
|
|||||||
|
|
||||||
Client.connect()
|
Client.connect()
|
||||||
const re = await Client.invoke("User.auth", {
|
const re = await Client.invoke("User.auth", {
|
||||||
access_token: data.access_token,
|
access_token: data.access_token || '',
|
||||||
})
|
})
|
||||||
if (re.code == 401)
|
if (re.code == 401)
|
||||||
loginDialogRef.current!.open = true
|
loginDialogRef.current!.open = true
|
||||||
else if (re.code != 200)
|
else if (re.code != 200)
|
||||||
snackbar({
|
if (checkApiSuccessOrSncakbar(re, "驗證失敗")) return
|
||||||
message: "驗證失敗: " + re.msg
|
|
||||||
})
|
|
||||||
})()
|
})()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -110,10 +98,18 @@ export default function App() {
|
|||||||
}}>
|
}}>
|
||||||
<LoginDialog
|
<LoginDialog
|
||||||
loginDialogRef={loginDialogRef}
|
loginDialogRef={loginDialogRef}
|
||||||
inputAccountRef={inputAccountRef}
|
loginInputAccountRef={loginInputAccountRef}
|
||||||
inputPasswordRef={inputPasswordRef}
|
loginInputPasswordRef={loginInputPasswordRef}
|
||||||
registerButtonRef={registerButtonRef}
|
registerDialogRef={registerDialogRef} />
|
||||||
loginButtonRef={loginButtonRef} />
|
|
||||||
|
<RegisterDialog
|
||||||
|
registerDialogRef={registerDialogRef}
|
||||||
|
registerInputUserNameRef={registerInputUserNameRef}
|
||||||
|
registerInputNickNameRef={registerInputNickNameRef}
|
||||||
|
registerInputPasswordRef={registerInputPasswordRef}
|
||||||
|
loginInputAccountRef={loginInputAccountRef}
|
||||||
|
loginInputPasswordRef={loginInputPasswordRef} />
|
||||||
|
|
||||||
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
|
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
|
||||||
<mdui-button-icon icon="menu" slot="top"></mdui-button-icon>
|
<mdui-button-icon icon="menu" slot="top"></mdui-button-icon>
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,50 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Button, Dialog, TextField } from "mdui";
|
import { Button, Dialog, TextField } from "mdui"
|
||||||
|
import useEventListener from "../useEventListener.ts"
|
||||||
|
import { checkApiSuccessOrSncakbar } from "../snackbar.ts"
|
||||||
|
import Client from "../../api/Client.ts"
|
||||||
|
|
||||||
|
import * as CryptoES from 'crypto-es'
|
||||||
|
import data from "../../Data.ts";
|
||||||
|
|
||||||
interface Refs {
|
interface Refs {
|
||||||
inputAccountRef: React.MutableRefObject<TextField | null>
|
loginInputAccountRef: React.MutableRefObject<TextField | null>
|
||||||
inputPasswordRef: React.MutableRefObject<TextField | null>
|
loginInputPasswordRef: React.MutableRefObject<TextField | null>
|
||||||
registerButtonRef: React.MutableRefObject<Button | null>
|
|
||||||
loginButtonRef: React.MutableRefObject<Button | null>
|
|
||||||
loginDialogRef: React.MutableRefObject<Dialog | null>
|
loginDialogRef: React.MutableRefObject<Dialog | null>
|
||||||
|
registerDialogRef: React.MutableRefObject<Dialog | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoginDialog({
|
export default function LoginDialog({
|
||||||
inputAccountRef,
|
loginInputAccountRef,
|
||||||
inputPasswordRef,
|
loginInputPasswordRef,
|
||||||
registerButtonRef,
|
loginDialogRef,
|
||||||
loginButtonRef,
|
registerDialogRef
|
||||||
loginDialogRef
|
|
||||||
}: Refs) {
|
}: Refs) {
|
||||||
return (
|
const loginButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||||
<mdui-dialog headline="登录" ref={loginDialogRef}>
|
const registerButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||||
|
useEventListener(registerButtonRef, 'click', () => registerDialogRef.current!.open = true)
|
||||||
|
useEventListener(loginButtonRef, 'click', async () => {
|
||||||
|
const account = loginInputAccountRef.current!.value
|
||||||
|
const password = loginInputPasswordRef.current!.value
|
||||||
|
|
||||||
<mdui-text-field label="账号" ref={inputAccountRef}></mdui-text-field>
|
const re = await Client.invoke("User.login", {
|
||||||
|
account: account,
|
||||||
|
password: CryptoES.SHA256(password).toString(CryptoES.Hex),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (checkApiSuccessOrSncakbar(re, "登錄失敗")) return
|
||||||
|
|
||||||
|
data.access_token = re.data!.access_token as string
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<mdui-dialog headline="登錄" ref={loginDialogRef}>
|
||||||
|
|
||||||
|
<mdui-text-field label="用戶 ID / 用戶名" ref={loginInputAccountRef}></mdui-text-field>
|
||||||
<div style={{
|
<div style={{
|
||||||
height: "10px",
|
height: "10px",
|
||||||
}}></div>
|
}}></div>
|
||||||
<mdui-text-field label="密码" ref={inputPasswordRef}></mdui-text-field>
|
<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={registerButtonRef}>注册</mdui-button>
|
||||||
<mdui-button slot="action" variant="text" ref={loginButtonRef}>登录</mdui-button>
|
<mdui-button slot="action" variant="text" ref={loginButtonRef}>登录</mdui-button>
|
||||||
|
|||||||
67
client/ui/dialog/RegisterDialog.tsx
Normal file
67
client/ui/dialog/RegisterDialog.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { Button, Dialog, TextField } from "mdui";
|
||||||
|
import useEventListener from "../useEventListener.ts";
|
||||||
|
import Client from "../../api/Client.ts";
|
||||||
|
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts";
|
||||||
|
|
||||||
|
import * as CryptoES from 'crypto-es'
|
||||||
|
|
||||||
|
interface Refs {
|
||||||
|
loginInputAccountRef: React.MutableRefObject<TextField | null>
|
||||||
|
loginInputPasswordRef: React.MutableRefObject<TextField | null>
|
||||||
|
registerInputUserNameRef: React.MutableRefObject<TextField | null>
|
||||||
|
registerInputNickNameRef: React.MutableRefObject<TextField | null>
|
||||||
|
registerInputPasswordRef: React.MutableRefObject<TextField | null>
|
||||||
|
registerDialogRef: React.MutableRefObject<Dialog | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RegisterDialog({
|
||||||
|
loginInputAccountRef,
|
||||||
|
loginInputPasswordRef,
|
||||||
|
registerInputUserNameRef,
|
||||||
|
registerInputNickNameRef,
|
||||||
|
registerInputPasswordRef,
|
||||||
|
registerDialogRef
|
||||||
|
}: Refs) {
|
||||||
|
const registerBackButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||||
|
const doRegisterButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||||
|
useEventListener(registerBackButtonRef, 'click', () => registerDialogRef.current!.open = false)
|
||||||
|
useEventListener(doRegisterButtonRef, 'click', async () => {
|
||||||
|
const username = registerInputUserNameRef.current!.value
|
||||||
|
const re = await Client.invoke("User.register", {
|
||||||
|
username: username,
|
||||||
|
nickname: registerInputNickNameRef.current!.value,
|
||||||
|
password: CryptoES.SHA256(registerInputPasswordRef.current!.value).toString(CryptoES.Hex),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (checkApiSuccessOrSncakbar(re, "注冊失敗")) return
|
||||||
|
loginInputAccountRef.current!.value = username == "" ? re.data!.userid as string : username
|
||||||
|
loginInputPasswordRef.current!.value = registerInputPasswordRef.current!.value
|
||||||
|
|
||||||
|
registerInputUserNameRef.current!.value = ""
|
||||||
|
registerInputNickNameRef.current!.value = ""
|
||||||
|
registerInputPasswordRef.current!.value = ""
|
||||||
|
registerDialogRef.current!.open = false
|
||||||
|
snackbar({
|
||||||
|
message: "注冊成功!",
|
||||||
|
placement: "top",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<mdui-dialog headline="注冊" ref={registerDialogRef}>
|
||||||
|
|
||||||
|
<mdui-text-field label="用戶名 (可選)" ref={registerInputUserNameRef}></mdui-text-field>
|
||||||
|
<div style={{
|
||||||
|
height: "10px",
|
||||||
|
}}></div>
|
||||||
|
<mdui-text-field label="昵稱" ref={registerInputNickNameRef}></mdui-text-field>
|
||||||
|
<div style={{
|
||||||
|
height: "10px",
|
||||||
|
}}></div>
|
||||||
|
<mdui-text-field label="密码" type="password" toggle-password ref={registerInputPasswordRef}></mdui-text-field>
|
||||||
|
|
||||||
|
<mdui-button slot="action" variant="text" ref={registerBackButtonRef}>返回</mdui-button>
|
||||||
|
<mdui-button slot="action" variant="text" ref={doRegisterButtonRef}>注冊</mdui-button>
|
||||||
|
</mdui-dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
98
client/ui/snackbar.ts
Normal file
98
client/ui/snackbar.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { snackbar as mduiSnackbar, Snackbar } from "mdui"
|
||||||
|
import ApiCallbackMessage from "../api/ApiCallbackMessage.ts"
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
/**
|
||||||
|
* Snackbar 出现的位置。默认为 `bottom`。可选值为:
|
||||||
|
* * `top`:位于顶部,居中对齐
|
||||||
|
* * `top-start`:位于顶部,左对齐
|
||||||
|
* * `top-end`:位于顶部,右对齐
|
||||||
|
* * `bottom`:位于底部,居中对齐
|
||||||
|
* * `bottom-start`:位于底部,左对齐
|
||||||
|
* * `bottom-end`:位于底部,右对齐
|
||||||
|
*/
|
||||||
|
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
|
||||||
|
/**
|
||||||
|
* 操作按钮的文本
|
||||||
|
*/
|
||||||
|
action?: string;
|
||||||
|
/**
|
||||||
|
* 是否在右侧显示关闭按钮
|
||||||
|
*/
|
||||||
|
closeable?: boolean;
|
||||||
|
/**
|
||||||
|
* 消息文本最多显示几行。默认不限制行数。可选值为
|
||||||
|
* * `1`:消息文本最多显示一行
|
||||||
|
* * `2`:消息文本最多显示两行
|
||||||
|
*/
|
||||||
|
messageLine?: 1 | 2;
|
||||||
|
/**
|
||||||
|
* 在多长时间后自动关闭(单位为毫秒)。设置为 0 时,不自动关闭。默认为 5 秒后自动关闭。
|
||||||
|
*/
|
||||||
|
autoCloseDelay?: number;
|
||||||
|
/**
|
||||||
|
* 点击或触摸 Snackbar 以外的区域时是否关闭 Snackbar
|
||||||
|
*/
|
||||||
|
closeOnOutsideClick?: boolean;
|
||||||
|
/**
|
||||||
|
* 队列名称。
|
||||||
|
* 默认不启用队列,在多次调用该函数时,将同时显示多个 snackbar。
|
||||||
|
* 可在该参数中传入一个队列名称,具有相同队列名称的 snackbar 函数,将在上一个 snackbar 关闭后才打开下一个 snackbar。
|
||||||
|
*/
|
||||||
|
queue?: string;
|
||||||
|
/**
|
||||||
|
* 点击 Snackbar 时的回调函数。
|
||||||
|
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||||
|
* @param snackbar
|
||||||
|
*/
|
||||||
|
onClick?: (snackbar: Snackbar) => void;
|
||||||
|
/**
|
||||||
|
* 点击操作按钮时的回调函数。
|
||||||
|
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||||
|
* 默认点击后会关闭 snackbar;若返回值为 false,则不关闭 snackbar;若返回值为 promise,则将在 promise 被 resolve 后,关闭 snackbar。
|
||||||
|
* @param snackbar
|
||||||
|
*/
|
||||||
|
onActionClick?: (snackbar: Snackbar) => void | boolean | Promise<void>;
|
||||||
|
/**
|
||||||
|
* Snackbar 开始显示时的回调函数。
|
||||||
|
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||||
|
* @param snackbar
|
||||||
|
*/
|
||||||
|
onOpen?: (snackbar: Snackbar) => void;
|
||||||
|
/**
|
||||||
|
* Snackbar 显示动画完成时的回调函数。
|
||||||
|
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||||
|
* @param snackbar
|
||||||
|
*/
|
||||||
|
onOpened?: (snackbar: Snackbar) => void;
|
||||||
|
/**
|
||||||
|
* Snackbar 开始隐藏时的回调函数。
|
||||||
|
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||||
|
* @param snackbar
|
||||||
|
*/
|
||||||
|
onClose?: (snackbar: Snackbar) => void;
|
||||||
|
/**
|
||||||
|
* Snackbar 隐藏动画完成时的回调函数。
|
||||||
|
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||||
|
* @param snackbar
|
||||||
|
*/
|
||||||
|
onClosed?: (snackbar: Snackbar) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SnackbarOptions extends Options {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkApiSuccessOrSncakbar(re: ApiCallbackMessage, msg_ahead: string, opinions_override: Options = {}): Snackbar | null {
|
||||||
|
return re.code != 200 ? snackbar(
|
||||||
|
Object.assign({
|
||||||
|
message: `${msg_ahead}: ${re.msg} [${re.code}]`,
|
||||||
|
placement: "top",
|
||||||
|
} as SnackbarOptions, opinions_override)
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function snackbar(opinions: SnackbarOptions) {
|
||||||
|
opinions.autoCloseDelay == null && (opinions.autoCloseDelay = 2500)
|
||||||
|
return mduiSnackbar(opinions)
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
export default function useEventListener<T extends HTMLElement>(ref: React.MutableRefObject<T>, eventName: string, callback: (event: Event) => void) {
|
export default function useEventListener<T extends HTMLElement | 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)
|
||||||
}, [ref, eventName, callback])
|
}, [ref, eventName, callback])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import path from 'node:path'
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import deno from '@deno/vite-plugin'
|
import deno from '@deno/vite-plugin'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"server": "deno task build && deno run --allow-read --allow-write --allow-env --allow-net --allow-sys ./server/main.ts",
|
"server": "deno task build && deno run --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run=deno ./server/main.ts",
|
||||||
"debug": "deno task build && deno run --watch --allow-read --allow-write --allow-env --allow-net --allow-sys ./server/main.ts",
|
"debug": "deno task build && deno run --watch --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run=deno ./server/main.ts",
|
||||||
"build": "cd ./client && deno task build"
|
"build": "cd ./client && deno task build"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as SocketIo from "socket.io"
|
|||||||
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
|
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
|
||||||
import EventCallbackFunction from "../typedef/EventCallbackFunction.ts"
|
import EventCallbackFunction from "../typedef/EventCallbackFunction.ts"
|
||||||
import BaseApi from "./BaseApi.ts"
|
import BaseApi from "./BaseApi.ts"
|
||||||
|
import DataWrongError from "./DataWrongError.ts";
|
||||||
|
|
||||||
export default class ApiManager {
|
export default class ApiManager {
|
||||||
static httpServer: HttpServerLike
|
static httpServer: HttpServerLike
|
||||||
@@ -40,11 +41,11 @@ 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)
|
const err = e as Error
|
||||||
try {
|
try {
|
||||||
callback({
|
callback({
|
||||||
code: 500,
|
code: err instanceof DataWrongError ? 400 : 500,
|
||||||
msg: "錯誤: " + e
|
msg: "錯誤: " + err.message
|
||||||
})
|
})
|
||||||
} catch(_e) {}
|
} catch(_e) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ export default abstract class BaseApi {
|
|||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
checkArgsEmpty(args: { [key: string]: unknown }, names: string[]) {
|
||||||
|
for (const k of names)
|
||||||
|
if (k in args && args[k] == '')
|
||||||
|
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)
|
||||||
|
|||||||
1
server/api/DataWrongError.ts
Normal file
1
server/api/DataWrongError.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default class DataWrongError extends Error {}
|
||||||
53
server/api/TokenManager.ts
Normal file
53
server/api/TokenManager.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
import config from "../config.ts"
|
||||||
|
import User from "../data/User.ts"
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
|
||||||
|
interface Token {
|
||||||
|
author: string
|
||||||
|
auth: string
|
||||||
|
made_time: number
|
||||||
|
expired_time: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKey(key: string, keyLength = 32) {
|
||||||
|
const hash = crypto.createHash('sha256')
|
||||||
|
hash.update(key)
|
||||||
|
const keyBuffer = hash.digest()
|
||||||
|
return keyLength ? keyBuffer.slice(0, keyLength) : keyBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TokenManager {
|
||||||
|
// TODO: 單令牌 -》 單 + 刷新 with 多設備管理
|
||||||
|
static makeAuth(user: User) {
|
||||||
|
return crypto.createHash("sha256").update(user.bean.id + user.getPassword() + config.salt).digest().toString('hex')
|
||||||
|
}
|
||||||
|
static encode(token: Token) {
|
||||||
|
return crypto.createCipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update(
|
||||||
|
JSON.stringify(token)
|
||||||
|
).toString('hex')
|
||||||
|
}
|
||||||
|
static decode(token: string) {
|
||||||
|
return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update(
|
||||||
|
Buffer.from(token, 'hex')
|
||||||
|
).toString()) as Token
|
||||||
|
}
|
||||||
|
|
||||||
|
static make(user: User, time: number = Date.now()) {
|
||||||
|
return this.encode({
|
||||||
|
author: user.bean.id,
|
||||||
|
auth: this.makeAuth(user),
|
||||||
|
made_time: time,
|
||||||
|
expired_time: time + (1 * 1000 * 60 * 60 * 24),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
static makeNewer(user: User, token: string) {
|
||||||
|
if (this.check(user, token))
|
||||||
|
return this.make(user, Date.now() + (1 * 1000 * 60 * 60 * 24))
|
||||||
|
}
|
||||||
|
static check(user: User, token: string) {
|
||||||
|
const tk = this.decode(token)
|
||||||
|
|
||||||
|
return this.makeAuth(user) == tk.auth
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,94 @@
|
|||||||
|
import User from "../data/User.ts";
|
||||||
import BaseApi from "./BaseApi.ts"
|
import BaseApi from "./BaseApi.ts"
|
||||||
|
import TokenManager from "./TokenManager.ts";
|
||||||
|
|
||||||
export default class UserApi extends BaseApi {
|
export default class UserApi extends BaseApi {
|
||||||
override getName(): string {
|
override getName(): string {
|
||||||
return "User"
|
return "User"
|
||||||
}
|
}
|
||||||
override onInit(): void {
|
override onInit(): void {
|
||||||
|
// 驗證
|
||||||
this.registerEvent("User.auth", (args) => {
|
this.registerEvent("User.auth", (args) => {
|
||||||
return {
|
if (this.checkArgsMissing(args, ['access_token'])) return {
|
||||||
msg: "",
|
msg: "參數缺失",
|
||||||
code: 401,
|
code: 400,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const access_token = TokenManager.decode(args.access_token as string)
|
||||||
|
|
||||||
|
if (access_token.expired_time > Date.now()) return {
|
||||||
|
msg: "登錄令牌失效",
|
||||||
|
code: 401,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
msg: "成功",
|
||||||
|
code: 200,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as Error
|
||||||
|
if (err.message.indexOf("JSON") != -1)
|
||||||
|
return {
|
||||||
|
msg: "無效的登錄令牌",
|
||||||
|
code: 401,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// 登錄
|
||||||
this.registerEvent("User.login", (args) => {
|
this.registerEvent("User.login", (args) => {
|
||||||
if (this.checkArgsMissing(args, ['account', 'password'])) return {
|
if (this.checkArgsMissing(args, ['account', 'password'])) return {
|
||||||
msg: "",
|
msg: "參數缺失",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
|
if (this.checkArgsEmpty(args, ['account', 'password'])) return {
|
||||||
|
msg: "參數不得為空",
|
||||||
code: 400,
|
code: 400,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = (User.findByUserName(args.account as string) || User.findById(args.account as string)) as User
|
||||||
|
if (user == null) return {
|
||||||
|
msg: "賬號或密碼錯誤",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.getPassword() == args.password) return {
|
||||||
|
msg: "成功",
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
access_token: TokenManager.make(user)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
msg: "",
|
msg: "賬號或密碼錯誤",
|
||||||
code: 501,
|
code: 400,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 注冊
|
||||||
|
this.registerEvent("User.register", (args) => {
|
||||||
|
if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
|
||||||
|
msg: "參數缺失",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
|
if (this.checkArgsEmpty(args, ['nickname', 'password'])) return {
|
||||||
|
msg: "參數不得為空",
|
||||||
|
code: 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
const username: string | null = args.username as string
|
||||||
|
const nickname: string = args.nickname as string
|
||||||
|
const password: string = args.password as string
|
||||||
|
|
||||||
|
const user = User.createWithUserNameChecked(username, password, nickname, null)
|
||||||
|
|
||||||
|
return {
|
||||||
|
msg: "成功",
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
userid: user.bean.id
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const prefix = isCompilingClient ? '.' : ''
|
|||||||
const default_data_path = "./thewhitesilk_data"
|
const default_data_path = "./thewhitesilk_data"
|
||||||
let config = {
|
let config = {
|
||||||
data_path: default_data_path,
|
data_path: default_data_path,
|
||||||
|
salt: "TWS_Demo",
|
||||||
|
aes_key: "01234567890123456",
|
||||||
server: {
|
server: {
|
||||||
use: "http",
|
use: "http",
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import config from '../config.ts'
|
|||||||
import UserBean from './UserBean.ts'
|
import UserBean from './UserBean.ts'
|
||||||
|
|
||||||
import FileManager from './FileManager.ts'
|
import FileManager from './FileManager.ts'
|
||||||
|
import { SQLInputValue } from "node:sqlite";
|
||||||
|
import DataWrongError from "../api/DataWrongError.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User.ts - Wrapper and manager
|
* User.ts - Wrapper and manager
|
||||||
@@ -18,12 +20,13 @@ import FileManager from './FileManager.ts'
|
|||||||
export default class User {
|
export default class User {
|
||||||
static table_name: string = "Users"
|
static table_name: string = "Users"
|
||||||
private static database: DatabaseSync = User.init()
|
private static database: DatabaseSync = User.init()
|
||||||
private static init(): DatabaseSync {
|
private static init() {
|
||||||
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, User.table_name + '.db'))
|
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, User.table_name + '.db'))
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS ${User.table_name} (
|
CREATE TABLE IF NOT EXISTS ${User.table_name} (
|
||||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
/* 用户 ID, 哈希 */ id TEXT,
|
/* 用户 ID, UUID */ id TEXT,
|
||||||
|
/* 密碼, 哈希 */ password TEXT,
|
||||||
/* 注册时间, 时间戳 */ registered_time INT8 NOT NULL,
|
/* 注册时间, 时间戳 */ registered_time INT8 NOT NULL,
|
||||||
/* 用戶名, 可選 */ username TEXT,
|
/* 用戶名, 可選 */ username TEXT,
|
||||||
/* 昵称 */ nickname TEXT NOT NULL,
|
/* 昵称 */ nickname TEXT NOT NULL,
|
||||||
@@ -34,29 +37,32 @@ export default class User {
|
|||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
static createWithUserNameChecked(userName: string | null, nickName: string, avatar: Buffer | null): User {
|
static createWithUserNameChecked(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
|
||||||
if (User.findAllBeansByCondition('username = ?', userName).length > 0)
|
if (userName && User.findAllBeansByCondition('username = ?', userName).length > 0)
|
||||||
throw new Error(`用户名 ${userName} 已存在`)
|
throw new DataWrongError(`用户名 ${userName} 已存在`)
|
||||||
return User.create(
|
return User.create(
|
||||||
userName,
|
userName,
|
||||||
|
password,
|
||||||
nickName,
|
nickName,
|
||||||
avatar
|
avatar
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(userName: string | null, nickName: string, avatar: Buffer | null): User {
|
static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
|
||||||
const user = new User(
|
const user = new User(
|
||||||
User.findAllBeansByCondition(
|
User.findAllBeansByCondition(
|
||||||
'count = ?',
|
'count = ?',
|
||||||
User.database.prepare(`INSERT INTO ${User.table_name} (
|
User.database.prepare(`INSERT INTO ${User.table_name} (
|
||||||
id,
|
id,
|
||||||
|
password,
|
||||||
registered_time,
|
registered_time,
|
||||||
username,
|
username,
|
||||||
nickname,
|
nickname,
|
||||||
avatar_file_hash,
|
avatar_file_hash,
|
||||||
settings
|
settings
|
||||||
) VALUES (?, ?, ?, ?, ?, ?);`).run(
|
) VALUES (?, ?, ?, ?, ?, ?, ?);`).run(
|
||||||
crypto.randomUUID(),
|
crypto.randomUUID(),
|
||||||
|
password,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
userName,
|
userName,
|
||||||
nickName,
|
nickName,
|
||||||
@@ -69,21 +75,21 @@ export default class User {
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
private static findAllBeansByCondition(condition: string, ...args: unknown[]): UserBean[] {
|
private static findAllBeansByCondition(condition: string, ...args: SQLInputValue[]): UserBean[] {
|
||||||
return User.database.prepare(`SELECT * FROM ${User.table_name} WHERE ${condition};`).all(...args)
|
return User.database.prepare(`SELECT * FROM ${User.table_name} WHERE ${condition};`).all(...args) as unknown as UserBean[]
|
||||||
}
|
}
|
||||||
static findById(id: string): User {
|
static findById(id: string) {
|
||||||
const beans = User.findAllBeansByCondition('id = ?', id)
|
const beans = User.findAllBeansByCondition('id = ?', id)
|
||||||
if (beans.length == 0)
|
if (beans.length == 0)
|
||||||
throw new Error(`找不到用户 ID 为 ${id} 的用户`)
|
return null
|
||||||
else if (beans.length > 1)
|
else if (beans.length > 1)
|
||||||
console.error(chalk.red(`警告: 查询 id = ${id} 时, 查询到多个相同用户 ID 的用户`))
|
console.error(chalk.red(`警告: 查询 id = ${id} 时, 查询到多个相同用户 ID 的用户`))
|
||||||
return new User(beans[0])
|
return new User(beans[0])
|
||||||
}
|
}
|
||||||
static findByUserName(userName: string): User {
|
static findByUserName(userName: string) {
|
||||||
const beans = User.findAllBeansByCondition('username = ?', userName)
|
const beans = User.findAllBeansByCondition('username = ?', userName)
|
||||||
if (beans.length == 0)
|
if (beans.length == 0)
|
||||||
throw new Error(`找不到用户名为 ${userName} 的用户`)
|
return null
|
||||||
else if (beans.length > 1)
|
else if (beans.length > 1)
|
||||||
console.error(chalk.red(`警告: 查询 username = ${userName} 时, 查询到多个相同用户名的用户`))
|
console.error(chalk.red(`警告: 查询 username = ${userName} 时, 查询到多个相同用户名的用户`))
|
||||||
return new User(beans[0])
|
return new User(beans[0])
|
||||||
@@ -94,30 +100,36 @@ export default class User {
|
|||||||
this.bean = bean
|
this.bean = bean
|
||||||
}
|
}
|
||||||
/* 一切的基础都是 count ID */
|
/* 一切的基础都是 count ID */
|
||||||
private setAttr(key: string, value: unknown): void {
|
private setAttr(key: string, value: unknown) {
|
||||||
User.database.prepare(`UPDATE ${User.table_name} SET ${key} = ? WHERE count = ?`).run(value, this.bean.count)
|
User.database.prepare(`UPDATE ${User.table_name} SET ${key} = ? WHERE count = ?`).run(value, this.bean.count)
|
||||||
this.bean[key] = value
|
this.bean[key] = value
|
||||||
}
|
}
|
||||||
getUserName(): string | null {
|
getUserName(): string | null {
|
||||||
return this.bean.username
|
return this.bean.username
|
||||||
}
|
}
|
||||||
setUserName(userName: string): void {
|
setUserName(userName: string) {
|
||||||
this.setAttr("username", userName)
|
this.setAttr("username", userName)
|
||||||
}
|
}
|
||||||
getNickName(): string {
|
getNickName(): string {
|
||||||
return this.bean.nickname
|
return this.bean.nickname
|
||||||
}
|
}
|
||||||
setNickName(nickName: string): void {
|
setNickName(nickName: string) {
|
||||||
this.setAttr("nickname", nickName)
|
this.setAttr("nickname", nickName)
|
||||||
}
|
}
|
||||||
|
getPassword(): string {
|
||||||
|
return this.bean.password
|
||||||
|
}
|
||||||
|
setPassword(password: string) {
|
||||||
|
this.setAttr("password", password)
|
||||||
|
}
|
||||||
getAvatar(): Buffer | null {
|
getAvatar(): Buffer | null {
|
||||||
return FileManager.findByHash(this.bean.avatar_file_hash)?.readSync()
|
return FileManager.findByHash(this.bean.avatar_file_hash)?.readSync()
|
||||||
}
|
}
|
||||||
setAvatar(avatar: Buffer): void {
|
setAvatar(avatar: Buffer) {
|
||||||
this.setAttr("avatar_file_hash", FileManager.uploadFile(`avatar_user_${this.bean.count}`, avatar).getHash())
|
this.setAttr("avatar_file_hash", FileManager.uploadFile(`avatar_user_${this.bean.count}`, avatar).getHash())
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings(): User.Settings {
|
getSettings() {
|
||||||
return new User.Settings(this, JSON.parse(this.bean.settings))
|
return new User.Settings(this, JSON.parse(this.bean.settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export default class UserBean {
|
export default class UserBean {
|
||||||
declare count: number
|
declare count: number
|
||||||
|
declare id: string
|
||||||
|
declare password: string
|
||||||
declare username: string | null
|
declare username: string | null
|
||||||
declare registered_time: number
|
declare registered_time: number
|
||||||
declare nickname: string
|
declare nickname: string
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import https from 'node:https'
|
|||||||
import readline from 'node:readline'
|
import readline from 'node:readline'
|
||||||
import process from "node:process"
|
import process from "node:process"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
|
import child_process from "node:child_process"
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -20,12 +21,12 @@ app.use((req, res, next) => {
|
|||||||
app.use('/', express.static(config.data_path + '/page_compiled'))
|
app.use('/', express.static(config.data_path + '/page_compiled'))
|
||||||
|
|
||||||
const httpServer: HttpServerLike = (
|
const httpServer: HttpServerLike = (
|
||||||
((config.server.use == 'http') && http.createServer(app)) ||
|
((config.server.use == 'http') && http.createServer(app)) ||
|
||||||
((config.server.use == 'https') && https.createServer(config.server.https, app)) ||
|
((config.server.use == 'https') && https.createServer(config.server.https, app)) ||
|
||||||
http.createServer(app)
|
http.createServer(app)
|
||||||
)
|
)
|
||||||
const io = new SocketIo.Server(httpServer, {
|
const io = new SocketIo.Server(httpServer, {
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ApiManager.initServer(httpServer, io)
|
ApiManager.initServer(httpServer, io)
|
||||||
@@ -33,13 +34,24 @@ ApiManager.initEvents()
|
|||||||
ApiManager.initAllApis()
|
ApiManager.initAllApis()
|
||||||
|
|
||||||
httpServer.listen(config.server.listen)
|
httpServer.listen(config.server.listen)
|
||||||
console.log(chalk.yellow("===== TheWhiteSilk Server ====="))
|
|
||||||
console.log(chalk.green("API & Web 服務已經開始運作"))
|
console.log(chalk.green("API & Web 服務已經開始運作"))
|
||||||
|
function help() {
|
||||||
|
console.log(chalk.yellow("===== TheWhiteSilk Server ====="))
|
||||||
|
console.log(chalk.yellow("b - 重新編譯前端"))
|
||||||
|
}
|
||||||
|
help()
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: process.stdout
|
||||||
})
|
})
|
||||||
rl.on('line', async (text) => {
|
rl.on('line', (text) => {
|
||||||
|
if (text == 'b') {
|
||||||
|
console.log(chalk.green("重新編譯..."))
|
||||||
|
child_process.spawnSync("deno", ["task", "build"], {
|
||||||
|
stdio: [process.stdin, process.stdout, process.stderr]
|
||||||
|
})
|
||||||
|
console.log(chalk.green("✓ 編譯完畢"))
|
||||||
|
help()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user