1
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
type Kind = 'campuses' | 'colleges' | 'majors' | 'classes' | 'terms' | 'buildings' | 'classrooms'
|
||||
interface Row { id: string; code: string; name: string; isEnabled: boolean; [key: string]: unknown }
|
||||
|
||||
const tabs: { key: Kind; label: string; hint: string }[] = [
|
||||
{ key: 'campuses', label: '校区', hint: '学校的物理校区' },
|
||||
{ key: 'colleges', label: '学院', hint: '教学组织单位' },
|
||||
{ key: 'majors', label: '专业', hint: '专业与授予学位' },
|
||||
{ key: 'classes', label: '行政班', hint: '按年级组织的班级' },
|
||||
{ key: 'terms', label: '学期', hint: '教学运行时间轴' },
|
||||
{ key: 'buildings', label: '教学楼', hint: '校区内教学建筑' },
|
||||
{ key: 'classrooms', label: '教室', hint: '可排课教学空间' },
|
||||
]
|
||||
|
||||
const auth = useAuthStore()
|
||||
const active = ref<Kind>('campuses')
|
||||
const rows = ref<Row[]>([])
|
||||
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 form = reactive<Record<string, any>>({})
|
||||
|
||||
const title = computed(() => tabs.find((x) => x.key === active.value)?.label ?? '')
|
||||
const canManage = computed(() =>
|
||||
auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false,
|
||||
)
|
||||
const filteredRows = computed(() => {
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return rows.value
|
||||
return rows.value.filter((row) =>
|
||||
[row.code, row.name, row.collegeName, row.campusName, row.majorName, row.buildingName]
|
||||
.filter(Boolean).some((value) => String(value).toLowerCase().includes(q)),
|
||||
)
|
||||
})
|
||||
|
||||
function resetForm(row?: Row) {
|
||||
Object.keys(form).forEach((key) => delete form[key])
|
||||
Object.assign(form, {
|
||||
code: '', name: '', sortOrder: 0, isEnabled: true,
|
||||
campusId: undefined, collegeId: undefined, majorId: undefined, buildingId: undefined,
|
||||
shortName: '', degreeType: '学士', schoolingYears: 4,
|
||||
grade: new Date().getFullYear(), counselorName: '',
|
||||
academicYear: `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
|
||||
season: 'Autumn', startDate: '', endDate: '', isCurrent: false,
|
||||
capacity: 60, roomType: '普通教室', equipment: '',
|
||||
}, row ?? {})
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = (await http.get(`/base-data/${active.value}`)).data
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReferences() {
|
||||
const kinds = ['campuses', 'colleges', 'majors', 'buildings'] as const
|
||||
await Promise.all(kinds.map(async (kind) => {
|
||||
references[kind] = (await http.get(`/base-data/${kind}`)).data
|
||||
}))
|
||||
}
|
||||
|
||||
async function changeTab() {
|
||||
keyword.value = ''
|
||||
await load()
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = ''
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: any) {
|
||||
editingId.value = row.id
|
||||
resetForm(row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.code?.trim() || !form.name?.trim()) {
|
||||
ElMessage.warning('请填写编码和名称。')
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await http.put(`/base-data/${active.value}/${editingId.value}`, form)
|
||||
} else {
|
||||
await http.post(`/base-data/${active.value}`, form)
|
||||
}
|
||||
ElMessage.success(editingId.value ? '已保存修改' : `已新增${title.value}`)
|
||||
dialogVisible.value = false
|
||||
await Promise.all([load(), loadReferences()])
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除“${row.name}”吗?已被业务引用的数据将无法删除。`,
|
||||
'删除确认',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||
)
|
||||
await http.delete(`/base-data/${active.value}/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([load(), loadReferences()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-stack">
|
||||
<section class="page-intro">
|
||||
<div>
|
||||
<span class="section-kicker">MASTER DATA</span>
|
||||
<h2>基础数据</h2>
|
||||
<p>先建立稳定的组织与教学资源编码,后续业务均从这里引用。</p>
|
||||
</div>
|
||||
<el-button v-if="canManage" type="primary" :icon="Plus" @click="openCreate">新增{{ title }}</el-button>
|
||||
</section>
|
||||
|
||||
<section class="data-card">
|
||||
<nav class="data-tabs" aria-label="基础数据分类">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
type="button"
|
||||
:class="{ active: active === tab.key }"
|
||||
@click="active = tab.key; changeTab()"
|
||||
>
|
||||
<b>{{ tab.label }}</b>
|
||||
<span>{{ tab.hint }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="table-toolbar">
|
||||
<el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索编码、名称或所属单位" />
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
<span>共 {{ filteredRows.length }} 条</span>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="filteredRows" class="data-table">
|
||||
<el-table-column prop="code" label="编码" min-width="130" />
|
||||
<el-table-column prop="name" label="名称" min-width="180" />
|
||||
<el-table-column v-if="active === 'colleges'" prop="campusName" label="所属校区" min-width="140" />
|
||||
<el-table-column v-if="active === 'majors'" prop="collegeName" label="所属学院" min-width="150" />
|
||||
<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 === '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>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="active === 'buildings'" prop="campusName" label="所属校区" min-width="140" />
|
||||
<el-table-column v-if="active === 'classrooms'" prop="buildingName" label="教学楼" min-width="130" />
|
||||
<el-table-column v-if="active === 'classrooms'" prop="capacity" label="容量" width="90" />
|
||||
<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 v-if="canManage" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无数据,点击右上角开始新增" /></template>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${title}`" width="560px">
|
||||
<el-form label-position="top" class="entity-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="编码" required><el-input v-model="form.code" placeholder="用于唯一识别" /></el-form-item>
|
||||
<el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item v-if="active === 'campuses'" label="地址">
|
||||
<el-input v-model="form.description" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="active === 'colleges' || active === 'buildings'" label="所属校区">
|
||||
<el-select v-model="form.campusId" clearable>
|
||||
<el-option v-for="item in references.campuses" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="active === 'colleges'" label="学院简称"><el-input v-model="form.shortName" /></el-form-item>
|
||||
<el-form-item v-if="active === 'majors'" label="所属学院" required>
|
||||
<el-select v-model="form.collegeId">
|
||||
<el-option v-for="item in references.colleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div v-if="active === 'majors'" class="form-grid">
|
||||
<el-form-item label="学位类型"><el-input v-model="form.degreeType" /></el-form-item>
|
||||
<el-form-item label="学制"><el-input-number v-model="form.schoolingYears" :min="1" :max="8" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item v-if="active === 'classes'" label="所属专业" required>
|
||||
<el-select v-model="form.majorId" filterable>
|
||||
<el-option v-for="item in references.majors" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</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>
|
||||
</div>
|
||||
<template v-if="active === 'terms'">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="学年"><el-input v-model="form.academicYear" /></el-form-item>
|
||||
<el-form-item label="学期季">
|
||||
<el-select v-model="form.season">
|
||||
<el-option label="秋季学期" value="Autumn" />
|
||||
<el-option label="春季学期" value="Spring" />
|
||||
<el-option label="夏季学期" value="Summer" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="开始日期"><el-date-picker v-model="form.startDate" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="结束日期"><el-date-picker v-model="form.endDate" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item><el-checkbox v-model="form.isCurrent">设为当前学期</el-checkbox></el-form-item>
|
||||
</template>
|
||||
<el-form-item v-if="active === 'classrooms'" label="所属教学楼" required>
|
||||
<el-select v-model="form.buildingId">
|
||||
<el-option v-for="item in references.buildings" :key="item.id" :label="`${item.campusName} · ${item.name}`" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div v-if="active === 'classrooms'" class="form-grid">
|
||||
<el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" :max="1000" /></el-form-item>
|
||||
<el-form-item label="教室类型"><el-input v-model="form.roomType" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item v-if="active === 'classrooms'" label="设备"><el-input v-model="form.equipment" /></el-form-item>
|
||||
<div class="form-grid compact">
|
||||
<el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" /></el-form-item>
|
||||
<el-form-item label="状态"><el-switch v-model="form.isEnabled" active-text="启用" inactive-text="停用" /></el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Calendar, Connection, OfficeBuilding, School } from '@element-plus/icons-vue'
|
||||
import http from '../api/http'
|
||||
|
||||
interface DashboardData {
|
||||
currentTerm?: { name: string; startDate: string; endDate: string }
|
||||
counts: Record<string, number>
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const data = ref<DashboardData>({ counts: {} })
|
||||
|
||||
const stats = computed(() => [
|
||||
{ label: '校区', value: data.value.counts.campuses ?? 0, unit: '个', icon: OfficeBuilding },
|
||||
{ label: '学院', value: data.value.counts.colleges ?? 0, unit: '个', icon: School },
|
||||
{ label: '专业', value: data.value.counts.majors ?? 0, unit: '个', icon: Connection },
|
||||
{ label: '行政班', value: data.value.counts.classes ?? 0, unit: '个', icon: Calendar },
|
||||
])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
data.value = (await http.get('/dashboard')).data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="dashboard">
|
||||
<section class="term-hero">
|
||||
<div>
|
||||
<span class="section-kicker">CURRENT TERM</span>
|
||||
<h2>{{ data.currentTerm?.name ?? '尚未设置当前学期' }}</h2>
|
||||
<p v-if="data.currentTerm">
|
||||
{{ data.currentTerm.startDate }} 至 {{ data.currentTerm.endDate }}
|
||||
</p>
|
||||
<p v-else>请先在基础数据中建立学期档案。</p>
|
||||
</div>
|
||||
<button type="button" @click="router.push('/base-data')">
|
||||
<span>维护学期与组织数据</span>
|
||||
<b>→</b>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="metric-grid">
|
||||
<article v-for="stat in stats" :key="stat.label" class="metric-card">
|
||||
<el-icon><component :is="stat.icon" /></el-icon>
|
||||
<div>
|
||||
<span>{{ stat.label }}</span>
|
||||
<strong>{{ stat.value }}<small>{{ stat.unit }}</small></strong>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid">
|
||||
<article class="work-card foundation-card">
|
||||
<div class="card-heading">
|
||||
<div>
|
||||
<span class="section-kicker">FOUNDATION</span>
|
||||
<h3>基础数据完整度</h3>
|
||||
</div>
|
||||
<span class="status-chip">运行中</span>
|
||||
</div>
|
||||
<div class="foundation-list">
|
||||
<div>
|
||||
<span>组织体系</span>
|
||||
<b>校区 → 学院 → 专业 → 行政班</b>
|
||||
<i class="done">已建立</i>
|
||||
</div>
|
||||
<div>
|
||||
<span>教学空间</span>
|
||||
<b>校区 → 教学楼 → 教室</b>
|
||||
<i class="done">已建立</i>
|
||||
</div>
|
||||
<div>
|
||||
<span>权限体系</span>
|
||||
<b>角色权限 + 学院数据范围</b>
|
||||
<i class="done">已建立</i>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="work-card phase-card">
|
||||
<span class="section-kicker">NEXT MILESTONE</span>
|
||||
<h3>下一段业务链</h3>
|
||||
<p>基础数据确认后,将进入学生、教师、课程库和培养方案。</p>
|
||||
<div class="phase-line">
|
||||
<span class="active">基础底座</span>
|
||||
<span>人员档案</span>
|
||||
<span>培养方案</span>
|
||||
<span>教学运行</span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const form = reactive({ userName: 'admin', password: 'Admin@123456' })
|
||||
|
||||
async function submit() {
|
||||
loading.value = true
|
||||
try {
|
||||
await auth.login(form.userName, form.password)
|
||||
await router.replace(String(route.query.redirect ?? '/dashboard'))
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="login-page">
|
||||
<section class="login-story">
|
||||
<div class="story-brand">
|
||||
<div class="brand-mark large" aria-hidden="true">
|
||||
<span v-for="index in 9" :key="index" />
|
||||
</div>
|
||||
<div>
|
||||
<strong>明序教务</strong>
|
||||
<small>MINGXU ACADEMIC SYSTEM</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="story-copy">
|
||||
<span class="eyebrow">大学教务管理平台</span>
|
||||
<h1>让每一项教学安排,<br />都有清晰的来处与去向。</h1>
|
||||
<p>从组织档案开始,逐步连接培养方案、教学任务、排课、选课与成绩。</p>
|
||||
</div>
|
||||
<div class="schedule-signature" aria-hidden="true">
|
||||
<div v-for="item in 20" :key="item" :class="{ active: [3, 8, 9, 14, 18].includes(item) }">
|
||||
<span v-if="[3, 8, 9, 14, 18].includes(item)">教学</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="story-foot">第一阶段 · 基础管理工作台</p>
|
||||
</section>
|
||||
|
||||
<section class="login-panel">
|
||||
<form class="login-form" @submit.prevent="submit">
|
||||
<div class="form-intro">
|
||||
<span>欢迎回来</span>
|
||||
<h2>登录教务工作台</h2>
|
||||
<p>使用学校分配的管理账号继续。</p>
|
||||
</div>
|
||||
<label>
|
||||
<span>账号</span>
|
||||
<el-input v-model="form.userName" size="large" autocomplete="username" />
|
||||
</label>
|
||||
<label>
|
||||
<span>密码</span>
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
size="large"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</label>
|
||||
<el-button
|
||||
class="login-submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
>
|
||||
进入工作台
|
||||
</el-button>
|
||||
<div class="dev-hint">
|
||||
<b>本地开发账号</b>
|
||||
<span>admin / Admin@123456</span>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { 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
|
||||
}
|
||||
interface Role { name: string; description: string }
|
||||
|
||||
const users = ref<UserRow[]>([])
|
||||
const roles = ref<Role[]>([])
|
||||
const colleges = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const keyword = ref('')
|
||||
const form = reactive({
|
||||
userName: '', displayName: '', password: '', staffNumber: '',
|
||||
collegeId: undefined as string | undefined, roles: [] as string[],
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [userRes, roleRes, collegeRes] = await Promise.all([
|
||||
http.get('/users'), http.get('/users/roles'), http.get('/base-data/colleges'),
|
||||
])
|
||||
users.value = userRes.data
|
||||
roles.value = roleRes.data
|
||||
colleges.value = collegeRes.data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, {
|
||||
userName: '', displayName: '', password: '', staffNumber: '',
|
||||
collegeId: undefined, roles: [],
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
await http.post('/users', form)
|
||||
ElMessage.success('账号已创建')
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function setStatus(row: any) {
|
||||
try {
|
||||
await http.put(`/users/${row.id}/status`, { isEnabled: !row.isEnabled })
|
||||
ElMessage.success(row.isEnabled ? '账号已停用' : '账号已启用')
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-stack">
|
||||
<section class="page-intro">
|
||||
<div>
|
||||
<span class="section-kicker">IDENTITY & ACCESS</span>
|
||||
<h2>用户与权限</h2>
|
||||
<p>账号决定谁能进入系统,角色与学院范围决定能看到哪些数据。</p>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">创建账号</el-button>
|
||||
</section>
|
||||
|
||||
<section class="data-card">
|
||||
<div class="table-toolbar">
|
||||
<el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索账号、姓名或工号" />
|
||||
<span>共 {{ users.length }} 个账号</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="users.filter((x) => !keyword || `${x.userName}${x.displayName}${x.staffNumber}`.includes(keyword))"
|
||||
>
|
||||
<el-table-column prop="userName" label="账号" min-width="130" />
|
||||
<el-table-column prop="displayName" label="姓名" min-width="120" />
|
||||
<el-table-column prop="staffNumber" label="工号/学号" min-width="130">
|
||||
<template #default="{ row }">{{ row.staffNumber || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="role in row.roles" :key="role" class="role-tag" effect="plain">{{ role }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后登录" min-width="170">
|
||||
<template #default="{ row }">{{ row.lastLoginAt ? new Date(row.lastLoginAt).toLocaleString() : '尚未登录' }}</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
<el-button link :type="row.isEnabled ? 'danger' : 'primary'" @click="setStatus(row)">
|
||||
{{ row.isEnabled ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="创建账号" width="540px">
|
||||
<el-form label-position="top" class="entity-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="登录账号" required><el-input v-model="form.userName" /></el-form-item>
|
||||
<el-form-item label="姓名" required><el-input v-model="form.displayName" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="初始密码" required><el-input v-model="form.password" type="password" show-password /></el-form-item>
|
||||
<el-form-item label="工号/学号"><el-input v-model="form.staffNumber" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="所属学院">
|
||||
<el-select v-model="form.collegeId" clearable>
|
||||
<el-option v-for="item in colleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" required>
|
||||
<el-select v-model="form.roles" 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-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="create">创建账号</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user