refactor: webui

This commit is contained in:
pk5ls20
2024-11-15 23:39:19 +08:00
parent 1ec1040e43
commit fe0bda11d3
33 changed files with 647 additions and 499 deletions

View File

@@ -0,0 +1,53 @@
import globals from 'globals';
import ts from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import prettier from 'eslint-plugin-prettier/recommended';
export default [
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
...ts.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
},
},
...vue.configs['flat/base'],
{
files: ['*.vue', '**/*.vue'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
},
{
rules: {
indent: ['error', 4],
semi: ['error', 'always'],
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'object-curly-spacing': ['error', 'always'],
'vue/v-for-delimiter-style': ['error', 'in'],
'vue/require-name-property': 'warn',
'vue/prefer-true-attribute-shorthand': 'warn',
'prefer-arrow-callback': 'warn',
},
},
prettier,
{
rules: {
'prettier/prettier': 'warn',
},
},
];

View File

@@ -4,18 +4,26 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"lint": "eslint . --fix",
"dev": "vite --host 127.0.0.1",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"eslint-plugin-prettier": "^5.2.1",
"qrcode": "^1.5.4",
"tdesign-vue-next": "^1.10.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-vue": "^5.1.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.31.0",
"globals": "^15.12.0",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vue-tsc": "^2.1.8"

View File

@@ -1,11 +1,7 @@
<template>
<div id="app">
<router-view />
</div>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
};
</script>
<script setup lang="ts"></script>

View File

@@ -1,14 +1,17 @@
export class QQLoginManager {
private retCredential: string;
private apiprefix: string;
private readonly apiPrefix: string;
// TODO:
//调试时http://127.0.0.1:6099/api 打包时 ../api
constructor(retCredential: string, apiprefix: string = 'http://127.0.0.1:6099/api') {
constructor(retCredential: string, apiPrefix: string = 'http://127.0.0.1:6099/api') {
this.retCredential = retCredential;
this.apiprefix = apiprefix;
this.apiPrefix = apiPrefix;
}
// TODO:
public async GetOB11Config(): Promise<any> {
try {
const ConfigResponse = await fetch(`${this.apiprefix}/OB11Config/GetConfig`, {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/GetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
@@ -22,14 +25,14 @@ export class QQLoginManager {
}
}
} catch (error) {
console.error("Error getting OB11 config:", error);
console.error('Error getting OB11 config:', error);
}
return {};
}
public async SetOB11Config(config: any): Promise<boolean> {
try {
const ConfigResponse = await fetch(`${this.apiprefix}/OB11Config/SetConfig`, {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
@@ -44,137 +47,137 @@ export class QQLoginManager {
}
}
} catch (error) {
console.error("Error setting OB11 config:", error);
console.error('Error setting OB11 config:', error);
}
return false;
}
public async checkQQLoginStatus(): Promise<boolean> {
try {
let QQLoginResponse = await fetch(`${this.apiprefix}/QQLogin/CheckLoginStatus`, {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
method: 'POST',
headers: {
'Authorization': "Bearer " + this.retCredential,
'Content-Type': 'application/json'
}
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.isLogin;
}
}
} catch (error) {
console.error("Error checking QQ login status:", error);
console.error('Error checking QQ login status:', error);
}
return false;
}
public async checkWebUiLogined(): Promise<boolean> {
try {
let LoginResponse = await fetch(`${this.apiprefix}/auth/check`, {
const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, {
method: 'POST',
headers: {
'Authorization': "Bearer " + this.retCredential,
'Content-Type': 'application/json'
}
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (LoginResponse.status == 200) {
let LoginResponseJson = await LoginResponse.json();
const LoginResponseJson = await LoginResponse.json();
if (LoginResponseJson.code == 0) {
return true;
}
}
} catch (error) {
console.error("Error checking web UI login status:", error);
console.error('Error checking web UI login status:', error);
}
return false;
}
public async loginWithToken(token: string): Promise<string | null> {
try {
let loginResponse = await fetch(`${this.apiprefix}/auth/login`, {
const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token })
body: JSON.stringify({ token: token }),
});
const loginResponseJson = await loginResponse.json();
let retCode = loginResponseJson.code;
const retCode = loginResponseJson.code;
if (retCode === 0) {
this.retCredential = loginResponseJson.data.Credential;
return this.retCredential;
}
} catch (error) {
console.error("Error logging in with token:", error);
console.error('Error logging in with token:', error);
}
return null;
}
public async getQQLoginQrcode(): Promise<string> {
try {
let QQLoginResponse = await fetch(`${this.apiprefix}/QQLogin/GetQQLoginQrcode`, {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, {
method: 'POST',
headers: {
'Authorization': "Bearer " + this.retCredential,
'Content-Type': 'application/json'
}
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.qrcode || "";
return QQLoginResponseJson.data.qrcode || '';
}
}
} catch (error) {
console.error("Error getting QQ login QR code:", error);
console.error('Error getting QQ login QR code:', error);
}
return "";
return '';
}
public async getQQQuickLoginList(): Promise<string[]> {
try {
let QQLoginResponse = await fetch(`${this.apiprefix}/QQLogin/GetQuickLoginList`, {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, {
method: 'POST',
headers: {
'Authorization': "Bearer " + this.retCredential,
'Content-Type': 'application/json'
}
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data || [];
}
}
} catch (error) {
console.error("Error getting QQ quick login list:", error);
console.error('Error getting QQ quick login list:', error);
}
return [];
}
public async setQuickLogin(uin: string): Promise<{ result: boolean, errMsg: string }> {
public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> {
try {
let QQLoginResponse = await fetch(`${this.apiprefix}/QQLogin/SetQuickLogin`, {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, {
method: 'POST',
headers: {
'Authorization': "Bearer " + this.retCredential,
'Content-Type': 'application/json'
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ uin: uin })
body: JSON.stringify({ uin: uin }),
});
if (QQLoginResponse.status == 200) {
let QQLoginResponseJson = await QQLoginResponse.json();
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return { result: true, errMsg: "" };
return { result: true, errMsg: '' };
} else {
return { result: false, errMsg: QQLoginResponseJson.message };
}
}
} catch (error) {
console.error("Error setting quick login:", error);
console.error('Error setting quick login:', error);
}
return { result: false, errMsg: "接口异常" };
return { result: false, errMsg: '接口异常' };
}
}
}

View File

@@ -1,56 +1,55 @@
<template>
<div class="dashboard-container">
<SidebarMenu :menuItems="menuItems" class="sidebar-menu" />
<div class="content">
<router-view />
<div class="dashboard-container">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
<div class="content">
<router-view />
</div>
</div>
</div>
</template>
<script>
<script setup lang="ts">
import { ref } from 'vue';
import SidebarMenu from './webui/Nav.vue';
export default {
components: {
SidebarMenu
},
data() {
return {
menuItems: [
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
{ value: 'item4', icon: 'setting', label: '其余配置', route: '/dashboard/other-config' },
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' }
]
};
}
interface MenuItem {
value: string;
icon: string;
label: string;
route: string;
}
const menuItems = ref<MenuItem[]>([
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
{ value: 'item4', icon: 'setting', label: '其余配置', route: '/dashboard/other-config' },
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
]);
</script>
<style scoped>
.dashboard-container {
display: flex;
flex-direction: row;
height: 100vh;
display: flex;
flex-direction: row;
height: 100vh;
}
.sidebar-menu {
position: relative;
z-index: 2;
position: relative;
z-index: 2;
}
.content {
flex: 1;
padding: 20px;
overflow: auto;
position: relative;
z-index: 1;
flex: 1;
padding: 20px;
overflow: auto;
position: relative;
z-index: 1;
}
@media (max-width: 768px) {
.content {
padding: 10px;
}
.content {
padding: 10px;
}
}
</style>
</style>

View File

@@ -2,67 +2,91 @@
<div class="login-container">
<h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods">
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'">Quick Login</t-button>
<t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'">QR Code</t-button>
<t-button
id="quick-login"
class="login-method"
:class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'"
>Quick Login</t-button
>
<t-button
id="qrcode-login"
class="login-method"
:class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'"
>QR Code</t-button
>
</div>
<div id="quick-login-dropdown" class="login-form" v-show="loginMethod === 'quick'">
<t-select id="quick-login-select" v-model="selectedAccount" @change="selectAccount"
placeholder="Select Account">
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select
id="quick-login-select"
v-model="selectedAccount"
placeholder="Select Account"
@change="selectAccount"
>
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select>
</div>
<div id="qrcode" class="qrcode" v-show="loginMethod === 'qrcode'">
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
<canvas ref="qrcodeCanvas"></canvas>
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import QRCode from 'qrcode';
import * as QRCode from 'qrcode';
import { useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '../backend/shell.ts';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
const loginMethod = ref('quick');
const quickLoginList = ref([]);
const selectedAccount = ref('');
const qrcodeCanvas = ref(null);
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth'));
let heartBeatTimer = null;
const loginMethod = ref<'quick' | 'qrcode'>('quick');
const quickLoginList = ref<string[]>([]);
const selectedAccount = ref<string>('');
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
let heartBeatTimer: number | null = null;
const selectAccount = async (accountName) => {
const selectAccount = async (accountName: string): Promise<void> => {
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
if (result) {
MessagePlugin.success("登录成功即将跳转");
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' });
} else {
MessagePlugin.error("登录失败," + errMsg);
await MessagePlugin.error('登录失败,' + errMsg);
}
};
const generateQrCode = (data, canvas) => {
QRCode.toCanvas(canvas, data, function (error) {
if (error) console.log(error);
console.log('QR Code generated!');
const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void => {
if (!canvas) {
console.error('Canvas element not found');
return;
}
QRCode.toCanvas(canvas, data, function (error: Error | null | undefined) {
if (error) {
console.error('Error generating QR Code:', error);
} else {
console.log('QR Code generated!');
}
});
};
const HeartBeat = async () => {
let isLogined = await qqLoginManager.checkQQLoginStatus();
const HeartBeat = async (): Promise<void> => {
const isLogined = await qqLoginManager.checkQQLoginStatus();
if (isLogined) {
clearInterval(heartBeatTimer);
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
await router.push({ path: '/dashboard/basic-info' });
}
};
const InitPages = async () => {
const InitPages = async (): Promise<void> => {
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
generateQrCode(await qqLoginManager.getQQLoginQrcode(), qrcodeCanvas.value);
heartBeatTimer = setInterval(HeartBeat, 3000);
const qrcodeData = await qqLoginManager.getQQLoginQrcode();
generateQrCode(qrcodeData, qrcodeCanvas.value);
heartBeatTimer = window.setInterval(HeartBeat, 3000);
};
onMounted(() => {
@@ -103,7 +127,7 @@ onMounted(() => {
.login-method.active {
background-color: #e6f0ff;
color: #007BFF;
color: #007bff;
}
.login-form,
@@ -125,7 +149,7 @@ onMounted(() => {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, .1);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
@@ -140,4 +164,4 @@ onMounted(() => {
width: 100%;
background-color: white;
}
</style>
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" :colon="true" :label-width="0" @submit="onSubmit">
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
<t-form-item name="password">
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
<template #prefix-icon>
@@ -14,32 +14,34 @@
</t-form-item>
</t-form>
</div>
<div class="footer">
Power By NapCat.WebUi
</div>
<div class="footer">Power By NapCat.WebUi</div>
</template>
<script setup>
<script setup lang="ts">
import '../css/style.css';
import '../css/font.css';
import { reactive, ref, onMounted } from 'vue';
import { reactive, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { LockOnIcon } from 'tdesign-icons-vue-next';
import { useRouter } from 'vue-router';
import { QQLoginManager } from '../backend/shell';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
const formData = reactive({
interface FormData {
token: string;
}
const formData: FormData = reactive({
token: '',
});
const handleLoginSuccess = async (credential) => {
const handleLoginSuccess = async (credential: string) => {
localStorage.setItem('auth', credential);
await checkLoginStatus();
};
const handleLoginFailure = (message) => {
const handleLoginFailure = (message: string) => {
MessagePlugin.error(message);
};
@@ -63,7 +65,7 @@ const checkLoginStatus = async () => {
}
};
const loginWithToken = async (token) => {
const loginWithToken = async (token: string) => {
const loginManager = new QQLoginManager('');
const credential = await loginManager.loginWithToken(token);
if (credential) {
@@ -75,15 +77,15 @@ const loginWithToken = async (token) => {
onMounted(() => {
const url = new URL(window.location.href);
const token = url.searchParams.get("token");
const token = url.searchParams.get('token');
if (token) {
loginWithToken(token);
}
checkLoginStatus();
});
const onSubmit = async ({ validateResult }) => {
if (validateResult === true) {
const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
if (validateResult) {
await loginWithToken(formData.token);
} else {
handleLoginFailure('请填写Token');
@@ -131,7 +133,7 @@ const onSubmit = async ({ validateResult }) => {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, .1);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
@@ -146,4 +148,4 @@ const onSubmit = async ({ validateResult }) => {
width: 100%;
background-color: white;
}
</style>
</style>

View File

@@ -1,77 +1,71 @@
<template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
<template #logo>
</template>
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
<template #icon>
<t-icon :name="item.icon" />
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
<template #logo> </template>
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
<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>
</router-link>
<template #operations>
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
<template #icon><t-icon :name="iconName" /></template>
</t-button>
</template>
{{ item.label }}
</t-menu-item>
</router-link>
<template #operations>
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
<template #icon><t-icon :name="iconName" /></template>
</t-button>
</template>
</t-menu>
</t-menu>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
<script setup lang="ts">
import { ref, defineProps } from 'vue';
export default defineComponent({
name: 'SidebarMenu',
props: {
menuItems: {
type: Array,
required: true
}
},
setup() {
const collapsed = ref(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref(collapsed.value ? 'menu-unfold' : 'menu-fold');
type MenuItem = {
value: string;
label: string;
route: string;
icon?: string;
disabled?: boolean;
};
const changeCollapsed = () => {
collapsed.value = !collapsed.value;
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
localStorage.setItem('sidebar-collapsed', collapsed.value);
};
defineProps<{
menuItems: MenuItem[];
}>();
return {
collapsed,
iconName,
changeCollapsed
};
}
});
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
const changeCollapsed = (): void => {
collapsed.value = !collapsed.value;
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
};
</script>
<style scoped>
.sidebar-menu {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 200px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 200px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.sidebar-menu {
width: 100px; /* 移动端侧边栏宽度 */
}
.sidebar-menu {
width: 100px; /* 移动端侧边栏宽度 */
}
}
.logo-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.menu-item {
margin-bottom: 10px;
margin-bottom: 10px;
}
</style>
</style>

View File

@@ -1,5 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import { createApp } from 'vue';
import App from './App.vue';
import {
Button as TButton,
Input as TInput,
@@ -25,7 +25,7 @@ import {
Space as TSpace,
Checkbox as TCheckbox,
Popup as TPopup,
Dialog as TDialog
Dialog as TDialog,
} from 'tdesign-vue-next';
import { router } from './router';
import 'tdesign-vue-next/es/style/index.css';
@@ -57,4 +57,4 @@ app.use(TSpace);
app.use(TCheckbox);
app.use(TPopup);
app.use(TDialog);
app.mount('#app');
app.mount('#app');

View File

@@ -3,11 +3,12 @@
<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>
<span class="item-content"><t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link></span>
<span class="item-content">
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
</span>
</t-list-item>
<t-list-item class="list-item">
<span class="item-label">版本信息:</span>
@@ -22,15 +23,11 @@
</div>
</template>
<script>
export default {
methods: {
joinQQGroup() {
// 加入QQ群的逻辑
window.open('https://jq.qq.com/?_wv=1027&k=123456789', '_blank');
}
}
}
<script setup lang="ts">
const joinQQGroup = () => {
// 加入QQ群的逻辑
window.open('https://jq.qq.com/?_wv=1027&k=123456789', '_blank');
};
</script>
<style scoped>
@@ -59,11 +56,11 @@ export default {
flex: 2;
display: flex;
flex-wrap: wrap;
justify-content: flex-end; /* 添加这一行 */
justify-content: flex-end;
}
.tag-item {
margin-right: 10px;
margin-bottom: 10px;
}
</style>
</style>

View File

@@ -1,18 +1,28 @@
<template>
<t-space>
<t-tabs v-model="value" theme="card" :addable="true" @add="showAddTabDialog" @remove="removeTab">
<t-tab-panel v-for="data in panelData" :key="data.value" :value="data.value" :label="data.label"
:removable="data.removable">
<t-tabs v-model="value" :addable="true" theme="card" @add="showAddTabDialog" @remove="removeTab">
<t-tab-panel
v-for="data in panelData"
:key="data.value"
:label="data.label"
:removable="data.removable"
:value="data.value"
>
<component :is="data.component" :config="data.config" />
</t-tab-panel>
</t-tabs>
</t-space>
<t-dialog :visible.sync="isDialogVisible" header="添加新选项卡" @confirm="addTab" @close="isDialogVisible = false">
<t-form :model="newTab" ref="form">
<t-form-item label="名称" name="name" :rules="[{ required: true, message: '请输入名称' }]">
<t-dialog
v-model:visible="isDialogVisible"
header="添加新选项卡"
@close="isDialogVisible = false"
@confirm="addTab"
>
<t-form ref="form" :model="newTab">
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
<t-input v-model="newTab.name" />
</t-form-item>
<t-form-item label="类型" name="type" :rules="[{ required: true, message: '请选择类型' }]">
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
<t-select v-model="newTab.type">
<t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option>
@@ -24,55 +34,61 @@
</t-dialog>
</template>
<script setup>
import { ref, shallowRef, onMounted, watch, nextTick } from 'vue';
<script setup lang="ts">
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { defaultOnebotConfig, mergeOnebotConfigs } from '../../../src/onebot/config/config';
import { QQLoginManager } from '../backend/shell';
import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from './network/HttpServerComponent.vue';
import HttpClientComponent from './network/HttpClientComponent.vue';
import WebsocketServerComponent from './network/WebsocketServerComponent.vue';
import WebsocketClientComponent from './network/WebsocketClientComponent.vue';
let id = 0;
const value = ref('first');
const panelData = ref([]);
const isDialogVisible = ref(false);
const newTab = ref({ name: '', type: '' });
interface PanelData {
value: string;
label: string;
removable: boolean;
component: any;
config: { name: string };
}
const componentMap = {
'httpServers': shallowRef(HttpServerComponent),
'httpClients': shallowRef(HttpClientComponent),
'websocketServers': shallowRef(WebsocketServerComponent),
'websocketClients': shallowRef(WebsocketClientComponent),
let id = 0;
const value = ref<string>('first');
const panelData = ref<PanelData[]>([]);
const isDialogVisible = ref<boolean>(false);
const newTab = ref<{ name: string; type: string }>({ name: '', type: '' });
const componentMap: Record<string, any> = {
httpServers: shallowRef(HttpServerComponent),
httpClients: shallowRef(HttpClientComponent),
websocketServers: shallowRef(WebsocketServerComponent),
websocketClients: shallowRef(WebsocketClientComponent),
};
const getOB11Config = async () => {
const getOB11Config = async (): Promise<any | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
const config = await loginManager.GetOB11Config();
return config;
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config) => {
const setOB11Config = async (config: any): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
const result = await loginManager.SetOB11Config(config);
return result;
return await loginManager.SetOB11Config(config);
};
const log = (message, data) => {
const log = (message: string, data: any) => {
console.log(message, data);
};
const createPanel = (type, name, id) => {
const createPanel = (type: string, name: string, id: number): PanelData => {
return {
value: `${type}-${id}`,
label: name,
@@ -82,10 +98,10 @@ const createPanel = (type, name, id) => {
};
};
const generatePanels = (networkConfig) => {
const panels = [];
const generatePanels = (networkConfig: any): PanelData[] => {
const panels: PanelData[] = [];
Object.keys(networkConfig).forEach((key) => {
networkConfig[key].forEach((config, index) => {
networkConfig[key].forEach((config: any, index: number) => {
const component = componentMap[key];
if (!component) {
console.error(`No component found for key: ${key}`);
@@ -100,6 +116,7 @@ const generatePanels = (networkConfig) => {
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOnebotConfigs(defaultOnebotConfig, userConfig);
const networkConfig = mergedConfig.network;
log('networkConfig:', networkConfig);
@@ -130,7 +147,11 @@ const addTab = async () => {
value.value = newPanel.value; // 强制重新渲染选项卡
};
const removeTab = ({ value: val, index }) => {
const closeDialog = () => {
isDialogVisible.value = false;
};
const removeTab = ({ value: val, index }: { value: string; index: number }) => {
if (index < 0) return false;
panelData.value.splice(index, 1);
if (panelData.value.length === 0) return;
@@ -140,8 +161,8 @@ const removeTab = ({ value: val, index }) => {
};
const syncConfig = async () => {
const networkConfig = {};
panelData.value.forEach(panel => {
const networkConfig: Record<string, any[]> = {};
panelData.value.forEach((panel) => {
const key = panel.value.split('-')[0];
if (!networkConfig[key]) {
networkConfig[key] = [];
@@ -149,6 +170,7 @@ const syncConfig = async () => {
networkConfig[key].push(panel.config);
});
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOnebotConfigs(defaultOnebotConfig, userConfig);
mergedConfig.network = networkConfig;
await setOB11Config(mergedConfig);
@@ -159,4 +181,4 @@ watch(panelData, syncConfig, { deep: true });
onMounted(() => {
loadConfig();
});
</script>
</script>

View File

@@ -4,4 +4,4 @@
<t-divider content="其余配置" align="left" />
</div>
</div>
</template>
</template>

View File

@@ -1,29 +1,42 @@
<template>
<div>
<h3>HTTP Client 配置</h3>
<t-form>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox 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-form-item>
</t-form>
</div>
<div>
<h3>HTTP Client 配置</h3>
<t-form>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox 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-form-item>
</t-form>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
config: Object,
<script setup lang="ts">
import { ref } from 'vue';
interface HttpClientConfig {
url: string;
messagePostFormat: string;
reportSelfMessage: boolean;
token: string;
debug: boolean;
}
const config = ref<HttpClientConfig>({
url: '',
messagePostFormat: '',
reportSelfMessage: false,
token: '',
debug: false,
});
</script>
</script>

View File

@@ -1,38 +1,57 @@
<template>
<div>
<h3>HTTP Server 配置</h3>
<t-form>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WebSocket">
<t-checkbox v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" type="text" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
<div>
<h3>HTTP Server 配置</h3>
<t-form>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WebSocket">
<t-checkbox v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" type="text" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
config: Object,
<script setup lang="ts">
import { ref } from 'vue';
interface HttpServerConfig {
port: number;
host: string;
enableCors: boolean;
enableWebsocket: boolean;
messagePostFormat: string;
reportSelfMessage: boolean;
token: string;
debug: boolean;
}
const config = ref<HttpServerConfig>({
port: 8080,
host: '',
enableCors: false,
enableWebsocket: false,
messagePostFormat: '',
reportSelfMessage: false,
token: '',
debug: false,
});
</script>
</script>

View File

@@ -1,32 +1,47 @@
<template>
<div>
<h3>WebSocket Client 配置</h3>
<t-form>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox 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-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
<div>
<h3>WebSocket Client 配置</h3>
<t-form>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox 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-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
config: Object,
<script setup lang="ts">
import { ref } from 'vue';
interface WsClientConfig {
url: string;
messagePostFormat: string;
reportSelfMessage: boolean;
token: string;
debug: boolean;
heartInterval: number;
}
const config = ref<WsClientConfig>({
url: '',
messagePostFormat: '',
reportSelfMessage: false,
token: '',
debug: false,
heartInterval: 0,
});
</script>
</script>

View File

@@ -1,38 +1,57 @@
<template>
<div>
<h3>WebSocket Server 配置</h3>
<t-form>
<t-form-item label="主机">
<t-input v-model="config.host" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox 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.enablePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
<div>
<h3>WebSocket Server 配置</h3>
<t-form>
<t-form-item label="主机">
<t-input v-model="config.host" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="消息格式">
<t-input v-model="config.messagePostFormat" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox 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.enablePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
config: Object,
<script setup lang="ts">
import { ref } from 'vue';
interface WsServerConfig {
host: string;
port: number;
messagePostFormat: string;
reportSelfMessage: boolean;
token: string;
enablePushEvent: boolean;
debug: boolean;
heartInterval: number;
}
const config = ref<WsServerConfig>({
host: '',
port: 8080,
messagePostFormat: '',
reportSelfMessage: false,
token: '',
enablePushEvent: false,
debug: false,
heartInterval: 0,
});
</script>
</script>

View File

@@ -1,4 +1,4 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Dashboard from '../components/Dashboard.vue';
import BasicInfo from '../pages/BasicInfo.vue';
import AboutUs from '../pages/AboutUs.vue';
@@ -8,25 +8,25 @@ import QQLogin from '../components/QQLogin.vue';
import WebUiLogin from '../components/WebUiLogin.vue';
import OtherConfig from '../pages/OtherConfig.vue';
const routes = [
{ path: '/', redirect: '/webui' },
{ path: '/webui', component: WebUiLogin, name: 'WebUiLogin' },
{ path: '/qqlogin', component: QQLogin, name: 'QQLogin' },
{
path: '/dashboard',
component: Dashboard,
children: [
{ path: '', redirect: 'basic-info' },
{ path: 'basic-info', component: BasicInfo, name: 'BasicInfo' },
{ path: 'network-config', component: NetWork, name: 'NetWork' },
{ path: 'log-view', component: LogView, name: 'LogView' },
{ path: 'other-config', component: OtherConfig, name: 'OtherConfig' },
{ path: 'about-us', component: AboutUs, name: 'AboutUs' }
]
}
]
const routes: Array<RouteRecordRaw> = [
{ path: '/', redirect: '/webui' },
{ path: '/webui', component: WebUiLogin, name: 'WebUiLogin' },
{ path: '/qqlogin', component: QQLogin, name: 'QQLogin' },
{
path: '/dashboard',
component: Dashboard,
children: [
{ path: '', redirect: 'basic-info' },
{ path: 'basic-info', component: BasicInfo, name: 'BasicInfo' },
{ path: 'network-config', component: NetWork, name: 'NetWork' },
{ path: 'log-view', component: LogView, name: 'LogView' },
{ path: 'other-config', component: OtherConfig, name: 'OtherConfig' },
{ path: 'about-us', component: AboutUs, name: 'AboutUs' },
],
},
];
export const router = createRouter({
history: createWebHashHistory(),
routes,
})
history: createWebHashHistory(),
routes,
});

View File

@@ -1,5 +0,0 @@
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -1,7 +1,34 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"jsxImportSource": "vue",
"lib": [
"DOM",
"DOM.Iterable"
],
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": [
"src/*"
]
},
"resolveJsonModule": true,
"types": [
"vite/client"
],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"experimentalDecorators": true,
"useDefineForClassFields": true
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [{"path": "./tsconfig.node.json"}]
}

View File

@@ -1,24 +1,11 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strictNullChecks": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,8 +1,8 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
base: './'
})
plugins: [vue()],
base: './',
});