Files
school-developer/src/views/stuwork/classfeelog/index.vue
2026-03-02 14:29:10 +08:00

749 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 200px"
@change="handleDeptChange">
<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="classCode">
<el-select
v-model="state.queryForm.classCode"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="state.queryForm.type"
placeholder="请选择类型"
clearable
style="width: 200px">
<el-option
v-for="item in typeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
班费记录列表
</span>
<div class="header-actions">
<el-button
icon="Plus"
type="primary"
@click="formDialogRef.openDialog()">
新增
</el-button>
<el-button
icon="DataAnalysis"
type="warning"
class="ml10"
@click="handleSummary">
班费汇总
</el-button>
<el-button
icon="Download"
type="success"
class="ml10"
@click="handleExport">
导出
</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" round>
{{ formatSchoolTerm(scope.row.schoolTerm) }}
</el-tag>
</template>
<!-- 发生时间列特殊模板 -->
<template v-else-if="col.prop === 'operatTime'" #default="scope">
<span>{{ scope.row.operatTime || '-' }}</span>
</template>
<!-- 类型列特殊模板 -->
<template v-else-if="col.prop === 'type'" #default="scope">
<el-tag size="small" type="info" effect="plain" round>
{{ formatType(scope.row.type) }}
</el-tag>
</template>
<!-- 金额列特殊模板 -->
<template v-else-if="col.prop === 'money'" #default="scope">
<el-tag v-if="scope.row.money !== null && scope.row.money !== undefined" size="small" type="success" effect="plain" round>
¥{{ scope.row.money.toFixed(2) }}
</el-tag>
<span v-else>-</span>
</template>
<!-- 附件列特殊模板 -->
<template v-else-if="col.prop === 'attachment'" #default="scope">
<el-button
v-if="scope.row.attachment"
icon="Document"
link
type="primary"
size="small"
@click="handleViewAttachment(scope.row)">
查看
</el-button>
<span v-else>-</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="EditPen"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" :image-size="120">
<el-button type="primary" icon="FolderAdd" @click="formDialogRef.openDialog()">新增记录</el-button>
</el-empty>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增/编辑表单弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
<!-- 班费汇总弹窗 -->
<el-dialog
title="班费使用汇总"
v-model="summaryDialogVisible"
:width="900"
:close-on-click-modal="false"
draggable>
<!-- 查询条件 -->
<el-form :inline="true" class="summary-form">
<el-form-item label="学年">
<el-select
v-model="summaryQuery.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 180px">
<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="学期">
<el-select
v-model="summaryQuery.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 150px">
<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>
<el-button type="primary" icon="Search" @click="handleSummaryQuery">查询</el-button>
</el-form-item>
</el-form>
<!-- 汇总表格 -->
<el-table
:data="summaryData"
v-loading="summaryLoading"
stripe
border
max-height="400"
style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="deptName" label="学院" min-width="120" show-overflow-tooltip />
<el-table-column prop="classNo" label="班级" width="100" show-overflow-tooltip />
<el-table-column prop="totalIncome" label="总收入" width="100" align="right">
<template #default="scope">
<span style="color: #67c23a; font-weight: bold;">
{{ scope.row.totalIncome ? '¥' + scope.row.totalIncome.toFixed(2) : '¥0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="totalExpense" label="总支出" width="100" align="right">
<template #default="scope">
<span style="color: #f56c6c; font-weight: bold;">
{{ scope.row.totalExpense ? '¥' + scope.row.totalExpense.toFixed(2) : '¥0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="openingBalance" label="期初结余" width="100" align="right">
<template #default="scope">
<span style="color: #409eff; font-weight: bold;">
{{ scope.row.openingBalance ? '¥' + scope.row.openingBalance.toFixed(2) : '¥0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="totalBalance" label="总结余" width="100" align="right">
<template #default="scope">
<span :style="{ color: scope.row.totalBalance >= 0 ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
{{ scope.row.totalBalance ? '¥' + scope.row.totalBalance.toFixed(2) : '¥0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="recordCount" label="记录数" width="80" align="center" />
</el-table>
<!-- 汇总统计 -->
<div v-if="summaryData.length > 0" class="summary-total">
<el-row :gutter="20">
<el-col :span="6">
<div class="total-item">
<span class="label">班级总数</span>
<span class="value">{{ summaryData.length }}</span>
</div>
</el-col>
<el-col :span="6">
<div class="total-item">
<span class="label">总收入合计</span>
<span class="value income">¥{{ totalSummary.totalIncome.toFixed(2) }}</span>
</div>
</el-col>
<el-col :span="6">
<div class="total-item">
<span class="label">总支出合计</span>
<span class="value expense">¥{{ totalSummary.totalExpense.toFixed(2) }}</span>
</div>
</el-col>
<el-col :span="6">
<div class="total-item">
<span class="label">总结余合计</span>
<span class="value balance">¥{{ totalSummary.totalBalance.toFixed(2) }}</span>
</div>
</el-col>
</el-row>
</div>
<template #footer>
<el-button @click="summaryDialogVisible = false"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="ClassFeeLog">
import { reactive, ref, onMounted, computed } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj, exportExcel, getSummary } from "/@/api/stuwork/classfeelog";
import { getDeptList } from "/@/api/basic/basicclass";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getClassListByRole } from "/@/api/basic/basicclass";
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, OfficeBuilding, Grid, Collection, Money, User, Document, Setting, Menu, Search, FolderAdd, EditPen, DataAnalysis } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容
const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref()
const showSearch = ref(true)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const deptList = ref<any[]>([])
const classList = ref<any[]>([])
const typeList = ref<any[]>([])
// 班费汇总相关
const summaryDialogVisible = ref(false)
const summaryLoading = ref(false)
const summaryData = ref<any[]>([])
const summaryQuery = reactive({
schoolYear: '',
schoolTerm: ''
})
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '学年', icon: Calendar },
{ prop: 'schoolTerm', label: '学期', icon: Clock },
{ prop: 'deptName', label: '学院', icon: OfficeBuilding },
{ prop: 'classNo', label: '班级', icon: Grid },
{ prop: 'operatTime', label: '发生时间', icon: Calendar, width: 180 },
{ prop: 'type', label: '类型', icon: Collection },
{ prop: 'money', label: '金额', icon: Money },
{ prop: 'operator', label: '经办人', icon: User },
{ prop: 'purpose', label: '用途', icon: Document, minWidth: 150 },
{ prop: 'attachment', label: '附件', width: 100 }
]
// 使用表格列控制 Hook
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange
} = useTableColumnControl(tableColumns)
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
schoolYear: '',
schoolTerm: '',
deptCode: '',
classCode: '',
type: ''
},
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 formatType = (value: string) => {
if (!value) return '-'
const item = typeList.value.find((item: any) => item.value === value)
return item ? item.label : value
}
// 学院变化
const handleDeptChange = () => {
// 可以根据学院筛选班级,这里暂时不实现
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.schoolYear = ''
state.queryForm.schoolTerm = ''
state.queryForm.deptCode = ''
state.queryForm.classCode = ''
state.queryForm.type = ''
getDataList()
}
// 查看附件
const handleViewAttachment = (row: any) => {
if (row.attachment) {
const urls = typeof row.attachment === 'string' ? row.attachment.split(',') : [row.attachment]
urls.forEach((url: string) => {
if (url.trim()) {
window.open(url.trim(), '_blank')
}
})
}
}
// 导出
const handleExport = async () => {
try {
const res = await exportExcel(state.queryForm)
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
const fileName = `班费记录_${new Date().getTime()}.xlsx`
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err.msg || '导出失败')
}
}
// 编辑
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 = []
}
}
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
if (res.data && Array.isArray(res.data)) {
deptList.value = res.data
} else {
deptList.value = []
}
} catch (err) {
deptList.value = []
}
}
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
if (res.data && Array.isArray(res.data)) {
classList.value = res.data
} else {
classList.value = []
}
} catch (err) {
classList.value = []
}
}
// 获取类型字典
const getTypeDict = async () => {
try {
const res = await getDicts('class_fee_type')
if (res.data && Array.isArray(res.data)) {
typeList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
}))
} else {
typeList.value = []
}
} catch (err) {
typeList.value = []
}
}
// 打开班费汇总弹窗
const handleSummary = () => {
// 默认选择当前学年学期
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
// 计算当前学年
let currentYear: string
if (month >= 9) {
currentYear = `${year}-${year + 1}`
} else {
currentYear = `${year - 1}-${year}`
}
// 计算当前学期
const currentTerm = (month >= 9 || month <= 1) ? '1' : '2'
summaryQuery.schoolYear = currentYear
summaryQuery.schoolTerm = currentTerm
summaryDialogVisible.value = true
handleSummaryQuery()
}
// 查询班费汇总数据
const handleSummaryQuery = async () => {
if (!summaryQuery.schoolYear || !summaryQuery.schoolTerm) {
useMessage().warning('请选择学年 and 学期')
return
}
summaryLoading.value = true
try {
const res = await getSummary(summaryQuery.schoolYear, summaryQuery.schoolTerm)
if (res.data && Array.isArray(res.data)) {
summaryData.value = res.data
} else {
summaryData.value = []
}
} catch (err: any) {
useMessage().error(err.msg || '获取汇总数据失败')
summaryData.value = []
} finally {
summaryLoading.value = false
}
}
// 汇总统计数据
const totalSummary = computed(() => {
let totalIncome = 0
let totalExpense = 0
let totalBalance = 0
summaryData.value.forEach(item => {
totalIncome += item.totalIncome || 0
totalExpense += item.totalExpense || 0
totalBalance += item.totalBalance || 0
})
return {
totalIncome,
totalExpense,
totalBalance
}
})
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getDeptListData()
getClassListData()
getTypeDict()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.summary-form {
margin-bottom: 15px;
}
.summary-total {
margin-top: 15px;
padding: 15px;
background: #f5f7fa;
border-radius: 4px;
.total-item {
text-align: center;
.label {
color: #909399;
font-size: 14px;
}
.value {
font-size: 18px;
font-weight: bold;
margin-left: 5px;
&.income {
color: #67c23a;
}
&.expense {
color: #f56c6c;
}
&.balance {
color: #409eff;
}
}
}
}
</style>