生产模块拆分为前缫/后缫;修复菜单卡顿与重复请求;支持 IIS 单站点部署

1. 生产模块按工序拆分为两个一级菜单
   - 生产-前缫(落丝记录)、生产-后缫(纤度检验、黑板检验)
   - 旧的「生产管理」(/prod) 目录及子菜单由种子自动清理,页面组件与权限标识保持不变
   - 消息中心跳转路径、首页快捷入口路径同步更新

2. 职工管理独立为一级菜单(原挂在「基础数据」目录下),并清理历史子菜单

3. 修复侧边栏菜单展开卡顿(基础数据等子项较多的目录尤其明显)
   - 去掉 Element Plus 的 height 过渡动画(会逐帧重排全部子项)
   - 收起时改用 height:0 + visibility:hidden 而非 display:none,子项提前完成布局
   - 菜单区独立滚动并预留滚动条宽度(scrollbar-gutter),消除展开时的横向抖动
   - 菜单项增加 contain 布局隔离,淡入动画改为容器级

4. 修复页面首次打开重复请求
   - 分页组件挂载时会触发一次 current-change,页码未变化时不再重复加载(18 个页面)
   - 职工管理去掉 onMounted 与默认选中部门的重复加载
   - 工种管理、原料庄口的注册表下拉由按 key 多次请求改为一次全量拉取

5. 后端增加前端静态托管与 SPA 路由回退,支持 IIS 单站点部署(前后端同源)
6. 新增部署脚本 deploy-iis.ps1(首次部署)与 update-iis.ps1(更新发布)
This commit is contained in:
yanpeng200212
2026-10-12 14:09:59 +08:00
parent 2ffb83830c
commit c3e2665257
47 changed files with 59270 additions and 109 deletions
@@ -0,0 +1,134 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.CQRS.SYS.Employees.Commands;
using F9WebAiTest.Application.CQRS.SYS.Employees.Queries;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers;
/// <summary>职工档案管理(含转正/离职/调岗/重新入职/生产信息维护)</summary>
[ApiController]
[Route("api/sys/employee")]
[Authorize]
public class SysEmployeeController : ControllerBase
{
private readonly IMediator _mediator;
public SysEmployeeController(IMediator mediator) => _mediator = mediator;
/// <summary>分页查询(部门含子部门;workStatus/banci 不传表示全部)</summary>
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] long depId = 0, [FromQuery] long worktypeId = 0,
[FromQuery] Employee_WorkStatus_Enum? workStatus = null, [FromQuery] Prod_Banci_Enum? banci = null,
[FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetEmployeePagedQuery(depId, worktypeId, workStatus,
banci, like ?? string.Empty, pageIndex, pageSize))));
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
var result = await _mediator.Send(new GetEmployeeByIdQuery(id));
return result == null
? NotFound(ApiResult<object>.Fail("职工不存在", 404))
: Ok(ApiResult<object>.Ok(result));
}
/// <summary>变更记录</summary>
[HttpGet("{id}/change-records")]
public async Task<IActionResult> GetChangeRecords(long id)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetEmployeeChangeRecordsQuery(id))));
/// <summary>调岗记录</summary>
[HttpGet("{id}/tiaogang-records")]
public async Task<IActionResult> GetTiaogangRecords(long id)
=> Ok(ApiResult<object>.Ok(await _mediator.Send(new GetEmployeeTiaogangRecordsQuery(id))));
[HttpPost]
public async Task<IActionResult> Create([FromBody] Employee_CreateCommand command)
{
try { return Ok(ApiResult<object>.Ok(new { id = await _mediator.Send(command) }, "新增成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] Employee_UpdateCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new Employee_DelCommand(id)), "删除成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>转正</summary>
[HttpPut("{id}/zhuanzheng")]
public async Task<IActionResult> Zhuanzheng(long id, [FromBody] Employee_ZhuanzhengCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "转正成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>取消转正</summary>
[HttpPut("{id}/zhuanzheng-cancel")]
public async Task<IActionResult> ZhuanzhengCancel(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new Employee_ZhuanzhengCancelCommand(id)), "已取消转正")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>离职</summary>
[HttpPut("{id}/lizhi")]
public async Task<IActionResult> Lizhi(long id, [FromBody] Employee_LizhiCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "已办理离职")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>取消离职</summary>
[HttpPut("{id}/lizhi-cancel")]
public async Task<IActionResult> LizhiCancel(long id)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(new Employee_LizhiCancelCommand(id)), "已取消离职")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>调岗</summary>
[HttpPut("{id}/tiaogang")]
public async Task<IActionResult> Tiaogang(long id, [FromBody] Employee_TiaogangCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "调岗成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>重新入职</summary>
[HttpPut("{id}/renew-ruzhis")]
public async Task<IActionResult> RenewRuzhi(long id, [FromBody] Employee_RenewRuzhiCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "重新入职成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>修改生产信息(工作组 / 顺序号)</summary>
[HttpPut("{id}/sc")]
public async Task<IActionResult> UpdateSc(long id, [FromBody] Employee_UpdateScCommand command)
{
try { return Ok(ApiResult<object>.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
}
+8
View File
@@ -98,6 +98,11 @@ using (var scope = app.Services.CreateScope())
DbSeeder.Seed(fsql, snowflake);
}
// 前端静态资源:Vue 打包产物放在 wwwroot。
// IIS 单站点部署时前端与 API 同源(浏览器访问 /api/...),无需跨域与反向代理。
app.UseDefaultFiles();
app.UseStaticFiles();
// NSwag 中间件(swagger 文档 + UI)
app.UseOpenApi();
app.UseSwaggerUi();
@@ -109,4 +114,7 @@ app.UseAuthorization();
app.MapControllers();
app.MapHub<NotificationHub>("/hubs/notification");
// 前端使用 history 路由,未匹配到 API/静态文件的路径统一交给 index.html
app.MapFallbackToFile("index.html");
app.Run();
Binary file not shown.
@@ -0,0 +1,324 @@
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Dtos.SYS.Employees;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Employees;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Entities.SYS.Employees;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using F9WebAiTest.Domain.Interfaces.SYS.Employees;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Employees.Commands;
/// <summary>新增职工(同时写一条"新入职"变更记录)</summary>
public record Employee_CreateCommand(
long DepId, long WorktypeId, string Code, string Name, string Gangwei, string Phone, string IdCard,
DateTime? BirthDate, string Address, string Gender, string Minzu, string HunyinStatus, string Huji,
DateTime? RuzhiDate, string JinjiLianxiren, string JinjiLianxirenPhone, string HetongType,
DateTime? HetongStartDate, DateTime? HetongEndDate, DateTime? TuixiuDate, DateTime? ShebaoDate, string ShebaoDesc,
string BankcardCode, string BiyeYuanxiao, string Zhuanye, string Xueli, Prod_Banci_Enum? Banci, string WorkGroup,
int Sort, string NamePinyin, string Memo) : IRequest<long>;
public class CreateEmployeeCommandHandler(
IEmployeeWriteRepository repository,
IEmployeeReadRepository readRepository,
ISnowflakeIdGenerator snowflakeIdGenerator,
IPublisher eventBus) : IRequestHandler<Employee_CreateCommand, long>
{
public async Task<long> Handle(Employee_CreateCommand command, CancellationToken cancellationToken)
{
if (command.DepId <= 0) throw new F9OperationException("请选择部门");
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("请输入姓名");
var entity = new Employee(snowflakeIdGenerator.CreateId(), command.DepId, command.WorktypeId,
Employee_WorkStatus_Enum.在职, command.Code ?? string.Empty, command.Name.Trim(),
command.Gangwei ?? string.Empty, command.Phone ?? string.Empty, command.IdCard ?? string.Empty,
command.BirthDate, command.Address ?? string.Empty, command.Gender ?? string.Empty, command.Minzu ?? string.Empty,
command.HunyinStatus ?? string.Empty, command.Huji ?? string.Empty, command.RuzhiDate, null, null, false,
command.JinjiLianxiren ?? string.Empty, command.JinjiLianxirenPhone ?? string.Empty,
command.HetongType ?? string.Empty, command.HetongStartDate, command.HetongEndDate, command.TuixiuDate,
command.ShebaoDate, command.ShebaoDesc ?? string.Empty, command.BankcardCode ?? string.Empty,
command.BiyeYuanxiao ?? string.Empty, command.Zhuanye ?? string.Empty, command.Xueli ?? string.Empty,
command.Banci, command.WorkGroup ?? string.Empty, command.Sort, command.NamePinyin ?? string.Empty,
command.Memo ?? string.Empty, string.Empty);
var result = await repository.AddAsync(entity);
if (result == false) throw new F9OperationException("职工", "新增");
// 变更记录:新入职
var depName = await readRepository.GetDepNameAsync(entity.DepId);
var worktypeName = await readRepository.GetWorktypeNameAsync(entity.WorktypeId);
await readRepository.AddChangeRecordAsync(new EmployeeChangeRecordDto(0, entity.Id,
entity.RuzhiDate ?? DateTime.Now.Date, Employee_ChangeStatus_Enum.新入职, "",
$"{depName},{worktypeName}", "新入职"));
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return entity.Id;
}
}
/// <summary>修改职工基本信息</summary>
public record Employee_UpdateCommand(
long Id, string Code, string Name, string Gangwei, string Phone, string IdCard, DateTime? BirthDate,
string Address, string Gender, string Minzu, string HunyinStatus, string Huji, DateTime? RuzhiDate,
string JinjiLianxiren, string JinjiLianxirenPhone, string HetongType, DateTime? HetongStartDate,
DateTime? HetongEndDate, DateTime? TuixiuDate, DateTime? ShebaoDate, string ShebaoDesc, string BankcardCode,
string BiyeYuanxiao, string Zhuanye, string Xueli, string NamePinyin, string Memo) : IRequest<bool>;
public class UpdateEmployeeCommandHandler(
IEmployeeWriteRepository repository,
IPublisher eventBus) : IRequestHandler<Employee_UpdateCommand, bool>
{
public async Task<bool> Handle(Employee_UpdateCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("请输入姓名");
entity.Update(command.Code ?? string.Empty, command.Name.Trim(), command.Gangwei ?? string.Empty,
command.Phone ?? string.Empty, command.IdCard ?? string.Empty, command.BirthDate, command.Address ?? string.Empty,
command.Gender ?? string.Empty, command.Minzu ?? string.Empty, command.HunyinStatus ?? string.Empty,
command.Huji ?? string.Empty, command.RuzhiDate, command.JinjiLianxiren ?? string.Empty,
command.JinjiLianxirenPhone ?? string.Empty, command.HetongType ?? string.Empty, command.HetongStartDate,
command.HetongEndDate, command.TuixiuDate, command.ShebaoDate, command.ShebaoDesc ?? string.Empty,
command.BankcardCode ?? string.Empty, command.BiyeYuanxiao ?? string.Empty, command.Zhuanye ?? string.Empty,
command.Xueli ?? string.Empty, command.NamePinyin ?? string.Empty, command.Memo ?? string.Empty);
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("职工", "修改");
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>删除职工</summary>
public record Employee_DelCommand(long Id) : IRequest<bool>;
public class DelEmployeeCommandHandler(
IEmployeeWriteRepository repository,
IPublisher eventBus) : IRequestHandler<Employee_DelCommand, bool>
{
public async Task<bool> Handle(Employee_DelCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
var result = await repository.SoftDeleteAsync(command.Id);
if (result == false) throw new F9OperationException("职工", "删除");
entity.Deleted();
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>转正(写"转正"变更记录)</summary>
public record Employee_ZhuanzhengCommand(long Id, DateTime ZhuanzhengDate) : IRequest<bool>;
public class ZhuanzhengCommandHandler(
IEmployeeWriteRepository repository,
IEmployeeReadRepository readRepository,
IPublisher eventBus) : IRequestHandler<Employee_ZhuanzhengCommand, bool>
{
public async Task<bool> Handle(Employee_ZhuanzhengCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
if (entity.IsZhuanzheng) throw new F9OperationException("该职工已转正");
entity.Zhuanzheng(command.ZhuanzhengDate);
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("转正", "操作");
var depName = await readRepository.GetDepNameAsync(entity.DepId);
var worktypeName = await readRepository.GetWorktypeNameAsync(entity.WorktypeId);
await readRepository.AddChangeRecordAsync(new EmployeeChangeRecordDto(0, entity.Id, command.ZhuanzhengDate,
Employee_ChangeStatus_Enum.转正, "", $"{depName},{worktypeName}", "转正"));
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>取消转正</summary>
public record Employee_ZhuanzhengCancelCommand(long Id) : IRequest<bool>;
public class ZhuanzhengCancelCommandHandler(
IEmployeeWriteRepository repository,
IPublisher eventBus) : IRequestHandler<Employee_ZhuanzhengCancelCommand, bool>
{
public async Task<bool> Handle(Employee_ZhuanzhengCancelCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
entity.ZhuanzhengCancel();
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("取消转正", "操作");
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>离职(写"离职"变更记录)</summary>
public record Employee_LizhiCommand(long Id, DateTime LizhiDate, string LizhiYuanyin) : IRequest<bool>;
public class LizhiCommandHandler(
IEmployeeWriteRepository repository,
IEmployeeReadRepository readRepository,
IPublisher eventBus) : IRequestHandler<Employee_LizhiCommand, bool>
{
public async Task<bool> Handle(Employee_LizhiCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
if (entity.WorkStatus == Employee_WorkStatus_Enum.离职) throw new F9OperationException("该职工已离职");
entity.Lizhi(command.LizhiDate, command.LizhiYuanyin);
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("离职", "操作");
var depName = await readRepository.GetDepNameAsync(entity.DepId);
var worktypeName = await readRepository.GetWorktypeNameAsync(entity.WorktypeId);
await readRepository.AddChangeRecordAsync(new EmployeeChangeRecordDto(0, entity.Id, command.LizhiDate,
Employee_ChangeStatus_Enum.离职, "", $"{depName},{worktypeName}", command.LizhiYuanyin ?? string.Empty));
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>取消离职</summary>
public record Employee_LizhiCancelCommand(long Id) : IRequest<bool>;
public class LizhiCancelCommandHandler(
IEmployeeWriteRepository repository,
IPublisher eventBus) : IRequestHandler<Employee_LizhiCancelCommand, bool>
{
public async Task<bool> Handle(Employee_LizhiCancelCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
entity.LizhiCancel();
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("取消离职", "操作");
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>调岗(写调岗记录 + 变更记录)</summary>
public record Employee_TiaogangCommand(long Id, long DepId, long WorkTypeId, DateTime Date, string GangweiName,
Prod_Banci_Enum? Sc_Banci) : IRequest<bool>;
public class TiaogangCommandHandler(
IEmployeeWriteRepository repository,
IEmployeeReadRepository readRepository,
IPublisher eventBus) : IRequestHandler<Employee_TiaogangCommand, bool>
{
public async Task<bool> Handle(Employee_TiaogangCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
if (command.DepId <= 0) throw new F9OperationException("请选择部门");
if (entity.WorkStatus == Employee_WorkStatus_Enum.离职) throw new F9OperationException("离职人员请使用「重新入职」");
var oldDepId = entity.DepId;
var oldWorktypeId = entity.WorktypeId;
entity.Tiaogang(command.DepId, command.WorkTypeId, command.GangweiName, command.Sc_Banci);
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("调岗", "操作");
await readRepository.AddTiaogangRecordAsync(entity.Id, oldDepId, oldWorktypeId, command.DepId,
command.WorkTypeId, command.Date);
var depName = await readRepository.GetDepNameAsync(command.DepId);
var worktypeName = await readRepository.GetWorktypeNameAsync(command.WorkTypeId);
await readRepository.AddChangeRecordAsync(new EmployeeChangeRecordDto(0, entity.Id, command.Date,
Employee_ChangeStatus_Enum.调岗, "", $"{depName},{worktypeName}", "调动"));
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>重新入职(离职人员)</summary>
public record Employee_RenewRuzhiCommand(long Id, long DepId, long WorkTypeId, DateTime Date, string GangweiName,
Prod_Banci_Enum? Sc_Banci) : IRequest<bool>;
public class RenewRuzhiCommandHandler(
IEmployeeWriteRepository repository,
IEmployeeReadRepository readRepository,
IPublisher eventBus) : IRequestHandler<Employee_RenewRuzhiCommand, bool>
{
public async Task<bool> Handle(Employee_RenewRuzhiCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
if (entity.WorkStatus != Employee_WorkStatus_Enum.离职) throw new F9OperationException("请选择离职人员");
if (command.DepId <= 0) throw new F9OperationException("请选择部门");
var oldDepId = entity.DepId;
var oldWorktypeId = entity.WorktypeId;
entity.RenewRuzhi(command.Date, command.DepId, command.WorkTypeId, command.GangweiName, command.Sc_Banci);
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("重新入职", "操作");
await readRepository.AddTiaogangRecordAsync(entity.Id, oldDepId, oldWorktypeId, command.DepId,
command.WorkTypeId, command.Date);
var depName = await readRepository.GetDepNameAsync(command.DepId);
var worktypeName = await readRepository.GetWorktypeNameAsync(command.WorkTypeId);
await readRepository.AddChangeRecordAsync(new EmployeeChangeRecordDto(0, entity.Id, command.Date,
Employee_ChangeStatus_Enum.再入职, "", $"{depName},{worktypeName}", "再入职"));
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
/// <summary>修改生产信息(工作组 / 顺序号)</summary>
public record Employee_UpdateScCommand(long Id, string Sc_WorkGroup, int Sc_Sort) : IRequest<bool>;
public class UpdateScCommandHandler(
IEmployeeWriteRepository repository,
IPublisher eventBus) : IRequestHandler<Employee_UpdateScCommand, bool>
{
public async Task<bool> Handle(Employee_UpdateScCommand command, CancellationToken cancellationToken)
{
var entity = await repository.GetByIdAsync(command.Id, cancellationToken)
?? throw new F9NotFoundException("职工不存在");
entity.UpdateSc(command.Sc_WorkGroup, command.Sc_Sort);
var result = await repository.UpdateAsync(entity);
if (result == false) throw new F9OperationException("生产信息", "修改");
foreach (var de in entity.DomainEvents) await eventBus.Publish(de, cancellationToken);
entity.ClearDomainEvents();
return true;
}
}
@@ -0,0 +1,50 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.SYS.Employees;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Employees;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Employees.Queries;
/// <summary>职工分页查询(部门含子部门 + 工种 + 在职状态 + 班次 + 关键字)</summary>
public record GetEmployeePagedQuery(long DepId = 0, long WorktypeId = 0, Employee_WorkStatus_Enum? WorkStatus = null,
Prod_Banci_Enum? Banci = null, string Like = "", int PageIndex = 1, int PageSize = 20)
: IRequest<PageResult<EmployeeReadDto_View>>;
public class GetEmployeePagedQueryHandler(IEmployeeReadRepository readRepository)
: IRequestHandler<GetEmployeePagedQuery, PageResult<EmployeeReadDto_View>>
{
public async Task<PageResult<EmployeeReadDto_View>> Handle(GetEmployeePagedQuery query, CancellationToken ct)
=> await readRepository.GetPagedAsync(query.DepId, query.WorktypeId, query.WorkStatus, query.Banci,
query.Like, query.PageIndex, query.PageSize);
}
/// <summary>按 Id 查询职工(编辑回显)</summary>
public record GetEmployeeByIdQuery(long Id) : IRequest<EmployeeReadDto_Edit>;
public class GetEmployeeByIdQueryHandler(IEmployeeReadRepository readRepository)
: IRequestHandler<GetEmployeeByIdQuery, EmployeeReadDto_Edit>
{
public async Task<EmployeeReadDto_Edit> Handle(GetEmployeeByIdQuery query, CancellationToken ct)
=> await readRepository.GetByIdForEditAsync(query.Id);
}
/// <summary>职工变更记录</summary>
public record GetEmployeeChangeRecordsQuery(long EmployeeId) : IRequest<List<EmployeeChangeRecordDto>>;
public class GetEmployeeChangeRecordsQueryHandler(IEmployeeReadRepository readRepository)
: IRequestHandler<GetEmployeeChangeRecordsQuery, List<EmployeeChangeRecordDto>>
{
public async Task<List<EmployeeChangeRecordDto>> Handle(GetEmployeeChangeRecordsQuery query, CancellationToken ct)
=> await readRepository.GetChangeRecordsAsync(query.EmployeeId);
}
/// <summary>职工调岗记录</summary>
public record GetEmployeeTiaogangRecordsQuery(long EmployeeId) : IRequest<List<EmployeeTiaogangRecordDto>>;
public class GetEmployeeTiaogangRecordsQueryHandler(IEmployeeReadRepository readRepository)
: IRequestHandler<GetEmployeeTiaogangRecordsQuery, List<EmployeeTiaogangRecordDto>>
{
public async Task<List<EmployeeTiaogangRecordDto>> Handle(GetEmployeeTiaogangRecordsQuery query, CancellationToken ct)
=> await readRepository.GetTiaogangRecordsAsync(query.EmployeeId);
}
@@ -0,0 +1,110 @@
using F9WebAiTest.Domain.Enums.SYS.Employees;
namespace F9WebAiTest.Application.Dtos.SYS.Employees;
/// <summary>职工列表读模型</summary>
public record EmployeeReadDto_View(
long Id,
long DepId,
string DepName,
long WorktypeId,
string WorktypeName,
Prod_Banci_Enum? Banci,
string BanciName,
string Gangwei,
Employee_WorkStatus_Enum WorkStatus,
bool IsZhuanzheng,
string Code,
string Name,
string Gender,
int Nianling,
string IdCard,
string Phone,
DateTime? BirthDate,
DateTime? RuzhiDate,
DateTime? ZhuanzhengDate,
DateTime? LizhiDate,
string LizhiYuanyin,
string WorkGroup,
int Sort,
string BankcardCode,
string HetongType,
DateTime? HetongStartDate,
DateTime? HetongEndDate,
DateTime? TuixiuDate,
DateTime? ShebaoDate,
string Minzu,
string HunyinStatus,
string Xueli,
string Huji,
string Address,
string JinjiLianxiren,
string JinjiLianxirenPhone,
string BiyeYuanxiao,
string Zhuanye,
string NamePinyin,
string Memo);
/// <summary>职工编辑读模型</summary>
public record EmployeeReadDto_Edit(
long Id,
long DepId,
long WorktypeId,
Employee_WorkStatus_Enum WorkStatus,
Prod_Banci_Enum? Banci,
string Code,
string Name,
string Gangwei,
string Phone,
string IdCard,
DateTime? BirthDate,
string Address,
string Gender,
string Minzu,
string HunyinStatus,
string Huji,
DateTime? RuzhiDate,
DateTime? LizhiDate,
DateTime? ZhuanzhengDate,
bool IsZhuanzheng,
string JinjiLianxiren,
string JinjiLianxirenPhone,
string HetongType,
DateTime? HetongStartDate,
DateTime? HetongEndDate,
DateTime? TuixiuDate,
DateTime? ShebaoDate,
string ShebaoDesc,
string BankcardCode,
string BiyeYuanxiao,
string Zhuanye,
string Xueli,
string WorkGroup,
int Sort,
string NamePinyin,
string Memo,
string LizhiYuanyin);
/// <summary>职工变更记录读模型</summary>
public record EmployeeChangeRecordDto(
long Id,
long EmployeeId,
DateTime Date,
Employee_ChangeStatus_Enum ChangeStatus,
string ChangeStatusName,
string ChangeShuoming,
string ChangeYuanyin);
/// <summary>职工调岗记录读模型</summary>
public record EmployeeTiaogangRecordDto(
long Id,
long EmployeeId,
long OldDepId,
string OldDepName,
long OldWorktypeId,
string OldWorktypeName,
long NewDepId,
string NewDepName,
long NewWorktypeId,
string NewWorktypeName,
DateTime TiaogangDate);
@@ -8,7 +8,8 @@ public record WorkTypeReadDto_View(
string BizType,
string SccbFenlei,
bool IsActive,
string DepartmentNames);
string DepartmentNames,
List<long> DepartmentIds);
/// <summary>工种编辑读模型</summary>
public record WorkTypeReadDto_Edit(
@@ -0,0 +1,35 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.SYS.Employees;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using F9WebAiTest.Domain.Interfaces;
namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.Employees;
/// <summary>职工读仓储(返回读模型 DTO)</summary>
public interface IEmployeeReadRepository : IDInterface
{
/// <summary>分页查询(部门含子部门;workStatus/banci 为 null 表示全部)</summary>
Task<PageResult<EmployeeReadDto_View>> GetPagedAsync(long depId, long worktypeId,
Employee_WorkStatus_Enum? workStatus, Prod_Banci_Enum? banci, string keyword, int pageIndex, int pageSize);
Task<EmployeeReadDto_Edit> GetByIdForEditAsync(long id);
/// <summary>变更记录(按日期倒序)</summary>
Task<List<EmployeeChangeRecordDto>> GetChangeRecordsAsync(long employeeId);
/// <summary>调岗记录(按日期倒序)</summary>
Task<List<EmployeeTiaogangRecordDto>> GetTiaogangRecordsAsync(long employeeId);
/// <summary>写变更记录</summary>
Task<bool> AddChangeRecordAsync(EmployeeChangeRecordDto record);
/// <summary>写调岗记录</summary>
Task<bool> AddTiaogangRecordAsync(long employeeId, long oldDepId, long oldWorktypeId, long newDepId,
long newWorktypeId, DateTime date);
/// <summary>部门名称(用于变更记录说明)</summary>
Task<string> GetDepNameAsync(long depId);
/// <summary>工种名称(用于变更记录说明)</summary>
Task<string> GetWorktypeNameAsync(long worktypeId);
}
@@ -0,0 +1,275 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using F9WebAiTest.Domain.Events.SYS.Employees;
namespace F9WebAiTest.Domain.Entities.SYS.Employees;
/// <summary>
/// 职工档案:部门/工种/岗位 + 基本信息 + 合同社保 + 教育经历 + 生产信息(班次/工作组/顺序号)。
/// 转正、离职、调岗、重新入职等通过行为方法变更,并单独记录变更/调岗历史。
/// </summary>
[Table(Name = "sys_employee")]
public class Employee : AggregateRoot
{
/// <summary>所属部门 Id</summary>
[Column(Name = "dep_id")] public long DepId { get; private set; }
/// <summary>工种 Id</summary>
[Column(Name = "worktype_id")] public long WorktypeId { get; private set; }
/// <summary>在职状态</summary>
[Column(Name = "work_status")] public Employee_WorkStatus_Enum WorkStatus { get; private set; }
/// <summary>工号</summary>
[Column(Name = "code")] public string Code { get; private set; }
/// <summary>姓名</summary>
[Column(Name = "name")] public string Name { get; private set; }
/// <summary>岗位</summary>
[Column(Name = "gangwei")] public string Gangwei { get; private set; }
/// <summary>电话</summary>
[Column(Name = "phone")] public string Phone { get; private set; }
/// <summary>身份证号</summary>
[Column(Name = "idcard")] public string IdCard { get; private set; }
/// <summary>出生日期</summary>
[Column(Name = "birth_date")] public DateTime? BirthDate { get; private set; }
/// <summary>住址</summary>
[Column(Name = "address")] public string Address { get; private set; }
/// <summary>性别</summary>
[Column(Name = "gender")] public string Gender { get; private set; }
/// <summary>民族</summary>
[Column(Name = "minzu")] public string Minzu { get; private set; }
/// <summary>婚姻状态</summary>
[Column(Name = "hunyin_status")] public string HunyinStatus { get; private set; }
/// <summary>户籍</summary>
[Column(Name = "huji")] public string Huji { get; private set; }
/// <summary>入职日期</summary>
[Column(Name = "ruzhi_date")] public DateTime? RuzhiDate { get; private set; }
/// <summary>离职日期</summary>
[Column(Name = "lizhi_date")] public DateTime? LizhiDate { get; private set; }
/// <summary>转正日期</summary>
[Column(Name = "zhuanzheng_date")] public DateTime? ZhuanzhengDate { get; private set; }
/// <summary>是否已转正</summary>
[Column(Name = "is_zhuanzheng")] public bool IsZhuanzheng { get; private set; }
/// <summary>紧急联系人</summary>
[Column(Name = "jinji_lianxiren")] public string JinjiLianxiren { get; private set; }
/// <summary>紧急联系人电话</summary>
[Column(Name = "jinji_lianxiren_phone")] public string JinjiLianxirenPhone { get; private set; }
/// <summary>合同类型</summary>
[Column(Name = "hetong_type")] public string HetongType { get; private set; }
/// <summary>劳动合同开始日期</summary>
[Column(Name = "hetong_start_date")] public DateTime? HetongStartDate { get; private set; }
/// <summary>劳动合同结束日期</summary>
[Column(Name = "hetong_end_date")] public DateTime? HetongEndDate { get; private set; }
/// <summary>退休日期</summary>
[Column(Name = "tuixiu_date")] public DateTime? TuixiuDate { get; private set; }
/// <summary>社保参保日期</summary>
[Column(Name = "shebao_date")] public DateTime? ShebaoDate { get; private set; }
/// <summary>社保参保情况说明</summary>
[Column(Name = "shebao_desc")] public string ShebaoDesc { get; private set; }
/// <summary>银行卡号</summary>
[Column(Name = "bankcard_code")] public string BankcardCode { get; private set; }
/// <summary>毕业院校</summary>
[Column(Name = "biye_yuanxiao")] public string BiyeYuanxiao { get; private set; }
/// <summary>专业</summary>
[Column(Name = "zhuanye")] public string Zhuanye { get; private set; }
/// <summary>学历</summary>
[Column(Name = "xueli")] public string Xueli { get; private set; }
/// <summary>班次(甲/乙)</summary>
[Column(Name = "banci")] public Prod_Banci_Enum? Banci { get; private set; }
/// <summary>工作组</summary>
[Column(Name = "work_group")] public string WorkGroup { get; private set; }
/// <summary>顺序号</summary>
[Column(Name = "sort")] public int Sort { get; private set; }
/// <summary>姓名拼音(简拼)</summary>
[Column(Name = "name_pinyin")] public string NamePinyin { get; private set; }
/// <summary>备注</summary>
[Column(Name = "memo")] public string Memo { get; private set; }
/// <summary>离职原因</summary>
[Column(Name = "lizhi_yuanyin")] public string LizhiYuanyin { get; private set; }
public Employee() { }
public Employee(long id, long depId, long worktypeId, Employee_WorkStatus_Enum workStatus, string code, string name,
string gangwei, string phone, string idCard, DateTime? birthDate, string address, string gender, string minzu,
string hunyinStatus, string huji, DateTime? ruzhiDate, DateTime? lizhiDate, DateTime? zhuanzhengDate,
bool isZhuanzheng, string jinjiLianxiren, string jinjiLianxirenPhone, string hetongType,
DateTime? hetongStartDate, DateTime? hetongEndDate, DateTime? tuixiuDate, DateTime? shebaoDate, string shebaoDesc,
string bankcardCode, string biyeYuanxiao, string zhuanye, string xueli, Prod_Banci_Enum? banci, string workGroup,
int sort, string namePinyin, string memo, string lizhiYuanyin)
{
Id = id;
DepId = depId;
WorktypeId = worktypeId;
WorkStatus = workStatus;
Code = code;
Name = name;
Gangwei = gangwei;
Phone = phone;
IdCard = idCard;
BirthDate = birthDate;
Address = address;
Gender = gender;
Minzu = minzu;
HunyinStatus = hunyinStatus;
Huji = huji;
RuzhiDate = ruzhiDate;
LizhiDate = lizhiDate;
ZhuanzhengDate = zhuanzhengDate;
IsZhuanzheng = isZhuanzheng;
JinjiLianxiren = jinjiLianxiren;
JinjiLianxirenPhone = jinjiLianxirenPhone;
HetongType = hetongType;
HetongStartDate = hetongStartDate;
HetongEndDate = hetongEndDate;
TuixiuDate = tuixiuDate;
ShebaoDate = shebaoDate;
ShebaoDesc = shebaoDesc;
BankcardCode = bankcardCode;
BiyeYuanxiao = biyeYuanxiao;
Zhuanye = zhuanye;
Xueli = xueli;
Banci = banci;
WorkGroup = workGroup;
Sort = sort;
NamePinyin = namePinyin;
Memo = memo;
LizhiYuanyin = lizhiYuanyin;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>修改基本信息(部门/工种/班次通过调岗变更)</summary>
public void Update(string code, string name, string gangwei, string phone, string idCard, DateTime? birthDate,
string address, string gender, string minzu, string hunyinStatus, string huji, DateTime? ruzhiDate,
string jinjiLianxiren, string jinjiLianxirenPhone, string hetongType, DateTime? hetongStartDate,
DateTime? hetongEndDate, DateTime? tuixiuDate, DateTime? shebaoDate, string shebaoDesc, string bankcardCode,
string biyeYuanxiao, string zhuanye, string xueli, string namePinyin, string memo)
{
Code = code;
Name = name;
Gangwei = gangwei;
Phone = phone;
IdCard = idCard;
BirthDate = birthDate;
Address = address;
Gender = gender;
Minzu = minzu;
HunyinStatus = hunyinStatus;
Huji = huji;
RuzhiDate = ruzhiDate;
JinjiLianxiren = jinjiLianxiren;
JinjiLianxirenPhone = jinjiLianxirenPhone;
HetongType = hetongType;
HetongStartDate = hetongStartDate;
HetongEndDate = hetongEndDate;
TuixiuDate = tuixiuDate;
ShebaoDate = shebaoDate;
ShebaoDesc = shebaoDesc;
BankcardCode = bankcardCode;
BiyeYuanxiao = biyeYuanxiao;
Zhuanye = zhuanye;
Xueli = xueli;
NamePinyin = namePinyin;
Memo = memo;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>转正</summary>
public void Zhuanzheng(DateTime date)
{
IsZhuanzheng = true;
ZhuanzhengDate = date.Date;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>取消转正</summary>
public void ZhuanzhengCancel()
{
IsZhuanzheng = false;
ZhuanzhengDate = null;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>离职</summary>
public void Lizhi(DateTime date, string yuanyin)
{
WorkStatus = Employee_WorkStatus_Enum.离职;
LizhiDate = date.Date;
LizhiYuanyin = yuanyin ?? string.Empty;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>取消离职</summary>
public void LizhiCancel()
{
WorkStatus = Employee_WorkStatus_Enum.在职;
LizhiDate = null;
LizhiYuanyin = string.Empty;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>调岗</summary>
public void Tiaogang(long depId, long worktypeId, string gangwei, Prod_Banci_Enum? banci)
{
DepId = depId;
WorktypeId = worktypeId;
if (!string.IsNullOrWhiteSpace(gangwei)) Gangwei = gangwei;
if (banci.HasValue) Banci = banci;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>重新入职(离职人员按新部门/工种入职)</summary>
public void RenewRuzhi(DateTime date, long depId, long worktypeId, string gangwei, Prod_Banci_Enum? banci)
{
WorkStatus = Employee_WorkStatus_Enum.在职;
DepId = depId;
WorktypeId = worktypeId;
RuzhiDate = date.Date;
LizhiDate = null;
LizhiYuanyin = string.Empty;
if (!string.IsNullOrWhiteSpace(gangwei)) Gangwei = gangwei;
if (banci.HasValue) Banci = banci;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
/// <summary>修改生产信息(工作组 / 顺序号)</summary>
public void UpdateSc(string workGroup, int sort)
{
WorkGroup = workGroup ?? string.Empty;
Sort = sort;
AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
public void Deleted() => AddDomainEvent(new Employee_ChangedDomainEvent(Id));
}
@@ -0,0 +1,36 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Enums.SYS.Employees;
namespace F9WebAiTest.Domain.Entities.SYS.Employees;
/// <summary>职工变更记录(入职/调岗/转正/离职/再入职)</summary>
[Table(Name = "sys_employee_change_record")]
public class EmployeeChangeRecord : BusinessEntity
{
[Column(Name = "employee_id")] public long EmployeeId { get; private set; }
/// <summary>变更日期</summary>
[Column(Name = "date")] public DateTime Date { get; private set; }
/// <summary>变更类型</summary>
[Column(Name = "change_status")] public Employee_ChangeStatus_Enum ChangeStatus { get; private set; }
/// <summary>变更说明(部门、岗位等)</summary>
[Column(Name = "change_shuoming")] public string ChangeShuoming { get; private set; }
/// <summary>变更原因</summary>
[Column(Name = "change_yuanyin")] public string ChangeYuanyin { get; private set; }
public EmployeeChangeRecord() { }
public EmployeeChangeRecord(long id, long employeeId, DateTime date, Employee_ChangeStatus_Enum changeStatus,
string changeShuoming, string changeYuanyin)
{
Id = id;
EmployeeId = employeeId;
Date = date;
ChangeStatus = changeStatus;
ChangeShuoming = changeShuoming;
ChangeYuanyin = changeYuanyin;
}
}
@@ -0,0 +1,35 @@
using FreeSql.DataAnnotations;
namespace F9WebAiTest.Domain.Entities.SYS.Employees;
/// <summary>职工调岗记录(记录调岗前后的部门与工种)</summary>
[Table(Name = "sys_employee_tiaogang_record")]
public class EmployeeTiaogangRecord : BusinessEntity
{
[Column(Name = "employee_id")] public long EmployeeId { get; private set; }
[Column(Name = "old_depid")] public long OldDepId { get; private set; }
[Column(Name = "old_worktypeid")] public long OldWorktypeId { get; private set; }
[Column(Name = "new_depid")] public long NewDepId { get; private set; }
[Column(Name = "new_worktypeid")] public long NewWorktypeId { get; private set; }
/// <summary>调岗日期</summary>
[Column(Name = "tiaogang_date")] public DateTime TiaogangDate { get; private set; }
public EmployeeTiaogangRecord() { }
public EmployeeTiaogangRecord(long id, long employeeId, long oldDepId, long oldWorktypeId, long newDepId,
long newWorktypeId, DateTime tiaogangDate)
{
Id = id;
EmployeeId = employeeId;
OldDepId = oldDepId;
OldWorktypeId = oldWorktypeId;
NewDepId = newDepId;
NewWorktypeId = newWorktypeId;
TiaogangDate = tiaogangDate;
}
}
@@ -0,0 +1,25 @@
namespace F9WebAiTest.Domain.Enums.SYS.Employees;
/// <summary>职工在职状态(与 F9 的 Employee_WorkStatus_Enum 一致)</summary>
public enum Employee_WorkStatus_Enum
{
在职 = 0,
离职 = 1
}
/// <summary>生产班次(与 F9 的 Prod_Banci_Enum 一致)</summary>
public enum Prod_Banci_Enum
{
甲 = 1,
乙 = 2
}
/// <summary>职工变更记录类型(与 F9 的 Employee_ChangeStatus_Enum 一致)</summary>
public enum Employee_ChangeStatus_Enum
{
新入职 = 1,
调岗 = 2,
离职 = 3,
再入职 = 4,
转正 = 5
}
@@ -0,0 +1,5 @@
using F9WebAiTest.Domain.Entities;
namespace F9WebAiTest.Domain.Events.SYS.Employees;
public record Employee_ChangedDomainEvent(long employeeId) : DomainEvent;
@@ -0,0 +1,12 @@
using F9WebAiTest.Domain.Entities.SYS.Employees;
namespace F9WebAiTest.Domain.Interfaces.SYS.Employees;
/// <summary>职工写仓储(聚合根仓储)</summary>
public interface IEmployeeWriteRepository : IDInterface
{
Task<Employee> GetByIdAsync(long id, CancellationToken cancellationToken);
Task<bool> AddAsync(Employee entity);
Task<bool> UpdateAsync(Employee entity);
Task<bool> SoftDeleteAsync(long id);
}
@@ -227,15 +227,21 @@ public static class BusinessSeedData
AddMenu(fsql, baseDirId, "生丝定级指标", "/base/dingjizhibiao", "base/dingjizhibiao/index", "TrendCharts", "base:dingjizhibiao:list", 3);
AddMenu(fsql, baseDirId, "机组车台", "/base/jizuchetai", "base/jizuchetai/index", "SetUp", "base:jizuchetai:list", 4);
// 生产管理目录
var prodDirId = fsql.Select<Menu>().Where(m => m.Path == "/prod").First()?.Id ?? 0;
if (prodDirId == 0)
// 生产模块按工序拆分为两个一级目录:生产-前缫(落丝)/ 生产-后缫(纤度、黑板检验)
var qianxianDirId = fsql.Select<Menu>().Where(m => m.Path == "/prod/qianxian").First()?.Id ?? 0;
if (qianxianDirId == 0)
{
prodDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "生产管理", Type = "directory", Path = "/prod", Icon = "Cpu", Sort = 4 });
qianxianDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "生产-前缫", Type = "directory", Path = "/prod/qianxian", Icon = "Cpu", Sort = 5 });
}
AddMenu(fsql, prodDirId, "落丝记录", "/prod/lsjl", "prod/lsjl/index", "Document", "prod:lsjl:list", 1);
AddMenu(fsql, prodDirId, "纤度检验", "/prod/xdjy", "prod/xdjy/index", "EditPen", "prod:xdjy:list", 2);
AddMenu(fsql, prodDirId, "黑板检验", "/prod/hbjy", "prod/hbjy/index", "Notebook", "prod:hbjy:list", 3);
AddMenu(fsql, qianxianDirId, "落丝记录", "/prod/qianxian/lsjl", "prod/lsjl/index", "Document", "prod:lsjl:list", 1);
var houxianDirId = fsql.Select<Menu>().Where(m => m.Path == "/prod/houxian").First()?.Id ?? 0;
if (houxianDirId == 0)
{
houxianDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "生产-后缫", Type = "directory", Path = "/prod/houxian", Icon = "DataLine", Sort = 6 });
}
AddMenu(fsql, houxianDirId, "纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "EditPen", "prod:xdjy:list", 1);
AddMenu(fsql, houxianDirId, "黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "Notebook", "prod:hbjy:list", 2);
var adminRole = fsql.Select<Role>().Where(r => r.Code == "admin").First();
if (adminRole != null)
@@ -60,12 +60,13 @@ public static class DbSeeder
// 6.6 F9 风格 RBAC 菜单(用户/角色/菜单/部门管理,写入新 sys_rbac_menus 表)
SeedSysRbacMenus(fsql, snowflake);
// 6.65 基础数据(工种 + 注册表配置 + 原料庄口 + 物资 + 往来单位,F9 导出数据幂等导入)
// 6.65 基础数据(工种 + 注册表配置 + 原料庄口 + 物资 + 往来单位 + 职工,F9 导出数据幂等导入)
WorkTypeSeedData.Seed(fsql, snowflake);
RegistrySeedData.Seed(fsql);
YuanliaoZhuangkouSeedData.Seed(fsql);
WuziSeedData.Seed(fsql);
GongyingshangSeedData.Seed(fsql);
EmployeeSeedData.Seed(fsql);
// 6.66 基础数据菜单(工种管理、账期管理、注册表、原料庄口、物资目录、往来单位)
SeedWorkTypeMenus(fsql, snowflake);
@@ -75,6 +76,12 @@ public static class DbSeeder
SeedWuziMenus(fsql, snowflake);
SeedGongyingshangMenus(fsql, snowflake);
// 6.67 职工管理(一级菜单,与"基础数据"平级)
SeedEmployeeTopMenu(fsql, snowflake);
// 6.68 生产模块(按工序拆分:生产-前缫 / 生产-后缫)
SeedProdMenus(fsql, snowflake);
// 6.7 为菜单创建权限单元(Permission),并修正旧菜单权限关联(统一权限模型)
FixMenuPermissions(fsql, snowflake);
@@ -175,7 +182,7 @@ public static class DbSeeder
{
Code = "TeyeFound", Name = "发现特野", Description = "纤度检验发现特野时自动推送提醒",
RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 1,
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/xdjy"
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/houxian/xdjy"
}).ExecuteAffrows();
}
@@ -186,7 +193,7 @@ public static class DbSeeder
{
Code = "TdcFound", Name = "发现特大糙", Description = "黑板检验发现特大糙时自动推送提醒",
RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 2,
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/hbjy"
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/houxian/hbjy"
}).ExecuteAffrows();
}
@@ -572,6 +579,203 @@ public static class DbSeeder
}
}
/// <summary>
/// 职工管理菜单:独立的一级菜单(原作为"基础数据"的子菜单)。
/// 会清理历史遗留的 /base/employee 子菜单,并规范一级菜单排序,避免同一页面出现两个入口。
/// </summary>
private static void SeedEmployeeTopMenu(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
const string menuPath = "/employee";
const string menuIcon = "Avatar";
// 1) 清理历史:曾是"基础数据"下的子菜单
var legacy = fsql.Select<SysMenu>().Where(m => m.Path == "/base/employee").First();
if (legacy != null)
{
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacy.Id}");
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacy.Id}");
Console.WriteLine("[DbSeeder] 已清理历史子菜单 /base/employee");
}
// 2) 创建/校正一级菜单
var menu = fsql.Select<SysMenu>().Where(m => m.Path == menuPath).First();
if (menu == null)
{
menu = new SysMenu(snowflake.CreateId(), "职工管理", menuIcon, MenuType_Enum.页面, null, null, 0, 4, 0);
menu.SetWebInfo(menuPath, "base/employee/index", "base:employee:list");
fsql.Insert(menu).ExecuteAffrows();
}
else if (!menu.IsActive || menu.IconSvg != menuIcon || menu.ParentId != 0)
{
fsql.Update<SysMenu>()
.Set(x => x.IconSvg, menuIcon)
.Set(x => x.IsActive, true)
.Set(x => x.ParentId, 0)
.Set(x => x.Level, 1)
.Where(x => x.Id == menu.Id)
.ExecuteAffrows();
}
// 3) 规范一级菜单顺序:考勤管理 / 基础数据 / 职工管理 / 生产管理 / 消息中心 / 系统管理
foreach (var (path, sort) in new (string Path, int Sort)[]
{
("/attendance", 2), ("/base", 3), ("/employee", 4),
("/prod", 5), ("/notification", 6), ("/sys", 7)
})
{
fsql.Update<SysMenu>()
.Set(x => x.SortOrder, sort)
.Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort)
.ExecuteAffrows();
}
// 4) admin 角色授权(后续 FixMenuPermissions 会把 permission_id 修正为权限单元 Id)
var adminRole = fsql.Select<SysRole>().Where(r => r.Code == "admin").First();
if (adminRole != null)
{
var any = fsql.Select<SysRolePermission>()
.Where(x => x.RoleId == adminRole.Id && x.PermissionId == menu.Id)
.First();
if (any == null)
{
fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, menu.Id)).ExecuteAffrows();
}
}
Console.WriteLine("[DbSeeder] 职工管理一级菜单已就绪");
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 职工管理菜单种子跳过:{ex}");
}
}
/// <summary>
/// 生产模块菜单:按缫丝工序拆分为「生产-前缫」(落丝记录)与「生产-后缫」(纤度检验、黑板检验)两个一级菜单。
/// 会清理旧的「生产管理」(/prod) 目录及其子菜单;页面组件与权限标识保持不变。
/// </summary>
private static void SeedProdMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
// 1) 清理旧「生产管理」结构(精确匹配历史路径)
foreach (var legacyPath in new[] { "/prod", "/prod/lsjl", "/prod/xdjy", "/prod/hbjy" })
{
var legacy = fsql.Select<SysMenu>().Where(m => m.Path == legacyPath).First();
if (legacy == null) continue;
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacy.Id}");
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacy.Id}");
Console.WriteLine($"[DbSeeder] 已清理旧生产菜单 {legacyPath}");
}
// 2) 两个一级目录 + 各自页面菜单
EnsureTopMenuWithChildren(fsql, snowflake, "/prod/qianxian", "生产-前缫", "Cpu", 5,
new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[]
{
("落丝记录", "/prod/qianxian/lsjl", "prod/lsjl/index", "prod:lsjl:list", "Document", 1)
});
EnsureTopMenuWithChildren(fsql, snowflake, "/prod/houxian", "生产-后缫", "DataLine", 6,
new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[]
{
("纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "prod:xdjy:list", "EditPen", 1),
("黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "prod:hbjy:list", "Notebook", 2)
});
// 3) 规范一级菜单顺序
foreach (var (path, sort) in new (string Path, int Sort)[]
{
("/attendance", 2), ("/base", 3), ("/employee", 4),
("/prod/qianxian", 5), ("/prod/houxian", 6), ("/notification", 7), ("/sys", 8)
})
{
fsql.Update<SysMenu>()
.Set(x => x.SortOrder, sort)
.Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort)
.ExecuteAffrows();
}
// 4) 消息跳转路径同步到新路由
fsql.Ado.ExecuteNonQuery("UPDATE NotificationTypes SET RouteTemplate = '/prod/houxian/xdjy' WHERE Code = 'TeyeFound'");
fsql.Ado.ExecuteNonQuery("UPDATE NotificationTypes SET RouteTemplate = '/prod/houxian/hbjy' WHERE Code = 'TdcFound'");
Console.WriteLine("[DbSeeder] 生产模块菜单已就绪(生产-前缫 / 生产-后缫)");
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 生产菜单种子跳过:{ex}");
}
}
/// <summary>
/// 确保一个一级目录菜单及其下属页面菜单存在(含父级、图标、启用状态校正),并给 admin 角色授权。
/// 注意:图标名必须是 Element Plus 图标组件名。
/// </summary>
private static void EnsureTopMenuWithChildren(IFreeSql fsql, ISnowflakeIdGenerator snowflake,
string dirPath, string dirName, string dirIcon, int dirSort,
(string Name, string Path, string Component, string Permission, string Icon, int Sort)[] children)
{
var dir = fsql.Select<SysMenu>().Where(m => m.Path == dirPath).First();
if (dir == null)
{
dir = new SysMenu(snowflake.CreateId(), dirName, dirIcon, MenuType_Enum.分组, null, null, 0, dirSort, 0);
dir.SetWebInfo(dirPath, "", "");
fsql.Insert(dir).ExecuteAffrows();
}
else if (!dir.IsActive || dir.IconSvg != dirIcon || dir.Name != dirName || dir.ParentId != 0)
{
fsql.Update<SysMenu>()
.Set(x => x.Name, dirName)
.Set(x => x.IconSvg, dirIcon)
.Set(x => x.IsActive, true)
.Set(x => x.ParentId, 0)
.Set(x => x.Level, 1)
.Where(x => x.Id == dir.Id)
.ExecuteAffrows();
}
var adminRoleId = fsql.Select<SysRole>().Where(r => r.Code == "admin").First()?.Id ?? 0;
EnsureMenuPermission(fsql, snowflake, adminRoleId, dir.Id);
foreach (var (name, path, component, permission, icon, sort) in children)
{
var child = fsql.Select<SysMenu>().Where(m => m.Path == path).First();
if (child == null)
{
child = new SysMenu(snowflake.CreateId(), name, icon, MenuType_Enum.页面, null, null, dir.Id, sort, 1);
child.SetWebInfo(path, component, permission);
fsql.Insert(child).ExecuteAffrows();
}
else if (!child.IsActive || child.IconSvg != icon || child.ParentId != dir.Id)
{
fsql.Update<SysMenu>()
.Set(x => x.IconSvg, icon)
.Set(x => x.IsActive, true)
.Set(x => x.ParentId, dir.Id)
.Set(x => x.Level, 2)
.Where(x => x.Id == child.Id)
.ExecuteAffrows();
}
EnsureMenuPermission(fsql, snowflake, adminRoleId, child.Id);
}
}
/// <summary>确保指定菜单已授权给角色(后续 FixMenuPermissions 会把 permission_id 修正为权限单元 Id)</summary>
private static void EnsureMenuPermission(IFreeSql fsql, ISnowflakeIdGenerator snowflake, long roleId, long menuId)
{
if (roleId <= 0 || menuId <= 0) return;
var any = fsql.Select<SysRolePermission>()
.Where(x => x.RoleId == roleId && x.PermissionId == menuId)
.First();
if (any == null)
{
fsql.Insert(new SysRolePermission(snowflake.CreateId(), roleId, menuId)).ExecuteAffrows();
}
}
/// <summary>
/// 确保"基础数据"目录下的菜单存在(含启用状态与图标校正),并给 admin 角色授权。
/// 注意:图标名必须是 Element Plus 图标组件名,前端按图标名渲染菜单图标。
@@ -0,0 +1,280 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Entities.SYS.Employees;
using F9WebAiTest.Domain.Enums.SYS.Employees;
namespace F9WebAiTest.Infrastructure.Data;
/// <summary>
/// 职工数据导入:来源于 F9 系统 biz_base_employee / biz_base_employee_Change_record /
/// biz_base_employee_tiaogang_record 三张表的导出数据。
/// 幂等:按原始 Id 判重;F9 部门 Id 通过"部门名称"映射到本系统部门(工种 Id 与 F9 一致,直接沿用)。
/// </summary>
public static class EmployeeSeedData
{
private const string EmployeeResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_base_employee.json";
private const string ChangeResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_base_employee_Change_record.json";
private const string TiaogangResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_base_employee_tiaogang_record.json";
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
public static void Seed(IFreeSql fsql)
{
try
{
var depMap = BuildDepartmentIdMap(fsql);
SeedEmployees(fsql, depMap);
SeedChangeRecords(fsql);
SeedTiaogangRecords(fsql, depMap);
}
catch (Exception ex)
{
Console.WriteLine($"[EmployeeSeed] 职工数据导入跳过:{ex.Message}");
}
}
/// <summary>导入职工档案</summary>
private static void SeedEmployees(IFreeSql fsql, Dictionary<long, long> depMap)
{
var items = Load<EmployeeSeedItem>(EmployeeResource);
if (items.Count == 0) return;
var existIds = fsql.Select<Employee>().DisableGlobalFilter("软删除").ToList().Select(x => x.Id).ToHashSet();
var inserts = new List<Employee>();
foreach (var item in items)
{
if (item.Id <= 0 || existIds.Contains(item.Id)) continue;
if (string.IsNullOrWhiteSpace(item.BaseName)) continue;
var depId = depMap.TryGetValue(item.BaseDepId, out var mapped) ? mapped : item.BaseDepId;
inserts.Add(new Employee(item.Id, depId, item.BaseWorktypeId,
(Employee_WorkStatus_Enum)item.BaseWorkStatus, item.BaseCode ?? string.Empty, item.BaseName.Trim(),
item.BaseGangwei ?? string.Empty, (item.BasePhone ?? string.Empty).Trim(),
(item.BaseIdCard ?? string.Empty).Trim(), ParseLegacyDate(item.BaseBirnDate), item.BaseAddress ?? string.Empty,
(item.BaseGender ?? string.Empty).Trim(), (item.BaseMinzu ?? string.Empty).Trim(),
item.BaseHunyinStatus ?? string.Empty, item.BaseHuji ?? string.Empty, ParseLegacyDate(item.BaseRuzhiDate),
ParseLegacyDate(item.BaseLizhiDate), ParseLegacyDate(item.BaseZhuanzhengDate), ParseBool(item.BaseIsZhuanzheng),
item.BaseJinjiLianxiren ?? string.Empty, item.BaseJinjiLianxirenPhone ?? string.Empty,
item.BaseHetongType ?? string.Empty, ParseLegacyDate(item.BaseLaodongHetongStartDate),
ParseLegacyDate(item.BaseLaodongHetongEndDate), ParseLegacyDate(item.BaseTuixiuDate),
ParseLegacyDate(item.BaseShebaoCanbaoDate), item.BaseShebaocanbaoDesc ?? string.Empty,
(item.BaseBanckcardCode ?? string.Empty).Trim(), item.KzBiyeYuanxiao ?? string.Empty, item.KbZhuanye ?? string.Empty,
item.KbXueli ?? string.Empty, item.ScBanci.HasValue ? (Prod_Banci_Enum)item.ScBanci.Value : null,
item.ScWorkGroup ?? string.Empty, item.ScSort, item.NamePinyin ?? string.Empty, item.Memo ?? string.Empty,
item.BaseLizhiYuanyin ?? string.Empty)
{
CreatedAt = ParseLegacyDate(item.CreatedAt) ?? DateTime.Now,
CreatedBy = item.CreatedBy ?? string.Empty,
UpdatedAt = ParseLegacyDate(item.UpdatedAt),
UpdateBy = item.UpdateBy ?? string.Empty
});
}
foreach (var batch in inserts.Chunk(200)) fsql.Insert(batch.ToList()).ExecuteAffrows();
Console.WriteLine($"[EmployeeSeed] 职工档案:来源 {items.Count} 条,新增 {inserts.Count} 条");
}
/// <summary>导入变更记录</summary>
private static void SeedChangeRecords(IFreeSql fsql)
{
var items = Load<ChangeRecordSeedItem>(ChangeResource);
if (items.Count == 0) return;
var existIds = fsql.Select<EmployeeChangeRecord>().DisableGlobalFilter("软删除").ToList().Select(x => x.Id).ToHashSet();
var employeeIds = fsql.Select<Employee>().DisableGlobalFilter("软删除").ToList().Select(x => x.Id).ToHashSet();
var inserts = new List<EmployeeChangeRecord>();
var orphan = 0;
foreach (var item in items)
{
if (item.Id <= 0 || existIds.Contains(item.Id)) continue;
if (!employeeIds.Contains(item.EmployeeId)) { orphan++; continue; }
inserts.Add(new EmployeeChangeRecord(item.Id, item.EmployeeId, ParseLegacyDate(item.Date) ?? DateTime.Now,
(Employee_ChangeStatus_Enum)item.ChangeStatus, item.ChangeShuoming ?? string.Empty,
item.ChangeYuanyin ?? string.Empty)
{
CreatedAt = ParseLegacyDate(item.CreatedAt) ?? DateTime.Now,
CreatedBy = item.CreatedBy ?? string.Empty
});
}
foreach (var batch in inserts.Chunk(200)) fsql.Insert(batch.ToList()).ExecuteAffrows();
Console.WriteLine($"[EmployeeSeed] 变更记录:来源 {items.Count} 条,新增 {inserts.Count} 条,孤儿 {orphan} 条");
}
/// <summary>导入调岗记录</summary>
private static void SeedTiaogangRecords(IFreeSql fsql, Dictionary<long, long> depMap)
{
var items = Load<TiaogangRecordSeedItem>(TiaogangResource);
if (items.Count == 0) return;
var existIds = fsql.Select<EmployeeTiaogangRecord>().DisableGlobalFilter("软删除").ToList().Select(x => x.Id).ToHashSet();
var employeeIds = fsql.Select<Employee>().DisableGlobalFilter("软删除").ToList().Select(x => x.Id).ToHashSet();
var inserts = new List<EmployeeTiaogangRecord>();
var orphan = 0;
foreach (var item in items)
{
if (item.Id <= 0 || existIds.Contains(item.Id)) continue;
if (!employeeIds.Contains(item.EmployeeId)) { orphan++; continue; }
inserts.Add(new EmployeeTiaogangRecord(item.Id, item.EmployeeId,
depMap.TryGetValue(item.OldDepId, out var od) ? od : item.OldDepId, item.OldWorktypeId,
depMap.TryGetValue(item.NewDepId, out var nd) ? nd : item.NewDepId, item.NewWorktypeId,
ParseLegacyDate(item.TiaogangDate) ?? DateTime.Now)
{
CreatedAt = ParseLegacyDate(item.CreatedAt) ?? DateTime.Now,
CreatedBy = item.CreatedBy ?? string.Empty
});
}
if (inserts.Count > 0) fsql.Insert(inserts).ExecuteAffrows();
Console.WriteLine($"[EmployeeSeed] 调岗记录:来源 {items.Count} 条,新增 {inserts.Count} 条,孤儿 {orphan} 条");
}
/// <summary>F9 部门 Id → 本系统部门 Id(以部门名称为桥梁,同工种模块的处理方式)</summary>
private static Dictionary<long, long> BuildDepartmentIdMap(IFreeSql fsql)
{
var map = new Dictionary<long, long>();
var byName = new Dictionary<string, long>();
foreach (var dep in fsql.Select<Department>().ToList())
{
if (string.IsNullOrWhiteSpace(dep.Name)) continue;
if (!byName.ContainsKey(dep.Name)) byName[dep.Name] = dep.Id;
}
foreach (var (f9Id, name, _, _, _) in BusinessSeedData.DeptData)
{
if (byName.TryGetValue(name, out var localId)) map[f9Id] = localId;
}
return map;
}
private static List<T> Load<T>(string resourceName)
{
var assembly = typeof(EmployeeSeedData).Assembly;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
Console.WriteLine($"[EmployeeSeed] 未找到内嵌资源:{resourceName}");
return new List<T>();
}
return JsonSerializer.Deserialize<List<T>>(stream, JsonOptions) ?? new List<T>();
}
/// <summary>兼容 "0"/"1" 字符串、数字与布尔写法</summary>
private static bool ParseBool(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.String:
var text = element.GetString();
return text == "1" || string.Equals(text, "true", StringComparison.OrdinalIgnoreCase);
case JsonValueKind.Number:
return element.GetInt32() != 0;
case JsonValueKind.True:
return true;
default:
return false;
}
}
private static readonly string[] LegacyDateFormats =
{
"d/M/yyyy H:mm:ss.fff", "d/M/yyyy H:mm:ss", "d/M/yyyy H:mm", "d/M/yyyy",
"M/d/yyyy H:mm:ss.fff", "M/d/yyyy H:mm:ss", "M/d/yyyy H:mm", "M/d/yyyy",
"yyyy-MM-dd HH:mm:ss.fff", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd", "yyyy/M/d"
};
/// <summary>解析 F9 导出的日期,空值与 1970 占位值返回 null</summary>
private static DateTime? ParseLegacyDate(string value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
if (DateTime.TryParseExact(value, LegacyDateFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var exact))
return exact.Year <= 1900 ? null : exact;
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var any))
return any.Year <= 1900 ? null : any;
return null;
}
private sealed class EmployeeSeedItem
{
[JsonPropertyName("id")] public long Id { get; set; }
[JsonPropertyName("created_by")] public string CreatedBy { get; set; }
[JsonPropertyName("created_at")] public string CreatedAt { get; set; }
[JsonPropertyName("update_by")] public string UpdateBy { get; set; }
[JsonPropertyName("updated_at")] public string UpdatedAt { get; set; }
[JsonPropertyName("base_depid")] public long BaseDepId { get; set; }
[JsonPropertyName("base_worktypeid")] public long BaseWorktypeId { get; set; }
[JsonPropertyName("base_workstatus")] public int BaseWorkStatus { get; set; }
[JsonPropertyName("base_code")] public string BaseCode { get; set; }
[JsonPropertyName("base_name")] public string BaseName { get; set; }
[JsonPropertyName("base_gangwei")] public string BaseGangwei { get; set; }
[JsonPropertyName("base_phone")] public string BasePhone { get; set; }
[JsonPropertyName("base_idcard")] public string BaseIdCard { get; set; }
[JsonPropertyName("base_birndate")] public string BaseBirnDate { get; set; }
[JsonPropertyName("base_address")] public string BaseAddress { get; set; }
[JsonPropertyName("base_gender")] public string BaseGender { get; set; }
[JsonPropertyName("base_minzu")] public string BaseMinzu { get; set; }
[JsonPropertyName("base_hunyinstatus")] public string BaseHunyinStatus { get; set; }
[JsonPropertyName("base_huji")] public string BaseHuji { get; set; }
[JsonPropertyName("base_ruzhidate")] public string BaseRuzhiDate { get; set; }
[JsonPropertyName("base_lizhidate")] public string BaseLizhiDate { get; set; }
[JsonPropertyName("base_zhuanzhengdate")] public string BaseZhuanzhengDate { get; set; }
[JsonPropertyName("base_iszhuanzheng")] public JsonElement BaseIsZhuanzheng { get; set; }
[JsonPropertyName("base_jinjilianxiren")] public string BaseJinjiLianxiren { get; set; }
[JsonPropertyName("base_jinjilianxiren_phone")] public string BaseJinjiLianxirenPhone { get; set; }
[JsonPropertyName("base_hetongtype")] public string BaseHetongType { get; set; }
[JsonPropertyName("base_laodonghetong_startdate")] public string BaseLaodongHetongStartDate { get; set; }
[JsonPropertyName("base_laodonghetong_enddate")] public string BaseLaodongHetongEndDate { get; set; }
[JsonPropertyName("base_tuixiu_date")] public string BaseTuixiuDate { get; set; }
[JsonPropertyName("base_shebaocanbao_date")] public string BaseShebaoCanbaoDate { get; set; }
[JsonPropertyName("base_shebaocanbao_desc")] public string BaseShebaocanbaoDesc { get; set; }
[JsonPropertyName("base_banckcardcode")] public string BaseBanckcardCode { get; set; }
[JsonPropertyName("kz_biyeyuanxiao")] public string KzBiyeYuanxiao { get; set; }
[JsonPropertyName("kb_zhuanye")] public string KbZhuanye { get; set; }
[JsonPropertyName("kb_xueli")] public string KbXueli { get; set; }
[JsonPropertyName("sc_banci")] public int? ScBanci { get; set; }
[JsonPropertyName("sc_workgroup")] public string ScWorkGroup { get; set; }
[JsonPropertyName("sc_sort")] public int ScSort { get; set; }
[JsonPropertyName("name_pinyin")] public string NamePinyin { get; set; }
[JsonPropertyName("memo")] public string Memo { get; set; }
[JsonPropertyName("base_lizhiyuanyin")] public string BaseLizhiYuanyin { get; set; }
}
private sealed class ChangeRecordSeedItem
{
[JsonPropertyName("id")] public long Id { get; set; }
[JsonPropertyName("created_by")] public string CreatedBy { get; set; }
[JsonPropertyName("created_at")] public string CreatedAt { get; set; }
[JsonPropertyName("employee_id")] public long EmployeeId { get; set; }
[JsonPropertyName("date")] public string Date { get; set; }
[JsonPropertyName("change_status")] public int ChangeStatus { get; set; }
[JsonPropertyName("change_shuoming")] public string ChangeShuoming { get; set; }
[JsonPropertyName("change_yuanyin")] public string ChangeYuanyin { get; set; }
}
private sealed class TiaogangRecordSeedItem
{
[JsonPropertyName("id")] public long Id { get; set; }
[JsonPropertyName("created_by")] public string CreatedBy { get; set; }
[JsonPropertyName("created_at")] public string CreatedAt { get; set; }
[JsonPropertyName("employee_id")] public long EmployeeId { get; set; }
[JsonPropertyName("old_depid")] public long OldDepId { get; set; }
[JsonPropertyName("old_worktypeid")] public long OldWorktypeId { get; set; }
[JsonPropertyName("new_depid")] public long NewDepId { get; set; }
[JsonPropertyName("new_worktypeid")] public long NewWorktypeId { get; set; }
[JsonPropertyName("tiaogang_date")] public string TiaogangDate { get; set; }
}
}
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,7 @@ using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.WorkTypes;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Registries;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.YuanliaoZhuangkous;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Employees;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Gongyingshangs;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.WuziMulus;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Zq;
@@ -23,6 +24,7 @@ using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using F9WebAiTest.Domain.Interfaces.SYS.WorkTypes;
using F9WebAiTest.Domain.Interfaces.SYS.Registries;
using F9WebAiTest.Domain.Interfaces.SYS.YuanliaoZhuangkous;
using F9WebAiTest.Domain.Interfaces.SYS.Employees;
using F9WebAiTest.Domain.Interfaces.SYS.Gongyingshangs;
using F9WebAiTest.Domain.Interfaces.SYS.WuziMulus;
using F9WebAiTest.Domain.Interfaces.SYS.Zq;
@@ -35,6 +37,7 @@ using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.WorkTypes;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Registries;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.YuanliaoZhuangkous;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Employees;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Gongyingshangs;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.WuziMulus;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Zq;
@@ -97,6 +100,7 @@ public static class DependencyInjection
services.AddScoped<IYuanliaoZhuangkouWriteRepository, YuanliaoZhuangkouWriteRepository>();
services.AddScoped<IWuziGroupWriteRepository, WuziGroupWriteRepository>();
services.AddScoped<IGongyingshangWriteRepository, GongyingshangWriteRepository>();
services.AddScoped<IEmployeeWriteRepository, EmployeeWriteRepository>();
services.AddScoped<IWuziMuluWriteRepository, WuziMuluWriteRepository>();
// 读仓储(Application 接口)
@@ -110,6 +114,7 @@ public static class DependencyInjection
services.AddScoped<IYuanliaoZhuangkouReadRepository, YuanliaoZhuangkouReadRepository>();
services.AddScoped<IWuziGroupReadRepository, WuziGroupReadRepository>();
services.AddScoped<IGongyingshangReadRepository, GongyingshangReadRepository>();
services.AddScoped<IEmployeeReadRepository, EmployeeReadRepository>();
services.AddScoped<IWuziMuluReadRepository, WuziMuluReadRepository>();
return services;
@@ -0,0 +1,240 @@
using FreeSql;
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Application.Dtos.SYS.Employees;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Employees;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Entities.SYS.Employees;
using F9WebAiTest.Domain.Entities.SYS.WorkTypes;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using F9WebAiTest.Domain.Interfaces.SYS.Employees;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Employees;
internal class EmployeeWriteRepository : F9RepositoryBase, IEmployeeWriteRepository
{
public EmployeeWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<Employee> GetByIdAsync(long id, CancellationToken cancellationToken)
=> await Orm.Select<Employee>().Where(x => x.Id == id && x.DeletedAt == null).FirstAsync(cancellationToken);
public async Task<bool> AddAsync(Employee entity)
{
entity.CreatedAt = DateTime.Now;
return await Orm.Insert(entity).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> UpdateAsync(Employee entity)
{
entity.UpdatedAt = DateTime.Now;
return await Orm.Update<Employee>().SetSource(entity).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> SoftDeleteAsync(long id)
=> await Orm.Update<Employee>()
.Set(x => x.IsDeleted, true)
.Set(x => x.DeletedAt, DateTime.Now)
.Where(x => x.Id == id)
.ExecuteAffrowsAsync() > 0;
}
internal class EmployeeReadRepository : F9RepositoryBase, IEmployeeReadRepository
{
private readonly ISnowflakeIdGenerator _snowflakeIdGenerator;
public EmployeeReadRepository(IFreeSql fsql, ISnowflakeIdGenerator snowflakeIdGenerator) : base(fsql)
=> _snowflakeIdGenerator = snowflakeIdGenerator;
public async Task<PageResult<EmployeeReadDto_View>> GetPagedAsync(long depId, long worktypeId,
Employee_WorkStatus_Enum? workStatus, Prod_Banci_Enum? banci, string keyword, int pageIndex, int pageSize)
{
if (pageIndex <= 0) pageIndex = 1;
if (pageSize <= 0) pageSize = 20;
var query = Orm.Select<Employee>().Where(x => x.DeletedAt == null);
if (depId > 0)
{
var depIds = await GetDepIdsWithChildrenAsync(depId);
query = query.Where(x => depIds.Contains(x.DepId));
}
if (worktypeId > 0) query = query.Where(x => x.WorktypeId == worktypeId);
if (workStatus.HasValue) query = query.Where(x => x.WorkStatus == workStatus.Value);
if (banci.HasValue) query = query.Where(x => x.Banci == banci.Value);
if (!string.IsNullOrWhiteSpace(keyword))
{
var kw = keyword.Trim();
query = query.Where(x => x.Name.Contains(kw) || x.Code.Contains(kw) || x.Phone.Contains(kw)
|| x.IdCard.Contains(kw) || x.Gangwei.Contains(kw) || x.NamePinyin.Contains(kw));
}
var total = await query.CountAsync();
var items = await query
.OrderBy(x => x.DepId)
.OrderBy(x => x.Sort)
.OrderBy(x => x.Name)
.Page(pageIndex, pageSize)
.ToListAsync(x => new EmployeeReadDto_View(x.Id, x.DepId, "", x.WorktypeId, "", x.Banci, "", x.Gangwei,
x.WorkStatus, x.IsZhuanzheng, x.Code, x.Name, x.Gender, 0, x.IdCard, x.Phone, x.BirthDate, x.RuzhiDate,
x.ZhuanzhengDate, x.LizhiDate, x.LizhiYuanyin, x.WorkGroup, x.Sort, x.BankcardCode, x.HetongType,
x.HetongStartDate, x.HetongEndDate, x.TuixiuDate, x.ShebaoDate, x.Minzu, x.HunyinStatus, x.Xueli, x.Huji,
x.Address, x.JinjiLianxiren, x.JinjiLianxirenPhone, x.BiyeYuanxiao, x.Zhuanye, x.NamePinyin, x.Memo));
if (items.Count == 0)
{
return new PageResult<EmployeeReadDto_View> { Items = items, Total = total, PageIndex = pageIndex, PageSize = pageSize };
}
var depNameMap = await GetDepNameMapAsync();
var worktypeNameMap = await GetWorktypeNameMapAsync();
var list = items
.Select(x => x with
{
DepName = depNameMap.TryGetValue(x.DepId, out var dn) ? dn : string.Empty,
WorktypeName = worktypeNameMap.TryGetValue(x.WorktypeId, out var wn) ? wn : string.Empty,
BanciName = x.Banci.HasValue ? x.Banci.Value.ToString() : string.Empty,
Nianling = CalcAge(x.BirthDate, x.IdCard)
})
.ToList();
return new PageResult<EmployeeReadDto_View> { Items = list, Total = total, PageIndex = pageIndex, PageSize = pageSize };
}
public async Task<EmployeeReadDto_Edit> GetByIdForEditAsync(long id)
=> await Orm.Select<Employee>()
.Where(x => x.Id == id && x.DeletedAt == null)
.FirstAsync(x => new EmployeeReadDto_Edit(x.Id, x.DepId, x.WorktypeId, x.WorkStatus, x.Banci, x.Code,
x.Name, x.Gangwei, x.Phone, x.IdCard, x.BirthDate, x.Address, x.Gender, x.Minzu, x.HunyinStatus, x.Huji,
x.RuzhiDate, x.LizhiDate, x.ZhuanzhengDate, x.IsZhuanzheng, x.JinjiLianxiren, x.JinjiLianxirenPhone,
x.HetongType, x.HetongStartDate, x.HetongEndDate, x.TuixiuDate, x.ShebaoDate, x.ShebaoDesc,
x.BankcardCode, x.BiyeYuanxiao, x.Zhuanye, x.Xueli, x.WorkGroup, x.Sort, x.NamePinyin, x.Memo,
x.LizhiYuanyin));
public async Task<List<EmployeeChangeRecordDto>> GetChangeRecordsAsync(long employeeId)
{
var rows = await Orm.Select<EmployeeChangeRecord>()
.Where(x => x.EmployeeId == employeeId)
.OrderByDescending(x => x.Date)
.ToListAsync(x => new EmployeeChangeRecordDto(x.Id, x.EmployeeId, x.Date, x.ChangeStatus, "",
x.ChangeShuoming, x.ChangeYuanyin));
return rows.Select(x => x with { ChangeStatusName = x.ChangeStatus.ToString() }).ToList();
}
public async Task<List<EmployeeTiaogangRecordDto>> GetTiaogangRecordsAsync(long employeeId)
{
var rows = await Orm.Select<EmployeeTiaogangRecord>()
.Where(x => x.EmployeeId == employeeId)
.OrderByDescending(x => x.TiaogangDate)
.ToListAsync(x => new EmployeeTiaogangRecordDto(x.Id, x.EmployeeId, x.OldDepId, "", x.OldWorktypeId, "",
x.NewDepId, "", x.NewWorktypeId, "", x.TiaogangDate));
if (rows.Count == 0) return rows;
var depNameMap = await GetDepNameMapAsync();
var worktypeNameMap = await GetWorktypeNameMapAsync();
return rows
.Select(x => x with
{
OldDepName = depNameMap.TryGetValue(x.OldDepId, out var odn) ? odn : string.Empty,
NewDepName = depNameMap.TryGetValue(x.NewDepId, out var ndn) ? ndn : string.Empty,
OldWorktypeName = worktypeNameMap.TryGetValue(x.OldWorktypeId, out var own) ? own : string.Empty,
NewWorktypeName = worktypeNameMap.TryGetValue(x.NewWorktypeId, out var nwn) ? nwn : string.Empty
})
.ToList();
}
public async Task<bool> AddChangeRecordAsync(EmployeeChangeRecordDto record)
{
var entity = new EmployeeChangeRecord(_snowflakeIdGenerator.CreateId(), record.EmployeeId,
record.Date, record.ChangeStatus, record.ChangeShuoming ?? string.Empty, record.ChangeYuanyin ?? string.Empty)
{
CreatedAt = DateTime.Now
};
return await Orm.Insert(entity).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> AddTiaogangRecordAsync(long employeeId, long oldDepId, long oldWorktypeId, long newDepId,
long newWorktypeId, DateTime date)
{
var entity = new EmployeeTiaogangRecord(_snowflakeIdGenerator.CreateId(), employeeId, oldDepId,
oldWorktypeId, newDepId, newWorktypeId, date)
{
CreatedAt = DateTime.Now
};
return await Orm.Insert(entity).ExecuteAffrowsAsync() > 0;
}
public async Task<string> GetDepNameAsync(long depId)
{
if (depId <= 0) return string.Empty;
var dep = await Orm.Select<Department>().Where(x => x.Id == depId).FirstAsync();
return dep?.Name ?? string.Empty;
}
public async Task<string> GetWorktypeNameAsync(long worktypeId)
{
if (worktypeId <= 0) return string.Empty;
var worktype = await Orm.Select<WorkType>().Where(x => x.Id == worktypeId).FirstAsync();
return worktype?.WorkName ?? string.Empty;
}
/// <summary>取部门及其全部下级部门 Id</summary>
private async Task<List<long>> GetDepIdsWithChildrenAsync(long rootDepId)
{
var all = await Orm.Select<Department>().Where(x => x.DeletedAt == null).ToListAsync();
var result = new HashSet<long> { rootDepId };
var changed = true;
while (changed)
{
changed = false;
foreach (var dep in all)
{
if (dep.ParentId != 0 && result.Contains(dep.ParentId) && result.Add(dep.Id)) changed = true;
}
}
return result.ToList();
}
private async Task<Dictionary<long, string>> GetDepNameMapAsync()
=> (await Orm.Select<Department>().Where(x => x.DeletedAt == null).ToListAsync())
.ToDictionary(x => x.Id, x => x.Name);
private async Task<Dictionary<long, string>> GetWorktypeNameMapAsync()
=> (await Orm.Select<WorkType>().Where(x => x.DeletedAt == null).ToListAsync())
.ToDictionary(x => x.Id, x => x.WorkName);
/// <summary>年龄:优先出生日期,其次从身份证号推算</summary>
private static int CalcAge(DateTime? birthDate, string idCard)
{
var birth = birthDate;
if (birth == null && !string.IsNullOrWhiteSpace(idCard))
{
var card = idCard.Trim();
string? datePart = card.Length switch
{
18 => card.Substring(6, 8),
15 => "19" + card.Substring(6, 6),
_ => null
};
if (datePart != null
&& DateTime.TryParseExact(datePart, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var parsed))
{
birth = parsed;
}
}
if (birth == null) return 0;
var today = DateTime.Today;
var age = today.Year - birth.Value.Year;
if (birth.Value.Date > today.AddYears(-age)) age--;
return age < 0 ? 0 : age;
}
}
@@ -28,11 +28,14 @@ internal class WorkTypeReadRepository : F9RepositoryBase, IWorkTypeReadRepositor
.OrderBy(x => x.SortOrder)
.OrderBy(x => x.Id)
.Page(pageIndex, pageSize)
.ToListAsync(x => new WorkTypeReadDto_View(x.Id, x.WorkName, x.SortOrder, x.BizType, x.SccbFenlei, x.IsActive, ""));
.ToListAsync(x => new WorkTypeReadDto_View(x.Id, x.WorkName, x.SortOrder, x.BizType, x.SccbFenlei, x.IsActive, "", null));
var nameMap = await GetDepartmentNameMapAsync(items.Select(x => x.Id).ToList());
// 部门名称与 Id 一次性带出,前端无需再逐个工种查询关联部门
var infoMap = await GetDepartmentInfoMapAsync(items.Select(x => x.Id).ToList());
var list = items
.Select(x => x with { DepartmentNames = nameMap.TryGetValue(x.Id, out var names) ? names : string.Empty })
.Select(x => infoMap.TryGetValue(x.Id, out var info)
? x with { DepartmentNames = info.Names, DepartmentIds = info.Ids }
: x with { DepartmentNames = string.Empty, DepartmentIds = new List<long>() })
.ToList();
return new PageResult<WorkTypeReadDto_View>
@@ -61,10 +64,10 @@ internal class WorkTypeReadRepository : F9RepositoryBase, IWorkTypeReadRepositor
.OrderBy(x => x.AssignedAt)
.ToListAsync(x => new WorkTypeRoleReadDto_Cache(x.Id, x.WorkTypeId, x.RoleId));
/// <summary>工种 Id → 所属部门名称串(顿号分隔)</summary>
private async Task<Dictionary<long, string>> GetDepartmentNameMapAsync(List<long> workTypeIds)
/// <summary>工种 Id → 所属部门名称串(顿号分隔)与部门 Id 列表</summary>
private async Task<Dictionary<long, (string Names, List<long> Ids)>> GetDepartmentInfoMapAsync(List<long> workTypeIds)
{
var map = new Dictionary<long, string>();
var map = new Dictionary<long, (string, List<long>)>();
if (workTypeIds == null || workTypeIds.Count == 0) return map;
var relations = await Orm.Select<WorkTypeDepartment>()
@@ -78,11 +81,12 @@ internal class WorkTypeReadRepository : F9RepositoryBase, IWorkTypeReadRepositor
foreach (var group in relations.GroupBy(x => x.WorkTypeId))
{
var names = group
.Select(r => depNameMap.TryGetValue(r.DepartmentId, out var name) ? name : null)
var ids = group.Select(r => r.DepartmentId).Distinct().ToList();
var names = ids
.Select(id => depNameMap.TryGetValue(id, out var name) ? name : null)
.Where(name => !string.IsNullOrEmpty(name))
.ToList();
map[group.Key] = string.Join("、", names);
map[group.Key] = (string.Join("、", names), ids);
}
return map;
+111
View File
@@ -0,0 +1,111 @@
# ============================================================
# 将「智慧缫丝系统」(f9-web-ai-test) 发布到本机 IIS
# - 自动安装 ASP.NET Core Hosting Bundle(IIS 托管 .NET 8 必需)
# - 创建应用池 / 站点 / 目录权限,并做一次自检
# 必须以【管理员身份】运行
# ============================================================
$ErrorActionPreference = 'Stop'
# 全过程写入日志文件,便于提权窗口关闭后回看
$logDir = Join-Path $PSScriptRoot '_runlog'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
try { Start-Transcript -Path (Join-Path $logDir 'deploy-iis.log') -Force -ErrorAction Stop | Out-Null } catch { }
$publishDir = 'C:\Users\32453\CodeBuddy\20260825142915\publish'
$siteName = 'F9WebAiTest'
$appPoolName = 'F9WebAiTest'
$preferredPort = 80
$ancmPath = 'C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll'
$hostingBundle = 'https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.21/dotnet-hosting-8.0.21-win.exe'
$installer = Join-Path $env:TEMP 'dotnet-hosting-8.0.21-win.exe'
function Info($m) { Write-Host "[部署] $m" -ForegroundColor Cyan }
function Warn($m) { Write-Host "[警告] $m" -ForegroundColor Yellow }
# ---------- 0. 前置校验 ----------
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw '请以管理员身份运行本脚本'
}
if (-not (Test-Path $publishDir)) { throw "发布目录不存在:$publishDir" }
if (-not (Test-Path (Join-Path $publishDir 'wwwroot\index.html'))) { throw "前端产物缺失:$publishDir\wwwroot\index.html" }
if (-not (Test-Path (Join-Path $publishDir 'F9WebAiTest.Api.dll'))) { throw "后端产物缺失:$publishDir\F9WebAiTest.Api.dll" }
if (-not (Test-Path 'C:\Windows\System32\inetsrv\appcmd.exe')) { throw '本机未安装 IIS,请先启用 IIS 功能' }
Import-Module WebAdministration
# ---------- 1. ASP.NET Core Hosting Bundle ----------
if (-not (Test-Path $ancmPath)) {
Info '未检测到 AspNetCoreModuleV2,开始下载 Hosting Bundle 8.0.21 ...'
Invoke-WebRequest -Uri $hostingBundle -OutFile $installer -UseBasicParsing
Info '下载完成,静默安装中(约 1-2 分钟)...'
Start-Process -FilePath $installer -ArgumentList '/install', '/quiet', '/norestart' -Wait
if (-not (Test-Path $ancmPath)) { throw 'Hosting Bundle 安装失败,请手动安装后重试' }
Info '安装完成,重启 IIS 使模块生效 ...'
& iisreset /noforce | Out-Null
} else {
Info 'AspNetCoreModuleV2 已安装,跳过'
}
# ---------- 2. 选择端口 ----------
$port = $preferredPort
$occupied = netstat -ano | Select-String 'LISTENING' | Select-String ":$port\s"
if ($occupied) {
$port = 8080
Warn "端口 $preferredPort 已被占用,改用 $port"
}
# ---------- 3. 应用池(无托管代码,与 .NET Core 应用匹配)----------
if (Test-Path "IIS:\AppPools\$appPoolName") {
Info "应用池 $appPoolName 已存在,更新配置"
} else {
New-WebAppPool -Name $appPoolName | Out-Null
Info "已创建应用池 $appPoolName"
}
Set-ItemProperty "IIS:\AppPools\$appPoolName" -Name managedRuntimeVersion -Value ''
Set-ItemProperty "IIS:\AppPools\$appPoolName" -Name startMode -Value 'AlwaysRunning'
# ---------- 4. 站点 ----------
if (Test-Path "IIS:\Sites\$siteName") {
Info "站点 $siteName 已存在,更新物理路径与应用池"
Set-ItemProperty "IIS:\Sites\$siteName" -Name physicalPath -Value $publishDir
Set-ItemProperty "IIS:\Sites\$siteName" -Name applicationPool -Value $appPoolName
if (-not (Get-WebBinding -Name $siteName -Protocol http -Port $port -ErrorAction SilentlyContinue)) {
New-WebBinding -Name $siteName -Protocol http -Port $port -IPAddress '*'
Info "已补充绑定 http://*:$port"
}
} else {
New-Website -Name $siteName -PhysicalPath $publishDir -Port $port -ApplicationPool $appPoolName -Force | Out-Null
Info "已创建站点 $siteName"
}
# ---------- 5. 目录权限(应用池身份需读写:SQLite 库文件、日志)----------
$aclUser = "IIS AppPool\$appPoolName"
& icacls $publishDir /grant "${aclUser}:(OI)(CI)M" /T /Q | Out-Null
Info "已授予 $aclUser 读写权限"
# ---------- 6. 启动 ----------
if ((Get-WebAppPoolState -Name $appPoolName).Value -ne 'Started') { Start-WebAppPool -Name $appPoolName }
if ((Get-Website -Name $siteName).State -ne 'Started') { Start-Website -Name $siteName }
Start-Sleep -Seconds 4
# ---------- 7. 自检 ----------
$url = "http://localhost:$port/"
Info '开始自检 ...'
try {
$r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 40
Info "首页:HTTP $($r.StatusCode)(前端已托管)"
} catch {
Warn "首页访问失败:$($_.Exception.Message)"
Warn "如为 500,请查看 事件查看器 → Windows 日志 → 应用程序,或打开 $publishDir\web.config 把 stdoutLogEnabled 改为 true 后重试"
}
try {
$login = Invoke-RestMethod -Uri "${url}api/auth/login" -Method Post -ContentType 'application/json' `
-Body '{"userName":"admin","password":"123456"}' -TimeoutSec 40
Info "登录接口:code=$($login.code) $($login.message)"
} catch {
Warn "登录接口调用失败:$($_.Exception.Message)"
}
Write-Host ''
Info "部署完成,访问地址:$url"
Info "API 文档:${url}swagger 默认账号:admin / 123456"
+78
View File
@@ -0,0 +1,78 @@
# ============================================================
# 一键更新 IIS 上的「智慧缫丝系统」(f9-web-ai-test)
# 改动代码后运行:停站点 → 重新发布后端 → 同步前端产物 → 启站点 → 自检
# 前置:先执行过 deploy-iis.ps1;前端已用 npm run build 构建
# 必须以【管理员身份】运行(停/启 IIS 站点、替换被占用的程序文件)
# ============================================================
$ErrorActionPreference = 'Stop'
$root = 'C:\Users\32453\CodeBuddy\20260825142915'
$webDir = Join-Path $root 'web'
$distDir = Join-Path $webDir 'dist'
$apiProject = Join-Path $root 'backend\F9WebAiTest.Api'
$publishDir = Join-Path $root 'publish'
$appPoolName = 'F9WebAiTest'
$siteName = 'F9WebAiTest'
$logDir = Join-Path $root '_runlog'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
try { Start-Transcript -Path (Join-Path $logDir 'update-iis.log') -Force -ErrorAction Stop | Out-Null } catch { }
function Info($m) { Write-Host "[更新] $m" -ForegroundColor Cyan }
function Warn($m) { Write-Host "[警告] $m" -ForegroundColor Yellow }
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw '请以管理员身份运行本脚本'
}
if (-not (Test-Path $distDir)) { throw "前端产物不存在:$distDir(请先在 web 目录执行 npm run build)" }
# 必须先加载 WebAdministration,IIS: 驱动器才存在(否则站点判断会误报不存在)
Import-Module WebAdministration
if (-not (Get-Website -Name $siteName -ErrorAction SilentlyContinue)) { throw "IIS 站点 $siteName 不存在,请先运行 deploy-iis.ps1" }
# ---------- 1. 停站点与应用池(释放 dll 与数据库文件占用)----------
if ((Get-Website -Name $siteName).State -ne 'Stopped') { Stop-Website -Name $siteName }
if ((Get-WebAppPoolState -Name $appPoolName).Value -ne 'Stopped') { Stop-WebAppPool -Name $appPoolName }
Start-Sleep -Seconds 3
Info '站点已停止,文件已释放'
# ---------- 2. 发布后端(Release)----------
Info '发布后端(Release)...'
& 'C:\Program Files\dotnet\dotnet.exe' publish $apiProject -c Release -o $publishDir 2>&1 |
Where-Object { $_ -match 'error|已成功|-> ' } | Select-Object -Last 5
if ($LASTEXITCODE -ne 0) { throw "dotnet publish 失败(退出码 $LASTEXITCODE)" }
# ---------- 3. 同步前端产物到 wwwroot ----------
$wwwroot = Join-Path $publishDir 'wwwroot'
if (Test-Path $wwwroot) { Remove-Item $wwwroot -Recurse -Force }
New-Item -ItemType Directory -Force -Path $wwwroot | Out-Null
Copy-Item (Join-Path $distDir '*') $wwwroot -Recurse -Force
Info "前端产物已同步:$wwwroot"
# ---------- 4. 目录权限兜底 ----------
& icacls $publishDir /grant "IIS AppPool\${appPoolName}:(OI)(CI)M" /T /Q | Out-Null
# ---------- 5. 启动站点 ----------
Start-WebAppPool -Name $appPoolName
Start-Website -Name $siteName
Start-Sleep -Seconds 5
Info '站点已启动'
# ---------- 6. 自检 ----------
$url = 'http://localhost/'
try {
$r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 40
Info "首页:HTTP $($r.StatusCode)"
} catch {
Warn "首页访问失败:$($_.Exception.Message)"
}
try {
$login = Invoke-RestMethod -Uri "${url}api/auth/login" -Method Post -ContentType 'application/json' `
-Body '{"userName":"admin","password":"123456"}' -TimeoutSec 40
Info "登录接口:code=$($login.code) $($login.message)"
} catch {
Warn "登录接口调用失败:$($_.Exception.Message)"
}
Write-Host ''
Info "更新完成,访问地址:$url"
+119 -37
View File
@@ -87,43 +87,45 @@ async function handleLogout() {
<el-icon class="logo-icon"><MagicStick /></el-icon>
<span v-if="!isCollapse" class="logo-text">智慧缫丝系统</span>
</div>
<el-menu
:default-active="route.path"
:collapse="isCollapse"
background-color="#0c3b2e"
text-color="#b5cdc2"
active-text-color="#e8c877"
router
class="menu"
>
<el-menu-item index="/dashboard">
<el-icon><HomeFilled /></el-icon>
<template #title>首页</template>
</el-menu-item>
<template v-for="menu in sidebarMenus" :key="menu.id">
<!-- 有子菜单 -->
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.path">
<template #title>
<el-icon><component :is="menu.icon || 'Menu'" /></el-icon>
<span>{{ menu.name }}</span>
</template>
<el-menu-item
v-for="child in menu.children"
:key="child.id"
:index="child.path"
>
<el-icon><component :is="child.icon || 'Document'" /></el-icon>
<template #title>{{ child.name }}</template>
</el-menu-item>
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item v-else :index="menu.path">
<el-icon><component :is="menu.icon || 'Menu'" /></el-icon>
<template #title>{{ menu.name }}</template>
<div class="menu-wrapper">
<el-menu
:default-active="route.path"
:collapse="isCollapse"
background-color="#0c3b2e"
text-color="#b5cdc2"
active-text-color="#e8c877"
router
class="menu"
>
<el-menu-item index="/dashboard">
<el-icon><HomeFilled /></el-icon>
<template #title>首页</template>
</el-menu-item>
</template>
</el-menu>
<template v-for="menu in sidebarMenus" :key="menu.id">
<!-- 有子菜单 -->
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.path">
<template #title>
<el-icon><component :is="menu.icon || 'Menu'" /></el-icon>
<span>{{ menu.name }}</span>
</template>
<el-menu-item
v-for="child in menu.children"
:key="child.id"
:index="child.path"
>
<el-icon><component :is="child.icon || 'Document'" /></el-icon>
<template #title>{{ child.name }}</template>
</el-menu-item>
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item v-else :index="menu.path">
<el-icon><component :is="menu.icon || 'Menu'" /></el-icon>
<template #title>{{ menu.name }}</template>
</el-menu-item>
</template>
</el-menu>
</div>
</aside>
<!-- 主区域 -->
@@ -187,17 +189,95 @@ async function handleLogout() {
}
.sidebar {
display: flex;
flex-direction: column;
width: 220px;
background: linear-gradient(180deg, #0f4a38 0%, #0c3b2e 100%);
transition: width 0.3s;
overflow-x: hidden;
overflow: hidden;
}
.sidebar.collapse {
width: 64px;
}
/* 折叠态下图标居中优先,不预留滚动槽(此时子菜单以浮层弹出,高度不变化,不会抖动) */
.sidebar.collapse .menu-wrapper {
scrollbar-gutter: auto;
}
/* 菜单区独立滚动,并始终预留滚动条宽度(scrollbar-gutter: stable):
否则展开"基础数据"这类子项较多的目录时,总高度跨过视口,滚动条突然出现/消失,
会把菜单横向挤压位移,视觉上就是一卡一卡 */
.menu-wrapper {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
.menu-wrapper::-webkit-scrollbar {
width: 6px;
}
.menu-wrapper::-webkit-scrollbar-track {
background: transparent;
}
.menu-wrapper::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.18);
border-radius: 3px;
}
.menu-wrapper::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.32);
}
/* Element Plus 默认用 transition: height 做展开动画,而 height 过渡不是合成层动画:
浏览器每一帧都要重新布局容器内的**全部**子菜单项,子项多时(如"基础数据"10 项)
在普通机器上会明显掉帧(实测单帧可达 120ms+)。
这里去掉 height 过渡,改用只涉及合成层的 opacity + transform 动画,视觉一样但不触发重排。 */
.menu-wrapper :deep(.el-collapse-transition-enter-active),
.menu-wrapper :deep(.el-collapse-transition-leave-active) {
transition: none !important;
}
/* 淡入只加在整个子菜单容器上(只产生一个合成层),
若加在每个子项上,会为 10 个元素分别创建合成层,展开瞬间反而更慢 */
.menu-wrapper :deep(.el-menu--inline) {
animation: menu-submenu-in 0.16s ease-out;
}
/* 布局/绘制隔离:展开时把每个菜单项的重排限制在自身内部,避免整块重排 */
.menu-wrapper :deep(.el-menu-item),
.menu-wrapper :deep(.el-sub-menu__title) {
contain: layout paint;
}
/* 收起时不要用 display:none(Element Plus 默认行为),而是用 height:0 + 隐藏裁剪:
这样子项在页面加载时就已完成布局,展开瞬间无需对全部子项做首次布局,
这正是"子项越多越卡"(基础数据 10 项)的根源所在 */
.menu-wrapper :deep(.el-sub-menu:not(.is-opened) > .el-menu--inline) {
display: block !important;
height: 0 !important;
overflow: hidden;
visibility: hidden;
}
@keyframes menu-submenu-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.logo {
flex: none;
height: 60px;
display: flex;
align-items: center;
@@ -294,6 +374,8 @@ async function handleLogout() {
flex: 1;
padding: 20px;
overflow-y: auto;
/* 内容区同样预留滚动条宽度,避免切换页面时宽度跳动 */
scrollbar-gutter: stable;
background: #f0f2f5;
}
</style>
+1
View File
@@ -58,6 +58,7 @@ export const componentMap = {
'base/yuanliaozhuangkou/index': () => import('../views/base/yuanliaozhuangkou/index.vue'),
'base/wuzimulu/index': () => import('../views/base/wuzimulu/index.vue'),
'base/gongyingshang/index': () => import('../views/base/gongyingshang/index.vue'),
'base/employee/index': () => import('../views/base/employee/index.vue'),
'prod/lsjl/index': () => import('../views/prod/lsjl/index.vue'),
'prod/xdjy/index': () => import('../views/prod/xdjy/index.vue'),
'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'),
+2
View File
@@ -36,6 +36,8 @@ function statusType(status) {
}
function handlePageChange(page) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (page === pageIndex.value) return;
pageIndex.value = page;
loadRecords();
}
@@ -43,6 +43,8 @@ function resetSearch() {
}
function handlePageChange(page) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (page === query.pageIndex) return;
query.pageIndex = page;
load();
}
+6 -1
View File
@@ -79,7 +79,12 @@ async function del(row) {
} catch (e) {}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
onMounted(() => { load(); loadGuige(); });
</script>
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -43,6 +43,13 @@ const form = reactive({
const vendorGroupTypes = [3, 4]; // 副产品 / 生丝 → 默认分组为"客户"
// 分页组件挂载时会触发一次 current-change:页码未变化时直接忽略,避免页面首次打开重复请求
function handlePageChange(p) {
if (p === query.pageIndex) return;
query.pageIndex = p;
loadList();
}
async function loadList() {
loading.value = true;
try {
@@ -190,12 +197,12 @@ onMounted(loadList);
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:current-page="query.pageIndex"
:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
@current-change="handlePageChange"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px">
+6 -1
View File
@@ -73,7 +73,12 @@ async function del(row) {
} catch (e) {}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
onMounted(load);
</script>
+6 -1
View File
@@ -71,7 +71,12 @@ async function del(row) {
} catch (e) {}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
onMounted(() => { load(); loadDepts(); });
</script>
+18 -9
View File
@@ -13,18 +13,20 @@ const query = reactive({ pageIndex: 1, pageSize: 20, keyword: '' });
const bizTypeOptions = ref(['生产', '辅助']);
const sccbFenleiOptions = ref(['无', '管理']);
// 一次拉取注册表全量后本地取用,避免每个键各发一次请求
async function loadRegistryOptions() {
try {
const load = async (keyName) => {
const res = await request.get('/sys/registry/by-key', {
params: { category: '基础数据\\工种', keyName }
});
return (res.data?.value || '')
const res = await request.get('/sys/registry');
const all = res.data || [];
const pick = (keyName) => {
const hit = all.find((x) => x.category === '基础数据\\工种' && x.keyName === keyName);
return (hit?.value || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
};
const [biz, fenlei] = await Promise.all([load('业务类型'), load('生产成本分类')]);
const biz = pick('业务类型');
const fenlei = pick('生产成本分类');
if (biz.length) bizTypeOptions.value = biz;
if (fenlei.length) sccbFenleiOptions.value = fenlei;
} catch (e) {
@@ -52,6 +54,13 @@ const boundRoleIds = ref([]);
const currentRow = ref(null);
// 分页组件挂载时会触发一次 current-change:页码未变化时直接忽略,避免页面首次打开重复请求
function handlePageChange(p) {
if (p === query.pageIndex) return;
query.pageIndex = p;
loadList();
}
async function loadList() {
loading.value = true;
try {
@@ -255,12 +264,12 @@ onMounted(() => {
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:current-page="query.pageIndex"
:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
@current-change="handlePageChange"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="460px">
+10 -3
View File
@@ -120,6 +120,13 @@ function handleNodeClick(node) {
loadMulu();
}
// 分页组件挂载时会触发一次 current-change:页码未变化时直接忽略,避免页面首次打开重复请求
function handlePageChange(p) {
if (p === query.pageIndex) return;
query.pageIndex = p;
loadMulu();
}
async function loadMulu() {
loading.value = true;
try {
@@ -397,12 +404,12 @@ onMounted(loadGroups);
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:current-page="query.pageIndex"
:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadMulu"
@current-change="handlePageChange"
/>
</div>
</div>
+24 -22
View File
@@ -49,27 +49,22 @@ function bendiwaidiLabel(v) {
return hit ? hit.label : v;
}
// 一次拉取注册表全量后本地取用,避免每个键各发一次请求
async function loadRegistryOptions() {
const load = async (keyName) => {
const res = await request.get('/sys/registry/by-key', {
params: { category: '基础数据\\原料庄口', keyName }
});
return (res.data?.value || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
};
try {
const [chandi, zhandian, jibie, canpinzhong] = await Promise.all([
load('产地'),
load('站点'),
load('季别'),
load('蚕品种')
]);
registryOptions.chandi = chandi;
registryOptions.zhandian = zhandian;
registryOptions.jibie = jibie;
registryOptions.canpinzhong = canpinzhong;
const res = await request.get('/sys/registry');
const all = res.data || [];
const pick = (keyName) => {
const hit = all.find((x) => x.category === '基础数据\\原料庄口' && x.keyName === keyName);
return (hit?.value || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
};
registryOptions.chandi = pick('产地');
registryOptions.zhandian = pick('站点');
registryOptions.jibie = pick('季别');
registryOptions.canpinzhong = pick('蚕品种');
} catch (e) {
// 注册表未配置时下拉为空,仍可手工录入
}
@@ -82,6 +77,13 @@ async function loadWuziOptions() {
} catch (e) {}
}
// 分页组件挂载时会触发一次 current-change:页码未变化时直接忽略,避免页面首次打开重复请求
function handlePageChange(p) {
if (p === query.pageIndex) return;
query.pageIndex = p;
loadList();
}
async function loadList() {
loading.value = true;
try {
@@ -274,12 +276,12 @@ onMounted(async () => {
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:current-page="query.pageIndex"
:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
@current-change="handlePageChange"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
+10 -3
View File
@@ -48,6 +48,13 @@ function defaultRange() {
return { start: str(start), end: str(end) };
}
// 分页组件挂载时会触发一次 current-change:页码未变化时直接忽略,避免页面首次打开重复请求
function handlePageChange(p) {
if (p === query.pageIndex) return;
query.pageIndex = p;
loadList();
}
async function loadList() {
loading.value = true;
try {
@@ -205,12 +212,12 @@ onMounted(loadList);
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:current-page="query.pageIndex"
:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
@current-change="handlePageChange"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="480px">
+6 -1
View File
@@ -80,7 +80,12 @@ async function del(row) {
} catch (e) {}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
onMounted(load);
</script>
+3 -3
View File
@@ -7,9 +7,9 @@ const userStore = useUserStore();
const user = userStore.userInfo || {};
const quickLinks = [
{ title: '落丝记录', desc: '缫丝生产 · 落丝管理', icon: 'DataLine', path: '/prod/lsjl', color: '#1e6b52' },
{ title: '纤度检验', desc: '纤度 / 野纤等级判定', icon: 'ScaleToOriginal', path: '/prod/xdjy', color: '#c9a54a' },
{ title: '黑板检验', desc: '清洁 / 洁净等级', icon: 'Grid', path: '/prod/hbjy', color: '#3f8f6e' },
{ title: '落丝记录', desc: '前缫 · 落丝管理', icon: 'DataLine', path: '/prod/qianxian/lsjl', color: '#1e6b52' },
{ title: '纤度检验', desc: '后缫 · 纤度 / 野纤等级判定', icon: 'ScaleToOriginal', path: '/prod/houxian/xdjy', color: '#c9a54a' },
{ title: '黑板检验', desc: '后缫 · 清洁 / 洁净等级', icon: 'Grid', path: '/prod/houxian/hbjy', color: '#3f8f6e' },
{ title: '考勤打卡', desc: '上下班签到签退', icon: 'AlarmClock', path: '/attendance/punch', color: '#b8873a' }
];
@@ -64,7 +64,12 @@ async function markAllRead() {
} catch (e) {}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
function resetPage() { query.pageIndex = 1; load(); }
function rowClass({ row }) {
+6 -1
View File
@@ -31,7 +31,12 @@ async function load() {
}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
// ==================== 编辑对话框 ====================
const dialogVisible = ref(false);
+6 -1
View File
@@ -124,7 +124,12 @@ async function del(row) {
} catch (e) {}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
onMounted(() => { load(); loadDepts(); loadZhuangkou(); loadGuige(); });
</script>
+6 -1
View File
@@ -32,7 +32,12 @@ async function load() {
}
}
function handlePageChange(p) { query.pageIndex = p; load(); }
function handlePageChange(p) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (p === query.pageIndex) return;
query.pageIndex = p;
load();
}
// ==================== 编辑对话框 ====================
const dialogVisible = ref(false);
+6 -1
View File
@@ -138,7 +138,12 @@ async function handleSaveRoles() {
} catch (e) {}
}
function handlePageChange(page) { query.page = page; loadList(); }
function handlePageChange(page) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (page === query.page) return;
query.page = page;
loadList();
}
onMounted(() => { loadList(); loadDepts(); loadRoles(); });
</script>
+2
View File
@@ -128,6 +128,8 @@ async function handleResetPwd(row) {
}
function handlePageChange(page) {
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
if (page === query.pageIndex) return;
query.pageIndex = page;
loadList();
}
+2
View File
@@ -6,6 +6,8 @@ export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
// 注意:这里刻意不做 server.warmup 全量预热。
// 页面组件一律按需加载:只有点进具体菜单项时,才编译并加载对应页面组件。
proxy: {
'/api': {
target: 'http://localhost:3000',