界面样式调整

This commit is contained in:
hxuanyu 2025-06-26 00:06:59 +08:00
parent d70b962ef5
commit cdf5f81601
9 changed files with 233 additions and 184 deletions

View File

@ -1,62 +1,62 @@
# OBS Overlay Widget # OBS 悬浮小组件
A collection of highly customizable widgets for OBS Studio streaming and recording, built with Vue 3, TypeScript, and Vite. 一个为 OBS Studio 直播和录制场景开发的高度可定制化小组件集合,基于 Vue 3、TypeScript 和 Vite 构建。
## Features ## 功能特点
- **Time and Date Display**: Customizable formats for showing current time and date - **时间和日期显示**:可自定义格式的时间和日期显示
- **Text Display**: Show fixed text or API-returned content with custom styling - **文本显示**:展示固定文字或 API 返回内容,支持自定义样式
- **Image Display**: Display local or remote images with customization options - **图片显示**:展示本地或远程图片,支持自定义设置
- **Split View Interface**: Configuration panel on the left, real-time preview on the right - **分屏界面**:左侧为配置面板,右侧为实时预览
- **Transparent Background**: All widgets have transparent backgrounds suitable for OBS overlay - **透明背景**:所有小组件均具有适合 OBS 悬浮的透明背景
- **URL Generation**: Automatically generates sharable URLs with encoded configuration - **URL 生成**:自动生成包含编码配置的可分享 URL
- **Pure Preview Mode**: Open generated URLs to display only the widget with transparent background - **纯预览模式**:打开生成的 URL 仅显示小组件内容,无配置界面,背景透明
## Widget Types ## 小组件类型
1. **Clock Widget**: Display current time with customizable format, style, and effects 1. **时钟小组件**:显示当前时间,可自定义格式、样式和特效
2. **Date Widget**: Show current date with customizable format, style, and effects 2. **日期小组件**:显示当前日期,可自定义格式、样式和特效
3. **Text Widget**: Display text with customizable styles including gradients, shadows, and fonts 3. **文本小组件**:显示文本,支持渐变、阴影、字体等自定义样式
4. **Image Widget**: Show images with customizable size, effects, and positioning 4. **图片小组件**:显示图片,可自定义大小、特效和位置
## Usage ## 使用方法
1. Select a widget type from the dropdown 1. 从下拉菜单中选择小组件类型
2. Configure the widget using the control panel on the left 2. 使用左侧控制面板配置小组件
3. See real-time preview on the right 3. 在右侧实时查看预览效果
4. Copy the generated URL to use in OBS Studio as a Browser Source 4. 复制生成的 URL在 OBS Studio 中作为浏览器源使用
## Development ## 开发
```bash ```bash
# Install dependencies # 安装依赖
npm install npm install
# Start development server # 启动开发服务器
npm run dev npm run dev
# Build for production # 构建生产版本
npm run build npm run build
# Preview production build # 预览生产构建
npm run preview npm run preview
``` ```
## Troubleshooting ## 故障排除
If you encounter TypeScript errors related to undefined properties, make sure that: 如果遇到与未定义属性相关的 TypeScript 错误,请确保:
1. All widget components handle possible undefined configuration properties 1. 所有小组件组件都能处理可能未定义的配置属性
2. Default values are provided for all configuration options 2. 为所有配置选项提供默认值
3. Use proper null checking (e.g., `props.config.property || defaultValue`) 3. 使用适当的空值检查(例如:`props.config.property || defaultValue`
## Integration with OBS Studio ## 与 OBS Studio 集成
1. Run this application on a web server or locally 1. 在 Web 服务器或本地运行此应用
2. Configure your widget using the configuration interface 2. 使用配置界面设置您的小组件
3. Copy the generated URL 3. 复制生成的 URL
4. In OBS Studio: 4. 在 OBS Studio 中:
- Add a "Browser Source" to your scene - 向您的场景添加"浏览器源"
- Paste the URL into the Browser Source URL field - 将 URL 粘贴到浏览器源 URL 字段
- Set width and height according to your needs - 根据需要设置宽度和高度
- Check "Shutdown source when not visible" for better performance - 勾选"不可见时关闭源"以获得更好的性能

View File

@ -1,28 +1,47 @@
<template> <template>
<div class="clock-config"> <div class="clock-config">
<h2>Clock Widget Settings</h2> <h2>时钟小组件设置</h2>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="Time Format"> <el-form-item label="时间格式">
<el-select v-model="localConfig.format" placeholder="Select format"> <el-select v-model="localConfig.format" placeholder="选择格式">
<el-option label="HH:mm:ss (24-hour)" value="HH:mm:ss" /> <el-option label="HH:mm:ss (24小时制)" value="HH:mm:ss" />
<el-option label="HH:mm (24-hour)" value="HH:mm" /> <el-option label="HH:mm (24小时制)" value="HH:mm" />
<el-option label="hh:mm:ss A (12-hour)" value="hh:mm:ss A" /> <el-option label="hh:mm:ss A (12小时制)" value="hh:mm:ss A" />
<el-option label="hh:mm A (12-hour)" value="hh:mm A" /> <el-option label="hh:mm A (12小时制)" value="hh:mm A" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="Show Seconds"> <el-form-item>
<el-switch v-model="localConfig.showSeconds" /> <div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<span>显示秒</span>
<el-switch v-model="localConfig.showSeconds" style="margin-left: 10px;" />
</div>
<div>
<span>显示日期</span>
<el-switch v-model="localConfig.showDate" style="margin-left: 10px;" />
</div>
</div>
</el-form-item> </el-form-item>
<el-divider>Text Style</el-divider> <el-form-item v-if="localConfig.showDate" label="日期格式">
<el-select v-model="localConfig.dateFormat" placeholder="选择格式">
<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="Font Size"> <el-divider>文本样式</el-divider>
<el-form-item label="字体大小">
<el-slider v-model="localConfig.fontSize" :min="12" :max="120" show-input /> <el-slider v-model="localConfig.fontSize" :min="12" :max="120" show-input />
</el-form-item> </el-form-item>
<el-form-item label="Font Family"> <el-form-item label="字体">
<el-select v-model="localConfig.fontFamily"> <el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" /> <el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" /> <el-option label="Helvetica" value="Helvetica" />
@ -34,58 +53,58 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="Font Weight"> <el-form-item label="字重">
<el-select v-model="localConfig.fontWeight"> <el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" /> <el-option label="普通" value="normal" />
<el-option label="Bold" value="bold" /> <el-option label="粗体" value="bold" />
<el-option label="Light" value="lighter" /> <el-option label="细体" value="lighter" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-divider>Color Settings</el-divider> <el-divider>颜色设置</el-divider>
<el-form-item label="Use Gradient Colors"> <el-form-item label="使用渐变色">
<el-switch v-model="localConfig.useGradient" /> <el-switch v-model="localConfig.useGradient" />
</el-form-item> </el-form-item>
<template v-if="!localConfig.useGradient"> <template v-if="!localConfig.useGradient">
<el-form-item label="Text Color"> <el-form-item label="文本颜色">
<el-color-picker v-model="localConfig.color" show-alpha /> <el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item> </el-form-item>
</template> </template>
<template v-else> <template v-else>
<el-form-item label="Gradient Start Color"> <el-form-item label="渐变起始颜色">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha /> <el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item> </el-form-item>
<el-form-item label="Gradient End Color"> <el-form-item label="渐变结束颜色">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha /> <el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item> </el-form-item>
</template> </template>
<el-divider>Effects</el-divider> <el-divider>特效</el-divider>
<el-form-item label="Text Shadow"> <el-form-item label="文字阴影">
<el-switch v-model="localConfig.textShadow" /> <el-switch v-model="localConfig.textShadow" />
</el-form-item> </el-form-item>
<template v-if="localConfig.textShadow"> <template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color"> <el-form-item label="阴影颜色">
<el-color-picker v-model="localConfig.shadowColor" show-alpha /> <el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item> </el-form-item>
<el-form-item label="Shadow Blur"> <el-form-item label="阴影模糊度">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input /> <el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item> </el-form-item>
</template> </template>
<el-form-item> <el-form-item>
<el-button-group> <el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button> <el-button type="primary" @click="applyPreset('modern')">现代风格</el-button>
<el-button type="success" @click="applyPreset('neon')">Neon</el-button> <el-button type="success" @click="applyPreset('neon')">霓虹风格</el-button>
<el-button type="warning" @click="applyPreset('elegant')">Elegant</el-button> <el-button type="warning" @click="applyPreset('elegant')">优雅风格</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button> <el-button type="danger" @click="applyPreset('minimal')">简约风格</el-button>
</el-button-group> </el-button-group>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -95,7 +114,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from 'vue';
// Define props interface for config // props
interface ClockConfig { interface ClockConfig {
format: string; format: string;
color: string; color: string;
@ -108,6 +127,8 @@ interface ClockConfig {
useGradient: boolean; useGradient: boolean;
gradientColors: string[]; gradientColors: string[];
showSeconds: boolean; showSeconds: boolean;
showDate: boolean;
dateFormat: string;
} }
// Define props with default values // Define props with default values
@ -125,7 +146,9 @@ const props = withDefaults(defineProps<{
shadowBlur: 4, shadowBlur: 4,
useGradient: false, useGradient: false,
gradientColors: ['#ff0000', '#0000ff'], gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true showSeconds: true,
showDate: false,
dateFormat: 'YYYY-MM-DD'
}) })
}); });
@ -146,7 +169,9 @@ const localConfig = ref<ClockConfig>({
shadowBlur: 4, shadowBlur: 4,
useGradient: false, useGradient: false,
gradientColors: ['#ff0000', '#0000ff'], gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true showSeconds: true,
showDate: false,
dateFormat: 'YYYY-MM-DD'
}); });
// Sync with parent config on mount // Sync with parent config on mount
@ -167,7 +192,9 @@ const presets = {
textShadow: true, textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.3)', shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10, shadowBlur: 10,
showSeconds: false showSeconds: false,
showDate: true,
dateFormat: 'YYYY-MM-DD'
}, },
neon: { neon: {
format: 'HH:mm:ss', format: 'HH:mm:ss',
@ -179,7 +206,8 @@ const presets = {
textShadow: true, textShadow: true,
shadowColor: 'rgba(57, 255, 20, 0.8)', shadowColor: 'rgba(57, 255, 20, 0.8)',
shadowBlur: 15, shadowBlur: 15,
showSeconds: true showSeconds: true,
showDate: false
}, },
elegant: { elegant: {
format: 'hh:mm A', format: 'hh:mm A',
@ -191,7 +219,9 @@ const presets = {
textShadow: true, textShadow: true,
shadowColor: 'rgba(0, 0, 0, 0.5)', shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 5, shadowBlur: 5,
showSeconds: false showSeconds: false,
showDate: true,
dateFormat: 'MMMM D, YYYY'
}, },
minimal: { minimal: {
format: 'HH:mm', format: 'HH:mm',
@ -201,7 +231,8 @@ const presets = {
color: '#ffffff', color: '#ffffff',
useGradient: false, useGradient: false,
textShadow: false, textShadow: false,
showSeconds: false showSeconds: false,
showDate: false
} }
}; };

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="date-config"> <div class="date-config">
<h2>Date Widget Settings</h2> <h2>日期小组件设置</h2>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="Date Format"> <el-form-item label="日期格式">
<el-select v-model="localConfig.format" placeholder="Select format"> <el-select v-model="localConfig.format" placeholder="选择格式">
<el-option label="YYYY-MM-DD" value="YYYY-MM-DD" /> <el-option label="YYYY-MM-DD" value="YYYY-MM-DD" />
<el-option label="MM/DD/YYYY" value="MM/DD/YYYY" /> <el-option label="MM/DD/YYYY" value="MM/DD/YYYY" />
<el-option label="DD/MM/YYYY" value="DD/MM/YYYY" /> <el-option label="DD/MM/YYYY" value="DD/MM/YYYY" />
@ -13,17 +13,17 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="Show Weekday"> <el-form-item label="显示星期">
<el-switch v-model="localConfig.showWeekday" /> <el-switch v-model="localConfig.showWeekday" />
</el-form-item> </el-form-item>
<el-divider>Text Style</el-divider> <el-divider>文本样式</el-divider>
<el-form-item label="Font Size"> <el-form-item label="字体大小">
<el-slider v-model="localConfig.fontSize" :min="12" :max="80" show-input /> <el-slider v-model="localConfig.fontSize" :min="12" :max="80" show-input />
</el-form-item> </el-form-item>
<el-form-item label="Font Family"> <el-form-item label="字体">
<el-select v-model="localConfig.fontFamily"> <el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" /> <el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" /> <el-option label="Helvetica" value="Helvetica" />
@ -35,58 +35,58 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="Font Weight"> <el-form-item label="字重">
<el-select v-model="localConfig.fontWeight"> <el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" /> <el-option label="普通" value="normal" />
<el-option label="Bold" value="bold" /> <el-option label="粗体" value="bold" />
<el-option label="Light" value="lighter" /> <el-option label="细体" value="lighter" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-divider>Color Settings</el-divider> <el-divider>颜色设置</el-divider>
<el-form-item label="Use Gradient Colors"> <el-form-item label="使用渐变色">
<el-switch v-model="localConfig.useGradient" /> <el-switch v-model="localConfig.useGradient" />
</el-form-item> </el-form-item>
<template v-if="!localConfig.useGradient"> <template v-if="!localConfig.useGradient">
<el-form-item label="Text Color"> <el-form-item label="文本颜色">
<el-color-picker v-model="localConfig.color" show-alpha /> <el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item> </el-form-item>
</template> </template>
<template v-else> <template v-else>
<el-form-item label="Gradient Start Color"> <el-form-item label="渐变起始颜色">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha /> <el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item> </el-form-item>
<el-form-item label="Gradient End Color"> <el-form-item label="渐变结束颜色">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha /> <el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item> </el-form-item>
</template> </template>
<el-divider>Effects</el-divider> <el-divider>特效</el-divider>
<el-form-item label="Text Shadow"> <el-form-item label="文字阴影">
<el-switch v-model="localConfig.textShadow" /> <el-switch v-model="localConfig.textShadow" />
</el-form-item> </el-form-item>
<template v-if="localConfig.textShadow"> <template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color"> <el-form-item label="阴影颜色">
<el-color-picker v-model="localConfig.shadowColor" show-alpha /> <el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item> </el-form-item>
<el-form-item label="Shadow Blur"> <el-form-item label="阴影模糊度">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input /> <el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item> </el-form-item>
</template> </template>
<el-form-item> <el-form-item>
<el-button-group> <el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button> <el-button type="primary" @click="applyPreset('modern')">现代风格</el-button>
<el-button type="success" @click="applyPreset('elegant')">Elegant</el-button> <el-button type="success" @click="applyPreset('elegant')">优雅风格</el-button>
<el-button type="warning" @click="applyPreset('casual')">Casual</el-button> <el-button type="warning" @click="applyPreset('casual')">休闲风格</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button> <el-button type="danger" @click="applyPreset('minimal')">简约风格</el-button>
</el-button-group> </el-button-group>
</el-form-item> </el-form-item>
</el-form> </el-form>

View File

@ -1,56 +1,56 @@
<template> <template>
<div class="image-config"> <div class="image-config">
<h2>Image Widget Settings</h2> <h2>图片小组件设置</h2>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="Image URL"> <el-form-item label="图片URL">
<el-input v-model="localConfig.imageUrl" placeholder="Enter image URL" /> <el-input v-model="localConfig.imageUrl" placeholder="输入图片URL" />
</el-form-item> </el-form-item>
<div class="preview-image" v-if="localConfig.imageUrl"> <div class="preview-image" v-if="localConfig.imageUrl">
<img :src="localConfig.imageUrl" alt="Preview" style="max-width: 100%; max-height: 150px;" /> <img :src="localConfig.imageUrl" alt="预览" style="max-width: 100%; max-height: 150px;" />
</div> </div>
<el-divider>Size & Appearance</el-divider> <el-divider>尺寸与外观</el-divider>
<el-form-item label="Width (px)"> <el-form-item label="宽度 (像素)">
<el-slider v-model="localConfig.width" :min="50" :max="800" show-input /> <el-slider v-model="localConfig.width" :min="50" :max="800" show-input />
</el-form-item> </el-form-item>
<el-form-item label="Height (px)"> <el-form-item label="高度 (像素)">
<el-slider v-model="localConfig.height" :min="50" :max="800" show-input /> <el-slider v-model="localConfig.height" :min="50" :max="800" show-input />
</el-form-item> </el-form-item>
<el-form-item label="Opacity"> <el-form-item label="透明度">
<el-slider v-model="localConfig.opacity" :min="0" :max="1" :step="0.01" show-input /> <el-slider v-model="localConfig.opacity" :min="0" :max="1" :step="0.01" show-input />
</el-form-item> </el-form-item>
<el-form-item label="Border Radius (px)"> <el-form-item label="圆角半径 (像素)">
<el-slider v-model="localConfig.borderRadius" :min="0" :max="100" show-input /> <el-slider v-model="localConfig.borderRadius" :min="0" :max="100" show-input />
</el-form-item> </el-form-item>
<el-divider>Effects</el-divider> <el-divider>特效</el-divider>
<el-form-item label="Shadow"> <el-form-item label="阴影">
<el-switch v-model="localConfig.shadow" /> <el-switch v-model="localConfig.shadow" />
</el-form-item> </el-form-item>
<template v-if="localConfig.shadow"> <template v-if="localConfig.shadow">
<el-form-item label="Shadow Color"> <el-form-item label="阴影颜色">
<el-color-picker v-model="localConfig.shadowColor" show-alpha /> <el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item> </el-form-item>
<el-form-item label="Shadow Blur"> <el-form-item label="阴影模糊度">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="50" show-input /> <el-slider v-model="localConfig.shadowBlur" :min="0" :max="50" show-input />
</el-form-item> </el-form-item>
</template> </template>
<el-form-item> <el-form-item>
<el-button-group> <el-button-group>
<el-button type="primary" @click="applyPreset('normal')">Normal</el-button> <el-button type="primary" @click="applyPreset('normal')">正常</el-button>
<el-button type="success" @click="applyPreset('rounded')">Rounded</el-button> <el-button type="success" @click="applyPreset('rounded')">圆角</el-button>
<el-button type="warning" @click="applyPreset('shadow')">Shadow</el-button> <el-button type="warning" @click="applyPreset('shadow')">阴影</el-button>
<el-button type="danger" @click="applyPreset('circular')">Circular</el-button> <el-button type="danger" @click="applyPreset('circular')">圆形</el-button>
</el-button-group> </el-button-group>
</el-form-item> </el-form-item>
</el-form> </el-form>

View File

@ -1,19 +1,19 @@
<template> <template>
<div class="text-config"> <div class="text-config">
<h2>Text Widget Settings</h2> <h2>文本小组件设置</h2>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="Text Content"> <el-form-item label="文本内容">
<el-input v-model="localConfig.text" type="textarea" :rows="3" placeholder="Enter text to display" /> <el-input v-model="localConfig.text" type="textarea" :rows="3" placeholder="输入要显示的文本" />
</el-form-item> </el-form-item>
<el-divider>Text Style</el-divider> <el-divider>文本样式</el-divider>
<el-form-item label="Font Size"> <el-form-item label="字体大小">
<el-slider v-model="localConfig.fontSize" :min="12" :max="100" show-input /> <el-slider v-model="localConfig.fontSize" :min="12" :max="100" show-input />
</el-form-item> </el-form-item>
<el-form-item label="Font Family"> <el-form-item label="字体">
<el-select v-model="localConfig.fontFamily"> <el-select v-model="localConfig.fontFamily">
<el-option label="Arial" value="Arial" /> <el-option label="Arial" value="Arial" />
<el-option label="Helvetica" value="Helvetica" /> <el-option label="Helvetica" value="Helvetica" />
@ -25,58 +25,58 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="Font Weight"> <el-form-item label="字重">
<el-select v-model="localConfig.fontWeight"> <el-select v-model="localConfig.fontWeight">
<el-option label="Normal" value="normal" /> <el-option label="普通" value="normal" />
<el-option label="Bold" value="bold" /> <el-option label="粗体" value="bold" />
<el-option label="Light" value="lighter" /> <el-option label="细体" value="lighter" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-divider>Color Settings</el-divider> <el-divider>颜色设置</el-divider>
<el-form-item label="Use Gradient Colors"> <el-form-item label="使用渐变色">
<el-switch v-model="localConfig.useGradient" /> <el-switch v-model="localConfig.useGradient" />
</el-form-item> </el-form-item>
<template v-if="!localConfig.useGradient"> <template v-if="!localConfig.useGradient">
<el-form-item label="Text Color"> <el-form-item label="文本颜色">
<el-color-picker v-model="localConfig.color" show-alpha /> <el-color-picker v-model="localConfig.color" show-alpha />
</el-form-item> </el-form-item>
</template> </template>
<template v-else> <template v-else>
<el-form-item label="Gradient Start Color"> <el-form-item label="渐变起始颜色">
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha /> <el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
</el-form-item> </el-form-item>
<el-form-item label="Gradient End Color"> <el-form-item label="渐变结束颜色">
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha /> <el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
</el-form-item> </el-form-item>
</template> </template>
<el-divider>Effects</el-divider> <el-divider>特效</el-divider>
<el-form-item label="Text Shadow"> <el-form-item label="文字阴影">
<el-switch v-model="localConfig.textShadow" /> <el-switch v-model="localConfig.textShadow" />
</el-form-item> </el-form-item>
<template v-if="localConfig.textShadow"> <template v-if="localConfig.textShadow">
<el-form-item label="Shadow Color"> <el-form-item label="阴影颜色">
<el-color-picker v-model="localConfig.shadowColor" show-alpha /> <el-color-picker v-model="localConfig.shadowColor" show-alpha />
</el-form-item> </el-form-item>
<el-form-item label="Shadow Blur"> <el-form-item label="阴影模糊度">
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input /> <el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
</el-form-item> </el-form-item>
</template> </template>
<el-form-item> <el-form-item>
<el-button-group> <el-button-group>
<el-button type="primary" @click="applyPreset('modern')">Modern</el-button> <el-button type="primary" @click="applyPreset('modern')">现代风格</el-button>
<el-button type="success" @click="applyPreset('neon')">Neon</el-button> <el-button type="success" @click="applyPreset('neon')">霓虹风格</el-button>
<el-button type="warning" @click="applyPreset('retro')">Retro</el-button> <el-button type="warning" @click="applyPreset('retro')">复古风格</el-button>
<el-button type="danger" @click="applyPreset('minimal')">Minimal</el-button> <el-button type="danger" @click="applyPreset('minimal')">简约风格</el-button>
</el-button-group> </el-button-group>
</el-form-item> </el-form-item>
</el-form> </el-form>

View File

@ -2,7 +2,7 @@
<div class="config-view"> <div class="config-view">
<div class="left-panel"> <div class="left-panel">
<div class="widget-selector"> <div class="widget-selector">
<el-select v-model="selectedWidget" placeholder="Select Widget" @change="handleWidgetChange"> <el-select v-model="selectedWidget" placeholder="选择小组件" @change="handleWidgetChange">
<el-option v-for="widget in widgets" :key="widget.value" :label="widget.label" :value="widget.value" /> <el-option v-for="widget in widgets" :key="widget.value" :label="widget.label" :value="widget.value" />
</el-select> </el-select>
</div> </div>
@ -15,7 +15,7 @@
<el-input v-model="generatedUrl" readonly> <el-input v-model="generatedUrl" readonly>
<template #append> <template #append>
<el-button @click="copyUrl"> <el-button @click="copyUrl">
<el-icon><CopyDocument /></el-icon> Copy <el-icon><CopyDocument /></el-icon> 复制
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
@ -38,42 +38,42 @@ import { CopyDocument } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { encodeConfig, decodeConfig } from '../utils/configUtils'; import { encodeConfig, decodeConfig } from '../utils/configUtils';
// Widget components and their configs //
import ClockWidget from '../widgets/ClockWidget.vue'; import ClockWidget from '../widgets/ClockWidget.vue';
import DateWidget from '../widgets/DateWidget.vue'; import DateWidget from '../widgets/DateWidget.vue';
import TextWidget from '../widgets/TextWidget.vue'; import TextWidget from '../widgets/TextWidget.vue';
import ImageWidget from '../widgets/ImageWidget.vue'; import ImageWidget from '../widgets/ImageWidget.vue';
// Config components //
import ClockConfig from '../components/config/ClockConfig.vue'; import ClockConfig from '../components/config/ClockConfig.vue';
import DateConfig from '../components/config/DateConfig.vue'; import DateConfig from '../components/config/DateConfig.vue';
import TextConfig from '../components/config/TextConfig.vue'; import TextConfig from '../components/config/TextConfig.vue';
import ImageConfig from '../components/config/ImageConfig.vue'; import ImageConfig from '../components/config/ImageConfig.vue';
const widgets = [ const widgets = [
{ label: 'Clock Widget', value: 'clock', component: ClockWidget, configComponent: ClockConfig }, { label: '时钟小组件', value: 'clock', component: ClockWidget, configComponent: ClockConfig },
{ label: 'Date Widget', value: 'date', component: DateWidget, configComponent: DateConfig }, { label: '日期小组件', value: 'date', component: DateWidget, configComponent: DateConfig },
{ label: 'Text Widget', value: 'text', component: TextWidget, configComponent: TextConfig }, { label: '文本小组件', value: 'text', component: TextWidget, configComponent: TextConfig },
{ label: 'Image Widget', value: 'image', component: ImageWidget, configComponent: ImageConfig }, { label: '图片小组件', value: 'image', component: ImageWidget, configComponent: ImageConfig },
]; ];
const selectedWidget = ref('clock'); const selectedWidget = ref('clock');
const currentWidgetConfig = ref({}); const currentWidgetConfig = ref({});
const generatedUrl = ref(''); const generatedUrl = ref('');
// Get widget component based on selection //
const currentWidgetComponent = computed(() => { const currentWidgetComponent = computed(() => {
const widget = widgets.find(w => w.value === selectedWidget.value); const widget = widgets.find(w => w.value === selectedWidget.value);
return widget?.component; return widget?.component;
}); });
// Get config component based on selection //
const currentConfigComponent = computed(() => { const currentConfigComponent = computed(() => {
const widget = widgets.find(w => w.value === selectedWidget.value); const widget = widgets.find(w => w.value === selectedWidget.value);
return widget?.configComponent; return widget?.configComponent;
}); });
// Set default config for each widget type //
const getDefaultConfig = (widgetType: string) => { const getDefaultConfig = (widgetType: string) => {
switch (widgetType) { switch (widgetType) {
case 'clock': case 'clock':
@ -139,13 +139,13 @@ const handleWidgetChange = () => {
updateGeneratedUrl(); updateGeneratedUrl();
}; };
// Update widget configuration //
const updateWidgetConfig = (newConfig: any) => { const updateWidgetConfig = (newConfig: any) => {
currentWidgetConfig.value = newConfig; currentWidgetConfig.value = newConfig;
updateGeneratedUrl(); updateGeneratedUrl();
}; };
// Generate preview URL // URL
const updateGeneratedUrl = () => { const updateGeneratedUrl = () => {
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
const configStr = encodeConfig({ const configStr = encodeConfig({
@ -155,16 +155,16 @@ const updateGeneratedUrl = () => {
generatedUrl.value = `${baseUrl}/preview?data=${configStr}`; generatedUrl.value = `${baseUrl}/preview?data=${configStr}`;
}; };
// Copy URL to clipboard // URL
const copyUrl = () => { const copyUrl = () => {
navigator.clipboard.writeText(generatedUrl.value).then(() => { navigator.clipboard.writeText(generatedUrl.value).then(() => {
ElMessage.success('URL copied to clipboard!'); ElMessage.success('URL 已复制到剪贴板!');
}).catch(() => { }).catch(() => {
ElMessage.error('Failed to copy URL'); ElMessage.error('复制 URL 失败');
}); });
}; };
// Check for query params on load (for direct linking) //
onMounted(() => { onMounted(() => {
const queryParams = new URLSearchParams(window.location.search); const queryParams = new URLSearchParams(window.location.search);
const data = queryParams.get('data'); const data = queryParams.get('data');
@ -175,15 +175,15 @@ onMounted(() => {
selectedWidget.value = decodedData.type; selectedWidget.value = decodedData.type;
currentWidgetConfig.value = decodedData.config; currentWidgetConfig.value = decodedData.config;
} catch (e) { } catch (e) {
ElMessage.error('Invalid configuration in URL'); ElMessage.error('URL 中的配置无效');
handleWidgetChange(); // Load default config handleWidgetChange(); //
} }
} else { } else {
handleWidgetChange(); // Load default config handleWidgetChange(); //
} }
}); });
// Update URL when configuration changes // URL
watch([selectedWidget, currentWidgetConfig], () => { watch([selectedWidget, currentWidgetConfig], () => {
updateGeneratedUrl(); updateGeneratedUrl();
}, { deep: true }); }, { deep: true });

View File

@ -2,8 +2,8 @@
<div class="home-view"> <div class="home-view">
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>OBS Overlay Widget</h1> <h1>OBS 悬浮小组件</h1>
<p>Create customizable widgets for OBS Studio streaming and recording</p> <p>OBS Studio 直播和录制场景创建可定制化小组件</p>
</div> </div>
<div class="cards"> <div class="cards">
@ -11,9 +11,9 @@
<div class="card-icon"> <div class="card-icon">
<el-icon><Setting /></el-icon> <el-icon><Setting /></el-icon>
</div> </div>
<div class="card-title">Configure Widgets</div> <div class="card-title">配置小组件</div>
<div class="card-description"> <div class="card-description">
Design and customize widgets for your OBS streams with an interactive interface 通过交互式界面设计和自定义 OBS 直播小组件
</div> </div>
</div> </div>
@ -21,53 +21,53 @@
<div class="card-icon"> <div class="card-icon">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
</div> </div>
<div class="card-title">Documentation</div> <div class="card-title">使用文档</div>
<div class="card-description"> <div class="card-description">
Learn how to use and integrate OBS Overlay Widgets into your streams 了解如何使用和集成 OBS 悬浮小组件到您的直播中
</div> </div>
</div> </div>
</div> </div>
<div class="features"> <div class="features">
<h2>Available Widgets</h2> <h2>可用小组件</h2>
<div class="widget-list"> <div class="widget-list">
<div class="widget-item"> <div class="widget-item">
<div class="widget-icon"></div> <div class="widget-icon"></div>
<div class="widget-info"> <div class="widget-info">
<h3>Clock Widget</h3> <h3>时钟小组件</h3>
<p>Display current time with customizable format, style, and effects</p> <p>显示当前时间可自定义格式样式和特效</p>
</div> </div>
</div> </div>
<div class="widget-item"> <div class="widget-item">
<div class="widget-icon">📅</div> <div class="widget-icon">📅</div>
<div class="widget-info"> <div class="widget-info">
<h3>Date Widget</h3> <h3>日期小组件</h3>
<p>Show current date with customizable format, style, and effects</p> <p>显示当前日期可自定义格式样式和特效</p>
</div> </div>
</div> </div>
<div class="widget-item"> <div class="widget-item">
<div class="widget-icon">📝</div> <div class="widget-icon">📝</div>
<div class="widget-info"> <div class="widget-info">
<h3>Text Widget</h3> <h3>文本小组件</h3>
<p>Display text with customizable styles including gradients, shadows, and fonts</p> <p>显示文本支持渐变阴影字体等自定义样式</p>
</div> </div>
</div> </div>
<div class="widget-item"> <div class="widget-item">
<div class="widget-icon">🖼</div> <div class="widget-icon">🖼</div>
<div class="widget-info"> <div class="widget-info">
<h3>Image Widget</h3> <h3>图片小组件</h3>
<p>Show images with customizable size, effects, and positioning</p> <p>显示图片可自定义大小特效和位置</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<p>OBS Overlay Widget &copy; 2025</p> <p>OBS 悬浮小组件 &copy; 2025</p>
</div> </div>
</div> </div>
</div> </div>
@ -84,7 +84,7 @@ const goToConfig = () => {
}; };
const goToDoc = () => { const goToDoc = () => {
// This would go to documentation in a real app //
window.open('https://github.com/yourusername/obs-overlay-widget', '_blank'); window.open('https://github.com/yourusername/obs-overlay-widget', '_blank');
}; };
</script> </script>

View File

@ -12,13 +12,13 @@
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { decodeConfig } from '../utils/configUtils'; import { decodeConfig } from '../utils/configUtils';
// Import widget components //
import ClockWidget from '../widgets/ClockWidget.vue'; import ClockWidget from '../widgets/ClockWidget.vue';
import DateWidget from '../widgets/DateWidget.vue'; import DateWidget from '../widgets/DateWidget.vue';
import TextWidget from '../widgets/TextWidget.vue'; import TextWidget from '../widgets/TextWidget.vue';
import ImageWidget from '../widgets/ImageWidget.vue'; import ImageWidget from '../widgets/ImageWidget.vue';
// Widget registry //
const widgetRegistry = { const widgetRegistry = {
'clock': ClockWidget, 'clock': ClockWidget,
'date': DateWidget, 'date': DateWidget,
@ -42,7 +42,7 @@ onMounted(() => {
widgetType.value = decodedData.type; widgetType.value = decodedData.type;
widgetConfig.value = decodedData.config; widgetConfig.value = decodedData.config;
} catch (e) { } catch (e) {
console.error('Invalid configuration in URL', e); console.error('URL 中的配置无效', e);
} }
} }
}); });

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="clock-widget" :style="clockStyle"> <div class="clock-widget" :style="clockStyle">
{{ currentTime }} <div>{{ currentTime }}</div>
<div v-if="props.config.showDate" :style="clockStyle">{{ currentDate }}</div>
</div> </div>
</template> </template>
@ -21,6 +22,8 @@ interface ClockConfig {
useGradient: boolean; useGradient: boolean;
gradientColors: string[]; gradientColors: string[];
showSeconds: boolean; showSeconds: boolean;
showDate: boolean;
dateFormat: string;
} }
// Define props with default values // Define props with default values
@ -38,18 +41,27 @@ const props = withDefaults(defineProps<{
shadowBlur: 4, shadowBlur: 4,
useGradient: false, useGradient: false,
gradientColors: ['#ff0000', '#0000ff'], gradientColors: ['#ff0000', '#0000ff'],
showSeconds: true showSeconds: true,
showDate: false,
dateFormat: 'YYYY-MM-DD'
}) })
}); });
// State for current time // State for current time and date
const currentTime = ref(''); const currentTime = ref('');
const currentDate = ref('');
// Update time string based on format // Update time string based on format
const updateTime = () => { const updateTime = () => {
const currentFormat = props.config.format || 'HH:mm:ss'; const currentFormat = props.config.format || 'HH:mm:ss';
const format = props.config.showSeconds ? currentFormat : currentFormat.replace(':ss', ''); const format = props.config.showSeconds ? currentFormat : currentFormat.replace(':ss', '');
currentTime.value = dayjs().format(format); currentTime.value = dayjs().format(format);
// Update date if enabled
if (props.config.showDate) {
const dateFormat = props.config.dateFormat || 'YYYY-MM-DD';
currentDate.value = dayjs().format(dateFormat);
}
}; };
// Interval for updating time // Interval for updating time
@ -71,7 +83,7 @@ onUnmounted(() => {
} }
}); });
// Update interval if showSeconds changes // Watch for showSeconds changes
watch(() => props.config.showSeconds, (newValue) => { watch(() => props.config.showSeconds, (newValue) => {
if (timeInterval !== null) { if (timeInterval !== null) {
window.clearInterval(timeInterval); window.clearInterval(timeInterval);
@ -82,6 +94,11 @@ watch(() => props.config.showSeconds, (newValue) => {
updateTime(); updateTime();
}); });
// Watch for showDate or dateFormat changes
watch([() => props.config.showDate, () => props.config.dateFormat], () => {
updateTime();
}, { deep: true });
// Computed styles for the clock // Computed styles for the clock
const clockStyle = computed(() => { const clockStyle = computed(() => {
const style: Record<string, string> = { const style: Record<string, string> = {
@ -116,5 +133,6 @@ const clockStyle = computed(() => {
padding: 10px; padding: 10px;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
user-select: none; user-select: none;
text-align: center;
} }
</style> </style>