多角色账号按 All > College > Class > Self 自动取最高权限。
学院管理员限制在所属学院。 辅导员通过稳定账号 ID 绑定行政班,避免重名串班。 教师只能访问本人档案、授课课程和所授课学生。 学生只能访问本人档案及所在班级课程。 教师/学生角色会自动校验并绑定工号或学号档案。 超级管理员可在用户页面调整角色、学院、工号/学号,并预览生效后的数据范围。
This commit is contained in:
Vendored
+1
@@ -11,6 +11,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface CurrentUser {
|
||||
displayName: string
|
||||
roles: string[]
|
||||
collegeId?: string
|
||||
effectiveDataScope: 'Self' | 'Class' | 'College' | 'All'
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
@@ -124,6 +124,13 @@ button { cursor: pointer; }
|
||||
.table-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||
.table-status.off { color: #a0a6b1; }
|
||||
.role-tag { margin: 2px 4px 2px 0; }
|
||||
.role-editor-summary {
|
||||
display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
|
||||
margin: -4px 0 20px; padding: 14px 16px;
|
||||
border-left: 3px solid var(--teal); background: #f5f7fb;
|
||||
}
|
||||
.role-editor-summary strong { color: var(--ink); font-size: 17px; }
|
||||
.role-editor-summary span { color: var(--muted); font-size: 13px; }
|
||||
.entity-form .el-select, .entity-form .el-date-editor { width: 100%; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
.form-grid.compact { align-items: center; }
|
||||
@@ -332,6 +339,7 @@ button { cursor: pointer; }
|
||||
.schedule-search { flex-wrap: wrap; }
|
||||
.schedule-search .el-input { width: 100%; }
|
||||
.schedule-search > span { width: 100%; margin-left: 0; }
|
||||
.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; }
|
||||
.login-page { display: block; min-height: 100vh; background: #f4f6f9; }
|
||||
|
||||
@@ -24,8 +24,8 @@ const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref('')
|
||||
const keyword = ref('')
|
||||
const references = reactive<Record<string, Row[]>>({
|
||||
campuses: [], colleges: [], majors: [], buildings: [],
|
||||
const references = reactive<Record<string, any[]>>({
|
||||
campuses: [], colleges: [], majors: [], buildings: [], counselors: [],
|
||||
})
|
||||
const form = reactive<Record<string, any>>({})
|
||||
|
||||
@@ -48,7 +48,7 @@ function resetForm(row?: Row) {
|
||||
code: '', name: '', sortOrder: 0, isEnabled: true,
|
||||
campusId: undefined, collegeId: undefined, majorId: undefined, buildingId: undefined,
|
||||
shortName: '', degreeType: '学士', schoolingYears: 4,
|
||||
grade: new Date().getFullYear(), counselorName: '',
|
||||
grade: new Date().getFullYear(), counselorUserId: undefined,
|
||||
academicYear: `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
|
||||
season: 'Autumn', startDate: '', endDate: '', isCurrent: false,
|
||||
capacity: 60, roomType: '普通教室', equipment: '',
|
||||
@@ -71,6 +71,9 @@ async function loadReferences() {
|
||||
await Promise.all(kinds.map(async (kind) => {
|
||||
references[kind] = (await http.get(`/base-data/${kind}`)).data
|
||||
}))
|
||||
if (canManage.value) {
|
||||
references.counselors = (await http.get('/base-data/counselors')).data
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTab() {
|
||||
@@ -168,6 +171,9 @@ onMounted(async () => {
|
||||
<el-table-column v-if="active === 'majors'" prop="degreeType" label="学位类型" min-width="120" />
|
||||
<el-table-column v-if="active === 'classes'" prop="majorName" label="所属专业" min-width="170" />
|
||||
<el-table-column v-if="active === 'classes'" prop="grade" label="年级" width="90" />
|
||||
<el-table-column v-if="active === 'classes'" prop="counselorName" label="辅导员" min-width="120">
|
||||
<template #default="{ row }">{{ row.counselorName || '未分配' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="active === 'terms'" prop="academicYear" label="学年" width="120" />
|
||||
<el-table-column v-if="active === 'terms'" label="当前学期" width="100">
|
||||
<template #default="{ row }"><el-tag v-if="row.isCurrent" type="success">当前</el-tag><span v-else>—</span></template>
|
||||
@@ -221,7 +227,16 @@ onMounted(async () => {
|
||||
</el-form-item>
|
||||
<div v-if="active === 'classes'" class="form-grid">
|
||||
<el-form-item label="年级"><el-input-number v-model="form.grade" :min="2000" :max="2200" /></el-form-item>
|
||||
<el-form-item label="辅导员"><el-input v-model="form.counselorName" /></el-form-item>
|
||||
<el-form-item label="辅导员账号">
|
||||
<el-select v-model="form.counselorUserId" clearable filterable>
|
||||
<el-option
|
||||
v-for="item in references.counselors"
|
||||
:key="item.id"
|
||||
:label="`${item.displayName}${item.staffNumber ? ` · ${item.staffNumber}` : ''}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<template v-if="active === 'terms'">
|
||||
<div class="form-grid">
|
||||
|
||||
+108
-5
@@ -1,24 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { EditPen, Plus, Search } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
|
||||
interface UserRow {
|
||||
id: string; userName: string; displayName: string; staffNumber?: string
|
||||
roles: string[]; isEnabled: boolean; lastLoginAt?: string
|
||||
collegeId?: string; roles: string[]; isEnabled: boolean; lastLoginAt?: string
|
||||
}
|
||||
interface Role { name: string; description: string }
|
||||
type DataScope = 'Self' | 'Class' | 'College' | 'All'
|
||||
interface Role { name: string; description: string; dataScope: DataScope }
|
||||
|
||||
const users = ref<UserRow[]>([])
|
||||
const roles = ref<Role[]>([])
|
||||
const colleges = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const roleDialogVisible = ref(false)
|
||||
const editingUser = ref<UserRow | null>(null)
|
||||
const editingRoles = ref<string[]>([])
|
||||
const editingStaffNumber = ref('')
|
||||
const editingCollegeId = ref<string>()
|
||||
const keyword = ref('')
|
||||
const form = reactive({
|
||||
userName: '', displayName: '', password: '', staffNumber: '',
|
||||
collegeId: undefined as string | undefined, roles: [] as string[],
|
||||
})
|
||||
const scopePriority: Record<DataScope, number> = {
|
||||
Self: 0,
|
||||
Class: 1,
|
||||
College: 2,
|
||||
All: 3,
|
||||
}
|
||||
const scopeNames: Record<DataScope, string> = {
|
||||
Self: '本人数据',
|
||||
Class: '所带班级',
|
||||
College: '所属学院',
|
||||
All: '全校数据',
|
||||
}
|
||||
const editingScope = computed(() => {
|
||||
const values = roles.value
|
||||
.filter((role) => editingRoles.value.includes(role.name))
|
||||
.map((role) => role.dataScope)
|
||||
const effective = values.reduce<DataScope>(
|
||||
(current, value) =>
|
||||
scopePriority[value] > scopePriority[current] ? value : current,
|
||||
'Self',
|
||||
)
|
||||
return scopeNames[effective]
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -63,6 +92,30 @@ async function setStatus(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function openRoleEditor(row: any) {
|
||||
editingUser.value = row
|
||||
editingRoles.value = [...row.roles]
|
||||
editingStaffNumber.value = row.staffNumber ?? ''
|
||||
editingCollegeId.value = row.collegeId
|
||||
roleDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveRoles() {
|
||||
if (!editingUser.value) return
|
||||
try {
|
||||
await http.put(`/users/${editingUser.value.id}/roles`, {
|
||||
staffNumber: editingStaffNumber.value,
|
||||
collegeId: editingCollegeId.value,
|
||||
roles: editingRoles.value,
|
||||
})
|
||||
ElMessage.success('角色与数据范围已更新')
|
||||
roleDialogVisible.value = false
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -102,8 +155,11 @@ onMounted(load)
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }"><span class="table-status" :class="{ off: !row.isEnabled }">{{ row.isEnabled ? '启用' : '停用' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<el-table-column label="操作" width="175">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="EditPen" @click="openRoleEditor(row)">
|
||||
调整角色
|
||||
</el-button>
|
||||
<el-button link :type="row.isEnabled ? 'danger' : 'primary'" @click="setStatus(row)">
|
||||
{{ row.isEnabled ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
@@ -138,5 +194,52 @@ onMounted(load)
|
||||
<el-button type="primary" @click="create">创建账号</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="roleDialogVisible" title="调整角色与数据范围" width="540px">
|
||||
<div v-if="editingUser" class="role-editor-summary">
|
||||
<strong>{{ editingUser.displayName }}</strong>
|
||||
<span>{{ editingUser.userName }} · {{ editingUser.staffNumber || '未关联工号/学号' }}</span>
|
||||
</div>
|
||||
<el-form label-position="top" class="entity-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="工号/学号">
|
||||
<el-input v-model="editingStaffNumber" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属学院">
|
||||
<el-select v-model="editingCollegeId" clearable>
|
||||
<el-option
|
||||
v-for="item in colleges"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="角色" required>
|
||||
<el-select v-model="editingRoles" multiple>
|
||||
<el-option
|
||||
v-for="role in roles"
|
||||
:key="role.name"
|
||||
:label="`${role.name} · ${role.description}`"
|
||||
:value="role.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
type="info"
|
||||
show-icon
|
||||
:title="`保存后有效范围:${editingScope}`"
|
||||
description="多角色账号自动采用最高数据范围。教师或学生角色要求账号的工号/学号能匹配对应档案。"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="roleDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!editingRoles.length" @click="saveRoles">
|
||||
保存角色
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user