基本功能实现

This commit is contained in:
2025-06-25 23:53:40 +08:00
parent 9e4aa2c725
commit d70b962ef5
21 changed files with 2318 additions and 54 deletions

View File

@@ -1,30 +1,16 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
// App.vue is the main component that uses the router
</script>
<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
<router-view />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="clock-config">
<h2>Clock Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Time Format">
<el-select v-model="localConfig.format" placeholder="Select format">
<el-option label="HH:mm:ss (24-hour)" value="HH:mm:ss" />
<el-option label="HH:mm (24-hour)" value="HH:mm" />
<el-option label="hh:mm:ss A (12-hour)" value="hh:mm:ss A" />
<el-option label="hh:mm A (12-hour)" value="hh:mm A" />
</el-select>
</el-form-item>
<el-form-item label="Show Seconds">
<el-switch v-model="localConfig.showSeconds" />
</el-form-item>
<el-divider>Text Style</el-divider>
<el-form-item label="Font Size">
<el-slider v-model="localConfig.fontSize" :min="12" :max="120" show-input />
</el-form-item>
<el-form-item label="Font Family">
<el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" />
<el-option label="Times New Roman" value="'Times New Roman'" />
<el-option label="Courier New" value="'Courier New'" />
<el-option label="Georgia" value="Georgia" />
<el-option label="Verdana" value="Verdana" />
<el-option label="Impact" value="Impact" />
</el-select>
</el-form-item>
<el-form-item label="Font Weight">
<el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" />
<el-option label="Bold" value="bold" />
<el-option label="Light" value="lighter" />
</el-select>
</el-form-item>
<el-divider>Color Settings</el-divider>
<el-form-item label="Use Gradient Colors">
<el-switch v-model="localConfig.useGradient" />
</el-form-item>
<template v-if="!localConfig.useGradient">
<el-form-item label="Text Color">
<el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item>
</template>
<template v-else>
<el-form-item label="Gradient Start Color">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item>
<el-form-item label="Gradient End Color">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item>
</template>
<el-divider>Effects</el-divider>
<el-form-item label="Text Shadow">
<el-switch v-model="localConfig.textShadow" />
</el-form-item>
<template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button>
<el-button type="success" @click="applyPreset('neon')">Neon</el-button>
<el-button type="warning" @click="applyPreset('elegant')">Elegant</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface ClockConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showSeconds: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ClockConfig>;
}>(), {
config: () => ({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: ClockConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<ClockConfig>({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
modern: {
format: 'HH:mm',
fontSize: 72,
fontFamily: 'Arial',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#3498db', '#9b59b6'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10,
showSeconds: false
},
neon: {
format: 'HH:mm:ss',
fontSize: 60,
fontFamily: 'Impact',
fontWeight: 'normal',
color: '#39ff14',
useGradient: false,
textShadow: true,
shadowColor: 'rgba(57, 255, 20, 0.8)',
shadowBlur: 15,
showSeconds: true
},
elegant: {
format: 'hh:mm A',
fontSize: 64,
fontFamily: 'Georgia',
fontWeight: 'normal',
useGradient: true,
gradientColors: ['#d4af37', '#f1c40f'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 5,
showSeconds: false
},
minimal: {
format: 'HH:mm',
fontSize: 56,
fontFamily: 'Helvetica',
fontWeight: 'lighter',
color: '#ffffff',
useGradient: false,
textShadow: false,
showSeconds: false
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.clock-config {
padding: 10px;
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<div class="date-config">
<h2>Date Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Date Format">
<el-select v-model="localConfig.format" placeholder="Select format">
<el-option label="YYYY-MM-DD" value="YYYY-MM-DD" />
<el-option label="MM/DD/YYYY" value="MM/DD/YYYY" />
<el-option label="DD/MM/YYYY" value="DD/MM/YYYY" />
<el-option label="MMMM D, YYYY" value="MMMM D, YYYY" />
<el-option label="D MMMM YYYY" value="D MMMM YYYY" />
</el-select>
</el-form-item>
<el-form-item label="Show Weekday">
<el-switch v-model="localConfig.showWeekday" />
</el-form-item>
<el-divider>Text Style</el-divider>
<el-form-item label="Font Size">
<el-slider v-model="localConfig.fontSize" :min="12" :max="80" show-input />
</el-form-item>
<el-form-item label="Font Family">
<el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" />
<el-option label="Times New Roman" value="'Times New Roman'" />
<el-option label="Courier New" value="'Courier New'" />
<el-option label="Georgia" value="Georgia" />
<el-option label="Verdana" value="Verdana" />
<el-option label="Impact" value="Impact" />
</el-select>
</el-form-item>
<el-form-item label="Font Weight">
<el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" />
<el-option label="Bold" value="bold" />
<el-option label="Light" value="lighter" />
</el-select>
</el-form-item>
<el-divider>Color Settings</el-divider>
<el-form-item label="Use Gradient Colors">
<el-switch v-model="localConfig.useGradient" />
</el-form-item>
<template v-if="!localConfig.useGradient">
<el-form-item label="Text Color">
<el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item>
</template>
<template v-else>
<el-form-item label="Gradient Start Color">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item>
<el-form-item label="Gradient End Color">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item>
</template>
<el-divider>Effects</el-divider>
<el-form-item label="Text Shadow">
<el-switch v-model="localConfig.textShadow" />
</el-form-item>
<template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button>
<el-button type="success" @click="applyPreset('elegant')">Elegant</el-button>
<el-button type="warning" @click="applyPreset('casual')">Casual</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface DateConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showWeekday: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<DateConfig>;
}>(), {
config: () => ({
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: DateConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<DateConfig>({
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
modern: {
format: 'YYYY-MM-DD',
fontSize: 42,
fontFamily: 'Arial',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#3498db', '#9b59b6'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10,
showWeekday: true
},
elegant: {
format: 'MMMM D, YYYY',
fontSize: 36,
fontFamily: 'Georgia',
fontWeight: 'normal',
useGradient: true,
gradientColors: ['#d4af37', '#f1c40f'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 5,
showWeekday: true
},
casual: {
format: 'D MMMM YYYY',
fontSize: 32,
fontFamily: 'Verdana',
fontWeight: 'normal',
color: '#2ecc71',
useGradient: false,
textShadow: true,
shadowColor: 'rgba(46, 204, 113, 0.5)',
shadowBlur: 8,
showWeekday: true
},
minimal: {
format: 'MM/DD/YYYY',
fontSize: 28,
fontFamily: 'Helvetica',
fontWeight: 'lighter',
color: '#ffffff',
useGradient: false,
textShadow: false,
showWeekday: false
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.date-config {
padding: 10px;
}
</style>

View File

@@ -0,0 +1,175 @@
<template>
<div class="image-config">
<h2>Image Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Image URL">
<el-input v-model="localConfig.imageUrl" placeholder="Enter image URL" />
</el-form-item>
<div class="preview-image" v-if="localConfig.imageUrl">
<img :src="localConfig.imageUrl" alt="Preview" style="max-width: 100%; max-height: 150px;" />
</div>
<el-divider>Size & Appearance</el-divider>
<el-form-item label="Width (px)">
<el-slider v-model="localConfig.width" :min="50" :max="800" show-input />
</el-form-item>
<el-form-item label="Height (px)">
<el-slider v-model="localConfig.height" :min="50" :max="800" show-input />
</el-form-item>
<el-form-item label="Opacity">
<el-slider v-model="localConfig.opacity" :min="0" :max="1" :step="0.01" show-input />
</el-form-item>
<el-form-item label="Border Radius (px)">
<el-slider v-model="localConfig.borderRadius" :min="0" :max="100" show-input />
</el-form-item>
<el-divider>Effects</el-divider>
<el-form-item label="Shadow">
<el-switch v-model="localConfig.shadow" />
</el-form-item>
<template v-if="localConfig.shadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="50" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('normal')">Normal</el-button>
<el-button type="success" @click="applyPreset('rounded')">Rounded</el-button>
<el-button type="warning" @click="applyPreset('shadow')">Shadow</el-button>
<el-button type="danger" @click="applyPreset('circular')">Circular</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface ImageConfig {
imageUrl: string;
width: number;
height: number;
opacity: number;
borderRadius: number;
shadow: boolean;
shadowColor: string;
shadowBlur: number;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ImageConfig>;
}>(), {
config: () => ({
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: ImageConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<ImageConfig>({
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
normal: {
width: 300,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false
},
rounded: {
width: 300,
height: 200,
opacity: 1,
borderRadius: 12,
shadow: false
},
shadow: {
width: 300,
height: 200,
opacity: 1,
borderRadius: 8,
shadow: true,
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 20
},
circular: {
width: 200,
height: 200,
opacity: 1,
borderRadius: 100,
shadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 15
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.image-config {
padding: 10px;
}
.preview-image {
display: flex;
justify-content: center;
margin: 10px 0;
padding: 10px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,208 @@
<template>
<div class="text-config">
<h2>Text Widget Settings</h2>
<el-form label-position="top">
<el-form-item label="Text Content">
<el-input v-model="localConfig.text" type="textarea" :rows="3" placeholder="Enter text to display" />
</el-form-item>
<el-divider>Text Style</el-divider>
<el-form-item label="Font Size">
<el-slider v-model="localConfig.fontSize" :min="12" :max="100" show-input />
</el-form-item>
<el-form-item label="Font Family">
<el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" />
<el-option label="Times New Roman" value="'Times New Roman'" />
<el-option label="Courier New" value="'Courier New'" />
<el-option label="Georgia" value="Georgia" />
<el-option label="Verdana" value="Verdana" />
<el-option label="Impact" value="Impact" />
</el-select>
</el-form-item>
<el-form-item label="Font Weight">
<el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" />
<el-option label="Bold" value="bold" />
<el-option label="Light" value="lighter" />
</el-select>
</el-form-item>
<el-divider>Color Settings</el-divider>
<el-form-item label="Use Gradient Colors">
<el-switch v-model="localConfig.useGradient" />
</el-form-item>
<template v-if="!localConfig.useGradient">
<el-form-item label="Text Color">
<el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item>
</template>
<template v-else>
<el-form-item label="Gradient Start Color">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item>
<el-form-item label="Gradient End Color">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item>
</template>
<el-divider>Effects</el-divider>
<el-form-item label="Text Shadow">
<el-switch v-model="localConfig.textShadow" />
</el-form-item>
<template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color">
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item>
<el-form-item label="Shadow Blur">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item>
</template>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button>
<el-button type="success" @click="applyPreset('neon')">Neon</el-button>
<el-button type="warning" @click="applyPreset('retro')">Retro</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// Define props interface for config
interface TextConfig {
text: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<TextConfig>;
}>(), {
config: () => ({
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff']
})
});
// Define emit
const emit = defineEmits<{
(event: 'update:config', config: TextConfig): void;
}>();
// Local config for two-way binding
const localConfig = ref<TextConfig>({
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff']
});
// Sync with parent config on mount
onMounted(() => {
// Merge default config with provided config
localConfig.value = { ...localConfig.value, ...props.config };
});
// Preset styles
const presets = {
modern: {
text: localConfig.value.text,
fontSize: 48,
fontFamily: 'Arial',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#3498db', '#9b59b6'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10
},
neon: {
text: localConfig.value.text,
fontSize: 54,
fontFamily: 'Impact',
fontWeight: 'normal',
color: '#39ff14',
useGradient: false,
textShadow: true,
shadowColor: 'rgba(57, 255, 20, 0.8)',
shadowBlur: 15
},
retro: {
text: localConfig.value.text,
fontSize: 42,
fontFamily: 'Courier New',
fontWeight: 'bold',
useGradient: true,
gradientColors: ['#f39c12', '#e74c3c'],
textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.6)',
shadowBlur: 6
},
minimal: {
text: localConfig.value.text,
fontSize: 36,
fontFamily: 'Helvetica',
fontWeight: 'lighter',
color: '#ffffff',
useGradient: false,
textShadow: false
}
};
// Apply preset
const applyPreset = (presetName: keyof typeof presets) => {
const preset = presets[presetName];
localConfig.value = { ...localConfig.value, ...preset };
};
// Watch for local changes and emit to parent
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
<style scoped>
.text-config {
padding: 10px;
}
</style>

View File

@@ -1,5 +1,12 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')

24
src/router/index.ts Normal file
View File

@@ -0,0 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'HomeView',
component: () => import('../views/HomeView.vue')
},
{
path: '/config',
name: 'ConfigView',
component: () => import('../views/ConfigView.vue')
},
{
path: '/preview',
name: 'PreviewView',
component: () => import('../views/PreviewView.vue')
}
]
});
export default router;

View File

@@ -5,7 +5,7 @@
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
background-color: transparent;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -13,38 +13,24 @@
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
padding: 0;
min-width: 320px;
min-height: 100vh;
background-color: transparent;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
/* Preview mode specific styles */
.preview-view {
background-color: transparent !important;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: transparent;
}
}
button:hover {
border-color: #646cff;

28
src/utils/configUtils.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* Utility functions for encoding and decoding widget configurations in URL
*/
/**
* Encodes widget configuration for URL sharing
* @param config The configuration object to encode
* @returns Encoded string for URL
*/
export const encodeConfig = (config: any): string => {
const jsonString = JSON.stringify(config);
return btoa(encodeURIComponent(jsonString));
};
/**
* Decodes widget configuration from URL parameter
* @param encodedString The encoded configuration string from URL
* @returns Decoded configuration object
*/
export const decodeConfig = (encodedString: string): any => {
try {
const jsonString = decodeURIComponent(atob(encodedString));
return JSON.parse(jsonString);
} catch (e) {
console.error('Failed to decode configuration', e);
throw new Error('Invalid configuration format');
}
};

248
src/views/ConfigView.vue Normal file
View File

@@ -0,0 +1,248 @@
<template>
<div class="config-view">
<div class="left-panel">
<div class="widget-selector">
<el-select v-model="selectedWidget" placeholder="Select Widget" @change="handleWidgetChange">
<el-option v-for="widget in widgets" :key="widget.value" :label="widget.label" :value="widget.value" />
</el-select>
</div>
<div class="config-panel">
<component :is="currentConfigComponent" v-if="currentConfigComponent" @update:config="updateWidgetConfig" :config="currentWidgetConfig" />
</div>
<div class="url-generator">
<el-input v-model="generatedUrl" readonly>
<template #append>
<el-button @click="copyUrl">
<el-icon><CopyDocument /></el-icon> Copy
</el-button>
</template>
</el-input>
</div>
</div>
<div class="right-panel">
<div class="preview-container">
<div class="preview-wrapper">
<component :is="currentWidgetComponent" v-if="currentWidgetComponent" :config="currentWidgetConfig" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { CopyDocument } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { encodeConfig, decodeConfig } from '../utils/configUtils';
// Widget components and their configs
import ClockWidget from '../widgets/ClockWidget.vue';
import DateWidget from '../widgets/DateWidget.vue';
import TextWidget from '../widgets/TextWidget.vue';
import ImageWidget from '../widgets/ImageWidget.vue';
// Config components
import ClockConfig from '../components/config/ClockConfig.vue';
import DateConfig from '../components/config/DateConfig.vue';
import TextConfig from '../components/config/TextConfig.vue';
import ImageConfig from '../components/config/ImageConfig.vue';
const widgets = [
{ label: 'Clock Widget', value: 'clock', component: ClockWidget, configComponent: ClockConfig },
{ label: 'Date Widget', value: 'date', component: DateWidget, configComponent: DateConfig },
{ label: 'Text Widget', value: 'text', component: TextWidget, configComponent: TextConfig },
{ label: 'Image Widget', value: 'image', component: ImageWidget, configComponent: ImageConfig },
];
const selectedWidget = ref('clock');
const currentWidgetConfig = ref({});
const generatedUrl = ref('');
// Get widget component based on selection
const currentWidgetComponent = computed(() => {
const widget = widgets.find(w => w.value === selectedWidget.value);
return widget?.component;
});
// Get config component based on selection
const currentConfigComponent = computed(() => {
const widget = widgets.find(w => w.value === selectedWidget.value);
return widget?.configComponent;
});
// Set default config for each widget type
const getDefaultConfig = (widgetType: string) => {
switch (widgetType) {
case 'clock':
return {
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
};
case 'date':
return {
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
};
case 'text':
return {
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
};
case 'image':
return {
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
};
default:
return {};
}
};
// Handle widget type change
const handleWidgetChange = () => {
currentWidgetConfig.value = getDefaultConfig(selectedWidget.value);
updateGeneratedUrl();
};
// Update widget configuration
const updateWidgetConfig = (newConfig: any) => {
currentWidgetConfig.value = newConfig;
updateGeneratedUrl();
};
// Generate preview URL
const updateGeneratedUrl = () => {
const baseUrl = window.location.origin;
const configStr = encodeConfig({
type: selectedWidget.value,
config: currentWidgetConfig.value
});
generatedUrl.value = `${baseUrl}/preview?data=${configStr}`;
};
// Copy URL to clipboard
const copyUrl = () => {
navigator.clipboard.writeText(generatedUrl.value).then(() => {
ElMessage.success('URL copied to clipboard!');
}).catch(() => {
ElMessage.error('Failed to copy URL');
});
};
// Check for query params on load (for direct linking)
onMounted(() => {
const queryParams = new URLSearchParams(window.location.search);
const data = queryParams.get('data');
if (data) {
try {
const decodedData = decodeConfig(data);
selectedWidget.value = decodedData.type;
currentWidgetConfig.value = decodedData.config;
} catch (e) {
ElMessage.error('Invalid configuration in URL');
handleWidgetChange(); // Load default config
}
} else {
handleWidgetChange(); // Load default config
}
});
// Update URL when configuration changes
watch([selectedWidget, currentWidgetConfig], () => {
updateGeneratedUrl();
}, { deep: true });
</script>
<style scoped>
.config-view {
display: flex;
height: 100vh;
overflow: hidden;
}
.left-panel {
width: 380px;
padding: 20px;
background-color: #f5f7fa;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.right-panel {
flex: 1;
background-color: transparent;
background-image: linear-gradient(45deg, #ddd 25%, transparent 25%),
linear-gradient(-45deg, #ddd 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ddd 75%),
linear-gradient(-45deg, transparent 75%, #ddd 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.widget-selector {
width: 100%;
}
.config-panel {
flex-grow: 1;
}
.url-generator {
margin-top: 20px;
}
.preview-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.preview-wrapper {
padding: 20px;
border-radius: 8px;
}
</style>

227
src/views/HomeView.vue Normal file
View File

@@ -0,0 +1,227 @@
<template>
<div class="home-view">
<div class="container">
<div class="header">
<h1>OBS Overlay Widget</h1>
<p>Create customizable widgets for OBS Studio streaming and recording</p>
</div>
<div class="cards">
<div class="card" @click="goToConfig">
<div class="card-icon">
<el-icon><Setting /></el-icon>
</div>
<div class="card-title">Configure Widgets</div>
<div class="card-description">
Design and customize widgets for your OBS streams with an interactive interface
</div>
</div>
<div class="card" @click="goToDoc">
<div class="card-icon">
<el-icon><Document /></el-icon>
</div>
<div class="card-title">Documentation</div>
<div class="card-description">
Learn how to use and integrate OBS Overlay Widgets into your streams
</div>
</div>
</div>
<div class="features">
<h2>Available Widgets</h2>
<div class="widget-list">
<div class="widget-item">
<div class="widget-icon"></div>
<div class="widget-info">
<h3>Clock Widget</h3>
<p>Display current time with customizable format, style, and effects</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">📅</div>
<div class="widget-info">
<h3>Date Widget</h3>
<p>Show current date with customizable format, style, and effects</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">📝</div>
<div class="widget-info">
<h3>Text Widget</h3>
<p>Display text with customizable styles including gradients, shadows, and fonts</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">🖼</div>
<div class="widget-info">
<h3>Image Widget</h3>
<p>Show images with customizable size, effects, and positioning</p>
</div>
</div>
</div>
</div>
<div class="footer">
<p>OBS Overlay Widget &copy; 2025</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { Setting, Document } from '@element-plus/icons-vue';
const router = useRouter();
const goToConfig = () => {
router.push('/config');
};
const goToDoc = () => {
// This would go to documentation in a real app
window.open('https://github.com/yourusername/obs-overlay-widget', '_blank');
};
</script>
<style scoped>
.home-view {
min-height: 100vh;
background-color: #f5f7fa;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 50px;
padding: 30px 0;
}
.header h1 {
font-size: 48px;
margin-bottom: 10px;
background: linear-gradient(45deg, #3498db, #9b59b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
}
.header p {
font-size: 18px;
color: #666;
}
.cards {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 50px;
}
.card {
background-color: white;
border-radius: 10px;
padding: 30px;
width: 300px;
text-align: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
}
.card-icon {
font-size: 48px;
margin-bottom: 20px;
color: #3498db;
}
.card-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 15px;
}
.card-description {
color: #666;
line-height: 1.6;
}
.features {
margin-bottom: 50px;
}
.features h2 {
text-align: center;
margin-bottom: 30px;
font-size: 32px;
color: #333;
}
.widget-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 20px;
}
.widget-item {
display: flex;
background-color: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
}
.widget-icon {
font-size: 36px;
margin-right: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.widget-info h3 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.widget-info p {
color: #666;
margin: 0;
line-height: 1.5;
}
.footer {
text-align: center;
padding: 20px 0;
color: #666;
border-top: 1px solid #eee;
}
@media (max-width: 768px) {
.cards {
flex-direction: column;
align-items: center;
}
.widget-list {
grid-template-columns: 1fr;
}
}
</style>

60
src/views/PreviewView.vue Normal file
View File

@@ -0,0 +1,60 @@
<template>
<div class="preview-view">
<component
:is="widgetComponent"
v-if="widgetComponent"
:config="widgetConfig"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { decodeConfig } from '../utils/configUtils';
// Import widget components
import ClockWidget from '../widgets/ClockWidget.vue';
import DateWidget from '../widgets/DateWidget.vue';
import TextWidget from '../widgets/TextWidget.vue';
import ImageWidget from '../widgets/ImageWidget.vue';
// Widget registry
const widgetRegistry = {
'clock': ClockWidget,
'date': DateWidget,
'text': TextWidget,
'image': ImageWidget
};
const widgetType = ref('');
const widgetConfig = ref({});
const widgetComponent = computed(() => {
return widgetRegistry[widgetType.value as keyof typeof widgetRegistry];
});
onMounted(() => {
const queryParams = new URLSearchParams(window.location.search);
const data = queryParams.get('data');
if (data) {
try {
const decodedData = decodeConfig(data);
widgetType.value = decodedData.type;
widgetConfig.value = decodedData.config;
} catch (e) {
console.error('Invalid configuration in URL', e);
}
}
});
</script>
<style scoped>
.preview-view {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="clock-widget" :style="clockStyle">
{{ currentTime }}
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import dayjs from 'dayjs';
// Define props interface
interface ClockConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showSeconds: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ClockConfig>;
}>(), {
config: () => ({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
})
});
// State for current time
const currentTime = ref('');
// Update time string based on format
const updateTime = () => {
const currentFormat = props.config.format || 'HH:mm:ss';
const format = props.config.showSeconds ? currentFormat : currentFormat.replace(':ss', '');
currentTime.value = dayjs().format(format);
};
// Interval for updating time
let timeInterval: number | null = null;
// Start the clock
onMounted(() => {
updateTime();
// Set interval based on whether seconds are shown
const intervalTime = props.config.showSeconds ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
});
// Clean up interval on component unmount
onUnmounted(() => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
});
// Update interval if showSeconds changes
watch(() => props.config.showSeconds, (newValue) => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
const intervalTime = newValue ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
updateTime();
});
// Computed styles for the clock
const clockStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 48}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.clock-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>

120
src/widgets/ClockWidget.vue Normal file
View File

@@ -0,0 +1,120 @@
<template>
<div class="clock-widget" :style="clockStyle">
{{ currentTime }}
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import dayjs from 'dayjs';
// Define props interface
interface ClockConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showSeconds: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ClockConfig>;
}>(), {
config: () => ({
format: 'HH:mm:ss',
color: '#ffffff',
fontSize: 48,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true
})
});
// State for current time
const currentTime = ref('');
// Update time string based on format
const updateTime = () => {
const currentFormat = props.config.format || 'HH:mm:ss';
const format = props.config.showSeconds ? currentFormat : currentFormat.replace(':ss', '');
currentTime.value = dayjs().format(format);
};
// Interval for updating time
let timeInterval: number | null = null;
// Start the clock
onMounted(() => {
updateTime();
// Set interval based on whether seconds are shown
const intervalTime = props.config.showSeconds ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
});
// Clean up interval on component unmount
onUnmounted(() => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
});
// Update interval if showSeconds changes
watch(() => props.config.showSeconds, (newValue) => {
if (timeInterval !== null) {
window.clearInterval(timeInterval);
}
const intervalTime = newValue ? 1000 : 60000;
timeInterval = window.setInterval(updateTime, intervalTime);
updateTime();
});
// Computed styles for the clock
const clockStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 48}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.clock-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>

130
src/widgets/DateWidget.vue Normal file
View File

@@ -0,0 +1,130 @@
<template>
<div class="date-widget" :style="dateStyle">
{{ currentDate }}
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import dayjs from 'dayjs';
// Define props interface
interface DateConfig {
format: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
showWeekday: boolean;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<DateConfig>;
}>(), {
config: () => ({
format: 'YYYY-MM-DD',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff'],
showWeekday: true
})
});
// State for current date
const currentDate = ref('');
// Update date string based on format
const updateDate = () => {
let dateStr = dayjs().format(props.config.format || 'YYYY-MM-DD');
// Add weekday if enabled
if (props.config.showWeekday) {
const weekday = dayjs().format('dddd');
dateStr = `${weekday}, ${dateStr}`;
}
currentDate.value = dateStr;
};
// Interval for updating date (at midnight)
let dateInterval: number | null = null;
// Start the date display and set interval to update at midnight
onMounted(() => {
updateDate();
// Calculate time until next midnight
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const timeUntilMidnight = tomorrow.getTime() - now.getTime();
// Set timeout to update at midnight, then set daily interval
setTimeout(() => {
updateDate();
// Update daily
dateInterval = window.setInterval(updateDate, 24 * 60 * 60 * 1000);
}, timeUntilMidnight);
});
// Clean up interval on component unmount
onUnmounted(() => {
if (dateInterval !== null) {
window.clearInterval(dateInterval);
}
});
// Watch for config changes
watch(() => props.config, () => {
updateDate();
}, { deep: true });
// Computed styles for the date
const dateStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 32}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.date-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="image-widget">
<img
v-if="config.imageUrl"
:src="config.imageUrl"
:style="imageStyle"
alt="Widget Image"
/>
<div v-else class="placeholder" :style="placeholderStyle">
Please set an image URL
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
// Define props interface
interface ImageConfig {
imageUrl: string;
width: number;
height: number;
opacity: number;
borderRadius: number;
shadow: boolean;
shadowColor: string;
shadowBlur: number;
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<ImageConfig>;
}>(), {
config: () => ({
imageUrl: '',
width: 200,
height: 200,
opacity: 1,
borderRadius: 0,
shadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10
})
});
// Computed styles for the image
const imageStyle = computed(() => {
const style: Record<string, string> = {
width: `${props.config.width || 200}px`,
height: `${props.config.height || 200}px`,
opacity: `${props.config.opacity || 1}`,
borderRadius: `${props.config.borderRadius || 0}px`,
objectFit: 'cover'
};
// Apply shadow if enabled
if (props.config.shadow) {
style.filter = `drop-shadow(0 0 ${props.config.shadowBlur || 10}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'})`;
}
return style;
});
// Style for placeholder when no image is set
const placeholderStyle = computed(() => {
return {
width: `${props.config.width || 200}px`,
height: `${props.config.height || 200}px`,
borderRadius: `${props.config.borderRadius || 0}px`,
opacity: `${props.config.opacity || 1}`,
};
});
</script>
<style scoped>
.image-widget {
display: inline-block;
}
.placeholder {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.1);
border: 2px dashed rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.7);
font-family: Arial, sans-serif;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="text-widget" :style="textStyle">
{{ config.text }}
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
// Define props interface
interface TextConfig {
text: string;
color: string;
fontSize: number;
fontWeight: string;
fontFamily: string;
textShadow: boolean;
shadowColor: string;
shadowBlur: number;
useGradient: boolean;
gradientColors: string[];
}
// Define props with default values
const props = withDefaults(defineProps<{
config: Partial<TextConfig>;
}>(), {
config: () => ({
text: 'Sample Text',
color: '#ffffff',
fontSize: 32,
fontWeight: 'normal',
fontFamily: 'Arial',
textShadow: false,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 4,
useGradient: false,
gradientColors: ['#ff0000', '#0000ff']
})
});
// Computed styles for the text
const textStyle = computed(() => {
const style: Record<string, string> = {
fontSize: `${props.config.fontSize || 32}px`,
fontFamily: props.config.fontFamily || 'Arial',
fontWeight: props.config.fontWeight || 'normal'
};
// Apply gradient or solid color
if (props.config.useGradient && (props.config.gradientColors || []).length >= 2) {
const colors = props.config.gradientColors || ['#ff0000', '#0000ff'];
style.backgroundImage = `linear-gradient(to right, ${colors.join(', ')})`;
style.webkitBackgroundClip = 'text';
style.backgroundClip = 'text';
style.color = 'transparent';
} else {
style.color = props.config.color || '#ffffff';
}
// Apply text shadow if enabled
if (props.config.textShadow) {
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
}
return style;
});
</script>
<style scoped>
.text-widget {
display: inline-block;
padding: 10px;
font-family: Arial, sans-serif;
user-select: none;
}
</style>