重构: 前端Vue3+Tailwind+Vite构建管线+设置组件拆分

This commit is contained in:
lxy
2026-05-27 02:42:25 +08:00
parent f3148bf72f
commit aee7997195
44 changed files with 4309 additions and 1720 deletions
+89
View File
@@ -0,0 +1,89 @@
<template>
<div class="p-4 select-none overflow-y-auto">
<div class="mb-3.5">
<h1 class="text-base font-semibold text-[var(--text-strong)]">桌面设置</h1>
<p class="text-[11px] text-[var(--text-weak)] mt-0.5">壁纸 · 布局 · 信息显示</p>
</div>
<ToggleSection :s="s" />
<WallpaperSection :s="s" @set-wp-type="setWpType" :current-color="currentColor" @pick-solid="onPickSolid" @pick-gradient="onPickGradient" @save-color="onSaveColor" @apply-color="onApplySavedColor" @remove-color="onRemoveSavedColor" />
<LayoutSection :s="s" />
<PhotoSection :s="s" />
<PersonalizeSection :s="s" />
<div class="text-center text-[11px] text-[var(--footer-color)] mt-3 py-1.5 tracking-wide">u-desktop v1.0</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { go } from './composables/useGoBridge'
import ToggleSection from './components/ToggleSection.vue'
import WallpaperSection from './components/WallpaperSection.vue'
import LayoutSection from './components/LayoutSection.vue'
import PhotoSection from './components/PhotoSection.vue'
import PersonalizeSection from './components/PersonalizeSection.vue'
import type { SettingsData } from '@shared/types'
const initDone = ref(false)
const s = reactive<SettingsData>({
lightTheme: false, wallpaper: true, time: true, weather: true,
zodiacCard: true, knowledgeCard: true, layout: 'single', zodiac: '射手座',
city: '', provinces: [], citiesByProv: {}, wallpaperType: 'theme', theme: '',
themes: [], color1: '', color2: '', colorGradient: false, savedColors: [],
bingAutoRefresh: false, knowledgeKeyword: '', knowledgePrompt: '',
wallpaperText: '', imagePath: '', showSeconds: true, ainewsCard: true,
photoDir: '', photoInterval: 15, photoCard: true, photoFrameMode: false, autoStart: false,
})
// Shared state for color section
const currentColor = reactive({ c1: '', c2: '', gradient: false })
function setWpType(type: string) {
const prev = s.wallpaperType
s.wallpaperType = type
if (!initDone.value) return
if (type === 'theme') go.saveWallpaperType('theme', s.theme)
else if (type === 'bing' && prev !== 'bing') go.enableBing()
}
async function onPickSolid() {
const c = await go.pickSolidColor()
if (c) { currentColor.c1 = c; currentColor.c2 = ''; currentColor.gradient = false }
}
async function onPickGradient() {
const res = await go.pickGradientColor()
if (res) {
const parts = res.split(',')
currentColor.c1 = parts[0]; currentColor.c2 = parts[1] || ''; currentColor.gradient = true
}
}
function onSaveColor() {
if (!currentColor.c1) return
go.addSavedColor(currentColor.c1, currentColor.c2, currentColor.gradient)
s.savedColors.push({ color1: currentColor.c1, color2: currentColor.c2, gradient: currentColor.gradient })
}
function onApplySavedColor(idx: number) {
go.applySavedColor(idx)
const sc = s.savedColors[idx]
currentColor.c1 = sc.color1; currentColor.c2 = sc.color2; currentColor.gradient = sc.gradient
}
async function onRemoveSavedColor(idx: number) {
await go.removeSavedColor(idx)
s.savedColors.splice(idx, 1)
}
onMounted(async () => {
const raw = await go.loadAllSettings()
const data = JSON.parse(raw) as SettingsData
if (data.lightTheme) document.documentElement.className = 'light'
Object.assign(s, { ...data, citiesByProv: data.citiesByProv || {}, provinces: data.provinces || [], themes: data.themes || [], savedColors: data.savedColors || [] })
if (data.color1) { currentColor.c1 = data.color1; currentColor.c2 = data.color2 || ''; currentColor.gradient = data.colorGradient || false }
setTimeout(() => {
const el = document.documentElement
if (go.resizeToFit) go.resizeToFit(el.scrollWidth, el.scrollHeight + 8)
}, 100)
initDone.value = true
})
</script>
@@ -0,0 +1,21 @@
<template>
<div class="mb-3">
<div class="text-[10px] font-semibold text-[var(--text-weak)] uppercase tracking-[1.5px] mb-1 pl-0.5">布局</div>
<div class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg">
<div class="flex justify-between items-center px-3.5 py-2">
<div class="text-xs font-medium text-[var(--text-muted)]">信息布局</div>
<select v-model="s.layout" @change="go.saveLayout(s.layout)"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-0.5 px-1.5 outline-none min-w-[80px] max-w-[160px] focus:border-[var(--input-border-focus)]">
<option value="single">合并卡片</option>
<option value="multi">独立卡片</option>
</select>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { go } from '../composables/useGoBridge'
import type { SettingsData } from '@shared/types'
defineProps<{ s: SettingsData }>()
</script>
@@ -0,0 +1,111 @@
<template>
<div class="mb-3">
<div class="text-[10px] font-semibold text-[var(--text-weak)] uppercase tracking-[1.5px] mb-1 pl-0.5">个性化</div>
<div class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg">
<div class="flex justify-between items-center px-3.5 py-2">
<div class="text-xs font-medium text-[var(--text-muted)]">我的星座</div>
<select v-model="s.zodiac" @change="go.saveZodiac(s.zodiac)"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-0.5 px-1.5 outline-none min-w-[80px] max-w-[160px] focus:border-[var(--input-border-focus)]">
<option v-for="z in zodiacs" :key="z" :value="z">{{ z }}</option>
</select>
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div><div class="text-xs font-medium text-[var(--text-muted)]">知识关键字</div><div class="text-[10px] text-[var(--text-weak)] mt-px">AI 将根据关键字生成知识小卡片</div></div>
<input type="text" v-model="s.knowledgeKeyword" @input="debounced(go.saveKnowledgeKeyword, s.knowledgeKeyword)"
placeholder="如: 历史、科学、冷知识"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-1 px-2 outline-none w-[140px] focus:border-[var(--input-border-focus)]">
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div><div class="text-xs font-medium text-[var(--text-muted)]">知识提示词</div><div class="text-[10px] text-[var(--text-weak)] mt-px">补充风格或方向,系统会保证内容密度</div></div>
<input type="text" v-model="s.knowledgePrompt" @input="debounced(go.saveKnowledgePrompt, s.knowledgePrompt)"
placeholder="如: 偏实践、给出判断标准"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-1 px-2 outline-none w-[140px] focus:border-[var(--input-border-focus)]">
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)] relative">
<div class="text-xs font-medium text-[var(--text-muted)]">天气城市</div>
<div class="city-picker" :class="{ open: cityOpen }" tabindex="0" @click="cityOpen = !cityOpen">
<span class="text-[var(--text)] text-[11px]">{{ cityDisplay }}</span>
<span class="absolute right-1.5 top-1/2 -translate-y-1/2 text-[var(--text-weak)] text-[10px] pointer-events-none">▾</span>
<div class="city-panel">
<div class="city-col">
<div v-for="p in s.provinces" :key="p" :class="{ active: p === activeProv }" @click.stop="activeProv = p">{{ p }}</div>
</div>
<div class="city-col">
<div v-for="c in currentCities" :key="c.id" :class="{ active: c.id === s.city }" @click.stop="onCitySelect(c)">{{ c.name }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { go } from '../composables/useGoBridge'
import { debounced } from '../composables/useDebounce'
import type { SettingsData } from '@shared/types'
const props = defineProps<{ s: SettingsData }>()
const zodiacs = ['白羊座','金牛座','双子座','巨蟹座','狮子座','处女座','天秤座','天蝎座','射手座','摩羯座','水瓶座','双鱼座']
const cityOpen = ref(false)
const activeProv = ref('')
const selectedCityName = ref('')
const currentCities = computed(() => {
if (!activeProv.value) return []
return (props.s.citiesByProv as any)[activeProv.value] || []
})
const cityDisplay = computed(() => {
if (activeProv.value && selectedCityName.value) return `${activeProv.value} · ${selectedCityName.value}`
return '未选择'
})
function onCitySelect(c: { id: string; name: string }) {
props.s.city = c.id
selectedCityName.value = c.name
cityOpen.value = false
go.saveCity(c.id)
}
function onDocClick(e: MouseEvent) {
const picker = document.querySelector('.city-picker')
if (picker && !picker.contains(e.target as Node)) cityOpen.value = false
}
onMounted(() => {
document.addEventListener('click', onDocClick)
// Find initial city
for (const p of Object.keys(props.s.citiesByProv)) {
for (const c of (props.s.citiesByProv as any)[p]) {
if (c.id === props.s.city) { activeProv.value = p; selectedCityName.value = c.name; break }
}
if (activeProv.value) break
}
})
onUnmounted(() => document.removeEventListener('click', onDocClick))
</script>
<style scoped>
.city-picker {
position: relative; background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; padding: 4px 24px 4px 8px; cursor: pointer;
font-size: 11px; min-width: 140px; max-width: 180px;
}
.city-picker:focus, .city-picker.open { border-color: var(--input-border-focus); }
.city-panel {
display: none; position: absolute; right: -1px; bottom: calc(100% + 4px);
background: var(--option-bg); border: 1px solid var(--input-border);
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25);
z-index: 1000; width: 260px; height: 200px; overflow: hidden;
}
.city-picker.open .city-panel { display: flex; }
.city-col { flex: 1; overflow-y: auto; border-right: 1px solid var(--card-divider); }
.city-col:last-child { border-right: none; }
.city-col div {
padding: 5px 10px; font-size: 11px; color: var(--text); cursor: pointer; white-space: nowrap;
}
.city-col div:hover { background: var(--input-bg); }
.city-col div.active { color: var(--accent); font-weight: 600; }
</style>
@@ -0,0 +1,61 @@
<template>
<div class="mb-3">
<div class="text-[10px] font-semibold text-[var(--text-weak)] uppercase tracking-[1.5px] mb-1 pl-0.5">相册</div>
<div class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg">
<div class="flex justify-between items-center px-3.5 py-2">
<div><div class="text-xs font-medium text-[var(--text-muted)]">相册展示</div></div>
<ToggleSwitch v-model="s.photoCard" />
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div><div class="text-xs font-medium text-[var(--text-muted)]">电子相册模式</div><div class="text-[10px] text-[var(--text-weak)] mt-px">照片铺满整个壁纸</div></div>
<ToggleSwitch v-model="s.photoFrameMode" />
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div class="text-[10px] text-[var(--text-weak)]">{{ s.photoDir || '未选择目录' }}</div>
<div class="flex gap-1">
<button class="btn" @click="onPickDir">选择目录</button>
<button v-if="s.photoDir" class="btn-sm" @click="onClearDir">清除</button>
</div>
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div class="text-xs font-medium text-[var(--text-muted)]">切换间隔</div>
<select v-model="s.photoInterval" @change="go.savePhotoInterval(Number(s.photoInterval))"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-0.5 px-1.5 outline-none min-w-[80px] max-w-[160px] focus:border-[var(--input-border-focus)]">
<option v-for="v in [5,10,15,20,30,60]" :key="v" :value="v">{{ v }} 秒</option>
</select>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { go } from '../composables/useGoBridge'
import ToggleSwitch from './ToggleSwitch.vue'
import type { SettingsData } from '@shared/types'
const props = defineProps<{ s: SettingsData }>()
async function onPickDir() {
const dir = await go.pickPhotoDir()
if (dir) props.s.photoDir = dir
}
async function onClearDir() {
await go.clearPhotoDir()
props.s.photoDir = ''
}
</script>
<style scoped>
.btn {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 4px 10px;
font-family: inherit; cursor: pointer; transition: background 0.15s; white-space: nowrap;
}
.btn:hover { background: var(--card-border); }
.btn-sm {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 10px; padding: 3px 8px;
font-family: inherit; cursor: pointer; white-space: nowrap;
}
.btn-sm:hover { background: var(--card-border); }
</style>
@@ -0,0 +1,47 @@
<template>
<div class="mb-3">
<div class="text-[10px] font-semibold text-[var(--text-weak)] uppercase tracking-[1.5px] mb-1 pl-0.5">显示控制</div>
<div class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg">
<div v-for="t in toggles" :key="t.key"
class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)] first:border-t-0"
:class="{ 'pl-8': t.sub }">
<div>
<div class="text-xs font-medium text-[var(--text-muted)]">{{ t.label }}</div>
<div v-if="t.desc" class="text-[10px] text-[var(--text-weak)] mt-px">{{ t.desc }}</div>
</div>
<ToggleSwitch v-model="(s as any)[t.key]" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { watch } from 'vue'
import ToggleSwitch from './ToggleSwitch.vue'
import { go } from '../composables/useGoBridge'
import type { SettingsData } from '@shared/types'
const props = defineProps<{ s: SettingsData }>()
const toggles: { key: keyof SettingsData; label: string; desc?: string; sub?: boolean }[] = [
{ key: 'autoStart', label: '开机启动', desc: '系统启动时自动运行' },
{ key: 'wallpaper', label: '显示壁纸' },
{ key: 'time', label: '时间日期' },
{ key: 'showSeconds', label: '显示秒', sub: true },
{ key: 'weather', label: '天气信息' },
{ key: 'zodiacCard', label: '星座运势' },
{ key: 'knowledgeCard', label: '知识卡片' },
{ key: 'ainewsCard', label: 'AI 资讯' },
]
const toggleKeys = toggles.map(t => t.key)
watch(
() => toggleKeys.map(k => (props.s as any)[k]),
() => {
const data: Record<string, boolean> = {}
for (const k of toggleKeys) data[k] = (props.s as any)[k]
go.saveToggles(data)
}
)
</script>
@@ -0,0 +1,28 @@
<template>
<label class="switch">
<input type="checkbox" :checked="modelValue" @change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)">
<span class="track"><span class="thumb"></span></span>
</label>
</template>
<script setup lang="ts">
defineProps<{ modelValue: boolean }>()
defineEmits<{ 'update:modelValue': [value: boolean] }>()
</script>
<style scoped>
.switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
.switch input { display: none; }
.switch .track {
position: absolute; inset: 0;
background: var(--toggle-track);
border-radius: 10px; cursor: pointer; transition: background 0.2s;
}
.switch .thumb {
position: absolute; width: 14px; height: 14px; top: 3px; left: 3px;
background: var(--toggle-thumb);
border-radius: 50%; transition: all 0.2s; pointer-events: none;
}
.switch input:checked + .track { background: var(--accent); }
.switch input:checked + .track .thumb { transform: translateX(16px); background: var(--text-strong); }
</style>
@@ -0,0 +1,198 @@
<template>
<div class="mb-3">
<div class="text-[10px] font-semibold text-[var(--text-weak)] uppercase tracking-[1.5px] mb-1 pl-0.5">壁纸</div>
<div class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg">
<!-- 类型选择 -->
<div class="flex justify-between items-center px-3.5 py-2">
<div class="text-xs font-medium text-[var(--text-muted)]">类型选择</div>
<div class="flex gap-1">
<button v-for="t in wpTypes" :key="t.value" @click="$emit('set-wp-type', t.value)"
:class="s.wallpaperType === t.value
? 'bg-[var(--accent)] border-[var(--accent)] text-[var(--text-strong)]'
: 'bg-[var(--input-bg)] border-[var(--input-border)] text-[var(--text)] hover:bg-[var(--card-border)]'"
class="border rounded-md text-[11px] py-1 px-2.5 font-[inherit] cursor-pointer whitespace-nowrap transition-colors">{{ t.label }}</button>
</div>
</div>
</div>
<!-- 主题 -->
<div v-if="s.wallpaperType === 'theme'" class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg mt-1.5">
<div class="flex justify-between items-center px-3.5 py-2">
<div class="text-xs font-medium text-[var(--text-muted)]">选择主题</div>
<select v-model="s.theme" @change="go.saveWallpaperType('theme', s.theme)"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-0.5 px-1.5 outline-none min-w-[80px] max-w-[160px] focus:border-[var(--input-border-focus)]">
<option v-for="t in s.themes" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
</div>
<div v-if="s.theme === 'text'" class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div class="text-xs font-medium text-[var(--text-muted)]">自定义文字</div>
<input type="text" v-model="s.wallpaperText" @input="debounced(go.saveWallpaperText, s.wallpaperText)"
placeholder="输入显示文字"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-1 px-2 outline-none w-[140px] focus:border-[var(--input-border-focus)]">
</div>
</div>
<!-- 本地图片 -->
<div v-if="s.wallpaperType === 'image'" class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg mt-1.5">
<div class="flex justify-between items-center px-3.5 py-2">
<div class="text-[10px] text-[var(--text-weak)] truncate mr-2">{{ s.imagePath || '未选择图片' }}</div>
<button class="btn" @click="onPickImage">选择图片</button>
</div>
</div>
<!-- Bing -->
<div v-if="s.wallpaperType === 'bing'" class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg mt-1.5">
<div class="flex justify-between items-center px-3.5 py-2">
<div class="flex items-center gap-2 min-w-0">
<img v-if="bing.url" :src="bingThumb" class="w-16 h-10 object-cover rounded flex-shrink-0">
<div class="min-w-0">
<div class="text-xs font-medium text-[var(--text-muted)] truncate">{{ bing.copyright || 'Bing 每日壁纸' }}</div>
<div class="text-[10px] text-[var(--text-weak)]">{{ bing.idx > 0 ? `${bing.idx} / ${bing.total}` : '' }}</div>
</div>
</div>
<div class="flex gap-1 flex-shrink-0">
<button class="btn-sm" @click="onBingPrev">&#9664;</button>
<button class="btn-sm" @click="onBingNext">&#9654;</button>
<button class="btn-sm" @click="onBingFav">{{ bing.fav ? '★' : '☆' }}</button>
</div>
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div><div class="text-xs font-medium text-[var(--text-muted)]">定时切换</div><div class="text-[10px] text-[var(--text-weak)] mt-px">每小时自动切换壁纸</div></div>
<ToggleSwitch v-model="s.bingAutoRefresh" />
</div>
<!-- 收藏列表 -->
<div v-if="bingFavs.length" class="px-3.5 py-2 border-t border-[var(--card-divider)]">
<div class="text-[10px] text-[var(--text-weak)] mb-1.5">收藏列表</div>
<div class="grid grid-cols-4 gap-1.5">
<div v-for="(f, i) in bingFavs" :key="i" class="cursor-pointer rounded overflow-hidden hover:opacity-80" @click="onBingSetByIdx(f.idx)">
<img :src="f.thumb" class="w-full h-10 object-cover rounded">
</div>
</div>
</div>
</div>
<!-- 纯色/渐变 -->
<div v-if="s.wallpaperType === 'color'" class="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg mt-1.5">
<div class="flex justify-between items-center px-3.5 py-2">
<div class="text-xs font-medium text-[var(--text-muted)]">选择颜色</div>
<div class="flex gap-1">
<button class="btn" @click="$emit('pick-solid')">纯色</button>
<button class="btn" @click="$emit('pick-gradient')">渐变</button>
</div>
</div>
<div v-if="currentColor.c1" class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div class="flex items-center gap-1.5">
<span class="inline-block w-6 h-4 rounded-sm border border-[var(--input-border)]" :style="swatchStyle"></span>
<span class="text-[10px] text-[var(--text-weak)]">当前颜色</span>
</div>
<button class="btn" @click="$emit('save-color')">收藏</button>
</div>
</div>
<!-- 已收藏颜色 -->
<div v-if="s.savedColors.length && s.wallpaperType === 'color'" class="flex flex-wrap gap-1.5 mt-1.5 px-0.5">
<div v-for="(c, i) in s.savedColors" :key="i" class="color-swatch" :style="swatchOf(c)"
@click="$emit('apply-color', i)" @contextmenu.prevent="$emit('remove-color', i)">
<span class="del">&times;</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { go } from '../composables/useGoBridge'
import { debounced } from '../composables/useDebounce'
import ToggleSwitch from './ToggleSwitch.vue'
import type { SettingsData, BingState } from '@shared/types'
const props = defineProps<{
s: SettingsData
currentColor: { c1: string; c2: string; gradient: boolean }
}>()
defineEmits<{
'set-wp-type': [type: string]
'pick-solid': []
'pick-gradient': []
'save-color': []
'apply-color': [idx: number]
'remove-color': [idx: number]
}>()
const wpTypes = [
{ value: 'theme', label: '主题' },
{ value: 'image', label: '本地图片' },
{ value: 'bing', label: 'Bing' },
{ value: 'color', label: '纯色/渐变' },
]
// Bing
const bing = ref<BingState>({ date: '', title: '', copyright: '', url: '', fav: false, idx: 0, total: 0 })
const bingThumb = ref('')
const bingFavs = ref<{ idx: number; thumb: string }[]>([])
async function loadBingInfo() {
const raw = await go.getBingInfo()
const st = JSON.parse(raw) as BingState
bing.value = st
if (st.url) bingThumb.value = st.url.replace('_1920x1080', '_400x240')
}
async function onBingPrev() { bing.value = JSON.parse(await go.bingPrev()) }
async function onBingNext() { bing.value = JSON.parse(await go.bingNext()) }
async function onBingFav() { bing.value = JSON.parse(await go.bingToggleFavorite()); await loadBingFavs() }
async function onBingSetByIdx(idx: number) { bing.value = JSON.parse(await go.bingSetByIdx(idx)) }
async function loadBingFavs() {
const raw = await go.getBingFavorites()
const list = JSON.parse(raw) as string[]
bingFavs.value = await Promise.all(list.map(async f => ({
idx: parseInt(f.match(/(\d+)/)?.[1] || '0', 10),
thumb: await go.bingThumbDataURI(f)
})))
}
watch(() => props.s.bingAutoRefresh, v => go.saveBingAutoRefresh(v))
watch(() => props.s.wallpaperType, t => {
if (t === 'bing') { loadBingInfo(); loadBingFavs() }
}, { immediate: true })
async function onPickImage() {
const p = await go.pickLocalImage()
if (p) props.s.imagePath = p
}
// Color swatches
function gradientBg(c1: string, c2: string, gradient: boolean) {
return { background: gradient && c2 ? `linear-gradient(135deg,${c1},${c2})` : c1 }
}
const swatchStyle = computed(() => props.currentColor.c1 ? gradientBg(props.currentColor.c1, props.currentColor.c2, props.currentColor.gradient) : {})
function swatchOf(c: { color1: string; color2: string; gradient: boolean }) { return gradientBg(c.color1, c.color2, c.gradient) }
</script>
<style scoped>
.btn {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 4px 10px;
font-family: inherit; cursor: pointer; transition: background 0.15s; white-space: nowrap;
}
.btn:hover { background: var(--card-border); }
.btn-sm {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 10px; padding: 3px 8px;
font-family: inherit; cursor: pointer; white-space: nowrap;
}
.btn-sm:hover { background: var(--card-border); }
.color-swatch {
width: 28px; height: 28px; border-radius: 6px; cursor: pointer;
border: 2px solid transparent; transition: border-color 0.15s; position: relative;
}
.color-swatch:hover { border-color: var(--accent); }
.color-swatch .del {
display: none; position: absolute; top: -6px; right: -6px;
width: 14px; height: 14px; border-radius: 50%;
background: #e53e3e; color: #fff; font-size: 9px; line-height: 14px;
text-align: center; cursor: pointer;
}
.color-swatch:hover .del { display: block; }
</style>
@@ -0,0 +1,5 @@
const timers = new Map<string, ReturnType<typeof setTimeout>>()
export function debounced(fn: (v: string) => void, val: string, ms = 500) {
clearTimeout(timers.get(fn.name))
timers.set(fn.name, setTimeout(() => fn(val), ms))
}
@@ -0,0 +1,32 @@
const w = window as any
export const go = {
loadAllSettings: (): Promise<string> => w.loadAllSettings(),
saveToggles: (data: Record<string, boolean>) => w.saveToggles(JSON.stringify(data)),
saveLayout: (layout: string) => w.saveLayout(layout),
saveZodiac: (zodiac: string) => w.saveZodiac(zodiac),
saveCity: (cityId: string) => w.saveCity(cityId),
saveWallpaperType: (type: string, theme: string) => w.saveWallpaperType(type, theme),
pickLocalImage: (): Promise<string> => w.pickLocalImage(),
enableBing: () => w.enableBing(),
bingPrev: (): Promise<string> => w.bingPrev(),
bingNext: (): Promise<string> => w.bingNext(),
bingToggleFavorite: (): Promise<string> => w.bingToggleFavorite(),
getBingInfo: (): Promise<string> => w.getBingInfo(),
saveBingAutoRefresh: (val: boolean) => w.saveBingAutoRefresh(val),
getBingFavorites: (): Promise<string> => w.getBingFavorites(),
bingSetByIdx: (idx: number): Promise<string> => w.bingSetByIdx(idx),
bingThumbDataURI: (filename: string): Promise<string> => w.bingThumbDataURI(filename),
pickSolidColor: (): Promise<string> => w.pickSolidColor(),
pickGradientColor: (): Promise<string> => w.pickGradientColor(),
addSavedColor: (c1: string, c2: string, gradient: boolean) => w.addSavedColor(c1, c2, gradient),
removeSavedColor: (idx: number): Promise<string> => w.removeSavedColor(idx),
applySavedColor: (idx: number) => w.applySavedColor(idx),
saveWallpaperText: (text: string) => w.saveWallpaperText(text),
saveKnowledgeKeyword: (keyword: string) => w.saveKnowledgeKeyword(keyword),
saveKnowledgePrompt: (prompt: string) => w.saveKnowledgePrompt(prompt),
pickPhotoDir: (): Promise<string> => w.pickPhotoDir(),
clearPhotoDir: () => w.clearPhotoDir(),
savePhotoInterval: (val: number) => w.savePhotoInterval(val),
resizeToFit: (w: number, h: number) => w.resizeToFit(w, h),
}
+6
View File
@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import '../tailwind.css'
import './settings.css'
createApp(App).mount('#app')
+48
View File
@@ -0,0 +1,48 @@
:root {
--bg: #0f0f1a;
--card-bg: rgba(255,255,255,0.04);
--card-border: rgba(255,255,255,0.06);
--card-divider: rgba(255,255,255,0.04);
--text: #e0e0e0;
--text-strong: #fff;
--text-weak: rgba(255,255,255,0.25);
--text-muted: rgba(255,255,255,0.5);
--input-bg: rgba(255,255,255,0.08);
--input-border: rgba(255,255,255,0.1);
--input-border-focus: #4f8cff;
--accent: #4f8cff;
--toggle-track: rgba(255,255,255,0.1);
--toggle-thumb: rgba(255,255,255,0.5);
--option-bg: #1a1a2e;
--footer-color: rgba(255,255,255,0.3);
}
.light {
--bg: #f5f5f5;
--card-bg: rgba(0,0,0,0.03);
--card-border: rgba(0,0,0,0.08);
--card-divider: rgba(0,0,0,0.05);
--text: #333;
--text-strong: #111;
--text-weak: rgba(0,0,0,0.35);
--text-muted: rgba(0,0,0,0.55);
--input-bg: rgba(0,0,0,0.04);
--input-border: rgba(0,0,0,0.12);
--input-border-focus: #2b6cb0;
--accent: #2b6cb0;
--toggle-track: rgba(0,0,0,0.12);
--toggle-thumb: rgba(0,0,0,0.45);
--option-bg: #fff;
--footer-color: rgba(0,0,0,0.35);
}
body {
font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
background: var(--bg); color: var(--text);
-webkit-font-smoothing: antialiased;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
select option { background: var(--option-bg); color: var(--text); }
.light ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); }
.light ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.22); }