mirror of
https://github.com/tiny-craft/tiny-rdm.git
synced 2026-04-23 00:17:09 +08:00
pref: merge web api to one file
This commit is contained in:
@@ -47,10 +47,6 @@ func registerPreferencesRoutes(rg *gin.RouterGroup) {
|
|||||||
c.JSON(http.StatusOK, services.Preferences().GetBuildInDecoder())
|
c.JSON(http.StatusOK, services.Preferences().GetBuildInDecoder())
|
||||||
})
|
})
|
||||||
|
|
||||||
g.GET("/version", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, services.Preferences().GetAppVersion())
|
|
||||||
})
|
|
||||||
|
|
||||||
g.GET("/check-update", func(c *gin.Context) {
|
g.GET("/check-update", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, services.Preferences().CheckForUpdate())
|
c.JSON(http.StatusOK, services.Preferences().CheckForUpdate())
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-22
@@ -3,8 +3,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,7 +15,7 @@ import (
|
|||||||
const maxRequestBodySize = 10 << 20 // 10MB
|
const maxRequestBodySize = 10 << 20 // 10MB
|
||||||
|
|
||||||
// SetupRouter creates the Gin router with all API routes and static file serving
|
// SetupRouter creates the Gin router with all API routes and static file serving
|
||||||
func SetupRouter(assets embed.FS) *gin.Engine {
|
func SetupRouter() *gin.Engine {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
@@ -57,9 +55,8 @@ func SetupRouter(assets embed.FS) *gin.Engine {
|
|||||||
|
|
||||||
// Public routes (no auth required)
|
// Public routes (no auth required)
|
||||||
registerAuthRoutes(r)
|
registerAuthRoutes(r)
|
||||||
r.GET("/api/version", func(c *gin.Context) {
|
r.GET("/api/preferences/version", func(c *gin.Context) {
|
||||||
resp := services.Preferences().GetAppVersion()
|
c.JSON(http.StatusOK, services.Preferences().GetAppVersion())
|
||||||
c.JSON(200, resp)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// WebSocket endpoint (auth checked via cookie + origin)
|
// WebSocket endpoint (auth checked via cookie + origin)
|
||||||
@@ -76,22 +73,6 @@ func SetupRouter(assets embed.FS) *gin.Engine {
|
|||||||
registerPreferencesRoutes(api)
|
registerPreferencesRoutes(api)
|
||||||
registerSystemRoutes(api)
|
registerSystemRoutes(api)
|
||||||
|
|
||||||
// Serve frontend static files from embedded assets
|
|
||||||
distFS, err := fs.Sub(assets, "frontend/dist")
|
|
||||||
if err == nil {
|
|
||||||
fileServer := http.FileServer(http.FS(distFS))
|
|
||||||
r.NoRoute(func(c *gin.Context) {
|
|
||||||
path := c.Request.URL.Path
|
|
||||||
f, ferr := http.FS(distFS).Open(path)
|
|
||||||
if ferr == nil {
|
|
||||||
f.Close()
|
|
||||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.FileFromFS("/", http.FS(distFS))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-16
@@ -23,17 +23,15 @@ import ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue'
|
|||||||
import { Info } from 'wailsjs/go/services/systemService.js'
|
import { Info } from 'wailsjs/go/services/systemService.js'
|
||||||
import DecoderDialog from '@/components/dialogs/DecoderDialog.vue'
|
import DecoderDialog from '@/components/dialogs/DecoderDialog.vue'
|
||||||
import { loadModule, trackEvent } from '@/utils/analytics.js'
|
import { loadModule, trackEvent } from '@/utils/analytics.js'
|
||||||
|
import { isWeb } from '@/utils/platform.js'
|
||||||
|
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
const connectionStore = useConnectionStore()
|
const connectionStore = useConnectionStore()
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const initializing = ref(true)
|
const initializing = ref(true)
|
||||||
|
|
||||||
// Detect if running in web mode (VITE_WEB=true at build time)
|
|
||||||
const isWebMode = import.meta.env.VITE_WEB === 'true'
|
|
||||||
|
|
||||||
// Web-only: lazy load LoginPage to avoid importing websocket.js in desktop mode
|
// Web-only: lazy load LoginPage to avoid importing websocket.js in desktop mode
|
||||||
const LoginPage = isWebMode ? defineAsyncComponent(() => import('@/components/LoginPage.vue')) : null
|
const LoginPage = isWeb() ? defineAsyncComponent(() => import('@/components/LoginPage.vue')) : null
|
||||||
|
|
||||||
// Viewport management for mobile
|
// Viewport management for mobile
|
||||||
const setViewport = (mode) => {
|
const setViewport = (mode) => {
|
||||||
@@ -57,7 +55,7 @@ const setViewport = (mode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auth state (web mode only)
|
// Auth state (web mode only)
|
||||||
const authChecking = ref(isWebMode) // desktop: false (skip), web: true (checking)
|
const authChecking = ref(isWeb()) // desktop: false (skip), web: true (checking)
|
||||||
const authenticated = ref(false)
|
const authenticated = ref(false)
|
||||||
const authEnabled = ref(false)
|
const authEnabled = ref(false)
|
||||||
|
|
||||||
@@ -82,33 +80,37 @@ const onLogin = async () => {
|
|||||||
// Reconnect WebSocket with auth cookie (dynamic import to avoid desktop issues)
|
// Reconnect WebSocket with auth cookie (dynamic import to avoid desktop issues)
|
||||||
try {
|
try {
|
||||||
const runtime = await import('wailsjs/runtime/runtime.js')
|
const runtime = await import('wailsjs/runtime/runtime.js')
|
||||||
if (runtime.ReconnectWebSocket) runtime.ReconnectWebSocket()
|
if (runtime.ReconnectWebSocket) {
|
||||||
|
runtime.ReconnectWebSocket()
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
// Capture login page choices before loadPreferences overwrites them
|
// Capture login page choices before loadPreferences overwrites them
|
||||||
const loginTheme = localStorage.getItem('rdm_login_theme')
|
const loginTheme = localStorage.getItem('rdm_login_theme')
|
||||||
const loginLang = localStorage.getItem('rdm_login_lang')
|
const loginLang = localStorage.getItem('rdm_login_lang')
|
||||||
await initApp()
|
await initApp()
|
||||||
// Sync login page choices to preferences
|
// Sync login page choices to preferences
|
||||||
let needSave = false
|
let prefUpdated = false
|
||||||
if (loginTheme && ['auto', 'light', 'dark'].includes(loginTheme)) {
|
if (loginTheme && ['auto', 'light', 'dark'].includes(loginTheme)) {
|
||||||
prefStore.general.theme = loginTheme
|
prefStore.general.theme = loginTheme
|
||||||
needSave = true
|
prefUpdated = true
|
||||||
}
|
}
|
||||||
if (loginLang) {
|
if (loginLang) {
|
||||||
const validLangs = ['auto', 'zh', 'tw', 'en', 'ja', 'ko', 'es', 'fr', 'ru', 'pt', 'tr']
|
const validLangs = ['auto', 'zh', 'tw', 'en', 'ja', 'ko', 'es', 'fr', 'ru', 'pt', 'tr']
|
||||||
if (validLangs.includes(loginLang)) {
|
if (validLangs.includes(loginLang)) {
|
||||||
prefStore.general.language = loginLang
|
prefStore.general.language = loginLang
|
||||||
i18n.locale.value = prefStore.currentLanguage
|
i18n.locale.value = prefStore.currentLanguage
|
||||||
needSave = true
|
prefUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (needSave) prefStore.savePreferences()
|
if (prefUpdated) {
|
||||||
|
prefStore.savePreferences()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initApp = async () => {
|
const initApp = async () => {
|
||||||
try {
|
try {
|
||||||
initializing.value = true
|
initializing.value = true
|
||||||
if (isWebMode) {
|
if (isWeb()) {
|
||||||
const prefResult = await prefStore.loadPreferences()
|
const prefResult = await prefStore.loadPreferences()
|
||||||
// If loadPreferences failed (e.g. 401 from expired session),
|
// If loadPreferences failed (e.g. 401 from expired session),
|
||||||
// rdm:unauthorized event already fired → silently abort init
|
// rdm:unauthorized event already fired → silently abort init
|
||||||
@@ -118,7 +120,7 @@ const initApp = async () => {
|
|||||||
await prefStore.loadFontList()
|
await prefStore.loadFontList()
|
||||||
await prefStore.loadBuildInDecoder()
|
await prefStore.loadBuildInDecoder()
|
||||||
await connectionStore.initConnections()
|
await connectionStore.initConnections()
|
||||||
if (prefStore.autoCheckUpdate) {
|
if (!isWeb() && prefStore.autoCheckUpdate) {
|
||||||
prefStore.checkForUpdate()
|
prefStore.checkForUpdate()
|
||||||
}
|
}
|
||||||
const env = await Environment()
|
const env = await Environment()
|
||||||
@@ -196,7 +198,7 @@ const onOrientationChange = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (isWebMode) {
|
if (isWeb()) {
|
||||||
// Apply saved login theme before auth check to prevent flash
|
// Apply saved login theme before auth check to prevent flash
|
||||||
const savedTheme = localStorage.getItem('rdm_login_theme')
|
const savedTheme = localStorage.getItem('rdm_login_theme')
|
||||||
if (savedTheme && ['auto', 'light', 'dark'].includes(savedTheme)) {
|
if (savedTheme && ['auto', 'light', 'dark'].includes(savedTheme)) {
|
||||||
@@ -225,7 +227,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (isWebMode) {
|
if (isWeb()) {
|
||||||
window.removeEventListener('rdm:unauthorized', onUnauthorized)
|
window.removeEventListener('rdm:unauthorized', onUnauthorized)
|
||||||
window.removeEventListener('orientationchange', onOrientationChange)
|
window.removeEventListener('orientationchange', onOrientationChange)
|
||||||
window.removeEventListener('resize', onOrientationChange)
|
window.removeEventListener('resize', onOrientationChange)
|
||||||
@@ -253,10 +255,10 @@ watch(
|
|||||||
:theme-overrides="prefStore.isDark ? darkThemeOverrides : themeOverrides"
|
:theme-overrides="prefStore.isDark ? darkThemeOverrides : themeOverrides"
|
||||||
class="fill-height">
|
class="fill-height">
|
||||||
<!-- Web mode: auth gate -->
|
<!-- Web mode: auth gate -->
|
||||||
<template v-if="isWebMode && authChecking">
|
<template v-if="isWeb() && authChecking">
|
||||||
<div style="width: 100vw; height: 100vh"></div>
|
<div style="width: 100vw; height: 100vh"></div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isWebMode && authEnabled && !authenticated">
|
<template v-else-if="isWeb() && authEnabled && !authenticated">
|
||||||
<component :is="LoginPage" @login="onLogin" />
|
<component :is="LoginPage" @login="onLogin" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { computed, h, onMounted, ref } from 'vue'
|
|||||||
import { NIcon, useThemeVars } from 'naive-ui'
|
import { NIcon, useThemeVars } from 'naive-ui'
|
||||||
import iconUrl from '@/assets/images/icon.png'
|
import iconUrl from '@/assets/images/icon.png'
|
||||||
import usePreferencesStore from '@/stores/preferences.js'
|
import usePreferencesStore from '@/stores/preferences.js'
|
||||||
|
import LangIcon from '@/components/icons/Lang.vue'
|
||||||
|
|
||||||
|
import { Login } from '@/utils/api.js'
|
||||||
|
import { lang } from '@/langs/index.js'
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
@@ -17,21 +21,18 @@ onMounted(() => {
|
|||||||
prefStore.general.theme = themeMode.value
|
prefStore.general.theme = themeMode.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const themeLabels = {
|
const getThemeLabels = (langKey) => {
|
||||||
zh: { auto: '自动', light: '浅色', dark: '暗黑' },
|
const l = lang[langKey] || lang.en
|
||||||
tw: { auto: '自動', light: '淺色', dark: '暗黑' },
|
const g = l.preferences?.general || {}
|
||||||
ja: { auto: '自動', light: 'ライト', dark: 'ダーク' },
|
return {
|
||||||
ko: { auto: '자동', light: '라이트', dark: '다크' },
|
auto: g.theme_auto || 'Auto',
|
||||||
es: { auto: 'Auto', light: 'Claro', dark: 'Oscuro' },
|
light: g.theme_light || 'Light',
|
||||||
fr: { auto: 'Auto', light: 'Clair', dark: 'Sombre' },
|
dark: g.theme_dark || 'Dark',
|
||||||
ru: { auto: 'Авто', light: 'Светлая', dark: 'Тёмная' },
|
}
|
||||||
pt: { auto: 'Auto', light: 'Claro', dark: 'Escuro' },
|
|
||||||
tr: { auto: 'Otomatik', light: 'Açık', dark: 'Koyu' },
|
|
||||||
en: { auto: 'Auto', light: 'Light', dark: 'Dark' },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeOptions = computed(() => {
|
const themeOptions = computed(() => {
|
||||||
const labels = themeLabels[currentLang.value] || themeLabels.en
|
const labels = getThemeLabels(currentLang.value)
|
||||||
return [
|
return [
|
||||||
{ label: labels.light, key: 'light', icon: renderIcon('sun') },
|
{ label: labels.light, key: 'light', icon: renderIcon('sun') },
|
||||||
{ label: labels.dark, key: 'dark', icon: renderIcon('moon') },
|
{ label: labels.dark, key: 'dark', icon: renderIcon('moon') },
|
||||||
@@ -40,7 +41,7 @@ const themeOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const currentThemeLabel = computed(() => {
|
const currentThemeLabel = computed(() => {
|
||||||
const labels = themeLabels[currentLang.value] || themeLabels.en
|
const labels = getThemeLabels(currentLang.value)
|
||||||
return labels[themeMode.value]
|
return labels[themeMode.value]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,24 +54,22 @@ const onThemeSelect = (key) => {
|
|||||||
|
|
||||||
// --- Language ---
|
// --- Language ---
|
||||||
const LANG_KEY = 'rdm_login_lang'
|
const LANG_KEY = 'rdm_login_lang'
|
||||||
const langNames = {
|
const langNames = Object.fromEntries(Object.entries(lang).map(([k, v]) => [k, v.name]))
|
||||||
zh: '简体中文', tw: '繁體中文', en: 'English', ja: '日本語', ko: '한국어',
|
const autoLabel = Object.fromEntries(
|
||||||
es: 'Español', fr: 'Français', ru: 'Русский', pt: 'Português', tr: 'Türkçe',
|
Object.entries(lang).map(([k, v]) => [k, v.preferences?.general?.theme_auto || 'Auto']),
|
||||||
}
|
)
|
||||||
const autoLabel = {
|
|
||||||
zh: '自动', tw: '自動', ja: '自動', ko: '자동', es: 'Auto',
|
|
||||||
fr: 'Auto', ru: 'Авто', pt: 'Auto', tr: 'Otomatik', en: 'Auto',
|
|
||||||
}
|
|
||||||
|
|
||||||
const detectSystemLang = () => {
|
const detectSystemLang = () => {
|
||||||
const sysLang = (navigator.language || '').toLowerCase()
|
const sysLang = (navigator.language || '').toLowerCase()
|
||||||
if (sysLang.startsWith('zh-tw') || sysLang.startsWith('zh-hant')) return 'tw'
|
if (sysLang.startsWith('zh-tw') || sysLang.startsWith('zh-hant')) {
|
||||||
|
return 'tw'
|
||||||
|
}
|
||||||
const prefix = sysLang.split('-')[0]
|
const prefix = sysLang.split('-')[0]
|
||||||
return langNames[prefix] ? prefix : 'en'
|
return langNames[prefix] ? prefix : 'en'
|
||||||
}
|
}
|
||||||
|
|
||||||
const langSetting = ref(localStorage.getItem(LANG_KEY) || 'auto')
|
const langSetting = ref(localStorage.getItem(LANG_KEY) || 'auto')
|
||||||
const currentLang = computed(() => langSetting.value === 'auto' ? detectSystemLang() : langSetting.value)
|
const currentLang = computed(() => (langSetting.value === 'auto' ? detectSystemLang() : langSetting.value))
|
||||||
|
|
||||||
const langOptions = computed(() => [
|
const langOptions = computed(() => [
|
||||||
{ label: autoLabel[currentLang.value] || 'Auto', key: 'auto' },
|
{ label: autoLabel[currentLang.value] || 'Auto', key: 'auto' },
|
||||||
@@ -91,31 +90,88 @@ const onLangSelect = (key) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SunSvg = {
|
const SunSvg = {
|
||||||
render: () => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', width: '1em', height: '1em', fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, [
|
render: () =>
|
||||||
h('circle', { cx: '12', cy: '12', r: '5' }),
|
h(
|
||||||
h('path', { d: 'M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42' }),
|
'svg',
|
||||||
]),
|
{
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
width: '1em',
|
||||||
|
height: '1em',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h('circle', { cx: '12', cy: '12', r: '5' }),
|
||||||
|
h('path', {
|
||||||
|
d: 'M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
const MoonSvg = {
|
const MoonSvg = {
|
||||||
render: () => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', width: '1em', height: '1em', fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, [
|
render: () =>
|
||||||
h('path', { d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z' }),
|
h(
|
||||||
]),
|
'svg',
|
||||||
|
{
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
width: '1em',
|
||||||
|
height: '1em',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
},
|
||||||
|
[h('path', { d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z' })],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
const AutoSvg = {
|
const AutoSvg = {
|
||||||
render: () => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', width: '1em', height: '1em', fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, [
|
render: () =>
|
||||||
h('circle', { cx: '12', cy: '12', r: '10' }),
|
h(
|
||||||
h('path', { d: 'M12 2a10 10 0 0 1 0 20V2' }),
|
'svg',
|
||||||
]),
|
{
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
width: '1em',
|
||||||
|
height: '1em',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
},
|
||||||
|
[h('circle', { cx: '12', cy: '12', r: '10' }), h('path', { d: 'M12 2a10 10 0 0 1 0 20V2' })],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
const LangSvg = {
|
const LangSvg = {
|
||||||
render: () => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', width: '1em', height: '1em', fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, [
|
render: () =>
|
||||||
h('path', { d: 'M5 8l6 6' }),
|
h(
|
||||||
h('path', { d: 'M4 14l6-6 2-3' }),
|
'svg',
|
||||||
h('path', { d: 'M2 5h12' }),
|
{
|
||||||
h('path', { d: 'M7 2h1' }),
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
h('path', { d: 'M22 22l-5-10-5 10' }),
|
viewBox: '0 0 24 24',
|
||||||
h('path', { d: 'M14 18h6' }),
|
width: '1em',
|
||||||
]),
|
height: '1em',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h('path', { d: 'M5 8l6 6' }),
|
||||||
|
h('path', { d: 'M4 14l6-6 2-3' }),
|
||||||
|
h('path', { d: 'M2 5h12' }),
|
||||||
|
h('path', { d: 'M7 2h1' }),
|
||||||
|
h('path', { d: 'M22 22l-5-10-5 10' }),
|
||||||
|
h('path', { d: 'M14 18h6' }),
|
||||||
|
],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap = { sun: SunSvg, moon: MoonSvg, auto: AutoSvg, lang: LangSvg }
|
const iconMap = { sun: SunSvg, moon: MoonSvg, auto: AutoSvg, lang: LangSvg }
|
||||||
@@ -123,16 +179,116 @@ const renderIcon = (name) => () => h(NIcon, null, { default: () => h(iconMap[nam
|
|||||||
|
|
||||||
// --- i18n texts ---
|
// --- i18n texts ---
|
||||||
const langTexts = {
|
const langTexts = {
|
||||||
zh: { title: '登录', username: '用户名', password: '密码', usernamePh: '请输入用户名', passwordPh: '请输入密码', submit: '登 录', tooMany: '尝试次数过多,请稍后再试', failed: '用户名或密码错误', network: '网络错误' },
|
zh: {
|
||||||
tw: { title: '登入', username: '使用者名稱', password: '密碼', usernamePh: '請輸入使用者名稱', passwordPh: '請輸入密碼', submit: '登 入', tooMany: '嘗試次數過多,請稍後再試', failed: '使用者名稱或密碼錯誤', network: '網路錯誤' },
|
title: '登录',
|
||||||
ja: { title: 'ログイン', username: 'ユーザー名', password: 'パスワード', usernamePh: 'ユーザー名を入力', passwordPh: 'パスワードを入力', submit: 'ログイン', tooMany: '試行回数が多すぎます', failed: 'ユーザー名またはパスワードが正しくありません', network: 'ネットワークエラー' },
|
username: '用户名',
|
||||||
ko: { title: '로그인', username: '사용자 이름', password: '비밀번호', usernamePh: '사용자 이름 입력', passwordPh: '비밀번호 입력', submit: '로그인', tooMany: '시도 횟수 초과, 잠시 후 다시 시도하세요', failed: '사용자 이름 또는 비밀번호가 올바르지 않습니다', network: '네트워크 오류' },
|
password: '密码',
|
||||||
es: { title: 'Iniciar sesión', username: 'Usuario', password: 'Contraseña', usernamePh: 'Ingrese usuario', passwordPh: 'Ingrese contraseña', submit: 'Entrar', tooMany: 'Demasiados intentos, intente más tarde', failed: 'Credenciales inválidas', network: 'Error de red' },
|
usernamePh: '请输入用户名',
|
||||||
fr: { title: 'Connexion', username: "Nom d'utilisateur", password: 'Mot de passe', usernamePh: "Entrez le nom d'utilisateur", passwordPh: 'Entrez le mot de passe', submit: 'Se connecter', tooMany: 'Trop de tentatives, réessayez plus tard', failed: 'Identifiants invalides', network: 'Erreur réseau' },
|
passwordPh: '请输入密码',
|
||||||
ru: { title: 'Вход', username: 'Имя пользователя', password: 'Пароль', usernamePh: 'Введите имя пользователя', passwordPh: 'Введите пароль', submit: 'Войти', tooMany: 'Слишком много попыток, попробуйте позже', failed: 'Неверные учётные данные', network: 'Ошибка сети' },
|
submit: '登 录',
|
||||||
pt: { title: 'Entrar', username: 'Usuário', password: 'Senha', usernamePh: 'Digite o usuário', passwordPh: 'Digite a senha', submit: 'Entrar', tooMany: 'Muitas tentativas, tente novamente mais tarde', failed: 'Credenciais inválidas', network: 'Erro de rede' },
|
tooMany: '尝试次数过多,请稍后再试',
|
||||||
tr: { title: 'Giriş', username: 'Kullanıcı adı', password: 'Şifre', usernamePh: 'Kullanıcı adını girin', passwordPh: 'Şifreyi girin', submit: 'Giriş Yap', tooMany: 'Çok fazla deneme, lütfen daha sonra tekrar deneyin', failed: 'Geçersiz kimlik bilgileri', network: 'Ağ hatası' },
|
failed: '用户名或密码错误',
|
||||||
en: { title: 'Sign In', username: 'Username', password: 'Password', usernamePh: 'Enter username', passwordPh: 'Enter password', submit: 'Sign In', tooMany: 'Too many attempts, please try later', failed: 'Invalid credentials', network: 'Network error' },
|
network: '网络错误',
|
||||||
|
},
|
||||||
|
tw: {
|
||||||
|
title: '登入',
|
||||||
|
username: '使用者名稱',
|
||||||
|
password: '密碼',
|
||||||
|
usernamePh: '請輸入使用者名稱',
|
||||||
|
passwordPh: '請輸入密碼',
|
||||||
|
submit: '登 入',
|
||||||
|
tooMany: '嘗試次數過多,請稍後再試',
|
||||||
|
failed: '使用者名稱或密碼錯誤',
|
||||||
|
network: '網路錯誤',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
title: 'ログイン',
|
||||||
|
username: 'ユーザー名',
|
||||||
|
password: 'パスワード',
|
||||||
|
usernamePh: 'ユーザー名を入力',
|
||||||
|
passwordPh: 'パスワードを入力',
|
||||||
|
submit: 'ログイン',
|
||||||
|
tooMany: '試行回数が多すぎます',
|
||||||
|
failed: 'ユーザー名またはパスワードが正しくありません',
|
||||||
|
network: 'ネットワークエラー',
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
title: '로그인',
|
||||||
|
username: '사용자 이름',
|
||||||
|
password: '비밀번호',
|
||||||
|
usernamePh: '사용자 이름 입력',
|
||||||
|
passwordPh: '비밀번호 입력',
|
||||||
|
submit: '로그인',
|
||||||
|
tooMany: '시도 횟수 초과, 잠시 후 다시 시도하세요',
|
||||||
|
failed: '사용자 이름 또는 비밀번호가 올바르지 않습니다',
|
||||||
|
network: '네트워크 오류',
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
title: 'Iniciar sesión',
|
||||||
|
username: 'Usuario',
|
||||||
|
password: 'Contraseña',
|
||||||
|
usernamePh: 'Ingrese usuario',
|
||||||
|
passwordPh: 'Ingrese contraseña',
|
||||||
|
submit: 'Entrar',
|
||||||
|
tooMany: 'Demasiados intentos, intente más tarde',
|
||||||
|
failed: 'Credenciales inválidas',
|
||||||
|
network: 'Error de red',
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
title: 'Connexion',
|
||||||
|
username: "Nom d'utilisateur",
|
||||||
|
password: 'Mot de passe',
|
||||||
|
usernamePh: "Entrez le nom d'utilisateur",
|
||||||
|
passwordPh: 'Entrez le mot de passe',
|
||||||
|
submit: 'Se connecter',
|
||||||
|
tooMany: 'Trop de tentatives, réessayez plus tard',
|
||||||
|
failed: 'Identifiants invalides',
|
||||||
|
network: 'Erreur réseau',
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
title: 'Вход',
|
||||||
|
username: 'Имя пользователя',
|
||||||
|
password: 'Пароль',
|
||||||
|
usernamePh: 'Введите имя пользователя',
|
||||||
|
passwordPh: 'Введите пароль',
|
||||||
|
submit: 'Войти',
|
||||||
|
tooMany: 'Слишком много попыток, попробуйте позже',
|
||||||
|
failed: 'Неверные учётные данные',
|
||||||
|
network: 'Ошибка сети',
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
title: 'Entrar',
|
||||||
|
username: 'Usuário',
|
||||||
|
password: 'Senha',
|
||||||
|
usernamePh: 'Digite o usuário',
|
||||||
|
passwordPh: 'Digite a senha',
|
||||||
|
submit: 'Entrar',
|
||||||
|
tooMany: 'Muitas tentativas, tente novamente mais tarde',
|
||||||
|
failed: 'Credenciais inválidas',
|
||||||
|
network: 'Erro de rede',
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
title: 'Giriş',
|
||||||
|
username: 'Kullanıcı adı',
|
||||||
|
password: 'Şifre',
|
||||||
|
usernamePh: 'Kullanıcı adını girin',
|
||||||
|
passwordPh: 'Şifreyi girin',
|
||||||
|
submit: 'Giriş Yap',
|
||||||
|
tooMany: 'Çok fazla deneme, lütfen daha sonra tekrar deneyin',
|
||||||
|
failed: 'Geçersiz kimlik bilgileri',
|
||||||
|
network: 'Ağ hatası',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
title: 'Sign In',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
usernamePh: 'Enter username',
|
||||||
|
passwordPh: 'Enter password',
|
||||||
|
submit: 'Sign In',
|
||||||
|
tooMany: 'Too many attempts, please try later',
|
||||||
|
failed: 'Invalid credentials',
|
||||||
|
network: 'Network error',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = computed(() => langTexts[currentLang.value] || langTexts.en)
|
const t = computed(() => langTexts[currentLang.value] || langTexts.en)
|
||||||
@@ -142,17 +298,6 @@ const username = ref('')
|
|||||||
const password = ref('')
|
const password = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
const appVersion = ref('')
|
|
||||||
|
|
||||||
;(async () => {
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/version')
|
|
||||||
const result = await resp.json()
|
|
||||||
if (result.success && result.data?.version) {
|
|
||||||
appVersion.value = result.data.version
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const canSubmit = computed(() => username.value.length > 0 && password.value.length > 0)
|
const canSubmit = computed(() => username.value.length > 0 && password.value.length > 0)
|
||||||
|
|
||||||
@@ -162,15 +307,15 @@ const handleLogin = async () => {
|
|||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/auth/login', {
|
const { msg, success = false } = await Login(username.value, password.value)
|
||||||
method: 'POST',
|
if (msg === 'too_many_attempts') {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
errorMsg.value = t.value.tooMany
|
||||||
credentials: 'same-origin',
|
return
|
||||||
body: JSON.stringify({ username: username.value, password: password.value }),
|
}
|
||||||
})
|
if (!success) {
|
||||||
const data = await resp.json()
|
errorMsg.value = t.value.failed
|
||||||
if (resp.status === 429) { errorMsg.value = t.value.tooMany; return }
|
return
|
||||||
if (!data.success) { errorMsg.value = t.value.failed; return }
|
}
|
||||||
emit('login')
|
emit('login')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg.value = t.value.network
|
errorMsg.value = t.value.network
|
||||||
@@ -228,21 +373,27 @@ const handleLogin = async () => {
|
|||||||
<div class="login-toolbar">
|
<div class="login-toolbar">
|
||||||
<n-dropdown :options="langOptions" size="small" trigger="hover" @select="onLangSelect">
|
<n-dropdown :options="langOptions" size="small" trigger="hover" @select="onLangSelect">
|
||||||
<span class="toolbar-btn">
|
<span class="toolbar-btn">
|
||||||
<n-icon :component="LangSvg" :size="14" />
|
<n-icon :component="LangIcon" :size="14" />
|
||||||
<span>{{ currentLangLabel }}</span>
|
<span>{{ currentLangLabel }}</span>
|
||||||
</span>
|
</span>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
<n-divider style="margin: 0 4px" vertical />
|
<n-divider style="margin: 0 4px" vertical />
|
||||||
<n-dropdown :options="themeOptions" size="small" trigger="hover" @select="onThemeSelect">
|
<n-dropdown :options="themeOptions" size="small" trigger="hover" @select="onThemeSelect">
|
||||||
<span class="toolbar-btn">
|
<span class="toolbar-btn">
|
||||||
<n-icon :component="themeMode === 'dark' ? MoonSvg : themeMode === 'light' ? SunSvg : AutoSvg" :size="14" />
|
<n-icon
|
||||||
|
:component="themeMode === 'dark' ? MoonSvg : themeMode === 'light' ? SunSvg : AutoSvg"
|
||||||
|
:size="14" />
|
||||||
<span>{{ currentThemeLabel }}</span>
|
<span>{{ currentThemeLabel }}</span>
|
||||||
</span>
|
</span>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
<template v-if="appVersion">
|
<template v-if="prefStore.appVersion">
|
||||||
<n-divider style="margin: 0 4px" vertical />
|
<n-divider style="margin: 0 4px" vertical />
|
||||||
<a class="toolbar-btn toolbar-link" href="https://github.com/tiny-craft/tiny-rdm" rel="noopener noreferrer" target="_blank">
|
<a
|
||||||
{{ appVersion }}
|
class="toolbar-btn toolbar-link"
|
||||||
|
href="https://github.com/tiny-craft/tiny-rdm"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank">
|
||||||
|
{{ prefStore.appVersion }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +468,9 @@ const handleLogin = async () => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: color 0.2s, background-color 0.2s;
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
background-color 0.2s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { CloseCli, StartCli } from 'wailsjs/go/services/cliService.js'
|
|||||||
import usePreferencesStore from 'stores/preferences.js'
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
import { i18nGlobal } from '@/utils/i18n.js'
|
import { i18nGlobal } from '@/utils/i18n.js'
|
||||||
import wcwidth from 'wcwidth'
|
import wcwidth from 'wcwidth'
|
||||||
|
import { isWeb } from '@/utils/platform.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: String,
|
name: String,
|
||||||
@@ -96,7 +97,7 @@ onMounted(async () => {
|
|||||||
EventsOn(`cmd:output:${props.name}`, receiveTermOutput)
|
EventsOn(`cmd:output:${props.name}`, receiveTermOutput)
|
||||||
|
|
||||||
// Wait for WebSocket with timeout (CLI needs it for real-time I/O, web mode only)
|
// Wait for WebSocket with timeout (CLI needs it for real-time I/O, web mode only)
|
||||||
if (import.meta.env.VITE_WEB === 'true') {
|
if (isWeb()) {
|
||||||
try {
|
try {
|
||||||
const { WaitForWebSocket } = await import('wailsjs/runtime/runtime.js')
|
const { WaitForWebSocket } = await import('wailsjs/runtime/runtime.js')
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import iconUrl from '@/assets/images/icon.png'
|
import iconUrl from '@/assets/images/icon.png'
|
||||||
import useDialog from 'stores/dialog.js'
|
import useDialog from 'stores/dialog.js'
|
||||||
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
|
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
|
||||||
import { GetAppVersion } from 'wailsjs/go/services/preferencesService.js'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const dialogStore = useDialog()
|
const dialogStore = useDialog()
|
||||||
const version = ref('')
|
const prefStore = usePreferencesStore()
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
GetAppVersion().then(({ data }) => {
|
|
||||||
version.value = data.version
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const onOpenSource = () => {
|
const onOpenSource = () => {
|
||||||
BrowserOpenURL('https://github.com/tiny-craft/tiny-rdm')
|
BrowserOpenURL('https://github.com/tiny-craft/tiny-rdm')
|
||||||
@@ -30,14 +23,14 @@ const onOpenWebsite = () => {
|
|||||||
<n-space :size="10" :wrap="false" :wrap-item="false" align="center" vertical>
|
<n-space :size="10" :wrap="false" :wrap-item="false" align="center" vertical>
|
||||||
<n-avatar :size="120" :src="iconUrl" color="#0000"></n-avatar>
|
<n-avatar :size="120" :src="iconUrl" color="#0000"></n-avatar>
|
||||||
<div class="about-app-title">Tiny RDM</div>
|
<div class="about-app-title">Tiny RDM</div>
|
||||||
<n-text>{{ version }}</n-text>
|
<n-text>{{ prefStore.appVersion }}</n-text>
|
||||||
<n-space :size="5" :wrap="false" :wrap-item="false" align="center">
|
<n-space :size="5" :wrap="false" :wrap-item="false" align="center">
|
||||||
<n-text class="about-link" @click="onOpenSource">{{ $t('dialogue.about.source') }}</n-text>
|
<n-text class="about-link" @click="onOpenSource">{{ $t('dialogue.about.source') }}</n-text>
|
||||||
<n-divider vertical />
|
<n-divider vertical />
|
||||||
<n-text class="about-link" @click="onOpenWebsite">{{ $t('dialogue.about.website') }}</n-text>
|
<n-text class="about-link" @click="onOpenWebsite">{{ $t('dialogue.about.website') }}</n-text>
|
||||||
</n-space>
|
</n-space>
|
||||||
<div :style="{ color: themeVars.textColor3 }" class="about-copyright">
|
<div :style="{ color: themeVars.textColor3 }" class="about-copyright">
|
||||||
Copyright © 2026 Tinycraft.cc All rights reserved
|
Copyright © {{ new Date().getFullYear() }} Tinycraft.cc All rights reserved
|
||||||
</div>
|
</div>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M28.2857 37H39.7143M42 42L39.7143 37L42 42ZM26 42L28.2857 37L26 42ZM28.2857 37L34 24L39.7143 37H28.2857Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M16 6L17 9"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M6 11H28"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M10 16C10 16 11.7895 22.2609 16.2632 25.7391C20.7368 29.2174 28 32 28 32"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 11C24 11 22.2105 19.2174 17.7368 23.7826C13.2632 28.3478 6 32 6 32"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
@@ -9,9 +9,24 @@ const props = defineProps({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M23.9917 6H6V42H24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
<path
|
||||||
<path d="M33 33L42 24L33 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
:stroke-width="props.strokeWidth"
|
||||||
<path d="M16 23.9917H42" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
d="M23.9917 6H6V42H24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M33 33L42 24L33 15"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M16 23.9917H42"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import bilibiliUrl from '@/assets/images/bilibili_official.png'
|
|||||||
import QRCode from '@/components/icons/QRCode.vue'
|
import QRCode from '@/components/icons/QRCode.vue'
|
||||||
import Twitter from '@/components/icons/Twitter.vue'
|
import Twitter from '@/components/icons/Twitter.vue'
|
||||||
import { trackEvent } from '@/utils/analytics.js'
|
import { trackEvent } from '@/utils/analytics.js'
|
||||||
import Logout from '@/components/icons/Logout.vue'
|
import LogoutIcon from '@/components/icons/Logout.vue'
|
||||||
import { isWeb } from '@/utils/platform.js'
|
import { isWeb } from '@/utils/platform.js'
|
||||||
|
import { Logout } from '@/utils/api.js'
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
const render = useRender()
|
const render = useRender()
|
||||||
@@ -140,7 +141,7 @@ const openGithub = () => {
|
|||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' })
|
await Logout()
|
||||||
} catch {}
|
} catch {}
|
||||||
window.dispatchEvent(new Event('rdm:unauthorized'))
|
window.dispatchEvent(new Event('rdm:unauthorized'))
|
||||||
}
|
}
|
||||||
@@ -207,9 +208,9 @@ const exThemeVars = computed(() => {
|
|||||||
@click="openGithub" />
|
@click="openGithub" />
|
||||||
<icon-button
|
<icon-button
|
||||||
v-if="isWeb()"
|
v-if="isWeb()"
|
||||||
:icon="Logout"
|
:icon="LogoutIcon"
|
||||||
:size="iconSize"
|
:size="iconSize"
|
||||||
:stroke-width="3"
|
:stroke-width="3.5"
|
||||||
:tooltip-delay="100"
|
:tooltip-delay="100"
|
||||||
t-tooltip="ribbon.logout"
|
t-tooltip="ribbon.logout"
|
||||||
@click="handleLogout" />
|
@click="handleLogout" />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import usePreferencesStore from 'stores/preferences.js'
|
|||||||
import { loadEnvironment } from '@/utils/platform.js'
|
import { loadEnvironment } from '@/utils/platform.js'
|
||||||
import { setupMonaco } from '@/utils/monaco.js'
|
import { setupMonaco } from '@/utils/monaco.js'
|
||||||
import { setupChart } from '@/utils/chart.js'
|
import { setupChart } from '@/utils/chart.js'
|
||||||
|
import { isWeb } from './utils/platform.js'
|
||||||
|
|
||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
@@ -24,8 +25,8 @@ async function setupApp() {
|
|||||||
setupMonaco()
|
setupMonaco()
|
||||||
setupChart()
|
setupChart()
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
const isWebMode = import.meta.env.VITE_WEB === 'true'
|
if (isWeb()) {
|
||||||
if (!isWebMode) {
|
await prefStore.loadAppVersion()
|
||||||
await prefStore.loadPreferences()
|
await prefStore.loadPreferences()
|
||||||
}
|
}
|
||||||
await setupDiscreteApi()
|
await setupDiscreteApi()
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { lang } from '@/langs/index.js'
|
|||||||
import { cloneDeep, findIndex, get, isEmpty, join, map, pick, set, some, split } from 'lodash'
|
import { cloneDeep, findIndex, get, isEmpty, join, map, pick, set, some, split } from 'lodash'
|
||||||
import {
|
import {
|
||||||
CheckForUpdate,
|
CheckForUpdate,
|
||||||
|
GetAppVersion,
|
||||||
GetBuildInDecoder,
|
GetBuildInDecoder,
|
||||||
GetFontList,
|
GetFontList,
|
||||||
GetPreferences,
|
GetPreferences,
|
||||||
RestorePreferences,
|
RestorePreferences,
|
||||||
SetPreferences
|
SetPreferences,
|
||||||
} from 'wailsjs/go/services/preferencesService.js'
|
} from 'wailsjs/go/services/preferencesService.js'
|
||||||
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
|
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
|
||||||
import { i18nGlobal } from '@/utils/i18n.js'
|
import { i18nGlobal } from '@/utils/i18n.js'
|
||||||
@@ -75,6 +76,7 @@ const usePreferencesStore = defineStore('preferences', {
|
|||||||
decoder: [],
|
decoder: [],
|
||||||
lastPref: {},
|
lastPref: {},
|
||||||
fontList: [],
|
fontList: [],
|
||||||
|
appVersion: '',
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
getSeparator() {
|
getSeparator() {
|
||||||
@@ -345,6 +347,17 @@ const usePreferencesStore = defineStore('preferences', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load app version
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async loadAppVersion() {
|
||||||
|
const { success, data } = await GetAppVersion()
|
||||||
|
if (success && data?.version) {
|
||||||
|
this.appVersion = data.version
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save preferences to local
|
* save preferences to local
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
|
|||||||
@@ -426,4 +426,12 @@ export async function SaveFile(title, defaultName, ext) {
|
|||||||
return { success: true, data: { path: '' } }
|
return { success: true, data: { path: '' } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Auth Service ====================
|
||||||
|
|
||||||
|
export async function Login(username, password) {
|
||||||
|
return await post('/auth/login', { username, password })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Logout() {
|
||||||
|
return await post('/auth/logout')
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
"tinyrdm/backend/consts"
|
"tinyrdm/backend/consts"
|
||||||
"tinyrdm/backend/services"
|
"tinyrdm/backend/services"
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ func main() {
|
|||||||
TitleBar: mac.TitleBarHiddenInset(),
|
TitleBar: mac.TitleBarHiddenInset(),
|
||||||
About: &mac.AboutInfo{
|
About: &mac.AboutInfo{
|
||||||
Title: fmt.Sprintf("%s %s", appName, version),
|
Title: fmt.Sprintf("%s %s", appName, version),
|
||||||
Message: "A modern lightweight cross-platform Redis desktop client.\n\nCopyright © 2026",
|
Message: "A modern lightweight cross-platform Redis desktop client.\n\nCopyright © " + time.Now().Format("2006"),
|
||||||
Icon: icon,
|
Icon: icon,
|
||||||
},
|
},
|
||||||
WebviewIsTransparent: false,
|
WebviewIsTransparent: false,
|
||||||
|
|||||||
+1
-5
@@ -4,7 +4,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,9 +14,6 @@ import (
|
|||||||
"tinyrdm/backend/services"
|
"tinyrdm/backend/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all:frontend/dist
|
|
||||||
var assets embed.FS
|
|
||||||
|
|
||||||
var version = "0.0.0"
|
var version = "0.0.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -60,7 +56,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup HTTP server
|
// Setup HTTP server
|
||||||
router := api.SetupRouter(assets)
|
router := api.SetupRouter()
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: fmt.Sprintf("0.0.0.0:%s", port),
|
Addr: fmt.Sprintf("0.0.0.0:%s", port),
|
||||||
Handler: router,
|
Handler: router,
|
||||||
|
|||||||
Reference in New Issue
Block a user