摘要

本论文介绍了一个基于深度学习的综合性皮肤病识别与检测系统,该系统集成了最新的YOLO系列目标检测算法、DeepSeek智能分析引擎和现代化的Web交互界面。系统实现了对七种常见皮肤病变的自动识别和分类,包括鲍温氏病、基底细胞癌、良性角化病变、皮肤纤维瘤、黑色素瘤、黑色素细胞痣和血管病变。通过构建前后端分离的架构,系统提供了用户友好的Web界面,支持图片、视频和实时摄像头检测等多种输入方式。系统采用MySQL数据库进行数据存储和管理,实现了用户认证、模型切换、检测记录管理和可视化分析等核心功能模块。实验结果表明,基于YOLO系列的深度学习模型在皮肤病变检测任务上表现出良好的性能,结合DeepSeek的智能分析功能,为皮肤病辅助诊断提供了高效、准确的技术支持。

关键词:深度学习,皮肤病识别,YOLO系列,目标检测,Web应用,智能医疗,计算机视觉

目录

 摘要

项目源码+数据集下载链接

引言

1.1 研究背景与意义

1.2 研究现状

1.3 本文贡献

背景

2.1 皮肤病学基础

2.2 深度学习在医学影像中的应用

2.3 YOLO系列算法演进

系统设计与架构

3.1 总体架构设计

3.2 功能模块详细设计

3.2.1 用户认证与管理系统

3.2.2 多模型切换与管理系统

3.2.3 图像处理与检测模块

3.2.4 DeepSeek智能分析模块

3.2.5 数据可视化与分析模块

3.2.6 记录管理与查询模块

3.3 数据库设计

数据集介绍

4.1 数据集构成

4.2 数据类别与分布

功能模块

登录注册模块

可视化模块

图像检测模块

视频检测模块

实时检测模块

图片识别记录管理

视频识别记录管理

摄像头识别记录管理

用户管理模块

数据管理模块(MySQL表设计)

模型训练结果

YOLOv8

YOLOv10

YOLOv11

YOLOv12

前端代码展示

后端代码展示

 项目源码+数据集下载链接

 项目安装教程


项目源码+数据集下载链接

完整代码在哔哩哔哩视频下方简介内获取

基于深度学习的皮肤病识别检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibili

基于深度学习的皮肤病识别检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1vt2YBKEhT/?vd_source=549d0b4e2b8999929a61a037fcce3b0fhttps://www.bilibili.com/video/BV1vt2YBKEhT

引言

1.1 研究背景与意义

皮肤癌是全球范围内最常见的癌症类型之一,每年有数百万人被诊断出患有各种皮肤病变。早期准确诊断对于提高皮肤癌治愈率至关重要,但传统的皮肤病诊断方法依赖于皮肤科医生的视觉检查和病理活检,这一过程既耗时又容易受到主观判断的影响。特别是在医疗资源匮乏的地区,专业的皮肤科医生数量有限,导致许多患者无法及时获得准确的诊断。

近年来,随着人工智能和深度学习技术的快速发展,计算机辅助诊断系统在医疗影像分析领域取得了显著进展。基于深度学习的皮肤病识别系统能够自动分析和识别皮肤病变特征,为医生提供客观、一致的诊断建议,有望成为改善皮肤病诊断效率和准确性的有力工具。

1.2 研究现状

目前,已有多种基于深度学习的皮肤病识别方法被提出。早期的研究多采用CNN(卷积神经网络)进行皮肤病分类,如ResNet、Inception、DenseNet等架构在ISIC数据集上取得了良好的分类效果。然而,这些方法通常只能对整张图像进行分类,无法定位病变的具体位置。

目标检测技术的发展为皮肤病识别提供了新的解决方案。YOLO(You Only Look Once)系列算法因其高效的检测速度和良好的准确率平衡,在实时目标检测任务中表现出色。从YOLOv1到最新的YOLOv12,每一代都在模型架构、训练策略和性能优化方面进行了创新改进。将YOLO系列模型应用于皮肤病检测任务,不仅可以识别病变类型,还能精确定位病变区域,为临床诊断提供更丰富的信息。

1.3 本文贡献

本文的主要贡献包括:

  1. 多模型集成系统:开发了一个支持YOLOv8、v10、v11、v12四种最新版本切换的皮肤病识别系统,用户可以灵活选择最适合其需求的模型。

  2. 智能分析增强:集成DeepSeek智能分析引擎,为检测结果提供详细的病理学解释和临床建议,增强了系统的实用性和可解释性。

  3. 全功能Web平台:设计并实现了一个功能完备的Web应用平台,支持多种输入方式(图片、视频、摄像头实时检测)和全面的用户管理功能。

  4. 完整数据管理:实现了检测记录的全面管理,包括图片识别记录、视频识别记录和摄像头识别记录,所有数据均存储在MySQL数据库中,便于后续分析和研究。

背景

2.1 皮肤病学基础

皮肤是人体最大的器官,也是最容易受到各种疾病影响的器官之一。本文系统识别的七种皮肤病变具有不同的临床特征和病理机制:

  1. 鲍温氏病(Bowen's Disease):一种表皮内鳞状细胞癌,被认为是皮肤鳞状细胞癌的早期阶段。

  2. 基底细胞癌(Basal Cell Carcinoma):最常见的皮肤癌类型,起源于皮肤基底细胞,通常生长缓慢,转移风险低。

  3. 良性角化病变(Benign Keratosis Lesions):包括脂溢性角化病、日光性角化病等良性皮肤病变。

  4. 皮肤纤维瘤(Dermatofibroma):一种常见的良性皮肤肿瘤,由成纤维细胞和胶原纤维组成。

  5. 黑色素瘤(Melanoma):最危险的皮肤癌类型,起源于黑色素细胞,具有较高的转移和死亡率。

  6. 黑色素细胞痣(Melanocytic Nevus):俗称痣,是黑色素细胞的良性聚集。

  7. 血管病变(Vascular Lesions):包括血管瘤、血管畸形等血管相关皮肤病变。

2.2 深度学习在医学影像中的应用

深度学习技术在医学影像分析中的应用已经成为研究热点。在皮肤病学领域,深度学习模型通过学习大量标注的皮肤病变图像,能够提取和识别病变的关键特征,如颜色、形状、纹理和边界不规则性等。与传统的计算机视觉方法相比,深度学习不需要手工设计特征,能够自动学习更加复杂和抽象的表示,因此在皮肤病识别任务上具有显著优势。

2.3 YOLO系列算法演进

YOLO系列算法自2015年首次提出以来,经历了多次重要更新:

  • YOLOv8:引入了新的骨干网络和特征金字塔结构,改进了训练策略和数据增强方法。

  • YOLOv10:在模型效率和准确率之间取得了更好的平衡,优化了模型结构和后处理流程。

  • YOLOv11:进一步改进了特征融合机制和多尺度预测策略。

  • YOLOv12:最新的版本,集成了最新的神经网络架构设计理念和训练优化技术。

这些版本的演进主要体现在模型架构优化、训练策略改进、推理速度提升和准确率提高等方面。

系统设计与架构

3.1 总体架构设计

本系统采用前后端分离的架构设计,前端负责用户交互界面,后端提供数据处理和模型推理服务。整体架构分为以下四个层次:

  1. 表示层(Presentation Layer):基于现代Web技术构建的用户界面,提供直观的操作体验。

  2. 业务逻辑层(Business Logic Layer):处理用户请求,协调各个功能模块的工作流程。

  3. 数据处理层(Data Processing Layer):负责图像预处理、模型推理和后处理操作。

  4. 数据持久层(Data Persistence Layer):基于MySQL数据库的数据存储和管理。

3.2 功能模块详细设计

3.2.1 用户认证与管理系统

用户认证模块采用安全的密码存储机制,所有用户密码均存储在数据库中。系统支持用户注册、登录、密码重置等功能,并实现了以下安全特性:

  • 密码强度检测:要求用户设置足够复杂的密码

  • 会话管理:安全的用户会话创建和维护

  • 权限控制:区分普通用户和管理员权限

管理员用户具有特殊权限,可以对系统用户进行全面的管理操作,包括添加新用户、修改用户信息、删除用户账户等。

3.2.2 多模型切换与管理系统

系统集成了四种YOLO模型(v8、v10、v11、v12),用户可以根据具体需求选择合适的模型。模型切换模块实现了以下功能:

  • 模型动态加载:无需重启系统即可切换模型

  • 模型性能对比:记录不同模型在相同任务上的表现

  • 模型版本管理:支持不同版本的模型共存和调用

3.2.3 图像处理与检测模块

系统支持三种输入模式的皮肤病检测:

  1. 图像检测:用户上传皮肤病变图片,系统返回检测结果和详细分析

  2. 视频检测:对上传的视频文件进行逐帧分析,识别其中的皮肤病变

  3. 实时摄像头检测:通过摄像头实时捕捉视频流,进行实时分析和预警

所有检测结果均包含病变类型、置信度、位置信息等,并自动保存到数据库中。

3.2.4 DeepSeek智能分析模块

DeepSeek智能分析模块是本系统的特色功能之一,它在基础检测结果之上,提供以下增强分析:

  • 病理学解释:详细说明检测到的病变类型的医学特征

  • 风险评估:基于病变类型和特征评估潜在风险等级

  • 临床建议:提供初步的诊疗建议和就医指导

  • 鉴别诊断:提示需要鉴别的其他相似病变

3.2.5 数据可视化与分析模块

系统提供多种数据可视化方式,帮助用户更好地理解检测结果:

  • 检测结果可视化:在原图上标注病变区域和类型

  • 统计图表:展示各类病变的分布情况和趋势变化

  • 用户行为分析:分析用户的使用模式和偏好

3.2.6 记录管理与查询模块

系统全面记录所有检测操作,包括:

  • 图像检测记录:保存每张检测图片的原图、结果图和分析报告

  • 视频检测记录:保存视频检测的摘要信息和关键帧结果

  • 实时检测记录:记录摄像头检测的统计结果和异常发现

  • 高级查询功能:支持按时间、病变类型、置信度等多种条件查询

3.3 数据库设计

系统使用MySQL数据库存储所有数据,主要数据表包括:

  1. 用户表(users):存储用户基本信息、权限设置和登录凭证

  2. 图片检测记录表

  3. 视频检测记录表

  4. 摄像头检测记录表

数据集介绍

4.1 数据集构成

本系统使用的皮肤病数据集共包含973张高质量皮肤病变图像,按照医学标准进行专业标注。数据集详细划分如下:

  • 训练集:681张图像,用于模型训练和参数优化

  • 验证集:97张图像,用于训练过程中的模型评估和超参数调整

  • 测试集:195张图像,用于最终模型性能评估

4.2 数据类别与分布

数据集涵盖七种常见皮肤病变类型,每类病变的医学特征和临床意义如下:

  1. 鲍温氏病(Bowen's Disease)

    • 特征:红色或棕红色的鳞屑性斑片,边界清晰但不规则

    • 临床意义:表皮内鳞状细胞癌,有潜在恶变风险

  2. 基底细胞癌(Basal Cell Carcinoma)

    • 特征:珍珠样丘疹或结节,表面可见毛细血管扩张

    • 临床意义:最常见的皮肤癌,局部侵袭性强但转移罕见

  3. 良性角化病变(Benign Keratosis Lesions)

    • 特征:界限清楚的褐色或黑色斑块,表面粗糙

    • 临床意义:良性病变,通常无需治疗

  4. 皮肤纤维瘤(Dermatofibroma)

    • 特征:坚硬的丘疹或结节,颜色从粉红到棕褐色不等

    • 临床意义:良性纤维组织增生,无需特殊处理

  5. 黑色素瘤(Melanoma)

    • 特征:不对称、边界不规则、颜色不均匀、直径较大

    • 临床意义:恶性程度最高的皮肤癌,早期诊断至关重要

  6. 黑色素细胞痣(Melanocytic Nevus)

    • 特征:圆形或椭圆形,颜色均匀,边界清晰

    • 临床意义:常见的良性色素性病变

  7. 血管病变(Vascular Lesions)

    • 特征:红色或紫色皮损,按压可褪色

    • 临床意义:包括血管瘤、血管畸形等多种类型

功能模块


✅ 用户登录注册:支持密码检测,保存到MySQL数据库。

✅ 支持四种YOLO模型切换,YOLOv8、YOLOv10、YOLOv11、YOLOv12。

✅ 信息可视化,数据可视化。

✅ 图片检测支持AI分析功能,deepseek

✅ 支持图像检测、视频检测和摄像头实时检测,检测结果保存到MySQL数据库。

✅ 图片识别记录管理、视频识别记录管理和摄像头识别记录管理。

✅ 用户管理模块,管理员可以对用户进行增删改查。

✅ 个人中心,可以修改自己的信息,密码姓名头像等等。
 

登录注册模块

可视化模块

图像检测模块

  • YOLO模型集成 (v8/v10/v11/v12)

  • DeepSeek多模态分析

  • 支持格式:JPG/PNG/MP4/RTSP

视频检测模块

实时检测模块

图片识别记录管理

视频识别记录管理

摄像头识别记录管理

用户管理模块

数据管理模块(MySQL表设计)

  • users - 用户信息表

  • imgrecords- 图片检测记录表

  • videorecords- 视频检测记录表

  • camerarecords- 摄像头检测记录表

模型训练结果

#coding:utf-8
#根据实际情况更换模型
# yolon.yaml (nano):轻量化模型,适合嵌入式设备,速度快但精度略低。
# yolos.yaml (small):小模型,适合实时任务。
# yolom.yaml (medium):中等大小模型,兼顾速度和精度。
# yolob.yaml (base):基本版模型,适合大部分应用场景。
# yolol.yaml (large):大型模型,适合对精度要求高的任务。
 
from ultralytics import YOLO
 
model_path = 'pt/yolo12s.pt'
data_path = 'data.yaml'
 
if __name__ == '__main__':
    model = YOLO(model_path)
    results = model.train(data=data_path,
                          epochs=500,
                          batch=64,
                          device='0',
                          workers=0,
                          project='runs',
                          name='exp',
                          )
 
 
 
 
 
 
 
 

YOLOv8

YOLOv10

YOLOv11

YOLOv12

前端代码展示

部分代码

<template>
	<div class="home-container layout-pd">
		<el-row :gutter="15" class="home-card-two mb15">
			<el-col :xs="24" :sm="14" :md="14" :lg="16" :xl="16">
				<div class="home-card-item">
					<div style="height: 100%" ref="homeBarRef"></div>
				</div>
			</el-col>
			<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8" class="home-media">
				<div class="home-card-item">
					<div style="height: 100%" ref="homePieRef"></div>
				</div>
			</el-col>
		</el-row>
		<el-row :gutter="15" class="home-card-three">
			<el-col :xs="24" :sm="14" :md="14" :lg="8" :xl="8" class="home-media">
				<div class="home-card-item">
					<div style="height: 100%" ref="homeradarRef"></div>
				</div>
			</el-col>
			<el-col :xs="24" :sm="10" :md="10" :lg="16" :xl="16">
				<div class="home-card-item">
					<div class="home-card-item-title">实时检测记录</div>
					<div class="home-monitor">
						<div class="flex-warp">
							<el-table :data="state.paginatedData" style="width: 100%" height="360" v-loading="state.loading">
								<el-table-column prop="patientName" label="患者姓名" align="center" width="120" />
								<el-table-column prop="label" label="检测结果" align="center" width="120">
									<template #default="scope">
										<el-tag 
											:type="getResultType(scope.row.label)"
											effect="light"
										>
											{{ formatLabel(scope.row.label) }}
										</el-tag>
									</template>
								</el-table-column>
								<el-table-column prop="confidence" label="置信度" align="center" width="120">
									<template #default="scope">
										{{ formatConfidence(scope.row.confidence) }}
									</template>
								</el-table-column>
								<el-table-column prop="weight" label="模型权重" align="center" width="120" />
								<el-table-column prop="conf" label="检测阈值" align="center" width="120" />
								<el-table-column prop="startTime" label="检测时间" align="center" width="180" />
								<el-table-column label="操作" align="center" width="100">
									<template #default="scope">
										<el-button link type="primary" size="small" @click="handleViewDetail(scope.row)">
											详情
										</el-button>
									</template>
								</el-table-column>
							</el-table>
							<div class="pagination-container">
								<el-pagination
									v-model:current-page="state.currentPage"
									v-model:page-size="state.pageSize"
									:page-sizes="[10, 20, 50, 100]"
									:small="true"
									:layout="layout"
									:total="state.total"
									@size-change="handleSizeChange"
									@current-change="handleCurrentChange"
								/>
							</div>
						</div>
					</div>
				</div>
			</el-col>
		</el-row>
		<el-row :gutter="15" class="home-card-three" style="margin-top: 15px;">
			<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
				<div class="home-card-item">
					<div style="height: 100%" ref="homeLineRef"></div>
				</div>
			</el-col>
		</el-row>

		<!-- 详情弹窗 -->
		<el-dialog
			v-model="state.detailDialogVisible"
			:title="`皮肤病检测记录详情 - ${state.selectedRecord?.patientName || ''}`"
			width="80%"
			:close-on-click-modal="false"
			:close-on-press-escape="false"
			center
		>
			<div class="detail-container" v-loading="state.detailLoading">
				<el-row :gutter="20">
					<!-- 检测图片 -->
					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
						<div class="detail-section">
							<h3 class="detail-title">皮肤病变图片</h3>
							<div class="image-container">
								<div class="img-wrapper" @click="previewImage(getImageUrl(state.selectedRecord?.inputImg), '皮肤病变图片')">
									<img 
										:src="getImageUrl(state.selectedRecord?.inputImg)" 
										alt="皮肤病变图片" 
										class="detection-image"
										v-if="state.selectedRecord?.inputImg"
									/>
									<div class="img-overlay" v-if="state.selectedRecord?.inputImg">
										<el-icon><View /></el-icon>
									</div>
									<div v-else class="image-placeholder">
										<el-icon><Picture /></el-icon>
										<span>暂无皮肤病变图片</span>
									</div>
								</div>
							</div>
						</div>
					</el-col>
					
					<!-- 检测信息 -->
					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
						<div class="detail-section">
							<h3 class="detail-title">检测信息</h3>
							<el-descriptions :column="1" border>
								<el-descriptions-item label="患者姓名">
									{{ state.selectedRecord?.patientName || '未知' }}
								</el-descriptions-item>
								
								<el-descriptions-item label="检测结果">
									<el-tag 
										:type="getResultType(state.selectedRecord?.label || '')"
										effect="light"
									>
										{{ formatLabel(state.selectedRecord?.label || '') }}
									</el-tag>
								</el-descriptions-item>
								
								<el-descriptions-item label="置信度">
									{{ formatConfidence(state.selectedRecord?.confidence || '') }}
								</el-descriptions-item>
								
								<el-descriptions-item label="模型权重">
									{{ state.selectedRecord?.weight || '未知' }}
								</el-descriptions-item>
								
								<el-descriptions-item label="检测阈值">
									{{ state.selectedRecord?.conf || '未知' }}
								</el-descriptions-item>
								
								<el-descriptions-item label="检测时间">
									{{ state.selectedRecord?.startTime || '未知' }}
								</el-descriptions-item>
								
								<el-descriptions-item label="患者年龄" v-if="state.selectedRecord?.patientAge">
									{{ state.selectedRecord.patientAge }} 岁
								</el-descriptions-item>
								
								<el-descriptions-item label="病变部位" v-if="state.selectedRecord?.bodyPart">
									{{ state.selectedRecord.bodyPart }}
								</el-descriptions-item>
								
								<el-descriptions-item label="检测详情" v-if="hasDetectionDetails">
									<div class="detection-details">
										<div 
											v-for="(item, index) in getDetectionDetails()" 
											:key="index"
											class="detail-item"
										>
											<span class="detail-label">{{ item.label }}:</span>
											<span class="detail-value">{{ item.confidence }}</span>
										</div>
									</div>
								</el-descriptions-item>
							</el-descriptions>
						</div>
					</el-col>
				</el-row>
				
				<!-- 原图与检测结果对比 -->
				<el-row :gutter="20" v-if="state.selectedRecord?.inputImg || state.selectedRecord?.outImg">
					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
						<div class="detail-section">
							<h3 class="detail-title">原始皮肤图片</h3>
							<div class="image-container">
								<div class="img-wrapper" @click="previewImage(getImageUrl(state.selectedRecord.inputImg), '原始皮肤图片')">
									<img 
										:src="getImageUrl(state.selectedRecord.inputImg)" 
										alt="原始皮肤图片" 
										class="detection-image"
										v-if="state.selectedRecord?.inputImg"
									/>
									<div class="img-overlay" v-if="state.selectedRecord?.inputImg">
										<el-icon><View /></el-icon>
									</div>
									<div v-else class="image-placeholder">
										<el-icon><Picture /></el-icon>
										<span>暂无原始皮肤图片</span>
									</div>
								</div>
							</div>
						</div>
					</el-col>
					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
						<div class="detail-section">
							<h3 class="detail-title">检测结果图片</h3>
							<div class="image-container">
								<div class="img-wrapper" @click="previewImage(getImageUrl(state.selectedRecord.outImg), '检测结果图片')">
									<img 
										:src="getImageUrl(state.selectedRecord.outImg)" 
										alt="检测结果图片" 
										class="detection-image"
										v-if="state.selectedRecord?.outImg"
									/>
									<div class="img-overlay" v-if="state.selectedRecord?.outImg">
										<el-icon><View /></el-icon>
									</div>
									<div v-else class="image-placeholder">
										<el-icon><Picture /></el-icon>
										<span>暂无检测结果图片</span>
									</div>
								</div>
							</div>
						</div>
					</el-col>
				</el-row>

				<!-- AI诊断建议 -->
				<el-row :gutter="20" v-if="state.selectedRecord?.suggestion">
					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
						<div class="detail-section">
							<h3 class="detail-title">AI诊断建议</h3>
							<div class="suggestion-content">
								<el-alert
									:type="getSuggestionAlertType(state.selectedRecord?.label)"
									:title="state.selectedRecord.suggestion"
									:closable="false"
									show-icon
								/>
							</div>
						</div>
					</el-col>
				</el-row>
			</div>
			
			<template #footer>
				<span class="dialog-footer">
					<el-button @click="state.detailDialogVisible = false">关闭</el-button>
					<el-button type="primary" @click="handleDownloadImage" :disabled="!state.selectedRecord?.inputImg">
						<el-icon><Download /></el-icon>
						下载检测图片
					</el-button>
					<el-button type="success" @click="handlePrintReport" :disabled="!state.selectedRecord">
						<el-icon><Printer /></el-icon>
						打印诊断报告
					</el-button>
				</span>
			</template>
		</el-dialog>

		<!-- 图片预览弹窗 -->
		<el-dialog 
			v-model="state.previewDialog.visible" 
			:title="state.previewDialog.title" 
			width="60%"
			align-center
			class="image-preview-dialog">
			<div class="preview-content">
				<img :src="state.previewDialog.imageUrl" :alt="state.previewDialog.title" class="preview-image" />
			</div>
		</el-dialog>
	</div>
</template>

<script setup lang="ts" name="home">
import { reactive, onMounted, ref, watch, nextTick, onActivated, markRaw, computed } from 'vue';
import * as echarts from 'echarts';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Picture, Download, View, Printer } from '@element-plus/icons-vue';
import request from '/@/utils/request';

// 定义变量内容
const homeLineRef = ref();
const homePieRef = ref();
const homeBarRef = ref();
const homeradarRef = ref();
const storesTagsViewRoutes = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);

// 皮肤病类别
const SKIN_DISEASE_TYPES = ['鲍温病', '基底细胞癌', '良性角化病病变', '皮肤纤维瘤', '黑色素瘤', '黑素细胞痣', '血管病变'];

const state = reactive({
	data: [] as any,
	paginatedData: [] as any,
	loading: false,
	currentPage: 1,
	pageSize: 10,
	total: 0,
	global: {
		homeChartOne: null,
		homeChartTwo: null,
		homeCharThree: null,
		homeCharFour: null,
		dispose: [null, '', undefined],
	} as any,
	myCharts: [] as EmptyArrayType,
	charts: {
		theme: '',
		bgColor: '',
		color: '#303133',
	},
	// 详情弹窗相关
	detailDialogVisible: false,
	detailLoading: false,
	selectedRecord: null as any,
	// 图片预览弹窗
	previewDialog: {
		visible: false,
		title: '',
		imageUrl: '',
	},
});

// 响应式分页数据
const layout = computed(() => {
	return window.innerWidth < 768 ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper';
});

// 获取图片URL
const getImageUrl = (imagePath: string) => {
	if (!imagePath) return '';
	// 如果已经是完整URL,直接返回
	if (imagePath.startsWith('http')) return imagePath;
	// 否则拼接基础URL
	return `/api${imagePath.startsWith('/') ? '' : '/'}${imagePath}`;
};

// 是否有检测详情
const hasDetectionDetails = computed(() => {
	if (!state.selectedRecord) return false;
	try {
		const labels = JSON.parse(state.selectedRecord.label || '[]');
		const confidences = JSON.parse(state.selectedRecord.confidence || '[]');
		return labels.length > 0 && confidences.length > 0;
	} catch {
		return false;
	}
});

// 获取检测详情
const getDetectionDetails = () => {
	if (!state.selectedRecord) return [];
	try {
		const labels = JSON.parse(state.selectedRecord.label || '[]');
		const confidences = JSON.parse(state.selectedRecord.confidence || '[]');
		
		return labels.map((label: string, index: number) => ({
			label,
			confidence: confidences[index] ? `${(parseFloat(confidences[index]) * 100).toFixed(1)}%` : '0%'
		}));
	} catch {
		return [];
	}
};

// 图片预览
const previewImage = (imageUrl: string, title: string) => {
	if (!imageUrl) {
		ElMessage.warning('没有可预览的图片');
		return;
	}
	state.previewDialog.imageUrl = imageUrl;
	state.previewDialog.title = title;
	state.previewDialog.visible = true;
};

// 分页处理
const handleSizeChange = (val: number) => {
	state.pageSize = val;
	state.currentPage = 1;
	updatePaginatedData();
};

const handleCurrentChange = (val: number) => {
	state.currentPage = val;
	updatePaginatedData();
};

const updatePaginatedData = () => {
	const start = (state.currentPage - 1) * state.pageSize;
	const end = start + state.pageSize;
	state.paginatedData = state.data.slice(start, end);
};

// 格式化标签显示
const formatLabel = (label: string) => {
	try {
		const labels = JSON.parse(label);
		return labels.length > 0 ? labels.join(', ') : '未检测到病变';
	} catch {
		return label || '未检测到病变';
	}
};

// 根据检测结果设置标签类型
const getResultType = (label: string) => {
	try {
		const labels = JSON.parse(label);
		// 恶性病变 - 高风险
		if (labels.includes('黑色素瘤') || labels.includes('基底细胞癌') || labels.includes('鲍温病')) {
			return 'danger';
		}
		// 良性病变 - 中等风险
		if (labels.includes('血管病变') || labels.includes('皮肤纤维瘤')) {
			return 'warning';
		}
		// 良性病变 - 低风险
		if (labels.includes('良性角化病病变') || labels.includes('黑素细胞痣')) {
			return 'success';
		}
		// 未检测到病变
		if (labels.length === 0) {
			return 'info';
		}
		return 'warning';
	} catch {
		return 'info';
	}
};

// 获取诊断建议的警告类型
const getSuggestionAlertType = (label: string) => {
	try {
		const labels = JSON.parse(label);
		// 恶性病变 - 高风险
		if (labels.includes('黑色素瘤') || labels.includes('基底细胞癌') || labels.includes('鲍温病')) {
			return 'error';
		}
		// 良性病变 - 中等风险
		if (labels.includes('血管病变') || labels.includes('皮肤纤维瘤')) {
			return 'warning';
		}
		// 良性病变 - 低风险
		if (labels.includes('良性角化病病变') || labels.includes('黑素细胞痣')) {
			return 'success';
		}
		return 'info';
	} catch {
		return 'info';
	}
};

// 格式化置信度显示
const formatConfidence = (confidence: string) => {
	try {
		const confidences = JSON.parse(confidence);
		if (confidences.length === 0) return '0%';
		
		const maxConfidence = Math.max(...confidences.map((conf: any) => {
			if (typeof conf === 'number') return conf * 100;
			if (typeof conf === 'string') {
				const num = parseFloat(conf.replace('%', ''));
				return isNaN(num) ? 0 : num;
			}
			return 0;
		}));
		return `${maxConfidence.toFixed(1)}%`;
	} catch {
		// 如果解析失败,尝试直接显示
		if (typeof confidence === 'number') {
			return `${(confidence * 100).toFixed(1)}%`;
		}
		return confidence || '0%';
	}
};

// 查看详情
const handleViewDetail = async (row: any) => {
	state.selectedRecord = row;
	state.detailDialogVisible = true;
	state.detailLoading = true;
	
	// 如果需要从服务器获取更详细的数据
	try {
		const res = await request.get(`/api/imgRecords/${row.id}`);
		if (res.code == 0) {
			// 确保数据格式一致
			const record = res.data;
			state.selectedRecord = {
				...record,
				// 确保图片字段正确
				inputImg: record.inputImg || record.imagePath,
				outImg: record.outImg || record.resultImagePath,
				// 确保患者姓名字段正确
				patientName: record.patientName || record.username
			};
		}
	} catch (error) {
		console.error('获取详情失败:', error);
		// 如果API调用失败,使用已有数据
		state.selectedRecord = row;
	} finally {
		state.detailLoading = false;
	}
};

// 下载图片
const handleDownloadImage = async () => {
	if (!state.selectedRecord?.inputImg) {
		ElMessage.warning('没有可下载的图片');
		return;
	}
	
	try {
		const imageUrl = getImageUrl(state.selectedRecord.inputImg);
		const response = await fetch(imageUrl);
		const blob = await response.blob();
		
		const url = window.URL.createObjectURL(blob);
		const a = document.createElement('a');
		a.href = url;
		
		// 从路径中提取文件名,如果没有则使用默认名称
		const filename = state.selectedRecord.inputImg.split('/').pop() || 
			`skin_disease_${state.selectedRecord.patientName}_${state.selectedRecord.startTime?.replace(/[: ]/g, '-') || 'unknown'}.jpg`;
		
		a.download = filename;
		document.body.appendChild(a);
		a.click();
		window.URL.revokeObjectURL(url);
		document.body.removeChild(a);
		
		ElMessage.success('图片下载成功');
	} catch (error) {
		console.error('下载图片失败:', error);
		ElMessage.error('图片下载失败');
	}
};

// 打印诊断报告
const handlePrintReport = () => {
	if (!state.selectedRecord) {
		ElMessage.warning('没有可打印的记录');
		return;
	}
	
	ElMessageBox.confirm(
		'确定要打印诊断报告吗?',
		'打印诊断报告',
		{
			confirmButtonText: '确定',
			cancelButtonText: '取消',
			type: 'warning',
		}
	).then(() => {
		// 这里可以调用打印功能
		ElMessage.success('诊断报告打印功能开发中');
	}).catch(() => {
		// 用户取消
	});
};

// 折线图 - 近十日预测数量
const initLineChart = () => {
	if (!state.global.dispose.some((b: any) => b === state.global.homeChartOne)) state.global.homeChartOne?.dispose();
	state.global.homeChartOne = markRaw(echarts.init(homeLineRef.value, state.charts.theme));
	
	// 统计每天的预测数量
	const counts: Record<string, number> = {};
	state.data.forEach((prediction: any) => {
		if (prediction.startTime) {
			const date = prediction.startTime.split(' ')[0];
			counts[date] = (counts[date] || 0) + 1;
		}
	});

	const sortedDatesDesc = Object.keys(counts).sort((a, b) => b.localeCompare(a));
	const latestDatesDesc = sortedDatesDesc.slice(0, 10);
	const latestDates = latestDatesDesc.sort((a, b) => a.localeCompare(b));

	const result = {
		dateData: latestDates,
		valueData: latestDates.map(date => counts[date])
	};

	const option = {
		backgroundColor: state.charts.bgColor,
		title: {
			text: '近十日皮肤病检测数量趋势',
			x: 'left',
			textStyle: { fontSize: 15, color: state.charts.color },
		},
		grid: { top: 70, right: 20, bottom: 30, left: 30 },
		tooltip: { 
			trigger: 'axis',
			formatter: (params: any) => {
				const data = params[0];
				return `${data.name}<br/>检测数量: ${data.value}`;
			}
		},
		xAxis: {
			data: result.dateData,
			axisLabel: {
				color: state.charts.color,
				rotate: 45
			},
		},
		yAxis: [
			{
				type: 'value',
				name: '检测数量',
				splitLine: { show: true, lineStyle: { type: 'dashed', color: state.charts.theme === 'dark' ? '#444' : '#f5f5f5' } },
				axisLabel: {
					color: state.charts.color,
				},
			},
		],
		series: [
			{
				name: '检测数量',
				type: 'line',
				symbolSize: 6,
				symbol: 'circle',
				smooth: true,
				data: result.valueData,
				lineStyle: { color: '#36A2EB' },
				itemStyle: { color: '#36A2EB', borderColor: '#36A2EB' },
				areaStyle: {
					color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
						{ offset: 0, color: '#36A2EBb3' },
						{ offset: 1, color: '#36A2EB03' },
					]),
				},
			},
		],
	};

	state.global.homeChartOne.setOption(option);
	state.myCharts.push(state.global.homeChartOne);
};

// 饼图 - 患者检测分布
const initPieChart = () => {
	if (!state.global.dispose.some((b: any) => b === state.global.homeChartTwo)) state.global.homeChartTwo?.dispose();
	state.global.homeChartTwo = markRaw(echarts.init(homePieRef.value, state.charts.theme));
	
	const patientNameCounts: Record<string, number> = {};
	state.data.forEach((prediction: any) => {
		const patientName = prediction.patientName || prediction.username || '未知患者';
		patientNameCounts[patientName] = (patientNameCounts[patientName] || 0) + 1;
	});

	const sortedPatientNames = Object.keys(patientNameCounts).sort((a, b) => patientNameCounts[b] - patientNameCounts[a]);
	const topPatientNames = sortedPatientNames.slice(0, 6);
	const topValues = topPatientNames.map(u => patientNameCounts[u]);

	const pieData = topPatientNames.map((patientName, i) => ({
		name: patientName,
		value: topValues[i]
	}));

	const option = {
		backgroundColor: state.charts.bgColor,
		title: {
			text: '患者检测分布',
			x: 'left',
			textStyle: { fontSize: '15', color: state.charts.color },
		},
		legend: {
			top: 'bottom',
			textStyle: {
				color: state.charts.color
			}
		},
		tooltip: {
			trigger: 'item',
			formatter: '{a} <br/>{b}: {c} ({d}%)'
		},
		series: [
			{
				type: 'pie',
				radius: ['40%', '70%'],
				center: ['50%', '50%'],
				avoidLabelOverlap: true,
				itemStyle: {
					borderRadius: 10,
					borderColor: state.charts.bgColor,
					borderWidth: 2
				},
				label: {
					show: true,
					formatter: '{b}: {c}次',
					color: state.charts.color
				},
				emphasis: {
					label: {
						show: true,
						fontSize: '14',
						fontWeight: 'bold'
					}
				},
				data: pieData
			}
		]
	};

	state.global.homeChartTwo.setOption(option);
	state.myCharts.push(state.global.homeChartTwo);
};

// 雷达图 - 患者置信度分析
const initradarChart = () => {
	if (!state.global.dispose.some((b: any) => b === state.global.homeCharFour)) state.global.homeCharFour?.dispose();
	state.global.homeCharFour = markRaw(echarts.init(homeradarRef.value, state.charts.theme));
	
	const confStatsByUser: Record<string, { total: number, count: number }> = {};
	
	state.data.forEach((prediction: any) => {
		const patientName = prediction.patientName || prediction.username || '未知患者';
		let confidenceValue = 0;
		
		try {
			const confidences = JSON.parse(prediction.confidence || '[]');
			if (confidences.length > 0) {
				// 取最大置信度
				confidenceValue = Math.max(...confidences.map((conf: any) => {
					if (typeof conf === 'number') return conf;
					if (typeof conf === 'string') {
						const num = parseFloat(conf.replace('%', '')) / 100;
						return isNaN(num) ? 0 : num;
					}
					return 0;
				}));
			}
		} catch {
			// 如果解析失败,尝试直接使用数值
			if (typeof prediction.confidence === 'number') {
				confidenceValue = prediction.confidence;
			}
		}
		
		if (!confStatsByUser[patientName]) {
			confStatsByUser[patientName] = { total: confidenceValue, count: 1 };
		} else {
			confStatsByUser[patientName].total += confidenceValue;
			confStatsByUser[patientName].count += 1;
		}
	});

	const avgConfData = Object.keys(confStatsByUser).map(patientName => ({
		patientName,
		avgConf: confStatsByUser[patientName].total / confStatsByUser[patientName].count,
	}));

	const topAvgConfData = avgConfData.slice(0, 7);
	const data = topAvgConfData.map(item => Number((item.avgConf * 100).toFixed(2)));
	const indicatorNames = topAvgConfData.map(item => item.patientName);

	const indicator = indicatorNames.map((name) => ({ 
		name, 
		max: 100 
	}));

	const option = {
		backgroundColor: state.charts.bgColor,
		title: {
			text: '患者检测置信度分析',
			x: 'left',
			textStyle: { fontSize: '15', color: state.charts.color },
		},
		tooltip: {
			formatter: (params: any) => {
				return `${params.name}: ${params.value}%`;
			}
		},
		radar: {
			radius: '65%',
			splitNumber: 4,
			indicator: indicator,
			axisName: {
				color: state.charts.color,
				fontSize: 12
			},
			splitArea: {
				areaStyle: {
					color: ['rgba(54,162,235,0.1)', 'rgba(54,162,235,0.05)'],
				}
			},
			splitLine: {
				lineStyle: {
					color: 'rgba(54,162,235,0.3)'
				}
			},
			axisLine: {
				lineStyle: {
					color: 'rgba(54,162,235,0.5)'
				}
			}
		},
		series: [{
			type: 'radar',
			data: [{
				value: data,
				name: '置信度',
				areaStyle: {
					color: 'rgba(54,162,235,0.3)'
				},
				lineStyle: {
					color: '#36A2EB'
				},
				itemStyle: {
					color: '#36A2EB'
				},
				label: {
					show: true,
					formatter: (params: any) => {
						return params.value + '%';
					}
				}
			}]
		}]
	};

	state.global.homeCharFour.setOption(option);
	state.myCharts.push(state.global.homeCharFour);
};

// 柱状图 - 检测结果统计
const initBarChart = () => {
	if (!state.global.dispose.some((b: any) => b === state.global.homeCharThree)) state.global.homeCharThree?.dispose();
	state.global.homeCharThree = markRaw(echarts.init(homeBarRef.value, state.charts.theme));
	
	const categories = SKIN_DISEASE_TYPES;
	const counts: Record<string, number> = {};
	categories.forEach(cat => counts[cat] = 0);

	state.data.forEach((item: any) => {
		let detectedDiseases: string[] = [];
		
		try {
			const labels: string[] = JSON.parse(item.label || '[]');
			detectedDiseases = labels;
		} catch {
			// 如果解析失败,检查原始label字段
			const label = item.label || '';
			if (label) {
				detectedDiseases = [label];
			}
		}
		
		// 统计每种皮肤病的出现次数
		detectedDiseases.forEach((disease: string) => {
			if (categories.includes(disease)) {
				counts[disease]++;
			}
		});
	});

	const countStrings = categories.map(cat => counts[cat]);
	// 定义颜色:恶性病变用红色,良性病变用绿色,其他用橙色
	const colors = ['#FF6B6B', '#FF6B6B', '#4CAF50', '#4CAF50', '#FF6B6B', '#4CAF50', '#FF9800'];

	const option = {
		backgroundColor: state.charts.bgColor,
		title: {
			text: '皮肤病类型检测分布',
			x: 'left',
			textStyle: { fontSize: '15', color: state.charts.color },
		},
		tooltip: { 
			trigger: 'axis',
			formatter: (params: any) => {
				const data = params[0];
				return `${data.name}<br/>数量: ${data.value}`;
			}
		},
		grid: { top: 70, right: 30, bottom: 30, left: 50 },
		xAxis: [
			{
				type: 'category',
				data: categories,
				axisTick: { show: false },
				axisLabel: {
					color: state.charts.color,
					rotate: 45
				},
			},
		],
		yAxis: [
			{
				type: 'value',
				name: '检测数量',
				splitLine: { 
					show: true, 
					lineStyle: { 
						type: 'dashed', 
						color: state.charts.theme === 'dark' ? '#444' : '#f5f5f5' 
					} 
				},
				axisLabel: {
					color: state.charts.color,
				},
			},
		],
		series: [
			{
				name: '检测数量',
				type: 'bar',
				barWidth: 40,
				itemStyle: {
					color: (params: any) => colors[params.dataIndex],
					borderRadius: [4, 4, 0, 0],
				},
				label: {
					show: true,
					position: 'top',
					color: state.charts.color
				},
				data: countStrings,
			},
		],
	};
	
	state.global.homeCharThree.setOption(option);
	state.myCharts.push(state.global.homeCharThree);
};

// 批量设置 echarts resize
const initEchartsResizeFun = () => {
	nextTick(() => {
		for (let i = 0; i < state.myCharts.length; i++) {
			setTimeout(() => {
				state.myCharts[i]?.resize();
			}, i * 1000);
		}
	});
};

const initEchartsResize = () => {
	window.addEventListener('resize', initEchartsResizeFun);
};

// 加载数据
const loadData = async () => {
	state.loading = true;
	try {
		const res = await request.get('/api/imgRecords/all');
		if (res.code == 0) {
			// 转换数据格式,确保与图片记录页面一致
			state.data = res.data.map((record: any, index: number) => {
				// 统一数据格式
				const transformedRecord = {
					id: record.id,
					num: index + 1,
					// 图片字段统一
					inputImg: record.inputImg || record.imagePath,
					outImg: record.outImg || record.resultImagePath,
					// 患者信息字段
					patientName: record.patientName || record.username,
					patientAge: record.patientAge,
					bodyPart: record.bodyPart,
					// 其他字段
					weight: record.weight,
					conf: record.conf,
					ai: record.ai,
					suggestion: record.suggestion,
					startTime: record.startTime,
					label: record.label,
					confidence: record.confidence,
					// 确保family字段存在
					family: record.family || []
				};
				
				// 如果没有family字段,尝试从label和confidence构建
				if (!transformedRecord.family || transformedRecord.family.length === 0) {
					try {
						const labels = JSON.parse(record.label || '[]');
						const confidences = JSON.parse(record.confidence || '[]');
						transformedRecord.family = labels.map((label: string, idx: number) => ({
							label: label,
							confidence: confidences[idx] || 0,
							startTime: record.startTime
						}));
					} catch (error) {
						console.error('构建family字段失败:', error);
						transformedRecord.family = [];
					}
				}
				
				return transformedRecord;
			}).reverse();
			
			state.total = state.data.length;
			updatePaginatedData();
			
			// 初始化图表
			setTimeout(() => {
				initLineChart();
				initradarChart();
				initPieChart();
				initBarChart();
			}, 100);
		} else {
			ElMessage.error(res.msg || '加载数据失败');
		}
	} catch (error) {
		console.error('加载数据失败:', error);
		ElMessage.error('加载数据失败,请检查网络连接');
	} finally {
		state.loading = false;
	}
};

// 页面加载时
onMounted(() => {
	loadData();
	initEchartsResize();
});

// 由于页面缓存原因,keep-alive
onActivated(() => {
	initEchartsResizeFun();
});

// 监听相关状态变化
watch(
	() => isTagsViewCurrenFull.value,
	() => {
		initEchartsResizeFun();
	}
);

watch(
	() => themeConfig.value.isIsDark,
	(isIsDark) => {
		nextTick(() => {
			state.charts.theme = isIsDark ? 'dark' : '';
			state.charts.bgColor = isIsDark ? 'transparent' : '';
			state.charts.color = isIsDark ? '#dadada' : '#303133';
			setTimeout(() => {
				initLineChart();
				initradarChart();
				initPieChart();
				initBarChart();
			}, 500);
		});
	},
	{
		deep: true,
		immediate: true,
	}
);
</script>

<style scoped lang="scss">
$homeNavLengh: 8;

.home-container {
	overflow: hidden;

	.home-card-one,
	.home-card-two,
	.home-card-three {
		.home-card-item {
			width: 100%;
			height: 130px;
			border-radius: 4px;
			transition: all ease 0.3s;
			padding: 20px;
			overflow: hidden;
			background: var(--el-color-white);
			color: var(--el-text-color-primary);
			border: 1px solid var(--next-border-color-light);

			&:hover {
				box-shadow: 0 2px 12px var(--next-color-dark-hover);
				transition: all ease 0.3s;
			}

			&-icon {
				width: 70px;
				height: 70px;
				border-radius: 100%;
				flex-shrink: 1;

				i {
					color: var(--el-text-color-placeholder);
				}
			}

			&-title {
				font-size: 15px;
				font-weight: bold;
				height: 30px;
				margin-bottom: 15px;
				color: var(--el-text-color-primary);
				border-bottom: 1px solid var(--next-border-color-light);
				padding-bottom: 10px;
			}
		}
	}

	.home-card-two,
	.home-card-three {
		.home-card-item {
			height: 400px;
			width: 100%;
			overflow: hidden;

			.home-monitor {
				height: 100%;

				.flex-warp-item {
					width: 25%;
					height: 111px;
					display: flex;

					.flex-warp-item-box {
						margin: auto;
						text-align: center;
						color: var(--el-text-color-primary);
						display: flex;
						border-radius: 5px;
						background: var(--next-bg-color);
						cursor: pointer;
						transition: all 0.3s ease;

						&:hover {
							background: var(--el-color-primary-light-9);
							transition: all 0.3s ease;
						}
					}

					@for $i from 0 through $homeNavLengh {
							.home-animation#{$i} {
							opacity: 0;
							animation-name: error-num;
							animation-duration: 0.5s;
							animation-fill-mode: forwards;
							animation-delay: calc($i/10) + s;
						}
					}
				}
			}
		}
	}
}

.pagination-container {
	display: flex;
	justify-content: flex-end;
	margin-top: 15px;
	padding: 10px 0;
}

/* 详情弹窗样式 */
.detail-container {
	padding: 10px 0;
}

.detail-section {
	margin-bottom: 20px;
}

.detail-title {
	font-size: 16px;
	font-weight: bold;
	margin-bottom: 15px;
	color: var(--el-text-color-primary);
	border-left: 4px solid var(--el-color-primary);
	padding-left: 10px;
}

.image-container {
	width: 100%;
	height: 300px;
	display: flex;
	justify-content: center;
	align-items: center;
	border: 1px solid var(--next-border-color-light);
	border-radius: 8px;
	overflow: hidden;
	background-color: var(--el-fill-color-light);
	margin-bottom: 10px;
}

.img-wrapper {
	position: relative;
	display: flex;
	justify-content: center;
	align-items: center;
	cursor: pointer;
	border-radius: 6px;
	overflow: hidden;
	height: 100%;
	width: 100%;
	
	&:hover {
		.img-overlay {
			opacity: 1;
		}
		
		.detection-image {
			transform: scale(1.05);
		}
	}
	
	.detection-image {
		width: 100%;
		height: 100%;
		object-fit: cover;
		border-radius: 4px;
		border: 1px solid var(--next-border-color-light);
		transition: transform 0.3s ease;
	}
	
	.img-overlay {
		position: absolute;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		background: rgba(0, 0, 0, 0.5);
		display: flex;
		justify-content: center;
		align-items: center;
		opacity: 0;
		transition: opacity 0.3s ease;
		
		.el-icon {
			color: white;
			font-size: 24px;
		}
	}
}

.image-error,
.image-placeholder {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	color: var(--el-text-color-secondary);
	
	.el-icon {
		font-size: 48px;
		margin-bottom: 10px;
	}
}

.image-actions {
	display: flex;
	gap: 10px;
	justify-content: center;
}

.detection-details {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.detail-item {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 4px 0;
}

.detail-label {
	font-weight: 500;
	color: var(--el-text-color-primary);
}

.detail-value {
	color: var(--el-text-color-regular);
}

.suggestion-content {
	margin-top: 10px;
}

.dialog-footer {
	display: flex;
	justify-content: flex-end;
	gap: 10px;
}

// 图片预览弹窗样式
.image-preview-dialog {
	.preview-content {
		display: flex;
		justify-content: center;
		align-items: center;
		
		.preview-image {
			max-width: 100%;
			max-height: 70vh;
			object-fit: contain;
			border-radius: 8px;
		}
	}
}

/* 响应式调整 */
@media (max-width: 768px) {
	.home-media {
		margin-top: 15px;
	}
	
	.pagination-container {
		justify-content: center;
	}
	
	.image-container {
		height: 250px;
	}
	
	.detail-section {
		margin-bottom: 15px;
	}
	
	.image-actions {
		flex-direction: column;
	}
	
	.dialog-footer {
		flex-direction: column;
	}
}

@keyframes error-num {
	0% {
		opacity: 0;
		transform: translateY(20px);
	}
	100% {
		opacity: 1;
		transform: translateY(0);
	}
}
</style>

后端代码展示

 项目源码+数据集下载链接

完整代码在哔哩哔哩视频下方简介内获取

基于深度学习的皮肤病识别检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibili

基于深度学习的皮肤病识别检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1vt2YBKEhT/?vd_source=549d0b4e2b8999929a61a037fcce3b0fhttps://www.bilibili.com/video/BV1vt2YBKEhT

 项目安装教程

https://www.bilibili.com/video/BV1YLsXzJE2X/?spm_id_from=333.1387.homepage.video_card.click

YOLO+spring boot+vue项目环境部署教程(YOLOv8、YOLOv10、YOLOv11、YOLOv12)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1YLsXzJE2X/?spm_id_from=333.1387.homepage.video_card.click&vd_source=549d0b4e2b8999929a61a037fcce3b0f

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐