新增 XSLPrintDot 项目,包含打印服务的核心功能和相关配置。实现打印机查询、打印任务处理、远程转发功能,并支持多平台设备ID获取。优化打印数据准备逻辑,增强系统的可维护性和扩展性,同时更新工作区配置以支持新项目。
This commit is contained in:
454
XSLPrintDot/frontend/src/App.vue
Normal file
454
XSLPrintDot/frontend/src/App.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { GetPrinters, StartServer, StopServer, GetAppMode, GetLogPort, GetSettings, SaveSettings } from '../wailsjs/go/main/App'
|
||||
import { EventsOn } from '../wailsjs/runtime/runtime'
|
||||
import Help from './components/Help.vue'
|
||||
import Settings from './components/Settings.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const appMode = ref('main')
|
||||
const logPort = ref(0)
|
||||
const logs = ref<string[]>([])
|
||||
const clientCount = ref(0)
|
||||
let logPollInterval: number | null = null
|
||||
let forwarderStream: EventSource | null = null
|
||||
let forwarderPoll: number | null = null
|
||||
|
||||
const config = reactive({
|
||||
port: '1122',
|
||||
key: ''
|
||||
})
|
||||
|
||||
const persistServerSettings = async () => {
|
||||
try {
|
||||
const s = await GetSettings()
|
||||
if (s) {
|
||||
s.serverPort = config.port
|
||||
s.serverKey = config.key
|
||||
await SaveSettings(s)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to save server settings', e)
|
||||
}
|
||||
}
|
||||
|
||||
const connectionUrl = computed(() => {
|
||||
let url = `ws://localhost:${config.port}/ws`
|
||||
if (config.key) {
|
||||
url += `?key=${encodeURIComponent(config.key)}`
|
||||
}
|
||||
return url
|
||||
})
|
||||
|
||||
const serverStatus = ref('Stopped')
|
||||
type PrinterInfo = {
|
||||
name: string
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
const printers = ref<PrinterInfo[]>([])
|
||||
const isLoadingPrinters = ref(false)
|
||||
|
||||
const refreshPrinters = async () => {
|
||||
if (isLoadingPrinters.value) return
|
||||
isLoadingPrinters.value = true
|
||||
printers.value = []
|
||||
|
||||
const minDelay = new Promise((resolve) => setTimeout(resolve, 800))
|
||||
|
||||
try {
|
||||
const [fetchedPrinters] = await Promise.all([
|
||||
GetPrinters(),
|
||||
minDelay
|
||||
])
|
||||
printers.value = fetchedPrinters.slice().sort((a, b) => {
|
||||
if (a.isDefault !== b.isDefault) {
|
||||
return a.isDefault ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
isLoadingPrinters.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleServer = async () => {
|
||||
if (serverStatus.value === 'Running') {
|
||||
try {
|
||||
await StopServer()
|
||||
serverStatus.value = 'Stopped'
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await StartServer(config.port, config.key)
|
||||
serverStatus.value = 'Running'
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RemoteStatus = {
|
||||
connected: boolean
|
||||
lastError: string
|
||||
lastChange: number
|
||||
autoReconnect?: boolean
|
||||
}
|
||||
|
||||
const remoteStatus = ref<RemoteStatus>({
|
||||
connected: false,
|
||||
lastError: '',
|
||||
lastChange: 0
|
||||
})
|
||||
const isConnecting = ref(false)
|
||||
const isDisconnecting = ref(false)
|
||||
const forwarderVisible = ref(false)
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:${logPort.value}/api/logs`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
logs.value = data.reverse()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch logs', e)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllLogs = async () => {
|
||||
logs.value = []
|
||||
try {
|
||||
await fetch(`http://localhost:${logPort.value}/api/logs/clear`, { method: 'POST' })
|
||||
} catch (e) {
|
||||
console.error('Failed to clear logs', e)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshRemoteStatus = async () => {
|
||||
if (logPort.value <= 0) return
|
||||
try {
|
||||
const resp = await fetch(`http://localhost:${logPort.value}/api/forwarder/status`)
|
||||
if (resp.ok) {
|
||||
remoteStatus.value = await resp.json()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch forwarder status', e)
|
||||
}
|
||||
}
|
||||
|
||||
const updateForwarderVisibility = (settings: any) => {
|
||||
if (!settings) {
|
||||
forwarderVisible.value = false
|
||||
return
|
||||
}
|
||||
const auth = (settings.remoteAuthUrl || '').trim()
|
||||
const ws = (settings.remoteWsUrl || '').trim()
|
||||
const clientId = (settings.remoteClientId || '').trim()
|
||||
const secret = (settings.remoteSecretKey || '').trim()
|
||||
forwarderVisible.value = auth !== '' && ws !== '' && clientId !== '' && secret !== ''
|
||||
}
|
||||
|
||||
const connectForwarder = async () => {
|
||||
if (isConnecting.value || remoteStatus.value.connected || logPort.value <= 0) return
|
||||
isConnecting.value = true
|
||||
try {
|
||||
await fetch(`http://localhost:${logPort.value}/api/forwarder/connect`, { method: 'POST' })
|
||||
} catch (e) {
|
||||
console.error('Failed to connect forwarder', e)
|
||||
} finally {
|
||||
isConnecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectForwarder = async () => {
|
||||
if (isDisconnecting.value || !remoteStatus.value.connected || logPort.value <= 0) return
|
||||
isDisconnecting.value = true
|
||||
try {
|
||||
const s = await GetSettings()
|
||||
if (s) {
|
||||
s.remoteAutoConnect = false
|
||||
await SaveSettings(s)
|
||||
}
|
||||
await fetch(`http://localhost:${logPort.value}/api/forwarder/disconnect`, { method: 'POST' })
|
||||
await fetch(`http://localhost:${logPort.value}/api/reload`, { method: 'POST' })
|
||||
} catch (e) {
|
||||
console.error('Failed to disconnect forwarder', e)
|
||||
} finally {
|
||||
isDisconnecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
appMode.value = await GetAppMode()
|
||||
|
||||
// Load settings for language
|
||||
try {
|
||||
const s = await GetSettings()
|
||||
if (s && s.language) {
|
||||
locale.value = s.language
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load settings", e)
|
||||
}
|
||||
|
||||
if (appMode.value === "logs") {
|
||||
logPort.value = await GetLogPort()
|
||||
fetchLogs()
|
||||
logPollInterval = setInterval(fetchLogs, 1000)
|
||||
} else if (appMode.value === "main") {
|
||||
// Main mode
|
||||
logPort.value = await GetLogPort()
|
||||
try {
|
||||
const s = await GetSettings()
|
||||
if (s) {
|
||||
config.port = s.serverPort || config.port
|
||||
config.key = s.serverKey || ''
|
||||
updateForwarderVisibility(s)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load server settings', e)
|
||||
}
|
||||
await refreshPrinters()
|
||||
await toggleServer()
|
||||
|
||||
await refreshRemoteStatus()
|
||||
if (logPort.value > 0 && 'EventSource' in window) {
|
||||
forwarderStream = new EventSource(`http://localhost:${logPort.value}/api/forwarder/stream`)
|
||||
forwarderStream.onmessage = (event) => {
|
||||
try {
|
||||
remoteStatus.value = JSON.parse(event.data)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
forwarderStream.onerror = () => {
|
||||
if (forwarderStream) {
|
||||
forwarderStream.close()
|
||||
forwarderStream = null
|
||||
}
|
||||
if (forwarderPoll === null) {
|
||||
forwarderPoll = window.setInterval(refreshRemoteStatus, 3000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
forwarderPoll = window.setInterval(refreshRemoteStatus, 3000)
|
||||
}
|
||||
|
||||
// Listen for client count updates
|
||||
EventsOn("client_count", (count: number) => {
|
||||
clientCount.value = count
|
||||
})
|
||||
|
||||
|
||||
// Listen for settings reload
|
||||
EventsOn("reload_settings", async () => {
|
||||
try {
|
||||
const s = await GetSettings()
|
||||
if (s && s.language) {
|
||||
locale.value = s.language
|
||||
}
|
||||
if (s) {
|
||||
config.port = s.serverPort || config.port
|
||||
config.key = s.serverKey || ''
|
||||
updateForwarderVisibility(s)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to reload settings", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (logPollInterval) clearInterval(logPollInterval)
|
||||
if (forwarderPoll !== null) {
|
||||
window.clearInterval(forwarderPoll)
|
||||
forwarderPoll = null
|
||||
}
|
||||
if (forwarderStream) {
|
||||
forwarderStream.close()
|
||||
forwarderStream = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Help v-if="appMode === 'help'" />
|
||||
<Settings v-else-if="appMode === 'settings'" />
|
||||
|
||||
<div v-else class="h-screen w-screen overflow-hidden bg-white text-gray-900 font-sans text-left flex flex-col relative">
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="flex-1 overflow-hidden relative">
|
||||
|
||||
<!-- LOGS MODE UI -->
|
||||
<div v-if="appMode === 'logs'" class="w-full h-full flex flex-col">
|
||||
<header class="p-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-800 mb-1 flex items-center gap-2">
|
||||
<i-material-symbols-terminal class="text-gray-700" />
|
||||
{{ t('logs.title') }}
|
||||
</h1>
|
||||
<p class="text-xs text-gray-500">{{ t('logs.subtitle') }}</p>
|
||||
</div>
|
||||
<button @click="clearAllLogs" class="text-xs text-red-600 hover:bg-red-50 px-3 py-1.5 border border-red-200 rounded-md transition-colors flex items-center gap-1">
|
||||
<i-material-symbols-delete-outline />
|
||||
{{ t('logs.clearAll') }}
|
||||
</button>
|
||||
</header>
|
||||
<div class="flex-1 bg-gray-900 text-gray-300 p-4 font-mono text-xs overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-transparent">
|
||||
<div v-for="(log, i) in logs" :key="i" class="border-b border-gray-800 last:border-0 pb-1 mb-1 break-words hover:bg-gray-800/50">
|
||||
{{ log }}
|
||||
</div>
|
||||
<div v-if="logs.length === 0" class="text-gray-600 italic py-4 text-center">{{ t('logs.empty') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN APP UI -->
|
||||
<div v-else class="w-full h-full flex flex-col">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="p-4 border-b border-gray-200 flex-none transition-colors duration-300"
|
||||
:class="clientCount > 0 ? 'bg-green-600 text-white' : 'bg-gray-50 text-gray-900'"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold mb-1 flex items-center gap-2">
|
||||
<i-material-symbols-print-connect :class="clientCount > 0 ? 'text-white' : 'text-blue-600'" />
|
||||
{{ t('main.title') }}
|
||||
</h1>
|
||||
<p class="text-xs" :class="clientCount > 0 ? 'text-green-100' : 'text-gray-500'">
|
||||
{{ t('main.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Client Count Badge -->
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-bold transition-all"
|
||||
:class="clientCount > 0 ? 'bg-white text-green-700 shadow-sm' : 'bg-gray-200 text-gray-600'"
|
||||
>
|
||||
<i-material-symbols-devices />
|
||||
<span>{{ clientCount }} {{ t('main.clients') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto scrollbar-hide">
|
||||
<!-- Server Control -->
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h2 class="text-base font-semibold mb-4 flex items-center gap-2">
|
||||
<i-material-symbols-dns class="text-gray-600" />
|
||||
<span class="w-2.5 h-2.5 rounded-full" :class="serverStatus === 'Running' ? 'bg-green-500' : 'bg-red-500'"></span>
|
||||
{{ t('main.serverControl') }}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">{{ t('main.port') }}</label>
|
||||
<input v-model="config.port" @change="persistServerSettings" type="text" class="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-gray-800 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all rounded-md" :disabled="serverStatus === 'Running'" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">{{ t('main.secretKey') }}</label>
|
||||
<input v-model="config.key" @change="persistServerSettings" type="password" class="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-gray-800 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all rounded-md" :disabled="serverStatus === 'Running'" :placeholder="t('main.placeholderKey')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="toggleServer"
|
||||
class="w-full py-2 px-4 font-semibold text-white transition-all active:opacity-90 rounded-md flex items-center justify-center gap-2"
|
||||
:class="serverStatus === 'Running' ? 'bg-red-500 hover:bg-red-600' : 'bg-blue-600 hover:bg-blue-700'"
|
||||
>
|
||||
<i-material-symbols-stop v-if="serverStatus === 'Running'" />
|
||||
<i-material-symbols-play-arrow v-else />
|
||||
{{ serverStatus === 'Running' ? t('main.stopServer') : t('main.startServer') }}
|
||||
</button>
|
||||
|
||||
<div class="mt-4 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<label class="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">{{ t('main.connectionUrl') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 bg-white border border-gray-300 px-2 py-1.5 text-xs text-gray-600 rounded select-all font-mono break-all">
|
||||
{{ connectionUrl }}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Forwarder -->
|
||||
<div v-if="forwarderVisible" class="p-4 border-t border-gray-200">
|
||||
<h2 class="text-base font-semibold mb-4 flex items-center gap-2">
|
||||
<i-material-symbols-cloud-sync class="text-gray-600" />
|
||||
<span class="w-2.5 h-2.5 rounded-full" :class="remoteStatus.connected ? 'bg-green-500' : 'bg-red-500'"></span>
|
||||
{{ t('settings.forwarding') }}
|
||||
</h2>
|
||||
|
||||
<button
|
||||
@click="remoteStatus.connected ? disconnectForwarder() : connectForwarder()"
|
||||
:disabled="isConnecting || isDisconnecting"
|
||||
class="w-full py-2 px-4 font-semibold rounded-md transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
:class="remoteStatus.connected ? 'bg-red-500 hover:bg-red-600 text-white' : 'bg-blue-600 hover:bg-blue-700 text-white'"
|
||||
>
|
||||
<i-material-symbols-stop v-if="remoteStatus.connected" />
|
||||
<i-material-symbols-play-arrow v-else />
|
||||
{{ remoteStatus.connected ? (isDisconnecting ? t('settings.disconnecting') : t('settings.disconnect')) : (isConnecting ? t('settings.connecting') : t('settings.connect')) }}
|
||||
</button>
|
||||
|
||||
<p v-if="remoteStatus.lastError" class="text-xs text-red-500 mt-2">
|
||||
<span class="font-medium">{{ t('settings.lastError') }}:</span>
|
||||
<span class="break-words">{{ remoteStatus.lastError }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Printers -->
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-base font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i-material-symbols-print class="text-gray-600" />
|
||||
{{ t('main.availablePrinters') }}
|
||||
</h2>
|
||||
<button
|
||||
@click="refreshPrinters"
|
||||
class="text-xs bg-gray-100 hover:bg-gray-200 text-blue-600 px-3 py-1.5 border border-gray-200 transition-colors rounded-md flex items-center gap-1"
|
||||
:disabled="isLoadingPrinters"
|
||||
>
|
||||
<i-material-symbols-refresh :class="{ 'animate-spin': isLoadingPrinters }" />
|
||||
{{ t('main.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingPrinters" class="text-gray-500 italic text-center py-6 bg-gray-50 border border-dashed border-gray-200 flex flex-col items-center gap-2">
|
||||
<span>{{ t('main.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="printers.length === 0" class="text-gray-400 italic text-center py-6 bg-gray-50 border border-dashed border-gray-200">
|
||||
{{ t('main.noPrinters') }}
|
||||
</div>
|
||||
<ul v-else class="grid grid-cols-1 gap-0 border border-gray-200 divide-y divide-gray-200">
|
||||
<li v-for="p in printers" :key="p.name" class="px-3 py-2 flex items-center gap-2 hover:bg-gray-50 transition-colors text-sm bg-white">
|
||||
<i-material-symbols-print class="text-lg opacity-70 text-gray-500" />
|
||||
<span class="font-medium truncate text-gray-700" :title="p.name">{{ p.name }}</span>
|
||||
<span v-if="p.isDefault" class="ml-auto text-[10px] px-2 py-0.5 rounded-full bg-blue-50 text-blue-700 border border-blue-100">
|
||||
{{ t('main.defaultPrinter') }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Reset some Wails default styles if needed */
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #f9fafb; /* gray-50 */
|
||||
}
|
||||
</style>
|
||||
93
XSLPrintDot/frontend/src/assets/fonts/OFL.txt
Normal file
93
XSLPrintDot/frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
BIN
XSLPrintDot/frontend/src/assets/images/logo-universal.png
Normal file
BIN
XSLPrintDot/frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
70
XSLPrintDot/frontend/src/components/Help.vue
Normal file
70
XSLPrintDot/frontend/src/components/Help.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GetUsageGuide } from '../../wailsjs/go/main/App'
|
||||
import { marked } from 'marked'
|
||||
|
||||
const content = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const markdown = await GetUsageGuide()
|
||||
content.value = await marked.parse(markdown)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
content.value = '<p class="text-red-500">Failed to load usage guide.</p>'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen w-screen bg-white flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto p-8 prose prose-sm max-w-none prose-slate">
|
||||
<div v-html="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@reference "tailwindcss";
|
||||
|
||||
/* Add some basic markdown styling overrides if needed */
|
||||
.prose h1 {
|
||||
@apply text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800;
|
||||
}
|
||||
.prose h2 {
|
||||
@apply text-xl font-bold mt-6 mb-3 text-gray-800;
|
||||
}
|
||||
.prose h3 {
|
||||
@apply text-lg font-bold mt-4 mb-2 text-gray-800;
|
||||
}
|
||||
.prose p {
|
||||
@apply mb-4 leading-relaxed text-gray-600;
|
||||
}
|
||||
.prose ul {
|
||||
@apply list-disc list-inside mb-4 pl-4 text-gray-600;
|
||||
}
|
||||
.prose code {
|
||||
@apply bg-gray-100 px-1 py-0.5 rounded text-sm font-mono text-pink-600;
|
||||
}
|
||||
.prose pre {
|
||||
@apply bg-gray-900 text-gray-100 p-4 rounded-md overflow-x-auto mb-4 text-sm font-mono;
|
||||
}
|
||||
.prose pre code {
|
||||
@apply bg-transparent p-0 text-gray-100;
|
||||
}
|
||||
.prose table {
|
||||
@apply min-w-full border-collapse border border-gray-300 mb-4;
|
||||
}
|
||||
.prose thead {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
.prose th {
|
||||
@apply border border-gray-300 px-4 py-2 text-left font-semibold text-gray-700;
|
||||
}
|
||||
.prose td {
|
||||
@apply border border-gray-300 px-4 py-2 text-gray-600;
|
||||
}
|
||||
.prose tr:nth-child(even) {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
</style>
|
||||
338
XSLPrintDot/frontend/src/components/Settings.vue
Normal file
338
XSLPrintDot/frontend/src/components/Settings.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { GetSettings, SaveSettings, Restart, GetLogPort, GetRemoteForwarderStatus, DisconnectRemoteForwarder, ConnectRemoteForwarder } from '../../wailsjs/go/main/App'
|
||||
import { main } from '../../wailsjs/go/models'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const settings = ref(new main.AppSettings({
|
||||
language: 'zh-CN',
|
||||
autoStart: false,
|
||||
remoteAutoConnect: true,
|
||||
remoteServer: '',
|
||||
remoteAuthUrl: '',
|
||||
remoteWsUrl: '',
|
||||
remoteClientId: '',
|
||||
remoteSecretKey: '',
|
||||
remoteClientName: '',
|
||||
windowWidth: 0,
|
||||
windowHeight: 0,
|
||||
windowX: 0,
|
||||
windowY: 0,
|
||||
maximized: false
|
||||
}))
|
||||
|
||||
const logPort = ref(0)
|
||||
const isConnecting = ref(false)
|
||||
const isDisconnecting = ref(false)
|
||||
const isSyncing = ref(false)
|
||||
const hasLoaded = ref(false)
|
||||
let autoSaveTimer: number | null = null
|
||||
|
||||
type RemoteStatus = {
|
||||
connected: boolean
|
||||
lastError: string
|
||||
lastChange: number
|
||||
autoReconnect?: boolean
|
||||
}
|
||||
|
||||
const remoteStatus = ref<RemoteStatus>({
|
||||
connected: false,
|
||||
lastError: '',
|
||||
lastChange: 0
|
||||
})
|
||||
|
||||
let remoteStatusTimer: number | null = null
|
||||
let remoteStatusStream: EventSource | null = null
|
||||
|
||||
const refreshRemoteStatus = async () => {
|
||||
try {
|
||||
if (logPort.value > 0) {
|
||||
const resp = await fetch(`http://localhost:${logPort.value}/api/forwarder/status`)
|
||||
if (resp.ok) {
|
||||
remoteStatus.value = await resp.json()
|
||||
if (typeof remoteStatus.value.autoReconnect === 'boolean') {
|
||||
setSettingsSilently(() => {
|
||||
settings.value.remoteAutoConnect = remoteStatus.value.autoReconnect as boolean
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
remoteStatus.value = await GetRemoteForwarderStatus()
|
||||
if (typeof remoteStatus.value.autoReconnect === 'boolean') {
|
||||
setSettingsSilently(() => {
|
||||
settings.value.remoteAutoConnect = remoteStatus.value.autoReconnect as boolean
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const setSettingsSilently = (update: () => void) => {
|
||||
isSyncing.value = true
|
||||
try {
|
||||
update()
|
||||
} finally {
|
||||
isSyncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const s = await GetSettings()
|
||||
setSettingsSilently(() => {
|
||||
settings.value = s
|
||||
locale.value = s.language
|
||||
})
|
||||
logPort.value = await GetLogPort()
|
||||
await refreshRemoteStatus()
|
||||
if (logPort.value > 0 && 'EventSource' in window) {
|
||||
remoteStatusStream = new EventSource(`http://localhost:${logPort.value}/api/forwarder/stream`)
|
||||
remoteStatusStream.onmessage = (event) => {
|
||||
try {
|
||||
remoteStatus.value = JSON.parse(event.data)
|
||||
if (typeof remoteStatus.value.autoReconnect === 'boolean') {
|
||||
settings.value.remoteAutoConnect = remoteStatus.value.autoReconnect
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
remoteStatusStream.onerror = () => {
|
||||
if (remoteStatusStream) {
|
||||
remoteStatusStream.close()
|
||||
remoteStatusStream = null
|
||||
}
|
||||
if (remoteStatusTimer === null) {
|
||||
remoteStatusTimer = window.setInterval(refreshRemoteStatus, 3000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remoteStatusTimer = window.setInterval(refreshRemoteStatus, 3000)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
hasLoaded.value = true
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (remoteStatusTimer !== null) {
|
||||
window.clearInterval(remoteStatusTimer)
|
||||
remoteStatusTimer = null
|
||||
}
|
||||
if (autoSaveTimer !== null) {
|
||||
window.clearTimeout(autoSaveTimer)
|
||||
autoSaveTimer = null
|
||||
}
|
||||
if (remoteStatusStream) {
|
||||
remoteStatusStream.close()
|
||||
remoteStatusStream = null
|
||||
}
|
||||
})
|
||||
|
||||
const isSaving = ref(false)
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (isSaving.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
// Save settings
|
||||
await SaveSettings(settings.value)
|
||||
|
||||
// Update locale immediately in this window
|
||||
locale.value = settings.value.language
|
||||
|
||||
// Notify main process to reload settings
|
||||
try {
|
||||
if (logPort.value > 0) {
|
||||
await fetch(`http://localhost:${logPort.value}/api/reload`, { method: 'POST' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Main process reload trigger failed", e)
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(settings, () => {
|
||||
if (isSyncing.value || !hasLoaded.value) return
|
||||
if (autoSaveTimer !== null) {
|
||||
window.clearTimeout(autoSaveTimer)
|
||||
}
|
||||
autoSaveTimer = window.setTimeout(() => {
|
||||
saveSettings()
|
||||
}, 400)
|
||||
}, { deep: true })
|
||||
|
||||
const disconnectRemote = async () => {
|
||||
if (isDisconnecting.value || !remoteStatus.value.connected) return
|
||||
isDisconnecting.value = true
|
||||
try {
|
||||
settings.value.remoteAutoConnect = false
|
||||
remoteStatus.value.autoReconnect = false
|
||||
await saveSettings()
|
||||
if (logPort.value > 0) {
|
||||
await fetch(`http://localhost:${logPort.value}/api/forwarder/disconnect`, { method: 'POST' })
|
||||
} else {
|
||||
await DisconnectRemoteForwarder()
|
||||
}
|
||||
await refreshRemoteStatus()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
isDisconnecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const connectRemote = async () => {
|
||||
if (isConnecting.value || remoteStatus.value.connected) return
|
||||
isConnecting.value = true
|
||||
try {
|
||||
if (logPort.value > 0) {
|
||||
await fetch(`http://localhost:${logPort.value}/api/forwarder/connect`, { method: 'POST' })
|
||||
} else {
|
||||
await ConnectRemoteForwarder()
|
||||
}
|
||||
await refreshRemoteStatus()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
isConnecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRemote = async () => {
|
||||
if (remoteStatus.value.connected) {
|
||||
await disconnectRemote()
|
||||
} else {
|
||||
await connectRemote()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen w-screen bg-gray-50 flex flex-col p-4 relative">
|
||||
|
||||
<h1 class="text-2xl font-bold mb-6 text-gray-800 flex items-center gap-2">
|
||||
<i-material-symbols-settings-outline />
|
||||
{{ t('settings.title') }}
|
||||
</h1>
|
||||
|
||||
<div class="space-y-6 flex-1 overflow-y-auto">
|
||||
|
||||
<!-- Language -->
|
||||
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-2">
|
||||
<i-material-symbols-language class="text-gray-500" />
|
||||
{{ t('settings.language') }}
|
||||
</label>
|
||||
<select v-model="settings.language" class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Auto Start -->
|
||||
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm flex items-center justify-between">
|
||||
<label class="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<i-material-symbols-power class="text-gray-500" />
|
||||
{{ t('settings.autoStart') }}
|
||||
</label>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="settings.autoStart" class="sr-only peer">
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Forwarding Service -->
|
||||
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm space-y-4">
|
||||
<h3 class="text-sm font-semibold text-gray-800 border-b border-gray-100 pb-2 flex items-center gap-2">
|
||||
<i-material-symbols-cloud-sync class="text-gray-600" />
|
||||
<span class="w-2.5 h-2.5 rounded-full" :class="remoteStatus.connected ? 'bg-green-500' : 'bg-red-500'"></span>
|
||||
{{ t('settings.forwarding') }}
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<i-material-symbols-sync class="text-gray-500" />
|
||||
{{ t('settings.autoConnect') }}
|
||||
</label>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="settings.remoteAutoConnect" class="sr-only peer">
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
@click="toggleRemote"
|
||||
:disabled="isConnecting || isDisconnecting"
|
||||
class="w-full py-2 px-4 font-semibold rounded-md shadow-sm transition-colors duration-200 text-sm disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
:class="remoteStatus.connected ? 'bg-red-500 hover:bg-red-600 text-white' : 'bg-blue-600 hover:bg-blue-700 text-white'"
|
||||
>
|
||||
<i-material-symbols-stop v-if="remoteStatus.connected" />
|
||||
<i-material-symbols-play-arrow v-else />
|
||||
{{ remoteStatus.connected ? (isDisconnecting ? t('settings.disconnecting') : t('settings.disconnect')) : (isConnecting ? t('settings.connecting') : t('settings.connect')) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="remoteStatus.lastError" class="text-xs text-red-500">
|
||||
<span class="font-medium">{{ t('settings.lastError') }}:</span>
|
||||
<span class="break-words">{{ remoteStatus.lastError }}</span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1 flex items-center gap-1">
|
||||
<i-material-symbols-lock-outline class="text-gray-400" />
|
||||
{{ t('settings.authAddress') }}
|
||||
</label>
|
||||
<input v-model="settings.remoteAuthUrl" type="text" class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-blue-500 focus:border-blue-500" placeholder="http://server:8080/api/client/login" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1 flex items-center gap-1">
|
||||
<i-material-symbols-link class="text-gray-400" />
|
||||
{{ t('settings.wsAddress') }}
|
||||
</label>
|
||||
<input v-model="settings.remoteWsUrl" type="text" class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-blue-500 focus:border-blue-500" placeholder="ws://server:8081/ws/client" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1 flex items-center gap-1">
|
||||
<i-material-symbols-badge class="text-gray-400" />
|
||||
{{ t('settings.clientId') }}
|
||||
</label>
|
||||
<input v-model="settings.remoteClientId" type="text" disabled class="w-full border border-gray-200 bg-gray-100 text-gray-500 rounded-md p-2 text-sm cursor-not-allowed" :title="t('settings.deviceIdReadonly')" />
|
||||
<p class="text-[11px] text-gray-400 mt-1">{{ t('settings.deviceIdReadonly') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1 flex items-center gap-1">
|
||||
<i-material-symbols-key class="text-gray-400" />
|
||||
{{ t('settings.secretKey') }}
|
||||
</label>
|
||||
<input v-model="settings.remoteSecretKey" type="password" class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-blue-500 focus:border-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1 flex items-center gap-1">
|
||||
<i-material-symbols-badge class="text-gray-400" />
|
||||
{{ t('settings.clientName') }}
|
||||
</label>
|
||||
<input v-model="settings.remoteClientName" type="text" class="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-blue-500 focus:border-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
15
XSLPrintDot/frontend/src/i18n.ts
Normal file
15
XSLPrintDot/frontend/src/i18n.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './locales/en.json'
|
||||
import zh from './locales/zh.json'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN', // default locale
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
'en': en,
|
||||
'zh-CN': zh
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
58
XSLPrintDot/frontend/src/locales/en.json
Normal file
58
XSLPrintDot/frontend/src/locales/en.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"language": "Language",
|
||||
"autoStart": "Start on Boot",
|
||||
"remotePrint": "Remote Print Server",
|
||||
"forwarding": "Cloud Print Forwarder",
|
||||
"serverAddress": "Server Address",
|
||||
"authAddress": "Auth Address",
|
||||
"wsAddress": "WS Address",
|
||||
"autoConnect": "Auto Reconnect",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"clientId": "Client ID",
|
||||
"secretKey": "Secret Key",
|
||||
"clientName": "Client Name (Optional)",
|
||||
"deviceIdReadonly": "Device ID is auto-detected and cannot be edited",
|
||||
"forwarderStatus": "Connection Status",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"connect": "Connect",
|
||||
"connecting": "Connecting...",
|
||||
"disconnect": "Disconnect",
|
||||
"disconnecting": "Disconnecting...",
|
||||
"lastError": "Last Error",
|
||||
"save": "Save Settings",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved",
|
||||
"restart": "Restart Now",
|
||||
"confirmRestart": "Restart Required",
|
||||
"confirmRestartMessage": "Applying these settings requires a restart. Do you want to restart now?",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"main": {
|
||||
"title": "XSL-PrintDot",
|
||||
"subtitle": "WebSocket Printer Bridge",
|
||||
"clients": "connections",
|
||||
"serverControl": "Server Control",
|
||||
"port": "Port",
|
||||
"secretKey": "Secret Key (Optional)",
|
||||
"placeholderKey": "Leave empty for no auth",
|
||||
"startServer": "Start Server",
|
||||
"stopServer": "Stop Server",
|
||||
"connectionUrl": "Connection URL",
|
||||
"availablePrinters": "Available Printers",
|
||||
"defaultPrinter": "Default",
|
||||
"refresh": "Refresh",
|
||||
"loading": "Loading...",
|
||||
"noPrinters": "No printers found.",
|
||||
"clientConnected": "Client Connected"
|
||||
},
|
||||
"logs": {
|
||||
"title": "System Logs",
|
||||
"subtitle": "Live system events",
|
||||
"clearAll": "Clear All",
|
||||
"empty": "No logs available yet..."
|
||||
}
|
||||
}
|
||||
58
XSLPrintDot/frontend/src/locales/zh.json
Normal file
58
XSLPrintDot/frontend/src/locales/zh.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"language": "语言",
|
||||
"autoStart": "开机自启动",
|
||||
"remotePrint": "远程打印服务器",
|
||||
"forwarding": "云打印中转服务",
|
||||
"serverAddress": "服务器地址",
|
||||
"authAddress": "鉴权地址",
|
||||
"wsAddress": "WS地址",
|
||||
"autoConnect": "自动重连",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"clientId": "客户端ID",
|
||||
"secretKey": "密钥",
|
||||
"clientName": "客户端名称(可选)",
|
||||
"deviceIdReadonly": "设备ID自动获取,无法修改",
|
||||
"forwarderStatus": "连接状态",
|
||||
"connected": "已连接",
|
||||
"disconnected": "未连接",
|
||||
"connect": "连接",
|
||||
"connecting": "连接中...",
|
||||
"disconnect": "断开",
|
||||
"disconnecting": "断开中...",
|
||||
"lastError": "最近错误",
|
||||
"save": "保存",
|
||||
"saving": "保存中...",
|
||||
"saved": "已保存",
|
||||
"restart": "立即重启",
|
||||
"confirmRestart": "需要重启",
|
||||
"confirmRestartMessage": "应用这些设置需要重启程序。是否立即重启?",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"main": {
|
||||
"title": "XSL-PrintDot",
|
||||
"subtitle": "WebSocket 打印桥接器",
|
||||
"clients": "个连接",
|
||||
"serverControl": "服务控制",
|
||||
"port": "端口",
|
||||
"secretKey": "密钥 (可选)",
|
||||
"placeholderKey": "留空则无需认证",
|
||||
"startServer": "启动服务",
|
||||
"stopServer": "停止服务",
|
||||
"connectionUrl": "连接地址",
|
||||
"availablePrinters": "可用打印机",
|
||||
"defaultPrinter": "默认",
|
||||
"refresh": "刷新",
|
||||
"loading": "加载中...",
|
||||
"noPrinters": "未找到打印机",
|
||||
"clientConnected": "客户端已连接"
|
||||
},
|
||||
"logs": {
|
||||
"title": "系统日志",
|
||||
"subtitle": "实时系统事件",
|
||||
"clearAll": "清除所有",
|
||||
"empty": "暂无日志..."
|
||||
}
|
||||
}
|
||||
6
XSLPrintDot/frontend/src/main.ts
Normal file
6
XSLPrintDot/frontend/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css';
|
||||
import i18n from './i18n'
|
||||
|
||||
createApp(App).use(i18n).mount('#app')
|
||||
42
XSLPrintDot/frontend/src/style.css
Normal file
42
XSLPrintDot/frontend/src/style.css
Normal file
@@ -0,0 +1,42 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
html {
|
||||
background-color: white;
|
||||
/* text-align: center; Removed to fix left alignment issues */
|
||||
color: #1a202c;
|
||||
overflow: hidden; /* Prevent window scrollbar */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #1a202c;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
overflow: hidden; /* Prevent body scrollbar */
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
html, body, #app {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
/* text-align: center; Removed to fix left alignment issues */
|
||||
}
|
||||
7
XSLPrintDot/frontend/src/vite-env.d.ts
vendored
Normal file
7
XSLPrintDot/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type {DefineComponent} from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
Reference in New Issue
Block a user