Merge branch 'developer' of ssh://code.cyweb.top:30033/scj/zhxy/v3/cloud-ui into developer

This commit is contained in:
吴红兵
2026-02-10 12:57:20 +08:00
29 changed files with 3499 additions and 70 deletions

View File

@@ -42,7 +42,7 @@ export const getObj = (id: string | number) => {
*/ */
export const delObj = (id: string | number) => { export const delObj = (id: string | number) => {
return request({ return request({
url: `/recruit/recruitstudentplan/deletById`, url: `/recruit/recruitstudentplan/deleteById`,
method: 'post', method: 'post',
data: { id :id}, data: { id :id},
}); });

View File

@@ -0,0 +1,14 @@
import request from '/@/utils/request';
/**
* 教室公物编辑及门锁密码
* 接口文档POST /api/stuwork/classassets/edit
* @param data 教室公物数据(包含 buildingNo, deptName, classCode, position, platformType, tyType, tvType, chairCnt, tableCnt, remarks, password 等)
*/
export const editAssets = (data: any) => {
return request({
url: '/stuwork/classassets/edit',
method: 'post',
data,
});
};

View File

@@ -0,0 +1,35 @@
import request from '/@/utils/request';
/**
* 分页查询宿舍点名列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/dormsignrecord/page',
method: 'get',
params: query
});
};
/**
* 新增宿舍点名
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/dormsignrecord',
method: 'post',
data
});
};
/**
* 初始化宿舍学生信息用于考勤
*/
export const initDormStuInfoForAttendance = () => {
return request({
url: '/stuwork/dormsignrecord/task/initDormStuInfoForAttendance',
method: 'get'
});
};

View File

@@ -0,0 +1,85 @@
import request from '/@/utils/request';
/**
* 文件列表(分页)
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/filemanager/page',
method: 'get',
params: query
});
};
/**
* 文件列表(不分页,按层级)
* @param query
*/
export const getList = (query?: any) => {
return request({
url: '/stuwork/filemanager/list',
method: 'get',
params: query
});
};
/**
* 文件详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/filemanager/detail',
method: 'get',
params: { id }
});
};
/**
* 新增文件
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/filemanager',
method: 'post',
data
});
};
/**
* 编辑文件
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/filemanager/edit',
method: 'post',
data
});
};
/**
* 编辑文件夹
* @param data
*/
export const editFile = (data: any) => {
return request({
url: '/stuwork/filemanager/editFile',
method: 'post',
data
});
};
/**
* 删除文件
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/filemanager/delete',
method: 'post',
data: ids
});
};

View File

@@ -0,0 +1,61 @@
import request from '/@/utils/request';
/**
* 分页查询德育计划列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/moralplan/page',
method: 'get',
params: query
});
};
/**
* 新增德育计划
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/moralplan',
method: 'post',
data
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/moralplan/detail',
method: 'get',
params: { id }
});
};
/**
* 编辑德育计划
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/moralplan/edit',
method: 'post',
data
});
};
/**
* 删除德育计划
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/moralplan/delete',
method: 'post',
data: ids
});
};

View File

@@ -25,3 +25,16 @@ export const addClassRoomAssign = (data: { buildingNo?: string | number; positio
}); });
}; };
/**
* 取消教室安排
* 接口文档POST /api/stuwork/teachclassroomassign/delClassRoomAssign
* @param data 教室基础数据(包含 id, classCode, position 等)
*/
export const delClassRoomAssign = (data: any) => {
return request({
url: '/stuwork/teachclassroomassign/delClassRoomAssign',
method: 'post',
data,
});
};

View File

@@ -0,0 +1,61 @@
import request from '/@/utils/request';
/**
* 分页查询学期活动列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/termactivity/page',
method: 'get',
params: query
});
};
/**
* 获取学期活动详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/termactivity/detail',
method: 'get',
params: { id }
});
};
/**
* 新增学期活动
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/termactivity',
method: 'post',
data
});
};
/**
* 编辑学期活动
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/termactivity/edit',
method: 'post',
data
});
};
/**
* 删除学期活动
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/termactivity/delete',
method: 'post',
data: ids
});
};

View File

@@ -155,6 +155,10 @@ export function useTableColumnControl(
* 根据 visibleColumns 和 columnOrder 计算最终显示的列 * 根据 visibleColumns 和 columnOrder 计算最终显示的列
*/ */
const visibleColumnsSorted = computed(() => { const visibleColumnsSorted = computed(() => {
// 如果 visibleColumns 为空,显示所有列(初始化时)
if (visibleColumns.value.length === 0) {
return tableColumns.filter(col => !col.alwaysShow && !col.fixed)
}
// 过滤出可见的列 // 过滤出可见的列
const columns = tableColumns.filter(col => { const columns = tableColumns.filter(col => {
const key = col.prop || col.label || '' const key = col.prop || col.label || ''

View File

@@ -15,6 +15,7 @@
</div> </div>
<el-upload <el-upload
ref="uploadRef"
class="upload-demo" class="upload-demo"
:action="uploadUrl" :action="uploadUrl"
:headers="headers" :headers="headers"
@@ -34,7 +35,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, nextTick } from 'vue';
import { ElNotification } from 'element-plus'; import { ElNotification } from 'element-plus';
import { Download, UploadFilled } from '@element-plus/icons-vue'; import { Download, UploadFilled } from '@element-plus/icons-vue';
import { Session } from '/@/utils/storage'; import { Session } from '/@/utils/storage';
@@ -54,6 +55,7 @@ const headers = computed(() => {
const uploadUrl = ref('') const uploadUrl = ref('')
const currentType = ref('') const currentType = ref('')
const uploadRef = ref<{ clearFiles?: () => void }>()
const titleMap: Record<string, string> = { const titleMap: Record<string, string> = {
titleRelation: '职称信息导入', titleRelation: '职称信息导入',
quaRelation: '职业资格信息导入', quaRelation: '职业资格信息导入',
@@ -68,6 +70,9 @@ const init = (type: any) => {
uploadUrl.value = '/professional/file/importTeacherOtherInfo?type=' + type uploadUrl.value = '/professional/file/importTeacherOtherInfo?type=' + type
title.value = titleMap[type] || '信息导入' title.value = titleMap[type] || '信息导入'
visible.value = true visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles()
})
} }
// Emits // Emits

View File

@@ -17,12 +17,14 @@
<!-- </div>--> <!-- </div>-->
<el-upload <el-upload
ref="uploadRef"
class="upload-demo" class="upload-demo"
action="/professional/file/makeImportTeacherInfoSimpleTask" action="/professional/file/makeImportTeacherInfoSimpleTask"
:headers="headers" :headers="headers"
:accept="'.xls,.xlsx'" :accept="'.xls,.xlsx'"
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess"
:on-error="handleAvatarError" :on-error="handleAvatarError"
:limit="1"
drag> drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon> <el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text"> <div class="el-upload__text">
@@ -38,7 +40,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, nextTick } from 'vue'
import { ElNotification } from 'element-plus' import { ElNotification } from 'element-plus'
import { Download, UploadFilled } from '@element-plus/icons-vue' import { Download, UploadFilled } from '@element-plus/icons-vue'
import { Session } from '/@/utils/storage' import { Session } from '/@/utils/storage'
@@ -53,9 +55,14 @@
} }
}) })
const uploadRef = ref<{ clearFiles?: () => void }>()
// 方法 // 方法
const init = () => { const init = () => {
visible.value = true visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles?.()
})
} }
const handleUploadSuccess = () => { const handleUploadSuccess = () => {

View File

@@ -1,10 +1,23 @@
<template> <template>
<el-dialog v-model="visible" :title="title" width="600" append-to-body> <el-dialog v-model="visible" :title="title" width="600" append-to-body>
<div style="text-align: center; margin-bottom: 20px"> <div style="text-align: center; margin-bottom: 20px" v-if="currentType!='R10003'">
<el-button type="success" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button> <el-button type="success" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
</div> </div>
<el-alert
v-if="currentType=='R10003'"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 20px;">
<template #title>
<span> 请从中招平台导出数据后导入</span>
</template>
</el-alert>
<el-upload <el-upload
ref="uploadRef"
class="upload-demo" class="upload-demo"
:action="uploadUrl" :action="uploadUrl"
:headers="headers" :headers="headers"
@@ -24,7 +37,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, nextTick } from 'vue';
import { ElNotification } from 'element-plus'; import { ElNotification } from 'element-plus';
import { Download, UploadFilled } from '@element-plus/icons-vue'; import { Download, UploadFilled } from '@element-plus/icons-vue';
import { Session } from '/@/utils/storage'; import { Session } from '/@/utils/storage';
@@ -44,20 +57,26 @@ const headers = computed(() => {
const uploadUrl = ref('') const uploadUrl = ref('')
const currentType = ref('') const currentType = ref('')
const uploadRef = ref<{ clearFiles?: () => void }>()
const titleMap: Record<string, string> = { const titleMap: Record<string, string> = {
planMajor: '计划专业导入' R10001: '计划专业导入',
R10002: '地区分数导入',
R10003: '中招平台数据导入'
} }
// 方法 // 方法
const init = (type: any) => { const init = (type: any) => {
currentType.value = type currentType.value = type
uploadUrl.value = '/professional/file/importTeacherOtherInfo?type=' + type uploadUrl.value = '/api/recruit/file/importRecruitInfo?type=' + type
title.value = titleMap[type] || '信息导入' title.value = titleMap[type] || '信息导入'
visible.value = true visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles()
})
} }
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'refreshData'): void (e: 'refreshDataList'): void
}>() }>()
const handleUploadSuccess = () => { const handleUploadSuccess = () => {
@@ -68,7 +87,7 @@ const handleUploadSuccess = () => {
type: 'success', type: 'success',
}); });
emit('refreshData') emit('refreshDataList')
}; };
const handleAvatarError = (err: any) => { const handleAvatarError = (err: any) => {

View File

@@ -50,7 +50,7 @@
<el-button v-if="hasAuth('recruit_recruitplanmajor_add')" type="primary" icon="FolderAdd" @click="addOrUpdateHandle"> </el-button> <el-button v-if="hasAuth('recruit_recruitplanmajor_add')" type="primary" icon="FolderAdd" @click="addOrUpdateHandle"> </el-button>
<el-button <el-button
v-auth="'professional_teacherinfo_import'" v-auth="'recruit_major_import'"
type="primary" type="primary"
plain plain
icon="UploadFilled" icon="UploadFilled"
@@ -124,7 +124,7 @@
<!-- 弹窗, 新增 / 修改 --> <!-- 弹窗, 新增 / 修改 -->
<table-form ref="addOrUpdateRef" @refreshDataList="getDataList" /> <table-form ref="addOrUpdateRef" @refreshDataList="getDataList" />
<import-recruit-info ref="ImportRecruitInfoRef"></import-recruit-info> <import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div> </div>
</div> </div>
</template> </template>
@@ -283,7 +283,7 @@ const resetQuery = () => {
const exportLoading = ref(false); const exportLoading = ref(false);
const handleImportDialog = () => { const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("planMajor"); ImportRecruitInfoRef.value?.init("R10001");
}; };
onMounted(() => { onMounted(() => {

View File

@@ -29,6 +29,15 @@
> >
</el-button> </el-button>
<el-button
v-auth="'recruit_areascore_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息
</el-button>
</div> </div>
<!-- 表格 --> <!-- 表格 -->
@@ -83,6 +92,9 @@
<!-- 弹窗, 新增 / 修改 --> <!-- 弹窗, 新增 / 修改 -->
<table-form ref="addOrUpdateRef" @refreshDataList="getDataList" /> <table-form ref="addOrUpdateRef" @refreshDataList="getDataList" />
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div> </div>
</div> </div>
</template> </template>
@@ -94,6 +106,8 @@ import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message' import { useMessage, useMessageBox } from '/@/hooks/message'
import { getList } from '/@/api/recruit/recruitstudentplangroup' import { getList } from '/@/api/recruit/recruitstudentplangroup'
import { fetchList, delObj } from '/@/api/recruit/recruitstudentplancorrectscoreconfig' import { fetchList, delObj } from '/@/api/recruit/recruitstudentplancorrectscoreconfig'
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const TableForm = defineAsyncComponent(() => import('./detaiform.vue')) const TableForm = defineAsyncComponent(() => import('./detaiform.vue'))
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
@@ -183,6 +197,12 @@ const resetQuery = () => {
getDataList() getDataList()
} }
const exportLoading = ref(false);
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10002");
};
onMounted(() => { onMounted(() => {
init() init()
}) })

View File

@@ -281,19 +281,28 @@
@click="handleAddData">新增 @click="handleAddData">新增
</el-button> </el-button>
<el-button <el-button
v-if="hasAuth('zipExport')" v-auth="'recruit_zzpt_import'"
type="warning" type="primary"
plain plain
icon="Download" icon="UploadFilled"
@click="downZip()">招生名单打包导出 :loading="exportLoading"
</el-button> @click="handleImportDialog"
<el-button >导入中招平台数据
class="ml10"
type="warning"
plain
icon="Download"
@click="handleExport()">名单导出
</el-button> </el-button>
<!-- <el-button-->
<!-- v-if="hasAuth('zipExport')"-->
<!-- type="warning"-->
<!-- plain-->
<!-- icon="Download"-->
<!-- @click="downZip()">招生名单打包导出-->
<!-- </el-button>-->
<!-- <el-button -->
<!-- class="ml10"-->
<!-- type="warning"-->
<!-- plain-->
<!-- icon="Download"-->
<!-- @click="handleExport()">名单导出-->
<!-- </el-button>-->
</div> </div>
</el-row> </el-row>
@@ -634,6 +643,8 @@
<AdmissionNoticeDialog ref="admissionNoticeDialogRef" @refresh="getDataList"></AdmissionNoticeDialog> <AdmissionNoticeDialog ref="admissionNoticeDialogRef" @refresh="getDataList"></AdmissionNoticeDialog>
<InterviewForm ref="interviewFormRef" @refresh="getDataList"></InterviewForm> <InterviewForm ref="interviewFormRef" @refresh="getDataList"></InterviewForm>
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div> </div>
</div> </div>
</template> </template>
@@ -681,6 +692,8 @@ const InterviewForm = defineAsyncComponent(() => import('/@/views/recruit/recrui
const PayQrcodeDialog = defineAsyncComponent(() => import('./PayQrcodeDialog.vue')) const PayQrcodeDialog = defineAsyncComponent(() => import('./PayQrcodeDialog.vue'))
const AdmissionNoticeDialog = defineAsyncComponent(() => import('./AdmissionNoticeDialog.vue')) const AdmissionNoticeDialog = defineAsyncComponent(() => import('./AdmissionNoticeDialog.vue'))
const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue')) const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'))
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
// 消息提示 hooks // 消息提示 hooks
const message = useMessage() const message = useMessage()
@@ -1131,6 +1144,10 @@ watch(() => dataForm.groupId, () => {
} }
}) })
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10003");
};
onMounted(() => { onMounted(() => {
init() init()
}) })

View File

@@ -0,0 +1,327 @@
<template>
<el-dialog
title="教室公物编辑"
v-model="visible"
:width="800"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="楼号" prop="buildingNo">
<el-input
v-model="form.buildingNo"
placeholder="楼号"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学院" prop="deptName">
<el-input
v-model="form.deptName"
placeholder="学院"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="班级" prop="classCode">
<el-input
v-model="form.classNo"
placeholder="班级"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="教室位置" prop="position">
<el-input
v-model="form.position"
placeholder="教室位置"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="讲台类型" prop="platformType">
<el-select
v-model="form.platformType"
placeholder="请选择讲台类型"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in platformTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="投影类型" prop="tyType">
<el-select
v-model="form.tyType"
placeholder="请选择投影类型"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in tyTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="电视机" prop="tvType">
<el-select
v-model="form.tvType"
placeholder="请选择电视机"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in tvTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="方凳数量" prop="chairCnt">
<el-input-number
v-model="form.chairCnt"
:min="0"
placeholder="请输入方凳数量"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="课桌数量" prop="tableCnt">
<el-input-number
v-model="form.tableCnt"
:min="0"
placeholder="请输入课桌数量"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</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="ClassroomAssetsDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editAssets } from '/@/api/stuwork/classassets'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const platformTypeList = ref<any[]>([])
const tyTypeList = ref<any[]>([])
const tvTypeList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
buildingNo: '',
deptName: '',
deptCode: '',
classCode: '',
classNo: '',
position: '',
platformType: '',
tyType: '',
tvType: '',
chairCnt: 0,
tableCnt: 0,
remarks: ''
})
// 定义校验规则
const dataRules = {
platformType: [
{ required: true, message: '请选择讲台类型', trigger: 'change' }
],
tyType: [
{ required: true, message: '请选择投影类型', trigger: 'change' }
],
tvType: [
{ required: true, message: '请选择电视机', trigger: 'change' }
],
chairCnt: [
{ required: true, message: '请填写方凳数量', trigger: 'blur' }
],
tableCnt: [
{ required: true, message: '请填写课桌数量', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (row: any) => {
visible.value = true
resetForm()
// 填充现有数据
if (row) {
form.id = row.id || ''
form.buildingNo = row.buildingNo || ''
form.deptName = row.deptName || ''
form.deptCode = row.deptCode || ''
form.classCode = row.classCode || ''
form.classNo = row.classNo || ''
form.position = row.position || ''
form.platformType = row.platformType || ''
form.tyType = row.tyType || ''
form.tvType = row.tvType || ''
form.chairCnt = row.chairCnt || 0
form.tableCnt = row.tableCnt || 0
form.remarks = row.remarks || ''
}
nextTick(() => {
dataFormRef.value?.clearValidate()
})
}
// 重置表单
const resetForm = () => {
form.id = ''
form.buildingNo = ''
form.deptName = ''
form.deptCode = ''
form.classCode = ''
form.classNo = ''
form.position = ''
form.platformType = ''
form.tyType = ''
form.tvType = ''
form.chairCnt = 0
form.tableCnt = 0
form.remarks = ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
id: form.id,
buildingNo: form.buildingNo,
deptName: form.deptName,
deptCode: form.deptCode,
classCode: form.classCode,
classNo: form.classNo,
position: form.position,
platformType: form.platformType,
tyType: form.tyType,
tvType: form.tvType,
chairCnt: form.chairCnt,
tableCnt: form.tableCnt,
remarks: form.remarks
}
await editAssets(submitData)
useMessage().success('保存成功')
visible.value = false
emit('refresh')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}
})
}
// 获取讲台类型字典
const getPlatformTypeDict = async () => {
try {
const res = await getDicts('platform_type')
if (res.data) {
platformTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
platformTypeList.value = []
}
}
// 获取投影类型字典
const getTyTypeDict = async () => {
try {
const res = await getDicts('ty_type')
if (res.data) {
tyTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
tyTypeList.value = []
}
}
// 获取电视机类型字典
const getTvTypeDict = async () => {
try {
const res = await getDicts('tv_type')
if (res.data) {
tvTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
tvTypeList.value = []
}
}
// 初始化
onMounted(() => {
getPlatformTypeDict()
getTyTypeDict()
getTvTypeDict()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -181,19 +181,43 @@
</template> </template>
</el-table-column> </el-table-column>
</template> </template>
<el-table-column label="操作" width="120" align="center" fixed="right"> <el-table-column label="操作" width="380" align="center" fixed="right">
<template #header> <template #header>
<el-icon><Setting /></el-icon> <el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span> <span style="margin-left: 4px">操作</span>
</template> </template>
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-if="!scope.row.classCode || !scope.row.position"
icon="Setting" icon="Setting"
link link
type="primary" type="primary"
@click="handleArrange(scope.row)"> @click="handleArrange(scope.row)">
教室安排 教室安排
</el-button> </el-button>
<el-button
icon="Close"
link
type="danger"
@click="handleCancelArrange(scope.row)">
取消教室安排
</el-button>
<el-button
icon="Collection"
link
type="success"
@click="handleAssets(scope.row)"
class="ml10">
教室公物
</el-button>
<el-button
icon="Lock"
link
type="warning"
@click="handlePassword(scope.row)"
class="ml10">
门锁密码
</el-button>
</template> </template>
</el-table-column> </el-table-column>
<template #empty> <template #empty>
@@ -213,6 +237,10 @@
<!-- 教室安排表单弹窗 --> <!-- 教室安排表单弹窗 -->
<arrange-dialog ref="arrangeDialogRef" @refresh="getDataList" /> <arrange-dialog ref="arrangeDialogRef" @refresh="getDataList" />
<!-- 教室公物编辑弹窗 -->
<assets-dialog ref="assetsDialogRef" @refresh="getDataList" />
<!-- 门锁密码编辑弹窗 -->
<password-dialog ref="passwordDialogRef" @refresh="getDataList" />
</div> </div>
</template> </template>
@@ -226,7 +254,10 @@ import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message"; import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue' import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import ArrangeDialog from './arrange.vue' import ArrangeDialog from './arrange.vue'
import { List, OfficeBuilding, CircleCheck, Location, UserFilled, Collection, Setting, Menu, Calendar, Search, Document } from '@element-plus/icons-vue' import AssetsDialog from './assets.vue'
import PasswordDialog from './password.vue'
import { delClassRoomAssign } from '/@/api/stuwork/teachclassroomassign'
import { List, OfficeBuilding, CircleCheck, Location, UserFilled, Collection, Setting, Menu, Calendar, Search, Document, Close, Lock } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn' import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容 // 定义变量内容
@@ -240,6 +271,8 @@ const platformTypeList = ref<any[]>([])
const tyTypeList = ref<any[]>([]) const tyTypeList = ref<any[]>([])
const tvTypeList = ref<any[]>([]) const tvTypeList = ref<any[]>([])
const arrangeDialogRef = ref() const arrangeDialogRef = ref()
const assetsDialogRef = ref()
const passwordDialogRef = ref()
// 表格列配置 // 表格列配置
const tableColumns = [ const tableColumns = [
@@ -416,6 +449,33 @@ const handleArrange = (row: any) => {
arrangeDialogRef.value?.openDialog(row) arrangeDialogRef.value?.openDialog(row)
} }
// 取消教室安排
const handleCancelArrange = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('确定要取消教室安排吗?')
await delClassRoomAssign(row)
useMessage().success('取消成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
if (!err?._messageShown) {
useMessage().error(err?.msg || '取消失败')
}
}
}
}
// 教室公物
const handleAssets = (row: any) => {
assetsDialogRef.value?.openDialog(row)
}
// 门锁密码
const handlePassword = (row: any) => {
passwordDialogRef.value?.openDialog(row)
}
// 获取系部列表 // 获取系部列表
const getDeptListData = async () => { const getDeptListData = async () => {
try { try {

View File

@@ -0,0 +1,156 @@
<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="120px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="楼号">
<el-input
v-model="form.buildingNo"
placeholder="楼号"
disabled />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="教室位置">
<el-input
v-model="form.position"
placeholder="教室位置"
disabled />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="门锁密码" prop="password">
<el-input
v-model="form.password"
type="password"
show-password
placeholder="请输入门锁密码"
clearable />
</el-form-item>
</el-col>
</el-row>
</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="ClassroomPasswordDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editAssets } from '/@/api/stuwork/classassets'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
// 提交表单数据
const form = reactive({
id: '',
buildingNo: '',
deptName: '',
deptCode: '',
classCode: '',
classNo: '',
position: '',
password: ''
})
// 定义校验规则
const dataRules = {
password: [
{ required: true, message: '请输入门锁密码', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (row: any) => {
visible.value = true
resetForm()
// 填充现有数据
if (row) {
form.id = row.id || ''
form.buildingNo = row.buildingNo || ''
form.deptName = row.deptName || ''
form.deptCode = row.deptCode || ''
form.classCode = row.classCode || ''
form.classNo = row.classNo || ''
form.position = row.position || ''
form.password = row.password || ''
}
nextTick(() => {
dataFormRef.value?.clearValidate()
})
}
// 重置表单
const resetForm = () => {
form.id = ''
form.buildingNo = ''
form.deptName = ''
form.deptCode = ''
form.classCode = ''
form.classNo = ''
form.position = ''
form.password = ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
// 只提交必要的字段用于修改门锁密码
const submitData = {
id: form.id,
buildingNo: form.buildingNo,
deptName: form.deptName,
deptCode: form.deptCode,
classCode: form.classCode,
classNo: form.classNo,
position: form.position,
password: form.password
}
await editAssets(submitData)
useMessage().success('保存成功')
visible.value = false
emit('refresh')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}
})
}
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -495,7 +495,9 @@ const confirmCheck = async () => {
checkDialogVisible.value = false checkDialogVisible.value = false
getDataList() getDataList()
} catch (err: any) { } catch (err: any) {
useMessage().error(err.msg || '考核失败') if (!err?._messageShown) {
useMessage().error(err?.msg || '考核失败')
}
} }
} }
@@ -512,7 +514,9 @@ const handleDelete = async (ids: string[]) => {
getDataList() getDataList()
useMessage().success('删除成功') useMessage().success('删除成功')
} catch (err: any) { } catch (err: any) {
useMessage().error(err.msg || '删除失败') if (!err?._messageShown) {
useMessage().error(err?.msg || '删除失败')
}
} }
} }

View File

@@ -0,0 +1,216 @@
<template>
<el-dialog
title="新增宿舍点名"
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-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="点名类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择点名类型"
clearable
style="width: 100%">
<el-option label="普通住宿点名" value="1" />
<el-option label="留宿点名" value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="楼号" prop="buildId">
<el-select
v-model="form.buildId"
placeholder="请选择楼号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in buildingList"
:key="item.buildingNo"
:label="item.buildingNo"
:value="item.buildingNo" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="form.roomNo"
placeholder="请输入房间号"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学生列表" prop="list">
<el-input
v-model="studentNosInput"
type="textarea"
:rows="5"
placeholder="请输入学号,多个学号用逗号或换行分隔"
style="width: 100%" />
<div class="form-tip">提示多个学号可用逗号或换行分隔</div>
</el-form-item>
</el-col>
</el-row>
</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="DormSignRecordFormDialog">
import { ref, reactive, nextTick, computed, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj } from '/@/api/stuwork/dormsignrecord'
import { getBuildingList } from '/@/api/stuwork/dormbuilding'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const buildingList = ref<any[]>([])
// 提交表单数据
const form = reactive({
type: '',
buildId: '',
roomNo: '',
list: [] as Array<{ stuNo: string }>
})
// 学生学号输入(用于显示和编辑)
const studentNosInput = ref('')
// 定义校验规则
const dataRules = {
type: [
{ required: true, message: '请选择点名类型', trigger: 'change' }
],
buildId: [
{ required: true, message: '请选择楼号', trigger: 'change' }
],
roomNo: [
{ required: true, message: '请输入房间号', trigger: 'blur' }
],
list: [
{ required: true, message: '请输入至少一个学号', trigger: 'change' },
{
validator: (rule: any, value: any, callback: any) => {
if (!value || value.length === 0) {
callback(new Error('请输入至少一个学号'))
} else {
callback()
}
},
trigger: 'change'
}
]
}
// 打开弹窗
const openDialog = async () => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.type = ''
form.buildId = ''
form.roomNo = ''
form.list = []
studentNosInput.value = ''
})
}
// 解析学号输入(支持逗号和换行分隔)
const parseStudentNos = (input: string): string[] => {
if (!input || !input.trim()) return []
return input
.split(/[,\n]/)
.map(s => s.trim())
.filter(s => s.length > 0)
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
// 解析学号列表
const stuNos = parseStudentNos(studentNosInput.value)
if (stuNos.length === 0) {
useMessage().warning('请输入至少一个学号')
return
}
form.list = stuNos.map(stuNo => ({ stuNo }))
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
await addObj({
type: form.type,
buildId: form.buildId,
roomNo: form.roomNo,
list: form.list
})
useMessage().success('新增成功')
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || '新增失败')
}
} finally {
loading.value = false
}
})
}
// 获取楼号列表
const getBuildingListData = async () => {
try {
const res = await getBuildingList()
buildingList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
buildingList.value = []
}
}
// 初始化
onMounted(() => {
getBuildingListData()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,367 @@
<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="state.queryForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="楼号" prop="buildNo">
<el-select
v-model="state.queryForm.buildNo"
placeholder="请选择楼号"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in buildingList"
:key="item.buildingNo"
:label="item.buildingNo"
:value="item.buildingNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="state.queryForm.roomNo"
placeholder="请输入房间号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="state.queryForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="点名类型" prop="type">
<el-select
v-model="state.queryForm.type"
placeholder="请选择点名类型"
clearable
style="width: 200px">
<el-option label="普通住宿点名" value="1" />
<el-option label="留宿点名" value="2" />
</el-select>
</el-form-item>
<el-form-item label="考勤状态" prop="sign">
<el-select
v-model="state.queryForm.sign"
placeholder="请选择考勤状态"
clearable
style="width: 200px">
<el-option label="未到" :value="0" />
<el-option label="已到" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="考勤日期" prop="date">
<el-date-picker
v-model="state.queryForm.date"
type="date"
placeholder="请选择考勤日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain 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"><Document /></el-icon>
宿舍点名管理列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增点名
</el-button>
<el-button
icon="RefreshLeft"
type="warning"
class="ml10"
@click="handleInit">
初始化
</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"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.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 === 'type'" #default="scope">
<el-tag size="small" :type="scope.row.type === '1' ? 'primary' : 'success'" effect="plain">
{{ scope.row.type === '1' ? '普通住宿点名' : '留宿点名' }}
</el-tag>
</template>
<!-- 考勤状态列特殊模板 -->
<template v-else-if="col.prop === 'sign'" #default="scope">
<el-tag size="small" :type="scope.row.sign === '1' || scope.row.sign === 1 ? 'success' : 'danger'" effect="plain">
{{ scope.row.sign === '1' || scope.row.sign === 1 ? '已到' : '未到' }}
</el-tag>
</template>
<!-- 是否扫过脸列特殊模板 -->
<template v-else-if="col.prop === 'isFace'" #default="scope">
<el-tag size="small" :type="scope.row.isFace === '1' || scope.row.isFace === 1 ? 'success' : 'info'" effect="plain">
{{ scope.row.isFace === '1' || scope.row.isFace === 1 ? '是' : '否' }}
</el-tag>
</template>
<!-- 是否请假列特殊模板 -->
<template v-else-if="col.prop === 'isApply'" #default="scope">
<el-tag size="small" :type="scope.row.isApply === '1' || scope.row.isApply === 1 ? 'warning' : 'info'" effect="plain">
{{ scope.row.isApply === '1' || scope.row.isApply === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增点名弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="DormSignRecord">
import { reactive, ref, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, initDormStuInfoForAttendance } from "/@/api/stuwork/dormsignrecord";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { getDeptList } from "/@/api/basic/basicclass";
import { getBuildingList } from "/@/api/stuwork/dormbuilding";
import FormDialog from './form.vue'
import { Search, Document, List, Menu, RefreshLeft, OfficeBuilding, Grid, User, CreditCard, House, Check, Calendar, UserFilled } from '@element-plus/icons-vue'
import RightToolbar from '/@/components/RightToolbar/index.vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
import { useRoute } from 'vue-router'
const route = useRoute()
const formDialogRef = ref()
const showSearch = ref(true)
const searchFormRef = ref()
const columnControlRef = ref<any>()
const deptList = ref<any[]>([])
const buildingList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'deptName', label: '学院', minWidth: 120, icon: OfficeBuilding },
{ prop: 'className', label: '班级', minWidth: 120, icon: Grid },
{ prop: 'stuName', label: '姓名', width: 100, icon: User },
{ prop: 'stuNo', label: '学号', width: 120, icon: CreditCard },
{ prop: 'roomNo', label: '房间号', width: 100, icon: House },
{ prop: 'buildId', label: '楼号', width: 80, icon: OfficeBuilding },
{ prop: 'type', label: '点名类型', width: 120, icon: Document },
{ prop: 'sign', label: '考勤状态', width: 100, icon: Check },
{ prop: 'date', label: '考勤日期', width: 120, icon: Calendar },
{ prop: 'isFace', label: '是否扫过脸', width: 100, icon: UserFilled },
{ prop: 'isApply', label: '是否请假', width: 100, icon: Document }
]
// 使用表格列控制
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange,
loadSavedConfig
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 立即加载配置
loadSavedConfig()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
deptCode: '',
buildNo: '',
roomNo: '',
stuNo: '',
type: '',
sign: '',
date: ''
},
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
return {
data: {
records: data?.records ?? [],
total: data?.total ?? 0
}
}
},
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true,
isPage: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.deptCode = ''
state.queryForm.buildNo = ''
state.queryForm.roomNo = ''
state.queryForm.stuNo = ''
state.queryForm.type = ''
state.queryForm.sign = ''
state.queryForm.date = ''
getDataList()
}
// 初始化
const handleInit = async () => {
const { confirm } = useMessageBox()
try {
await confirm('确定要初始化宿舍学生信息用于考勤吗?')
await initDormStuInfoForAttendance()
useMessage().success('初始化成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
if (!err?._messageShown) {
useMessage().error(err?.msg || '初始化失败')
}
}
}
}
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
deptList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
deptList.value = []
}
}
// 获取楼号列表
const getBuildingListData = async () => {
try {
const res = await getBuildingList()
buildingList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
buildingList.value = []
}
}
// 初始化
onMounted(() => {
getDeptListData()
getBuildingListData()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -184,11 +184,24 @@ const TimeRuleTableComponent = defineComponent({
default: ({ row }: any) => { default: ({ row }: any) => {
const timeList = getTimeList(row) const timeList = getTimeList(row)
return h('div', { class: 'time-slots-container' }, return h('div', { class: 'time-slots-container' },
timeList.map((timeSlot: any, index: number) => timeList.map((timeSlot: any, index: number) => {
h('div', { key: index, class: 'time-slot-item' }, [ // 通过数组索引更新,确保响应式
const updateStartTime = (val: string) => {
const list = getTimeList(row)
if (list[index]) {
list[index].startTime = val || ''
}
}
const updateEndTime = (val: string) => {
const list = getTimeList(row)
if (list[index]) {
list[index].endTime = val || ''
}
}
return h('div', { key: index, class: 'time-slot-item' }, [
h(ElTimePicker, { h(ElTimePicker, {
modelValue: timeSlot.startTime, modelValue: timeSlot.startTime || '',
'onUpdate:modelValue': (val: string) => { timeSlot.startTime = val }, 'onUpdate:modelValue': updateStartTime,
format: 'HH:mm', format: 'HH:mm',
valueFormat: 'HH:mm', valueFormat: 'HH:mm',
placeholder: '开始时间', placeholder: '开始时间',
@@ -196,8 +209,8 @@ const TimeRuleTableComponent = defineComponent({
}), }),
h('span', { style: { margin: '0 10px' } }, '至'), h('span', { style: { margin: '0 10px' } }, '至'),
h(ElTimePicker, { h(ElTimePicker, {
modelValue: timeSlot.endTime, modelValue: timeSlot.endTime || '',
'onUpdate:modelValue': (val: string) => { timeSlot.endTime = val }, 'onUpdate:modelValue': updateEndTime,
format: 'HH:mm', format: 'HH:mm',
valueFormat: 'HH:mm', valueFormat: 'HH:mm',
placeholder: '结束时间', placeholder: '结束时间',
@@ -213,7 +226,7 @@ const TimeRuleTableComponent = defineComponent({
style: { marginLeft: '10px' } style: { marginLeft: '10px' }
}) })
]) ])
) })
) )
} }
}), }),

View File

@@ -0,0 +1,144 @@
<template>
<el-dialog
:title="form.id ? '编辑文件夹' : '新建文件夹'"
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-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="文件夹名称" prop="classification">
<el-input
v-model="form.classification"
placeholder="请输入文件夹名称"
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</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="FileManagerFolderDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editFile, getDetail } from '/@/api/stuwork/filemanager'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
// 提交表单数据
const form = reactive({
id: '',
classification: '',
parentId: null as string | null
})
// 定义校验规则
const dataRules = {
classification: [
{ required: true, message: '请输入文件夹名称', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.classification = ''
form.parentId = row?.parentId ?? null
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.classification = row.classification || ''
form.parentId = row.parentId || null
// 如果需要获取详情
if (row.id && !row.classification) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.classification = res.data.classification || ''
form.parentId = res.data.parentId || form.parentId
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 提交表单(新增文件夹需要通过新增文件接口,但只传 classification 和 parentId
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') {
// 新增文件夹:使用 addObj但只传 classification 和 parentId
const { addObj } = await import('/@/api/stuwork/filemanager')
await addObj({
classification: form.classification,
parentId: form.parentId || '-1',
level: '0' // 文件夹层级为 0
})
useMessage().success('新建成功')
} else {
// 编辑文件夹:使用 editFile 接口
await editFile({
id: form.id,
parentId: form.parentId || '-1',
classification: form.classification
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新建失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,181 @@
<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-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="文件名称" prop="fileName">
<el-input
v-model="form.fileName"
placeholder="请输入文件名称"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="文件上传" prop="fileUrl">
<Upload
v-model="form.fileUrl"
:limit="1"
uploadFileUrl="/stuwork/file/upload"
type="default" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注"
:maxlength="250"
show-word-limit
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</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="FileManagerFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/filemanager'
import Upload from '/@/components/Upload/index.vue'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
// 提交表单数据
const form = reactive({
id: '',
fileName: '',
fileUrl: '',
remarks: '',
parentId: null as string | null,
level: '1'
})
// 定义校验规则
const dataRules = {
fileName: [
{ required: true, message: '请输入文件名称', trigger: 'blur' }
],
fileUrl: [
{ required: true, message: '请上传文件', trigger: 'change' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.fileName = ''
form.fileUrl = ''
form.remarks = ''
form.parentId = row?.parentId ?? null
form.level = row?.level || '1'
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.fileName = row.fileName || ''
form.fileUrl = row.fileUrl || ''
form.remarks = row.remarks || ''
form.parentId = row.parentId || null
form.level = row.level || '1'
// 如果需要获取详情
if (row.id && !row.fileUrl) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.fileName = res.data.fileName || form.fileName
form.fileUrl = res.data.fileUrl || ''
form.remarks = res.data.remarks || ''
form.parentId = res.data.parentId || form.parentId
form.level = res.data.level || form.level
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
fileName: form.fileName,
fileUrl: form.fileUrl,
remarks: form.remarks,
parentId: form.parentId || '-1',
level: form.level
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
...submitData
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,345 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 内容卡片 -->
<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="handleAddFolder">
新建文件夹
</el-button>
<el-button
icon="FolderAdd"
type="success"
class="ml10"
@click="handleAddFile">
上传文件
</el-button>
<el-button
icon="Refresh"
class="ml10"
@click="getDataList">
刷新
</el-button>
</div>
</div>
</template>
<!-- 面包屑导航 -->
<div class="breadcrumb-wrapper mb16" v-if="breadcrumbList.length > 0">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<el-button link @click="handleBreadcrumbClick(null)">
<el-icon><Folder /></el-icon>
根目录
</el-button>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.id">
<el-button
link
@click="handleBreadcrumbClick(item)"
:disabled="index === breadcrumbList.length - 1">
{{ item.classification || item.fileName }}
</el-button>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 表格 -->
<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 }}
</template>
</el-table-column>
<el-table-column prop="classification" label="文件夹名称" min-width="200" show-overflow-tooltip>
<template #header>
<el-icon><Folder /></el-icon>
<span style="margin-left: 4px">文件夹名称</span>
</template>
<template #default="scope">
<span v-if="scope.row.classification">{{ scope.row.classification }}</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="fileName" label="文件名称" min-width="200" show-overflow-tooltip>
<template #header>
<el-icon><Document /></el-icon>
<span style="margin-left: 4px">文件名称</span>
</template>
<template #default="scope">
<span v-if="scope.row.fileName">{{ scope.row.fileName }}</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="level" label="层级" width="80" align="center">
<template #header>
<el-icon><Grid /></el-icon>
<span style="margin-left: 4px">层级</span>
</template>
</el-table-column>
<el-table-column prop="remarks" label="备注" min-width="150" show-overflow-tooltip>
<template #header>
<el-icon><EditPen /></el-icon>
<span style="margin-left: 4px">备注</span>
</template>
<template #default="scope">
<span v-if="scope.row.remarks">{{ scope.row.remarks }}</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
<template #header>
<el-icon><Clock /></el-icon>
<span style="margin-left: 4px">创建时间</span>
</template>
</el-table-column>
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<!-- 文件夹进入编辑删除 -->
<template v-if="scope.row.classification">
<el-button
icon="FolderOpened"
link
type="primary"
@click="handleEnterFolder(scope.row)">
进入
</el-button>
<el-button
icon="Edit"
link
type="primary"
@click="handleEditFolder(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
<!-- 文件下载编辑删除 -->
<template v-else>
<el-button
icon="Download"
link
type="primary"
@click="handleDownload(scope.row)">
下载
</el-button>
<el-button
icon="Edit"
link
type="primary"
@click="handleEditFile(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 新增/编辑文件弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- 新增/编辑文件夹弹窗 -->
<folder-dialog ref="folderDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="FileManager">
import { reactive, ref, onMounted, computed } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/filemanager";
import { useMessage, useMessageBox } from "/@/hooks/message";
import FormDialog from './form.vue'
import FolderDialog from './folder.vue'
import { List, Document, Folder, Grid, EditPen, Clock, Setting, FolderOpened, Download } from '@element-plus/icons-vue'
// 定义变量内容
const formDialogRef = ref()
const folderDialogRef = ref()
const currentParentId = ref<string | null>(null)
const breadcrumbList = ref<any[]>([])
// 配置 useTable接口返回数组非分页结构
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
parentId: null as string | null
},
pageList: async (params: any) => {
const res = await fetchList(params)
// 数据链是 data.data.records需要正确解析
let list: any[] = []
if (res?.data) {
// 如果 res.data 是数组,直接使用
if (Array.isArray(res.data)) {
list = res.data
}
// 如果 res.data.data 存在且是对象,尝试取 records
else if (res.data.data && res.data.data.records && Array.isArray(res.data.data.records)) {
list = res.data.data.records
}
// 如果 res.data.records 存在且是数组,直接使用
else if (res.data.records && Array.isArray(res.data.records)) {
list = res.data.records
}
}
return {
data: list // isPage: false 时,直接返回数组
}
},
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true,
isPage: false // 不分页
})
// table hook
const {
getDataList,
tableStyle
} = useTable(state)
// 进入文件夹
const handleEnterFolder = (row: any) => {
currentParentId.value = row.id
breadcrumbList.value.push(row)
state.queryForm.parentId = row.id
getDataList()
}
// 面包屑点击
const handleBreadcrumbClick = (item: any | null) => {
if (item === null) {
// 返回根目录
currentParentId.value = null
breadcrumbList.value = []
state.queryForm.parentId = null
} else {
// 返回指定目录
const index = breadcrumbList.value.findIndex(b => b.id === item.id)
if (index >= 0) {
breadcrumbList.value = breadcrumbList.value.slice(0, index + 1)
currentParentId.value = item.id
state.queryForm.parentId = item.id
}
}
getDataList()
}
// 新增文件夹
const handleAddFolder = () => {
folderDialogRef.value?.openDialog('add', { parentId: currentParentId.value })
}
// 新增文件
const handleAddFile = () => {
formDialogRef.value?.openDialog('add', { parentId: currentParentId.value })
}
// 编辑文件夹
const handleEditFolder = (row: any) => {
folderDialogRef.value?.openDialog('edit', row)
}
// 编辑文件
const handleEditFile = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 下载文件
const handleDownload = (row: any) => {
if (!row.fileUrl) {
useMessage().warning('文件地址无效')
return
}
// fileUrl 可能是完整 URL 或相对路径
if (row.fileUrl.startsWith('http://') || row.fileUrl.startsWith('https://')) {
window.open(row.fileUrl, '_blank')
} else {
// 相对路径,需要拼接 baseURL
const baseURL = import.meta.env.VITE_API_URL || ''
const url = baseURL + (row.fileUrl.startsWith('/') ? row.fileUrl : '/' + row.fileUrl)
window.open(url, '_blank')
}
}
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
const name = row.classification || row.fileName || '该项'
await confirm(`确定要删除${name}吗?`)
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err?.msg || '删除失败')
}
}
}
// 初始化
onMounted(() => {
getDataList()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.breadcrumb-wrapper {
padding: 12px 16px;
background: #f5f7fa;
border-radius: 4px;
:deep(.el-breadcrumb) {
.el-breadcrumb__item {
.el-button {
padding: 0;
font-size: 14px;
}
}
}
}
.mb16 {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,253 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="800"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
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="24" class="mb20">
<el-form-item label="标题" prop="title">
<el-input
v-model="form.title"
placeholder="请输入标题"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="作者" prop="author">
<el-input
v-model="form.author"
placeholder="请输入作者"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="内容" prop="content">
<Editor
v-model:getHtml="form.content"
:height="'400'"
placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
</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="MoralPlanFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/moralplan'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
import Editor from '/@/components/Editor/index.vue'
const emit = defineEmits(['refresh'])
// 定义变量内容
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 form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
title: '',
content: '',
author: ''
})
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
],
author: [
{ required: true, message: '请输入作者', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.title = ''
form.content = ''
form.author = ''
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.title = row.title || ''
form.content = row.content || ''
form.author = row.author || ''
// 如果需要获取详情
if (row.id && !row.content) {
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.title = res.data.title || ''
form.content = res.data.content || ''
form.author = res.data.author || ''
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 学年列表(班级管理-学年接口)
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 onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
title: form.title,
content: form.content,
author: form.author
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
...submitData
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 初始化:加载学年、学期
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,356 @@
<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="state.queryForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="state.queryForm.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="state.queryForm.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
v-model="state.queryForm.title"
placeholder="请输入标题"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="state.queryForm.content"
placeholder="请输入内容"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input
v-model="state.queryForm.author"
placeholder="请输入作者"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain 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"><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"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.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 === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- 内容列特殊模板可能较长截断显示 -->
<template v-else-if="col.prop === 'content'" #default="scope">
<span :title="scope.row.content">{{ scope.row.content ? (scope.row.content.length > 50 ? scope.row.content.substring(0, 50) + '...' : scope.row.content) : '-' }}</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>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑表单弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="MoralPlan">
import { reactive, ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/moralplan";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
import { List, Calendar, Clock, Document, User, Setting, Menu, Search } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容
const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
const searchFormRef = ref()
const showSearch = ref(true)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'title', label: '标题', minWidth: 200 },
{ prop: 'content', label: '内容', minWidth: 250 },
{ prop: 'author', label: '作者' },
{ prop: 'createTime', label: '创建时间', width: 180 },
{ prop: 'updateTime', label: '更新时间', width: 180 }
]
// 列配置映射(用于图标)
const columnConfigMap: Record<string, { icon: any }> = {
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
title: { icon: Document },
content: { icon: Document },
author: { icon: User },
createTime: { icon: Clock },
updateTime: { icon: Clock }
}
// 使用表格列控制hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
schoolTerm: '',
title: '',
content: '',
author: ''
},
pageList: fetchList,
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
}
const dictItem = schoolTermList.value.find(item => item.value == value)
return dictItem ? dictItem.label : value
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
state.queryForm.schoolTerm = ''
state.queryForm.title = ''
state.queryForm.content = ''
state.queryForm.author = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('确定要删除该德育计划吗?')
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '删除失败')
}
}
}
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
if (res.data && Array.isArray(res.data)) {
schoolYearList.value = res.data
} else {
schoolYearList.value = []
}
} 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 = []
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -10,7 +10,8 @@
:model="form" :model="form"
:rules="dataRules" :rules="dataRules"
label-width="120px" label-width="120px"
v-loading="loading"> v-loading="loading"
:key="form.turnoverType">
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="12" class="mb20"> <el-col :span="12" class="mb20">
<el-form-item label="学年" prop="schoolYear"> <el-form-item label="学年" prop="schoolYear">
@@ -47,7 +48,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" class="mb20"> <el-col :span="12" class="mb20" v-if="!isDropoutType">
<el-form-item label="原班级" prop="oldClassCode"> <el-form-item label="原班级" prop="oldClassCode">
<el-select <el-select
v-model="form.oldClassCode" v-model="form.oldClassCode"
@@ -66,7 +67,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" class="mb20"> <el-col :span="12" class="mb20" v-if="!isDropoutType">
<el-form-item label="现班级" prop="newClassCode"> <el-form-item label="现班级" prop="newClassCode">
<el-select <el-select
v-model="form.newClassCode" v-model="form.newClassCode"
@@ -90,7 +91,8 @@
v-model="form.turnoverType" v-model="form.turnoverType"
placeholder="请选择异动类型" placeholder="请选择异动类型"
clearable clearable
style="width: 100%"> style="width: 100%"
@change="handleTurnoverTypeChange">
<el-option <el-option
v-for="item in turnoverTypeList" v-for="item in turnoverTypeList"
:key="item.value" :key="item.value"
@@ -299,20 +301,29 @@ const selectedStudentsText = computed(() => {
return `已选择 ${form.selectedStudents.length} 名学生` return `已选择 ${form.selectedStudents.length} 名学生`
}) })
// 定义校验规则 // 判断是否为退学类型
const dataRules = { const isDropoutType = computed(() => {
if (!form.turnoverType) return false
// 查找异动类型字典中 label 包含"退学"的项
const dropoutItem = turnoverTypeList.value.find((item: any) => {
const label = item.label || item.dictLabel || item.name || ''
return label.includes('退学')
})
if (dropoutItem) {
return dropoutItem.value === form.turnoverType || dropoutItem.dictValue === form.turnoverType || dropoutItem.code === form.turnoverType
}
return false
})
// 定义校验规则(动态)
const dataRules = computed(() => {
const rules: any = {
schoolYear: [ schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' } { required: true, message: '请选择学年', trigger: 'change' }
], ],
schoolTerm: [ schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' } { required: true, message: '请选择学期', trigger: 'change' }
], ],
oldClassCode: [
{ required: true, message: '请选择原班级', trigger: 'change' }
],
newClassCode: [
{ required: true, message: '请选择现班级', trigger: 'change' }
],
turnoverType: [ turnoverType: [
{ required: true, message: '请选择异动类型', trigger: 'change' } { required: true, message: '请选择异动类型', trigger: 'change' }
], ],
@@ -322,7 +333,20 @@ const dataRules = {
selectedStudents: [ selectedStudents: [
{ required: true, message: '请至少选择一个学生', trigger: 'change', type: 'array', min: 1 } { required: true, message: '请至少选择一个学生', trigger: 'change', type: 'array', min: 1 }
] ]
} }
// 如果不是退学类型,原班级和现班级为必填
if (!isDropoutType.value) {
rules.oldClassCode = [
{ required: true, message: '请选择原班级', trigger: 'change' }
]
rules.newClassCode = [
{ required: true, message: '请选择现班级', trigger: 'change' }
]
}
return rules
})
// 原班级变化时,更新学生搜索条件 // 原班级变化时,更新学生搜索条件
const handleOldClassChange = () => { const handleOldClassChange = () => {
@@ -331,14 +355,30 @@ const handleOldClassChange = () => {
form.selectedStudents = [] form.selectedStudents = []
} }
// 异动类型变化时处理
const handleTurnoverTypeChange = () => {
// 如果切换为退学类型,清空原班级和现班级
if (isDropoutType.value) {
form.oldClassCode = ''
form.newClassCode = ''
// 清空已选学生(因为退学不需要原班级,学生选择逻辑会受影响)
form.selectedStudents = []
}
// 重新验证表单
nextTick(() => {
dataFormRef.value?.clearValidate(['oldClassCode', 'newClassCode'])
})
}
// 打开学生选择弹窗 // 打开学生选择弹窗
const openStudentDialog = () => { const openStudentDialog = () => {
if (!form.oldClassCode) { // 如果是退学类型,不需要原班级
if (!isDropoutType.value && !form.oldClassCode) {
useMessage().warning('请先选择原班级') useMessage().warning('请先选择原班级')
return return
} }
studentDialogVisible.value = true studentDialogVisible.value = true
studentSearchForm.classCode = form.oldClassCode studentSearchForm.classCode = form.oldClassCode || ''
studentSearchForm.classNo = '' studentSearchForm.classNo = ''
studentSearchForm.stuNo = '' studentSearchForm.stuNo = ''
studentSearchForm.realName = '' studentSearchForm.realName = ''
@@ -354,10 +394,13 @@ const handleStudentSearch = async () => {
const params: any = { const params: any = {
current: studentPagination.currentPage, current: studentPagination.currentPage,
size: studentPagination.pageSize, size: studentPagination.pageSize,
classCode: studentSearchForm.classCode || undefined,
stuNo: studentSearchForm.stuNo || undefined, stuNo: studentSearchForm.stuNo || undefined,
realName: studentSearchForm.realName || undefined realName: studentSearchForm.realName || undefined
} }
// 如果不是退学类型且有原班级,才添加 classCode 条件
if (!isDropoutType.value && studentSearchForm.classCode) {
params.classCode = studentSearchForm.classCode
}
const res = await getStudentList(params) const res = await getStudentList(params)
if (res.data && res.data.records) { if (res.data && res.data.records) {
studentTableData.value = res.data.records studentTableData.value = res.data.records
@@ -494,8 +537,6 @@ const onSubmit = async () => {
const submitData: any = { const submitData: any = {
schoolYear: form.schoolYear, schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm, schoolTerm: form.schoolTerm,
oldClassCode: form.oldClassCode,
newClassCode: form.newClassCode,
turnoverType: form.turnoverType, turnoverType: form.turnoverType,
turnYear: form.turnYear || '', turnYear: form.turnYear || '',
turnoverDate: form.turnoverDate, turnoverDate: form.turnoverDate,
@@ -506,6 +547,12 @@ const onSubmit = async () => {
})) }))
} }
// 如果不是退学类型,才添加原班级和现班级
if (!isDropoutType.value) {
submitData.oldClassCode = form.oldClassCode
submitData.newClassCode = form.newClassCode
}
// 编辑时需要包含id和单个学生信息 // 编辑时需要包含id和单个学生信息
if (operType.value === 'edit') { if (operType.value === 'edit') {
submitData.id = form.id submitData.id = form.id

View File

@@ -0,0 +1,253 @@
<template>
<el-dialog
:title="form.id ? '编辑' : '新增'"
v-model="visible"
:width="800"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
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="24" class="mb20">
<el-form-item label="标题" prop="title">
<el-input
v-model="form.title"
placeholder="请输入标题"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="作者" prop="author">
<el-input
v-model="form.author"
placeholder="请输入作者"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="内容" prop="content">
<Editor
v-model:getHtml="form.content"
:height="'400'"
placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
</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="TermActivityFormDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/termactivity'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
import Editor from '/@/components/Editor/index.vue'
const emit = defineEmits(['refresh'])
// 定义变量内容
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 form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
title: '',
content: '',
author: ''
})
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
],
author: [
{ required: true, message: '请输入作者', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (type: string = 'add', row?: any) => {
visible.value = true
operType.value = type
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.title = ''
form.content = ''
form.author = ''
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.title = row.title || ''
form.content = row.content || ''
form.author = row.author || ''
// 如果需要获取详情
if (row.id && !row.content) {
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.title = res.data.title || ''
form.content = res.data.content || ''
form.author = res.data.author || ''
}
}).finally(() => {
loading.value = false
})
}
}
})
}
// 学年列表(班级管理-学年接口)
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 onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
title: form.title,
content: form.content,
author: form.author
}
if (operType.value === 'add') {
await addObj(submitData)
useMessage().success('新增成功')
} else {
await editObj({
id: form.id,
...submitData
})
useMessage().success('编辑成功')
}
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || (operType.value === 'add' ? '新增失败' : '编辑失败'))
}
} finally {
loading.value = false
}
})
}
// 初始化:加载学年、学期
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,366 @@
<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="state.queryForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="state.queryForm.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="state.queryForm.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
v-model="state.queryForm.title"
placeholder="请输入标题"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input
v-model="state.queryForm.author"
placeholder="请输入作者"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain 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"><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"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.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 === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- 内容列特殊模板截断显示 -->
<template v-else-if="col.prop === 'content'" #default="scope">
<el-tooltip
v-if="scope.row.content"
:content="scope.row.content.replace(/<[^>]*>/g, '')"
placement="top"
effect="dark">
<div class="content-preview" v-html="getContentPreview(scope.row.content)"></div>
</el-tooltip>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="180" 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>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="TermActivity">
import { reactive, ref, computed, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/termactivity";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getDicts } from '/@/api/admin/dict'
import FormDialog from './form.vue'
import { Search, Document, List, Setting, Menu, Calendar, EditPen, User, Clock } from '@element-plus/icons-vue'
import RightToolbar from '/@/components/RightToolbar/index.vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
import { useRoute } from 'vue-router'
const route = useRoute()
const formDialogRef = ref()
const showSearch = ref(true)
const searchFormRef = ref()
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '学年', width: 120, icon: Calendar },
{ prop: 'schoolTerm', label: '学期', width: 100, icon: Calendar },
{ prop: 'title', label: '标题', minWidth: 200, icon: Document },
{ prop: 'content', label: '内容', minWidth: 300, icon: EditPen },
{ prop: 'author', label: '作者', width: 120, icon: User },
{ prop: 'createTime', label: '创建时间', width: 180, icon: Clock }
]
// 使用表格列控制
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange,
loadSavedConfig
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 立即加载配置,确保初始化时列可见
loadSavedConfig()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
schoolTerm: '',
title: '',
author: ''
},
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
return {
data: {
records: data?.records ?? [],
total: data?.total ?? 0
}
}
},
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true,
isPage: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 格式化学期
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') {
return '-'
}
const dictItem = schoolTermList.value.find(item => item.value == value)
return dictItem ? dictItem.label : value
}
// 获取内容预览去除HTML标签截断
const getContentPreview = (content: string) => {
if (!content) return '-'
const text = content.replace(/<[^>]*>/g, '')
return text.length > 50 ? text.substring(0, 50) + '...' : text
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
state.queryForm.schoolTerm = ''
state.queryForm.title = ''
state.queryForm.author = ''
getDataList()
}
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)
}
// 删除
const handleDelete = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm(`确定要删除"${row.title || '该项'}"吗?`)
await delObj([row.id])
useMessage().success('删除成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err?.msg || '删除失败')
}
}
}
// 获取学年列表
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 = []
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.content-preview {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
</style>