教学点名:教学班、考勤表均改为服务端分页;支持课程/任务关键词、学期、考勤表名称/备注、状态、签到方式筛选。
成绩管理:确认原本已是服务端分页,且已有学期、状态、课程关键词与学生关键词筛选,无需重复改造。 新增服务端分页与筛选测试。
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Security.Claims;
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ClosedXML.Excel;
|
using ClosedXML.Excel;
|
||||||
|
using Jiaowu.Api.Contracts;
|
||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
@@ -33,14 +34,32 @@ public sealed class AttendanceController(
|
|||||||
[Authorize(Roles = AttendanceRoles)]
|
[Authorize(Roles = AttendanceRoles)]
|
||||||
public async Task<ActionResult> GetMyTasks(
|
public async Task<ActionResult> GetMyTasks(
|
||||||
Guid? academicTermId,
|
Guid? academicTermId,
|
||||||
CancellationToken cancellationToken)
|
string? keyword = null,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
page = Math.Max(1, page);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 50);
|
||||||
var tasks = AccessibleTasks().AsNoTracking()
|
var tasks = AccessibleTasks().AsNoTracking()
|
||||||
.Where(x => x.Status == TeachingTaskStatus.Published);
|
.Where(x => x.Status == TeachingTaskStatus.Published);
|
||||||
if (academicTermId.HasValue)
|
if (academicTermId.HasValue)
|
||||||
tasks = tasks.Where(x => x.AcademicTermId == academicTermId);
|
tasks = tasks.Where(x => x.AcademicTermId == academicTermId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
keyword = keyword.Trim();
|
||||||
|
tasks = tasks.Where(x =>
|
||||||
|
x.TaskNumber.Contains(keyword) ||
|
||||||
|
x.Name.Contains(keyword) ||
|
||||||
|
x.Course!.Code.Contains(keyword) ||
|
||||||
|
x.Course.Name.Contains(keyword));
|
||||||
|
}
|
||||||
|
var total = await tasks.CountAsync(cancellationToken);
|
||||||
var result = await tasks
|
var result = await tasks
|
||||||
.OrderBy(x => x.Course!.Code)
|
.OrderBy(x => x.Course!.Code)
|
||||||
|
.ThenBy(x => x.TaskNumber)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
@@ -63,22 +82,44 @@ public sealed class AttendanceController(
|
|||||||
sheet.TeachingTaskId == x.Id)
|
sheet.TeachingTaskId == x.Id)
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
return Ok(result);
|
return Ok(new PagedResult<object>(result, total, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("sheets")]
|
[HttpGet("sheets")]
|
||||||
[Authorize(Roles = AttendanceRoles)]
|
[Authorize(Roles = AttendanceRoles)]
|
||||||
public async Task<ActionResult> GetSheets(
|
public async Task<ActionResult> GetSheets(
|
||||||
Guid teachingTaskId,
|
Guid teachingTaskId,
|
||||||
CancellationToken cancellationToken)
|
string? keyword = null,
|
||||||
|
AttendanceSheetStatus? status = null,
|
||||||
|
AttendanceCheckInMethod? checkInMethod = null,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
page = Math.Max(1, page);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 50);
|
||||||
var task = await AccessibleTasks().AsNoTracking()
|
var task = await AccessibleTasks().AsNoTracking()
|
||||||
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
|
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
|
||||||
if (task is null) return NotFound();
|
if (task is null) return NotFound();
|
||||||
|
|
||||||
var sheets = await db.AttendanceSheets.AsNoTracking()
|
var source = db.AttendanceSheets.AsNoTracking()
|
||||||
.Where(x => x.TeachingTaskId == teachingTaskId)
|
.Where(x => x.TeachingTaskId == teachingTaskId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
keyword = keyword.Trim();
|
||||||
|
source = source.Where(x => x.Name.Contains(keyword) || x.Notes!.Contains(keyword));
|
||||||
|
}
|
||||||
|
if (status.HasValue)
|
||||||
|
source = source.Where(x => x.Status == status.Value);
|
||||||
|
if (checkInMethod.HasValue)
|
||||||
|
source = source.Where(x => x.CheckInMethod == checkInMethod.Value);
|
||||||
|
|
||||||
|
var total = await source.CountAsync(cancellationToken);
|
||||||
|
var sheets = await source
|
||||||
.OrderByDescending(x => x.AttendanceDate)
|
.OrderByDescending(x => x.AttendanceDate)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
@@ -99,7 +140,7 @@ public sealed class AttendanceController(
|
|||||||
TotalCount = x.Records.Count
|
TotalCount = x.Records.Count
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
return Ok(sheets);
|
return Ok(new PagedResult<object>(sheets, total, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("sheets")]
|
[HttpPost("sheets")]
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public sealed class ClickHouseAnalyticsProjectionWorker(
|
|||||||
(x.AttendanceSheetId.CompareTo(cursorSheetId) > 0 ||
|
(x.AttendanceSheetId.CompareTo(cursorSheetId) > 0 ||
|
||||||
x.AttendanceSheetId == cursorSheetId && x.StudentId.CompareTo(cursorStudentId) > 0))
|
x.AttendanceSheetId == cursorSheetId && x.StudentId.CompareTo(cursorStudentId) > 0))
|
||||||
.OrderBy(x => x.AttendanceSheet!.AttendanceDate).ThenBy(x => x.AttendanceSheetId).ThenBy(x => x.StudentId).Take(options.BatchSize)
|
.OrderBy(x => x.AttendanceSheet!.AttendanceDate).ThenBy(x => x.AttendanceSheetId).ThenBy(x => x.StudentId).Take(options.BatchSize)
|
||||||
.Select(x => new { x.AttendanceSheetId, x.StudentId, AttendanceDate = x.AttendanceSheet!.AttendanceDate, TeachingTaskId = x.AttendanceSheet.TeachingTaskId, AcademicTermId = x.AttendanceSheet.TeachingTask!.AcademicTermId, CollegeId = x.AttendanceSheet.TeachingTask.Course!.CollegeId, Status = (byte)x.Status, x.CheckInAt, CheckedInMethod = x.CheckedInMethod == null ? null : (byte?)x.CheckedInMethod, AppealStatus = (byte)x.AppealStatus, ProjectedAt = projectedAt })
|
.Select(x => new { x.AttendanceSheetId, x.StudentId, AttendanceDate = x.AttendanceSheet!.AttendanceDate, TeachingTaskId = x.AttendanceSheet.TeachingTaskId, AcademicTermId = x.AttendanceSheet.TeachingTask!.AcademicTermId, CollegeId = x.AttendanceSheet.TeachingTask.Course!.CollegeId, x.Status, x.CheckInAt, x.CheckedInMethod, x.AppealStatus, ProjectedAt = projectedAt })
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
if (rows.Count == 0) return count;
|
if (rows.Count == 0) return count;
|
||||||
await client.InsertAsync("attendanceRecords", rows, cancellationToken);
|
await client.InsertAsync("attendanceRecords", rows, cancellationToken);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Jiaowu.Api.Contracts;
|
||||||
|
using Jiaowu.Api.Controllers;
|
||||||
|
using Jiaowu.Api.Domain.Academic;
|
||||||
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Tests;
|
||||||
|
|
||||||
|
public sealed class AttendancePaginationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task TasksAndSheets_AreFilteredAndReturnedByPage()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
await using var db = new AppDbContext(
|
||||||
|
new DbContextOptionsBuilder<AppDbContext>().UseSqlite(connection).Options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||||
|
var course = new Course
|
||||||
|
{
|
||||||
|
Code = "CS101", Name = "程序设计基础", CollegeId = college.Id,
|
||||||
|
Credits = 4, TotalHours = 64, LectureHours = 48, PracticeHours = 16,
|
||||||
|
Nature = CourseNature.MajorRequired, AssessmentMethod = AssessmentMethod.Examination
|
||||||
|
};
|
||||||
|
var term = new AcademicTerm
|
||||||
|
{
|
||||||
|
Code = "2026-1", Name = "2026—2027 学年第一学期", AcademicYear = "2026-2027",
|
||||||
|
Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 1),
|
||||||
|
EndDate = new DateOnly(2027, 1, 20)
|
||||||
|
};
|
||||||
|
var tasks = Enumerable.Range(1, 11).Select(number => new TeachingTask
|
||||||
|
{
|
||||||
|
TaskNumber = $"2026-1-CS101-{number:00}", Name = $"程序设计教学班 {number}",
|
||||||
|
AcademicTermId = term.Id, CourseId = course.Id, Capacity = 60,
|
||||||
|
Status = TeachingTaskStatus.Published
|
||||||
|
}).ToList();
|
||||||
|
var sheets = Enumerable.Range(1, 11).Select(number => new AttendanceSheet
|
||||||
|
{
|
||||||
|
TeachingTaskId = tasks[0].Id,
|
||||||
|
Name = number == 11 ? "期末扫码点名" : $"第 {number} 周点名",
|
||||||
|
AttendanceDate = new DateTime(2026, 9, number),
|
||||||
|
Status = number == 11 ? AttendanceSheetStatus.Submitted : AttendanceSheetStatus.Draft,
|
||||||
|
CheckInMethod = number == 11 ? AttendanceCheckInMethod.QrCode : AttendanceCheckInMethod.Manual
|
||||||
|
}).ToList();
|
||||||
|
db.AddRange(college, course, term);
|
||||||
|
db.AddRange(tasks);
|
||||||
|
db.AddRange(sheets);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var controller = new AttendanceController(db, new AllScope());
|
||||||
|
var taskResult = Assert.IsType<OkObjectResult>(await controller.GetMyTasks(
|
||||||
|
term.Id, "程序设计", 1, 10, CancellationToken.None));
|
||||||
|
var taskPage = Assert.IsType<PagedResult<object>>(taskResult.Value);
|
||||||
|
Assert.Equal(11, taskPage.Total);
|
||||||
|
Assert.Equal(10, taskPage.Items.Count);
|
||||||
|
|
||||||
|
var sheetResult = Assert.IsType<OkObjectResult>(await controller.GetSheets(
|
||||||
|
tasks[0].Id, "期末", AttendanceSheetStatus.Submitted,
|
||||||
|
AttendanceCheckInMethod.QrCode, 1, 10, CancellationToken.None));
|
||||||
|
var sheetPage = Assert.IsType<PagedResult<object>>(sheetResult.Value);
|
||||||
|
Assert.Equal(1, sheetPage.Total);
|
||||||
|
Assert.Single(sheetPage.Items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AllScope : ICurrentUserDataScope
|
||||||
|
{
|
||||||
|
public CurrentUserScope Current { get; } = new(
|
||||||
|
Guid.NewGuid(), "测试管理员", null, DataScope.All,
|
||||||
|
new HashSet<string>([SystemRoles.SuperAdmin]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,16 @@ const isNativeApp = Capacitor.isNativePlatform()
|
|||||||
const now = ref(Date.now())
|
const now = ref(Date.now())
|
||||||
const fileInput = ref<HTMLInputElement>()
|
const fileInput = ref<HTMLInputElement>()
|
||||||
const termId = ref<string>()
|
const termId = ref<string>()
|
||||||
|
const taskKeyword = ref('')
|
||||||
|
const taskPage = ref(1)
|
||||||
|
const taskPageSize = 12
|
||||||
|
const taskTotal = ref(0)
|
||||||
|
const sheetKeyword = ref('')
|
||||||
|
const sheetStatus = ref<string>()
|
||||||
|
const sheetMethod = ref<string>()
|
||||||
|
const sheetPage = ref(1)
|
||||||
|
const sheetPageSize = 12
|
||||||
|
const sheetTotal = ref(0)
|
||||||
const activeMode = ref<'rollcall' | 'statistics'>('rollcall')
|
const activeMode = ref<'rollcall' | 'statistics'>('rollcall')
|
||||||
const studentKeyword = ref('')
|
const studentKeyword = ref('')
|
||||||
const classFilter = ref('')
|
const classFilter = ref('')
|
||||||
@@ -108,18 +118,27 @@ const filteredStudents = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadTasks() {
|
async function loadTasks(resetPage = false) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
tasks.value = (await http.get('/attendance/my-tasks', {
|
if (resetPage) taskPage.value = 1
|
||||||
params: { academicTermId: termId.value || undefined },
|
const response = (await http.get('/attendance/my-tasks', {
|
||||||
|
params: {
|
||||||
|
academicTermId: termId.value || undefined,
|
||||||
|
keyword: taskKeyword.value.trim() || undefined,
|
||||||
|
page: taskPage.value,
|
||||||
|
pageSize: taskPageSize,
|
||||||
|
},
|
||||||
})).data
|
})).data
|
||||||
|
tasks.value = response.items
|
||||||
|
taskTotal.value = response.total
|
||||||
const preferred = tasks.value.find((item: any) => item.id === selectedTask.value?.id)
|
const preferred = tasks.value.find((item: any) => item.id === selectedTask.value?.id)
|
||||||
?? tasks.value[0]
|
?? tasks.value[0]
|
||||||
if (preferred) await selectTask(preferred)
|
if (preferred) await selectTask(preferred)
|
||||||
else {
|
else {
|
||||||
selectedTask.value = null
|
selectedTask.value = null
|
||||||
sheets.value = []
|
sheets.value = []
|
||||||
|
sheetTotal.value = 0
|
||||||
statistics.value = null
|
statistics.value = null
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -137,13 +156,37 @@ async function selectTask(task: any) {
|
|||||||
studentKeyword.value = ''
|
studentKeyword.value = ''
|
||||||
classFilter.value = ''
|
classFilter.value = ''
|
||||||
attentionFilter.value = ''
|
attentionFilter.value = ''
|
||||||
|
sheetPage.value = 1
|
||||||
|
sheetKeyword.value = ''
|
||||||
|
sheetStatus.value = undefined
|
||||||
|
sheetMethod.value = undefined
|
||||||
|
await loadSheets()
|
||||||
|
if (activeMode.value === 'statistics') await loadStatistics()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSheets(resetPage = false) {
|
||||||
|
if (!selectedTask.value) return
|
||||||
try {
|
try {
|
||||||
sheets.value = (await http.get('/attendance/sheets', {
|
if (resetPage) sheetPage.value = 1
|
||||||
params: { teachingTaskId: task.id },
|
const response = (await http.get('/attendance/sheets', {
|
||||||
|
params: {
|
||||||
|
teachingTaskId: selectedTask.value.id,
|
||||||
|
keyword: sheetKeyword.value.trim() || undefined,
|
||||||
|
status: sheetStatus.value,
|
||||||
|
checkInMethod: sheetMethod.value,
|
||||||
|
page: sheetPage.value,
|
||||||
|
pageSize: sheetPageSize,
|
||||||
|
},
|
||||||
})).data
|
})).data
|
||||||
if (activeMode.value === 'statistics') await loadStatistics()
|
sheets.value = response.items
|
||||||
|
sheetTotal.value = response.total
|
||||||
|
if (!sheets.value.some((sheet: any) => sheet.id === selectedSheet.value?.id)) {
|
||||||
|
selectedSheet.value = null
|
||||||
|
sheetDetail.value = null
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sheets.value = []
|
sheets.value = []
|
||||||
|
sheetTotal.value = 0
|
||||||
ElMessage.error(apiErrorMessage(error))
|
ElMessage.error(apiErrorMessage(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,14 +723,22 @@ onUnmounted(() => {
|
|||||||
<h2>教学点名</h2>
|
<h2>教学点名</h2>
|
||||||
<p>完成课堂点名,并持续查看本课程每位学生的出勤表现。</p>
|
<p>完成课堂点名,并持续查看本课程每位学生的出勤表现。</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button :icon="Refresh" @click="loadTasks">刷新</el-button>
|
<el-button :icon="Refresh" @click="() => loadTasks()">刷新</el-button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="attendance-toolbar">
|
<section class="attendance-toolbar">
|
||||||
<el-select v-model="termId" clearable placeholder="全部学期" @change="loadTasks">
|
<el-input
|
||||||
|
v-model="taskKeyword"
|
||||||
|
clearable
|
||||||
|
placeholder="课程、任务号或教学班"
|
||||||
|
@keyup.enter="loadTasks(true)"
|
||||||
|
@clear="loadTasks(true)"
|
||||||
|
/>
|
||||||
|
<el-select v-model="termId" clearable placeholder="全部学期" @change="loadTasks(true)">
|
||||||
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
|
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<span>共 {{ tasks.length }} 个教学班</span>
|
<el-button :icon="Search" @click="loadTasks(true)">查询</el-button>
|
||||||
|
<span>共 {{ taskTotal }} 个教学班</span>
|
||||||
<el-radio-group v-model="activeMode" class="mode-switch" size="small">
|
<el-radio-group v-model="activeMode" class="mode-switch" size="small">
|
||||||
<el-radio-button value="rollcall">点名记录</el-radio-button>
|
<el-radio-button value="rollcall">点名记录</el-radio-button>
|
||||||
<el-radio-button value="statistics">课程统计</el-radio-button>
|
<el-radio-button value="statistics">课程统计</el-radio-button>
|
||||||
@@ -712,6 +763,16 @@ onUnmounted(() => {
|
|||||||
<small>{{ task.termName }} · {{ task.studentCount }} 名学生 · {{ task.sheetCount }} 张考勤表</small>
|
<small>{{ task.termName }} · {{ task.studentCount }} 名学生 · {{ task.sheetCount }} 张考勤表</small>
|
||||||
</button>
|
</button>
|
||||||
<el-empty v-if="!tasks.length" description="没有可点名的教学班" />
|
<el-empty v-if="!tasks.length" description="没有可点名的教学班" />
|
||||||
|
<el-pagination
|
||||||
|
v-if="taskTotal > taskPageSize"
|
||||||
|
small
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:current-page="taskPage"
|
||||||
|
:page-size="taskPageSize"
|
||||||
|
:total="taskTotal"
|
||||||
|
@current-change="(page: number) => { taskPage = page; loadTasks() }"
|
||||||
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<template v-if="activeMode === 'rollcall'">
|
<template v-if="activeMode === 'rollcall'">
|
||||||
@@ -727,6 +788,25 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建考勤表</el-button>
|
<el-button type="primary" :icon="Plus" @click="openCreate">新建考勤表</el-button>
|
||||||
</header>
|
</header>
|
||||||
|
<div class="attendance-sheet-filters">
|
||||||
|
<el-input
|
||||||
|
v-model="sheetKeyword"
|
||||||
|
clearable
|
||||||
|
placeholder="考勤表名称或备注"
|
||||||
|
@keyup.enter="loadSheets(true)"
|
||||||
|
@clear="loadSheets(true)"
|
||||||
|
/>
|
||||||
|
<el-select v-model="sheetStatus" clearable placeholder="全部状态" @change="loadSheets(true)">
|
||||||
|
<el-option label="草稿" value="Draft" />
|
||||||
|
<el-option label="已提交" value="Submitted" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="sheetMethod" clearable placeholder="全部方式" @change="loadSheets(true)">
|
||||||
|
<el-option label="教师点名" value="Manual" />
|
||||||
|
<el-option label="扫码签到" value="QrCode" />
|
||||||
|
<el-option label="定位签到" value="Location" />
|
||||||
|
</el-select>
|
||||||
|
<el-button :icon="Search" @click="loadSheets(true)">筛选</el-button>
|
||||||
|
</div>
|
||||||
<div class="attendance-sheet-list">
|
<div class="attendance-sheet-list">
|
||||||
<button
|
<button
|
||||||
v-for="sheet in sheets"
|
v-for="sheet in sheets"
|
||||||
@@ -766,6 +846,16 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
<el-empty v-if="!sheets.length" description="尚未创建考勤表,点击右上方按钮新建" />
|
<el-empty v-if="!sheets.length" description="尚未创建考勤表,点击右上方按钮新建" />
|
||||||
</div>
|
</div>
|
||||||
|
<el-pagination
|
||||||
|
v-if="sheetTotal > sheetPageSize"
|
||||||
|
small
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
:current-page="sheetPage"
|
||||||
|
:page-size="sheetPageSize"
|
||||||
|
:total="sheetTotal"
|
||||||
|
@current-change="(page: number) => { sheetPage = page; loadSheets() }"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -1192,7 +1282,8 @@ onUnmounted(() => {
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #fbfcfd;
|
background: #fbfcfd;
|
||||||
}
|
}
|
||||||
.attendance-toolbar .el-select { width: 260px; }
|
.attendance-toolbar .el-input,
|
||||||
|
.attendance-toolbar .el-select { width: 220px; }
|
||||||
.attendance-toolbar > span { color: var(--muted); font-size: 12px; }
|
.attendance-toolbar > span { color: var(--muted); font-size: 12px; }
|
||||||
.mode-switch { margin-left: auto; }
|
.mode-switch { margin-left: auto; }
|
||||||
.attendance-workspace {
|
.attendance-workspace {
|
||||||
@@ -1256,6 +1347,21 @@ onUnmounted(() => {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.attendance-sheet-list { flex: 1; overflow-y: auto; }
|
.attendance-sheet-list { flex: 1; overflow-y: auto; }
|
||||||
|
.attendance-sheet-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #fbfcfd;
|
||||||
|
}
|
||||||
|
.attendance-sheet-filters .el-input { flex: 1; min-width: 140px; }
|
||||||
|
.attendance-sheet-filters .el-select { width: 115px; }
|
||||||
|
.attendance-task-list :deep(.el-pagination),
|
||||||
|
.attendance-detail :deep(.el-pagination) {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
.attendance-sheet-list > button {
|
.attendance-sheet-list > button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
@@ -1708,7 +1814,11 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.attendance-toolbar { align-items: stretch; flex-wrap: wrap; }
|
.attendance-toolbar { align-items: stretch; flex-wrap: wrap; }
|
||||||
|
.attendance-toolbar .el-input,
|
||||||
.attendance-toolbar .el-select { width: 100%; }
|
.attendance-toolbar .el-select { width: 100%; }
|
||||||
|
.attendance-sheet-filters { flex-wrap: wrap; }
|
||||||
|
.attendance-sheet-filters .el-input,
|
||||||
|
.attendance-sheet-filters .el-select { width: 100%; flex-basis: 100%; }
|
||||||
.attendance-toolbar > span { align-self: center; }
|
.attendance-toolbar > span { align-self: center; }
|
||||||
.mode-switch { margin-left: auto; }
|
.mode-switch { margin-left: auto; }
|
||||||
.statistics-head { align-items: flex-start; flex-direction: column; }
|
.statistics-head { align-items: flex-start; flex-direction: column; }
|
||||||
|
|||||||
Reference in New Issue
Block a user