fix(web): 恢复视频继续播放和进度保存
- 将播放器收敛为单个共享实例,避免列表/网格内多个 video ref 导致进度保存失效 - 恢复视频播放器上方的继续播放提示 - 在播放、暂停、拖动、结束、切换页面和离开页面时保存播放位置 - 保留文件浏览分页和排序参数的前端调用
This commit is contained in:
parent
27e4029f4a
commit
c6b05c12e5
4
AGENTS.md
Normal file
4
AGENTS.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Codex Rules
|
||||||
|
|
||||||
|
- Do not manually edit files under `FileShare-EFCore/Migrations/**`. These files are generated by EF scripts.
|
||||||
|
- When database schema changes are needed, update the EF model/configuration only and ask the user to generate migrations with the project scripts.
|
||||||
@ -73,6 +73,7 @@ namespace FileShare_EFCore.Database
|
|||||||
entity.HasIndex(e => e.AbsolutePath).IsUnique().HasDatabaseName("idx-managed-file-record-absolute-path");
|
entity.HasIndex(e => e.AbsolutePath).IsUnique().HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
entity.HasIndex(e => new { e.MediaType, e.Exists }).HasDatabaseName("idx-managed-file-record-media-type-exists");
|
entity.HasIndex(e => new { e.MediaType, e.Exists }).HasDatabaseName("idx-managed-file-record-media-type-exists");
|
||||||
entity.HasIndex(e => e.LastPlayedAt).HasDatabaseName("idx-managed-file-record-last-played-at");
|
entity.HasIndex(e => e.LastPlayedAt).HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
entity.HasIndex(e => e.FileCreationTimeUtc).HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
entity.Property(e => e.FileName).HasMaxLength(260);
|
entity.Property(e => e.FileName).HasMaxLength(260);
|
||||||
entity.Property(e => e.RelativePath).HasMaxLength(1024);
|
entity.Property(e => e.RelativePath).HasMaxLength(1024);
|
||||||
entity.Property(e => e.AbsolutePath).HasMaxLength(2048);
|
entity.Property(e => e.AbsolutePath).HasMaxLength(2048);
|
||||||
|
|||||||
455
FileShare-EFCore/Migrations/MySQL/20260523023700_AutoMigration_20260523103531.Designer.cs
generated
Normal file
455
FileShare-EFCore/Migrations/MySQL/20260523023700_AutoMigration_20260523103531.Designer.cs
generated
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FileShare_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.MySQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MySqlAppDataContext))]
|
||||||
|
[Migration("20260523023700_AutoMigration_20260523103531")]
|
||||||
|
partial class AutoMigration_20260523103531
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AbsolutePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("varchar(2048)")
|
||||||
|
.HasColumnName("absolute-path");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<bool>("Exists")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("exists");
|
||||||
|
|
||||||
|
b.Property<string>("Extension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("varchar(32)")
|
||||||
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("varchar(260)")
|
||||||
|
.HasColumnName("file-name");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastPlayedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("last-played-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("last-seen-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastWriteTimeUtc")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("last-write-time-utc");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("MediaType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("media-type");
|
||||||
|
|
||||||
|
b.Property<double?>("PlaybackPosition")
|
||||||
|
.HasColumnType("double")
|
||||||
|
.HasColumnName("playback-position");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<long>("SizeBytes")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("size-bytes");
|
||||||
|
|
||||||
|
b.Property<int?>("ThumbnailId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("thumbnail-id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.Property<double?>("VideoDuration")
|
||||||
|
.HasColumnType("double")
|
||||||
|
.HasColumnName("video-duration");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-file-record");
|
||||||
|
|
||||||
|
b.HasIndex("AbsolutePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
|
b.HasIndex("LastPlayedAt")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("ThumbnailId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-thumbnail-id");
|
||||||
|
|
||||||
|
b.HasIndex("MediaType", "Exists")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-media-type-exists");
|
||||||
|
|
||||||
|
b.ToTable("managed-file-record", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库文件记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("display-name");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAvailable")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is-available");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("is-enabled");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanCompletedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("last-scan-completed-at");
|
||||||
|
|
||||||
|
b.Property<string>("LastScanError")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("varchar(2000)")
|
||||||
|
.HasColumnName("last-scan-error");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanStartedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("last-scan-started-at");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<int>("ScanIntervalMinutes")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("scan-interval-minutes");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-library-root");
|
||||||
|
|
||||||
|
b.HasIndex("Path")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-library-root-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-library-root", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库根目录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-thumbnail-map");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("RelativePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-relative-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-thumbnail-map", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件缩略图映射记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ThumbnailId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnail");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Thumbnails")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.MySQL
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260523103531 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record",
|
||||||
|
type: "datetime(6)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record",
|
||||||
|
column: "file-creation-time-utc");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -112,6 +112,10 @@ namespace FileShare_EFCore.Migrations.MySQL
|
|||||||
.HasColumnType("varchar(32)")
|
.HasColumnType("varchar(32)")
|
||||||
.HasColumnName("extension");
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
b.Property<string>("FileName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(260)
|
.HasMaxLength(260)
|
||||||
@ -173,6 +177,9 @@ namespace FileShare_EFCore.Migrations.MySQL
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
b.HasIndex("LastPlayedAt")
|
b.HasIndex("LastPlayedAt")
|
||||||
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
|||||||
470
FileShare-EFCore/Migrations/PostgreSQL/20260523023643_AutoMigration_20260523103531.Designer.cs
generated
Normal file
470
FileShare-EFCore/Migrations/PostgreSQL/20260523023643_AutoMigration_20260523103531.Designer.cs
generated
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FileShare_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.PostgreSQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PostgreSqlAppDataContext))]
|
||||||
|
[Migration("20260523023643_AutoMigration_20260523103531")]
|
||||||
|
partial class AutoMigration_20260523103531
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AbsolutePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)")
|
||||||
|
.HasColumnName("absolute-path");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<bool>("Exists")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("exists");
|
||||||
|
|
||||||
|
b.Property<string>("Extension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("character varying(260)")
|
||||||
|
.HasColumnName("file-name");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastPlayedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last-played-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last-seen-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastWriteTimeUtc")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last-write-time-utc");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("MediaType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("media-type");
|
||||||
|
|
||||||
|
b.Property<double?>("PlaybackPosition")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("playback-position");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<long>("SizeBytes")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("size-bytes");
|
||||||
|
|
||||||
|
b.Property<int?>("ThumbnailId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("thumbnail-id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.Property<double?>("VideoDuration")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("video-duration");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-file-record");
|
||||||
|
|
||||||
|
b.HasIndex("AbsolutePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
|
b.HasIndex("LastPlayedAt")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("ThumbnailId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-thumbnail-id");
|
||||||
|
|
||||||
|
b.HasIndex("MediaType", "Exists")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-media-type-exists");
|
||||||
|
|
||||||
|
b.ToTable("managed-file-record", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库文件记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("display-name");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAvailable")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is-available");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is-enabled");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanCompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last-scan-completed-at");
|
||||||
|
|
||||||
|
b.Property<string>("LastScanError")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)")
|
||||||
|
.HasColumnName("last-scan-error");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanStartedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last-scan-started-at");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<int>("ScanIntervalMinutes")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("scan-interval-minutes");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-library-root");
|
||||||
|
|
||||||
|
b.HasIndex("Path")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-library-root-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-library-root", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库根目录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-thumbnail-map");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("RelativePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-relative-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-thumbnail-map", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件缩略图映射记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ThumbnailId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnail");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Thumbnails")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.PostgreSQL
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260523103531 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record",
|
||||||
|
column: "file-creation-time-utc");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -119,6 +119,10 @@ namespace FileShare_EFCore.Migrations.PostgreSQL
|
|||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)")
|
||||||
.HasColumnName("extension");
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
b.Property<string>("FileName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(260)
|
.HasMaxLength(260)
|
||||||
@ -180,6 +184,9 @@ namespace FileShare_EFCore.Migrations.PostgreSQL
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
b.HasIndex("LastPlayedAt")
|
b.HasIndex("LastPlayedAt")
|
||||||
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
|||||||
453
FileShare-EFCore/Migrations/SQLite/20260523023607_AutoMigration_20260523103531.Designer.cs
generated
Normal file
453
FileShare-EFCore/Migrations/SQLite/20260523023607_AutoMigration_20260523103531.Designer.cs
generated
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FileShare_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.SQLite
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqliteAppDataContext))]
|
||||||
|
[Migration("20260523023607_AutoMigration_20260523103531")]
|
||||||
|
partial class AutoMigration_20260523103531
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AbsolutePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("absolute-path");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<bool>("Exists")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exists");
|
||||||
|
|
||||||
|
b.Property<string>("Extension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file-name");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastPlayedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last-played-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last-seen-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastWriteTimeUtc")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last-write-time-utc");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("MediaType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("media-type");
|
||||||
|
|
||||||
|
b.Property<double?>("PlaybackPosition")
|
||||||
|
.HasColumnType("REAL")
|
||||||
|
.HasColumnName("playback-position");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<long>("SizeBytes")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("size-bytes");
|
||||||
|
|
||||||
|
b.Property<int?>("ThumbnailId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("thumbnail-id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.Property<double?>("VideoDuration")
|
||||||
|
.HasColumnType("REAL")
|
||||||
|
.HasColumnName("video-duration");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-file-record");
|
||||||
|
|
||||||
|
b.HasIndex("AbsolutePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
|
b.HasIndex("LastPlayedAt")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("ThumbnailId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-thumbnail-id");
|
||||||
|
|
||||||
|
b.HasIndex("MediaType", "Exists")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-media-type-exists");
|
||||||
|
|
||||||
|
b.ToTable("managed-file-record", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库文件记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("display-name");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAvailable")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is-available");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("is-enabled");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanCompletedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last-scan-completed-at");
|
||||||
|
|
||||||
|
b.Property<string>("LastScanError")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last-scan-error");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanStartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last-scan-started-at");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<int>("ScanIntervalMinutes")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("scan-interval-minutes");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-library-root");
|
||||||
|
|
||||||
|
b.HasIndex("Path")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-library-root-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-library-root", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库根目录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-thumbnail-map");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("RelativePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-relative-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-thumbnail-map", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件缩略图映射记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ThumbnailId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnail");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Thumbnails")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.SQLite
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260523103531 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record",
|
||||||
|
column: "file-creation-time-utc");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -110,6 +110,10 @@ namespace FileShare_EFCore.Migrations.SQLite
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("extension");
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
b.Property<string>("FileName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(260)
|
.HasMaxLength(260)
|
||||||
@ -171,6 +175,9 @@ namespace FileShare_EFCore.Migrations.SQLite
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
b.HasIndex("LastPlayedAt")
|
b.HasIndex("LastPlayedAt")
|
||||||
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
|||||||
470
FileShare-EFCore/Migrations/SqlServer/20260523023625_AutoMigration_20260523103531.Designer.cs
generated
Normal file
470
FileShare-EFCore/Migrations/SqlServer/20260523023625_AutoMigration_20260523103531.Designer.cs
generated
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FileShare_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.SqlServer
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqlServerAppDataContext))]
|
||||||
|
[Migration("20260523023625_AutoMigration_20260523103531")]
|
||||||
|
partial class AutoMigration_20260523103531
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AbsolutePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)")
|
||||||
|
.HasColumnName("absolute-path");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<bool>("Exists")
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasColumnName("exists");
|
||||||
|
|
||||||
|
b.Property<string>("Extension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("nvarchar(32)")
|
||||||
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("nvarchar(260)")
|
||||||
|
.HasColumnName("file-name");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastPlayedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("last-played-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("last-seen-at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastWriteTimeUtc")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("last-write-time-utc");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("MediaType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)")
|
||||||
|
.HasColumnName("media-type");
|
||||||
|
|
||||||
|
b.Property<double?>("PlaybackPosition")
|
||||||
|
.HasColumnType("float")
|
||||||
|
.HasColumnName("playback-position");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<long>("SizeBytes")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("size-bytes");
|
||||||
|
|
||||||
|
b.Property<int?>("ThumbnailId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("thumbnail-id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.Property<double?>("VideoDuration")
|
||||||
|
.HasColumnType("float")
|
||||||
|
.HasColumnName("video-duration");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-file-record");
|
||||||
|
|
||||||
|
b.HasIndex("AbsolutePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
|
b.HasIndex("LastPlayedAt")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("ThumbnailId")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-thumbnail-id");
|
||||||
|
|
||||||
|
b.HasIndex("MediaType", "Exists")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-media-type-exists");
|
||||||
|
|
||||||
|
b.ToTable("managed-file-record", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库文件记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("display-name");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAvailable")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is-available");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasColumnName("is-enabled");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanCompletedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("last-scan-completed-at");
|
||||||
|
|
||||||
|
b.Property<string>("LastScanError")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)")
|
||||||
|
.HasColumnName("last-scan-error");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastScanStartedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("last-scan-started-at");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<int>("ScanIntervalMinutes")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("scan-interval-minutes");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-library-root");
|
||||||
|
|
||||||
|
b.HasIndex("Path")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-library-root-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-library-root", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件库根目录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)")
|
||||||
|
.HasColumnName("content-type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryRootId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("library-root-id");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)")
|
||||||
|
.HasColumnName("relative-path");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-managed-thumbnail-map");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryRootId")
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-root-id");
|
||||||
|
|
||||||
|
b.HasIndex("RelativePath")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-managed-thumbnail-map-relative-path");
|
||||||
|
|
||||||
|
b.ToTable("managed-thumbnail-map", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("文件缩略图映射记录");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ThumbnailId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnail");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
|
||||||
|
.WithMany("Thumbnails")
|
||||||
|
.HasForeignKey("LibraryRootId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("LibraryRoot");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
|
||||||
|
b.Navigation("Thumbnails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FileShare_EFCore.Migrations.SqlServer
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260523103531 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record",
|
||||||
|
column: "file-creation-time-utc");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "idx-managed-file-record-file-creation-time",
|
||||||
|
table: "managed-file-record");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "file-creation-time-utc",
|
||||||
|
table: "managed-file-record");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -119,6 +119,10 @@ namespace FileShare_EFCore.Migrations.SqlServer
|
|||||||
.HasColumnType("nvarchar(32)")
|
.HasColumnType("nvarchar(32)")
|
||||||
.HasColumnName("extension");
|
.HasColumnName("extension");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FileCreationTimeUtc")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("file-creation-time-utc");
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
b.Property<string>("FileName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(260)
|
.HasMaxLength(260)
|
||||||
@ -180,6 +184,9 @@ namespace FileShare_EFCore.Migrations.SqlServer
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
.HasDatabaseName("idx-managed-file-record-absolute-path");
|
||||||
|
|
||||||
|
b.HasIndex("FileCreationTimeUtc")
|
||||||
|
.HasDatabaseName("idx-managed-file-record-file-creation-time");
|
||||||
|
|
||||||
b.HasIndex("LastPlayedAt")
|
b.HasIndex("LastPlayedAt")
|
||||||
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
.HasDatabaseName("idx-managed-file-record-last-played-at");
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,10 @@ namespace FileShare_EFCore.Models
|
|||||||
[Column("last-write-time-utc")]
|
[Column("last-write-time-utc")]
|
||||||
public DateTime LastWriteTimeUtc { get; set; }
|
public DateTime LastWriteTimeUtc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>鏂囦欢鍦ㄦ枃浠剁郴缁熶腑鐨勫垱寤烘椂闂?UTC銆?/summary>
|
||||||
|
[Column("file-creation-time-utc")]
|
||||||
|
public DateTime? FileCreationTimeUtc { get; set; }
|
||||||
|
|
||||||
/// <summary>媒体类型:text、video、audio。</summary>
|
/// <summary>媒体类型:text、video、audio。</summary>
|
||||||
[Column("media-type")]
|
[Column("media-type")]
|
||||||
[MaxLength(20)]
|
[MaxLength(20)]
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using FileShare_Common.Core;
|
|||||||
using FileShare_Services.Core;
|
using FileShare_Services.Core;
|
||||||
using FileShare_Services.Services.FileLibrary;
|
using FileShare_Services.Services.FileLibrary;
|
||||||
using FileShare_Services.Services.QrCode;
|
using FileShare_Services.Services.QrCode;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace FileShare_Services.Endpoints
|
namespace FileShare_Services.Endpoints
|
||||||
{
|
{
|
||||||
@ -119,12 +120,15 @@ namespace FileShare_Services.Endpoints
|
|||||||
private static async Task<object?> GetFileStreamAsync(ServiceEndpointContext ctx)
|
private static async Task<object?> GetFileStreamAsync(ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||||
var service = sp?.GetService(typeof(IFileStreamService)) as IFileStreamService;
|
if (sp is null) return null;
|
||||||
if (service is null) return null;
|
|
||||||
|
|
||||||
if (!int.TryParse(ctx.Query.GetValueOrDefault("id"), out var id) || id <= 0)
|
if (!int.TryParse(ctx.Query.GetValueOrDefault("id"), out var id) || id <= 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
using var scope = sp.CreateScope();
|
||||||
|
var service = scope.ServiceProvider.GetService<IFileStreamService>();
|
||||||
|
if (service is null) return null;
|
||||||
|
|
||||||
return await service.GetFileStreamAsync(id);
|
return await service.GetFileStreamAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record SaveFileProgressRequest(
|
public sealed record SaveFileProgressRequest(
|
||||||
[property: JsonPropertyName("id")] int Id,
|
[property: JsonPropertyName("id")] int Id,
|
||||||
[property: JsonPropertyName("position")] double Position);
|
[property: JsonPropertyName("position")] double? Position);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页搜索已扫描文件的请求。
|
/// 分页搜索已扫描文件的请求。
|
||||||
@ -74,7 +74,9 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
[property: JsonPropertyName("pageSize")] int PageSize = 24,
|
[property: JsonPropertyName("pageSize")] int PageSize = 24,
|
||||||
[property: JsonPropertyName("mediaType")] string? MediaType = null,
|
[property: JsonPropertyName("mediaType")] string? MediaType = null,
|
||||||
[property: JsonPropertyName("keyword")] string? Keyword = null,
|
[property: JsonPropertyName("keyword")] string? Keyword = null,
|
||||||
[property: JsonPropertyName("rootId")] int RootId = 0);
|
[property: JsonPropertyName("rootId")] int RootId = 0,
|
||||||
|
[property: JsonPropertyName("sortBy")] string? SortBy = null,
|
||||||
|
[property: JsonPropertyName("sortDirection")] string? SortDirection = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 磁盘驱动器信息。
|
/// 磁盘驱动器信息。
|
||||||
@ -121,6 +123,7 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
string Extension,
|
string Extension,
|
||||||
long SizeBytes,
|
long SizeBytes,
|
||||||
DateTime LastWriteTimeUtc,
|
DateTime LastWriteTimeUtc,
|
||||||
|
DateTime? FileCreationTimeUtc,
|
||||||
string MediaType,
|
string MediaType,
|
||||||
string ContentType,
|
string ContentType,
|
||||||
string StreamUrl,
|
string StreamUrl,
|
||||||
@ -136,7 +139,12 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record BrowseDirectoryRequest(
|
public sealed record BrowseDirectoryRequest(
|
||||||
[property: JsonPropertyName("rootId")] int RootId = 0,
|
[property: JsonPropertyName("rootId")] int RootId = 0,
|
||||||
[property: JsonPropertyName("path")] string? Path = null);
|
[property: JsonPropertyName("path")] string? Path = null,
|
||||||
|
[property: JsonPropertyName("page")] int Page = 1,
|
||||||
|
[property: JsonPropertyName("pageSize")] int PageSize = 48,
|
||||||
|
[property: JsonPropertyName("mediaType")] string? MediaType = null,
|
||||||
|
[property: JsonPropertyName("sortBy")] string? SortBy = "name",
|
||||||
|
[property: JsonPropertyName("sortDirection")] string? SortDirection = "asc");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 浏览文件库目录的响应,包含当前路径、子目录列表和文件列表。
|
/// 浏览文件库目录的响应,包含当前路径、子目录列表和文件列表。
|
||||||
@ -144,7 +152,11 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
public sealed record BrowseDirectoryResponse(
|
public sealed record BrowseDirectoryResponse(
|
||||||
string CurrentPath,
|
string CurrentPath,
|
||||||
List<string> Subdirectories,
|
List<string> Subdirectories,
|
||||||
List<FileRecordDto> Files);
|
List<FileRecordDto> Files,
|
||||||
|
int Total,
|
||||||
|
int Page,
|
||||||
|
int PageSize,
|
||||||
|
int TotalPages);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文本文件预览内容,支持截断标记。
|
/// 文本文件预览内容,支持截断标记。
|
||||||
|
|||||||
@ -104,7 +104,12 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IApiResponse> SaveFileProgressAsync(SaveFileProgressRequest request)
|
public async Task<IApiResponse> SaveFileProgressAsync(SaveFileProgressRequest request)
|
||||||
{
|
{
|
||||||
await fileLibrary.SaveFileProgressAsync(request.Id, request.Position);
|
if (request.Id <= 0 || request.Position is null || double.IsNaN(request.Position.Value) || double.IsInfinity(request.Position.Value))
|
||||||
|
{
|
||||||
|
return ResponseHelper.Failure(400, "参数错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
await fileLibrary.SaveFileProgressAsync(request.Id, request.Position.Value);
|
||||||
return ResponseHelper.Succeed();
|
return ResponseHelper.Succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -180,6 +180,7 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
record.Extension = info.Extension.ToLowerInvariant();
|
record.Extension = info.Extension.ToLowerInvariant();
|
||||||
record.SizeBytes = info.Length;
|
record.SizeBytes = info.Length;
|
||||||
record.LastWriteTimeUtc = info.LastWriteTimeUtc;
|
record.LastWriteTimeUtc = info.LastWriteTimeUtc;
|
||||||
|
record.FileCreationTimeUtc = info.CreationTimeUtc;
|
||||||
record.MediaType = mediaType;
|
record.MediaType = mediaType;
|
||||||
record.ContentType = contentType;
|
record.ContentType = contentType;
|
||||||
record.Exists = true;
|
record.Exists = true;
|
||||||
@ -334,9 +335,7 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
}
|
}
|
||||||
|
|
||||||
var total = await query.CountAsync(cancellationToken);
|
var total = await query.CountAsync(cancellationToken);
|
||||||
var items = await query
|
var items = await ApplyFileSort(query, request.SortBy, request.SortDirection)
|
||||||
.OrderBy(file => file.MediaType)
|
|
||||||
.ThenBy(file => file.RelativePath)
|
|
||||||
.Skip((page - 1) * pageSize)
|
.Skip((page - 1) * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.Select(file => ToFileDto(file))
|
.Select(file => ToFileDto(file))
|
||||||
@ -359,6 +358,9 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
public async Task<BrowseDirectoryResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default)
|
public async Task<BrowseDirectoryResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var rootId = request.RootId;
|
var rootId = request.RootId;
|
||||||
|
var page = Math.Clamp(request.Page, 1, 100000);
|
||||||
|
var pageSize = Math.Clamp(request.PageSize, 1, 100);
|
||||||
|
var mediaType = request.MediaType?.Trim();
|
||||||
// URL 友好的正斜杠,用于响应和内存处理
|
// URL 友好的正斜杠,用于响应和内存处理
|
||||||
var prefix = (request.Path ?? "").Trim().Replace('\\', '/').Trim('/');
|
var prefix = (request.Path ?? "").Trim().Replace('\\', '/').Trim('/');
|
||||||
// Windows 反斜杠,用于数据库查询
|
// Windows 反斜杠,用于数据库查询
|
||||||
@ -378,7 +380,7 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
var allFiles = await query.ToListAsync(cancellationToken);
|
var allFiles = await query.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var subdirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var subdirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var currentFiles = new List<FileRecordDto>();
|
var currentFiles = new List<ManagedFileRecord>();
|
||||||
|
|
||||||
foreach (var file in allFiles)
|
foreach (var file in allFiles)
|
||||||
{
|
{
|
||||||
@ -390,7 +392,7 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
var slashIndex = remaining.IndexOf('/');
|
var slashIndex = remaining.IndexOf('/');
|
||||||
if (slashIndex < 0)
|
if (slashIndex < 0)
|
||||||
{
|
{
|
||||||
currentFiles.Add(ToFileDto(file));
|
currentFiles.Add(file);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -398,10 +400,28 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(mediaType) && !mediaType.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
currentFiles = currentFiles
|
||||||
|
.Where(file => file.MediaType.Equals(mediaType, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = currentFiles.Count;
|
||||||
|
var files = ApplyFileSort(currentFiles, request.SortBy, request.SortDirection)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(ToFileDto)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return new BrowseDirectoryResponse(
|
return new BrowseDirectoryResponse(
|
||||||
prefix,
|
prefix,
|
||||||
subdirs.OrderBy(d => d, StringComparer.OrdinalIgnoreCase).ToList(),
|
subdirs.OrderBy(d => d, StringComparer.OrdinalIgnoreCase).ToList(),
|
||||||
currentFiles);
|
files,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
pageSize > 0 ? (int)Math.Ceiling((double)total / pageSize) : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -586,6 +606,76 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
return Math.Clamp(interval ?? DefaultScanIntervalMinutes, 1, 1440);
|
return Math.Clamp(interval ?? DefaultScanIntervalMinutes, 1, 1440);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ManagedFileRecord> ApplyFileSort(
|
||||||
|
IQueryable<ManagedFileRecord> query,
|
||||||
|
string? sortBy,
|
||||||
|
string? sortDirection)
|
||||||
|
{
|
||||||
|
var descending = IsDescending(sortDirection);
|
||||||
|
return NormalizeSortBy(sortBy) switch
|
||||||
|
{
|
||||||
|
"size" => descending
|
||||||
|
? query.OrderByDescending(file => file.SizeBytes).ThenBy(file => file.FileName)
|
||||||
|
: query.OrderBy(file => file.SizeBytes).ThenBy(file => file.FileName),
|
||||||
|
"created" => descending
|
||||||
|
? query.OrderByDescending(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||||
|
: query.OrderBy(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||||
|
"modified" => descending
|
||||||
|
? query.OrderByDescending(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||||
|
: query.OrderBy(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||||
|
"type" => descending
|
||||||
|
? query.OrderByDescending(file => file.MediaType).ThenByDescending(file => file.Extension).ThenBy(file => file.FileName)
|
||||||
|
: query.OrderBy(file => file.MediaType).ThenBy(file => file.Extension).ThenBy(file => file.FileName),
|
||||||
|
_ => descending
|
||||||
|
? query.OrderByDescending(file => file.FileName).ThenBy(file => file.RelativePath)
|
||||||
|
: query.OrderBy(file => file.FileName).ThenBy(file => file.RelativePath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IOrderedEnumerable<ManagedFileRecord> ApplyFileSort(
|
||||||
|
IEnumerable<ManagedFileRecord> files,
|
||||||
|
string? sortBy,
|
||||||
|
string? sortDirection)
|
||||||
|
{
|
||||||
|
var descending = IsDescending(sortDirection);
|
||||||
|
return NormalizeSortBy(sortBy) switch
|
||||||
|
{
|
||||||
|
"size" => descending
|
||||||
|
? files.OrderByDescending(file => file.SizeBytes).ThenBy(file => file.FileName)
|
||||||
|
: files.OrderBy(file => file.SizeBytes).ThenBy(file => file.FileName),
|
||||||
|
"created" => descending
|
||||||
|
? files.OrderByDescending(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||||
|
: files.OrderBy(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||||
|
"modified" => descending
|
||||||
|
? files.OrderByDescending(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||||
|
: files.OrderBy(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||||
|
"type" => descending
|
||||||
|
? files.OrderByDescending(file => file.MediaType).ThenByDescending(file => file.Extension).ThenBy(file => file.FileName)
|
||||||
|
: files.OrderBy(file => file.MediaType).ThenBy(file => file.Extension).ThenBy(file => file.FileName),
|
||||||
|
_ => descending
|
||||||
|
? files.OrderByDescending(file => file.FileName).ThenBy(file => file.RelativePath)
|
||||||
|
: files.OrderBy(file => file.FileName).ThenBy(file => file.RelativePath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeSortBy(string? sortBy)
|
||||||
|
{
|
||||||
|
return sortBy?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"size" or "sizebytes" => "size",
|
||||||
|
"created" or "createdat" or "createtime" or "creationtime" => "created",
|
||||||
|
"modified" or "lastwrite" or "lastwritetime" or "lastwritetimeutc" or "time" or "date" => "modified",
|
||||||
|
"type" or "mediatype" or "extension" or "ext" => "type",
|
||||||
|
_ => "name",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDescending(string? sortDirection)
|
||||||
|
{
|
||||||
|
return sortDirection?.Trim().Equals("desc", StringComparison.OrdinalIgnoreCase) == true
|
||||||
|
|| sortDirection?.Trim().Equals("descending", StringComparison.OrdinalIgnoreCase) == true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 安全获取驱动器属性值,驱动器未就绪或访问异常时返回 null。
|
/// 安全获取驱动器属性值,驱动器未就绪或访问异常时返回 null。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -641,6 +731,7 @@ namespace FileShare_Services.Services.FileLibrary
|
|||||||
file.Extension,
|
file.Extension,
|
||||||
file.SizeBytes,
|
file.SizeBytes,
|
||||||
file.LastWriteTimeUtc,
|
file.LastWriteTimeUtc,
|
||||||
|
file.FileCreationTimeUtc,
|
||||||
file.MediaType,
|
file.MediaType,
|
||||||
file.ContentType,
|
file.ContentType,
|
||||||
$"/api/files/{file.Id}/stream",
|
$"/api/files/{file.Id}/stream",
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export interface FileRecordDto {
|
|||||||
extension: string
|
extension: string
|
||||||
sizeBytes: number
|
sizeBytes: number
|
||||||
lastWriteTimeUtc: string
|
lastWriteTimeUtc: string
|
||||||
|
fileCreationTimeUtc: string | null
|
||||||
mediaType: 'text' | 'video' | 'audio'
|
mediaType: 'text' | 'video' | 'audio'
|
||||||
contentType: string
|
contentType: string
|
||||||
streamUrl: string
|
streamUrl: string
|
||||||
@ -53,6 +54,10 @@ export interface BrowseDirectoryResponse {
|
|||||||
currentPath: string
|
currentPath: string
|
||||||
subdirectories: string[]
|
subdirectories: string[]
|
||||||
files: FileRecordDto[]
|
files: FileRecordDto[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalPages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextPreviewDto {
|
export interface TextPreviewDto {
|
||||||
@ -95,10 +100,10 @@ export const api = {
|
|||||||
request('library/roots/delete', { method: 'POST', body: { id } }),
|
request('library/roots/delete', { method: 'POST', body: { id } }),
|
||||||
scanRoot: (id: number) =>
|
scanRoot: (id: number) =>
|
||||||
request<LibraryRootDto>('library/roots/scan', { method: 'POST', body: { id } }),
|
request<LibraryRootDto>('library/roots/scan', { method: 'POST', body: { id } }),
|
||||||
searchFiles: (params: { page: number; pageSize: number; mediaType?: MediaType; keyword?: string; rootId?: number }) =>
|
searchFiles: (params: { page: number; pageSize: number; mediaType?: MediaType; keyword?: string; rootId?: number; sortBy?: string; sortDirection?: string }) =>
|
||||||
request<PagedResponse<FileRecordDto>>(`files${qs(params)}`),
|
request<PagedResponse<FileRecordDto>>(`files${qs(params)}`),
|
||||||
browseDirectory: (rootId: number, path: string) =>
|
browseDirectory: (params: { rootId: number; path: string; page?: number; pageSize?: number; mediaType?: MediaType; sortBy?: string; sortDirection?: string }) =>
|
||||||
request<BrowseDirectoryResponse>(`files/browse${qs({ rootId, path })}`),
|
request<BrowseDirectoryResponse>(`files/browse${qs(params)}`),
|
||||||
getTextPreview: (id: number) =>
|
getTextPreview: (id: number) =>
|
||||||
request<TextPreviewDto>(`files/text${qs({ id })}`),
|
request<TextPreviewDto>(`files/text${qs({ id })}`),
|
||||||
mediaUrl: (path: string) => apiUrl(path),
|
mediaUrl: (path: string) => apiUrl(path),
|
||||||
|
|||||||
@ -768,6 +768,8 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,6 +780,35 @@ a {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-control select {
|
||||||
|
width: auto;
|
||||||
|
min-width: 118px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 4px 28px 4px 9px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-control.compact select {
|
||||||
|
min-width: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
/* File grid / card view */
|
/* File grid / card view */
|
||||||
.file-grid {
|
.file-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -1132,6 +1163,10 @@ a {
|
|||||||
.view-toggle {
|
.view-toggle {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-toolbar-right {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
@media (min-width: 900px) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { api, type BrowseDirectoryResponse, type FileRecordDto, type LibraryRootDto, type TextPreviewDto } from '../api'
|
import { api, type BrowseDirectoryResponse, type FileRecordDto, type LibraryRootDto, type TextPreviewDto } from '../api'
|
||||||
import QrCodeModal from './QrCodeModal.vue'
|
import QrCodeModal from './QrCodeModal.vue'
|
||||||
|
|
||||||
@ -31,6 +31,10 @@ const isSearching = ref(false)
|
|||||||
|
|
||||||
// File filter
|
// File filter
|
||||||
const filterType = ref<'all' | 'video' | 'audio' | 'text'>('all')
|
const filterType = ref<'all' | 'video' | 'audio' | 'text'>('all')
|
||||||
|
const sortBy = ref<'name' | 'size' | 'created' | 'type'>('name')
|
||||||
|
const sortDirection = ref<'asc' | 'desc'>('asc')
|
||||||
|
const browsePage = ref(1)
|
||||||
|
const browsePageSize = ref(48)
|
||||||
|
|
||||||
// Video resume
|
// Video resume
|
||||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||||
@ -55,11 +59,7 @@ const breadcrumbs = computed(() => {
|
|||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredFiles = computed(() => {
|
const displayedFiles = computed(() => browseData.value?.files ?? [])
|
||||||
if (!browseData.value) return []
|
|
||||||
if (filterType.value === 'all') return browseData.value.files
|
|
||||||
return browseData.value.files.filter((f) => f.mediaType === filterType.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedMediaUrl = computed(() => selectedFile.value ? api.mediaUrl(selectedFile.value.streamUrl) : '')
|
const selectedMediaUrl = computed(() => selectedFile.value ? api.mediaUrl(selectedFile.value.streamUrl) : '')
|
||||||
const selectedThumbnailUrl = computed(() =>
|
const selectedThumbnailUrl = computed(() =>
|
||||||
@ -99,6 +99,10 @@ function formatDate(value: string | null) {
|
|||||||
return new Date(value).toLocaleString()
|
return new Date(value).toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCreatedTime(file: FileRecordDto) {
|
||||||
|
return file.fileCreationTimeUtc ? `创建 ${formatDate(file.fileCreationTimeUtc)}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
function setError(error: unknown) {
|
function setError(error: unknown) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '操作失败'
|
errorMessage.value = error instanceof Error ? error.message : '操作失败'
|
||||||
}
|
}
|
||||||
@ -112,7 +116,15 @@ async function browseDirectory() {
|
|||||||
try {
|
try {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
loading.value = true
|
loading.value = true
|
||||||
browseData.value = await api.browseDirectory(rootId.value, currentBrowsePath.value)
|
browseData.value = await api.browseDirectory({
|
||||||
|
rootId: rootId.value,
|
||||||
|
path: currentBrowsePath.value,
|
||||||
|
page: browsePage.value,
|
||||||
|
pageSize: browsePageSize.value,
|
||||||
|
mediaType: filterType.value,
|
||||||
|
sortBy: sortBy.value,
|
||||||
|
sortDirection: sortDirection.value,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error)
|
setError(error)
|
||||||
} finally {
|
} finally {
|
||||||
@ -133,6 +145,7 @@ async function loadRecentFiles(type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchTab(tab: 'recent-added' | 'recent-played' | 'libraries') {
|
function switchTab(tab: 'recent-added' | 'recent-played' | 'libraries') {
|
||||||
|
flushVideoProgress()
|
||||||
activeTab.value = tab
|
activeTab.value = tab
|
||||||
isBrowsingRoots.value = true
|
isBrowsingRoots.value = true
|
||||||
browseData.value = null
|
browseData.value = null
|
||||||
@ -143,9 +156,11 @@ function switchTab(tab: 'recent-added' | 'recent-played' | 'libraries') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function enterRoot(id: number) {
|
async function enterRoot(id: number) {
|
||||||
|
flushVideoProgress()
|
||||||
rootId.value = id
|
rootId.value = id
|
||||||
isBrowsingRoots.value = false
|
isBrowsingRoots.value = false
|
||||||
browsePath.value = []
|
browsePath.value = []
|
||||||
|
browsePage.value = 1
|
||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
textPreview.value = null
|
textPreview.value = null
|
||||||
activeTab.value = 'libraries'
|
activeTab.value = 'libraries'
|
||||||
@ -153,16 +168,19 @@ async function enterRoot(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backToRoots() {
|
function backToRoots() {
|
||||||
|
flushVideoProgress()
|
||||||
isBrowsingRoots.value = true
|
isBrowsingRoots.value = true
|
||||||
rootId.value = undefined
|
rootId.value = undefined
|
||||||
browseData.value = null
|
browseData.value = null
|
||||||
browsePath.value = []
|
browsePath.value = []
|
||||||
|
browsePage.value = 1
|
||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
textPreview.value = null
|
textPreview.value = null
|
||||||
activeTab.value = 'libraries'
|
activeTab.value = 'libraries'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateTo(path: string) {
|
async function navigateTo(path: string) {
|
||||||
|
flushVideoProgress()
|
||||||
if (path === '') {
|
if (path === '') {
|
||||||
browsePath.value = []
|
browsePath.value = []
|
||||||
} else {
|
} else {
|
||||||
@ -170,18 +188,45 @@ async function navigateTo(path: string) {
|
|||||||
}
|
}
|
||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
textPreview.value = null
|
textPreview.value = null
|
||||||
|
browsePage.value = 1
|
||||||
await browseDirectory()
|
await browseDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enterSubdirectory(name: string) {
|
async function enterSubdirectory(name: string) {
|
||||||
|
flushVideoProgress()
|
||||||
browsePath.value.push(name)
|
browsePath.value.push(name)
|
||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
textPreview.value = null
|
textPreview.value = null
|
||||||
|
browsePage.value = 1
|
||||||
|
await browseDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeFilter(type: 'all' | 'video' | 'audio' | 'text') {
|
||||||
|
flushVideoProgress()
|
||||||
|
filterType.value = type
|
||||||
|
browsePage.value = 1
|
||||||
|
selectedFile.value = null
|
||||||
|
textPreview.value = null
|
||||||
await browseDirectory()
|
await browseDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeFilter(type: 'all' | 'video' | 'audio' | 'text') {
|
async function changeSort() {
|
||||||
filterType.value = type
|
flushVideoProgress()
|
||||||
|
browsePage.value = 1
|
||||||
|
selectedFile.value = null
|
||||||
|
textPreview.value = null
|
||||||
|
await browseDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeBrowsePage(page: number) {
|
||||||
|
if (!browseData.value) return
|
||||||
|
const nextPage = Math.min(Math.max(page, 1), Math.max(browseData.value.totalPages, 1))
|
||||||
|
if (nextPage === browsePage.value) return
|
||||||
|
flushVideoProgress()
|
||||||
|
browsePage.value = nextPage
|
||||||
|
selectedFile.value = null
|
||||||
|
textPreview.value = null
|
||||||
|
await browseDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doSearch() {
|
async function doSearch() {
|
||||||
@ -201,6 +246,7 @@ async function doSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exitSearch() {
|
function exitSearch() {
|
||||||
|
flushVideoProgress()
|
||||||
isSearching.value = false
|
isSearching.value = false
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
searchResults.value = []
|
searchResults.value = []
|
||||||
@ -224,8 +270,11 @@ function saveVideoProgress(position?: number) {
|
|||||||
const video = videoRef.value
|
const video = videoRef.value
|
||||||
if (!video) return
|
if (!video) return
|
||||||
|
|
||||||
const nextPosition = position ?? Math.floor(video.currentTime)
|
const rawPosition = position ?? video.currentTime
|
||||||
if (nextPosition < 0) return
|
if (!Number.isFinite(rawPosition)) return
|
||||||
|
|
||||||
|
const nextPosition = Math.floor(rawPosition)
|
||||||
|
if (!Number.isFinite(nextPosition) || nextPosition < 0) return
|
||||||
|
|
||||||
const fileId = selectedFile.value.id
|
const fileId = selectedFile.value.id
|
||||||
api.saveFileProgress(fileId, nextPosition)
|
api.saveFileProgress(fileId, nextPosition)
|
||||||
@ -233,9 +282,15 @@ function saveVideoProgress(position?: number) {
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flushVideoProgress() {
|
||||||
|
const video = videoRef.value
|
||||||
|
if (!video || video.currentTime === 0 || !Number.isFinite(video.currentTime) || video.ended) return
|
||||||
|
saveVideoProgress()
|
||||||
|
}
|
||||||
|
|
||||||
function handleVideoTimeUpdate() {
|
function handleVideoTimeUpdate() {
|
||||||
const video = videoRef.value
|
const video = videoRef.value
|
||||||
if (!video || video.paused || video.currentTime === 0) return
|
if (!video || video.paused || video.currentTime === 0 || !Number.isFinite(video.currentTime)) return
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (now - lastPositionSave < 5000) return
|
if (now - lastPositionSave < 5000) return
|
||||||
@ -244,12 +299,13 @@ function handleVideoTimeUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleVideoPause() {
|
function handleVideoPause() {
|
||||||
const video = videoRef.value
|
flushVideoProgress()
|
||||||
if (!video || video.currentTime === 0 || video.ended) return
|
|
||||||
saveVideoProgress()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVideoPlay() {
|
function handleVideoPlay() {
|
||||||
|
if (showResumePrompt.value && resumePosition.value > 0) {
|
||||||
|
resumeRequested.value = true
|
||||||
|
}
|
||||||
seekToResumePosition()
|
seekToResumePosition()
|
||||||
showResumePrompt.value = false
|
showResumePrompt.value = false
|
||||||
}
|
}
|
||||||
@ -267,7 +323,7 @@ function resetResume() {
|
|||||||
function tryResume(file: FileRecordDto) {
|
function tryResume(file: FileRecordDto) {
|
||||||
if (file.mediaType !== 'video' || !file.playbackPosition || file.playbackPosition <= 0) return
|
if (file.mediaType !== 'video' || !file.playbackPosition || file.playbackPosition <= 0) return
|
||||||
resumePosition.value = file.playbackPosition
|
resumePosition.value = file.playbackPosition
|
||||||
resumeRequested.value = true
|
resumeRequested.value = false
|
||||||
showResumePrompt.value = true
|
showResumePrompt.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +349,7 @@ function dismissResume() {
|
|||||||
if (videoRef.value) {
|
if (videoRef.value) {
|
||||||
videoRef.value.currentTime = 0
|
videoRef.value.currentTime = 0
|
||||||
}
|
}
|
||||||
|
saveVideoProgress(0)
|
||||||
resetResume()
|
resetResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +359,7 @@ async function selectSearchFile(file: FileRecordDto) {
|
|||||||
|
|
||||||
rootId.value = file.libraryRootId
|
rootId.value = file.libraryRootId
|
||||||
browsePath.value = relativeParts
|
browsePath.value = relativeParts
|
||||||
|
browsePage.value = 1
|
||||||
isBrowsingRoots.value = false
|
isBrowsingRoots.value = false
|
||||||
activeTab.value = 'libraries'
|
activeTab.value = 'libraries'
|
||||||
exitSearch()
|
exitSearch()
|
||||||
@ -311,6 +369,7 @@ async function selectSearchFile(file: FileRecordDto) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function selectFile(file: FileRecordDto) {
|
async function selectFile(file: FileRecordDto) {
|
||||||
|
flushVideoProgress()
|
||||||
resetResume()
|
resetResume()
|
||||||
lastPositionSave = 0
|
lastPositionSave = 0
|
||||||
selectedFile.value = file
|
selectedFile.value = file
|
||||||
@ -333,6 +392,7 @@ async function selectFile(file: FileRecordDto) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
window.addEventListener('beforeunload', flushVideoProgress)
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await loadRoots()
|
await loadRoots()
|
||||||
@ -342,6 +402,11 @@ onMounted(async () => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('beforeunload', flushVideoProgress)
|
||||||
|
flushVideoProgress()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -354,7 +419,7 @@ onMounted(async () => {
|
|||||||
<template v-else-if="activeTab === 'recent-played'">{{ recentFiles.length }} 个文件</template>
|
<template v-else-if="activeTab === 'recent-played'">{{ recentFiles.length }} 个文件</template>
|
||||||
<template v-else-if="isBrowsingRoots">{{ activeRoots.length }} 个目录</template>
|
<template v-else-if="isBrowsingRoots">{{ activeRoots.length }} 个目录</template>
|
||||||
<template v-else-if="browseData">
|
<template v-else-if="browseData">
|
||||||
{{ browseData.subdirectories.length }} 个文件夹 · {{ browseData.files.length }} 个文件
|
{{ browseData.subdirectories.length }} 个文件夹 · {{ browseData.total }} 个文件
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -377,6 +442,84 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<p v-if="errorMessage" class="error-banner">{{ errorMessage }}</p>
|
<p v-if="errorMessage" class="error-banner">{{ errorMessage }}</p>
|
||||||
|
|
||||||
|
<!-- Media player -->
|
||||||
|
<section v-if="selectedFile" class="player-panel">
|
||||||
|
<div class="player-title">
|
||||||
|
<div>
|
||||||
|
<h2>{{ selectedFile.fileName }}</h2>
|
||||||
|
<p>{{ selectedRoot?.displayName ?? '文件库' }} · {{ selectedFile.relativePath }}</p>
|
||||||
|
</div>
|
||||||
|
<span>{{ selectedFile.extension }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedFile.mediaType === 'video'" class="video-info-bar">
|
||||||
|
<span v-if="selectedFile.videoDuration">时长 {{ formatDuration(selectedFile.videoDuration) }}</span>
|
||||||
|
<span>{{ formatSize(selectedFile.sizeBytes) }}</span>
|
||||||
|
<span>{{ selectedFile.contentType }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showResumePrompt" class="resume-banner">
|
||||||
|
<span>从 {{ formatDuration(resumePosition) }} 继续播放?</span>
|
||||||
|
<div class="resume-banner-actions">
|
||||||
|
<button type="button" class="resume-yes" @click="resumePlayback">继续</button>
|
||||||
|
<button type="button" class="resume-no" @click="dismissResume">从头播放</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-media-wrapper">
|
||||||
|
<video
|
||||||
|
v-if="selectedFile.mediaType === 'video' && selectedFile.browserPlayable"
|
||||||
|
ref="videoRef"
|
||||||
|
:key="selectedFile.id"
|
||||||
|
:poster="selectedThumbnailUrl || undefined"
|
||||||
|
controls
|
||||||
|
playsinline
|
||||||
|
webkit-playsinline
|
||||||
|
preload="metadata"
|
||||||
|
@loadedmetadata="seekToResumePosition"
|
||||||
|
@canplay="seekToResumePosition"
|
||||||
|
@play="handleVideoPlay"
|
||||||
|
@timeupdate="handleVideoTimeUpdate"
|
||||||
|
@pause="handleVideoPause"
|
||||||
|
@ended="handleVideoEnded"
|
||||||
|
@seeked="handleVideoPause"
|
||||||
|
>
|
||||||
|
<source :src="selectedMediaUrl" :type="selectedFile.contentType" />
|
||||||
|
</video>
|
||||||
|
<div
|
||||||
|
v-else-if="selectedFile.mediaType === 'video' && !selectedFile.browserPlayable"
|
||||||
|
class="unsupported-player"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="selectedThumbnailUrl"
|
||||||
|
:src="selectedThumbnailUrl"
|
||||||
|
class="unsupported-thumb"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<p class="unsupported">浏览器不支持在线播放此格式。</p>
|
||||||
|
</div>
|
||||||
|
<audio
|
||||||
|
v-else-if="selectedFile.mediaType === 'audio' && selectedFile.browserPlayable"
|
||||||
|
:key="selectedFile.id"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
>
|
||||||
|
<source :src="selectedMediaUrl" :type="selectedFile.contentType" />
|
||||||
|
</audio>
|
||||||
|
<pre v-else-if="selectedFile.mediaType === 'text'">{{ textPreview?.content ?? '加载中...' }}</pre>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="selectedFile.mediaType !== 'text'"
|
||||||
|
class="open-media-link"
|
||||||
|
:href="selectedMediaUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
新窗口打开原视频/音频
|
||||||
|
</a>
|
||||||
|
<p v-if="textPreview?.truncated" class="hint">文本超过 1 MB,已截断显示。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Root tabs -->
|
<!-- Root tabs -->
|
||||||
<nav v-if="isBrowsingRoots && !isSearching" class="root-tabs">
|
<nav v-if="isBrowsingRoots && !isSearching" class="root-tabs">
|
||||||
<button
|
<button
|
||||||
@ -420,6 +563,7 @@ onMounted(async () => {
|
|||||||
{{ formatSize(file.sizeBytes) }}
|
{{ formatSize(file.sizeBytes) }}
|
||||||
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
|
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
|
||||||
</small>
|
</small>
|
||||||
|
<small v-if="formatCreatedTime(file)">{{ formatCreatedTime(file) }}</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -431,6 +575,7 @@ onMounted(async () => {
|
|||||||
<span>
|
<span>
|
||||||
<strong>{{ file.fileName }}</strong>
|
<strong>{{ file.fileName }}</strong>
|
||||||
<small>{{ formatSize(file.sizeBytes) }}</small>
|
<small>{{ formatSize(file.sizeBytes) }}</small>
|
||||||
|
<small v-if="formatCreatedTime(file)">{{ formatCreatedTime(file) }}</small>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -559,85 +704,8 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Media player -->
|
|
||||||
<section v-if="selectedFile" class="player-panel">
|
|
||||||
<div class="player-title">
|
|
||||||
<div>
|
|
||||||
<h2>{{ selectedFile.fileName }}</h2>
|
|
||||||
<p>{{ selectedRoot?.displayName ?? '文件库' }} · {{ selectedFile.relativePath }}</p>
|
|
||||||
</div>
|
|
||||||
<span>{{ selectedFile.extension }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedFile.mediaType === 'video'" class="video-info-bar">
|
|
||||||
<span v-if="selectedFile.videoDuration">时长 {{ formatDuration(selectedFile.videoDuration) }}</span>
|
|
||||||
<span>{{ formatSize(selectedFile.sizeBytes) }}</span>
|
|
||||||
<span>{{ selectedFile.contentType }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showResumePrompt" class="resume-banner">
|
|
||||||
<span>从 {{ formatDuration(resumePosition) }} 继续播放?</span>
|
|
||||||
<div class="resume-banner-actions">
|
|
||||||
<button type="button" class="resume-yes" @click="resumePlayback">继续</button>
|
|
||||||
<button type="button" class="resume-no" @click="dismissResume">从头播放</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="player-media-wrapper">
|
|
||||||
<video
|
|
||||||
v-if="selectedFile.mediaType === 'video' && selectedFile.browserPlayable"
|
|
||||||
ref="videoRef"
|
|
||||||
:key="selectedFile.id"
|
|
||||||
:poster="selectedThumbnailUrl || undefined"
|
|
||||||
controls
|
|
||||||
playsinline
|
|
||||||
webkit-playsinline
|
|
||||||
preload="metadata"
|
|
||||||
@loadedmetadata="seekToResumePosition"
|
|
||||||
@canplay="seekToResumePosition"
|
|
||||||
@play="handleVideoPlay"
|
|
||||||
@timeupdate="handleVideoTimeUpdate"
|
|
||||||
@pause="handleVideoPause"
|
|
||||||
@ended="handleVideoEnded"
|
|
||||||
>
|
|
||||||
<source :src="selectedMediaUrl" :type="selectedFile.contentType" />
|
|
||||||
</video>
|
|
||||||
<div
|
|
||||||
v-else-if="selectedFile.mediaType === 'video' && !selectedFile.browserPlayable"
|
|
||||||
class="unsupported-player"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="selectedThumbnailUrl"
|
|
||||||
:src="selectedThumbnailUrl"
|
|
||||||
class="unsupported-thumb"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<p class="unsupported">浏览器不支持在线播放此格式。</p>
|
|
||||||
</div>
|
|
||||||
<audio
|
|
||||||
v-else-if="selectedFile.mediaType === 'audio' && selectedFile.browserPlayable"
|
|
||||||
:key="selectedFile.id"
|
|
||||||
controls
|
|
||||||
preload="metadata"
|
|
||||||
>
|
|
||||||
<source :src="selectedMediaUrl" :type="selectedFile.contentType" />
|
|
||||||
</audio>
|
|
||||||
<pre v-else-if="selectedFile.mediaType === 'text'">{{ textPreview?.content ?? '加载中...' }}</pre>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
v-if="selectedFile.mediaType !== 'text'"
|
|
||||||
class="open-media-link"
|
|
||||||
:href="selectedMediaUrl"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
新窗口打开原视频/音频
|
|
||||||
</a>
|
|
||||||
<p v-if="textPreview?.truncated" class="hint">文本超过 1 MB,已截断显示。</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Files -->
|
<!-- Files -->
|
||||||
<section v-if="browseData.files.length > 0" class="browse-section">
|
<section class="browse-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="filter-chips">
|
<div class="filter-chips">
|
||||||
<button type="button" :class="{ active: filterType === 'all' }" @click="changeFilter('all')">全部</button>
|
<button type="button" :class="{ active: filterType === 'all' }" @click="changeFilter('all')">全部</button>
|
||||||
@ -645,70 +713,97 @@ onMounted(async () => {
|
|||||||
<button type="button" :class="{ active: filterType === 'audio' }" @click="changeFilter('audio')">音频</button>
|
<button type="button" :class="{ active: filterType === 'audio' }" @click="changeFilter('audio')">音频</button>
|
||||||
<button type="button" :class="{ active: filterType === 'text' }" @click="changeFilter('text')">文本</button>
|
<button type="button" :class="{ active: filterType === 'text' }" @click="changeFilter('text')">文本</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-toggle">
|
<div class="file-toolbar-right">
|
||||||
<button type="button" :class="{ active: viewMode === 'list' }" @click="viewMode = 'list'">列表</button>
|
<label class="sort-control">
|
||||||
<button type="button" :class="{ active: viewMode === 'grid' }" @click="viewMode = 'grid'">网格</button>
|
<span>排序</span>
|
||||||
|
<select v-model="sortBy" @change="changeSort">
|
||||||
|
<option value="name">名称</option>
|
||||||
|
<option value="size">大小</option>
|
||||||
|
<option value="created">创建时间</option>
|
||||||
|
<option value="type">文件类型</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="sort-control compact">
|
||||||
|
<span>方向</span>
|
||||||
|
<select v-model="sortDirection" @change="changeSort">
|
||||||
|
<option value="asc">升序</option>
|
||||||
|
<option value="desc">降序</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="view-toggle">
|
||||||
|
<button type="button" :class="{ active: viewMode === 'list' }" @click="viewMode = 'list'">列表</button>
|
||||||
|
<button type="button" :class="{ active: viewMode === 'grid' }" @click="viewMode = 'grid'">网格</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid view -->
|
<!-- Grid view -->
|
||||||
<div v-if="viewMode === 'grid'" class="file-grid">
|
<div v-if="displayedFiles.length > 0 && viewMode === 'grid'" class="file-grid">
|
||||||
<button
|
<template v-for="file in displayedFiles" :key="file.id">
|
||||||
v-for="file in filteredFiles"
|
<button
|
||||||
:key="file.id"
|
class="file-card"
|
||||||
class="file-card"
|
:class="{ active: selectedFile?.id === file.id }"
|
||||||
:class="{ active: selectedFile?.id === file.id }"
|
type="button"
|
||||||
type="button"
|
@click="selectFile(file)"
|
||||||
@click="selectFile(file)"
|
>
|
||||||
>
|
<img
|
||||||
<img
|
v-if="file.thumbnailUrl"
|
||||||
v-if="file.thumbnailUrl"
|
:src="api.thumbnailUrl(file.thumbnailUrl)"
|
||||||
:src="api.thumbnailUrl(file.thumbnailUrl)"
|
class="card-thumb"
|
||||||
class="card-thumb"
|
alt=""
|
||||||
alt=""
|
loading="lazy"
|
||||||
loading="lazy"
|
/>
|
||||||
/>
|
<div v-else class="card-thumb-placeholder">{{ file.mediaType }}</div>
|
||||||
<div v-else class="card-thumb-placeholder">{{ file.mediaType }}</div>
|
<div class="card-body">
|
||||||
<div class="card-body">
|
<strong>{{ file.fileName }}</strong>
|
||||||
<strong>{{ file.fileName }}</strong>
|
<small>
|
||||||
<small>
|
{{ formatSize(file.sizeBytes) }}
|
||||||
{{ formatSize(file.sizeBytes) }}
|
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
|
||||||
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
|
</small>
|
||||||
</small>
|
<small v-if="formatCreatedTime(file)">{{ formatCreatedTime(file) }}</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- List view -->
|
<!-- List view -->
|
||||||
<div v-else class="file-list">
|
<div v-else-if="displayedFiles.length > 0" class="file-list">
|
||||||
<button
|
<template v-for="file in displayedFiles" :key="file.id">
|
||||||
v-for="file in filteredFiles"
|
<button
|
||||||
:key="file.id"
|
class="mobile-file"
|
||||||
class="mobile-file"
|
:class="{ active: selectedFile?.id === file.id }"
|
||||||
:class="{ active: selectedFile?.id === file.id }"
|
type="button"
|
||||||
type="button"
|
@click="selectFile(file)"
|
||||||
@click="selectFile(file)"
|
>
|
||||||
>
|
<img
|
||||||
<img
|
v-if="file.thumbnailUrl"
|
||||||
v-if="file.thumbnailUrl"
|
:src="api.thumbnailUrl(file.thumbnailUrl)"
|
||||||
:src="api.thumbnailUrl(file.thumbnailUrl)"
|
class="file-thumb"
|
||||||
class="file-thumb"
|
alt=""
|
||||||
alt=""
|
loading="lazy"
|
||||||
loading="lazy"
|
/>
|
||||||
/>
|
<span v-else class="type-badge">{{ file.mediaType }}</span>
|
||||||
<span v-else class="type-badge">{{ file.mediaType }}</span>
|
<span>
|
||||||
<span>
|
<strong>{{ file.fileName }}</strong>
|
||||||
<strong>{{ file.fileName }}</strong>
|
<small>
|
||||||
<small>
|
{{ formatSize(file.sizeBytes) }}
|
||||||
{{ formatSize(file.sizeBytes) }}
|
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
|
||||||
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
|
</small>
|
||||||
</small>
|
<small v-if="formatCreatedTime(file)">{{ formatCreatedTime(file) }}</small>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="browseData.totalPages > 1" class="mobile-pager">
|
||||||
|
<button type="button" :disabled="browsePage <= 1" @click="changeBrowsePage(browsePage - 1)">上一页</button>
|
||||||
|
<span>第 {{ browseData.page }} / {{ browseData.totalPages }} 页 · 共 {{ browseData.total }} 个</span>
|
||||||
|
<button type="button" :disabled="browsePage >= browseData.totalPages" @click="changeBrowsePage(browsePage + 1)">下一页</button>
|
||||||
|
</div>
|
||||||
|
<p v-else-if="displayedFiles.length === 0 && filterType !== 'all'" class="empty-state">当前分类没有文件</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p v-if="browseData.subdirectories.length === 0 && browseData.files.length === 0" class="empty-state">
|
<p v-if="browseData.subdirectories.length === 0 && browseData.total === 0 && filterType === 'all'" class="empty-state">
|
||||||
此目录下没有支持的文件
|
此目录下没有支持的文件
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user