Compare commits

..

10 Commits

54 changed files with 7041 additions and 447 deletions

1
.env.development Normal file
View File

@ -0,0 +1 @@
VITE_APP_MODE=dev

1
.env.production Normal file
View File

@ -0,0 +1 @@
VITE_APP_MODE=prod

7
.gitignore vendored
View File

@ -28,4 +28,9 @@ coverage
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
package-lock.json components.d.ts
auto-imports.d.ts
# Personal
**/_priv_*
Makefile

21
LICENCE.txt Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2024 kerms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1 +1,3 @@
# 允斯调试器的内嵌网页版上位机 # 允斯无线透传器的内嵌网页版上位机

9
auto-imports.d.ts vendored
View File

@ -1,9 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}

20
components.d.ts vendored
View File

@ -1,20 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
ElButton: typeof import('element-plus/es')['ElButton']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInput: typeof import('element-plus/es')['ElInput']
InlineSvg: typeof import('./src/components/InlineSvg.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

2
env.d.ts vendored
View File

@ -1 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link id="favicon" rel="icon" href="data:,">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <title>Vite App</title>
</head> </head>

5620
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,19 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": ". ./set_env.sh && vite",
"devh": ". ./set_env.sh && vite --host",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": ". ./set_env.sh && vite preview",
"build-only": "vite build", "build-only": ". ./set_env.sh && vite build",
"build:dev": ". ./set_env.sh && vite build --mode development",
"type-check": "vue-tsc --build --force", "type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"element-plus": "^2.6.1", "@vueuse/core": "^10.9.0",
"element-plus": "^2.7.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
@ -40,6 +43,9 @@
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0", "unplugin-vue-components": "^0.26.0",
"vite": "^5.1.6", "vite": "^5.1.6",
"vite-plugin-css-injected-by-js": "^3.5.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-singlefile": "^2.0.1",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue-tsc": "^2.0.6" "vue-tsc": "^2.0.6"
} }

3
set_env.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
export VITE_APP_GIT_TAG=$(git describe --tags | cut -d'-' -f1,2)
export VITE_APP_LAST_COMMIT=$(git log -1 --format=%cd)

View File

@ -1,64 +1,64 @@
<script setup lang="ts"> <script setup lang="ts">
import {useWsStore} from "@/stores/websocket"; import {useWsStore} from "@/stores/websocket";
import type {ControlMsg, ServerMsg} from "@/composables/broadcastChannelDef";
import type {IWebsocketService} from "@/composables/websocket/websocketService"; import type {IWebsocketService} from "@/composables/websocket/websocketService";
import {ControlEvent, ControlMsgType} from "@/composables/broadcastChannelDef";
import {getWebsocketService} from "@/composables/websocket/websocketService"; import {getWebsocketService} from "@/composables/websocket/websocketService";
import {onMounted, onUnmounted} from "vue"; import {onMounted, onUnmounted} from "vue";
import {changeFavicon} from "@/composables/importFavicon";
import {logHelloMessage} from "@/composables/logConsoleMsg";
import NavBar from "@/views/navigation/NavBar.vue";
import type {ControlMsg, ServerMsg} from "@/api";
import {ControlEvent, ControlMsgType} from "@/api";
import {routeCtrlMsg, routeModuleServerMsg} from "@/router/msgRouter";
import {globalNotify} from "@/composables/notification";
import {isDevMode} from "@/composables/buildMode";
const wsState = useWsStore(); const wsState = useWsStore();
const onClientCtrl = (msg: ControlMsg) => { const onClientCtrl = (msg: ControlMsg) => {
console.log("App.vue:", msg); if (isDevMode()) {
console.log("App.vue:", msg);
}
if (msg.type === ControlMsgType.WS_EVENT) { if (msg.type === ControlMsgType.WS_EVENT) {
wsState.$patch({state: msg.data as ControlEvent}) wsState.$patch({state: msg.data as ControlEvent})
routeCtrlMsg(msg);
if (msg.data === ControlEvent.CONNECTED) {
globalNotify("调试器已连接", "success");
}
} }
}; };
const onServerMsg = (msg: ServerMsg) => { const onServerMsg = (msg: ServerMsg) => {
console.log("App.vue:", msg); if (isDevMode()) {
console.log("App.vue:", msg);
}
routeModuleServerMsg(msg);
}; };
let websocketService: IWebsocketService; let websocketService: IWebsocketService;
onMounted(() => { onMounted(() => {
// const host = window.location
console.log("App.vue mounted")
const host = "192.168.43.61"; logHelloMessage();
let host = "";
if (isDevMode()) {
host = import.meta.env.VITE_DEVICE_HOST_NAME || "dap.local";
} else {
host = window.location.host
}
websocketService = getWebsocketService(); websocketService = getWebsocketService();
websocketService.init(host, onServerMsg, onClientCtrl); websocketService.init(host, onServerMsg, onClientCtrl);
changeFavicon();
}); });
onUnmounted(() => { onUnmounted(() => {
}); });
import NavBar from "@/views/navigation/NavBar.vue";
</script> </script>
<template> <template>
<header> <div class="flex flex-col h-screen">
<nav-bar/> <header>
</header> <nav-bar/>
</header>
<p class="m-0">This is body</p> <RouterView/>
<RouterView/> </div>
<p>end</p>
<!-- <p>{{ test }}</p>-->
<el-button type="danger"><p>test</p></el-button>
</template> </template>
<style>
.app {
background-color: #ddd;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
.router-active {
background-color: #666;
cursor: default;
}
</style>

View File

@ -1,15 +1,12 @@
import {sendJsonMsg} from '@/composables/broadcastChannelDef' import {type ApiJsonMsg, sendJsonMsg, WtModuleID} from '@/api'
export enum WifiCmd {
import {type ApiJsonMsg} from '@/api'
const WifiModuleID = 1;
enum WifiCmd {
UNKNOWN = 0, UNKNOWN = 0,
WIFI_API_JSON_GET_AP_INFO, WIFI_API_JSON_STA_GET_AP_INFO,
WIFI_API_JSON_CONNECT, WIFI_API_JSON_CONNECT,
WIFI_API_JSON_GET_SCAN, WIFI_API_JSON_GET_SCAN,
WIFI_API_JSON_DISCONNECT, WIFI_API_JSON_DISCONNECT,
WIFI_API_JSON_AP_GET_INFO,
} }
interface WifiMsgOut extends ApiJsonMsg { interface WifiMsgOut extends ApiJsonMsg {
@ -19,23 +16,31 @@ interface WifiMsgOut extends ApiJsonMsg {
export function wifi_get_scan_list() { export function wifi_get_scan_list() {
const msg : WifiMsgOut = { const msg : WifiMsgOut = {
module: WifiModuleID, module: WtModuleID.WIFI,
cmd: WifiCmd.WIFI_API_JSON_GET_SCAN, cmd: WifiCmd.WIFI_API_JSON_GET_SCAN,
} }
sendJsonMsg(msg); sendJsonMsg(msg);
} }
export function wifi_get_ap_info() { export function wifi_sta_get_ap_info() {
const msg : WifiMsgOut = { const msg : WifiMsgOut = {
module: WifiModuleID, module: WtModuleID.WIFI,
cmd: WifiCmd.WIFI_API_JSON_GET_AP_INFO, cmd: WifiCmd.WIFI_API_JSON_STA_GET_AP_INFO,
}
sendJsonMsg(msg);
}
export function wifi_ap_get_info() {
const msg : WifiMsgOut = {
module: WtModuleID.WIFI,
cmd: WifiCmd.WIFI_API_JSON_AP_GET_INFO,
} }
sendJsonMsg(msg); sendJsonMsg(msg);
} }
export function wifi_connect_to(ssid: string, password: string) { export function wifi_connect_to(ssid: string, password: string) {
const msg: WifiMsgOut = { const msg: WifiMsgOut = {
module: WifiModuleID, module: WtModuleID.WIFI,
cmd: WifiCmd.WIFI_API_JSON_CONNECT, cmd: WifiCmd.WIFI_API_JSON_CONNECT,
ssid: ssid, ssid: ssid,
password: password, password: password,
@ -43,13 +48,16 @@ export function wifi_connect_to(ssid: string, password: string) {
sendJsonMsg(msg); sendJsonMsg(msg);
} }
export interface WifiInfo { export interface WifiInfo extends ApiJsonMsg {
rssi: number; rssi: number;
ssid: string; ssid: string;
gateway: string;
ip: string;
mac: string; mac: string;
netmask: string;
wifiLogo?: string; wifiLogo?: string;
} }
export interface WifiList { export interface WifiList extends ApiJsonMsg {
scan_list: Array<WifiInfo>; scan_list: Array<WifiInfo>;
} }

52
src/api/binDataDef.ts Normal file
View File

@ -0,0 +1,52 @@
import type {WtModuleID} from "@/api/index";
export enum WtDataType {
RESERVED = 0x00,
/* primitive type */
EVENT = 0x02,
ROUTE_HDR = 0x03,
RAW_BROADCAST = 0x04,
/* broadcast data */
CMD_BROADCAST = 0x11,
/* targeted data */
RAW = 0x20,
CMD = 0x21,
RESPONSE = 0x22,
/* standard protocols */
PROTOBUF = 0x40,
JSON = 0x41,
MQTT = 0x42,
}
export interface ApiBinaryMsg {
data_type: WtDataType,
module: WtModuleID,
sub_mod: number,
payload: Uint8Array;
}
export function decodeHeader(arrayBuffer: ArrayBuffer) : ApiBinaryMsg {
// Create a DataView to access the data in the ArrayBuffer
const dataView = new DataView(arrayBuffer);
// Extract the data_type from the first byte
const data_type = dataView.getUint8(0) as WtDataType;
// Extract the module_id and sub_id from the next bytes
const module = dataView.getUint8(1);
const sub_mod = dataView.getUint8(2);
const payload = new Uint8Array(arrayBuffer.slice(4));
// Constructing the header object
return {
data_type,
module,
sub_mod,
payload,
};
}

View File

@ -1,4 +1,60 @@
import {getWebsocketService} from "@/composables/websocket/websocketService";
import type {ApiBinaryMsg} from "@/api/binDataDef";
export interface ApiJsonMsg { export interface ApiJsonMsg {
module: number; module: number;
cmd: number; cmd: number;
}
export enum ControlMsgType {
WS_EVENT = "WS_EVENT",
WS_SET_HOST = "WS_SET_HOST",
WS_GET_STATE = "WS_GET_STATE",
}
export enum ControlEvent {
DISCONNECTED = "DISCONNECTED",
LOADED = "LOADED",
CONNECTED = "CONNECTED",
CONNECTING = "CONNECTING",
BROKEN = "BROKEN",
}
export interface ControlMsg {
type: ControlMsgType,
data: ControlEvent | string,
}
export interface ServerMsg {
type: "json" | "binary"
data: string | ArrayBuffer;
}
export enum WtModuleID {
WIFI = 1,
DATA_FLOW = 2,
UART = 4,
}
export function sendJsonMsg(apiJsonMsg: ApiJsonMsg) {
const msg: ServerMsg = {
type: "json",
data: JSON.stringify(apiJsonMsg),
};
getWebsocketService().send(msg);
}
export function sendBinMsg(binMsg: ApiBinaryMsg) {
const buffer = new Uint8Array(4 + binMsg.payload.length);
buffer[0] = binMsg.data_type;
buffer[1] = binMsg.module;
buffer[2] = binMsg.sub_mod;
buffer[3] = 0; // Reserved byte
buffer.set(binMsg.payload, 4); // Append payload after header
const msg: ServerMsg = {
type: "binary",
data: buffer,
};
getWebsocketService().send(msg);
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="m136-80-56-56 264-264H160v-80h320v320h-80v-184L136-80Zm344-400v-320h80v184l264-264 56 56-264 264h184v80H480Z"/></svg>

After

Width:  |  Height:  |  Size: 206 B

45
src/assets/icon/favicon.svg Executable file
View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 170 170">
<style type="text/css">
.st0{fill:#FEAD04;}
.st1{fill:#032E3E;}
.st2{fill:#FFFFFF;}
.st3{fill:#FF0135;}
.st4{fill:#FA7E0C;}
</style>
<path class="st0" d="M20.1,59.7h29.7V35.5H21.6c-0.8,0-1.3,0.2-1.8,0.7L9.5,45.4c-3,2.9-2.6,5.1,0.5,7.5l8.7,6.4
C19.1,59.6,19.6,59.7,20.1,59.7L20.1,59.7z"/>
<path class="st1" d="M27.1,50.5h23.4v-7H27.1c-2,0-3.6,1.6-3.6,3.5S25.1,50.5,27.1,50.5L27.1,50.5z"/>
<path class="st1" d="M79.7,168c31.7,0.7,58.5-13.8,80.2-54.7c2.7-5.5-2.1-9.9-11-9.9c-13.7,0.1-35.1-17.6-36.6-26.4
c-17.5,6.1-26.8,14-40.1,14c-11.1,0-15-0.8-22.3-4.9c-1.9,0.6-3.9,1-6.2,0.9c-8.3-0.3-22.9-7.6-34.4-24.1c-3.8-5.5-5.9-2.6-6.9,3.5
C-6.4,116.8,29.1,166.3,79.7,168L79.7,168z"/>
<path class="st2" d="M76.4,91.7c-11.1,0-19.5-4.5-23.6-7.1V36.3C57.5,28.1,67.7,23,79.5,23h0.6c9.6,0.1,28,8.6,29.3,28.4v24.5
c-3.3,4-12.8,13.8-28.1,15.6C79.7,91.7,78,91.7,76.4,91.7z"/>
<path class="st1" d="M79.5,26c0.2,0,0.4,0,0.6,0c4,0.1,10.5,2,15.9,6.1c6.4,4.8,9.9,11.3,10.4,19.4v23.3
c-3.6,4.1-12.1,12.1-25.4,13.7c-1.5,0.2-3,0.3-4.5,0.3c-9.2,0-16.5-3.3-20.6-5.8V37.1C60.2,30.2,69.1,26,79.5,26L79.5,26 M79.5,20
c-13.7,0-24.7,6.2-29.7,15.5v50.7c2.5,1.8,12.5,8.6,26.6,8.6c1.7,0,3.4-0.1,5.2-0.3c18.6-2.2,28.9-15.1,30.7-17.5
c0-8.5,0-17.1,0-25.6C111,29,90.5,20.1,80.2,20C79.9,20,79.7,20,79.5,20L79.5,20z M49.8,86.2L49.8,86.2L49.8,86.2z M49.8,86.2
L49.8,86.2L49.8,86.2L49.8,86.2z"/>
<circle class="st1" cx="77.6" cy="47.9" r="7.5"/>
<path class="st2" d="M112.4,76.9c-37.2-15.2-45.8-17.1-62.6,9.3c-10.3,16.3-10.5,31.2-6.4,45c3.3,11,11.6,24.1,27.9,27
c0.3,0.1-6.7-13.9,1.6-35.5C78.4,108.6,92.3,88.1,112.4,76.9L112.4,76.9z"/>
<path class="st3" d="M78.4,167.9c0.4,0,0.9,0,1.3,0.1c31.7,0.7,58.5-13.8,80.2-54.7c2.7-5.5-2.1-9.9-11-9.9
c-11.3,0.1-28-12.1-34.2-21.3c-5.4,2.9-10.9,6.4-16,10.8c-11,9.3-20.9,20.3-24.9,34.6c-2.7,10-2.7,21.8,1.8,36.1L78.4,167.9
L78.4,167.9z"/>
<path class="st0" d="M156.7,43.3l-22.1,71.4c-0.2,0.7,0.2,1.9,0.9,1.8l6.5-0.9c0.8-0.1,1.5-0.2,1.8-0.9l25.1-64.5
c0.3-0.7-0.2-1.5-0.9-1.8l-3.6-1.5l-5.8-4.5C157.9,41.9,156.9,42.6,156.7,43.3L156.7,43.3z"/>
<path class="st4" d="M143.6,114.8c0-0.1,0.1-0.1,0.1-0.2l25.1-64.5c0.3-0.7-0.2-1.5-0.9-1.8l-3-1.2l-24.5,66.5L143.6,114.8
L143.6,114.8z"/>
<path class="st0" d="M139.3,46.2L126,117.5c-0.1,0.8,0.4,1.5,1.1,1.6l8.6,1.6c0.8,0.1,1.5-0.4,1.6-1.1l13.3-71.3
c0.1-0.8-0.4-1.5-1.1-1.6l-8.6-1.6C140.2,44.9,139.4,45.4,139.3,46.2z"/>
<path class="st0" d="M121,45.3l1.6,76.8c0,0.8,0.7,1.4,1.4,1.4l8.8-0.2c0.8,0,1.4-0.7,1.4-1.4l-1.6-76.8c0-0.8-0.7-1.4-1.4-1.4
l-8.8,0.2C121.6,43.9,121,44.6,121,45.3z"/>
<path class="st4" d="M132.9,123.3c0.8,0,1.4-0.7,1.4-1.4L133,58.5l-1,4.2L132.9,123.3z"/>
<path class="st4" d="M138,111.1c0.8,0.1,1.5-0.4,1.6-1.1l11.1-62.4l-1.8,3.9L138,111.1z"/>
<path class="st1" d="M163.3,70.5c-14.3,0.1-41.6,2.9-64.6,22.4c-11,9.3-20.9,20.3-24.9,34.6c-2.7,10-2.7,21.8,1.8,36.1l-9.4-1.8
c-3.8-14.1-3.5-26.1-0.7-36.5C70,108.9,81,96.7,93.1,86.4c27.1-22.9,59.5-24.7,73.6-24.4L163.3,70.5L163.3,70.5z"/>
<path class="st1" d="M79.7,168c22.2,0.5,42.1-6.5,59.5-24.9c-1.5-1.6-3.8-4.1-4.5-4.5c-0.9-0.5-2.8-1.9-6.6,2.1
c-17.8,19.3-51.4,18.1-51.4,18.1c-1.1,0.1-2.2-0.1-3.2,0.1c-5.5,1.2-8.5,4.1-9.5,7C69.1,167,74.3,167.8,79.7,168L79.7,168z"/>
<path class="st1" d="M94.2,132.9c-2.3-0.4-3.8-2.6-3.4-4.9c0.4-2.3,2.6-3.8,4.9-3.4c0.1,0,22.6,4.4,39.9-13.4c1.6-1.7,4.3-1.7,6-0.1
s1.7,4.3,0.1,6C121.1,138.1,94.2,132.9,94.2,132.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16.949 14.121 19.071 12a5.008 5.008 0 0 0 0-7.071 5.006 5.006 0 0 0-7.071 0l-.707.707 1.414 1.414.707-.707a3.007 3.007 0 0 1 4.243 0 3.005 3.005 0 0 1 0 4.243l-2.122 2.121a2.723 2.723 0 0 1-.844.57L13.414 12l1.414-1.414-.707-.707a4.965 4.965 0 0 0-3.535-1.465c-.235 0-.464.032-.691.066L3.707 2.293 2.293 3.707l18 18 1.414-1.414-5.536-5.536c.277-.184.538-.396.778-.636zm-6.363 3.536a3.007 3.007 0 0 1-4.243 0 3.005 3.005 0 0 1 0-4.243l1.476-1.475-1.414-1.414L4.929 12a5.008 5.008 0 0 0 0 7.071 4.983 4.983 0 0 0 3.535 1.462A4.982 4.982 0 0 0 12 19.071l.707-.707-1.414-1.414-.707.707z"></path></svg>

After

Width:  |  Height:  |  Size: 667 B

1
src/assets/icon/link.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.465 11.293c1.133-1.133 3.109-1.133 4.242 0l.707.707 1.414-1.414-.707-.707c-.943-.944-2.199-1.465-3.535-1.465s-2.592.521-3.535 1.465L4.929 12a5.008 5.008 0 0 0 0 7.071 4.983 4.983 0 0 0 3.535 1.462A4.982 4.982 0 0 0 12 19.071l.707-.707-1.414-1.414-.707.707a3.007 3.007 0 0 1-4.243 0 3.005 3.005 0 0 1 0-4.243l2.122-2.121z"></path><path d="m12 4.929-.707.707 1.414 1.414.707-.707a3.007 3.007 0 0 1 4.243 0 3.005 3.005 0 0 1 0 4.243l-2.122 2.121c-1.133 1.133-3.109 1.133-4.242 0L10.586 12l-1.414 1.414.707.707c.943.944 2.199 1.465 3.535 1.465s2.592-.521 3.535-1.465L19.071 12a5.008 5.008 0 0 0 0-7.071 5.006 5.006 0 0 0-7.071 0z"></path></svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M120-120v-320h80v184l504-504H520v-80h320v320h-80v-184L256-200h184v80H120Z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M876.8 156.8c0-9.6-3.2-16-9.6-22.4-6.4-6.4-12.8-9.6-22.4-9.6-9.6 0-16 3.2-22.4 9.6L736 220.8c-64-32-137.6-51.2-224-60.8-160 16-288 73.6-377.6 176C44.8 438.4 0 496 0 512s48 73.6 134.4 176c22.4 25.6 44.8 48 73.6 67.2l-86.4 89.6c-6.4 6.4-9.6 12.8-9.6 22.4 0 9.6 3.2 16 9.6 22.4 6.4 6.4 12.8 9.6 22.4 9.6 9.6 0 16-3.2 22.4-9.6l704-710.4c3.2-6.4 6.4-12.8 6.4-22.4Zm-646.4 528c-76.8-70.4-128-128-153.6-172.8 28.8-48 80-105.6 153.6-172.8C304 272 400 230.4 512 224c64 3.2 124.8 19.2 176 44.8l-54.4 54.4C598.4 300.8 560 288 512 288c-64 0-115.2 22.4-160 64s-64 96-64 160c0 48 12.8 89.6 35.2 124.8L256 707.2c-9.6-6.4-19.2-16-25.6-22.4Zm140.8-96c-12.8-22.4-19.2-48-19.2-76.8 0-44.8 16-83.2 48-112 32-28.8 67.2-48 112-48 28.8 0 54.4 6.4 73.6 19.2zM889.599 336c-12.8-16-28.8-28.8-41.6-41.6l-48 48c73.6 67.2 124.8 124.8 150.4 169.6-28.8 48-80 105.6-153.6 172.8-73.6 67.2-172.8 108.8-284.8 115.2-51.2-3.2-99.2-12.8-140.8-28.8l-48 48c57.6 22.4 118.4 38.4 188.8 44.8 160-16 288-73.6 377.6-176C979.199 585.6 1024 528 1024 512s-48.001-73.6-134.401-176Z"></path><path fill="currentColor" d="M511.998 672c-12.8 0-25.6-3.2-38.4-6.4l-51.2 51.2c28.8 12.8 57.6 19.2 89.6 19.2 64 0 115.2-22.4 160-64 41.6-41.6 64-96 64-160 0-32-6.4-64-19.2-89.6l-51.2 51.2c3.2 12.8 6.4 25.6 6.4 38.4 0 44.8-16 83.2-48 112-32 28.8-67.2 48-112 48Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/assets/icon/view.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288zm0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.192 160.192 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160"></path></svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19.4998H12.01M2 8.81929C3.69692 7.30051 5.74166 6.16236 8 5.53906M5 12.8584C5.86251 12.0129 6.87754 11.3223 8 10.8319M16 5.53906C18.2583 6.16236 20.3031 7.30051 22 8.81929M16 10.8319C17.1225 11.3223 18.1375 12.0129 19 12.8584M12 4.5V15.4998" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1,3 @@
.todo-menu-item {
@apply opacity-30
}

21
src/assets/page.css Normal file
View File

@ -0,0 +1,21 @@
.text-layout {
@apply mx-auto max-w-2xl w-full sm:min-w-[640px] px-2
}
.page-title {
@apply text-center my-6 text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl
}
.router-link {
@apply text-sm text-gray-500 hover:text-gray-500
}
.router-link-active {
@apply text-blue-600 font-bold;
cursor: default;
}
.el-checkbox:hover {
background-color: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-8);
}

View File

@ -0,0 +1,135 @@
.tgl {
display: none;
}
.tgl, .tgl:after, .tgl:before, .tgl *, .tgl *:after, .tgl *:before, .tgl + .tgl-btn {
box-sizing: border-box;
}
.tgl + .tgl-btn {
border-bottom: 2px ridge;
display: block;
width: 4em;
height: 2em;
position: relative;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tgl + .tgl-btn:after, .tgl + .tgl-btn:before {
position: relative;
display: block;
content: "";
width: 50%;
height: 100%;
}
.tgl + .tgl-btn:after {
left: 0;
}
.tgl + .tgl-btn:before {
display: none;
}
.tgl:checked + .tgl-btn:after {
left: 50%;
}
/**
* Skewed switch
*/
.tgl-skewed + .tgl-btn {
overflow: hidden;
backface-visibility: hidden;
transition: all 0.2s ease;
font-family: sans-serif;
background: #888;
border-radius: 4px;
}
.tgl-skewed + .tgl-btn:after, .tgl-skewed + .tgl-btn:before {
display: inline-block;
transition: all 0.1s ease;
width: 100%;
text-align: center;
position: absolute;
line-height: 2em;
font-weight: bold;
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
}
.tgl-skewed + .tgl-btn:after {
left: 100%;
content: attr(data-tg-on);
}
.tgl-skewed + .tgl-btn:before {
left: 0;
content: attr(data-tg-off);
}
.tgl-skewed + .tgl-btn:active {
background: #888;
}
.tgl-skewed + .tgl-btn:active:before {
left: -10%;
}
.tgl-skewed:checked + .tgl-btn {
background: #86d993;
}
.tgl-skewed:checked + .tgl-btn:before {
left: -100%;
}
.tgl-skewed:checked + .tgl-btn:after {
left: 0;
}
.tgl-skewed:checked + .tgl-btn:active:after {
left: 10%;
}
.tgl-skewed:disabled + .tgl-btn {
opacity: 0.4;
cursor: not-allowed;
border: none;
}
/* FLIP */
.tgl-flip + .tgl-btn {
padding: 2px;
transition: all 0.2s ease;
font-family: sans-serif;
perspective: 100px;
}
.tgl-flip + .tgl-btn:after, .tgl-flip + .tgl-btn:before {
display: inline-block;
transition: all 0.4s ease;
width: 100%;
text-align: center;
position: absolute;
line-height: 2em;
font-weight: bold;
color: #fff;
top: 0;
left: 0;
backface-visibility: hidden;
border-radius: 4px;
}
.tgl-flip + .tgl-btn:after {
content: attr(data-tg-on);
background: #02C66F;
transform: rotateY(-180deg);
}
.tgl-flip + .tgl-btn:before {
background: #FF3A19;
content: attr(data-tg-off);
}
.tgl-flip + .tgl-btn:active:before {
transform: rotateY(-20deg);
}
.tgl-flip:checked + .tgl-btn:before {
transform: rotateY(180deg);
}
.tgl-flip:checked + .tgl-btn:after {
transform: rotateY(0);
left: 0;
background: #7FC6A6;
}
.tgl-flip:checked + .tgl-btn:active:after {
transform: rotateY(20deg);
}

View File

@ -0,0 +1,25 @@
<template>
<div class="flex justify-between">
<p v-if="isPasswordVisible">{{ password }}</p>
<p v-else></p>
<span @click="togglePasswordVisibility" class="flex items-center">
<InlineSvg v-show="isPasswordVisible" name="view" width="16"></InlineSvg>
<InlineSvg v-show="!isPasswordVisible" name="view-hide" width="16"></InlineSvg>
</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import InlineSvg from "@/components/InlineSvg.vue";
const isPasswordVisible = ref(false);
defineProps<{
password: string
}>()
const togglePasswordVisibility = () => {
isPasswordVisible.value = !isPasswordVisible.value;
};
</script>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import {ref} from "vue";
const isChecked = ref(false);
const props = defineProps({
id: {
type: String,
required: true,
},
class: {
type: String,
required: false,
}
});
const clickEmit = defineEmits(['click']);
const handleClick = () => {
clickEmit('click', props.id, !isChecked.value);
};
</script>
<template>
<div :class="props.class">
<input :id="props.id" type="checkbox" class="tgl tgl-skewed" v-model="isChecked" @click="handleClick"/>
<label data-tg-off="OFF" data-tg-on="ON" :for="props.id" class="tgl-btn"></label>
</div>
</template>

View File

@ -1,43 +1,4 @@
import {type ApiJsonMsg} from "@/api"
import {getWebsocketService} from "@/composables/websocket/websocketService";
export const toServer = new BroadcastChannel("toServer"); export const toServer = new BroadcastChannel("toServer");
export const toClient = new BroadcastChannel("toClient"); export const toClient = new BroadcastChannel("toClient");
export const toWebsocketCtrl = new BroadcastChannel("toWebsocketCtrl"); export const toWebsocketCtrl = new BroadcastChannel("toWebsocketCtrl");
export const toClientCtrl = new BroadcastChannel("toClientCtrl"); export const toClientCtrl = new BroadcastChannel("toClientCtrl");
export enum ControlMsgType {
WS_EVENT = "WS_EVENT",
WS_SET_HOST = "WS_SET_HOST",
WS_GET_STATE = "WS_GET_STATE",
}
export enum ControlEvent {
DISCONNECTED = "DISCONNECTED",
LOADED = "LOADED",
CONNECTED = "CONNECTED",
CONNECTING = "CONNECTING",
}
export interface ControlMsg {
type: ControlMsgType,
data: ControlEvent | string,
}
export interface ServerMsg {
type: "json" | "binary"
data: object
}
export function sendJsonMsg(apiJsonMsg: ApiJsonMsg) {
const msg: ServerMsg = {
type: "json",
data: apiJsonMsg,
};
getWebsocketService().send(msg);
// toServer.postMessage(msg);
}
export function sendBinMsg(msg: ApiJsonMsg) {
// toServer.postMessage(JSON.stringify(msg));
}

View File

@ -0,0 +1,3 @@
export function isDevMode() {
return import.meta.env.VITE_APP_MODE === 'dev';
}

View File

@ -0,0 +1,20 @@
import {h, render} from "vue";
import MYSVG from "@/assets/icon/favicon.svg";
export function changeFavicon() {
const SVGComponent = MYSVG;
const container = document.createElement('div');
render(h(SVGComponent), container);
const svgElement = container.innerHTML;
const svgEncoded = encodeURIComponent(svgElement)
.replace(/'/g, '%27')
.replace(/"/g, '%22');
const link = document.getElementById('favicon');
if (link instanceof HTMLLinkElement) {
link.href = `data:image/svg+xml,${svgEncoded}`;
}
}

View File

@ -0,0 +1,22 @@
export function logHelloMessage() {
console.log(
" ███████\n" +
" █▒ ██\n" +
" ▒▒▒▒▒▒▒▒█ ██ ██ ▒\n" +
" ▒▒▒▒▒▒▒▒▒█ ██░ ░█ ▒▒▒ ▒▒ ▒▒▒\n" +
" █ ██ ░█ ▒▒▒▒▒▒▒▒▒\n" +
"███ ██ ░█ ██████▓▓█\n" +
"█████ █▒ ░███▒▒▒▒▒▒▒▒\n" +
"█████████░ ▓██▓▓▓▓▒▒▒▒▒▒\n" +
"████████▒ ▒██▓▓▓▓▓▓▒▒▒▒▒\n" +
" ███████ ██▓▓▓▓▓▓▓▓▒▒▒▒▓▓▓\n" +
" ███████ ██▓▓▓▓▓▓▓▓▓▒██▓▓▓▓\n" +
" ██████ ░█▓▓▓▓███████▓▓▓▓▓\n" +
" █████ ██▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n" +
" █████ ██▓▓▓▓▓▓▓▓▓▓███\n" +
" ████▒█▓▓▓▓▓█████\n" +
" ██████████\n" +
"\n" +
"Logo是什么意义意义就是...没有意义。\n" +
"大概是一起去整点赛博薯条吧。");
}

View File

@ -0,0 +1,22 @@
import {ElMessage, ElNotification} from "element-plus";
type NotificationType = 'error' | 'warning' | 'info' | 'success' ;
export function globalNotify(msg: string, type: NotificationType) {
ElMessage({
message: msg,
grouping: true,
type: type,
duration: 2000,
showClose: true,
offset: 50,
})
}
export function globalNotifyRightSide(msg: string, type: NotificationType) {
ElNotification({
message: msg,
type: type,
duration: 1500,
})
}

View File

@ -1,9 +1,9 @@
import MyWorker from '@/composables/websocket/ws.sharedworker?sharedworker&inline' import MyWorker from '@/composables/websocket/ws.sharedworker?sharedworker'
import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper"; import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper";
import type {ControlMsg, ServerMsg} from "@/composables/broadcastChannelDef"; import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef";
import {ControlMsgType, toClient, toClientCtrl} from "@/composables/broadcastChannelDef"; import type {ControlMsg, ServerMsg} from "@/api";
import {ControlEvent, ControlMsgType} from "@/api";
const toServer = new BroadcastChannel("toServer"); import {isDevMode} from "@/composables/buildMode";
export interface IWebsocketService { export interface IWebsocketService {
init(host: string, init(host: string,
@ -29,13 +29,18 @@ class WebsocketShared implements IWebsocketService{
public static getInstance(): IWebsocketService { public static getInstance(): IWebsocketService {
if (!WebsocketShared.instance) { if (!WebsocketShared.instance) {
if (isDevMode()) {
console.log("New Shared Worker");
}
WebsocketShared.instance = new WebsocketShared(); WebsocketShared.instance = new WebsocketShared();
} }
return WebsocketShared.instance; return WebsocketShared.instance;
} }
private constructor() { private constructor() {
console.log("Shared Websocket init") if (isDevMode()) {
console.log("Shared Websocket init");
}
this.msgCallback = () => {} this.msgCallback = () => {}
this.ctrlCallback = () => {} this.ctrlCallback = () => {}
this.messageEventProxy = this.messageEventProxy.bind(this); this.messageEventProxy = this.messageEventProxy.bind(this);
@ -44,14 +49,15 @@ class WebsocketShared implements IWebsocketService{
} }
init(host: string, msgCallback: (msg: ServerMsg) => any, ctrlCallback: (msg: ControlMsg) => any): void { init(host: string, msgCallback: (msg: ServerMsg) => any, ctrlCallback: (msg: ControlMsg) => any): void {
console.log("webworker init") if (isDevMode()) {
console.log("webworker init");
}
toClient.removeEventListener("message", this.messageEventProxy); toClient.removeEventListener("message", this.messageEventProxy);
toClientCtrl.removeEventListener("message", this.controlEventProxy); toClientCtrl.removeEventListener("message", this.controlEventProxy);
this.msgCallback = msgCallback; this.msgCallback = msgCallback;
this.ctrlCallback = ctrlCallback; this.ctrlCallback = ctrlCallback;
toClient.addEventListener("message", this.messageEventProxy); toClient.addEventListener("message", this.messageEventProxy);
toClientCtrl.addEventListener("message", this.controlEventProxy); toClientCtrl.addEventListener("message", this.controlEventProxy);
// this.worker.port.onmessage = this.controlEventProxy;
this.worker.port.postMessage({type: ControlMsgType.WS_SET_HOST, data: host} as ControlMsg) this.worker.port.postMessage({type: ControlMsgType.WS_SET_HOST, data: host} as ControlMsg)
} }
@ -60,9 +66,11 @@ class WebsocketShared implements IWebsocketService{
toClientCtrl.removeEventListener("message", this.controlEventProxy); toClientCtrl.removeEventListener("message", this.controlEventProxy);
} }
reload(): void {
// this.worker.terminate();
}
send(msg: ServerMsg): void { send(msg: ServerMsg): void {
console.log("Websocket Service send (not really)", msg)
toServer.postMessage(msg); toServer.postMessage(msg);
} }
@ -71,6 +79,7 @@ class WebsocketShared implements IWebsocketService{
} }
controlEventProxy(ev: MessageEvent<ControlMsg>) { controlEventProxy(ev: MessageEvent<ControlMsg>) {
this.ctrlCallback(ev.data); this.ctrlCallback(ev.data);
} }
} }
@ -93,8 +102,9 @@ class WebsocketClassic implements IWebsocketService{
} }
init(host: string, msgCallback: (ev: ServerMsg) => any, ctrlCallback: (msg: ControlMsg) => any): void { init(host: string, msgCallback: (ev: ServerMsg) => any, ctrlCallback: (msg: ControlMsg) => any): void {
console.log("Websocket Service INIT called", WebsocketClassic.count); if (isDevMode()) {
console.log("Websocket Service INIT called", WebsocketClassic.count);
}
this.socket.init(host, msgCallback, ctrlCallback); this.socket.init(host, msgCallback, ctrlCallback);
} }

View File

@ -1,5 +1,7 @@
import type {ServerMsg, ControlMsg} from "@/composables/broadcastChannelDef";
import {ControlEvent, ControlMsgType} from "@/composables/broadcastChannelDef"; import type {ApiJsonMsg, ControlMsg, ServerMsg} from "@/api";
import {ControlEvent, ControlMsgType} from "@/api";
import {isDevMode} from "@/composables/buildMode";
interface IWebsocket { interface IWebsocket {
@ -21,7 +23,7 @@ class WebsocketDummy implements IWebsocket {
} }
class OneTimeWebsocket implements IWebsocket { class OneTimeWebsocket implements IWebsocket {
private readonly heartBeatTimeout: number = 2; private readonly heartBeatTimeout: number = 1;
private readonly host: string; private readonly host: string;
private readonly intervalId: number; private readonly intervalId: number;
private readonly msgCallback: (ev: ServerMsg) => any; private readonly msgCallback: (ev: ServerMsg) => any;
@ -30,6 +32,8 @@ class OneTimeWebsocket implements IWebsocket {
private socket: WebSocket; private socket: WebSocket;
private heartBeatTimeCount: number; private heartBeatTimeCount: number;
private stoped: boolean; private stoped: boolean;
private cleared: boolean;
private hasBeenConnected: boolean;
constructor(host: string, constructor(host: string,
msgCallback: (ev: ServerMsg) => any, msgCallback: (ev: ServerMsg) => any,
@ -38,6 +42,8 @@ class OneTimeWebsocket implements IWebsocket {
) { ) {
this.host = host; this.host = host;
this.stoped = false; this.stoped = false;
this.cleared = false;
this.hasBeenConnected = false;
this.msgCallback = msgCallback; this.msgCallback = msgCallback;
this.ctrlCallback = ctrlCallback; this.ctrlCallback = ctrlCallback;
this.closeCallback = closeCallback; this.closeCallback = closeCallback;
@ -51,8 +57,14 @@ class OneTimeWebsocket implements IWebsocket {
if (this.heartBeatTimeCount > this.heartBeatTimeout) { if (this.heartBeatTimeCount > this.heartBeatTimeout) {
/* did not receive packet "heartBeatTimeout" times, /* did not receive packet "heartBeatTimeout" times,
* connection may be lost: close the socket */ * connection may be lost: close the socket */
this.socket.close(); if (this.socket.readyState === this.socket.OPEN) {
console.log("interval: ", this.heartBeatTimeCount, "state: ", this.socket.readyState); console.log("No heart beat, break connection");
this.close();
this.clear();
}
if (isDevMode()) {
console.log("interval: ", this.heartBeatTimeCount, "state: ", this.socket.readyState);
}
} }
this.heartBeatTimeCount++; this.heartBeatTimeCount++;
@ -69,51 +81,40 @@ class OneTimeWebsocket implements IWebsocket {
return return
const msg: ServerMsg = { const msg: ServerMsg = {
data: {}, data: ev.data,
type: "json", type: "json",
} }
this.msgCallback(msg);
if (typeof ev.data === "string") { if (typeof ev.data === "string") {
try { msg.type = "json"
msg.data = JSON.parse(ev.data);
this.msgCallback(msg);
} catch (e) {
return;
}
} else { } else {
console.log(typeof ev.data); msg.type = "binary";
} }
this.msgCallback(msg);
} }
this.socket.onclose = () => { this.socket.onclose = (ev) => {
console.log('WebSocket Disconnected'); if (isDevMode()) {
console.log("ws closed", ev.reason, ev.code);
clearInterval(this.intervalId); }
this.socket.onclose = null this.socket.onclose = null
this.socket.onopen = null this.socket.onopen = null
this.socket.onerror = null this.socket.onerror = null
this.socket.onmessage = null; this.socket.onmessage = null;
this.clear();
const msg: ControlMsg = {
type: ControlMsgType.WS_EVENT,
data: ControlEvent.DISCONNECTED,
}
this.ctrlCallback(msg);
this.closeCallback();
}; };
this.socket.onerror = (error) => { this.socket.onerror = (error) => {
console.error('WebSocket Error', error); this.close();
this.socket.close();
}; };
this.socket.onopen = ev => { this.socket.onopen = ev => {
console.log('WebSocket Connected'); // console.log('WebSocket Connected');
if (this.stoped) { if (this.stoped) {
this.close(); this.close();
return; return;
} }
this.heartBeatTimeCount = 0; this.heartBeatTimeCount = 0;
this.hasBeenConnected = true;
const msg: ControlMsg = { const msg: ControlMsg = {
type: ControlMsgType.WS_EVENT, type: ControlMsgType.WS_EVENT,
data: ControlEvent.CONNECTED, data: ControlEvent.CONNECTED,
@ -137,13 +138,26 @@ class OneTimeWebsocket implements IWebsocket {
if (this.socket.readyState !== WebSocket.OPEN) { if (this.socket.readyState !== WebSocket.OPEN) {
return; return;
} }
if (isDevMode()) {
console.log('WebSocket proxies data ', msg); console.log('WebSocket proxies data ', msg);
if (msg.type === "binary") {
// this.socket.send(msg.data);
} else if (msg.type === "json") {
this.socket.send(JSON.stringify(msg.data));
} }
this.socket.send(msg.data);
}
clear() {
if (this.cleared) {
return;
}
this.cleared = true;
clearInterval(this.intervalId);
const msg: ControlMsg = {
type: ControlMsgType.WS_EVENT,
data: ControlEvent.DISCONNECTED,
}
this.ctrlCallback(msg);
this.closeCallback();
} }
} }
@ -185,7 +199,6 @@ export class WebsocketWrapper {
this.msgCallback = msgCallback; this.msgCallback = msgCallback;
this.ctrlCallback = ctrlCallback; this.ctrlCallback = ctrlCallback;
this.socket = new OneTimeWebsocket(host, this.msgCallback, this.ctrlCallback, this.closeCallback); this.socket = new OneTimeWebsocket(host, this.msgCallback, this.ctrlCallback, this.closeCallback);
} }
private closeCallback() { private closeCallback() {
@ -194,7 +207,7 @@ export class WebsocketWrapper {
} }
this.timeoutId = setTimeout(() => this.timeoutId = setTimeout(() =>
this.newConnection(this.host, this.msgCallback, this.ctrlCallback), this.newConnection(this.host, this.msgCallback, this.ctrlCallback),
2000); 1000);
} }
deinit() { deinit() {
@ -204,7 +217,6 @@ export class WebsocketWrapper {
} }
send(msg: ServerMsg) { send(msg: ServerMsg) {
console.log('WebSocket send: not ready', msg); this.socket.send(msg)
// this.socket.send(msg)
} }
} }

View File

@ -1,8 +1,11 @@
import type {ControlMsg, ServerMsg} from "@/api";
declare const self: SharedWorkerGlobalScope; declare const self: SharedWorkerGlobalScope;
import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper"; import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper";
import type {ControlMsg, ServerMsg} from "@/composables/broadcastChannelDef"; import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef";
import {ControlEvent, ControlMsgType, toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef"; import {ControlEvent, ControlMsgType} from "@/api";
import {isDevMode} from "@/composables/buildMode";
const websocket = new WebsocketWrapper(); const websocket = new WebsocketWrapper();
let host = ""; let host = "";
@ -19,7 +22,9 @@ function msgBroadcast(msg: ServerMsg) {
self.onconnect = function(event) { self.onconnect = function(event) {
const port = event.ports[0]; const port = event.ports[0];
port.onmessage = function (e: MessageEvent<ControlMsg>) { port.onmessage = function (e: MessageEvent<ControlMsg>) {
console.log('Received message in SharedWorker:', e.data); if (isDevMode()) {
console.log('Received message in SharedWorker:', e.data);
}
if (e.data.type === ControlMsgType.WS_SET_HOST) { if (e.data.type === ControlMsgType.WS_SET_HOST) {
if (host === "" && e.data.data !== "") { if (host === "" && e.data.data !== "") {
host = e.data.data; host = e.data.data;

View File

@ -3,9 +3,7 @@ import zh from '@/locales/zh'
import en from '@/locales/en' import en from '@/locales/en'
// const locale = localStorage.getItem('lang') || 'zh'; // const locale = localStorage.getItem('lang') || 'zh';
const locale = 'zh'; export const locale = 'zh';
console.log("langggggg:", locale);
const i18n = createI18n({ const i18n = createI18n({
globalInjection: true, globalInjection: true,

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
</script>
<template>
<div class="my-2">
<slot/>
</div>
</template>
<style scoped>
</style>

14
src/locales/index.ts Normal file
View File

@ -0,0 +1,14 @@
import i18n from '@/i18n';
import zh from '@/locales/zh';
type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: `${Key}`
}[keyof ObjectType & (string | number)];
type TranslationKeys = NestedKeyOf<typeof zh>;
export function translate<K extends TranslationKeys>(key: K | string): string {
return i18n.global.t(key.toLowerCase());
}

View File

@ -1,17 +1,20 @@
export default { export default {
DISCONNECTED: "未连接", disconnected: "未连接",
CONNECTED: "已连接", connected: "已连接",
CONNECTING: "连接中", connecting: "连接中",
WS: { ws: {
DISCONNECTED: "未连接", disconnected: "未连接",
CONNECTED: "已连接", connected: "已连接",
CONNECTING: "连接中", connecting: "连接中",
}, },
PAGE: { page: {
HOME: "主页", home: "主页",
ABOUT: "关于", wifi: "Wi-Fi",
FEEDBACK: "反馈", about: "关于",
uart: "UART透传",
feedback: "反馈",
close: "关闭",
}, },
} }

View File

@ -1,4 +1,9 @@
import '@/assets/tailwind.css' import '@/assets/tailwind.css'
import '@/assets/toggle_skewed.css'
import '@/assets/page.css'
import '@/assets/navigation.css'
import 'element-plus/dist/index.css';
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
@ -10,7 +15,7 @@ import router from './router'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router)
app.use(i18n); app.use(i18n);
app.use(router)
app.mount('#app') app.mount('#app')

View File

@ -1,29 +1,54 @@
import { createRouter, createWebHistory } from 'vue-router' import {createRouter, createWebHistory} from 'vue-router'
import Home from '@/views/Home.vue' import Home from '@/views/Home.vue'
import Wifi from '@/views/Wifi.vue' import Wifi from '@/views/Wifi.vue'
import Feedback from '@/views/Feedback.vue'
import About from '@/views/About.vue'
import Uart from '@/views/Uart.vue'
import Page404 from '@/views/404.vue'
import {translate} from "@/locales";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: Home meta: {title: translate("page.home")},
}, { // component: Wifi
path: '/home:ext(.*)', redirect: () => '/wifi',
component: Home, }, {
}, { path: '/home:ext(.*)',
path: '/wifi:ext(.*)', meta: {title: translate("page.home")},
component: Wifi, redirect: () => '/',
}, { }, {
path: '/about:ext(.*)', path: '/wifi:ext(.*)',
name: 'about', meta: {title: translate('page.wifi')},
// route level code-splitting component: Wifi,
// this generates a separate chunk (About.[hash].js) for this route }, {
// which is lazy-loaded when the route is visited. path: '/about:ext(.*)',
component: () => import('@/views/About.vue') meta: {title: translate('page.about')},
} component: About,
] }, {
path: '/uart:ext(.*)',
meta: {title: translate('page.uart')},
component: Uart,
}, {
path: '/feedback:ext(.*)',
meta: {title: translate('page.feedback')},
name: 'feedback',
component: Feedback,
}, {
path: '/:catchAll(.*)', // This will match all paths that aren't matched by above routes
name: 'NotFound',
component: Page404,
},
]
}) })
router.beforeEach((to, from, next) => {
document.title = typeof to.meta.title === 'string' ? to.meta.title + " | 允斯工作室" : '允斯调试器';
next();
});
export default router export default router

76
src/router/msgRouter.ts Normal file
View File

@ -0,0 +1,76 @@
import type {ApiJsonMsg, ControlMsg, ServerMsg} from "@/api";
import {isDevMode} from "@/composables/buildMode";
import {type ApiBinaryMsg, decodeHeader} from "@/api/binDataDef";
export interface IModuleCallback {
ctrlCallback: (msg: ControlMsg) => void;
serverJsonMsgCallback: (msg: ApiJsonMsg) => void;
serverBinMsgCallback: (msg: ApiBinaryMsg) => void;
}
const moduleMap = new Map<number, IModuleCallback>();
export function registerModule(moduleId: number, moduleCallback: IModuleCallback): boolean {
if (moduleMap.has(moduleId)) {
return false;
}
moduleMap.set(moduleId, moduleCallback);
return true;
}
export function unregisterModule(moduleId: number) {
moduleMap.delete(moduleId);
}
export function routeModuleServerMsg(msg: ServerMsg) {
if (msg.type === "json") {
let jsonMsg: ApiJsonMsg;
try {
jsonMsg = JSON.parse(msg.data as string) as ApiJsonMsg;
if (jsonMsg.cmd === undefined ||
jsonMsg.module === undefined
){
console.log("Server msg has no cmd or module", msg.data);
return;
}
} catch (e) {
console.log(e);
return;
}
const module = jsonMsg.module;
const moduleHandler = moduleMap.get(module);
if (moduleHandler) {
moduleHandler.serverJsonMsgCallback(jsonMsg);
} else {
if (isDevMode()) {
console.log("routeModuleServerMsg module not loaded", module);
}
}
} else {
const arr = msg.data as ArrayBuffer;
if (arr.byteLength < 4) {
if (isDevMode()) {
console.log("binary message too short");
}
return;
}
const binaryMsg = decodeHeader(msg.data as ArrayBuffer);
const moduleHandler = moduleMap.get(binaryMsg.module);
if (moduleHandler) {
moduleHandler.serverBinMsgCallback(binaryMsg);
} else {
if (isDevMode()) {
console.log("routeModuleServerMsg ignored:", msg, binaryMsg);
}
}
}
}
export function routeCtrlMsg(msg: ControlMsg) {
for (const item of moduleMap) {
item[1].ctrlCallback(msg);
}
}

View File

@ -1,5 +1,6 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {ControlEvent} from "@/composables/broadcastChannelDef";
import {ControlEvent} from "@/api";
export const useWsStore = defineStore('websocket', { export const useWsStore = defineStore('websocket', {
state: () => { state: () => {

5
src/utils/emitter.ts Normal file
View File

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

17
src/views/404.vue Normal file
View File

@ -0,0 +1,17 @@
<script setup lang="ts">
</script>
<template>
<div class="text-layout">
<h1 class="page-title">404</h1>
<h2 class="text-center">页面不存在</h2>
<RouterLink to="/">
<el-card class="text-center">返回首页</el-card>
</RouterLink>
</div>
</template>
<style scoped>
</style>

View File

@ -1,12 +1,77 @@
<script setup lang="ts">
const version = import.meta.env.VITE_APP_GIT_TAG || "v0.0.0";
const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
</script>
<template> <template>
<h2>About Page</h2> <div class="text-layout">
<!-- Your about page content goes here --> <el-divider></el-divider>
<h2>About Page</h2> <el-divider>关于</el-divider>
<el-divider></el-divider>
<el-collapse>
<el-collapse-item title="关于网页版上位机">
<el-descriptions border :column="1" class="mt-5 description-style">
<el-descriptions-item label="版本">{{ version }}</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ compileTime }}</el-descriptions-item>
<el-descriptions-item label="许可证">MIT</el-descriptions-item>
</el-descriptions>
<el-descriptions title="鸣谢" border :column="1" class="mt-5 description-style">
<el-descriptions-item label="vuejs"><a target="_blank" href="https://github.com/vuejs/vue/blob/main/LICENSE">MIT</a>
</el-descriptions-item>
<el-descriptions-item label="typescript"><a
href="https://github.com/microsoft/TypeScript/blob/main/LICENSE.txt">Apache 2.0</a></el-descriptions-item>
<el-descriptions-item label="vite"><a target="_blank" href="https://github.com/vitejs/vite/blob/main/LICENSE">MIT</a>
</el-descriptions-item>
<el-descriptions-item label="tailwindcss"><a
href="https://github.com/tailwindlabs/tailwindcss/blob/master/LICENSE">MIT</a></el-descriptions-item>
<el-descriptions-item label="element-plus"><a
href="https://github.com/element-plus/element-plus/blob/dev/LICENSE">MIT</a></el-descriptions-item>
<el-descriptions-item label="pinia"><a target="_blank" href="https://github.com/vuejs/pinia/blob/v2/LICENSE">MIT</a>
</el-descriptions-item>
<el-descriptions-item label="mitt"><a target="_blank" href="https://github.com/developit/mitt/blob/main/LICENSE">MIT</a>
</el-descriptions-item>
<el-descriptions-item label="vue-router"><a
href="https://github.com/vuejs/vue-router/blob/dev/LICENSE">MIT</a></el-descriptions-item>
<el-descriptions-item label="vue-i18n"><a target="_blank" href="https://github.com/kazupon/vue-i18n?tab=MIT-1-ov-file#readme">MIT</a>
</el-descriptions-item>
<el-descriptions-item label="lightningcss"><a
href="https://github.com/parcel-bundler/lightningcss/blob/master/LICENSE">MPL-2.0 license</a>
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
<el-collapse-item title="关于下位机">
<el-descriptions border :column="1" class="mt-5 description-style">
<el-descriptions-item label="官网"><a target="_blank" href="https://yunsi.studio/wireless-proxy">允斯工作室</a></el-descriptions-item>
<el-descriptions-item label="版本">-</el-descriptions-item>
</el-descriptions>
<el-descriptions title="鸣谢" border :column="1" class="mt-5 description-style">
<el-descriptions-item label="windowsair"><a target="_blank" href="https://github.com/windowsair/wireless-esp8266-dap">wireless-esp8266-dap</a>
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
<el-descriptions title="作者:空空(kerms)" border :column="1" class="mt-5 description-style">
<el-descriptions-item label="官网"><a target="_blank" href="https://yunsi.studio/">允斯工作室https://yunsi.studio/</a></el-descriptions-item>
<el-descriptions-item label="github"><a target="_blank" href="https://github.com/kerms">https://github.com/kerms</a>
</el-descriptions-item>
<el-descriptions-item label="邮箱">kerms@niazo.org</el-descriptions-item>
<el-descriptions-item label="BiliBili"><a target="_blank" href="https://space.bilibili.com/3461571571353885">3461571571353885</a>
</el-descriptions-item>
<el-descriptions-item label="QQ群">642246000</el-descriptions-item>
<el-descriptions-item label="备注">欢迎大家来打扰啊</el-descriptions-item>
</el-descriptions>
</div>
<el-divider></el-divider>
</template> </template>
<style scoped>
.description-style :deep(.el-descriptions__label) {
@apply w-32
}
<script setup lang="ts"> </style>
let name = "about-a";
</script>

20
src/views/Feedback.vue Normal file
View File

@ -0,0 +1,20 @@
<template>
<div class="text-layout">
<el-divider></el-divider>
<el-divider>反馈</el-divider>
<el-divider></el-divider>
<el-descriptions title="反馈/建议/需要新功能" border :column="1">
<el-descriptions-item label="QQ群">642246000</el-descriptions-item>
<el-descriptions-item label="作者邮箱">kerms@niazo.org</el-descriptions-item>
</el-descriptions>
</div>
<el-divider></el-divider>
</template>
<script setup lang="ts">
</script>

View File

@ -1,23 +1,18 @@
<template> <template>
<h2>Home Page</h2> <div class="text-layout">
<!-- Your home page content goes here --> <h2 class="page-title">主页</h2>
<h2>Home Page</h2>
<nav> <router-link to="/wifi">
<!-- <RouterLink to="/">Home</RouterLink>--> <el-card>
<!-- <RouterLink to="/wifi">Wifi</RouterLink>--> <p class="text-center">Wi-Fi设置</p>
<!-- <RouterLink to="/about">About</RouterLink>--> </el-card>
<!-- <RouterLink to="/test">Test</RouterLink>--> </router-link>
<!-- <RouterLink to="/home">home</RouterLink>-->
</nav> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
document.title = "Home";
import InlineSvg from "@/components/InlineSvg.vue";
</script> </script>
<style scoped>
</style>

9
src/views/Uart.vue Normal file
View File

@ -0,0 +1,9 @@
<script setup lang="ts">
</script>
<template>
<div class="text-layout">
<h2 class="page-title opacity-10">尽请期待</h2>
</div>
</template>

View File

@ -1,7 +1,12 @@
<template> <template>
<div class="wifiView"> <div class="text-layout">
<h2>Wifi View</h2> <h1 class="page-title">
<el-form label-width="auto" ref="formRef" :model="ssidValidateForm"> Wi-Fi 配置
</h1>
<el-divider></el-divider>
<h2 class="mb-4 text-xl font-bold tracking-tight md:text-2xl lg:text-3xl">连接Wi-Fi</h2>
<el-form label-width="auto" ref="formRef" :model="ssidValidateForm" class="m-auto">
<el-form-item <el-form-item
label="Wi-Fi名" label="Wi-Fi名"
prop="wifiSsid" prop="wifiSsid"
@ -19,129 +24,328 @@
value-key="ssid" value-key="ssid"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="flex"> <div class="flex items-center border-b">
<InlineSvg :name="item.wifiLogo" class="h-6 pr-4"></InlineSvg> <InlineSvg :name="item.wifiLogo" class="h-6 pr-4"></InlineSvg>
<!-- <span class="w-10">{{ item.rssi }}</span>--> <!-- <span class="w-10">{{ item.rssi }}</span>-->
<div>{{ item.ssid }}</div> <div>{{ item.ssid }}</div>
</div> </div>
</template> </template>
</el-autocomplete> </el-autocomplete>
<div class="h-8"> <div class="h-8">
<el-button class="h-8" @click="onScanClick">扫描</el-button> <el-button class="h-8" @click="onScanClick">{{ scanText }}</el-button>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="密码"> <el-form-item label="密码">
<el-input <el-input
v-model="password" v-model="ssidValidateForm.password"
show-password show-password
type="password" type="password"
clearable clearable
/> />
</el-form-item> </el-form-item>
<div class="mb-2">
<el-alert type="info" show-icon>
如果不是通过透传器的热点连接更换Wi-Fi将导致此界面与透传器断开连接
</el-alert>
</div>
<div class="flex justify-center"> <div class="flex justify-center">
<el-button @click="onConnect" type="primary">连接</el-button> <el-button @click="onConnectClick" type="primary">连接</el-button>
</div> </div>
</el-form> </el-form>
<el-divider></el-divider>
<el-descriptions
title="Wi-Fi终端信息"
:column="1"
border
class="description-style"
>
<el-descriptions-item label="asd">
<template #label >
<div>
信号强度
</div>
</template>
<template #default >
{{ wifiStaApInfo.rssi }}
</template>
</el-descriptions-item>
<el-descriptions-item span="1">
<template #label>
<div>
SSID
</div>
</template>
{{ wifiStaApInfo.ssid }}
</el-descriptions-item>
<!-- <el-descriptions-item span="6" >-->
<!-- <template #label>-->
<!-- <div>-->
<!-- 密码-->
<!-- </div>-->
<!-- </template>-->
<!-- <password-viewer password="asdasdasd"></password-viewer>-->
<!-- </el-descriptions-item>-->
<el-descriptions-item span="4">
<template #label>
<div>
IP
</div>
</template>
{{ wifiStaApInfo.ip }}
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
MAC
</div>
</template>
{{ wifiStaApInfo.mac }}
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
网关
</div>
</template>
{{ wifiStaApInfo.gateway }}
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
掩码
</div>
</template>
{{ wifiStaApInfo.netmask }}
</el-descriptions-item>
</el-descriptions>
<el-divider></el-divider>
<el-descriptions
title="Wi-Fi热点信息"
:column="1"
border
class="description-style"
>
<el-descriptions-item span="6">
<template #label>
<div>
SSID
</div>
</template>
{{ wifiApInfo.ssid }}
</el-descriptions-item>
<!-- <el-descriptions-item span="6">-->
<!-- <template #label>-->
<!-- <div>-->
<!-- 密码-->
<!-- </div>-->
<!-- </template>-->
<!-- <password-viewer password="asdasdasd"></password-viewer>-->
<!-- </el-descriptions-item>-->
<el-descriptions-item span="4">
<template #label>
<div>
IP
</div>
</template>
{{ wifiApInfo.ip }}
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
MAC
</div>
</template>
{{ wifiApInfo.mac }}
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
网关
</div>
</template>
{{ wifiApInfo.gateway }}
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
掩码
</div>
</template>
{{ wifiApInfo.netmask }}
</el-descriptions-item>
</el-descriptions>
<el-divider></el-divider>
</div> </div>
<el-button class="h-8">扫描</el-button>
<el-button type="primary">连接</el-button>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {inject, onMounted, onUnmounted, reactive, ref} from "vue"; import {computed, onMounted, onUnmounted, reactive, ref} from "vue";
import {wifi_get_ap_info, wifi_get_scan_list, type WifiInfo, type WifiList} from "@/api/apiWifi";
import type {FormInstance} from "element-plus";
import type {ServerMsg,ControlMsg} from "@/composables/broadcastChannelDef";
import { import {
ControlEvent, wifi_sta_get_ap_info,
ControlMsgType wifi_get_scan_list,
} from "@/composables/broadcastChannelDef"; WifiCmd,
import InlineSvg from "@/components/InlineSvg.vue"; type WifiInfo,
import {getWebsocketService} from "@/composables/websocket/websocketService"; type WifiList,
wifi_ap_get_info, wifi_connect_to
} from "@/api/apiWifi";
import type {FormInstance} from "element-plus";
import InlineSvg from "@/components/InlineSvg.vue";
import type {ApiJsonMsg, ControlMsg, ServerMsg} from "@/api";
import {ControlEvent, ControlMsgType, WtModuleID} from "@/api";
import {registerModule, unregisterModule} from "@/router/msgRouter";
import {useWsStore} from "@/stores/websocket";
import {globalNotify, globalNotifyRightSide} from "@/composables/notification";
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
let wifiListPlaceholder = ref("我的WIFI") let wifiListPlaceholder = ref("我的WIFI")
let ssidValidateForm = reactive({ let ssidValidateForm = reactive({
wifiSsid: "" wifiSsid: "",
password: "",
}) })
const password = ref('')
let scanning = false;
let wsStore = useWsStore();
const defWifiInfo: WifiInfo = {
cmd: 1,
module: 1,
gateway: "未连接",
ip: "未连接",
mac: "未连接",
rssi: 0,
netmask: "未连接",
ssid: "未连接",
}
let wifiStaApInfo = reactive<WifiInfo>({...defWifiInfo});
let wifiApInfo = reactive<WifiInfo>({...defWifiInfo});
let scanning = ref(false);
let scan_cb: any; let scan_cb: any;
let options: Array<WifiInfo> = [ let connectBtnClicked = 0;
let options: Array<WifiInfo> = [];
] const scanText = computed(() => {
return scanning.value ? "扫描中" : "扫描";
});
const querySearch = (queryString: string, cb: any) => { const querySearch = (queryString: string, cb: any) => {
if (scanning) { if (scanning.value) {
scan_cb = cb; scan_cb = cb;
} else { } else {
cb(options); cb(options);
} }
} }
const onClientMsg = (ev: MessageEvent<ServerMsg>) => { const onClientMsg = (msg: ApiJsonMsg) => {
if (ev.data.type !== "json") { switch (msg.cmd as WifiCmd) {
return; case WifiCmd.UNKNOWN:
} break;
case WifiCmd.WIFI_API_JSON_STA_GET_AP_INFO: {
const json: object = ev.data.data; const info = msg as WifiInfo;
if (info.rssi === 0) {
let wifiList: WifiList; Object.assign(wifiStaApInfo, defWifiInfo);
try { } else {
wifiList = ev.data.data as WifiList; Object.assign(wifiStaApInfo, info);
console.log(wifiList); }
} catch (e) { if (connectBtnClicked) {
return; connectBtnClicked = 0;
} globalNotifyRightSide(wifiStaApInfo.ssid + " 连接成功", "success");
scanning = false; }
wifiList.scan_list.forEach(value => { break;
if (value.rssi > -50) { }
value.wifiLogo = "wifi-3"; case WifiCmd.WIFI_API_JSON_CONNECT:
} else if (value.rssi > -65) { break;
value.wifiLogo = "wifi-2"; case WifiCmd.WIFI_API_JSON_GET_SCAN: {
} else { const list = msg as WifiList;
value.wifiLogo = "wifi-1"; scanning.value = false;
list.scan_list.forEach(value => {
if (value.rssi > -50) {
value.wifiLogo = "wifi-3";
} else if (value.rssi > -65) {
value.wifiLogo = "wifi-2";
} else {
value.wifiLogo = "wifi-1";
}
});
options = list.scan_list;
if (scan_cb) {
scan_cb(options);
scan_cb = null;
}
globalNotifyRightSide("扫描完成", "success");
break;
}
case WifiCmd.WIFI_API_JSON_DISCONNECT:
break;
case WifiCmd.WIFI_API_JSON_AP_GET_INFO: {
const info = msg as WifiInfo;
Object.assign(wifiApInfo, info);
break;
} }
});
options = wifiList.scan_list;
if (scan_cb) {
scan_cb(options);
scan_cb = null;
} }
}; };
const onClientCtrl = (ev: MessageEvent<ControlMsg>) => { const onClientCtrl = (msg: ControlMsg) => {
if (ev.data.type !== ControlMsgType.WS_EVENT) { if (msg.type !== ControlMsgType.WS_EVENT) {
return return
} }
if (ev.data.data === ControlEvent.CONNECTED) { if (msg.data === ControlEvent.DISCONNECTED) {
wifi_get_ap_info(); Object.assign(wifiStaApInfo, defWifiInfo);
Object.assign(wifiApInfo, defWifiInfo);
}
if (msg.data === ControlEvent.CONNECTED) {
wifi_sta_get_ap_info();
wifi_ap_get_info();
} }
}; };
function onScanClick() { function onScanClick() {
scanning = true; if (wsStore.state !== ControlEvent.CONNECTED) {
globalNotify("调试器未连接", 'error');
return;
}
scanning.value = true;
wifi_get_scan_list(); wifi_get_scan_list();
} }
function onConnect() { function onConnectClick() {
console.log(ssidValidateForm.wifiSsid, password.value); if (wsStore.state !== ControlEvent.CONNECTED) {
globalNotify("调试器未连接", 'error');
return;
}
if (ssidValidateForm.wifiSsid !== "") {
wifi_connect_to(ssidValidateForm.wifiSsid, ssidValidateForm.password);
connectBtnClicked = 1;
}
} }
onMounted(() => { onMounted(() => {
registerModule(WtModuleID.WIFI, {
ctrlCallback: onClientCtrl,
serverJsonMsgCallback: onClientMsg,
serverBinMsgCallback: () => {},
});
wifi_sta_get_ap_info();
wifi_ap_get_info();
}); });
onUnmounted(() => { onUnmounted(() => {
unregisterModule(WtModuleID.WIFI);
}); });
@ -149,10 +353,7 @@ onUnmounted(() => {
<style scoped> <style scoped>
.wifiView { .description-style :deep(.el-descriptions__label) {
background-color: bisque; @apply w-32
border-radius: 5px;
padding: 20px;
} }
</style> </style>

View File

@ -1,44 +1,54 @@
<template> <template>
<nav class="relative px-4 py-1 flex justify-between items-center border-b"> <nav class="relative px-2 py-0.5 sm:py-1 flex justify-between items-center border-b h-full">
<div class="flex"> <div class="flex">
<button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 p-3"> <button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 pl-1 mx-4">
<svg class="block h-4 w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <svg class="block h-3 lg:h-4 lg:w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<title>Mobile menu</title> <title>导航侧栏</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path> <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path>
</svg> </svg>
</button> </button>
<router-link to="/" class="text-3xl px-4 font-bold leading-none"> <router-link to="/" class="text-3xl px-4 font-bold leading-none hidden items-center sm:flex" title="走,去码头整点薯条">
<InlineSvg name="home" class="h-10"></InlineSvg> <InlineSvg name="favicon" class="h-5 lg:h-8"></InlineSvg>
</router-link> </router-link>
<!-- <a class="text-3xl px-4 font-bold leading-none" href="/">--> <!-- <a class="text-3xl px-4 font-bold leading-none" href="/">-->
<!-- <InlineSvg name="home" class="h-10"></InlineSvg>--> <!-- <InlineSvg name="home" class="h-10"></InlineSvg>-->
<!-- </a>--> <!-- </a>-->
<router-link to="/" class="flex items-center text-sm text-blue-600 font-bold">主页</router-link> <!-- <router-link to="/" class="flex items-center text-sm text-blue-600 font-bold">主页</router-link>-->
<!-- <a class="flex items-center text-sm text-blue-600 font-bold" href="/">主页6</a>--> <!-- <a class="flex items-center text-sm text-blue-600 font-bold" href="/">主页6</a>-->
<div class="flex pt-0.5 sm:pt-1 ml-4 text-sm items-center sm:hidden">
<router-link :to="route.fullPath">{{ route.meta.title }}</router-link>
</div>
</div> </div>
<div class="flex"> <div class="flex">
<ul class="hidden absolute top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2 md:flex md:mx-auto md:items-center md:w-auto md:space-x-6"> <ul class="hidden absolute top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2 sm:flex sm:mx-auto sm:items-center sm:w-auto sm:space-x-6">
<li><router-link to="/wifi" title="Wifi" class="text-sm text-gray-400 hover:text-gray-800">wifi</router-link></li> <li v-for="(item, index) in menuItems" :key="index" class="router-link">
<!-- <li><a class="text-sm text-gray-400 hover:text-gray-500" href="#">Home</a></li>--> <router-link :to="item.href" :class="item?.class">{{item.name}}</router-link>
<!-- <li><a class="text-sm text-blue-600 font-bold">About Us</a></li>--> </li>
<!-- <li><a class="text-sm text-gray-400 hover:text-gray-500" href="#">Services</a></li>-->
</ul> </ul>
</div> </div>
<!-- <a class="md:ml-auto md:mr-3"></a>--> <!-- <a class="md:ml-auto md:mr-3"></a>-->
<div class="flex"> <div class="flex h-full">
<button @click="stateMenuOpen=true" <div id="page-spec-slot" class="content-center h-full flex flex-row"></div>
class="py-2 px-6 bg-blue-500 hover:bg-blue-600 text-sm text-white font-bold rounded-xl transition duration-200"> <div class="lg:hidden">
<span class="flex"> <el-button :type="wsColor" size="small" class="transition duration-1000 min-h-full">
<svg class="mr-2" width="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <InlineSvg v-show="wsColor!=='success'" name="link-off" class="mr-2" width="20"></InlineSvg>
<path d="M12 19.4998H12.01M2 8.81929C3.69692 7.30051 5.74166 6.16236 8 5.53906M5 12.8584C5.86251 12.0129 6.87754 11.3223 8 10.8319M16 5.53906C18.2583 6.16236 20.3031 7.30051 22 8.81929M16 10.8319C17.1225 11.3223 18.1375 12.0129 19 12.8584M12 4.5V15.4998" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <InlineSvg v-show="wsColor==='success'" name="link" class="mr-2" width="20"></InlineSvg>
</svg> <div class="text-xs sm:text-sm lg:text-base">{{ wsState }}</div>
<span>{{ wsState }}</span> </el-button>
</span> </div>
</button> <div class="hidden lg:flex">
<!-- <span>{{ $t("Disconnected") }}</span>--> <el-button :type="wsColor" size="large" class="transition duration-1000 min-h-full">
<InlineSvg v-show="wsColor!=='success'" name="link-off" class="mr-2" width="20"></InlineSvg>
<InlineSvg v-show="wsColor==='success'" name="link" class="mr-2" width="20"></InlineSvg>
<div class="text-base">{{ wsState }}</div>
</el-button>
</div>
</div> </div>
</nav> </nav>
<div :class='["custom-drawer", {open: sideMenuOpen}]'> <div :class='["custom-drawer", {open: sideMenuOpen}]'>
@ -46,26 +56,33 @@
v-model="sideMenuOpen" v-model="sideMenuOpen"
:with-header="false" :with-header="false"
size="" size=""
:direction="'ltr'"> :direction="'ltr'"
<div :class="[sideMenuItemClass]" class="px-6" @click="sideMenuOpen=false"> >
<InlineSvg name="cross" class="w-6"></InlineSvg> <div :class="[sideMenuItemClass]" class="pr-6 flex text-gray-500" @click="sideMenuOpen=false">
</div> <InlineSvg name="cross" class="h-6"></InlineSvg>
<div class="flex-col justify-between m-4 mt-0">
<div> <div>
<ul> <p class="h-6 flex items-center">{{ $t("page.close") }}</p>
<li v-for="(item, index) in menuItems" class="mb-1" :key="index">
<router-link @click="sideMenuOpen=false" :title="item.name" :to="item.href" :class="sideMenuItemClass">{{ item.name }}</router-link>
<!-- <a :href="item.href" :class="sideMenuItemClass">{{ item.name }}</a>-->
</li>
</ul>
</div>
<div class="mt-auto">
<p class="my-4 text-xs text-center text-gray-400">
<span>Copyright kerms 2024</span>
</p>
</div> </div>
</div> </div>
<div class="flex flex-col justify-between m-4 mt-0">
<ul>
<li v-for="(item, index) in menuItems" class="mb-1" :key="index">
<router-link @click="sideMenuOpen=false" :title="item.name" :to="item.href" :class="[sideMenuItemClass, item?.class]">{{ item.name }}</router-link>
</li>
</ul>
</div>
<template #footer>
<div>
<el-button @click="toggle">
<InlineSvg v-if="!isFullscreen" name="open-in-full" width="16px" fill="#000000"></InlineSvg>
<p v-if="!isFullscreen">全屏</p>
<InlineSvg v-if="isFullscreen" name="close-fullscreen" width="16px" fill="#000000"></InlineSvg>
<p v-if="isFullscreen">缩小</p>
</el-button>
</div>
</template>
</el-drawer> </el-drawer>
</div> </div>
@ -79,12 +96,7 @@
<div class="flex-col justify-between m-4 bg-white"> <div class="flex-col justify-between m-4 bg-white">
<div class="mt-auto"> <div class="mt-auto">
<div class="pt-6">
<a class="block px-4 py-3 mb-3 leading-loose text-xs text-center font-semibold bg-gray-50 hover:bg-gray-100 rounded-xl"
href="#">Sign in</a>
<a class="block px-4 py-3 mb-2 leading-loose text-xs text-center text-white font-semibold bg-blue-600 hover:bg-blue-700 rounded-xl"
href="#">Sign Up</a>
</div>
</div> </div>
</div> </div>
</el-drawer> </el-drawer>
@ -93,41 +105,60 @@
<script lang="ts" setup> <script lang="ts" setup>
import InlineSvg from "@/components/InlineSvg.vue"; import InlineSvg from "@/components/InlineSvg.vue";
import {computed, ref, toRef} from "vue"; import {computed, ref} from "vue";
import {useWsStore} from "@/stores/websocket"; import {useWsStore} from "@/stores/websocket";
import {useI18n} from "vue-i18n"; import {translate} from "@/locales";
import {ControlEvent} from "@/api";
import {useRoute} from "vue-router";
import { useFullscreen } from '@vueuse/core'
const { t } = useI18n()
const wsStore = useWsStore(); const wsStore = useWsStore();
const {isFullscreen, toggle} = useFullscreen();
const route = useRoute();
const sideMenuItemClass = "block p-4 text-sm font-semibold text-gray-400 hover:bg-blue-50 hover:text-blue-600 rounded" const sideMenuItemClass = "block p-4 text-sm font-semibold hover:bg-blue-50 hover:text-blue-600 rounded"
const sideMenuOpen = ref(false); const sideMenuOpen = ref(false);
const stateMenuOpen = ref(false) const stateMenuOpen = ref(false)
const wsState = computed(() => { const wsColor = computed(() => {
let ret = "danger";
return t(wsStore.state); switch (wsStore.state) {
case ControlEvent.DISCONNECTED:
ret = "danger";
break
case ControlEvent.CONNECTED:
ret = "success";
break
case ControlEvent.CONNECTING:
ret = "warning";
break
}
return ret;
}); });
const menuItems = ([ const wsState = computed(() => {
{ return translate(wsStore.state);
name: "Home", });
type Item = {
name: string;
href: string;
class?: string;
};
const menuItems: Item[] = ([
/* {
name: translate("page.home"),
href: "/", href: "/",
}, { }, */{
name: "About Us", name: translate("page.wifi"),
href: "/about",
}, {
name: "Services",
href: "/",
}, {
name: "Wifi",
href: "/wifi", href: "/wifi",
}, { }, {
name: "Contact", name: translate("page.about"),
href: "/", href: "/about",
}, { }, {
name: "6", name: translate("page.feedback"),
href: "/", href: "/feedback",
}, },
]); ]);
@ -139,18 +170,23 @@ const menuItems = ([
border: solid 1px; border: solid 1px;
}*/ }*/
/* drawer */
.custom-drawer :deep(.el-drawer) {
transition: all 0.1s; /* Custom duration*/
}
/* drawer overlay */ /* drawer overlay */
.custom-drawer.open :deep(.el-overlay) { .custom-drawer :deep(.el-overlay) {
transition: all 0s; /* Custom duration*/ transition: all 0s; /* Custom duration*/
} }
.custom-drawer :deep(.el-drawer) {
transition: all 0s; /* Custom duration*/
}
.custom-drawer.open :deep(.el-drawer) {
transition: all 0.05s; /* Custom duration*/
}
.custom-drawer :deep(.el-drawer__body) { .custom-drawer :deep(.el-drawer__body) {
padding: 0; padding: 0;
} }
</style> </style>

View File

@ -2,76 +2,118 @@ import { fileURLToPath, URL } from 'node:url'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite' import {ConfigEnv, defineConfig, loadEnv} from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import svgLoader from "vite-svg-loader"; import svgLoader from "vite-svg-loader";
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
import { viteSingleFile } from 'vite-plugin-singlefile'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default ({mode}: ConfigEnv) => {
plugins: [ process.env = {...process.env, ...loadEnv(mode, process.cwd())};
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
svgLoader(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
outDir: '/tmp/zhuang/dap-web-dist/',
emptyOutDir: true,
cssMinify: 'lightningcss',
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
if (!assetInfo || !assetInfo.name) {
return 'default-filename.ext';
}
const info = assetInfo.name.split(".");
let extType = info[info.length - 1];
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
extType = "img";
} else if (/woff|woff2/.test(extType)) {
extType = "css";
} else if (/css/.test(extType)) {
extType = "css";
return "style.css"
}
// return `[name]-[hash][extname]`;
return `[name][extname]`;
},
// chunkFileNames: "[name]-[hash].js",
chunkFileNames: "[name].js",
// entryFileNames: "[name]-[hash].js",
entryFileNames: (chunkInfo) => {
// console.log(chunkInfo)
return "script.js"
},
sourcemapFileNames: "map-[name].js", return defineConfig({
// sanitizeFileName: "anit-[name].js", plugins: [
// entryFileNames: (chunkInfo) => { vue(),
// console.log(chunkInfo) AutoImport({
// return `${chunkInfo.name}.js` resolvers: [ElementPlusResolver()],
// }, }),
manualChunks(id) { Components({
/* if (id.match('.*!/src/.*shared[a-zA-Z0-9-_]*[.](ts|js).*')) { resolvers: [ElementPlusResolver()],
// Prevent bundling node_modules into common chunks }),
return 'bundle-shared'; svgLoader(),
} cssInjectedByJsPlugin(),
else */{ viteSingleFile(),
// Prevent bundling node_modules into common chunks ],
return 'script' define: {},
} resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
cacheDir: process.env.VITE_CACHE_DIR || undefined,
worker: {
rollupOptions: {
output: {
inlineDynamicImports: true,
minifyInternalExports: true,
// entryFileNames: (chunkInfo) => {
// // console.log(chunkInfo)
// if (chunkInfo.name.includes("shared")) {
// console.log(chunkInfo.name);
// }
// return "worker.js";
// },
entryFileNames: "[name].js",
}
}
},
build: {
// target: 'es2015',
outDir: process.env.VITE_OUTPUT_DIR || undefined,
emptyOutDir: true,
cssMinify: 'lightningcss',
rollupOptions: {
output: {
inlineDynamicImports: true,
minifyInternalExports: true,
assetFileNames: (assetInfo) => {
if (!assetInfo || !assetInfo.name) {
return 'default-filename.ext';
}
const info = assetInfo.name.split(".");
let extType = info[info.length - 1];
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
extType = "img";
} else if (/woff|woff2/.test(extType)) {
extType = "css";
} else if (/css/.test(extType)) {
extType = "css";
return "style.css"
}
console.log(assetInfo)
return `[name]-[hash][extname]`;
},
// chunkFileNames: "[name]-[hash].js",
// chunkFileNames: "[name][hash].js",
chunkFileNames(chunkInfo) {
// Check if this chunk is your SharedWorker
// console.log(chunkInfo)
// For other chunks, use the default naming scheme
return 'assets/[name]-[hash].js';
},
// entryFileNames: "[name]-[hash].js",
entryFileNames: (chunkInfo) => {
// console.log(chunkInfo)
if (chunkInfo.name.includes("shared")) {
console.log(chunkInfo.name);
}
return "script.js";
},
sourcemapFileNames: "map-[name].js",
// sanitizeFileName: "anit-[name].js",
// entryFileNames: (chunkInfo) => {
// console.log(chunkInfo)
// return `${chunkInfo.name}.js`
// },
// manualChunks(id) {
// /* if (id.match('.*!/src/.*shared[a-zA-Z0-9-_]*[.](ts|js).*')) {
// // Prevent bundling node_modules into common chunks
// return 'bundle-shared';
// }
// else */{
// // Prevent bundling node_modules into common chunks
// return 'script'
// }
// },
manualChunks: undefined,
}, },
}, }
} },
}, })
}) };