优化条件筛选

This commit is contained in:
2026-08-11 16:42:10 +08:00 Unverified
parent eb20211f0e
commit 130bffbd01
6 changed files with 152 additions and 1 deletions
@@ -237,6 +237,10 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
int? page, int? page,
int? pageSize, int? pageSize,
string? keyword, string? keyword,
Guid? collegeId,
Guid? majorId,
int? grade,
bool? isEnabled,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (page.HasValue || pageSize.HasValue) if (page.HasValue || pageSize.HasValue)
@@ -245,6 +249,11 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
var source = db.AdministrativeClasses.AsNoTracking(); var source = db.AdministrativeClasses.AsNoTracking();
if (collegeId.HasValue)
source = source.Where(x => x.Major!.CollegeId == collegeId.Value);
if (majorId.HasValue) source = source.Where(x => x.MajorId == majorId.Value);
if (grade.HasValue) source = source.Where(x => x.Grade == grade.Value);
if (isEnabled.HasValue) source = source.Where(x => x.IsEnabled == isEnabled.Value);
if (!string.IsNullOrWhiteSpace(keyword)) if (!string.IsNullOrWhiteSpace(keyword))
{ {
keyword = keyword.Trim(); keyword = keyword.Trim();
@@ -511,6 +520,11 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
int? page, int? page,
int? pageSize, int? pageSize,
string? keyword, string? keyword,
Guid? campusId,
Guid? buildingId,
int? minimumCapacity,
TeachingVenueNature? teachingVenueNature,
bool? isEnabled,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (page.HasValue || pageSize.HasValue) if (page.HasValue || pageSize.HasValue)
@@ -519,6 +533,14 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
var source = db.Classrooms.AsNoTracking(); var source = db.Classrooms.AsNoTracking();
if (campusId.HasValue)
source = source.Where(x => x.Building!.CampusId == campusId.Value);
if (buildingId.HasValue) source = source.Where(x => x.BuildingId == buildingId.Value);
if (minimumCapacity.HasValue)
source = source.Where(x => x.Capacity >= minimumCapacity.Value);
if (teachingVenueNature.HasValue)
source = source.Where(x => (x.TeachingVenueNature & teachingVenueNature.Value) != 0);
if (isEnabled.HasValue) source = source.Where(x => x.IsEnabled == isEnabled.Value);
if (!string.IsNullOrWhiteSpace(keyword)) if (!string.IsNullOrWhiteSpace(keyword))
{ {
keyword = keyword.Trim(); keyword = keyword.Trim();
@@ -93,6 +93,9 @@ public sealed class GradeAnalyticsController(
public async Task<ActionResult> GetTeachingClasses( public async Task<ActionResult> GetTeachingClasses(
Guid? academicTermId, Guid? academicTermId,
string? keyword, string? keyword,
Guid? collegeId = null,
string? teacherKeyword = null,
bool? riskOnly = null,
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -105,12 +108,23 @@ public sealed class GradeAnalyticsController(
.Where(x => VisibleTeachingTasks().Any(task => task.Id == x.TeachingTaskId)); .Where(x => VisibleTeachingTasks().Any(task => task.Id == x.TeachingTaskId));
if (academicTermId.HasValue) if (academicTermId.HasValue)
source = source.Where(x => x.AcademicTermId == academicTermId); source = source.Where(x => x.AcademicTermId == academicTermId);
if (collegeId.HasValue)
source = source.Where(x => x.TeachingTask!.Course!.CollegeId == collegeId.Value);
if (keyword is not null) if (keyword is not null)
source = source.Where(x => source = source.Where(x =>
x.TeachingTask!.TaskNumber.Contains(keyword) || x.TeachingTask!.TaskNumber.Contains(keyword) ||
x.TeachingTask.Name.Contains(keyword) || x.TeachingTask.Name.Contains(keyword) ||
x.TeachingTask.Course!.Code.Contains(keyword) || x.TeachingTask.Course!.Code.Contains(keyword) ||
x.TeachingTask.Course.Name.Contains(keyword)); x.TeachingTask.Course.Name.Contains(keyword));
if (!string.IsNullOrWhiteSpace(teacherKeyword))
{
teacherKeyword = teacherKeyword.Trim();
source = source.Where(x => x.TeachingTask!.Teachers.Any(item =>
item.Teacher!.Name.Contains(teacherKeyword) ||
item.Teacher.TeacherNumber.Contains(teacherKeyword)));
}
if (riskOnly == true)
source = source.Where(x => x.PassRate < 60 || x.AverageScore < 60);
var total = await source.CountAsync(cancellationToken); var total = await source.CountAsync(cancellationToken);
var items = await source var items = await source
@@ -24,6 +24,10 @@ public sealed class UsersController(
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
string? keyword = null, string? keyword = null,
string? roleName = null,
Guid? collegeId = null,
bool? isEnabled = null,
bool? hasLoggedIn = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (page < 1 || pageSize is < 1 or > 100) if (page < 1 || pageSize is < 1 or > 100)
@@ -42,6 +46,21 @@ public sealed class UsersController(
where userRole.UserId == user.Id && role.Name!.Contains(keyword) where userRole.UserId == user.Id && role.Name!.Contains(keyword)
select role.Id).Any()); select role.Id).Any());
} }
if (!string.IsNullOrWhiteSpace(roleName))
{
roleName = roleName.Trim();
query = query.Where(user =>
(from userRole in db.UserRoles
join candidateRole in db.Roles on userRole.RoleId equals candidateRole.Id
where userRole.UserId == user.Id && candidateRole.Name == roleName
select userRole.RoleId).Any());
}
if (collegeId.HasValue) query = query.Where(user => user.CollegeId == collegeId.Value);
if (isEnabled.HasValue) query = query.Where(user => user.IsEnabled == isEnabled.Value);
if (hasLoggedIn.HasValue)
query = hasLoggedIn.Value
? query.Where(user => user.LastLoginAt != null)
: query.Where(user => user.LastLoginAt == null);
var total = await query.CountAsync(cancellationToken); var total = await query.CountAsync(cancellationToken);
var users = await query var users = await query
+57
View File
@@ -77,6 +77,16 @@ const references = reactive<Record<string, any[]>>({
campuses: [], colleges: [], majors: [], buildings: [], counselors: [], campuses: [], colleges: [], majors: [], buildings: [], counselors: [],
}) })
const form = reactive<Record<string, any>>({}) const form = reactive<Record<string, any>>({})
const listFilters = reactive({
collegeId: undefined as string | undefined,
majorId: undefined as string | undefined,
grade: undefined as number | undefined,
campusId: undefined as string | undefined,
buildingId: undefined as string | undefined,
minimumCapacity: undefined as number | undefined,
teachingVenueNature: undefined as number | undefined,
isEnabled: undefined as boolean | undefined,
})
const title = computed(() => tabs.value.find((x) => x.key === active.value)?.label ?? '') const title = computed(() => tabs.value.find((x) => x.key === active.value)?.label ?? '')
const canManage = computed(() => const canManage = computed(() =>
@@ -90,6 +100,10 @@ const filteredRows = computed(() => {
.filter(Boolean).some((value) => String(value).toLowerCase().includes(q)), .filter(Boolean).some((value) => String(value).toLowerCase().includes(q)),
) )
}) })
const filteredMajors = computed(() => references.majors.filter((item: any) =>
!listFilters.collegeId || item.collegeId === listFilters.collegeId))
const filteredBuildings = computed(() => references.buildings.filter((item: any) =>
!listFilters.campusId || item.campusId === listFilters.campusId))
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
const total = ref(0) const total = ref(0)
@@ -154,6 +168,18 @@ async function load(resetPage = true) {
page: currentPage.value, page: currentPage.value,
pageSize: pageSize.value, pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined, keyword: keyword.value.trim() || undefined,
...(active.value === 'classes' ? {
collegeId: listFilters.collegeId,
majorId: listFilters.majorId,
grade: listFilters.grade,
isEnabled: listFilters.isEnabled,
} : {
campusId: listFilters.campusId,
buildingId: listFilters.buildingId,
minimumCapacity: listFilters.minimumCapacity,
teachingVenueNature: listFilters.teachingVenueNature,
isEnabled: listFilters.isEnabled,
}),
} }
: undefined, : undefined,
}) })
@@ -187,6 +213,11 @@ async function loadReferences() {
async function changeTab() { async function changeTab() {
keyword.value = '' keyword.value = ''
Object.assign(listFilters, {
collegeId: undefined, majorId: undefined, grade: undefined,
campusId: undefined, buildingId: undefined, minimumCapacity: undefined,
teachingVenueNature: undefined, isEnabled: undefined,
})
await load() await load()
} }
@@ -388,6 +419,32 @@ watch(
<div class="table-toolbar"> <div class="table-toolbar">
<el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索编码、名称或所属单位" /> <el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索编码、名称或所属单位" />
<template v-if="active === 'classes'">
<el-select v-model="listFilters.collegeId" clearable placeholder="全部学院" @change="() => { listFilters.majorId = undefined; load() }">
<el-option v-for="item in references.colleges" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="listFilters.majorId" clearable filterable placeholder="全部专业" @change="load()">
<el-option v-for="item in filteredMajors" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-input-number v-model="listFilters.grade" :min="1900" :max="2200" controls-position="right" placeholder="入学年级" @change="load()" />
</template>
<template v-else-if="active === 'classrooms'">
<el-select v-model="listFilters.campusId" clearable placeholder="全部校区" @change="() => { listFilters.buildingId = undefined; load() }">
<el-option v-for="item in references.campuses" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="listFilters.buildingId" clearable filterable placeholder="全部教学楼" @change="load()">
<el-option v-for="item in filteredBuildings" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="listFilters.teachingVenueNature" clearable placeholder="场地性质" @change="load()">
<el-option v-for="item in venueNatureOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-input-number v-model="listFilters.minimumCapacity" :min="1" controls-position="right" placeholder="最小容量" @change="load()" />
</template>
<el-select v-if="serverPaged" v-model="listFilters.isEnabled" clearable placeholder="全部状态" @change="load()">
<el-option label="启用" :value="true" />
<el-option label="停用" :value="false" />
</el-select>
<el-button :icon="Search" @click="load()">查询</el-button>
<el-button :icon="Refresh" @click="() => load(false)">刷新</el-button> <el-button :icon="Refresh" @click="() => load(false)">刷新</el-button>
<template v-if="canManage"> <template v-if="canManage">
<el-button :icon="Document" @click="downloadTemplate">下载模板</el-button> <el-button :icon="Document" @click="downloadTemplate">下载模板</el-button>
+18 -1
View File
@@ -39,6 +39,7 @@ interface TeachingClassItem {
} }
const terms = ref<any[]>([]) const terms = ref<any[]>([])
const colleges = ref<any[]>([])
const auth = useAuthStore() const auth = useAuthStore()
const canManageSchedule = computed(() => auth.user?.roles.some(role => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false) const canManageSchedule = computed(() => auth.user?.roles.some(role => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false)
const classes = ref<TeachingClassItem[]>([]) const classes = ref<TeachingClassItem[]>([])
@@ -46,6 +47,9 @@ const selected = ref<TeachingClassItem>()
const report = ref<any>() const report = ref<any>()
const termId = ref<string>() const termId = ref<string>()
const keyword = ref('') const keyword = ref('')
const collegeId = ref<string>()
const teacherKeyword = ref('')
const riskOnly = ref(false)
const page = ref(1) const page = ref(1)
const total = ref(0) const total = ref(0)
const pageSize = 12 const pageSize = 12
@@ -109,6 +113,9 @@ async function loadClasses(reset = false) {
params: { params: {
academicTermId: termId.value, academicTermId: termId.value,
keyword: keyword.value.trim() || undefined, keyword: keyword.value.trim() || undefined,
collegeId: collegeId.value,
teacherKeyword: teacherKeyword.value.trim() || undefined,
riskOnly: riskOnly.value || undefined,
page: page.value, page: page.value,
pageSize, pageSize,
}, },
@@ -343,7 +350,12 @@ watch(historyMetric, async () => {
onMounted(async () => { onMounted(async () => {
try { try {
terms.value = (await http.get('/base-data/terms')).data const [termResponse, collegeResponse] = await Promise.all([
http.get('/base-data/terms'),
http.get('/base-data/colleges'),
])
terms.value = termResponse.data
colleges.value = collegeResponse.data
termId.value = defaultAcademicTermId(terms.value) termId.value = defaultAcademicTermId(terms.value)
} catch (error) { } catch (error) {
ElMessage.error(apiErrorMessage(error)) ElMessage.error(apiErrorMessage(error))
@@ -409,7 +421,12 @@ onBeforeUnmount(() => {
<el-select v-model="termId" clearable placeholder="全部学期" style="width: 240px" @change="loadClasses(true)"> <el-select v-model="termId" clearable placeholder="全部学期" style="width: 240px" @change="loadClasses(true)">
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" /> <el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" />
</el-select> </el-select>
<el-select v-model="collegeId" clearable placeholder="全部开课学院" style="width: 190px" @change="loadClasses(true)">
<el-option v-for="college in colleges" :key="college.id" :label="college.name" :value="college.id" />
</el-select>
<el-input v-model="keyword" clearable placeholder="课程、教学班名称或编号" :prefix-icon="Search" @keyup.enter="loadClasses(true)" /> <el-input v-model="keyword" clearable placeholder="课程、教学班名称或编号" :prefix-icon="Search" @keyup.enter="loadClasses(true)" />
<el-input v-model="teacherKeyword" clearable placeholder="任课教师姓名或工号" :prefix-icon="Search" @keyup.enter="loadClasses(true)" @clear="loadClasses(true)" />
<el-checkbox v-model="riskOnly" @change="loadClasses(true)">仅看预警班</el-checkbox>
<el-button type="primary" @click="loadClasses(true)">查询</el-button> <el-button type="primary" @click="loadClasses(true)">查询</el-button>
</section> </section>
+22
View File
@@ -28,6 +28,10 @@ const editingRoles = ref<string[]>([])
const editingStaffNumber = ref('') const editingStaffNumber = ref('')
const editingCollegeId = ref<string>() const editingCollegeId = ref<string>()
const keyword = ref('') const keyword = ref('')
const roleFilter = ref<string>()
const collegeFilter = ref<string>()
const enabledFilter = ref<boolean>()
const loginFilter = ref<boolean>()
const page = ref(1) const page = ref(1)
const total = ref(0) const total = ref(0)
const pageSize = 20 const pageSize = 20
@@ -69,6 +73,10 @@ async function load(resetPage = false) {
page: page.value, page: page.value,
pageSize, pageSize,
keyword: keyword.value.trim() || undefined, keyword: keyword.value.trim() || undefined,
roleName: roleFilter.value,
collegeId: collegeFilter.value,
isEnabled: enabledFilter.value,
hasLoggedIn: loginFilter.value,
}, },
}), }),
http.get('/users/roles'), http.get('/users/roles'),
@@ -231,6 +239,20 @@ onMounted(load)
@clear="load(true)" @clear="load(true)"
@keyup.enter="load(true)" @keyup.enter="load(true)"
/> />
<el-select v-model="roleFilter" clearable placeholder="全部角色" @change="load(true)">
<el-option v-for="role in roles" :key="role.name" :label="role.name" :value="role.name" />
</el-select>
<el-select v-model="collegeFilter" clearable placeholder="全部学院" @change="load(true)">
<el-option v-for="college in colleges" :key="college.id" :label="college.name" :value="college.id" />
</el-select>
<el-select v-model="enabledFilter" clearable placeholder="全部账号状态" @change="load(true)">
<el-option label="启用" :value="true" />
<el-option label="停用" :value="false" />
</el-select>
<el-select v-model="loginFilter" clearable placeholder="全部登录情况" @change="load(true)">
<el-option label="已登录过" :value="true" />
<el-option label="从未登录" :value="false" />
</el-select>
<el-button :icon="Search" @click="load(true)">查询</el-button> <el-button :icon="Search" @click="load(true)">查询</el-button>
<el-button :icon="Document" @click="downloadTemplate">下载模板</el-button> <el-button :icon="Document" @click="downloadTemplate">下载模板</el-button>
<el-button :icon="Upload" :loading="importing" @click="chooseImportFile">Excel 导入</el-button> <el-button :icon="Upload" :loading="importing" @click="chooseImportFile">Excel 导入</el-button>