This commit is contained in:
zpc 2026-03-16 22:59:37 +08:00
parent fa6b186563
commit a7e5c11007
9 changed files with 710 additions and 0 deletions

View File

@ -155,6 +155,15 @@ public class AdminBusinessDbContext : DbContext
#endregion
#region
/// <summary>
/// 报告页面配置表
/// </summary>
public DbSet<ReportPageConfig> ReportPageConfigs { get; set; } = null!;
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
@ -398,5 +407,15 @@ public class AdminBusinessDbContext : DbContext
entity.HasIndex(e => e.Status)
.HasDatabaseName("IX_business_pages_status");
});
// =============================================
// ReportPageConfig 配置
// =============================================
modelBuilder.Entity<ReportPageConfig>(entity =>
{
// SortOrder 索引
entity.HasIndex(e => e.SortOrder)
.HasDatabaseName("ix_rpc_sort_order");
});
}
}

View File

@ -0,0 +1,68 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Admin.Business.Entities;
/// <summary>
/// 报告页面配置表
/// </summary>
[Table("report_page_configs")]
public class ReportPageConfig
{
/// <summary>
/// 主键ID
/// </summary>
[Key]
public long Id { get; set; }
/// <summary>
/// 页面类型1静态图片 2网页截图
/// </summary>
public int PageType { get; set; }
/// <summary>
/// 页面标识名称
/// </summary>
[Required]
[MaxLength(50)]
public string PageName { get; set; } = null!;
/// <summary>
/// 页面显示标题
/// </summary>
[Required]
[MaxLength(100)]
public string Title { get; set; } = null!;
/// <summary>
/// 排序序号
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 静态图片路径PageType=1 时使用)
/// </summary>
[MaxLength(500)]
public string? ImageUrl { get; set; }
/// <summary>
/// 网页路由路径PageType=2 时使用)
/// </summary>
[MaxLength(200)]
public string? RouteUrl { get; set; }
/// <summary>
/// 状态0禁用 1启用
/// </summary>
public int Status { get; set; } = 1;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdateTime { get; set; }
}

View File

@ -265,6 +265,20 @@ public static class ErrorCodes
#endregion
#region (3800-3899)
/// <summary>
/// 报告页面配置不存在
/// </summary>
public const int ReportPageConfigNotFound = 3801;
/// <summary>
/// 页面标识名称已存在
/// </summary>
public const int ReportPageNameExists = 3802;
#endregion
#region (5000-5999)
/// <summary>

View File

@ -0,0 +1,135 @@
namespace MiAssessment.Admin.Business.Models.ReportPageConfig;
/// <summary>
/// 报告页面配置 DTO
/// </summary>
public class ReportPageConfigDto
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 页面类型1静态图片 2网页截图
/// </summary>
public int PageType { get; set; }
/// <summary>
/// 页面类型名称
/// </summary>
public string PageTypeName { get; set; } = "";
/// <summary>
/// 页面标识名称
/// </summary>
public string PageName { get; set; } = null!;
/// <summary>
/// 页面显示标题
/// </summary>
public string Title { get; set; } = null!;
/// <summary>
/// 排序序号
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 静态图片路径
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 网页路由路径
/// </summary>
public string? RouteUrl { get; set; }
/// <summary>
/// 状态0禁用 1启用
/// </summary>
public int Status { get; set; }
/// <summary>
/// 状态名称
/// </summary>
public string StatusName { get; set; } = "";
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
}
/// <summary>
/// 报告页面配置查询请求
/// </summary>
public class ReportPageConfigQueryRequest : PagedRequest
{
/// <summary>
/// 页面类型筛选
/// </summary>
public int? PageType { get; set; }
/// <summary>
/// 状态筛选
/// </summary>
public int? Status { get; set; }
/// <summary>
/// 标题关键字
/// </summary>
public string? Title { get; set; }
}
/// <summary>
/// 创建报告页面配置请求
/// </summary>
public class CreateReportPageConfigRequest
{
/// <summary>
/// 页面类型1静态图片 2网页截图
/// </summary>
public int PageType { get; set; }
/// <summary>
/// 页面标识名称
/// </summary>
public string PageName { get; set; } = null!;
/// <summary>
/// 页面显示标题
/// </summary>
public string Title { get; set; } = null!;
/// <summary>
/// 排序序号
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 静态图片路径
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 网页路由路径
/// </summary>
public string? RouteUrl { get; set; }
/// <summary>
/// 状态0禁用 1启用
/// </summary>
public int Status { get; set; } = 1;
}
/// <summary>
/// 更新报告页面配置请求
/// </summary>
public class UpdateReportPageConfigRequest : CreateReportPageConfigRequest
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
}

View File

@ -0,0 +1,40 @@
using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.ReportPageConfig;
namespace MiAssessment.Admin.Business.Services.Interfaces;
/// <summary>
/// 报告页面配置服务接口
/// </summary>
public interface IReportPageConfigService
{
/// <summary>
/// 获取报告页面配置列表
/// </summary>
Task<PagedResult<ReportPageConfigDto>> GetListAsync(ReportPageConfigQueryRequest request);
/// <summary>
/// 创建报告页面配置
/// </summary>
Task<long> CreateAsync(CreateReportPageConfigRequest request);
/// <summary>
/// 更新报告页面配置
/// </summary>
Task<bool> UpdateAsync(UpdateReportPageConfigRequest request);
/// <summary>
/// 删除报告页面配置
/// </summary>
Task<bool> DeleteAsync(long id);
/// <summary>
/// 更新状态
/// </summary>
Task<bool> UpdateStatusAsync(long id, int status);
/// <summary>
/// 批量更新排序
/// </summary>
Task<bool> UpdateSortAsync(List<SortItem> items);
}

View File

@ -0,0 +1,207 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MiAssessment.Admin.Business.Data;
using MiAssessment.Admin.Business.Entities;
using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.Common;
using MiAssessment.Admin.Business.Models.ReportPageConfig;
using MiAssessment.Admin.Business.Services.Interfaces;
namespace MiAssessment.Admin.Business.Services;
/// <summary>
/// 报告页面配置服务实现
/// </summary>
public class ReportPageConfigService : IReportPageConfigService
{
private readonly AdminBusinessDbContext _dbContext;
private readonly ILogger<ReportPageConfigService> _logger;
private static readonly Dictionary<int, string> PageTypeNames = new()
{
{ 1, "静态图片" },
{ 2, "网页截图" }
};
private static readonly Dictionary<int, string> StatusNames = new()
{
{ 0, "禁用" },
{ 1, "启用" }
};
public ReportPageConfigService(
AdminBusinessDbContext dbContext,
ILogger<ReportPageConfigService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
/// <inheritdoc />
public async Task<PagedResult<ReportPageConfigDto>> GetListAsync(ReportPageConfigQueryRequest request)
{
var query = _dbContext.ReportPageConfigs.AsNoTracking().AsQueryable();
if (request.PageType.HasValue)
query = query.Where(p => p.PageType == request.PageType.Value);
if (request.Status.HasValue)
query = query.Where(p => p.Status == request.Status.Value);
if (!string.IsNullOrWhiteSpace(request.Title))
query = query.Where(p => p.Title.Contains(request.Title));
var total = await query.CountAsync();
var items = await query
.OrderBy(p => p.SortOrder)
.Skip(request.Skip)
.Take(request.PageSize)
.Select(p => new ReportPageConfigDto
{
Id = p.Id,
PageType = p.PageType,
PageName = p.PageName,
Title = p.Title,
SortOrder = p.SortOrder,
ImageUrl = p.ImageUrl,
RouteUrl = p.RouteUrl,
Status = p.Status,
CreateTime = p.CreateTime
})
.ToListAsync();
// 内存中映射名称
foreach (var item in items)
{
item.PageTypeName = PageTypeNames.TryGetValue(item.PageType, out var pn) ? pn : "未知";
item.StatusName = StatusNames.TryGetValue(item.Status, out var sn) ? sn : "未知";
}
return PagedResult<ReportPageConfigDto>.Create(items, total, request.Page, request.PageSize);
}
/// <inheritdoc />
public async Task<long> CreateAsync(CreateReportPageConfigRequest request)
{
if (string.IsNullOrWhiteSpace(request.PageName))
throw new BusinessException(ErrorCodes.ParamError, "页面标识不能为空");
if (string.IsNullOrWhiteSpace(request.Title))
throw new BusinessException(ErrorCodes.ParamError, "页面标题不能为空");
// 检查 PageName 唯一性
var exists = await _dbContext.ReportPageConfigs
.AnyAsync(p => p.PageName == request.PageName);
if (exists)
throw new BusinessException(ErrorCodes.ReportPageNameExists, "页面标识已存在");
var entity = new ReportPageConfig
{
PageType = request.PageType,
PageName = request.PageName,
Title = request.Title,
SortOrder = request.SortOrder,
ImageUrl = request.ImageUrl,
RouteUrl = request.RouteUrl,
Status = request.Status,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
_dbContext.ReportPageConfigs.Add(entity);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("创建报告页面配置成功ID: {Id}, 标题: {Title}", entity.Id, entity.Title);
return entity.Id;
}
/// <inheritdoc />
public async Task<bool> UpdateAsync(UpdateReportPageConfigRequest request)
{
var entity = await _dbContext.ReportPageConfigs.FindAsync(request.Id);
if (entity == null)
throw new BusinessException(ErrorCodes.ReportPageConfigNotFound, "报告页面配置不存在");
if (string.IsNullOrWhiteSpace(request.PageName))
throw new BusinessException(ErrorCodes.ParamError, "页面标识不能为空");
if (string.IsNullOrWhiteSpace(request.Title))
throw new BusinessException(ErrorCodes.ParamError, "页面标题不能为空");
// 检查 PageName 唯一性(排除自身)
var exists = await _dbContext.ReportPageConfigs
.AnyAsync(p => p.PageName == request.PageName && p.Id != request.Id);
if (exists)
throw new BusinessException(ErrorCodes.ReportPageNameExists, "页面标识已存在");
entity.PageType = request.PageType;
entity.PageName = request.PageName;
entity.Title = request.Title;
entity.SortOrder = request.SortOrder;
entity.ImageUrl = request.ImageUrl;
entity.RouteUrl = request.RouteUrl;
entity.Status = request.Status;
entity.UpdateTime = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("更新报告页面配置成功ID: {Id}", entity.Id);
return true;
}
/// <inheritdoc />
public async Task<bool> DeleteAsync(long id)
{
var entity = await _dbContext.ReportPageConfigs.FindAsync(id);
if (entity == null)
throw new BusinessException(ErrorCodes.ReportPageConfigNotFound, "报告页面配置不存在");
_dbContext.ReportPageConfigs.Remove(entity);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("删除报告页面配置成功ID: {Id}", id);
return true;
}
/// <inheritdoc />
public async Task<bool> UpdateStatusAsync(long id, int status)
{
var entity = await _dbContext.ReportPageConfigs.FindAsync(id);
if (entity == null)
throw new BusinessException(ErrorCodes.ReportPageConfigNotFound, "报告页面配置不存在");
entity.Status = status;
entity.UpdateTime = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("更新报告页面配置状态成功ID: {Id}, 状态: {Status}", id, status);
return true;
}
/// <inheritdoc />
public async Task<bool> UpdateSortAsync(List<SortItem> items)
{
if (items == null || items.Count == 0) return true;
var ids = items.Select(i => i.Id).ToList();
var entities = await _dbContext.ReportPageConfigs
.Where(p => ids.Contains(p.Id))
.ToListAsync();
foreach (var item in items)
{
var entity = entities.FirstOrDefault(e => e.Id == item.Id);
if (entity != null)
{
entity.SortOrder = item.Sort;
entity.UpdateTime = DateTime.Now;
}
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation("批量更新报告页面排序成功,更新数量: {Count}", items.Count);
return true;
}
}

View File

@ -0,0 +1,75 @@
@page "/report/full"
@model MiAssessment.Api.Pages.Report.FullReportModel
@{
ViewData["Title"] = "测评报告完整预览";
Layout = null;
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测评报告完整预览</title>
<link rel="stylesheet" href="/css/pages/full-report.css" />
</head>
<body>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="full-error">
<p>@Model.ErrorMessage</p>
</div>
}
else
{
<div class="full-container">
<div class="full-header">
<span class="full-header-title">测评报告预览</span>
<span class="full-header-count">共 @Model.Pages.Count 页</span>
</div>
<div class="full-pages">
@foreach (var item in Model.Pages)
{
<div class="full-page-wrapper">
<div class="full-page-frame">
<iframe class="full-page-iframe"
data-src="@Model.BuildPageUrl(item.RouteUrl!)"
frameborder="0"
scrolling="no"
title="@item.Title"></iframe>
</div>
</div>
}
</div>
</div>
}
<script>
// IntersectionObserver 懒加载 iframe
(function () {
var iframes = document.querySelectorAll('iframe.full-page-iframe[data-src]');
if (!iframes.length) return;
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var iframe = entry.target;
if (!iframe.src) {
iframe.src = iframe.getAttribute('data-src');
}
observer.unobserve(iframe);
}
});
}, {
rootMargin: '500px 0px'
});
iframes.forEach(function (iframe) {
observer.observe(iframe);
});
})();
</script>
</body>
</html>

View File

@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
namespace MiAssessment.Api.Pages.Report;
/// <summary>
/// 报告完整预览页 PageModel
/// 路由:/report/full?recordId=3
/// 读取 report_page_configs 表,按 SortOrder 顺序展示所有报告页面
/// </summary>
public class FullReportModel : PageModel
{
private readonly MiAssessmentDbContext _dbContext;
/// <summary>
/// 测评记录ID
/// </summary>
public long RecordId { get; set; }
/// <summary>
/// 所有启用的报告页面配置(按 SortOrder 排序)
/// </summary>
public List<ReportPageConfig> Pages { get; set; } = new();
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage { get; set; }
public FullReportModel(MiAssessmentDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IActionResult> OnGetAsync(long? recordId)
{
if (recordId == null || recordId <= 0)
{
ErrorMessage = "缺少测评记录参数";
return Page();
}
RecordId = recordId.Value;
// 只加载网页截图页PageType=2静态图片页有专门的网页不需要在此展示
Pages = await _dbContext.ReportPageConfigs
.AsNoTracking()
.Where(p => p.Status == 1 && p.PageType == 2)
.OrderBy(p => p.SortOrder)
.ToListAsync();
return Page();
}
/// <summary>
/// 构建 iframe 的完整 URL自动拼接 recordId
/// </summary>
public string BuildPageUrl(string routeUrl)
{
if (string.IsNullOrEmpty(routeUrl)) return "";
// 如果已有查询参数,用 & 拼接;否则用 ?
var separator = routeUrl.Contains('?') ? "&" : "?";
return $"{routeUrl}{separator}recordId={RecordId}";
}
}

View File

@ -0,0 +1,84 @@
/* 报告完整预览页样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #e8e8e8;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
min-height: 100vh;
}
/* 错误提示 */
.full-error {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
color: #ff4d4f;
font-size: 18px;
}
/* 外层容器 */
.full-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px 0 60px;
}
/* 顶部信息栏 */
.full-header {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
padding: 16px 0 24px;
}
.full-header-title {
font-size: 20px;
font-weight: 600;
color: #333;
}
.full-header-count {
font-size: 14px;
color: #999;
}
/* 页面列表 */
.full-pages {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
/* 单个页面包装 */
.full-page-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
/* 页面框架(固定 1309×926 */
.full-page-frame {
width: 1309px;
height: 926px;
background-color: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
}
/* iframe */
.full-page-iframe {
width: 1309px;
height: 926px;
border: none;
display: block;
}