feat: 从 git 历史迁移业务管理前端页面

从 commit 0b7d625 恢复并迁移以下页面:

基础数据管理:
- 年级管理 (business/base/grade)
- 班级管理 (business/base/class)
- 学科管理 (business/base/subject)
- 区域管理 (business/base/region)

业务功能:
- 学校管理 (business/school) + 3个组件
- 会员管理 (business/member) + 1个组件
- 学生管理 (business/student) + 2个组件
- 应用管理 (business/application) + 1个组件

API 路径修改:/api/xxx → /business/xxx

@author pangu
This commit is contained in:
神码-方晓辉 2026-02-02 16:04:47 +08:00
parent 1939180830
commit 0e75c175b5
15 changed files with 3171 additions and 0 deletions

View File

@ -0,0 +1,180 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
destroy-on-close
@closed="handleClosed"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-form-item label="应用名称" prop="appName">
<el-input v-model="form.appName" placeholder="请输入应用名称" maxlength="50" />
</el-form-item>
<el-form-item label="应用编码" prop="appCode">
<el-input v-model="form.appCode" placeholder="保存后自动生成" disabled />
</el-form-item>
<el-form-item label="应用描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
placeholder="请输入应用描述"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入联系人" maxlength="20" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" maxlength="11" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="form.status"
active-value="0"
inactive-value="1"
active-text="正常"
inactive-text="停用"
/>
</el-form-item>
<el-form-item label="接口授权" prop="apiAuth">
<el-checkbox-group v-model="form.apiAuth">
<el-checkbox
v-for="item in apiAuthOptions"
:key="item.value"
:label="item.value"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 应用管理 - 新增/编辑弹窗
* @author pangu
*/
import { addApplication, getApiAuthOptions, updateApplication } from '@/api/pangu/application'
import { ElMessage } from 'element-plus'
import { computed, nextTick, reactive, ref } from 'vue'
const emit = defineEmits(['success'])
const visible = ref(false)
const submitLoading = ref(false)
const formRef = ref(null)
const isEdit = ref(false)
const apiAuthOptions = ref([])
const form = reactive({
id: null,
appName: '',
appCode: '',
description: '',
contactName: '',
contactPhone: '',
status: '0',
apiAuth: []
})
const rules = {
appName: [
{ required: true, message: '请输入应用名称', trigger: 'blur' }
],
contactPhone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
]
}
const dialogTitle = computed(() => isEdit.value ? '编辑应用' : '新增应用')
//
const loadAuthOptions = async () => {
try {
const res = await getApiAuthOptions()
if (res.code === 200) {
apiAuthOptions.value = res.data
}
} catch (error) {
console.error('加载接口授权选项失败:', error)
}
}
//
const open = (row = null) => {
visible.value = true
loadAuthOptions()
if (row) {
isEdit.value = true
nextTick(() => {
Object.assign(form, {
id: row.id,
appName: row.appName,
appCode: row.appCode,
description: row.description || '',
contactName: row.contactName || '',
contactPhone: row.contactPhone || '',
status: row.status || '0',
apiAuth: row.apiAuth || []
})
})
} else {
isEdit.value = false
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const api = isEdit.value ? updateApplication : addApplication
const res = await api(form)
if (res.code === 200) {
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
visible.value = false
emit('success')
} else {
ElMessage.error(res.msg || '操作失败')
}
} catch (error) {
console.error('表单验证失败或提交失败:', error)
} finally {
submitLoading.value = false
}
}
//
const handleClosed = () => {
formRef.value?.resetFields()
Object.assign(form, {
id: null,
appName: '',
appCode: '',
description: '',
contactName: '',
contactPhone: '',
status: '0',
apiAuth: []
})
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,194 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="应用名称">
<el-input v-model="queryParams.appName" placeholder="请输入应用名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="应用编码">
<el-input v-model="queryParams.appCode" placeholder="请输入应用编码" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="appName" label="应用名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="appCode" label="应用编码" width="120" />
<el-table-column prop="apis" label="授权接口" min-width="250">
<template #default="{ row }">
<el-tag v-for="api in (row.apis || []).slice(0, 3)" :key="api" size="small" style="margin-right: 4px; margin-bottom: 2px">
{{ api }}
</el-tag>
<el-tag v-if="(row.apis || []).length > 3" size="small" type="info">+{{ row.apis.length - 3 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="createBy" label="创建人" width="100" />
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="warning" link :icon="Key" @click="handleResetSecret(row)">重置密钥</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<AppDialog ref="appDialogRef" @success="handleQuery" />
<!-- 密钥弹窗 -->
<SecretDialog ref="secretDialogRef" />
</div>
</template>
<script setup>
/**
* 应用管理页面
* @author pangu
*/
import { Delete, Edit, Key, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import request from '@/utils/request'
import AppDialog from './components/AppDialog.vue'
import SecretDialog from './components/SecretDialog.vue'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 10,
appName: '',
appCode: '',
status: ''
})
const appDialogRef = ref()
const secretDialogRef = ref()
//
const getList = async () => {
loading.value = true
try {
const res = await request.get('/business/application/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.value = {
pageNum: 1,
pageSize: 10,
appName: '',
appCode: '',
status: ''
}
getList()
}
//
const handleAdd = () => {
appDialogRef.value?.open()
}
//
const handleEdit = (row) => {
appDialogRef.value?.open(row)
}
//
const handleResetSecret = async (row) => {
ElMessageBox.confirm(`确定要重置应用"${row.appName}"的密钥吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.put(`/api/application/resetSecret/${row.id}`)
if (res.code === 200) {
ElMessage.success('重置成功')
secretDialogRef.value?.open(res.data)
}
}).catch(() => {})
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除应用"${row.appName}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/application/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,205 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="班级名称">
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="className" label="班级名称" min-width="150" />
<el-table-column prop="classCode" label="班级编码" width="120" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" :close-on-click-modal="false" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="班级名称" prop="className">
<el-input v-model="form.className" placeholder="请输入班级名称" />
</el-form-item>
<el-form-item label="班级编码" prop="classCode">
<el-input v-model="form.classCode" placeholder="自动生成" disabled />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-value="0" inactive-value="1" active-text="正常" inactive-text="停用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
/**
* 班级管理页面
* @author pangu
*/
import { Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import request from '@/utils/request'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 10,
className: '',
status: ''
})
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref()
const form = ref({
id: null,
className: '',
classCode: '',
sort: 0,
status: '0'
})
const rules = {
className: [{ required: true, message: '请输入班级名称', trigger: 'blur' }]
}
//
const getList = async () => {
loading.value = true
try {
const res = await request.get('/business/class/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.value = { pageNum: 1, pageSize: 10, className: '', status: '' }
getList()
}
//
const handleAdd = () => {
dialogTitle.value = '新增班级'
form.value = { id: null, className: '', classCode: 'C' + Date.now(), sort: 0, status: '0' }
dialogVisible.value = true
}
//
const handleEdit = (row) => {
dialogTitle.value = '编辑班级'
form.value = { ...row }
dialogVisible.value = true
}
//
const handleSubmit = async () => {
await formRef.value?.validate()
const isEdit = !!form.value.id
const res = isEdit
? await request.put('/business/class', form.value)
: await request.post('/business/class', form.value)
if (res.code === 200) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
dialogVisible.value = false
getList()
}
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除班级"${row.className}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/class/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,205 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="年级名称">
<el-input v-model="queryParams.gradeName" placeholder="请输入年级名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="name" label="年级名称" min-width="150" />
<el-table-column prop="code" label="年级编码" width="120" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" :close-on-click-modal="false" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="年级名称" prop="name">
<el-input v-model="form.name" placeholder="请输入年级名称" />
</el-form-item>
<el-form-item label="年级编码" prop="code">
<el-input v-model="form.code" placeholder="自动生成" disabled />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-value="0" inactive-value="1" active-text="正常" inactive-text="停用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
/**
* 年级管理页面
* @author pangu
*/
import { Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import request from '@/utils/request'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 10,
gradeName: '',
status: ''
})
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref()
const form = ref({
id: null,
name: '',
code: '',
sort: 0,
status: '0'
})
const rules = {
name: [{ required: true, message: '请输入年级名称', trigger: 'blur' }]
}
//
const getList = async () => {
loading.value = true
try {
const res = await request.get('/business/grade/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.value = { pageNum: 1, pageSize: 10, gradeName: '', status: '' }
getList()
}
//
const handleAdd = () => {
dialogTitle.value = '新增年级'
form.value = { id: null, name: '', code: '', sort: 0, status: '0' }
dialogVisible.value = true
}
//
const handleEdit = (row) => {
dialogTitle.value = '编辑年级'
form.value = { ...row }
dialogVisible.value = true
}
//
const handleSubmit = async () => {
await formRef.value?.validate()
const isEdit = !!form.value.id
const res = isEdit
? await request.put('/business/grade', form.value)
: await request.post('/business/grade', form.value)
if (res.code === 200) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
dialogVisible.value = false
getList()
}
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除年级"${row.name}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/grade/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,229 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="区域名称">
<el-input v-model="queryParams.regionName" placeholder="请输入区域名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
<el-button type="info" :icon="Sort" @click="toggleExpand">{{ isExpand ? '折叠' : '展开' }}</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd()">新增</el-button>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="tableData"
row-key="id"
border
:default-expand-all="isExpand"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
style="width: 100%"
>
<el-table-column prop="regionName" label="区域名称" min-width="200" />
<el-table-column prop="regionCode" label="区域编码" width="120" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Plus" @click="handleAdd(row)">新增</el-button>
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" :close-on-click-modal="false" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="上级区域" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="regionTreeOptions"
:props="{ value: 'id', label: 'regionName', children: 'children' }"
check-strictly
placeholder="请选择上级区域"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="区域名称" prop="regionName">
<el-input v-model="form.regionName" placeholder="请输入区域名称" />
</el-form-item>
<el-form-item label="区域编码" prop="regionCode">
<el-input v-model="form.regionCode" placeholder="请输入区域编码" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-value="0" inactive-value="1" active-text="正常" inactive-text="停用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
/**
* 区域管理页面
* @author pangu
*/
import { Delete, Edit, Plus, Refresh, Search, Sort } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import request from '@/utils/request'
const loading = ref(false)
const tableData = ref([])
const isExpand = ref(true)
const regionTreeOptions = ref([])
const queryParams = ref({
regionName: '',
status: ''
})
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref()
const form = ref({
id: null,
parentId: null,
regionName: '',
regionCode: '',
sort: 0,
status: '0'
})
const rules = {
regionName: [{ required: true, message: '请输入区域名称', trigger: 'blur' }],
regionCode: [{ required: true, message: '请输入区域编码', trigger: 'blur' }]
}
//
const getList = async () => {
loading.value = true
try {
const res = await request.get('/business/region/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.data
//
regionTreeOptions.value = [{ id: 0, regionName: '根节点', children: res.data }]
}
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
getList()
}
//
const resetQuery = () => {
queryParams.value = { regionName: '', status: '' }
getList()
}
// /
const toggleExpand = () => {
isExpand.value = !isExpand.value
getList()
}
//
const handleAdd = (row) => {
dialogTitle.value = '新增区域'
form.value = {
id: null,
parentId: row ? row.id : 0,
regionName: '',
regionCode: '',
sort: 0,
status: '0'
}
dialogVisible.value = true
}
//
const handleEdit = (row) => {
dialogTitle.value = '编辑区域'
form.value = { ...row }
dialogVisible.value = true
}
//
const handleSubmit = async () => {
await formRef.value?.validate()
const isEdit = !!form.value.id
const res = isEdit
? await request.put('/business/region', form.value)
: await request.post('/business/region', form.value)
if (res.code === 200) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
dialogVisible.value = false
getList()
}
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除区域"${row.regionName}"吗?删除后子区域也将被删除。`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/region/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,205 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="学科名称">
<el-input v-model="queryParams.subjectName" placeholder="请输入学科名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="subjectName" label="学科名称" min-width="150" />
<el-table-column prop="subjectCode" label="学科编码" width="120" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" :close-on-click-modal="false" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="学科名称" prop="subjectName">
<el-input v-model="form.subjectName" placeholder="请输入学科名称" />
</el-form-item>
<el-form-item label="学科编码" prop="subjectCode">
<el-input v-model="form.subjectCode" placeholder="自动生成" disabled />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-value="0" inactive-value="1" active-text="正常" inactive-text="停用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
/**
* 学科管理页面
* @author pangu
*/
import { Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import request from '@/utils/request'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 10,
subjectName: '',
status: ''
})
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref()
const form = ref({
id: null,
subjectName: '',
subjectCode: '',
sort: 0,
status: '0'
})
const rules = {
subjectName: [{ required: true, message: '请输入学科名称', trigger: 'blur' }]
}
//
const getList = async () => {
loading.value = true
try {
const res = await request.get('/business/subject/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.value = { pageNum: 1, pageSize: 10, subjectName: '', status: '' }
getList()
}
//
const handleAdd = () => {
dialogTitle.value = '新增学科'
form.value = { id: null, subjectName: '', subjectCode: 'S' + Date.now(), sort: 0, status: '0' }
dialogVisible.value = true
}
//
const handleEdit = (row) => {
dialogTitle.value = '编辑学科'
form.value = { ...row }
dialogVisible.value = true
}
//
const handleSubmit = async () => {
await formRef.value?.validate()
const isEdit = !!form.value.id
const res = isEdit
? await request.put('/business/subject', form.value)
: await request.post('/business/subject', form.value)
if (res.code === 200) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
dialogVisible.value = false
getList()
}
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除学科"${row.subjectName}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/subject/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,383 @@
<!--
会员编辑弹窗
@author pangu
-->
<template>
<el-dialog
v-model="visible"
:title="memberId ? '编辑会员' : '新增会员'"
width="700px"
:close-on-click-modal="false"
destroy-on-close
@open="handleOpen"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname" placeholder="请输入昵称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.gender">
<el-radio value="1"></el-radio>
<el-radio value="2"></el-radio>
<el-radio value="0">未知</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="form.birthday"
type="date"
placeholder="请选择"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="身份类型" prop="identityType">
<el-radio-group v-model="form.identityType">
<el-radio value="1">家长</el-radio>
<el-radio value="2">教师</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-switch
v-model="form.status"
active-value="0"
inactive-value="1"
active-text="正常"
inactive-text="停用"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 教师身份时显示学校信息 -->
<template v-if="form.identityType === '2'">
<el-divider content-position="left">学校信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="区域" prop="regionId">
<el-cascader
v-model="form.regionIds"
:options="regionTree"
:props="{ value: 'id', label: 'label', checkStrictly: true }"
placeholder="请选择区域"
clearable
style="width: 100%"
@change="handleRegionChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学校" prop="schoolId">
<el-select v-model="form.schoolId" placeholder="请选择学校" clearable style="width: 100%" @change="handleSchoolChange">
<el-option v-for="item in schoolList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年级" prop="gradeId">
<el-select v-model="form.gradeId" placeholder="请选择年级" clearable style="width: 100%" @change="handleGradeChange">
<el-option v-for="item in gradeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班级" prop="classId">
<el-select v-model="form.classId" placeholder="请选择班级" clearable style="width: 100%">
<el-option v-for="item in classList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- 绑定学生 -->
<el-divider content-position="left">绑定学生</el-divider>
<el-row style="margin-bottom: 12px;">
<el-button type="primary" size="small" :icon="Plus" @click="handleAddStudent">添加学生</el-button>
</el-row>
<el-table :data="form.students" border size="small" max-height="200">
<template #empty>
<el-empty description="暂无绑定学生" :image-size="60" />
</template>
<el-table-column prop="name" label="姓名" min-width="80" />
<el-table-column prop="studentNo" label="学号" width="120" />
<el-table-column prop="schoolName" label="学校" min-width="120" show-overflow-tooltip />
<el-table-column prop="gradeName" label="年级" width="80" />
<el-table-column prop="className" label="班级" width="60" />
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="handleRemoveStudent($index)">移除</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { addMember, getClassList, getGradeList, getMember, getRegionTree, getSchoolList, updateMember } from '@/api/pangu/member'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, reactive, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
memberId: {
type: [Number, null],
default: null
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const formRef = ref(null)
const submitLoading = ref(false)
//
const form = reactive({
phone: '',
nickname: '',
gender: '0',
birthday: '',
identityType: '1',
status: '0',
regionIds: [],
regionId: null,
schoolId: null,
gradeId: null,
classId: null,
students: []
})
//
const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
]
}
//
const regionTree = ref([])
const schoolList = ref([])
const gradeList = ref([])
const classList = ref([])
/**
* 弹窗打开时加载数据
*/
const handleOpen = async () => {
//
try {
const res = await getRegionTree()
regionTree.value = res.data || []
} catch (e) {
//
}
//
if (props.memberId) {
try {
const res = await getMember(props.memberId)
if (res.data) {
Object.assign(form, res.data)
//
if (form.regionId) {
await loadSchoolList(form.regionId)
}
if (form.schoolId) {
await loadGradeList(form.schoolId)
}
if (form.gradeId) {
await loadClassList(form.gradeId)
}
}
} catch (e) {
//
}
} else {
resetForm()
}
}
/**
* 重置表单
*/
const resetForm = () => {
form.phone = ''
form.nickname = ''
form.gender = '0'
form.birthday = ''
form.identityType = '1'
form.status = '0'
form.regionIds = []
form.regionId = null
form.schoolId = null
form.gradeId = null
form.classId = null
form.students = []
}
/**
* 加载学校列表
*/
const loadSchoolList = async (regionId) => {
try {
const res = await getSchoolList(regionId)
schoolList.value = res.data || []
} catch (e) {
schoolList.value = []
}
}
/**
* 加载年级列表
*/
const loadGradeList = async (schoolId) => {
try {
const res = await getGradeList(schoolId)
gradeList.value = res.data || []
} catch (e) {
gradeList.value = []
}
}
/**
* 加载班级列表
*/
const loadClassList = async (gradeId) => {
try {
const res = await getClassList(gradeId)
classList.value = res.data || []
} catch (e) {
classList.value = []
}
}
/**
* 区域变更
*/
const handleRegionChange = (val) => {
form.regionId = val && val.length ? val[val.length - 1] : null
form.schoolId = null
form.gradeId = null
form.classId = null
schoolList.value = []
gradeList.value = []
classList.value = []
if (form.regionId) {
loadSchoolList(form.regionId)
}
}
/**
* 学校变更
*/
const handleSchoolChange = () => {
form.gradeId = null
form.classId = null
gradeList.value = []
classList.value = []
if (form.schoolId) {
loadGradeList(form.schoolId)
}
}
/**
* 年级变更
*/
const handleGradeChange = () => {
form.classId = null
classList.value = []
if (form.gradeId) {
loadClassList(form.gradeId)
}
}
/**
* 添加学生模拟实际应该弹出学生选择器
*/
const handleAddStudent = () => {
ElMessage.info('请选择要绑定的学生')
}
/**
* 移除学生
*/
const handleRemoveStudent = (index) => {
form.students.splice(index, 1)
}
/**
* 提交表单
*/
const handleSubmit = async () => {
try {
await formRef.value?.validate()
} catch (e) {
return
}
submitLoading.value = true
try {
if (props.memberId) {
await updateMember({ ...form, id: props.memberId })
ElMessage.success('修改成功')
} else {
await addMember(form)
ElMessage.success('新增成功')
}
visible.value = false
emit('success')
} finally {
submitLoading.value = false
}
}
//
watch(visible, (val) => {
if (!val) {
formRef.value?.resetFields()
}
})
</script>

View File

@ -0,0 +1,233 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="手机号">
<el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="身份类型">
<el-select v-model="queryParams.identityType" placeholder="全部" clearable style="width: 120px">
<el-option label="家长" value="1" />
<el-option label="教师" value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item label="注册时间">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="memberNo" label="会员编号" width="140" />
<el-table-column prop="phone" label="手机号" width="120">
<template #default="{ row }">
{{ maskPhone(row.phone) }}
</template>
</el-table-column>
<el-table-column prop="nickname" label="昵称" min-width="100" show-overflow-tooltip />
<el-table-column prop="gender" label="性别" width="60" align="center">
<template #default="{ row }">
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
</template>
</el-table-column>
<el-table-column prop="birthday" label="出生日期" width="100" />
<el-table-column prop="identityType" label="身份类型" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.identityType === '1' ? 'success' : 'warning'">
{{ row.identityType === '1' ? '家长' : '教师' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="registerTime" label="注册时间" width="160" />
<el-table-column prop="registerSource" label="注册来源" width="80" align="center" />
<el-table-column prop="status" label="状态" width="70" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="warning" link :icon="Key" @click="handleResetPwd(row)">重置密码</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<MemberDialog ref="memberDialogRef" @success="handleQuery" />
<!-- 重置密码弹窗 -->
<ResetPwdDialog ref="resetPwdDialogRef" />
</div>
</template>
<script setup>
/**
* 会员管理页面
* @author pangu
*/
import { Delete, Edit, Key, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import request from '@/utils/request'
import MemberDialog from './components/MemberDialog.vue'
import ResetPwdDialog from './components/ResetPwdDialog.vue'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const dateRange = ref([])
const queryParams = ref({
pageNum: 1,
pageSize: 10,
phone: '',
nickname: '',
identityType: '',
status: '',
beginTime: '',
endTime: ''
})
const memberDialogRef = ref()
const resetPwdDialogRef = ref()
//
const maskPhone = (phone) => {
if (!phone || phone.length !== 11) return phone
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
//
const getList = async () => {
loading.value = true
//
if (dateRange.value && dateRange.value.length === 2) {
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
} else {
queryParams.value.beginTime = ''
queryParams.value.endTime = ''
}
try {
const res = await request.get('/business/member/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.value = {
pageNum: 1,
pageSize: 10,
phone: '',
nickname: '',
identityType: '',
status: '',
beginTime: '',
endTime: ''
}
dateRange.value = []
getList()
}
//
const handleAdd = () => {
memberDialogRef.value?.open()
}
//
const handleEdit = (row) => {
memberDialogRef.value?.open(row)
}
//
const handleResetPwd = (row) => {
resetPwdDialogRef.value?.open(row)
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除会员"${row.nickname}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/member/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<el-dialog
v-model="dialogVisible"
title="新增班级"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<div style="margin-bottom: 16px;">
<span style="font-weight: bold;">学校</span>
<span>{{ currentSchool?.schoolName }}</span>
</div>
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="选择年级" prop="gradeId">
<el-select v-model="form.gradeId" placeholder="请先选择年级" style="width: 100%">
<el-option
v-for="item in gradeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="选择班级" prop="classIds">
<el-checkbox-group v-model="form.classIds">
<el-row :gutter="10">
<el-col :span="8" v-for="item in classOptions" :key="item.value">
<el-checkbox :label="item.value" style="margin-bottom: 8px;">
{{ item.label }}
</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 新增班级弹窗
* @author pangu
*/
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import { addGradeClass, getClassOptions, getGradeOptions } from '@/api/pangu/school'
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref(null)
const currentSchool = ref(null)
const gradeOptions = ref([])
const classOptions = ref([])
//
const form = ref({
schoolId: null,
gradeId: '',
classIds: []
})
//
const fetchGradeOptions = async () => {
try {
const res = await getGradeOptions()
if (res.code === 200) {
gradeOptions.value = res.data
}
} catch (error) {
console.error('获取年级选项失败:', error)
}
}
//
const fetchClassOptions = async () => {
try {
const res = await getClassOptions()
if (res.code === 200) {
classOptions.value = res.data
}
} catch (error) {
console.error('获取班级选项失败:', error)
}
}
//
const open = (school) => {
dialogVisible.value = true
currentSchool.value = school
form.value = {
schoolId: school.id,
gradeId: '',
classIds: []
}
}
//
const handleSubmit = async () => {
if (!form.value.gradeId) {
ElMessage.warning('请选择年级')
return
}
if (form.value.classIds.length === 0) {
ElMessage.warning('请至少选择一个班级')
return
}
submitLoading.value = true
try {
const res = await addGradeClass(form.value)
if (res.code === 200) {
ElMessage.success('添加班级成功')
dialogVisible.value = false
emit('success')
}
} catch (error) {
console.error('添加班级失败:', error)
} finally {
submitLoading.value = false
}
}
//
onMounted(() => {
fetchGradeOptions()
fetchClassOptions()
})
//
defineExpose({ open })
</script>

View File

@ -0,0 +1,109 @@
<template>
<el-dialog
v-model="dialogVisible"
title="新增年级"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<div style="margin-bottom: 16px;">
<span style="font-weight: bold;">学校</span>
<span>{{ currentSchool?.schoolName }}</span>
</div>
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="选择年级" prop="gradeIds">
<el-checkbox-group v-model="form.gradeIds">
<el-row :gutter="10">
<el-col :span="8" v-for="item in gradeOptions" :key="item.value">
<el-checkbox :label="item.value" style="margin-bottom: 8px;">
{{ item.label }}
</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 新增年级弹窗
* @author pangu
*/
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import { addSchoolGrade, getGradeOptions } from '@/api/pangu/school'
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref(null)
const currentSchool = ref(null)
const gradeOptions = ref([])
//
const form = ref({
schoolId: null,
gradeIds: []
})
//
const fetchGradeOptions = async () => {
try {
const res = await getGradeOptions()
if (res.code === 200) {
gradeOptions.value = res.data
}
} catch (error) {
console.error('获取年级选项失败:', error)
}
}
//
const open = (school) => {
dialogVisible.value = true
currentSchool.value = school
form.value = {
schoolId: school.id,
gradeIds: []
}
}
//
const handleSubmit = async () => {
if (form.value.gradeIds.length === 0) {
ElMessage.warning('请至少选择一个年级')
return
}
submitLoading.value = true
try {
const res = await addSchoolGrade(form.value)
if (res.code === 200) {
ElMessage.success('添加年级成功')
dialogVisible.value = false
emit('success')
}
} catch (error) {
console.error('添加年级失败:', error)
} finally {
submitLoading.value = false
}
}
//
onMounted(() => {
fetchGradeOptions()
})
//
defineExpose({ open })
</script>

View File

@ -0,0 +1,225 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-form-item label="学校名称" prop="schoolName">
<el-input v-model="form.schoolName" placeholder="请输入学校名称" />
</el-form-item>
<el-form-item label="学校类型" prop="schoolType">
<el-select v-model="form.schoolType" placeholder="请选择学校类型" style="width: 100%">
<el-option label="小学" value="小学" />
<el-option label="初中" value="初中" />
<el-option label="高中" value="高中" />
<el-option label="九年一贯制" value="九年一贯制" />
<el-option label="完全中学" value="完全中学" />
</el-select>
</el-form-item>
<el-form-item label="所属区域" prop="regionId">
<el-cascader
v-model="form.regionIds"
:options="regionTree"
:props="{
value: 'regionId',
label: 'regionName',
children: 'children',
checkStrictly: true
}"
placeholder="请选择所属区域"
clearable
style="width: 100%"
@change="handleRegionChange"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="form.status"
active-value="0"
inactive-value="1"
active-text="正常"
inactive-text="停用"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 学校新增/编辑弹窗
* @author pangu
*/
import { ElMessage } from 'element-plus'
import { computed, ref } from 'vue'
import { addSchool, updateSchool } from '@/api/pangu/school'
//
const props = defineProps({
regionTree: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref(null)
const isEdit = ref(false)
//
const form = ref({
id: null,
schoolName: '',
schoolType: '',
regionId: null,
regionIds: [],
regionName: '',
status: '0'
})
//
const dialogTitle = computed(() => isEdit.value ? '编辑学校' : '新增学校')
//
const rules = {
schoolName: [
{ required: true, message: '请输入学校名称', trigger: 'blur' }
],
schoolType: [
{ required: true, message: '请选择学校类型', trigger: 'change' }
],
regionId: [
{ required: true, message: '请选择所属区域', trigger: 'change' }
]
}
//
const getRegionPath = (ids, tree) => {
const names = []
const findPath = (nodes, targetId, path) => {
for (const node of nodes) {
if (ids.includes(node.regionId)) {
path.push(node.regionName)
}
if (node.children) {
findPath(node.children, targetId, path)
}
}
}
findPath(tree, ids[ids.length - 1], names)
return names.join('/')
}
//
const handleRegionChange = (value) => {
if (value && value.length > 0) {
form.value.regionId = value[value.length - 1]
form.value.regionName = getRegionPath(value, props.regionTree)
} else {
form.value.regionId = null
form.value.regionName = ''
}
}
//
const open = (row) => {
dialogVisible.value = true
isEdit.value = !!row
if (row) {
//
form.value = {
id: row.id,
schoolName: row.schoolName,
schoolType: row.schoolType,
regionId: row.regionId,
regionIds: getRegionIdPath(row.regionId),
regionName: row.regionName,
status: row.status
}
} else {
//
form.value = {
id: null,
schoolName: '',
schoolType: '',
regionId: null,
regionIds: [],
regionName: '',
status: '0'
}
}
}
// IDID
const getRegionIdPath = (regionId) => {
if (!regionId) return []
const id = regionId.toString()
const path = []
// ID1
if (id.length >= 1) {
path.push(parseInt(id.charAt(0)))
}
// ID2
if (id.length >= 2) {
path.push(parseInt(id.substring(0, 2)))
}
// ID3
if (id.length >= 3) {
path.push(parseInt(id))
}
return path
}
//
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
const submitData = {
...form.value,
regionIds: undefined //
}
const res = isEdit.value
? await updateSchool(submitData)
: await addSchool(submitData)
if (res.code === 200) {
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
dialogVisible.value = false
emit('success')
}
} catch (error) {
console.error('提交失败:', error)
} finally {
submitLoading.value = false
}
})
}
//
defineExpose({ open })
</script>

View File

@ -0,0 +1,250 @@
<template>
<div class="app-container">
<el-row :gutter="16">
<!-- 左侧区域树 -->
<el-col :span="5">
<el-card shadow="never">
<template #header>
<span>区域筛选</span>
</template>
<el-input v-model="treeFilterText" placeholder="输入关键字过滤" clearable style="margin-bottom: 12px" />
<el-scrollbar height="calc(100vh - 260px)">
<el-tree
ref="treeRef"
:data="regionTree"
:props="{ label: 'regionName', children: 'children' }"
node-key="regionId"
highlight-current
:filter-node-method="filterNode"
@node-click="handleNodeClick"
/>
</el-scrollbar>
</el-card>
</el-col>
<!-- 右侧列表 -->
<el-col :span="19">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="学校名称">
<el-input v-model="queryParams.schoolName" placeholder="请输入学校名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 120px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="schoolName" label="学校名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="schoolType" label="学校类型" width="100" align="center" />
<el-table-column prop="regionName" label="所属区域" min-width="180" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="createBy" label="创建人" width="100" />
<el-table-column label="操作" width="280" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link :icon="Collection" @click="handleAddGrade(row)">新增年级</el-button>
<el-button type="primary" link :icon="Files" @click="handleAddClass(row)">新增班级</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑弹窗 -->
<SchoolDialog ref="schoolDialogRef" :region-tree="regionTree" @success="handleQuery" />
<!-- 新增年级弹窗 -->
<GradeDialog ref="gradeDialogRef" @success="handleQuery" />
<!-- 新增班级弹窗 -->
<ClassDialog ref="classDialogRef" @success="handleQuery" />
</div>
</template>
<script setup>
/**
* 学校管理页面
* @author pangu
*/
import { Collection, Delete, Edit, Files, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref, watch } from 'vue'
import request from '@/utils/request'
import ClassDialog from './components/ClassDialog.vue'
import GradeDialog from './components/GradeDialog.vue'
import SchoolDialog from './components/SchoolDialog.vue'
//
const treeRef = ref()
const treeFilterText = ref('')
const regionTree = ref([])
const selectedRegionId = ref(null)
//
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
//
const queryParams = ref({
pageNum: 1,
pageSize: 10,
schoolName: '',
status: '',
regionId: ''
})
//
const schoolDialogRef = ref()
const gradeDialogRef = ref()
const classDialogRef = ref()
//
watch(treeFilterText, (val) => {
treeRef.value?.filter(val)
})
//
const filterNode = (value, data) => {
if (!value) return true
return data.regionName.includes(value)
}
//
const getRegionTree = async () => {
const res = await request.get('/business/region/tree')
if (res.code === 200) {
regionTree.value = res.data
}
}
//
const getList = async () => {
loading.value = true
try {
const res = await request.get('/business/school/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
//
const handleNodeClick = (data) => {
selectedRegionId.value = data.regionId
queryParams.value.regionId = data.regionId
queryParams.value.pageNum = 1
getList()
}
//
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.value = {
pageNum: 1,
pageSize: 10,
schoolName: '',
status: '',
regionId: ''
}
selectedRegionId.value = null
treeRef.value?.setCurrentKey(null)
getList()
}
//
const handleAdd = () => {
schoolDialogRef.value?.open()
}
//
const handleEdit = (row) => {
schoolDialogRef.value?.open(row)
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除学校"${row.schoolName}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/school/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
//
const handleAddGrade = (row) => {
gradeDialogRef.value?.open(row)
}
//
const handleAddClass = (row) => {
classDialogRef.value?.open(row)
}
onMounted(() => {
getRegionTree()
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<el-dialog
v-model="visible"
title="批量导入学生"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<el-alert
title="导入说明"
type="info"
:closable="false"
style="margin-bottom: 16px"
>
<template #default>
<div style="line-height: 1.8">
1. 请先下载导入模板按模板格式填写数据<br>
2. 支持 xlsxxls 格式文件单次最多导入500条<br>
3. 必填字段姓名学校年级班级
</div>
</template>
</el-alert>
<div style="margin-bottom: 16px;">
<el-button type="primary" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
</div>
<el-upload
ref="uploadRef"
:action="uploadUrl"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:show-file-list="true"
:limit="1"
accept=".xlsx,.xls"
drag
>
<el-icon class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 xlsx/xls 文件</div>
</template>
</el-upload>
<!-- 导入结果 -->
<div v-if="importResult" style="margin-top: 16px;">
<el-alert
:title="`导入完成:成功 ${importResult.successCount} 条,失败 ${importResult.failCount} 条`"
:type="importResult.failCount > 0 ? 'warning' : 'success'"
:closable="false"
/>
<div v-if="importResult.failList && importResult.failList.length > 0" style="margin-top: 12px;">
<el-table :data="importResult.failList" border size="small" max-height="200">
<el-table-column prop="row" label="行号" width="80" />
<el-table-column prop="reason" label="失败原因" min-width="200" />
</el-table>
</div>
</div>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 学生批量导入弹窗
* @author pangu
*/
import { Download, Upload } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, ref } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const uploadRef = ref(null)
const importResult = ref(null)
//
const uploadUrl = '/business/student/import'
//
const uploadHeaders = computed(() => {
const token = localStorage.getItem('token')
return token ? { Authorization: 'Bearer ' + token } : {}
})
//
const handleDownloadTemplate = () => {
//
ElMessage.info('模板下载功能需要对接后端下载接口')
}
//
const beforeUpload = (file) => {
const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls')
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件')
return false
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
ElMessage.error('文件大小不能超过 10MB')
return false
}
importResult.value = null
return true
}
//
const handleSuccess = (response) => {
if (response.code === 200) {
importResult.value = response.data
if (response.data.failCount === 0) {
ElMessage.success('导入成功')
emit('success')
} else {
ElMessage.warning('部分数据导入失败,请查看失败原因')
}
} else {
ElMessage.error(response.msg || '导入失败')
}
}
//
const handleError = () => {
ElMessage.error('文件上传失败,请重试')
}
</script>
<style scoped>
.el-icon--upload {
font-size: 48px;
color: #c0c4cc;
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<el-dialog
v-model="visible"
:title="isEdit ? '编辑学生' : '新增学生'"
width="600px"
:close-on-click-modal="false"
destroy-on-close
@open="handleOpen"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入学生姓名" maxlength="20" />
</el-form-item>
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="请输入学号" maxlength="30" />
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.gender">
<el-radio value="1"></el-radio>
<el-radio value="2"></el-radio>
<el-radio value="0">未知</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="form.birthday"
type="month"
placeholder="请选择出生年月"
format="YYYY-MM"
value-format="YYYY-MM"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="学校信息" prop="schoolPath" required>
<el-cascader
v-model="form.schoolPath"
:options="schoolTree"
:props="{
value: 'id',
label: 'label',
children: 'children',
checkStrictly: false
}"
placeholder="请选择学校/年级/班级"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="学科" prop="subject">
<el-select v-model="form.subject" placeholder="请选择学科" clearable style="width: 100%">
<el-option v-for="item in subjectList" :key="item.id" :label="item.name" :value="item.name" />
</el-select>
</el-form-item>
<el-form-item label="归属用户" prop="userId">
<el-input v-model="form.userNickname" placeholder="请输入用户昵称搜索" readonly>
<template #append>
<el-button @click="handleSelectUser">选择</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 学生新增/编辑弹窗
* @author pangu
*/
import { addStudent, getStudent, updateStudent } from '@/api/pangu/student'
import { ElMessage } from 'element-plus'
import { computed, reactive, ref } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
studentId: {
type: [Number, null],
default: null
},
schoolTree: {
type: Array,
default: () => []
},
subjectList: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const isEdit = computed(() => !!props.studentId)
const formRef = ref(null)
const submitLoading = ref(false)
const initialForm = {
id: null,
name: '',
studentNo: '',
gender: '1',
birthday: '',
schoolPath: [],
subject: '',
userId: null,
userNickname: ''
}
const form = reactive({ ...initialForm })
const rules = {
name: [
{ required: true, message: '请输入学生姓名', trigger: 'blur' }
],
schoolPath: [
{ required: true, message: '请选择学校/年级/班级', trigger: 'change' }
]
}
//
const handleOpen = async () => {
Object.assign(form, initialForm)
formRef.value?.clearValidate()
if (props.studentId) {
try {
const res = await getStudent(props.studentId)
if (res.data) {
const data = res.data
form.id = data.id
form.name = data.name
form.studentNo = data.studentNo
form.gender = data.gender
form.birthday = data.birthday
form.subject = data.subject
form.userId = data.userId
form.userNickname = data.userNickname
// schoolPath
form.schoolPath = [data.schoolId]
}
} catch (e) {
console.error('获取学生详情失败:', e)
}
}
}
//
const handleSelectUser = () => {
//
ElMessage.info('用户选择功能需要对接会员管理模块')
}
//
const handleSubmit = async () => {
try {
await formRef.value?.validate()
} catch (e) {
return
}
submitLoading.value = true
try {
const submitData = {
...form,
schoolId: form.schoolPath[0],
gradeId: form.schoolPath[1],
classId: form.schoolPath[2]
}
delete submitData.schoolPath
if (isEdit.value) {
await updateStudent(submitData)
ElMessage.success('修改成功')
} else {
await addStudent(submitData)
ElMessage.success('新增成功')
}
visible.value = false
emit('success')
} catch (e) {
//
} finally {
submitLoading.value = false
}
}
</script>

View File

@ -0,0 +1,259 @@
<template>
<div class="app-container">
<el-row :gutter="16">
<!-- 左侧学校树 -->
<el-col :span="5">
<el-card shadow="never">
<template #header>
<span>学校筛选</span>
</template>
<el-input v-model="treeFilterText" placeholder="输入关键字过滤" clearable style="margin-bottom: 12px" />
<el-scrollbar height="calc(100vh - 260px)">
<el-tree
ref="treeRef"
:data="schoolTree"
:props="{ label: 'label', children: 'children' }"
node-key="id"
highlight-current
:filter-node-method="filterNode"
@node-click="handleNodeClick"
/>
</el-scrollbar>
</el-card>
</el-col>
<!-- 右侧列表 -->
<el-col :span="19">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="学生姓名">
<el-input v-model="queryParams.name" placeholder="请输入学生姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="学号">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="queryParams.gender" placeholder="全部" clearable style="width: 100px">
<el-option label="男" value="1" />
<el-option label="女" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" :icon="Upload" @click="handleImport">导入</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="studentNo" label="学号" width="130" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="gender" label="性别" width="60" align="center">
<template #default="{ row }">
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
</template>
</el-table-column>
<el-table-column prop="birthday" label="出生年月" width="100" />
<el-table-column prop="schoolName" label="学校" min-width="150" show-overflow-tooltip />
<el-table-column prop="gradeName" label="年级" width="80" />
<el-table-column prop="className" label="班级" width="80" />
<el-table-column prop="subject" label="学科" width="80" />
<el-table-column prop="userNickname" label="归属用户" width="100" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑弹窗 -->
<StudentDialog ref="studentDialogRef" @success="handleQuery" />
<!-- 导入弹窗 -->
<ImportDialog ref="importDialogRef" @success="handleQuery" />
</div>
</template>
<script setup>
/**
* 学生管理页面
* @author pangu
*/
import { Delete, Edit, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref, watch } from 'vue'
import request from '@/utils/request'
import ImportDialog from './components/ImportDialog.vue'
import StudentDialog from './components/StudentDialog.vue'
//
const treeRef = ref()
const treeFilterText = ref('')
const schoolTree = ref([])
//
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
//
const queryParams = ref({
pageNum: 1,
pageSize: 10,
name: '',
studentNo: '',
gender: '',
schoolId: '',
gradeId: '',
classId: ''
})
//
const studentDialogRef = ref()
const importDialogRef = ref()
//
watch(treeFilterText, (val) => {
treeRef.value?.filter(val)
})
//
const filterNode = (value, data) => {
if (!value) return true
return data.label.includes(value)
}
//
const getSchoolTree = async () => {
const res = await request.get('/business/student/schoolTree')
if (res.code === 200) {
schoolTree.value = res.data
}
}
//
const getList = async () => {
loading.value = true
try {
const res = await request.get('/business/student/list', { params: queryParams.value })
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
//
const handleNodeClick = (data) => {
//
if (data.type === 'school') {
queryParams.value.schoolId = data.id
queryParams.value.gradeId = ''
queryParams.value.classId = ''
} else if (data.type === 'grade') {
queryParams.value.gradeId = data.id
queryParams.value.classId = ''
} else if (data.type === 'class') {
queryParams.value.classId = data.id
}
queryParams.value.pageNum = 1
getList()
}
//
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.value = {
pageNum: 1,
pageSize: 10,
name: '',
studentNo: '',
gender: '',
schoolId: '',
gradeId: '',
classId: ''
}
treeRef.value?.setCurrentKey(null)
getList()
}
//
const handleAdd = () => {
studentDialogRef.value?.open()
}
//
const handleEdit = (row) => {
studentDialogRef.value?.open(row)
}
//
const handleImport = () => {
importDialogRef.value?.open()
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除学生"${row.name}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/api/student/${row.id}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}).catch(() => {})
}
onMounted(() => {
getSchoolTree()
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>