Initial commit: 述职计时器

This commit is contained in:
2026-01-21 18:21:56 +08:00
commit 7efff7d62e
4 changed files with 869 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
dist/
*.spec
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

BIN
Timer.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

832
countdown.py Normal file
View File

@@ -0,0 +1,832 @@
"""
述职计时器 - 倒计时应用程序
使用 PySide6 构建的美观倒计时器
"""
import sys
import json
from pathlib import Path
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QDialog, QSpinBox, QCheckBox, QSlider, QFormLayout, QGroupBox
)
from PySide6.QtCore import QTimer, Qt, QPropertyAnimation, QEasingCurve, Property
from PySide6.QtGui import QFont
from PySide6.QtMultimedia import QSoundEffect, QAudioOutput, QMediaPlayer
from PySide6.QtCore import QUrl
class ConfigDialog(QDialog):
"""配置对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("其它设置")
self.setModal(True)
self.resize(400, 300)
layout = QFormLayout()
# 自定义倒计时时间(秒)
self.custom_time_spin = QSpinBox()
self.custom_time_spin.setRange(1, 3600)
self.custom_time_spin.setValue(300)
self.custom_time_spin.setSuffix("")
layout.addRow("自定义倒计时:", self.custom_time_spin)
# 提前告警时间(秒)
self.alert_time_spin = QSpinBox()
self.alert_time_spin.setRange(0, 600)
self.alert_time_spin.setValue(60)
self.alert_time_spin.setSuffix("")
layout.addRow("提前告警:", self.alert_time_spin)
# 置顶选项
self.topmost_checkbox = QCheckBox()
self.topmost_checkbox.setChecked(True)
layout.addRow("窗口置顶:", self.topmost_checkbox)
# 透明度滑块
self.opacity_slider = QSlider(Qt.Horizontal)
self.opacity_slider.setRange(10, 100)
self.opacity_slider.setValue(90)
self.opacity_slider.setTickPosition(QSlider.TicksBelow)
self.opacity_slider.setTickInterval(10)
self.opacity_label = QLabel("90%")
self.opacity_slider.valueChanged.connect(
lambda v: self.opacity_label.setText(f"{v}%")
)
opacity_layout = QHBoxLayout()
opacity_layout.addWidget(self.opacity_slider)
opacity_layout.addWidget(self.opacity_label)
layout.addRow("窗口透明度:", opacity_layout)
# 测试告警按钮
self.test_alert_button = QPushButton("测试告警声音")
self.test_alert_button.clicked.connect(self.test_alert_sound)
self.test_alert_button.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
layout.addRow("", self.test_alert_button)
# 按钮
button_layout = QHBoxLayout()
self.ok_button = QPushButton("确定")
self.ok_button.clicked.connect(self.accept)
self.ok_button.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
self.cancel_button = QPushButton("取消")
self.cancel_button.clicked.connect(self.reject)
self.cancel_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #da190b;
}
""")
self.exit_button = QPushButton("退出")
self.exit_button.clicked.connect(self.exit_application)
self.exit_button.setStyleSheet("""
QPushButton {
background-color: #95a5a6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #7f8c8d;
}
""")
button_layout.addWidget(self.ok_button)
button_layout.addWidget(self.cancel_button)
button_layout.addWidget(self.exit_button)
layout.addRow(button_layout)
self.setLayout(layout)
# 样式
self.setStyleSheet("""
QDialog {
background-color: #f5f5f5;
}
QLabel {
font-size: 13px;
color: #333;
}
QSpinBox {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
}
""")
def test_alert_sound(self):
"""测试告警声音"""
try:
QApplication.beep()
# 循环播放2次
QTimer.singleShot(500, QApplication.beep)
QTimer.singleShot(1000, QApplication.beep)
except Exception as e:
print(f"播放测试告警音失败: {e}")
def exit_application(self):
"""退出应用程序"""
self.accept() # 关闭对话框
# 退出整个应用程序
QApplication.quit()
class CountdownTimer(QMainWindow):
"""主倒计时器窗口"""
def __init__(self):
super().__init__()
# 配置
self.config_file = Path.home() / ".countdown_timer_config.json"
self.load_config()
# 状态
self.remaining_seconds = 0
self.total_seconds = 0
self.is_running = False
self.alert_played = False
self.alert_loop_count = 0
# 定时器
self.timer = QTimer()
self.timer.timeout.connect(self.update_timer)
# 闪烁动画定时器
self.blink_timer = QTimer()
self.blink_timer.timeout.connect(self.toggle_blink)
self.blink_state = True
# 音频播放器(用于告警提示音)
self.media_player = None
self.init_ui()
self.apply_config()
# 鼠标位置跟踪(用于窗口移动)
self.drag_position = None
# 状态管理
self.current_state = "normal" # normal 或 mini
self.state_change_timer = None
def mousePressEvent(self, event):
"""鼠标按下事件 - 开始移动窗口"""
if event.button() == Qt.LeftButton:
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
"""鼠标移动事件 - 移动窗口"""
if self.drag_position is not None and event.buttons() == Qt.LeftButton:
self.move(event.globalPosition().toPoint() - self.drag_position)
event.accept()
def mouseReleaseEvent(self, event):
"""鼠标释放事件 - 停止移动窗口"""
if event.button() == Qt.LeftButton:
self.drag_position = None
event.accept()
def load_config(self):
"""加载配置"""
default_config = {
"alert_seconds": 60,
"topmost": True,
"opacity": 90,
"custom_seconds": 300
}
if self.config_file.exists():
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
self.config = json.load(f)
# 合并默认值
for key, value in default_config.items():
if key not in self.config:
self.config[key] = value
except:
self.config = default_config
else:
self.config = default_config
def save_config(self):
"""保存配置"""
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.config, f, ensure_ascii=False, indent=2)
except:
pass
def apply_config(self):
"""应用配置"""
# 透明度
self.setWindowOpacity(self.config["opacity"] / 100.0)
# 置顶
if self.config["topmost"]:
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
else:
self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
self.show()
# 保存初始窗口位置(等待窗口显示完成)
QTimer.singleShot(100, self.save_initial_position)
def save_initial_position(self):
"""保存初始窗口位置"""
self.initial_pos = self.pos()
def init_ui(self):
"""初始化UI"""
# 设置无边框窗口
self.setWindowFlags(Qt.FramelessWindowHint)
# 设置最小窗口大小(初始状态)
self.setMinimumSize(250, 100)
# 设置初始窗口大小(倒计时模式)
self.resize(500, 300)
# 中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(30, 30, 30, 30)
# 倒计时显示
self.time_label = QLabel("00:00")
self.time_label.setAlignment(Qt.AlignCenter)
self.time_label.setStyleSheet("""
QLabel {
font-size: 72px;
font-weight: bold;
color: #3498db;
font-family: 'Courier New', monospace;
background-color: #ecf0f1;
border-radius: 10px;
padding: 20px;
}
""")
main_layout.addWidget(self.time_label)
# 完成提示标签(初始隐藏)
self.finish_label = QLabel("时间已到")
self.finish_label.setAlignment(Qt.AlignCenter)
self.finish_label.setStyleSheet("""
QLabel {
font-size: 48px;
font-weight: bold;
color: #e74c3c;
background-color: #fee;
border-radius: 10px;
padding: 20px;
}
""")
self.finish_label.hide()
main_layout.addWidget(self.finish_label)
# 创建可折叠的按钮区域
self.button_group = QGroupBox()
self.button_group.setStyleSheet("""
QGroupBox {
border: 2px solid #bdc3c7;
border-radius: 8px;
margin-top: 10px;
padding-top: 10px;
background-color: #f8f9fa;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
# 折叠/展开按钮
self.collapse_button = QPushButton("")
self.collapse_button.setFixedSize(30, 30)
self.collapse_button.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
self.collapse_button.clicked.connect(self.toggle_button_group)
# 按钮布局
button_layout = QHBoxLayout()
button_layout.setSpacing(15)
# 创建按钮
self.btn_5min = self.create_button("5分钟", lambda: self.start_countdown(300))
self.btn_6min = self.create_button("6分钟", lambda: self.start_countdown(360))
self.btn_other = self.create_button("其它", self.open_config, "#95a5a6")
button_layout.addWidget(self.btn_5min)
button_layout.addWidget(self.btn_6min)
button_layout.addWidget(self.btn_other)
# 控制按钮
control_layout = QHBoxLayout()
self.btn_pause = self.create_button("暂停", self.pause_countdown, "#f39c12")
self.btn_reset = self.create_button("重置", self.reset_countdown, "#e74c3c")
self.btn_pause.setEnabled(False)
self.btn_reset.setEnabled(False)
control_layout.addWidget(self.btn_pause)
control_layout.addWidget(self.btn_reset)
# 添加到按钮组
group_layout = QVBoxLayout()
group_layout.addLayout(button_layout)
group_layout.addLayout(control_layout)
self.button_group.setLayout(group_layout)
# 主布局添加折叠按钮和按钮组
main_layout.addWidget(self.collapse_button)
main_layout.addWidget(self.button_group)
# 默认折叠按钮区
self.button_group.hide()
self.collapse_button.setText("")
central_widget.setLayout(main_layout)
# 窗口样式
self.setStyleSheet("""
QMainWindow {
background-color: #ffffff;
}
""")
def create_button(self, text, callback, color="#3498db"):
"""创建样式化按钮"""
button = QPushButton(text)
button.clicked.connect(callback)
button.setMinimumHeight(20) # 减小按钮高度
button.setMinimumWidth(50) # 设置最小宽度
button.setStyleSheet(f"""
QPushButton {{
background-color: {color};
color: white;
border: none;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
padding: 2px 5px;
}}
QPushButton:hover {{
background-color: {self.darken_color(color)};
}}
QPushButton:pressed {{
background-color: {self.darken_color(color, 0.3)};
}}
QPushButton:disabled {{
background-color: #bdc3c7;
}}
""")
return button
def darken_color(self, hex_color, factor=0.15):
"""使颜色变暗"""
hex_color = hex_color.lstrip('#')
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
r = int(r * (1 - factor))
g = int(g * (1 - factor))
b = int(b * (1 - factor))
return f"#{r:02x}{g:02x}{b:02x}"
def start_countdown(self, seconds):
"""开始倒计时"""
self.remaining_seconds = seconds
self.total_seconds = seconds
self.is_running = True
self.alert_played = False
self.alert_loop_count = 0
# 折叠按钮区域
self.button_group.hide()
self.collapse_button.setText("")
# 设置2秒后切换到微缩状态的定时器
if self.state_change_timer:
self.state_change_timer.stop()
self.state_change_timer = QTimer()
self.state_change_timer.timeout.connect(self.switch_to_mini_state)
self.state_change_timer.start(2000) # 2秒后切换
# 立即切换到微缩状态
self.switch_to_mini_state()
self.timer.start(1000)
self.update_display()
# 隐藏完成标签,显示倒计时
self.finish_label.hide()
self.time_label.show()
self.blink_timer.stop()
# 更新按钮状态
self.btn_pause.setEnabled(True)
self.btn_pause.setText("暂停")
self.btn_reset.setEnabled(True)
def pause_countdown(self):
"""暂停/继续倒计时"""
if self.is_running:
self.timer.stop()
self.is_running = False
self.btn_pause.setText("继续")
else:
self.timer.start(1000)
self.is_running = True
self.btn_pause.setText("暂停")
def reset_countdown(self):
"""重置倒计时"""
self.timer.stop()
self.blink_timer.stop()
self.is_running = False
self.remaining_seconds = 0
self.alert_played = False
self.alert_loop_count = 0
# 恢复原始窗口大小
self.resize(500, 300)
# 恢复窗口置顶状态
self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
self.show()
self.time_label.setText("00:00")
self.time_label.show()
self.finish_label.hide()
self.btn_pause.setEnabled(False)
self.btn_pause.setText("暂停")
self.btn_reset.setEnabled(False)
# 停止音频
if self.media_player:
self.media_player.stop()
def switch_to_mini_state(self):
"""切换到微缩状态"""
self.current_state = "mini"
# 调整窗口大小为微缩模式200x80像素增加高度以容纳倒计时
self.resize(200, 80)
# 移动窗口位置到距离top 50px距离右边200px
screen = QApplication.primaryScreen().geometry()
screen_width = screen.width()
screen_height = screen.height()
new_x = screen_width - 200 - 200 # 距离右边200px
new_y = 50 # 距离top 50px
self.move(new_x, new_y)
# 隐藏按钮区域
self.button_group.hide()
self.collapse_button.setText("")
# 调整布局边距以适应微缩状态
central_widget = self.centralWidget()
main_layout = central_widget.layout()
main_layout.setSpacing(5)
main_layout.setContentsMargins(10, 10, 10, 10)
# 调整倒计时标签大小以适应微缩状态
self.time_label.setStyleSheet("""
QLabel {
font-size: 12px;
font-weight: bold;
color: #3498db;
font-family: 'Courier New', monospace;
background-color: #ecf0f1;
border-radius: 3px;
padding: 2px;
}
""")
# 隐藏完成提示标签
self.finish_label.hide()
# 确保窗口在最前面
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
self.show()
def switch_to_normal_state(self):
"""切换到正常状态"""
self.current_state = "normal"
# 取消2秒定时器如果存在
if self.state_change_timer and self.state_change_timer.isActive():
self.state_change_timer.stop()
# 恢复原始窗口大小
self.resize(500, 300)
# 恢复初始窗口位置
if hasattr(self, 'initial_pos') and self.initial_pos.x() >= 0 and self.initial_pos.y() >= 0:
self.move(self.initial_pos)
# 恢复窗口置顶状态
self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
self.show()
# 恢复布局边距
central_widget = self.centralWidget()
main_layout = central_widget.layout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(30, 30, 30, 30)
# 恢复倒计时标签的正常样式
self.time_label.setStyleSheet("""
QLabel {
font-size: 72px;
font-weight: bold;
color: #3498db;
font-family: 'Courier New', monospace;
background-color: #ecf0f1;
border-radius: 10px;
padding: 20px;
}
""")
# 如果按钮区域之前是展开的,重新展开
if not self.button_group.isHidden():
self.button_group.hide()
self.collapse_button.setText("")
def update_timer(self):
"""更新计时器"""
if self.remaining_seconds > 0:
self.remaining_seconds -= 1
self.update_display()
# 检查是否需要播放告警
if (not self.alert_played and
self.remaining_seconds <= self.config["alert_seconds"] and
self.remaining_seconds > 0):
self.play_alert()
self.alert_played = True
else:
# 倒计时结束
self.timer.stop()
self.is_running = False
self.show_finish()
def update_display(self):
"""更新显示"""
minutes = self.remaining_seconds // 60
seconds = self.remaining_seconds % 60
self.time_label.setText(f"{minutes:02d}:{seconds:02d}")
# 根据剩余时间改变颜色
if self.remaining_seconds <= 10:
self.time_label.setStyleSheet("""
QLabel {
font-size: 72px;
font-weight: bold;
color: #e74c3c;
font-family: 'Courier New', monospace;
background-color: #fee;
border-radius: 10px;
padding: 20px;
}
""")
elif self.remaining_seconds <= self.config["alert_seconds"]:
self.time_label.setStyleSheet("""
QLabel {
font-size: 72px;
font-weight: bold;
color: #f39c12;
font-family: 'Courier New', monospace;
background-color: #fef5e7;
border-radius: 10px;
padding: 20px;
}
""")
else:
self.time_label.setStyleSheet("""
QLabel {
font-size: 72px;
font-weight: bold;
color: #3498db;
font-family: 'Courier New', monospace;
background-color: #ecf0f1;
border-radius: 10px;
padding: 20px;
}
""")
def play_alert(self):
"""播放告警提示音"""
# 使用系统提示音
# 注意:这里使用一个简单的实现
# 实际使用中,你可以替换为自定义音频文件
try:
# 尝试播放系统提示音
QApplication.beep()
# 设置循环播放2次总共3次
self.alert_loop_count = 0
self.alert_timer = QTimer()
self.alert_timer.timeout.connect(self.play_alert_loop)
self.alert_timer.start(1000) # 每秒播放一次
except Exception as e:
print(f"播放告警音失败: {e}")
def play_alert_loop(self):
"""循环播放告警"""
if self.alert_loop_count < 2:
QApplication.beep()
self.alert_loop_count += 1
else:
self.alert_timer.stop()
def show_finish(self):
"""显示完成提示"""
# 调整窗口大小为缩小模式150x50像素
self.resize(150, 50)
# 移动窗口位置到距离top 30px距离右边边缘30px
screen = QApplication.primaryScreen().geometry()
screen_width = screen.width()
screen_height = screen.height()
new_x = screen_width - 150 - 30 # 距离右边30px
new_y = 30 # 距离top 30px
self.move(new_x, new_y)
# 确保窗口在最前面
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
self.show()
self.time_label.hide()
self.finish_label.show()
# 开始闪烁动画
self.blink_state = True
self.blink_timer.start(500) # 每500ms切换一次
# 播放提示音
QApplication.beep()
self.btn_pause.setEnabled(False)
def toggle_blink(self):
"""切换闪烁状态"""
if self.blink_state:
self.finish_label.setStyleSheet("""
QLabel {
font-size: 48px;
font-weight: bold;
color: #e74c3c;
background-color: #fee;
border-radius: 10px;
padding: 20px;
}
""")
else:
self.finish_label.setStyleSheet("""
QLabel {
font-size: 52px;
font-weight: bold;
color: #c0392b;
background-color: #fadbd8;
border-radius: 10px;
padding: 25px;
}
""")
self.blink_state = not self.blink_state
def toggle_button_group(self):
"""切换按钮区域的折叠/展开状态"""
# 如果当前是微缩状态,点击箭头暂停倒计时并切换回正常状态
if self.current_state == "mini":
# 暂停倒计时
self.timer.stop()
self.is_running = False
self.btn_pause.setText("暂停")
# 切换回正常状态
self.switch_to_normal_state()
return
# 如果当前是正常状态
if self.button_group.isHidden():
self.button_group.show()
self.collapse_button.setText("")
# 如果正在运行倒计时,恢复原始窗口大小
if self.is_running:
self.switch_to_normal_state()
else:
self.button_group.hide()
self.collapse_button.setText("")
# 如果正在运行倒计时,暂停倒计时并切换到微缩状态
if self.is_running:
# 暂停倒计时
self.timer.stop()
self.is_running = False
# 切换到微缩状态
self.switch_to_mini_state()
# 按钮文本保持为"暂停",因为倒计时已经被暂停
self.btn_pause.setText("暂停")
def open_config(self):
"""打开配置对话框"""
dialog = ConfigDialog(self)
# 加载当前配置
dialog.custom_time_spin.setValue(self.config["custom_seconds"])
dialog.alert_time_spin.setValue(self.config["alert_seconds"])
dialog.topmost_checkbox.setChecked(self.config["topmost"])
dialog.opacity_slider.setValue(self.config["opacity"])
if dialog.exec() == QDialog.Accepted:
# 保存配置
self.config["custom_seconds"] = dialog.custom_time_spin.value()
self.config["alert_seconds"] = dialog.alert_time_spin.value()
self.config["topmost"] = dialog.topmost_checkbox.isChecked()
self.config["opacity"] = dialog.opacity_slider.value()
self.save_config()
self.apply_config()
# 启动自定义倒计时
self.start_countdown(self.config["custom_seconds"])
def main():
"""主函数"""
app = QApplication(sys.argv)
# 设置应用程序样式
app.setStyle("Fusion")
window = CountdownTimer()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

18
要求.txt Normal file
View File

@@ -0,0 +1,18 @@
目的写一个python语言的pyside6库作为界面的应用程序。
功能:倒计时显示和提醒。
界面需求:整体要美观大方。
1. 窗口无标题栏,可以通过点击并拖动任何位置移动窗口
2. 倒计时显示区域:显示分和秒的倒计时,字体大且醒目
3. 界面分为2个状态
a. 正常状态:显示时间,点击箭头,显示按钮区域,再点击箭头,按钮区域折叠
b. 微缩状态:点击"5分钟"后延迟2秒界面改为微缩状态只有时间区域和向下箭头
4. 微缩状态详情:
- 窗口长为200px高为60px
- 距离top为50px距离屏幕右边留出空白200px的距离
- 点击向下箭头,回到正常状态的位置和大小
5. 快捷按钮:第一个按钮是"5分钟",第二个按钮是"6分钟",第三个按钮是"其它"
6. 其它按钮功能:打开配置页面,可以设置自定义倒计时时间、提前告警时间、窗口置顶选项、透明度设置
7. 告警功能倒计时结束前设置的秒数时播放告警提示音循环3次
8. 倒计时结束后:显示"时间已到"文字,带有跳动和闪动效果
9. 配置对话框:增加退出按钮,只有通过此按钮才能退出程序
10. 测试要求使用pyqt-test库作为gui程序的测试工具。