فهرست منبع

添加自定义标题颜色功能,修复已知BUG

宝臣 王 3 ماه پیش
والد
کامیت
a714891eca

+ 1 - 0
DutyApp/App.axaml

@@ -14,6 +14,7 @@
         <FluentTheme />
         <controls:ControlThemes />
         <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
+        <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
     </Application.Styles>
 
     <Application.Resources>

+ 2 - 0
DutyApp/App.axaml.cs

@@ -78,6 +78,7 @@ namespace DutyApp
             services.AddTransient<InformationViewModel>();
             services.AddTransient<JobTitleViewModel>();
             services.AddTransient<HomePreviewViewModel>();
+            services.AddTransient<DutyStatusViewModel>();
         }
 
         public static void ConfigureViews(IServiceCollection services)
@@ -91,6 +92,7 @@ namespace DutyApp
             services.AddTransient<JobTitlePage>();
             services.AddTransient<DutyPreviewWindow>();
             services.AddTransient<HomePreviewView>();
+            services.AddTransient<DutyStatusView>();
         }
     }
 }

+ 11 - 0
DutyApp/Data/DutyContext.cs

@@ -5,7 +5,9 @@ using DutyApp.Models;
 using Microsoft.EntityFrameworkCore;
 
 using System.Reflection;
+using Avalonia.Media;
 using Microsoft.Extensions.Logging;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 
 namespace DutyApp.Data
 {
@@ -25,6 +27,8 @@ namespace DutyApp.Data
 
         public DbSet<Information> Information { get; set; }
 
+        public DbSet<DutyStatus> DutyStatus { get; set; }
+
         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
         {
             base.OnConfiguring(optionsBuilder);
@@ -51,8 +55,15 @@ namespace DutyApp.Data
                 .HasMany(x => x.DailyDutyRecords)
                 .WithOne(x => x.Picture)
                 .IsRequired(false);
+
+            modelBuilder.Entity<DutyStatus>()
+                .Property(e => e.Color)
+                .HasConversion(new ColorToStringConverter());
         }
 
+        public class ColorToStringConverter() : ValueConverter<Color, string>(v => v.ToString(),
+            v => Color.Parse(v));
+
         // 数据库迁移指令
         // Add-Migration InitialCreate
     }

+ 5 - 1
DutyApp/DutyApp.csproj

@@ -44,9 +44,10 @@
 	</ItemGroup>
 
 	<ItemGroup>
+		<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.1.3" />
 		<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.3" />
 		<PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
-		<PackageReference Include="Avalonia.Labs.Controls" Version="11.1.0" />
+		<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.10.1" />
 		<PackageReference Include="Avalonia.Svg.Skia" Version="11.1.0" />
 		<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
 		<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
@@ -72,6 +73,9 @@
 		<Compile Update="Views\DailyNewsPage.axaml.cs">
 			<DependentUpon>DailyNewsPage.axaml</DependentUpon>
 		</Compile>
+		<Compile Update="Views\DutyStatusView.axaml.cs">
+		  <DependentUpon>DutyStatusView.axaml</DependentUpon>
+		</Compile>
 	</ItemGroup>
 
 

+ 309 - 0
DutyApp/Migrations/20240817023349_202408171033.Designer.cs

@@ -0,0 +1,309 @@
+// <auto-generated />
+using System;
+using DutyApp.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DutyApp.Migrations
+{
+    [DbContext(typeof(DutyContext))]
+    [Migration("20240817023349_202408171033")]
+    partial class _202408171033
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "7.0.20");
+
+            modelBuilder.Entity("DutyApp.Models.ByteImage", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("DutyOfficerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("ImageBytes")
+                        .HasColumnType("BLOB");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DutyOfficerId")
+                        .IsUnique();
+
+                    b.ToTable("ByteImages");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DailyDutyRecord", b =>
+                {
+                    b.Property<Guid>("RecordId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateOnly>("DutyDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DutyOfficer")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("DutyOfficerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("PictureId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Status")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("RecordId");
+
+                    b.HasIndex("PictureId");
+
+                    b.ToTable("DailyDutyRecords");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DailyNews", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DesFontFamily")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("DesFontSize")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasMaxLength(500)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("InformationId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("TitleFontFamily")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("TitleFontSize")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("InformationId");
+
+                    b.ToTable("DailyNews");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DutyImage", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("ImageBytes")
+                        .HasColumnType("BLOB");
+
+                    b.Property<string>("ImageName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Md5")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("DutyImages");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DutyOfficer", b =>
+                {
+                    b.Property<Guid>("DutyOfficerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("DutyOfficerId");
+
+                    b.ToTable("DutyOfficer");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DutyStatus", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Color")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("DutyStatus");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.Information", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateOnly>("Date")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DayOfWeek")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Holiday")
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Temperature")
+                        .IsRequired()
+                        .HasMaxLength(5)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UvIndex")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Weather")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Wind")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Information");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.JobTitle", b =>
+                {
+                    b.Property<Guid>("TitleId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("TitleId");
+
+                    b.ToTable("JobTitles");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.ByteImage", b =>
+                {
+                    b.HasOne("DutyApp.Models.DutyOfficer", "DutyOfficer")
+                        .WithOne("Picture")
+                        .HasForeignKey("DutyApp.Models.ByteImage", "DutyOfficerId");
+
+                    b.Navigation("DutyOfficer");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DailyDutyRecord", b =>
+                {
+                    b.HasOne("DutyApp.Models.ByteImage", "Picture")
+                        .WithMany("DailyDutyRecords")
+                        .HasForeignKey("PictureId");
+
+                    b.Navigation("Picture");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DailyNews", b =>
+                {
+                    b.HasOne("DutyApp.Models.Information", null)
+                        .WithMany("DailyNews")
+                        .HasForeignKey("InformationId");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.ByteImage", b =>
+                {
+                    b.Navigation("DailyDutyRecords");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.DutyOfficer", b =>
+                {
+                    b.Navigation("Picture");
+                });
+
+            modelBuilder.Entity("DutyApp.Models.Information", b =>
+                {
+                    b.Navigation("DailyNews");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 36 - 0
DutyApp/Migrations/20240817023349_202408171033.cs

@@ -0,0 +1,36 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DutyApp.Migrations
+{
+    /// <inheritdoc />
+    public partial class _202408171033 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "DutyStatus",
+                columns: table => new
+                {
+                    Id = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Name = table.Column<string>(type: "TEXT", maxLength: 10, nullable: false),
+                    Color = table.Column<string>(type: "TEXT", nullable: false),
+                    Index = table.Column<int>(type: "INTEGER", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_DutyStatus", x => x.Id);
+                });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "DutyStatus");
+        }
+    }
+}

+ 23 - 0
DutyApp/Migrations/DutyContextModelSnapshot.cs

@@ -173,6 +173,29 @@ namespace DutyApp.Migrations
                     b.ToTable("DutyOfficer", (string)null);
                 });
 
+            modelBuilder.Entity("DutyApp.Models.DutyStatus", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Color")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("DutyStatus", (string)null);
+                });
+
             modelBuilder.Entity("DutyApp.Models.Information", b =>
                 {
                     b.Property<Guid>("Id")

+ 2 - 3
DutyApp/Models/DutyOfficer.cs

@@ -1,8 +1,7 @@
-using System;
-using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
 
+using System;
 using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
 
 namespace DutyApp.Models
 {

+ 28 - 0
DutyApp/Models/DutyStatus.cs

@@ -0,0 +1,28 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+using Avalonia.Media;
+
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace DutyApp.Models
+{
+    public class DutyStatus : ObservableObject
+    {
+        private Color _color = Colors.White;
+
+        [Key]
+        public Guid Id { get; set; } = new();
+
+        [StringLength(10)]
+        public string Name { get; set; } = string.Empty;
+
+        public Color Color
+        {
+            get => _color;
+            set => SetProperty(ref _color, value);
+        }
+
+        public int Index { get; set; }
+    }
+}

+ 3 - 5
DutyApp/Models/JobTitle.cs

@@ -1,9 +1,7 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-
-using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
 
-using DutyApp.ViewModels;
+using System;
+using System.ComponentModel.DataAnnotations;
 
 namespace DutyApp.Models
 {

+ 1 - 0
DutyApp/ViewLocator.cs

@@ -24,6 +24,7 @@ namespace DutyApp
             RegisterViewFactory<DutyOfficerViewModel, DutyOfficerPage>();
             RegisterViewFactory<InformationViewModel, InformationPage>();
             RegisterViewFactory<JobTitleViewModel, JobTitlePage>();
+            RegisterViewFactory<DutyStatusViewModel, DutyStatusView>();
         }
 
         public Control Build(object? data)

+ 17 - 12
DutyApp/ViewModels/DailyDutyRecordViewModel.cs

@@ -15,6 +15,7 @@ using System.ComponentModel;
 using System.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls;
+using Avalonia.Media;
 using DutyApp.Customs;
 using DutyApp.Views;
 
@@ -33,15 +34,7 @@ namespace DutyApp.ViewModels
         private ObservableCollection<string> _dutyOfficersName = [];
 
         [ObservableProperty]
-        private ObservableCollection<string> _statusList =
-        [
-            "值班领导",
-            "值班员",
-            "在位",
-            "出差",
-            "请假",
-            "休假"
-        ];
+        private ObservableCollection<string> _statusList = [];
 
         [ObservableProperty]
         private DailyDutyRecord? _selectedItem;
@@ -58,6 +51,8 @@ namespace DutyApp.ViewModels
         [ObservableProperty]
         private DateOnly _currentDate = DateOnly.FromDateTime(DateTime.Today);
 
+        private Dictionary<string, Color> _status = [];
+
         public DailyDutyRecordViewModel()
         {
             DailyDutyRecords.CollectionChanged += DailyDutyRecords_CollectionChanged;
@@ -132,6 +127,16 @@ namespace DutyApp.ViewModels
             {
                 DutyOfficersName.Add(officer.Name);
             }
+
+            var dutyStatusList = await _dutyContext
+                .DutyStatus
+                .OrderBy(x => x.Index)
+                .ToListAsync();
+            foreach (var status in dutyStatusList)
+            {
+                StatusList.Add(status.Name);
+                _status.Add(status.Name, status.Color);
+            }
         }
 
         [RelayCommand]
@@ -161,8 +166,8 @@ namespace DutyApp.ViewModels
 
             if (DailyDutyRecords.Count > 0)
             {
-                var result = await MessageBox.Show("导入上一次值班记录,会清空当前值班记录,是否导入?", "警告", MessageBoxButtons.YesNoCancel);
-                if (result == MessageBoxResult.Cancel) return;
+                var result = await MessageBox.Show("导入上一次值班记录,会清空当前值班记录,是否导入?", "警告", MessageBoxButtons.YesNo);
+                if (result == MessageBoxResult.No) return;
 
                 foreach (var record in DailyDutyRecords)
                 {
@@ -307,7 +312,7 @@ namespace DutyApp.ViewModels
             var previewWindow = Ioc.Default.GetRequiredService<DutyPreviewWindow>();
             previewWindow.Records = DailyDutyRecords;
 
-            previewWindow.Draw();
+            previewWindow.Draw(_status);
             previewWindow.Show();
         }
     }

+ 115 - 0
DutyApp/ViewModels/DutyStatusViewModel.cs

@@ -0,0 +1,115 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+
+using DutyApp.Data;
+using DutyApp.Models;
+
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Input;
+using DutyApp.Customs;
+using Microsoft.EntityFrameworkCore;
+using Avalonia.Controls;
+
+namespace DutyApp.ViewModels
+{
+    public partial class DutyStatusViewModel : ViewModelBase
+    {
+        private readonly DutyContext? _context = Ioc.Default.GetRequiredService<DutyContext>();
+
+        [ObservableProperty]
+        private ObservableCollection<DutyStatus> _dutyStatuses = [];
+
+        [ObservableProperty]
+        private DutyStatus? _selectedStatus;
+
+        public DutyStatusViewModel()
+        {
+            _ = InitDataAsync();
+        }
+
+        private async Task InitDataAsync()
+        {
+            if (_context == null) return;
+
+            var statusList = await _context.DutyStatus
+                .OrderBy(x => x.Index)
+                .ToListAsync();
+            foreach (var status in statusList)
+            {
+                DutyStatuses.Add(status);
+            }
+        }
+
+        [RelayCommand]
+        private void AddStatus()
+        {
+            var status = new DutyStatus();
+
+            DutyStatuses.Add(status);
+        }
+
+        [RelayCommand]
+        private async Task RemoveStatus()
+        {
+            if (SelectedStatus == null || _context == null) return;
+
+            var dutyStatus = await _context.DutyStatus.FirstOrDefaultAsync(x => x.Id == SelectedStatus.Id);
+            if (dutyStatus!=null)
+            {
+                _context.DutyStatus.Remove(dutyStatus);
+
+                var result = await _context.SaveChangesAsync();
+                if (result == 0)
+                {
+                    await MessageBox.Show("删除失败");
+                    return;
+                }
+            }
+
+            DutyStatuses.Remove(SelectedStatus);
+        }
+
+        [RelayCommand]
+        private async Task SaveStatus(DataGrid grid)
+        {
+            if (_context == null) return;
+
+            for (var i = 0; i < DutyStatuses.Count; i++)
+            {
+                var status = DutyStatuses[i];
+                status.Index = i;
+            }
+
+            for (var i = 0; i < DutyStatuses.Count; i++)
+            {
+                var dutyStatus = DutyStatuses[i];
+                if (string.IsNullOrEmpty(dutyStatus.Name))
+                {
+                    grid.SelectedIndex = i;
+                    await MessageBox.Show("请填写状态名称");
+                    return;
+                }
+
+                var status = _context.Entry(dutyStatus).State;
+                switch (status)
+                {
+                    case EntityState.Modified:
+                        _context.DutyStatus.Update(dutyStatus);
+                        break;
+                    case EntityState.Added:
+                    case EntityState.Detached:
+                        await _context.DutyStatus.AddAsync(dutyStatus);
+                        break;
+                }
+            }
+
+            var change = await _context.SaveChangesAsync();
+            if (change >= 1)
+            {
+                await MessageBox.Show("保存成功");
+            }
+        }
+    }
+}

+ 1 - 0
DutyApp/ViewModels/MainViewModel.cs

@@ -39,6 +39,7 @@ namespace DutyApp.ViewModels
             new ListItemTemplate(typeof(DailyDutyRecordViewModel), RegularFontUtil.Calendar_32, "每日值班记录"),
             new ListItemTemplate(typeof(DutyOfficerViewModel), RegularFontUtil.Guest_48, "值班人员登记"),
             new ListItemTemplate(typeof(JobTitleViewModel), RegularFontUtil.Contact_Card_48, "职位登记"),
+            new ListItemTemplate(typeof(DutyStatusViewModel), RegularFontUtil.Status_48, "值班状态登记"),
         ];
 
         [ObservableProperty]

+ 19 - 0
DutyApp/Views/DutyControls/DailyDuty.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Media;
 
 using DutyApp.Models;
 
@@ -12,6 +13,7 @@ public partial class DailyDuty : UserControl
         InitializeComponent();
 
         RecordProperty.Changed.AddClassHandler<DailyDuty>(OnRecordChanged);
+        TitleColorProperty.Changed.AddClassHandler<DailyDuty>(OnTitleColorChanged);
     }
 
     public DailyDutyRecord Record
@@ -33,6 +35,23 @@ public partial class DailyDuty : UserControl
         content.DutyContent.UserByteImage = record.Picture;
     }
 
+
+    public Color TitleColor
+    {
+        get => GetValue(TitleColorProperty);
+        set => SetValue(TitleColorProperty, value);
+    }
+
+    public static readonly StyledProperty<Color> TitleColorProperty =
+        AvaloniaProperty.Register<DailyDuty, Color>(nameof(TitleColor), Colors.Black);
+
+    private void OnTitleColorChanged(DailyDuty content, AvaloniaPropertyChangedEventArgs args)
+    {
+        if (args.NewValue is not Color color) return;
+
+        content.DutyTitle.TitleColor = color;
+    }
+
     private void Control_OnSizeChanged(object? sender, SizeChangedEventArgs e)
     {
         DutyTitle.ShapeWidth = e.NewSize.Width;

+ 15 - 0
DutyApp/Views/DutyControls/DutyTitle.axaml.cs

@@ -16,6 +16,7 @@ public partial class DutyTitle : UserControl
         ShapeHeightProperty.Changed.AddClassHandler<DutyTitle>(OnSizeChanged);
         TitleProperty.Changed.AddClassHandler<DutyTitle>(OnTitleChanged);
         TitleSizeProperty.Changed.AddClassHandler<DutyTitle>(OnTitleSizeChanged);
+        TitleColorProperty.Changed.AddClassHandler<DutyTitle>(OnTitleColorChanged);
     }
 
     public double ShapeWidth
@@ -55,6 +56,20 @@ public partial class DutyTitle : UserControl
         sender.TitleText.Text = args.NewValue as string;
     }
 
+    public Color? TitleColor
+    {
+        get => GetValue(TitleColorProperty);
+        set => SetValue(TitleColorProperty, value);
+    }
+
+    public static readonly StyledProperty<Color> TitleColorProperty =
+        AvaloniaProperty.Register<DutyTitle, Color>(nameof(TitleColor));
+
+    private void OnTitleColorChanged(DutyTitle sender, AvaloniaPropertyChangedEventArgs args)
+    {
+        sender.TitleText.Foreground = new SolidColorBrush((Color)(args.NewValue ?? Colors.White));
+    }
+
     public double TitleSize
     {
         get => (int)GetValue(TitleSizeProperty);

+ 5 - 6
DutyApp/Views/DutyPreviewWindow.axaml

@@ -3,7 +3,6 @@
     xmlns="https://github.com/avaloniaui"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-    xmlns:dutyControls="clr-namespace:DutyApp.Views.DutyControls"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:utils="clr-namespace:DutyApp.Utils"
     xmlns:viewModels="clr-namespace:DutyApp.ViewModels"
@@ -45,16 +44,16 @@
                     <Grid Margin="10" RowDefinitions="*,*,100">
 
                         <Grid
-                            x:Name="FirstGrid"
-                            Grid.Row="0"
+                            x:Name="SecondGrid"
+                            Grid.Row="1"
                             Margin="0,30,0,0" />
 
                         <StackPanel
-                            x:Name="SecondGrid"
-                            Grid.Row="1"
+                            x:Name="FirstGrid"
+                            Grid.Row="0"
                             Margin="0,30,0,0"
                             Orientation="Horizontal"
-                            SizeChanged="SecondGrid_OnSizeChanged" />
+                            SizeChanged="FirstGrid_OnSizeChanged" />
 
                         <Grid Grid.Row="2" ColumnDefinitions="*,*,*,*,*,*">
 

+ 25 - 23
DutyApp/Views/DutyPreviewWindow.axaml.cs

@@ -1,3 +1,4 @@
+using System.Collections.Generic;
 using Avalonia.Controls;
 
 using DutyApp.Models;
@@ -5,6 +6,7 @@ using DutyApp.Views.DutyControls;
 
 using System.Collections.ObjectModel;
 using System.Linq;
+using Avalonia.Media;
 
 namespace DutyApp.Views;
 
@@ -17,69 +19,69 @@ public partial class DutyPreviewWindow : Window
         InitializeComponent();
     }
 
-    public void Draw()
+    public void Draw(Dictionary<string, Color> statusList)
     {
         if (Records.Count == 0) { return; }
 
-        // 获取前一半位人员
-        var odd = Records.Count % 2;
-        var middle = Records.Count / 2;
-        var media = odd == 0 ? middle : middle + 1;
-        var top = Records.Take(media).ToArray();
+        // 优先设置第二行
+        var media = Records.Count / 2;
+        var rest = Records.Count - media;
 
+        var top = Records.Skip(media).Take(rest).ToArray();
         for (var index = 0; index < top.Length; index++)
         {
             var record = top[index];
             var dailDuty = new DailyDuty()
             {
-                Record = record
+                Record = record,
+                TitleColor = statusList.TryGetValue(record.Status, out var value) ? value : Colors.White
             };
 
-            FirstGrid.ColumnDefinitions.Add(new ColumnDefinition());
-            FirstGrid.Children.Add(dailDuty);
+            SecondGrid.ColumnDefinitions.Add(new ColumnDefinition());
+            SecondGrid.Children.Add(dailDuty);
             Grid.SetColumn(dailDuty, index);
         }
 
-        // 获取剩余人员
-        var rest = Records.Count - media;
-        var next = Records.Skip(media).Take(rest).ToArray();
+        // 设置第一行
+        var next = Records.Take(media).ToArray();
         foreach (var record in next)
         {
             var dailDuty = new DailyDuty()
             {
-                Record = record
+                Record = record,
+                TitleColor = statusList.TryGetValue(record.Status, out var value) ? value : Colors.White,
             };
 
-            SecondGrid.Children.Add(dailDuty);
+            FirstGrid.Children.Add(dailDuty);
         }
 
         var dutyCount = Records.AsEnumerable().Count(x => x.Status is "在位" or "值班领导" or "值班员");
         var tripCount = Records.AsEnumerable().Count(x => x.Status == "出差");
-        var leaveCount = Records.AsEnumerable().Count(x => x.Status is "请假" or "休假");
+        var leaveCount = Records.AsEnumerable().Count(x => x.Status is "请假" or "休假" or "事假");
 
         DutyText.Text = $"在位人数:{dutyCount}";
         TripText.Text = $"出差人数: {tripCount}";
-        LeaveText.Text = $"(休)假人数:{leaveCount}";
+        LeaveText.Text = $"(休)假人数:{leaveCount}";
     }
 
-    private void SecondGrid_OnSizeChanged(object? sender, SizeChangedEventArgs e)
+    private void FirstGrid_OnSizeChanged(object? sender, SizeChangedEventArgs e)
     {
         if (sender is not StackPanel panel) return;
 
-        var firstOrDefault = FirstGrid.ColumnDefinitions.FirstOrDefault();
+        var secondDefault = SecondGrid.ColumnDefinitions.FirstOrDefault();
 
-        if (firstOrDefault == null) { return; }
+        if (secondDefault == null) { return; }
 
-        foreach (var child in SecondGrid.Children)
+        foreach (var child in FirstGrid.Children)
         {
-            child.Width = firstOrDefault.ActualWidth;
+            child.Width = secondDefault.ActualWidth;
         }
 
         var firstGridChildren = FirstGrid.Children.Count;
         var secondGridChildren = SecondGrid.Children.Count;
-        if (secondGridChildren < firstGridChildren)
+        if (secondGridChildren > firstGridChildren)
         {
-            panel.Spacing = firstOrDefault.ActualWidth / (secondGridChildren - 1);
+            panel.Spacing = secondDefault.ActualWidth / (firstGridChildren - 1);
         }
     }
 }

+ 170 - 0
DutyApp/Views/DutyStatusView.axaml

@@ -0,0 +1,170 @@
+<UserControl
+    x:Class="DutyApp.Views.DutyStatusView"
+    xmlns="https://github.com/avaloniaui"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:behaviors="clr-namespace:DutyApp.Behaviors"
+    xmlns:converters="clr-namespace:Avalonia.Markup.Xaml.Converters;assembly=Avalonia.Markup.Xaml"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
+    xmlns:idd="clr-namespace:Avalonia.Xaml.Interactions.DragAndDrop;assembly=Avalonia.Xaml.Interactions.DragAndDrop"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:utils="clr-namespace:DutyApp.Utils"
+    xmlns:viewModels="clr-namespace:DutyApp.ViewModels"
+    d:DesignHeight="450"
+    d:DesignWidth="800"
+    x:DataType="viewModels:DutyStatusViewModel"
+    mc:Ignorable="d">
+    <UserControl.DataContext>
+        <viewModels:DutyStatusViewModel />
+    </UserControl.DataContext>
+
+    <UserControl.Resources>
+        <converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
+    </UserControl.Resources>
+
+    <UserControl.Styles>
+        <Style Selector="DataGrid.DragAndDrop">
+            <Style.Resources>
+                <behaviors:JobTitleDataGridDropHandler x:Key="ItemsDataGridDropHandler" />
+            </Style.Resources>
+            <Setter Property="RowHeaderWidth" Value="24" />
+            <Setter Property="(i:Interaction.Behaviors)">
+                <i:BehaviorCollectionTemplate>
+                    <i:BehaviorCollection>
+                        <idd:ContextDropBehavior Handler="{StaticResource ItemsDataGridDropHandler}" />
+                    </i:BehaviorCollection>
+                </i:BehaviorCollectionTemplate>
+            </Setter>
+        </Style>
+
+        <Style Selector="DataGrid.DragAndDrop DataGridRow.DraggingUp">
+            <Setter Property="AdornerLayer.Adorner">
+                <Template>
+                    <Border BorderBrush="{DynamicResource SystemAccentColor}" BorderThickness="0,2,0,0" />
+                </Template>
+            </Setter>
+        </Style>
+
+        <Style Selector="DataGrid.DragAndDrop DataGridRow.DraggingDown">
+            <Setter Property="AdornerLayer.Adorner">
+                <Template>
+                    <Border BorderBrush="{DynamicResource SystemAccentColor}" BorderThickness="0,0,0,2" />
+                </Template>
+            </Setter>
+        </Style>
+
+        <Style Selector="DataGrid.DragAndDrop DataGridRowHeader">
+            <Setter Property="(i:Interaction.Behaviors)">
+                <i:BehaviorCollectionTemplate>
+                    <i:BehaviorCollection>
+                        <behaviors:ContextDragWithDirectionBehavior HorizontalDragThreshold="3" VerticalDragThreshold="3" />
+                    </i:BehaviorCollection>
+                </i:BehaviorCollectionTemplate>
+            </Setter>
+            <Setter Property="Content">
+                <Template>
+                    <TextBlock
+                        HorizontalAlignment="Center"
+                        VerticalAlignment="Center"
+                        FontFamily="{StaticResource FluentSystemIconsRegular}"
+                        FontSize="12"
+                        Text="{x:Static utils:RegularFontUtil.Re_Order_Dots_Vertical_24}" />
+                </Template>
+            </Setter>
+        </Style>
+
+        <Style Selector="DataGrid.ItemsDragAndDrop">
+            <Style.Resources>
+                <behaviors:JobTitleDataGridDropHandler x:Key="ItemsDataGridDropHandler" />
+            </Style.Resources>
+            <Setter Property="(i:Interaction.Behaviors)">
+                <i:BehaviorCollectionTemplate>
+                    <i:BehaviorCollection>
+                        <idd:ContextDropBehavior Handler="{StaticResource ItemsDataGridDropHandler}" />
+                    </i:BehaviorCollection>
+                </i:BehaviorCollectionTemplate>
+            </Setter>
+        </Style>
+    </UserControl.Styles>
+
+    <Grid Margin="10" RowDefinitions="Auto,*">
+        <StackPanel
+            Grid.Row="0"
+            HorizontalAlignment="Right"
+            VerticalAlignment="Center"
+            Orientation="Horizontal">
+            <Button Background="Transparent" Command="{Binding AddStatusCommand}">
+                <StackPanel VerticalAlignment="Center" Orientation="Horizontal">
+                    <TextBlock
+                        VerticalAlignment="Center"
+                        FontFamily="{StaticResource FluentSystemIconsRegular}"
+                        Text="{x:Static utils:RegularFontUtil.Add_48}" />
+                    <TextBlock VerticalAlignment="Center" Text="新增" />
+                </StackPanel>
+            </Button>
+            <Button Background="Transparent" Command="{Binding RemoveStatusCommand}">
+                <StackPanel VerticalAlignment="Center" Orientation="Horizontal">
+                    <TextBlock
+                        VerticalAlignment="Center"
+                        FontFamily="{StaticResource FluentSystemIconsRegular}"
+                        Text="{x:Static utils:RegularFontUtil.Delete_48}" />
+                    <TextBlock VerticalAlignment="Center" Text="删除" />
+                </StackPanel>
+            </Button>
+            <Button
+                Background="Transparent"
+                Command="{Binding SaveStatusCommand}"
+                CommandParameter="{Binding ElementName=DataGrid}">
+                <StackPanel VerticalAlignment="Center" Orientation="Horizontal">
+                    <TextBlock
+                        VerticalAlignment="Center"
+                        FontFamily="{StaticResource FluentSystemIconsRegular}"
+                        Text="{x:Static utils:RegularFontUtil.Save_32}" />
+                    <TextBlock VerticalAlignment="Center" Text="保存" />
+                </StackPanel>
+            </Button>
+        </StackPanel>
+
+        <DataGrid
+            x:Name="DataGrid"
+            Grid.Row="1"
+            AutoGenerateColumns="False"
+            BorderBrush="LightGray"
+            BorderThickness="1"
+            Classes="DragAndDrop ItemsDragAndDrop"
+            CornerRadius="3"
+            GridLinesVisibility="All"
+            HeadersVisibility="All"
+            ItemsSource="{Binding DutyStatuses, Mode=TwoWay}"
+            SelectedItem="{Binding SelectedStatus}">
+            <DataGrid.Columns>
+                <DataGridTextColumn
+                    Width="5"
+                    IsReadOnly="True"
+                    IsVisible="False" />
+                <DataGridTextColumn
+                    Width="200"
+                    Binding="{Binding Name}"
+                    Header="状态*"
+                    IsVisible="True" />
+                <DataGridTemplateColumn
+                    Width="200"
+                    Header="颜色*"
+                    IsVisible="True">
+                    <DataGridTemplateColumn.CellTemplate>
+                        <DataTemplate>
+                            <TextBlock Background="{Binding Color, Converter={StaticResource ColorToBrushConverter}}" />
+                        </DataTemplate>
+                    </DataGridTemplateColumn.CellTemplate>
+                    <DataGridTemplateColumn.CellEditingTemplate>
+                        <DataTemplate>
+                            <Grid>
+                                <ColorPicker Width="200" Color="{Binding Color, Mode=TwoWay}" />
+                            </Grid>
+                        </DataTemplate>
+                    </DataGridTemplateColumn.CellEditingTemplate>
+                </DataGridTemplateColumn>
+            </DataGrid.Columns>
+        </DataGrid>
+    </Grid>
+</UserControl>

+ 11 - 0
DutyApp/Views/DutyStatusView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace DutyApp.Views;
+
+public partial class DutyStatusView : UserControl
+{
+    public DutyStatusView()
+    {
+        InitializeComponent();
+    }
+}

+ 1 - 1
DutyApp/Views/HomePageView.axaml

@@ -86,7 +86,7 @@
 
                 <controls:FlipView
                     x:Name="ImageFlipView"
-                    Grid.Column=""
+                    Grid.Column="0"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
                     Background="Transparent"