文档完善

This commit is contained in:
hxuanyu 2025-06-26 00:25:43 +08:00
parent de73a041b1
commit 03a19beafa
4 changed files with 378 additions and 116 deletions

242
README.md
View File

@ -4,27 +4,63 @@
## 功能特点
- **时间和日期显示**:可自定义格式的时间和日期显示
- **文本显示**:展示固定文字或 API 返回内容,支持自定义样式
- **图片显示**:展示本地或远程图片,支持自定义设置
- **多种小组件**:内置时钟、日期、文本、图片等多种可配置小组件
- **动态首页**:自动展示所有已注册小组件,便于预览和选择
- **分屏界面**:左侧为配置面板,右侧为实时预览
- **透明背景**:所有小组件均具有适合 OBS 悬浮的透明背景
- **URL 生成**:自动生成包含编码配置的可分享 URL
- **纯预览模式**:打开生成的 URL 仅显示小组件内容,无配置界面,背景透明
- **可扩展性**:集中化的小组件注册机制,便于添加新的小组件类型
## 小组件类型
1. **时钟小组件**:显示当前时间,可自定义格式、样式和特效
1. **时钟小组件**:显示当前时间,可自定义格式、样式和特效,支持显示日期
2. **日期小组件**:显示当前日期,可自定义格式、样式和特效
3. **文本小组件**:显示文本,支持渐变、阴影、字体等自定义样式
4. **图片小组件**:显示图片,可自定义大小、特效和位置
## 使用方法
1. 从下拉菜单中选择小组件类型
2. 使用左侧控制面板配置小组件
3. 在右侧实时查看预览效果
4. 复制生成的 URL在 OBS Studio 中作为浏览器源使用
1. 在首页浏览并选择需要的小组件类型
2. 在配置页面中,从下拉菜单中选择小组件类型
3. 使用左侧控制面板配置小组件属性和样式
4. 在右侧实时查看预览效果
5. 复制生成的 URL在 OBS Studio 中作为浏览器源使用
## 小组件配置详解
### 时钟小组件
- **时间格式**:支持 24 小时制 (HH:mm:ss) 和 12 小时制 (hh:mm:ss A)
- **显示秒**:切换是否显示秒数
- **显示日期**:切换是否在时钟下方显示日期
- **日期格式**:多种日期格式可选,如 YYYY-MM-DD、MM/DD/YYYY 等
- **样式设置**
- 字体大小、字体类型、字重
- 文本颜色或渐变色
- 文字阴影及模糊度
### 日期小组件
- **日期格式**:多种格式可选,支持年月日不同顺序排列
- **显示星期**:切换是否显示星期几
- **样式设置**:与时钟小组件类似,支持字体、颜色和阴影自定义
### 文本小组件
- **文本内容**:自定义显示的文字内容
- **样式设置**
- 字体大小、字体类型、字重
- 文本颜色或渐变色
- 文字阴影及模糊度
### 图片小组件
- **图片 URL**:输入远程图片的 URL
- **尺寸设置**:自定义宽度和高度
- **透明度**:调整图片透明度
- **圆角**:调整图片圆角半径
- **阴影效果**:添加阴影及调整阴影颜色和模糊度
## 开发
@ -42,6 +78,196 @@ npm run build
npm run preview
```
## 项目结构
```
src/
├── assets/ # 静态资源
├── components/ # 组件
│ └── config/ # 小组件配置面板组件
│ ├── ClockConfig.vue
│ ├── DateConfig.vue
│ ├── TextConfig.vue
│ └── ImageConfig.vue
├── router/ # 路由配置
├── utils/ # 工具函数
│ └── configUtils.ts # 配置编码/解码工具
├── views/ # 页面视图
│ ├── HomeView.vue # 主页
│ ├── ConfigView.vue # 配置页面
│ └── PreviewView.vue# 预览页面
├── widgets/ # 小组件实现
│ ├── registry.ts # 小组件注册中心
│ ├── ClockWidget.vue
│ ├── DateWidget.vue
│ ├── TextWidget.vue
│ └── ImageWidget.vue
├── App.vue # 应用入口组件
└── main.ts # 应用入口文件
```
## 实现原理
本项目基于 Vue 3 组合式 API 和 TypeScript 构建,核心实现包括:
1. **小组件系统**:每个小组件都是独立的 Vue 组件,通过 props 接收配置
2. **配置系统**:每种小组件对应一个配置组件,使用 v-model 进行双向绑定
3. **集中注册机制**:通过 `registry.ts` 统一管理所有小组件,便于扩展和维护
4. **动态首页展示**:自动从注册表中读取小组件信息展示在首页,通过映射添加图标和描述
5. **URL 参数传递**:使用 Base64 编码将配置数据序列化到 URL 中
6. **预览模式**:通过 URL 参数判断是否处于纯预览模式,隐藏配置界面
7. **响应式更新**:使用 Vue 的响应式系统实现配置变更时的实时预览
## 最近更新
- **界面全面中文化**:所有小组件界面、配置面板、提示文本已全部中文化
- **时钟小组件增强**:时钟小组件支持显示日期功能,样式与时间保持一致
- **动态首页改进**:首页自动读取注册的小组件列表,支持内容滚动
- **小组件注册中心**:新增 `registry.ts` 作为小组件注册中心,统一管理
- **响应式布局优化**:改进移动端和小屏幕适配体验
## 如何添加新小组件
要添加新的小组件,需要以下步骤:
1. **创建小组件组件**:在 `src/widgets/` 目录下创建新的 Vue 组件,如 `NewWidget.vue`
```vue
<template>
<div class="new-widget" :style="widgetStyle">
<!-- 小组件内容 -->
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
// 定义配置接口
interface NewWidgetConfig {
// 添加所需配置属性
property1: string;
property2: number;
// ...
}
// 定义 props注意包含默认值和类型
const props = withDefaults(defineProps<{
config: Partial<NewWidgetConfig>;
}>(), {
config: () => ({
property1: 'default',
property2: 0,
// ...提供默认值
})
});
// 计算样式
const widgetStyle = computed(() => {
// 返回基于配置的样式对象
return {
// ...
};
});
</script>
<style scoped>
.new-widget {
/* 基础样式 */
}
</style>
```
1. **创建配置组件**:在 `src/components/config/` 目录下创建对应的配置组件,如 `NewConfig.vue`
```vue
<template>
<div class="new-config">
<h2>新小组件设置</h2>
<el-form label-position="top">
<!-- 配置表单项 -->
<el-form-item label="属性1">
<el-input v-model="localConfig.property1" />
</el-form-item>
<el-form-item label="属性2">
<el-slider v-model="localConfig.property2" :min="0" :max="100" show-input />
</el-form-item>
<!-- 更多配置项 -->
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// 定义配置接口
interface NewWidgetConfig {
property1: string;
property2: number;
// ...
}
// 定义 props
const props = withDefaults(defineProps<{
config: Partial<NewWidgetConfig>;
}>(), {
config: () => ({
property1: 'default',
property2: 0,
// ...
})
});
// 定义 emit
const emit = defineEmits<{
(event: 'update:config', config: NewWidgetConfig): void;
}>();
// 本地配置用于双向绑定
const localConfig = ref<NewWidgetConfig>({
property1: 'default',
property2: 0,
// ...
});
// 同步初始配置
onMounted(() => {
localConfig.value = { ...localConfig.value, ...props.config };
});
// 监听本地变更并发送更新
watch(localConfig, (newConfig) => {
emit('update:config', { ...newConfig });
}, { deep: true });
</script>
```
1. **注册新小组件**:在 `src/widgets/registry.ts` 中添加新小组件
```typescript
// 导入新组件
import NewWidget from './NewWidget.vue';
import NewConfig from '../components/config/NewConfig.vue';
// 小组件注册表
export const widgets = [
// 现有小组件...
{ label: '新小组件', value: 'new', component: NewWidget, configComponent: NewConfig },
];
// 在 getDefaultConfig 函数中添加默认配置
case 'new':
return {
property1: 'default',
property2: 0,
// ...
};
```
添加完成后,新小组件会自动出现在首页的可用小组件列表中,并可在配置页面中使用。
## 故障排除
如果遇到与未定义属性相关的 TypeScript 错误,请确保:

View File

@ -41,24 +41,8 @@ import { CopyDocument } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { encodeConfig, decodeConfig } from '../utils/configUtils';
//
import ClockWidget from '../widgets/ClockWidget.vue';
import DateWidget from '../widgets/DateWidget.vue';
import TextWidget from '../widgets/TextWidget.vue';
import ImageWidget from '../widgets/ImageWidget.vue';
//
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: '时钟小组件', value: 'clock', component: ClockWidget, configComponent: ClockConfig },
{ label: '日期小组件', value: 'date', component: DateWidget, configComponent: DateConfig },
{ label: '文本小组件', value: 'text', component: TextWidget, configComponent: TextConfig },
{ label: '图片小组件', value: 'image', component: ImageWidget, configComponent: ImageConfig },
];
//
import { widgets, getDefaultConfig } from '../widgets/registry';
const selectedWidget = ref('clock');
const currentWidgetConfig = ref({});
@ -76,66 +60,6 @@ const currentConfigComponent = computed(() => {
return widget?.configComponent;
});
//
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);

View File

@ -32,35 +32,11 @@
<h2>可用小组件</h2>
<div class="widget-list">
<div class="widget-item">
<div class="widget-icon"></div>
<div v-for="widget in widgets" :key="widget.value" class="widget-item">
<div class="widget-icon">{{ widget.icon }}</div>
<div class="widget-info">
<h3>时钟小组件</h3>
<p>显示当前时间可自定义格式样式和特效</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">📅</div>
<div class="widget-info">
<h3>日期小组件</h3>
<p>显示当前日期可自定义格式样式和特效</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">📝</div>
<div class="widget-info">
<h3>文本小组件</h3>
<p>显示文本支持渐变阴影字体等自定义样式</p>
</div>
</div>
<div class="widget-item">
<div class="widget-icon">🖼</div>
<div class="widget-info">
<h3>图片小组件</h3>
<p>显示图片可自定义大小特效和位置</p>
<h3>{{ widget.label }}</h3>
<p>{{ widget.description }}</p>
</div>
</div>
</div>
@ -76,16 +52,58 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { Setting, Document } from '@element-plus/icons-vue';
import { ref, onMounted } from 'vue';
//
import { widgets as registeredWidgets } from '../widgets/registry';
const router = useRouter();
//
interface WidgetItem {
value: string;
label: string;
icon: string;
description: string;
}
const widgets = ref<WidgetItem[]>([]);
//
const widgetIcons = {
'clock': '⏰',
'date': '📅',
'text': '📝',
'image': '🖼️',
//
};
//
const widgetDescriptions = {
'clock': '显示当前时间,可自定义格式、样式和特效',
'date': '显示当前日期,可自定义格式、样式和特效',
'text': '显示文本,支持渐变、阴影、字体等自定义样式',
'image': '显示图片,可自定义大小、特效和位置',
//
};
//
onMounted(() => {
widgets.value = registeredWidgets.map((widget: any) => ({
value: widget.value as string,
label: widget.label as string,
icon: widgetIcons[widget.value as keyof typeof widgetIcons] || '🔧', //
description: widgetDescriptions[widget.value as keyof typeof widgetDescriptions] || '自定义小组件'
}));
});
const goToConfig = () => {
router.push('/config');
};
const goToDoc = () => {
//
window.open('https://github.com/yourusername/obs-overlay-widget', '_blank');
window.open('https://github.com/hanxuanyu/obs-overlay-widget', '_blank');
};
</script>
@ -94,11 +112,15 @@ const goToDoc = () => {
min-height: 100vh;
background-color: #f5f7fa;
padding: 20px;
/* 添加溢出滚动,确保内容可以完整显示 */
overflow-y: auto;
}
.container {
max-width: 1200px;
margin: 0 auto;
/* 添加底部间距,确保页脚有足够空间 */
padding-bottom: 30px;
}
.header {
@ -175,8 +197,10 @@ const goToDoc = () => {
.widget-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
/* 确保底部有足够的边距 */
margin-bottom: 30px;
}
.widget-item {
@ -185,6 +209,13 @@ const goToDoc = () => {
border-radius: 10px;
padding: 20px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
/* 添加过渡效果 */
transition: transform 0.3s, box-shadow 0.3s;
}
.widget-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.12);
}
.widget-icon {

81
src/widgets/registry.ts Normal file
View File

@ -0,0 +1,81 @@
// 小组件组件及其配置
import ClockWidget from './ClockWidget.vue';
import DateWidget from './DateWidget.vue';
import TextWidget from './TextWidget.vue';
import ImageWidget from './ImageWidget.vue';
// 配置组件
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';
// 小组件注册表
export const widgets = [
{ label: '时钟小组件', value: 'clock', component: ClockWidget, configComponent: ClockConfig },
{ label: '日期小组件', value: 'date', component: DateWidget, configComponent: DateConfig },
{ label: '文本小组件', value: 'text', component: TextWidget, configComponent: TextConfig },
{ label: '图片小组件', value: 'image', component: ImageWidget, configComponent: ImageConfig },
];
// 获取小组件默认配置
export 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,
showDate: false,
dateFormat: 'YYYY-MM-DD'
};
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 {};
}
};