Merge branch 'master' into codex/shiyan
This commit is contained in:
@@ -79,6 +79,38 @@ public sealed class OperationsController(
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(await healthService.CheckAsync(cancellationToken));
|
||||
|
||||
[HttpGet("swagger")]
|
||||
public async Task<ActionResult<SwaggerDocumentationSettings>> GetSwaggerSettings(
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(new SwaggerDocumentationSettings(await IsSwaggerEnabledAsync(cancellationToken)));
|
||||
|
||||
[HttpPut("swagger")]
|
||||
public async Task<ActionResult<SwaggerDocumentationSettings>> UpdateSwaggerSettings(
|
||||
UpdateSwaggerDocumentationSettings request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var setting = await db.SystemFeatureSettings.SingleOrDefaultAsync(
|
||||
x => x.Key == SystemFeatureKeys.SwaggerDocumentation,
|
||||
cancellationToken);
|
||||
if (setting is null)
|
||||
{
|
||||
setting = new SystemFeatureSetting
|
||||
{
|
||||
Key = SystemFeatureKeys.SwaggerDocumentation,
|
||||
IsEnabled = request.IsEnabled
|
||||
};
|
||||
db.SystemFeatureSettings.Add(setting);
|
||||
}
|
||||
else
|
||||
{
|
||||
setting.IsEnabled = request.IsEnabled;
|
||||
setting.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Ok(new SwaggerDocumentationSettings(setting.IsEnabled));
|
||||
}
|
||||
|
||||
[HttpGet("audit-logs")]
|
||||
public async Task<ActionResult<PagedResult<AuditLogItem>>> GetAuditLogs(
|
||||
[FromQuery] int page = 1,
|
||||
@@ -523,6 +555,12 @@ public sealed class OperationsController(
|
||||
x.CreatedAt >= from,
|
||||
cancellationToken);
|
||||
|
||||
private async Task<bool> IsSwaggerEnabledAsync(CancellationToken cancellationToken) =>
|
||||
await db.SystemFeatureSettings.AsNoTracking()
|
||||
.Where(x => x.Key == SystemFeatureKeys.SwaggerDocumentation)
|
||||
.Select(x => (bool?)x.IsEnabled)
|
||||
.SingleOrDefaultAsync(cancellationToken) ?? false;
|
||||
|
||||
private ActionResult? ValidatePaging(int page, int pageSize)
|
||||
{
|
||||
if (page is < 1 or > 100000 || pageSize is < 1 or > 100)
|
||||
@@ -604,3 +642,7 @@ public sealed record OperationsSummary(
|
||||
public sealed record CreateBackupRequest([MaxLength(200)] string? Note);
|
||||
|
||||
public sealed record RestoreDrillRequest([Required] string Confirmation);
|
||||
|
||||
public sealed record SwaggerDocumentationSettings(bool IsEnabled);
|
||||
|
||||
public sealed record UpdateSwaggerDocumentationSettings(bool IsEnabled);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Jiaowu.Api.Domain.Common;
|
||||
|
||||
namespace Jiaowu.Api.Domain.System;
|
||||
|
||||
public static class SystemFeatureKeys
|
||||
{
|
||||
public const string SwaggerDocumentation = "SwaggerDocumentation";
|
||||
}
|
||||
|
||||
public sealed class SystemFeatureSetting : EntityBase
|
||||
{
|
||||
public required string Key { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
}
|
||||
@@ -137,6 +137,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
Set<BackgroundJobOutboxMessage>();
|
||||
public DbSet<AppUpdateRelease> AppUpdateReleases =>
|
||||
Set<AppUpdateRelease>();
|
||||
public DbSet<SystemFeatureSetting> SystemFeatureSettings =>
|
||||
Set<SystemFeatureSetting>();
|
||||
public DbSet<RefreshSession> RefreshSessions => Set<RefreshSession>();
|
||||
|
||||
protected override void ConfigureConventions(
|
||||
@@ -1455,6 +1457,12 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.HasIndex(x => x.CreatedAt);
|
||||
});
|
||||
|
||||
builder.Entity<SystemFeatureSetting>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Key).HasMaxLength(100);
|
||||
entity.HasIndex(x => x.Key).IsUnique();
|
||||
});
|
||||
|
||||
builder.Entity<OfficialDocument>(entity =>
|
||||
{
|
||||
entity.Property(x => x.DocumentNumber).HasMaxLength(50);
|
||||
|
||||
@@ -90,6 +90,8 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260809_47_course_grade_distribution";
|
||||
private const string TeachingTaskGradeAnalyticsMigration =
|
||||
"20260809_48_teaching_task_grade_analytics";
|
||||
private const string SwaggerDocumentationSettingMigration =
|
||||
"20260809_49_swagger_documentation_setting";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -669,6 +671,14 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
TeachingTaskGradeAnalyticsMigration,
|
||||
TeachingTaskGradeAnalyticsStatements,
|
||||
cancellationToken);
|
||||
var swaggerSettingsExist = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'SystemFeatureSettings'")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
SwaggerDocumentationSettingMigration,
|
||||
swaggerSettingsExist ? [] : SwaggerDocumentationSettingStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -2913,4 +2923,21 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
ADD COLUMN "Kind" INTEGER NOT NULL DEFAULT 1;
|
||||
"""
|
||||
];
|
||||
|
||||
private static readonly string[] SwaggerDocumentationSettingStatements =
|
||||
[
|
||||
"""
|
||||
CREATE TABLE "SystemFeatureSettings" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_SystemFeatureSettings" PRIMARY KEY,
|
||||
"Key" TEXT NOT NULL,
|
||||
"IsEnabled" INTEGER NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE UNIQUE INDEX "IX_SystemFeatureSettings_Key"
|
||||
ON "SystemFeatureSettings" ("Key");
|
||||
"""
|
||||
];
|
||||
}
|
||||
|
||||
+6595
File diff suppressed because it is too large
Load Diff
+44
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SwaggerDocumentationSetting : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SystemFeatureSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
Key = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
|
||||
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SystemFeatureSettings", x => x.Id);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SystemFeatureSettings_Key",
|
||||
table: "SystemFeatureSettings",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SystemFeatureSettings");
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -4778,6 +4778,34 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("BackgroundJobOutboxMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.System.SystemFeatureSetting", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SystemFeatureSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<Import Project="..\..\versions.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>2.3.2-beta.1</Version>
|
||||
<Version>$(JiaowuBackendVersion)</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<SpaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../web'))</SpaRoot>
|
||||
@@ -18,15 +20,26 @@
|
||||
Include="..\..\.env.example"
|
||||
Link=".env.example"
|
||||
CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content
|
||||
Include="..\..\versions.props"
|
||||
Link="versions.props"
|
||||
CopyToOutputDirectory="PreserveNewest"
|
||||
CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.1.1" />
|
||||
<AssemblyMetadata
|
||||
Include="SwaggerDocumentVersion"
|
||||
Value="$(JiaowuSwaggerVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.1" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -40,11 +53,11 @@
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.8.0" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="3.119.2" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.2" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.2" />
|
||||
<PackageReference Include="SkiaSharp" Version="4.151.1" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="4.151.1" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target
|
||||
|
||||
+43
-15
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Domain.System;
|
||||
using Jiaowu.Api.Infrastructure.BackgroundJobs;
|
||||
using Jiaowu.Api.Infrastructure.Grades;
|
||||
using Jiaowu.Api.Infrastructure.Configuration;
|
||||
@@ -23,10 +24,11 @@ using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
EnvironmentFile.Load();
|
||||
@@ -62,6 +64,13 @@ if (confirmProductionDemoData && !seedDemoData)
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var swaggerDocumentVersion = typeof(Program).Assembly
|
||||
.GetCustomAttributes(
|
||||
typeof(System.Reflection.AssemblyMetadataAttribute),
|
||||
inherit: false)
|
||||
.OfType<System.Reflection.AssemblyMetadataAttribute>()
|
||||
.SingleOrDefault(x => x.Key == "SwaggerDocumentVersion")?.Value
|
||||
?? "v1";
|
||||
|
||||
if (seedDemoData && builder.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -608,10 +617,10 @@ builder.Services.AddControllers()
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
options.SwaggerDoc(swaggerDocumentVersion, new OpenApiInfo
|
||||
{
|
||||
Title = "大学教务管理系统 API",
|
||||
Version = "v1"
|
||||
Version = swaggerDocumentVersion
|
||||
});
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
@@ -621,17 +630,10 @@ builder.Services.AddSwaggerGen(options =>
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header
|
||||
});
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
options.AddSecurityRequirement(_ => new OpenApiSecurityRequirement
|
||||
{
|
||||
[
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
}
|
||||
new OpenApiSecuritySchemeReference("Bearer", null, null)
|
||||
] = []
|
||||
});
|
||||
});
|
||||
@@ -640,11 +642,37 @@ var app = builder.Build();
|
||||
|
||||
app.UseExceptionHandler();
|
||||
app.UseResponseCompression();
|
||||
if (app.Environment.IsDevelopment())
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
if (context.Request.Path.StartsWithSegments("/swagger"))
|
||||
{
|
||||
var isEnabled = await context.RequestServices
|
||||
.GetRequiredService<AppDbContext>()
|
||||
.SystemFeatureSettings
|
||||
.AsNoTracking()
|
||||
.Where(x => x.Key == SystemFeatureKeys.SwaggerDocumentation)
|
||||
.Select(x => (bool?)x.IsEnabled)
|
||||
.SingleOrDefaultAsync(context.RequestAborted) ?? false;
|
||||
if (!isEnabled)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.SwaggerEndpoint(
|
||||
$"/swagger/{swaggerDocumentVersion}/swagger.json",
|
||||
$"大学教务管理系统 API {swaggerDocumentVersion}");
|
||||
options.DocExpansion(DocExpansion.None);
|
||||
options.DefaultModelsExpandDepth(-1);
|
||||
options.DefaultModelExpandDepth(1);
|
||||
options.EnableFilter();
|
||||
});
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -33,6 +33,39 @@ public sealed class OperationsControllerTests
|
||||
Assert.Equal(SystemRoles.SuperAdmin, authorize.Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Swagger_documentation_is_closed_by_default_and_can_be_enabled()
|
||||
{
|
||||
var root = CreateTemporaryRoot();
|
||||
try
|
||||
{
|
||||
await using var fixture = await OperationsFixture.CreateAsync(root);
|
||||
|
||||
var initial = await fixture.Controller.GetSwaggerSettings(
|
||||
CancellationToken.None);
|
||||
var initialSettings = Assert.IsType<SwaggerDocumentationSettings>(
|
||||
Assert.IsType<OkObjectResult>(initial.Result).Value);
|
||||
Assert.False(initialSettings.IsEnabled);
|
||||
|
||||
var updated = await fixture.Controller.UpdateSwaggerSettings(
|
||||
new UpdateSwaggerDocumentationSettings(true),
|
||||
CancellationToken.None);
|
||||
var updatedSettings = Assert.IsType<SwaggerDocumentationSettings>(
|
||||
Assert.IsType<OkObjectResult>(updated.Result).Value);
|
||||
Assert.True(updatedSettings.IsEnabled);
|
||||
|
||||
var persisted = await fixture.Controller.GetSwaggerSettings(
|
||||
CancellationToken.None);
|
||||
var persistedSettings = Assert.IsType<SwaggerDocumentationSettings>(
|
||||
Assert.IsType<OkObjectResult>(persisted.Result).Value);
|
||||
Assert.True(persistedSettings.IsEnabled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteTemporaryRoot(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Audit_and_failed_job_queries_return_operational_records()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<JiaowuBackendVersion>2.3.2-beta.2</JiaowuBackendVersion>
|
||||
<JiaowuFrontendVersion>2.3.2-beta.2</JiaowuFrontendVersion>
|
||||
<JiaowuSwaggerVersion>2.3.2-beta.2</JiaowuSwaggerVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Generated
+1
-2
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.3.2-beta.1",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "web",
|
||||
"version": "2.3.2-beta.1",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^8.4.2",
|
||||
"@capacitor/barcode-scanner": "^3.1.0",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "2.3.2-beta.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -90,6 +90,12 @@ const filteredRows = computed(() => {
|
||||
.filter(Boolean).some((value) => String(value).toLowerCase().includes(q)),
|
||||
)
|
||||
})
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const pagedRows = computed(() => filteredRows.value.slice(
|
||||
(currentPage.value - 1) * pageSize.value,
|
||||
currentPage.value * pageSize.value,
|
||||
))
|
||||
const venueNatureOptions = [
|
||||
{ value: 1, label: '普通教室' },
|
||||
{ value: 2, label: '实验室' },
|
||||
@@ -99,8 +105,17 @@ const venueNatureOptions = [
|
||||
{ value: 32, label: '体育场地' },
|
||||
{ value: 64, label: '艺术场地' },
|
||||
]
|
||||
const venueNatureLabel = (value: number) => venueNatureOptions
|
||||
.filter((item) => (Number(value) & item.value) !== 0)
|
||||
const venueNatureValue = (value: unknown) => {
|
||||
if (typeof value === 'number') return value
|
||||
if (typeof value !== 'string') return 0
|
||||
const names: Record<string, number> = {
|
||||
GeneralClassroom: 1, Laboratory: 2, TrainingRoom: 4, ComputerLab: 8,
|
||||
LanguageLab: 16, SportsVenue: 32, ArtsVenue: 64,
|
||||
}
|
||||
return value.split(',').reduce((sum, name) => sum | (names[name.trim()] ?? 0), 0)
|
||||
}
|
||||
const venueNatureLabel = (value: unknown) => venueNatureOptions
|
||||
.filter((item) => (venueNatureValue(value) & item.value) !== 0)
|
||||
.map((item) => item.label)
|
||||
.join('、') || '未设置'
|
||||
|
||||
@@ -116,7 +131,7 @@ function resetForm(row?: Row) {
|
||||
capacity: 60, roomType: '普通教室', teachingVenueNatures: [1], equipment: '',
|
||||
}, row ?? {})
|
||||
if (active.value === 'classrooms') {
|
||||
const value = Number((row as any)?.teachingVenueNature ?? 1)
|
||||
const value = venueNatureValue((row as any)?.teachingVenueNature ?? 1)
|
||||
form.teachingVenueNatures = venueNatureOptions
|
||||
.filter((item) => (value & item.value) !== 0)
|
||||
.map((item) => item.value)
|
||||
@@ -125,6 +140,7 @@ function resetForm(row?: Row) {
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
currentPage.value = 1
|
||||
try {
|
||||
rows.value = (await http.get(`/base-data/${active.value}`)).data
|
||||
} catch (error) {
|
||||
@@ -290,6 +306,11 @@ onMounted(async () => {
|
||||
await Promise.all([load(), loadReferences()])
|
||||
})
|
||||
|
||||
watch(
|
||||
() => keyword.value,
|
||||
() => { currentPage.value = 1 },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.meta.baseGroup,
|
||||
async () => {
|
||||
@@ -363,7 +384,7 @@ watch(
|
||||
|
||||
<div v-if="active === 'terms'" class="term-mobile-list">
|
||||
<article
|
||||
v-for="row in filteredRows"
|
||||
v-for="row in pagedRows"
|
||||
:key="row.id"
|
||||
:class="academicTermRowClass(row)"
|
||||
>
|
||||
@@ -406,7 +427,7 @@ watch(
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredRows"
|
||||
:data="pagedRows"
|
||||
:row-class-name="termTableRowClass"
|
||||
class="data-table"
|
||||
:class="{ 'term-desktop-table': active === 'terms' }"
|
||||
@@ -472,6 +493,15 @@ watch(
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无数据,点击右上角开始新增" /></template>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="filteredRows.length > pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
class="table-pagination"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:total="filteredRows.length"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${title}`" width="560px">
|
||||
|
||||
@@ -98,12 +98,18 @@ interface PageResult<T> {
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
interface SwaggerDocumentationSettings {
|
||||
isEnabled: boolean
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const refreshing = ref(false)
|
||||
const backupBusy = ref(false)
|
||||
const summary = ref<Summary>()
|
||||
const backups = ref<BackupArtifact[]>([])
|
||||
const activeLedger = ref<'audit' | 'jobs'>('audit')
|
||||
const swaggerEnabled = ref(false)
|
||||
const swaggerBusy = ref(false)
|
||||
|
||||
const auditLoading = ref(false)
|
||||
const auditRows = ref<AuditLog[]>([])
|
||||
@@ -204,12 +210,36 @@ function httpStatusType(status: number) {
|
||||
}
|
||||
|
||||
async function loadSummary() {
|
||||
const [{ data: summaryData }, { data: backupData }] = await Promise.all([
|
||||
const [{ data: summaryData }, { data: backupData }, { data: swaggerData }] = await Promise.all([
|
||||
http.get<Summary>('/operations/summary'),
|
||||
http.get<BackupArtifact[]>('/operations/backups'),
|
||||
http.get<SwaggerDocumentationSettings>('/operations/swagger'),
|
||||
])
|
||||
summary.value = summaryData
|
||||
backups.value = backupData
|
||||
swaggerEnabled.value = swaggerData.isEnabled
|
||||
}
|
||||
|
||||
async function updateSwaggerDocumentation(value: string | number | boolean) {
|
||||
if (typeof value !== 'boolean') return
|
||||
const isEnabled = value
|
||||
swaggerBusy.value = true
|
||||
try {
|
||||
const { data } = await http.put<SwaggerDocumentationSettings>(
|
||||
'/operations/swagger',
|
||||
{ isEnabled },
|
||||
)
|
||||
swaggerEnabled.value = data.isEnabled
|
||||
ElMessage.success(data.isEnabled ? 'Swagger 文档已开放。' : 'Swagger 文档已关闭。')
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
swaggerBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openSwaggerDocumentation() {
|
||||
window.open('/swagger', '_blank', 'noopener')
|
||||
}
|
||||
|
||||
async function loadAudit() {
|
||||
@@ -371,6 +401,35 @@ onMounted(refreshAll)
|
||||
</el-button>
|
||||
</header>
|
||||
|
||||
<section class="swagger-panel">
|
||||
<div>
|
||||
<span>API DOCUMENTATION</span>
|
||||
<h3>Swagger 文档</h3>
|
||||
<p>
|
||||
{{ swaggerEnabled
|
||||
? '文档与 OpenAPI 定义当前已对外开放。'
|
||||
: '文档当前关闭,访问 /swagger 将返回 404。' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="swagger-actions">
|
||||
<el-switch
|
||||
:model-value="swaggerEnabled"
|
||||
:loading="swaggerBusy"
|
||||
active-text="已开放"
|
||||
inactive-text="已关闭"
|
||||
@change="updateSwaggerDocumentation"
|
||||
/>
|
||||
<el-button
|
||||
v-if="swaggerEnabled"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openSwaggerDocumentation"
|
||||
>
|
||||
打开文档
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="signal-board" aria-label="系统健康链路">
|
||||
<div class="signal-summary">
|
||||
<span>当前值守结论</span>
|
||||
@@ -670,6 +729,28 @@ onMounted(refreshAll)
|
||||
padding: 4px 2px 16px;
|
||||
}
|
||||
|
||||
.swagger-panel {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-left: 4px solid var(--signal);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.swagger-panel span {
|
||||
color: var(--signal);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.swagger-panel h3 { margin: 4px 0; }
|
||||
.swagger-panel p { color: var(--muted); margin: 0; }
|
||||
.swagger-actions { align-items: center; display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
|
||||
.console-kicker,
|
||||
.panel-heading span,
|
||||
.signal-summary > span,
|
||||
@@ -1011,6 +1092,7 @@ onMounted(refreshAll)
|
||||
@media (max-width: 560px) {
|
||||
.operations-console { gap: 12px; }
|
||||
.console-heading { align-items: flex-start; gap: 14px; }
|
||||
.swagger-panel { align-items: flex-start; flex-direction: column; }
|
||||
.console-heading p { font-size: 12px; line-height: 1.5; }
|
||||
.signal-track { display: block; }
|
||||
.signal-node { border-bottom: 1px solid #e4e9ea; padding: 13px 12px; }
|
||||
|
||||
+7
-4
@@ -5,9 +5,12 @@ import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(new URL('./package.json', import.meta.url), 'utf8'),
|
||||
) as { version: string }
|
||||
const versions = readFileSync(new URL('../versions.props', import.meta.url), 'utf8')
|
||||
const frontendVersion = /<JiaowuFrontendVersion>([^<]+)<\/JiaowuFrontendVersion>/.exec(versions)?.[1]
|
||||
|
||||
if (!frontendVersion) {
|
||||
throw new Error('versions.props 中缺少 JiaowuFrontendVersion。')
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
@@ -16,7 +19,7 @@ export default defineConfig(({ mode }) => {
|
||||
|
||||
return {
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(packageJson.version),
|
||||
__APP_VERSION__: JSON.stringify(frontendVersion),
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
|
||||
Reference in New Issue
Block a user