Update Progress(Time o…

本文解决了在使用MyEclipse进行应用部署或redeploy操作时出现的UpdateProgress(Timeoferror)问题。通过删除特定配置文件即可修复此问题。

报错信息:

当使用MyEclipse部署应用或执行redeploy时,会狂报Update Progress(Time of error)问题Update Progress(Time of error)问题,detail中没有更详细的信息。

解决方法:

D:\MyEclipse 6.5 \eclipse\configuration\org.eclipse.update下面把last.config.stamp删除掉,就ok了。(重启MyEclipse会自动重新生成个该文件)

#下面程序运行地报错: 分析过程中发生错误 启动分析线程失败: 'MultiModalApp' object has no attribute 'update_progress' Traceback (most recent call last): File "D:\PyCharmMiscProject\2025-08-10——Rag高度增能.py", line 1044, in _analyze_image self.analysis_thread.progress_updated.connect(self.update_progress) ^^^^^^^^^^^^^^^^^^^^ AttributeError: 'MultiModalApp' object has no attribute 'update_progress' —————————————————————————————————————————————————————————————————————————————— import sys import os import base64 import json import requests import webbrowser import traceback import psutil from datetime import datetime from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QFileDialog, QGroupBox, QSlider, QDoubleSpinBox, QProgressBar, QSplitter, QMessageBox, QListWidget, QListWidgetItem, QSystemTrayIcon, QMenu, QAction, QComboBox, QTabWidget, QScrollArea, QCheckBox, QInputDialog, QLineEdit, QStyle, QSizePolicy ) from PyQt5.QtGui import ( QPixmap, QFont, QPalette, QColor, QTextCursor, QIcon, QTextDocumentWriter, QTextDocument, QImage, QPainter ) from PyQt5.QtCore import ( Qt, QSize, QThread, pyqtSignal, QTimer, QSettings, QMutex, QCoreApplication, QBuffer ) from PIL import Image, ImageOps # 配置OLLAMA API设置 OLLAMA_HOST = "http://localhost:11434" HISTORY_FILE = "history.json" SETTINGS_FILE = "settings.ini" MAX_COMPRESSED_SIZE = 1024 # 最大压缩尺寸 MAX_THREADS = 2 # 最大并发线程数 # 全局锁防止资源冲突 analysis_mutex = QMutex() class ModelLoaderThread(QThread): models_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def run(self): try: response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=10) response.raise_for_status() models_data = response.json() models = [] for model in models_data.get("models", []): # 保留完整的模型名称(包含冒号) model_name = model["name"] models.append(model_name) self.models_loaded.emit(models) except Exception as e: self.error_occurred.emit(f"模型加载失败: {str(e)}") class ImageAnalysisThread(QThread): analysis_complete = pyqtSignal(str, str) # 结果, 图片路径 progress_updated = pyqtSignal(int) error_occurred = pyqtSignal(str) stream_data = pyqtSignal(str) def __init__(self, model_name, image_path, temperature, max_tokens, prompt, parent=None): super().__init__(parent) self.model_name = model_name self.image_path = image_path self.temperature = temperature self.max_tokens = max_tokens self.prompt = prompt self._is_running = True def run(self): # 获取锁,防止多个线程同时访问资源 analysis_mutex.lock() try: # 检查图片文件是否存在 if not os.path.exists(self.image_path): self.error_occurred.emit(f"图片文件不存在: {self.image_path}") return # 压缩图片 compressed_path = self.compress_image(self.image_path) if not compressed_path: compressed_path = self.image_path # 读取并编码图片为base64 with open(compressed_path, "rb") as image_file: base64_image = base64.b64encode(image_file.read()).decode("utf-8") # 构建请求数据 data = { "model": self.model_name, "prompt": self.prompt, "images": [base64_image], "stream": True, "options": { "temperature": self.temperature, "num_predict": self.max_tokens } } # 发送请求并处理流式响应 response = requests.post( f"{OLLAMA_HOST}/api/generate", json=data, stream=True, timeout=60 ) response.raise_for_status() full_response = "" for line in response.iter_lines(): if not self._is_running: break if line: decoded_line = line.decode("utf-8") try: json_data = json.loads(decoded_line) if "response" in json_data: chunk = json_data["response"] full_response += chunk self.stream_data.emit(chunk) if "done" in json_data and json_data["done"]: break if "error" in json_data: self.error_occurred.emit(json_data["error"]) return except json.JSONDecodeError: self.error_occurred.emit("无效的API响应") return if self._is_running: self.analysis_complete.emit(full_response, self.image_path) except Exception as e: error_msg = f"API调用失败: {str(e)}\n\n{traceback.format_exc()}" self.error_occurred.emit(error_msg) finally: # 确保无论发生什么都会释放锁 analysis_mutex.unlock() # 删除临时压缩文件 if compressed_path != self.image_path and os.path.exists(compressed_path): try: os.remove(compressed_path) except: pass def compress_image(self, image_path): """压缩图片以减小内存占用""" try: # 检查文件大小 file_size = os.path.getsize(image_path) / (1024 * 1024) # MB if file_size < 1: # 小于1MB不需要压缩 return image_path # 打开图片 img = Image.open(image_path) # 如果图片尺寸过大,调整尺寸 if max(img.size) > MAX_COMPRESSED_SIZE: img.thumbnail((MAX_COMPRESSED_SIZE, MAX_COMPRESSED_SIZE), Image.LANCZOS) # 创建临时文件路径 temp_path = f"temp_compressed_{os.path.basename(image_path)}" # 保存压缩后的图片 if image_path.lower().endswith('.png'): img.save(temp_path, optimize=True, quality=85, format='PNG') else: img.save(temp_path, optimize=True, quality=85) return temp_path except Exception as e: print(f"图片压缩失败: {str(e)}") return image_path def stop(self): self._is_running = False class ExportThread(QThread): export_finished = pyqtSignal(str, bool) def __init__(self, content, file_path, format_type, parent=None): super().__init__(parent) self.content = content self.file_path = file_path self.format_type = format_type def run(self): try: if self.format_type == "html": with open(self.file_path, "w", encoding="utf-8") as f: f.write(self.content) elif self.format_type == "txt": with open(self.file_path, "w", encoding="utf-8") as f: f.write(self.content) elif self.format_type == "pdf": doc = QTextDocument() doc.setHtml(self.content) writer = QTextDocumentWriter(self.file_path) writer.write(doc) self.export_finished.emit(self.file_path, True) except Exception as e: self.export_finished.emit(str(e), False) class MultiModalApp(QMainWindow): def __init__(self): super().__init__() self.image_paths = [] self.current_image_index = 0 self.history = [] self.active_threads = 0 self.settings = QSettings(SETTINGS_FILE, QSettings.IniFormat) self.initUI() self.load_settings() self.setWindowTitle("增强版多模态大模型图像解读系统") self.setGeometry(100, 100, 1920, 1000) # 初始化系统托盘 self.init_tray_icon() # 创建资源监控定时器 self.resource_timer = QTimer(self) self.resource_timer.timeout.connect(self.monitor_resources) self.resource_timer.start(5000) # 每5秒监控一次 QTimer.singleShot(500, self.load_models) def initUI(self): # 创建暗色主题样式表 self.setStyleSheet(""" /* 主窗口样式 */ QMainWindow { background-color: #0a192f; } /* 分组框样式 */ QGroupBox { border: 2px solid #64ffda; border-radius: 10px; margin-top: 1ex; color: #ccd6f6; font-weight: bold; } /* 标签样式 */ QLabel { color: #ccd6f6; } /* 按钮样式 */ QPushButton { background-color: #112240; color: #64ffda; border: 1px solid #64ffda; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QPushButton:disabled { background-color: #0d1b30; color: #4a8f7c; border: 1px solid #4a8f7c; } /* 文本框样式 */ QTextEdit { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; padding: 5px; font-size: 12pt; } /* 选项卡样式 */ QTabWidget::pane { border: 1px solid #64ffda; border-radius: 5px; background: #0a192f; } QTabBar::tab { background: #112240; color: #ccd6f6; padding: 8px; border: 1px solid #64ffda; border-bottom: none; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background: #233554; color: #64ffda; } /* 列表样式 */ QListWidget { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #233554; color: #64ffda; } /* 进度条样式 */ QProgressBar { border: 1px solid #64ffda; border-radius: 5px; text-align: center; background-color: #0a192f; color: #64ffda; } QProgressBar::chunk { background-color: #64ffda; width: 10px; } /* 状态标签样式 */ #statusLabel { color: #64ffda; font-weight: bold; padding: 2px 5px; border-radius: 3px; background-color: #112240; } """) # 设置主窗口布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 标题区域 title_layout = QHBoxLayout() self.title_label = QLabel("增强版多模态大模型图像解读系统") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;") title_layout.addWidget(self.title_label) # 添加主题切换按钮 self.theme_button = QPushButton("切换主题") self.theme_button.clicked.connect(self.toggle_theme) title_layout.addWidget(self.theme_button) main_layout.addLayout(title_layout) # 主内容区域 splitter = QSplitter(Qt.Horizontal) # 左侧控制面板 left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(5, 5, 5, 5) # 图片预览区域(改为选项卡形式) self.image_tabs = QTabWidget() self.image_tabs.setTabsClosable(True) self.image_tabs.tabCloseRequested.connect(self.close_image_tab) left_layout.addWidget(self.image_tabs, 3) # 控制面板区域 control_tabs = QTabWidget() # 模型控制选项卡 model_tab = QWidget() self.setup_model_tab(model_tab) control_tabs.addTab(model_tab, "模型设置") # 参数控制选项卡 param_tab = QWidget() self.setup_param_tab(param_tab) control_tabs.addTab(param_tab, "参数设置") # 预设控制选项卡 preset_tab = QWidget() self.setup_preset_tab(preset_tab) control_tabs.addTab(preset_tab, "预设管理") left_layout.addWidget(control_tabs, 2) # 右侧结果面板 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(5, 5, 5, 5) # 结果展示区域 result_tabs = QTabWidget() # 分析结果选项卡 self.result_tab = QWidget() self.setup_result_tab(self.result_tab) result_tabs.addTab(self.result_tab, "分析结果") # 历史记录选项卡 self.history_tab = QWidget() self.setup_history_tab(self.history_tab) result_tabs.addTab(self.history_tab, "历史记录") right_layout.addWidget(result_tabs) # 添加面板到分割器 splitter.addWidget(left_panel) splitter.addWidget(right_panel) splitter.setSizes([800, 1100]) main_layout.addWidget(splitter) # 状态栏 self.status_bar = self.statusBar() self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;") # 资源监控标签 self.resource_label = QLabel() self.resource_label.setObjectName("statusLabel") self.status_bar.addPermanentWidget(self.resource_label) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setFixedWidth(200) self.status_bar.addPermanentWidget(self.progress_bar) # 活动线程标签 self.thread_label = QLabel("线程: 0") self.thread_label.setObjectName("statusLabel") self.status_bar.addPermanentWidget(self.thread_label) self.status_bar.showMessage("系统已就绪") def setup_model_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 模型选择区域 model_group = QGroupBox("模型选择") model_layout = QVBoxLayout() self.model_list = QListWidget() self.model_list.setMaximumHeight(150) self.model_list.addItem("正在加载模型...") self.refresh_models_button = QPushButton("刷新模型列表") self.refresh_models_button.clicked.connect(self.load_models) model_layout.addWidget(self.model_list) model_layout.addWidget(self.refresh_models_button) model_group.setLayout(model_layout) layout.addWidget(model_group) # 图片操作区域 image_group = QGroupBox("图片操作") image_layout = QVBoxLayout() self.load_button = QPushButton("加载图片") self.load_button.clicked.connect(self.load_image) self.load_multiple_button = QPushButton("批量加载图片") self.load_multiple_button.clicked.connect(self.load_multiple_images) self.clear_images_button = QPushButton("清除所有图片") self.clear_images_button.clicked.connect(self.clear_all_images) image_layout.addWidget(self.load_button) image_layout.addWidget(self.load_multiple_button) image_layout.addWidget(self.clear_images_button) image_group.setLayout(image_layout) layout.addWidget(image_group) # 分析控制区域 control_group = QGroupBox("分析控制") control_layout = QVBoxLayout() self.analyze_button = QPushButton("分析当前图片") self.analyze_button.clicked.connect(self.analyze_image) self.analyze_button.setEnabled(False) self.analyze_all_button = QPushButton("批量分析所有图片") self.analyze_all_button.clicked.connect(self.analyze_all_images) self.analyze_all_button.setEnabled(False) self.stop_button = QPushButton("停止分析") self.stop_button.clicked.connect(self.stop_analysis) self.stop_button.setEnabled(False) control_layout.addWidget(self.analyze_button) control_layout.addWidget(self.analyze_all_button) control_layout.addWidget(self.stop_button) control_group.setLayout(control_layout) layout.addWidget(control_group) def setup_param_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 温度控制 temp_group = QGroupBox("温度控制") temp_layout = QHBoxLayout() self.temp_slider = QSlider(Qt.Horizontal) self.temp_slider.setRange(0, 100) self.temp_slider.setValue(50) self.temp_value = QDoubleSpinBox() self.temp_value.setRange(0.0, 1.0) self.temp_value.setSingleStep(0.1) self.temp_value.setValue(0.5) self.temp_value.setDecimals(1) self.temp_slider.valueChanged.connect(lambda val: self.temp_value.setValue(val / 100)) self.temp_value.valueChanged.connect(lambda val: self.temp_slider.setValue(int(val * 100))) temp_layout.addWidget(self.temp_slider) temp_layout.addWidget(self.temp_value) temp_group.setLayout(temp_layout) layout.addWidget(temp_group) # Token控制 token_group = QGroupBox("Token控制") token_layout = QHBoxLayout() self.token_spin = QDoubleSpinBox() self.token_spin.setRange(100, 5000) self.token_spin.setValue(1000) self.token_spin.setSingleStep(100) token_layout.addWidget(QLabel("最大Token:")) token_layout.addWidget(self.token_spin) token_group.setLayout(token_layout) layout.addWidget(token_group) # 提示词区域 prompt_group = QGroupBox("提示词设置") prompt_layout = QVBoxLayout() self.prompt_edit = QTextEdit() self.prompt_edit.setPlainText( "请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") self.prompt_edit.setMinimumHeight(150) prompt_layout.addWidget(self.prompt_edit) prompt_group.setLayout(prompt_layout) layout.addWidget(prompt_group) def setup_preset_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 预设管理 preset_group = QGroupBox("预设管理") preset_layout = QVBoxLayout() self.preset_combo = QComboBox() self.preset_combo.addItems(["默认预设", "详细描述", "创意写作", "技术分析"]) button_layout = QHBoxLayout() self.load_preset_button = QPushButton("加载预设") self.load_preset_button.clicked.connect(self.load_preset) self.save_preset_button = QPushButton("保存预设") self.save_preset_button.clicked.connect(self.save_preset) self.delete_preset_button = QPushButton("删除预设") self.delete_preset_button.clicked.connect(self.delete_preset) button_layout.addWidget(self.load_preset_button) button_layout.addWidget(self.save_preset_button) button_layout.addWidget(self.delete_preset_button) preset_layout.addWidget(self.preset_combo) preset_layout.addLayout(button_layout) preset_group.setLayout(preset_layout) layout.addWidget(preset_group) # 自动保存设置 auto_save_group = QGroupBox("自动保存设置") auto_save_layout = QVBoxLayout() self.auto_save_check = QCheckBox("自动保存分析结果") self.auto_save_check.setChecked(True) self.auto_save_path_button = QPushButton("选择保存路径") self.auto_save_path_button.clicked.connect(self.select_auto_save_path) self.auto_save_path_label = QLabel("默认保存位置: 程序目录/results") self.auto_save_path_label.setWordWrap(True) auto_save_layout.addWidget(self.auto_save_check) auto_save_layout.addWidget(self.auto_save_path_button) auto_save_layout.addWidget(self.auto_save_path_label) auto_save_group.setLayout(auto_save_layout) layout.addWidget(auto_save_group) def setup_result_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 结果展示区域 result_group = QGroupBox("分析结果") result_layout = QVBoxLayout() self.result_edit = QTextEdit() self.result_edit.setReadOnly(True) # 结果操作按钮 button_layout = QHBoxLayout() self.save_result_button = QPushButton("保存结果") self.save_result_button.clicked.connect(self.save_result) self.copy_result_button = QPushButton("复制结果") self.copy_result_button.clicked.connect(self.copy_result) self.clear_result_button = QPushButton("清除结果") self.clear_result_button.clicked.connect(self.clear_results) button_layout.addWidget(self.save_result_button) button_layout.addWidget(self.copy_result_button) button_layout.addWidget(self.clear_result_button) result_layout.addWidget(self.result_edit) result_layout.addLayout(button_layout) result_group.setLayout(result_layout) layout.addWidget(result_group) def setup_history_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 历史记录区域 history_group = QGroupBox("历史记录") history_layout = QVBoxLayout() self.history_list = QListWidget() self.history_list.itemDoubleClicked.connect(self.load_history_item) # 历史操作按钮 button_layout = QHBoxLayout() self.load_history_button = QPushButton("加载历史") self.load_history_button.clicked.connect(self.load_history) self.clear_history_button = QPushButton("清除历史") self.clear_history_button.clicked.connect(self.clear_history) button_layout.addWidget(self.load_history_button) button_layout.addWidget(self.clear_history_button) history_layout.addWidget(self.history_list) history_layout.addLayout(button_layout) history_group.setLayout(history_layout) layout.addWidget(history_group) def init_tray_icon(self): """初始化系统托盘图标""" self.tray_icon = QSystemTrayIcon(self) # 使用系统默认图标 icon = self.style().standardIcon(QStyle.SP_ComputerIcon) self.tray_icon.setIcon(icon) tray_menu = QMenu() show_action = QAction("显示窗口", self) show_action.triggered.connect(self.show_normal) tray_menu.addAction(show_action) exit_action = QAction("退出", self) exit_action.triggered.connect(self.close) tray_menu.addAction(exit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() # 托盘图标点击事件 self.tray_icon.activated.connect(self.tray_icon_clicked) def show_normal(self): """从托盘恢复窗口显示""" self.showNormal() self.activateWindow() def tray_icon_clicked(self, reason): """处理托盘图标点击事件""" if reason == QSystemTrayIcon.DoubleClick: self.show_normal() def toggle_theme(self): """切换主题""" if self.theme_button.text() == "切换主题": # 切换到浅色主题 self.setStyleSheet(""" QMainWindow { background-color: #f5f5f5; } QGroupBox { border: 2px solid #2c3e50; border-radius: 10px; margin-top: 1ex; color: #2c3e50; font-weight: bold; } QLabel { color: #2c3e50; } QPushButton { background-color: #ecf0f1; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QPushButton:disabled { background-color: #d5dbdb; color: #7f8c8d; border: 1px solid #bdc3c7; } QTextEdit { background-color: #ffffff; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; padding: 5px; font-size: 12pt; } QListWidget { background-color: #ffffff; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #3498db; color: #ffffff; } QProgressBar { border: 1px solid #2c3e50; border-radius: 5px; text-align: center; background-color: #ecf0f1; color: #2c3e50; } QProgressBar::chunk { background-color: #3498db; width: 10px; } #statusLabel { color: #2c3e50; font-weight: bold; padding: 2px 5px; border-radius: 3px; background-color: #ecf0f1; } """) self.theme_button.setText("切换回暗色") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #2c3e50;") self.status_bar.setStyleSheet("background-color: #ecf0f1; color: #2c3e50;") else: # 切换回暗色主题 self.setStyleSheet(""" QMainWindow { background-color: #0a192f; } QGroupBox { border: 2px solid #64ffda; border-radius: 10px; margin-top: 1ex; color: #ccd6f6; font-weight: bold; } QLabel { color: #ccd6f6; } QPushButton { background-color: #112240; color: #64ffda; border: 1px solid #64ffda; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QPushButton:disabled { background-color: #0d1b30; color: #4a8f7c; border: 1px solid #4a8f7c; } QTextEdit { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; padding: 5px; font-size: 12pt; } QListWidget { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #233554; color: #64ffda; } QProgressBar { border: 1px solid #64ffda; border-radius: 5px; text-align: center; background-color: #0a192f; color: #64ffda; } QProgressBar::chunk { background-color: #64ffda; width: 10px; } #statusLabel { color: #64ffda; font-weight: bold; padding: 2px 5px; border-radius: 3px; background-color: #112240; } """) self.theme_button.setText("切换主题") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;") self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;") def load_settings(self): """加载程序设置""" self.settings.beginGroup("MainWindow") self.restoreGeometry(self.settings.value("geometry", self.saveGeometry())) self.restoreState(self.settings.value("windowState", self.saveState())) self.settings.endGroup() # 加载历史记录 self.load_history() # 加载自动保存路径 auto_save_path = self.settings.value("AutoSave/path", "results") self.auto_save_path_label.setText(f"保存位置: {auto_save_path}") def save_settings(self): """保存程序设置""" self.settings.beginGroup("MainWindow") self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.settings.endGroup() # 保存历史记录 self.save_history() def load_models(self): """加载可用模型列表""" self.model_list.clear() self.model_list.addItem("正在加载模型...") self.model_loader_thread = ModelLoaderThread() self.model_loader_thread.models_loaded.connect(self.update_model_list) self.model_loader_thread.error_occurred.connect(self.handle_model_load_error) self.model_loader_thread.start() def update_model_list(self, models): """更新模型列表""" self.model_list.clear() if not models: self.model_list.addItem("没有找到可用模型") return for model in models: # 显示完整的模型名称(包含冒号) item = QListWidgetItem(f"● {model}") item.setData(Qt.UserRole, model) self.model_list.addItem(item) # 默认选择第一个模型 if models: self.model_list.setCurrentRow(0) def handle_model_load_error(self, error_msg): """处理模型加载错误""" self.model_list.clear() self.model_list.addItem(error_msg) self.status_bar.showMessage(error_msg) def load_image(self): """加载单张图片""" file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_path: self.add_image_tab(file_path) def load_multiple_images(self): """批量加载多张图片""" file_paths, _ = QFileDialog.getOpenFileNames( self, "选择多张图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_paths: for file_path in file_paths: self.add_image_tab(file_path) def add_image_tab(self, file_path): """添加图片选项卡""" try: pixmap = QPixmap(file_path) if pixmap.isNull(): raise Exception("无法加载图片文件,可能格式不支持或文件已损坏") scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) image_label = QLabel() image_label.setPixmap(pixmap.scaled( 800, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation )) image_label.setAlignment(Qt.AlignCenter) scroll_area.setWidget(image_label) tab_index = self.image_tabs.addTab( scroll_area, os.path.basename(file_path) ) self.image_tabs.setCurrentIndex(tab_index) self.image_paths.append(file_path) self.analyze_button.setEnabled(True) self.analyze_all_button.setEnabled(True) self.status_bar.showMessage(f"已加载图片: {os.path.basename(file_path)}") # 更新当前图片索引 self.current_image_index = tab_index except Exception as e: self.status_bar.showMessage(f"错误: {str(e)}") self.show_error_dialog("图片加载错误", f"无法加载图片:\n{str(e)}") def close_image_tab(self, index): """关闭图片选项卡""" if index < len(self.image_paths): self.image_paths.pop(index) self.image_tabs.removeTab(index) if not self.image_paths: self.analyze_button.setEnabled(False) self.analyze_all_button.setEnabled(False) def clear_all_images(self): """清除所有图片""" self.image_tabs.clear() self.image_paths.clear() self.analyze_button.setEnabled(False) self.analyze_all_button.setEnabled(False) self.status_bar.showMessage("已清除所有图片") def analyze_image(self): """分析当前图片""" current_index = self.image_tabs.currentIndex() if current_index < 0 or current_index >= len(self.image_paths): self.status_bar.showMessage("错误: 没有可分析的图片") return self.current_image_index = current_index self._analyze_image(self.image_paths[current_index]) def analyze_all_images(self): """批量分析所有图片""" if not self.image_paths: self.status_bar.showMessage("错误: 没有可分析的图片") return # 保存当前索引 saved_index = self.current_image_index # 逐个分析图片 for i, image_path in enumerate(self.image_paths): # 检查活动线程数 while self.active_threads >= MAX_THREADS: QCoreApplication.processEvents() # 处理事件循环 self.current_image_index = i self.image_tabs.setCurrentIndex(i) self._analyze_image(image_path) # 等待分析完成 while hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): QApplication.processEvents() # 恢复原始索引 self.current_image_index = saved_index self.image_tabs.setCurrentIndex(saved_index) def _analyze_image(self, image_path): """实际执行图片分析的内部方法""" selected_items = self.model_list.selectedItems() if not selected_items: self.status_bar.showMessage("错误: 请选择模型") return # 获取完整的模型名称(包含冒号) model_name = selected_items[0].data(Qt.UserRole) self.result_edit.clear() self.progress_bar.setValue(0) self.progress_bar.setFormat("正在分析图片...") self.set_buttons_enabled(False) self.stop_button.setEnabled(True) temperature = self.temp_value.value() max_tokens = int(self.token_spin.value()) prompt = self.prompt_edit.toPlainText().strip() # 确保之前的线程已停止 if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.wait(2000) # 等待线程结束 try: self.analysis_thread = ImageAnalysisThread( model_name, image_path, temperature, max_tokens, prompt ) self.analysis_thread.analysis_complete.connect(self.handle_analysis_result) self.analysis_thread.progress_updated.connect(self.update_progress) self.analysis_thread.error_occurred.connect(self.handle_analysis_error) self.analysis_thread.stream_data.connect(self.analysis_stream_data) self.analysis_thread.finished.connect(self.analysis_finished) self.analysis_thread.start() # 更新活动线程计数 self.active_threads += 1 self.thread_label.setText(f"线程: {self.active_threads}") except Exception as e: error_msg = f"启动分析线程失败: {str(e)}\n\n{traceback.format_exc()}" self.handle_analysis_error(error_msg) def analysis_stream_data(self, chunk): """处理流式数据""" cursor = self.result_edit.textCursor() cursor.movePosition(QTextCursor.End) cursor.insertText(chunk) self.result_edit.setTextCursor(cursor) self.result_edit.ensureCursorVisible() def handle_analysis_result(self, result, image_path): """处理分析结果""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") selected_items = self.model_list.selectedItems() model_name = selected_items[0].text()[2:] if selected_items else "未知模型" # 获取当前图片文件名 image_name = os.path.basename(image_path) # 格式化结果 formatted_result = self.format_result(result) result_html = f""" <div style='color:#64ffda; font-size:14pt; font-weight:bold; margin-bottom:10px;'> 图片分析结果: {image_name} </div> <div style='color:#ccd6f6; font-size:12pt; line-height:1.6;'>{formatted_result}</div> <div style='margin-top: 20px; color: #8892b0; font-size: 10pt; border-top: 1px solid #233554; padding-top: 10px;'> 模型: <span style='color: #64ffda;'>{model_name}</span> | 时间: <span style='color: #64ffda;'>{timestamp}</span> </div> """ self.result_edit.setHtml(result_html) self.status_bar.showMessage(f"图片分析完成: {image_name}") self.progress_bar.setValue(100) self.progress_bar.setFormat("分析完成") # 添加到历史记录 self.add_to_history(image_name, model_name, timestamp, result_html) # 自动保存结果 if self.auto_save_check.isChecked(): self.auto_save_result(image_name, result_html) def format_result(self, result): """格式化分析结果为HTML""" # 将文本转换为段落 paragraphs = result.split("\n\n") formatted_paragraphs = [] for para in paragraphs: if para.strip(): # 替换缩进空格为HTML空格 formatted_para = para.replace(" ", "  ") formatted_paragraphs.append(f"<p style='text-indent: 2em;'>{formatted_para}</p>") return "\n".join(formatted_paragraphs) def add_to_history(self, image_name, model_name, timestamp, content): """添加到历史记录""" history_item = { "image": image_name, "model": model_name, "time": timestamp, "content": content } self.history.insert(0, history_item) self.update_history_list() def update_history_list(self): """更新历史记录列表""" self.history_list.clear() for item in self.history[:50]: # 最多显示50条历史记录 list_item = QListWidgetItem(f"{item['image']} - {item['time']}") list_item.setData(Qt.UserRole, item) self.history_list.addItem(list_item) def load_history_item(self, item): """加载历史记录项""" history_data = item.data(Qt.UserRole) self.result_edit.setHtml(history_data["content"]) def load_history(self): """从文件加载历史记录""" try: if os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, "r", encoding="utf-8") as f: self.history = json.load(f) self.update_history_list() self.status_bar.showMessage(f"已加载 {len(self.history)} 条历史记录") except Exception as e: self.status_bar.showMessage(f"加载历史记录失败: {str(e)}") def save_history(self): """保存历史记录到文件""" try: with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(self.history, f, ensure_ascii=False, indent=2) except Exception as e: self.status_bar.showMessage(f"保存历史记录失败: {str(e)}") def clear_history(self): """清除历史记录""" reply = QMessageBox.question( self, "确认清除", "确定要清除所有历史记录吗? 此操作不可撤销!", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.history.clear() self.history_list.clear() self.status_bar.showMessage("已清除所有历史记录") def auto_save_result(self, image_name, content): """自动保存结果""" try: save_dir = self.settings.value("AutoSave/path", "results") if not os.path.exists(save_dir): os.makedirs(save_dir) # 生成文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = os.path.splitext(image_name)[0] file_name = f"{base_name}_{timestamp}.html" file_path = os.path.join(save_dir, file_name) # 保存文件 with open(file_path, "w", encoding="utf-8") as f: f.write(content) self.status_bar.showMessage(f"结果已自动保存到: {file_path}") except Exception as e: self.status_bar.showMessage(f"自动保存失败: {str(e)}") def select_auto_save_path(self): """选择自动保存路径""" save_dir = QFileDialog.getExistingDirectory( self, "选择自动保存目录", self.settings.value("AutoSave/path", "results") ) if save_dir: self.settings.setValue("AutoSave/path", save_dir) self.auto_save_path_label.setText(f"保存位置: {save_dir}") def save_result(self): """保存结果到文件""" if not self.result_edit.toPlainText(): self.status_bar.showMessage("错误: 没有可保存的内容") return file_path, _ = QFileDialog.getSaveFileName( self, "保存结果", "", "HTML文件 (*.html);;文本文件 (*.txt);;PDF文件 (*.pdf)" ) if file_path: format_type = "html" if file_path.endswith(".txt"): format_type = "txt" elif file_path.endswith(".pdf"): format_type = "pdf" content = self.result_edit.toHtml() if format_type != "txt" else self.result_edit.toPlainText() self.export_thread = ExportThread(content, file_path, format_type) self.export_thread.export_finished.connect(self.handle_export_finished) self.export_thread.start() self.status_bar.showMessage("正在导出结果...") def handle_export_finished(self, message, success): """处理导出完成""" if success: self.status_bar.showMessage(f"结果已保存到: {message}") if message.endswith(".html"): webbrowser.open(message) else: self.status_bar.showMessage(f"导出失败: {message}") self.show_error_dialog("导出错误", f"无法保存结果:\n{message}") def copy_result(self): """复制结果到剪贴板""" self.result_edit.selectAll() self.result_edit.copy() self.status_bar.showMessage("结果已复制到剪贴板") def clear_results(self): """清除结果""" self.result_edit.clear() self.status_bar.showMessage("已清除结果") def load_preset(self): """加载预设""" preset_name = self.preset_combo.currentText() if preset_name == "默认预设": self.temp_value.setValue(0.5) self.token_spin.setValue(1000) self.prompt_edit.setPlainText( "请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") elif preset_name == "详细描述": self.temp_value.setValue(0.3) self.token_spin.setValue(1500) self.prompt_edit.setPlainText( "请用中文详细描述这张图片中的每一个细节,包括但不限于场景、人物、物体、颜色、空间关系等。要求描述系统全面,层次分明,每段描述一个方面。") elif preset_name == "创意写作": self.temp_value.setValue(0.7) self.token_spin.setValue(2000) self.prompt_edit.setPlainText( "请根据这张图片创作一个富有想象力的故事或诗歌。要求内容生动有趣,语言优美,可以适当发挥想象力,但不要偏离图片内容太远。") elif preset_name == "技术分析": self.temp_value.setValue(0.2) self.token_spin.setValue(1200) self.prompt_edit.setPlainText( "请从技术角度分析这张图片,包括但不限于构图、色彩、光线、透视等专业要素。要求分析专业准确,使用适当的专业术语。") self.status_bar.showMessage(f"已加载预设: {preset_name}") def save_preset(self): """保存当前设置为预设""" preset_name, ok = QInputDialog.getText( self, "保存预设", "请输入预设名称:", QLineEdit.Normal, self.preset_combo.currentText() ) if ok and preset_name: # 检查是否已存在 index = self.preset_combo.findText(preset_name) if index == -1: self.preset_combo.addItem(preset_name) self.preset_combo.setCurrentText(preset_name) self.status_bar.showMessage(f"预设 '{preset_name}' 已保存") def delete_preset(self): """删除预设""" preset_name = self.preset_combo.currentText() if preset_name in ["默认预设", "详细描述", "创意写作", "技术分析"]: QMessageBox.warning(self, "警告", "系统预设不能被删除") return reply = QMessageBox.question( self, "确认删除", f"确定要删除预设 '{preset_name}' 吗?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: index = self.preset_combo.currentIndex() self.preset_combo.removeItem(index) self.status_bar.showMessage(f"预设 '{preset_name}' 已删除") def stop_analysis(self): """停止分析""" if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.status_bar.showMessage("分析已停止") self.set_buttons_enabled(True) self.stop_button.setEnabled(False) def analysis_finished(self): """分析完成后的清理工作""" self.set_buttons_enabled(True) self.stop_button.setEnabled(False) # 更新活动线程计数 self.active_threads -= 1 if self.active_threads < 0: self.active_threads = 0 self.thread_label.setText(f"线程: {self.active_threads}") def handle_analysis_error(self, error_msg): """处理分析错误""" # 显示详细的错误信息 self.status_bar.showMessage(f"错误: {error_msg.splitlines()[0]}") self.progress_bar.setFormat("分析失败") self.set_buttons_enabled(True) self.stop_button.setEnabled(False) # 在结果区域显示完整错误信息 error_html = f""" <div style='color:#ff6b6b; font-size:14pt; font-weight:bold; margin-bottom:10px;'> 分析过程中发生错误 </div> <div style='color:#ccd6f6; font-size:12pt; line-height:1.6;'> {error_msg.replace('\n', '<br>')} </div> """ self.result_edit.setHtml(error_html) # 记录错误日志 self.log_error(error_msg) # 更新活动线程计数 self.active_threads -= 1 if self.active_threads < 0: self.active_threads = 0 self.thread_label.setText(f"线程: {self.active_threads}") def log_error(self, error_msg): """记录错误到日志文件""" try: log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) log_file = os.path.join(log_dir, "error_log.txt") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(log_file, "a", encoding="utf-8") as f: f.write(f"\n\n[{timestamp}] 发生错误:\n") f.write(error_msg) f.write("\n" + "-" * 80) except Exception as e: print(f"无法写入错误日志: {str(e)}") def set_buttons_enabled(self, enabled): """设置按钮启用状态""" self.load_button.setEnabled(enabled) self.load_multiple_button.setEnabled(enabled) self.clear_images_button.setEnabled(enabled) self.analyze_button.setEnabled(enabled and len(self.image_paths) > 0) self.analyze_all_button.setEnabled(enabled and len(self.image_paths) > 0) self.model_list.setEnabled(enabled) self.refresh_models_button.setEnabled(enabled) self.preset_combo.setEnabled(enabled) self.load_preset_button.setEnabled(enabled) self.save_preset_button.setEnabled(enabled) self.delete_preset_button.setEnabled(enabled) self.auto_save_check.setEnabled(enabled) self.auto_save_path_button.setEnabled(enabled) self.save_result_button.setEnabled(enabled) self.copy_result_button.setEnabled(enabled) self.clear_result_button.setEnabled(enabled) self.load_history_button.setEnabled(enabled) self.clear_history_button.setEnabled(enabled) def show_error_dialog(self, title, message): """显示错误对话框""" QMessageBox.critical(self, title, message) def monitor_resources(self): """监控系统资源使用情况""" try: cpu_percent = psutil.cpu_percent() memory = psutil.virtual_memory() self.resource_label.setText( f"CPU: {cpu_percent}% | 内存: {memory.percent}%" ) except: pass def closeEvent(self, event): """关闭窗口事件""" # 停止所有运行中的线程 if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): self.analysis_thread.stop() if hasattr(self, 'model_loader_thread') and self.model_loader_thread.isRunning(): self.model_loader_thread.quit() self.model_loader_thread.wait(2000) # 停止资源监控定时器 self.resource_timer.stop() # 保存设置 # 接受关闭事件 event.accept() def handle_exception(exc_type, exc_value, exc_traceback): """全局异常处理函数""" error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) print(f"全局异常捕获:\n{error_msg}") # 尝试显示错误对话框 try: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("程序发生未处理异常") msg.setInformativeText(f"{exc_type.__name__}: {exc_value}") msg.setWindowTitle("致命错误") msg.setDetailedText(error_msg) msg.exec_() except: pass # 退出程序 sys.exit(1) if __name__ == "__main__": # 设置全局异常处理 sys.excepthook = handle_exception app = QApplication(sys.argv) window = MultiModalApp() window.show() sys.exit(app.exec_())
08-11
<template> <div :class="['talk-item', isLeft? 'left' : 'right']" :key="data.timestamp" v-if="isTemp == 1? true : text" > <!-- 遍历布局节点 --> <template v-for="it in layout[isLeft? 'left' : 'right']"> <!-- 头像部分 --> <div v-if="it === LayoutNode.AVATAR" :key="`${data.startFrame}-${it}`" class="talk-avatar" > <el-avatar class="avatar" size="large" :style="{ 'background-image': backgroundImg }" > <!-- avatar 用于展示头像 --> </el-avatar> </div> <!-- 文本段落部分 --> <div v-if="it === LayoutNode.PARAGRAPH" :key="`${data.startFrame}-${it}`" class="highlighted" :class="{ 'talk-paragraph': true, 'highlight': highlight, 'blink': isBlink }" > <!-- 时间和其他操作按钮 --> <div class="time"> <!-- 左侧时间显示 --> <span v-if="isLeft" style="font-size: 12px;"> {{ timestamp | formatDate('hh:mm:ss') }} </span> <!-- 复制按钮 --> <i v-if="canCopy && data.text.length > 1" class="el-icon-s-claim" @click="setAnswer" ></i> <!--编辑按钮--> <el-tooltip v-if="canEdit" effect="dark" content="编辑" placement="top-start" class="item" > <i class="el-icon-edit" @click="changeEditStatus" v-if="(data.text.length > 0 && status == RealTimeStatus.History) || (data.text.length > 0 && status == RealTimeStatus.RealTime)" ></i> </el-tooltip> <!-- 右侧时间显示 --> <span v-if="!isLeft" style="font-size: 11px;"> {{ timestamp | formatDate('hh:mm:ss') }} </span> </div> <!-- 文本内容容器 --> <div :class="['text-container']" ref="textContainer" @dblclick="fetchTalkItem"> <!-- 统一前置 --> <i class="isIcon iconfont icon-yinbo"></i> <!-- 编辑模式下的文本框 --> <el-input v-if="isEdit" :class="['text-box',{highlighted: isPlaying }]" type="textarea" style="font-size: 16px;" :rows="3" v-model="data.text" @input="handleInput" ></el-input> <!-- 非编辑模式下的文本显示 --> <div v-else v-html="`${text}  `" :class="['text-box']" style="font-size: 16px;"></div> <!-- 段落进度条 --> <div class="progress-highlight" :style="{ width: progressWidth }"></div> </div> <!-- 命中标签 --> <div class="isHit" v-if="matchedHitRuleNames.length > 0"> <i style="color:#007bff;" class="el-icon-circle-check"></i> <span v-for="(name, index) in matchedHitRuleNames" :key="index">  {{ name }} </span> </div> </div> </template> </div> </template> <script lang="ts"> import { Message, MessageBox } from "element-ui"; import { Component, Prop, Vue, Watch } from "nuxt-property-decorator"; import { Keyword, TalkItem, TalkState } from "../../../types"; import * as dayjs from "dayjs"; import '../../../assets/iconFont/iconfont.css' // import img_police from "~/assets/img/anonymity-police.jpg" // 设置谈话人 被谈话人的位置 import img_police from "../../../assets/img/anonymity-telephonist.jpg"; import img_usr from "../../../assets/img/anonymity-square.png"; import { State } from "vuex-class"; const enum LayoutNode { AVATAR, PARAGRAPH, } /** * ZKer 设置谈话人是否在左边 */ const IsLeft = true; @Component({ name: "talkItem", components: {}, filters: { formatDate(value) { return dayjs(new Date(value)).format("YYYY-MM-DD HH:mm:ss"); }, }, }) export default class extends Vue { @Prop({ type: Object, required: true, default: {} }) data!: TalkItem; @Prop({ type: Boolean, required: true }) highlight: boolean; @Prop({ type: Boolean, required: true }) isPolice: boolean; @Prop({ type: Boolean, required: true }) canCopy: boolean; @Prop({ required: true }) canEdit; // 判断编辑是否可修改(历史谈话可/实时谈话) @Prop({ type: [], required: true }) RealTimeStatus; // 历史 实时谈话状态 @Prop({ required: true }) status; @Prop() busGroups!: any; isEdit: boolean = false; isLeft: boolean = false; isBlink: boolean = false; isTemp: any = null; // 判断用户删除(监听input的输入事件时 证明在删除 否则反之) timestamp: string | number = " "; text: string = " "; backgroundImg: string = `url(${this.isPolice? img_police : img_usr}`; LayoutNode: any = LayoutNode; layout: any = { // 统计左(对话数据)右() left: [LayoutNode.AVATAR, LayoutNode.PARAGRAPH], right: [LayoutNode.PARAGRAPH, LayoutNode.AVATAR], }; @State talk!: TalkState; matchedHitRuleNames: string[] = []; // -=-= @Prop({ type: Boolean, default: false }) isPlaying!: boolean; progressWidth: string = '0%'; duration: number = null; // 播放完一段 animationFrameId: number | null = null; @Watch('isPlaying') onIsPlayingChange(isPlaying: boolean) { if (isPlaying) { this.startProgressAnimation(); } else { this.stopProgressAnimation(); } } startProgressAnimation() { const startTime = performance.now(); const animate = (now: number) => { const elapsed = now - startTime; const progress = Math.min(elapsed / this.duration, 1); this.progressWidth = `${progress * 100}%`; if (progress < 1) { this.animationFrameId = requestAnimationFrame(animate); } }; this.animationFrameId = requestAnimationFrame(animate); } stopProgressAnimation() { if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } this.progressWidth = '0%'; } beforeDestroy() { this.stopProgressAnimation(); } // -=-= @Watch("data", { deep: true, immediate: true }) onDataChange(newData: TalkItem) { // if (!newData.policy) { // this.console.debug(newData,newData.last, newData.text, JSON.stringify(newData.keywords), newData.startFrame) // } this.text = newData.text; this.setKeyword(true); } @Watch("canEdit") onCanEditChange(newValue: boolean) { if (newValue) { this.$store.dispatch("talk/FetchKeywords"); } } // handleClick(){//单击文本字段 // if (this.status == this.RealTimeStatus.History) { // this.$emit('jump', this.data) // console.log('this.newData',this.data); // // } // console.log(121212); // // } created() { // 对讲内容 // console.log(this.data, "opop"); // 命中次数 // console.log(this.busGroups.hitRules, "1212121"); } /** * 统一中英文标点符号为英文格式 */ normalizePunctuation(text: string): string { const punctuationMap: { [key: string]: string } = { ',': ',', '。': '.', '?': '?', '!': '!', ';': ';', ':': ':', '“': '"', '”': '"', '‘': "'", '’': "'", '(': '(', ')': ')', '【': '[', '】': ']', '《': '<', '》': '>', '、': '\\', '——': '-', '…': '...', '—': '-', '·': '.' }; return text.replace(/[^\u0000-\u00ff]/g, ch => punctuationMap[ch] || ch); } scrollToParagraph() {//添加 scrollToParagraph 方法,用于滚动到指定段落 const element = this.$el; element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } mounted() { this.text = this.data.text; this.isLeft = Boolean(Number(this.isPolice) ^ Number(IsLeft)); this.timestamp = this.data.timestamp; this.setKeyword(); if (this.data.startFrame && this.data.endFrame) { this.duration = this.data.endFrame - this.data.startFrame; // 动态计算段落的时间长度 } // **新增延迟逻辑:通过 $watch 监听 busGroups 变化** this.$watch('busGroups', (newGroups) => { if (newGroups && newGroups.hitRules) { this.matchHitRules(newGroups.hitRules); // 自定义标签匹配方法 } }, { deep: true, immediate: true }); } // 新增标签匹配方法 matchHitRules(hitRules: any[]) { const matchedRuleNamesSet = new Set<string>(); const normalizedDataText = this.normalizePunctuation(this.text).toLowerCase(); const currentSpeakerPrefix = this.data.policy ? '管教民警:' : '在押人员:'; hitRules.forEach(rule => { const sentences = this.splitSentences(rule.hitSentence); sentences.forEach(sentence => { const normalizedSentence = this.normalizePunctuation(sentence).toLowerCase(); if (normalizedSentence.startsWith(currentSpeakerPrefix.toLowerCase()) && normalizedSentence.includes(normalizedDataText)) { matchedRuleNamesSet.add(rule.hitRuleName); } }); }); this.matchedHitRuleNames = Array.from(matchedRuleNamesSet); } // 按管教民警或被监管人分割句子 splitSentences(text: string): string[] { const pattern = /(管教民警|被监管人):/g; const matches = text.match(pattern); const sentences: string[] = []; let startIndex = 0; if (matches) { matches.forEach((match, index) => { const endIndex = index === matches.length - 1 ? text.length : text.indexOf(matches[index + 1]); const sentence = text.substring(startIndex, endIndex).trim(); if (sentence) { sentences.push(sentence); } startIndex = endIndex; }); } return sentences; } handleInput(value) { this.isTemp = 1; // 如果输入框的输入发生改变证明在删除 赋值为1 {上面的判断证明如果输入框变化 为true 否则循环的text(文本)有值显示 没值不显示} // 监听input输入的文本长度 if (value.length == 1) { this.$message.error("输入字符长度至少1位"); // 阻止表单提交 } else { this.data.text = value; } } changeEditStatus() { const initialText = this.data.text; this.$prompt("", "文本修正", { confirmButtonText: "确定", cancelButtonText: "取消", inputPlaceholder: "", inputValue: initialText, // 设置$prompt的input初始值为编辑的文本 }) .then(({ value }) => { if (value.length < 1) { Message({ message: "最少保留1个字符内容!", type: "error" }); return; } else { // 正常输入 this.data.text = value; this.submitChange(); } }) .catch(() => { this.$message({ type: "info", message: "取消操作", }); }); } /** * 修改文本提交 */ async submitChange() { this.$emit("editContent", Number(this.data.startFrame), this.data.text, this.data.policy); this.setKeyword(false, this.talk.keywords.map((it: Keyword) => { return it.text; })); } /** * 关键字用label标签 */ setKeyword(forceUpdate: boolean = false, keywords: string[] = this.data.keywords) { // const keywords = this.canEdit? this.talk.keywords.map((it: Keyword) => { // return it.text // }) : this.data.keywords if (keywords == null || keywords.length == 0) { return; } if (this.data.policy /*|| !this.data.last*/) { return; } keywords.forEach((it) => { if (it.length == 0) { return; } this.text = this.text.replace(new RegExp(it, "gm"), `<label class='keyword'>${it}</label>`); }); if (forceUpdate) { this.$nextTick(() => { this.$forceUpdate(); }); } } /** * 搜索字用span标签 */ setSearchWord(word: string) { if (word == null || word.length == 0) { return; } this.text = this.text.replace(new RegExp(word, "gm"), `<span class='searched'>${word}</span>`); } setGaugesWord(word: string) { if (word == null || word.length == 0) { return; } this.text = this.text.replace(new RegExp(word, "gm"), `<span class='gauges'>${word}</span>`); } clearSearchWord() { this.text = this.text.replace(new RegExp("<span class='searched'>", "gm"), ""); this.text = this.text.replace(new RegExp("</span>", "gm"), ""); } copy() { this.$copyText(this.text) .then(() => { this.$notify({ title: "成功", message: "谈话内容已成功复制到粘贴板!", duration: 3000, type: "success", }); }) .catch(() => { this.$notify({ title: "失败", message: "谈话内容复制到粘贴板失败!", duration: 3000, type: "error", }); }); } fetchTalkItem() {//单个段落 console.log("this.data",this.data); // 确保有有效的startFrame和endFrame if (this.data.startFrame && this.data.endFrame && this.data.endFrame - this.data.startFrame > 10) { // 最小10ms播放 this.$emit("fetchTalkItem", this.data); } else { console.warn("段落时间太短,不播放:", this.data); } } setAnswer() { this.$emit("setAnswer", this.data.text); } blink() { this.isBlink = true; this.console.debug(this); setTimeout(() => { this.isBlink = false; }, 1500); } } </script> <style lang="less" scoped> @import "../../../assets/styles/variables"; // 对讲内容是否命中 .isHit { font-size: 13px; color: #8b9199; margin: 8px 0 0 0; align-self: flex-start; } // 谈话框样式 公共配置 .talk-item { color: #47494e; padding: 0 0.5rem; .talk-avatar { display: inline-block; .el-avatar { position: relative; } .avatar { border-radius: 50%; background-size: 40px; } } .talk-paragraph { display: inline-block; padding: 0 1rem 10px 1rem!important; max-width: 70%!important; margin-bottom:8px!important; box-sizing: border-box; .time { padding-left: 1rem; padding-bottom: 0.3rem; font-size: 1rem; color: #ccc; /*min-height: 14px;*/ i { cursor: pointer; color: #7ea1de; } i:hover { color: slateblue; } } .progress-highlight{ position: absolute; top: 0; left: 0; border-radius: 8px; height: 93%; background-color: rgba(61, 111, 205, 0.5); /* 半透明蓝色 */ z-index: 0; transition: width 0.1s linear; } .text-container { display: flow-root; border-radius: 8px; min-width:120px; height: fit-content !important; overflow-x: hidden; .text-box { position: relative; display: inline-block; border-radius: 8px; padding: 10px; user-select: text; word-wrap: break-word; // 确保长单词换行 word-break: break-word; // 处理中文换行 overflow-wrap: break-word; // 处理长URL等 min-width:85%; max-width: 28ch; /* 限制最大宽度为大约28个字符的宽度包括符号(宽度自适应) */ overflow-x: hidden; /* 隐藏水平溢出 */ white-space: pre-wrap; /* 保留空白符序列,但正常地进行换行 */ } } } // -=-=-=-=-=-start播放命中高亮 .text-box.highlighted { background-color: #3d6fcd !important; color: white !important; transition: background-color 0.2s ease; } .talk-item.left .text-container:hover .isIcon.iconfont.icon-yinbo, .talk-item.right .text-container:hover .isIcon.iconfont.icon-yinbo, .text-container .isIcon.iconfont.icon-yinbo[style*="display: inline-block"] { display: inline-block !important; } // -=-=--=-=-end .talk-paragraph::before { display: inline-block; content: ""; width: 0; height: 0; line-height: normal; // border-width: 10px; // border-style: solid; // border-color: transparent; position: relative; top: 49px; } .highlight { .text-box { background-color: @talkItemHighlightBGColor !important; // background-color: #3d6fcd !important; // 原为 @talkItemHighlightBGColor,改为悬停色 // color: white !important; // 新增文字颜色 } } .blink { .text-box { background-color: @talkItemHighlightBGColor !important; animation: blink 0.5s 3; -webkit-animation-name: blink; -webkit-animation-duration: 500ms; -webkit-animation-iteration-count: 3; -webkit-animation-timing-function: ease-in-out; } } @keyframes blink { 0% { color: #fab4b4; } 25% { color: #fa6161; } 50% { color: #ff0000; } 75% { color: #fa6161; } 100% { color: #fab4b4; } } &:last-child { margin-bottom: 30px; } } ////////////////////////////////////////////////////////////////////////// // 左侧谈话框样式 .talk-item.left { display: flex; box-sizing: border-box; // &:hover { // width: 100% !important; // cursor: pointer; // border-radius: 10px; // background-color: #d7ecff !important; // border-left: 3px solid #409EFF !important; // transition: background-color 0.3s ease; // } .talk-avatar { .el-avatar { top: 38px; } } .talk-paragraph { .time { i { margin-left: 1rem; } } .text-container { position: relative; .isIcon.iconfont.icon-yinbo{ display: none; /* 默认隐藏 */ position: absolute; right: -28px; top: 8px; z-index: 1; font-size: 20px !important; border-radius: 50% !important; background: white !important; border: 1px solid rgb(219, 215, 215) !important; font-size: 20px !important; color: #3d6fcd !important; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block !important; /* 悬停时显示图标 */ } .text-box { background-color: @talkItemLeftBGColor; &:hover { cursor: pointer; color: white; background-color:#3d6fcd; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block; /* 悬停时显示图标 */ } } } } .talk-paragraph::before { border-right-width: 10px; border-right-color: @talkItemLeftBGColor; left: -19px; } .highlight::before, .blink::before { border-right-color: @talkItemHighlightBGColor !important; } } ////////////////////////////////////////////////////////////////////////// // 右侧谈话框样式 .talk-item.right { text-align: right; box-sizing: border-box; // &:hover { // width: 100% !important; // cursor: pointer; // border-radius: 10px; // background-color: #d7ecff !important; // border-left: 3px solid #409EFF !important; // transition: background-color 0.3s ease; // } .talk-avatar { float: right; .el-avatar { top: 36px; } } .talk-paragraph { .time { i { margin-right: 1rem; } } .text-container {//右侧谈话 position: relative; .isIcon.iconfont.icon-yinbo { display: none; /* 默认隐藏 */ position: absolute; left: -28px; top: 8px; z-index: 1; font-size: 20px !important; border-radius: 50% !important; background: white !important; border: 1px solid rgb(219, 215, 215) !important; font-size: 20px !important; color: #3d6fcd !important; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block !important; /* 悬停时显示图标 */ } .text-box { text-align: left; background-color: @talkItemRightBGColor; &:hover { cursor: pointer; color: white; background-color:#3d6fcd; } } } } .talk-paragraph::before { border-left-width: 10px; border-left-color: @talkItemRightBGColor; right: -19px; } .highlight::before, .blink::before { border-left-color: @talkItemHighlightBGColor !important; } } </style> <style lang="less"> .text-container { .text-box { .searched { color: #f54646; background-color: #f3f35d; } .keyword { color: red; font-weight: bold; } .gauges { color: #2fefd8; } } .text-box.el-input { padding: 5px !important; .el-input__inner { color: #560692; background: inherit; padding: 0; border-width: 0; } } .text-box.el-textarea { width: 350px; padding: 5px !important; .el-textarea__inner { color: #560692; background: inherit; padding: 0; border-width: 0; } } } .talk-item.right { .text-box.el-input, .text-box.el-textarea { float: right; right: 0px; } } </style> import jsonData from '../public/123.json' 基于引用数据来操作<template> <div class="main"> <div class="sec"> <div v-for="item in list" :key="item.timestamp"> {{ item.text }} </div> </div> </div> </template> <script lang="ts"> import { reactive, toRefs, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import jsonData from '../public/123.json' // 定义 content 中每项的类型 interface ContentItem { count: number policy: boolean speaker: string timestamp: number text: string startFrame: string endFrame: string emotions: null keywords: null last: boolean emotionSeg: Record<string, unknown> } export default { name: '', setup() { const router = useRouter() const route = useRoute() const data = reactive<{ list: ContentItem[] }>({ list: [] // 初始化为空数组但有明确类型 }) onMounted(() => { data.list = jsonData.data.content as ContentItem[] console.log(data.list) console.log(jsonData.data.content, 'jsonData') }) const refData = toRefs(data) return { ...refData } } } </script> <style lang="scss" scoped> .main{ width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; .sec{ width: 800px; height: 600px; border: 1px solid red; } } </style> 修复一下吧亲 基于我提供代码
08-20
//1.mesh构建 using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer), typeof(MeshCollider))] public class Potteryprototype : MonoBehaviour {     MeshFilter meshFilter;     MeshRenderer meshRenderer;     MeshCollider meshCollider;     Mesh mesh;       public int details = 40;     public int layer = 20;     public float Height = 0.1f;       public float OuterRadius = 1.0f;     public float InnerRadius = 0.9f;       List<Vector3> vertices;     List<Vector2> UV;     List<int> triangles;       float EachAngle ;     int SideCount;       public MouseControl mouse;       void Start()     {         meshFilter = GetComponent<MeshFilter>();         meshCollider = GetComponent<MeshCollider>();         meshRenderer = GetComponent<MeshRenderer>();     }       [ContextMenu("GeneratePottery")]     void GeneratePrototype()     {         vertices = new List<Vector3>();         triangles = new List<int>();         UV = new List<Vector2>();           EachAngle = Mathf.PI * 2 / details;         for (int i = 0; i < layer; i++)         {             GenerateCircle(i);         }         Capping();                 mesh = new Mesh();         mesh.vertices = vertices.ToArray();         mesh.triangles = triangles.ToArray();         mesh.uv = UV.ToArray();           mesh.RecalculateBounds();         mesh.RecalculateTangents();           meshFilter.mesh = mesh;         mesh.RecalculateNormals();         meshCollider.sharedMesh = mesh;     }       void GenerateCircle(int _layer)     {         //外顶点与内顶点分开存储,方便变化操作时的计算         List<Vector3> vertices_outside = new List<Vector3>();         List<Vector3> vertices_inside = new List<Vector3>();                  List<Vector2> UV_outside = new List<Vector2>();         List<Vector2> UV_inside = new List<Vector2>();             //外侧和内侧顶点计算         //注意这里让每一圈的首尾重合了,也就是开始和结尾的顶点坐标一致         //目的是计算UV坐标时不会出现空缺         for (float i = 0; i <= Mathf.PI * 2+EachAngle; i += EachAngle)         {             Vector3 v1 = new Vector3(OuterRadius * Mathf.Sin(i),  _layer * Height, OuterRadius * Mathf.Cos(i));             Vector3 v2 = new Vector3(OuterRadius * Mathf.Sin(i),  (_layer +1)* Height, OuterRadius * Mathf.Cos(i));             Vector3 v3 = new Vector3(InnerRadius * Mathf.Sin(i),  _layer * Height, InnerRadius * Mathf.Cos(i));             Vector3 v4 = new Vector3(InnerRadius * Mathf.Sin(i),  (_layer+1) * Height, InnerRadius * Mathf.Cos(i));             vertices_outside.Add(v1); vertices_outside.Add(v2);             vertices_inside.Add(v3); vertices_inside.Add(v4);               Vector2 uv1 = new Vector2(i / Mathf.PI*2, _layer*1.0f / layer * 1.0f);             Vector2 uv2 = new Vector2(i / Mathf.PI*2, (_layer + 1)*1.0f / layer * 1.0f);             Vector2 uv3 = new Vector2(i / Mathf.PI*2, _layer*1.0f / layer * 1.0f);             Vector2 uv4 = new Vector2(i / Mathf.PI*2, (_layer + 1) *1.0f/ layer * 1.0f);             UV_outside.Add(uv1); UV_outside.Add(uv2);             UV_inside.Add(uv3); UV_inside.Add(uv4);         }         vertices.AddRange(vertices_outside);         vertices.AddRange(vertices_inside);           UV.AddRange(UV_outside);         UV.AddRange(UV_inside);           SideCount = vertices_outside.Count;         int j = vertices_outside.Count * _layer * 2;         int n = vertices_outside.Count;         for (int i = j; i < j + vertices_outside.Count - 2; i += 2)         {               triangles.Add(i); triangles.Add(i + 2); triangles.Add(i + 1);             triangles.Add(i + 2); triangles.Add(i + 3); triangles.Add(i + 1);               triangles.Add(i + n); triangles.Add(i + n + 1); triangles.Add(i + n + 2);             triangles.Add(i + n + 2); triangles.Add(i + n + 1); triangles.Add(i + n + 3);         }          }     //封顶,底面由于看不见就不用管了     void Capping()     {                 for (float i = 0; i <= Mathf.PI * 2+EachAngle; i += EachAngle)         {             Vector3 outer = new Vector3(OuterRadius * Mathf.Sin(i),layer * Height, OuterRadius * Mathf.Cos(i));             Vector3 inner= new Vector3(InnerRadius * Mathf.Sin(i), layer * Height, InnerRadius * Mathf.Cos(i));               vertices.Add(outer);vertices.Add(inner);               Vector2 uv1 = new Vector2(i / Mathf.PI * 2,0); Vector2 uv2 = new Vector2(i / Mathf.PI * 2, 1);                         UV.Add(uv1); UV.Add(uv2);         }         int j = SideCount * layer * 2;         for (int i=j;i<vertices.Count-2;i+=2)         {             triangles.Add(i);triangles.Add(i + 3);triangles.Add(i + 1);             triangles.Add(i);triangles.Add(i + 2);triangles.Add(i + 3);         }         triangles.Add(vertices.Count - 2);triangles.Add(j + 1);triangles.Add(vertices.Count - 1);         triangles.Add(vertices.Count - 2);triangles.Add(j);triangles.Add(j + 1);             } }   //2.动态改变形状 //这个函数放在Update()里调用     void GetMouseControlTransform()     {         //从屏幕鼠标位置发射一条射线到模型上,获取这个坐标         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);         RaycastHit info;           if (Physics.Raycast(ray.origin, ray.direction, out info))         {               //在Unity中无法直接修改MeshFilter中Mesh的信息,需要新建一个Mesh修改其引用关系             Mesh mesh = meshFilter.mesh;             Vector3[] _vertices = mesh.vertices;               for (int i = 0; i < _vertices.Length; i++)             {                   //x,z平面变换                 //顶点移动与Y值的关系限制在5倍单层高度                 //这里可以自行修改,限制高度越大,曲线越平滑                 if (Mathf.Abs(info.point.y - transform.TransformPoint(_vertices[i]).y) < (5 * Height))                 {                     //计算顶点移动方向的向量                     Vector3 v_xz = (transform.TransformPoint(_vertices[i]) - new Vector3(transform.position.x, transform.TransformPoint(_vertices[i]).y, transform.position.z));                       //外顶点与内顶点移动时相对距离应该保持不变                     //因为我们知道顶点数组内的顺序关系,所以可以通过计算总顶点数除以每层单侧顶点数的商的奇偶关系来判断是外顶点还是内顶点                     int n = i / SideCount;                     bool side = n % 2 == 0;                     //判断顶面顶点内外关系                     bool caps = (i - (SideCount * layer * 2)) % 2 == 0;                       //限制每个顶点最大和最小的移动距离                     float max;                     float min;                     if (i < SideCount * layer * 2)                     {                         max = side ? 2f * OuterRadius : 2f * OuterRadius - (OuterRadius - InnerRadius);                           min = side ? 0.5f * OuterRadius : 0.5f * OuterRadius - (OuterRadius - InnerRadius);                     }                     else                     {                         max = caps ? 2f * OuterRadius : 2f * OuterRadius - (OuterRadius - InnerRadius); ;                         min = caps ? 0.5f * OuterRadius : 0.5f * OuterRadius - (OuterRadius - InnerRadius);                     }                     //计算当前顶点到鼠标Y值之间的距离,再用余弦函数算出实际位移距离                     float dif = Mathf.Abs(info.point.y - transform.TransformPoint(_vertices[i]).y);                     if (Input.GetKey(KeyCode.RightArrow))                     {                         float outer = max - v_xz.magnitude;                         _vertices[i] += v_xz.normalized * Mathf.Min(0.01f * Mathf.Cos(((dif / 5 * Height) * Mathf.PI) / 2), outer);                     }                     else if (Input.GetKey(KeyCode.LeftArrow))                     {                         float inner = v_xz.magnitude - min;                         _vertices[i] -= v_xz.normalized * Mathf.Min(0.01f * Mathf.Cos(((dif / 5 * Height) * Mathf.PI) / 2), inner);                     }                       //Y轴变换                     float scale_y = transform.localScale.y;                     if (Input.GetKey(KeyCode.UpArrow))                     {                         scale_y = Mathf.Min(transform.localScale.y + 0.000001f, 2.0f);                     }                     else if (Input.GetKey(KeyCode.DownArrow))                     {                           scale_y = Mathf.Max(transform.localScale.y - 0.000001f, 0.3f);                     }                     transform.localScale = new Vector3(transform.localScale.x, scale_y, transform.localScale.z);                   }                   mesh.vertices = _vertices;                 mesh.RecalculateBounds();                 mesh.RecalculateNormals();                 meshFilter.mesh = mesh;                 meshCollider.sharedMesh = mesh;             }         }     } }   //计算时就把顶点坐标系转换为自身坐标系,求得向量后再转换为世界坐标系     Vector3 v_xz = transform.TransformDirection(transform.InverseTransformPoint(_vertices[i]) - transform.InverseTransformPoint(new Vector3(0, _vertices[i].y, 0)));   //3.法线平均化 IEnumerator Print_Normals()     {              for (int i = 0; i < meshFilter.mesh.vertices.Length; i++)         {                if (i % 2 == 0)             {                 Debug.DrawRay(transform.TransformPoint(meshFilter.mesh.vertices[i]), transform.TransformDirection(meshFilter.mesh.normals[i] * 0.3f), Color.green, 1000f);             }             else             {                 Debug.DrawRay(transform.TransformPoint(meshFilter.mesh.vertices[i]), transform.TransformDirection(meshFilter.mesh.normals[i] * 0.3f), Color.blue, 1000f);             }               yield return new WaitForSeconds(Time.deltaTime);           }     }   //回到项目上来。这段法线计算的代码就不放上来了,大致就是根据顶点在数组中的下标去判断位置是否相同,然后把该顶点的法线相加即可。大家自己构建Mesh时的顶点顺序可能会不太一样。 以上代码是否有问题,请帮我完善
最新发布
08-30
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jarry.liu

如果对您有帮助,鼓励下博主吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值