新增 XSLPrintDot 项目,包含打印服务的核心功能和相关配置。实现打印机查询、打印任务处理、远程转发功能,并支持多平台设备ID获取。优化打印数据准备逻辑,增强系统的可维护性和扩展性,同时更新工作区配置以支持新项目。

This commit is contained in:
geht
2026-05-14 12:04:18 +08:00
parent 296bc2a4b2
commit 687b9bebed
65 changed files with 9080 additions and 1 deletions

View 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>

View 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>