兵马未动 粮草先行

This commit is contained in:
RISE
2026-02-08 23:47:50 +08:00
parent 00a005e65f
commit 2670340af3
47 changed files with 3909 additions and 257 deletions

View File

@@ -1,28 +1,59 @@
<template>
<el-dialog
title="查看详情"
v-model="visible"
:close-on-click-modal="false"
<el-dialog
title="查看详情"
v-model="visible"
:close-on-click-modal="false"
draggable
width="800px">
<div v-loading="loading" class="detail-container" v-if="detailData">
<el-descriptions :column="2" border>
width="960px">
<div class="detail-container">
<!-- 活动主信息来自列表行 -->
<el-descriptions v-if="mainInfo.activityTheme" :column="2" border class="mb16">
<el-descriptions-item label="活动主题" :span="2">
{{ detailData.activityTheme || '-' }}
{{ mainInfo.activityTheme || '-' }}
</el-descriptions-item>
<el-descriptions-item label="活动说明" :span="2">
{{ detailData.remarks || '-' }}
{{ mainInfo.remarks || '-' }}
</el-descriptions-item>
<el-descriptions-item label="活动兼报数">
{{ detailData.maxSub !== undefined && detailData.maxSub !== null ? detailData.maxSub : '-' }}
{{ mainInfo.maxSub !== undefined && mainInfo.maxSub !== null ? mainInfo.maxSub : '-' }}
</el-descriptions-item>
<el-descriptions-item label="开始时间">
{{ parseTime(detailData.startTime, '{y}-{m}-{d}') }}
{{ mainInfo.startTime ? parseTime(mainInfo.startTime, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
<el-descriptions-item label="结束时间">
{{ parseTime(detailData.endTime, '{y}-{m}-{d}') }}
{{ mainInfo.endTime ? parseTime(mainInfo.endTime, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 子项目列表接口getActivityInfoSubList -->
<div class="sub-title">子项目列表</div>
<el-table
:data="subList"
v-loading="loading"
border
size="small"
max-height="400"
style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="subTitle" label="子项目名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="deptName" label="学院" width="110" show-overflow-tooltip />
<el-table-column prop="classNo" label="班号" width="80" align="center" />
<el-table-column prop="classMasterName" label="班主任" width="90" show-overflow-tooltip />
<el-table-column prop="startTime" label="开始时间" width="155" align="center">
<template #default="scope">
{{ scope.row.startTime ? parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" width="155" align="center">
<template #default="scope">
{{ scope.row.endTime ? parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column prop="maxNum" label="人数限制" width="88" align="center" />
<el-table-column prop="applyNums" label="已报名" width="78" align="center" />
<el-table-column prop="position" label="地点" min-width="100" show-overflow-tooltip />
<el-table-column prop="projectDescription" label="项目描述" min-width="180" show-overflow-tooltip />
</el-table>
<el-empty v-if="!loading && subList.length === 0" description="暂无子项目" :image-size="80" />
</div>
<template #footer>
<span class="dialog-footer">
@@ -33,47 +64,40 @@
</template>
<script setup lang="ts" name="ActivityInfoDetailDialog">
import { ref } from 'vue'
import { getDetail } from '/@/api/stuwork/activityinfo'
import { ref, reactive } from 'vue'
import { getActivityInfoSubList } from '/@/api/stuwork/activityinfosub'
import { parseTime } from '/@/utils/formatTime'
import { useMessage } from '/@/hooks/message'
// 定义变量内容
const visible = ref(false)
const loading = ref(false)
const detailData = ref<any>({})
const mainInfo = reactive<Record<string, any>>({})
const subList = ref<any[]>([])
// 打开弹窗
const openDialog = async (id: string) => {
/**
* 打开弹窗:使用接口 getActivityInfoSubList(activityInfoId) 获取详情子项目列表
* @param activityInfoId 活动信息ID
* @param row 列表行数据,用于展示活动主题等主信息
*/
const openDialog = async (activityInfoId: string, row?: any) => {
visible.value = true
loading.value = true
detailData.value = {}
subList.value = []
Object.keys(mainInfo).forEach((k) => delete mainInfo[k])
if (row && typeof row === 'object') {
Object.assign(mainInfo, row)
}
try {
const res = await getDetail(id)
if (res.data) {
// 根据接口文档,返回的数据可能是 { records: [...], total: ... } 格式
// 如果是列表格式,取第一条;如果是对象,直接使用
if (res.data.records && Array.isArray(res.data.records) && res.data.records.length > 0) {
detailData.value = res.data.records[0]
} else if (res.data.records && Array.isArray(res.data.records)) {
// 列表为空
useMessage().warning('未找到详情数据')
visible.value = false
} else {
// 直接是对象
detailData.value = res.data
}
}
} catch (err: any) {
useMessage().error(err.msg || '获取详情失败')
visible.value = false
const res = await getActivityInfoSubList(activityInfoId)
const data = res.data
subList.value = Array.isArray(data) ? data : []
} catch (_err) {
subList.value = []
} finally {
loading.value = false
}
}
// 暴露方法
defineExpose({
openDialog
})
@@ -81,7 +105,15 @@ defineExpose({
<style scoped lang="scss">
.detail-container {
padding: 20px 0;
padding: 8px 0;
}
.mb16 {
margin-bottom: 16px;
}
.sub-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
</style>

View File

@@ -250,9 +250,9 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// 查看详情
// 查看详情接口getActivityInfoSubList传入活动ID与行数据用于展示
const handleView = (row: any) => {
detailDialogRef.value?.openDialog(row.id)
detailDialogRef.value?.openDialog(row.id, row)
}
// 编辑

View File

@@ -48,6 +48,17 @@
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="默认扣分值" prop="score">
<el-input-number
v-model="form.score"
:min="0"
:max="999"
:precision="0"
placeholder="请输入默认扣分值"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
@@ -86,12 +97,13 @@ const loading = ref(false)
const operType = ref('add')
const categoryList = ref<any[]>([])
// 提交表单数据
// 提交表单数据与接口文档一致edit 含 score 默认扣分值)
const form = reactive({
id: '',
categortyId: '',
pointName: '',
standard: '',
score: undefined as number | undefined,
remarks: ''
})
@@ -117,6 +129,7 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.categortyId = ''
form.pointName = ''
form.standard = ''
form.score = undefined
form.remarks = ''
// 编辑时填充数据
@@ -125,20 +138,21 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.categortyId = row.categortyId || ''
form.pointName = row.pointName || ''
form.standard = row.standard || ''
form.score = row.score !== undefined && row.score !== null ? Number(row.score) : undefined
form.remarks = row.remarks || ''
// 如果需要获取详情
if (row.id && (!row.pointName || !row.standard)) {
// 如果需要获取详情(含 score
if (row.id && (!row.pointName || row.standard === undefined || row.score === undefined)) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.categortyId = res.data.categortyId || ''
form.pointName = res.data.pointName || ''
form.standard = res.data.standard || ''
form.score = res.data.score !== undefined && res.data.score !== null ? Number(res.data.score) : undefined
form.remarks = res.data.remarks || ''
}
}).catch((err) => {
}).finally(() => {
}).catch(() => {}).finally(() => {
loading.value = false
})
}
@@ -155,10 +169,12 @@ const onSubmit = async () => {
loading.value = true
try {
// 与接口文档一致categortyId, pointName, standard, score, remarks
const submitData = {
categortyId: form.categortyId,
pointName: form.pointName,
standard: form.standard || '',
score: form.score !== undefined && form.score !== null ? Number(form.score) : 0,
remarks: form.remarks || ''
}

View File

@@ -17,6 +17,21 @@
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="考核项" prop="categortyId">
<el-select
v-model="state.queryForm.categortyId"
placeholder="请选择考核项"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in categoryList"
:key="item.id"
:label="item.category"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="指标名称" prop="pointName">
<el-input
v-model="state.queryForm.pointName"
@@ -155,9 +170,10 @@
</template>
<script setup lang="ts" name="AssessmentPoint">
import { reactive, ref } from 'vue'
import { reactive, ref, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/assessmentpoint";
import { getList as getAssessmentCategoryList } from "/@/api/stuwork/assessmentcategory";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
@@ -169,6 +185,7 @@ const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref()
const showSearch = ref(true)
const categoryList = ref<any[]>([])
// 表格列配置
const tableColumns = [
@@ -194,9 +211,10 @@ const tableStyle = {
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// 配置 useTable
// 配置 useTable(分页接口支持 categortyId、pointName与考核项对应
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
categortyId: '',
pointName: ''
},
pageList: fetchList,
@@ -215,13 +233,28 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// 获取考核项列表与表单考核项对应接口assessmentcategory/list
const getCategoryList = async () => {
try {
const res = await getAssessmentCategoryList()
categoryList.value = res.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
categoryList.value = []
}
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.categortyId = ''
state.queryForm.pointName = ''
getDataList()
}
onMounted(() => {
getCategoryList()
})
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)

View File

@@ -69,8 +69,7 @@
v-model="form.score"
:precision="0"
:step="1"
:min="0"
placeholder="请输入分数"
placeholder="请输入分数(可为负数)"
style="width: 100%" />
</el-form-item>
</el-col>

View File

@@ -12,6 +12,53 @@
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="form.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="form.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="班号" prop="classCode">
<el-select
v-model="form.classCode"
placeholder="请选择班号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classCode" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="标题" prop="title">
<el-input
@@ -52,9 +99,12 @@
</template>
<script setup lang="ts" name="ClassPlanFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/classplan'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getDicts } from '/@/api/admin/dict'
import Editor from '/@/components/Editor/index.vue'
const emit = defineEmits(['refresh'])
@@ -64,10 +114,16 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const classList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
classCode: '',
title: '',
content: '',
remarks: ''
@@ -75,6 +131,15 @@ const form = reactive({
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
classCode: [
{ required: true, message: '请选择班号', trigger: 'change' }
],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
@@ -92,6 +157,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.classCode = ''
form.title = ''
form.content = ''
form.remarks = ''
@@ -99,6 +167,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.classCode = row.classCode || ''
form.title = row.title || ''
form.content = row.content || ''
form.remarks = row.remarks || ''
@@ -108,6 +179,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.schoolYear = res.data.schoolYear || form.schoolYear
form.schoolTerm = res.data.schoolTerm || form.schoolTerm
form.classCode = res.data.classCode || form.classCode
form.content = res.data.content || ''
form.remarks = res.data.remarks || ''
}
@@ -119,6 +193,43 @@ const openDialog = async (type: string = 'add', row?: any) => {
})
}
// 学年列表(班级管理-学年接口)
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 学期字典(系统通用)
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res?.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 班级列表(班级管理目录下)
const getClassListData = async () => {
try {
const res = await getClassListByRole()
classList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
classList.value = []
}
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
@@ -129,6 +240,9 @@ const onSubmit = async () => {
loading.value = true
try {
const submitData = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
classCode: form.classCode,
title: form.title,
content: form.content,
remarks: form.remarks
@@ -154,6 +268,13 @@ const onSubmit = async () => {
})
}
// 初始化:加载学年、学期、班级
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getClassListData()
})
// 暴露方法
defineExpose({
openDialog

View File

@@ -79,7 +79,7 @@
<script setup lang="ts" name="ClassroomBaseArrangeDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editObj } from '/@/api/stuwork/classroombase'
import { addClassRoomAssign } from '/@/api/stuwork/teachclassroomassign'
import { queryAllClass } from '/@/api/basic/basicclass'
import { getClassRoomList } from '/@/api/stuwork/teachclassroom'
import { getBuildingList } from '/@/api/stuwork/teachbuilding'
@@ -150,17 +150,16 @@ const onSubmit = async () => {
loading.value = true
try {
await editObj({
id: form.id,
await addClassRoomAssign({
buildingNo: form.buildingNo,
classCode: form.classCode,
position: form.position
position: form.position,
classCode: form.classCode
})
useMessage().success('教室安排成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '教室安排失败')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}

View File

@@ -45,40 +45,6 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="form.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="班级" prop="classNo">
<el-select
v-model="form.classNo"
placeholder="请选择班级"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classNo">
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -95,8 +61,6 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { initObj } from '/@/api/stuwork/classtheme'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getDeptList } from '/@/api/basic/basicclass'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
@@ -107,15 +71,11 @@ const visible = ref(false)
const loading = ref(false)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const deptList = ref<any[]>([])
const classList = ref<any[]>([])
// 提交表单数据
const form = reactive({
schoolYear: '',
schoolTerm: '',
deptCode: '',
classNo: ''
schoolTerm: ''
})
// 定义校验规则
@@ -125,12 +85,6 @@ const dataRules = {
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
deptCode: [
{ required: true, message: '请选择学院', trigger: 'change' }
],
classNo: [
{ required: true, message: '请选择班级', trigger: 'change' }
]
}
@@ -141,8 +95,6 @@ const openDialog = () => {
dataFormRef.value?.resetFields()
form.schoolYear = ''
form.schoolTerm = ''
form.deptCode = ''
form.classNo = ''
})
}
@@ -192,34 +144,10 @@ const getSchoolTermDict = async () => {
}
}
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
if (res.data && Array.isArray(res.data)) {
deptList.value = res.data
}
} catch (err) {
}
}
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
if (res.data && Array.isArray(res.data)) {
classList.value = res.data
}
} catch (err) {
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getDeptListData()
getClassListData()
})
// 暴露方法

View File

@@ -89,9 +89,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -138,7 +140,8 @@ const loading = ref(false)
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -206,28 +209,33 @@ const handleStudentChange = (stuNo: string) => {
}
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
form.bedNo = ''
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
// 根据返回的数据结构处理床位号列表
// 假设返回的是数字数组或对象数组
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -321,3 +329,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>

View File

@@ -211,9 +211,13 @@
<el-icon><component :is="columnConfigMap[col.prop || '']?.icon || OfficeBuilding" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 床位号列特殊模板 -->
<!-- 床位号列haveStudent true 时变色标记有人 -->
<template v-if="col.prop === 'bedNo'" #default="scope">
<el-tag v-if="scope.row.bedNo" size="small" type="info" effect="plain">
<el-tag
v-if="scope.row.bedNo"
size="small"
:type="scope.row.haveStudent ? 'warning' : 'info'"
effect="plain">
{{ scope.row.bedNo }}
</el-tag>
<span v-else>-</span>
@@ -274,6 +278,12 @@
<!-- 转宿弹窗 -->
<TransferDialog ref="transferDialogRef" @refresh="getDataList" />
<!-- 宿舍互换弹窗 -->
<SwapDialog ref="swapDialogRef" @refresh="getDataList" />
<!-- 打印宿舍卡弹窗 -->
<PrintCardDialog ref="printCardDialogRef" />
</div>
</template>
@@ -281,7 +291,7 @@
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObjs } from "/@/api/stuwork/dormroomstudent";
import { fetchList, delObjs, exportEmptyPeopleRoomExcel } from "/@/api/stuwork/dormroomstudent";
import { getDeptList } from "/@/api/basic/basicclass";
import { getBuildingList } from "/@/api/stuwork/dormbuilding";
import { fetchDormRoomTreeList } from "/@/api/stuwork/dormroom";
@@ -289,6 +299,8 @@ import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue';
import TransferDialog from './transfer.vue';
import SwapDialog from './swap.vue';
import PrintCardDialog from './printCard.vue';
import TreeSelect from '/@/components/TreeSelect/index.vue';
import { List, OfficeBuilding, House, Grid, UserFilled, Phone, CreditCard, Avatar, User, Setting, Menu, Search, Document } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
@@ -301,6 +313,8 @@ const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
const transferDialogRef = ref()
const swapDialogRef = ref()
const printCardDialogRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const deptList = ref<any[]>([])
@@ -425,29 +439,91 @@ const handleDormDataTypeChange = (dormdataType: string) => {
getDormRoomTreeListData(dormdataType)
}
// 打印宿舍卡
// 打印宿舍卡(按房间号查询后打印)
const handlePrintCard = () => {
useMessage().warning('功能开发中')
printCardDialogRef.value?.openDialog()
}
// 宿舍互换
// 宿舍互换(两名学生互换宿舍)
const handleRoomSwap = () => {
useMessage().warning('功能开发中')
const query = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
swapDialogRef.value?.openDialog(query)
}
// 导出
const handleExport = () => {
useMessage().warning('功能开发中')
// 导出:空 n 人宿舍导出(按当前筛选条件传参)
const handleExport = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `空宿舍导出_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 名单导出
const handleExportList = () => {
useMessage().warning('功能开发中')
// 名单导出:与导出共用空 n 人宿舍导出接口,文件名区分
const handleExportList = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `住宿学生名单_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 编辑(与转宿共用接口 edit修改房间/床位/是否舍长)
const handleEdit = (row: any) => {
transferDialogRef.value?.openDialog(row)
}
// 转宿
const handleTransfer = (row: any) => {
transferDialogRef.value.openDialog(row)
transferDialogRef.value?.openDialog(row)
}
// 退宿

View File

@@ -0,0 +1,152 @@
<template>
<el-dialog
title="打印宿舍卡"
v-model="visible"
:close-on-click-modal="false"
width="640px"
destroy-on-close
@closed="onClosed">
<div v-loading="loading">
<el-form :inline="true" class="mb16">
<el-form-item label="房间号">
<el-input v-model="roomNo" placeholder="请输入房间号" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFetch">查询并预览</el-button>
</el-form-item>
</el-form>
<div v-if="printData.length" ref="printAreaRef" class="print-area">
<div v-for="(room, idx) in printData" :key="idx" class="room-card mb16">
<div class="room-title">房间号{{ room.roomNo }}</div>
<table class="print-table">
<thead>
<tr>
<th>姓名</th>
<th>学号</th>
<th>床位</th>
<th>是否舍长</th>
<th>班级</th>
</tr>
</thead>
<tbody>
<tr v-for="(s, i) in (room.dormRoomStudentVOList || [])" :key="i">
<td>{{ s.realName }}</td>
<td>{{ s.stuNo }}</td>
<td>{{ s.bedNo }}</td>
<td>{{ s.isLeader === '1' ? '是' : '否' }}</td>
<td>{{ s.className || s.classNo }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handlePrint" :disabled="!printData.length"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentPrintCard">
import { ref } from 'vue'
import { useMessage } from '/@/hooks/message'
import { printDormRoomData } from '/@/api/stuwork/dormroomstudent'
const visible = ref(false)
const loading = ref(false)
const roomNo = ref('')
const printData = ref<any[]>([])
const printAreaRef = ref<HTMLElement>()
const openDialog = (initialRoomNo?: string) => {
visible.value = true
roomNo.value = initialRoomNo || ''
printData.value = []
}
const onClosed = () => {
printData.value = []
}
const handleFetch = async () => {
const no = (roomNo.value || '').trim()
if (!no) {
useMessage().warning('请输入房间号')
return
}
loading.value = true
try {
const res = await printDormRoomData(no)
const data = res?.data ?? res
printData.value = Array.isArray(data) ? data : (data ? [data] : [])
if (!printData.value.length) {
useMessage().info('该房间暂无数据')
}
} catch (err: any) {
useMessage().error(err?.msg || '查询失败')
printData.value = []
} finally {
loading.value = false
}
}
const handlePrint = () => {
if (!printData.value.length) return
const win = window.open('', '_blank')
if (!win) {
useMessage().error('无法打开打印窗口')
return
}
const el = printAreaRef.value
if (el) {
win.document.write(`
<!DOCTYPE html><html><head><meta charset="utf-8"><title>宿舍卡</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px 8px; text-align: center; }
.room-title { font-weight: bold; margin-bottom: 8px; }
</style>
</head><body>${el.innerHTML}</body></html>
`)
win.document.close()
win.focus()
setTimeout(() => {
win.print()
win.close()
}, 300)
}
}
defineExpose({ openDialog })
</script>
<style scoped lang="scss">
.print-area {
max-height: 60vh;
overflow: auto;
}
.room-card {
padding: 12px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
.room-title {
margin-bottom: 8px;
}
.print-table {
width: 100%;
border-collapse: collapse;
th,
td {
border: 1px solid var(--el-border-color);
padding: 6px 8px;
text-align: center;
}
}
.mb16 {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<el-dialog
title="宿舍互换"
v-model="visible"
:close-on-click-modal="false"
draggable
width="520px">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
:validate-on-rule-change="false"
v-loading="loading">
<el-form-item label="学生一" prop="sourceSutNo">
<el-select
v-model="form.sourceSutNo"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onSourceChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</el-select>
</el-form-item>
<el-form-item label="学生二" prop="targetStuNO">
<el-select
v-model="form.targetStuNO"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onTargetChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :loading="submitting"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentSwapDialog">
import { ref, reactive } from 'vue'
import { useMessage } from '/@/hooks/message'
import { exchangeRoom } from '/@/api/stuwork/dormroomstudent'
import { fetchList } from '/@/api/stuwork/dormroomstudent'
const emit = defineEmits(['refresh'])
const formRef = ref()
const visible = ref(false)
const loading = ref(false)
const submitting = ref(false)
const studentOptions = ref<any[]>([])
const form = reactive({
sourceSutNo: '',
targetStuNO: ''
})
const rules = {
sourceSutNo: [{ required: true, message: '请选择学生一', trigger: 'change' }],
targetStuNO: [{ required: true, message: '请选择学生二', trigger: 'change' }]
}
const onSourceChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.targetStuNO = ''
}
}
const onTargetChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.sourceSutNo = ''
}
}
const openDialog = async (queryParams?: any) => {
visible.value = true
form.sourceSutNo = ''
form.targetStuNO = ''
loading.value = true
try {
const res = await fetchList({
current: 1,
size: 500,
...queryParams
})
const list = res?.data?.records ?? res?.records ?? []
studentOptions.value = Array.isArray(list) ? list : []
} catch (err) {
studentOptions.value = []
} finally {
loading.value = false
}
}
const onSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
if (form.sourceSutNo === form.targetStuNO) {
useMessage().warning('请选择两名不同学生')
return
}
submitting.value = true
try {
await exchangeRoom({
sourceSutNo: form.sourceSutNo,
targetStuNO: form.targetStuNO
})
useMessage().success('互换成功')
visible.value = false
emit('refresh')
} catch (err: any) {
console.error(err)
} finally {
submitting.value = false
}
})
}
defineExpose({ openDialog })
</script>

View File

@@ -41,9 +41,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -95,7 +97,8 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -119,26 +122,33 @@ const dataRules = {
]
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
form.bedNo = ''
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -157,20 +167,20 @@ const openDialog = async (row: any) => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = row.id || ''
form.roomNo = row.roomNo || ''
await nextTick()
dataFormRef.value?.resetFields()
form.id = row.id || ''
form.roomNo = row.roomNo || ''
form.bedNo = row.bedNo || ''
form.stuNo = row.stuNo || ''
form.isLeader = row.isLeader || '0'
bedNoList.value = []
// 如果有房间号,先拉取床位列表再回填床位号(避免 handleRoomChange 清空 bedNo
if (form.roomNo) {
await handleRoomChange(form.roomNo)
form.bedNo = row.bedNo || ''
form.stuNo = row.stuNo || ''
form.isLeader = row.isLeader || '0'
bedNoList.value = []
// 如果有房间号,获取床位号列表
if (form.roomNo) {
handleRoomChange(form.roomNo)
}
})
}
}
// 提交表单
@@ -224,3 +234,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>

View File

@@ -0,0 +1,227 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选 -->
<el-card class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
统计条件
</span>
</div>
</template>
<el-form :model="queryForm" :inline="true" class="search-form">
<el-form-item label="毕业年份" prop="graduYear">
<el-select
v-model="queryForm.graduYear"
placeholder="请选择毕业年份"
clearable
style="width: 160px">
<el-option
v-for="y in graduYearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadStatistics">查询统计</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 汇总卡片 -->
<el-row :gutter="16" class="summary-row">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-label">毕业生总数</div>
<div class="stat-value">{{ summary.total }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-success">
<div class="stat-label">确认毕业</div>
<div class="stat-value">{{ summary.confirmed }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-warning">
<div class="stat-label">待确认</div>
<div class="stat-value">{{ summary.pending }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-danger">
<div class="stat-label">不可毕业</div>
<div class="stat-value">{{ summary.rejected }}</div>
</el-card>
</el-col>
</el-row>
<!-- 按学院统计表格 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
按学院统计
</span>
</div>
</template>
<el-table
:data="deptStats"
v-loading="loading"
stripe
border
class="modern-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="deptName" label="学院" min-width="140" show-overflow-tooltip />
<el-table-column prop="total" label="应毕业人数" width="110" align="center" />
<el-table-column prop="pending" label="待确认" width="90" align="center" />
<el-table-column prop="confirmed" label="确认毕业" width="100" align="center" />
<el-table-column prop="rejected" label="不可毕业" width="100" align="center" />
<el-table-column prop="completionRate" label="完成率" width="100" align="center">
<template #default="scope">
<span :class="completionRateClass(scope.row.completionRate)">
{{ scope.row.completionRate }}
</span>
</template>
</el-table-column>
</el-table>
<template #empty>
<el-empty description="请选择毕业年份并点击「查询统计」" :image-size="100" />
</template>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="GradustuAnalyse">
import { reactive, ref, computed, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { fetchListForAnalyse } from '/@/api/stuwork/gradustu'
import { Search, Document } from '@element-plus/icons-vue'
const queryForm = reactive({
graduYear: ''
})
const loading = ref(false)
const rawList = ref<any[]>([])
const graduYearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 汇总:总人数、确认毕业、待确认、不可毕业
const summary = computed(() => {
const list = rawList.value
let pending = 0
let confirmed = 0
let rejected = 0
list.forEach((item: any) => {
const s = String(item.status ?? '')
if (s === '0') pending++
else if (s === '1') confirmed++
else if (s === '-1') rejected++
})
return {
total: list.length,
pending,
confirmed,
rejected
}
})
// 按学院聚合
const deptStats = computed(() => {
const list = rawList.value
const map: Record<string, { deptCode: string; deptName: string; total: number; pending: number; confirmed: number; rejected: number }> = {}
list.forEach((item: any) => {
const code = item.deptCode || '未知'
const name = item.deptName || item.deptCode || '未知'
if (!map[code]) {
map[code] = { deptCode: code, deptName: name, total: 0, pending: 0, confirmed: 0, rejected: 0 }
}
const row = map[code]
row.total++
const s = String(item.status ?? '')
if (s === '0') row.pending++
else if (s === '1') row.confirmed++
else if (s === '-1') row.rejected++
})
return Object.values(map).map((row) => ({
...row,
completionRate: row.total > 0 ? ((row.confirmed / row.total) * 100).toFixed(1) + '%' : '0%'
}))
})
const completionRateClass = (rate: string) => {
const num = parseFloat(rate)
if (num >= 100) return 'rate-high'
if (num >= 80) return 'rate-mid'
return 'rate-low'
}
const loadStatistics = async () => {
if (!queryForm.graduYear) {
useMessage().warning('请选择毕业年份')
return
}
loading.value = true
try {
const list = await fetchListForAnalyse(queryForm.graduYear)
rawList.value = list
} catch (err: any) {
useMessage().error(err.msg || '获取数据失败')
rawList.value = []
} finally {
loading.value = false
}
}
onMounted(() => {
queryForm.graduYear = String(new Date().getFullYear())
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.summary-row {
margin-bottom: 16px;
}
.stat-card {
text-align: center;
.stat-label {
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 24px;
font-weight: 600;
margin-top: 8px;
}
&.stat-success .stat-value {
color: var(--el-color-success);
}
&.stat-warning .stat-value {
color: var(--el-color-warning);
}
&.stat-danger .stat-value {
color: var(--el-color-danger);
}
}
.rate-high {
color: var(--el-color-success);
font-weight: 500;
}
.rate-mid {
color: var(--el-color-warning);
}
.rate-low {
color: var(--el-color-danger);
}
</style>

View File

@@ -0,0 +1,342 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="毕业年份" prop="graduYear">
<el-select
v-model="searchForm.graduYear"
placeholder="请选择毕业年份"
clearable
filterable
style="width: 140px">
<el-option
v-for="y in graduYearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item label="毕业状态" prop="status">
<el-select
v-model="searchForm.status"
placeholder="请选择"
clearable
style="width: 140px">
<el-option label="待确认" value="0" />
<el-option label="确认毕业" value="1" />
<el-option label="不可毕业" value="-1" />
</el-select>
</el-form-item>
<el-form-item label="毕业类型" prop="type">
<el-select
v-model="searchForm.type"
placeholder="请选择"
clearable
style="width: 120px">
<el-option label="段段清" value="1" />
<el-option label="正常毕业" value="2" />
</el-select>
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="searchForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 140px" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="请输入姓名"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="searchForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 160px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item label="班号" prop="classNo">
<el-input
v-model="searchForm.classNo"
placeholder="请输入班号"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
学生毕业处理列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
:loading="makeLoading"
@click="handleMakeGraduStu">
生成毕业生信息
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'status'" #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small">
{{ formatStatus(scope.row.status) }}
</el-tag>
</template>
<template v-else-if="col.prop === 'type'" #default="scope">
{{ scope.row.type === '1' ? '段段清' : scope.row.type === '2' ? '正常毕业' : scope.row.type || '-' }}
</template>
</el-table-column>
</template>
<template #empty>
<el-empty description="暂无数据,请选择条件查询或先生成毕业生信息" :image-size="120" />
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="Gradustu">
import { reactive, ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList, makeGraduStu } from '/@/api/stuwork/gradustu'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { getDeptList } from '/@/api/basic/basicclass'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import {
List,
Calendar,
UserFilled,
Document,
Setting,
Menu,
Search,
Grid
} from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const makeLoading = ref(false)
const deptList = ref<any[]>([])
// 毕业年份:当前年前后各 5 年
const graduYearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'stuNo', label: '学号', icon: UserFilled },
{ prop: 'realName', label: '姓名', icon: UserFilled },
{ prop: 'classNo', label: '班号', icon: Grid },
{ prop: 'majorName', label: '专业名称', icon: Document, minWidth: 140 },
{ prop: 'graduYear', label: '毕业年份', icon: Calendar },
{ prop: 'status', label: '毕业状态', icon: Document },
{ prop: 'type', label: '毕业类型', icon: Document },
{ prop: 'scoreCondition', label: '学分情况', icon: Document, minWidth: 100 },
{ prop: 'skillCondition', label: '技能情况', icon: Document, minWidth: 100 },
{ prop: 'conductCondition', label: '操行情况', icon: Document, minWidth: 100 },
{ prop: 'specialGradu', label: '特殊毕业', icon: Document, minWidth: 100 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
graduYear: '',
status: '',
type: '',
stuNo: '',
realName: '',
deptCode: '',
classNo: ''
})
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const formatStatus = (status: string) => {
const map: Record<string, string> = { '0': '待确认', '1': '确认毕业', '-1': '不可毕业' }
return map[status] || status || '-'
}
const statusTagType = (status: string) => {
const map: Record<string, string> = { '0': 'warning', '1': 'success', '-1': 'danger' }
return map[status] || 'info'
}
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.graduYear = ''
searchForm.status = ''
searchForm.type = ''
searchForm.stuNo = ''
searchForm.realName = ''
searchForm.deptCode = ''
searchForm.classNo = ''
getDataList()
}
const handleMakeGraduStu = async () => {
try {
await useMessageBox().confirm('确定要生成毕业生信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
makeLoading.value = true
try {
await makeGraduStu({ type: '0' })
useMessage().success('生成成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '生成失败')
} finally {
makeLoading.value = false
}
}
const loadDeptList = async () => {
try {
const res = await getDeptList()
deptList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
deptList.value = []
}
}
onMounted(() => {
loadDeptList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,154 @@
<template>
<el-dialog
title="新增排班"
v-model="visible"
:width="500"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-form-item label="值班日期" prop="date">
<el-date-picker
v-model="form.date"
type="date"
placeholder="选择值班日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled-date="disabledDate"
style="width: 100%" />
</el-form-item>
<el-form-item label="预约师" prop="teacherUserName">
<el-select
v-model="form.teacherUserName"
placeholder="请选择预约师"
filterable
clearable
style="width: 100%">
<el-option
v-for="item in teacherList"
:key="item.userName"
:label="(item.realName || '') + (item.userName ? ` (${item.userName})` : '')"
:value="item.userName" />
</el-select>
</el-form-item>
<el-form-item label="周类型" prop="weekType">
<el-select
v-model="form.weekType"
placeholder="请选择"
clearable
style="width: 100%">
<el-option label="单周" value="single" />
<el-option label="双周" value="double" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="PsychologicalCounselingDutyFormDialog">
import { ref, reactive, nextTick, watch } from 'vue'
import { useMessage } from '/@/hooks/message'
import { saveDuty } from '/@/api/stuwork/psychologicalcounselingduty'
import { getList } from '/@/api/stuwork/psychologicalcounselingteacher'
const props = defineProps<{
year?: number
month?: number
}>()
const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const teacherList = ref<any[]>([])
const form = reactive({
date: '',
teacherUserName: '',
weekType: 'single'
})
const dataRules = {
date: [{ required: true, message: '请选择值班日期', trigger: 'change' }],
teacherUserName: [{ required: true, message: '请选择预约师', trigger: 'change' }]
}
// 限制只能选当前年月的日期
const disabledDate = (time: Date) => {
if (!props.year || !props.month) return false
const y = time.getFullYear()
const m = time.getMonth() + 1
if (y !== props.year) return true
if (m !== props.month) return true
return false
}
const loadTeacherList = async () => {
try {
const res = await getList()
if (res.data && Array.isArray(res.data)) {
teacherList.value = res.data
} else {
teacherList.value = []
}
} catch (err) {
teacherList.value = []
}
}
const openDialog = () => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
form.date = ''
form.teacherUserName = ''
form.weekType = 'single'
})
}
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
await saveDuty([
{
date: form.date,
teacherUserName: form.teacherUserName,
weekType: form.weekType || 'single'
}
])
useMessage().success('排班成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '排班失败')
} finally {
loading.value = false
}
})
}
watch(visible, (v) => {
if (v) loadTeacherList()
})
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,310 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="年份" prop="year">
<el-select
v-model="searchForm.year"
placeholder="请选择年份"
clearable
style="width: 120px">
<el-option
v-for="y in yearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item label="月份" prop="month">
<el-select
v-model="searchForm.month"
placeholder="请选择月份"
clearable
style="width: 120px">
<el-option
v-for="m in 12"
:key="m"
:label="m + '月'"
:value="m" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Calendar /></el-icon>
值班管理列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef?.openDialog()">
新增排班
</el-button>
<el-button
icon="Delete"
type="danger"
plain
:disabled="!dataList.length"
@click="handleClearMonth">
一键清空本月
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="dataList"
v-loading="loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'reservation'" #default="scope">
<el-tag :type="scope.row.reservation === '1' ? 'success' : 'info'" size="small">
{{ scope.row.reservation === '1' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Delete"
link
type="danger"
@click="handleClearOne(scope.row)">
清除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无值班数据,请选择年月查询或新增排班" :image-size="120">
<el-button type="primary" icon="FolderAdd" @click="formDialogRef?.openDialog()">新增排班</el-button>
</el-empty>
</template>
</el-table>
</el-card>
</div>
<!-- 新增排班弹窗 -->
<FormDialog
ref="formDialogRef"
:year="searchForm.year"
:month="searchForm.month"
@refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PsychologicalCounselingDuty">
import { reactive, ref, computed, onMounted, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { getDutyByMonth, clearDuty, clearOneDuty } from '/@/api/stuwork/psychologicalcounselingduty'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { List, Calendar, UserFilled, Phone, Document, Setting, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const FormDialog = defineAsyncComponent(() => import('./form.vue'))
const route = useRoute()
const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const loading = ref(false)
const dataList = ref<any[]>([])
// 年份选项:当前年前后各 5 年
const yearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'date', label: '值班日期', icon: Calendar },
{ prop: 'dutyTime', label: '值班时间', icon: Calendar },
{ prop: 'realName', label: '教师姓名', icon: UserFilled },
{ prop: 'userName', label: '用户名', icon: UserFilled },
{ prop: 'phone', label: '联系方式', icon: Phone, minWidth: 120 },
{ prop: 'reservation', label: '是否预约', icon: Document }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 表格样式(与 useTable 一致)
const tableStyle = {
cellStyle: {},
headerCellStyle: {}
}
// 搜索表单
const searchForm = reactive({
year: new Date().getFullYear(),
month: new Date().getMonth() + 1
})
const getDataList = async () => {
if (!searchForm.year || !searchForm.month) {
useMessage().warning('请选择年份和月份')
return
}
loading.value = true
try {
const res = await getDutyByMonth({
year: searchForm.year,
month: searchForm.month
})
dataList.value = Array.isArray(res.data) ? res.data : []
} catch (err: any) {
useMessage().error(err.msg || '获取值班表失败')
dataList.value = []
} finally {
loading.value = false
}
}
const handleReset = () => {
const now = new Date()
searchForm.year = now.getFullYear()
searchForm.month = now.getMonth() + 1
getDataList()
}
const handleClearMonth = async () => {
if (!searchForm.year || !searchForm.month) {
useMessage().warning('请先选择年份和月份')
return
}
try {
await useMessageBox().confirm(
`确定要清空 ${searchForm.year}${searchForm.month}月 的全部值班吗?`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
try {
await clearDuty({
year: Number(searchForm.year),
month: Number(searchForm.month)
})
useMessage().success('清空成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '清空失败')
}
}
const handleClearOne = async (row: any) => {
const dateStr = row.date || row.dutyTime || ''
if (!dateStr) {
useMessage().warning('无法获取日期')
return
}
const day = dateStr.split(' ')[0] || dateStr
try {
await useMessageBox().confirm(`确定要清除 ${day} 的值班吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await clearOneDuty({ days: day })
useMessage().success('清除成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '清除失败')
}
}
onMounted(() => {
getDataList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,309 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="600"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-form-item label="值班教师" prop="teacherNo">
<el-select
v-model="form.teacherNo"
placeholder="请选择值班教师"
filterable
clearable
style="width: 100%"
:disabled="!!form.id"
@change="handleTeacherChange">
<el-option
v-for="item in teacherList"
:key="item.userName"
:label="(item.realName || '') + (item.userName ? ` (${item.userName})` : '')"
:value="item.userName" />
</el-select>
</el-form-item>
<el-form-item label="预约时间" prop="reservationTime">
<el-date-picker
v-model="form.reservationTime"
type="datetime"
placeholder="选择预约时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
:disabled="!!form.id" />
</el-form-item>
<el-form-item label="班号" prop="classCode">
<el-select
v-model="form.classCode"
placeholder="请选择班号"
clearable
filterable
style="width: 100%"
:disabled="!!form.id"
@change="handleClassChange">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classCode" />
</el-select>
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-select
v-model="form.stuNo"
:placeholder="form.classCode ? (studentLoading ? '加载中...' : '请选择学生') : '请先选择班号'"
clearable
filterable
style="width: 100%"
:disabled="!form.classCode || !!form.id || studentLoading"
:loading="studentLoading"
@change="handleStudentChange">
<el-option
v-for="item in studentList"
:key="item.stuNo"
:label="`${item.realName || item.stuName || ''} (${item.stuNo})`"
:value="item.stuNo" />
</el-select>
</el-form-item>
<el-form-item label="学生姓名" prop="stuName">
<el-input v-model="form.stuName" placeholder="选学生后自动带出" disabled />
</el-form-item>
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="选学生后自动带出" disabled />
</el-form-item>
<el-form-item label="学生留言" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入学生留言"
maxlength="250"
show-word-limit
:disabled="!!form.id" />
</el-form-item>
<template v-if="form.id">
<el-form-item label="是否处理" prop="isHandle">
<el-select
v-model="form.isHandle"
placeholder="请选择"
clearable
style="width: 100%">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="老师备注" prop="teaRemark">
<el-input
v-model="form.teaRemark"
type="textarea"
:rows="3"
placeholder="请输入老师备注"
maxlength="250"
show-word-limit />
</el-form-item>
</template>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="PsychologicalCounselingReservationFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/psychologicalcounselingreservation'
import { getList } from '/@/api/stuwork/psychologicalcounselingteacher'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { queryAllStudentByClassCode } from '/@/api/basic/basicstudent'
const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const studentLoading = ref(false)
const operType = ref('add')
const teacherList = ref<any[]>([])
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const form = reactive({
id: '',
teacherNo: '',
realName: '',
reservationTime: '',
classCode: '',
classNo: '',
stuNo: '',
stuName: '',
phone: '',
remarks: '',
isHandle: '0',
teaRemark: ''
})
const dataRules = {
teacherNo: [{ required: true, message: '请选择值班教师', trigger: 'change' }],
reservationTime: [{ required: true, message: '请选择预约时间', trigger: 'change' }],
classCode: [{ required: true, message: '请选择班号', trigger: 'change' }],
stuNo: [{ required: true, message: '请选择学生', trigger: 'change' }]
}
const handleTeacherChange = (val: string) => {
const item = teacherList.value.find((t: any) => t.userName === val)
form.realName = item ? (item.realName || '') : ''
}
const handleClassChange = async () => {
form.stuNo = ''
form.stuName = ''
form.phone = ''
studentList.value = []
if (!form.classCode) return
studentLoading.value = true
try {
const res = await queryAllStudentByClassCode(form.classCode)
studentList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
studentList.value = []
} finally {
studentLoading.value = false
}
}
const handleStudentChange = (val: string) => {
const item = studentList.value.find((s: any) => s.stuNo === val)
if (item) {
form.stuName = item.realName || item.stuName || ''
form.phone = item.phone || item.mobile || ''
} else {
form.stuName = ''
form.phone = ''
}
}
const openDialog = (type: string = 'add', row?: any) => {
operType.value = type
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.teacherNo = ''
form.realName = ''
form.reservationTime = ''
form.classCode = ''
form.classNo = ''
form.stuNo = ''
form.stuName = ''
form.phone = ''
form.remarks = ''
form.isHandle = '0'
form.teaRemark = ''
studentList.value = []
if (type === 'edit' && row) {
form.id = row.id
form.teacherNo = row.teacherNo || ''
form.realName = row.realName || ''
form.reservationTime = row.reservationTime || ''
form.classNo = row.classNo || ''
const cls = classList.value.find((c: any) => c.classNo === row.classNo)
form.classCode = cls ? cls.classCode : (row.classCode || '')
form.stuNo = row.stuNo || ''
form.stuName = row.stuName || ''
form.phone = row.phone || ''
form.remarks = row.remarks || ''
form.isHandle = row.isHandle !== undefined && row.isHandle !== null ? String(row.isHandle) : '0'
form.teaRemark = row.teaRemark || ''
}
})
}
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
const selectedClass = classList.value.find((c: any) => c.classCode === form.classCode)
const classNo = selectedClass ? selectedClass.classNo : form.classNo || form.classCode
loading.value = true
try {
if (operType.value === 'add') {
await addObj({
teacherNo: form.teacherNo,
realName: form.realName,
reservationTime: form.reservationTime ? (form.reservationTime.length === 10 ? form.reservationTime + ' 00:00:00' : form.reservationTime) : '',
classNo,
stuNo: form.stuNo,
stuName: form.stuName,
phone: form.phone,
remarks: form.remarks
})
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
teacherNo: form.teacherNo,
realName: form.realName,
reservationTime: form.reservationTime,
classNo: form.classNo || classNo,
stuNo: form.stuNo,
stuName: form.stuName,
phone: form.phone,
remarks: form.remarks,
isHandle: form.isHandle,
teaRemark: form.teaRemark || ''
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} finally {
loading.value = false
}
})
}
const loadTeacherList = async () => {
try {
const res = await getList()
teacherList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
teacherList.value = []
}
}
const loadClassList = async () => {
try {
const res = await getClassListByRole()
classList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
classList.value = []
}
}
onMounted(() => {
loadTeacherList()
loadClassList()
})
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,296 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="searchForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 160px" />
</el-form-item>
<el-form-item label="班号" prop="classNo">
<el-input
v-model="searchForm.classNo"
placeholder="请输入班号"
clearable
style="width: 160px" />
</el-form-item>
<el-form-item label="预约时间" prop="reservationTime">
<el-date-picker
v-model="searchForm.reservationTime"
type="date"
placeholder="选择预约时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 180px" />
</el-form-item>
<el-form-item label="是否处理" prop="isHandle">
<el-select
v-model="searchForm.isHandle"
placeholder="请选择"
clearable
style="width: 120px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
预约记录列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef?.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'isHandle'" #default="scope">
<el-tag :type="scope.row.isHandle === '1' ? 'success' : 'info'" size="small">
{{ scope.row.isHandle === '1' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120">
<el-button type="primary" icon="FolderAdd" @click="formDialogRef?.openDialog()">新增预约记录</el-button>
</el-empty>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<FormDialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PsychologicalCounselingReservation">
import { reactive, ref, onMounted, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList, delObj } from '/@/api/stuwork/psychologicalcounselingreservation'
import { useMessage, useMessageBox } from '/@/hooks/message'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import {
List,
Calendar,
UserFilled,
Phone,
EditPen,
Setting,
Menu,
Search,
Document,
FolderAdd
} from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const FormDialog = defineAsyncComponent(() => import('./form.vue'))
const route = useRoute()
const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'reservationTime', label: '预约时间', icon: Calendar, minWidth: 160 },
{ prop: 'realName', label: '值班老师', icon: UserFilled },
{ prop: 'classNo', label: '班号', icon: Document },
{ prop: 'stuNo', label: '学号', icon: UserFilled },
{ prop: 'stuName', label: '学生姓名', icon: UserFilled },
{ prop: 'phone', label: '联系方式', icon: Phone, minWidth: 120 },
{ prop: 'isHandle', label: '是否处理', icon: Document },
{ prop: 'teaRemark', label: '老师备注', icon: EditPen, minWidth: 120 },
{ prop: 'remarks', label: '学生留言', icon: EditPen, minWidth: 120 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
stuNo: '',
classNo: '',
reservationTime: '',
isHandle: ''
})
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.stuNo = ''
searchForm.classNo = ''
searchForm.reservationTime = ''
searchForm.isHandle = ''
getDataList()
}
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该预约记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '删除失败')
}
}
onMounted(() => {})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -0,0 +1,186 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="600"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-form-item label="教师姓名" prop="userName">
<el-select
v-model="form.userName"
placeholder="请选择教师"
filterable
clearable
style="width: 100%"
:disabled="!!form.id"
@change="handleTeacherChange">
<el-option
v-for="item in teacherList"
:key="item.teacherNo || item.userName"
:label="(item.realName || item.name) + (item.teacherNo ? ` (${item.teacherNo})` : '')"
:value="item.teacherNo || item.userName">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="真实姓名" prop="realName">
<el-input
v-model="form.realName"
placeholder="选教师后自动带出,可修改"
clearable />
</el-form-item>
<el-form-item label="联系方式" prop="phone">
<el-input
v-model="form.phone"
placeholder="请输入联系方式"
clearable />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="4"
placeholder="请输入备注"
maxlength="250"
show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="PsychologicalCounselingTeacherFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/psychologicalcounselingteacher'
import { getTeacherBaseList } from '/@/api/professional/professionaluser/teacherbase'
const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add')
const teacherList = ref<any[]>([])
const form = reactive({
id: '',
userName: '',
realName: '',
phone: '',
remarks: ''
})
const dataRules = {
userName: [
{ required: true, message: '请选择教师', trigger: 'change' }
]
}
const handleTeacherChange = (val: string) => {
const item = teacherList.value.find(
(t: any) => (t.teacherNo || t.userName) === val
)
if (item) {
form.realName = item.realName || item.name || ''
form.phone = item.phone || item.tel || ''
} else {
form.realName = ''
form.phone = ''
}
}
const openDialog = (type: string = 'add', row?: any) => {
operType.value = type
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.userName = ''
form.realName = ''
form.phone = ''
form.remarks = ''
if (type === 'edit' && row) {
form.id = row.id
form.userName = row.userName || ''
form.realName = row.realName || ''
form.phone = row.phone || ''
form.remarks = row.remarks || ''
}
})
}
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
if (operType.value === 'add') {
await addObj({
userName: form.userName,
realName: form.realName,
phone: form.phone,
remarks: form.remarks
})
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
userName: form.userName,
realName: form.realName,
phone: form.phone,
remarks: form.remarks
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} finally {
loading.value = false
}
})
}
const loadTeacherList = async () => {
try {
const res = await getTeacherBaseList()
if (res.data && Array.isArray(res.data)) {
teacherList.value = res.data
} else {
teacherList.value = []
}
} catch (err) {
teacherList.value = []
}
}
onMounted(() => {
loadTeacherList()
})
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,244 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="教师姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="请输入教师姓名"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
预约师管理列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef?.openDialog()">
新增
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120">
<el-button type="primary" icon="FolderAdd" @click="formDialogRef?.openDialog()">新增预约师</el-button>
</el-empty>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑表单弹窗 -->
<FormDialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PsychologicalCounselingTeacher">
import { reactive, ref, onMounted, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList, delObj } from '/@/api/stuwork/psychologicalcounselingteacher'
import { useMessage, useMessageBox } from '/@/hooks/message'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { List, UserFilled, Phone, EditPen, Setting, Menu, Search, Document, FolderAdd } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const FormDialog = defineAsyncComponent(() => import('./form.vue'))
const route = useRoute()
const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'userName', label: '用户名', icon: UserFilled },
{ prop: 'realName', label: '教师姓名', icon: UserFilled },
{ prop: 'phone', label: '联系方式', icon: Phone, minWidth: 120 },
{ prop: 'remarks', label: '备注', icon: EditPen, minWidth: 150 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单
const searchForm = reactive({
realName: ''
})
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.realName = ''
getDataList()
}
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm('确定要删除该预约师吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '删除失败')
}
}
onMounted(() => {})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -31,12 +31,20 @@
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="奖项名称" prop="ruleName">
<el-input
v-model="form.ruleName"
placeholder="请输入奖项名称"
<el-form-item label="奖项名称" prop="ruleId">
<el-select
v-model="form.ruleId"
placeholder="请选择奖项名称"
clearable
style="width: 100%" />
filterable
style="width: 100%"
@change="handleRewardRuleChange">
<el-option
v-for="item in rewardRuleList"
:key="item.id"
:label="item.ruleName"
:value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
@@ -67,6 +75,7 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/rewardclass'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getList as getRewardRuleList } from '/@/api/stuwork/rewardrule'
const emit = defineEmits(['refresh'])
@@ -76,6 +85,8 @@ const visible = ref(false)
const loading = ref(false)
const operType = ref('add')
const classList = ref<any[]>([])
/** 奖项列表(来自接口 /stuwork/rewardrule/list用于班级的奖项类型可传 ruleType 筛选) */
const rewardRuleList = ref<any[]>([])
// 提交表单数据
const form = reactive({
@@ -91,8 +102,8 @@ const dataRules = {
classCode: [
{ required: true, message: '请选择班号', trigger: 'change' }
],
ruleName: [
{ required: true, message: '请输入奖项名称', trigger: 'blur' }
ruleId: [
{ required: true, message: '请选择奖项名称', trigger: 'change' }
]
}
@@ -117,6 +128,8 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.ruleId = row.ruleId || ''
form.ruleName = row.ruleName || ''
form.remarks = row.remarks || ''
// 选择奖项后同步 ruleName
handleRewardRuleChange(form.ruleId)
// 如果需要获取详情
if (row.id && (!row.ruleName || !row.remarks)) {
@@ -138,8 +151,12 @@ const openDialog = async (type: string = 'add', row?: any) => {
}
// 班级变化
const handleClassChange = () => {
// 可以根据班级获取相关信息,这里暂时不实现
const handleClassChange = () => {}
// 选择奖项时同步 ruleName提交时需要
const handleRewardRuleChange = (id: string) => {
const item = rewardRuleList.value.find((r) => r.id === id)
form.ruleName = item ? item.ruleName : ''
}
// 提交表单
@@ -192,9 +209,21 @@ const getClassListData = async () => {
}
}
// 获取奖项列表接口GET /stuwork/rewardrule/list
const getRewardRuleData = async () => {
try {
const res = await getRewardRuleList()
const list = res.data && Array.isArray(res.data) ? res.data : []
rewardRuleList.value = list
} catch (err) {
rewardRuleList.value = []
}
}
// 初始化
onMounted(() => {
getClassListData()
getRewardRuleData()
})
// 暴露方法

View File

@@ -12,6 +12,39 @@
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="form.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="form.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="宿舍号" prop="roomNo">
<el-select
@@ -30,12 +63,20 @@
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="奖项名称" prop="ruleName">
<el-input
v-model="form.ruleName"
placeholder="请输入奖项名称"
<el-form-item label="奖项名称" prop="ruleId">
<el-select
v-model="form.ruleId"
placeholder="请选择奖项名称"
clearable
style="width: 100%" />
filterable
style="width: 100%"
@change="handleRewardRuleChange">
<el-option
v-for="item in rewardRuleList"
:key="item.id"
:label="item.ruleName"
:value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
@@ -66,6 +107,9 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/rewarddorm'
import { dormRoomList } from '/@/api/stuwork/dormroom'
import { getList as getRewardRuleList } from '/@/api/stuwork/rewardrule'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
@@ -75,10 +119,16 @@ const visible = ref(false)
const loading = ref(false)
const operType = ref('add')
const roomList = ref<any[]>([])
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
/** 奖项列表(来自接口 /stuwork/rewardrule/list */
const rewardRuleList = ref<any[]>([])
// 提交表单数据
// 提交表单数据与接口文档一致roomNo, ruleId, ruleName, schoolYear, schoolTerm, remarks
const form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
roomNo: '',
ruleId: '',
ruleName: '',
@@ -87,11 +137,17 @@ const form = reactive({
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
roomNo: [
{ required: true, message: '请选择宿舍号', trigger: 'change' }
],
ruleName: [
{ required: true, message: '请输入奖项名称', trigger: 'blur' }
ruleId: [
{ required: true, message: '请选择奖项名称', trigger: 'change' }
]
}
@@ -104,6 +160,8 @@ const openDialog = async (type: string = 'add', row?: any) => {
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.roomNo = ''
form.ruleId = ''
form.ruleName = ''
@@ -112,23 +170,27 @@ const openDialog = async (type: string = 'add', row?: any) => {
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.roomNo = row.roomNo || ''
form.ruleId = row.ruleId || ''
form.ruleName = row.ruleName || ''
form.remarks = row.remarks || ''
handleRewardRuleChange(form.ruleId)
// 如果需要获取详情
if (row.id && (!row.ruleName || !row.remarks)) {
if (row.id && (!row.ruleName || !row.schoolYear)) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.schoolYear = res.data.schoolYear || ''
form.schoolTerm = res.data.schoolTerm || ''
form.roomNo = res.data.roomNo || ''
form.ruleId = res.data.ruleId || ''
form.ruleName = res.data.ruleName || ''
form.remarks = res.data.remarks || ''
}
}).catch((err) => {
}).finally(() => {
}).catch(() => {}).finally(() => {
loading.value = false
})
}
@@ -136,6 +198,12 @@ const openDialog = async (type: string = 'add', row?: any) => {
})
}
// 选择奖项时同步 ruleName提交时需要
const handleRewardRuleChange = (id: string) => {
const item = rewardRuleList.value.find((r) => r.id === id)
form.ruleName = item ? item.ruleName : ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
@@ -145,10 +213,13 @@ const onSubmit = async () => {
loading.value = true
try {
// 与接口文档一致roomNo, ruleId, ruleName, schoolYear, schoolTerm, remarks
const submitData = {
roomNo: form.roomNo,
ruleId: form.ruleId || '', // 如果没有ruleId传空字符串
ruleId: form.ruleId != null && form.ruleId !== '' ? String(form.ruleId) : '',
ruleName: form.ruleName,
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
remarks: form.remarks || ''
}
@@ -164,8 +235,8 @@ const onSubmit = async () => {
}
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
} catch (_err) {
// 不再在此处弹 errorrequest 拦截器已统一弹过,避免重复两个 error 弹窗
} finally {
loading.value = false
}
@@ -186,9 +257,50 @@ const getRoomListData = async () => {
}
}
// 获取奖项列表接口GET /stuwork/rewardrule/list
const getRewardRuleData = async () => {
try {
const res = await getRewardRuleList()
const list = res.data && Array.isArray(res.data) ? res.data : []
rewardRuleList.value = list
} catch (err) {
rewardRuleList.value = []
}
}
// 获取学年列表
const getSchoolYearData = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 获取学期字典
const getSchoolTermData = async () => {
try {
const res = await getDicts('school_term')
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 初始化
onMounted(() => {
getRoomListData()
getRewardRuleData()
getSchoolYearData()
getSchoolTermData()
})
// 暴露方法

View File

@@ -104,12 +104,62 @@
</el-table>
</el-row>
</div>
<!-- 查看详情弹窗接口queryDataByStuNo 通过学年学号查看详情按当前学期筛选 -->
<el-dialog
v-model="viewDialogVisible"
title="学期操行考核详情"
width="800px"
destroy-on-close
@close="viewDetailList = []">
<div v-if="viewRow" class="view-summary">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="学号">{{ viewRow.stuNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewRow.realName }}</el-descriptions-item>
<el-descriptions-item label="学年">{{ queryForm.schoolYear }}</el-descriptions-item>
<el-descriptions-item label="学期">{{ formatSchoolTerm(queryForm.schoolTerm) }}</el-descriptions-item>
<el-descriptions-item label="学期总评" :span="2">
{{ viewRow.score != null && viewRow.score !== undefined ? Number(viewRow.score).toFixed(2) : '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="view-detail-title">考核记录</div>
<el-table
:data="viewDetailList"
v-loading="viewLoading"
border
size="small"
max-height="400"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="schoolTerm" label="学期" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="conductType" label="类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.conductType === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.conductType === '1' ? '加分' : scope.row.conductType === '0' ? '扣分' : scope.row.conductType || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="score" label="分数" width="80" align="center">
<template #default="scope">
{{ scope.row.score != null && scope.row.score !== undefined ? Number(scope.row.score) : '-' }}
</template>
</el-table-column>
<el-table-column prop="description" label="情况记录" min-width="140" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="100" show-overflow-tooltip />
</el-table>
<template v-if="viewDetailList.length === 0 && !viewLoading">
<el-empty description="暂无考核记录" :image-size="80" />
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="StuConductTerm">
import { reactive, ref, onMounted, computed } from 'vue'
import { getStuConductTerm } from "/@/api/stuwork/stuconduct";
import { getStuConductTerm, queryDataByStuNo } from "/@/api/stuwork/stuconduct";
import { getClassListByRole } from "/@/api/basic/basicclass";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
@@ -129,6 +179,10 @@ const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const viewDialogVisible = ref(false)
const viewLoading = ref(false)
const viewRow = ref<any>(null)
const viewDetailList = ref<any[]>([])
// 查询表单
const queryForm = reactive({
@@ -263,14 +317,20 @@ const getDataList = async () => {
} else {
studentList.value = []
}
} catch (err: any) {
useMessage().error(err.msg || '获取数据列表失败')
} catch (_err) {
studentList.value = []
} finally {
loading.value = false
}
}
// 格式化学期显示
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') return '-'
const dictItem = schoolTermList.value.find((item: any) => item.value == value)
return dictItem ? dictItem.label : value
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
@@ -280,10 +340,29 @@ const handleReset = () => {
studentList.value = []
}
// 查看详情
const handleView = (row: any) => {
// 可以跳转到详情页面或打开详情弹窗
useMessage().info('查看详情功能待实现')
// 查看详情接口GET /stuwork/stuconduct/queryDataByStuNo按当前学年+学号拉取后筛本学期记录)
const handleView = async (row: any) => {
if (!queryForm.schoolYear || !row.stuNo) {
useMessage().warning('缺少学年或学号')
return
}
viewRow.value = row
viewDialogVisible.value = true
viewDetailList.value = []
viewLoading.value = true
try {
const res = await queryDataByStuNo({
schoolYear: queryForm.schoolYear,
stuNo: row.stuNo
})
const list = Array.isArray(res.data) ? res.data : []
const term = queryForm.schoolTerm
viewDetailList.value = term ? list.filter((r: any) => String(r.schoolTerm) === String(term)) : list
} catch (_err) {
viewDetailList.value = []
} finally {
viewLoading.value = false
}
}
// 获取学年列表
@@ -350,6 +429,15 @@ onMounted(() => {
}
}
.view-summary {
margin-bottom: 16px;
}
.view-detail-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
// 确保页面可以滚动
.layout-padding {
height: 100%;

View File

@@ -90,12 +90,61 @@
</el-table>
</el-row>
</div>
<!-- 查看详情弹窗接口queryDataByStuNo 通过学年学号查看详情 -->
<el-dialog
v-model="viewDialogVisible"
title="学年操行考核详情"
width="800px"
destroy-on-close
@close="viewDetailList = []">
<div v-if="viewRow" class="view-summary">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="学号">{{ viewRow.stuNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewRow.realName }}</el-descriptions-item>
<el-descriptions-item label="学年">{{ queryForm.schoolYear }}</el-descriptions-item>
<el-descriptions-item label="学年总评">
{{ viewRow.score != null && viewRow.score !== undefined ? Number(viewRow.score).toFixed(2) : '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="view-detail-title">考核记录</div>
<el-table
:data="viewDetailList"
v-loading="viewLoading"
border
size="small"
max-height="400"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="schoolTerm" label="学期" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="conductType" label="类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.conductType === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.conductType === '1' ? '加分' : scope.row.conductType === '0' ? '扣分' : scope.row.conductType || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="score" label="分数" width="80" align="center">
<template #default="scope">
{{ scope.row.score != null && scope.row.score !== undefined ? Number(scope.row.score) : '-' }}
</template>
</el-table-column>
<el-table-column prop="description" label="情况记录" min-width="140" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="100" show-overflow-tooltip />
</el-table>
<template v-if="viewDetailList.length === 0 && !viewLoading">
<el-empty description="暂无考核记录" :image-size="80" />
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="StuConductYear">
import { reactive, ref, onMounted, computed } from 'vue'
import { getStuConductYear } from "/@/api/stuwork/stuconduct";
import { getStuConductYear, queryDataByStuNo } from "/@/api/stuwork/stuconduct";
import { getClassListByRole } from "/@/api/basic/basicclass";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { useMessage } from "/@/hooks/message";
@@ -113,6 +162,10 @@ const loading = ref(false)
const schoolYearList = ref<any[]>([])
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const viewDialogVisible = ref(false)
const viewLoading = ref(false)
const viewRow = ref<any>(null)
const viewDetailList = ref<any[]>([])
// 查询表单
const queryForm = reactive({
@@ -245,8 +298,7 @@ const getDataList = async () => {
} else {
studentList.value = []
}
} catch (err: any) {
useMessage().error(err.msg || '获取数据列表失败')
} catch (_err) {
studentList.value = []
} finally {
loading.value = false
@@ -261,10 +313,27 @@ const handleReset = () => {
studentList.value = []
}
// 查看详情
const handleView = (row: any) => {
// 可以跳转到详情页面或打开详情弹窗
useMessage().info('查看详情功能待实现')
// 查看详情接口GET /stuwork/stuconduct/queryDataByStuNo通过学年学号查看详情
const handleView = async (row: any) => {
if (!queryForm.schoolYear || !row.stuNo) {
useMessage().warning('缺少学年或学号')
return
}
viewRow.value = row
viewDialogVisible.value = true
viewDetailList.value = []
viewLoading.value = true
try {
const res = await queryDataByStuNo({
schoolYear: queryForm.schoolYear,
stuNo: row.stuNo
})
viewDetailList.value = Array.isArray(res.data) ? res.data : []
} catch (_err) {
viewDetailList.value = []
} finally {
viewLoading.value = false
}
}
// 获取学年列表
@@ -313,6 +382,15 @@ onMounted(() => {
}
}
.view-summary {
margin-bottom: 16px;
}
.view-detail-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
// 确保页面可以滚动
.layout-padding {
height: 100%;

View File

@@ -0,0 +1,309 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 筛选条件 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="searchForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="handleSearch"
class="search-form">
<el-form-item label="毕业年份" prop="graduYear">
<el-select
v-model="searchForm.graduYear"
placeholder="请选择毕业年份"
clearable
filterable
style="width: 140px">
<el-option
v-for="y in graduYearOptions"
:key="y"
:label="y + '年'"
:value="y" />
</el-select>
</el-form-item>
<el-form-item label="毕业状态" prop="status">
<el-select
v-model="searchForm.status"
placeholder="请选择"
clearable
style="width: 140px">
<el-option label="待确认" value="0" />
<el-option label="确认毕业" value="1" />
<el-option label="不可毕业" value="-1" />
</el-select>
</el-form-item>
<el-form-item label="毕业类型" prop="type">
<el-select
v-model="searchForm.type"
placeholder="请选择"
clearable
style="width: 120px">
<el-option label="段段清" value="1" />
<el-option label="正常毕业" value="2" />
</el-select>
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="searchForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 140px" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input
v-model="searchForm.realName"
placeholder="请输入姓名"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="searchForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 160px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item label="班号" prop="classNo">
<el-input
v-model="searchForm.classNo"
placeholder="请输入班号"
clearable
style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
毕业学生名单
</span>
<div class="header-actions">
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange">
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
show-overflow-tooltip
align="center">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<template v-if="col.prop === 'status'" #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small">
{{ formatStatus(scope.row.status) }}
</el-tag>
</template>
<template v-else-if="col.prop === 'type'" #default="scope">
{{ scope.row.type === '1' ? '段段清' : scope.row.type === '2' ? '正常毕业' : scope.row.type || '-' }}
</template>
</el-table-column>
</template>
<template #empty>
<el-empty description="暂无数据,请选择条件查询" :image-size="120" />
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts" name="Stugraducheck">
import { reactive, ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList } from '/@/api/stuwork/stugraducheck'
import { getDeptList } from '/@/api/basic/basicclass'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import {
List,
Calendar,
UserFilled,
Document,
Menu,
Search,
Grid
} from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
const route = useRoute()
const searchFormRef = ref()
const columnControlRef = ref<any>()
const showSearch = ref(true)
const deptList = ref<any[]>([])
// 毕业年份:当前年前后各 5 年
const graduYearOptions = computed(() => {
const y = new Date().getFullYear()
return Array.from({ length: 11 }, (_, i) => y - 5 + i)
})
// 表格列配置与其它页面一致icon 写在列上)
const tableColumns = [
{ prop: 'stuNo', label: '学号', icon: UserFilled },
{ prop: 'realName', label: '姓名', icon: UserFilled },
{ prop: 'classNo', label: '班号', icon: Grid },
{ prop: 'majorName', label: '专业名称', icon: Document, minWidth: 140 },
{ prop: 'graduYear', label: '毕业年份', icon: Calendar },
{ prop: 'status', label: '毕业状态', icon: Document },
{ prop: 'type', label: '毕业类型', icon: Document },
{ prop: 'scoreCondition', label: '学分情况', icon: Document, minWidth: 100 },
{ prop: 'skillCondition', label: '技能情况', icon: Document, minWidth: 100 },
{ prop: 'conductCondition', label: '操行情况', icon: Document, minWidth: 100 }
]
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
// 搜索表单与接口文档一致graduYear, status, type, stuNo, realName, deptCode, classCode 等)
const searchForm = reactive({
graduYear: '',
status: '',
type: '',
stuNo: '',
realName: '',
deptCode: '',
classNo: ''
})
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
const formatStatus = (status: string) => {
const map: Record<string, string> = { '0': '待确认', '1': '确认毕业', '-1': '不可毕业' }
return map[status] || status || '-'
}
const statusTagType = (status: string) => {
const map: Record<string, string> = { '0': 'warning', '1': 'success', '-1': 'danger' }
return map[status] || 'info'
}
const handleSearch = () => {
getDataList()
}
const handleReset = () => {
searchFormRef.value?.resetFields()
searchForm.graduYear = ''
searchForm.status = ''
searchForm.type = ''
searchForm.stuNo = ''
searchForm.realName = ''
searchForm.deptCode = ''
searchForm.classNo = ''
getDataList()
}
const loadDeptList = async () => {
try {
const res = await getDeptList()
deptList.value = Array.isArray(res.data) ? res.data : []
} catch (err) {
deptList.value = []
}
}
onMounted(() => {
loadDeptList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -188,9 +188,13 @@ const handleClassChange = async (classCode: string) => {
}
try {
const res = await queryStudentListByClass({ classCode, current: 1, size: 10000 })
if (res.data && Array.isArray(res.data.records)) {
studentList.value = res.data.records
// 接口文档GET 仅 classCode返回 data 为数组
const res = await queryStudentListByClass({ classCode })
const data = res?.data
if (Array.isArray(data)) {
studentList.value = data
} else if (data?.records && Array.isArray(data.records)) {
studentList.value = data.records
} else {
studentList.value = []
}

View File

@@ -181,9 +181,13 @@ const handleClassChange = async (classCode: string) => {
}
try {
const res = await queryStudentListByClass({ classCode, current: 1, size: 10000 })
if (res.data && Array.isArray(res.data.records)) {
studentList.value = res.data.records
// 接口文档GET仅 classCode返回 data 为数组,非 data.records
const res = await queryStudentListByClass({ classCode })
const data = res?.data
if (Array.isArray(data)) {
studentList.value = data
} else if (data?.records && Array.isArray(data.records)) {
studentList.value = data.records
} else {
studentList.value = []
}

View File

@@ -305,13 +305,18 @@ const getClassListData = async () => {
}
}
// 获取学生列表
// 获取学生列表接口文档GET 仅 classCode返回 data 为数组)
const getStudentListData = async () => {
if (!form.classCode) return
try {
const res = await queryStudentListByClass(form.classCode)
if (res.data) {
studentList.value = Array.isArray(res.data) ? res.data : []
const res = await queryStudentListByClass({ classCode: form.classCode })
const data = res?.data
if (Array.isArray(data)) {
studentList.value = data
} else if (data?.records && Array.isArray(data.records)) {
studentList.value = data.records
} else {
studentList.value = []
}
} catch (err) {
studentList.value = []

View File

@@ -207,7 +207,7 @@
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="150" align="center" fixed="right">
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
@@ -220,6 +220,13 @@
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="RefreshLeft"
link
type="warning"
@click="handleCancel(scope.row)">
异动撤销
</el-button>
<el-button
icon="Delete"
link
@@ -250,7 +257,7 @@
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj, exportData } from "/@/api/stuwork/stuturnover";
import { fetchList, delObj, exportData, cancelObj } from "/@/api/stuwork/stuturnover";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
import { getDeptListByLevelTwo } from "/@/api/basic/basicdept";
@@ -258,7 +265,7 @@ import { list as getClassList } from "/@/api/basic/basicclass";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
import { List, Calendar, Clock, OfficeBuilding, Grid, CreditCard, Avatar, Collection, Document, Setting, Menu, Search } from '@element-plus/icons-vue'
import { List, Calendar, Clock, OfficeBuilding, Grid, CreditCard, Avatar, Collection, Document, Setting, Menu, Search, RefreshLeft } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
@@ -400,6 +407,20 @@ const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 异动撤销
const handleCancel = async (row: any) => {
try {
await useMessageBox().confirm('确定要撤销该条学籍异动吗?')
await cancelObj([row.id])
useMessage().success('撤销成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err?.msg || '撤销失败')
}
}
}
// 删除
const handleDelete = async (row: any) => {
try {

View File

@@ -58,6 +58,7 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj } from '/@/api/stuwork/stuworkstudyalternate'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { queryAllStudentByClassCode } from '/@/api/basic/basicstudent'
const emit = defineEmits(['refresh'])
@@ -94,7 +95,7 @@ const openDialog = async () => {
})
}
// 提交表单
// 提交表单(接口文档要求 classCode、dateRange、stuNoList
const onSubmit = async () => {
if (!dataFormRef.value) return
@@ -108,15 +109,27 @@ const onSubmit = async () => {
loading.value = true
try {
// 按班级获取学生列表,提取学号作为 stuNoList接口必传
const stuRes = await queryAllStudentByClassCode(form.classCode)
const list = stuRes?.data && Array.isArray(stuRes.data) ? stuRes.data : []
const stuNoList = list.map((s: any) => s.stuNo).filter(Boolean)
if (stuNoList.length === 0) {
useMessage().warning('该班级暂无学生,无法新增工学交替')
loading.value = false
return
}
await addObj({
classCode: form.classCode,
dateRange: form.dateRange
dateRange: form.dateRange,
stuNoList
})
useMessage().success('新增成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '新增失败')
if (!err?._messageShown) {
useMessage().error(err?.msg || '新增失败')
}
} finally {
loading.value = false
}

View File

@@ -170,7 +170,7 @@
:min-width="col.minWidth"
:width="col.width">
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<el-icon><component :is="columnConfigMap[col.prop || '']?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 学期列特殊模板 -->
@@ -290,7 +290,7 @@ const {
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, route.path)
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 搜索表单
const searchForm = reactive({
@@ -303,10 +303,20 @@ const searchForm = reactive({
dateRange: null as [string, string] | null
})
// 配置 useTable
// 配置 useTable(接口返回 data.tableData.records / data.tableData.total需包装以适配 hook 的 res.data[props.item] 取数)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: searchForm,
pageList: fetchList,
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
const tableData = data?.tableData
return {
data: {
'tableData.records': tableData?.records ?? [],
'tableData.total': tableData?.total ?? data?.total ?? 0
}
}
},
props: {
item: 'tableData.records',
totalCount: 'tableData.total'
@@ -397,13 +407,25 @@ const getSchoolTermList = async () => {
try {
const res = await getDicts('school_term')
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 学期列展示:按字典转成文案
const formatSchoolTerm = (value: string | number) => {
if (value === '' || value == null) return '-'
const item = schoolTermList.value.find((t: any) => String(t.value) === String(value))
return item ? item.label : value
}
// 初始化
onMounted(() => {
getDeptListData()