添加计时器小组件,支持倒计时和正计时功能,包含丰富的样式和配置选项
This commit is contained in:
parent
5af0b3396e
commit
f7ffeb54c2
34
README.md
34
README.md
@ -18,6 +18,7 @@
|
||||
2. **日期小组件**:显示当前日期,可自定义格式、样式和特效
|
||||
3. **文本小组件**:显示文本,支持渐变、阴影、字体等自定义样式
|
||||
4. **图片小组件**:显示图片,可自定义大小、特效和位置
|
||||
5. **计时器小组件**:支持倒计时和正计时功能,具有丰富的样式和提示功能
|
||||
|
||||
## 使用方法
|
||||
|
||||
@ -62,6 +63,34 @@
|
||||
- **圆角**:调整图片圆角半径
|
||||
- **阴影效果**:添加阴影及调整阴影颜色和模糊度
|
||||
|
||||
### 计时器小组件
|
||||
|
||||
- **计时器模式**:
|
||||
- **倒计时**:设定时长,从指定时间倒数至零
|
||||
- **正计时**:从零开始向上计时(秒表功能)
|
||||
- **时间设置**:
|
||||
- 倒计时模式:可设置分钟和秒数
|
||||
- 时间格式:支持 mm:ss 和 hh:mm:ss 格式
|
||||
- 毫秒显示:可选择是否显示毫秒
|
||||
- **控制功能**:
|
||||
- 自动开始:组件加载后自动开始计时
|
||||
- 手动控制:开始、暂停、重置按钮
|
||||
- 点击切换:点击计时器进行状态切换
|
||||
- **倒计时专属功能**:
|
||||
- **警告设置**:设置警告阈值和警告颜色
|
||||
- **进度条**:显示剩余时间进度,可自定义颜色和高度
|
||||
- **音效提示**:倒计时结束播放提示音,可调节音量
|
||||
- **结束颜色**:倒计时结束时的特殊颜色显示
|
||||
- **样式设置**:
|
||||
- 字体大小、字体类型、字重
|
||||
- 文本颜色或渐变色
|
||||
- 文字阴影及模糊度
|
||||
- **预设样式**:
|
||||
- 游戏风格:适合游戏直播的醒目样式
|
||||
- 会议计时:适合会议和演讲的专业样式
|
||||
- 运动计时:适合健身和运动的动感样式
|
||||
- 简约风格:简洁的极简主义设计
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
@ -100,6 +129,11 @@ src/
|
||||
│ ├── date/ # 日期小组件模块
|
||||
│ ├── text/ # 文本小组件模块
|
||||
│ ├── image/ # 图片小组件模块
|
||||
│ ├── timer/ # 计时器小组件模块
|
||||
│ │ ├── index.ts # 模块导出
|
||||
│ │ ├── types.ts # 类型定义
|
||||
│ │ ├── Widget.vue # 小组件组件
|
||||
│ │ └── Config.vue # 配置组件
|
||||
│ └── registry.ts # 小组件注册中心
|
||||
```
|
||||
|
||||
|
@ -3,6 +3,7 @@ import ClockWidget from './clock';
|
||||
import DateWidget from './date';
|
||||
import TextWidget from './text';
|
||||
import ImageWidget from './image';
|
||||
import TimerWidget from './timer';
|
||||
|
||||
|
||||
// 导入类型定义
|
||||
@ -14,6 +15,7 @@ export const widgets: WidgetRegistration[] = [
|
||||
DateWidget,
|
||||
TextWidget,
|
||||
ImageWidget,
|
||||
TimerWidget,
|
||||
];
|
||||
|
||||
// 获取小组件显示信息列表(用于 UI 展示)
|
||||
|
360
src/widgets/timer/Config.vue
Normal file
360
src/widgets/timer/Config.vue
Normal file
@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="timer-config">
|
||||
<h2>计时器小组件设置</h2>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-divider>预设样式</el-divider>
|
||||
|
||||
<el-form-item>
|
||||
<el-button-group>
|
||||
<el-button type="primary" @click="applyPreset('gaming')">游戏风格</el-button>
|
||||
<el-button type="success" @click="applyPreset('meeting')">会议计时</el-button>
|
||||
<el-button type="warning" @click="applyPreset('workout')">运动计时</el-button>
|
||||
<el-button type="danger" @click="applyPreset('minimal')">简约风格</el-button>
|
||||
</el-button-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider>基本设置</el-divider>
|
||||
|
||||
<el-form-item label="计时器模式">
|
||||
<el-radio-group v-model="localConfig.mode">
|
||||
<el-radio value="countdown">倒计时</el-radio>
|
||||
<el-radio value="stopwatch">正计时</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="localConfig.mode === 'countdown'" label="倒计时时长">
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<el-input-number
|
||||
v-model="durationMinutes"
|
||||
:min="0"
|
||||
:max="999"
|
||||
controls-position="right"
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span>分钟</span>
|
||||
<el-input-number
|
||||
v-model="durationSeconds"
|
||||
:min="0"
|
||||
:max="59"
|
||||
controls-position="right"
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span>秒</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<span>自动开始</span>
|
||||
<el-switch v-model="localConfig.autoStart" style="margin-left: 10px;" />
|
||||
</div>
|
||||
<div>
|
||||
<span>显示毫秒</span>
|
||||
<el-switch v-model="localConfig.showMilliseconds" style="margin-left: 10px;" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="时间格式">
|
||||
<el-select v-model="localConfig.format" placeholder="选择格式">
|
||||
<el-option label="mm:ss (分:秒)" value="mm:ss" />
|
||||
<el-option label="hh:mm:ss (时:分:秒)" value="hh:mm:ss" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider>文本样式</el-divider>
|
||||
|
||||
<el-form-item label="字体大小">
|
||||
<el-slider v-model="localConfig.fontSize" :min="12" :max="120" show-input />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字体">
|
||||
<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-option label="JetBrains Mono" value="'JetBrains Mono'" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字重">
|
||||
<el-select v-model="localConfig.fontWeight">
|
||||
<el-option label="普通" value="normal" />
|
||||
<el-option label="粗体" value="bold" />
|
||||
<el-option label="细体" value="lighter" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider>颜色设置</el-divider>
|
||||
|
||||
<el-form-item label="使用渐变色">
|
||||
<el-switch v-model="localConfig.useGradient" />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="!localConfig.useGradient">
|
||||
<el-form-item label="文本颜色">
|
||||
<el-color-picker v-model="localConfig.color" show-alpha />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-form-item label="渐变起始颜色">
|
||||
<el-color-picker v-model="localConfig.gradientColors[0]" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="渐变结束颜色">
|
||||
<el-color-picker v-model="localConfig.gradientColors[1]" show-alpha />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item v-if="localConfig.mode === 'countdown'" label="结束颜色">
|
||||
<el-color-picker v-model="localConfig.finishedColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="localConfig.mode === 'countdown'">
|
||||
<el-divider>警告设置</el-divider>
|
||||
|
||||
<el-form-item label="警告阈值(秒)">
|
||||
<el-input-number
|
||||
v-model="localConfig.warningThreshold"
|
||||
:min="0"
|
||||
:max="3600"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="警告颜色">
|
||||
<el-color-picker v-model="localConfig.warningColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider>进度条设置</el-divider>
|
||||
|
||||
<el-form-item label="显示进度条">
|
||||
<el-switch v-model="localConfig.showProgress" />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="localConfig.showProgress">
|
||||
<el-form-item label="进度条颜色">
|
||||
<el-color-picker v-model="localConfig.progressColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="进度条高度">
|
||||
<el-slider v-model="localConfig.progressHeight" :min="1" :max="20" show-input />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-divider>音效设置</el-divider>
|
||||
|
||||
<el-form-item label="播放提示音">
|
||||
<el-switch v-model="localConfig.playSound" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="localConfig.playSound" label="音量">
|
||||
<el-slider v-model="localConfig.soundVolume" :min="0" :max="100" show-input />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-divider>特效</el-divider>
|
||||
|
||||
<el-form-item label="文字阴影">
|
||||
<el-switch v-model="localConfig.textShadow" />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="localConfig.textShadow">
|
||||
<el-form-item label="阴影颜色">
|
||||
<el-color-picker v-model="localConfig.shadowColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="阴影模糊度">
|
||||
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import type { TimerConfig } from './types';
|
||||
|
||||
// Define props with default values
|
||||
const props = withDefaults(defineProps<{
|
||||
config: Partial<TimerConfig>;
|
||||
}>(), {
|
||||
config: () => ({
|
||||
mode: 'countdown',
|
||||
duration: 300,
|
||||
autoStart: false,
|
||||
showMilliseconds: false,
|
||||
format: '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'],
|
||||
finishedColor: '#ff0000',
|
||||
playSound: true,
|
||||
soundVolume: 50,
|
||||
warningThreshold: 30,
|
||||
warningColor: '#ff6b35',
|
||||
showProgress: true,
|
||||
progressColor: '#3498db',
|
||||
progressHeight: 4
|
||||
})
|
||||
});
|
||||
|
||||
// Define emit
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:config', config: TimerConfig): void;
|
||||
}>();
|
||||
|
||||
// Local config for two-way binding
|
||||
const localConfig = ref<TimerConfig>({
|
||||
mode: 'countdown',
|
||||
duration: 300,
|
||||
autoStart: false,
|
||||
showMilliseconds: false,
|
||||
format: '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'],
|
||||
finishedColor: '#ff0000',
|
||||
playSound: true,
|
||||
soundVolume: 50,
|
||||
warningThreshold: 30,
|
||||
warningColor: '#ff6b35',
|
||||
showProgress: true,
|
||||
progressColor: '#3498db',
|
||||
progressHeight: 4
|
||||
});
|
||||
|
||||
// 持续时间的分钟和秒数
|
||||
const durationMinutes = computed({
|
||||
get: () => Math.floor((localConfig.value.duration || 0) / 60),
|
||||
set: (value: number) => {
|
||||
const seconds = (localConfig.value.duration || 0) % 60;
|
||||
localConfig.value.duration = value * 60 + seconds;
|
||||
}
|
||||
});
|
||||
|
||||
const durationSeconds = computed({
|
||||
get: () => (localConfig.value.duration || 0) % 60,
|
||||
set: (value: number) => {
|
||||
const minutes = Math.floor((localConfig.value.duration || 0) / 60);
|
||||
localConfig.value.duration = minutes * 60 + value;
|
||||
}
|
||||
});
|
||||
|
||||
// Sync with parent config on mount
|
||||
onMounted(() => {
|
||||
// Merge default config with provided config
|
||||
localConfig.value = { ...localConfig.value, ...props.config };
|
||||
});
|
||||
|
||||
// Preset styles
|
||||
const presets = {
|
||||
gaming: {
|
||||
mode: 'countdown',
|
||||
duration: 1800, // 30分钟
|
||||
format: 'mm:ss',
|
||||
fontSize: 64,
|
||||
fontFamily: 'Impact',
|
||||
fontWeight: 'bold',
|
||||
useGradient: true,
|
||||
gradientColors: ['#ff6b35', '#f7931e'],
|
||||
textShadow: true,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.8)',
|
||||
shadowBlur: 8,
|
||||
showProgress: true,
|
||||
progressColor: '#ff6b35',
|
||||
progressHeight: 6,
|
||||
warningThreshold: 60,
|
||||
warningColor: '#ff0000',
|
||||
playSound: true,
|
||||
soundVolume: 80
|
||||
},
|
||||
meeting: {
|
||||
mode: 'countdown',
|
||||
duration: 3600, // 60分钟
|
||||
format: 'hh:mm:ss',
|
||||
fontSize: 52,
|
||||
fontFamily: 'Arial',
|
||||
fontWeight: 'normal',
|
||||
color: '#2ecc71',
|
||||
useGradient: false,
|
||||
textShadow: true,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 4,
|
||||
showProgress: true,
|
||||
progressColor: '#2ecc71',
|
||||
progressHeight: 4,
|
||||
warningThreshold: 300,
|
||||
warningColor: '#f39c12',
|
||||
finishedColor: '#e74c3c',
|
||||
playSound: true,
|
||||
soundVolume: 60
|
||||
},
|
||||
workout: {
|
||||
mode: 'stopwatch',
|
||||
format: 'mm:ss',
|
||||
fontSize: 72,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontWeight: 'bold',
|
||||
useGradient: true,
|
||||
gradientColors: ['#e74c3c', '#c0392b'],
|
||||
textShadow: true,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
shadowBlur: 6,
|
||||
showMilliseconds: true,
|
||||
autoStart: false
|
||||
},
|
||||
minimal: {
|
||||
mode: 'countdown',
|
||||
duration: 300,
|
||||
format: 'mm:ss',
|
||||
fontSize: 48,
|
||||
fontFamily: 'Helvetica',
|
||||
fontWeight: 'lighter',
|
||||
color: '#ffffff',
|
||||
useGradient: false,
|
||||
textShadow: false,
|
||||
showProgress: false,
|
||||
playSound: false,
|
||||
autoStart: 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>
|
||||
.timer-config {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
354
src/widgets/timer/Widget.vue
Normal file
354
src/widgets/timer/Widget.vue
Normal file
@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="timer-widget" :style="containerStyle">
|
||||
<div class="timer-display" :style="timerStyle" @click="toggleTimer">
|
||||
{{ displayTime }}
|
||||
</div>
|
||||
<div
|
||||
v-if="props.config.showProgress && props.config.mode === 'countdown'"
|
||||
class="progress-bar"
|
||||
:style="progressBarStyle"
|
||||
>
|
||||
<div class="progress-fill" :style="progressFillStyle"></div>
|
||||
</div>
|
||||
<div class="timer-controls" v-if="!props.config.autoStart">
|
||||
<button @click="startTimer" :disabled="status === 'running'" class="control-btn start">
|
||||
{{ status === 'paused' ? '继续' : '开始' }}
|
||||
</button>
|
||||
<button @click="pauseTimer" :disabled="status !== 'running'" class="control-btn pause">
|
||||
暂停
|
||||
</button>
|
||||
<button @click="resetTimer" class="control-btn reset">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import type { TimerConfig, TimerStatus, TimerMode } from './types';
|
||||
|
||||
// Define props with default values
|
||||
const props = withDefaults(defineProps<{
|
||||
config: Partial<TimerConfig>;
|
||||
}>(), {
|
||||
config: () => ({
|
||||
mode: 'countdown',
|
||||
duration: 300,
|
||||
autoStart: false,
|
||||
showMilliseconds: false,
|
||||
format: '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'],
|
||||
finishedColor: '#ff0000',
|
||||
playSound: true,
|
||||
soundVolume: 50,
|
||||
warningThreshold: 30,
|
||||
warningColor: '#ff6b35',
|
||||
showProgress: true,
|
||||
progressColor: '#3498db',
|
||||
progressHeight: 4
|
||||
})
|
||||
});
|
||||
|
||||
// 状态管理
|
||||
const status = ref<TimerStatus>('stopped');
|
||||
const currentTime = ref(0); // 当前时间(秒)
|
||||
const startTime = ref(0);
|
||||
const pausedTime = ref(0);
|
||||
|
||||
// 计时器相关
|
||||
let animationFrame: number | null = null;
|
||||
|
||||
// 计算显示时间
|
||||
const displayTime = computed(() => {
|
||||
const time = Math.max(0, currentTime.value);
|
||||
const totalSeconds = Math.floor(time);
|
||||
const milliseconds = Math.floor((time % 1) * 1000);
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
const format = props.config.format || 'mm:ss';
|
||||
|
||||
if (format.includes('hh')) {
|
||||
const timeStr = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
return props.config.showMilliseconds ? `${timeStr}.${milliseconds.toString().padStart(3, '0')}` : timeStr;
|
||||
} else {
|
||||
const timeStr = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
return props.config.showMilliseconds ? `${timeStr}.${milliseconds.toString().padStart(3, '0')}` : timeStr;
|
||||
}
|
||||
});
|
||||
|
||||
// 计算当前颜色
|
||||
const currentColor = computed(() => {
|
||||
if (status.value === 'finished') {
|
||||
return props.config.finishedColor || '#ff0000';
|
||||
}
|
||||
|
||||
if (props.config.mode === 'countdown' && props.config.warningThreshold) {
|
||||
if (currentTime.value <= props.config.warningThreshold && currentTime.value > 0) {
|
||||
return props.config.warningColor || '#ff6b35';
|
||||
}
|
||||
}
|
||||
|
||||
return props.config.color || '#ffffff';
|
||||
});
|
||||
|
||||
// 进度条样式
|
||||
const progressBarStyle = computed(() => ({
|
||||
width: '100%',
|
||||
height: `${props.config.progressHeight || 4}px`,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
borderRadius: '2px',
|
||||
marginTop: '8px',
|
||||
overflow: 'hidden'
|
||||
}));
|
||||
|
||||
const progressFillStyle = computed(() => {
|
||||
const progress = props.config.mode === 'countdown'
|
||||
? (currentTime.value / (props.config.duration || 300)) * 100
|
||||
: 0;
|
||||
|
||||
return {
|
||||
width: `${Math.max(0, Math.min(100, progress))}%`,
|
||||
height: '100%',
|
||||
backgroundColor: props.config.progressColor || '#3498db',
|
||||
transition: 'width 0.1s ease'
|
||||
};
|
||||
});
|
||||
|
||||
// 容器样式
|
||||
const containerStyle = computed(() => ({
|
||||
textAlign: 'center',
|
||||
userSelect: 'none'
|
||||
}));
|
||||
|
||||
// 计时器显示样式
|
||||
const timerStyle = computed(() => {
|
||||
const style: Record<string, string> = {
|
||||
fontSize: `${props.config.fontSize || 48}px`,
|
||||
fontFamily: props.config.fontFamily || 'Arial',
|
||||
fontWeight: props.config.fontWeight || 'normal',
|
||||
cursor: props.config.autoStart ? 'default' : 'pointer',
|
||||
marginBottom: '10px'
|
||||
};
|
||||
|
||||
// 应用渐变或纯色
|
||||
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 = currentColor.value;
|
||||
}
|
||||
|
||||
// 应用文字阴影
|
||||
if (props.config.textShadow) {
|
||||
style.textShadow = `0 0 ${props.config.shadowBlur || 4}px ${props.config.shadowColor || 'rgba(0,0,0,0.5)'}`;
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
// 更新计时器
|
||||
const updateTimer = () => {
|
||||
if (status.value !== 'running') return;
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = (now - startTime.value + pausedTime.value) / 1000;
|
||||
|
||||
if (props.config.mode === 'countdown') {
|
||||
currentTime.value = (props.config.duration || 300) - elapsed;
|
||||
|
||||
if (currentTime.value <= 0) {
|
||||
currentTime.value = 0;
|
||||
status.value = 'finished';
|
||||
playFinishSound();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
currentTime.value = elapsed;
|
||||
}
|
||||
|
||||
animationFrame = requestAnimationFrame(updateTimer);
|
||||
};
|
||||
|
||||
// 播放结束声音
|
||||
const playFinishSound = () => {
|
||||
if (!props.config.playSound) return;
|
||||
|
||||
// 创建音频上下文和生成蜂鸣声
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
|
||||
gainNode.gain.setValueAtTime((props.config.soundVolume || 50) / 100, audioContext.currentTime);
|
||||
|
||||
oscillator.start();
|
||||
oscillator.stop(audioContext.currentTime + 0.2);
|
||||
} catch (error) {
|
||||
console.warn('无法播放提示音:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 控制函数
|
||||
const startTimer = () => {
|
||||
if (status.value === 'stopped') {
|
||||
// 重新开始
|
||||
if (props.config.mode === 'countdown') {
|
||||
currentTime.value = props.config.duration || 300;
|
||||
} else {
|
||||
currentTime.value = 0;
|
||||
}
|
||||
pausedTime.value = 0;
|
||||
}
|
||||
|
||||
startTime.value = Date.now();
|
||||
status.value = 'running';
|
||||
updateTimer();
|
||||
};
|
||||
|
||||
const pauseTimer = () => {
|
||||
if (status.value === 'running') {
|
||||
status.value = 'paused';
|
||||
pausedTime.value += Date.now() - startTime.value;
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetTimer = () => {
|
||||
status.value = 'stopped';
|
||||
pausedTime.value = 0;
|
||||
|
||||
if (props.config.mode === 'countdown') {
|
||||
currentTime.value = props.config.duration || 300;
|
||||
} else {
|
||||
currentTime.value = 0;
|
||||
}
|
||||
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTimer = () => {
|
||||
if (props.config.autoStart) return;
|
||||
|
||||
if (status.value === 'running') {
|
||||
pauseTimer();
|
||||
} else if (status.value === 'paused' || status.value === 'stopped') {
|
||||
startTimer();
|
||||
} else if (status.value === 'finished') {
|
||||
resetTimer();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听配置变化
|
||||
watch(() => props.config.duration, (newDuration) => {
|
||||
if (status.value === 'stopped' && props.config.mode === 'countdown') {
|
||||
currentTime.value = newDuration || 300;
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.config.mode, (newMode) => {
|
||||
resetTimer();
|
||||
});
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
if (props.config.mode === 'countdown') {
|
||||
currentTime.value = props.config.duration || 300;
|
||||
} else {
|
||||
currentTime.value = 0;
|
||||
}
|
||||
|
||||
if (props.config.autoStart) {
|
||||
startTimer();
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.timer-widget {
|
||||
display: inline-block;
|
||||
padding: 15px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.timer-display {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.timer-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.control-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.control-btn.start {
|
||||
background: rgba(76, 175, 80, 0.8);
|
||||
}
|
||||
|
||||
.control-btn.pause {
|
||||
background: rgba(255, 193, 7, 0.8);
|
||||
}
|
||||
|
||||
.control-btn.reset {
|
||||
background: rgba(244, 67, 54, 0.8);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
14
src/widgets/timer/index.ts
Normal file
14
src/widgets/timer/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import Widget from './Widget.vue';
|
||||
import Config from './Config.vue';
|
||||
import { defaultConfig } from './types';
|
||||
import { createWidget } from '../createWidget';
|
||||
|
||||
export default createWidget({
|
||||
label: '计时器小组件',
|
||||
value: 'timer',
|
||||
icon: '⏱️',
|
||||
description: '支持倒计时和正计时功能,可自定义样式、进度条和提示音',
|
||||
component: Widget,
|
||||
configComponent: Config,
|
||||
defaultConfig
|
||||
});
|
57
src/widgets/timer/types.ts
Normal file
57
src/widgets/timer/types.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// 计时器模式
|
||||
export type TimerMode = 'countdown' | 'stopwatch';
|
||||
|
||||
// 计时器状态
|
||||
export type TimerStatus = 'stopped' | 'running' | 'paused' | 'finished';
|
||||
|
||||
// 计时器小组件配置接口
|
||||
export interface TimerConfig {
|
||||
mode: TimerMode;
|
||||
duration: number; // 倒计时持续时间(秒)
|
||||
autoStart: boolean; // 是否自动开始
|
||||
showMilliseconds: boolean; // 是否显示毫秒
|
||||
format: string; // 时间显示格式
|
||||
color: string;
|
||||
fontSize: number;
|
||||
fontWeight: string;
|
||||
fontFamily: string;
|
||||
textShadow: boolean;
|
||||
shadowColor: string;
|
||||
shadowBlur: number;
|
||||
useGradient: boolean;
|
||||
gradientColors: string[];
|
||||
finishedColor: string; // 倒计时结束时的颜色
|
||||
playSound: boolean; // 是否播放提示音
|
||||
soundVolume: number; // 音量 (0-100)
|
||||
warningThreshold: number; // 警告阈值(秒)
|
||||
warningColor: string; // 警告颜色
|
||||
showProgress: boolean; // 是否显示进度条
|
||||
progressColor: string; // 进度条颜色
|
||||
progressHeight: number; // 进度条高度
|
||||
}
|
||||
|
||||
// 计时器小组件默认配置
|
||||
export const defaultConfig: TimerConfig = {
|
||||
mode: 'countdown',
|
||||
duration: 300, // 5分钟
|
||||
autoStart: false,
|
||||
showMilliseconds: false,
|
||||
format: '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'],
|
||||
finishedColor: '#ff0000',
|
||||
playSound: true,
|
||||
soundVolume: 50,
|
||||
warningThreshold: 30,
|
||||
warningColor: '#ff6b35',
|
||||
showProgress: true,
|
||||
progressColor: '#3498db',
|
||||
progressHeight: 4
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user