完善提问模块

This commit is contained in:
2025-06-25 23:18:32 +08:00
parent 64f226c714
commit b845b5994a
6 changed files with 354 additions and 22 deletions

View File

@@ -1,3 +1,4 @@
import random
from typing import Optional
from openpyxl.reader.excel import load_workbook
@@ -8,8 +9,9 @@ from openpyxl.worksheet.worksheet import Worksheet
class PickerStudent:
total_time: int = 0
def __init__(self, name: str, position: int, scores: list[int]):
def __init__(self, name: str, so: str, position: int, scores: list[int]):
self._name = name
self._so = so
self._position = position
self._scores = scores
self._modify = False
@@ -43,6 +45,24 @@ class PickerStudent:
self._scores[index] = new_score
self.modified()
@staticmethod
def pick(student: list['PickerStudent']) -> Optional['PickerStudent']:
filtered = [item for item in student if item.weight != 100 and item.weight > 0]
if not filtered:
return None
# 计算倒数权重
weights = [1 / item.weight for item in filtered]
total = sum(weights)
r = random.uniform(0, total)
cumulative = 0
for item, w in zip(filtered, weights):
cumulative += w
if r <= cumulative:
return item
return None
@classmethod
def set_total_time(cls, total_time: int):
cls.total_time = total_time
@@ -63,6 +83,14 @@ class PickerStudent:
def weight(self) -> int:
return int(sum(1 for x in self._scores if x != 0) / self.total_time * 100)
@property
def name(self) -> str:
return self._name
@property
def so(self) -> str:
return self._so
def saved(self):
self._modify = False
@@ -80,7 +108,8 @@ class PickerExcel:
wb: Optional[Workbook]
ws: Optional[Worksheet]
path: str = ''
max_time_position = 'L1'
max_time_position = 'M1'
start_row = 5
def __init__(self, path: str):
self.open(path)
@@ -88,7 +117,7 @@ class PickerExcel:
@classmethod
def open(cls, path: str):
cls.path = path
cls.wb = load_workbook(path)
cls.wb = load_workbook(path, keep_vba=True)
cls.ws = cls.wb.active
PickerStudent.set_total_time(cls.ws[cls.max_time_position].value)
@@ -99,13 +128,40 @@ class PickerExcel:
if cls.wb and cls.ws:
ret = []
for row in cls.ws.iter_rows(min_row=4, max_col=4 + cls.ws[cls.max_time_position].value, values_only=True):
for index, row in enumerate(cls.ws.iter_rows(
min_row=cls.start_row,
max_col=4 + cls.ws[cls.max_time_position].value,
values_only=True)
):
if (name := row[2]) is None:
break
ret.append(PickerStudent(name, int(row[0]) + 3, row[4:]))
ret.append(PickerStudent(name, row[1], cls.start_row + index, row[4:]))
return ret
else:
raise Exception('No Workbook or Worksheet')
raise Exception('No Workbook or Worksheet')
@classmethod
def read_total_time(cls, path: Optional[str] = None) -> int:
if path:
cls.open(path)
if cls.wb and cls.ws:
val = cls.ws[cls.max_time_position].value
if isinstance(val, int):
return val
raise Exception(f'总次数读取错误,期待类型 {type(1)} 但实际为 {type(val)}\n'
f'可能的解决方法:\n'
f'1、确保总次数位于 \'{cls.max_time_position}\' 单元格;\n'
f'2、确保选择了正确的 Excel 文件。')
raise Exception('No Workbook or Worksheet')
@classmethod
def save_total_time(cls, path: Optional[str] = None, value: Optional[int] = None) -> None:
if path:
cls.open(path)
if cls.wb and cls.ws and value:
cls.ws[cls.max_time_position].value = value
cls.save()
@classmethod
def write_back(cls, student: PickerStudent):
@@ -116,6 +172,7 @@ class PickerExcel:
cls.ws.cell(row=row, column=start_col + i, value=val if val != 0 else '')
student.saved()
cls.save()
@classmethod
def write_all(cls, students: list[PickerStudent]):

View File

View File

@@ -1,4 +1,11 @@
from PySide6.QtWidgets import QFrame
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QFrame
from PySide6.QtCore import QTimer, Qt, Signal
import sys
import random
from qfluentwidgets import DisplayLabel
from module.picker.schema import PickerStudent
class Widget(QFrame):
@@ -7,3 +14,44 @@ class Widget(QFrame):
super().__init__(parent=parent)
# 必须给子界面设置全局唯一的对象名
self.setObjectName(key.replace(' ', '-'))
class RollingTextWidget(QWidget):
finishSignal = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.current_index = 0
self.items = []
self.label = DisplayLabel("", self)
self.label.setAlignment(Qt.AlignCenter)
self.layout = QVBoxLayout()
self.layout.addWidget(self.label)
self.setLayout(self.layout)
self.rolling_timer = QTimer(self)
self.rolling_timer.setInterval(50) # 滚动速度(毫秒)
self.rolling_timer.timeout.connect(self.update_text)
self.stop_timer = QTimer(self)
self.stop_timer.setSingleShot(True)
self.stop_timer.timeout.connect(self.stop_rolling)
def update_text(self):
# 每次显示下一个字符
self.current_index = (self.current_index + 1) % len(self.items)
self.label.setText(self.items[self.current_index].name)
def start_rolling(self):
if not self.rolling_timer.isActive():
self.rolling_timer.start()
self.stop_timer.start(2000) # 2秒后停止滚动
def stop_rolling(self):
self.rolling_timer.stop()
self.finishSignal.emit()
def set_items(self, items: list[PickerStudent]):
self.items = items

16
ui/components/window.py Normal file
View File

@@ -0,0 +1,16 @@
from PySide6.QtWidgets import QHBoxLayout
from qfluentwidgets import FluentTitleBar
from qfluentwidgets.window.fluent_window import FluentWindowBase
class MyWindow(FluentWindowBase):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitleBar(FluentTitleBar(self))
self.widgetLayout = QHBoxLayout(self)
self.widgetLayout.setContentsMargins(0, 48, 0, 0)
self.hBoxLayout.addLayout(self.widgetLayout)
self.titleBar.raise_()
self.showMaximized()

View File

@@ -20,21 +20,22 @@ class MainWindow(MSFluentWindow):
self.achievementInterface = AchievementWidget('Achievement Interface', self)
self.defenseInterface = DefenseWidget('Defense Interface', self)
self.aboutInterface = AboutWidget('About Interface', self)
self.pickerInterface = PickerWidget('Picker Interface', self)
if not DEVELOPMENT_ENV:
self.pickerInterface = PickerWidget('Picker Interface', self)
self.testInterface = TestWidget('Test Interface', self)
self.achievementInterface.error.connect(self.showError)
self.defenseInterface.errorSignal.connect(self.showError)
self.pickerInterface.errorSignal.connect(self.showError)
self.initNavigation()
self.initWindow()
def initNavigation(self):
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度')
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩')
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩题目')
self.addSubInterface(self.pickerInterface, FluentIcon.PEOPLE, '提问')
if not DEVELOPMENT_ENV:
self.addSubInterface(self.pickerInterface, FluentIcon.PEOPLE, '抽答')
self.addSubInterface(self.testInterface, FluentIcon.VIEW, '测试')
self.addSubInterface(self.aboutInterface, FluentIcon.INFO, '关于', position=NavigationItemPosition.BOTTOM)
@@ -45,7 +46,10 @@ class MainWindow(MSFluentWindow):
self.setWindowIcon(QIcon(':/images/logo.png'))
def showError(self, title: str, message: str):
MessageBox(title, message, self).exec()
box = MessageBox(title, message, self)
box.yesButton.setText("关闭")
box.cancelButton.hide()
box.exec()
def showEvent(self, event: QShowEvent):
super().showEvent(event)

View File

@@ -1,27 +1,132 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout
from qfluentwidgets import GroupHeaderCardWidget, PushButton, FluentIcon, PrimaryPushButton, IconWidget, BodyLabel
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QStackedWidget, QGridLayout, QFileDialog
from qfluentwidgets import GroupHeaderCardWidget, PushButton, FluentIcon, PrimaryPushButton, IconWidget, BodyLabel, \
SegmentedWidget, SpinBox, LargeTitleLabel, DisplayLabel, SubtitleLabel
from module.picker.schema import PickerExcel, PickerStudent
from ui.components.widget import RollingTextWidget
from ui import MAIN_THEME_COLOR
from ui.components.widget import Widget
from ui.components.window import MyWindow
class PickerWidget(Widget):
def __init__(self, key: str, parent=None):
super().__init__(key, parent)
class PSMMain(MyWindow):
finishSignal = Signal()
def __init__(self, student: PickerStudent, parent=None):
super().__init__(parent)
self.student = student
self.grade_edit = SpinBox(self)
self.init_layout()
def init_layout(self):
main_layout = QVBoxLayout()
hLayout = QHBoxLayout()
name_layout = QVBoxLayout()
date_layout = QVBoxLayout()
hLayout.addStretch()
hLayout.addLayout(name_layout)
hLayout.addLayout(date_layout)
hLayout.addStretch()
hLayout.setSpacing(20)
so = LargeTitleLabel(f"{self.student.so}", self)
so.setAlignment(Qt.AlignCenter)
sname = DisplayLabel(
f"{self.student.name[0] + ' ' + self.student.name[1] if len(self.student.name) == 2 else self.student.name}",
self)
sname.setAlignment(Qt.AlignCenter)
name_layout.addWidget(so)
name_layout.addWidget(sname)
self.grade_edit.setFixedWidth(260)
self.grade_edit.setRange(0, 100)
grade_layout = QHBoxLayout()
grade_layout.setContentsMargins(0, 0, 0, 48)
grade_layout.setSpacing(20)
submit_button = PrimaryPushButton("确定", self)
cancel_button = PushButton("重置", self)
submit_button.clicked.connect(self.close_modal)
cancel_button.clicked.connect(lambda: self.grade_edit.clear())
submit_button.setFixedWidth(120)
cancel_button.setFixedWidth(120)
grade_layout.addWidget(submit_button)
grade_layout.addWidget(cancel_button)
date_layout.addWidget(self.grade_edit)
date_layout.addLayout(grade_layout)
main_layout.addStretch()
main_layout.addLayout(hLayout)
main_layout.addStretch()
# 快速打分
quick_grade_layout = QVBoxLayout()
quick_grade_layout.setContentsMargins(0, 0, 0, 0)
quick_grade_layout.setSpacing(20)
quick_grade_title = SubtitleLabel("快速打分", self)
quick_grade_title.setAlignment(Qt.AlignCenter)
quick_grade_layout.addWidget(quick_grade_title)
grid_wrapper_layout = QHBoxLayout()
grid_layout = QGridLayout()
buttons = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
for i, num in enumerate(buttons):
row = i // 3
col = i % 3
btn = PushButton(str(num))
btn.setFixedSize(80, 80)
# 使用lambda绑定事件
btn.clicked.connect(lambda _, n=num: self.grade_edit.setValue(n))
grid_layout.addWidget(btn, row, col)
grid_wrapper_layout.addStretch()
grid_wrapper_layout.addLayout(grid_layout)
grid_wrapper_layout.addStretch()
quick_grade_layout.addLayout(grid_layout)
quick_grade_layout.addLayout(grid_wrapper_layout)
main_layout.addLayout(quick_grade_layout)
self.widgetLayout.addLayout(main_layout)
def close_modal(self):
self.student.append_score(self.grade_edit.value())
self.finishSignal.emit()
self.close()
class PickStudentMode(QWidget):
errorSignal = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.card = GroupHeaderCardWidget(self)
self.vbox = QVBoxLayout(self)
self.vbox.setContentsMargins(0, 0, 0, 0)
self.chooseBtn = PushButton("打开")
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "抽签")
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
self.bottomLayout = QHBoxLayout()
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
self.hintLabel = BodyLabel("点击抽签按钮以开始抽签 👉")
self.hintLabel = BodyLabel("点击开始按钮以开始抽签 👉")
self.spinbox = SpinBox()
self.rollingText = RollingTextWidget(self)
self.card.setTitle("输入选项")
self.chooseBtn.setFixedWidth(120)
self.startButton.setFixedWidth(120)
self.startButton.setEnabled(False)
self.spinbox.setRange(0, 6)
self.spinbox.setFixedWidth(120)
self.spinbox.setEnabled(False)
self.hintIcon.setFixedSize(16, 16)
self.hintIcon.autoFillBackground()
@@ -33,9 +138,111 @@ class PickerWidget(Widget):
self.bottomLayout.addWidget(self.startButton, 0, Qt.AlignRight)
self.bottomLayout.setAlignment(Qt.AlignVCenter)
self.group = self.card.addGroup(FluentIcon.DOCUMENT, "抽答名单", "选择抽答名单", self.chooseBtn)
self.group.setSeparatorVisible(True)
self.group = self.card.addGroup(FluentIcon.DOCUMENT, "提问名单", "选择提问名单", self.chooseBtn)
self.spinGroup = self.card.addGroup(FluentIcon.SETTING, "提问次数", "设置提问的最大次数", self.spinbox)
self.spinGroup.setSeparatorVisible(True)
self.card.vBoxLayout.addLayout(self.bottomLayout)
self.vbox.addWidget(self.card)
self.vbox.addWidget(self.rollingText)
self.vbox.addStretch(1)
# ==============================
self.chooseBtn.clicked.connect(self.choose_file)
self.spinbox.valueChanged.connect(lambda: PickerExcel.save_total_time(value=self.spinbox.value()))
self.startButton.clicked.connect(self.start_rolling)
self.rollingText.finishSignal.connect(self.finish_rolling)
# ==============================
self.filepath = ""
self.students = []
def choose_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsm);")
if file_path:
self.group.setContent("已选择文件:" + file_path)
self.filepath = file_path
self.startButton.setEnabled(True)
self.init_spinbox_value()
def init_spinbox_value(self):
if not self.filepath:
return
try:
PickerExcel.open(self.filepath)
self.spinbox.setValue(PickerExcel.read_total_time())
self.spinbox.setEnabled(True)
except Exception as e:
self.errorSignal.emit(str(e))
self.spinbox.setEnabled(False)
self.startButton.setEnabled(False)
def start_rolling(self):
self.students = PickerExcel.read_student()
self.rollingText.set_items(self.students)
self.rollingText.start_rolling()
self.startButton.setEnabled(False)
def finish_rolling(self):
stu = PickerStudent.pick(self.students)
if not (stu.so and stu.name):
self.errorSignal.emit("学生信息读取失败")
self.rollingText.label.setText(stu.name)
timer = QTimer(self)
timer.setSingleShot(True)
timer.timeout.connect(lambda: self.show_screen(stu))
timer.timeout.connect(lambda: self.startButton.setEnabled(True))
timer.start(1000)
def show_screen(self, stu: PickerStudent):
screen = PSMMain(stu)
screen.finishSignal.connect(lambda: PickerExcel.write_back(stu))
screen.finishSignal.connect(lambda: self.rollingText.label.clear())
class PickQuestionMode(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
lable = DisplayLabel('🚧', self)
lable.setAlignment(Qt.AlignCenter)
self.layout = QHBoxLayout(self)
self.layout.addStretch()
self.layout.addWidget(lable)
self.layout.addStretch()
class PickerWidget(Widget):
errorSignal = Signal(str, str)
def __init__(self, key: str, parent=None):
super().__init__(key, parent)
self.vbox = QVBoxLayout(self)
self.menu = SegmentedWidget(self)
self.stack = QStackedWidget(self)
self.psm = PickStudentMode(self)
self.pqm = PickQuestionMode(self)
self.add_sub_interface(self.psm, "pickStudentMode", "选人模式")
self.add_sub_interface(self.pqm, "pickQuestionMode", "选题模式")
self.menu.setCurrentItem("pickStudentMode")
self.vbox.addWidget(self.menu)
self.vbox.addWidget(self.stack)
self.vbox.addStretch(1)
# ===========================
self.stack.currentChanged.connect(self.on_stack_index_changed)
self.psm.errorSignal.connect(lambda n: self.errorSignal.emit("😢 不好出错了", n))
def add_sub_interface(self, widget: QWidget, obj_name: str, text: str):
widget.setObjectName(obj_name)
self.stack.addWidget(widget)
self.menu.addItem(routeKey=obj_name, text=text, onClick=lambda: self.stack.setCurrentWidget(widget))
def on_stack_index_changed(self, index: int):
widget = self.stack.widget(index)
self.stack.setCurrentWidget(widget)