页面美化
This commit is contained in:
+48
@@ -3,10 +3,13 @@ import { ref, onMounted } from 'vue'
|
|||||||
import PromptEditor from './components/PromptEditor.vue'
|
import PromptEditor from './components/PromptEditor.vue'
|
||||||
import PromptManager from './components/PromptManager.vue'
|
import PromptManager from './components/PromptManager.vue'
|
||||||
import PresetManager from './components/PresetManager.vue'
|
import PresetManager from './components/PresetManager.vue'
|
||||||
|
import BackgroundCanvas from './components/BackgroundCanvas.vue'
|
||||||
|
import DevtoolsBanner from './components/DevtoolsBanner.vue'
|
||||||
import { usePromptStore } from './stores/promptStore'
|
import { usePromptStore } from './stores/promptStore'
|
||||||
|
|
||||||
const currentView = ref<'editor' | 'manager' | 'presets'>('editor')
|
const currentView = ref<'editor' | 'manager' | 'presets'>('editor')
|
||||||
const isDark = ref(false)
|
const isDark = ref(false)
|
||||||
|
const showBackground = ref(true)
|
||||||
const store = usePromptStore()
|
const store = usePromptStore()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -16,6 +19,8 @@ onMounted(() => {
|
|||||||
updateTheme()
|
updateTheme()
|
||||||
// 初始化词库与编辑器状态(仅一次)
|
// 初始化词库与编辑器状态(仅一次)
|
||||||
store.initialize()
|
store.initialize()
|
||||||
|
const bg = localStorage.getItem('bg.enabled')
|
||||||
|
showBackground.value = bg === null ? true : bg === 'on'
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
@@ -31,10 +36,17 @@ function updateTheme() {
|
|||||||
function switchView(view: 'editor' | 'manager' | 'presets') {
|
function switchView(view: 'editor' | 'manager' | 'presets') {
|
||||||
currentView.value = view
|
currentView.value = view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleBackground() {
|
||||||
|
showBackground.value = !showBackground.value
|
||||||
|
localStorage.setItem('bg.enabled', showBackground.value ? 'on' : 'off')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container" :class="{ dark: isDark }">
|
<div class="app-container" :class="{ dark: isDark }">
|
||||||
|
<BackgroundCanvas v-if="showBackground" />
|
||||||
|
<DevtoolsBanner />
|
||||||
<!-- 顶部导航栏 -->
|
<!-- 顶部导航栏 -->
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
@@ -100,6 +112,18 @@ function switchView(view: 'editor' | 'manager' | 'presets') {
|
|||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="bg-toggle" :class="{ active: showBackground }" @click="toggleBackground" title="背景开关">
|
||||||
|
<svg v-if="showBackground" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="6" cy="12" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="12" cy="9" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="18" cy="13" r="1.5" fill="currentColor"/>
|
||||||
|
<path d="M4 16c4-2 8-2 12 0" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 5l14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<circle cx="12" cy="12" r="6" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -277,6 +301,30 @@ body {
|
|||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-toggle:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-toggle.active {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
/* 主要内容区域 */
|
/* 主要内容区域 */
|
||||||
.app-main {
|
.app-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<canvas ref="canvas" class="bg-canvas"></canvas>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
|
let ctx: CanvasRenderingContext2D | null = null
|
||||||
|
let width = 0
|
||||||
|
let height = 0
|
||||||
|
let dpr = 1
|
||||||
|
let raf = 0
|
||||||
|
let running = true
|
||||||
|
|
||||||
|
type Particle = {
|
||||||
|
baseX: number
|
||||||
|
baseY: number
|
||||||
|
r: number
|
||||||
|
vx: number
|
||||||
|
vy: number
|
||||||
|
speed: number
|
||||||
|
phase: number
|
||||||
|
depth: number
|
||||||
|
alpha: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let particles: Particle[] = []
|
||||||
|
let mouseX = 0
|
||||||
|
let mouseY = 0
|
||||||
|
let lastThemeKey = ''
|
||||||
|
|
||||||
|
function parseColor(c: string): [number, number, number] {
|
||||||
|
const s = c.trim()
|
||||||
|
if (s.startsWith('#')) {
|
||||||
|
const v = s.slice(1)
|
||||||
|
const n = v.length === 3 ? v.split('').map(ch => ch + ch).join('') : v
|
||||||
|
const r = parseInt(n.slice(0, 2), 16)
|
||||||
|
const g = parseInt(n.slice(2, 4), 16)
|
||||||
|
const b = parseInt(n.slice(4, 6), 16)
|
||||||
|
return [r, g, b]
|
||||||
|
}
|
||||||
|
const m = s.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i)
|
||||||
|
if (m) return [Number(m[1]), Number(m[2]), Number(m[3])]
|
||||||
|
return [219, 234, 254]
|
||||||
|
}
|
||||||
|
|
||||||
|
function mix(a: [number, number, number], b: [number, number, number], t: number): [number, number, number] {
|
||||||
|
return [
|
||||||
|
Math.round(a[0] * (1 - t) + b[0] * t),
|
||||||
|
Math.round(a[1] * (1 - t) + b[1] * t),
|
||||||
|
Math.round(a[2] * (1 - t) + b[2] * t)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function luminance([r, g, b]: [number, number, number]) {
|
||||||
|
const sr = r / 255
|
||||||
|
const sg = g / 255
|
||||||
|
const sb = b / 255
|
||||||
|
return 0.2126 * sr + 0.7152 * sg + 0.0722 * sb
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPalette() {
|
||||||
|
const cs = getComputedStyle(document.documentElement)
|
||||||
|
const accent = cs.getPropertyValue('--color-accent-light') || '#dbeafe'
|
||||||
|
const bg = cs.getPropertyValue('--color-bg-primary') || '#ffffff'
|
||||||
|
const a = parseColor(accent)
|
||||||
|
const b = parseColor(bg)
|
||||||
|
const mixed = mix(b, a, 3)
|
||||||
|
const key = `${mixed.join(',')}`
|
||||||
|
const lum = luminance(b)
|
||||||
|
const alpha = lum > 0.7 ? 0.42 : 0.28
|
||||||
|
return { rgb: mixed, key, alpha }
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
if (!canvas.value) return
|
||||||
|
dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||||||
|
width = window.innerWidth
|
||||||
|
height = window.innerHeight
|
||||||
|
canvas.value.width = Math.floor(width * dpr)
|
||||||
|
canvas.value.height = Math.floor(height * dpr)
|
||||||
|
canvas.value.style.width = `${width}px`
|
||||||
|
canvas.value.style.height = `${height}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
function initParticles() {
|
||||||
|
const area = width * height
|
||||||
|
const count = Math.max(80, Math.floor(area / (60 * 60)))
|
||||||
|
particles = Array.from({ length: count }).map(() => {
|
||||||
|
const depth = 0.35 + Math.random() * 0.65
|
||||||
|
const r = 0.9 + Math.random() * 1.6
|
||||||
|
const speed = 0.4 + Math.random() * 0.8
|
||||||
|
const vx = (Math.random() - 0.5) * 0.6
|
||||||
|
const vy = (Math.random() - 0.5) * 0.6
|
||||||
|
return {
|
||||||
|
baseX: Math.random() * width,
|
||||||
|
baseY: Math.random() * height,
|
||||||
|
r,
|
||||||
|
vx,
|
||||||
|
vy,
|
||||||
|
speed,
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
depth,
|
||||||
|
alpha: 0.7 + Math.random() * 0.3
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!ctx) return
|
||||||
|
const pal = getPalette()
|
||||||
|
if (pal.key !== lastThemeKey) lastThemeKey = pal.key
|
||||||
|
ctx.clearRect(0, 0, width * dpr, height * dpr)
|
||||||
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
|
const [r, g, b] = pal.rgb
|
||||||
|
const t = performance.now() / 1000
|
||||||
|
const mx = (mouseX / width - 0.5)
|
||||||
|
const my = (mouseY / height - 0.5)
|
||||||
|
const offsetX = mx * 160
|
||||||
|
const offsetY = my * 100
|
||||||
|
for (const p of particles) {
|
||||||
|
const x = (p.baseX + Math.sin(t * p.speed + p.phase) * p.vx * 50 + offsetX * p.depth) * dpr
|
||||||
|
const y = (p.baseY + Math.cos(t * p.speed + p.phase) * p.vy * 50 + offsetY * p.depth) * dpr
|
||||||
|
const a = pal.alpha * p.alpha * (0.85 + 0.15 * Math.sin(t * (p.speed * 0.6) + p.phase))
|
||||||
|
ctx.fillStyle = `rgba(${r},${g},${b},${a})`
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, y, Math.max(0.5, p.r * dpr), 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastFrame = 0
|
||||||
|
|
||||||
|
function loop(ts: number) {
|
||||||
|
if (!running) return
|
||||||
|
if (ts - lastFrame > 33) {
|
||||||
|
lastFrame = ts
|
||||||
|
draw()
|
||||||
|
}
|
||||||
|
raf = requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
mouseX = e.clientX
|
||||||
|
mouseY = e.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVisibilityChange() {
|
||||||
|
running = !document.hidden
|
||||||
|
if (running) {
|
||||||
|
lastFrame = 0
|
||||||
|
raf = requestAnimationFrame(loop)
|
||||||
|
} else {
|
||||||
|
cancelAnimationFrame(raf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!canvas.value) return
|
||||||
|
ctx = canvas.value.getContext('2d')
|
||||||
|
resize()
|
||||||
|
initParticles()
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
resize()
|
||||||
|
initParticles()
|
||||||
|
})
|
||||||
|
window.addEventListener('mousemove', onMouseMove, { passive: true })
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||||
|
raf = requestAnimationFrame(loop)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
running = false
|
||||||
|
cancelAnimationFrame(raf)
|
||||||
|
window.removeEventListener('mousemove', onMouseMove)
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-canvas {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
let running = false
|
||||||
|
let watcher: number | null = null
|
||||||
|
let anim: number | null = null
|
||||||
|
let frame = 0
|
||||||
|
|
||||||
|
const repoUrl = 'https://github.com/kjqwer/prompt'
|
||||||
|
|
||||||
|
function waveFrame(f: number, width: number) {
|
||||||
|
const chars = ['▁','▂','▃','▄','▅','▆','▇','█']
|
||||||
|
let fmt = ''
|
||||||
|
const styles: string[] = []
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
const t = (f * 0.35) + (i * 0.25)
|
||||||
|
const y = Math.sin(t)
|
||||||
|
const idx = Math.max(0, Math.min(chars.length - 1, Math.floor(((y + 1) / 2) * (chars.length - 1))))
|
||||||
|
const hue = (i * 12 + f * 9) % 360
|
||||||
|
fmt += '%c' + chars[idx]
|
||||||
|
styles.push(`color:hsl(${hue} 90% 60%); font-weight:900; font-size:16px;`)
|
||||||
|
}
|
||||||
|
console.log(fmt, ...styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFrame() {
|
||||||
|
console.clear()
|
||||||
|
waveFrame(frame, 36)
|
||||||
|
const title = '%c✦ Prompt · 提示词编辑器'
|
||||||
|
const titleStyle = 'padding:12px 18px; font-size:16px; font-weight:800; color:#fff; background:linear-gradient(90deg,#0ea5e9,#22d3ee,#a78bfa,#f472b6,#fb7185); border-radius:10px; text-shadow:0 1px 1px rgba(0,0,0,.2);'
|
||||||
|
console.log(title, titleStyle)
|
||||||
|
const linkLabel = '%cGitHub ↗ ' + repoUrl
|
||||||
|
const linkStyle = 'color:#93c5fd; font-size:13px; font-weight:700;'
|
||||||
|
console.log(linkLabel, linkStyle)
|
||||||
|
const badge = '%c Inspire · Create · Play '
|
||||||
|
const badgeStyle = 'padding:6px 12px; font-size:12px; color:#111827; background:linear-gradient(90deg,#fde68a,#86efac,#93c5fd,#c4b5fd,#fbcfe8); border-radius:999px;'
|
||||||
|
console.log(badge, badgeStyle)
|
||||||
|
frame++
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAnim() {
|
||||||
|
if (running) return
|
||||||
|
running = true
|
||||||
|
if (anim) clearInterval(anim)
|
||||||
|
anim = window.setInterval(renderFrame, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAnim() {
|
||||||
|
if (!running) return
|
||||||
|
running = false
|
||||||
|
if (anim) {
|
||||||
|
clearInterval(anim)
|
||||||
|
anim = null
|
||||||
|
}
|
||||||
|
console.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDevtoolsOpen() {
|
||||||
|
const threshold = 160
|
||||||
|
const w = Math.abs(window.outerWidth - window.innerWidth)
|
||||||
|
const h = Math.abs(window.outerHeight - window.innerHeight)
|
||||||
|
return w > threshold || h > threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
watcher = window.setInterval(() => {
|
||||||
|
if (isDevtoolsOpen()) startAnim()
|
||||||
|
else stopAnim()
|
||||||
|
}, 600)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (watcher) {
|
||||||
|
clearInterval(watcher)
|
||||||
|
watcher = null
|
||||||
|
}
|
||||||
|
stopAnim()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template></template>
|
||||||
@@ -97,6 +97,7 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.dataset = deepClone(baseline!);
|
this.dataset = deepClone(baseline!);
|
||||||
|
this.promptText = '1girl, solo, long hair, blue eyes, smile, looking_at_viewer, upper_body, outdoors, sunset';
|
||||||
}
|
}
|
||||||
// 若无恢复语言,则按数据集进行推断
|
// 若无恢复语言,则按数据集进行推断
|
||||||
if (!this.selectedLang) {
|
if (!this.selectedLang) {
|
||||||
|
|||||||
Reference in New Issue
Block a user