mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Merge pull request #604 from Ander-pixe/webui-new
feat:实时日志、关于and部分样式优化
This commit is contained in:
@@ -5,12 +5,13 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||
"webui:dev": "vite",
|
||||
"webui:dev": "vite --host",
|
||||
"webui:build": "vite build",
|
||||
"webui:preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"qrcode": "^1.5.4",
|
||||
"tdesign-icons-vue-next": "^0.3.3",
|
||||
"tdesign-vue-next": "^1.10.3",
|
||||
@@ -20,6 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-legacy": "^5.4.3",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
|
@@ -109,4 +109,4 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', haddingFbars);
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
||||
<style></style>
|
||||
|
BIN
napcat.webui/src/assets/0xProtoNerdFont-Italic.ttf
Normal file
BIN
napcat.webui/src/assets/0xProtoNerdFont-Italic.ttf
Normal file
Binary file not shown.
66
napcat.webui/src/backend/githubApi.ts
Normal file
66
napcat.webui/src/backend/githubApi.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export class githubApiManager {
|
||||
public async GetBaseData(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse= await fetch('https://api.github.com/repos/NapNeko/NapCatQQ', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting github data :', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async GetReleasesData(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/releases', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting releases data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async GetPullsData(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/pulls', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Pulls data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async GetContributors(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/contributors', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Pulls data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
72
napcat.webui/src/backend/log.ts
Normal file
72
napcat.webui/src/backend/log.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
type LogListItem = string;
|
||||
type LogListData = LogListItem[];
|
||||
let eventSourcePoly: EventSourcePolyfill | null = null;
|
||||
export class LogManager {
|
||||
private readonly retCredential: string;
|
||||
private readonly apiPrefix: string;
|
||||
|
||||
//调试时http://127.0.0.1:6099/api 打包时 ../api
|
||||
constructor(retCredential: string, apiPrefix: string = '../api') {
|
||||
this.retCredential = retCredential;
|
||||
this.apiPrefix = apiPrefix;
|
||||
}
|
||||
public async GetLogList(): Promise<LogListData> {
|
||||
try {
|
||||
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLogList`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return ConfigResponseJson?.data as LogListData;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting LogList:', error);
|
||||
}
|
||||
return [] as LogListData;
|
||||
}
|
||||
public async GetLog(FileName: string): Promise<string> {
|
||||
try {
|
||||
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLog?id=${FileName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return ConfigResponseJson?.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting LogData:', error);
|
||||
}
|
||||
return 'null';
|
||||
}
|
||||
public async getRealTimeLogs(): Promise<EventSourcePolyfill | null> {
|
||||
this.creatEventSource();
|
||||
return eventSourcePoly;
|
||||
}
|
||||
private creatEventSource() {
|
||||
try {
|
||||
eventSourcePoly = new EventSourcePolyfill(`${this.apiPrefix}/Log/GetLogRealTime`, {
|
||||
heartbeatTimeout: 3 * 60 * 1000,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
Accept: 'text/event-stream',
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建SSE连接出错:', error);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,18 +1,28 @@
|
||||
<template>
|
||||
<t-layout class="dashboard-container">
|
||||
<div ref="menuRef">
|
||||
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
|
||||
<div v-if="!mediaQuery.matches">
|
||||
<SidebarMenu
|
||||
:menu-items="menuItems"
|
||||
class="sidebar-menu"
|
||||
:menu-width="sidebarWidth"
|
||||
/>
|
||||
</div>
|
||||
<t-layout>
|
||||
<router-view />
|
||||
</t-layout>
|
||||
<div v-if="mediaQuery.matches" class="bottom-menu">
|
||||
<BottomMenu :menu-items="menuItems" />
|
||||
</div>
|
||||
</t-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import SidebarMenu from './webui/Nav.vue';
|
||||
import BottomMenu from './webui/NavBottom.vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
const sidebarWidth = ['232px', '64px'];
|
||||
interface MenuItem {
|
||||
value: string;
|
||||
icon: string;
|
||||
@@ -27,13 +37,18 @@ const menuItems = ref<MenuItem[]>([
|
||||
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
|
||||
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
|
||||
]);
|
||||
const menuRef = ref<HTMLDivElement | null>(null);
|
||||
emitter.on('sendMenu', (event) => {
|
||||
emitter.emit('sendWidth', menuRef.value?.offsetWidth);
|
||||
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
|
||||
const menuWidth = event ? sidebarWidth[1] : sidebarWidth[0];
|
||||
emitter.emit('sendWidth', menuWidth);
|
||||
localStorage.setItem('menuWidth', menuWidth.toString() || '0');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
|
||||
if (mediaQuery.matches){
|
||||
localStorage.setItem('menuWidth', '0');
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -49,6 +64,12 @@ onMounted(() => {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.bottom-menu {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
@@ -56,3 +77,19 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
@media (max-width: 768px) {
|
||||
.t-head-menu__inner .t-menu:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.t-head-menu__inner{
|
||||
width: 100%;
|
||||
}
|
||||
.t-head-menu .t-menu{
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.t-menu__content{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<t-card class="layout">
|
||||
<t-card class="layout" :bordered="false">
|
||||
<div class="login-container">
|
||||
<h2 class="sotheby-font">QQ Login</h2>
|
||||
<div class="login-methods">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<t-card class="layout">
|
||||
<t-card class="layout" :bordered="false">
|
||||
<div class="login-container">
|
||||
<h2 class="sotheby-font">WebUi Login</h2>
|
||||
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
|
||||
<t-menu theme="light" :width="menuWidth" :collapsed="collapsed" class="sidebar-menu">
|
||||
<template #logo>
|
||||
<div class="logo">
|
||||
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
|
||||
@@ -43,10 +43,11 @@ type MenuItem = {
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
menuItems: MenuItem[];
|
||||
menuWidth: string | number | Array<string | number>;
|
||||
}>();
|
||||
const mediaQuery = window.matchMedia('(max-width: 800px)');
|
||||
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
|
||||
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
|
||||
const disBtn = ref<boolean>(false);
|
||||
@@ -57,12 +58,10 @@ const changeCollapsed = (): void => {
|
||||
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
||||
};
|
||||
watch(collapsed, (newValue, oldValue) => {
|
||||
setTimeout(() => {
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
}, 300);
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
});
|
||||
onMounted(() => {
|
||||
const mediaQuery = window.matchMedia('(max-width: 800px)');
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
disBtn.value = e.matches;
|
||||
if (e.matches) {
|
||||
|
37
napcat.webui/src/components/webui/NavBottom.vue
Normal file
37
napcat.webui/src/components/webui/NavBottom.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<t-head-menu theme="light" class="bottom-menu">
|
||||
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
||||
<t-tooltip :content="item.label" placement="top">
|
||||
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
||||
<template #icon>
|
||||
<t-icon :name="item.icon" />
|
||||
</template>
|
||||
|
||||
<!-- {{item.label}}-->
|
||||
</t-menu-item>
|
||||
</t-tooltip>
|
||||
</router-link>
|
||||
</t-head-menu>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
type MenuItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
route: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
menuItems: MenuItem[];
|
||||
}>();
|
||||
</script>
|
||||
<style scoped>
|
||||
.bottom-menu {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 0.8px solid #ddd;
|
||||
}
|
||||
</style>
|
@@ -4,3 +4,10 @@
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'ProtoNerdFontItalic';
|
||||
src: url('../assets/0xProtoNerdFont-Italic.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
@@ -40,6 +40,11 @@ import {
|
||||
Aside as TAside,
|
||||
Popconfirm as Tpopconfirm,
|
||||
Empty as TEmpty,
|
||||
Dropdown as TDropdown,
|
||||
Typography as TTypographyText,
|
||||
TreeSelect as TTreeSelect,
|
||||
Loading as TLoading,
|
||||
HeadMenu as THeadMenu
|
||||
} from 'tdesign-vue-next';
|
||||
import { router } from './router';
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
@@ -84,4 +89,9 @@ app.use(TFooter);
|
||||
app.use(TAside);
|
||||
app.use(Tpopconfirm);
|
||||
app.use(TEmpty);
|
||||
app.use(TDropdown);
|
||||
app.use(TTypographyText);
|
||||
app.use(TTreeSelect);
|
||||
app.use(TLoading);
|
||||
app.use(THeadMenu);
|
||||
app.mount('#app');
|
||||
|
@@ -1,23 +1,97 @@
|
||||
<template>
|
||||
<div class="about-us">
|
||||
<div>
|
||||
<t-divider content="面板关于信息" align="left" />
|
||||
<t-alert theme="success" message="NapCat.WebUi is running" />
|
||||
<t-list class="list">
|
||||
<t-list-item class="list-item">
|
||||
<span class="item-label">开发人员:</span>
|
||||
<t-divider content="面板关于信息" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<info-circle-icon></info-circle-icon>
|
||||
<div style="margin-left: 5px">面板关于信息</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
<t-alert theme="success" class="header" message="NapCat.WebUi is running" />
|
||||
<t-list>
|
||||
<t-list-item>
|
||||
<div class="label-box">
|
||||
<star-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Star:</span>
|
||||
</div>
|
||||
<span class="item-content">
|
||||
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/stargazers">{{
|
||||
githubBastData?.stargazers_count
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item class="list-item">
|
||||
<span class="item-label">版本信息:</span>
|
||||
<t-list-item>
|
||||
<tips-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">issues:</span>
|
||||
<span class="item-content">
|
||||
<t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag>
|
||||
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
|
||||
<t-tag class="tag-item" theme="success">
|
||||
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/issues">{{
|
||||
githubBastData?.open_issues_count
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<git-pull-request-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Pull Requests:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/pulls">{{githubPullData?.length
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item >
|
||||
<bookmark-add-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Releases:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/releases">{{
|
||||
githubReleasesData&&githubReleasesData[0]?timeDifference(githubReleasesData[0].published_at) + '前更新':''
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<usergroup-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Contributors:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/graphs/contributors">{{githubContributorsData?.length}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<browse-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Watchers:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/watchers">{{
|
||||
githubBastData?.watchers
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<fork-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Fork:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/fork">{{
|
||||
githubBastData?.forks_count
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<statue-of-jesus-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">License:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ#License-1-ov-file">{{
|
||||
githubBastData?.license.key
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<component-layout-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Version:</span>
|
||||
<span class="item-content">
|
||||
<t-tag class="tag-item pgk-color"> WebUi: {{ pkg.version }} </t-tag>
|
||||
<t-tag class="tag-item nc-color">
|
||||
NapCat:
|
||||
{{ githubReleasesData&&githubReleasesData[0] ?.tag_name ? githubReleasesData[0].tag_name : napCatVersion }}
|
||||
</t-tag>
|
||||
<t-tag class="tag-item td-color"> TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </t-tag>
|
||||
</span>
|
||||
</t-list-item>
|
||||
</t-list>
|
||||
@@ -28,6 +102,51 @@
|
||||
<script setup lang="ts">
|
||||
import pkg from '../../package.json';
|
||||
import { napCatVersion } from '../../../src/common/version';
|
||||
import {
|
||||
InfoCircleIcon,
|
||||
TipsFilledIcon,
|
||||
StarFilledIcon,
|
||||
GitPullRequestFilledIcon,
|
||||
ForkFilledIcon,
|
||||
StatueOfJesusFilledIcon,
|
||||
BookmarkAddFilledIcon,
|
||||
UsergroupFilledIcon,
|
||||
BrowseFilledIcon,
|
||||
ComponentLayoutFilledIcon,
|
||||
} from 'tdesign-icons-vue-next';
|
||||
import { githubApiManager } from '@/backend/githubApi';
|
||||
import { onMounted, ref } from 'vue';
|
||||
const githubApi = new githubApiManager();
|
||||
const githubBastData = ref<any>(null);
|
||||
const githubReleasesData = ref<any>(null);
|
||||
const githubContributorsData = ref<any>(null);
|
||||
const githubPullData = ref<any>(null);
|
||||
const getBaseData = async () => {
|
||||
githubBastData.value = await githubApi.GetBaseData();
|
||||
githubReleasesData.value = await githubApi.GetReleasesData();
|
||||
githubContributorsData.value = await githubApi.GetContributors();
|
||||
githubPullData.value = await githubApi.GetPullsData();
|
||||
};
|
||||
const timeDifference = (timestamp: string): string => {
|
||||
const givenTime = new Date(timestamp);
|
||||
const currentTime = new Date();
|
||||
const diffInMilliseconds = currentTime.getTime() - givenTime.getTime();
|
||||
|
||||
const seconds = Math.floor(diffInMilliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟`;
|
||||
} else {
|
||||
return `${seconds}秒`;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
getBaseData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -35,23 +154,26 @@ import { napCatVersion } from '../../../src/common/version';
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.list {
|
||||
.label-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
padding: 5px;
|
||||
color: #ffffff;
|
||||
border-radius: 3px;
|
||||
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
|
||||
}
|
||||
.item-label {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
@@ -64,3 +186,37 @@ import { napCatVersion } from '../../../src/common/version';
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.t-list-item {
|
||||
padding: 5px var(--td-comp-paddingLR-l);
|
||||
}
|
||||
.item-label {
|
||||
flex: 2;
|
||||
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.pgk-color {
|
||||
color: white;
|
||||
background-image: linear-gradient(-225deg, #9be15d 0%, #00e3ae 100%);
|
||||
}
|
||||
.nc-color {
|
||||
color: white;
|
||||
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
|
||||
}
|
||||
.td-color {
|
||||
color: white;
|
||||
background-image: linear-gradient(225deg, #0acffe 0%, #495aff 100%);
|
||||
}
|
||||
.header {
|
||||
background-image: linear-gradient(225deg, #dfffcd 0%, #90f9c4 48%, #39f3bb 100%) !important;
|
||||
}
|
||||
.link-text{
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(-225deg, #B6CEE8 0%, #F578DC 100%);
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,6 +1,600 @@
|
||||
<template>
|
||||
<div class="log-view">
|
||||
<h1>面板日志信息</h1>
|
||||
<p>这里显示面板的日志信息。</p>
|
||||
<div class="title">
|
||||
<t-divider content="日志查看" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<system-log-icon></system-log-icon>
|
||||
<div style="margin-left: 5px">日志查看</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
</div>
|
||||
<div class="tab-box">
|
||||
<t-tabs default-value="realtime" @change="selectType">
|
||||
<t-tab-panel value="realtime" label="实时日志"></t-tab-panel>
|
||||
<t-tab-panel value="history" label="历史日志"></t-tab-panel>
|
||||
</t-tabs>
|
||||
</div>
|
||||
<div class="card-box">
|
||||
<t-card class="card" :bordered="true">
|
||||
<template #actions>
|
||||
<t-row :align="'middle'" justify="center" :style="{ gap: smallScreen.matches ? '5px' : '24px' }">
|
||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
||||
<t-tooltip content="清理日志">
|
||||
<t-button variant="text" shape="square" @click="clearLogs">
|
||||
<clear-icon></clear-icon>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</t-col>
|
||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
||||
<t-tooltip content="下载日志">
|
||||
<t-button variant="text" shape="square" @click="downloadText">
|
||||
<download-icon></download-icon>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</t-col>
|
||||
<t-col
|
||||
v-if="LogDataType === 'history'"
|
||||
flex="auto"
|
||||
style="display: inline-flex; justify-content: center">
|
||||
<t-tooltip content="历史日志">
|
||||
<t-button variant="text" shape="square" @click="historyLog">
|
||||
<history-icon></history-icon>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</t-col>
|
||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
||||
<div class="tag-box">
|
||||
<t-tag class="t-tag" :style="{ backgroundImage: typeKey[optValue.description] }">{{
|
||||
optValue.content }}</t-tag>
|
||||
</div>
|
||||
<t-dropdown :options="options" :min-column-width="112" @click="openTypeList">
|
||||
<t-button variant="text" shape="square">
|
||||
<more-icon />
|
||||
</t-button>
|
||||
</t-dropdown>
|
||||
</t-col>
|
||||
</t-row>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="content" ref="contentBox">
|
||||
<div v-for="item in LogDataType === 'realtime'
|
||||
? realtimeLogHtmlList.get(optValue.description)
|
||||
: historyLogHtmlList.get(optValue.description)">
|
||||
<span>{{ item.time }}</span><span :id="item.type">{{ item.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-card>
|
||||
</div>
|
||||
<t-dialog v-model:visible="visibleBody" header="历史日志" :destroy-on-close="true" :show-in-attached-element="true"
|
||||
:on-confirm="GetLogList" class=".t-dialog__ctx .t-dialog__position">
|
||||
<t-select v-model="value" :options="logFileData" placeholder="请选择日志" :multiple="true"
|
||||
style="text-align: left" />
|
||||
</t-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { MoreIcon, ClearIcon, DownloadIcon, HistoryIcon, SystemLogIcon } from 'tdesign-icons-vue-next';
|
||||
import { nextTick, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { LogManager } from '@/backend/log';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
const smallScreen = window.matchMedia('(max-width: 768px)');
|
||||
const LogDataType = ref<string>('realtime');
|
||||
const visibleBody = ref<boolean>(false);
|
||||
const contentBox = ref<HTMLElement | null>(null);
|
||||
let isMouseEntered = false;
|
||||
const logManager = new LogManager(localStorage.getItem('auth') || '');
|
||||
const eventSource = ref<EventSourcePolyfill | null>(null);
|
||||
const intervalId = ref<number | null>(null);
|
||||
const isPaused = ref(false);
|
||||
interface OptionItem {
|
||||
content: string;
|
||||
value: number;
|
||||
description: string;
|
||||
}
|
||||
const options = ref<OptionItem[]>([
|
||||
{
|
||||
content: '全部',
|
||||
value: 1,
|
||||
description: 'all',
|
||||
},
|
||||
{
|
||||
content: '调试',
|
||||
value: 2,
|
||||
description: 'debug',
|
||||
},
|
||||
{
|
||||
content: '提示',
|
||||
value: 3,
|
||||
description: 'info',
|
||||
},
|
||||
{
|
||||
content: '警告',
|
||||
value: 4,
|
||||
description: 'warn',
|
||||
},
|
||||
{
|
||||
content: '错误',
|
||||
value: 5,
|
||||
description: 'error',
|
||||
},
|
||||
{
|
||||
content: '致命',
|
||||
value: 5,
|
||||
description: 'fatal',
|
||||
},
|
||||
]);
|
||||
const typeKey = ref<Record<string, string>>({
|
||||
all: 'linear-gradient(60deg,#16a085 0%, #f4d03f 100%)',
|
||||
debug: 'linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%)',
|
||||
info: 'linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%)',
|
||||
warn: 'linear-gradient(to right, #e14fad 0%, #f9d423 48%, #e37318 100%)',
|
||||
error: 'linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%)',
|
||||
fatal: 'linear-gradient(-225deg, #fd0700, #ec567f)',
|
||||
});
|
||||
interface logHtml {
|
||||
type?: string;
|
||||
content: string;
|
||||
color?: string;
|
||||
time?: string;
|
||||
}
|
||||
type LogHtmlMap = Map<string, logHtml[]>;
|
||||
const realtimeLogHtmlList = ref<LogHtmlMap>(
|
||||
new Map([
|
||||
['all', []],
|
||||
['debug', []],
|
||||
['info', []],
|
||||
['warn', []],
|
||||
['error', []],
|
||||
['fatal', []],
|
||||
])
|
||||
);
|
||||
const historyLogHtmlList = ref<LogHtmlMap>(
|
||||
new Map([
|
||||
['all', []],
|
||||
['debug', []],
|
||||
['info', []],
|
||||
['warn', []],
|
||||
['error', []],
|
||||
['fatal', []],
|
||||
])
|
||||
);
|
||||
const logFileData = ref<{ label: string; value: string }[]>([]);
|
||||
const value = ref([]);
|
||||
const optValue = ref<OptionItem>({
|
||||
content: '全部',
|
||||
value: 1,
|
||||
description: 'all',
|
||||
});
|
||||
const openTypeList = (data: OptionItem) => {
|
||||
optValue.value = data;
|
||||
};
|
||||
const logType = ['debug', 'info', 'warn', 'error', 'fatal'];
|
||||
//清理log
|
||||
const clearLogs = () => {
|
||||
if (LogDataType.value === 'realtime') {
|
||||
clearAllLogs(realtimeLogHtmlList);
|
||||
} else {
|
||||
clearAllLogs(historyLogHtmlList);
|
||||
}
|
||||
};
|
||||
const clearAllLogs = (logList: Ref<Map<string, Array<logHtml>>>) => {
|
||||
if ((optValue.value && optValue.value.description === 'all') || !optValue.value) {
|
||||
logList.value = new Map([
|
||||
['all', []],
|
||||
['debug', []],
|
||||
['info', []],
|
||||
['warn', []],
|
||||
['error', []],
|
||||
['fatal', []],
|
||||
]);
|
||||
} else {
|
||||
logList.value.set(optValue.value.description, []);
|
||||
}
|
||||
};
|
||||
//定时清理log
|
||||
|
||||
const TimerClear = () => {
|
||||
clearAllLogs(realtimeLogHtmlList);
|
||||
};
|
||||
const startTimer = () => {
|
||||
if (!isPaused.value) {
|
||||
intervalId.value = window.setInterval(TimerClear, 0.5 * 60 * 1000);
|
||||
}
|
||||
};
|
||||
const pauseTimer = () => {
|
||||
if (intervalId.value) {
|
||||
window.clearInterval(intervalId.value);
|
||||
isPaused.value = true;
|
||||
}
|
||||
};
|
||||
const resumeTimer = () => {
|
||||
if (isPaused.value) {
|
||||
startTimer();
|
||||
isPaused.value = false;
|
||||
}
|
||||
};
|
||||
const stopTimer = () => {
|
||||
if (intervalId.value) {
|
||||
window.clearInterval(intervalId.value);
|
||||
intervalId.value = null;
|
||||
}
|
||||
};
|
||||
const extractContent = (text: string): string | null => {
|
||||
const regex = /\[([^\]]+)]/;
|
||||
const match = regex.exec(text);
|
||||
if (match && match[1]) {
|
||||
const extracted = match[1].toLowerCase();
|
||||
if (logType.includes(extracted)) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const loadData = (text: string, loadType: string) => {
|
||||
const lines = text.split(/\r\n/);
|
||||
lines.forEach((line) => {
|
||||
if (loadType === 'realtime') {
|
||||
let remoteJson = JSON.parse(line) as { message: string, level: string };
|
||||
const type = remoteJson.level;
|
||||
const actualType = type || 'other';
|
||||
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
|
||||
const data: logHtml = {
|
||||
type: actualType,
|
||||
content: remoteJson.message,
|
||||
color: color,
|
||||
time: '',
|
||||
};
|
||||
updateLogList(realtimeLogHtmlList, actualType, data);
|
||||
} else if (loadType === 'history') {
|
||||
const type = extractContent(line);
|
||||
const actualType = type || 'other';
|
||||
const timeRegex = /(\d{2}-\d{2} \d{2}:\d{2}:\d{2})|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
|
||||
const match = timeRegex.exec(line);
|
||||
let time = match ? match[0] : null;
|
||||
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
|
||||
const data: logHtml = {
|
||||
type: actualType,
|
||||
content: line.slice(match ? match[0].length : 0) || '',
|
||||
color: color,
|
||||
time: time ? time + ' ' : '',
|
||||
};
|
||||
updateLogList(historyLogHtmlList, actualType, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateLogList = (logList: Ref<Map<string, Array<logHtml>>>, actualType: string, data: logHtml) => {
|
||||
const allLogs = logList.value.get('all');
|
||||
if (Array.isArray(allLogs)) {
|
||||
allLogs.push(data);
|
||||
}
|
||||
if (actualType !== 'other') {
|
||||
const typeLogs = logList.value.get(actualType);
|
||||
if (Array.isArray(typeLogs)) {
|
||||
typeLogs.push(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
const selectType = (key: string) => {
|
||||
LogDataType.value = key;
|
||||
};
|
||||
interface CustomURL extends URL {
|
||||
recycleObjectURL: (url: string) => void;
|
||||
}
|
||||
|
||||
const isCompatibleWithCustomURL = (obj: any): obj is CustomURL => {
|
||||
return typeof obj === 'object' && obj !== null && typeof (obj as any).recycleObjectURL === 'function';
|
||||
};
|
||||
|
||||
const recycleURL = (url: string) => {
|
||||
if (isCompatibleWithCustomURL(window.URL)) {
|
||||
const customURL = window.URL as CustomURL;
|
||||
customURL.recycleObjectURL(url);
|
||||
}
|
||||
};
|
||||
const generateTXT = (textContent: string, fileName: string) => {
|
||||
try {
|
||||
const blob = new Blob([textContent], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
recycleURL(url);
|
||||
} catch (error) {
|
||||
console.error('下载文本时出现错误:', error);
|
||||
}
|
||||
};
|
||||
const downloadText = () => {
|
||||
if (LogDataType.value === 'realtime') {
|
||||
const logs = realtimeLogHtmlList.value.get(optValue.value.description);
|
||||
if (logs && logs.length > 0) {
|
||||
const result = logs.map((obj) => obj.content).join('\r\n');
|
||||
generateTXT(result, '实时日志');
|
||||
} else {
|
||||
MessagePlugin.error('暂无可下载日志');
|
||||
}
|
||||
} else {
|
||||
const logs = historyLogHtmlList.value.get(optValue.value.description);
|
||||
if (logs && logs.length > 0) {
|
||||
const result = logs.map((obj) => obj.content).join('\r\n');
|
||||
generateTXT(result, '历史日志');
|
||||
} else {
|
||||
MessagePlugin.error('暂无可下载日志');
|
||||
}
|
||||
}
|
||||
};
|
||||
const historyLog = async () => {
|
||||
value.value = [];
|
||||
visibleBody.value = true;
|
||||
const res = await logManager.GetLogList();
|
||||
clearAllLogs(historyLogHtmlList);
|
||||
if (res.length > 0) {
|
||||
logFileData.value = res.map((ele: string) => {
|
||||
return { label: ele, value: ele };
|
||||
});
|
||||
} else {
|
||||
logFileData.value = [];
|
||||
}
|
||||
};
|
||||
const GetLogList = async () => {
|
||||
if (value.value.length > 0) {
|
||||
for (const ele of value.value) {
|
||||
try {
|
||||
const data = await logManager.GetLog(ele);
|
||||
if (data && data !== 'null') {
|
||||
loadData(data, 'history');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取日志 ${ele} 时出现错误:`, error);
|
||||
}
|
||||
}
|
||||
visibleBody.value = false;
|
||||
} else {
|
||||
MessagePlugin.error('请选择日志');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRealTimeLogs = async () => {
|
||||
eventSource.value = await logManager.getRealTimeLogs();
|
||||
if (eventSource.value) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-expect-error
|
||||
eventSource.value.onmessage = (event: MessageEvent) => {
|
||||
console.log(event.data)
|
||||
loadData(event.data, 'realtime');
|
||||
};
|
||||
}
|
||||
};
|
||||
const closeRealTimeLogs = async () => {
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close();
|
||||
}
|
||||
};
|
||||
const scrollToBottom = () => {
|
||||
if (!isMouseEntered) {
|
||||
nextTick(() => {
|
||||
if (contentBox.value) {
|
||||
contentBox.value.scrollTop = contentBox.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const observeDOMChanges = () => {
|
||||
if (contentBox.value) {
|
||||
const observer = new MutationObserver(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
observer.observe(contentBox.value, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
const showScrollbar = () => {
|
||||
if (contentBox.value) {
|
||||
contentBox.value.style.overflow = 'auto';
|
||||
}
|
||||
};
|
||||
const hideScrollbar = () => {
|
||||
if (contentBox.value) {
|
||||
contentBox.value.style.overflow = 'hidden';
|
||||
if (!isMouseEntered) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
realtimeLogHtmlList,
|
||||
() => {
|
||||
if (!isMouseEntered) {
|
||||
scrollToBottom();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
historyLogHtmlList,
|
||||
() => {
|
||||
if (!isMouseEntered) {
|
||||
scrollToBottom();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fetchRealTimeLogs();
|
||||
startTimer();
|
||||
contentBox.value = document.querySelector('.content');
|
||||
if (contentBox.value) {
|
||||
contentBox.value.style.overflow = 'hidden';
|
||||
contentBox.value.addEventListener('mouseenter', () => {
|
||||
isMouseEntered = true;
|
||||
showScrollbar();
|
||||
pauseTimer();
|
||||
});
|
||||
contentBox.value.addEventListener('mouseleave', () => {
|
||||
isMouseEntered = false;
|
||||
hideScrollbar();
|
||||
resumeTimer();
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 1000);
|
||||
});
|
||||
observeDOMChanges();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
closeRealTimeLogs();
|
||||
stopTimer();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.title {
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
.tab-box {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.card-box {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 56vh;
|
||||
background-image: url('@/assets/logo.png');
|
||||
border: 1px solid #ddd6d6 !important;
|
||||
padding: 5px 10px;
|
||||
text-align: left;
|
||||
overflow-y: auto;
|
||||
margin-top: -10px;
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.content span {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@keyframes fadeInOnce {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutOnce {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content div {
|
||||
animation: fadeInOnce 0.5s forwards;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #888888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tag-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.t-tag {
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#debug {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%);
|
||||
}
|
||||
|
||||
#info {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%);
|
||||
}
|
||||
|
||||
#warn {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(225deg, #e14fad 0%, #f9d423 48%, #e37318 100%);
|
||||
}
|
||||
|
||||
#error {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%);
|
||||
}
|
||||
|
||||
#fatal {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(to right, #fd0700, #ec567f);
|
||||
}
|
||||
|
||||
#other {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(to top, #3f51b1 0%, #5a55ae 13%, #7b5fac 25%, #8f6aae 38%, #a86aa4 50%, #cc6b8e 62%, #f18271 75%, #f3a469 87%, #f7c978 100%);
|
||||
}
|
||||
|
||||
@media (max-width: 786px) {
|
||||
.content {
|
||||
height: 50vh;
|
||||
font-family: ProtoNerdFontItalic, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 14.3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.card {
|
||||
padding: 5px 10px 20px 10px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 786px) {
|
||||
.card {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<div ref="headerBox" class="title">
|
||||
<t-divider content="网络配置" align="left" />
|
||||
<t-divider content="网络配置" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<wifi1-icon />
|
||||
<div style="margin-left: 5px">网络配置</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
<t-divider align="right">
|
||||
<t-button @click="addConfig()">
|
||||
<template #icon><add-icon /></template>
|
||||
添加配置</t-button>
|
||||
添加配置</t-button
|
||||
>
|
||||
</t-divider>
|
||||
</div>
|
||||
<div v-if="loadPage" ref="setting" class="setting">
|
||||
@@ -16,86 +24,142 @@
|
||||
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
|
||||
</t-tabs>
|
||||
</div>
|
||||
<t-loading attach="#alice" :loading="!loadPage" :showOverlay="false">
|
||||
<div id="alice" v-if="!loadPage" style="height: 80vh;position: relative" ></div>
|
||||
</t-loading>
|
||||
<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" :description="item.type" :style="{ width: cardWidth + 'px' }"
|
||||
:header-bordered="true" class="setting-card">
|
||||
<t-card
|
||||
:title="item.name"
|
||||
:description="item.type"
|
||||
: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)">
|
||||
<t-popconfirm content="确认删除" @confirm="delConfig(item)">
|
||||
<delete-icon size="20px"></delete-icon>
|
||||
</t-popconfirm>
|
||||
</t-space>
|
||||
</template>
|
||||
<div class="setting-content">
|
||||
<t-card class="card-address" :style="{
|
||||
borderLeft: '7px solid ' + (item.enable ?
|
||||
'var(--td-success-color)' :
|
||||
'var(--td-error-color)')
|
||||
}">
|
||||
<div class="local-box" v-if="item.host&&item.port">
|
||||
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
|
||||
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.host + ':' + item.port)"></copy-icon>
|
||||
</div>
|
||||
<div class="local-box" v-if="item.url">
|
||||
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
|
||||
<strong class="local" >{{ item.url }}</strong>
|
||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
||||
</div>
|
||||
<t-card
|
||||
class="card-address"
|
||||
:style="{
|
||||
borderLeft:
|
||||
'7px solid ' + (item.enable ? 'var(--td-success-color)' : 'var(--td-error-color)'),
|
||||
}"
|
||||
>
|
||||
<div class="local-box" v-if="item.host && item.port">
|
||||
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
|
||||
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
||||
<copy-icon
|
||||
class="copy-icon"
|
||||
size="20px"
|
||||
@click="copyText(item.host + ':' + item.port)"
|
||||
></copy-icon>
|
||||
</div>
|
||||
<div class="local-box" v-if="item.url">
|
||||
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
|
||||
<strong class="local">{{ item.url }}</strong>
|
||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
||||
</div>
|
||||
</t-card>
|
||||
<t-collapse :default-value="[0]" expand-mutex style="margin-top:10px;" class="info-coll">
|
||||
<t-collapse :default-value="[0]" expand-mutex style="margin-top: 10px" class="info-coll">
|
||||
<t-collapse-panel header="基础信息">
|
||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info">
|
||||
<t-descriptions
|
||||
size="small"
|
||||
:layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info"
|
||||
>
|
||||
<t-descriptions-item v-if="item.token" label="连接密钥">
|
||||
<div v-if="mediumScreen.matches||largeScreen.matches" class="token-view">
|
||||
<div v-if="mediumScreen.matches || largeScreen.matches" class="token-view">
|
||||
<span>{{ showToken ? item.token : '******' }}</span>
|
||||
<browse-icon class="browse-icon" v-if="showToken" size="18px"
|
||||
@click="showToken = false"></browse-icon>
|
||||
<browse-off-icon class="browse-icon" v-else size="18px"
|
||||
@click="showToken = true"></browse-off-icon>
|
||||
<browse-icon
|
||||
class="browse-icon"
|
||||
v-if="showToken"
|
||||
size="18px"
|
||||
@click="showToken = false"
|
||||
></browse-icon>
|
||||
<browse-off-icon
|
||||
class="browse-icon"
|
||||
v-else
|
||||
size="18px"
|
||||
@click="showToken = true"
|
||||
></browse-off-icon>
|
||||
</div>
|
||||
<div v-else>
|
||||
<t-popup :showArrow="true" trigger="click">
|
||||
<t-tag theme="primary">点击查看</t-tag>
|
||||
<template #content>
|
||||
<div @click="copyText(item.token)">{{item.token}}</div>
|
||||
<div @click="copyText(item.token)">{{ item.token }}</div>
|
||||
</template>
|
||||
</t-popup>
|
||||
</div>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="消息格式">{{ item.messagePostFormat }}</t-descriptions-item>
|
||||
<t-descriptions-item label="消息格式">{{
|
||||
item.messagePostFormat
|
||||
}}</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</t-collapse-panel>
|
||||
<t-collapse-panel header="状态信息">
|
||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info">
|
||||
<t-descriptions
|
||||
size="small"
|
||||
:layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info"
|
||||
>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
|
||||
<t-tag class="tag-item" :theme="item.debug ? 'success' : 'danger'">
|
||||
{{ item.debug ? '开启' : '关闭' }}</t-tag>
|
||||
<t-tag
|
||||
:class="item.debug ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'debug')"
|
||||
>
|
||||
{{ item.debug ? '开启' : '关闭' }}</t-tag
|
||||
>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
|
||||
label="Websocket 功能">
|
||||
<t-tag class="tag-item" :theme="item.enableWebsocket ? 'success' : 'danger'">
|
||||
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
|
||||
<t-descriptions-item
|
||||
v-if="item.hasOwnProperty('enableWebsocket')"
|
||||
label="Websocket 功能"
|
||||
>
|
||||
<t-tag
|
||||
:class="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'enableWebsocket')"
|
||||
>
|
||||
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag
|
||||
>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
|
||||
<t-tag class="tag-item" :theme="item.enableCors ? 'success' : 'danger'">
|
||||
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
|
||||
<t-descriptions-item
|
||||
v-if="item.hasOwnProperty('enableCors')"
|
||||
label="跨域放行"
|
||||
>
|
||||
<t-tag :class="item.enableCors ? 'tag-item-on' : 'tag-item-off'" @click="toggleProperty(item, 'enableCors')">
|
||||
{{ item.enableCors ? '开启' : '关闭' }}</t-tag
|
||||
>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="上报自身消息">
|
||||
<t-tag class="tag-item" :theme="item.reportSelfMessage ? 'success' : 'danger'">
|
||||
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
|
||||
<t-descriptions-item
|
||||
v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="上报自身消息"
|
||||
>
|
||||
<t-tag
|
||||
:class="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'reportSelfMessage')"
|
||||
>
|
||||
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag
|
||||
>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="强制推送事件">
|
||||
<t-tag class="tag-item"
|
||||
:theme="item.enableForcePushEvent ? 'success' : 'danger'">
|
||||
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
|
||||
<t-descriptions-item
|
||||
v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="强制推送事件"
|
||||
>
|
||||
<t-tag
|
||||
class="tag-item"
|
||||
:class="item.enableForcePushEvent ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'enableForcePushEvent')"
|
||||
>
|
||||
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag
|
||||
>
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</t-collapse-panel>
|
||||
@@ -105,20 +169,34 @@
|
||||
</div>
|
||||
<div style="height: 20vh"></div>
|
||||
</div>
|
||||
<t-card v-else>
|
||||
<t-card v-else>
|
||||
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
|
||||
</t-card>
|
||||
</div>
|
||||
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
|
||||
:show-in-attached-element="true" placement="center" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog--defaul">
|
||||
<div slot="body" class="dialog-body" >
|
||||
<t-dialog
|
||||
v-model:visible="visibleBody"
|
||||
:header="dialogTitle"
|
||||
:destroy-on-close="true"
|
||||
:show-in-attached-element="true"
|
||||
:on-confirm="saveConfig"
|
||||
class=".t-dialog__ctx .t-dialog__position"
|
||||
>
|
||||
<div slot="body" class="dialog-body">
|
||||
<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-form-item
|
||||
style="text-align: left"
|
||||
:rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
|
||||
label="名称"
|
||||
name="name"
|
||||
>
|
||||
<t-input v-model="newTab.name" />
|
||||
</t-form-item>
|
||||
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
|
||||
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" @change="onloadDefault">
|
||||
<t-option value="httpServers">HTTP 服务器</t-option>
|
||||
<t-option value="httpClients">HTTP 客户端</t-option>
|
||||
@@ -127,8 +205,10 @@
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
<div>
|
||||
<component :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
||||
:config="newTab.data" />
|
||||
<component
|
||||
:is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
||||
:config="newTab.data"
|
||||
/>
|
||||
</div>
|
||||
</t-form>
|
||||
</div>
|
||||
@@ -136,8 +216,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AddIcon, DeleteIcon, Edit2Icon, ServerFilledIcon, CopyIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-vue-next';
|
||||
import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue';
|
||||
import {
|
||||
AddIcon,
|
||||
DeleteIcon,
|
||||
Edit2Icon,
|
||||
ServerFilledIcon,
|
||||
CopyIcon,
|
||||
BrowseOffIcon,
|
||||
BrowseIcon,
|
||||
Wifi1Icon,
|
||||
} from 'tdesign-icons-vue-next';
|
||||
import { onMounted, onUnmounted, ref, resolveDynamicComponent, watch } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
import {
|
||||
mergeNetworkDefaultConfig,
|
||||
@@ -187,7 +276,7 @@ const operateType = ref<string>('');
|
||||
//配置项索引
|
||||
const configIndex = ref<number>(0);
|
||||
//保存时所用数据
|
||||
const networkConfig: NetworkConfig & { [key: string]: any; } = {
|
||||
const networkConfig: NetworkConfig & { [key: string]: any } = {
|
||||
websocketClients: [],
|
||||
websocketServers: [],
|
||||
httpClients: [],
|
||||
@@ -235,6 +324,18 @@ const editConfig = (item: any) => {
|
||||
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
||||
visibleBody.value = true;
|
||||
};
|
||||
const toggleProperty = async (item: any, tagData: string) => {
|
||||
const type = getKeyByValue(typeCh, item.type);
|
||||
const newData = { ...item };
|
||||
newData[tagData] = !item[tagData];
|
||||
if (type) {
|
||||
newTab.value = { name: item.name, data: newData, type: type };
|
||||
}
|
||||
operateType.value = 'edit';
|
||||
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
||||
await saveConfig();
|
||||
};
|
||||
|
||||
const delConfig = (item: any) => {
|
||||
const type = getKeyByValue(typeCh, item.type);
|
||||
if (type) {
|
||||
@@ -252,7 +353,6 @@ const selectType = (key: ComponentKey) => {
|
||||
};
|
||||
|
||||
const onloadDefault = (key: ComponentKey) => {
|
||||
console.log(key);
|
||||
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
|
||||
};
|
||||
//检测重名
|
||||
@@ -350,22 +450,21 @@ const loadConfig = async () => {
|
||||
};
|
||||
|
||||
const copyText = async (text: string) => {
|
||||
const input = document.createElement('input');
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
await navigator.clipboard.writeText(text);
|
||||
document.body.removeChild(input);
|
||||
MessagePlugin.success('复制成功');
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
MessagePlugin.success('复制成功');
|
||||
} catch (err) {
|
||||
console.error('复制失败', err);
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
// 得根据卡片宽度改,懒得改了;先不管了
|
||||
// if(window.innerWidth < 540) {
|
||||
// infoOneCol.value= true
|
||||
// } else {
|
||||
// infoOneCol.value= false
|
||||
// }
|
||||
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
|
||||
if (mediumScreen.matches) {
|
||||
cardWidth.value = (tabsWidth.value - 20) / 2;
|
||||
@@ -375,29 +474,43 @@ const handleResize = () => {
|
||||
cardWidth.value = tabsWidth.value;
|
||||
}
|
||||
loadPage.value = true;
|
||||
setTimeout(() => {
|
||||
setTimeout(()=>{
|
||||
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
|
||||
}, 300);
|
||||
},300)
|
||||
};
|
||||
emitter.on('sendWidth', (width) => {
|
||||
if (typeof width === 'number' && !isNaN(width)) {
|
||||
menuWidth.value = width;
|
||||
handleResize();
|
||||
if (typeof width === 'string') {
|
||||
const strWidth = width as string;
|
||||
menuWidth.value = parseInt(strWidth);
|
||||
}
|
||||
});
|
||||
watch(menuWidth, (newValue, oldValue) => {
|
||||
loadPage.value = false;
|
||||
setTimeout(()=>{
|
||||
handleResize();
|
||||
},300)
|
||||
});
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
const cachedWidth = localStorage.getItem('menuWidth');
|
||||
if (cachedWidth) {
|
||||
menuWidth.value = parseInt(cachedWidth);
|
||||
setTimeout(() => {
|
||||
setTimeout(()=>{
|
||||
handleResize();
|
||||
}, 300);
|
||||
},300)
|
||||
}
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('resize', ()=>{
|
||||
setTimeout(()=>{
|
||||
handleResize();
|
||||
},300)
|
||||
});
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('resize', ()=>{
|
||||
setTimeout(()=>{
|
||||
handleResize();
|
||||
},300)
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -437,7 +550,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.local-icon{
|
||||
.local-icon {
|
||||
flex: 1;
|
||||
}
|
||||
.local {
|
||||
@@ -448,14 +561,12 @@ onUnmounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
.copy-icon {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
||||
.token-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -467,11 +578,22 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.browse-icon{
|
||||
|
||||
.tag-item-on{
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%) !important;
|
||||
}
|
||||
.tag-item-off{
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
background-image: linear-gradient(to top, rgba(255, 8, 68, 0.93) 0%, #D54941 100%) !important;
|
||||
}
|
||||
.browse-icon {
|
||||
flex: 2;
|
||||
}
|
||||
:global(.t-dialog__ctx .t-dialog--defaul) {
|
||||
margin: 0 20px;
|
||||
:global(.t-dialog__ctx .t-dialog__position) {
|
||||
padding: 48px 10px;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.setting-box {
|
||||
@@ -483,7 +605,6 @@ onUnmounted(() => {
|
||||
.setting-box {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-box {
|
||||
@@ -494,9 +615,8 @@ onUnmounted(() => {
|
||||
line-height: 400px !important;
|
||||
}
|
||||
|
||||
|
||||
.dialog-body {
|
||||
max-height: 60vh;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -515,12 +635,6 @@ onUnmounted(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-address .t-card__body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-base-info .t-descriptions__header {
|
||||
font-size: 15px;
|
||||
margin-bottom: 0;
|
||||
@@ -530,7 +644,7 @@ onUnmounted(() => {
|
||||
padding: 0 var(--td-comp-paddingLR-l) !important;
|
||||
}
|
||||
|
||||
.setting-base-info tr>td:last-child {
|
||||
.setting-base-info tr > td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<div class="title">
|
||||
<t-divider content="其余配置" align="left" />
|
||||
<t-divider content="其余配置" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<setting-icon />
|
||||
<div style="margin-left: 5px">其余配置</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
</div>
|
||||
<t-card class="card">
|
||||
<div class="other-config-container">
|
||||
@@ -29,11 +36,12 @@ import { ref, onMounted } from 'vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
import { SettingIcon } from 'tdesign-icons-vue-next';
|
||||
|
||||
const otherConfig = ref<Partial<OneBotConfig>>({
|
||||
musicSignUrl: '',
|
||||
enableLocalFile2Url: false,
|
||||
parseMultMsg: true
|
||||
parseMultMsg: true,
|
||||
});
|
||||
|
||||
const labelAlign = ref<string>();
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="URL">
|
||||
<t-input v-model="config.url" />
|
||||
@@ -11,13 +11,13 @@
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-checkbox v-model="config.reportSelfMessage" />
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="端口">
|
||||
<t-input v-model.number="config.port" type="number" />
|
||||
@@ -11,10 +11,10 @@
|
||||
<t-input v-model="config.host" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 CORS">
|
||||
<t-checkbox v-model="config.enableCors" />
|
||||
<t-switch v-model="config.enableCors" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 WS">
|
||||
<t-checkbox v-model="config.enableWebsocket" />
|
||||
<t-switch v-model="config.enableWebsocket" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
@@ -23,7 +23,7 @@
|
||||
<t-input v-model="config.token" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="URL">
|
||||
<t-input v-model="config.url" />
|
||||
@@ -11,13 +11,13 @@
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-checkbox v-model="config.reportSelfMessage" />
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
<t-form-item label="心跳间隔">
|
||||
<t-input v-model.number="config.heartInterval" type="number" />
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="主机">
|
||||
<t-input v-model="config.host" />
|
||||
@@ -14,16 +14,16 @@
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="上报自身消息">
|
||||
<t-checkbox v-model="config.reportSelfMessage" />
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="强制推送事件">
|
||||
<t-checkbox v-model="config.enableForcePushEvent" />
|
||||
<t-switch v-model="config.enableForcePushEvent" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
<t-form-item label="心跳间隔">
|
||||
<t-input v-model.number="config.heartInterval" type="number" />
|
||||
|
@@ -13,7 +13,8 @@
|
||||
"dev:shell": "vite build --mode shell",
|
||||
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
||||
"lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||
"depend": "cd dist && npm install --omit=dev"
|
||||
"depend": "cd dist && npm install --omit=dev",
|
||||
"dev:depend": "npm i && cd napcat.webui && npm i"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
@@ -25,6 +26,7 @@
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@sinclair/typebox": "^0.34.9",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/node": "^22.0.1",
|
||||
@@ -48,8 +50,7 @@
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"@sinclair/typebox": "^0.34.9"
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
|
@@ -18,13 +18,16 @@ export const LogListHandler: RequestHandler = async (_, res) => {
|
||||
const logList = await WebUiConfigWrapper.GetLogsList();
|
||||
return sendSuccess(res, logList);
|
||||
};
|
||||
|
||||
// 实时日志(SSE)
|
||||
export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
const listener = (log: string) => {
|
||||
try {
|
||||
res.write(log + '\n');
|
||||
res.write(`data: ${log}\n\n`);
|
||||
} catch (error) {
|
||||
// ignore
|
||||
console.error('向客户端写入日志数据时出错:', error);
|
||||
}
|
||||
};
|
||||
logSubscription.subscribe(listener);
|
||||
|
@@ -1,9 +1,16 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
|
||||
// CORS 中间件,跨域用
|
||||
export const cors: RequestHandler = (_, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
export const cors: RequestHandler = (req, res, next) => {
|
||||
const origin = req.headers.origin || '*';
|
||||
res.header('Access-Control-Allow-Origin', origin);
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(204);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
Reference in New Issue
Block a user