修改webui

This commit is contained in:
纸凤孤凰
2024-11-25 02:17:48 +08:00
parent 152be29739
commit 3fbed815a5
18 changed files with 847 additions and 521 deletions

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" /> <link rel="icon" type="image/svg+xml" href="./logo_webui.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NapCat WebUI</title> <title>NapCat WebUI</title>
</head> </head>

View File

@@ -14,7 +14,7 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"tdesign-icons-vue-next": "^0.3.3", "tdesign-icons-vue-next": "^0.3.3",
"tdesign-vue-next": "^1.10.3", "tdesign-vue-next": "^1.10.3",
"vue": "^3.5.12", "vue": "^3.5.13",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -1,7 +1,119 @@
<template> <template>
<div id="app"> <div id="app" theme-mode="dark">
<router-view /> <router-view />
</div> </div>
<div v-if="show">
<t-sticky-tool
shape="round"
placement="right-bottom"
:offset="[-50, 10]"
@click="changeTheme"
>
<t-sticky-item label="浅色" popup="切换浅色模式" >
<template #icon><sunny-icon/></template>
</t-sticky-item>
<t-sticky-item label="深色" popup="切换深色模式" >
<template #icon><mode-dark-icon/></template>
</t-sticky-item>
<t-sticky-item label="自动" popup="跟随系统" >
<template #icon><control-platform-icon/></template>
</t-sticky-item>
</t-sticky-tool>
</div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';
import { ControlPlatformIcon, ModeDarkIcon, SunnyIcon } from 'tdesign-icons-vue-next';
const smallScreen = window.matchMedia('(max-width: 768px)');
interface Item {
label: string;
popup: string;
}
interface Context {
item: Item;
}
enum ThemeMode {
Dark = 'dark',
Light = 'light',
Auto = 'auto',
}
const themeLabelMap: Record<string, ThemeMode> = {
'浅色': ThemeMode.Light,
'深色': ThemeMode.Dark,
'自动': ThemeMode.Auto,
};
const show = ref<boolean>(true)
const createSetThemeAttributeFunction = () => {
let mediaQueryForAutoTheme: MediaQueryList | null = null;
return (mode: ThemeMode | null) => {
const element = document.documentElement;
if (mode === ThemeMode.Dark) {
element.setAttribute('theme-mode', ThemeMode.Dark);
} else if (mode === ThemeMode.Light) {
element.removeAttribute('theme-mode');
} else if (mode === ThemeMode.Auto) {
mediaQueryForAutoTheme = window.matchMedia('(prefers-color-scheme: dark)');
const handleMediaChange = (e: MediaQueryListEvent) => {
if (e.matches) {
element.setAttribute('theme-mode', ThemeMode.Dark);
} else {
element.removeAttribute('theme-mode');
}
};
mediaQueryForAutoTheme.addEventListener('change', handleMediaChange);
const event = new Event('change');
Object.defineProperty(event,'matches', {
value: mediaQueryForAutoTheme.matches,
writable: false
});
mediaQueryForAutoTheme.dispatchEvent(event);
onBeforeUnmount(() => {
if (mediaQueryForAutoTheme) {
mediaQueryForAutoTheme.removeEventListener('change', handleMediaChange);
}
});
}
};
};
const setThemeAttribute = createSetThemeAttributeFunction();
const getStoredTheme = (): ThemeMode | null => {
return localStorage.getItem('theme') as ThemeMode | null;
};
const initTheme = () => {
const storedTheme = getStoredTheme();
if (storedTheme === null) {
setThemeAttribute(ThemeMode.Auto);
} else {
setThemeAttribute(storedTheme);
}
};
const changeTheme = (context: Context) => {
const themeLabel = themeLabelMap[context.item.label] as ThemeMode;
console.log(themeLabel);
setThemeAttribute(themeLabel);
localStorage.setItem('theme', themeLabel);
};
const haddingFbars = () => {
show.value = !smallScreen.matches;
if (smallScreen.matches) {
localStorage.setItem('theme', 'auto');
}
};
onMounted(() => {
initTheme();
haddingFbars()
window.addEventListener('resize', haddingFbars);
});
onUnmounted(() => {
window.removeEventListener('resize', haddingFbars);
});
</script>
<style scoped>
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -1,16 +1,18 @@
<template> <template>
<div class="dashboard-container"> <t-layout class="dashboard-container">
<div ref="menuRef">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" /> <SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
<div class="content"> </div>
<t-layout>
<router-view /> <router-view />
</div> </t-layout>
</div> </t-layout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import SidebarMenu from './webui/Nav.vue'; import SidebarMenu from './webui/Nav.vue';
import emitter from '@/ts/event-bus';
interface MenuItem { interface MenuItem {
value: string; value: string;
icon: string; icon: string;
@@ -18,6 +20,7 @@ interface MenuItem {
route: string; route: string;
} }
const menuItems = ref<MenuItem[]>([ const menuItems = ref<MenuItem[]>([
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' }, { value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' }, { value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
@@ -25,13 +28,23 @@ const menuItems = ref<MenuItem[]>([
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' }, { value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' }, { value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
]); ]);
const menuRef=ref<any>(null)
emitter.on('sendMenu', (event) => {
emitter.emit('sendWidth',menuRef.value.offsetWidth);
localStorage.setItem('menuWidth', menuRef.value.offsetWidth);
});
onMounted(() => {
localStorage.setItem('menuWidth', menuRef.value.offsetWidth);
});
</script> </script>
<style scoped> <style scoped>
.dashboard-container { .dashboard-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100vh; height: 100vh;
width: 100%;
} }
.sidebar-menu { .sidebar-menu {
@@ -39,13 +52,6 @@ const menuItems = ref<MenuItem[]>([
z-index: 2; z-index: 2;
} }
.content {
flex: 1;
/* padding: 20px; */
overflow: auto;
position: relative;
z-index: 1;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.content { .content {

View File

@@ -1,7 +1,9 @@
<template> <template>
<t-card class="layout">
<div class="login-container"> <div class="login-container">
<h2 class="sotheby-font">QQ Login</h2> <h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods"> <div class="login-methods">
<t-tooltip content="快速登录" >
<t-button <t-button
id="quick-login" id="quick-login"
class="login-method" class="login-method"
@@ -9,6 +11,8 @@
@click="loginMethod = 'quick'" @click="loginMethod = 'quick'"
>Quick Login</t-button >Quick Login</t-button
> >
</t-tooltip>
<t-tooltip content="二维码登录" >
<t-button <t-button
id="qrcode-login" id="qrcode-login"
class="login-method" class="login-method"
@@ -16,6 +20,7 @@
@click="loginMethod = 'qrcode'" @click="loginMethod = 'qrcode'"
>QR Code</t-button >QR Code</t-button
> >
</t-tooltip>
</div> </div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form"> <div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select <t-select
@@ -31,6 +36,8 @@
<canvas ref="qrcodeCanvas"></canvas> <canvas ref="qrcodeCanvas"></canvas>
</div> </div>
</div> </div>
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
</t-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -95,14 +102,16 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.layout{
height: 100vh;
}
.login-container { .login-container {
padding: 20px; padding: 20px;
border-radius: 5px; border-radius: 5px;
background-color: white;
max-width: 400px; max-width: 400px;
min-width: 300px; min-width: 300px;
position: relative; position: relative;
margin: 0 auto; margin: 50px auto;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@@ -161,7 +170,5 @@ onMounted(() => {
bottom: 20px; bottom: 20px;
left: 0; left: 0;
right: 0; right: 0;
width: 100%;
background-color: white;
} }
</style> </style>

View File

@@ -1,4 +1,5 @@
<template> <template>
<t-card class="layout">
<div class="login-container"> <div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2> <h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit"> <t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
@@ -14,7 +15,8 @@
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
<div class="footer">Power By NapCat.WebUi</div> <t-footer class="footer">Power By NapCat.WebUi</t-footer>
</t-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -94,14 +96,16 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
</script> </script>
<style scoped> <style scoped>
.layout{
height: 100vh;
}
.login-container { .login-container {
padding: 20px; padding: 20px;
border-radius: 5px; border-radius: 5px;
background-color: white;
max-width: 400px; max-width: 400px;
min-width: 300px; min-width: 300px;
position: relative; position: relative;
margin: 0 auto; margin: 50px auto;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@@ -145,7 +149,5 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
bottom: 20px; bottom: 20px;
left: 0; left: 0;
right: 0; right: 0;
width: 100%;
background-color: white;
} }
</style> </style>

View File

@@ -1,16 +1,25 @@
<template> <template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu"> <t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
<template #logo> </template> <template #logo>
<div class="logo">
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
<div class="logo-textBox">
<div class="logo-text">{{ collapsed? '' : 'NapCat' }}</div>
</div>
</div>
</template>
<router-link v-for="item in menuItems" :key="item.value" :to="item.route"> <router-link v-for="item in menuItems" :key="item.value" :to="item.route">
<t-tooltip :disabled="!collapsed" :content="item.label" placement="right">
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item"> <t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
<template #icon> <template #icon>
<t-icon :name="item.icon" /> <t-icon :name="item.icon" />
</template> </template>
{{ item.label }} {{ item.label }}
</t-menu-item> </t-menu-item>
</t-tooltip>
</router-link> </router-link>
<template #operations> <template #operations>
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed"> <t-button :disabled="disBtn" class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
<template #icon><t-icon :name="iconName" /></template> <template #icon><t-icon :name="iconName" /></template>
</t-button> </t-button>
</template> </template>
@@ -18,7 +27,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps } from 'vue'; import { ref, defineProps, onMounted, watch } from 'vue';
import emitter from '@/ts/event-bus';
type MenuItem = { type MenuItem = {
value: string; value: string;
@@ -31,15 +41,39 @@ type MenuItem = {
defineProps<{ defineProps<{
menuItems: MenuItem[]; menuItems: MenuItem[];
}>(); }>();
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true'); const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold'); const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
const disBtn = ref<boolean>(false);
const changeCollapsed = (): void => { const changeCollapsed = (): void => {
collapsed.value = !collapsed.value; collapsed.value = !collapsed.value;
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold'; iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
localStorage.setItem('sidebar-collapsed', collapsed.value.toString()); localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
}; };
watch(collapsed, (newValue, oldValue) => {
setTimeout(()=>{
emitter.emit('sendMenu', collapsed.value);
},300)
});
onMounted(() => {
const mediaQuery = window.matchMedia('(max-width: 800px)');
const handleMediaChange = (e: MediaQueryListEvent) => {
disBtn.value = e.matches;
if (e.matches) {
collapsed.value = e.matches;
}
};
mediaQuery.addEventListener('change', handleMediaChange);
const event = new Event('change');
Object.defineProperty(event,'matches', {
value: mediaQuery.matches,
writable: false
});
mediaQuery.dispatchEvent(event);
return () => {
mediaQuery.removeEventListener('change', handleMediaChange);
};
});
</script> </script>
<style scoped> <style scoped>
@@ -57,12 +91,28 @@ const changeCollapsed = (): void => {
width: 100px; /* 移动端侧边栏宽度 */ width: 100px; /* 移动端侧边栏宽度 */
} }
} }
.logo{
display: flex;
width: auto;
height: 100%;
}
.logo-img{
object-fit: contain;
margin-top: 8px;
margin-bottom: 8px;
}
.logo-textBox{
display: flex;
align-items: center;
margin-left: 10px;
}
.logo-text { .logo-text {
display: block; display: block;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 22px;
font-family: Sotheby, Helvetica, monospace;
} }
.menu-item { .menu-item {

View File

@@ -27,10 +27,17 @@ import {
Popup as TPopup, Popup as TPopup,
Dialog as TDialog, Dialog as TDialog,
Switch as TSwitch, Switch as TSwitch,
Tooltip as Tooltip,
StickyTool as TStickyTool,
StickyItem as TStickyItem,
Layout as TLayout,
Content as TContent,
Footer as TFooter,
Aside as TAside,
Popconfirm as Tpopconfirm,
} from 'tdesign-vue-next'; } from 'tdesign-vue-next';
import { router } from './router'; import { router } from './router';
import 'tdesign-vue-next/es/style/index.css'; import 'tdesign-vue-next/es/style/index.css';
const app = createApp(App); const app = createApp(App);
app.use(router); app.use(router);
app.use(TButton); app.use(TButton);
@@ -59,4 +66,12 @@ app.use(TCheckbox);
app.use(TPopup); app.use(TPopup);
app.use(TDialog); app.use(TDialog);
app.use(TSwitch); app.use(TSwitch);
app.use(Tooltip);
app.use(TStickyTool);
app.use(TStickyItem);
app.use(TLayout);
app.use(TContent);
app.use(TFooter);
app.use(TAside);
app.use(Tpopconfirm);
app.mount('#app'); app.mount('#app');

View File

@@ -1,122 +1,301 @@
<template> <template>
<t-space class="full-space"> <div ref="headerBox" class="title">
<template v-if="clientPanelData.length > 0"> <t-divider content="网络配置" align="left" />
<t-tabs <t-divider align="right">
v-model="activeTab" <t-button @click="addConfig()">
:addable="true" <template #icon><add-icon /></template>
theme="card" 添加配置</t-button>
@add="showAddTabDialog" </t-divider>
@remove="removeTab"
class="full-tabs"
>
<t-tab-panel
v-for="(config, idx) in clientPanelData"
:key="idx"
:label="config.name"
:removable="true"
:value="idx"
class="full-tab-panel"
>
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
<div class="button-container">
<t-button @click="saveConfig" style="width: 100px; height: 40px">保存</t-button>
</div> </div>
</t-tab-panel> <div v-if="loadPage" ref="setting" class="setting">
<t-tabs ref="tabsRef" :style="{width:tabsWidth+'px'}" default-value="all" @change="selectType">
<t-tab-panel value="all" label="全部"></t-tab-panel>
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
<t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
<t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
</t-tabs> </t-tabs>
</div>
<div v-if="loadPage" class="card-box" :style="{width:tabsWidth+'px'}">
<div class="setting-box" :style="{maxHeight:cardHeight+'px'}" v-if="cardConfig.length>0">
<div v-for="(item,index) in cardConfig" :key="index">
<t-card :title="item.name" :style="{width:cardWidth+'px'}" :header-bordered="true" class="setting-card">
<template #actions>
<t-space>
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
<t-popconfirm theme="danger" content="确认删除" @confirm="delConfig(item)">
<delete-icon size="20px" ></delete-icon>
</t-popconfirm>
</t-space>
</template> </template>
<template v-else> <div class="setting-content">
<EmptyStateComponent :showAddTabDialog="showAddTabDialog" /> <div>类型{{item.type}}</div>
</template> <div class="setting-address">
<div v-if="item.host" class="local">地址{{item.host}}</div>
<div v-if="item.url" class="local">地址{{item.url}}</div>
<div class="none-box"></div>
<div v-if="item.port" class="port">端口{{ item.port }}</div>
<div v-if="item.heartInterval&&!item.port" class="port">心跳间隔{{ item.heartInterval }}</div>
</div>
<div v-if="item.heartInterval&&item.port" class="port">心跳间隔{{ item.heartInterval }}</div>
<div class="setting-address">
<div class="local">Token{{item.token}}</div>
<div class="none-box"></div>
<div class="port">消息格式{{item.messagePostFormat}}</div>
</div>
<div class="setting-status">
<div class="status-tag">
<div >状态</div>
<t-tag class="tag-item" :theme="(item.enable ? 'success' : 'danger')"> {{item.enable ? "启用" : "禁用"}}</t-tag>
</div>
<div class="none-box"></div>
<div class="status-deBug">
<div>调试</div>
<t-tag class="tag-item" :theme="(item.debug ? 'success' : 'danger')"> {{item.debug ? "开启" : "关闭"}}</t-tag>
</div>
</div>
<div class="setting-status">
<div v-if="item.hasOwnProperty('enableWebsocket')" class="status-tag">
<div >WS </div>
<t-tag class="tag-item" :theme="(item.enableWebsocket ? 'success' : 'danger')"> {{item.enableWebsocket ? "启用" : "禁用"}}</t-tag>
</div>
<div class="none-box"></div>
<div v-if="item.hasOwnProperty('enableCors')" class="status-deBug">
<div>跨域</div>
<t-tag class="tag-item" :theme="(item.enableCors ? 'success' : 'danger')"> {{item.enableCors ? "开启" : "关闭"}}</t-tag>
</div>
</div>
<div v-if="item.hasOwnProperty('reportSelfMessage')" class="status-deBug" style="margin-top: 2px">
<div>上报自身消息</div>
<t-tag class="tag-item" :theme="(item.reportSelfMessage ? 'success' : 'danger')"> {{item.reportSelfMessage ? "开启" : "关闭"}}</t-tag>
</div>
<div v-if="item.hasOwnProperty('enableForcePushEvent')" class="status-deBug" style="margin-top: 2px">
<div>强制推送事件</div>
<t-tag class="tag-item" :theme="(item.enableForcePushEvent ? 'success' : 'danger')"> {{item.enableForcePushEvent ? "开启" : "关闭"}}</t-tag>
</div>
</div>
</t-card>
</div>
<div style="height: 20vh"></div>
</div>
<t-card class="card-none" v-else >
<div class="card-noneText">暂无网络配置</div>
</t-card>
</div>
<t-dialog <t-dialog
v-model:visible="isDialogVisible" v-model:visible="visibleBody"
header="添加网络配置" :header="dialogTitle"
@close="isDialogVisible = false" :destroy-on-close="true"
@confirm="addTab" :show-in-attached-element="true"
placement="center"
:on-confirm="saveConfig"
> >
<t-form ref="form" :model="newTab"> <div slot="body" class="dialog-body">
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name"> <t-form ref="form" :data="newTab" labelAlign="left" :model="newTab" >
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称',trigger: 'blur' }]" label="名称" name="name">
<t-input v-model="newTab.name" /> <t-input v-model="newTab.name" />
</t-form-item> </t-form-item>
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type"> <t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型',trigger: 'change'}]" label="类型" name="type">
<t-select v-model="newTab.type"> <t-select v-model="newTab.type" @change="onloadDefault">
<t-option value="httpServers">HTTP 服务器</t-option> <t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option> <t-option value="httpClients">HTTP 客户端</t-option>
<t-option value="websocketServers">WebSocket 服务器</t-option> <t-option value="websocketServers">WebSocket 服务器</t-option>
<t-option value="websocketClients">WebSocket 客户端</t-option> <t-option value="websocketClients">WebSocket 客户端</t-option>
</t-select> </t-select>
</t-form-item> </t-form-item>
<div>
<component :is="resolveDynamicComponent(getComponent(newTab.type as ConfigKey))" :config="newTab.data" />
</div>
</t-form> </t-form>
</div>
</t-dialog> </t-dialog>
</t-space>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted } from 'vue'; import { AddIcon, DeleteIcon, Edit2Icon } from 'tdesign-icons-vue-next';
import { MessagePlugin } from 'tdesign-vue-next'; import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue';
import emitter from '@/ts/event-bus';
import { import {
httpServerDefaultConfigs,
httpClientDefaultConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig, HttpClientConfig,
httpClientDefaultConfigs,
HttpServerConfig, HttpServerConfig,
WebsocketClientConfig, httpServerDefaultConfigs,
WebsocketServerConfig, mergeOneBotConfigs,
NetworkConfig, NetworkConfig,
OneBotConfig, OneBotConfig,
mergeOneBotConfigs, WebsocketClientConfig,
websocketClientDefaultConfigs,
WebsocketServerConfig,
websocketServerDefaultConfigs,
} from '../../../src/onebot/config/config'; } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue'; import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue'; import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue'; import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue'; import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
import EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue'; import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
const tabsWidth=ref<number>(0)
const menuWidth=ref<number>(0)
const cardWidth=ref<number>(0)
const cardHeight=ref<number>(0)
const mediumScreen = window.matchMedia('(min-width: 768px) and (max-width: 1024px)');
const largeScreen = window.matchMedia('(min-width: 1025px)');
const headerBox=ref<any>(null)
const setting=ref<any>(null)
const loadPage=ref<boolean>(false);
const visibleBody=ref<boolean>(false);
const newTab = ref<{ name: string; data:any; type: string }>({ name: '', data:{},type: '' });
const dialogTitle = ref<string>('');
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients'; type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig; type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
type ComponentUnion =
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent;
const componentMap: Record<ConfigKey, ComponentUnion> = { const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs, httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs, httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs, websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs, websocketClients: websocketClientDefaultConfigs,
}; };
interface ConfigMap { const componentMap: Record<
httpServers: HttpServerConfig; ConfigKey,
httpClients: HttpClientConfig; | typeof HttpServerComponent
websocketServers: WebsocketServerConfig; | typeof HttpClientComponent
websocketClients: WebsocketClientConfig; | typeof WebsocketServerComponent
| typeof WebsocketClientComponent
> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
type ComponentKey = keyof typeof componentMap;
//操作类型
const operateType = ref<string>('');
//配置项索引
const configIndex = ref<number>(0);
//保存时所用数据
interface NetworkConfigType {
[key: string]: any;
websocketClients: any[];
websocketServers: any[];
httpClients: any[];
httpServers: any[];
} }
const networkConfig: NetworkConfigType = {
"websocketClients": [],
"websocketServers": [],
"httpClients": [],
"httpServers": []
};
interface ClientPanel<K extends ConfigKey = ConfigKey> { //挂载的数据
name: string; const WebConfg = ref(new Map<string, Array<null>>([
key: K; ["all", []],
data: ConfigMap[K]; ["httpServers", []],
["httpClients", []],
["websocketServers", []],
["websocketClients", []]
]));
interface TypeChType {
[key: string]: string;
httpServers: string;
httpClients: string;
websocketServers: string;
websocketClients: string;
} }
const typeCh:TypeChType = {
const activeTab = ref<number>(0); "httpServers":"HTTP 服务器" ,
const isDialogVisible = ref(false); "httpClients":"HTTP 客户端",
const newTab = ref<{ name: string; type: ConfigKey }>({ name: '', type: 'httpServers' }); "websocketServers":"WebSocket 服务器",
const clientPanelData: Ref<ClientPanel[]> = ref([]); "websocketClients":"WebSocket 客户端"
};
const getComponent = (type: ConfigKey) => { const getKeyByValue=(obj: TypeChType, value: string): string | undefined =>{
return Object.entries(obj).find(([_, v]) => v === value)?.[0];
}
const cardConfig = ref<any>([])
const getComponent = (type: ComponentKey) => {
return componentMap[type]; return componentMap[type];
}; };
const addConfig=()=>{
dialogTitle.value="添加配置"
newTab.value={ name: '', data:{},type: '' };
operateType.value='add'
visibleBody.value=true;
}
const editConfig = (item:any) => {
dialogTitle.value="修改配置"
const type = getKeyByValue(typeCh,item.type)
if (type){
newTab.value={ name: item.name, data:item,type:type };
}
operateType.value='edit'
configIndex.value=networkConfig[newTab.value.type].findIndex((obj:any) => obj.name === item.name);
visibleBody.value=true;
}
const delConfig = (item:any) => {
const type = getKeyByValue(typeCh,item.type)
if (type){
newTab.value={ name: item.name, data:item,type:type };
}
configIndex.value=configIndex.value=networkConfig[newTab.value.type].findIndex((obj:any) => obj.name === item.name);
operateType.value='delete'
saveConfig()
}
const selectType = (key: string) => {
cardConfig.value = WebConfg.value.get(key)
}
const onloadDefault = (key: ConfigKey) => {
console.log(key)
newTab.value.data=structuredClone(defaultConfigs[key])
}
//检测重名
const checkName=(name:string) =>{
const allConfigs = WebConfg.value.get('all')?.findIndex((obj:any) => obj.name === name);
if (newTab.value.name===''|| newTab.value.type===''){
MessagePlugin.error('请填写完整信息');
return false;
}
else if(allConfigs===-1||newTab.value.data.name===name){
return true;
}
else {
MessagePlugin.error('名称已存在');
return false;
}
}
//保存
const saveConfig = async () => {
if(operateType.value=='add'){
if(!checkName(newTab.value.name))return;
newTab.value.data.name=newTab.value.name
networkConfig[newTab.value.type].push(newTab.value.data);
}
else if(operateType.value=='edit'){
if(!checkName(newTab.value.name))return;
newTab.value.data.name=newTab.value.name
networkConfig[newTab.value.type][configIndex.value]=newTab.value.data;
}
else if(operateType.value=='delete'){
networkConfig[newTab.value.type].splice(configIndex.value,1)
}
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = networkConfig;
const success = await setOB11Config(userConfig);
if (success) {
operateType.value='';
configIndex.value=0
MessagePlugin.success('配置保存成功');
await loadConfig()
visibleBody.value=false;
} else {
MessagePlugin.error('配置保存失败');
}
};
const getOB11Config = async (): Promise<OneBotConfig | undefined> => { const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth'); const storedCredential = localStorage.getItem('auth');
if (!storedCredential) { if (!storedCredential) {
@@ -136,114 +315,165 @@ const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const loginManager = new QQLoginManager(storedCredential); const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config); return await loginManager.SetOB11Config(config);
}; };
//获取卡片数据
const addToPanel = <K extends ConfigKey>(configs: ConfigMap[K][], key: K) => { const getAllData = (data: NetworkConfig) => {
configs.forEach((config) => clientPanelData.value.push({ name: config.name, data: config, key })); cardConfig.value=[]
}; WebConfg.value.set("all", [])
Object.entries(data).forEach(([key, configs]) => {
const addConfigDataToPanel = (data: NetworkConfig) => { if (key in defaultConfigs) {
(Object.keys(data) as ConfigKey[]).forEach((key) => { networkConfig[key]=[...configs]
addToPanel(data[key], key); const newConfigsArray = configs.map((config:any) => ({
...config,
type:typeCh[key]
}));
WebConfg.value.set(key,newConfigsArray);
const allConfigs = WebConfg.value.get('all');
if (allConfigs) {
const newAllConfigs = [...allConfigs,...newConfigsArray];
WebConfg.value.set('all', newAllConfigs);
}
cardConfig.value=WebConfg.value.get("all")
}
}); });
}; }
const parsePanelData = (): NetworkConfig => {
const result: NetworkConfig = {
httpServers: [],
httpClients: [],
websocketServers: [],
websocketClients: [],
};
clientPanelData.value.forEach((panel) => {
(result[panel.key] as Array<typeof panel.data>).push(panel.data);
});
return result;
};
const loadConfig = async () => { const loadConfig = async () => {
try { try {
const userConfig = await getOB11Config(); const userConfig = await getOB11Config();
if (!userConfig) return; if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig); const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network); getAllData(mergedConfig.network)
} catch (error) { } catch (error) {
console.error('Error loading config:', error); console.error('Error loading config:', error);
} }
}; };
const saveConfig = async () => { const handleResize = () => {
const config = parsePanelData(); tabsWidth.value = window.innerWidth- 40 -menuWidth.value
const userConfig = await getOB11Config(); if (mediumScreen.matches) {
if (!userConfig) { cardWidth.value = (tabsWidth.value-20)/2;
await MessagePlugin.error('无法获取配置!');
return;
} }
userConfig.network = config; else if(largeScreen.matches) {
const success = await setOB11Config(userConfig); cardWidth.value = (tabsWidth.value-40)/3;
if (success) {
await MessagePlugin.success('配置保存成功');
}else { }else {
await MessagePlugin.error('配置保存失败'); cardWidth.value = tabsWidth.value;
} }
loadPage.value=true
setTimeout(()=>{
cardHeight.value=window.innerHeight-headerBox.value.offsetHeight-setting.value.offsetHeight-20;
},300)
}; };
emitter.on('sendWidth', (width) => {
const showAddTabDialog = () => { if (typeof width === 'number' &&!isNaN(width)) {
newTab.value = { name: '', type: 'httpServers' }; menuWidth.value = width;
isDialogVisible.value = true; handleResize()
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.value.some((panel) => panel.name === name)) {
await MessagePlugin.error('选项卡名称已存在');
return;
} }
const defaultConfig = structuredClone(defaultConfigMap[type]); });
defaultConfig.name = name;
clientPanelData.value.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.value.length - 1;
await MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.value.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => { onMounted(() => {
loadConfig(); loadConfig()
const cachedWidth = localStorage.getItem('menuWidth');
if (cachedWidth) {
menuWidth.value = parseInt(cachedWidth);
setTimeout(()=>{
handleResize()
},300)
}
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
}); });
</script> </script>
<style scoped> <style scoped>
.full-space { .title {
padding: 20px 20px 0 20px;
display: flex;
justify-content: space-between;
}
.setting {
margin: 0 20px;
}
.setting-box {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
overflow-y: auto;
}
.setting-card {
width: 100%; width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
} }
.setting-content{
.full-tabs {
width: 100%; width: 100%;
height: 100%; text-align: left;
}
.setting-address{
display: flex; display: flex;
flex-direction: column; margin-top: 2px;
}
.local{
flex: 5.5;
margin-bottom: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.none-box{
flex: 0.5;
}
.port{
flex: 4;
margin-top: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.setting-status{
display: flex;
margin-top: 2px;
}
.status-deBug{
display: flex;
flex: 4;
}
.status-tag{
display: flex;
flex: 5.5;
} }
.full-tab-panel { @media (max-width: 1024px) {
flex: 1; .setting-box {
display: flex; grid-template-columns: 1fr 1fr;
flex-direction: column; }
} }
.button-container { @media (max-width: 786px) {
display: flex; .setting-box {
justify-content: center; grid-template-columns: 1fr;
margin-top: 20px; }
.setting-address{
display: block;
}
}
.card-box {
margin: 10px 20px 0 20px;
}
.card-none {
line-height: 200px;
}
.card-noneText {
font-size: 16px;
}
.dialog-body{
max-height: 60vh;
overflow-y: auto;
}
::-webkit-scrollbar {
width: 0;
background: transparent;
} }
</style> </style>

View File

@@ -1,25 +1,24 @@
<template> <template>
<div> <div class="title">
<t-divider content="其余配置" align="left" /> <t-divider content="其余配置" align="left" />
</div> </div>
<t-card class="card">
<div class="other-config-container"> <div class="other-config-container">
<div class="other-config"> <div class="other-config">
<t-form ref="form" :model="otherConfig" class="form"> <t-form ref="form" :model="otherConfig" :label-align="labelAlign" label-width="auto" colon>
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item"> <t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
<t-input v-model="otherConfig.musicSignUrl" /> <t-input v-model="otherConfig.musicSignUrl" />
</t-form-item> </t-form-item>
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item"> <t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
<t-switch v-model="otherConfig.enableLocalFile2Url" /> <t-switch v-model="otherConfig.enableLocalFile2Url" />
</t-form-item> </t-form-item>
<t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
<t-switch v-model="otherConfig.parseMultMsg" />
</t-form-item>
</t-form> </t-form>
<div class="button-container"> <div class="button-container">
<t-button @click="saveConfig">保存</t-button> <t-button @click="saveConfig">保存</t-button>
</div> </div>
</div> </div>
</div> </div>
</t-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -31,9 +30,9 @@ import { QQLoginManager } from '@/backend/shell';
const otherConfig = ref<Partial<OneBotConfig>>({ const otherConfig = ref<Partial<OneBotConfig>>({
musicSignUrl: '', musicSignUrl: '',
enableLocalFile2Url: false, enableLocalFile2Url: false,
parseMultMsg: true
}); });
const labelAlign = ref<string>();
const getOB11Config = async (): Promise<OneBotConfig | undefined> => { const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth'); const storedCredential = localStorage.getItem('auth');
if (!storedCredential) { if (!storedCredential) {
@@ -60,7 +59,6 @@ const loadConfig = async () => {
if (userConfig) { if (userConfig) {
otherConfig.value.musicSignUrl = userConfig.musicSignUrl; otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url; otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
otherConfig.value.parseMultMsg = userConfig.parseMultMsg;
} }
} catch (error) { } catch (error) {
console.error('Error loading config:', error); console.error('Error loading config:', error);
@@ -73,7 +71,6 @@ const saveConfig = async () => {
if (userConfig) { if (userConfig) {
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || ''; userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false; userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
userConfig.parseMultMsg = otherConfig.value.parseMultMsg ?? true;
const success = await setOB11Config(userConfig); const success = await setOB11Config(userConfig);
if (success) { if (success) {
MessagePlugin.success('配置保存成功'); MessagePlugin.success('配置保存成功');
@@ -86,55 +83,62 @@ const saveConfig = async () => {
MessagePlugin.error('配置保存失败'); MessagePlugin.error('配置保存失败');
} }
}; };
onMounted(() => { onMounted(() => {
loadConfig(); loadConfig();
const mediaQuery = window.matchMedia('(max-width: 768px)');
const handleMediaChange = (e: MediaQueryListEvent) => {
if (e.matches) {
labelAlign.value = 'top';
}
else {
labelAlign.value = 'left';
}
};
mediaQuery.addEventListener('change', handleMediaChange);
const event = new Event('change');
Object.defineProperty(event,'matches', {
value: mediaQuery.matches,
writable: false
});
mediaQuery.dispatchEvent(event);
return () => {
mediaQuery.removeEventListener('change', handleMediaChange);
};
}); });
</script> </script>
<style scoped> <style scoped>
.title{
padding: 20px 20px 0 20px;
}
.card{
margin: 0 20px;
padding-top: 20px;
padding-bottom: 20px;
}
.other-config-container { .other-config-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
padding: 20px;
box-sizing: border-box; box-sizing: border-box;
} }
.other-config { .other-config {
width: 100%; width: 100%;
max-width: 600px; max-width: 500px;
background: #fff;
padding: 20px;
border-radius: 8px; border-radius: 8px;
} }
.form {
display: flex;
flex-direction: column;
}
.form-item { .form-item {
display: flex;
flex-direction: column;
margin-bottom: 20px; margin-bottom: 20px;
text-align: left;
} }
.button-container { .button-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 20px;
} }
@media (min-width: 768px) {
.form-item {
flex-direction: row;
align-items: center;
}
.form-item t-input,
.form-item t-switch {
flex: 1;
margin-left: 20px;
}
}
</style> </style>

View File

@@ -1,22 +0,0 @@
<template>
<div class="empty-state">
<p>当前没有网络配置</p>
<t-button @click="showAddTabDialog">添加网络配置</t-button>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
defineProps<{ showAddTabDialog: () => void }>();
</script>
<style scoped>
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
</style>

View File

@@ -1,8 +1,6 @@
<template> <template>
<div class="container"> <div>
<div class="form-container"> <t-form labelAlign="left">
<h3>HTTP Client 配置</h3>
<t-form>
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
@@ -23,7 +21,6 @@
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -36,33 +33,16 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }, { label: 'String', value: 'string' }
]); ]);
watch( watch(() => props.config.messagePostFormat, (newValue) => {
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') { if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array'; props.config.messagePostFormat = 'array';
} }
} });
);
</script> </script>
<style scoped> <style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style> </style>

View File

@@ -1,8 +1,6 @@
<template> <template>
<div class="container"> <div>
<div class="form-container"> <t-form labelAlign="left">
<h3>HTTP Server 配置</h3>
<t-form>
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
@@ -29,7 +27,6 @@
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -42,33 +39,16 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }, { label: 'String', value: 'string' }
]); ]);
watch( watch(() => props.config.messagePostFormat, (newValue) => {
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') { if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array'; props.config.messagePostFormat = 'array';
} }
} });
);
</script> </script>
<style scoped> <style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style> </style>

View File

@@ -1,8 +1,6 @@
<template> <template>
<div class="container"> <div>
<div class="form-container"> <t-form labelAlign="left">
<h3>WebSocket Client 配置</h3>
<t-form>
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
@@ -26,7 +24,6 @@
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -39,33 +36,16 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }, { label: 'String', value: 'string' }
]); ]);
watch( watch(() => props.config.messagePostFormat, (newValue) => {
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') { if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array'; props.config.messagePostFormat = 'array';
} }
} });
);
</script> </script>
<style scoped> <style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style> </style>

View File

@@ -1,8 +1,6 @@
<template> <template>
<div class="container"> <div>
<div class="form-container"> <t-form labelAlign="left">
<h3>WebSocket Server 配置</h3>
<t-form>
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-checkbox v-model="config.enable" />
</t-form-item> </t-form-item>
@@ -32,7 +30,6 @@
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -45,33 +42,15 @@ const props = defineProps<{
const messageFormatOptions = ref([ const messageFormatOptions = ref([
{ label: 'Array', value: 'array' }, { label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }, { label: 'String', value: 'string' }
]); ]);
watch( watch(() => props.config.messagePostFormat, (newValue) => {
() => props.config.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') { if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array'; props.config.messagePostFormat = 'array';
} }
} });
);
</script> </script>
<style scoped> <style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style> </style>

View File

@@ -0,0 +1,3 @@
import mitt from "mitt"
const emitter = mitt();
export default emitter;