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",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||||
"webui:dev": "vite",
|
"webui:dev": "vite --host",
|
||||||
"webui:build": "vite build",
|
"webui:build": "vite build",
|
||||||
"webui:preview": "vite preview"
|
"webui:preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"event-source-polyfill": "^1.0.31",
|
||||||
"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",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
|
"@types/event-source-polyfill": "^1.0.5",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vitejs/plugin-legacy": "^5.4.3",
|
"@vitejs/plugin-legacy": "^5.4.3",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
@@ -109,4 +109,4 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener('resize', haddingFbars);
|
window.removeEventListener('resize', haddingFbars);
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<t-layout class="dashboard-container">
|
<t-layout class="dashboard-container">
|
||||||
<div ref="menuRef">
|
<div v-if="!mediaQuery.matches">
|
||||||
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
|
<SidebarMenu
|
||||||
|
:menu-items="menuItems"
|
||||||
|
class="sidebar-menu"
|
||||||
|
:menu-width="sidebarWidth"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<t-layout>
|
<t-layout>
|
||||||
<router-view />
|
<router-view />
|
||||||
</t-layout>
|
</t-layout>
|
||||||
|
<div v-if="mediaQuery.matches" class="bottom-menu">
|
||||||
|
<BottomMenu :menu-items="menuItems" />
|
||||||
|
</div>
|
||||||
</t-layout>
|
</t-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import SidebarMenu from './webui/Nav.vue';
|
import SidebarMenu from './webui/Nav.vue';
|
||||||
|
import BottomMenu from './webui/NavBottom.vue';
|
||||||
import emitter from '@/ts/event-bus';
|
import emitter from '@/ts/event-bus';
|
||||||
|
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||||
|
const sidebarWidth = ['232px', '64px'];
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
value: string;
|
value: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -27,13 +37,18 @@ 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<HTMLDivElement | null>(null);
|
|
||||||
emitter.on('sendMenu', (event) => {
|
emitter.on('sendMenu', (event) => {
|
||||||
emitter.emit('sendWidth', menuRef.value?.offsetWidth);
|
const menuWidth = event ? sidebarWidth[1] : sidebarWidth[0];
|
||||||
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
|
emitter.emit('sendWidth', menuWidth);
|
||||||
|
localStorage.setItem('menuWidth', menuWidth.toString() || '0');
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
|
if (mediaQuery.matches){
|
||||||
|
localStorage.setItem('menuWidth', '0');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -49,6 +64,12 @@ onMounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
.bottom-menu {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.content {
|
.content {
|
||||||
@@ -56,3 +77,19 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<t-card class="layout">
|
<t-card class="layout" :bordered="false">
|
||||||
<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">
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-card class="layout">
|
<t-card class="layout" :bordered="false">
|
||||||
<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">
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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>
|
<template #logo>
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
|
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
|
||||||
@@ -43,10 +43,11 @@ type MenuItem = {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
menuItems: MenuItem[];
|
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 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 disBtn = ref<boolean>(false);
|
||||||
@@ -57,12 +58,10 @@ const changeCollapsed = (): void => {
|
|||||||
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
||||||
};
|
};
|
||||||
watch(collapsed, (newValue, oldValue) => {
|
watch(collapsed, (newValue, oldValue) => {
|
||||||
setTimeout(() => {
|
|
||||||
emitter.emit('sendMenu', collapsed.value);
|
emitter.emit('sendMenu', collapsed.value);
|
||||||
}, 300);
|
|
||||||
});
|
});
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const mediaQuery = window.matchMedia('(max-width: 800px)');
|
emitter.emit('sendMenu', collapsed.value);
|
||||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||||
disBtn.value = e.matches;
|
disBtn.value = e.matches;
|
||||||
if (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-weight: normal;
|
||||||
font-style: 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,
|
Aside as TAside,
|
||||||
Popconfirm as Tpopconfirm,
|
Popconfirm as Tpopconfirm,
|
||||||
Empty as TEmpty,
|
Empty as TEmpty,
|
||||||
|
Dropdown as TDropdown,
|
||||||
|
Typography as TTypographyText,
|
||||||
|
TreeSelect as TTreeSelect,
|
||||||
|
Loading as TLoading,
|
||||||
|
HeadMenu as THeadMenu
|
||||||
} 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';
|
||||||
@@ -84,4 +89,9 @@ app.use(TFooter);
|
|||||||
app.use(TAside);
|
app.use(TAside);
|
||||||
app.use(Tpopconfirm);
|
app.use(Tpopconfirm);
|
||||||
app.use(TEmpty);
|
app.use(TEmpty);
|
||||||
|
app.use(TDropdown);
|
||||||
|
app.use(TTypographyText);
|
||||||
|
app.use(TTreeSelect);
|
||||||
|
app.use(TLoading);
|
||||||
|
app.use(THeadMenu);
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
@@ -1,23 +1,97 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="about-us">
|
<div class="about-us">
|
||||||
<div>
|
<div>
|
||||||
<t-divider content="面板关于信息" align="left" />
|
<t-divider content="面板关于信息" align="left">
|
||||||
<t-alert theme="success" message="NapCat.WebUi is running" />
|
<template #content>
|
||||||
<t-list class="list">
|
<div style="display: flex; justify-content: center; align-items: center">
|
||||||
<t-list-item class="list-item">
|
<info-circle-icon></info-circle-icon>
|
||||||
<span class="item-label">开发人员:</span>
|
<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">
|
<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>
|
</span>
|
||||||
</t-list-item>
|
</t-list-item>
|
||||||
<t-list-item class="list-item">
|
<t-list-item>
|
||||||
<span class="item-label">版本信息:</span>
|
<tips-filled-icon class="item-icon" size="large" />
|
||||||
|
<span class="item-label">issues:</span>
|
||||||
<span class="item-content">
|
<span class="item-content">
|
||||||
<t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag>
|
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/issues">{{
|
||||||
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
|
githubBastData?.open_issues_count
|
||||||
<t-tag class="tag-item" theme="success">
|
}}</t-link>
|
||||||
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
|
</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>
|
||||||
|
<t-tag class="tag-item td-color"> TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </t-tag>
|
||||||
</span>
|
</span>
|
||||||
</t-list-item>
|
</t-list-item>
|
||||||
</t-list>
|
</t-list>
|
||||||
@@ -28,6 +102,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import pkg from '../../package.json';
|
import pkg from '../../package.json';
|
||||||
import { napCatVersion } from '../../../src/common/version';
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -35,23 +154,26 @@ import { napCatVersion } from '../../../src/common/version';
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.label-box {
|
||||||
.list {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
}
|
|
||||||
|
|
||||||
.list-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: 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 {
|
.item-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: bold;
|
margin-left: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-content {
|
.item-content {
|
||||||
flex: 2;
|
flex: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -64,3 +186,37 @@ import { napCatVersion } from '../../../src/common/version';
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<div class="log-view">
|
<div class="title">
|
||||||
<h1>面板日志信息</h1>
|
<t-divider content="日志查看" align="left">
|
||||||
<p>这里显示面板的日志信息。</p>
|
<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>
|
</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>
|
</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>
|
<template>
|
||||||
<div ref="headerBox" class="title">
|
<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-divider align="right">
|
||||||
<t-button @click="addConfig()">
|
<t-button @click="addConfig()">
|
||||||
<template #icon><add-icon /></template>
|
<template #icon><add-icon /></template>
|
||||||
添加配置</t-button>
|
添加配置</t-button
|
||||||
|
>
|
||||||
</t-divider>
|
</t-divider>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loadPage" ref="setting" class="setting">
|
<div v-if="loadPage" ref="setting" class="setting">
|
||||||
@@ -16,86 +24,142 @@
|
|||||||
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
|
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
|
||||||
</t-tabs>
|
</t-tabs>
|
||||||
</div>
|
</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 v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
|
||||||
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
|
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
|
||||||
<div v-for="(item, index) in cardConfig" :key="index">
|
<div v-for="(item, index) in cardConfig" :key="index">
|
||||||
<t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }"
|
<t-card
|
||||||
:header-bordered="true" class="setting-card">
|
:title="item.name"
|
||||||
|
:description="item.type"
|
||||||
|
:style="{ width: cardWidth + 'px' }"
|
||||||
|
:header-bordered="true"
|
||||||
|
class="setting-card"
|
||||||
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<t-space>
|
<t-space>
|
||||||
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
|
<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>
|
<delete-icon size="20px"></delete-icon>
|
||||||
</t-popconfirm>
|
</t-popconfirm>
|
||||||
</t-space>
|
</t-space>
|
||||||
</template>
|
</template>
|
||||||
<div class="setting-content">
|
<div class="setting-content">
|
||||||
<t-card class="card-address" :style="{
|
<t-card
|
||||||
borderLeft: '7px solid ' + (item.enable ?
|
class="card-address"
|
||||||
'var(--td-success-color)' :
|
:style="{
|
||||||
'var(--td-error-color)')
|
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>
|
>
|
||||||
|
<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>
|
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
||||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.host + ':' + item.port)"></copy-icon>
|
<copy-icon
|
||||||
|
class="copy-icon"
|
||||||
|
size="20px"
|
||||||
|
@click="copyText(item.host + ':' + item.port)"
|
||||||
|
></copy-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="local-box" v-if="item.url">
|
<div class="local-box" v-if="item.url">
|
||||||
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
|
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
|
||||||
<strong class="local" >{{ item.url }}</strong>
|
<strong class="local">{{ item.url }}</strong>
|
||||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
||||||
</div>
|
</div>
|
||||||
</t-card>
|
</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-collapse-panel header="基础信息">
|
||||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
<t-descriptions
|
||||||
class="setting-base-info">
|
size="small"
|
||||||
|
:layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||||
|
class="setting-base-info"
|
||||||
|
>
|
||||||
<t-descriptions-item v-if="item.token" label="连接密钥">
|
<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>
|
<span>{{ showToken ? item.token : '******' }}</span>
|
||||||
<browse-icon class="browse-icon" v-if="showToken" size="18px"
|
<browse-icon
|
||||||
@click="showToken = false"></browse-icon>
|
class="browse-icon"
|
||||||
<browse-off-icon class="browse-icon" v-else size="18px"
|
v-if="showToken"
|
||||||
@click="showToken = true"></browse-off-icon>
|
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>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<t-popup :showArrow="true" trigger="click">
|
<t-popup :showArrow="true" trigger="click">
|
||||||
<t-tag theme="primary">点击查看</t-tag>
|
<t-tag theme="primary">点击查看</t-tag>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div @click="copyText(item.token)">{{item.token}}</div>
|
<div @click="copyText(item.token)">{{ item.token }}</div>
|
||||||
</template>
|
</template>
|
||||||
</t-popup>
|
</t-popup>
|
||||||
</div>
|
</div>
|
||||||
</t-descriptions-item>
|
</t-descriptions-item>
|
||||||
<t-descriptions-item label="消息格式">{{ item.messagePostFormat }}</t-descriptions-item>
|
<t-descriptions-item label="消息格式">{{
|
||||||
|
item.messagePostFormat
|
||||||
|
}}</t-descriptions-item>
|
||||||
</t-descriptions>
|
</t-descriptions>
|
||||||
</t-collapse-panel>
|
</t-collapse-panel>
|
||||||
<t-collapse-panel header="状态信息">
|
<t-collapse-panel header="状态信息">
|
||||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
<t-descriptions
|
||||||
class="setting-base-info">
|
size="small"
|
||||||
|
:layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||||
|
class="setting-base-info"
|
||||||
|
>
|
||||||
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
|
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
|
||||||
<t-tag class="tag-item" :theme="item.debug ? 'success' : 'danger'">
|
<t-tag
|
||||||
{{ item.debug ? '开启' : '关闭' }}</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>
|
||||||
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
|
<t-descriptions-item
|
||||||
label="Websocket 功能">
|
v-if="item.hasOwnProperty('enableWebsocket')"
|
||||||
<t-tag class="tag-item" :theme="item.enableWebsocket ? 'success' : 'danger'">
|
label="Websocket 功能"
|
||||||
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
|
>
|
||||||
|
<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>
|
||||||
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
|
<t-descriptions-item
|
||||||
<t-tag class="tag-item" :theme="item.enableCors ? 'success' : 'danger'">
|
v-if="item.hasOwnProperty('enableCors')"
|
||||||
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
|
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>
|
||||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
<t-descriptions-item
|
||||||
label="上报自身消息">
|
v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||||
<t-tag class="tag-item" :theme="item.reportSelfMessage ? 'success' : 'danger'">
|
label="上报自身消息"
|
||||||
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
|
>
|
||||||
|
<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>
|
||||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
<t-descriptions-item
|
||||||
label="强制推送事件">
|
v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||||
<t-tag class="tag-item"
|
label="强制推送事件"
|
||||||
:theme="item.enableForcePushEvent ? 'success' : 'danger'">
|
>
|
||||||
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
|
<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-item>
|
||||||
</t-descriptions>
|
</t-descriptions>
|
||||||
</t-collapse-panel>
|
</t-collapse-panel>
|
||||||
@@ -109,16 +173,30 @@
|
|||||||
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
|
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
|
||||||
</t-card>
|
</t-card>
|
||||||
</div>
|
</div>
|
||||||
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
|
<t-dialog
|
||||||
:show-in-attached-element="true" placement="center" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog--defaul">
|
v-model:visible="visibleBody"
|
||||||
<div slot="body" class="dialog-body" >
|
: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 ref="form" :data="newTab" labelAlign="left" :model="newTab">
|
||||||
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
|
<t-form-item
|
||||||
label="名称" name="name">
|
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 style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
|
<t-form-item
|
||||||
label="类型" name="type">
|
style="text-align: left"
|
||||||
|
:rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
|
||||||
|
label="类型"
|
||||||
|
name="type"
|
||||||
|
>
|
||||||
<t-select v-model="newTab.type" @change="onloadDefault">
|
<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>
|
||||||
@@ -127,8 +205,10 @@
|
|||||||
</t-select>
|
</t-select>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<div>
|
<div>
|
||||||
<component :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
<component
|
||||||
:config="newTab.data" />
|
:is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
||||||
|
:config="newTab.data"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,8 +216,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AddIcon, DeleteIcon, Edit2Icon, ServerFilledIcon, CopyIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-vue-next';
|
import {
|
||||||
import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue';
|
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 emitter from '@/ts/event-bus';
|
||||||
import {
|
import {
|
||||||
mergeNetworkDefaultConfig,
|
mergeNetworkDefaultConfig,
|
||||||
@@ -187,7 +276,7 @@ const operateType = ref<string>('');
|
|||||||
//配置项索引
|
//配置项索引
|
||||||
const configIndex = ref<number>(0);
|
const configIndex = ref<number>(0);
|
||||||
//保存时所用数据
|
//保存时所用数据
|
||||||
const networkConfig: NetworkConfig & { [key: string]: any; } = {
|
const networkConfig: NetworkConfig & { [key: string]: any } = {
|
||||||
websocketClients: [],
|
websocketClients: [],
|
||||||
websocketServers: [],
|
websocketServers: [],
|
||||||
httpClients: [],
|
httpClients: [],
|
||||||
@@ -235,6 +324,18 @@ const editConfig = (item: any) => {
|
|||||||
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
||||||
visibleBody.value = true;
|
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 delConfig = (item: any) => {
|
||||||
const type = getKeyByValue(typeCh, item.type);
|
const type = getKeyByValue(typeCh, item.type);
|
||||||
if (type) {
|
if (type) {
|
||||||
@@ -252,7 +353,6 @@ const selectType = (key: ComponentKey) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onloadDefault = (key: ComponentKey) => {
|
const onloadDefault = (key: ComponentKey) => {
|
||||||
console.log(key);
|
|
||||||
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
|
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
|
||||||
};
|
};
|
||||||
//检测重名
|
//检测重名
|
||||||
@@ -350,22 +450,21 @@ const loadConfig = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copyText = async (text: string) => {
|
const copyText = async (text: string) => {
|
||||||
const input = document.createElement('input');
|
const textarea = document.createElement('textarea');
|
||||||
input.value = text;
|
textarea.value = text;
|
||||||
document.body.appendChild(input);
|
document.body.appendChild(textarea);
|
||||||
input.select();
|
textarea.select();
|
||||||
await navigator.clipboard.writeText(text);
|
try {
|
||||||
document.body.removeChild(input);
|
document.execCommand('copy');
|
||||||
MessagePlugin.success('复制成功');
|
MessagePlugin.success('复制成功');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败', err);
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
// 得根据卡片宽度改,懒得改了;先不管了
|
|
||||||
// if(window.innerWidth < 540) {
|
|
||||||
// infoOneCol.value= true
|
|
||||||
// } else {
|
|
||||||
// infoOneCol.value= false
|
|
||||||
// }
|
|
||||||
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
|
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
|
||||||
if (mediumScreen.matches) {
|
if (mediumScreen.matches) {
|
||||||
cardWidth.value = (tabsWidth.value - 20) / 2;
|
cardWidth.value = (tabsWidth.value - 20) / 2;
|
||||||
@@ -375,29 +474,43 @@ const handleResize = () => {
|
|||||||
cardWidth.value = tabsWidth.value;
|
cardWidth.value = tabsWidth.value;
|
||||||
}
|
}
|
||||||
loadPage.value = true;
|
loadPage.value = true;
|
||||||
setTimeout(() => {
|
setTimeout(()=>{
|
||||||
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
|
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
|
||||||
}, 300);
|
},300)
|
||||||
};
|
};
|
||||||
emitter.on('sendWidth', (width) => {
|
emitter.on('sendWidth', (width) => {
|
||||||
if (typeof width === 'number' && !isNaN(width)) {
|
if (typeof width === 'string') {
|
||||||
menuWidth.value = width;
|
const strWidth = width as string;
|
||||||
handleResize();
|
menuWidth.value = parseInt(strWidth);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
watch(menuWidth, (newValue, oldValue) => {
|
||||||
|
loadPage.value = false;
|
||||||
|
setTimeout(()=>{
|
||||||
|
handleResize();
|
||||||
|
},300)
|
||||||
|
});
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadConfig();
|
loadConfig();
|
||||||
const cachedWidth = localStorage.getItem('menuWidth');
|
const cachedWidth = localStorage.getItem('menuWidth');
|
||||||
if (cachedWidth) {
|
if (cachedWidth) {
|
||||||
menuWidth.value = parseInt(cachedWidth);
|
menuWidth.value = parseInt(cachedWidth);
|
||||||
setTimeout(() => {
|
setTimeout(()=>{
|
||||||
handleResize();
|
handleResize();
|
||||||
}, 300);
|
},300)
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', ()=>{
|
||||||
|
setTimeout(()=>{
|
||||||
|
handleResize();
|
||||||
|
},300)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', ()=>{
|
||||||
|
setTimeout(()=>{
|
||||||
|
handleResize();
|
||||||
|
},300)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -437,7 +550,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.local-icon{
|
.local-icon {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.local {
|
.local {
|
||||||
@@ -448,14 +561,12 @@ onUnmounted(() => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.copy-icon {
|
.copy-icon {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.token-view {
|
.token-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -467,11 +578,22 @@ onUnmounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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;
|
flex: 2;
|
||||||
}
|
}
|
||||||
:global(.t-dialog__ctx .t-dialog--defaul) {
|
:global(.t-dialog__ctx .t-dialog__position) {
|
||||||
margin: 0 20px;
|
padding: 48px 10px;
|
||||||
}
|
}
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.setting-box {
|
.setting-box {
|
||||||
@@ -483,7 +605,6 @@ onUnmounted(() => {
|
|||||||
.setting-box {
|
.setting-box {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-box {
|
.card-box {
|
||||||
@@ -494,9 +615,8 @@ onUnmounted(() => {
|
|||||||
line-height: 400px !important;
|
line-height: 400px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
max-height: 60vh;
|
max-height: 50vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,12 +635,6 @@ onUnmounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-address .t-card__body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-base-info .t-descriptions__header {
|
.setting-base-info .t-descriptions__header {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -530,7 +644,7 @@ onUnmounted(() => {
|
|||||||
padding: 0 var(--td-comp-paddingLR-l) !important;
|
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;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="title">
|
<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>
|
</div>
|
||||||
<t-card class="card">
|
<t-card class="card">
|
||||||
<div class="other-config-container">
|
<div class="other-config-container">
|
||||||
@@ -29,11 +36,12 @@ import { ref, onMounted } from 'vue';
|
|||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
import { QQLoginManager } from '@/backend/shell';
|
||||||
|
import { SettingIcon } from 'tdesign-icons-vue-next';
|
||||||
|
|
||||||
const otherConfig = ref<Partial<OneBotConfig>>({
|
const otherConfig = ref<Partial<OneBotConfig>>({
|
||||||
musicSignUrl: '',
|
musicSignUrl: '',
|
||||||
enableLocalFile2Url: false,
|
enableLocalFile2Url: false,
|
||||||
parseMultMsg: true
|
parseMultMsg: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const labelAlign = ref<string>();
|
const labelAlign = ref<string>();
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<t-form labelAlign="left">
|
<t-form labelAlign="left">
|
||||||
<t-form-item label="启用">
|
<t-form-item label="启用">
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-switch v-model="config.enable" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="URL">
|
<t-form-item label="URL">
|
||||||
<t-input v-model="config.url" />
|
<t-input v-model="config.url" />
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="报告自身消息">
|
<t-form-item label="报告自身消息">
|
||||||
<t-checkbox v-model="config.reportSelfMessage" />
|
<t-switch v-model="config.reportSelfMessage" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="Token">
|
<t-form-item label="Token">
|
||||||
<t-input v-model="config.token" />
|
<t-input v-model="config.token" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="调试模式">
|
<t-form-item label="调试模式">
|
||||||
<t-checkbox v-model="config.debug" />
|
<t-switch v-model="config.debug" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<t-form labelAlign="left">
|
<t-form labelAlign="left">
|
||||||
<t-form-item label="启用">
|
<t-form-item label="启用">
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-switch v-model="config.enable" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="端口">
|
<t-form-item label="端口">
|
||||||
<t-input v-model.number="config.port" type="number" />
|
<t-input v-model.number="config.port" type="number" />
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
<t-input v-model="config.host" type="text" />
|
<t-input v-model="config.host" type="text" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="启用 CORS">
|
<t-form-item label="启用 CORS">
|
||||||
<t-checkbox v-model="config.enableCors" />
|
<t-switch v-model="config.enableCors" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="启用 WS">
|
<t-form-item label="启用 WS">
|
||||||
<t-checkbox v-model="config.enableWebsocket" />
|
<t-switch v-model="config.enableWebsocket" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="消息格式">
|
<t-form-item label="消息格式">
|
||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<t-input v-model="config.token" type="text" />
|
<t-input v-model="config.token" type="text" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="调试模式">
|
<t-form-item label="调试模式">
|
||||||
<t-checkbox v-model="config.debug" />
|
<t-switch v-model="config.debug" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<t-form labelAlign="left">
|
<t-form labelAlign="left">
|
||||||
<t-form-item label="启用">
|
<t-form-item label="启用">
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-switch v-model="config.enable" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="URL">
|
<t-form-item label="URL">
|
||||||
<t-input v-model="config.url" />
|
<t-input v-model="config.url" />
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="报告自身消息">
|
<t-form-item label="报告自身消息">
|
||||||
<t-checkbox v-model="config.reportSelfMessage" />
|
<t-switch v-model="config.reportSelfMessage" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="Token">
|
<t-form-item label="Token">
|
||||||
<t-input v-model="config.token" />
|
<t-input v-model="config.token" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="调试模式">
|
<t-form-item label="调试模式">
|
||||||
<t-checkbox v-model="config.debug" />
|
<t-switch v-model="config.debug" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="心跳间隔">
|
<t-form-item label="心跳间隔">
|
||||||
<t-input v-model.number="config.heartInterval" type="number" />
|
<t-input v-model.number="config.heartInterval" type="number" />
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<t-form labelAlign="left">
|
<t-form labelAlign="left">
|
||||||
<t-form-item label="启用">
|
<t-form-item label="启用">
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-switch v-model="config.enable" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="主机">
|
<t-form-item label="主机">
|
||||||
<t-input v-model="config.host" />
|
<t-input v-model="config.host" />
|
||||||
@@ -14,16 +14,16 @@
|
|||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="上报自身消息">
|
<t-form-item label="上报自身消息">
|
||||||
<t-checkbox v-model="config.reportSelfMessage" />
|
<t-switch v-model="config.reportSelfMessage" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="Token">
|
<t-form-item label="Token">
|
||||||
<t-input v-model="config.token" />
|
<t-input v-model="config.token" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="强制推送事件">
|
<t-form-item label="强制推送事件">
|
||||||
<t-checkbox v-model="config.enableForcePushEvent" />
|
<t-switch v-model="config.enableForcePushEvent" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="调试模式">
|
<t-form-item label="调试模式">
|
||||||
<t-checkbox v-model="config.debug" />
|
<t-switch v-model="config.debug" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="心跳间隔">
|
<t-form-item label="心跳间隔">
|
||||||
<t-input v-model.number="config.heartInterval" type="number" />
|
<t-input v-model.number="config.heartInterval" type="number" />
|
||||||
|
@@ -13,7 +13,8 @@
|
|||||||
"dev:shell": "vite build --mode shell",
|
"dev:shell": "vite build --mode shell",
|
||||||
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
||||||
"lint": "eslint --fix src/**/*.{js,ts,vue}",
|
"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": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
"@sinclair/typebox": "^0.34.9",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/fluent-ffmpeg": "^2.1.24",
|
"@types/fluent-ffmpeg": "^2.1.24",
|
||||||
"@types/node": "^22.0.1",
|
"@types/node": "^22.0.1",
|
||||||
@@ -48,8 +50,7 @@
|
|||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
"vite-plugin-cp": "^4.0.8",
|
"vite-plugin-cp": "^4.0.8",
|
||||||
"vite-tsconfig-paths": "^5.1.0",
|
"vite-tsconfig-paths": "^5.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0"
|
||||||
"@sinclair/typebox": "^0.34.9"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
|
@@ -18,13 +18,16 @@ export const LogListHandler: RequestHandler = async (_, res) => {
|
|||||||
const logList = await WebUiConfigWrapper.GetLogsList();
|
const logList = await WebUiConfigWrapper.GetLogsList();
|
||||||
return sendSuccess(res, logList);
|
return sendSuccess(res, logList);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 实时日志(SSE)
|
// 实时日志(SSE)
|
||||||
export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
const listener = (log: string) => {
|
const listener = (log: string) => {
|
||||||
try {
|
try {
|
||||||
res.write(log + '\n');
|
res.write(`data: ${log}\n\n`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore
|
console.error('向客户端写入日志数据时出错:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
logSubscription.subscribe(listener);
|
logSubscription.subscribe(listener);
|
||||||
|
@@ -1,9 +1,16 @@
|
|||||||
import type { RequestHandler } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
|
|
||||||
// CORS 中间件,跨域用
|
// CORS 中间件,跨域用
|
||||||
export const cors: RequestHandler = (_, res, next) => {
|
export const cors: RequestHandler = (req, res, next) => {
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
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-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-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();
|
next();
|
||||||
};
|
};
|
Reference in New Issue
Block a user