From f1e362a820b2f85e59a04ce1f228f6005934c553 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Mon, 15 Jun 2026 23:36:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=89=8D=E7=AB=AF=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/public/vite.svg | 1 - web/src/assets/vue.svg | 1 - web/src/components/ActionBar.vue | 62 ++++++++++----------- web/src/components/ComputerUseSheet.vue | 1 + web/src/components/HelloWorld.vue | 41 -------------- web/src/composables/useComputerUseSocket.ts | 3 +- web/src/composables/useFeatureVisibility.ts | 20 +++++++ web/src/i18n/en-US.ts | 6 +- web/src/i18n/zh-CN.ts | 6 +- web/src/lib/utils.ts | 35 +++++++++--- web/src/views/ConsoleView.vue | 20 ++++++- web/src/views/LoginView.vue | 2 + web/src/views/SettingsView.vue | 35 ++++++++++-- web/src/views/SetupView.vue | 3 + 14 files changed, 141 insertions(+), 95 deletions(-) delete mode 100644 web/public/vite.svg delete mode 100644 web/src/assets/vue.svg delete mode 100644 web/src/components/HelloWorld.vue create mode 100644 web/src/composables/useFeatureVisibility.ts diff --git a/web/public/vite.svg b/web/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/web/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/vue.svg b/web/src/assets/vue.svg deleted file mode 100644 index 770e9d33..00000000 --- a/web/src/assets/vue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/components/ActionBar.vue b/web/src/components/ActionBar.vue index 99137f03..68b83a76 100644 --- a/web/src/components/ActionBar.vue +++ b/web/src/components/ActionBar.vue @@ -5,9 +5,9 @@ import { useRouter } from 'vue-router' import { useSystemStore } from '@/stores/system' import { Button } from '@/components/ui/button' import { - Popover, PopoverContent, PopoverTrigger, + Popover, } from '@/components/ui/popover' import { Tooltip, @@ -32,7 +32,6 @@ import { ClipboardPaste, HardDrive, Keyboard, - Cable, Settings, Maximize, Power, @@ -65,6 +64,7 @@ const props = defineProps<{ videoMode?: VideoMode ttydRunning?: boolean showTerminal?: boolean + showComputerUse?: boolean }>() const emit = defineEmits<{ @@ -87,7 +87,6 @@ const videoPopoverOpen = ref(false) const hidPopoverOpen = ref(false) const audioPopoverOpen = ref(false) const msdDialogOpen = ref(false) -const extensionOpen = ref(false) const mobileAtxOpen = ref(false) const mobilePasteOpen = ref(false) @@ -126,7 +125,7 @@ let resizeObserver: ResizeObserver | null = null type CollapsibleItem = | 'video' | 'audio' | 'hid' | 'msd' | 'atx' | 'paste' - | 'stats' | 'extension' | 'settings' + | 'stats' | 'terminal' | 'settings' interface ItemSpec { id: CollapsibleItem @@ -141,7 +140,7 @@ const ITEM_SPECS: ItemSpec[] = [ { id: 'atx', side: 'left' }, { id: 'paste', side: 'left' }, { id: 'stats', side: 'right' }, - { id: 'extension', side: 'right' }, + { id: 'terminal', side: 'right' }, { id: 'settings', side: 'right' }, ] @@ -197,7 +196,7 @@ const RIGHT_FIXED_PX = 120 const collapsibleItems = computed(() => { const items = ITEM_SPECS.slice(3).filter(item => { if (item.id === 'msd' && !showMsd.value) return false - if (item.id === 'extension' && props.showTerminal === false) return false + if (item.id === 'terminal' && props.showTerminal === false) return false return true }) return items @@ -342,30 +341,27 @@ const hasOverflow = computed(() => { - -
- - - - - -
+ +
+ + + -
- - + + +

{{ t('extensions.ttyd.title') }}

+
+ +
@@ -385,10 +381,10 @@ const hasOverflow = computed(() => {
-
+
- - + + - - - + + + diff --git a/web/src/components/ComputerUseSheet.vue b/web/src/components/ComputerUseSheet.vue index 44417b0a..6d918861 100644 --- a/web/src/components/ComputerUseSheet.vue +++ b/web/src/components/ComputerUseSheet.vue @@ -335,6 +335,7 @@ onMounted(loadConfig)
diff --git a/web/src/components/HelloWorld.vue b/web/src/components/HelloWorld.vue deleted file mode 100644 index b58e52b9..00000000 --- a/web/src/components/HelloWorld.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/web/src/composables/useComputerUseSocket.ts b/web/src/composables/useComputerUseSocket.ts index c6be050d..6a61cc0d 100644 --- a/web/src/composables/useComputerUseSocket.ts +++ b/web/src/composables/useComputerUseSocket.ts @@ -1,5 +1,6 @@ import { ref, onUnmounted } from 'vue' import { buildWsUrl } from '@/types/websocket' +import { generateUUID } from '@/lib/utils' import type { ComputerUseScreenshot, ComputerUseSession, ComputerUseAction } from '@/api' export type ComputerUseServerMessage = @@ -16,7 +17,7 @@ export function useComputerUseSocket(options: { }) { const connected = ref(false) const error = ref(null) - const clientId = crypto.randomUUID() + const clientId = generateUUID() let ws: WebSocket | null = null let connectPromise: Promise | null = null diff --git a/web/src/composables/useFeatureVisibility.ts b/web/src/composables/useFeatureVisibility.ts new file mode 100644 index 00000000..51329f9a --- /dev/null +++ b/web/src/composables/useFeatureVisibility.ts @@ -0,0 +1,20 @@ +import { useLocalStorage } from '@vueuse/core' +import type { RemovableRef } from '@vueuse/core' + +export type FeatureVisibilityKey = 'webTerminal' | 'computerUse' +export type FeatureVisibility = Record + +const DEFAULT_FEATURE_VISIBILITY: FeatureVisibility = { + webTerminal: true, + computerUse: true, +} + +const featureVisibility = useLocalStorage( + 'featureVisibility', + DEFAULT_FEATURE_VISIBILITY, + { mergeDefaults: true }, +) + +export function useFeatureVisibility(): RemovableRef { + return featureVisibility +} diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index c3db1a5d..0c6552d9 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -105,8 +105,7 @@ export default { mouseRelative: 'Relative Mouse', mouseAbsoluteTip: 'Absolute positioning - direct screen coordinate mapping', mouseRelativeTip: 'Relative positioning - sends mouse movement deltas', - extension: 'Extension', - extensionTip: 'Extension features', + webTerminal: 'Web Terminal', stats: 'Stats', statsTip: 'View connection statistics', settings: 'Settings', @@ -708,6 +707,9 @@ export default { atxWolInterfaceHint: 'Specify network interface for WOL packets, leave empty for default routing', themeDesc: 'Choose the interface color scheme', languageDesc: 'Choose the interface display language', + featureVisibility: 'Feature Visibility', + featureVisibilityDesc: 'Control which feature entry points are shown on the console page', + computerUseAgent: 'Computer Use Agent', videoSettings: 'Video Capture', videoSettingsDesc: 'Configure capture device format, resolution and frame rate', videoDevice: 'Video Device', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index 2cb7f1e2..83f7d690 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -105,8 +105,7 @@ export default { mouseRelative: '相对鼠标', mouseAbsoluteTip: '绝对定位模式 - 直接映射屏幕坐标', mouseRelativeTip: '相对定位模式 - 发送鼠标移动增量', - extension: '扩展', - extensionTip: '扩展功能', + webTerminal: '网页终端', stats: '连接统计', statsTip: '查看连接状态', settings: '设置', @@ -707,6 +706,9 @@ export default { atxWolInterfaceHint: '指定发送 WOL 包的网络接口,留空则使用系统默认路由', themeDesc: '选择界面颜色方案', languageDesc: '选择界面显示语言', + featureVisibility: '功能展示', + featureVisibilityDesc: '控制控制台页面显示的功能入口', + computerUseAgent: 'Computer Use Agent', videoSettings: '视频采集', videoSettingsDesc: '配置视频采集设备的格式、分辨率与帧率', videoDevice: '视频设备', diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index d7fc534c..f61864b8 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -7,17 +7,34 @@ export function cn(...inputs: ClassValue[]) { /** * Generate a UUID v4 with fallback for older browsers - * Uses crypto.randomUUID() if available, otherwise falls back to manual generation + * Uses crypto.randomUUID() in secure contexts and crypto.getRandomValues() + * where randomUUID is unavailable, such as HTTP LAN access. */ export function generateUUID(): string { - // Use native API if available (modern browsers) - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID() + const webCrypto = globalThis.crypto + + if (typeof webCrypto?.randomUUID === 'function') { + return webCrypto.randomUUID() } - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0 - const v = c === 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) + const bytes = new Uint8Array(16) + if (typeof webCrypto?.getRandomValues === 'function') { + webCrypto.getRandomValues(bytes) + } else { + for (let i = 0; i < bytes.length; i++) { + bytes[i] = Math.floor(Math.random() * 256) + } + } + + bytes[6] = (bytes[6]! & 0x0f) | 0x40 + bytes[8] = (bytes[8]! & 0x3f) | 0x80 + + const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')) + return [ + hex.slice(0, 4).join(''), + hex.slice(4, 6).join(''), + hex.slice(6, 8).join(''), + hex.slice(8, 10).join(''), + hex.slice(10, 16).join(''), + ].join('-') } diff --git a/web/src/views/ConsoleView.vue b/web/src/views/ConsoleView.vue index 74e029e3..a01b45de 100644 --- a/web/src/views/ConsoleView.vue +++ b/web/src/views/ConsoleView.vue @@ -11,6 +11,7 @@ import { useHidWebSocket } from '@/composables/useHidWebSocket' import { useWebRTC } from '@/composables/useWebRTC' import { useVideoSession } from '@/composables/useVideoSession' import { useComputerUseSocket, type ComputerUseServerMessage } from '@/composables/useComputerUseSocket' +import { useFeatureVisibility } from '@/composables/useFeatureVisibility' import { getUnifiedAudio } from '@/composables/useUnifiedAudio' import { streamApi, hidApi, atxApi, atxConfigApi, authApi, computerUseApi } from '@/api' import type { ComputerUseScreenshot, ComputerUseSession } from '@/api' @@ -185,7 +186,10 @@ const changingPassword = ref(false) const ttydStatus = ref<{ available: boolean; running: boolean } | null>(null) const showTerminalDialog = ref(false) -const showTerminal = computed(() => ttydStatus.value?.available !== false) +const featureVisibility = useFeatureVisibility() +const terminalAvailable = computed(() => ttydStatus.value?.available !== false) +const showTerminal = computed(() => terminalAvailable.value && featureVisibility.value.webTerminal) +const showComputerUse = computed(() => featureVisibility.value.computerUse) const isDark = ref(document.documentElement.classList.contains('dark')) @@ -772,6 +776,7 @@ function clearComputerUseTimeline() { } async function openComputerUse() { + if (!showComputerUse.value) return computerUseOpen.value = true await computerUseSocket.connect().catch(() => {}) computerUseSession.value = await computerUseApi.session().catch(() => computerUseSession.value) @@ -1818,6 +1823,14 @@ watch(() => webrtc.videoTrack.value, async (track) => { } }) +watch(showTerminal, (visible) => { + if (!visible) showTerminalDialog.value = false +}) + +watch(showComputerUse, (visible) => { + if (!visible) computerUseOpen.value = false +}) + watch(() => webrtc.audioTrack.value, async (track) => { videoDebugLog('WebRTC audio track ref changed', { hasTrack: Boolean(track), @@ -2820,6 +2833,7 @@ onUnmounted(() => { :video-mode="videoMode" :ttyd-running="ttydStatus?.running" :show-terminal="showTerminal" + :show-computer-use="showComputerUse" @toggle-fullscreen="toggleFullscreen" @toggle-stats="statsSheetOpen = true" @toggle-virtual-keyboard="handleToggleVirtualKeyboard" @@ -3037,6 +3051,7 @@ onUnmounted(() => {
{ id="currentPassword" v-model="currentPassword" type="password" + autocomplete="current-password" :placeholder="t('auth.currentPasswordPlaceholder')" /> @@ -3129,6 +3145,7 @@ onUnmounted(() => { id="newPassword" v-model="newPassword" type="password" + autocomplete="new-password" :placeholder="t('auth.newPasswordPlaceholder')" /> @@ -3138,6 +3155,7 @@ onUnmounted(() => { id="confirmPassword" v-model="confirmPassword" type="password" + autocomplete="new-password" :placeholder="t('auth.confirmPasswordPlaceholder')" /> diff --git a/web/src/views/LoginView.vue b/web/src/views/LoginView.vue index e1809248..4e92ff18 100644 --- a/web/src/views/LoginView.vue +++ b/web/src/views/LoginView.vue @@ -82,6 +82,7 @@ async function handleLogin() { id="username" v-model="username" type="text" + autocomplete="username" :placeholder="t('auth.username')" class="pl-10" /> @@ -96,6 +97,7 @@ async function handleLogin() { id="password" v-model="password" :type="showPassword ? 'text' : 'password'" + autocomplete="current-password" :placeholder="t('auth.password')" class="pl-10 pr-10" /> diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index a396f41f..afa7b79a 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -51,6 +51,7 @@ import type { import { FrpProxyType, FrpcConfigMode } from '@/types/generated' import { formatFpsLabel, toConfigFps } from '@/lib/fps' import { useClipboard } from '@/composables/useClipboard' +import { useFeatureVisibility } from '@/composables/useFeatureVisibility' import { getVideoFormatState } from '@/lib/video-format-support' import { formatVideoDeviceLabel } from '@/lib/video-device-label' import AppLayout from '@/components/AppLayout.vue' @@ -111,6 +112,7 @@ import { Globe, Loader2, AlertTriangle, + Bot, } from 'lucide-vue-next' const { t, te } = useI18n() @@ -119,6 +121,7 @@ const router = useRouter() const systemStore = useSystemStore() const configStore = useConfigStore() const authStore = useAuthStore() +const featureVisibility = useFeatureVisibility() const isWindows = computed(() => systemStore.platform?.mode === 'windows') const isAndroid = computed(() => systemStore.platform?.mode === 'android_amlogic') @@ -2762,6 +2765,29 @@ watch(isWindows, () => { + + + + {{ t('settings.featureVisibility') }} + {{ t('settings.featureVisibilityDesc') }} + + +
+ + +
+
+ + +
+
+
@@ -2968,6 +2994,7 @@ watch(isWindows, () => { id="turn-password" v-model="config.turn_password" :type="showPasswords ? 'text' : 'password'" + autocomplete="off" :disabled="!config.stun_server && !config.turn_server" />