优化
This commit is contained in:
1 parent
e5afe60a11
commit
7994ed1ed3
8 files changed
+408
-13
No files matched your search
@@ -348,8 +348,8 @@ onMounted(() => {
|
||||
<b>{{ auth.user?.displayName ?? '系统管理员' }}</b>
|
||||
<span>{{ auth.user?.roles?.[0] ?? '教务人员' }}</span>
|
||||
</div>
|
||||
<el-button text @click="router.push('/account')">个人账户</el-button>
|
||||
<el-button text @click="signOut">退出</el-button>
|
||||
<el-button class="desktop-account-action" text @click="router.push('/account')">个人账户</el-button>
|
||||
<el-button class="desktop-account-action" text @click="signOut">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -431,6 +431,16 @@ onMounted(() => {
|
||||
</template>
|
||||
</el-menu>
|
||||
|
||||
<div class="mobile-account-actions">
|
||||
<button type="button" @click="router.push('/account'); mobileMenu = false">
|
||||
<span>个人账户</span>
|
||||
<small>{{ auth.user?.displayName ?? '当前用户' }}</small>
|
||||
</button>
|
||||
<button type="button" class="mobile-sign-out" @click="signOut">
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<div v-if="mobileMenu" class="mobile-mask" @click="mobileMenu = false" />
|
||||
|
||||
+23
-9
@@ -16,21 +16,24 @@
|
||||
--el-color-primary: #233876;
|
||||
--el-color-success: #087f73;
|
||||
--el-border-radius-base: 7px;
|
||||
--safe-area-top: env(safe-area-inset-top, 0px);
|
||||
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; }
|
||||
html { min-height: 100%; background: #f3f5f8; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; min-height: 100dvh; }
|
||||
button, input, textarea, select { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
#app { min-height: 100vh; }
|
||||
#app { min-height: 100vh; min-height: 100dvh; }
|
||||
|
||||
.shell { min-height: 100vh; }
|
||||
.shell { min-height: 100vh; min-height: 100dvh; }
|
||||
.app-header {
|
||||
position: sticky; top: 0; z-index: 20;
|
||||
box-shadow: 0 5px 18px rgba(19,34,73,.08);
|
||||
}
|
||||
.topbar {
|
||||
height: 70px; padding: 0 32px; display: flex; align-items: center; gap: 24px;
|
||||
min-height: calc(70px + var(--safe-area-top)); padding: var(--safe-area-top) 32px 0; display: flex; align-items: center; gap: 24px;
|
||||
color: white; background:
|
||||
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px),
|
||||
linear-gradient(112deg, #152550 0%, #1d326c 72%, #176a70 130%);
|
||||
@@ -83,7 +86,7 @@ button { cursor: pointer; }
|
||||
.horizontal-menu .el-sub-menu__icon-arrow { display: none; }
|
||||
.mobile-navigation {
|
||||
position: fixed; inset: 0 auto 0 0; z-index: 30; width: 286px;
|
||||
display: flex; flex-direction: column; overflow-y: auto;
|
||||
display: flex; flex-direction: column; overflow-y: auto; padding-top: var(--safe-area-top);
|
||||
color: #eef2ff; background:
|
||||
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px),
|
||||
@@ -96,6 +99,12 @@ button { cursor: pointer; }
|
||||
min-height: 70px; padding: 0 18px 0 20px; display: flex; align-items: center;
|
||||
justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,.1);
|
||||
}
|
||||
.mobile-account-actions { margin: auto 20px calc(18px + var(--safe-area-bottom)); padding-top: 14px; display: grid; gap: 8px; border-top: 1px solid rgba(255,255,255,.12); }
|
||||
.mobile-account-actions button { min-height: 46px; padding: 10px 13px; display: flex; align-items: center; justify-content: space-between; border: 1px solid rgba(255,255,255,.13); border-radius: 7px; color: #eef2ff; background: rgba(255,255,255,.06); text-align: left; }
|
||||
.mobile-account-actions button:hover { background: rgba(255,255,255,.12); }
|
||||
.mobile-account-actions button span { font-size: 13px; font-weight: 650; }
|
||||
.mobile-account-actions button small { color: #aeb9d8; font-size: 11px; }
|
||||
.mobile-account-actions .mobile-sign-out { justify-content: center; color: #cbd3ea; background: transparent; }
|
||||
.mobile-navigation-head button {
|
||||
width: 34px; height: 34px; border: 1px solid rgba(255,255,255,.17);
|
||||
border-radius: 6px; color: #cbd3ea; background: rgba(255,255,255,.05);
|
||||
@@ -119,7 +128,7 @@ button { cursor: pointer; }
|
||||
width: 38px; height: 38px; border: 1px solid rgba(255,255,255,.18);
|
||||
border-radius: 7px; color: white; background: rgba(255,255,255,.07);
|
||||
}
|
||||
.content { padding: 28px 32px 48px; max-width: 1540px; margin: 0 auto; }
|
||||
.content { padding: 28px 32px calc(48px + var(--safe-area-bottom)); max-width: 1540px; margin: 0 auto; }
|
||||
.mobile-only { display: none; }
|
||||
|
||||
.section-kicker { color: var(--teal); font: 700 11px/1.2 "Microsoft YaHei", sans-serif; letter-spacing: .1em; }
|
||||
@@ -1289,7 +1298,7 @@ button { cursor: pointer; }
|
||||
.mobile-mask { position: fixed; inset: 0; z-index: 25; background: rgba(13,24,48,.52); }
|
||||
.desktop-only { display: none; }
|
||||
.mobile-only { display: inline-grid; place-items: center; }
|
||||
.topbar { height: 66px; padding: 0 16px; gap: 13px; }
|
||||
.topbar { min-height: calc(66px + var(--safe-area-top)); padding: var(--safe-area-top) 16px 0; gap: 13px; }
|
||||
.top-brand { min-width: 0; padding-right: 0; border-right: none; }
|
||||
.top-brand .brand-mark { width: 27px; height: 27px; }
|
||||
.top-brand strong { font-size: 17px; }
|
||||
@@ -1298,7 +1307,8 @@ button { cursor: pointer; }
|
||||
.user-copy { display: none; }
|
||||
.avatar { width: 32px; height: 32px; font-size: 12px; }
|
||||
.topbar .user-block { gap: 4px; }
|
||||
.content { padding: 18px 14px 35px; }
|
||||
.desktop-account-action { display: none; }
|
||||
.content { padding: 18px 14px calc(35px + var(--safe-area-bottom)); }
|
||||
.term-hero { min-height: 220px; padding: 25px; flex-direction: column; align-items: flex-start; justify-content: flex-end; }
|
||||
.term-hero button { margin: 22px 0 0; width: 100%; }
|
||||
.metric-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
@@ -1392,7 +1402,7 @@ button { cursor: pointer; }
|
||||
.exam-row-actions { grid-column:2/-1; padding-bottom:10px; text-align:left; }
|
||||
.role-editor-summary { align-items: flex-start; flex-direction: column; gap: 4px; }
|
||||
.form-grid, .form-grid.three { grid-template-columns: 1fr; gap: 0; }
|
||||
.el-dialog { width: calc(100vw - 24px) !important; }
|
||||
.el-dialog { width: calc(100vw - 24px) !important; max-height: calc(100dvh - var(--safe-area-top) - var(--safe-area-bottom) - 24px); overflow-y: auto; }
|
||||
.login-page { display: block; min-height: 100vh; background: #f4f6f9; }
|
||||
.login-story { min-height: 310px; padding: 30px 25px; }
|
||||
.story-copy { margin: auto 0 0; }
|
||||
@@ -1409,6 +1419,10 @@ button { cursor: pointer; }
|
||||
.option-filter-grid.classes { grid-template-columns: 1fr; }
|
||||
.batch-action-bar > div:last-child { width: 100%; }
|
||||
.batch-action-bar > div:last-child .el-button { flex: 1 1 auto; }
|
||||
.page-actions, .page-intro-actions { gap: 8px; }
|
||||
.page-actions .el-button, .page-intro-actions .el-button { min-height: 42px; margin-left: 0; }
|
||||
.pagination-bar { min-height: 0; padding: 12px 14px; align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||
.pagination-bar .el-pagination { width: 100%; overflow-x: auto; justify-content: flex-start; }
|
||||
.selection-round-strip > button { flex-basis: 240px; }
|
||||
.selection-window { display: block; }
|
||||
.window-seal { min-height: 54px; padding: 10px; display: flex; border-right: none; border-bottom: 1px solid rgba(255,255,255,.13); }
|
||||
|
||||
@@ -25,6 +25,7 @@ const importing = ref(false)
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const activationDialog = ref(false)
|
||||
const activationLoading = ref(false)
|
||||
@@ -54,6 +55,11 @@ const canManage = computed(() =>
|
||||
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role),
|
||||
) ?? false,
|
||||
)
|
||||
const canExport = computed(() =>
|
||||
auth.user?.roles.some((role) =>
|
||||
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(role),
|
||||
) ?? false,
|
||||
)
|
||||
const scopedCollegeId = computed(() =>
|
||||
auth.user?.roles.some((role) => ['CollegeAdmin', 'Counselor'].includes(role))
|
||||
? auth.user.collegeId
|
||||
@@ -179,6 +185,17 @@ async function load() {
|
||||
const { data } = await http.get<PageResult>(`/personnel/${active.value}`, { params })
|
||||
rows.value = data.items
|
||||
total.value = data.total
|
||||
selectedRows.value = []
|
||||
if (active.value === 'students' && rows.value.length) {
|
||||
const ids = rows.value.map((row) => row.id)
|
||||
const { data: summaries } = await http.get('/personnel/students/current-courses', {
|
||||
params: { studentIds: ids },
|
||||
})
|
||||
const byId = new Map(summaries.map((item: any) => [item.id, item]))
|
||||
rows.value = rows.value.map((row) => ({ ...row, ...(byId.get(row.id) ?? {
|
||||
currentCourses: [], totalCredits: 0,
|
||||
}) }))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
@@ -252,6 +269,28 @@ async function exportRows() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportStudentCurrentCourses(selectedOnly = false) {
|
||||
if (selectedOnly && selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先勾选需要导出的学生。')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await downloadApiFile(
|
||||
'/personnel/students/current-courses/export',
|
||||
'学生正在修读课程.xlsx',
|
||||
{ params: { ...exportParams(), studentIds: selectedOnly
|
||||
? selectedRows.value.map((row) => row.id) : undefined } },
|
||||
)
|
||||
ElMessage.success(selectedOnly ? `已导出所选 ${selectedRows.value.length} 名学生` : '已导出筛选学生的修读课程')
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(selection: any[]) {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
function chooseImportFile() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
@@ -420,6 +459,7 @@ watch(active, async () => {
|
||||
<el-select v-model="query.majorId" clearable filterable placeholder="全部专业">
|
||||
<el-option v-for="item in filteredMajors" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<el-input-number v-model="query.enrollmentYear" :min="2000" :max="2200" controls-position="right" placeholder="入学年级" />
|
||||
<el-select v-model="query.classId" clearable filterable placeholder="全部班级">
|
||||
<el-option v-for="item in filteredClasses" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
@@ -445,10 +485,15 @@ watch(active, async () => {
|
||||
@change="handleImport"
|
||||
/>
|
||||
</template>
|
||||
<el-button :icon="Download" @click="exportRows">导出结果</el-button>
|
||||
<el-button v-if="canExport" :icon="Download" @click="exportRows">导出结果</el-button>
|
||||
<template v-if="active === 'students' && canExport">
|
||||
<el-button :icon="Download" type="primary" @click="exportStudentCurrentCourses()">导出修读课程</el-button>
|
||||
<el-button :icon="Download" :disabled="selectedRows.length === 0" @click="exportStudentCurrentCourses(true)">导出所选({{ selectedRows.length }})</el-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" class="data-table registry-table">
|
||||
<el-table v-loading="loading" :data="rows" class="data-table registry-table" @selection-change="onSelectionChange">
|
||||
<el-table-column v-if="active === 'students'" type="selection" width="48" />
|
||||
<el-table-column :label="numberLabel" width="135">
|
||||
<template #default="{ row }"><span class="registry-number">{{ active === 'teachers' ? row.teacherNumber : row.studentNumber }}</span></template>
|
||||
</el-table-column>
|
||||
@@ -470,6 +515,15 @@ watch(active, async () => {
|
||||
<el-table-column v-if="active === 'students'" prop="className" label="行政班" min-width="155" />
|
||||
<el-table-column v-if="active === 'students'" prop="majorName" label="专业" min-width="170" />
|
||||
<el-table-column v-if="active === 'students'" prop="enrollmentYear" label="入学年级" width="105" />
|
||||
<el-table-column v-if="active === 'students'" label="正在修读课程 / 学分" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.currentCourses?.length">{{ row.currentCourses.map((course: any) => `${course.courseName}(${course.credits}学分)`).join(';') }}</span>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="active === 'students'" label="总学分" width="95">
|
||||
<template #default="{ row }">{{ row.totalCredits ?? 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="95">
|
||||
<template #default="{ row }">
|
||||
<span class="table-status" :class="{ off: row.status !== 'Active' }">
|
||||
|
||||
Reference in new issue
Block a user