DesktopModePanel.vue 5.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <script setup lang="ts">
  2. import { computed, onMounted, ref } from 'vue'
  3. import AppIcon from '@/components/common/AppIcon.vue'
  4. type Mode = 'client' | 'server'
  5. const desktop = window.yaoyaoDesktop
  6. const state = ref<{ mode: Mode; serverURL: string; switching: boolean; supportedModes?: Mode[] }>()
  7. const selected = ref<Mode>('client')
  8. const busy = ref(false)
  9. const error = ref('')
  10. const notice = ref('')
  11. const options = computed(() => ([
  12. { mode: 'client', title: '客户端模式', description: '连接另一台电脑上的夭夭服务器。本机无需安装 Hermes。', icon: 'link' },
  13. { mode: 'server', title: '服务器模式', description: '在这台电脑上运行夭夭服务,使用本机 Hermes,并供其他客户端连接。', icon: 'monitor' },
  14. ] as const).filter(option => !state.value?.supportedModes || state.value.supportedModes.includes(option.mode)))
  15. const canSwitch = computed(() => options.value.length > 1)
  16. async function load() {
  17. busy.value = true; error.value = ''
  18. try {
  19. state.value = await desktop!.modeState!()
  20. selected.value = state.value.mode
  21. } catch (e) { error.value = e instanceof Error ? e.message : '无法读取运行模式,请重试。' }
  22. finally { busy.value = false }
  23. }
  24. async function switchMode() {
  25. if (busy.value || !state.value || selected.value === state.value.mode) return
  26. busy.value = true; error.value = ''; notice.value = ''
  27. try {
  28. const result = await desktop!.switchMode!(selected.value)
  29. if (!result.ok) throw new Error(result.error || '切换运行模式未完成,请重试。')
  30. if (result.pendingLogin) notice.value = '请在当前页面完成服务器连接与登录。'
  31. else await load()
  32. } catch (e) { error.value = e instanceof Error ? e.message : '切换运行模式未完成,请重试。' }
  33. finally { busy.value = false }
  34. }
  35. async function changeServer() {
  36. busy.value = true; error.value = ''; notice.value = ''
  37. try { await desktop!.openRemoteLogin!() }
  38. catch (e) { error.value = e instanceof Error ? e.message : '无法打开服务器登录页面。' }
  39. finally { busy.value = false }
  40. }
  41. onMounted(load)
  42. </script>
  43. <template>
  44. <section class="desktop-mode-panel" aria-label="服务器与客户端模式" :aria-busy="busy">
  45. <div v-if="state" class="mode-status">
  46. <strong>当前:{{ state.mode === 'client' ? '客户端模式' : '服务器模式' }}</strong>
  47. <span v-if="state.serverURL">{{ state.serverURL }}</span>
  48. </div>
  49. <p v-else-if="busy" role="status">正在读取运行模式…</p>
  50. <fieldset v-if="canSwitch" :disabled="busy || !state || state.switching">
  51. <legend>这台电脑如何使用夭夭?</legend>
  52. <label v-for="option in options" :key="option.mode" class="mode-option" :class="{ selected: selected === option.mode }">
  53. <input v-model="selected" type="radio" name="desktop-running-mode" :value="option.mode" />
  54. <AppIcon :name="option.icon" :size="22" />
  55. <span><strong>{{ option.title }}</strong><small>{{ option.description }}</small></span>
  56. </label>
  57. </fieldset>
  58. <p class="mode-note">{{ canSwitch ? '切换后会打开对应服务的页面,并记住选择。' : '此客户端连接已有服务器。' }}数据保留在原服务器;连接另一台服务器时,可能需要登录。</p>
  59. <p v-if="error" class="mode-error" role="alert">{{ error }}</p>
  60. <p v-if="notice" role="status">{{ notice }}</p>
  61. <div class="mode-actions">
  62. <button v-if="!state" type="button" :disabled="busy" @click="load">重新读取</button>
  63. <button v-else-if="canSwitch" class="mode-primary" type="button" :disabled="busy || state.switching || selected === state.mode" @click="switchMode">
  64. {{ busy ? '正在切换…' : selected === state.mode ? '正在使用此模式' : selected === 'client' ? '切换为客户端' : '切换为服务器' }}
  65. </button>
  66. <button v-if="desktop?.openRemoteLogin" type="button" :disabled="busy || state?.switching" @click="changeServer">更换服务器…</button>
  67. </div>
  68. </section>
  69. </template>
  70. <style scoped>
  71. .desktop-mode-panel{display:grid;gap:20px;max-width:640px;color:var(--text-primary)}
  72. .mode-status{display:grid;gap:6px;padding:16px;border-radius:12px;background:var(--surface-soft)}
  73. .mode-status span{color:var(--text-secondary);font-size:13px;overflow-wrap:anywhere}
  74. fieldset{display:grid;gap:12px;min-width:0;margin:0;padding:0;border:0}
  75. legend{padding:0 0 12px;font-weight:600}
  76. .mode-option{display:flex;align-items:center;gap:12px;padding:18px;border:1px solid var(--line-strong);border-radius:12px;cursor:pointer}
  77. .mode-option.selected{border-color:var(--accent);background:var(--settings-selected)}
  78. .mode-option input{flex:none;margin:0;accent-color:var(--accent)}
  79. .mode-option span{display:grid;gap:6px;min-width:0}
  80. .mode-option small{font-size:13px;line-height:1.6;color:var(--text-secondary)}
  81. .mode-option:focus-within{outline:2px solid var(--accent);outline-offset:3px}
  82. fieldset:disabled .mode-option{cursor:default;opacity:.65}
  83. .mode-note{margin:0;color:var(--text-secondary);font-size:13px;line-height:1.7}
  84. .mode-error{margin:0;color:var(--danger)}
  85. .mode-actions{display:flex;flex-wrap:wrap;gap:12px}
  86. .mode-actions button{min-height:44px;padding:10px 18px;border:1px solid var(--line-strong);border-radius:10px;background:var(--surface-soft);color:var(--text-primary);font:inherit;cursor:pointer}
  87. .mode-actions .mode-primary{background:var(--accent);border-color:var(--accent);color:var(--text-on-solid)}
  88. .mode-actions button:disabled{opacity:.5;cursor:default}
  89. .mode-actions button:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
  90. </style>