新增 XSLPrintDot 项目,包含打印服务的核心功能和相关配置。实现打印机查询、打印任务处理、远程转发功能,并支持多平台设备ID获取。优化打印数据准备逻辑,增强系统的可维护性和扩展性,同时更新工作区配置以支持新项目。
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user