恢复最新前端代码

This commit is contained in:
2026-08-26 09:52:26 +08:00
parent 12cac0fce3
commit f2ad0509c4
349 changed files with 42475 additions and 6 deletions
@@ -0,0 +1,531 @@
<template>
<div class="app-container joint-training-page">
<div class="list-title">联教联训管理</div>
<!-- ==================== 1. 查询条件区域 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="40px" class="search-form">
<el-row :gutter="6">
<el-col :span="4">
<el-form-item label="名称">
<el-input v-model="searchForm.mc" placeholder="请输入名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="2">
<div class="search-actions">
<el-button type="primary" @click="handleQuery">查询</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 操作区域 ==================== -->
<el-card shadow="never" class="upload-card">
<div class="upload-row">
<div class="upload-left">
<el-button type="primary" @click="handleAdd">新增联教联训</el-button>
<el-button icon="el-icon-download" @click="handleExport">导出 Excel</el-button>
<el-button icon="el-icon-download" @click="handleDownloadTemplate">联教联训数据文件模板下载</el-button>
<el-button icon="el-icon-folder-opened" @click="handleChooseFile">选择文件</el-button>
<span class="file-name" :class="{ 'has-file': selectedFile }">{{ fileName }}</span>
<input ref="fileInputRef" type="file" accept=".xls,.xlsx" style="display: none" @change="handleFileChange" />
<el-button type="primary" icon="el-icon-upload2" :loading="uploading" @click="handleUpload">上传数据</el-button>
</div>
</div>
</el-card>
<!-- ==================== 3. 数据表格 ==================== -->
<el-card shadow="never" class="table-card">
<div class="table-title">联教联训记录列表</div>
<el-table v-loading="tableLoading" :data="tableData" border stripe style="width: 100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="bh" label="编号" width="150" align="center" show-overflow-tooltip />
<el-table-column prop="mc" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="xz" label="性质" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="yj" label="依据" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="ksrq" label="开始日期" width="110" align="center" :formatter="fmtDateTime" />
<el-table-column prop="jsrq" label="结束日期" width="110" align="center" :formatter="fmtDateTime" />
<el-table-column prop="ts" label="天数" width="70" align="center" />
<el-table-column prop="jbksl" label="基本课时量" width="90" align="center" />
<el-table-column prop="kslx" label="课时类型" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="xmlx" label="项目类型" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="sm" label="说明" min-width="120" show-overflow-tooltip />
<el-table-column prop="bz" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="160" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" @click="handleEdit(row)">修改</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 4. 新增/修改对话框 ==================== -->
<el-dialog
:visible="dialogVisible"
:title="dialogTitle"
width="720px"
:close-on-click-modal="false"
@update:visible="val => dialogVisible = val"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" class="add-form">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="编号">
<el-input v-model="form.bh" placeholder="留空则自动生成" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称" prop="mc">
<el-input v-model="form.mc" placeholder="请输入名称" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="性质">
<el-input v-model="form.xz" placeholder="请输入性质" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="依据">
<el-input v-model="form.yj" placeholder="请输入依据" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="开始日期">
<el-date-picker v-model="form.ksrq" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择开始日期" clearable class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期">
<el-date-picker v-model="form.jsrq" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择结束日期" clearable class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="天数">
<el-input-number v-model="form.ts" :min="0" :precision="1" :step="1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="基本课时量">
<el-input-number v-model="form.jbksl" :min="0" :precision="1" :step="0.5" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="课时类型">
<el-input v-model="form.kslx" placeholder="请输入课时类型" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目类型">
<el-input v-model="form.xmlx" placeholder="请输入项目类型" clearable />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="说明">
<el-input v-model="form.sm" type="textarea" :rows="2" placeholder="请输入说明" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.bz" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 5. 详情对话框 ==================== -->
<el-dialog :visible="detailVisible" title="联教联训详情" width="640px" @update:visible="val => detailVisible = val">
<el-descriptions :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh }}</el-descriptions-item>
<el-descriptions-item label="名称">{{ detailData.mc }}</el-descriptions-item>
<el-descriptions-item label="性质">{{ fmtEmpty(detailData.xz) }}</el-descriptions-item>
<el-descriptions-item label="依据">{{ fmtEmpty(detailData.yj) }}</el-descriptions-item>
<el-descriptions-item label="开始日期">{{ fmtDateTime(null, null, detailData.ksrq) }}</el-descriptions-item>
<el-descriptions-item label="结束日期">{{ fmtDateTime(null, null, detailData.jsrq) }}</el-descriptions-item>
<el-descriptions-item label="天数">{{ fmtValue(detailData.ts) }}</el-descriptions-item>
<el-descriptions-item label="基本课时量">{{ fmtValue(detailData.jbksl) }}</el-descriptions-item>
<el-descriptions-item label="课时类型">{{ fmtEmpty(detailData.kslx) }}</el-descriptions-item>
<el-descriptions-item label="项目类型">{{ fmtEmpty(detailData.xmlx) }}</el-descriptions-item>
<el-descriptions-item label="说明" :span="2">{{ fmtEmpty(detailData.sm) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ fmtEmpty(detailData.bz) }}</el-descriptions-item>
</el-descriptions>
<div slot="footer">
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { saveAs } from 'file-saver'
import {
listJointTraining,
addJointTraining,
updateJointTraining,
deleteJointTraining,
getJointTraining,
importJointTraining,
exportJointTraining
} from '@/api/classHour/jointTraining'
export default {
name: 'JointTrainingIndex',
data() {
return {
// ==================== 1. 查询条件 ====================
searchForm: {
mc: ''
},
// ==================== 2. 文件操作 ====================
selectedFile: null,
uploading: false,
// ==================== 3. 表格数据 ====================
tableLoading: false,
tableData: [],
pageNum: 1,
pageSize: 10,
total: 0,
// ==================== 4. 新增/修改表单 ====================
dialogVisible: false,
dialogTitle: '新增联教联训',
saving: false,
form: this.createEmptyForm(),
rules: {
mc: [{ required: true, message: '请输入名称', trigger: 'blur' }]
},
// ==================== 5. 详情 ====================
detailVisible: false,
detailData: {}
}
},
computed: {
fileName() {
return (this.selectedFile && this.selectedFile.name) || '未选择任何文件'
}
},
mounted() {
this.fetchList()
},
methods: {
createEmptyForm() {
return {
bh: '',
mc: '',
xz: '',
yj: '',
ksrq: '',
jsrq: '',
ts: null,
jbksl: null,
kslx: '',
xmlx: '',
sm: '',
bz: ''
}
},
/** 把后端 LocalDateTime(含 T/毫秒)规范为表单可回显的格式 */
normalizeDateTime(val) {
if (!val) return ''
return String(val).replace('T', ' ').replace(/\.\d+$/, '').slice(0, 19)
},
// ==================== 查询 ====================
fetchList() {
this.tableLoading = true
const params = { pageNum: this.pageNum, pageSize: this.pageSize }
if (this.searchForm.mc && this.searchForm.mc.trim()) params.mc = this.searchForm.mc.trim()
listJointTraining(params).then(res => {
const data = (res && res.data) || {}
this.tableData = data.records || []
this.total = data.total || 0
this.tableLoading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.tableLoading = false
})
},
handleQuery() {
this.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
/** 日期时间显示(去掉 T、截断到分钟) */
fmtDateTime(row, column, cellValue) {
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
return String(cellValue).replace('T', ' ').slice(0, 16)
},
/** 空值显示为 '-' */
fmtEmpty(val) {
return val === null || val === undefined || val === '' ? '-' : val
},
/** 数值空值显示为 '-'(保留 0 */
fmtValue(val) {
return val === null || val === undefined ? '-' : val
},
// ==================== 新增 / 修改 ====================
handleAdd() {
this.form = this.createEmptyForm()
this.dialogTitle = '新增联教联训'
this.dialogVisible = true
},
handleEdit(row) {
getJointTraining(row.bh).then(res => {
const d = (res && res.data) || {}
this.form = {
bh: d.bh || '',
mc: d.mc || '',
xz: d.xz || '',
yj: d.yj || '',
ksrq: this.normalizeDateTime(d.ksrq),
jsrq: this.normalizeDateTime(d.jsrq),
ts: d.ts === null || d.ts === undefined ? null : d.ts,
jbksl: d.jbksl === null || d.jbksl === undefined ? null : d.jbksl,
kslx: d.kslx || '',
xmlx: d.xmlx || '',
sm: d.sm || '',
bz: d.bz || ''
}
this.dialogTitle = '修改联教联训'
this.dialogVisible = true
}).catch(() => {})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) {
this.$message.warning('请完善必填项后再提交')
return
}
const payload = this.buildPayload()
this.saving = true
const req = payload.bh ? updateJointTraining(payload) : addJointTraining(payload)
req.then(() => {
this.$message.success(this.dialogTitle === '修改联教联训' ? '修改成功' : '新增成功')
this.dialogVisible = false
this.fetchList()
}).catch(() => {}).finally(() => {
this.saving = false
})
})
},
/** 构建请求体,仅含实体非空字段(创建/修改时间为后端自动维护,不传) */
buildPayload() {
const f = this.form
const payload = {}
if (f.bh && f.bh.trim()) payload.bh = f.bh.trim()
if (f.mc && f.mc.trim()) payload.mc = f.mc.trim()
if (f.xz && f.xz.trim()) payload.xz = f.xz.trim()
if (f.yj && f.yj.trim()) payload.yj = f.yj.trim()
if (f.ksrq) payload.ksrq = f.ksrq
if (f.jsrq) payload.jsrq = f.jsrq
if (f.ts !== null && f.ts !== undefined) payload.ts = f.ts
if (f.jbksl !== null && f.jbksl !== undefined) payload.jbksl = f.jbksl
if (f.kslx && f.kslx.trim()) payload.kslx = f.kslx.trim()
if (f.xmlx && f.xmlx.trim()) payload.xmlx = f.xmlx.trim()
if (f.sm && f.sm.trim()) payload.sm = f.sm.trim()
if (f.bz && f.bz.trim()) payload.bz = f.bz.trim()
return payload
},
// ==================== 删除 ====================
handleDelete(row) {
this.$confirm(`确定要删除「${row.mc || row.bh || '该记录'}」吗?`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => deleteJointTraining(row.bh)).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch(() => {})
},
// ==================== 详情 ====================
handleDetail(row) {
getJointTraining(row.bh).then(res => {
this.detailData = (res && res.data) || {}
this.detailVisible = true
}).catch(() => {})
},
// ==================== 导出 ====================
handleExport() {
exportJointTraining().then(blob => {
saveAs(blob, '联教联训管理.xlsx')
this.$message.success('导出成功')
}).catch(() => {})
},
// ==================== 文件选择与导入 ====================
handleChooseFile() {
this.$refs.fileInputRef && this.$refs.fileInputRef.click()
},
handleFileChange(e) {
const input = e.target
this.selectedFile = (input.files && input.files[0]) || null
},
handleUpload() {
if (!this.selectedFile) {
this.$message.warning('请先选择文件')
return
}
this.uploading = true
importJointTraining(this.selectedFile).then(res => {
const count = res && res.data
if (count !== null && count !== undefined) {
this.$message.success(`导入成功,共 ${count} 条记录`)
} else {
this.$message.success((res && res.msg) || '导入成功')
}
this.selectedFile = null
this.$refs.fileInputRef && (this.$refs.fileInputRef.value = '')
this.pageNum = 1
this.fetchList()
}).catch(() => {}).finally(() => {
this.uploading = false
})
},
handleDownloadTemplate() {
// 后端暂未提供模板下载接口,仅作提示
this.$message.info('后端暂未提供该接口')
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.joint-training-page {
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
// ==================== 1. 查询条件区域 ====================
.search-card {
margin-bottom: 16px;
.search-form {
.w-full {
width: 100%;
}
.search-actions {
display: flex;
justify-content: flex-end;
align-items: center;
height: 40px;
}
}
}
// ==================== 2. 操作区域 ====================
.upload-card {
margin-bottom: 16px;
.upload-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
.upload-left {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
.file-name {
font-size: 12px;
color: #909399;
&.has-file {
color: #303133;
}
}
}
}
}
// ==================== 3. 数据表格 ====================
.table-card {
margin-bottom: 16px;
.table-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.text-danger {
color: #f56c6c;
}
}
// ==================== 4. 新增/修改表单 ====================
.add-form {
max-height: 62vh;
overflow-y: auto;
padding-right: 4px;
}
}
</style>
+679
View File
@@ -0,0 +1,679 @@
<template>
<div class="app-container plan-page">
<!-- ==================== Tab 切换 ==================== -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="课时费方案" name="fee" />
<el-tab-pane label="课时方案" name="coeff" />
</el-tabs>
<!-- ==================== 1. 查询与新增 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" class="search-form" @submit.native.prevent>
<el-form-item label="名称">
<el-input v-model="currentForm.mc" placeholder="请输入名称" clearable style="width: 220px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button type="primary" plain @click="handleAdd">新增{{ currentTitle }}</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ==================== 2. 数据表格 ==================== -->
<el-card shadow="never" class="table-card">
<!-- 课时费方案 -->
<el-table v-if="activeTab === 'fee'" v-loading="feeState.loading" :data="feeState.list" border stripe style="width: 100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="bh" label="编号" width="150" align="center" show-overflow-tooltip />
<el-table-column prop="mc" label="名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="mrbzksl" label="默认标准课时量" width="120" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.mrbzksl) }}</template>
</el-table-column>
<el-table-column prop="mrksfbz" label="默认课时费标准" width="120" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.mrksfbz) }}</template>
</el-table-column>
<el-table-column prop="mrclksfbz" label="默认超量课时费标准" width="150" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.mrclksfbz) }}</template>
</el-table-column>
<el-table-column prop="xgjxbzksfbz" label="相关教学补助课时费标准" width="180" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.xgjxbzksfbz) }}</template>
</el-table-column>
<el-table-column prop="jxyxbz" label="绩效优秀补助" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.jxyxbz) }}</template>
</el-table-column>
<el-table-column prop="jxlhbz" label="绩效良好补助" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.jxlhbz) }}</template>
</el-table-column>
<el-table-column prop="jxzdbz" label="绩效中等补助" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.jxzdbz) }}</template>
</el-table-column>
<el-table-column prop="jxjgbz" label="绩效及格补助" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.jxjgbz) }}</template>
</el-table-column>
<el-table-column prop="jxbjgbz" label="绩效不及格补助" width="120" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.jxbjgbz) }}</template>
</el-table-column>
<el-table-column prop="sm" label="说明" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="160" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" @click="handleEdit(row)">修改</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 课时方案 -->
<el-table v-else v-loading="coeffState.loading" :data="coeffState.list" border stripe style="width: 100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="bh" label="编号" width="150" align="center" show-overflow-tooltip />
<el-table-column prop="mc" label="名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="zjksxs" label="主讲课时系数" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.zjksxs) }}</template>
</el-table-column>
<el-table-column prop="fjksxs" label="辅讲课时系数" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.fjksxs) }}</template>
</el-table-column>
<el-table-column prop="mbxyrs" label="满班学员人数" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.mbxyrs) }}</template>
</el-table-column>
<el-table-column prop="dz10xybjxs" label="递增10学员班级系数" width="150" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.dz10xybjxs) }}</template>
</el-table-column>
<el-table-column prop="bjxssx" label="班级系数上限" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.bjxssx) }}</template>
</el-table-column>
<el-table-column prop="d78jksl" label="第78节课时量" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.d78jksl) }}</template>
</el-table-column>
<el-table-column prop="yjksl" label="夜间课时量" width="100" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.yjksl) }}</template>
</el-table-column>
<el-table-column prop="mrktjxffxs" label="默认课堂教法系数" width="130" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.mrktjxffxs) }}</template>
</el-table-column>
<el-table-column prop="hbxszl" label="合班系数增量" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.hbxszl) }}</template>
</el-table-column>
<el-table-column prop="hbxssx" label="合班系数上限" width="110" align="right">
<template slot-scope="{ row }">{{ fmtValue(row.hbxssx) }}</template>
</el-table-column>
<el-table-column prop="sm" label="说明" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" @click="handleEdit(row)">修改</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="currentState.pageNum"
:page-size="currentState.pageSize"
:total="currentState.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 3. 新增/修改对话框 ==================== -->
<el-dialog
:visible="dialogVisible"
:title="dialogTitle"
width="760px"
:close-on-click-modal="false"
@update:visible="val => dialogVisible = val"
>
<!-- 课时费方案表单 -->
<el-form v-if="activeTab === 'fee'" ref="feeFormRef" :model="feeForm" :rules="feeRules" label-width="160px" class="add-form">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="编号">
<el-input v-model="feeForm.bh" placeholder="留空则自动生成" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称" prop="mc">
<el-input v-model="feeForm.mc" placeholder="请输入名称" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="默认标准课时量" prop="mrbzksl">
<el-input-number v-model="feeForm.mrbzksl" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="默认课时费标准" prop="mrksfbz">
<el-input-number v-model="feeForm.mrksfbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="默认超量课时费标准" prop="mrclksfbz">
<el-input-number v-model="feeForm.mrclksfbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="相关教学补助课时费标准" prop="xgjxbzksfbz">
<el-input-number v-model="feeForm.xgjxbzksfbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="绩效优秀补助" prop="jxyxbz">
<el-input-number v-model="feeForm.jxyxbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="绩效良好补助" prop="jxlhbz">
<el-input-number v-model="feeForm.jxlhbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="绩效中等补助" prop="jxzdbz">
<el-input-number v-model="feeForm.jxzdbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="绩效及格补助" prop="jxjgbz">
<el-input-number v-model="feeForm.jxjgbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="绩效不及格补助" prop="jxbjgbz">
<el-input-number v-model="feeForm.jxbjgbz" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="说明">
<el-input v-model="feeForm.sm" type="textarea" :rows="2" placeholder="请输入说明" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="feeForm.bz" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<!-- 课时方案表单 -->
<el-form v-else ref="coeffFormRef" :model="coeffForm" :rules="coeffRules" label-width="160px" class="add-form">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="编号">
<el-input v-model="coeffForm.bh" placeholder="留空则自动生成" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称" prop="mc">
<el-input v-model="coeffForm.mc" placeholder="请输入名称" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="主讲课时系数">
<el-input-number v-model="coeffForm.zjksxs" :min="0" :precision="2" :step="0.1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="辅讲课时系数">
<el-input-number v-model="coeffForm.fjksxs" :min="0" :precision="2" :step="0.1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="满班学员人数">
<el-input-number v-model="coeffForm.mbxyrs" :min="0" :precision="0" :step="1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="递增10学员班级系数">
<el-input-number v-model="coeffForm.dz10xybjxs" :min="0" :precision="2" :step="0.1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="班级系数上限">
<el-input-number v-model="coeffForm.bjxssx" :min="0" :precision="2" :step="0.1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="第78节课时量">
<el-input-number v-model="coeffForm.d78jksl" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="夜间课时量">
<el-input-number v-model="coeffForm.yjksl" :min="0" :precision="2" :step="1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="默认课堂教学方法系数">
<el-input-number v-model="coeffForm.mrktjxffxs" :min="0" :precision="2" :step="0.1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="合班系数增量">
<el-input-number v-model="coeffForm.hbxszl" :min="0" :precision="2" :step="0.1" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合班系数上限">
<el-input-number v-model="coeffForm.hbxssx" :min="0" :precision="2" :step="0.1" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="说明">
<el-input v-model="coeffForm.sm" type="textarea" :rows="2" placeholder="请输入说明" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="coeffForm.bz" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 4. 详情对话框 ==================== -->
<el-dialog :visible="detailVisible" :title="`${currentTitle}详情`" width="680px" @update:visible="val => detailVisible = val">
<el-descriptions v-if="activeTab === 'fee'" :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh }}</el-descriptions-item>
<el-descriptions-item label="名称">{{ detailData.mc }}</el-descriptions-item>
<el-descriptions-item label="默认标准课时量">{{ fmtValue(detailData.mrbzksl) }}</el-descriptions-item>
<el-descriptions-item label="默认课时费标准">{{ fmtValue(detailData.mrksfbz) }}</el-descriptions-item>
<el-descriptions-item label="默认超量课时费标准">{{ fmtValue(detailData.mrclksfbz) }}</el-descriptions-item>
<el-descriptions-item label="相关教学补助课时费标准">{{ fmtValue(detailData.xgjxbzksfbz) }}</el-descriptions-item>
<el-descriptions-item label="绩效优秀补助">{{ fmtValue(detailData.jxyxbz) }}</el-descriptions-item>
<el-descriptions-item label="绩效良好补助">{{ fmtValue(detailData.jxlhbz) }}</el-descriptions-item>
<el-descriptions-item label="绩效中等补助">{{ fmtValue(detailData.jxzdbz) }}</el-descriptions-item>
<el-descriptions-item label="绩效及格补助">{{ fmtValue(detailData.jxjgbz) }}</el-descriptions-item>
<el-descriptions-item label="绩效不及格补助">{{ fmtValue(detailData.jxbjgbz) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ fmtEmpty(detailData.cjsj) }}</el-descriptions-item>
<el-descriptions-item label="说明" :span="2">{{ fmtEmpty(detailData.sm) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ fmtEmpty(detailData.bz) }}</el-descriptions-item>
</el-descriptions>
<el-descriptions v-else :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh }}</el-descriptions-item>
<el-descriptions-item label="名称">{{ detailData.mc }}</el-descriptions-item>
<el-descriptions-item label="主讲课时系数">{{ fmtValue(detailData.zjksxs) }}</el-descriptions-item>
<el-descriptions-item label="辅讲课时系数">{{ fmtValue(detailData.fjksxs) }}</el-descriptions-item>
<el-descriptions-item label="满班学员人数">{{ fmtValue(detailData.mbxyrs) }}</el-descriptions-item>
<el-descriptions-item label="递增10学员班级系数">{{ fmtValue(detailData.dz10xybjxs) }}</el-descriptions-item>
<el-descriptions-item label="班级系数上限">{{ fmtValue(detailData.bjxssx) }}</el-descriptions-item>
<el-descriptions-item label="第78节课时量">{{ fmtValue(detailData.d78jksl) }}</el-descriptions-item>
<el-descriptions-item label="夜间课时量">{{ fmtValue(detailData.yjksl) }}</el-descriptions-item>
<el-descriptions-item label="默认课堂教学方法系数">{{ fmtValue(detailData.mrktjxffxs) }}</el-descriptions-item>
<el-descriptions-item label="合班系数增量">{{ fmtValue(detailData.hbxszl) }}</el-descriptions-item>
<el-descriptions-item label="合班系数上限">{{ fmtValue(detailData.hbxssx) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ fmtEmpty(detailData.cjsj) }}</el-descriptions-item>
<el-descriptions-item label="说明" :span="2">{{ fmtEmpty(detailData.sm) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ fmtEmpty(detailData.bz) }}</el-descriptions-item>
</el-descriptions>
<div slot="footer">
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listFeeStandard,
getFeeStandard,
addFeeStandard,
updateFeeStandard,
deleteFeeStandard,
listCoefficientPlan,
getCoefficientPlan,
addCoefficientPlan,
updateCoefficientPlan
} from '@/api/classHour/plan'
export default {
name: 'ClassHourPlan',
data() {
return {
// ==================== Tab ====================
activeTab: 'fee',
// ==================== 查询条件 ====================
feeSearch: { mc: '' },
coeffSearch: { mc: '' },
// ==================== 列表状态 ====================
feeState: this.createEmptyState(),
coeffState: this.createEmptyState(),
// ==================== 新增/修改 ====================
dialogVisible: false,
dialogTitle: '',
saving: false,
feeForm: this.createEmptyFeeForm(),
coeffForm: this.createEmptyCoeffForm(),
feeRules: {
mc: [{ required: true, message: '请输入名称', trigger: 'blur' }],
mrbzksl: [{ required: true, message: '请输入默认标准课时量', trigger: 'blur' }],
mrksfbz: [{ required: true, message: '请输入默认课时费标准', trigger: 'blur' }],
mrclksfbz: [{ required: true, message: '请输入默认超量课时费标准', trigger: 'blur' }],
xgjxbzksfbz: [{ required: true, message: '请输入相关教学补助课时费标准', trigger: 'blur' }],
jxyxbz: [{ required: true, message: '请输入绩效优秀补助', trigger: 'blur' }],
jxlhbz: [{ required: true, message: '请输入绩效良好补助', trigger: 'blur' }],
jxzdbz: [{ required: true, message: '请输入绩效中等补助', trigger: 'blur' }],
jxjgbz: [{ required: true, message: '请输入绩效及格补助', trigger: 'blur' }],
jxbjgbz: [{ required: true, message: '请输入绩效不及格补助', trigger: 'blur' }]
},
coeffRules: {
mc: [{ required: true, message: '请输入名称', trigger: 'blur' }]
},
// ==================== 详情 ====================
detailVisible: false,
detailData: {}
}
},
computed: {
currentForm() {
return this.activeTab === 'fee' ? this.feeSearch : this.coeffSearch
},
currentState() {
return this.activeTab === 'fee' ? this.feeState : this.coeffState
},
currentTitle() {
return this.activeTab === 'fee' ? '课时费方案' : '课时方案'
}
},
mounted() {
this.fetchList()
},
methods: {
createEmptyState() {
return { list: [], loading: false, pageNum: 1, pageSize: 10, total: 0 }
},
createEmptyFeeForm() {
return {
bh: '',
mc: '',
mrbzksl: null,
mrksfbz: null,
mrclksfbz: null,
xgjxbzksfbz: null,
jxyxbz: null,
jxlhbz: null,
jxzdbz: null,
jxjgbz: null,
jxbjgbz: null,
sm: '',
bz: ''
}
},
createEmptyCoeffForm() {
return {
bh: '',
mc: '',
zjksxs: null,
fjksxs: null,
mbxyrs: null,
dz10xybjxs: null,
bjxssx: null,
d78jksl: null,
yjksl: null,
mrktjxffxs: null,
hbxszl: null,
hbxssx: null,
sm: '',
bz: ''
}
},
// ==================== 查询 ====================
fetchList() {
const state = this.currentState
const params = { pageNum: state.pageNum, pageSize: state.pageSize }
if (this.currentForm.mc && this.currentForm.mc.trim()) params.mc = this.currentForm.mc.trim()
state.loading = true
const req = this.activeTab === 'fee' ? listFeeStandard(params) : listCoefficientPlan(params)
req.then(res => {
const data = (res && res.data) || {}
state.list = data.records || []
state.total = data.total || 0
}).catch(() => {
state.list = []
state.total = 0
}).finally(() => {
state.loading = false
})
},
handleTabClick() {
this.currentState.pageNum = 1
this.fetchList()
},
handleQuery() {
this.currentState.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.currentState.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.currentState.pageSize = size
this.currentState.pageNum = 1
this.fetchList()
},
// ==================== 新增 / 修改 ====================
handleAdd() {
this.feeForm = this.createEmptyFeeForm()
this.coeffForm = this.createEmptyCoeffForm()
this.dialogTitle = `新增${this.currentTitle}`
this.dialogVisible = true
},
handleEdit(row) {
const req = this.activeTab === 'fee' ? getFeeStandard(row.bh) : getCoefficientPlan(row.bh)
req.then(res => {
const d = (res && res.data) || {}
if (this.activeTab === 'fee') {
this.feeForm = {
bh: d.bh || '',
mc: d.mc || '',
mrbzksl: this.toNull(d.mrbzksl),
mrksfbz: this.toNull(d.mrksfbz),
mrclksfbz: this.toNull(d.mrclksfbz),
xgjxbzksfbz: this.toNull(d.xgjxbzksfbz),
jxyxbz: this.toNull(d.jxyxbz),
jxlhbz: this.toNull(d.jxlhbz),
jxzdbz: this.toNull(d.jxzdbz),
jxjgbz: this.toNull(d.jxjgbz),
jxbjgbz: this.toNull(d.jxbjgbz),
sm: d.sm || '',
bz: d.bz || ''
}
} else {
this.coeffForm = {
bh: d.bh || '',
mc: d.mc || '',
zjksxs: this.toNull(d.zjksxs),
fjksxs: this.toNull(d.fjksxs),
mbxyrs: this.toNull(d.mbxyrs),
dz10xybjxs: this.toNull(d.dz10xybjxs),
bjxssx: this.toNull(d.bjxssx),
d78jksl: this.toNull(d.d78jksl),
yjksl: this.toNull(d.yjksl),
mrktjxffxs: this.toNull(d.mrktjxffxs),
hbxszl: this.toNull(d.hbxszl),
hbxssx: this.toNull(d.hbxssx),
sm: d.sm || '',
bz: d.bz || ''
}
}
this.dialogTitle = `修改${this.currentTitle}`
this.dialogVisible = true
}).catch(() => {})
},
handleSubmit() {
const isFee = this.activeTab === 'fee'
const formRef = isFee ? this.$refs.feeFormRef : this.$refs.coeffFormRef
formRef.validate(valid => {
if (!valid) {
this.$message.warning('请完善必填项后再提交')
return
}
const payload = isFee ? this.buildFeePayload() : this.buildCoeffPayload()
this.saving = true
const req = payload.bh
? (isFee ? updateFeeStandard(payload) : updateCoefficientPlan(payload))
: (isFee ? addFeeStandard(payload) : addCoefficientPlan(payload))
req.then(() => {
this.$message.success(this.dialogTitle.indexOf('修改') === 0 ? '修改成功' : '新增成功')
this.dialogVisible = false
this.fetchList()
}).catch(() => {}).finally(() => {
this.saving = false
})
})
},
toNull(val) {
return val === null || val === undefined ? null : val
},
/** 课时费方案请求体:仅含实体非空字段(创建/修改时间后端自动维护) */
buildFeePayload() {
const f = this.feeForm
const p = {}
if (f.bh && f.bh.trim()) p.bh = f.bh.trim()
if (f.mc && f.mc.trim()) p.mc = f.mc.trim()
;['mrbzksl', 'mrksfbz', 'mrclksfbz', 'xgjxbzksfbz', 'jxyxbz', 'jxlhbz', 'jxzdbz', 'jxjgbz', 'jxbjgbz'].forEach(k => {
if (f[k] !== null && f[k] !== undefined) p[k] = f[k]
})
if (f.sm && f.sm.trim()) p.sm = f.sm.trim()
if (f.bz && f.bz.trim()) p.bz = f.bz.trim()
return p
},
/** 课时方案请求体:仅含实体非空字段 */
buildCoeffPayload() {
const f = this.coeffForm
const p = {}
if (f.bh && f.bh.trim()) p.bh = f.bh.trim()
if (f.mc && f.mc.trim()) p.mc = f.mc.trim()
;['zjksxs', 'fjksxs', 'mbxyrs', 'dz10xybjxs', 'bjxssx', 'd78jksl', 'yjksl', 'mrktjxffxs', 'hbxszl', 'hbxssx'].forEach(k => {
if (f[k] !== null && f[k] !== undefined) p[k] = f[k]
})
if (f.sm && f.sm.trim()) p.sm = f.sm.trim()
if (f.bz && f.bz.trim()) p.bz = f.bz.trim()
return p
},
// ==================== 删除(仅课时费方案有删除接口) ====================
handleDelete(row) {
this.$confirm(`确定要删除「${row.mc || row.bh || '该方案'}」吗?`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => deleteFeeStandard(row.bh)).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch(() => {})
},
// ==================== 详情 ====================
handleDetail(row) {
const req = this.activeTab === 'fee' ? getFeeStandard(row.bh) : getCoefficientPlan(row.bh)
req.then(res => {
this.detailData = (res && res.data) || {}
this.detailVisible = true
}).catch(() => {})
},
/** 数值空值显示为 '-'(保留 0 */
fmtValue(val) {
return val === null || val === undefined ? '-' : val
},
/** 空值显示为 '-' */
fmtEmpty(val) {
return val === null || val === undefined || val === '' ? '-' : val
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.plan-page {
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
// ==================== 1. 查询区域 ====================
.search-card {
margin-bottom: 16px;
.search-form {
.el-form-item {
margin-bottom: 0;
}
}
}
// ==================== 2. 数据表格 ====================
.table-card {
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.text-danger {
color: #f56c6c;
}
}
// ==================== 3. 新增/修改表单 ====================
.add-form {
max-height: 62vh;
overflow-y: auto;
padding-right: 4px;
.w-full {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,299 @@
<template>
<div class="app-container subsidy-page">
<div class="list-title">课时补助核算</div>
<!-- ==================== Tab 切换 ==================== -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="课时统计" name="hour" />
<el-tab-pane label="课时费统计" name="fee" />
</el-tabs>
<!-- ==================== 1. 查询条件 ==================== -->
<!-- 课时统计 -->
<el-card v-if="activeTab === 'hour'" shadow="never" class="search-card">
<el-form :model="hourForm" label-width="100px" class="search-form" @submit.native.prevent>
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="学期年度">
<el-input v-model="hourForm.nd" placeholder="如 2026" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="学期序号">
<el-input v-model="hourForm.xq" placeholder="如 1" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="教员姓名">
<el-input v-model="hourForm.jyxm" placeholder="请输入教员姓名" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6" class="search-actions-col">
<div class="search-actions">
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button type="primary" plain @click="handleExport">导出 Excel</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 课时费统计 -->
<el-card v-else shadow="never" class="search-card">
<el-form :model="feeForm" label-width="100px" class="search-form" @submit.native.prevent>
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="学期年度">
<el-input v-model="feeForm.nd" placeholder="如 2026" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="学期序号">
<el-input v-model="feeForm.xq" placeholder="如 1" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="教员姓名">
<el-input v-model="feeForm.jyxm" placeholder="请输入教员姓名" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="标准编号">
<el-input v-model="feeForm.ksfbzbh" placeholder="课时费标准编号" clearable />
</el-form-item>
</el-col>
<el-col :span="24" class="search-actions-col">
<div class="search-actions">
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button type="primary" plain @click="handleExport">导出 Excel</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 数据表格 ==================== -->
<el-card shadow="never" class="table-card">
<div class="table-title">{{ activeTab === 'hour' ? '课时统计列表' : '课时费统计列表' }}</div>
<el-table v-loading="currentState.loading" :data="currentState.list" border stripe style="width: 100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="jysdh" label="教研室代号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="jysmc" label="教研室名称" width="120" show-overflow-tooltip />
<el-table-column prop="jybh" label="教员编号" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="jyxm" label="教员姓名" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="zc" label="职称" width="90" align="center" show-overflow-tooltip />
<el-table-column prop="nf" label="年份" width="70" align="center" />
<el-table-column prop="sbn" label="上半年" width="90" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.sbn) }}</template>
</el-table-column>
<el-table-column prop="xbn" label="下半年" width="90" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.xbn) }}</template>
</el-table-column>
<el-table-column prop="zj" label="总计" width="90" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.zj) }}</template>
</el-table-column>
<el-table-column prop="bzksl" label="标准课时量" width="100" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.bzksl) }}</template>
</el-table-column>
<el-table-column prop="cks" label="超课时" width="90" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.cks) }}</template>
</el-table-column>
<el-table-column prop="jsgs" label="计算过程" min-width="140" show-overflow-tooltip />
<el-table-column prop="jg" label="结果" width="90" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.jg) }}</template>
</el-table-column>
<el-table-column prop="ksfbzmc" label="课时费标准" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="ksfbz" label="课时费单价" width="100" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.ksfbz) }}</template>
</el-table-column>
<el-table-column prop="clksfbz" label="超量课时费单价" width="110" align="right" header-align="center">
<template slot-scope="{ row }">{{ fmtValue(row.clksfbz) }}</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="currentState.pageNum"
:page-size="currentState.pageSize"
:total="currentState.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
</div>
</template>
<script>
import { saveAs } from 'file-saver'
import {
listHourStatistics,
exportHourStatistics,
listFeeStatistics,
exportFeeStatistics
} from '@/api/classHour/hourStat'
export default {
name: 'SubsidyIndex',
data() {
return {
// ==================== Tab ====================
activeTab: 'hour',
// ==================== 课时统计查询条件 ====================
hourForm: {
nd: '',
xq: '',
jyxm: ''
},
// ==================== 课时费统计查询条件 ====================
feeForm: {
nd: '',
xq: '',
jyxm: '',
ksfbzbh: ''
},
// ==================== 各 Tab 列表状态 ====================
hourState: this.createEmptyState(),
feeState: this.createEmptyState()
}
},
computed: {
currentForm() {
return this.activeTab === 'hour' ? this.hourForm : this.feeForm
},
currentState() {
return this.activeTab === 'hour' ? this.hourState : this.feeState
}
},
mounted() {
this.fetchList()
},
methods: {
createEmptyState() {
return { list: [], loading: false, pageNum: 1, pageSize: 20, total: 0 }
},
/** 构建查询参数,仅传非空字段(与后端 Query 字段一致) */
buildParams(form) {
const params = {}
if (form.nd && form.nd.trim()) params.nd = form.nd.trim()
if (form.xq && form.xq.trim()) params.xq = form.xq.trim()
if (form.jyxm && form.jyxm.trim()) params.jyxm = form.jyxm.trim()
if (form.ksfbzbh && form.ksfbzbh.trim()) params.ksfbzbh = form.ksfbzbh.trim()
return params
},
// ==================== 查询 ====================
fetchList() {
const state = this.currentState
const form = this.currentForm
const params = Object.assign(
{ pageNum: state.pageNum, pageSize: state.pageSize },
this.buildParams(form)
)
state.loading = true
const req = this.activeTab === 'hour' ? listHourStatistics(params) : listFeeStatistics(params)
req.then(res => {
const data = (res && res.data) || {}
state.list = data.records || []
state.total = data.total || 0
}).catch(() => {
state.list = []
state.total = 0
}).finally(() => {
state.loading = false
})
},
handleTabClick() {
this.currentState.pageNum = 1
this.fetchList()
},
handleQuery() {
this.currentState.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.currentState.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.currentState.pageSize = size
this.currentState.pageNum = 1
this.fetchList()
},
// ==================== 导出 ====================
handleExport() {
const clean = this.buildParams(this.currentForm)
const isHour = this.activeTab === 'hour'
const req = isHour ? exportHourStatistics(clean) : exportFeeStatistics(clean)
const fileName = isHour ? '课时统计.xlsx' : '课时费统计.xlsx'
req.then(blob => {
saveAs(blob, fileName)
this.$message.success('导出成功')
}).catch(() => {})
},
/** 数值空值显示为 '-'(保留 0 */
fmtValue(val) {
return val === null || val === undefined ? '-' : val
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.subsidy-page {
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
// ==================== 1. 查询条件区域 ====================
.search-card {
margin-bottom: 16px;
.search-form {
.search-actions-col {
display: flex;
align-items: center;
}
.search-actions {
display: flex;
gap: 12px;
}
}
}
// ==================== 2. 数据表格 ====================
.table-card {
.table-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
}
}
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
const animationDuration = 6000
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
top: 10,
left: '2%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: [{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisTick: {
alignWithLabel: true
}
}],
yAxis: [{
type: 'value',
axisTick: {
show: false
}
}],
series: [{
name: 'pageA',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [79, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageB',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [80, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageC',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [30, 52, 200, 334, 390, 330, 220],
animationDuration
}]
})
}
}
}
</script>
+135
View File
@@ -0,0 +1,135 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '350px'
},
autoResize: {
type: Boolean,
default: true
},
chartData: {
type: Object,
required: true
}
},
data() {
return {
chart: null
}
},
watch: {
chartData: {
deep: true,
handler(val) {
this.setOptions(val)
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
},
setOptions({ expectedData, actualData } = {}) {
this.chart.setOption({
xAxis: {
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 10,
right: 10,
bottom: 20,
top: 30,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: ['expected', 'actual']
},
series: [{
name: 'expected', itemStyle: {
normal: {
color: '#FF005A',
lineStyle: {
color: '#FF005A',
width: 2
}
}
},
smooth: true,
type: 'line',
data: expectedData,
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: 'actual',
smooth: true,
type: 'line',
itemStyle: {
normal: {
color: '#3888fa',
lineStyle: {
color: '#3888fa',
width: 2
},
areaStyle: {
color: '#f3f8ff'
}
}
},
data: actualData,
animationDuration: 2800,
animationEasing: 'quadraticOut'
}]
})
}
}
}
</script>
+181
View File
@@ -0,0 +1,181 @@
<template>
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('newVisitis')">
<div class="card-panel-icon-wrapper icon-people">
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
访客
</div>
<count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('messages')">
<div class="card-panel-icon-wrapper icon-message">
<svg-icon icon-class="message" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
消息
</div>
<count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('purchases')">
<div class="card-panel-icon-wrapper icon-money">
<svg-icon icon-class="money" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
金额
</div>
<count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('shoppings')">
<div class="card-panel-icon-wrapper icon-shopping">
<svg-icon icon-class="shopping" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
订单
</div>
<count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num" />
</div>
</div>
</el-col>
</el-row>
</template>
<script>
import CountTo from 'vue-count-to'
export default {
components: {
CountTo
},
methods: {
handleSetLineChartData(type) {
this.$emit('handleSetLineChartData', type)
}
}
}
</script>
<style lang="scss" scoped>
.panel-group {
margin-top: 18px;
.card-panel-col {
margin-bottom: 32px;
}
.card-panel {
height: 108px;
cursor: pointer;
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
border-color: rgba(0, 0, 0, .05);
&:hover {
.card-panel-icon-wrapper {
color: #fff;
}
.icon-people {
background: #40c9c6;
}
.icon-message {
background: #36a3f7;
}
.icon-money {
background: #f4516c;
}
.icon-shopping {
background: #34bfa3
}
}
.icon-people {
color: #40c9c6;
}
.icon-message {
color: #36a3f7;
}
.icon-money {
color: #f4516c;
}
.icon-shopping {
color: #34bfa3
}
.card-panel-icon-wrapper {
float: left;
margin: 14px 0 0 14px;
padding: 16px;
transition: all 0.38s ease-out;
border-radius: 6px;
}
.card-panel-icon {
float: left;
font-size: 48px;
}
.card-panel-description {
float: right;
font-weight: bold;
margin: 26px;
margin-left: 0px;
.card-panel-text {
line-height: 18px;
color: rgba(0, 0, 0, 0.45);
font-size: 16px;
margin-bottom: 12px;
}
.card-panel-num {
font-size: 20px;
}
}
}
}
@media (max-width:550px) {
.card-panel-description {
display: none;
}
.card-panel-icon-wrapper {
float: none !important;
width: 100%;
height: 100%;
margin: 0 !important;
.svg-icon {
display: block;
margin: 14px auto !important;
float: none !important;
}
}
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
left: 'center',
bottom: '10',
data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts']
},
series: [
{
name: 'WEEKLY WRITE ARTICLES',
type: 'pie',
roseType: 'radius',
radius: [15, 95],
center: ['50%', '38%'],
data: [
{ value: 320, name: 'Industries' },
{ value: 240, name: 'Technology' },
{ value: 149, name: 'Forex' },
{ value: 100, name: 'Gold' },
{ value: 59, name: 'Forecasts' }
],
animationEasing: 'cubicInOut',
animationDuration: 2600
}
]
})
}
}
}
</script>
@@ -0,0 +1,116 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
const animationDuration = 3000
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
radar: {
radius: '66%',
center: ['50%', '42%'],
splitNumber: 8,
splitArea: {
areaStyle: {
color: 'rgba(127,95,132,.3)',
opacity: 1,
shadowBlur: 45,
shadowColor: 'rgba(0,0,0,.5)',
shadowOffsetX: 0,
shadowOffsetY: 15
}
},
indicator: [
{ name: 'Sales', max: 10000 },
{ name: 'Administration', max: 20000 },
{ name: 'Information Techology', max: 20000 },
{ name: 'Customer Support', max: 20000 },
{ name: 'Development', max: 20000 },
{ name: 'Marketing', max: 20000 }
]
},
legend: {
left: 'center',
bottom: '10',
data: ['Allocated Budget', 'Expected Spending', 'Actual Spending']
},
series: [{
type: 'radar',
symbolSize: 0,
areaStyle: {
normal: {
shadowBlur: 13,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
opacity: 1
}
},
data: [
{
value: [5000, 7000, 12000, 11000, 15000, 14000],
name: 'Allocated Budget'
},
{
value: [4000, 9000, 15000, 15000, 13000, 11000],
name: 'Expected Spending'
},
{
value: [5500, 11000, 12000, 15000, 12000, 12000],
name: 'Actual Spending'
}
],
animationDuration: animationDuration
}]
})
}
}
}
</script>
@@ -0,0 +1,56 @@
import { debounce } from '@/utils'
export default {
data() {
return {
$_sidebarElm: null,
$_resizeHandler: null
}
},
mounted() {
this.initListener()
},
activated() {
if (!this.$_resizeHandler) {
// avoid duplication init
this.initListener()
}
// when keep-alive chart activated, auto resize
this.resize()
},
beforeDestroy() {
this.destroyListener()
},
deactivated() {
this.destroyListener()
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.$_resizeHandler()
}
},
initListener() {
this.$_resizeHandler = debounce(() => {
this.resize()
}, 100)
window.addEventListener('resize', this.$_resizeHandler)
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
},
destroyListener() {
window.removeEventListener('resize', this.$_resizeHandler)
this.$_resizeHandler = null
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
},
resize() {
const { chart } = this
chart && chart.resize()
}
}
}
+88
View File
@@ -0,0 +1,88 @@
<template>
<div class="errPage-container">
<el-button icon="arrow-left" class="pan-back-btn" @click="back">
返回
</el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">
401错误!
</h1>
<h2>您没有访问权限</h2>
<h6>对不起您没有访问权限请不要进行非法操作您可以返回主页面</h6>
<ul class="list-unstyled">
<li class="link-type">
<router-link to="/">
回首页
</router-link>
</li>
</ul>
</el-col>
<el-col :span="12">
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
</el-col>
</el-row>
</div>
</template>
<script>
import errGif from '@/assets/401_images/401.gif'
export default {
name: 'Page401',
data() {
return {
errGif: errGif + '?' + +new Date()
}
},
methods: {
back() {
if (this.$route.query.noGoBack) {
this.$router.push({ path: '/' })
} else {
this.$router.go(-1)
}
}
}
}
</script>
<style lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none!important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
width: 100%;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
+233
View File
@@ -0,0 +1,233 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">
404错误!
</div>
<div class="bullshit__headline">
{{ message }}
</div>
<div class="bullshit__info">
对不起您正在寻找的页面不存在尝试检查URL的错误然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容
</div>
<router-link to="/" class="bullshit__return-home">
返回首页
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message() {
return '找不到网页!'
}
}
}
</script>
<style lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>
+326
View File
@@ -0,0 +1,326 @@
<template>
<div class="app-container home">
<!-- 通知公告 -->
<el-row :gutter="20" class="bottom-row">
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<el-card class="module-card notice-card" shadow="hover">
<div slot="header" class="card-header">
<span><i class="el-icon-bell card-icon"></i> 通知公告</span>
<el-button type="text" class="more-btn" @click="handleMore('notice')">更多 <i class="el-icon-arrow-right"></i></el-button>
</div>
<div class="card-body notice-body" v-loading="noticeLoading">
<div v-if="noticeList.length === 0" class="empty-state mini-empty">
<i class="el-icon-inbox empty-icon"></i>
<p>暂无公告</p>
</div>
<div v-else class="notice-list">
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" @click="handleViewNotice(item)">
<div class="notice-main">
<el-tag size="mini" :type="item.noticeType === '1' ? 'warning' : 'success'" class="notice-tag">
{{ item.noticeType === '1' ? '通知' : '公告' }}
</el-tag>
<span class="notice-title">{{ item.noticeTitle }}</span>
</div>
<div class="notice-meta">
<span>{{ item.createBy || '系统' }}</span>
<span>{{ item.createTime }}</span>
</div>
</div>
</div>
</div>
<div class="card-footer">
<el-pagination
@size-change="handleNoticeSizeChange"
@current-change="handleNoticeCurrentChange"
:current-page="noticePage.currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="noticePage.pageSize"
:total="noticePage.total"
layout="total, prev, pager, next"
small
/>
</div>
</el-card>
</el-col>
</el-row>
<notice-detail-view ref="noticeViewRef" />
</div>
</template>
<script>
import { listNotice } from "@/api/system/notice"
import NoticeDetailView from "@/layout/components/HeaderNotice/DetailView"
export default {
name: "Index",
components: { NoticeDetailView },
data() {
return {
version: "3.9.2",
noticeList: [],
noticeLoading: false,
noticePage: {
currentPage: 1,
pageSize: 10,
total: 0
}
}
},
mounted() {
this.loadNoticeList()
},
methods: {
goTarget(href) {
window.open(href, "_blank")
},
handleMore(type) {
const titleMap = {
notice: '通知公告'
}
const targetTitle = titleMap[type]
if (targetTitle) {
const routes = this.$store.getters.sidebarRouters || []
const findRouteByTitle = (routeList, parentPath = '') => {
for (const route of routeList) {
const fullPath = parentPath ? parentPath + '/' + route.path : route.path
if (route.meta && route.meta.title === targetTitle) {
return fullPath
}
if (route.children && route.children.length) {
const found = findRouteByTitle(route.children, fullPath)
if (found) return found
}
}
return null
}
const targetPath = findRouteByTitle(routes)
if (targetPath) {
this.$router.push(targetPath)
return
}
}
this.$message.info("更多功能暂未开放")
},
loadNoticeList() {
this.noticeLoading = true
listNotice({
pageNum: this.noticePage.currentPage,
pageSize: this.noticePage.pageSize
}).then(response => {
this.noticeList = response.rows || []
this.noticePage.total = response.total || 0
this.noticeLoading = false
}).catch(() => {
this.noticeLoading = false
})
},
handleViewNotice(item) {
this.$refs.noticeViewRef.open(item)
},
handleNoticeSizeChange(val) {
this.noticePage.pageSize = val
this.noticePage.currentPage = 1
this.loadNoticeList()
},
handleNoticeCurrentChange(val) {
this.noticePage.currentPage = val
this.loadNoticeList()
}
}
}
</script>
<style scoped lang="scss">
.home {
background: linear-gradient(135deg, #f5f7fa 0%, #eef0f8 50%, #f5f7fa 100%);
min-height: calc(100vh - 84px);
padding: 20px;
position: relative;
.bottom-row {
margin-bottom: 20px;
}
.module-card {
height: 420px;
width: 100%;
display: flex;
flex-direction: column;
border-radius: 10px;
border: none;
transition: all 0.3s ease;
background: linear-gradient(180deg, #ffffff 0%, #f8f9fd 100%);
box-shadow: 0 4px 20px rgba(0, 135, 90, 0.06);
&:hover {
box-shadow: 0 8px 30px rgba(0, 135, 90, 0.12);
}
::v-deep .el-card__header {
padding: 14px 20px;
border-bottom: 1px solid rgba(0, 135, 90, 0.08);
background: linear-gradient(135deg, var(--edu-green-dark) 0%, var(--edu-green-primary) 100%);
border-radius: 10px 10px 0 0;
flex-shrink: 0;
}
::v-deep .el-card__body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 15px;
color: #ffffff;
.card-icon {
margin-right: 6px;
color: var(--edu-green-light);
font-size: 16px;
}
.more-btn {
padding: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.75);
&:hover {
color: #ffffff;
}
i {
font-size: 12px;
}
}
}
.card-body {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
padding: 15px 20px;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
.card-footer {
flex-shrink: 0;
padding: 10px 20px 15px;
text-align: right;
border-top: 1px solid rgba(0, 135, 90, 0.06);
background: linear-gradient(180deg, #ffffff 0%, #f4f6fb 100%);
overflow-x: auto;
::v-deep .el-pagination__total,
::v-deep .el-pagination__jump,
::v-deep .el-pager li {
color: #5a5f7a;
}
::v-deep .el-pager li.active,
::v-deep .el-pager li:hover {
color: var(--edu-green-dark);
}
}
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 160px;
color: #8a90ad;
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
color: #b8bed8;
}
p {
margin: 0;
font-size: 14px;
}
}
.notice-list {
.notice-item {
padding: 14px 0;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
transition: all 0.2s;
&:last-child {
border-bottom: none;
}
&:hover {
background: rgba(0, 135, 90, 0.03);
margin: 0 -20px;
padding-left: 20px;
padding-right: 20px;
}
.notice-main {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
.notice-tag {
flex-shrink: 0;
&.el-tag--warning {
background-color: rgba(0, 135, 90, 0.08);
border-color: rgba(0, 135, 90, 0.15);
color: var(--edu-green-dark);
}
&.el-tag--success {
background-color: rgba(0, 135, 90, 0.06);
border-color: rgba(0, 135, 90, 0.12);
color: var(--edu-green-primary);
}
}
.notice-title {
flex: 1;
font-size: 13px;
color: #303133;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
color: var(--edu-green-dark);
}
}
}
.notice-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #909399;
padding-left: 42px;
}
}
}
}
</style>
+375
View File
@@ -0,0 +1,375 @@
<template>
<div class="lock-container">
<!-- 动态粒子背景 -->
<canvas ref="particleCanvas" class="particle-bg"></canvas>
<!-- 时钟 -->
<div class="lock-time">{{ currentTime }}</div>
<div class="lock-date">{{ currentDate }}</div>
<!-- 锁屏卡片 -->
<div class="lock-card">
<div class="avatar-wrap">
<img :src="avatar" class="lock-avatar" @error="onAvatarError" />
<div class="lock-icon">🔒</div>
</div>
<div class="lock-username">{{ nickName }}</div>
<div class="lock-hint">系统已锁定请输入密码解锁</div>
<div class="input-wrap" :class="{ shake: isShaking }">
<input ref="passwordInput" v-model="password" type="password" placeholder="请输入登录密码" class="lock-input" @keydown.enter="handleUnlock" autocomplete="off" />
<button class="unlock-btn" @click="handleUnlock" :disabled="loading">
<span v-if="!loading"></span>
<span v-else class="loading-dot">···</span>
</button>
</div>
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
<div class="lock-footer">
<a href="/login" @click.prevent="goLogin">退出重新登录</a>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { unlockScreen } from '@/api/login'
import defAva from '@/assets/images/profile.jpg'
export default {
name: 'LockScreen',
data() {
return {
password: '',
loading: false,
errorMsg: '',
isShaking: false,
currentTime: '',
currentDate: '',
timer: null,
animationId: null,
particles: []
}
},
computed: {
...mapGetters(['avatar', 'nickName'])
},
mounted() {
this.startClock()
this.initParticles()
this.$nextTick(() => {
this.$refs.passwordInput && this.$refs.passwordInput.focus()
})
},
beforeDestroy() {
clearInterval(this.timer)
cancelAnimationFrame(this.animationId)
},
methods: {
onAvatarError(e) {
e.target.src = defAva
},
startClock() {
const update = () => {
const now = new Date()
const h = String(now.getHours()).padStart(2, '0')
const m = String(now.getMinutes()).padStart(2, '0')
const s = String(now.getSeconds()).padStart(2, '0')
this.currentTime = `${h}:${m}:${s}`
const days = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']
this.currentDate = `${now.getFullYear()}${now.getMonth()+1}${now.getDate()}${days[now.getDay()]}`
}
update()
this.timer = setInterval(update, 1000)
},
async handleUnlock() {
if (!this.password) {
this.showError('请输入密码')
return
}
this.loading = true
this.errorMsg = ''
try {
await unlockScreen(this.password)
const lockPath = this.$store.getters.lockPath // 取锁屏前的路径
await this.$store.dispatch('lock/unlockScreen')
this.$router.replace(lockPath)
} catch (err) {
const msg = err.message || err.toString()
this.showError(msg)
this.password = ''
this.$refs.passwordInput && this.$refs.passwordInput.focus()
} finally {
this.loading = false
}
},
showError(msg) {
this.errorMsg = msg
this.isShaking = true
setTimeout(() => { this.isShaking = false }, 600)
},
goLogin() {
this.$store.dispatch('lock/unlockScreen')
this.$store.dispatch('LogOut').then(() => {
this.$router.push('/login')
})
},
// 粒子背景
initParticles() {
const canvas = this.$refs.particleCanvas
if (!canvas) return
const ctx = canvas.getContext('2d')
const resize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
resize()
window.addEventListener('resize', resize)
const count = 80
for (let i = 0; i < count; i++) {
this.particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 2 + 1,
dx: (Math.random() - 0.5) * 0.6,
dy: (Math.random() - 0.5) * 0.6,
alpha: Math.random() * 0.5 + 0.2
})
}
const draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
this.particles.forEach(p => {
ctx.beginPath()
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
ctx.fillStyle = `rgba(255,255,255,${p.alpha})`
ctx.fill()
p.x += p.dx
p.y += p.dy
if (p.x < 0 || p.x > canvas.width) p.dx *= -1
if (p.y < 0 || p.y > canvas.height) p.dy *= -1
})
// 连线
for (let i = 0; i < this.particles.length; i++) {
for (let j = i + 1; j < this.particles.length; j++) {
const a = this.particles[i], b = this.particles[j]
const dist = Math.hypot(a.x - b.x, a.y - b.y)
if (dist < 120) {
ctx.beginPath()
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.strokeStyle = `rgba(255,255,255,${0.15 * (1 - dist / 120)})`
ctx.lineWidth = 0.5
ctx.stroke()
}
}
}
this.animationId = requestAnimationFrame(draw)
}
draw()
}
}
}
</script>
<style scoped>
.lock-container {
position: fixed;
inset: 0;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow: hidden;
}
.particle-bg {
position: absolute;
inset: 0;
z-index: 0;
}
.lock-time {
position: relative;
z-index: 1;
font-size: 72px;
font-weight: 200;
color: #fff;
letter-spacing: 4px;
text-shadow: 0 0 40px rgba(255,255,255,0.3);
margin-bottom: 8px;
font-variant-numeric: tabular-nums;
}
.lock-date {
position: relative;
z-index: 1;
font-size: 15px;
color: rgba(255,255,255,0.6);
margin-bottom: 48px;
letter-spacing: 2px;
}
.lock-card {
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 24px;
padding: 40px 48px;
width: 360px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 25px 60px rgba(0,0,0,0.4);
}
.avatar-wrap {
position: relative;
margin-bottom: 16px;
}
.lock-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.3);
object-fit: cover;
display: block;
}
.lock-icon {
position: absolute;
bottom: -4px;
right: -4px;
background: rgba(255,255,255,0.15);
border-radius: 50%;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
backdrop-filter: blur(8px);
}
.lock-username {
color: #fff;
font-size: 18px;
font-weight: 600;
margin-bottom: 6px;
letter-spacing: 1px;
}
.lock-hint {
color: rgba(255,255,255,0.5);
font-size: 13px;
margin-bottom: 28px;
}
.input-wrap {
width: 100%;
display: flex;
align-items: center;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 50px;
padding: 4px 4px 4px 20px;
transition: border-color 0.3s;
}
.input-wrap:focus-within {
border-color: rgba(255,255,255,0.6);
background: rgba(255,255,255,0.13);
}
.input-wrap.shake {
animation: shake 0.5s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-6px); }
80% { transform: translateX(6px); }
}
.lock-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #fff;
font-size: 15px;
padding: 10px 0;
}
.lock-input::placeholder {
color: rgba(255,255,255,0.35);
}
.unlock-btn {
width: 42px;
height: 42px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
transition: transform 0.2s, opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.unlock-btn:hover:not(:disabled) {
transform: scale(1.08);
}
.unlock-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-dot {
font-size: 13px;
letter-spacing: 1px;
}
.error-msg {
margin-top: 14px;
color: #ff7675;
font-size: 13px;
text-align: center;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.lock-footer {
margin-top: 24px;
}
.lock-footer a {
color: rgba(255,255,255,0.4);
font-size: 13px;
text-decoration: none;
transition: color 0.2s;
}
.lock-footer a:hover {
color: rgba(255,255,255,0.8);
}
</style>
+180
View File
@@ -0,0 +1,180 @@
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{title}}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
>
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>{{ footerContent }}</span>
</div>
</div>
</template>
<script>
import Cookies from "js-cookie"
import { encrypt, decrypt } from '@/utils/jsencrypt'
export default {
name: "Login",
data() {
return {
title: process.env.VUE_APP_TITLE,
footerContent: "Copyright © 2018-2026 roomroot. All Rights Reserved.",
loginForm: {
username: "admin",
password: "admin123",
rememberMe: false
},
loginRules: {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" }
],
password: [
{ required: true, trigger: "blur", message: "请输入您的密码" }
]
},
loading: false,
// 注册开关
register: false,
redirect: undefined
}
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
created() {
this.getCookie()
},
methods: {
getCookie() {
const username = Cookies.get("username")
const password = Cookies.get("password")
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 })
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 })
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 })
} else {
Cookies.remove("username")
Cookies.remove("password")
Cookies.remove('rememberMe')
}
const loginData = {
username: this.loginForm.username,
password: this.loginForm.password
}
this.$store.dispatch("Login", loginData).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{})
}).catch(() => {
this.loading = false
})
}
})
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
z-index: 1;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
</style>
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="app-container">
<el-row :gutter="10">
<el-col :span="24" class="card-box">
<el-card>
<div slot="header"><span><i class="el-icon-monitor"></i> 基本信息</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%">
<tbody>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">Redis版本</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">端口</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">客户端数</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">运行时间()</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">使用内存</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">使用CPU</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">内存配置</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">AOF是否开启</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "否" : "是" }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">RDB是否成功</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">Key数量</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
<td class="el-table__cell is-leaf"><div class="cell">网络入口/出口</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="12" class="card-box">
<el-card>
<div slot="header"><span><i class="el-icon-pie-chart"></i> 命令统计</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<div ref="commandstats" style="height: 420px" />
</div>
</el-card>
</el-col>
<el-col :span="12" class="card-box">
<el-card>
<div slot="header"><span><i class="el-icon-odometer"></i> 内存信息</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<div ref="usedmemory" style="height: 420px" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { getCache } from "@/api/monitor/cache"
import * as echarts from "echarts"
export default {
name: "Cache",
data() {
return {
// 统计命令信息
commandstats: null,
// 使用内存
usedmemory: null,
// cache信息
cache: []
}
},
created() {
this.getList()
this.openLoading()
},
methods: {
/** 查缓存询信息 */
getList() {
getCache().then((response) => {
this.cache = response.data
this.$modal.closeLoading()
this.commandstats = echarts.init(this.$refs.commandstats, "macarons")
this.commandstats.setOption({
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c} ({d}%)",
},
series: [
{
name: "命令",
type: "pie",
roseType: "radius",
radius: [15, 95],
center: ["50%", "38%"],
data: response.data.commandStats,
animationEasing: "cubicInOut",
animationDuration: 1000,
}
]
})
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
this.usedmemory.setOption({
tooltip: {
formatter: "{b} <br/>{a} : " + this.cache.info.used_memory_human,
},
series: [
{
name: "峰值",
type: "gauge",
min: 0,
max: 1000,
detail: {
formatter: this.cache.info.used_memory_human,
},
data: [
{
value: parseFloat(this.cache.info.used_memory_human),
name: "内存消耗",
}
]
}
]
})
window.addEventListener("resize", () => {
this.commandstats.resize()
this.usedmemory.resize()
})
})
},
// 打开加载层
openLoading() {
this.$modal.loading("正在加载缓存监控数据,请稍候!")
}
}
}
</script>
+241
View File
@@ -0,0 +1,241 @@
<template>
<div class="app-container">
<el-row :gutter="10">
<el-col :span="8">
<el-card style="height: calc(100vh - 125px)">
<div slot="header">
<span><i class="el-icon-collection"></i> 缓存列表</span>
<el-button
style="float: right; padding: 3px 0"
type="text"
icon="el-icon-refresh-right"
@click="refreshCacheNames()"
></el-button>
</div>
<el-table
v-loading="loading"
:data="cacheNames"
:height="tableHeight"
highlight-current-row
@row-click="getCacheKeys"
style="width: 100%"
>
<el-table-column
label="序号"
width="60"
type="index"
></el-table-column>
<el-table-column
label="缓存名称"
align="center"
prop="cacheName"
:show-overflow-tooltip="true"
:formatter="nameFormatter"
></el-table-column>
<el-table-column
label="备注"
align="center"
prop="remark"
:show-overflow-tooltip="true"
/>
<el-table-column
label="操作"
width="60"
align="center"
class-name="small-padding fixed-width"
>
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleClearCacheName(scope.row)"
></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="8">
<el-card style="height: calc(100vh - 125px)">
<div slot="header">
<span><i class="el-icon-key"></i> 键名列表</span>
<el-button
style="float: right; padding: 3px 0"
type="text"
icon="el-icon-refresh-right"
@click="refreshCacheKeys()"
></el-button>
</div>
<el-table
v-loading="subLoading"
:data="cacheKeys"
:height="tableHeight"
highlight-current-row
@row-click="handleCacheValue"
style="width: 100%"
>
<el-table-column
label="序号"
width="60"
type="index"
></el-table-column>
<el-table-column
label="缓存键名"
align="center"
:show-overflow-tooltip="true"
:formatter="keyFormatter"
>
</el-table-column>
<el-table-column
label="操作"
width="60"
align="center"
class-name="small-padding fixed-width"
>
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleClearCacheKey(scope.row)"
></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="8">
<el-card :bordered="false" style="height: calc(100vh - 125px)">
<div slot="header">
<span><i class="el-icon-document"></i> 缓存内容</span>
<el-button
style="float: right; padding: 3px 0"
type="text"
icon="el-icon-refresh-right"
@click="handleClearCacheAll()"
>清理全部</el-button
>
</div>
<el-form :model="cacheForm">
<el-row :gutter="32">
<el-col :offset="1" :span="22">
<el-form-item label="缓存名称:" prop="cacheName">
<el-input v-model="cacheForm.cacheName" :readOnly="true" />
</el-form-item>
</el-col>
<el-col :offset="1" :span="22">
<el-form-item label="缓存键名:" prop="cacheKey">
<el-input v-model="cacheForm.cacheKey" :readOnly="true" />
</el-form-item>
</el-col>
<el-col :offset="1" :span="22">
<el-form-item label="缓存内容:" prop="cacheValue">
<el-input
v-model="cacheForm.cacheValue"
type="textarea"
:rows="8"
:readOnly="true"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { listCacheName, listCacheKey, getCacheValue, clearCacheName, clearCacheKey, clearCacheAll } from "@/api/monitor/cache"
export default {
name: "CacheList",
data() {
return {
cacheNames: [],
cacheKeys: [],
cacheForm: {},
loading: true,
subLoading: false,
nowCacheName: "",
tableHeight: window.innerHeight - 200
}
},
created() {
this.getCacheNames()
},
methods: {
/** 查询缓存名称列表 */
getCacheNames() {
this.loading = true
listCacheName().then(response => {
this.cacheNames = response.data
this.loading = false
})
},
/** 刷新缓存名称列表 */
refreshCacheNames() {
this.getCacheNames()
this.$modal.msgSuccess("刷新缓存列表成功")
},
/** 清理指定名称缓存 */
handleClearCacheName(row) {
clearCacheName(row.cacheName).then(response => {
this.$modal.msgSuccess("清理缓存名称[" + row.cacheName + "]成功")
this.getCacheKeys()
})
},
/** 查询缓存键名列表 */
getCacheKeys(row) {
const cacheName = row !== undefined ? row.cacheName : this.nowCacheName
if (cacheName === "") {
return
}
this.subLoading = true
listCacheKey(cacheName).then(response => {
this.cacheKeys = response.data
this.subLoading = false
this.nowCacheName = cacheName
})
},
/** 刷新缓存键名列表 */
refreshCacheKeys() {
this.getCacheKeys()
this.$modal.msgSuccess("刷新键名列表成功")
},
/** 清理指定键名缓存 */
handleClearCacheKey(cacheKey) {
clearCacheKey(cacheKey).then(response => {
this.$modal.msgSuccess("清理缓存键名[" + cacheKey + "]成功")
this.getCacheKeys()
})
},
/** 列表前缀去除 */
nameFormatter(row) {
return row.cacheName.replace(":", "")
},
/** 键名前缀去除 */
keyFormatter(cacheKey) {
return cacheKey.replace(this.nowCacheName, "")
},
/** 查询缓存内容详细 */
handleCacheValue(cacheKey) {
getCacheValue(this.nowCacheName, cacheKey).then(response => {
this.cacheForm = response.data
})
},
/** 清理全部缓存 */
handleClearCacheAll() {
clearCacheAll().then(response => {
this.$modal.msgSuccess("清理全部缓存成功")
})
}
}
}
</script>
@@ -0,0 +1,15 @@
<template>
<i-frame :src="url" />
</template>
<script>
import iFrame from "@/components/iFrame/index"
export default {
name: "Druid",
components: { iFrame },
data() {
return {
url: process.env.VUE_APP_BASE_API + "/druid/login.html"
}
}
}
</script>
+197
View File
@@ -0,0 +1,197 @@
<template>
<el-dialog :title="type === 'log' ? '调度日志详细' : '任务详细'" :visible.sync="visible" width="780px" append-to-body @close="$emit('update:visible', false)">
<div class="detail-wrap">
<template v-if="type === 'log'">
<!-- 基本信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-info"></i> 基本信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">日志编号</span><span class="detail-value">{{ form.jobLogId }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag v-if="form.status == 0" type="success" size="small">正常</el-tag>
<el-tag v-else type="danger" size="small">失败</el-tag>
</div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">开始时间</span><span class="detail-value">{{ form.startTime }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">结束时间</span><span class="detail-value">{{ form.endTime }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">记录时间</span><span class="detail-value">{{ form.createTime }}</span></div>
</el-col>
<el-col :span="12" v-if="form.status == 0 && form.startTime && form.endTime">
<div class="detail-item"><span class="detail-label">执行耗时</span><span class="detail-value">{{ costTime }} 毫秒</span></div>
</el-col>
</el-row>
</div>
<!-- 任务信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-time"></i> 任务信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">任务名称</span><span class="detail-value">{{ form.jobName }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">任务分组</span>
<dict-tag :options="dict.type.sys_job_group" :value="form.jobGroup" />
</div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item"><span class="detail-label">日志信息</span><span class="detail-value">{{ form.jobMessage }}</span></div>
</el-col>
</el-row>
</div>
<!-- 调用目标 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-s-operation"></i> 调用目标</div>
<div class="code-body">
<div class="code-wrap"><pre class="code-pre">{{ form.invokeTarget || '(无)' }}</pre></div>
</div>
</div>
<!-- 异常信息 -->
<div class="detail-card" v-if="form.status == 1">
<div class="detail-card-title error-title"><i class="el-icon-warning"></i> 异常信息</div>
<div class="error-body"><div class="error-msg">{{ form.exceptionInfo }}</div></div>
</div>
</template>
<template v-else>
<!-- 任务配置 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-setting"></i> 任务配置</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">任务编号</span><span class="detail-value">{{ form.jobId }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">任务名称</span><span class="detail-value">{{ form.jobName }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">任务分组</span>
<dict-tag :options="dict.type.sys_job_group" :value="form.jobGroup" />
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag v-if="form.status == 0" type="success" size="small">正常</el-tag>
<el-tag v-else type="info" size="small">暂停</el-tag>
</div>
</el-col>
</el-row>
</div>
<!-- 调度信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-date"></i> 调度信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">cron 表达式</span><span class="detail-value mono">{{ form.cronExpression }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">下次执行时间</span><span class="detail-value">{{ parseTime(form.nextValidTime) }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行策略</span>
<el-tag v-if="form.misfirePolicy == 0" type="info" size="small">默认策略</el-tag>
<el-tag v-else-if="form.misfirePolicy == 1" type="warning" size="small">立即执行</el-tag>
<el-tag v-else-if="form.misfirePolicy == 2" type="primary" size="small">执行一次</el-tag>
<el-tag v-else-if="form.misfirePolicy == 3" type="danger" size="small">放弃执行</el-tag>
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">并发执行</span>
<el-tag v-if="form.concurrent == 0" type="success" size="small">允许</el-tag>
<el-tag v-else type="danger" size="small">禁止</el-tag>
</div>
</el-col>
</el-row>
</div>
<!-- 执行方法 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-s-operation"></i> 执行方法</div>
<div class="code-body">
<div class="code-wrap"><pre class="code-pre">{{ form.invokeTarget || '(无)' }}</pre></div>
</div>
</div>
<!-- 元信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-document"></i> 元信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">创建人</span><span class="detail-value">{{ form.createBy || '-' }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">创建时间</span><span class="detail-value">{{ form.createTime }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">更新人</span><span class="detail-value">{{ form.updateBy || '-' }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">更新时间</span><span class="detail-value">{{ form.updateTime || '-' }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row" v-if="form.remark">
<el-col :span="24">
<div class="detail-item"><span class="detail-label">备注</span><span class="detail-value">{{ form.remark }}</span></div>
</el-col>
</el-row>
</div>
</template>
</div>
</el-dialog>
</template>
<script>
export default {
dicts: ['sys_job_group'],
props: {
visible: { type: Boolean, default: false },
row: { type: Object, default: () => ({}) },
// 'job' 任务详细 | 'log' 调度日志详细
type: { type: String, default: 'job' }
},
computed: {
form() { return this.row || {} },
costTime() {
if (!this.form.startTime || !this.form.endTime) return 0
return new Date(this.form.endTime).getTime() - new Date(this.form.startTime).getTime()
}
}
}
</script>
<style scoped>
.detail-label {
width: 80px;
}
</style>
+508
View File
@@ -0,0 +1,508 @@
<template>
<div class="app-container monitor-job">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="任务名称" prop="jobName">
<el-input
v-model="queryParams.jobName"
placeholder="请输入任务名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="任务组名" prop="jobGroup">
<el-select v-model="queryParams.jobGroup" placeholder="请选择任务组名" clearable>
<el-option
v-for="dict in dict.type.sys_job_group"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="任务状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable>
<el-option
v-for="dict in dict.type.sys_job_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['monitor:job:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['monitor:job:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['monitor:job:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['monitor:job:export']"
>导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-s-operation"
size="mini"
@click="handleJobLog"
v-hasPermi="['monitor:job:query']"
>日志</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange" :height="tableHeight">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="任务编号" width="100" align="center" prop="jobId" />
<el-table-column label="任务名称" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<a class="link-type" style="cursor:pointer" @click="handleView(scope.row)">{{ scope.row.jobName }}</a>
</template>
</el-table-column>
<el-table-column label="任务组名" align="center" prop="jobGroup">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_job_group" :value="scope.row.jobGroup"/>
</template>
</el-table-column>
<el-table-column label="调用目标字符串" align="center" prop="invokeTarget" :show-overflow-tooltip="true" />
<el-table-column label="cron执行表达式" align="center" prop="cronExpression" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center">
<template slot-scope="scope">
<el-switch
v-model="scope.row.status"
active-value="0"
inactive-value="1"
@change="handleStatusChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['monitor:job:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['monitor:job:remove']"
>删除</el-button>
<el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['monitor:job:changeStatus', 'monitor:job:query']">
<el-button size="mini" type="text" icon="el-icon-d-arrow-right">更多</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="handleRun" icon="el-icon-caret-right"
v-hasPermi="['monitor:job:changeStatus']">执行一次</el-dropdown-item>
<el-dropdown-item command="handleJobLog" icon="el-icon-s-operation"
v-hasPermi="['monitor:job:query']">调度日志</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改定时任务对话框 -->
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="任务名称" prop="jobName">
<el-input v-model="form.jobName" placeholder="请输入任务名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务分组" prop="jobGroup">
<el-select v-model="form.jobGroup" placeholder="请选择任务分组">
<el-option
v-for="dict in dict.type.sys_job_group"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="invokeTarget">
<span slot="label">
调用方法
<el-tooltip placement="top">
<div slot="content">
Bean调用示例ryTask.ryParams('ry')
<br />Class类调用示例com.roomroot.quartz.task.RyTask.ryParams('ry')
<br />参数说明支持字符串布尔类型长整型浮点型整型
</div>
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-input v-model="form.invokeTarget" placeholder="请输入调用目标字符串" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="cron表达式" prop="cronExpression">
<el-input v-model="form.cronExpression" placeholder="请输入cron执行表达式">
<template slot="append">
<el-button type="primary" @click="handleShowCron">
生成表达式
<i class="el-icon-time el-icon--right"></i>
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="24" v-if="form.jobId !== undefined">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.sys_job_status"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="执行策略" prop="misfirePolicy">
<el-radio-group v-model="form.misfirePolicy" size="small">
<el-radio-button label="1">立即执行</el-radio-button>
<el-radio-button label="2">执行一次</el-radio-button>
<el-radio-button label="3">放弃执行</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否并发" prop="concurrent">
<el-radio-group v-model="form.concurrent" size="small">
<el-radio-button label="0">允许</el-radio-button>
<el-radio-button label="1">禁止</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<el-dialog title="Cron表达式生成器" :visible.sync="openCron" append-to-body destroy-on-close class="scrollbar">
<crontab @hide="openCron=false" @fill="crontabFill" :expression="expression"></crontab>
</el-dialog>
<!-- 任务日志详细 -->
<job-detail :visible.sync="openView" :row="form" type="job" />
</div>
</template>
<script>
import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job"
import JobDetail from './detail'
import Crontab from '@/components/Crontab'
export default {
components: { Crontab, JobDetail },
name: "Job",
dicts: ['sys_job_group', 'sys_job_status'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 定时任务表格数据
jobList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否显示详细弹出层
openView: false,
// 是否显示Cron表达式弹出层
openCron: false,
// 传入的表达式
expression: "",
// 表格高度
tableHeight: window.innerHeight - 240,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
jobName: undefined,
jobGroup: undefined,
status: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
jobName: [
{ required: true, message: "任务名称不能为空", trigger: "blur" }
],
invokeTarget: [
{ required: true, message: "调用目标字符串不能为空", trigger: "blur" }
],
cronExpression: [
{ required: true, message: "cron执行表达式不能为空", trigger: "blur" }
]
}
}
},
created() {
this.getList()
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
watch: {
showSearch() {
this.$nextTick(() => {
this.getTableHeight()
})
}
},
methods: {
getTableHeight() {
const offset = this.showSearch ? 240 : 190
this.tableHeight = window.innerHeight - offset
},
/** 查询定时任务列表 */
getList() {
this.loading = true
listJob(this.queryParams).then(response => {
this.jobList = response.rows
this.total = response.total
this.loading = false
})
},
// 任务组名字典翻译
jobGroupFormat(row, column) {
return this.selectDictLabel(this.dict.type.sys_job_group, row.jobGroup)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
jobId: undefined,
jobName: undefined,
jobGroup: undefined,
invokeTarget: undefined,
cronExpression: undefined,
misfirePolicy: 1,
concurrent: 1,
status: "0"
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.jobId)
this.single = selection.length != 1
this.multiple = !selection.length
},
// 更多操作触发
handleCommand(command, row) {
switch (command) {
case "handleRun":
this.handleRun(row)
break
case "handleView":
this.handleView(row)
break
case "handleJobLog":
this.handleJobLog(row)
break
default:
break
}
},
// 任务状态修改
handleStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用"
this.$modal.confirm('确认要"' + text + '""' + row.jobName + '"任务吗?').then(function() {
return changeJobStatus(row.jobId, row.status)
}).then(() => {
this.$modal.msgSuccess(text + "成功")
}).catch(function() {
row.status = row.status === "0" ? "1" : "0"
})
},
/* 立即执行一次 */
handleRun(row) {
this.$modal.confirm('确认要立即执行一次"' + row.jobName + '"任务吗?').then(function() {
return runJob(row.jobId, row.jobGroup)
}).then(() => {
this.$modal.msgSuccess("执行成功")
}).catch(() => {})
},
/** 任务详细信息 */
handleView(row) {
getJob(row.jobId).then(response => {
this.form = response.data
this.openView = true
})
},
/** cron表达式按钮操作 */
handleShowCron() {
this.expression = this.form.cronExpression
this.openCron = true
},
/** 确定后回传值 */
crontabFill(value) {
this.form.cronExpression = value
},
/** 任务日志列表查询 */
handleJobLog(row) {
const jobId = row.jobId || 0
this.$router.push('/monitor/job-log/index/' + jobId)
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加任务"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const jobId = row.jobId || this.ids
getJob(jobId).then(response => {
this.form = response.data
this.open = true
this.title = "修改任务"
})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.jobId != undefined) {
updateJob(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addJob(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const jobIds = row.jobId || this.ids
this.$modal.confirm('是否确认删除定时任务编号为"' + jobIds + '"的数据项?').then(function() {
return delJob(jobIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('monitor/job/export', {
...this.queryParams
}, `job_${new Date().getTime()}.xlsx`)
}
}
}
</script>
<style scoped>
.monitor-job ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.monitor-job ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.monitor-job ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.monitor-job ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
+300
View File
@@ -0,0 +1,300 @@
<template>
<div class="app-container monitor-job-log">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="任务名称" prop="jobName">
<el-input
v-model="queryParams.jobName"
placeholder="请输入任务名称"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="任务组名" prop="jobGroup">
<el-select
v-model="queryParams.jobGroup"
placeholder="请选择任务组名"
clearable
style="width: 240px"
>
<el-option
v-for="dict in dict.type.sys_job_group"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="执行状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择执行状态"
clearable
style="width: 240px"
>
<el-option
v-for="dict in dict.type.sys_common_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="执行时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['monitor:job:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
@click="handleClean"
v-hasPermi="['monitor:job:remove']"
>清空</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['monitor:job:export']"
>导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button
icon="el-icon-close"
size="mini"
@click="handleClose"
>关闭</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="jobLogList" @selection-change="handleSelectionChange" :height="tableHeight">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="日志编号" width="80" align="center" prop="jobLogId" />
<el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
<el-table-column label="任务组名" align="center" prop="jobGroup" :show-overflow-tooltip="true">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_job_group" :value="scope.row.jobGroup"/>
</template>
</el-table-column>
<el-table-column label="调用目标字符串" align="center" prop="invokeTarget" :show-overflow-tooltip="true" />
<el-table-column label="日志信息" align="center" prop="jobMessage" :show-overflow-tooltip="true" />
<el-table-column label="执行状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="执行时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['monitor:job:query']"
>详细</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<job-log-detail :visible.sync="open" :row="form" type="log" />
</div>
</template>
<script>
import { getJob} from "@/api/monitor/job"
import { listJobLog, delJobLog, cleanJobLog } from "@/api/monitor/jobLog"
import JobLogDetail from './detail'
export default {
name: "JobLog",
components: { JobLogDetail },
dicts: ['sys_common_status', 'sys_job_group'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 调度日志表格数据
jobLogList: [],
// 是否显示弹出层
open: false,
// 日期范围
dateRange: [],
// 表单参数
form: {},
// 表格高度
tableHeight: window.innerHeight - 240,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
jobName: undefined,
jobGroup: undefined,
status: undefined
}
}
},
created() {
const jobId = this.$route.params && this.$route.params.jobId
if (jobId !== undefined && jobId != 0) {
getJob(jobId).then(response => {
this.queryParams.jobName = response.data.jobName
this.queryParams.jobGroup = response.data.jobGroup
this.getList()
})
} else {
this.getList()
}
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
watch: {
showSearch() {
this.$nextTick(() => {
this.getTableHeight()
})
}
},
methods: {
getTableHeight() {
const offset = this.showSearch ? 240 : 190
this.tableHeight = window.innerHeight - offset
},
/** 查询调度日志列表 */
getList() {
this.loading = true
listJobLog(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.jobLogList = response.rows
this.total = response.total
this.loading = false
}
)
},
// 返回按钮
handleClose() {
const obj = { path: "/monitor/job" }
this.$tab.closeOpenPage(obj)
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm("queryForm")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.jobLogId)
this.multiple = !selection.length
},
/** 详细按钮操作 */
handleView(row) {
this.open = true
this.form = row
},
/** 删除按钮操作 */
handleDelete(row) {
const jobLogIds = this.ids
this.$modal.confirm('是否确认删除调度日志编号为"' + jobLogIds + '"的数据项?').then(function() {
return delJobLog(jobLogIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 清空按钮操作 */
handleClean() {
this.$modal.confirm('是否确认清空所有调度日志数据项?').then(function() {
return cleanJobLog()
}).then(() => {
this.getList()
this.$modal.msgSuccess("清空成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('/monitor/jobLog/export', {
...this.queryParams
}, `log_${new Date().getTime()}.xlsx`)
}
}
}
</script>
<style scoped>
.monitor-job-log ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.monitor-job-log ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.monitor-job-log ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.monitor-job-log ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
@@ -0,0 +1,274 @@
<template>
<div class="app-container monitor-logininfor">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="登录地址" prop="ipaddr">
<el-input
v-model="queryParams.ipaddr"
placeholder="请输入登录地址"
clearable
style="width: 240px;"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="用户名称" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入用户名称"
clearable
style="width: 240px;"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="登录状态"
clearable
style="width: 240px"
>
<el-option
v-for="dict in dict.type.sys_common_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="登录时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['monitor:logininfor:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
@click="handleClean"
v-hasPermi="['monitor:logininfor:remove']"
>清空</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-unlock"
size="mini"
:disabled="single"
@click="handleUnlock"
v-hasPermi="['monitor:logininfor:unlock']"
>解锁</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['monitor:logininfor:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<div class="table-wrapper">
<el-table ref="tables" v-loading="loading" :data="list" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange" height="100%">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="访问编号" align="center" prop="infoId" />
<el-table-column label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
<el-table-column label="登录地址" align="center" prop="ipaddr" width="130" :show-overflow-tooltip="true" />
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
<el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
<el-table-column label="操作系统" align="center" prop="os" />
<el-table-column label="登录状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="操作信息" align="center" prop="msg" :show-overflow-tooltip="true" />
<el-table-column label="登录日期" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.loginTime) }}</span>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
import { list, delLogininfor, cleanLogininfor, unlockLogininfor } from "@/api/monitor/logininfor"
export default {
name: "Logininfor",
dicts: ['sys_common_status'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 选择用户名
selectName: "",
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 表格数据
list: [],
// 日期范围
dateRange: [],
// 默认排序
defaultSort: { prop: "loginTime", order: "descending" },
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
ipaddr: undefined,
userName: undefined,
status: undefined
}
}
},
created() {
this.getList()
},
methods: {
/** 查询登录日志列表 */
getList() {
this.loading = true
list(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.list = response.rows
this.total = response.total
this.loading = false
}
)
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm("queryForm")
this.queryParams.pageNum = 1
this.$refs.tables.sort(this.defaultSort.prop, this.defaultSort.order)
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.infoId)
this.single = selection.length!=1
this.multiple = !selection.length
this.selectName = selection.map(item => item.userName)
},
/** 排序触发事件 */
handleSortChange(column, prop, order) {
this.queryParams.orderByColumn = column.prop
this.queryParams.isAsc = column.order
this.getList()
},
/** 删除按钮操作 */
handleDelete(row) {
const infoIds = row.infoId || this.ids
this.$modal.confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?').then(function() {
return delLogininfor(infoIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 清空按钮操作 */
handleClean() {
this.$modal.confirm('是否确认清空所有登录日志数据项?').then(function() {
return cleanLogininfor()
}).then(() => {
this.getList()
this.$modal.msgSuccess("清空成功")
}).catch(() => {})
},
/** 解锁按钮操作 */
handleUnlock() {
const username = this.selectName
this.$modal.confirm('是否确认解锁用户"' + username + '"数据项?').then(function() {
return unlockLogininfor(username)
}).then(() => {
this.$modal.msgSuccess("用户" + username + "解锁成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('monitor/logininfor/export', {
...this.queryParams
}, `logininfor_${new Date().getTime()}.xlsx`)
}
}
}
</script>
<style scoped>
.monitor-logininfor {
display: flex;
flex-direction: column;
height: calc(100vh - 124px);
}
.table-wrapper {
flex: 1;
overflow: hidden;
}
.monitor-logininfor ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.monitor-logininfor ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.monitor-logininfor ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.monitor-logininfor ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
+152
View File
@@ -0,0 +1,152 @@
<template>
<div class="app-container monitor-online">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
<el-form-item label="登录地址" prop="ipaddr">
<el-input
v-model="queryParams.ipaddr"
placeholder="请输入登录地址"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="用户名称" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入用户名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="list.slice((pageNum-1)*pageSize,pageNum*pageSize)"
style="width: 100%;"
:height="tableHeight"
>
<el-table-column label="序号" type="index" align="center">
<template slot-scope="scope">
<span>{{(pageNum - 1) * pageSize + scope.$index + 1}}</span>
</template>
</el-table-column>
<el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
<el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="部门名称" align="center" prop="deptName" />
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
<el-table-column label="浏览器" align="center" prop="browser" />
<el-table-column label="操作系统" align="center" prop="os" />
<el-table-column label="登录时间" align="center" prop="loginTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.loginTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleForceLogout(scope.row)"
v-hasPermi="['monitor:online:forceLogout']"
>强退</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="pageNum" :limit.sync="pageSize" />
</div>
</template>
<script>
import { list, forceLogout } from "@/api/monitor/online"
export default {
name: "Online",
data() {
return {
// 遮罩层
loading: true,
// 总条数
total: 0,
// 表格数据
list: [],
pageNum: 1,
pageSize: 10,
// 表格高度
tableHeight: window.innerHeight - 240,
// 查询参数
queryParams: {
ipaddr: undefined,
userName: undefined
}
}
},
created() {
this.getList()
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
methods: {
getTableHeight() {
this.tableHeight = window.innerHeight - 240
},
/** 查询登录日志列表 */
getList() {
this.loading = true
list(this.queryParams).then(response => {
this.list = response.rows
this.total = response.total
this.loading = false
})
},
/** 搜索按钮操作 */
handleQuery() {
this.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
/** 强退按钮操作 */
handleForceLogout(row) {
this.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?').then(function() {
return forceLogout(row.tokenId)
}).then(() => {
this.getList()
this.$modal.msgSuccess("强退成功")
}).catch(() => {})
}
}
}
</script>
<style scoped>
.monitor-online ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.monitor-online ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.monitor-online ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.monitor-online ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
@@ -0,0 +1,147 @@
<template>
<el-dialog title="操作日志详细" :visible.sync="visible" width="780px" append-to-body @close="$emit('update:visible', false)">
<div class="detail-wrap">
<!-- 基本信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-info"></i> 基本信息</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">操作模块</span><span class="detail-value">{{ form.title }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item"><span class="detail-label">业务类型</span><span class="detail-value">{{ typeLabel }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">操作时间</span><span class="detail-value">{{ form.operTime }}</span></div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<span class="detail-label">执行状态</span>
<el-tag v-if="form.status === 0" type="success" size="small"><i class="el-icon-check"></i> 正常</el-tag>
<el-tag v-else type="danger" size="small"><i class="el-icon-close"></i> 异常</el-tag>
</div>
</el-col>
</el-row>
</div>
<!-- 操作人员 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-user"></i> 操作人员</div>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">操作人员</span><span class="detail-value">{{ form.operName }}</span></div>
</el-col>
<el-col :span="12" v-if="form.deptName">
<div class="detail-item"><span class="detail-label">所属部门</span><span class="detail-value">{{ form.deptName }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item">
<span class="detail-label">操作地址</span>
<span class="detail-value">{{ form.operIp }}&nbsp;&nbsp;<span class="detail-location">{{ form.operLocation }}</span></span>
</div>
</el-col>
</el-row>
</div>
<!-- 请求信息 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-sort"></i> 请求信息</div>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item">
<span class="detail-label">请求地址</span>
<span class="detail-value">
<span :class="'method-tag method-' + form.requestMethod">{{ form.requestMethod }}</span>
{{ form.operUrl }}
</span>
</div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="24">
<div class="detail-item"><span class="detail-label">操作方法</span><span class="detail-value mono">{{ form.method }}</span></div>
</el-col>
</el-row>
<el-row class="detail-row">
<el-col :span="12">
<div class="detail-item"><span class="detail-label">消耗时间</span><span class="detail-value">{{ form.costTime }} 毫秒</span></div>
</el-col>
</el-row>
</div>
<!-- 请求参数 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-upload2"></i> 请求参数</div>
<div class="code-body">
<div class="code-wrap">
<div class="code-action">
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.operParam)">复制</el-button>
</div>
<pre class="code-pre">{{ formatJson(form.operParam) }}</pre>
</div>
</div>
</div>
<!-- 返回参数 -->
<div class="detail-card">
<div class="detail-card-title"><i class="el-icon-download"></i> 返回参数</div>
<div class="code-body">
<div class="code-wrap">
<div class="code-action">
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.jsonResult)">复制</el-button>
</div>
<pre class="code-pre">{{ formatJson(form.jsonResult) }}</pre>
</div>
</div>
</div>
<!-- 异常信息 -->
<div class="detail-card" v-if="form.status !== 0">
<div class="detail-card-title error-title"><i class="el-icon-warning"></i> 异常信息</div>
<div class="error-body">
<div class="error-msg">{{ form.errorMsg }}</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'OperlogDetail',
dicts: ['sys_oper_type'],
props: {
visible: { type: Boolean, default: false },
row: { type: Object, default: () => ({}) }
},
computed: {
form() { return this.row || {} },
typeLabel() { return this.selectDictLabel(this.dict.type.sys_oper_type, this.form.businessType) || '-' }
},
methods: {
formatJson(str) {
if (!str) return '(无数据)'
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }
},
copyText(str) {
const text = this.formatJson(str)
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => this.$message({ message: '已复制', type: 'success', duration: 1500 }))
} else {
const ta = document.createElement('textarea')
ta.value = text
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
this.$message({ message: '已复制', type: 'success', duration: 1500 })
}
}
}
}
</script>
@@ -0,0 +1,306 @@
<template>
<div class="app-container monitor-operlog">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="操作地址" prop="operIp">
<el-input
v-model="queryParams.operIp"
placeholder="请输入操作地址"
clearable
style="width: 240px;"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="系统模块" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入系统模块"
clearable
style="width: 240px;"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="操作人员" prop="operName">
<el-input
v-model="queryParams.operName"
placeholder="请输入操作人员"
clearable
style="width: 240px;"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="类型" prop="businessType">
<el-select
v-model="queryParams.businessType"
placeholder="操作类型"
clearable
style="width: 240px"
>
<el-option
v-for="dict in dict.type.sys_oper_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="操作状态"
clearable
style="width: 240px"
>
<el-option
v-for="dict in dict.type.sys_common_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="操作时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['monitor:operlog:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
@click="handleClean"
v-hasPermi="['monitor:operlog:remove']"
>清空</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['monitor:operlog:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<div class="table-wrapper">
<el-table ref="tables" v-loading="loading" :data="list" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange" height="100%">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="日志编号" align="center" prop="operId" />
<el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
<el-table-column label="操作类型" align="center" prop="businessType">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_oper_type" :value="scope.row.businessType"/>
</template>
</el-table-column>
<el-table-column label="操作人员" align="center" prop="operName" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
<el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
<el-table-column label="操作地点" align="center" prop="operLocation" :show-overflow-tooltip="true" />
<el-table-column label="操作状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="操作日期" align="center" prop="operTime" width="160" sortable="custom" :sort-orders="['descending', 'ascending']">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.operTime) }}</span>
</template>
</el-table-column>
<el-table-column label="消耗时间" align="center" prop="costTime" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']">
<template slot-scope="scope">
<span>{{ scope.row.costTime }}毫秒</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row,scope.index)"
v-hasPermi="['monitor:operlog:query']"
>详细</el-button>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<operlog-detail :visible.sync="detailVisible" :row="detailRow" />
</div>
</template>
<script>
import OperlogDetail from './detail'
import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog"
export default {
name: "Operlog",
components: { OperlogDetail },
dicts: ['sys_oper_type', 'sys_common_status'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 表格数据
list: [],
// 是否显示弹出层
detailVisible: false,
detailRow: {},
// 日期范围
dateRange: [],
// 默认排序
defaultSort: { prop: "operTime", order: "descending" },
// 表单参数
form: {},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
operIp: undefined,
title: undefined,
operName: undefined,
businessType: undefined,
status: undefined
}
}
},
created() {
this.getList()
},
methods: {
/** 查询登录日志 */
getList() {
this.loading = true
list(this.addDateRange(this.queryParams, this.dateRange)).then( response => {
this.list = response.rows
this.total = response.total
this.loading = false
}
)
},
// 详细按钮操作
handleDetail(row) {
this.detailRow = row
this.detailVisible = true
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm("queryForm")
this.queryParams.pageNum = 1
this.$refs.tables.sort(this.defaultSort.prop, this.defaultSort.order)
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.operId)
this.multiple = !selection.length
},
/** 排序触发事件 */
handleSortChange(column, prop, order) {
this.queryParams.orderByColumn = column.prop
this.queryParams.isAsc = column.order
this.getList()
},
/** 删除按钮操作 */
handleDelete(row) {
const operIds = row.operId || this.ids
this.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?').then(function() {
return delOperlog(operIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 清空按钮操作 */
handleClean() {
this.$modal.confirm('是否确认清空所有操作日志数据项?').then(function() {
return cleanOperlog()
}).then(() => {
this.getList()
this.$modal.msgSuccess("清空成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('monitor/operlog/export', {
...this.queryParams
}, `operlog_${new Date().getTime()}.xlsx`)
}
}
}
</script>
<style scoped>
.monitor-operlog {
display: flex;
flex-direction: column;
height: calc(100vh - 124px);
}
.table-wrapper {
flex: 1;
overflow: hidden;
}
.monitor-operlog ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.monitor-operlog ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.monitor-operlog ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.monitor-operlog ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
+207
View File
@@ -0,0 +1,207 @@
<template>
<div class="app-container">
<el-row :gutter="10">
<el-col :span="12" class="card-box">
<el-card>
<div slot="header"><span><i class="el-icon-cpu"></i> CPU</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<thead>
<tr>
<th class="el-table__cell is-leaf"><div class="cell">属性</div></th>
<th class="el-table__cell is-leaf"><div class="cell"></div></th>
</tr>
</thead>
<tbody>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">核心数</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.cpuNum }}</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">用户使用率</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.used }}%</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">系统使用率</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.sys }}%</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">当前空闲率</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.free }}%</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="12" class="card-box">
<el-card>
<div slot="header"><span><i class="el-icon-tickets"></i> 内存</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<thead>
<tr>
<th class="el-table__cell is-leaf"><div class="cell">属性</div></th>
<th class="el-table__cell is-leaf"><div class="cell">内存</div></th>
<th class="el-table__cell is-leaf"><div class="cell">JVM</div></th>
</tr>
</thead>
<tbody>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">总内存</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.total }}G</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.total }}M</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">已用内存</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.used}}G</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.used}}M</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">剩余内存</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.free }}G</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.free }}M</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">使用率</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem" :class="{'text-danger': server.mem.usage > 80}">{{ server.mem.usage }}%</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm" :class="{'text-danger': server.jvm.usage > 80}">{{ server.jvm.usage }}%</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box">
<el-card>
<div slot="header">
<span><i class="el-icon-monitor"></i> 服务器信息</span>
</div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">服务器名称</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerName }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">操作系统</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osName }}</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">服务器IP</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerIp }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">系统架构</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osArch }}</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box">
<el-card>
<div slot="header">
<span><i class="el-icon-coffee-cup"></i> Java虚拟机信息</span>
</div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;table-layout:fixed;">
<tbody>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">Java名称</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.name }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">Java版本</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.version }}</div></td>
</tr>
<tr>
<td class="el-table__cell is-leaf"><div class="cell">启动时间</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.startTime }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">运行时长</div></td>
<td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.runTime }}</div></td>
</tr>
<tr>
<td colspan="1" class="el-table__cell is-leaf"><div class="cell">安装路径</div></td>
<td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.home }}</div></td>
</tr>
<tr>
<td colspan="1" class="el-table__cell is-leaf"><div class="cell">项目路径</div></td>
<td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.userDir }}</div></td>
</tr>
<tr>
<td colspan="1" class="el-table__cell is-leaf"><div class="cell">运行参数</div></td>
<td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.inputArgs }}</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box">
<el-card>
<div slot="header">
<span><i class="el-icon-receiving"></i> 磁盘状态</span>
</div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<thead>
<tr>
<th class="el-table__cell el-table__cell is-leaf"><div class="cell">盘符路径</div></th>
<th class="el-table__cell is-leaf"><div class="cell">文件系统</div></th>
<th class="el-table__cell is-leaf"><div class="cell">盘符类型</div></th>
<th class="el-table__cell is-leaf"><div class="cell">总大小</div></th>
<th class="el-table__cell is-leaf"><div class="cell">可用大小</div></th>
<th class="el-table__cell is-leaf"><div class="cell">已用大小</div></th>
<th class="el-table__cell is-leaf"><div class="cell">已用百分比</div></th>
</tr>
</thead>
<tbody v-if="server.sysFiles">
<tr v-for="(sysFile, index) in server.sysFiles" :key="index">
<td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.dirName }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.sysTypeName }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.typeName }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.total }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.free }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.used }}</div></td>
<td class="el-table__cell is-leaf"><div class="cell" :class="{'text-danger': sysFile.usage > 80}">{{ sysFile.usage }}%</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { getServer } from "@/api/monitor/server"
export default {
name: "Server",
data() {
return {
// 服务器信息
server: []
}
},
created() {
this.getList()
this.openLoading()
},
methods: {
/** 查询服务器信息 */
getList() {
getServer().then(response => {
this.server = response.data
this.$modal.closeLoading()
})
},
// 打开加载层
openLoading() {
this.$modal.loading("正在加载服务监控数据,请稍候!")
}
}
}
</script>
+12
View File
@@ -0,0 +1,12 @@
<script>
export default {
created() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
render: function(h) {
return h() // avoid warning message
}
}
</script>
+167
View File
@@ -0,0 +1,167 @@
<template>
<div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">{{title}}</h3>
<el-form-item prop="username">
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password" :rules="registerPwdValidator">
<el-input
v-model="registerForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleRegister"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
auto-complete="off"
placeholder="确认密码"
@keyup.enter.native="handleRegister"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleRegister"
>
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right;">
<router-link class="link-type" :to="'/login'">使用已有账户登录</router-link>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-register-footer">
<span>{{ footerContent }}</span>
</div>
</div>
</template>
<script>
import { register } from "@/api/login"
import passwordRule from "@/utils/passwordRule"
export default {
mixins: [passwordRule],
data() {
return {
title: process.env.VUE_APP_TITLE,
footerContent: "Copyright © 2018-2026 roomroot. All Rights Reserved.",
registerForm: {
username: "",
password: "",
confirmPassword: ""
},
loading: false
}
},
computed: {
registerRules() {
return {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" },
{ min: 2, max: 20, message: '用户账号长度必须介于 2 和 20 之间', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: "请再次输入您的密码", trigger: "blur" },
{
validator: (rule, value, callback) => {
if (this.registerForm.password !== value) {
callback(new Error("两次输入的密码不一致"))
} else {
callback()
}
}, trigger: "blur"
}
]
}
}
},
methods: {
handleRegister() {
this.$refs.registerForm.validate(valid => {
if (valid) {
this.loading = true
register(this.registerForm).then(() => {
const username = this.registerForm.username
this.$alert("<font color='red'>恭喜你,您的账号 " + username + " 注册成功!</font>", '系统提示', {
dangerouslyUseHTMLString: true,
type: 'success'
}).then(() => {
this.$router.push("/login")
}).catch(() => {})
}).catch(() => {
this.loading = false
})
}
})
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.register {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.register-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.register-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.el-register-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
</style>
@@ -0,0 +1,399 @@
<template>
<div class="app-container plan-import-page">
<!-- ==================== 1. 查询条件 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="100px" class="search-form">
<!-- 第一行课程时间独占整行 -->
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="课程时间">
<div class="date-range">
<span></span>
<el-date-picker
v-model="searchForm.kssj"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="开始时间"
clearable
class="date-input"
/>
<span></span>
<el-date-picker
v-model="searchForm.jssj"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="结束时间"
clearable
class="date-input"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 下面分左右两栏 -->
<el-row :gutter="24">
<el-col :xs="24" :md="12">
<el-form-item label="检查状态">
<div class="checkbox-group">
<el-checkbox v-model="searchForm.jcztYtg">已通过</el-checkbox>
<el-checkbox v-model="searchForm.jcztWtg">未通过</el-checkbox>
</div>
</el-form-item>
<el-form-item label="教员姓名">
<el-input v-model="searchForm.jyxm" placeholder="请输入教员姓名" clearable />
</el-form-item>
<el-form-item label="教学场地名称">
<el-input v-model="searchForm.jxcdmc" placeholder="请输入教学场地名称" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="忽略状态">
<div class="checkbox-group">
<el-checkbox v-model="searchForm.hlztYc">有用</el-checkbox>
<el-checkbox v-model="searchForm.hlztYbl">已被忽略</el-checkbox>
</div>
</el-form-item>
<el-form-item label="班次名称">
<el-input v-model="searchForm.bcmc" placeholder="请输入班次名称" clearable />
</el-form-item>
<el-form-item label="科目名称">
<el-input v-model="searchForm.kmmc" placeholder="请输入科目名称" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 操作按钮区 ==================== -->
<el-card shadow="never" class="action-card">
<div class="action-bar">
<div class="bar-left">
<el-button icon="el-icon-download" @click="handleDownloadTemplate">
课程表数据文件模板下载
</el-button>
</div>
<div class="bar-center">
<div class="file-area">
<input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display: none" @change="handleFileChange" />
<el-button icon="el-icon-folder-opened" @click="handleChooseFile">选择文件</el-button>
<span class="file-name">{{ selectedFileName }}</span>
</div>
</div>
<div class="bar-right">
<el-button type="primary" @click="handleImportToSystem" :loading="importing">缓存数据导入到系统</el-button>
<el-button type="primary" icon="el-icon-upload2" @click="handleUploadToCache">上传数据至缓存</el-button>
<el-button @click="handleCheckCache">检查缓存数据</el-button>
<el-button type="danger" plain @click="handleDeleteCache">删除缓存数据</el-button>
</div>
</div>
</el-card>
<!-- ==================== 3. 数据表格 ==================== -->
<el-card shadow="never" class="table-card">
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row class="import-table">
<el-table-column prop="nd" label="年度" width="60" align="center" />
<el-table-column prop="xh" label="序号" width="53" align="center" />
<el-table-column prop="yxh" label="原序号" width="67" align="center" />
<el-table-column prop="rq" label="日期" width="120" align="center" :formatter="formatDate" />
<el-table-column prop="yjc" label="原节次" width="67" align="center" />
<el-table-column prop="jc" label="节次" width="53" align="center" />
<el-table-column prop="kcmc" label="课程名称" width="85" show-overflow-tooltip />
<el-table-column prop="bc" label="班次" width="53" show-overflow-tooltip />
<el-table-column prop="zrdw" label="责任单位" width="81" show-overflow-tooltip />
<el-table-column prop="skjy" label="授课教员" width="81" align="center" />
<el-table-column prop="jxcd" label="教学场地" width="81" align="center" />
<el-table-column prop="jxnr" label="教学内容" width="81" show-overflow-tooltip />
<el-table-column prop="jxff" label="教学方法" width="81" align="center" />
<el-table-column prop="jybz" label="教员备注" width="81" show-overflow-tooltip />
<el-table-column prop="jwbz" label="教务备注" width="81" show-overflow-tooltip />
<el-table-column prop="jcjg" label="检查结果" width="81" align="center" />
<el-table-column prop="ytgjc" label="已通过检查" width="95" align="center">
<template slot-scope="{ row }">
<span>{{ row.ytgjc ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column prop="ybhl" label="已被忽略" width="81" align="center">
<template slot-scope="{ row }">
<span>{{ row.ybhl ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column prop="ybcl" label="已被处理" width="81" align="center">
<template slot-scope="{ row }">
<span>{{ row.ybcl ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column prop="ybdr" label="已被导入" align="center">
<template slot-scope="{ row }">
<span>{{ row.ybdr ? '是' : '否' }}</span>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination-wrapper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
</div>
</template>
<script>
import { listTeachingPlan, importTeachingPlan, downloadTeachingPlanTemplate } from '@/api/schedule/teachingPlan'
export default {
name: 'PlanImportIndex',
data() {
return {
// ==================== 查询表单 ====================
searchForm: {
kssj: '',
jssj: '',
jcztYtg: false,
jcztWtg: true,
hlztYc: false,
hlztYbl: true,
jyxm: '',
bcmc: '',
jxcdmc: '',
kmmc: ''
},
// ==================== 文件上传与导入 ====================
selectedFileName: '未选择任何文件',
selectedFile: null,
importing: false,
// ==================== 表格数据 ====================
tableData: [],
loading: false,
pageNum: 1,
pageSize: 20,
total: 0
}
},
mounted() {
this.fetchList()
},
methods: {
// ==================== 查询 ====================
// 仅传后端 Mapper 支持的字段:skjy(授课教员) bc(班次) kcmc(课程名称) ytgjc(已通过检查)
// 课程时间、忽略状态、教学场地名称后端列表查询不支持,不传
buildSearchParams() {
const f = this.searchForm
const params = { pageNum: this.pageNum, pageSize: this.pageSize }
if (f.jyxm && f.jyxm.trim()) params.skjy = f.jyxm.trim()
if (f.bcmc && f.bcmc.trim()) params.bc = f.bcmc.trim()
if (f.kmmc && f.kmmc.trim()) params.kcmc = f.kmmc.trim()
// 检查状态:仅勾选「已通过」或「未通过」其中之一时才按该条件过滤
if (f.jcztYtg && !f.jcztWtg) params.ytgjc = true
else if (!f.jcztYtg && f.jcztWtg) params.ytgjc = false
return params
},
fetchList() {
this.loading = true
listTeachingPlan(this.buildSearchParams()).then(response => {
const data = response.data || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handlePageChange(current) {
this.pageNum = current
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
/** 日期格式化 */
formatDate(row, column, cellValue) {
return cellValue ? String(cellValue).substring(0, 10) : ''
},
// ==================== 文件选择 ====================
handleChooseFile() {
this.$refs.fileInputRef && this.$refs.fileInputRef.click()
},
handleFileChange(event) {
const target = event.target
const file = target.files && target.files[0]
if (file) {
this.selectedFile = file
this.selectedFileName = file.name
} else {
this.selectedFile = null
this.selectedFileName = '未选择任何文件'
}
},
// ==================== 下载模板 ====================
handleDownloadTemplate() {
downloadTeachingPlanTemplate().then(blob => {
this.downloadBlob(blob, '教学实施计划导入模板.xlsx')
this.$message.success('模板下载成功')
}).catch(() => {})
},
downloadBlob(blob, fileName) {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
},
// ==================== 导入到系统 ====================
handleImportToSystem() {
if (!this.selectedFile) {
this.$message.warning('请先选择文件')
return
}
this.importing = true
importTeachingPlan(this.selectedFile).then(res => {
const count = res && res.data
if (count !== null && count !== undefined) {
this.$message.success(`导入成功,共 ${count} 条记录`)
} else {
this.$message.success((res && res.msg) || '导入成功')
}
this.selectedFile = null
this.selectedFileName = '未选择任何文件'
this.$refs.fileInputRef && (this.$refs.fileInputRef.value = '')
this.pageNum = 1
this.fetchList()
}).catch(() => {}).finally(() => {
this.importing = false
})
},
// ==================== 缓存操作(后端暂未提供) ====================
handleUploadToCache() {
this.$message.info('后端暂未提供该接口')
},
handleCheckCache() {
this.$message.info('后端暂未提供该接口')
},
handleDeleteCache() {
this.$message.info('后端暂未提供该接口')
}
}
}
</script>
<style scoped lang="scss">
.plan-import-page {
.search-card {
margin-bottom: 16px;
.search-form {
.date-range {
display: flex;
align-items: center;
gap: 8px;
.date-input {
width: 180px;
flex-shrink: 0;
}
}
.checkbox-group {
display: flex;
align-items: center;
gap: 16px;
}
.form-tip {
color: #f56c6c;
font-size: 12px;
margin-bottom: 12px;
padding-left: 100px;
}
.search-actions {
display: flex;
justify-content: flex-end;
padding-top: 8px;
}
}
}
.action-card {
margin-bottom: 16px;
.action-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
.bar-left,
.bar-center,
.bar-right {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.bar-right {
margin-left: auto;
}
.file-area {
display: flex;
align-items: center;
gap: 8px;
.file-name {
color: #606266;
font-size: 14px;
}
}
}
}
.table-card {
.import-table {
width: 100%;
}
}
}
</style>
+249
View File
@@ -0,0 +1,249 @@
<template>
<div class="app-container score-page">
<!-- ==================== 1. 查询条件区域 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :model="queryForm" label-width="90px" inline class="search-form">
<el-form-item label="学员编号">
<el-input v-model="queryForm.xybh" placeholder="请输入学员编号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="年度">
<el-input v-model="queryForm.nd" placeholder="如 2026" clearable style="width: 140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ==================== 2. 成绩列表区域 ==================== -->
<div class="toolbar">
<el-button type="primary" :loading="exportLoading" @click="handleExportGrades">导出课程成绩</el-button>
<el-button type="primary" :loading="exportLoading" @click="handleExportProcessGrades">导出课程过程成绩</el-button>
</div>
<el-card shadow="never" class="table-card">
<el-tabs v-model="activeTab">
<!-- 课程成绩 -->
<el-tab-pane label="课程成绩" name="grades">
<el-table v-loading="loading" :data="gradesData" stripe border style="width: 100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="xybh" label="学员编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="kmbh" label="科目编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="klx" label="课类型" min-width="90" align="center" />
<el-table-column prop="kscj" label="考试成绩" min-width="90" align="center" />
<el-table-column prop="pscj" label="平时成绩" min-width="90" align="center" />
<el-table-column prop="zzcj" label="最终成绩" min-width="90" align="center" />
<el-table-column prop="bkcj" label="补考成绩" min-width="90" align="center" />
<el-table-column prop="bkcs" label="补考次数" min-width="90" align="center" />
<el-table-column prop="ksqk" label="考试情况" min-width="90" align="center" />
<el-table-column prop="qwxf" label="期望学分" min-width="90" align="center" />
<el-table-column prop="ytg" label="已通过" min-width="80" align="center" />
<el-table-column prop="nd" label="年度" min-width="80" align="center" />
</el-table>
<el-empty v-if="!loading && gradesData.length === 0" description="暂无课程成绩数据" :image-size="60" />
</el-tab-pane>
<!-- 课程过程成绩 -->
<el-tab-pane label="课程过程成绩" name="process">
<el-table v-loading="loading" :data="processGradesData" stripe border style="width: 100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="xybh" label="学员编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="kmbh" label="科目编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="klx" label="课类型" min-width="90" align="center" />
<el-table-column prop="yscj" label="原始成绩" min-width="90" align="center" />
<el-table-column prop="zzcj" label="最终成绩" min-width="90" align="center" />
<el-table-column prop="bkcj1" label="补考1" min-width="80" align="center" />
<el-table-column prop="bkcj2" label="补考2" min-width="80" align="center" />
<el-table-column prop="bkcj3" label="补考3" min-width="80" align="center" />
<el-table-column prop="ksqk" label="考试情况" min-width="90" align="center" />
<el-table-column prop="nd" label="年度" min-width="80" align="center" />
</el-table>
<el-empty v-if="!loading && processGradesData.length === 0" description="暂无课程过程成绩数据" :image-size="60" />
</el-tab-pane>
</el-tabs>
<div class="pagination">
<el-pagination
:current-page="pagination.pageNum"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</div>
</template>
<script>
import {
listStudentGrades,
listStudentProcessGrades,
exportStudentGrades,
exportStudentProcessGrades
} from "@/api/studentRecords/studentRecords"
export default {
name: 'ScoreIndex',
data() {
return {
// ==================== 查询条件 ====================
queryForm: {
xybh: '',
nd: ''
},
// ==================== 数据表格 ====================
// 数据来源:grades 课程成绩 / process 课程过程成绩
activeTab: 'grades',
loading: false,
gradesData: [],
processGradesData: [],
pagination: { pageNum: 1, pageSize: 20, total: 0 },
// ==================== 导出 ====================
exportLoading: false
}
},
watch: {
activeTab() {
this.handleSearch()
}
},
mounted() {
this.loadData()
},
methods: {
/** 组装查询参数,仅传非空值 */
buildQueryParams(params) {
const xybh = this.queryForm.xybh && this.queryForm.xybh.trim()
const nd = this.queryForm.nd && this.queryForm.nd.trim()
if (xybh) params.xybh = xybh
if (nd) params.nd = nd
},
loadData() {
this.loading = true
const params = {
pageNum: this.pagination.pageNum,
pageSize: this.pagination.pageSize
}
this.buildQueryParams(params)
const request = this.activeTab === 'grades' ? listStudentGrades(params) : listStudentProcessGrades(params)
request.then(res => {
const data = res.data || {}
const records = data.records || []
if (this.activeTab === 'grades') {
this.gradesData = records
} else {
this.processGradesData = records
}
this.pagination.total = data.total || 0
this.loading = false
}).catch(() => {
if (this.activeTab === 'grades') {
this.gradesData = []
} else {
this.processGradesData = []
}
this.pagination.total = 0
this.loading = false
})
},
handleSearch() {
this.pagination.pageNum = 1
this.loadData()
},
handlePageChange(page) {
this.pagination.pageNum = page
this.loadData()
},
handleSizeChange(size) {
this.pagination.pageSize = size
this.pagination.pageNum = 1
this.loadData()
},
/** 通用 blob 下载 */
downloadBlob(blob, filename) {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
},
/** 校验导出所需学员编号 */
requireXybh(exportName) {
const xybh = this.queryForm.xybh && this.queryForm.xybh.trim()
if (!xybh) {
this.$message.warning('导出' + exportName + '前请先输入学员编号')
return null
}
return xybh
},
/** 导出课程成绩 Excel */
handleExportGrades() {
const xybh = this.requireXybh('课程成绩')
if (!xybh) return
this.exportLoading = true
exportStudentGrades(xybh).then(res => {
this.downloadBlob(res, `课程成绩_${xybh}.xls`)
this.$message.success('课程成绩导出成功')
}).catch(() => {}).finally(() => {
this.exportLoading = false
})
},
/** 导出课程过程成绩 Excel */
handleExportProcessGrades() {
const xybh = this.requireXybh('课程过程成绩')
if (!xybh) return
this.exportLoading = true
exportStudentProcessGrades(xybh).then(res => {
this.downloadBlob(res, `课程过程成绩_${xybh}.xls`)
this.$message.success('课程过程成绩导出成功')
}).catch(() => {}).finally(() => {
this.exportLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.score-page {
padding: 20px;
.search-card {
margin-bottom: 16px;
.search-form {
.el-form-item {
margin-bottom: 0;
}
}
}
.toolbar {
margin-bottom: 16px;
}
.table-card {
.el-table {
width: 100%;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
}
</style>
@@ -0,0 +1,717 @@
<template>
<div class="app-container shift-team-page">
<!-- ==================== 1. 查询条件 ==================== -->
<el-card shadow="never" class="search-card">
<el-form ref="searchFormRef" :model="searchForm" label-width="90px" class="search-form">
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="学员队名称">
<el-input v-model="searchForm.xydmc" placeholder="请输入学员队名称" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="年级">
<el-input v-model="searchForm.nj" placeholder="请输入年级" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="专业代号">
<el-input v-model="searchForm.zydh" placeholder="请输入专业代号" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="学员队类型">
<el-input v-model="searchForm.xydlx" placeholder="请输入学员队类型" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="任务类别">
<el-input v-model="searchForm.rwlb" placeholder="请输入任务类别" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="虚实类型">
<el-select v-model="searchForm.xslx" placeholder="请选择虚实类型" clearable class="w-full">
<el-option label="虚" :value="0" />
<el-option label="实" :value="1" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row class="search-actions-row">
<el-col :span="24" class="search-actions">
<el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 工具栏新增 / 导入 / 导出 ==================== -->
<div class="toolbar">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增学员队</el-button>
<el-button icon="el-icon-download" @click="handleExport">导出 Excel</el-button>
<el-button icon="el-icon-download" @click="handleDownloadTemplate">模板下载</el-button>
<div class="file-input">
<input ref="fileInputRef" type="file" accept=".xls,.xlsx" style="display: none" @change="handleFileChange" />
<el-button icon="el-icon-folder-opened" @click="handleChooseFile">选择文件</el-button>
<span class="file-name">{{ fileName }}</span>
<el-button type="primary" icon="el-icon-upload2" :loading="uploading" @click="handleUpload">上传数据</el-button>
</div>
</div>
<!-- ==================== 3. 列表 ==================== -->
<el-card shadow="never" class="table-card">
<div class="table-header">
<span class="table-title">教学班次列表</span>
</div>
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row>
<el-table-column prop="xydbh" label="学员队编号" min-width="120" show-overflow-tooltip />
<el-table-column prop="xydmc" label="学员队名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="nj" label="年级" width="90" align="center" />
<el-table-column prop="zydh" label="专业代号" min-width="110" show-overflow-tooltip />
<el-table-column prop="rwlb" label="任务类别" width="100" align="center" />
<el-table-column label="虚实类型" width="90" align="center">
<template slot-scope="{ row }">
<el-tag :type="Number(row.xslx) === 1 ? 'success' : 'info'" size="mini">
{{ Number(row.xslx) === 1 ? '实' : '虚' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="在校状态" width="90" align="center">
<template slot-scope="{ row }">
<el-tag :type="Number(row.zxzt) === 1 ? 'success' : 'danger'" size="mini">
{{ Number(row.zxzt) === 1 ? '在校' : '毕业' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="xydrs" label="人数" width="80" align="center" />
<el-table-column label="入学日期" width="110" align="center">
<template slot-scope="{ row }">{{ formatDate(row.rxrq) }}</template>
</el-table-column>
<el-table-column label="毕业日期" width="110" align="center">
<template slot-scope="{ row }">{{ formatDate(row.byrq) }}</template>
</el-table-column>
<el-table-column label="操作" width="190" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" icon="el-icon-view" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" icon="el-icon-edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination-wrapper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 4. 新增/编辑对话框 ==================== -->
<el-dialog
:visible="formDialogVisible"
:title="dialogTitle"
width="760px"
:close-on-click-modal="false"
@update:visible="val => formDialogVisible = val"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="130px" class="form-dialog-form">
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="学员队编号" prop="xydbh">
<el-input v-model="form.xydbh" placeholder="请输入学员队编号" :disabled="!isAdd" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学员队名称" prop="xydmc">
<el-input v-model="form.xydmc" placeholder="请输入学员队名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学员队人数" prop="xydrs">
<el-input-number v-model="form.xydrs" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年级">
<el-input v-model="form.nj" placeholder="请输入年级" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业代号" prop="zydh">
<el-input v-model="form.zydh" placeholder="请输入专业代号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业教室编号">
<el-input v-model="form.zyjsbh" placeholder="请输入专业教室编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学员队类型" prop="xydlx">
<el-input v-model="form.xydlx" placeholder="请输入学员队类型" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="节次类别" prop="jclb">
<el-input v-model="form.jclb" placeholder="请输入节次类别" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="系统模式" prop="xtms">
<el-input v-model="form.xtms" placeholder="请输入系统模式" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="简称">
<el-input v-model="form.jc" placeholder="请输入简称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属单位">
<el-input v-model="form.ssdw" placeholder="请输入所属单位" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序号">
<el-input-number v-model="form.xh" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">培训与状态</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="任务类别" prop="rwlb">
<el-input v-model="form.rwlb" placeholder="请输入任务类别" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="虚实类型" prop="xslx">
<el-select v-model="form.xslx" class="w-full">
<el-option label="虚" :value="0" />
<el-option label="实" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主体培训任务" prop="ztpxrw">
<el-select v-model="form.ztpxrw" class="w-full">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训任务标识号">
<el-input v-model="form.pxrwbsh" placeholder="请输入培训任务标识号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="在校状态">
<el-select v-model="form.zxzt" class="w-full">
<el-option label="在校" :value="1" />
<el-option label="毕业" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入学日期" prop="rxrq">
<el-date-picker v-model="form.rxrq" type="date" value-format="yyyy-MM-dd" placeholder="请选择入学日期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="毕业日期" prop="byrq">
<el-date-picker v-model="form.byrq" type="date" value-format="yyyy-MM-dd" placeholder="请选择毕业日期" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="JSON字段">
<el-input v-model="form.jsonzd" placeholder="请输入 JSON 字段" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.bz" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="formDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="formSaving" @click="handleFormSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 5. 详情对话框 ==================== -->
<el-dialog title="学员队详情" :visible="detailDialogVisible" width="720px" :close-on-click-modal="false"
@update:visible="val => detailDialogVisible = val">
<div v-loading="detailLoading" class="detail-body">
<el-descriptions :column="2" border>
<el-descriptions-item label="学员队编号">{{ detailForm.xydbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队名称">{{ detailForm.xydmc || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队人数">{{ fmtValue(detailForm.xydrs) }}</el-descriptions-item>
<el-descriptions-item label="年级">{{ detailForm.nj || '-' }}</el-descriptions-item>
<el-descriptions-item label="专业代号">{{ detailForm.zydh || '-' }}</el-descriptions-item>
<el-descriptions-item label="专业教室编号">{{ detailForm.zyjsbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队类型">{{ detailForm.xydlx || '-' }}</el-descriptions-item>
<el-descriptions-item label="节次类别">{{ detailForm.jclb || '-' }}</el-descriptions-item>
<el-descriptions-item label="系统模式">{{ detailForm.xtms || '-' }}</el-descriptions-item>
<el-descriptions-item label="简称">{{ detailForm.jc || '-' }}</el-descriptions-item>
<el-descriptions-item label="所属单位">{{ detailForm.ssdw || '-' }}</el-descriptions-item>
<el-descriptions-item label="序号">{{ fmtValue(detailForm.xh) }}</el-descriptions-item>
<el-descriptions-item label="任务类别">{{ detailForm.rwlb || '-' }}</el-descriptions-item>
<el-descriptions-item label="虚实类型">
<el-tag :type="Number(detailForm.xslx) === 1 ? 'success' : 'info'" size="mini">
{{ Number(detailForm.xslx) === 1 ? '实' : '虚' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="主体培训任务">
<el-tag :type="Number(detailForm.ztpxrw) === 1 ? 'success' : 'info'" size="mini">
{{ Number(detailForm.ztpxrw) === 1 ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="培训任务标识号">{{ detailForm.pxrwbsh || '-' }}</el-descriptions-item>
<el-descriptions-item label="在校状态">
<el-tag :type="Number(detailForm.zxzt) === 1 ? 'success' : 'danger'" size="mini">
{{ Number(detailForm.zxzt) === 1 ? '在校' : '毕业' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="入学日期">{{ detailForm.rxrq || '-' }}</el-descriptions-item>
<el-descriptions-item label="毕业日期">{{ detailForm.byrq || '-' }}</el-descriptions-item>
<el-descriptions-item label="JSON字段" :span="2">{{ detailForm.jsonzd || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailForm.bz || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { saveAs } from 'file-saver'
import {
listTeam,
getTeam,
addTeam,
updateTeam,
delTeam,
importTeam,
exportTeam
} from '@/api/studentRecords/team'
export default {
name: 'ShiftTeamIndex',
data() {
return {
// ==================== 查询条件 ====================
searchForm: {
xydmc: '',
nj: '',
zydh: '',
xydlx: '',
rwlb: '',
xslx: undefined
},
// ==================== 文件导入 ====================
selectedFile: null,
uploading: false,
// ==================== 列表数据 ====================
loading: false,
tableData: [],
total: 0,
pageNum: 1,
pageSize: 20,
// ==================== 新增/编辑 ====================
formDialogVisible: false,
dialogTitle: '新增学员队',
isAdd: true,
formSaving: false,
form: this.createEmptyForm(),
formRules: {
xydbh: [{ required: true, message: '请输入学员队编号', trigger: 'blur' }],
xydmc: [{ required: true, message: '请输入学员队名称', trigger: 'blur' }],
xydrs: [{ required: true, message: '请输入学员队人数', trigger: 'change' }],
zydh: [{ required: true, message: '请输入专业代号', trigger: 'blur' }],
rwlb: [{ required: true, message: '请输入任务类别', trigger: 'blur' }],
xslx: [{ required: true, message: '请选择虚实类型', trigger: 'change' }],
rxrq: [{ required: true, message: '请选择入学日期', trigger: 'change' }],
byrq: [{ required: true, message: '请选择毕业日期', trigger: 'change' }],
xydlx: [{ required: true, message: '请输入学员队类型', trigger: 'blur' }],
jclb: [{ required: true, message: '请输入节次类别', trigger: 'blur' }],
xtms: [{ required: true, message: '请输入系统模式', trigger: 'blur' }],
ztpxrw: [{ required: true, message: '请选择主体培训任务', trigger: 'change' }]
},
// ==================== 详情 ====================
detailDialogVisible: false,
detailLoading: false,
detailForm: {}
}
},
computed: {
fileName() {
return (this.selectedFile && this.selectedFile.name) || '未选择任何文件'
}
},
mounted() {
this.fetchList()
},
methods: {
createEmptyForm() {
return {
xydbh: '',
xydmc: '',
xydrs: undefined,
nj: '',
zydh: '',
zyjsbh: '',
bz: '',
zxzt: 1,
xh: undefined,
rwlb: '',
xslx: 1,
rxrq: '',
byrq: '',
xydlx: '',
pxrwbsh: '',
jclb: '',
xtms: '',
jsonzd: '',
jc: '',
ssdw: '',
ztpxrw: 1
}
},
// ==================== 日期/数值展示 ====================
formatDate(val) {
if (!val) return ''
return String(val).substring(0, 10)
},
fmtValue(val) {
return val === null || val === undefined || val === '' ? '-' : val
},
/** 构建查询条件(不含分页),供列表查询与导出共用 */
buildQuery() {
const params = {}
;['xydmc', 'nj', 'zydh', 'xydlx', 'rwlb'].forEach(key => {
const value = this.searchForm[key]
if (value !== '' && value !== null && value !== undefined) {
params[key] = String(value).trim()
}
})
if (this.searchForm.xslx !== undefined && this.searchForm.xslx !== '' && this.searchForm.xslx !== null) {
params.xslx = this.searchForm.xslx
}
return params
},
// ==================== 查询列表 ====================
fetchList() {
this.loading = true
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
...this.buildQuery()
}
listTeam(params).then(response => {
const data = response.data || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handleReset() {
this.$refs.searchFormRef.resetFields()
this.pageNum = 1
this.fetchList()
},
handlePageChange(current) {
this.pageNum = current
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
// ==================== 新增/编辑 ====================
handleAdd() {
this.isAdd = true
this.dialogTitle = '新增学员队'
this.form = this.createEmptyForm()
this.formDialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
handleEdit(row) {
this.isAdd = false
this.dialogTitle = '编辑学员队'
this.form = this.createEmptyForm()
this.formDialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
getTeam(row.xydbh).then(response => {
const d = response.data || {}
this.form = {
xydbh: d.xydbh || '',
xydmc: d.xydmc || '',
xydrs: d.xydrs !== null && d.xydrs !== undefined ? d.xydrs : undefined,
nj: d.nj || '',
zydh: d.zydh || '',
zyjsbh: d.zyjsbh || '',
bz: d.bz || '',
zxzt: d.zxzt !== null && d.zxzt !== undefined ? Number(d.zxzt) : 1,
xh: d.xh !== null && d.xh !== undefined ? d.xh : undefined,
rwlb: d.rwlb || '',
xslx: d.xslx !== null && d.xslx !== undefined ? Number(d.xslx) : 1,
rxrq: this.formatDate(d.rxrq),
byrq: this.formatDate(d.byrq),
xydlx: d.xydlx || '',
pxrwbsh: d.pxrwbsh || '',
jclb: d.jclb || '',
xtms: d.xtms || '',
jsonzd: d.jsonzd || '',
jc: d.jc || '',
ssdw: d.ssdw || '',
ztpxrw: d.ztpxrw !== null && d.ztpxrw !== undefined ? Number(d.ztpxrw) : 1
}
}).catch(() => {})
},
buildPayload() {
const f = this.form
const payload = {
xydbh: f.xydbh,
xydmc: f.xydmc,
zydh: f.zydh,
rwlb: f.rwlb,
xydlx: f.xydlx,
jclb: f.jclb,
xtms: f.xtms,
zxzt: Number(f.zxzt),
xslx: Number(f.xslx),
ztpxrw: Number(f.ztpxrw)
}
// 数值字段
if (f.xydrs !== '' && f.xydrs !== null && f.xydrs !== undefined) payload.xydrs = Number(f.xydrs)
if (f.xh !== '' && f.xh !== null && f.xh !== undefined) payload.xh = Number(f.xh)
// 日期字段
if (f.rxrq) payload.rxrq = f.rxrq
if (f.byrq) payload.byrq = f.byrq
// 其余文本字段非空才传
;['nj', 'zyjsbh', 'pxrwbsh', 'jsonzd', 'jc', 'ssdw', 'bz'].forEach(key => {
if (f[key] !== '' && f[key] !== null && f[key] !== undefined) {
payload[key] = String(f[key]).trim()
}
})
return payload
},
handleFormSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.formSaving = true
const payload = this.buildPayload()
const request = this.isAdd ? addTeam(payload) : updateTeam(payload)
request.then(() => {
this.$message.success(this.isAdd ? '新增成功' : '修改成功')
this.formDialogVisible = false
this.fetchList()
}).catch(() => {
}).finally(() => {
this.formSaving = false
})
})
},
// ==================== 删除 ====================
handleDelete(row) {
this.$confirm(`确定要删除「${row.xydmc || row.xydbh}」吗?`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return delTeam(row.xydbh)
}).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch(() => {})
},
// ==================== 详情 ====================
handleDetail(row) {
this.detailDialogVisible = true
this.detailLoading = true
this.detailForm = {}
getTeam(row.xydbh).then(response => {
this.detailForm = response.data || {}
this.detailLoading = false
}).catch(() => {
this.detailLoading = false
})
},
// ==================== 导出 ====================
handleExport() {
exportTeam(this.buildQuery()).then(blob => {
saveAs(blob, '班次(学员队)管理.xlsx')
this.$message.success('导出成功')
}).catch(() => {})
},
// ==================== 模板下载 / 导入 ====================
handleDownloadTemplate() {
// 后端暂未提供模板下载接口,仅作提示
this.$message.info('后端暂未提供该接口')
},
handleChooseFile() {
this.$refs.fileInputRef && this.$refs.fileInputRef.click()
},
handleFileChange(e) {
const input = e.target
this.selectedFile = (input.files && input.files[0]) || null
},
handleUpload() {
if (!this.selectedFile) {
this.$message.warning('请先选择文件')
return
}
this.uploading = true
importTeam(this.selectedFile).then(res => {
const count = res && res.data
if (count !== null && count !== undefined) {
this.$message.success(`导入成功,共 ${count} 条记录`)
} else {
this.$message.success((res && res.msg) || '导入成功')
}
this.selectedFile = null
this.$refs.fileInputRef && (this.$refs.fileInputRef.value = '')
this.pageNum = 1
this.fetchList()
}).catch(() => {}).finally(() => {
this.uploading = false
})
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.shift-team-page {
.search-card {
margin-bottom: 16px;
.search-form {
.w-full {
width: 100%;
}
.search-actions-row {
margin-top: 2px;
border-top: 1px dashed #ebeef5;
padding-top: 14px;
}
.search-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
}
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 16px;
.file-input {
display: flex;
align-items: center;
gap: 8px;
.file-name {
font-size: 12px;
color: #909399;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.table-card {
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.table-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
.form-dialog-form {
max-height: 62vh;
overflow-y: auto;
padding-right: 6px;
::v-deep .el-divider--horizontal {
margin: 4px 0 16px;
}
}
.text-danger {
color: #f56c6c;
padding: 0;
}
}
</style>
@@ -0,0 +1,494 @@
<template>
<div class="app-container status-change-page">
<!-- ==================== 1. 查询条件 ==================== -->
<el-card shadow="never" class="search-card">
<el-form ref="searchFormRef" :model="searchForm" label-width="90px" class="search-form">
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="编号">
<el-input v-model="searchForm.bh" placeholder="请输入编号" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="学员编号">
<el-input v-model="searchForm.xybh" placeholder="请输入学员编号" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="申请类型">
<el-select v-model="searchForm.sqlx" placeholder="请选择申请类型" clearable class="w-full">
<el-option v-for="item in applyTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="状态">
<el-select v-model="searchForm.zt" placeholder="请选择状态" clearable class="w-full">
<el-option v-for="(item, key) in statusMap" :key="key" :label="item.label" :value="Number(key)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-actions">
<div class="search-buttons">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 列表 ==================== -->
<el-card shadow="never" class="table-card">
<div class="table-header">
<span class="table-title">学籍异动申请列表</span>
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增学籍异动申请</el-button>
</div>
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="bh" label="编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="xybh" label="学员编号" min-width="120" show-overflow-tooltip />
<el-table-column prop="sqlx" label="申请类型" width="100" align="center" />
<el-table-column prop="sy" label="事由" min-width="140" show-overflow-tooltip />
<el-table-column prop="bz" label="备注" min-width="140" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center">
<template slot-scope="{ row }">
<el-tag :type="statusTagType(row.zt)" size="mini" effect="light">{{ statusLabel(row.zt) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="提交时间" width="160" align="center">
<template slot-scope="{ row }">{{ row.tjsj ? fmtDateTime(row.tjsj) : '-' }}</template>
</el-table-column>
<el-table-column label="创建时间" width="160" align="center">
<template slot-scope="{ row }">{{ row.cjsj ? fmtDateTime(row.cjsj) : '-' }}</template>
</el-table-column>
<el-table-column label="修改时间" width="160" align="center">
<template slot-scope="{ row }">{{ row.xgsj ? fmtDateTime(row.xgsj) : '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="230" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button v-if="!row.tjsj" type="text" size="small" icon="el-icon-upload2" @click="handleSubmit(row)">提交</el-button>
<el-button v-else type="text" size="small" icon="el-icon-check" disabled>已提交</el-button>
<el-button type="text" size="small" icon="el-icon-view" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" icon="el-icon-edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && tableData.length === 0" description="暂无学籍异动申请数据" :image-size="60" />
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination-wrapper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 3. 新增/编辑对话框 ==================== -->
<el-dialog
:visible="formDialogVisible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
@update:visible="val => formDialogVisible = val"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="110px" class="add-form">
<el-form-item label="编号" prop="bh">
<el-input v-model="form.bh" placeholder="请输入编号" :disabled="!isAdd" />
</el-form-item>
<el-form-item label="学员编号" prop="xybh">
<el-input v-model="form.xybh" placeholder="请输入学员编号" />
</el-form-item>
<el-form-item label="申请类型" prop="sqlx">
<el-select v-model="form.sqlx" placeholder="请选择申请类型" class="w-full">
<el-option v-for="item in applyTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="事由" prop="sy">
<el-input v-model="form.sy" type="textarea" :rows="3" placeholder="请输入事由" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.bz" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="状态" prop="zt">
<el-select v-model="form.zt" placeholder="请选择状态" class="w-full">
<el-option v-for="(item, key) in statusMap" :key="key" :label="item.label" :value="Number(key)" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="formDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="formSaving" @click="handleFormSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 4. 详情对话框 ==================== -->
<el-dialog title="学籍异动申请详情" :visible="detailDialogVisible" width="760px" :close-on-click-modal="false"
@update:visible="val => detailDialogVisible = val">
<div v-loading="detailLoading" class="detail-body">
<el-descriptions :column="2" border>
<el-descriptions-item label="编号">{{ detailForm.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员编号">{{ detailForm.xybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请类型">{{ detailForm.sqlx || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTagType(detailForm.zt)" size="mini">{{ statusLabel(detailForm.zt) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="事由" :span="2">{{ detailForm.sy || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailForm.bz || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailForm.cjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="修改时间">{{ detailForm.xgsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ detailForm.tjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队审核意见">{{ detailForm.xydshyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队干部编号">{{ detailForm.xydgbbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队干部审核时间">{{ detailForm.xydshsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="管理员账户编号">{{ detailForm.glyzhbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="管理员审核意见">{{ detailForm.glyshyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="管理员审核时间">{{ detailForm.glyshsj || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listApplication,
getApplication,
addApplication,
updateApplication,
delApplication
} from '@/api/studentRecords/application'
export default {
name: 'StatusChangeIndex',
data() {
return {
// ==================== 申请类型选项(学籍异动) ====================
applyTypeOptions: ['休学', '复学', '退学', '转学', '留级', '转专业'],
// ==================== 状态映射:0-待审核 1-已审核 2-驳回等 ====================
statusMap: {
0: { label: '待审核', type: 'warning' },
1: { label: '已审核', type: 'success' },
2: { label: '驳回', type: 'danger' }
},
// ==================== 查询条件 ====================
searchForm: {
bh: '',
xybh: '',
sqlx: '',
zt: undefined
},
// ==================== 列表数据 ====================
loading: false,
tableData: [],
total: 0,
pageNum: 1,
pageSize: 20,
// ==================== 新增/编辑 ====================
formDialogVisible: false,
dialogTitle: '新增学籍异动申请',
isAdd: true,
formSaving: false,
form: this.createEmptyForm(),
formRules: {
bh: [{ required: true, message: '请输入编号', trigger: 'blur' }],
xybh: [{ required: true, message: '请输入学员编号', trigger: 'blur' }],
sqlx: [{ required: true, message: '请选择申请类型', trigger: 'change' }],
sy: [{ required: true, message: '请输入事由', trigger: 'blur' }],
zt: [{ required: true, message: '请选择状态', trigger: 'change' }]
},
// ==================== 详情 ====================
detailDialogVisible: false,
detailLoading: false,
detailForm: {}
}
},
mounted() {
this.fetchList()
},
methods: {
createEmptyForm() {
return {
bh: '',
xybh: '',
sqlx: '',
sy: '',
bz: '',
zt: 0
}
},
// ==================== 状态展示 ====================
// 统一格式化后端 LocalDateTime(去除 T、截断到秒)
fmtDateTime(val) {
if (val === null || val === undefined || val === '') return ''
return String(val).replace('T', ' ').slice(0, 19)
},
statusLabel(zt) {
const item = this.statusMap[zt]
return item ? item.label : (zt !== null && zt !== undefined ? String(zt) : '-')
},
statusTagType(zt) {
const item = this.statusMap[zt]
return item ? item.type : 'info'
},
// ==================== 查询列表 ====================
fetchList() {
this.loading = true
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize
}
// 等值筛选,留空不传
;['bh', 'xybh', 'sqlx'].forEach(key => {
const value = this.searchForm[key]
if (value !== '' && value !== null && value !== undefined) {
params[key] = String(value).trim()
}
})
if (this.searchForm.zt !== undefined && this.searchForm.zt !== '' && this.searchForm.zt !== null) {
params.zt = this.searchForm.zt
}
listApplication(params).then(response => {
const data = response.data || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handleReset() {
this.$refs.searchFormRef.resetFields()
this.pageNum = 1
this.fetchList()
},
handlePageChange(current) {
this.pageNum = current
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
// ==================== 新增/编辑 ====================
handleAdd() {
this.isAdd = true
this.dialogTitle = '新增学籍异动申请'
this.form = this.createEmptyForm()
this.formDialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
handleEdit(row) {
this.isAdd = false
this.dialogTitle = '编辑学籍异动申请'
this.form = this.createEmptyForm()
this.formDialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
getApplication(row.bh).then(response => {
const data = response.data || {}
this.form = {
bh: data.bh || '',
xybh: data.xybh || '',
sqlx: data.sqlx || '',
sy: data.sy || '',
bz: data.bz || '',
zt: data.zt !== null && data.zt !== undefined ? Number(data.zt) : 0
}
}).catch(() => {})
},
buildPayload() {
const f = this.form
const payload = {
bh: f.bh,
xybh: f.xybh,
sqlx: f.sqlx,
sy: f.sy,
zt: Number(f.zt)
}
// 备注留空则移除
if (f.bz !== '' && f.bz !== null && f.bz !== undefined) {
payload.bz = f.bz
}
return payload
},
handleFormSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.formSaving = true
const payload = this.buildPayload()
const request = this.isAdd ? addApplication(payload) : updateApplication(payload)
request.then(() => {
this.$message.success(this.isAdd ? '新增成功' : '修改成功')
this.formDialogVisible = false
this.fetchList()
}).catch(() => {
}).finally(() => {
this.formSaving = false
})
})
},
// ==================== 提交 ====================
// 无独立提交接口,通过编辑接口写入提交时间 tjsj(zt 保持原值不变)
formatNow() {
const d = new Date()
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
},
handleSubmit(row) {
this.$confirm(`确定要提交编号为「${row.bh}」的学籍异动申请吗?`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const payload = {
bh: row.bh,
xybh: row.xybh,
sqlx: row.sqlx,
sy: row.sy,
zt: row.zt !== null && row.zt !== undefined ? Number(row.zt) : 0,
tjsj: this.formatNow()
}
if (row.bz) {
payload.bz = row.bz
}
return updateApplication(payload)
}).then(() => {
this.$message.success('提交成功')
this.fetchList()
}).catch(() => {})
},
// ==================== 删除 ====================
handleDelete(row) {
this.$confirm(`确定要删除编号为「${row.bh}」的学籍异动申请吗?`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return delApplication(row.bh)
}).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch(() => {})
},
// ==================== 详情 ====================
handleDetail(row) {
this.detailDialogVisible = true
this.detailLoading = true
this.detailForm = {}
getApplication(row.bh).then(response => {
this.detailForm = response.data || {}
this.detailLoading = false
}).catch(() => {
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.status-change-page {
.search-card {
margin-bottom: 16px;
}
.search-form {
.w-full {
width: 100%;
}
.search-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 8px;
.search-tip {
font-size: 12px;
color: #909399;
}
.search-buttons {
display: flex;
gap: 12px;
}
}
}
.table-card {
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.table-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
.add-form {
max-height: 60vh;
overflow-y: auto;
padding-right: 4px;
}
.text-danger {
color: #f56c6c;
padding: 0;
}
}
</style>
@@ -0,0 +1,256 @@
<template>
<div class="app-container warning-result-page">
<!-- ==================== 1. 查询条件 ==================== -->
<el-card shadow="never" class="search-card">
<el-form ref="searchFormRef" :model="searchForm" label-width="110px" class="search-form">
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="年度">
<el-input v-model="searchForm.nd" placeholder="请输入年度" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="预警条件编号">
<el-input v-model="searchForm.yjtjbh" placeholder="请输入预警条件编号" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="学员编号">
<el-input v-model="searchForm.xybh" placeholder="请输入学员编号" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
<el-form-item label="发布">
<el-select v-model="searchForm.fb" placeholder="请选择发布状态" clearable class="w-full">
<el-option label="未发布" :value="0" />
<el-option label="已发布" :value="1" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row class="search-actions-row">
<el-col :span="24" class="search-actions">
<el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 列表 ==================== -->
<el-card shadow="never" class="table-card">
<div class="table-header">
<span class="table-title">预警结果列表</span>
</div>
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row>
<el-table-column prop="bh" label="编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="nd" label="年度" width="90" align="center" />
<el-table-column prop="yjtjbh" label="预警条件编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="xybh" label="学员编号" min-width="120" show-overflow-tooltip />
<el-table-column prop="cjsj" label="创建时间" width="160" show-overflow-tooltip />
<el-table-column label="发布" width="90" align="center">
<template slot-scope="{ row }">
<el-tag :type="Number(row.fb) === 1 ? 'success' : 'info'" size="mini">
{{ Number(row.fb) === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fbsj" label="发布时间" width="160" show-overflow-tooltip />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" icon="el-icon-view" @click="handleDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination-wrapper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 3. 详情对话框 ==================== -->
<el-dialog title="预警结果详情" :visible="detailDialogVisible" width="560px" :close-on-click-modal="false"
@update:visible="val => detailDialogVisible = val">
<div v-loading="detailLoading" class="detail-body">
<el-descriptions :column="2" border>
<el-descriptions-item label="编号">{{ detailForm.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="年度">{{ detailForm.nd || '-' }}</el-descriptions-item>
<el-descriptions-item label="预警条件编号">{{ detailForm.yjtjbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员编号">{{ detailForm.xybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailForm.cjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ detailForm.fbsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="发布">
<el-tag :type="Number(detailForm.fb) === 1 ? 'success' : 'info'" size="mini">
{{ Number(detailForm.fb) === 1 ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listWarningResult,
getWarningResult
} from '@/api/studentRecords/warningResult'
export default {
name: 'WarningResult',
data() {
return {
// ==================== 查询条件 ====================
searchForm: {
nd: '',
yjtjbh: '',
xybh: '',
fb: undefined
},
// ==================== 列表数据 ====================
loading: false,
tableData: [],
total: 0,
pageNum: 1,
pageSize: 20,
// ==================== 详情 ====================
detailDialogVisible: false,
detailLoading: false,
detailForm: {}
}
},
created() {
this.fetchList()
},
methods: {
// ==================== 查询列表 ====================
fetchList() {
this.loading = true
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize
}
// 文本条件非空时才传
;['nd', 'yjtjbh', 'xybh'].forEach(key => {
const value = this.searchForm[key]
if (value !== '' && value !== null && value !== undefined) {
params[key] = String(value).trim()
}
})
// 发布:等值筛选(选中时才传)
if (this.searchForm.fb !== undefined && this.searchForm.fb !== '' && this.searchForm.fb !== null) {
params.fb = this.searchForm.fb
}
listWarningResult(params).then(response => {
const data = response.data || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handleReset() {
this.$refs.searchFormRef.resetFields()
this.pageNum = 1
this.fetchList()
},
handlePageChange(current) {
this.pageNum = current
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
// ==================== 详情 ====================
handleDetail(row) {
this.detailDialogVisible = true
this.detailLoading = true
this.detailForm = {}
getWarningResult(row.bh).then(response => {
this.detailForm = response.data || {}
this.detailLoading = false
}).catch(() => {
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.warning-result-page {
.search-card {
margin-bottom: 16px;
}
.search-form {
.w-full {
width: 100%;
}
.search-actions-row {
margin-top: 2px;
border-top: 1px dashed #ebeef5;
padding-top: 14px;
}
.search-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
}
.table-card {
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.table-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
}
</style>
+380
View File
@@ -0,0 +1,380 @@
<template>
<div class="app-container system-config">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="参数名称" prop="configName">
<el-input
v-model="queryParams.configName"
placeholder="请输入参数名称"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="参数键名" prop="configKey">
<el-input
v-model="queryParams.configKey"
placeholder="请输入参数键名"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="系统内置" prop="configType">
<el-select v-model="queryParams.configType" placeholder="系统内置" clearable>
<el-option
v-for="dict in dict.type.sys_yes_no"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:config:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['system:config:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:config:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:config:export']"
>导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-refresh"
size="mini"
@click="handleRefreshCache"
v-hasPermi="['system:config:remove']"
>刷新缓存</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="configList" :height="tableHeight" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="参数主键" align="center" prop="configId" />
<el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
<el-table-column label="参数键名" align="center" prop="configKey" :show-overflow-tooltip="true" />
<el-table-column label="参数键值" align="center" prop="configValue" :show-overflow-tooltip="true" />
<el-table-column label="系统内置" align="center" prop="configType">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_yes_no" :value="scope.row.configType"/>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:config:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:config:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="参数名称" prop="configName">
<el-input v-model="form.configName" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数键名" prop="configKey">
<el-input v-model="form.configKey" placeholder="请输入参数键名" />
</el-form-item>
<el-form-item label="参数键值" prop="configValue">
<el-input v-model="form.configValue" type="textarea" placeholder="请输入参数键值" />
</el-form-item>
<el-form-item label="系统内置" prop="configType">
<el-radio-group v-model="form.configType">
<el-radio
v-for="dict in dict.type.sys_yes_no"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from "@/api/system/config"
export default {
name: "Config",
dicts: ['sys_yes_no'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 表格高度
tableHeight: window.innerHeight - 240,
// 总条数
total: 0,
// 参数表格数据
configList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 日期范围
dateRange: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
configName: undefined,
configKey: undefined,
configType: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
configName: [
{ required: true, message: "参数名称不能为空", trigger: "blur" }
],
configKey: [
{ required: true, message: "参数键名不能为空", trigger: "blur" }
],
configValue: [
{ required: true, message: "参数键值不能为空", trigger: "blur" }
]
}
}
},
created() {
this.getList()
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
watch: {
showSearch() {
this.$nextTick(() => {
this.getTableHeight()
})
}
},
methods: {
getTableHeight() {
const offset = this.showSearch ? 240 : 190
this.tableHeight = window.innerHeight - offset
},
/** 查询参数列表 */
getList() {
this.loading = true
listConfig(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.configList = response.rows
this.total = response.total
this.loading = false
}
)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
configId: undefined,
configName: undefined,
configKey: undefined,
configValue: undefined,
configType: "Y",
remark: undefined
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm("queryForm")
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加参数"
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.configId)
this.single = selection.length != 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const configId = row.configId || this.ids
getConfig(configId).then(response => {
this.form = response.data
this.open = true
this.title = "修改参数"
})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.configId != undefined) {
updateConfig(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addConfig(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const configIds = row.configId || this.ids
this.$modal.confirm('是否确认删除参数编号为"' + configIds + '"的数据项?').then(function() {
return delConfig(configIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('system/config/export', {
...this.queryParams
}, `config_${new Date().getTime()}.xlsx`)
},
/** 刷新缓存按钮操作 */
handleRefreshCache() {
refreshCache().then(() => {
this.$modal.msgSuccess("刷新成功")
})
}
}
}
</script>
<style scoped>
.system-config ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.system-config ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.system-config ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.system-config ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
+203
View File
@@ -0,0 +1,203 @@
<template>
<el-drawer :title="drawerTitle" :visible.sync="localVisible" direction="rtl" size="700px" append-to-body>
<div class="drawer-wrap">
<div v-if="loading" class="drawer-loading">
<i class="el-icon-loading"></i>
<span>加载中...</span>
</div>
<div v-else-if="!dataList.length" class="drawer-empty">
<i class="el-icon-document"></i>
<div>暂无字典数据</div>
</div>
<template v-else>
<el-row :gutter="12" class="stat-row">
<el-col :span="disabledCount > 0 ? 8 : 12">
<div class="stat-card">
<div class="stat-num">{{ dataList.length }}</div>
<div class="stat-label">共计条目</div>
</div>
</el-col>
<el-col :span="disabledCount > 0 ? 8 : 12">
<div class="stat-card">
<div class="stat-num success">{{ normalCount }}</div>
<div class="stat-label">正常</div>
</div>
</el-col>
<el-col :span="8" v-if="disabledCount > 0">
<div class="stat-card">
<div class="stat-num danger">{{ disabledCount }}</div>
<div class="stat-label">停用</div>
</div>
</el-col>
</el-row>
<div v-for="item in dataList" :key="item.dictCode" class="dict-item">
<div class="dict-cell">
<div class="dict-cell-key">标签</div>
<div class="dict-cell-val">
<el-tag v-if="item.listClass && item.listClass !== 'default'" :type="item.listClass === 'primary' ? '' : item.listClass" size="small">{{ item.dictLabel }}</el-tag>
<span v-else>{{ item.dictLabel }}</span>
</div>
</div>
<div class="dict-cell">
<div class="dict-cell-key">键值</div>
<div class="dict-cell-val">{{ item.dictValue }}</div>
</div>
<div class="dict-cell">
<div class="dict-cell-key">状态</div>
<div class="dict-cell-val">
<el-tag :type="item.status === '0' ? 'success' : 'danger'" size="small">
{{ item.status === '0' ? '正常' : '停用' }}
</el-tag>
</div>
</div>
</div>
</template>
</div>
</el-drawer>
</template>
<script>
import { listData } from '@/api/system/dict/data'
export default {
props: {
visible: { type: Boolean, default: false },
row: { type: Object, default: () => ({}) }
},
data() {
return {
localVisible: false,
loading: false,
dataList: []
}
},
computed: {
drawerTitle() {
return this.row.dictName + ' ' + (this.row.dictType || '')
},
normalCount() {
return this.dataList.filter(r => r.status === '0').length
},
disabledCount() {
return this.dataList.filter(r => r.status !== '0').length
}
},
watch: {
visible(val) {
this.localVisible = val
},
localVisible(val) {
this.$emit('update:visible', val)
if (val) {
this.loadData()
} else {
this.dataList = []
}
}
},
methods: {
loadData() {
if (!this.row || !this.row.dictType) return
this.loading = true
this.dataList = []
listData({ dictType: this.row.dictType, pageSize: 100, pageNum: 1 }).then(response => {
this.dataList = response.rows || []
}).catch(() => {}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped>
.drawer-wrap {
padding: 0 20px 20px;
}
.drawer-loading {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
color: #aaa;
font-size: 13px;
gap: 8px;
}
.drawer-empty {
text-align: center;
color: #bbb;
padding: 60px 0;
font-size: 13px;
}
.drawer-empty i {
font-size: 36px;
display: block;
margin-bottom: 8px;
}
.stat-row {
margin-bottom: 16px;
}
.stat-card {
background: #f7f9fb;
border: 1px solid #e8ecf0;
border-radius: 6px;
padding: 10px 14px;
text-align: center;
}
.stat-num {
font-size: 22px;
font-weight: 700;
color: #2c3e50;
}
.stat-num.success { color: #27ae60; }
.stat-num.danger { color: #e74c3c; }
.stat-label {
font-size: 11px;
color: #95a5a6;
margin-top: 4px;
}
.dict-item {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
border: 1px solid #e8ecf0;
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
}
.dict-cell {
display: grid;
grid-template-columns: 70px 1fr;
border-right: 1px solid #f0f4f8;
}
.dict-cell:last-child {
border-right: 0;
}
.dict-cell-key {
padding: 9px 14px;
font-size: 12px;
color: #888;
background: #f7f9fb;
border-right: 1px solid #f0f4f8;
}
.dict-cell-val {
padding: 9px 14px;
font-size: 13px;
color: #2c3e50;
word-break: break-all;
display: flex;
align-items: center;
}
</style>
@@ -0,0 +1,109 @@
<template>
<el-dialog :title="`「${noticeTitle}」已读用户`" :visible.sync="visible" width="760px" top="6vh" append-to-body @close="handleClose">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" style="margin-bottom: 4px;">
<el-form-item prop="searchValue">
<el-input
v-model="queryParams.searchValue"
placeholder="登录名称 / 用户名称"
clearable
prefix-icon="el-icon-search"
style="width: 220px;"
@keyup.enter.native="handleQuery"
@clear="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
<el-form-item style="float: right; margin-right: 0;">
<span class="read-stat">
<strong>{{ total }}</strong> 人已读
</span>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="userList" size="small" stripe height="340px">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column label="登录名称" prop="userName" align="center" :show-overflow-tooltip="true" />
<el-table-column label="用户名称" prop="nickName" align="center" :show-overflow-tooltip="true" />
<el-table-column label="所属部门" prop="deptName" align="center" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" prop="phonenumber" align="center" width="120" />
<el-table-column label="阅读时间" prop="readTime" align="center" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.readTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" style="padding: 6px 0px;"/>
</el-dialog>
</template>
<script>
import { listNoticeReadUsers } from "@/api/system/notice"
export default {
name: "ReadUsers",
data() {
return {
visible: false,
loading: false,
noticeId: undefined,
noticeTitle: "",
total: 0,
userList: [],
queryParams: {
pageNum: 1,
pageSize: 10,
noticeId: undefined,
searchValue: undefined
}
}
},
methods: {
open(row) {
this.noticeId = row.noticeId
this.noticeTitle = row.noticeTitle
this.queryParams.noticeId = row.noticeId
this.queryParams.searchValue = undefined
this.queryParams.pageNum = 1
this.visible = true
this.getList()
},
getList() {
this.loading = true
listNoticeReadUsers(this.queryParams).then(res => {
this.userList = res.rows
this.total = res.total
}).finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
handleClose() {
this.userList = []
this.total = 0
this.queryParams.searchValue = undefined
}
}
}
</script>
<style scoped>
.read-stat {
font-size: 13px;
color: #606266;
line-height: 28px;
}
.read-stat strong {
color: #00875A;
font-size: 15px;
margin: 0 2px;
}
</style>
+369
View File
@@ -0,0 +1,369 @@
<template>
<div class="app-container system-notice">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="公告标题" prop="noticeTitle">
<el-input
v-model="queryParams.noticeTitle"
placeholder="请输入公告标题"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="操作人员" prop="createBy">
<el-input
v-model="queryParams.createBy"
placeholder="请输入操作人员"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="类型" prop="noticeType">
<el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable>
<el-option
v-for="dict in dict.type.sys_notice_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:notice:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['system:notice:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:notice:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange" :height="tableHeight">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" prop="noticeId" width="100" />
<el-table-column label="公告标题" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.noticeTitle }}</a>
</template>
</el-table-column>
<el-table-column label="公告类型" align="center" prop="noticeType" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_notice_type" :value="scope.row.noticeType"/>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_notice_status" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="创建者" align="center" prop="createBy" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="100">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-user"
@click="handleReadUsers(scope.row)"
v-hasPermi="['system:notice:list']"
>阅读用户</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:notice:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:notice:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改公告对话框 -->
<el-dialog :title="title" :visible.sync="open" width="780px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="公告标题" prop="noticeTitle">
<el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="公告类型" prop="noticeType">
<el-select v-model="form.noticeType" placeholder="请选择公告类型">
<el-option
v-for="dict in dict.type.sys_notice_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.sys_notice_status"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="内容">
<editor v-model="form.noticeContent" :min-height="192"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<notice-detail-view ref="noticeViewRef" />
<read-users-dialog ref="readUsersRef" />
</div>
</template>
<script>
import NoticeDetailView from "@/layout/components/HeaderNotice/DetailView"
import ReadUsersDialog from "./ReadUsers"
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice"
export default {
name: "Notice",
components: { NoticeDetailView, ReadUsersDialog },
dicts: ['sys_notice_status', 'sys_notice_type'],
data() {
return {
// 遮罩层
loading: true,
// 表格高度
tableHeight: window.innerHeight - 240,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 公告表格数据
noticeList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
noticeTitle: undefined,
createBy: undefined,
status: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
noticeTitle: [
{ required: true, message: "公告标题不能为空", trigger: "blur" }
],
noticeType: [
{ required: true, message: "公告类型不能为空", trigger: "change" }
]
}
}
},
created() {
this.getList()
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
watch: {
showSearch() {
this.$nextTick(() => {
this.getTableHeight()
})
}
},
methods: {
/** 获取表格高度 */
getTableHeight() {
const offset = this.showSearch ? 240 : 190
this.tableHeight = window.innerHeight - offset
},
/** 查询公告列表 */
getList() {
this.loading = true
listNotice(this.queryParams).then(response => {
this.noticeList = response.rows
this.total = response.total
this.loading = false
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
noticeId: undefined,
noticeTitle: undefined,
noticeType: undefined,
noticeContent: undefined,
status: "0"
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.noticeId)
this.single = selection.length != 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加公告"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const noticeId = row.noticeId || this.ids
getNotice(noticeId).then(response => {
this.form = response.data
this.open = true
this.title = "修改公告"
})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.noticeId != undefined) {
updateNotice(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addNotice(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
}
})
},
/** 查看公告详情 */
handleViewData(row) {
this.$refs.noticeViewRef.open(row)
},
/** 查看已读用户 */
handleReadUsers(row) {
this.$refs.readUsersRef.open(row)
},
/** 删除按钮操作 */
handleDelete(row) {
const noticeIds = row.noticeId || this.ids
this.$modal.confirm('是否确认删除公告编号为"' + noticeIds + '"的数据项?').then(function() {
return delNotice(noticeIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
}
}
</script>
<style scoped>
.system-notice ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.system-notice ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.system-notice ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.system-notice ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
+234
View File
@@ -0,0 +1,234 @@
<template>
<div class="app-container system-role-auth-user">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
<el-form-item label="用户名称" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入用户名称"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input
v-model="queryParams.phonenumber"
placeholder="请输入手机号码"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="openSelectUser"
v-hasPermi="['system:role:add']"
>添加用户</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-circle-close"
size="mini"
:disabled="multiple"
@click="cancelAuthUserAll"
v-hasPermi="['system:role:remove']"
>批量取消授权</el-button>
</el-col>
<el-col :span="1.5">
<el-button
icon="el-icon-close"
size="mini"
@click="handleClose"
>关闭</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange" :height="tableHeight">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-circle-close"
@click="cancelAuthUser(scope.row)"
v-hasPermi="['system:role:remove']"
>取消授权</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<select-user ref="select" :roleId="queryParams.roleId" @ok="handleQuery" />
</div>
</template>
<script>
import { allocatedUserList, authUserCancel, authUserCancelAll } from "@/api/system/role"
import selectUser from "./selectUser"
export default {
name: "AuthUser",
dicts: ['sys_normal_disable'],
components: { selectUser },
data() {
return {
// 遮罩层
loading: true,
// 表格高度
tableHeight: window.innerHeight - 240,
// 选中用户组
userIds: [],
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 用户表格数据
userList: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
roleId: undefined,
userName: undefined,
phonenumber: undefined
}
}
},
created() {
const roleId = this.$route.params && this.$route.params.roleId
if (roleId) {
this.queryParams.roleId = roleId
this.getList()
}
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
watch: {
showSearch() {
this.$nextTick(() => {
this.getTableHeight()
})
}
},
methods: {
/** 获取表格高度 */
getTableHeight() {
const offset = this.showSearch ? 240 : 190
this.tableHeight = window.innerHeight - offset
},
/** 查询授权用户列表 */
getList() {
this.loading = true
allocatedUserList(this.queryParams).then(response => {
this.userList = response.rows
this.total = response.total
this.loading = false
}
)
},
// 返回按钮
handleClose() {
const obj = { path: "/system/role" }
this.$tab.closeOpenPage(obj)
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.userIds = selection.map(item => item.userId)
this.multiple = !selection.length
},
/** 打开授权用户表弹窗 */
openSelectUser() {
this.$refs.select.show()
},
/** 取消授权按钮操作 */
cancelAuthUser(row) {
const roleId = this.queryParams.roleId
this.$modal.confirm('确认要取消该用户"' + row.userName + '"角色吗?').then(function() {
return authUserCancel({ userId: row.userId, roleId: roleId })
}).then(() => {
this.getList()
this.$modal.msgSuccess("取消授权成功")
}).catch(() => {})
},
/** 批量取消授权按钮操作 */
cancelAuthUserAll() {
const roleId = this.queryParams.roleId
const userIds = this.userIds.join(",")
this.$modal.confirm('是否取消选中用户授权数据项?').then(function() {
return authUserCancelAll({ roleId: roleId, userIds: userIds })
}).then(() => {
this.getList()
this.$modal.msgSuccess("取消授权成功")
}).catch(() => {})
}
}
}
</script>
<style scoped>
.system-role-auth-user ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.system-role-auth-user ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.system-role-auth-user ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.system-role-auth-user ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
@@ -0,0 +1,136 @@
<template>
<!-- 授权用户 -->
<el-dialog title="选择用户" :visible.sync="visible" width="800px" top="5vh" append-to-body>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="用户名称" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入用户名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input
v-model="queryParams.phonenumber"
placeholder="请输入手机号码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row>
<el-table @row-click="clickRow" ref="table" :data="userList" @selection-change="handleSelectionChange" height="260px">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-row>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSelectUser"> </el-button>
<el-button @click="visible = false"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { unallocatedUserList, authUserSelectAll } from "@/api/system/role"
export default {
dicts: ['sys_normal_disable'],
props: {
// 角色编号
roleId: {
type: [Number, String]
}
},
data() {
return {
// 遮罩层
visible: false,
// 选中数组值
userIds: [],
// 总条数
total: 0,
// 未授权用户数据
userList: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
roleId: undefined,
userName: undefined,
phonenumber: undefined
}
}
},
methods: {
// 显示弹框
show() {
this.queryParams.roleId = this.roleId
this.getList()
this.visible = true
},
clickRow(row) {
this.$refs.table.toggleRowSelection(row)
},
// 多选框选中数据
handleSelectionChange(selection) {
this.userIds = selection.map(item => item.userId)
},
// 查询表数据
getList() {
unallocatedUserList(this.queryParams).then(res => {
this.userList = res.rows
this.total = res.total
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
/** 选择授权用户操作 */
handleSelectUser() {
const roleId = this.queryParams.roleId
const userIds = this.userIds.join(",")
if (userIds == "") {
this.$modal.msgError("请选择要分配的用户")
return
}
authUserSelectAll({ roleId: roleId, userIds: userIds }).then(res => {
this.$modal.msgSuccess(res.msg)
this.visible = false
this.$emit("ok")
})
}
}
}
</script>
+152
View File
@@ -0,0 +1,152 @@
<template>
<div class="app-container system-user-auth-role">
<h4 class="form-header h4">基本信息</h4>
<el-form ref="form" :model="form" label-width="80px">
<el-row>
<el-col :span="8" :offset="2">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" disabled />
</el-form-item>
</el-col>
<el-col :span="8" :offset="2">
<el-form-item label="登录账号" prop="userName">
<el-input v-model="form.userName" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
<h4 class="form-header h4">角色信息</h4>
<el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="table" @selection-change="handleSelectionChange" :data="roles.slice((pageNum-1)*pageSize,pageNum*pageSize)" :height="tableHeight">
<el-table-column label="序号" type="index" align="center">
<template slot-scope="scope">
<span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55" />
<el-table-column label="角色编号" align="center" prop="roleId" />
<el-table-column label="角色名称" align="center" prop="roleName" />
<el-table-column label="权限字符" align="center" prop="roleKey" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="pageNum" :limit.sync="pageSize" />
<el-form label-width="100px">
<el-form-item style="text-align: center;margin-left:-120px;margin-top:30px;">
<el-button type="primary" @click="submitForm()">提交</el-button>
<el-button @click="close()">返回</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { getAuthRole, updateAuthRole } from "@/api/system/user"
export default {
name: "AuthRole",
data() {
return {
// 遮罩层
loading: true,
// 表格高度
tableHeight: window.innerHeight - 240,
// 分页信息
total: 0,
pageNum: 1,
pageSize: 10,
// 选中角色编号
roleIds: [],
// 角色信息
roles: [],
// 用户信息
form: {}
}
},
created() {
const userId = this.$route.params && this.$route.params.userId
if (userId) {
this.loading = true
getAuthRole(userId).then((response) => {
this.form = response.user
this.roles = response.roles
this.total = this.roles.length
this.$nextTick(() => {
this.roles.forEach((row) => {
if (row.flag) {
this.$refs.table.toggleRowSelection(row)
}
})
})
this.loading = false
})
}
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
methods: {
/** 获取表格高度 */
getTableHeight() {
this.tableHeight = window.innerHeight - 240
},
/** 单击选中行数据 */
clickRow(row) {
if (this.checkSelectable(row)) {
this.$refs.table.toggleRowSelection(row)
}
},
// 多选框选中数据
handleSelectionChange(selection) {
this.roleIds = selection.map((item) => item.roleId)
},
// 保存选中的数据编号
getRowKey(row) {
return row.roleId
},
// 检查角色状态
checkSelectable(row) {
return row.status === "0" ? true : false
},
/** 提交按钮 */
submitForm() {
const userId = this.form.userId
const roleIds = this.roleIds.join(",")
updateAuthRole({ userId: userId, roleIds: roleIds }).then(() => {
this.$modal.msgSuccess("授权成功")
this.close()
})
},
/** 关闭按钮 */
close() {
const obj = { path: "/system/user" }
this.$tab.closeOpenPage(obj)
}
}
}
</script>
<style scoped>
.system-user-auth-role ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.system-user-auth-role ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.system-user-auth-role ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.system-user-auth-role ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
@@ -0,0 +1,109 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="6" :xs="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>个人信息</span>
</div>
<div>
<div class="text-center">
<userAvatar />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<svg-icon icon-class="user" />用户名称
<div class="pull-right">{{ user.userName }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="phone" />手机号码
<div class="pull-right">{{ user.phonenumber }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="email" />用户邮箱
<div class="pull-right">{{ user.email }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="tree" />所属部门
<div class="pull-right" v-if="user.dept">{{ user.dept.deptName }} / {{ postGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="peoples" />所属角色
<div class="pull-right">{{ roleGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="date" />创建日期
<div class="pull-right">{{ user.createTime }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="login" />上次登录时间
<div class="pull-right">{{ loginStats.LAST_LOGIN_TIME || '首次登录' }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="online" />登录次数
<div class="pull-right">{{ loginStats.LOGIN_COUNT || 0 }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="ip" />最后登录IP
<div class="pull-right">{{ loginStats.LAST_LOGIN_IP || '-' }}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-col :span="18" :xs="24">
<el-card>
<div slot="header" class="clearfix">
<span>基本资料</span>
</div>
<el-tabs v-model="selectedTab">
<el-tab-pane label="基本资料" name="userinfo">
<userInfo :user="user" />
</el-tab-pane>
<el-tab-pane label="修改密码" name="resetPwd">
<resetPwd />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import userAvatar from "./userAvatar"
import userInfo from "./userInfo"
import resetPwd from "./resetPwd"
import { getUserProfile } from "@/api/system/user"
export default {
name: "Profile",
components: { userAvatar, userInfo, resetPwd },
data() {
return {
user: {},
roleGroup: {},
postGroup: {},
loginStats: {},
selectedTab: "userinfo"
}
},
created() {
const activeTab = this.$route.params && this.$route.params.activeTab
if (activeTab) {
this.selectedTab = activeTab
}
this.getUser()
},
methods: {
getUser() {
getUserProfile().then(response => {
this.user = response.data
this.roleGroup = response.roleGroup
this.postGroup = response.postGroup
this.loginStats = response.loginStats || {}
})
}
}
}
</script>
@@ -0,0 +1,70 @@
<template>
<el-form ref="form" :model="user" :rules="formRules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword" :rules="infoPwdValidator">
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="submit">保存</el-button>
<el-button size="mini" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { updateUserPwd } from "@/api/system/user"
import passwordRule from "@/utils/passwordRule"
export default {
mixins: [passwordRule],
data() {
return {
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
}
}
},
computed: {
formRules() {
return {
oldPassword: [
{ required: true, message: "旧密码不能为空", trigger: "blur" }
],
confirmPassword: [
{ required: true, message: "确认密码不能为空", trigger: "blur" },
{
validator: (rule, value, callback) => {
if (this.user.newPassword !== value) {
callback(new Error("两次输入的密码不一致"))
} else {
callback()
}
}, trigger: "blur"
}
]
}
}
},
methods: {
submit() {
this.$refs["form"].validate(valid => {
if (valid) {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(() => {
this.$modal.msgSuccess("修改成功")
})
}
})
},
close() {
this.$tab.closePage()
}
}
}
</script>
@@ -0,0 +1,184 @@
<template>
<div>
<div class="user-info-head" @click="editCropper()"><img v-bind:src="options.img" title="点击上传头像" class="img-circle img-lg" /></div>
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col :xs="24" :md="12" :style="{height: '350px'}">
<vue-cropper
ref="cropper"
:img="options.img"
:info="true"
:autoCrop="options.autoCrop"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:fixedBox="options.fixedBox"
:outputType="options.outputType"
@realTime="realTime"
v-if="visible"
/>
</el-col>
<el-col :xs="24" :md="12" :style="{height: '350px'}">
<div class="avatar-upload-preview">
<img :src="previews.url" :style="previews.img" />
</div>
</el-col>
</el-row>
<br />
<el-row>
<el-col :lg="2" :sm="3" :xs="3">
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
<el-button size="small">
选择
<i class="el-icon-upload el-icon--right"></i>
</el-button>
</el-upload>
</el-col>
<el-col :lg="{span: 1, offset: 2}" :sm="2" :xs="2">
<el-button icon="el-icon-plus" size="small" @click="changeScale(1)"></el-button>
</el-col>
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
<el-button icon="el-icon-minus" size="small" @click="changeScale(-1)"></el-button>
</el-col>
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
<el-button icon="el-icon-refresh-left" size="small" @click="rotateLeft()"></el-button>
</el-col>
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
<el-button icon="el-icon-refresh-right" size="small" @click="rotateRight()"></el-button>
</el-col>
<el-col :lg="{span: 2, offset: 6}" :sm="2" :xs="2">
<el-button type="primary" size="small" @click="uploadImg()"> </el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script>
import store from "@/store"
import { VueCropper } from "vue-cropper"
import { uploadAvatar } from "@/api/system/user"
import { debounce } from '@/utils'
export default {
components: { VueCropper },
data() {
return {
// 是否显示弹出层
open: false,
// 是否显示cropper
visible: false,
// 弹出层标题
title: "修改头像",
options: {
img: store.getters.avatar, //裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
outputType:"png", // 默认生成截图为PNG格式
filename: 'avatar' // 文件名称
},
previews: {},
resizeHandler: null
}
},
methods: {
// 编辑头像
editCropper() {
this.open = true
},
// 打开弹出层结束时的回调
modalOpened() {
this.visible = true
if (!this.resizeHandler) {
this.resizeHandler = debounce(() => {
this.refresh()
}, 100)
}
window.addEventListener("resize", this.resizeHandler)
},
// 刷新组件
refresh() {
this.$refs.cropper.refresh()
},
// 覆盖默认的上传行为
requestUpload() {
},
// 向左旋转
rotateLeft() {
this.$refs.cropper.rotateLeft()
},
// 向右旋转
rotateRight() {
this.$refs.cropper.rotateRight()
},
// 图片缩放
changeScale(num) {
num = num || 1
this.$refs.cropper.changeScale(num)
},
// 上传预处理
beforeUpload(file) {
if (file.type.indexOf("image/") == -1) {
this.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。")
} else {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
this.options.img = reader.result
this.options.filename = file.name
}
}
},
// 上传图片
uploadImg() {
this.$refs.cropper.getCropBlob(data => {
let formData = new FormData()
formData.append("avatarfile", data, this.options.filename)
uploadAvatar(formData).then(response => {
this.open = false
this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl
store.commit('SET_AVATAR', this.options.img)
this.$modal.msgSuccess("修改成功")
this.visible = false
})
})
},
// 实时预览
realTime(data) {
this.previews = data
},
// 关闭窗口
closeDialog() {
this.options.img = store.getters.avatar
this.visible = false
window.removeEventListener("resize", this.resizeHandler)
}
}
}
</script>
<style scoped lang="scss">
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover:after {
content: '+';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #eee;
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
line-height: 110px;
border-radius: 50%;
}
</style>
@@ -0,0 +1,88 @@
<template>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" maxlength="30" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" maxlength="50" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.sex">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="submit">保存</el-button>
<el-button size="mini" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { updateUserProfile } from "@/api/system/user"
export default {
props: {
user: {
type: Object
}
},
data() {
return {
form: {},
// 表单校验
rules: {
nickName: [
{ required: true, message: "用户昵称不能为空", trigger: "blur" }
],
email: [
{ required: true, message: "邮箱地址不能为空", trigger: "blur" },
{
type: "email",
message: "请输入正确的邮箱地址",
trigger: ["blur", "change"]
}
],
phonenumber: [
{ required: true, message: "手机号码不能为空", trigger: "blur" },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur"
}
]
}
}
},
watch: {
user: {
handler(user) {
if (user) {
this.form = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex }
}
},
immediate: true
}
},
methods: {
submit() {
this.$refs["form"].validate(valid => {
if (valid) {
updateUserProfile(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.user.phonenumber = this.form.phonenumber
this.user.email = this.form.email
})
}
})
},
close() {
this.$tab.closePage()
}
}
}
</script>
+177
View File
@@ -0,0 +1,177 @@
<template>
<el-drawer title="用户信息详情" :visible.sync="visible" direction="rtl" size="68%" append-to-body :before-close="handleClose" custom-class="detail-drawer">
<div v-loading="loading" class="drawer-content">
<!-- 基本信息 -->
<h4 class="section-header">基本信息</h4>
<el-row :gutter="20" class="mb8">
<el-col :span="12">
<div class="info-item">
<label class="info-label">用户名称</label>
<span class="info-value plaintext">{{ info.nickName }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label class="info-label">归属部门</label>
<span class="info-value plaintext">{{ (info.dept && info.dept.deptName) }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="mb8">
<el-col :span="12">
<div class="info-item">
<label class="info-label">手机号码</label>
<span class="info-value plaintext">{{ info.phonenumber }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label class="info-label">邮箱</label>
<span class="info-value plaintext">{{ info.email }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="mb8">
<el-col :span="12">
<div class="info-item">
<label class="info-label">登录账号</label>
<span class="info-value plaintext">{{ info.userName }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label class="info-label">用户状态</label>
<span class="info-value plaintext">
<el-tag size="small" :type="info.status === '0' ? 'success' : 'danger'">{{ info.status === '0' ? '正常' : '停用' }}</el-tag>
</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="mb8">
<el-col :span="12">
<div class="info-item">
<label class="info-label">岗位</label>
<span class="info-value plaintext">{{ postNames || '无岗位' }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label class="info-label">用户性别</label>
<span class="info-value plaintext">{{ sexLabel }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="mb8">
<el-col :span="24">
<div class="info-item full-width">
<label class="info-label">角色</label>
<span class="info-value plaintext">{{ roleNames || '无角色' }}</span>
</div>
</el-col>
</el-row>
<!-- 其他信息 -->
<h4 class="section-header">其他信息</h4>
<el-row :gutter="20" class="mb8">
<el-col :span="12">
<div class="info-item">
<label class="info-label">创建者</label>
<span class="info-value plaintext">{{ info.createBy }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label class="info-label">创建时间</label>
<span class="info-value plaintext">{{ info.createTime }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="mb8">
<el-col :span="12">
<div class="info-item">
<label class="info-label">更新者</label>
<span class="info-value plaintext">{{ info.updateBy }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label class="info-label">更新时间</label>
<span class="info-value plaintext">{{ info.updateTime }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="mb8">
<el-col :span="12">
<div class="info-item">
<label class="info-label">最后登录IP</label>
<span class="info-value plaintext">{{ info.loginIp }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label class="info-label">最后登录时间</label>
<span class="info-value plaintext">{{ info.loginDate }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="mb8">
<el-col :span="24">
<div class="info-item full-width">
<label class="info-label">备注</label>
<span class="info-value plaintext">{{ info.remark }}</span>
</div>
</el-col>
</el-row>
</div>
</el-drawer>
</template>
<script>
import { getUser } from '@/api/system/user'
export default {
name: 'UserViewDrawer',
dicts: ['sys_user_sex'],
data() {
return {
visible: false,
loading: false,
info: {},
postOptions: [],
roleOptions: []
}
},
computed: {
sexLabel() {
return this.selectDictLabel(this.dict.type.sys_user_sex, this.info.sex) || '-'
},
postNames() {
if (!this.postOptions.length) return ''
const ids = this.info.postIds || []
return this.postOptions.filter(p => ids.includes(p.postId)).map(p => p.postName).join('、') || ''
},
roleNames() {
if (!this.roleOptions.length) return ''
const ids = this.info.roleIds || []
return this.roleOptions.filter(r => ids.includes(r.roleId)).map(r => r.roleName).join('、') || ''
}
},
methods: {
open(userId) {
this.visible = true
this.loading = true
getUser(userId).then(res => {
this.info = res.data || {}
this.postOptions = res.posts || []
this.roleOptions = res.roles || []
this.info.postIds = res.postIds || []
this.info.roleIds = res.roleIds || []
}).finally(() => {
this.loading = false
})
},
handleClose() {
this.visible = false
}
}
}
</script>
@@ -0,0 +1,753 @@
<template>
<div class="school-calendar-editor">
<!-- 标题栏 -->
<div class="sce-titlebar">
<div class="sce-title-left">
<span class="sce-semester-name">{{ semesterName }}</span>
</div>
<div class="sce-title-center">
<span class="sce-date-range"> {{ dateRangeText }} {{ totalWeeks }} </span>
</div>
<div class="sce-title-right">
<i class="el-icon-close" title="关闭" @click="handleClose" />
</div>
</div>
<!-- 设置面板 -->
<div class="sce-panel">
<div class="sce-panel-row">
<span class="sce-label">事件名称:</span>
<el-input v-model="toolbar.eventName" size="small" class="sce-event-input" placeholder="请输入校历事件名称" />
<el-checkbox v-model="toolbar.bold" class="sce-cb">加粗显示</el-checkbox>
<el-checkbox v-model="toolbar.schedulable" class="sce-cb">可排课</el-checkbox>
<el-checkbox v-model="toolbar.mainCourse" class="sce-cb">正课</el-checkbox>
<el-checkbox v-model="toolbar.remarkShow" class="sce-cb">备注显示</el-checkbox>
<span class="sce-label">备注:</span>
<el-input
v-model="toolbar.remark"
size="small"
type="textarea"
:rows="1"
class="sce-remark-input"
resize="none"
placeholder="备注信息"
/>
</div>
<div class="sce-panel-row">
<el-button type="primary" size="small" icon="el-icon-check" @click="applyEvent">设定</el-button>
<el-button type="danger" size="small" icon="el-icon-delete" @click="deleteEvent">删除</el-button>
<el-button size="small" icon="el-icon-brush" @click="absorbEvent">吸取</el-button>
<el-button size="small" icon="el-icon-refresh" @click="refreshGrid">刷新</el-button>
<span class="sce-divider" />
<el-checkbox v-model="show78" class="sce-cb">显示78节</el-checkbox>
<el-checkbox v-model="showNight" class="sce-cb">显示晚上</el-checkbox>
<el-checkbox v-model="showLateNight" class="sce-cb">显示夜间</el-checkbox>
</div>
</div>
<!-- 时间编排区域 -->
<div ref="gridWrap" class="sce-grid-wrap" @mousedown="onGridMouseDown">
<table class="sce-grid" :class="{ 'is-selecting': selecting }">
<thead>
<tr>
<th class="sce-corner sce-th-weekno" :rowspan="2">周次</th>
<th class="sce-corner sce-th-weekrange" :rowspan="2">日期段</th>
<th v-for="col in visibleColumns" :key="'h1-' + col.colIndex" :colspan="1" class="sce-th-day">
{{ weekDayLabel(col.dayIndex) }}
</th>
</tr>
<tr>
<th v-for="col in visibleColumns" :key="'h2-' + col.colIndex" class="sce-th-slot">
{{ col.slotLabel }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(week, wIdx) in weeks" :key="'w-' + wIdx">
<td class="sce-week-cell sce-weekno-cell">{{ wIdx + 1 }}</td>
<td class="sce-week-cell sce-weekrange-cell">{{ week.rangeText }}</td>
<td
v-for="col in visibleColumns"
:key="'c-' + wIdx + '-' + col.colIndex"
class="sce-cell"
:class="cellClass(wIdx, col)"
:data-key="cellKey(wIdx, col.colIndex)"
@dblclick="onCellDblClick(wIdx, col)"
>
<span v-if="getEvent(wIdx, col.colIndex)" class="sce-event-name" :class="{ 'is-bold': getEvent(wIdx, col.colIndex).bold }">{{ getEvent(wIdx, col.colIndex).name }}</span>
<span v-else class="sce-date-num">{{ dateNumberOf(wIdx, col.dayIndex) }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="sce-status">
已选 {{ selectedKeys.length }} 个时间格
<span v-if="absorbPreview" class="sce-absorb-tip">已吸取{{ absorbPreview }}</span>
</div>
</div>
</template>
<script>
import { getSemester } from '@/api/teachBusiness/semester'
import { listXqxlb, updateXqxlb } from '@/api/teachBusiness/xqxlb'
// 节次定义:12/34/56 常显,78/晚上/夜间由开关控制可见
const SLOT_DEFS = [
{ key: '12', label: '1-2', ctrl: null },
{ key: '34', label: '3-4', ctrl: null },
{ key: '56', label: '5-6', ctrl: null },
{ key: '78', label: '7-8', ctrl: 'show78' },
{ key: 'night', label: '晚上', ctrl: 'showNight' },
{ key: 'late', label: '夜间', ctrl: 'showLateNight' }
]
// 前端节次 key -> 后端 xqxlb.courseClass 节次范围(如 12 节 -> "1-2"、夜间 -> "11-12"
const SLOT_COURSE_MAP = {
'12': '1-2',
'34': '3-4',
'56': '5-6',
'78': '7-8',
night: '9-10',
late: '11-12'
}
const WEEK_DAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
function pad2(n) {
return n < 10 ? '0' + n : '' + n
}
function fmtDate(d) {
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
}
export default {
name: 'SchoolCalendarEditor',
props: {
nd: { type: [String, Number], default: '' },
semesterName: { type: String, default: '' }
},
data() {
return {
// 学期日期范围
startDate: null,
endDate: null,
totalWeeks: 0,
dateRangeText: '',
weeks: [],
// 显示开关(默认不勾选,每天仅显示 1-2/3-4/5-6 三个节次)
show78: false,
showNight: false,
showLateNight: false,
// 工具栏表单
toolbar: {
eventName: '',
bold: false,
schedulable: false,
mainCourse: false,
remarkShow: false,
remark: ''
},
// 事件存储:key = cellKey -> event
events: {},
// 暂存被隐藏节次列上的事件:key = 周-星期-节次 -> event,列再次显示时恢复
hiddenEvents: {},
// 选择集(用数组以保证 Vue2 响应式)
selectedKeys: [],
// 拖拽状态
selecting: false,
dragStart: null,
dragStartSelected: false, // 按下起点在按下前是否已选中
moved: false, // 是否发生移动(区分点击与拖拽)
dragAddMode: false, // ctrl 拖拽为追加
// 窗口控制
absorbPreview: ''
}
},
computed: {
// 当前可见列(由开关过滤,默认每天三个节次)
visibleColumns() {
const cols = []
let colIndex = 0
WEEK_DAYS.forEach((dayLabel, dayIndex) => {
SLOT_DEFS.forEach(slot => {
if (slot.ctrl && !this[slot.ctrl]) return
cols.push({ colIndex: colIndex, dayIndex: dayIndex, slotKey: slot.key, slotLabel: slot.label, dayLabel: dayLabel })
colIndex++
})
})
return cols
},
// xqxlb 的年度(nd)为年份(如 2026),由 6 位学期代号(如 202601)截取前 4 位得出
xqxlbNd() {
return Number(String(this.nd).slice(0, 4))
}
},
watch: {
nd: {
immediate: true,
handler(val) {
if (val) this.loadSemester()
}
},
visibleColumns(newCols, oldCols) {
// 节次列显隐会改变列索引,按「周期-星期-节次」重定位事件,避免事件随时间格列偏移
if (oldCols && oldCols.length) {
const oldPosOf = index => (oldCols[index] || null)
const newIndex = (dayIndex, slotKey) => {
const c = newCols.find(x => x.dayIndex === dayIndex && x.slotKey === slotKey)
return c ? c.colIndex : -1
}
const next = {}
Object.keys(this.events).forEach(key => {
const m = key.match(/^w(\d+)-c(\d+)$/)
if (!m) return
const wIdx = +m[1]
const old = oldPosOf(+m[2])
if (!old) return
const newC = newIndex(old.dayIndex, old.slotKey)
if (newC < 0) {
// 该节次列被隐藏:事件暂存,待列再次显示时恢复
this.hiddenEvents[this.cellStableKey(wIdx, old.dayIndex, old.slotKey)] = this.events[key]
return
}
next['w' + wIdx + '-c' + newC] = this.events[key]
})
// 恢复重新显示的隐藏节次事件
Object.keys(this.hiddenEvents).forEach(stable => {
const p = stable.split('-')
const wIdx = +p[0]
const dayIndex = +p[1]
const slotKey = p[2]
const newC = newIndex(dayIndex, slotKey)
if (newC >= 0) {
next['w' + wIdx + '-c' + newC] = this.hiddenEvents[stable]
delete this.hiddenEvents[stable]
}
})
this.events = next
}
// 列变化后清空无效选择(列索引含义会变)
this.selectedKeys = []
}
},
beforeDestroy() {
this.removeGlobalListeners()
},
methods: {
/* ---------- 数据加载 ---------- */
loadSemester() {
if (!this.nd) return
getSemester(this.nd)
.then(res => {
const data = res.data || res || {}
const kx = (data.kxrq || '').toString().slice(0, 10)
const jx = (data.jsrq || '').toString().slice(0, 10)
if (kx && jx) {
this.startDate = new Date(kx.replace(/-/g, '/'))
this.endDate = new Date(jx.replace(/-/g, '/'))
this.buildWeeks()
}
this.loadXqxlbEvents()
})
.catch(() => {
this.$message.warning('学期详情获取失败,请确认学期数据')
})
},
// 从后端拉取本学期校历事件,渲染到对应时间格(年度由 6 位学期代号截取前 4 位)
loadXqxlbEvents() {
listXqxlb({ nd: this.xqxlbNd }).then(res => {
const data = res.data
const list = Array.isArray(data) ? data : (data && data.records) || []
return list
}).then(list => {
const loaded = {}
list.forEach(item => {
const pos = this.locateXqxlb(item)
if (!pos) return
const ev = {
bh: item.bh,
name: item.jqmc || '',
bold: !!item.jc,
schedulable: !!item.kpk,
mainCourse: !!item.zdpk,
remarkShow: !!item.bzxs,
remark: item.bz || ''
}
const colIndex = this.colIndexOf(pos.dayIndex, pos.slotKey)
if (colIndex >= 0) {
loaded[this.cellKey(pos.wIdx, colIndex)] = ev
} else {
// 该节次列当前隐藏:暂存(含 bh),待列显示时由 visibleColumns 监听恢复
this.hiddenEvents[this.cellStableKey(pos.wIdx, pos.dayIndex, pos.slotKey)] = ev
}
})
// 合并到现有格子(后端数据覆盖已有状态)
this.events = Object.assign({}, this.events, loaded)
}).catch(() => {
this.$message.warning('校历事件加载失败')
})
},
// 根据假期记录反推时间格位置(jqsj 日期 + courseClass 节次),与列是否可见无关
locateXqxlb(item) {
if (!this.startDate || !item.jqsj) return null
const d = new Date(String(item.jqsj).slice(0, 10).replace(/-/g, '/'))
if (isNaN(d.getTime())) return null
const offset = Math.round((d - this.startDate) / (24 * 3600 * 1000))
if (offset < 0) return null
const wIdx = Math.floor(offset / 7)
const dayIndex = offset % 7
const slotKey = this.courseClassToSlotKey(item.courseClass)
if (slotKey === null) return null
return { wIdx, dayIndex, slotKey }
},
// 后端 courseClass 节次范围 -> 前端节次 key
courseClassToSlotKey(courseClass) {
if (!courseClass) return null
const s = String(courseClass).trim()
for (const key in SLOT_COURSE_MAP) {
if (s === SLOT_COURSE_MAP[key]) return key
}
// 兼容 "910"/"1112" 等紧凑写法
if (s === '910') return 'night'
if (s === '1112') return 'late'
return null
},
buildWeeks() {
if (!this.startDate || !this.endDate) return
const start = new Date(this.startDate)
const end = new Date(this.endDate)
const days = Math.round((end - start) / (24 * 3600 * 1000)) + 1
this.totalWeeks = Math.ceil(days / 7)
this.dateRangeText = fmtDate(start) + ' 到 ' + fmtDate(end)
const weeks = []
for (let w = 0; w < this.totalWeeks; w++) {
const wkStart = new Date(start)
wkStart.setDate(start.getDate() + w * 7)
const wkEnd = new Date(wkStart)
wkEnd.setDate(wkStart.getDate() + 6)
weeks.push({
rangeText: pad2(wkStart.getMonth() + 1) + '-' + pad2(wkStart.getDate()) +
'至' + pad2(wkEnd.getMonth() + 1) + '-' + pad2(wkEnd.getDate())
})
}
this.weeks = weeks
},
colIndexOf(dayIndex, slotKey) {
const col = this.visibleColumns.find(c => c.dayIndex === dayIndex && c.slotKey === slotKey)
return col ? col.colIndex : -1
},
/* ---------- 单元格工具 ---------- */
cellKey(wIdx, colIndex) {
return 'w' + wIdx + '-c' + colIndex
},
// 稳定的单元格身份:不随列显隐变化的周-星期-节次
cellStableKey(wIdx, dayIndex, slotKey) {
return wIdx + '-' + dayIndex + '-' + slotKey
},
getEvent(wIdx, colIndex) {
return this.events[this.cellKey(wIdx, colIndex)] || null
},
weekDayLabel(dayIndex) {
return WEEK_DAYS[dayIndex]
},
// 单元格内显示的日期数字(如 0703)
dateNumberOf(wIdx, dayIndex) {
if (!this.startDate) return ''
const d = new Date(this.startDate)
d.setDate(this.startDate.getDate() + wIdx * 7 + dayIndex)
return pad2(d.getMonth() + 1) + pad2(d.getDate())
},
cellClass(wIdx, col) {
const classes = []
// 星期交替背景(单双日不同底色)
if (col.dayIndex % 2 === 0) classes.push('sce-cell-odd')
else classes.push('sce-cell-even')
if (this.selectedKeys.includes(this.cellKey(wIdx, col.colIndex))) classes.push('is-selected')
const ev = this.getEvent(wIdx, col.colIndex)
if (ev) {
classes.push('has-event')
if (!ev.schedulable) classes.push('no-schedule')
}
return classes
},
/* ---------- 点击 / 拖拽多选 ---------- */
onGridMouseDown(e) {
const cell = e.target.closest('.sce-cell')
if (!cell) {
// 点击空白区(非单元格,含表头、周次列等)取消所有选中
if (this.selectedKeys.length) this.selectedKeys = []
return
}
e.preventDefault()
const key = cell.getAttribute('data-key')
this.dragStart = key
this.dragAddMode = e.ctrlKey || e.metaKey
// 记录起点格在按下时的选中状态,用于松手时判定"纯点击"的切换行为
this.dragStartSelected = this.selectedKeys.includes(key)
this.moved = false
this.selecting = true
// 非 ctrl 模式下,若起点本身已选中(可能准备拖拽扩展),先不清除,避免误清空
if (!this.dragAddMode && !this.dragStartSelected) this.selectedKeys = []
this.addGlobalListeners()
},
addGlobalListeners() {
document.addEventListener('mousemove', this.onGridMouseMove)
document.addEventListener('mouseup', this.onGridMouseUp)
},
removeGlobalListeners() {
document.removeEventListener('mousemove', this.onGridMouseMove)
document.removeEventListener('mouseup', this.onGridMouseUp)
},
onGridMouseMove(e) {
if (!this.selecting || !this.dragStart) return
if (!this.moved) this.moved = true
const el = document.elementFromPoint(e.clientX, e.clientY)
const cell = el && el.closest ? el.closest('.sce-cell') : null
if (!cell) return
const curKey = cell.getAttribute('data-key')
this.selectRectangle(this.dragStart, curKey, this.dragAddMode)
},
onGridMouseUp() {
if (!this.selecting) {
this.removeGlobalListeners()
return
}
// 纯点击(未移动)时,做单选 / 切换
if (!this.moved) {
const key = this.dragStart
if (this.dragAddMode) {
// Ctrl + 单击:切换该格选中
if (this.selectedKeys.includes(key)) {
this.selectedKeys = this.selectedKeys.filter(k => k !== key)
} else {
this.selectedKeys.push(key)
}
} else {
// 普通单击:已选中则取消该格,未选中则仅选中该格
if (this.dragStartSelected) {
this.selectedKeys = this.selectedKeys.filter(k => k !== key)
} else {
this.selectedKeys = [key]
}
}
}
this.selecting = false
this.dragStart = null
this.dragStartSelected = false
this.removeGlobalListeners()
},
// 根据起点终点 key 选中矩形范围内所有格
selectRectangle(startKey, endKey, addMode) {
const parse = k => {
const m = k.match(/^w(\d+)-c(\d+)$/)
return m ? { w: +m[1], c: +m[2] } : null
}
const s = parse(startKey)
const t = parse(endKey)
if (!s || !t) return
if (!addMode) this.selectedKeys = []
const minW = Math.min(s.w, t.w)
const maxW = Math.max(s.w, t.w)
const minC = Math.min(s.c, t.c)
const maxC = Math.max(s.c, t.c)
for (let w = minW; w <= maxW; w++) {
for (let c = minC; c <= maxC; c++) {
const key = this.cellKey(w, c)
if (this.selectedKeys.indexOf(key) === -1) this.selectedKeys.push(key)
}
}
},
/* ---------- 事件操作 ---------- */
applyEvent() {
if (!this.selectedKeys.length) {
this.$message.warning('请先选择时间格')
return
}
const name = this.toolbar.eventName.trim()
if (!name) {
this.$message.warning('请输入事件名称')
return
}
const keys = [...this.selectedKeys]
keys.forEach(key => {
const prev = this.events[key]
this.$set(this.events, key, {
bh: prev ? prev.bh : undefined,
name: name,
bold: this.toolbar.bold,
schedulable: this.toolbar.schedulable,
mainCourse: this.toolbar.mainCourse,
remarkShow: this.toolbar.remarkShow,
remark: this.toolbar.remark
})
})
this.persistEvents(keys)
},
deleteEvent() {
if (!this.selectedKeys.length) {
this.$message.warning('请先选择时间格')
return
}
const keys = [...this.selectedKeys]
keys.forEach(key => {
this.$delete(this.events, key)
})
this.$message.success('已删除 ' + keys.length + ' 个时间格事件')
},
absorbEvent() {
// 吸取:取选择中第一个有事件的格
let target = null
for (const key of this.selectedKeys) {
if (this.events[key]) { target = this.events[key]; break }
}
if (!target) {
this.$message.warning('所选时间格中无事件可吸取')
return
}
this.toolbar.eventName = target.name
this.toolbar.bold = !!target.bold
this.toolbar.schedulable = !!target.schedulable
this.toolbar.mainCourse = !!target.mainCourse
this.toolbar.remarkShow = !!target.remarkShow
this.toolbar.remark = target.remark || ''
this.absorbPreview = target.name
},
onCellDblClick(wIdx, col) {
const ev = this.getEvent(wIdx, col.colIndex)
if (ev) {
this.toolbar.eventName = ev.name
this.toolbar.bold = !!ev.bold
this.toolbar.schedulable = !!ev.schedulable
this.toolbar.mainCourse = !!ev.mainCourse
this.toolbar.remarkShow = !!ev.remarkShow
this.toolbar.remark = ev.remark || ''
this.absorbPreview = ev.name
this.$message.info('已吸取事件:' + ev.name)
}
},
refreshGrid() {
this.selectedKeys = []
this.absorbPreview = ''
this.events = {}
this.hiddenEvents = {}
this.loadXqxlbEvents()
this.$message.info('已刷新')
},
/* ---------- 后端持久化(xqxlb 接口) ---------- */
// 批量保存选中格事件,返回各格子保存成功/失败的个数
persistEvents(keys) {
const tasks = keys
.filter(key => this.events[key])
.map(key => {
const payload = this.buildXqxlbPayload(key, this.events[key])
if (!payload) return Promise.resolve(false)
return updateXqxlb(payload)
.then(() => true)
.catch(() => false)
})
return Promise.all(tasks).then(results => {
const ok = results.filter(r => r === true).length
const fail = results.length - ok
if (fail > 0) {
this.$message.error(fail + ' 个事件保存失败,请检查后端接口')
} else if (ok > 0) {
this.$message.success('已设定 ' + ok + ' 个时间格')
}
// 保存成功后重新拉取,回写各记录主键,避免重复新增
this.loadXqxlbEvents()
return { ok: ok, fail: fail }
})
},
// 构造 xqxlb 提交数据
buildXqxlbPayload(key, ev) {
const pos = this.parseCellKey(key)
if (!pos) return null
const d = new Date(this.startDate)
d.setDate(this.startDate.getDate() + pos.wIdx * 7 + pos.dayIndex)
return {
delFlag: 0,
bh: ev.bh || undefined,
nd: this.xqxlbNd,
jqsj: fmtDate(d),
jqmc: ev.name || '',
jc: !!ev.bold,
bz: ev.remark || null,
kpk: !!ev.schedulable,
bzxs: !!ev.remarkShow,
zdpk: !!ev.mainCourse,
courseClass: SLOT_COURSE_MAP[pos.slotKey] || null
}
},
// 时间格 key -> { wIdx, dayIndex, slotKey }
parseCellKey(key) {
const m = key.match(/^w(\d+)-c(\d+)$/)
if (!m) return null
const colIndex = +m[2]
const col = this.visibleColumns.find(c => c.colIndex === colIndex)
if (!col) return null
return { wIdx: +m[1], dayIndex: col.dayIndex, slotKey: col.slotKey }
},
/* ---------- 返回学期管理 ---------- */
handleClose() {
this.$emit('close')
this.$router.replace({ path: '/teachBusiness/semester' })
}
}
}
</script>
<style scoped lang="scss">
.school-calendar-editor {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
/* 标题栏 */
.sce-titlebar {
display: flex;
align-items: center;
height: 44px;
padding: 0 14px;
background: var(--edu-green-primary, #00875a);
color: #fff;
.sce-title-left { flex: 1; font-size: 15px; font-weight: 600; }
.sce-title-center { flex: 1; text-align: center; font-size: 13px; opacity: 0.9; }
.sce-title-right { flex: 0 0 auto; text-align: right; }
.sce-title-right i {
margin-left: 14px;
cursor: pointer;
font-size: 18px;
opacity: 0.85;
&:hover { opacity: 1; }
}
}
/* 设置面板 */
.sce-panel {
padding: 10px 14px;
border-bottom: 1px solid #ebeef5;
background: #fafafa;
.sce-panel-row {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 8px;
&:last-child { margin-bottom: 0; }
}
.sce-label { font-size: 13px; color: #606266; margin-right: 6px; white-space: nowrap; }
.sce-event-input { width: 200px; margin-right: 12px; }
.sce-remark-input { width: 240px; margin-left: 6px; }
.sce-cb { margin-right: 14px; }
.sce-divider { width: 1px; height: 18px; background: #dcdfe6; margin: 0 14px; }
}
/* 时间编排区 */
.sce-grid-wrap {
flex: 1;
overflow: auto;
padding: 8px 10px;
background: #fff;
}
.sce-grid {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
font-size: 12px;
user-select: none;
th, td {
border: 1px solid #e4e7ed;
text-align: center;
padding: 0;
}
.sce-corner {
background: #f5f7fa;
font-weight: 600;
color: #303133;
vertical-align: middle;
}
.sce-th-weekno { width: 52px; }
.sce-th-weekrange { width: 110px; }
.sce-th-day {
background: var(--edu-green-primary, #00875a);
color: #fff;
font-weight: 600;
height: 26px;
}
.sce-th-slot {
background: #ecf5f0;
color: #00875a;
height: 22px;
font-weight: 500;
}
.sce-week-cell {
background: #f5f7fa;
vertical-align: middle;
white-space: nowrap;
}
.sce-weekno-cell {
font-weight: 600;
color: #303133;
font-size: 13px;
}
.sce-weekrange-cell {
font-size: 11px;
color: #909399;
}
.sce-cell {
height: 30px;
position: relative;
cursor: pointer;
background: #fff;
transition: background 0.15s;
.sce-date-num { color: #c0c4cc; }
// 星期交替底色:周一/三/五/日为浅绿,周二/四/六为白,便于区分星期
&.sce-cell-odd { background: #eef6f1; }
&.sce-cell-even { background: #ffffff; }
&.has-event { background: #e6f4ea; }
&.has-event .sce-event-name { color: #00663e; font-weight: 500; }
&.has-event .sce-event-name.is-bold { font-weight: 700; }
&.no-schedule { background: #fde2e2; }
&.is-selected {
outline: 2px solid var(--edu-green-primary, #00875a);
outline-offset: -2px;
background: #d4ecdf !important;
}
}
}
.sce-status {
padding: 6px 14px;
font-size: 12px;
color: #909399;
border-top: 1px solid #ebeef5;
background: #fafafa;
.sce-absorb-tip { margin-left: 12px; color: #00875a; }
}
}
</style>
@@ -0,0 +1,489 @@
<template>
<div class="teach-calendar app-container">
<!-- 顶部标题学期名称-校历 -->
<div class="cal-titlebar">
<span class="cal-title">{{ titleText }}</span>
</div>
<!-- 显示控制栏 -->
<div class="cal-panel">
<el-checkbox v-model="showDate" class="cal-cb">显示日期</el-checkbox>
<div class="cal-panel-right">
<el-button size="mini" type="primary" icon="el-icon-download" @click="exportExcel">另存为Excel</el-button>
</div>
</div>
<!-- 学期校历表格 -->
<div class="cal-grid-wrap">
<table class="cal-grid">
<thead>
<tr>
<th class="cal-corner cal-th-weekno" :rowspan="2">周次</th>
<th class="cal-corner cal-th-weekrange" :rowspan="2">日期段</th>
<th v-for="day in weekDayHeaders" :key="'h1-' + day.dayIndex" :colspan="day.colCount" class="cal-th-day">
{{ day.label }}
</th>
</tr>
<tr>
<th v-for="col in visibleColumns" :key="'h2-' + col.colIndex" class="cal-th-slot">
{{ col.slotLabel }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(week, wIdx) in weeks" :key="'w-' + wIdx">
<td class="cal-week-cell cal-weekno-cell">{{ wIdx + 1 }}</td>
<td class="cal-week-cell cal-weekrange-cell">{{ week.rangeText }}</td>
<td
v-for="col in visibleColumns"
:key="'c-' + wIdx + '-' + col.colIndex"
class="cal-cell"
:class="cellClass(wIdx, col)"
>
<template v-if="getEvent(wIdx, col.colIndex)">
<span class="cal-event-name">{{ getEvent(wIdx, col.colIndex).name }}</span>
</template>
<span v-else-if="showDate" class="cal-date-num">{{ dateNumberOf(wIdx, col.dayIndex) }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 底部状态栏 -->
<div class="cal-status">
<span v-if="nd"> {{ dateRangeText }} {{ totalWeeks }} </span>
<span v-else>请选择学期</span>
</div>
</div>
</template>
<script>
import { saveAs } from 'file-saver'
import { listSemester, getSemester } from '@/api/teachBusiness/semester'
import { listXqxlb } from '@/api/teachBusiness/xqxlb'
// 节次定义:全部常显
const SLOT_DEFS = [
{ key: '12', label: '1-2' },
{ key: '34', label: '3-4' },
{ key: '56', label: '5-6' },
{ key: '78', label: '7-8' },
{ key: 'night', label: '晚上' },
{ key: 'late', label: '夜间' }
]
// 前端节次 key -> 后端 xqxlb.courseClass 节次范围
const SLOT_COURSE_MAP = {
'12': '1-2',
'34': '3-4',
'56': '5-6',
'78': '7-8',
night: '9-10',
late: '11-12'
}
const WEEK_DAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
function pad2(n) {
return n < 10 ? '0' + n : '' + n
}
function fmtDate(d) {
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
}
export default {
name: 'SemesterCalendar',
data() {
return {
// 当前学期代号(跟随顶栏切换学期,自动更新为当前学期)
nd: '',
// 学期日期范围
startDate: null,
endDate: null,
totalWeeks: 0,
dateRangeText: '',
weeks: [],
// 显示控制:默认不勾选显示日期;节次全部展示
showDate: false,
// 事件存储:key = cellKey -> event
events: {},
loading: false
}
},
computed: {
// 页面标题:学期名称-校历
titleText() {
if (!this.nd) return '学期校历'
return this.getSemesterName(this.nd) + '-校历'
},
// 当前可见列(全部节次常显)
visibleColumns() {
const cols = []
let colIndex = 0
WEEK_DAYS.forEach((dayLabel, dayIndex) => {
SLOT_DEFS.forEach(slot => {
cols.push({ colIndex: colIndex, dayIndex: dayIndex, slotKey: slot.key, slotLabel: slot.label, dayLabel: dayLabel })
colIndex++
})
})
return cols
},
// 表头第一行星期:每个星期合并其下全部节次列
weekDayHeaders() {
return WEEK_DAYS.map((label, dayIndex) => ({
dayIndex: dayIndex,
label: label,
colCount: SLOT_DEFS.length
}))
}
},
created() {
// 初始加载当前学期
this.loadCurrentSemester()
// 顶栏切换学期后自动刷新为当前学期的校历
this.$root.$on('semester-current-changed', this.loadCurrentSemester)
},
beforeDestroy() {
this.$root.$off('semester-current-changed', this.loadCurrentSemester)
},
methods: {
/* ---------- 加载当前学期 ---------- */
loadCurrentSemester() {
this.loading = true
listSemester({ pageNum: 1, pageSize: 100 }).then(response => {
const data = response.data || {}
const list = data.records || []
// 跟随顶栏当前学期(dqxq=true),无则取第一条
const current = list.find(i => i.dqxq) || list[0]
this.loading = false
if (current) {
if (String(this.nd) !== String(current.nd)) {
this.nd = current.nd
this.loadSemester()
}
}
}).catch(() => {
this.loading = false
})
},
/* ---------- 数据加载 ---------- */
loadSemester() {
if (!this.nd) return
this.startDate = null
this.endDate = null
getSemester(this.nd)
.then(res => {
const data = res.data || res || {}
const kx = (data.kxrq || '').toString().slice(0, 10)
const jx = (data.jsrq || '').toString().slice(0, 10)
if (kx && jx) {
this.startDate = new Date(kx.replace(/-/g, '/'))
this.endDate = new Date(jx.replace(/-/g, '/'))
this.buildWeeks()
}
this.loadXqxlbEvents()
})
.catch(() => {
this.$message.warning('学期详情获取失败,请确认学期数据')
})
},
// 从后端拉取本学期校历事件,渲染到对应时间格(年度为 6 位学期代号截取前 4 位)
loadXqxlbEvents() {
listXqxlb({ nd: String(this.nd).slice(0, 4) }).then(res => {
const data = res.data
const list = Array.isArray(data) ? data : (data && data.records) || []
const loaded = {}
list.forEach(item => {
const pos = this.locateXqxlb(item)
if (pos) {
loaded[pos.key] = {
bh: item.bh,
name: item.jqmc || '',
schedulable: !!item.kpk,
mainCourse: !!item.zdpk,
remarkShow: !!item.bzxs,
remark: item.bz || ''
}
}
})
this.events = loaded
}).catch(() => {
this.$message.warning('校历事件加载失败')
})
},
// 根据假期记录反推时间格 keyjqsj 日期 + courseClass 节次)
locateXqxlb(item) {
if (!this.startDate || !item.jqsj) return null
const d = new Date(String(item.jqsj).slice(0, 10).replace(/-/g, '/'))
if (isNaN(d.getTime())) return null
const offset = Math.round((d - this.startDate) / (24 * 3600 * 1000))
if (offset < 0) return null
const wIdx = Math.floor(offset / 7)
const dayIndex = offset % 7
const slotKey = this.courseClassToSlotKey(item.courseClass)
if (slotKey === null) return null
const colIndex = this.colIndexOf(dayIndex, slotKey)
if (colIndex < 0) return null
return { key: this.cellKey(wIdx, colIndex) }
},
// 后端 courseClass 节次范围 -> 前端节次 key
courseClassToSlotKey(courseClass) {
if (!courseClass) return null
const s = String(courseClass).trim()
for (const key in SLOT_COURSE_MAP) {
if (s === SLOT_COURSE_MAP[key]) return key
}
// 兼容 "910"/"1112" 等紧凑写法
if (s === '910') return 'night'
if (s === '1112') return 'late'
return null
},
buildWeeks() {
if (!this.startDate || !this.endDate) return
const start = new Date(this.startDate)
const end = new Date(this.endDate)
const days = Math.round((end - start) / (24 * 3600 * 1000)) + 1
this.totalWeeks = Math.ceil(days / 7)
this.dateRangeText = fmtDate(start) + ' 到 ' + fmtDate(end)
const weeks = []
for (let w = 0; w < this.totalWeeks; w++) {
const wkStart = new Date(start)
wkStart.setDate(start.getDate() + w * 7)
const wkEnd = new Date(wkStart)
wkEnd.setDate(wkStart.getDate() + 6)
weeks.push({
rangeText: pad2(wkStart.getMonth() + 1) + '-' + pad2(wkStart.getDate()) +
'至' + pad2(wkEnd.getMonth() + 1) + '-' + pad2(wkEnd.getDate())
})
}
this.weeks = weeks
},
colIndexOf(dayIndex, slotKey) {
const col = this.visibleColumns.find(c => c.dayIndex === dayIndex && c.slotKey === slotKey)
return col ? col.colIndex : -1
},
/* ---------- 单元格工具 ---------- */
cellKey(wIdx, colIndex) {
return 'w' + wIdx + '-c' + colIndex
},
getEvent(wIdx, colIndex) {
return this.events[this.cellKey(wIdx, colIndex)] || null
},
weekDayLabel(dayIndex) {
return WEEK_DAYS[dayIndex]
},
// 单元格内显示的日期数字(如 0703)
dateNumberOf(wIdx, dayIndex) {
if (!this.startDate) return ''
const d = new Date(this.startDate)
d.setDate(this.startDate.getDate() + wIdx * 7 + dayIndex)
return pad2(d.getMonth() + 1) + pad2(d.getDate())
},
cellClass(wIdx, col) {
const classes = []
// 星期交替背景(单双日不同底色)
if (col.dayIndex % 2 === 0) classes.push('cal-cell-odd')
else classes.push('cal-cell-even')
const ev = this.getEvent(wIdx, col.colIndex)
if (ev) {
classes.push('has-event')
if (!ev.schedulable) classes.push('no-schedule')
}
return classes
},
/** 学期代号 -> 学期名称(如 202601 -> 2026年春季学期) */
getSemesterName(nd) {
if (!nd) return ''
const s = String(nd)
const xn = s.slice(0, 4)
const xq = s.slice(4)
const map = { '01': '春季学期', '02': '夏季学期', '03': '秋季学期' }
return xn + '年' + (map[xq] || '第' + xq + '学期')
},
/* ---------- 另存为Excel ---------- */
exportExcel() {
if (!this.weeks.length) {
this.$message.warning('暂无校历数据可导出')
return
}
// 按星期分组节次列,用于表头合并
const dayCols = []
this.visibleColumns.forEach(col => {
if (!dayCols[col.dayIndex]) dayCols[col.dayIndex] = []
dayCols[col.dayIndex].push(col)
})
let html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">'
html += '<head><meta charset="utf-8"></head><body>'
html += '<table border="1" cellspacing="0" cellpadding="4" style="border-collapse:collapse;">'
// 标题行
html += '<tr><td colspan="' + (this.visibleColumns.length + 2) + '" style="font-size:15px;font-weight:bold;text-align:center;">' + this.titleText + '</td></tr>'
// 表头第一行:星期(合并同类天)
html += '<tr><th rowspan="2">周次</th><th rowspan="2">日期段</th>'
WEEK_DAYS.forEach((dayLabel, dayIndex) => {
const cols = dayCols[dayIndex] || []
if (cols.length) html += '<th colspan="' + cols.length + '">' + dayLabel + '</th>'
})
html += '</tr>'
// 表头第二行:节次
html += '<tr>'
this.visibleColumns.forEach(col => {
html += '<th>' + col.slotLabel + '</th>'
})
html += '</tr>'
// 数据行
this.weeks.forEach((week, wIdx) => {
html += '<tr>'
html += '<td>' + (wIdx + 1) + '</td>'
html += '<td>' + week.rangeText + '</td>'
this.visibleColumns.forEach(col => {
const ev = this.getEvent(wIdx, col.colIndex)
if (ev) {
const remark = ev.remark && ev.remarkShow ? '' + ev.remark + '' : ''
html += '<td>' + ev.name + remark + '</td>'
} else if (this.showDate) {
html += '<td>' + this.dateNumberOf(wIdx, col.dayIndex) + '</td>'
} else {
html += '<td></td>'
}
})
html += '</tr>'
})
html += '</table></body></html>'
const blob = new Blob(['\ufeff' + html], { type: 'application/vnd.ms-excel;charset=utf-8' })
saveAs(blob, (this.titleText || '学期校历') + '.xls')
this.$message.success('已导出Excel')
}
}
}
</script>
<style scoped lang="scss">
.teach-calendar {
height: 100%;
display: flex;
flex-direction: column;
padding: 12px;
box-sizing: border-box;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
/* 顶部标题栏 */
.cal-titlebar {
display: flex;
align-items: center;
justify-content: center;
height: 44px;
background: var(--edu-green-primary, #00875a);
color: #fff;
border-radius: 4px 4px 0 0;
.cal-title {
font-size: 15px;
font-weight: 600;
}
}
/* 显示控制栏 */
.cal-panel {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 8px 14px;
border-bottom: 1px solid #ebeef5;
background: #fafafa;
.cal-cb { margin-right: 14px; }
.cal-panel-right { margin-left: auto; }
}
/* 表格区域 */
.cal-grid-wrap {
flex: 1;
overflow: auto;
padding: 8px 10px;
background: #fff;
}
.cal-grid {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
font-size: 12px;
th, td {
border: 1px solid #e4e7ed;
text-align: center;
padding: 0;
}
.cal-corner {
background: #f5f7fa;
font-weight: 600;
color: #303133;
vertical-align: middle;
}
.cal-th-weekno { width: 52px; }
.cal-th-weekrange { width: 110px; }
.cal-th-day {
background: var(--edu-green-primary, #00875a);
color: #fff;
font-weight: 600;
height: 26px;
}
.cal-th-slot {
background: #ecf5f0;
color: #00875a;
height: 22px;
font-weight: 500;
}
.cal-week-cell {
background: #f5f7fa;
vertical-align: middle;
white-space: nowrap;
}
.cal-weekno-cell {
font-weight: 600;
color: #303133;
font-size: 13px;
}
.cal-weekrange-cell {
font-size: 11px;
color: #909399;
}
.cal-cell {
height: 30px;
position: relative;
background: #fff;
.cal-date-num { color: #c0c4cc; }
// 星期交替底色:周一/三/五/日为浅绿,周二/四/六为白
&.cal-cell-odd { background: #eef6f1; }
&.cal-cell-even { background: #ffffff; }
&.has-event { background: #e6f4ea; }
&.has-event .cal-event-name { color: #00663e; font-weight: 500; }
&.no-schedule { background: #fde2e2; }
}
}
/* 底部状态栏 */
.cal-status {
padding: 6px 14px;
font-size: 12px;
color: #909399;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
}
</style>
@@ -0,0 +1,53 @@
<template>
<div class="teach-calendar app-container">
<school-calendar-editor
v-if="nd"
:nd="nd"
:semester-name="semesterName"
@close="handleClose"
/>
<div v-else class="calendar-empty">未指定学期请从学期管理页面进入</div>
</div>
</template>
<script>
import SchoolCalendarEditor from '@/views/teachBusiness/calendar/components/SchoolCalendarEditor'
export default {
name: 'SemesterCalendarEdit',
components: { SchoolCalendarEditor },
data() {
return {
nd: '',
semesterName: ''
}
},
created() {
this.nd = this.$route.query.nd || ''
this.semesterName = this.$route.query.name || ''
},
methods: {
handleClose() {
this.$router.replace({ path: '/teachBusiness/semester' })
}
}
}
</script>
<style scoped>
.teach-calendar {
height: 100%;
display: flex;
flex-direction: column;
padding: 12px;
box-sizing: border-box;
}
.calendar-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
}
</style>
@@ -0,0 +1,621 @@
<template>
<div class="app-container teach-semester">
<!-- 工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新添</el-button>
</el-col>
<el-col :span="1.8">
<el-button type="success" plain icon="el-icon-date" size="mini" :disabled="!currentSemester"
@click="handleEditCalendar">
编辑校历
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">
批量删除
</el-button>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="semesterList" border :height="tableHeight" highlight-current-row
@current-change="handleCurrentChange" @selection-change="handleSelectionChange">
<el-table-column type="selection" align="center" width="50" />
<el-table-column label="学期名称" align="center" width="250">
<template slot-scope="scope">
<span>{{ getSemesterName(scope.row.nd) }}</span>
</template>
</el-table-column>
<el-table-column label="当前" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.dqxq" type="success" size="mini">当前</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="开学日期" align="center" prop="kxrq" width="160">
<template slot-scope="scope">
<span>{{ formatDate(scope.row.kxrq) }}</span>
</template>
</el-table-column>
<el-table-column label="结束日期" align="center" prop="jsrq" width="160">
<template slot-scope="scope">
<span>{{ formatDate(scope.row.jsrq) }}</span>
</template>
</el-table-column>
<el-table-column label="周数" align="center" prop="sdzs" width="100" />
<el-table-column label="调课不审批" align="center" width="120">
<template slot-scope="scope">
<el-tag :type="scope.row.tkbsp ? 'primary' : 'info'" size="mini">{{ scope.row.tkbsp ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="禁止调课" align="center" width="120">
<template slot-scope="scope">
<el-tag :type="scope.row.jztk ? 'danger' : 'info'" size="mini">{{ scope.row.jztk ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="终结成绩定及格" align="center" width="160">
<template slot-scope="scope">
<el-tag :type="scope.row.zjcjdjg ? 'primary' : 'info'" size="mini">{{ scope.row.zjcjdjg ? '是' : '否'
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="课时系数方案" align="center" width="160">
<template slot-scope="scope">
<span>{{ getCoefficientLabel(scope.row.ksxsfabh) }}</span>
</template>
</el-table-column>
<el-table-column label="锁定时长" align="center" width="100">
<template slot-scope="scope">
<span>{{ scope.row.sdjldw || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" class-name="table-action-column">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleUpdate(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" class="text-danger"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
<!-- 学期信息弹窗 -->
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body class="semester-dialog">
<el-alert v-if="isSemesterNameDuplicated()" class="semester-dup-alert" type="warning" show-icon
title="该学期已存在,不能重复添加" :closable="false" />
<el-form ref="form" class="semester-dialog-form" :model="form" :rules="rules" label-width="96px"
label-position="right">
<el-row :gutter="20" class="semester-dialog-body">
<!-- 左侧表单区 -->
<el-col :span="15" class="semester-form-col">
<el-form-item label="学期名称">
<el-input :value="getSemesterName(buildNd(form.xn, form.xq))" readonly placeholder="请在下方选择年份和学期类型"
prefix-icon="el-icon-office-building" />
</el-form-item>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="年份" prop="xn">
<el-select v-model="form.xn" placeholder="请选择年份" :disabled="!isAdd" style="width: 100%"
@change="checkSemesterNameUnique">
<el-option v-for="item in yearOptions" :key="item" :label="item + '年'" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学期类型" prop="xq">
<el-select v-model="form.xq" placeholder="请选择学期类型" :disabled="!isAdd" style="width: 100%"
@change="checkSemesterNameUnique">
<el-option v-for="item in semesterOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="开学日期" prop="kxrq">
<el-date-picker v-model="form.kxrq" type="date" placeholder="开学日期" value-format="yyyy-MM-dd"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="jsrq">
<el-date-picker v-model="form.jsrq" type="date" placeholder="结束日期" value-format="yyyy-MM-dd"
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="学期周数" prop="sdzs">
<el-input-number v-model="form.sdzs" :min="1" :max="60" controls-position="right"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课时系数方案" prop="ksxsfabh">
<el-select v-model="form.ksxsfabh" placeholder="请选择" clearable style="width: 100%">
<el-option v-for="item in coefficientOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="锁定时长">
<div class="lock-group">
<el-input-number v-model="form.lockValue" :min="0" :max="99" controls-position="right"
style="width: 120px" />
<el-radio-group v-model="form.lockUnit" class="lock-unit" style="margin-left: 28px">
<el-radio label="周"></el-radio>
<el-radio label="月"></el-radio>
</el-radio-group>
</div>
</el-form-item>
<div class="form-tip">以当前周星期四中午12点为界限锁定锁定周数周的教学实施计划被锁定的教学实施计划不允许教员自行调整</div>
</el-col>
<!-- 右侧业务开关区 -->
<el-col :span="9" class="semester-switch-col">
<div class="switch-panel-title">业务开关</div>
<div class="switch-panel">
<div class="switch-item">
<el-checkbox v-model="form.dqxq">当前学期</el-checkbox>
<div class="switch-tip">设定当前学期后系统启动时会自动将该学期设置为默认学期</div>
</div>
<div class="switch-item">
<el-checkbox v-model="form.jztk">禁止调课</el-checkbox>
<div class="switch-tip">设定禁止调课教学信息系统中就无法对课程信息进行调整</div>
</div>
<div class="switch-item">
<el-checkbox v-model="form.tkbsp">调课不审批</el-checkbox>
<div class="switch-tip">
设定调课不审批在教学信息系统中调课时如果只是更改教员或更改授课地点调课申请会立即生效而对于改变授课地点调课申请会立即生效而对于改变授课时间增加或减少授课教员的调课申请依然不受该设置影响依然需要逐级审批后才能生效
</div>
</div>
<div class="switch-item">
<el-checkbox v-model="form.zjcjdjg">终结成绩定及格</el-checkbox>
<div class="switch-tip">设定终结成绩定及格学员终结性成绩不及格的最终成绩即为不及格</div>
</div>
<div class="switch-item">
<el-checkbox v-model="form.jwldshtk">教务机关审核教学计划调整</el-checkbox>
<div class="switch-tip">勾选教务机关需要从行政入口对每个教学计划调整申请进行审批同时再由管理员身份教务参谋进行最后的把关不勾选仅由管理员身份教务参谋进行最后的把关</div>
</div>
</div>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listSemester, getSemester, addSemester, updateSemester, delSemester } from "@/api/teachBusiness/semester"
export default {
name: "Semester",
data() {
return {
// 遮罩层
loading: true,
// 表格高度
tableHeight: window.innerHeight - 280,
// 总条数
total: 0,
// 学期列表数据
semesterList: [],
// 选中的学期数组
ids: [],
// 当前点击选中的学期
currentSemester: null,
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否新增
isAdd: true,
// 学年选项(当前年份 ± 10 年)
yearOptions: [],
// 学期选项:01 春季、02 夏季、03 秋季
semesterOptions: [
{ value: '01', label: '春季学期' },
{ value: '02', label: '夏季学期' },
{ value: '03', label: '秋季学期' }
],
// 课时系数方案选项(引用基础数据表,暂以静态占位)
coefficientOptions: [
{ value: '1', label: '标准系数方案' },
{ value: '2', label: '综合系数方案' },
{ value: '3', label: '实训系数方案' }
],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10
},
// 表单参数
form: {},
// 表单校验
rules: {
xn: [
{ required: true, message: "请选择年份", trigger: "change" }
],
xq: [
{ required: true, message: "请选择学期类型", trigger: "change" }
],
kxrq: [
{ required: true, message: "请选择开学日期", trigger: "change" }
],
jsrq: [
{ required: true, message: "请选择结束日期", trigger: "change" }
]
}
}
},
created() {
this.initYearOptions()
this.getList()
// 顶栏切换当前学期后,自动刷新本页数据
this.$root.$on('semester-current-changed', this.handleCurrentChanged)
},
beforeDestroy() {
this.$root.$off('semester-current-changed', this.handleCurrentChanged)
},
methods: {
/** 顶栏切换当前学期后自动刷新 */
handleCurrentChanged() {
this.getList()
},
/** 初始化学年选项 */
initYearOptions() {
const current = new Date().getFullYear()
const list = []
for (let i = current - 10; i <= current + 10; i++) {
list.push(i)
}
this.yearOptions = list
},
/** 学年+学期 -> 学期代号(如 2026 + 01 = 202601 */
buildNd(xn, xq) {
if (xn === undefined || xn === null || xn === '' || !xq) return ''
return String(xn) + String(xq)
},
/** 学期代号 -> 学期名称(如 202601 -> 2026年春季学期) */
getSemesterName(nd) {
if (!nd) return ''
const s = String(nd)
const xn = s.slice(0, 4)
const xq = s.slice(4)
const map = { '01': '春季学期', '02': '夏季学期', '03': '秋季学期' }
return xn + '年' + (map[xq] || '第' + xq + '学期')
},
/** 学期代号 -> {xn, xq} */
parseNd(nd) {
if (!nd) return { xn: undefined, xq: undefined }
const s = String(nd)
return { xn: s.slice(0, 4), xq: s.slice(4) }
},
/** 新增时校验学期名称(年份+学期类型拼接)是否已存在于表格 */
isSemesterNameDuplicated() {
if (!this.isAdd) return false
const nd = this.buildNd(this.form.xn, this.form.xq)
if (!nd) return false
return this.semesterList.some(item => String(item.nd) === String(nd))
},
/** 年份/学期类型切换时实时刷新顶部重复提示 */
checkSemesterNameUnique() {
this.$forceUpdate()
},
/** 课时系数方案编号 -> 名称 */
getCoefficientLabel(value) {
if (!value && value !== 0) return '-'
const found = this.coefficientOptions.find(o => String(o.value) === String(value))
return found ? found.label : value
},
/** 格式化后端 LocalDateTime2026-02-23T00:00:00 -> 2026-02-23 */
formatDate(value) {
if (!value) return ''
return String(value).slice(0, 10)
},
/** 查询学期列表 */
getList() {
this.loading = true
listSemester(this.queryParams).then(response => {
const data = response.data || {}
this.semesterList = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.semesterList = []
this.total = 0
this.loading = false
})
},
/** 多选变化 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.nd)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 单击行选中变化:记录当前选中的学期,用于启用「编辑校历」 */
handleCurrentChange(currentRow) {
this.currentSemester = currentRow || null
},
/** 组装提交数据 */
buildPayload(form) {
const lockValue = form.lockValue !== undefined && form.lockValue !== null ? form.lockValue : ''
return {
delFlag: 0,
nd: this.buildNd(form.xn, form.xq),
dqxq: !!form.dqxq,
kxrq: form.kxrq ? form.kxrq + 'T00:00:00' : null,
jsrq: form.jsrq ? form.jsrq + 'T00:00:00' : null,
tkbsp: !!form.tkbsp,
jztk: !!form.jztk,
zjcjdjg: !!form.zjcjdjg,
jwldshtk: !!form.jwldshtk,
ksxsfabh: form.ksxsfabh || null,
sdzs: form.sdzs,
sdjldw: lockValue === '' ? '' : lockValue + form.lockUnit,
xh: form.xh,
jsonzd: "{'1':1}",
kstjsj: null
}
},
/** 新增按钮 */
handleAdd() {
this.reset()
this.isAdd = true
this.open = true
this.title = "学期信息 - 新增"
},
/** 编辑按钮 */
handleUpdate(row) {
const semesterId = row && row.nd ? row.nd : this.ids[0]
if (!semesterId) return
this.reset()
this.isAdd = false
getSemester(semesterId).then(response => {
const data = response.data || {}
const { xn, xq } = this.parseNd(data.nd)
// 解析锁定时长(如 "2周" / "1月" / "1"
let lockValue = data.sdjldw
let lockUnit = '周'
if (lockValue) {
const s = String(lockValue)
if (s.endsWith('周')) { lockValue = s.slice(0, -1); lockUnit = '周' }
else if (s.endsWith('月')) { lockValue = s.slice(0, -1); lockUnit = '月' }
}
this.form = {
xn: xn,
xq: xq,
dqxq: !!data.dqxq,
jztk: !!data.jztk,
tkbsp: !!data.tkbsp,
zjcjdjg: !!data.zjcjdjg,
jwldshtk: !!data.jwldshtk,
ksxsfabh: data.ksxsfabh,
kxrq: this.formatDate(data.kxrq),
jsrq: this.formatDate(data.jsrq),
sdzs: data.sdzs,
lockValue: lockValue,
lockUnit: lockUnit,
xh: data.xh
}
this.open = true
this.title = "学期信息 - 编辑"
}).catch(() => { })
},
/** 编辑校历按钮:对当前选中的学期编辑校历 */
handleEditCalendar() {
const row = this.currentSemester
if (!row || !row.nd) return
this.$router.push({
path: '/teachBusiness/semester/semesterCalendar',
query: { nd: row.nd, name: this.getSemesterName(row.nd) }
})
},
/** 删除按钮 */
handleDelete(row) {
const semesterIds = row && row.nd ? [row.nd] : this.ids
if (!semesterIds.length) return
const names = semesterIds.map(nd => this.getSemesterName(nd)).join('、')
this.$modal.confirm('确认删除学期【' + names + '】吗?').then(() => {
// 批量删除:逐个调用
const delList = semesterIds.map(nd => delSemester(nd))
return Promise.all(delList)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => { })
},
/** 提交表单 */
submitForm() {
if (this.isSemesterNameDuplicated()) {
this.$modal.msgWarning("该学期已存在,不能重复添加")
return
}
this.$refs["form"].validate(valid => {
if (valid) {
const payload = this.buildPayload(this.form)
const selfNd = payload.nd
// 当前学期全局只能有一个:若本次设为当前,先把其他 dqxq=true 的取消
const cancelOthers = payload.dqxq
? this.cancelOtherCurrent(selfNd)
: Promise.resolve()
cancelOthers.then(() => {
if (this.isAdd) {
return addSemester(payload)
}
return updateSemester(payload)
}).then(() => {
this.$modal.msgSuccess(this.isAdd ? "新增成功" : "修改成功")
this.open = false
this.getList()
}).catch(() => { })
}
})
},
/** 取消其它“当前学期”,保证全局唯一 */
cancelOtherCurrent(selfNd) {
return listSemester({ pageNum: 1, pageSize: 100 }).then(response => {
const data = response.data || {}
const list = data.records || []
const others = list.filter(i => i.dqxq && String(i.nd) !== String(selfNd))
if (!others.length) return Promise.resolve()
return Promise.all(others.map(o => updateSemester({ ...o, dqxq: false })))
})
},
/** 重置表单 */
reset() {
this.form = {
xn: undefined,
xq: undefined,
dqxq: false,
jztk: false,
tkbsp: false,
zjcjdjg: false,
jwldshtk: false,
ksxsfabh: undefined,
kxrq: undefined,
jsrq: undefined,
sdzs: 20,
lockValue: 1,
lockUnit: '周',
xh: 1
}
this.resetForm("form")
},
/** 取消按钮 */
cancel() {
this.open = false
this.reset()
}
}
}
</script>
<style scoped lang="scss">
.teach-semester {
.mb8 {
margin-bottom: 8px;
}
/* 弹窗内表单:防止 label 文字换行、字段拥挤换行 */
::v-deep .el-dialog__body {
padding-top: 16px;
padding-bottom: 16px;
}
/* 对话框整体两栏布局 */
.semester-dialog-body {
display: block;
}
/* 顶部学期重复提示与表单间距 */
.semester-dup-alert {
margin-bottom: 16px;
}
.semester-form-col {
padding-right: 4px;
}
.semester-switch-col {
padding-left: 24px;
border-left: 1px solid #ebeef5;
}
.semester-dialog-form {
::v-deep .el-form-item {
margin-bottom: 18px;
}
::v-deep .el-form-item__label {
white-space: nowrap;
line-height: 32px;
padding-right: 10px;
color: #606266;
}
::v-deep .el-form-item__content {
line-height: 32px;
}
.el-input,
.el-select,
.el-date-editor {
width: 100%;
}
}
.form-tip {
font-size: 12px;
color: #909399;
line-height: 1.5;
margin-top: 4px;
white-space: normal;
padding-left: 2px;
}
.lock-group {
display: flex;
align-items: center;
}
.switch-panel-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 14px;
padding-left: 2px;
}
.switch-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.switch-item {
padding: 11px 13px;
background: #fafafa;
border: 1px solid #ebeef5;
border-radius: 6px;
transition: border-color 0.2s, background 0.2s;
&:hover {
border-color: var(--edu-green-primary);
background: #fff;
}
.el-checkbox {
height: 26px;
font-weight: 600;
color: #303133;
}
.switch-tip {
font-size: 12px;
line-height: 1.6;
color: #909399;
text-align: justify;
margin: 2px 0 0 2px;
}
}
}
</style>
@@ -0,0 +1,296 @@
<template>
<div class="app-container teaching-task-page">
<!-- ==================== 页面标题 ==================== -->
<div class="page-title">教学任务列表</div>
<!-- ==================== 1. 查询条件区域 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="80px" class="search-form">
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="任务名称">
<el-input
v-model="searchForm.rwmc"
placeholder="请输入任务名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="年度">
<el-select
v-model="searchForm.nd"
placeholder="请选择年度"
clearable
style="width: 100%"
>
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="状态">
<el-select
v-model="searchForm.zt"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<el-option
v-for="opt in statusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<div class="search-actions">
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
</div>
</el-form>
</el-card>
<!-- ==================== 2. 数据表格 ==================== -->
<el-card shadow="never" class="table-card">
<el-table v-loading="loading" :data="tableData" border stripe>
<template slot="empty">
<span>无数据</span>
</template>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="bh" label="编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="rwmc" label="任务名称" width="250" align="center" show-overflow-tooltip />
<el-table-column prop="nd" label="年度" width="150" align="center" />
<el-table-column label="状态" width="100" align="center">
<template slot-scope="{ row }">
<el-tag :type="row.zt === '已发布' ? 'success' : 'info'" size="small">
{{ row.zt || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fbsj" label="发布时间" width="200" align="center" :formatter="fmtDateTime" />
<el-table-column prop="jssj" label="结束时间" width="200" align="center" :formatter="fmtDateTime" />
<el-table-column prop="cjsj" label="创建时间" width="200" align="center" :formatter="fmtDateTime" />
<el-table-column prop="jcxqscsj" label="教材需求生成时间" width="200" align="center" :formatter="fmtDateTime" />
<el-table-column label="操作" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 3. 详情对话框 ==================== -->
<el-dialog
:visible="detailVisible"
title="教学任务详情"
width="620px"
:close-on-click-modal="false"
@update:visible="val => detailVisible = val"
>
<el-descriptions v-if="detailData.bh" :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ detailData.rwmc || '-' }}</el-descriptions-item>
<el-descriptions-item label="年度">{{ fmtVal(detailData.nd) }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.zt || '-' }}</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ fmtVal(detailData.fbsj) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ fmtVal(detailData.jssj) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ fmtVal(detailData.cjsj) }}</el-descriptions-item>
<el-descriptions-item label="教材需求生成时间">{{ fmtVal(detailData.jcxqscsj) }}</el-descriptions-item>
</el-descriptions>
<div v-else v-loading="detailLoading" class="detail-empty">加载中...</div>
<div slot="footer">
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
/**
* 教学任务列表(只读查询页)
* 对应菜单:teachBusiness/teachingTask(原「分组课列表」页,下载类功能后端无接口已移除)
* 仅提供 分页查询 list + 详情 get?bh=,不做增删改发(避免与教学任务计划管理页重复操作同一实体)
* 增删改发统一在 src/views/teachOffice/taskPlan/index.vue(教学任务计划管理)完成
*/
import { listTeachingTask, getTeachingTask } from '@/api/teachBusiness/teachingTask'
import { listAllSemester } from '@/api/teachBusiness/semester'
export default {
name: 'TeachingTask',
data() {
return {
// ==================== 1. 查询条件 ====================
searchForm: {
rwmc: '',
nd: undefined,
zt: ''
},
yearOptions: [],
statusOptions: [
{ label: '未发布', value: '未发布' },
{ label: '已发布', value: '已发布' }
],
// ==================== 2. 表格数据 ====================
loading: false,
tableData: [],
pageNum: 1,
pageSize: 10,
total: 0,
// ==================== 3. 详情 ====================
detailVisible: false,
detailLoading: false,
detailData: {}
}
},
created() {
this.loadYearOptions().then(() => this.fetchList())
},
methods: {
/* ---------- 年度下拉(数据来自 /semester/all,禁止硬编码) ---------- */
loadYearOptions() {
return listAllSemester().then(response => {
const list = response.data || []
const map = {}
list.forEach(item => {
if (item.nd !== null && item.nd !== undefined && item.nd !== '') map[item.nd] = item
})
const arr = Object.keys(map).sort((a, b) => String(b).localeCompare(String(a)))
this.yearOptions = arr
// 默认年度:优先当前学期(dqxq=true),否则取最新年度
const current = list.find(item => item.dqxq === true)
const defaultNd = (current && current.nd) || arr[0]
if (!this.searchForm.nd) this.searchForm.nd = defaultNd
return arr
}).catch(() => {
this.yearOptions = []
return []
})
},
/* ---------- 通用格式化 ---------- */
fmtDateTime(row, column, cellValue) {
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
return String(cellValue).replace('T', ' ').slice(0, 16)
},
fmtVal(val) {
if (val === null || val === undefined || val === '') return '-'
return String(val).replace('T', ' ').slice(0, 16)
},
/* ---------- 列表加载 ---------- */
fetchList() {
this.loading = true
const params = { pageNum: this.pageNum, pageSize: this.pageSize }
if (this.searchForm.rwmc && this.searchForm.rwmc.trim()) params.rwmc = this.searchForm.rwmc.trim()
if (this.searchForm.nd !== undefined && this.searchForm.nd !== null && this.searchForm.nd !== '') {
params.nd = this.searchForm.nd
}
if (this.searchForm.zt && this.searchForm.zt.trim()) params.zt = this.searchForm.zt.trim()
listTeachingTask(params).then(res => {
const data = (res && res.data) || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleQuery() {
this.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
/* ---------- 详情(只读,走真实 get?bh= 接口) ---------- */
handleDetail(row) {
this.detailVisible = true
this.detailLoading = true
this.detailData = {}
getTeachingTask(row.bh).then(res => {
this.detailData = (res && res.data) || {}
this.detailLoading = false
}).catch(() => {
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.teaching-task-page {
.page-title {
text-align: center;
font-size: 20px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
// ==================== 1. 查询条件区域 ====================
.search-card {
margin-bottom: 16px;
.search-form {
.search-actions {
display: flex;
justify-content: flex-end;
padding-top: 12px;
border-top: 1px solid #ebeef5;
}
}
}
// ==================== 2. 数据表格 ====================
.table-card {
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
}
// ==================== 3. 详情 ====================
.detail-empty {
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
}
}
</style>
@@ -0,0 +1,417 @@
<template>
<div class="app-container conflict-page">
<!-- 冲突检查区域 -->
<div class="section-card">
<div class="section-header">
<div>
<div class="section-title">教学资源冲突检查</div>
<div class="section-desc">当前检查年度{{ nd ? nd + ' 年' : '未选择' }}可单独检查或执行全部检查</div>
</div>
<div class="header-actions">
<el-form inline class="toolbar-form" @submit.native.prevent>
<el-form-item label="年度" class="year-item">
<el-select v-model="nd" placeholder="请选择年度" style="width: 130px" @change="handleNdChange">
<el-option v-for="y in yearOptions" :key="y" :label="y + ' 年'" :value="y" />
</el-select>
</el-form-item>
</el-form>
<el-button size="mini" :disabled="loading" @click="handleReset">重置</el-button>
<el-button type="primary" size="mini" :loading="loading" @click="handleCheckAll">执行全部检查</el-button>
</div>
</div>
<!-- 检查项卡片网格 -->
<div class="check-grid">
<div
v-for="item in conflictItems"
:key="item.key"
class="check-item-card"
:class="{ active: activeKey === item.key }"
@click="selectCard(item)"
>
<div class="check-item-header">
<span class="check-item-name">{{ item.label }}</span>
<el-tag :type="getStatus(item).type" size="mini">{{ getStatus(item).text }}</el-tag>
</div>
<div class="check-item-desc">{{ item.desc }}</div>
<div class="check-item-footer">
<span class="conflict-count">
冲突数
<b :class="item.count > 0 ? 'danger-text' : 'normal-text'">{{ item.count }}</b>
</span>
<el-button type="primary" plain size="mini" :disabled="loading" @click.stop="handleCheck(item)">检查</el-button>
</div>
</div>
</div>
</div>
<!-- 冲突明细区域 -->
<div class="section-card">
<div class="section-header detail-header">
<div class="section-title">
冲突明细
<span v-if="activeKey" class="active-label"> {{ activeLabel }}</span>
</div>
<span v-if="activeKey" class="detail-count"> {{ detailTotal }} 条记录</span>
</div>
<template v-if="activeKey">
<el-table v-loading="detailLoading" :data="activeDetails" border stripe size="mini" max-height="500">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column label="冲突号" width="90" align="center" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.conflictNo || '-' }}</template>
</el-table-column>
<el-table-column prop="courseNames" label="课程" min-width="150" align="center" show-overflow-tooltip />
<el-table-column label="日期" width="100" align="center">
<template slot-scope="scope">{{ fmtDate(scope.row.rq) }}</template>
</el-table-column>
<el-table-column prop="jc" label="节次" width="70" align="center" />
<el-table-column prop="xydNames" label="班次" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="teacherNames" label="教员" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="classroomNames" label="场地" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="responsibleDept" label="责任单位" min-width="110" align="center" show-overflow-tooltip />
</el-table>
<el-pagination
v-if="detailTotal > 0"
class="detail-pagination"
background
layout="total, prev, pager, next"
:current-page="detailQuery.pageNum"
:page-size="detailQuery.pageSize"
:total="detailTotal"
@current-change="handlePageChange"
/>
<el-empty v-if="detailTotal === 0 && !detailLoading" description="暂无冲突明细" />
</template>
<el-empty v-else v-show="!loading" description="点击上方卡片「检查」按钮,在此查看冲突明细" />
</div>
</div>
</template>
<script>
import { checkConflict, checkAllConflict, resetConflict, getConflictDetails } from '@/api/teachBusiness/timetableConflict'
import { listAllSemester } from '@/api/teachBusiness/semester'
// 四类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量;
// 检查状态与冲突数完全来自后端接口)
const CARD_DEFS = [
{ key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' },
{ key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' },
{ key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程' },
{ key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用' }
]
export default {
name: 'TimetableConflict',
data() {
return {
loading: false,
detailLoading: false,
// 年度(数据来自 /semester/all,真实后端数据)
nd: undefined,
yearOptions: [],
conflictItems: CARD_DEFS.map(c => ({ ...c, count: 0, checked: false })),
activeKey: '',
activeDetails: [],
detailTotal: 0,
detailQuery: { pageNum: 1, pageSize: 20 }
}
},
computed: {
activeLabel() {
const item = this.conflictItems.find(i => i.key === this.activeKey)
return item ? item.label : ''
}
},
created() {
this.loadYearOptions()
},
methods: {
/* ---------- 年度下拉(数据来自 /semester/all ---------- */
loadYearOptions() {
return listAllSemester().then(response => {
const list = response.data || []
// 去重并按年度倒序
const map = {}
list.forEach(item => {
if (item.nd !== null && item.nd !== undefined && item.nd !== '') map[item.nd] = item
})
const arr = Object.keys(map).sort((a, b) => String(b).localeCompare(String(a)))
this.yearOptions = arr
// 默认年度:优先当前学期(dqxq=true),否则取最新年度
const current = list.find(item => item.dqxq === true)
this.nd = (current && current.nd) || arr[0]
return arr
}).catch(() => {
this.yearOptions = []
this.nd = undefined
return []
})
},
fmtDate(rq) {
return (rq || '').substring(0, 10)
},
getStatus(item) {
if (!item.checked) return { type: 'info', text: '未检查' }
return item.count > 0
? { type: 'danger', text: '存在 ' + item.count + ' 处冲突' }
: { type: 'success', text: '无冲突' }
},
/* 用后端返回的单卡片结果同步对应卡片状态 */
applyCard(card) {
if (!card) return
const item = this.conflictItems.find(i => i.key === card.dimensionCode)
if (!item) return
item.checked = card.checked
item.count = card.conflictCount
},
applySummary(summary) {
if (!summary || !summary.cards) return
summary.cards.forEach(card => this.applyCard(card))
},
/* ---------- 单个检查 ---------- */
handleCheck(item) {
if (!this.nd) {
this.$message.warning('请先选择年度')
return
}
this.loading = true
checkConflict({ nd: this.nd, dimensionCode: item.key }).then(response => {
this.applyCard(response.data)
this.activeKey = item.key
this.detailQuery.pageNum = 1
this.loadDetails()
if (item.count > 0) {
this.$message.warning('「' + item.label + '」检查完成,发现 ' + item.count + ' 处冲突')
} else {
this.$message.success('「' + item.label + '」检查完成,未发现冲突')
}
}).finally(() => {
this.loading = false
})
},
/* ---------- 全部检查 ---------- */
handleCheckAll() {
if (!this.nd) {
this.$message.warning('请先选择年度')
return
}
this.loading = true
checkAllConflict({ nd: this.nd }).then(response => {
const summary = response.data
this.applySummary(summary)
const total = summary ? summary.totalConflictCount : 0
this.$message.success('全部检查完成,共发现 ' + total + ' 处冲突')
const firstConflict = this.conflictItems.find(i => i.checked && i.count > 0)
const firstChecked = this.conflictItems.find(i => i.checked)
this.activeKey = firstConflict ? firstConflict.key : (firstChecked ? firstChecked.key : '')
this.detailQuery.pageNum = 1
if (this.activeKey) {
this.loadDetails()
} else {
this.activeDetails = []
this.detailTotal = 0
}
}).finally(() => {
this.loading = false
})
},
/* ---------- 重置 ---------- */
handleReset() {
if (!this.nd) {
this.$message.warning('请先选择年度')
return
}
resetConflict({ nd: this.nd }).then(response => {
this.applySummary(response.data)
this.activeKey = ''
this.activeDetails = []
this.detailTotal = 0
this.$message.success('检查结果已重置')
})
},
/* 点击卡片:仅已检查的卡片可查看其明细 */
selectCard(item) {
if (!item.checked) return
this.activeKey = item.key
this.detailQuery.pageNum = 1
this.loadDetails()
},
/* ---------- 冲突明细 ---------- */
loadDetails() {
if (!this.activeKey) {
this.activeDetails = []
this.detailTotal = 0
return
}
this.detailLoading = true
getConflictDetails({
nd: this.nd,
dimensionCode: this.activeKey,
pageNum: this.detailQuery.pageNum,
pageSize: this.detailQuery.pageSize
}).then(response => {
const page = response.data || {}
this.activeDetails = page.records || []
this.detailTotal = page.total || 0
}).catch(() => {
this.activeDetails = []
this.detailTotal = 0
}).finally(() => {
this.detailLoading = false
})
},
handlePageChange(page) {
this.detailQuery.pageNum = page
this.loadDetails()
},
/* 切换年度:该年度检查状态未知,重置为未检查 */
handleNdChange() {
this.conflictItems.forEach(i => {
i.checked = false
i.count = 0
})
this.activeKey = ''
this.activeDetails = []
this.detailTotal = 0
this.detailQuery.pageNum = 1
}
}
}
</script>
<style scoped lang="scss">
.conflict-page {
.section-card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 16px 20px;
margin-bottom: 16px;
.section-title {
font-size: 16px;
font-weight: 700;
color: #303133;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
.header-actions {
display: flex;
align-items: center;
gap: 12px;
.toolbar-form {
margin-right: 4px;
.year-item {
margin-bottom: 0;
}
}
}
}
.section-desc {
margin-top: 8px;
font-size: 13px;
color: #909399;
}
.detail-header {
margin-bottom: 12px;
}
}
/* 检查项卡片网格 */
.check-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.check-item-card {
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 14px 16px;
transition: box-shadow 0.2s ease;
cursor: pointer;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&.active {
border-color: var(--edu-green-primary, #00875a);
box-shadow: 0 0 0 2px rgba(0, 135, 90, 0.15);
}
.check-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
.check-item-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
}
.check-item-desc {
margin-top: 8px;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
.check-item-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
.conflict-count {
font-size: 13px;
color: #606266;
b {
font-size: 18px;
}
}
}
}
.danger-text {
color: #f56c6c;
font-weight: 700;
}
.normal-text {
color: #67c23a;
font-weight: 700;
}
.active-label {
font-size: 14px;
font-weight: 400;
color: var(--edu-green-primary, #00875a);
}
.detail-count {
font-size: 13px;
color: #909399;
}
.detail-pagination {
margin-top: 12px;
text-align: right;
}
}
</style>
@@ -0,0 +1,676 @@
<template>
<div class="app-container training-plan-page">
<!-- ==================== 1. 查询条件区域 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="100px" class="search-form" @submit.native.prevent>
<el-row :gutter="24">
<el-col :xs="24" :md="8">
<el-form-item label="专业名称">
<el-input v-model="searchForm.zymc" placeholder="请输入专业名称" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="专业代码">
<el-input v-model="searchForm.zydm" placeholder="请输入专业代码" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="培训类型">
<el-select
v-model="searchForm.pxlx"
placeholder="请选择培训类型"
clearable
filterable
class="w-full"
>
<el-option
v-for="item in trainingTypeOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="培训层次">
<el-select
v-model="searchForm.pxcc"
placeholder="请选择培训层次"
clearable
filterable
class="w-full"
>
<el-option
v-for="item in trainingLevelOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<div class="search-actions">
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 操作与上传区域 ==================== -->
<el-card shadow="never" class="action-card">
<div class="action-row">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新建</el-button>
<el-button icon="el-icon-download" @click="handleDownload">下载</el-button>
</div>
<div class="action-row">
<el-button icon="el-icon-download" @click="handleTemplateDownload">人才培养方案目录模板下载</el-button>
</div>
<div class="upload-row">
<div class="upload-left">
<el-button icon="el-icon-folder-opened" @click="handleChooseFile">选择文件</el-button>
<span class="file-name" :class="{ 'has-file': selectedFile }">
<template v-if="selectedFile">{{ fileName }}</template>
<template v-else>请选择要上传的文件</template>
</span>
<input ref="fileInputRef" type="file" style="display: none" @change="handleFileChange" />
</div>
<div class="upload-right">
<el-button type="primary" icon="el-icon-upload2" :disabled="!selectedFile" @click="handleUpload">上传数据</el-button>
</div>
</div>
</el-card>
<!-- ==================== 3. 数据表格区域 ==================== -->
<el-card shadow="never" class="table-card">
<div class="list-header">
<div class="list-title">人才培养方案列表</div>
</div>
<el-table v-loading="loading" :data="tableData" border stripe highlight-current-row>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="zydh" label="专业代号" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="zymc" label="专业名称" width="140" align="center" show-overflow-tooltip />
<el-table-column prop="zyfx" label="专业方向" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="zydm" label="专业代码" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="pxlx" label="培训类型" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="pxcc" label="培训层次" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="pxlx2" label="培训类型2" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="xylb" label="学员类别" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="xnz" label="学年制" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="xqs" label="学期数" width="80" align="center" />
<el-table-column prop="zybb" label="专业版本" width="100" align="center" show-overflow-tooltip />
<el-table-column label="主干专业" width="90" align="center">
<template slot-scope="scope">{{ fmtYesNo(scope.row.zgzy) }}</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="isTrue(scope.row.ty) ? 'danger' : 'success'" size="mini">
{{ isTrue(scope.row.ty) ? '停用' : '启用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="启用时间" width="150" align="center">
<template slot-scope="scope">{{ fmtDateTime(scope.row.qysj) }}</template>
</el-table-column>
<el-table-column label="停用时间" width="150" align="center">
<template slot-scope="scope">{{ fmtDateTime(scope.row.tysj) }}</template>
</el-table-column>
<el-table-column label="操作" width="240" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button>
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-circle-close" class="danger-text-btn"
:disabled="isTrue(scope.row.ty)" @click="handleDisable(scope.row)">停用</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="dialog.title" :visible.sync="dialog.visible" width="860px" append-to-body
:close-on-click-modal="false">
<el-form ref="trainingForm" :model="dialog.form" :rules="rules" label-width="130px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="专业代号" prop="zydh">
<el-input v-model="dialog.form.zydh" placeholder="请输入专业代号" :disabled="dialog.isEdit" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业名称" prop="zymc">
<el-input v-model="dialog.form.zymc" placeholder="请输入专业名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业代码" prop="zydm">
<el-input v-model="dialog.form.zydm" placeholder="请输入专业代码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业方向">
<el-input v-model="dialog.form.zyfx" placeholder="请输入专业方向" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训层次" prop="pxcc">
<el-select
v-model="dialog.form.pxcc"
placeholder="请选择培训层次"
filterable
class="training-dict-select"
>
<el-option
v-for="item in trainingLevelOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训类型" prop="pxlx">
<el-select
v-model="dialog.form.pxlx"
placeholder="请选择培训类型"
filterable
class="training-dict-select"
>
<el-option
v-for="item in trainingTypeOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训类型2">
<el-input v-model="dialog.form.pxlx2" placeholder="请输入培训类型2" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学员类别">
<el-input v-model="dialog.form.xylb" placeholder="请输入学员类别" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学年制">
<el-input v-model="dialog.form.xnz" placeholder="请输入学年制" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学期数">
<el-input-number v-model="dialog.form.xqs" :min="1" :max="20" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业版本">
<el-input v-model="dialog.form.zybb" placeholder="请输入专业版本" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="自定义分类">
<el-input v-model="dialog.form.zdyfl" placeholder="请输入自定义分类" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教学管理机构编号">
<el-input v-model="dialog.form.jxgljgbh" placeholder="请输入教学管理机构编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规范名称">
<el-input v-model="dialog.form.gfmc" placeholder="请输入规范名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业规范">
<el-input v-model="dialog.form.zygf" placeholder="请输入专业规范" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="简称">
<el-input v-model="dialog.form.jc" placeholder="请输入简称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主干专业">
<el-radio-group v-model="dialog.form.zgzy">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业备注">
<el-input v-model="dialog.form.zybz" placeholder="请输入专业备注" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="培养目标">
<el-input v-model="dialog.form.pymb" type="textarea" :rows="3" placeholder="请输入培养目标" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialog.visible = false"> </el-button>
<el-button type="primary" :loading="dialog.submitting" @click="submitDialog"> </el-button>
</div>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog title="人才培养方案详情" :visible.sync="detail.visible" width="760px" append-to-body
:close-on-click-modal="false">
<div v-loading="detail.loading" class="detail-body">
<el-descriptions :column="2" border>
<el-descriptions-item label="专业代号">{{ fmtVal(detail.data.zydh) }}</el-descriptions-item>
<el-descriptions-item label="专业名称">{{ fmtVal(detail.data.zymc) }}</el-descriptions-item>
<el-descriptions-item label="专业代码">{{ fmtVal(detail.data.zydm) }}</el-descriptions-item>
<el-descriptions-item label="专业方向">{{ fmtVal(detail.data.zyfx) }}</el-descriptions-item>
<el-descriptions-item label="培训类型">{{ fmtVal(detail.data.pxlx) }}</el-descriptions-item>
<el-descriptions-item label="培训层次">{{ fmtVal(detail.data.pxcc) }}</el-descriptions-item>
<el-descriptions-item label="培训类型2">{{ fmtVal(detail.data.pxlx2) }}</el-descriptions-item>
<el-descriptions-item label="学员类别">{{ fmtVal(detail.data.xylb) }}</el-descriptions-item>
<el-descriptions-item label="学年制">{{ fmtVal(detail.data.xnz) }}</el-descriptions-item>
<el-descriptions-item label="学期数">{{ fmtVal(detail.data.xqs) }}</el-descriptions-item>
<el-descriptions-item label="专业版本">{{ fmtVal(detail.data.zybb) }}</el-descriptions-item>
<el-descriptions-item label="主干专业">{{ fmtYesNo(detail.data.zgzy) }}</el-descriptions-item>
<el-descriptions-item label="自定义分类">{{ fmtVal(detail.data.zdyfl) }}</el-descriptions-item>
<el-descriptions-item label="教学管理机构编号">{{ fmtVal(detail.data.jxgljgbh) }}</el-descriptions-item>
<el-descriptions-item label="规范名称">{{ fmtVal(detail.data.gfmc) }}</el-descriptions-item>
<el-descriptions-item label="专业规范">{{ fmtVal(detail.data.zygf) }}</el-descriptions-item>
<el-descriptions-item label="简称">{{ fmtVal(detail.data.jc) }}</el-descriptions-item>
<el-descriptions-item label="专业标识号">{{ fmtVal(detail.data.zybsh) }}</el-descriptions-item>
<el-descriptions-item label="学科专业信息标识号">{{ fmtVal(detail.data.xkzyxxbsh) }}</el-descriptions-item>
<el-descriptions-item label="教学大纲编号">{{ fmtVal(detail.data.jxdgbh) }}</el-descriptions-item>
<el-descriptions-item label="人培编号">{{ fmtVal(detail.data.rpbh) }}</el-descriptions-item>
<el-descriptions-item label="节次类别">{{ fmtVal(detail.data.jclb) }}</el-descriptions-item>
<el-descriptions-item label="系统模式">{{ fmtVal(detail.data.xtms) }}</el-descriptions-item>
<el-descriptions-item label="启用时间">{{ fmtDateTime(detail.data.qysj) }}</el-descriptions-item>
<el-descriptions-item label="停用时间">{{ fmtDateTime(detail.data.tysj) }}</el-descriptions-item>
<el-descriptions-item label="专业备注" :span="2">{{ fmtVal(detail.data.zybz) }}</el-descriptions-item>
<el-descriptions-item label="培养目标" :span="2">{{ fmtVal(detail.data.pymb) }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detail.visible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
addTraining,
disableTraining,
updateTraining,
getTraining,
listTraining
} from '@/api/teachBusiness/training'
import { getDicts } from '@/api/system/dict/data'
const TRAINING_TYPE_DICT_CODE = 'train_type'
const TRAINING_LEVEL_DICT_CODE = 'train_level'
export default {
name: 'TrainingPlan',
data() {
return {
loading: false,
// 查询条件(仅传后端 ZYBMapper 支持的字段)
searchForm: {
zymc: '',
zydm: '',
pxlx: '',
pxcc: ''
},
trainingTypeOptions: [],
trainingLevelOptions: [],
// 列表
tableData: [],
total: 0,
pageNum: 1,
pageSize: 20,
// 上传
selectedFile: null,
fileName: '',
// 新增/编辑弹窗
dialog: {
visible: false,
title: '',
isEdit: false,
submitting: false,
form: this.createEmptyForm()
},
// 详情弹窗
detail: {
visible: false,
loading: false,
data: {}
},
rules: {
zydh: [{ required: true, message: '请输入专业代号', trigger: 'blur' }],
zymc: [{ required: true, message: '请输入专业名称', trigger: 'blur' }],
zydm: [{ required: true, message: '请输入专业代码', trigger: 'blur' }],
pxcc: [{ required: true, message: '请选择培训层次', trigger: 'change' }],
pxlx: [{ required: true, message: '请选择培训类型', trigger: 'change' }]
}
}
},
created() {
this.loadTrainingDictionaries()
this.fetchList()
},
methods: {
/**
* 从系统字典加载培训类型和培训层次,提交标签以匹配现有业务数据。
*/
loadTrainingDictionaries() {
return Promise.all([
getDicts(TRAINING_TYPE_DICT_CODE),
getDicts(TRAINING_LEVEL_DICT_CODE)
]).then(([typeResponse, levelResponse]) => {
this.trainingTypeOptions = typeResponse.data || []
this.trainingLevelOptions = levelResponse.data || []
}).catch(() => {
this.trainingTypeOptions = []
this.trainingLevelOptions = []
})
},
/* ---------- 列表加载 ---------- */
fetchList() {
this.loading = true
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize
}
Object.keys(this.searchForm).forEach(key => {
const value = this.searchForm[key]
if (value !== '' && value !== null && value !== undefined) {
params[key] = value
}
})
listTraining(params).then(response => {
const data = response.data || {}
this.tableData = data.records || []
this.total = data.total || 0
}).catch(() => {
this.tableData = []
this.total = 0
}).finally(() => {
this.loading = false
})
},
/* ---------- 查询 / 重置 ---------- */
handleQuery() {
this.pageNum = 1
this.fetchList()
},
handleReset() {
this.searchForm = { zymc: '', zydm: '', pxlx: '', pxcc: '' }
this.pageNum = 1
this.fetchList()
},
/* ---------- 分页 ---------- */
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
/* ---------- 新增 / 编辑 ---------- */
handleAdd() {
this.dialog.title = '新建人才培养方案'
this.dialog.isEdit = false
this.dialog.form = this.createEmptyForm()
this.dialog.visible = true
this.$nextTick(() => {
if (this.$refs.trainingForm) this.$refs.trainingForm.clearValidate()
})
},
handleEdit(row) {
this.dialog.title = '编辑人才培养方案'
this.dialog.isEdit = true
this.dialog.form = Object.assign({}, this.createEmptyForm(), row)
this.dialog.visible = true
this.$nextTick(() => {
if (this.$refs.trainingForm) this.$refs.trainingForm.clearValidate()
})
},
submitDialog() {
this.$refs.trainingForm.validate(valid => {
if (!valid) return
this.dialog.submitting = true
const payload = this.cleanPayload(this.dialog.form)
const isEdit = this.dialog.isEdit
const req = isEdit ? updateTraining(payload) : addTraining(payload)
req.then(() => {
this.$message.success(isEdit ? '修改成功' : '新增成功')
this.dialog.visible = false
this.fetchList()
}).catch(() => {}).finally(() => {
this.dialog.submitting = false
})
})
},
/* ---------- 详情 ---------- */
handleDetail(row) {
this.detail.visible = true
this.detail.loading = true
this.detail.data = {}
getTraining(row.zydh).then(response => {
this.detail.data = response.data || {}
}).catch(() => {
this.detail.data = {}
}).finally(() => {
this.detail.loading = false
})
},
/* ---------- 停用(逻辑删除) ---------- */
handleDisable(row) {
this.$confirm('确定停用人才培养方案「' + (row.zymc || row.zydh) + '」吗?停用后列表不再展示。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return disableTraining(row.zydh)
}).then(() => {
this.$message.success('停用成功')
this.fetchList()
}).catch(() => {})
},
/* ---------- 下载 / 模板下载 / 上传(后端未提供接口) ---------- */
handleDownload() {
this.$message.warning('后端暂未提供该接口')
},
handleTemplateDownload() {
this.$message.warning('后端暂未提供该接口')
},
handleChooseFile() {
this.$refs.fileInputRef && this.$refs.fileInputRef.click()
},
handleFileChange(e) {
const file = e.target.files && e.target.files[0]
this.selectedFile = file || null
this.fileName = file ? file.name : ''
},
handleUpload() {
this.$message.warning('后端暂未提供该接口')
},
/* ---------- 工具 ---------- */
createEmptyForm() {
return {
zydh: '',
zymc: '',
zyfx: '',
zydm: '',
pxcc: '',
pxlx: '',
pxlx2: '',
xylb: '',
xnz: '',
xqs: null,
zybb: '',
zdyfl: '',
jxgljgbh: '',
gfmc: '',
zygf: '',
jc: '',
zgzy: false,
zybz: '',
pymb: ''
}
},
/** 移除空值(''/null/undefined),保留 0/false 等有效值 */
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
isTrue(val) {
return val === true || val === 1 || val === '1' || val === 'true'
},
fmtYesNo(val) {
return this.isTrue(val) ? '是' : '否'
},
fmtDateTime(val) {
const s = (val || '').substring(0, 10)
return s || '-'
},
fmtVal(val) {
return val === '' || val === null || val === undefined ? '-' : val
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.training-plan-page {
.search-card {
margin-bottom: 16px;
.search-form {
.w-full {
width: 100%;
}
.search-actions {
padding-top: 4px;
}
}
}
.action-card {
margin-bottom: 16px;
.action-row {
margin-bottom: 12px;
&:last-of-type {
margin-bottom: 0;
}
}
.upload-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 12px;
background: #fafafa;
border: 1px dashed #dcdfe6;
border-radius: 4px;
.upload-left {
display: flex;
align-items: center;
gap: 12px;
.file-name {
font-size: 13px;
color: #909399;
&.has-file {
color: #409eff;
}
}
}
}
}
.table-card {
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.pagination {
margin-top: 16px;
text-align: right;
}
}
.detail-body {
min-height: 60px;
}
}
.danger-text-btn {
color: #f56c6c;
&:hover {
color: #f78989;
}
}
.training-dict-select {
width: 100%;
}
</style>
+437
View File
@@ -0,0 +1,437 @@
<template>
<div class="app-container teach-notice">
<!-- 查询条件区域 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="公告类别">
<el-select
v-model="queryParams.category"
placeholder="请选择"
clearable
style="width: 160px"
>
<el-option
v-for="item in categoryOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="发布日期">
<el-date-picker
v-model="queryParams.publishDateFrom"
type="date"
placeholder="从"
value-format="yyyy-MM-dd"
style="width: 140px"
/>
<span class="range-sep">-</span>
<el-date-picker
v-model="queryParams.publishDateTo"
type="date"
placeholder="到"
value-format="yyyy-MM-dd"
style="width: 140px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据列表区域 -->
<div class="list-heading">
<div class="list-title">公告列表</div>
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</div>
<el-table v-loading="loading" :data="noticeList" border :height="tableHeight">
<el-table-column label="序号" type="index" align="center" width="70" />
<el-table-column label="发布时间" align="center" width="165">
<template slot-scope="scope">{{ formatTime(scope.row.publishTime) }}</template>
</el-table-column>
<el-table-column label="标题" align="left" prop="title" min-width="300" :show-overflow-tooltip="true" />
<el-table-column label="公告状态" align="center" width="100">
<template slot-scope="scope">
<el-tag :type="statusTagType(scope.row.status)">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="公告类别" align="center" prop="category" width="110" />
<el-table-column label="优先级" align="center" prop="priority" width="80" />
<el-table-column label="阅读次数" align="center" prop="readCount" width="90" />
<el-table-column label="接收/已读" align="center" width="110">
<template slot-scope="scope">{{ scope.row.recipientCount }}/{{ scope.row.readerCount }}</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
width="300"
fixed="right"
class-name="small-padding fixed-width action-column"
>
<template slot-scope="scope">
<el-button
type="text"
size="mini"
icon="el-icon-view"
@click="handleView(scope.row)"
>详情</el-button>
<el-button
type="text"
size="mini"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
v-if="scope.row.status !== '已下架'"
type="text"
size="mini"
icon="el-icon-download"
@click="handleOffline(scope.row)"
>下架</el-button>
<el-button
type="text"
size="mini"
icon="el-icon-delete"
style="color: #f56c6c"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 新增/修改 对话框 -->
<el-dialog
:title="dialogTitle"
:visible="dialogVisible"
:close-on-click-modal="false"
width="600px"
@update:visible="handleDialogVisible"
>
<el-form ref="noticeForm" :model="form" :rules="rules" label-width="80px">
<el-form-item label="公告标题" prop="title">
<el-input v-model="form.title" placeholder="请输入公告标题" />
</el-form-item>
<el-form-item label="公告类别" prop="category">
<el-select v-model="form.category" placeholder="请选择公告类别" clearable>
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="公告正文" prop="content">
<el-input type="textarea" v-model="form.content" rows="8" placeholder="请输入公告正文" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" size="mini" @click="submitForm"> </el-button>
<el-button size="mini" @click="handleDialogVisible(false)"> </el-button>
</div>
</el-dialog>
<!-- 详情 对话框 -->
<el-dialog
title="公告详情"
:visible="viewVisible"
:close-on-click-modal="false"
width="600px"
@update:visible="handleViewVisible"
>
<div class="detail-body">
<div class="detail-row">
<span class="detail-label">公告标题</span>
<span class="detail-value">{{ viewForm.title }}</span>
</div>
<div class="detail-row">
<span class="detail-label">公告类别</span>
<span class="detail-value">{{ viewForm.category }}</span>
</div>
<div class="detail-row">
<span class="detail-label">公告状态</span>
<span class="detail-value">{{ viewForm.status }}</span>
</div>
<div class="detail-row">
<span class="detail-label">优先级</span>
<span class="detail-value">{{ viewForm.priority }}</span>
</div>
<div class="detail-row">
<span class="detail-label">发布时间</span>
<span class="detail-value">{{ viewForm.publishTime }}</span>
</div>
<div class="detail-row">
<span class="detail-label">阅读次数</span>
<span class="detail-value">{{ viewForm.readCount }}</span>
</div>
<div class="detail-row">
<span class="detail-label">接收/已读</span>
<span class="detail-value">{{ viewForm.recipientCount }}/{{ viewForm.readerCount }}</span>
</div>
<div class="detail-row">
<span class="detail-label">发布范围</span>
<span class="detail-value">{{ viewForm.scopeName }}</span>
</div>
<div class="detail-row">
<span class="detail-label">公告正文</span>
<span class="detail-value detail-content">{{ viewForm.content }}</span>
</div>
<div class="detail-row">
<span class="detail-label">备注</span>
<span class="detail-value">{{ viewForm.remark }}</span>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button size="mini" @click="handleViewVisible(false)"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listTeachNotice, getTeachNotice, addTeachNotice, updateTeachNotice, delTeachNotice } from "@/api/teachNotice"
export default {
name: "TeachNotice",
data() {
return {
loading: true,
tableHeight: window.innerHeight - 320,
total: 0,
noticeList: [],
categoryOptions: ['教学公告', '考试通知', '调课通知'],
queryParams: {
pageNum: 1,
pageSize: 10,
title: undefined,
category: undefined,
publishDateFrom: undefined,
publishDateTo: undefined
},
// 新增/修改 对话框
dialogVisible: false,
dialogTitle: '',
form: {},
rules: {
title: [{ required: true, message: "公告标题不能为空", trigger: "blur" }],
content: [{ required: true, message: "公告正文不能为空", trigger: "blur" }],
category: [{ required: true, message: "公告类别不能为空", trigger: "change" }]
},
// 详情 对话框
viewVisible: false,
viewForm: {}
}
},
created() {
this.getList()
},
methods: {
/** 查询列表 */
getList() {
this.loading = true
listTeachNotice(this.queryParams).then(response => {
const data = response.data || {}
this.noticeList = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.noticeList = []
this.total = 0
this.loading = false
})
},
/** 时间格式化:yyyy-MM-dd HH:mm:ss */
formatTime(time) {
if (!time) return ''
const date = new Date(time)
if (isNaN(date.getTime())) return time
const pad = n => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
},
/** 状态标签颜色 */
statusTagType(status) {
if (status === '已发布') return 'success'
if (status === '已下架') return 'info'
return 'warning'
},
/** 查询按钮 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮 */
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
title: undefined,
category: undefined,
publishDateFrom: undefined,
publishDateTo: undefined
}
this.handleQuery()
},
/** 新增 */
handleAdd() {
this.dialogTitle = '新增公告'
this.form = { title: undefined, content: undefined, category: undefined }
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.noticeForm && this.$refs.noticeForm.clearValidate()
})
},
/** 修改 */
handleUpdate(row) {
getTeachNotice(row.id).then(response => {
const data = response.data || {}
this.form = {
id: data.id,
title: data.title,
content: data.content,
category: data.category
}
}).catch(() => {})
this.dialogTitle = '修改公告'
this.dialogVisible = true
},
/** 提交新增/修改 */
submitForm() {
this.$refs.noticeForm.validate(valid => {
if (!valid) return
const isAdd = !this.form.id
if (isAdd) {
addTeachNotice(this.form).then(() => {
this.$message.success("新增成功")
this.dialogVisible = false
this.getList()
}).catch(() => {})
} else {
updateTeachNotice(this.form).then(() => {
this.$message.success("修改成功")
this.dialogVisible = false
this.getList()
}).catch(() => {})
}
})
},
/** 下架 */
handleOffline(row) {
this.$confirm("确认下架该公告吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
updateTeachNotice({ id: row.id, status: '已下架' }).then(() => {
this.$message.success("下架成功")
this.getList()
}).catch(() => {})
}).catch(() => {})
},
/** 删除(级联删除:仅拟制/已下架可删,后端校验) */
handleDelete(row) {
this.$confirm("确认删除该公告吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
delTeachNotice(row.id).then(() => {
this.$message.success("删除成功")
this.getList()
}).catch(() => {})
}).catch(() => {})
},
/** 详情 */
handleView(row) {
getTeachNotice(row.id).then(response => {
this.viewForm = response.data || {}
this.viewVisible = true
}).catch(() => {})
},
/** 对话框关闭事件(Vue2 兼容) */
handleDialogVisible(val) {
this.dialogVisible = val
},
handleViewVisible(val) {
this.viewVisible = val
}
}
}
</script>
<style scoped lang="scss">
.teach-notice {
.range-sep {
margin: 0 8px;
color: #909399;
}
.list-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
padding-left: 10px;
border-left: 4px solid var(--edu-green-primary);
}
::v-deep .action-column .cell {
white-space: nowrap;
}
.detail-body {
padding: 0 8px;
.detail-row {
display: flex;
padding: 6px 0;
font-size: 13px;
line-height: 1.6;
.detail-label {
flex-shrink: 0;
width: 90px;
color: #909399;
text-align: right;
margin-right: 12px;
}
.detail-value {
flex: 1;
color: #303133;
word-break: break-all;
}
.detail-content {
white-space: pre-wrap;
}
}
}
@media (max-width: 768px) {
.list-heading {
align-items: stretch;
flex-direction: column;
}
.list-heading .el-button {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,550 @@
<template>
<div class="app-container management-page elective-page">
<!-- ==================== 2. 查询条件区域 ==================== -->
<div class="section-card query-card">
<div class="section-heading">
<div>
<h2 class="section-title">筛选条件</h2>
<p class="section-description">按课程班次教员及状态筛选选修课</p>
</div>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
</div>
<el-form>
<el-row :gutter="32">
<!-- 左栏 -->
<el-col :xs="24" :md="12">
<div class="query-field">
<span class="q-label no-check">授课教员</span>
<el-input v-model="searchForm.js" placeholder="请输入授课教员" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">可选班次年级</span>
<el-input v-model="searchForm.kbcnj" placeholder="请输入可选班次年级" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">选修课状态</span>
<el-select
v-model="searchForm.xxkzt"
placeholder="请选择选修课状态"
clearable
class="q-control"
>
<el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
</el-select>
</div>
</el-col>
<!-- 右栏 -->
<el-col :xs="24" :md="12">
<div class="query-field">
<span class="q-label no-check">课程科目名称</span>
<el-input v-model="searchForm.kckmmc" placeholder="请输入课程科目名称" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">选修班名称</span>
<el-input v-model="searchForm.xxbmc" placeholder="请输入选修班名称" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">培训层次</span>
<el-select v-model="searchForm.pxcc" placeholder="请选择培训层次" clearable class="q-control">
<el-option v-for="item in pxccOptions" :key="item" :label="item" :value="item" />
</el-select>
</div>
</el-col>
</el-row>
</el-form>
</div>
<!-- ==================== 3. 导出按钮区域 ==================== -->
<div class="section-card export-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">数据导出</h2>
<p class="section-description">下载选修班及学员名单</p>
</div>
</div>
<div class="export-bar">
<el-button icon="el-icon-download" @click="handleDownloadList">下载选修班列表</el-button>
<el-button icon="el-icon-download" @click="handleDownloadWord">下载选修班学员名单Word</el-button>
<el-button icon="el-icon-download" @click="handleDownloadExcel">下载选修班学员名单Excel</el-button>
</div>
</div>
<!-- ==================== 4. 操作按钮区域 ==================== -->
<div class="section-card action-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">选修课操作</h2>
<p class="section-description">新建选修课或对已选数据执行批量操作</p>
</div>
</div>
<div class="action-row">
<el-button type="primary" icon="el-icon-plus" @click="handleOpenNew">新建选修课</el-button>
<el-button type="primary" plain icon="el-icon-circle-check" @click="handleOpenEarly">
所选批量开放报名
</el-button>
<el-button type="warning" plain icon="el-icon-remove-outline" @click="handleCancelEarly">
所选批量取消开放报名
</el-button>
<el-button type="danger" plain icon="el-icon-circle-close" @click="handleStopSignup">
批量停止报名
</el-button>
<el-button icon="el-icon-refresh" @click="handleReverseRange">
所选批量根据学员反向确定班次范围
</el-button>
</div>
<div class="action-row">
<el-button icon="el-icon-download" @click="handleDownloadTemplate">选修课数据文件模板下载</el-button>
</div>
</div>
<!-- ==================== 5. 文件上传区域 ==================== -->
<div class="section-card upload-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">数据导入</h2>
<p class="section-description">选择填写完成的模板文件并上传</p>
</div>
</div>
<div class="upload-row">
<div class="upload-left">
<el-button icon="el-icon-folder-opened" @click="handleSelectFile">选择文件</el-button>
<span class="file-name" :class="{ 'has-file': selectedFile }">{{ fileName }}</span>
<input ref="fileInputRef" type="file" style="display: none" @change="handleFileChange" />
</div>
<div class="upload-right">
<el-button type="primary" icon="el-icon-upload2" @click="handleUpload">上传数据</el-button>
</div>
</div>
</div>
<!-- ==================== 6. 数据表格区域 ==================== -->
<div class="section-card table-card">
<div class="section-heading section-heading--compact table-heading">
<div>
<h2 class="section-title">选修课列表</h2>
<p class="section-description"> {{ total }} 条数据</p>
</div>
</div>
<el-table v-loading="loading" :data="tableData" border stripe class="main-table" @selection-change="handleSelectionChange">
<template slot="empty">
<span>无数据</span>
</template>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" :index="tableIndex" />
<el-table-column prop="kmc" label="课程" min-width="110" show-overflow-tooltip />
<el-table-column prop="jcmc" label="教材" min-width="120" show-overflow-tooltip />
<el-table-column prop="jyxm" label="实施教员" width="90" align="center" />
<el-table-column prop="jsmc" label="教学场地" width="100" show-overflow-tooltip />
<el-table-column prop="kxdlbc" label="显示可选队别班次" min-width="150" align="center" show-overflow-tooltip />
<el-table-column label="计划学时/运行学时/学分" width="130" align="center">
<template slot="header">
<div class="triple-header">
<span>计划学时</span>
<span>运行学时</span>
<span>学分</span>
</div>
</template>
<template slot-scope="{ row }">
<div class="triple-cell">
<span>{{ row.jhyxxsxf }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="开课结课日期" min-width="150" align="center">
<template slot-scope="{ row }">
{{ row.kkjkrq }}
</template>
</el-table-column>
<el-table-column label="报名日期" min-width="140" align="center">
<template slot-scope="{ row }">
{{ row.bmrq }}
</template>
</el-table-column>
<el-table-column prop="jhrs" label="计划人数" width="90" align="center" />
<el-table-column prop="yqsm" label="要求说明" min-width="120" show-overflow-tooltip />
</el-table>
<div class="pagination-wrap">
<el-pagination
background
layout="total, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 新建选修课弹窗 -->
<el-dialog
:visible="dialogVisible"
title="新建选修课"
width="560px"
@update:visible="val => (dialogVisible = val)"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="课程名称" prop="kcmc">
<el-input v-model="form.kcmc" placeholder="请输入课程名称" clearable />
</el-form-item>
<el-form-item label="实施教员" prop="js">
<el-input v-model="form.js" placeholder="请输入实施教员" clearable />
</el-form-item>
<el-form-item label="教材">
<el-input v-model="form.jc" placeholder="请输入教材" clearable />
</el-form-item>
<el-form-item label="教学场地">
<el-input v-model="form.jxcd" placeholder="请输入教学场地" clearable />
</el-form-item>
<el-form-item label="计划学时">
<el-input-number v-model="form.jhsxs" :min="0" :precision="0" class="w-full" />
</el-form-item>
<el-form-item label="运行学时">
<el-input-number v-model="form.yxsxs" :min="0" :precision="0" class="w-full" />
</el-form-item>
<el-form-item label="学分">
<el-input-number v-model="form.xf" :min="0" :precision="1" class="w-full" />
</el-form-item>
<el-form-item label="计划人数">
<el-input-number v-model="form.jh" :min="0" :precision="0" class="w-full" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleNewConfirm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listElective,
batchOpenElective,
batchCancelOpenElective,
batchStopElective,
exportElectiveList,
exportElectiveStudentsExcel,
exportElectiveStudentsWord
} from '@/api/teachOffice/elective'
import { saveAs } from 'file-saver'
export default {
name: 'Elective',
data() {
return {
// ==================== 下拉选项 ====================
statusOptions: ['拟制', '开放报名', '停止报名', '已开课'],
pxccOptions: ['无', '本科', '硕士', '博士'],
// ==================== 查询条件 ====================
searchForm: {
js: '',
kbcnj: '',
xxkzt: '',
kckmmc: '',
xxbmc: '',
pxcc: ''
},
// ==================== 数据表格 ====================
tableData: [],
total: 0,
loading: false,
selectedRows: [],
// ==================== 文件上传 ====================
selectedFile: null,
// ==================== 新建选修课弹窗 ====================
dialogVisible: false,
form: {
kcmc: '',
js: '',
jc: '',
jxcd: '',
jhsxs: 0,
yxsxs: 0,
xf: 0,
jh: 0
},
rules: {
kcmc: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
js: [{ required: true, message: '请输入实施教员', trigger: 'blur' }]
},
// ==================== 分页 ====================
currentPage: 1,
pageSize: 20
}
},
computed: {
fileName() {
return (this.selectedFile && this.selectedFile.name) || '未选择任何文件'
}
},
created() {
this.loadList()
},
methods: {
// 组装查询过滤条件(列表与导出共用,导出不含分页参数)
buildFilter() {
const q = {}
if (this.searchForm.js) q.jyxm = this.searchForm.js
if (this.searchForm.kbcnj) q.kxbcnj = this.searchForm.kbcnj
if (this.searchForm.kckmmc) q.kmc = this.searchForm.kckmmc
if (this.searchForm.xxbmc) q.xxbmc = this.searchForm.xxbmc
if (this.searchForm.xxkzt) q.xkzt = this.searchForm.xxkzt
if (this.searchForm.pxcc) q.pxcc = this.searchForm.pxcc
return q
},
// ==================== 查询 ====================
handleQuery() {
this.currentPage = 1
this.loadList()
},
loadList() {
this.loading = true
const params = Object.assign({ pageNum: this.currentPage, pageSize: this.pageSize }, this.buildFilter())
listElective(params)
.then(res => {
const data = (res && res.data) || {}
this.tableData = data.records || []
this.total = data.total || 0
})
.finally(() => {
this.loading = false
})
},
// ==================== 导出 ====================
handleDownloadList() {
exportElectiveList(this.buildFilter())
.then(blob => {
saveAs(blob, '选修班列表.xlsx')
this.$message.success('选修班列表已下载')
})
.catch(() => {})
},
handleDownloadWord() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
exportElectiveStudentsWord(bhList)
.then(blob => {
saveAs(blob, '选修班学员名单.docx')
this.$message.success('选修班学员名单Word已下载')
})
.catch(() => {})
},
handleDownloadExcel() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
exportElectiveStudentsExcel(bhList)
.then(blob => {
saveAs(blob, '选修班学员名单.xlsx')
this.$message.success('选修班学员名单Excel已下载')
})
.catch(() => {})
},
// ==================== 操作按钮 ====================
handleOpenEarly() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
batchOpenElective(bhList)
.then(() => {
this.$message.success('所选班已开放报名')
this.loadList()
})
.catch(() => {})
},
handleCancelEarly() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
batchCancelOpenElective(bhList)
.then(() => {
this.$message.success('所选班已取消开放报名')
this.loadList()
})
.catch(() => {})
},
handleStopSignup() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
batchStopElective(bhList)
.then(() => {
this.$message.success('已批量停止报名')
this.loadList()
})
.catch(() => {})
},
handleReverseRange() {
this.$message.warning('后端暂未提供该接口')
},
handleDownloadTemplate() {
this.$message.warning('后端暂未提供该接口')
},
// ==================== 文件上传 ====================
handleSelectFile() {
this.$refs.fileInputRef && this.$refs.fileInputRef.click()
},
handleFileChange(e) {
this.selectedFile = e.target.files[0] || null
},
handleUpload() {
if (!this.selectedFile) {
this.$message.warning('请先选择文件')
return
}
this.$message.warning('后端暂未提供该接口')
},
// ==================== 表格多选 ====================
handleSelectionChange(rows) {
this.selectedRows = rows
},
// ==================== 新建选修课弹窗 ====================
handleOpenNew() {
this.$message.warning('后端暂未提供该接口')
},
handleNewConfirm() {
if (!this.$refs.formRef) return
this.$refs.formRef.validate((valid) => {
if (!valid) return
console.log('新建选修课:', JSON.parse(JSON.stringify(this.form)))
this.$message.success('新建选修课成功(前端模拟)')
this.dialogVisible = false
})
},
// ==================== 分页 ====================
handlePageChange(page) {
this.currentPage = page
this.loadList()
},
// 表格序号(跨页连续)
tableIndex(index) {
return (this.currentPage - 1) * this.pageSize + index + 1
}
}
}
</script>
<style scoped lang="scss">
// ==================== 2. 查询条件区域 ====================
.query-card {
.query-field {
display: flex;
align-items: center;
margin-bottom: 14px;
min-height: 32px;
.q-label {
width: 120px;
flex-shrink: 0;
font-size: 13px;
color: #303133;
text-align: right;
margin-right: 10px;
white-space: nowrap;
}
.q-control {
flex: 1;
min-width: 0;
}
}
}
.export-bar {
align-items: center;
}
// ==================== 4. 操作按钮区域 ====================
.action-card {
.action-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
// ==================== 5. 文件上传区域 ====================
.upload-card {
.upload-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
.upload-left {
display: flex;
align-items: center;
gap: 12px;
.file-name {
font-size: 12px;
color: #999;
&.has-file {
color: #303133;
}
}
}
.upload-right {
display: flex;
align-items: center;
}
}
}
// ==================== 6. 数据表格区域 ====================
.table-card {
.main-table {
::v-deep .cell {
font-size: 12px;
}
}
.triple-header,
.triple-cell {
display: flex;
flex-direction: column;
line-height: 1.4;
}
.pagination-wrap {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
</style>
@@ -0,0 +1,752 @@
<template>
<el-dialog
:visible="dialogVisible"
:title="`班历管理——${semesterTitle}`"
width="1200px"
top="6vh"
:close-on-click-modal="false"
class="class-calendar-dialog"
@update:visible="val => dialogVisible = val"
>
<div v-loading="loading" class="class-calendar">
<!-- ==================== 1. 学期与班次信息 ==================== -->
<div class="info-panel">
<div class="info-title-row">
<span class="semester-name">{{ semesterTitle }}</span>
<span class="class-name">{{ className }}</span>
</div>
<div class="date-range-row">
<div class="range-item">学期日期{{ fmtDate(safeSemester.kxrq) }} {{ fmtDate(safeSemester.jsrq) }} {{ semesterWeeks }} </div>
<div class="range-item">班历记录{{ records.length }} 总学时 {{ summary.totalXs }}总学分 {{ summary.totalXf }}</div>
</div>
</div>
<!-- ==================== 2. 操作按钮区域 ==================== -->
<div class="settings-panel">
<div class="action-buttons-row">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增记录</el-button>
<el-button size="small" @click="handleAutoGenerate">自动生成必修课程</el-button>
<el-button size="small" @click="handlePresetMerge">预设合班</el-button>
<el-button size="small" @click="handlePresetSplit">预设拆班</el-button>
<el-button size="small" @click="handleApplyRegion">选定区域应用于其它班次</el-button>
<el-button size="small" @click="handleApplyWhole">整个班历应用于其它班次</el-button>
<el-button size="small" icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
</div>
</div>
<!-- ==================== 3. 班历记录表格 ==================== -->
<div class="table-area">
<el-table
ref="tableRef"
:data="records"
border
stripe
size="small"
max-height="480"
class="calendar-table"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="kcxh" label="课次序号" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="jhkcxh" label="计划课次" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="jc" label="简称" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="kbh" label="课编号" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="klx" label="课类型" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="jybh" label="教员编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="jsbh" label="教室编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="zks" label="周课时" width="70" align="center" show-overflow-tooltip />
<el-table-column prop="xs" label="学时" width="60" align="center" show-overflow-tooltip />
<el-table-column prop="llxs" label="理论学时" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="sjxs" label="实践学时" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="xf" label="学分" width="60" align="center" show-overflow-tooltip />
<el-table-column prop="rs" label="人数" width="60" align="center" show-overflow-tooltip />
<el-table-column prop="ksbh" label="考试编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="ksdd" label="考试地点" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="bz" label="备注" min-width="120" align="left" header-align="center" show-overflow-tooltip />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="select-tip">
提示勾选多条记录后可进行 预设合班 / 预设拆班 / 选定区域应用于其它班次 操作删除为物理删除请谨慎操作
</div>
</div>
</div>
<div slot="footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</div>
<!-- ==================== 班历记录新增/编辑弹窗 ==================== -->
<el-dialog
:visible="editVisible"
:title="editTitle"
width="760px"
:close-on-click-modal="false"
append-to-body
@update:visible="val => editVisible = val"
>
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="120px" size="small" class="edit-form">
<div class="form-section-title">基本信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="简称" prop="jc">
<el-input v-model="editForm.jc" placeholder="课程简称" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课编号">
<el-input v-model="editForm.kbh" placeholder="课程编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="课类型">
<el-input v-model="editForm.klx" placeholder="如 必修/选修/实践" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课次序号">
<el-input-number v-model="editForm.kcxh" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="计划课次序号">
<el-input-number v-model="editForm.jhkcxh" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序号标识">
<el-input-number v-model="editForm.xhbs" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="教员编号">
<el-input v-model="editForm.jybh" placeholder="教员编号" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教室编号">
<el-input v-model="editForm.jsbh" placeholder="教室编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="人数">
<el-input-number v-model="editForm.rs" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周课时">
<el-input-number v-model="editForm.zks" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<div class="form-section-title">学时学分</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="学时">
<el-input-number v-model="editForm.xs" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="理论学时">
<el-input-number v-model="editForm.llxs" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="实践学时">
<el-input-number v-model="editForm.sjxs" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学分">
<el-input-number v-model="editForm.xf" :min="0" :precision="1" :step="0.5" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<div class="form-section-title">考试信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="考试课时">
<el-input-number v-model="editForm.ksks" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="考试编号">
<el-input v-model="editForm.ksbh" placeholder="考试编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="考试地点">
<el-input v-model="editForm.ksdd" placeholder="考试地点" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成绩分制">
<el-input v-model="editForm.cjfz" placeholder="如 百分制/等级制" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="不计入平均分">
<el-select v-model="editForm.bjrxypjf" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="考试课时不显示">
<el-select v-model="editForm.ksksbxs" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<div class="form-section-title">教研室计划</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="教研室代号">
<el-input v-model="editForm.jysdh" placeholder="教研室代号" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划教员编号">
<el-input v-model="editForm.jysjhjybh" placeholder="教研室计划教员编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="计划备注">
<el-input v-model="editForm.jysjhbz" type="textarea" :rows="2" placeholder="教研室计划备注" clearable />
</el-form-item>
<div class="form-section-title">配档设置</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="配档序号">
<el-input-number v-model="editForm.pdxh" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="配档起始周">
<el-input-number v-model="editForm.pdqsz" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="配档按周">
<el-input-number v-model="editForm.pdaz" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="占正课时间">
<el-input-number v-model="editForm.pdzzksj" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="配档编组">
<el-input-number v-model="editForm.pdbz" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="同班异课选修编组">
<el-select v-model="editForm.pdtbykxxbz" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="启用自定义学时">
<el-select v-model="editForm.pdqyzdyxs" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="编组">
<el-input-number v-model="editForm.bz2" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="自定义学时分布">
<el-input v-model="editForm.pdzdyxsfbsj" type="textarea" :rows="2" placeholder="配档自定义学时分布数据" clearable />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="editForm.bz" type="textarea" :rows="2" placeholder="备注" clearable />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="editVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="saving" @click="handleEditSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 目标班次学期选取弹窗 ==================== -->
<ClassTeamSelectDialog
:visible="teamSelectVisible"
:semester-list="semesterOptions"
:exclude-bh="safeSemester.bh"
:exclude-note="copyNote"
@update:visible="val => teamSelectVisible = val"
@confirm="handleTeamConfirm"
/>
</el-dialog>
</template>
<script>
import ClassTeamSelectDialog from './ClassTeamSelectDialog.vue'
import {
listClassCalendar,
updateClassCalendar,
batchCopyCalendar,
batchCopyCalendarBySemester,
addTeamTask,
delTeamTask,
batchPresetMerge,
batchPresetSplit,
autoGenerateRequiredCourses
} from '@/api/studentRecords/classCalendar'
export default {
name: 'ClassCalendarDialog',
components: { ClassTeamSelectDialog },
props: {
visible: { type: Boolean, default: false },
/** 选中的班次学期记录(含 bh/xydbh/xqdc/nd/xydmc/kxrq/jsrq */
semester: { type: Object, default: null },
/** 可选的目标班次学期列表(已含班次名称的完整列表) */
semesterOptions: { type: Array, default: () => [] }
},
data() {
return {
loading: false,
saving: false,
records: [],
selection: [],
// ==================== 新增/编辑 ====================
editVisible: false,
editMode: 'add',
editForm: this.createEmptyEditForm(),
editRules: {
jc: [{ required: true, message: '请输入课程简称', trigger: 'blur' }]
},
// ==================== 复制到其它班次 ====================
teamSelectVisible: false,
applyMode: 'region',
copyNote: ''
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
safeSemester() {
return this.semester || {}
},
className() {
const s = this.safeSemester
return s.xydmc || s.xydbh || ''
},
semesterTitle() {
const s = this.safeSemester
const nd = s.nd !== undefined && s.nd !== null && s.nd !== '' ? `${s.nd}` : ''
const xq = this.xqdcLabel(s.xqdc)
return `${nd}${xq}`
},
semesterWeeks() {
const s = this.safeSemester
if (!s.kxrq || !s.jsrq) return '-'
const start = new Date(String(s.kxrq).slice(0, 10))
const end = new Date(String(s.jsrq).slice(0, 10))
const diff = Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1
return diff > 0 ? Math.ceil(diff / 7) : '-'
},
/** 班历统计:课程数 / 总学时 / 总学分 */
summary() {
const totalXs = this.records.reduce((sum, r) => sum + (Number(r.xs) || 0), 0)
const totalXf = this.records.reduce((sum, r) => sum + (Number(r.xf) || 0), 0)
return { totalXs, totalXf }
},
editTitle() {
return this.editMode === 'edit' ? '编辑班历记录' : '新增班历记录'
}
},
watch: {
visible(val) {
if (val) {
this.loadData()
}
}
},
methods: {
/* ---------- 格式化 ---------- */
fmtDate(val) {
if (!val) return '-'
return String(val).slice(0, 10)
},
xqdcLabel(val) {
if (val === 1 || val === '1') return '第1学期'
if (val === 2 || val === '2') return '第2学期'
if (val === 3 || val === '3') return '第3学期'
return val !== undefined && val !== null && val !== '' ? `${val}学期` : '-'
},
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
/* ---------- 列表加载 ---------- */
loadData() {
const s = this.semester || {}
if (!s.bh) return
this.loading = true
listClassCalendar(s.bh).then(res => {
this.records = (res && res.data) || []
this.loading = false
}).catch(() => {
this.records = []
this.loading = false
})
},
handleSelectionChange(rows) {
this.selection = rows
},
/* ---------- 新增 / 编辑 ---------- */
createEmptyEditForm() {
const s = this.semester || {}
return {
bh: '',
nd: s.nd,
xydbh: s.xydbh || '',
xqdc: s.xqdc,
xydxqbh: s.bh || '',
jc: '',
kbh: '',
klx: '',
kcxh: undefined,
jhkcxh: undefined,
xhbs: undefined,
jybh: '',
jsbh: '',
rs: undefined,
zks: undefined,
xs: undefined,
llxs: undefined,
sjxs: undefined,
xf: undefined,
ksks: undefined,
ksbh: '',
ksdd: '',
cjfz: '',
bjrxypjf: undefined,
ksksbxs: undefined,
jysdh: '',
jysjhjybh: '',
jysjhbz: '',
pdxh: undefined,
pdqsz: undefined,
pdaz: undefined,
pdzzksj: undefined,
pdbz: undefined,
pdtbykxxbz: undefined,
pdqyzdyxs: undefined,
pdzdyxsfbsj: '',
bz2: undefined,
bz: ''
}
},
handleAdd() {
this.editMode = 'add'
this.editForm = this.createEmptyEditForm()
this.editVisible = true
this.$nextTick(() => {
if (this.$refs.editFormRef) this.$refs.editFormRef.clearValidate()
})
},
handleEdit(row) {
this.editMode = 'edit'
const s = this.semester || {}
const form = this.createEmptyEditForm()
Object.keys(form).forEach(key => {
if (row[key] !== undefined && row[key] !== null) form[key] = row[key]
})
form.xydxqbh = s.bh || row.xydxqbh
this.editForm = form
this.editVisible = true
this.$nextTick(() => {
if (this.$refs.editFormRef) this.$refs.editFormRef.clearValidate()
})
},
handleEditSubmit() {
this.$refs.editFormRef.validate(valid => {
if (!valid) return
const payload = this.cleanPayload({ ...this.editForm })
const req = this.editMode === 'edit'
? updateClassCalendar(payload)
: addTeamTask(payload)
this.saving = true
req.then(() => {
this.saving = false
this.editVisible = false
this.$message.success(this.editMode === 'edit' ? '班历记录已更新' : '班历记录已新增')
this.loadData()
}).catch(() => {
this.saving = false
})
})
},
/* ---------- 删除 ---------- */
handleDelete(row) {
this.$confirm(`确定删除该班历记录(${row.jc || row.bh})吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delTeamTask(row.bh).then(() => {
this.$message.success('已删除')
this.loadData()
})
}).catch(() => {})
},
/* ---------- 自动生成必修课程 ---------- */
handleAutoGenerate() {
const s = this.semester || {}
if (!s.xydbh || !s.xqdc) {
this.$message.warning('当前班次学期缺少学员队编号或学期第次,无法自动生成')
return
}
this.$confirm('将按专业教学计划自动生成该班次的必修课程(已存在的课程不会重复添加),是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
autoGenerateRequiredCourses(s.xydbh, s.xqdc).then(res => {
const count = (res && res.data) || 0
this.$message.success(`已自动生成 ${count} 门必修课程`)
this.loadData()
})
}).catch(() => {})
},
/* ---------- 预设合班 / 拆班 ---------- */
handlePresetMerge() {
const bhs = this.selection.map(r => r.bh)
if (!bhs.length) {
this.$message.warning('请先勾选要合班的班历记录')
return
}
this.$confirm(`将按年度为选中的 ${bhs.length} 条记录设置统一的课次序号,是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
batchPresetMerge(bhs).then(res => {
const count = (res && res.data) || 0
this.$message.success(`已完成 ${count} 条记录的预设合班`)
this.loadData()
})
}).catch(() => {})
},
handlePresetSplit() {
const bhs = this.selection.map(r => r.bh)
if (!bhs.length) {
this.$message.warning('请先勾选要拆班的班历记录')
return
}
this.$confirm(`将清空选中的 ${bhs.length} 条记录的课次序号,是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
batchPresetSplit(bhs).then(res => {
const count = (res && res.data) || 0
this.$message.success(`已完成 ${count} 条记录的预设拆班`)
this.loadData()
})
}).catch(() => {})
},
/* ---------- 应用于其它班次 ---------- */
handleApplyRegion() {
const bhs = this.selection.map(r => r.bh)
if (!bhs.length) {
this.$message.warning('请先勾选要应用的班历记录(选定区域)')
return
}
this.applyMode = 'region'
this.copyNote = `将选中的 ${bhs.length} 条班历记录复制到所选班次`
this.teamSelectVisible = true
},
handleApplyWhole() {
this.applyMode = 'whole'
this.copyNote = '将整个班历复制到所选班次'
this.teamSelectVisible = true
},
handleTeamConfirm(targets) {
if (!targets || !targets.length) return
const targetBhList = targets.map(t => t.bh)
if (this.applyMode === 'whole') {
batchCopyCalendarBySemester({
sourceXydxqbh: this.semester.bh,
targetBhList: targetBhList
}).then(res => {
const data = (res && res.data) || {}
this.$message.success(`已复制整个班历,共新增 ${data.count || 0} 条记录`)
this.loadData()
})
} else {
batchCopyCalendar({
sourceBhList: this.selection.map(r => r.bh),
targetBhList: targetBhList
}).then(res => {
const data = (res && res.data) || {}
this.$message.success(`已复制选定区域,共新增 ${data.count || 0} 条记录`)
this.loadData()
})
}
},
/* ---------- 刷新 ---------- */
handleRefresh() {
this.loadData()
this.$message.success('已刷新班历')
}
}
}
</script>
<style scoped lang="scss">
.class-calendar {
// ========== 1. 学期与班次信息 ==========
.info-panel {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 20px;
margin-bottom: 12px;
.info-title-row {
display: flex;
align-items: baseline;
gap: 20px;
margin-bottom: 8px;
.semester-name,
.class-name {
font-size: 20px;
font-weight: 700;
color: #1e6fff;
}
}
.date-range-row {
display: flex;
flex-direction: column;
gap: 4px;
.range-item {
font-size: 13px;
font-weight: 600;
color: #1e6fff;
}
}
}
// ========== 2. 操作按钮区域 ==========
.settings-panel {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 20px;
margin-bottom: 12px;
.action-buttons-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
}
// ========== 3. 表格区域 ==========
.table-area {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
.select-tip {
padding: 6px 12px;
font-size: 12px;
color: #909399;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
}
.text-danger {
color: #f56c6c;
}
}
// ========== 弹窗内表单分区标题 ==========
.edit-form {
.form-section-title {
font-size: 13px;
font-weight: 600;
color: #303133;
margin: 6px 0 10px;
padding: 6px 10px;
background: #f0f7ff;
border-left: 3px solid #1e6fff;
}
}
</style>
@@ -0,0 +1,223 @@
<template>
<el-dialog
:visible="dialogVisible"
:title="isEdit ? '编辑班次学期信息' : '教学班次学期信息明细'"
width="620px"
class="class-detail-dialog"
:close-on-click-modal="false"
@update:visible="val => dialogVisible = val"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" class="detail-form">
<!-- 班次信息只读展示 -->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="班次">
<el-input :value="form.xydmc || form.xydbh" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班次编号">
<el-input v-model="form.xydbh" :readonly="isEdit" :disabled="isEdit" placeholder="编辑模式不可修改" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="年度" prop="nd">
<el-select v-model="form.nd" placeholder="请选择年度" style="width: 100%">
<el-option v-for="y in yearOptions" :key="y" :label="`${y}年`" :value="y" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学期第次" prop="xqdc">
<el-select v-model="form.xqdc" placeholder="请选择学期" style="width: 100%">
<el-option v-for="opt in levelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="开学日期" prop="kxrq">
<el-date-picker v-model="form.kxrq" type="date" value-format="yyyy-MM-dd" placeholder="请选择" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="jsrq">
<el-date-picker v-model="form.jsrq" type="date" value-format="yyyy-MM-dd" placeholder="请选择" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="专用教室">
<el-input v-model="form.zyjsbh" placeholder="请输入专用教室编号" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教学任务">
<el-select v-model="form.jxrwbh" clearable filterable placeholder="请选择教学任务" style="width: 100%">
<el-option v-for="opt in taskOptions" :key="opt.bh" :label="opt.rwmc" :value="opt.bh" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="开放教员排课">
<el-checkbox v-model="form.kfjypk">开放教员排课</el-checkbox>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.bz" type="textarea" :rows="3" placeholder="请输入备注" clearable />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ClassSemesterDetailDialog',
props: {
visible: { type: Boolean, default: false },
/** 编辑模式:edit / add */
mode: { type: String, default: 'add' },
/** 编辑时的班次学期记录 */
semester: { type: Object, default: null },
/** 新增时选中的班次(学员队)信息 */
teamInfo: { type: Object, default: null },
/** 年度下拉 */
yearOptions: { type: Array, default: () => [] },
/** 教学任务下拉({ bh, rwmc } */
taskOptions: { type: Array, default: () => [] }
},
data() {
return {
form: this.createEmptyForm(),
levelOptions: [
{ label: '第1学期', value: 1 },
{ label: '第2学期', value: 2 },
{ label: '第3学期', value: 3 }
],
saving: false,
rules: {
nd: [{ required: true, message: '请选择年度', trigger: 'change' }],
xqdc: [{ required: true, message: '请选择学期第次', trigger: 'change' }],
kxrq: [{ required: true, message: '请选择开学日期', trigger: 'change' }],
jsrq: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
isEdit() {
return this.mode === 'edit'
}
},
watch: {
visible(val) {
if (!val) return
this.initForm()
this.$nextTick(() => {
if (this.$refs.formRef) this.$refs.formRef.clearValidate()
})
}
},
methods: {
createEmptyForm() {
return {
bh: '',
xydbh: '',
xydmc: '',
nd: undefined,
xqdc: undefined,
kxrq: '',
jsrq: '',
zyjsbh: '',
jxrwbh: undefined,
kfjypk: false,
bz: ''
}
},
/** 打开时按编辑/新增模式初始化表单 */
initForm() {
const form = this.createEmptyForm()
if (this.isEdit && this.semester) {
const row = this.semester
form.bh = row.bh || ''
form.xydbh = row.xydbh || ''
form.xydmc = row.xydmc || ''
form.nd = row.nd
form.xqdc = row.xqdc
form.kxrq = row.kxrq ? String(row.kxrq).slice(0, 10) : ''
form.jsrq = row.jsrq ? String(row.jsrq).slice(0, 10) : ''
form.zyjsbh = row.zyjsbh || ''
form.jxrwbh = row.jxrwbh || undefined
form.kfjypk = Number(row.kfjypk) === 1
form.bz = row.bz || ''
} else if (this.teamInfo) {
form.xydbh = this.teamInfo.xydbh || ''
form.xydmc = this.teamInfo.xydmc || ''
// 默认取当前选中年度的第一学期,日期可后续修改
form.nd = this.yearOptions[0]
form.xqdc = 1
}
this.form = form
},
/** 移除空值(保留 0 等有效值) */
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
if (!this.form.xydbh) {
this.$message.warning('缺少班次编号,无法保存')
return
}
const payload = this.cleanPayload({
bh: this.form.bh,
xydbh: this.form.xydbh,
nd: this.form.nd,
xqdc: this.form.xqdc,
kxrq: this.form.kxrq,
jsrq: this.form.jsrq,
zyjsbh: this.form.zyjsbh,
jxrwbh: this.form.jxrwbh,
kfjypk: this.form.kfjypk ? 1 : 0,
bz: this.form.bz
})
this.$emit('submit', payload)
})
}
}
}
</script>
<style scoped lang="scss">
.class-detail-dialog {
.detail-form {
// 只读输入框灰色底
::v-deep .el-input__inner[readonly] {
background-color: #f5f7fa;
}
}
}
</style>
@@ -0,0 +1,224 @@
<template>
<el-dialog
:visible="dialogVisible"
:title="title"
width="900px"
top="8vh"
:close-on-click-modal="false"
class="team-select-dialog"
@update:visible="val => dialogVisible = val"
>
<div class="team-select">
<!-- ==================== 操作说明 ==================== -->
<div v-if="excludeNote" class="note-bar">
<i class="el-icon-info"></i>
<span>{{ excludeNote }}</span>
</div>
<!-- ==================== 查询条件区域 ==================== -->
<div class="query-panel">
<el-form :inline="true" class="query-form" @submit.native.prevent>
<el-form-item label="班次编号">
<el-input
v-model="queryForm.xydbh"
placeholder="请输入班次编号(模糊)"
clearable
style="width: 220px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button size="small" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- ==================== 目标班次学期表格 ==================== -->
<div class="table-panel">
<el-table
ref="tableRef"
v-loading="loading"
:data="filteredList"
border
stripe
size="small"
max-height="360"
class="team-table"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="44" align="center" reserve-selection />
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="nd" label="年度" width="80" align="center" show-overflow-tooltip />
<el-table-column label="学期第次" width="90" align="center">
<template slot-scope="{ row }">{{ xqdcLabel(row.xqdc) }}</template>
</el-table-column>
<el-table-column prop="xydbh" label="班次编号" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="kxrq" label="开学日期" width="110" align="center" :formatter="fmtDate" />
<el-table-column prop="jsrq" label="结束日期" width="110" align="center" :formatter="fmtDate" />
<el-table-column prop="bz" label="备注" min-width="120" align="left" header-align="center" show-overflow-tooltip />
</el-table>
<el-empty v-if="!filteredList.length" description="暂无可选的目标班次学期" :image-size="60" />
</div>
<!-- ==================== 底部按钮区域 ==================== -->
<div class="footer-actions">
<span class="selected-count">已选择 {{ selection.length }} 个班次学期</span>
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" :disabled="!selection.length" @click="handleConfirm">确定</el-button>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ClassTeamSelectDialog',
props: {
visible: { type: Boolean, default: false },
/** 标题(可选) */
title: { type: String, default: '目标班次学期选取' },
/** 可选的目标班次学期列表(含 bh/nd/xqdc/xydbh/kxrq/jsrq 等) */
semesterList: { type: Array, default: () => [] },
/** 需要排除的班次学期编号(当前班次学期,不可复制给自己) */
excludeBh: { type: String, default: '' },
/** 顶部操作说明 */
excludeNote: { type: String, default: '' }
},
data() {
return {
loading: false,
queryForm: { xydbh: '' },
selection: []
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
/** 排除当前班次学期后的可选列表 */
availableList() {
const excludeBh = this.excludeBh || ''
return (this.semesterList || []).filter(item => item.bh && String(item.bh) !== String(excludeBh))
},
/** 本地过滤后的列表 */
filteredList() {
const kw = (this.queryForm.xydbh || '').trim()
if (!kw) return this.availableList
return this.availableList.filter(item => item.xydbh && String(item.xydbh).includes(kw))
}
},
watch: {
visible(val) {
if (val) {
this.selection = []
this.queryForm.xydbh = ''
this.$nextTick(() => {
if (this.$refs.tableRef) this.$refs.tableRef.clearSelection()
})
}
}
},
methods: {
fmtDate(val) {
if (!val) return '-'
return String(val).slice(0, 10)
},
xqdcLabel(val) {
if (val === 1 || val === '1') return '第1学期'
if (val === 2 || val === '2') return '第2学期'
if (val === 3 || val === '3') return '第3学期'
return val !== undefined && val !== null && val !== '' ? `${val}学期` : '-'
},
handleSelectionChange(rows) {
this.selection = rows
},
handleQuery() {},
handleResetQuery() {
this.queryForm.xydbh = ''
},
handleConfirm() {
if (!this.selection.length) {
this.$message.warning('请先勾选目标班次学期')
return
}
this.$emit('confirm', this.selection.slice())
this.dialogVisible = false
}
}
}
</script>
<style scoped lang="scss">
.team-select {
display: flex;
flex-direction: column;
gap: 12px;
// ========== 操作说明 ==========
.note-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
font-size: 13px;
color: #1e6fff;
background: #ecf5ff;
border: 1px solid #d9ecff;
border-radius: 4px;
}
// ========== 查询条件区域 ==========
.query-panel {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px 12px 2px;
.query-form {
::v-deep .el-form-item {
margin-bottom: 8px;
}
}
}
// ========== 表格区域 ==========
.table-panel {
position: relative;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
overflow: hidden;
.team-table {
width: 100%;
}
::v-deep .el-empty {
padding: 12px 0;
}
}
// ========== 底部按钮区域 ==========
.footer-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #ebeef5;
.selected-count {
flex: 1;
font-size: 13px;
color: #606266;
}
}
}
</style>
@@ -0,0 +1,649 @@
<template>
<div class="app-container shift-semester-page">
<!-- ==================== 页面标题 ==================== -->
<div class="page-title">班次学期信息管理</div>
<!-- ==================== 1. 查询条件区域 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="班次编号">
<el-input
v-model="searchForm.xydbh"
placeholder="请输入班次编号(模糊)"
clearable
style="width: 220px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="年度">
<el-select v-model="filterYear" placeholder="全部年度" clearable style="width: 140px" @change="handleYearChange">
<el-option v-for="y in yearFilterOptions" :key="y" :label="`${y}年`" :value="y" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ==================== 2. 工具栏按钮区 ==================== -->
<div class="list-toolbar">
<div class="left-group">
<el-button type="primary" icon="el-icon-plus" @click="handleBatchAdd">批量创建班次学期</el-button>
<el-button type="primary" plain icon="el-icon-document-add" @click="handleAdd">新增</el-button>
<el-button type="primary" plain icon="el-icon-edit" :disabled="!currentRow" @click="handleEdit">编辑</el-button>
<el-button type="danger" plain icon="el-icon-delete" :disabled="!selection.length" @click="handleDelete">删除所选</el-button>
<el-button type="primary" plain icon="el-icon-date" :disabled="!selection.length" @click="handleQuickSetDates">快速设定学期日期</el-button>
<el-button type="primary" plain icon="el-icon-notebook-2" :disabled="!selection.length" @click="handleSetJxrw">批量设置教学任务</el-button>
</div>
<div class="right-group">
<el-button type="success" plain icon="el-icon-calendar" :disabled="!currentRow" @click="handleEditCalendar">编辑班历</el-button>
</div>
</div>
<!-- ==================== 3. 班次学期信息列表 ==================== -->
<el-card shadow="never" class="table-card">
<el-table
ref="tableRef"
v-loading="loading"
:data="displayData"
border
stripe
highlight-current-row
size="small"
max-height="480"
class="class-table"
@selection-change="handleSelectionChange"
@current-change="handleCurrentChange"
>
<template slot="empty">
<span>无数据</span>
</template>
<el-table-column type="selection" width="44" align="center" />
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="xydbh" label="班次编号" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="nd" label="年度" width="80" align="center" show-overflow-tooltip />
<el-table-column label="学期第次" width="90" align="center">
<template slot-scope="{ row }">{{ xqdcLabel(row.xqdc) }}</template>
</el-table-column>
<el-table-column prop="kxrq" label="开学日期" width="110" align="center" :formatter="fmtDate" />
<el-table-column prop="jsrq" label="结束日期" width="110" align="center" :formatter="fmtDate" />
<el-table-column prop="zyjsbh" label="专用教室" width="100" align="center" show-overflow-tooltip />
<el-table-column label="教学任务" min-width="160" align="center" show-overflow-tooltip>
<template slot-scope="{ row }">{{ taskName(row.jxrwbh) }}</template>
</el-table-column>
<el-table-column label="开放教员排课" width="110" align="center">
<template slot-scope="{ row }">
<el-tag v-if="Number(row.kfjypk) === 1" type="success" size="small">开放</el-tag>
<el-tag v-else type="info" size="small">关闭</el-tag>
</template>
</el-table-column>
<el-table-column prop="bz" label="备注" min-width="120" align="left" header-align="center" show-overflow-tooltip />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" @click="handleEditCalendar(row)">班历</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDeleteRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="list-pagination"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-card>
<!-- ==================== 班次学期新增/编辑弹窗 ==================== -->
<ClassSemesterDetailDialog
:visible="detailVisible"
:mode="detailMode"
:semester="detailSemester"
:team-info="detailTeamInfo"
:year-options="yearOptions"
:task-options="taskOptions"
@update:visible="val => detailVisible = val"
@submit="handleDetailSubmit"
/>
<!-- ==================== 班历管理弹窗 ==================== -->
<ClassCalendarDialog
:visible="calendarVisible"
:semester="calendarSemester"
:semester-options="semesterOptions"
@update:visible="val => calendarVisible = val"
/>
<!-- ==================== 批量创建班次学期弹窗 ==================== -->
<el-dialog
:visible="batchAddVisible"
title="批量创建班次学期"
width="520px"
:close-on-click-modal="false"
@update:visible="val => batchAddVisible = val"
>
<el-form ref="batchAddFormRef" :model="batchAddForm" :rules="batchAddRules" label-width="100px">
<el-form-item label="年度" prop="nd">
<el-select v-model="batchAddForm.nd" placeholder="请选择年度" style="width: 100%">
<el-option v-for="y in yearOptions" :key="y" :label="`${y}年`" :value="y" />
</el-select>
</el-form-item>
<el-form-item label="学期" prop="xqdc">
<el-select v-model="batchAddForm.xqdc" placeholder="请选择学期" style="width: 100%">
<el-option label="春季学期" value="春季学期" />
<el-option label="夏季学期" value="夏季学期" />
<el-option label="秋季学期" value="秋季学期" />
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startTime">
<el-date-picker v-model="batchAddForm.startTime" type="date" value-format="yyyy-MM-dd" placeholder="请选择开始日期" style="width: 100%" />
</el-form-item>
<el-form-item label="结束日期" prop="endTime">
<el-date-picker v-model="batchAddForm.endTime" type="date" value-format="yyyy-MM-dd" placeholder="请选择结束日期" style="width: 100%" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="batchAddVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleBatchAddSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 快速设定学期日期弹窗 ==================== -->
<el-dialog
:visible="dateRangeVisible"
title="快速设定学期日期"
width="480px"
:close-on-click-modal="false"
@update:visible="val => dateRangeVisible = val"
>
<el-alert
type="info"
:closable="false"
show-icon
:title="`将对选中的 ${selection.length} 条班次学期统一设定开学/结束日期`"
style="margin-bottom: 16px"
/>
<el-form ref="dateRangeFormRef" :model="dateRangeForm" :rules="dateRangeRules" label-width="100px">
<el-form-item label="开学日期" prop="kxrq">
<el-date-picker v-model="dateRangeForm.kxrq" type="date" value-format="yyyy-MM-dd" placeholder="请选择开学日期" style="width: 100%" />
</el-form-item>
<el-form-item label="结束日期" prop="jsrq">
<el-date-picker v-model="dateRangeForm.jsrq" type="date" value-format="yyyy-MM-dd" placeholder="请选择结束日期" style="width: 100%" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dateRangeVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleDateRangeSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 批量设置教学任务弹窗 ==================== -->
<el-dialog
:visible="jxrwVisible"
title="批量设置教学任务"
width="480px"
:close-on-click-modal="false"
@update:visible="val => jxrwVisible = val"
>
<el-alert
type="info"
:closable="false"
show-icon
:title="`将对选中的 ${selection.length} 条班次学期统一设置教学任务`"
style="margin-bottom: 16px"
/>
<el-form ref="jxrwFormRef" :model="jxrwForm" :rules="jxrwRules" label-width="100px">
<el-form-item label="教学任务" prop="jxrwbh">
<el-select v-model="jxrwForm.jxrwbh" placeholder="请选择教学任务" filterable style="width: 100%">
<el-option v-for="opt in taskOptions" :key="opt.bh" :label="opt.rwmc" :value="opt.bh" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="jxrwVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleJxrwSubmit">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import ClassSemesterDetailDialog from './ClassSemesterDetailDialog.vue'
import ClassCalendarDialog from './ClassCalendarDialog.vue'
import { listSemester, allSemester, addSemester, updateSemester, delSemester, batchDeleteSemester, batchUpdateDateRange, batchUpdateJxrwbh, batchAddFromElective } from '@/api/studentRecords/semester'
import { listAllSemester } from '@/api/teachBusiness/semester'
import { listTeachingTask } from '@/api/teachBusiness/teachingTask'
export default {
name: 'ShiftSemester',
components: {
ClassSemesterDetailDialog,
ClassCalendarDialog
},
data() {
return {
loading: false,
saving: false,
// ==================== 查询条件 ====================
searchForm: { xydbh: '' },
filterYear: undefined,
yearFilterOptions: [],
// ==================== 列表与分页 ====================
tableData: [],
selection: [],
currentRow: null,
pageNum: 1,
pageSize: 10,
total: 0,
// ==================== 下拉数据 ====================
/** 年度下拉(来自 /semester/all */
yearOptions: [],
/** 教学任务下拉({ bh, rwmc } */
taskOptions: [],
/** 全部班次学期(用于班历复制目标选择) */
semesterOptions: [],
// ==================== 班次学期新增/编辑 ====================
detailVisible: false,
detailMode: 'add',
detailSemester: null,
detailTeamInfo: null,
// ==================== 班历管理 ====================
calendarVisible: false,
calendarSemester: null,
// ==================== 批量创建班次学期 ====================
batchAddVisible: false,
batchAddForm: { nd: undefined, xqdc: '', startTime: '', endTime: '' },
batchAddRules: {
nd: [{ required: true, message: '请选择年度', trigger: 'change' }],
xqdc: [{ required: true, message: '请选择学期', trigger: 'change' }],
startTime: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
},
// ==================== 快速设定学期日期 ====================
dateRangeVisible: false,
dateRangeForm: { kxrq: '', jsrq: '' },
dateRangeRules: {
kxrq: [{ required: true, message: '请选择开学日期', trigger: 'change' }],
jsrq: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
},
// ==================== 批量设置教学任务 ====================
jxrwVisible: false,
jxrwForm: { jxrwbh: undefined },
jxrwRules: {
jxrwbh: [{ required: true, message: '请选择教学任务', trigger: 'change' }]
}
}
},
computed: {
/** 本地按年度过滤(后端 /semester/list 仅支持 xydbh 模糊筛选) */
displayData() {
if (!this.filterYear) return this.tableData
return this.tableData.filter(item => String(item.nd) === String(this.filterYear))
}
},
mounted() {
this.loadYearOptions()
this.loadTaskOptions()
this.loadSemesterOptions()
this.loadList()
},
methods: {
/* ==================== 格式化 ==================== */
fmtDate(val) {
if (!val) return '-'
return String(val).slice(0, 10)
},
xqdcLabel(val) {
if (val === 1 || val === '1') return '第1学期'
if (val === 2 || val === '2') return '第2学期'
if (val === 3 || val === '3') return '第3学期'
return val !== undefined && val !== null && val !== '' ? `${val}学期` : '-'
},
/** 教学任务编号 → 名称 */
taskName(bh) {
if (!bh) return '-'
const opt = this.taskOptions.find(o => o.bh === bh)
return opt ? opt.rwmc : bh
},
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
/* ==================== 下拉数据加载 ==================== */
loadYearOptions() {
listAllSemester().then(res => {
const list = (res && res.data) || []
const years = list.map(x => x.nd).filter(v => v !== undefined && v !== null && v !== '')
this.yearOptions = [...new Set(years)].sort((a, b) => Number(a) - Number(b))
}).catch(() => {
this.yearOptions = []
})
},
loadTaskOptions() {
listTeachingTask({ pageNum: 1, pageSize: 1000 }).then(res => {
const list = (res && res.data && res.data.records) || []
this.taskOptions = list.filter(o => o.bh)
}).catch(() => {
this.taskOptions = []
})
},
loadSemesterOptions() {
allSemester().then(res => {
this.semesterOptions = (res && res.data) || []
}).catch(() => {
this.semesterOptions = []
})
},
/* ==================== 列表加载 ==================== */
loadList() {
this.loading = true
const query = this.cleanPayload({
pageNum: this.pageNum,
pageSize: this.pageSize,
xydbh: this.searchForm.xydbh
})
listSemester(query).then(res => {
const data = (res && res.data) || {}
this.tableData = data.records || []
this.total = data.total || 0
this.buildYearFilterOptions()
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
/** 本地年度过滤下拉(从当前数据动态生成,避免硬编码) */
buildYearFilterOptions() {
const years = this.tableData.map(r => r.nd).filter(v => v !== undefined && v !== null && v !== '')
this.yearFilterOptions = [...new Set(years)].sort((a, b) => Number(a) - Number(b))
},
handleYearChange() {
this.$nextTick(() => {
if (this.$refs.tableRef) this.$refs.tableRef.setCurrentRow(null)
this.currentRow = null
})
},
handleSizeChange(size) {
this.pageSize = size
this.loadList()
},
handlePageChange(page) {
this.pageNum = page
this.loadList()
},
handleSelectionChange(rows) {
this.selection = rows
},
handleCurrentChange(row) {
this.currentRow = row
},
/* ==================== 查询 / 重置 ==================== */
handleQuery() {
this.pageNum = 1
this.loadList()
},
handleReset() {
this.searchForm.xydbh = ''
this.filterYear = undefined
this.pageNum = 1
this.loadList()
},
/* ==================== 新增 / 编辑 ==================== */
handleAdd() {
this.detailMode = 'add'
this.detailSemester = null
this.detailTeamInfo = null
this.detailVisible = true
},
handleEdit(row) {
const target = row || this.currentRow
if (!target) {
this.$message.warning('请先选择一条班次学期信息')
return
}
this.detailMode = 'edit'
this.detailSemester = target
this.detailTeamInfo = null
this.detailVisible = true
},
handleDetailSubmit(payload) {
this.saving = true
const req = this.detailMode === 'edit'
? updateSemester(payload)
: addSemester(payload)
req.then(() => {
this.saving = false
this.detailVisible = false
this.$message.success(this.detailMode === 'edit' ? '班次学期已更新' : '班次学期已新增')
this.loadList()
this.loadSemesterOptions()
}).catch(() => {
this.saving = false
})
},
/* ==================== 删除 ==================== */
handleDelete() {
if (!this.selection.length) {
this.$message.warning('请先勾选要删除的班次学期')
return
}
this.$confirm(`确定删除选中的 ${this.selection.length} 条班次学期吗?删除后其下班历记录将一并删除。`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const bhList = this.selection.map(r => r.bh)
batchDeleteSemester(bhList).then(() => {
this.$message.success('已删除')
this.loadList()
this.loadSemesterOptions()
})
}).catch(() => {})
},
handleDeleteRow(row) {
this.$confirm(`确定删除班次学期「${row.xydbh || row.bh}」吗?删除后其下班历记录将一并删除。`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delSemester(row.bh).then(() => {
this.$message.success('已删除')
this.loadList()
this.loadSemesterOptions()
})
}).catch(() => {})
},
/* ==================== 编辑班历 ==================== */
handleEditCalendar(row) {
const target = row || this.currentRow
if (!target) {
this.$message.warning('请先在列表中选择一条班次学期信息')
return
}
this.calendarSemester = target
this.calendarVisible = true
},
/* ==================== 批量创建班次学期 ==================== */
handleBatchAdd() {
this.batchAddForm = { nd: this.yearOptions[0] || undefined, xqdc: '', startTime: '', endTime: '' }
this.batchAddVisible = true
this.$nextTick(() => {
if (this.$refs.batchAddFormRef) this.$refs.batchAddFormRef.clearValidate()
})
},
handleBatchAddSubmit() {
this.$refs.batchAddFormRef.validate(valid => {
if (!valid) return
const f = this.batchAddForm
this.saving = true
batchAddFromElective({
nd: f.nd,
xqdc: f.xqdc,
startTime: f.startTime,
endTime: f.endTime
}).then(res => {
this.saving = false
this.batchAddVisible = false
const count = (res && res.data) || 0
this.$message.success(`已创建 ${count} 个班次学期`)
this.loadList()
this.loadSemesterOptions()
}).catch(() => {
this.saving = false
})
})
},
/* ==================== 快速设定学期日期 ==================== */
handleQuickSetDates() {
if (!this.selection.length) {
this.$message.warning('请先勾选要设定日期的班次学期')
return
}
this.dateRangeForm = { kxrq: '', jsrq: '' }
this.dateRangeVisible = true
this.$nextTick(() => {
if (this.$refs.dateRangeFormRef) this.$refs.dateRangeFormRef.clearValidate()
})
},
handleDateRangeSubmit() {
this.$refs.dateRangeFormRef.validate(valid => {
if (!valid) return
const f = this.dateRangeForm
this.saving = true
batchUpdateDateRange({
bhList: this.selection.map(r => r.bh),
xqkssj: f.kxrq,
xqjssj: f.jsrq
}).then(res => {
this.saving = false
this.dateRangeVisible = false
const count = (res && res.data) || 0
this.$message.success(`已更新 ${count} 条班次学期的日期`)
this.loadList()
}).catch(() => {
this.saving = false
})
})
},
/* ==================== 批量设置教学任务 ==================== */
handleSetJxrw() {
if (!this.selection.length) {
this.$message.warning('请先勾选要设置教学任务的班次学期')
return
}
this.jxrwForm = { jxrwbh: undefined }
this.jxrwVisible = true
this.$nextTick(() => {
if (this.$refs.jxrwFormRef) this.$refs.jxrwFormRef.clearValidate()
})
},
handleJxrwSubmit() {
this.$refs.jxrwFormRef.validate(valid => {
if (!valid) return
this.saving = true
batchUpdateJxrwbh({
bhList: this.selection.map(r => r.bh),
jxrwbh: this.jxrwForm.jxrwbh
}).then(res => {
this.saving = false
this.jxrwVisible = false
const count = (res && res.data) || 0
this.$message.success(`已更新 ${count} 条班次学期的教学任务`)
this.loadList()
}).catch(() => {
this.saving = false
})
})
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.shift-semester-page {
// ========== 页面标题 ==========
.page-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-left: 10px;
border-left: 4px solid var(--edu-green-primary);
}
// ========== 1. 查询条件区域 ==========
.search-card {
margin-bottom: 16px;
}
// ========== 2. 工具栏按钮区 ==========
.list-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
.left-group,
.right-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
}
// ========== 3. 数据表格区 ==========
.table-card {
.class-table {
width: 100%;
}
.text-danger {
color: #f56c6c;
}
.list-pagination {
margin-top: 12px;
text-align: right;
}
}
}
</style>
@@ -0,0 +1,744 @@
<template>
<div class="app-container management-page subject-page">
<!-- ==================== 2. 查询条件区域 ==================== -->
<el-card shadow="never" class="search-card">
<div class="section-heading">
<div>
<h2 class="section-title">筛选条件</h2>
<p class="section-description">按课程名称代码和培训属性筛选课程科目</p>
</div>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
</div>
<el-form :model="searchForm">
<el-row :gutter="32">
<!-- 左栏 -->
<el-col :xs="24" :md="12">
<div class="query-field">
<span class="q-label no-check">课名称</span>
<el-input v-model="searchForm.kmc" placeholder="请输入课名称" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">科目代码</span>
<el-input v-model="searchForm.kmdm" placeholder="请输入科目代码" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">培训层次</span>
<el-input v-model="searchForm.pxcc" placeholder="请输入培训层次" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">课程类型</span>
<el-input v-model="searchForm.kclx" placeholder="请输入课程类型" clearable class="q-control" />
</div>
</el-col>
<!-- 右栏 -->
<el-col :xs="24" :md="12">
<div class="query-field">
<span class="q-label no-check">简称</span>
<el-input v-model="searchForm.jc" placeholder="请输入简称" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">教研室代号</span>
<el-input v-model="searchForm.jysdh" placeholder="请输入教研室代号" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">培训类型</span>
<el-input v-model="searchForm.pxlx" placeholder="请输入培训类型" clearable class="q-control" />
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 3. 操作按钮区域 ==================== -->
<el-card shadow="never" class="action-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">课程科目操作</h2>
<p class="section-description">维护课程科目或下载业务数据与导入模板</p>
</div>
</div>
<div class="action-row">
<el-button type="primary" icon="el-icon-plus" @click="handleOpenDialog">新建课程科目</el-button>
<el-button type="danger" plain icon="el-icon-delete" @click="handleBatchDelete">删除所选</el-button>
<el-button icon="el-icon-download" @click="handleDownloadCourse">下载课程科目基本信息</el-button>
<el-button icon="el-icon-download" @click="handleDownloadTextbook">下载课程教材基本信息</el-button>
</div>
<div class="action-row">
<el-button icon="el-icon-download" @click="handleDownloadTemplate">课程科目数据文件模板下载</el-button>
</div>
</el-card>
<!-- ==================== 4. 文件上传区域 ==================== -->
<el-card shadow="never" class="upload-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">数据导入</h2>
<p class="section-description">选择填写完成的课程科目模板并上传</p>
</div>
</div>
<div class="upload-row">
<div class="upload-left">
<el-button icon="el-icon-folder-opened" @click="handleSelectFile">选择文件</el-button>
<span class="file-name" :class="{ 'has-file': selectedFile }">{{ fileName }}</span>
<input ref="fileInputRef" type="file" style="display: none" @change="handleFileChange" />
</div>
<div class="upload-right">
<el-checkbox v-model="coverExist">覆盖已存在课程科目</el-checkbox>
<el-button type="primary" icon="el-icon-upload2" @click="handleUpload">上传数据</el-button>
</div>
</div>
</el-card>
<!-- ==================== 6. 数据表格区域 ==================== -->
<el-card shadow="never" class="table-card">
<div class="section-heading section-heading--compact table-heading">
<div>
<h2 class="section-title">课程科目列表</h2>
<p class="section-description"> {{ total }} 条数据</p>
</div>
</div>
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="40" align="center" />
<el-table-column prop="kbh" label="课编号" width="130" show-overflow-tooltip />
<el-table-column prop="kmc" label="课名称" width="200" show-overflow-tooltip />
<el-table-column prop="jc" label="简称" width="130" show-overflow-tooltip />
<el-table-column prop="jysdh" label="教研室代号" width="150" show-overflow-tooltip />
<el-table-column prop="kclx" label="课程类型" width="150" align="center" />
<el-table-column prop="pxlx" label="培训类型" width="150" align="center" />
<el-table-column prop="pxcc" label="培训层次" width="200" align="center" />
<el-table-column prop="xf" label="学分" align="center" />
<el-table-column prop="xs" label="学时" width="160" align="center" />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination-wrapper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- 新增/编辑课程科目弹窗 -->
<el-dialog
:visible="dialogVisible"
:title="dialogTitle"
width="1000px"
:close-on-click-modal="false"
class="add-dialog"
@update:visible="val => dialogVisible = val"
>
<el-form ref="addFormRef" :model="addForm" :rules="rules" label-width="120px" class="add-form" :status-icon="false">
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="课编号" prop="kbh">
<el-input v-model="addForm.kbh" :disabled="isEdit" placeholder="新增时留空自动生成" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="课名称" prop="kmc">
<el-input v-model="addForm.kmc" placeholder="请输入课名称" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="简称" prop="jc">
<el-input v-model="addForm.jc" placeholder="请输入简称" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="教研室代号" prop="jysdh">
<el-input v-model="addForm.jysdh" placeholder="请输入教研室代号" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="学分" prop="xf">
<el-input-number v-model="addForm.xf" :min="0" :precision="1" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="学时" prop="xs">
<el-input-number v-model="addForm.xs" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="培训层次" prop="pxcc">
<el-input v-model="addForm.pxcc" placeholder="请输入培训层次" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="培训类型" prop="pxlx">
<el-input v-model="addForm.pxlx" placeholder="请输入培训类型" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="课程类型" prop="kclx">
<el-input v-model="addForm.kclx" placeholder="请输入课程类型" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="考试课时" prop="ksks">
<el-input-number v-model="addForm.ksks" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="成绩分制" prop="cjfz">
<el-input v-model="addForm.cjfz" placeholder="请输入成绩分制" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="不计入学员平均分" prop="bjrxypjf">
<el-select v-model="addForm.bjrxypjf" class="w-full">
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="理论学时" prop="llxs">
<el-input-number v-model="addForm.llxs" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="实践学时" prop="sjxs">
<el-input-number v-model="addForm.sjxs" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="周课时" prop="zks">
<el-input-number v-model="addForm.zks" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="课类型" prop="klx">
<el-input v-model="addForm.klx" placeholder="请输入课类型" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="考试课时不显示" prop="ksksbxs">
<el-select v-model="addForm.ksksbxs" class="w-full">
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="课程统一编号" prop="kctybh">
<el-input v-model="addForm.kctybh" placeholder="请输入课程统一编号" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="辅导答疑课时" prop="fddyks">
<el-input-number v-model="addForm.fddyks" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="自修课时" prop="zxks">
<el-input-number v-model="addForm.zxks" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="修订日期" prop="xdrq">
<el-input v-model="addForm.xdrq" placeholder="如 2026-08-24" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="节次优选级" prop="jcyxj">
<el-input v-model="addForm.jcyxj" placeholder="请输入节次优选级" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="课程支持方式" prop="kczcfs">
<el-input v-model="addForm.kczcfs" placeholder="请输入课程支持方式" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="课程建设方式" prop="kcjsfs">
<el-input v-model="addForm.kcjsfs" placeholder="请输入课程建设方式" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="政治类课程" prop="zzlkc">
<el-select v-model="addForm.zzlkc" class="w-full">
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="考核方式" prop="khfs">
<el-input v-model="addForm.khfs" placeholder="请输入考核方式" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="形成性成绩分值" prop="xcxcjfz">
<el-input-number v-model="addForm.xcxcjfz" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="终结性成绩分值" prop="zjxcjfz">
<el-input-number v-model="addForm.zjxcjfz" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="密级" prop="mj">
<el-input v-model="addForm.mj" placeholder="请输入密级" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="评选情况" prop="pxqk">
<el-input v-model="addForm.pxqk" placeholder="请输入评选情况" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24">
<el-form-item label="备注">
<el-input v-model="addForm.bz" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="addLoading" @click="handleSubmit">保存</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import { listKb as listSubject, addKb as addSubject, updateKb as updateSubject, deleteKb as deleteSubject, getKb } from '@/api/teachOffice/kb'
export default {
name: 'SubjectIndex',
data() {
return {
// ==================== 1. 顶部状态提示 ====================
statusTip: '请输入查询条件',
// ==================== 2. 查询条件 ====================
searchForm: {
kmc: '',
jc: '',
kmdm: '',
jysdh: '',
kclx: '',
pxlx: '',
pxcc: ''
},
// ==================== 列表数据 ====================
loading: false,
tableData: [],
total: 0,
pageNum: 1,
pageSize: 20,
selectedRows: [],
// ==================== 文件上传 ====================
selectedFile: null,
coverExist: false,
// ==================== 新增/编辑弹窗 ====================
dialogVisible: false,
addLoading: false,
isEdit: false,
addForm: this.createEmptyForm(),
rules: {
kmc: [{ required: true, message: '请输入课名称', trigger: 'blur' }],
jysdh: [{ required: true, message: '请输入教研室代号', trigger: 'blur' }],
xf: [{ required: true, message: '请输入学分', trigger: 'blur' }],
pxcc: [{ required: true, message: '请输入培训层次', trigger: 'blur' }],
pxlx: [{ required: true, message: '请输入培训类型', trigger: 'blur' }],
kclx: [{ required: true, message: '请输入课程类型', trigger: 'blur' }],
ksks: [{ required: true, message: '请输入考试课时', trigger: 'blur' }],
cjfz: [{ required: true, message: '请输入成绩分制', trigger: 'blur' }],
llxs: [{ required: true, message: '请输入理论学时', trigger: 'blur' }],
sjxs: [{ required: true, message: '请输入实践学时', trigger: 'blur' }],
zks: [{ required: true, message: '请输入周课时', trigger: 'blur' }],
klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }],
kctybh: [{ required: true, message: '请输入课程统一编号', trigger: 'blur' }],
fddyks: [{ required: true, message: '请输入辅导答疑课时', trigger: 'blur' }],
zxks: [{ required: true, message: '请输入自修课时', trigger: 'blur' }],
xdrq: [{ required: true, message: '请输入修订日期', trigger: 'blur' }],
jcyxj: [{ required: true, message: '请输入节次优选级', trigger: 'blur' }],
kczcfs: [{ required: true, message: '请输入课程支持方式', trigger: 'blur' }],
kcjsfs: [{ required: true, message: '请输入课程建设方式', trigger: 'blur' }],
khfs: [{ required: true, message: '请输入考核方式', trigger: 'blur' }],
xcxcjfz: [{ required: true, message: '请输入形成性成绩分值', trigger: 'blur' }],
zjxcjfz: [{ required: true, message: '请输入终结性成绩分值', trigger: 'blur' }],
mj: [{ required: true, message: '请输入密级', trigger: 'blur' }],
pxqk: [{ required: true, message: '请输入评选情况', trigger: 'blur' }]
}
}
},
computed: {
dialogTitle() {
return this.isEdit ? '编辑课程科目' : '新增课程科目'
},
fileName() {
return (this.selectedFile && this.selectedFile.name) || '未选择任何文件'
}
},
mounted() {
this.fetchList()
},
methods: {
/** 移除空值 */
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
/** 创建空白课程科目表单 */
createEmptyForm() {
return {
kbh: '', kmc: '', jysdh: '', bz: '', xh: '', xf: 0, jc: '', xs: 0,
pxcc: '', pxlx: '', kclx: '', ksks: 0, py: '', gfmc: '', cjfz: '',
bjrxypjf: 0, jxglbmbh: '', llxs: 0, sjxs: 0, kmdm: '', zks: 0, klx: '',
ksksbxs: 0, kctybh: '', sydx: '', fddyks: 0, zxks: 0, bb: '', xdrq: '',
jcyxj: '', kczcfs: '', kcjsfs: '', jsonzd: '', zzlkc: 0, khfs: '',
xcxcjfz: 0, zjxcjfz: 0, mj: '', pxqk: ''
}
},
resetForm() {
this.addForm = this.createEmptyForm()
},
// ==================== 列表加载 ====================
buildQuery() {
return this.cleanPayload({
pageNum: this.pageNum,
pageSize: this.pageSize,
kmc: this.searchForm.kmc,
jc: this.searchForm.jc,
kmdm: this.searchForm.kmdm,
jysdh: this.searchForm.jysdh,
kclx: this.searchForm.kclx,
pxlx: this.searchForm.pxlx,
pxcc: this.searchForm.pxcc
})
},
fetchList() {
this.loading = true
listSubject(this.buildQuery())
.then(res => {
const data = (res && res.data) || {}
this.tableData = data.records || []
this.total = data.total || 0
this.statusTip = `共检索到 ${this.total} 条记录`
})
.finally(() => {
this.loading = false
})
},
// ==================== 查询 ====================
handleQuery() {
this.pageNum = 1
this.fetchList()
},
handlePageChange(current) {
this.pageNum = current
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
handleSelectionChange(rows) {
this.selectedRows = rows
},
// ==================== 删除所选 ====================
handleBatchDelete() {
if (this.selectedRows.length === 0) {
this.$message.warning('请先选择要删除的记录')
return
}
const kbhList = this.selectedRows.map(r => r.kbh)
this.$confirm(`确定删除所选 ${kbhList.length} 条课程科目吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const tasks = kbhList.map(kbh => deleteSubject(kbh))
Promise.all(tasks)
.then(() => {
this.$message.success(`已删除 ${kbhList.length} 条记录`)
this.fetchList()
})
.catch(() => {
this.$message.error('删除失败')
})
}).catch(() => {})
},
/** 单条删除 */
handleDelete(row) {
this.$confirm(`确定删除课程科目「${row.kmc || row.kbh}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return deleteSubject(row.kbh)
}).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch((err) => {
if (err && err !== 'cancel') {
this.$message.error('删除失败')
}
})
},
// ==================== 下载(后端暂未提供) ====================
handleDownloadCourse() {
this.$message.warning('后端暂未提供该接口')
},
handleDownloadTextbook() {
this.$message.warning('后端暂未提供该接口')
},
handleDownloadTemplate() {
this.$message.warning('后端暂未提供该接口')
},
// ==================== 文件上传(后端暂未提供) ====================
handleSelectFile() {
this.$refs.fileInputRef && this.$refs.fileInputRef.click()
},
handleFileChange(e) {
const input = e.target
this.selectedFile = (input.files && input.files[0]) || null
},
handleUpload() {
this.$message.warning('后端暂未提供该接口')
},
// ==================== 新增/编辑弹窗 ====================
handleOpenDialog() {
this.isEdit = false
this.resetForm()
this.dialogVisible = true
},
handleEdit(row) {
this.addLoading = true
getKb(row.kbh)
.then(res => {
const data = (res && res.data) || row
this.isEdit = true
this.addForm = Object.assign(this.createEmptyForm(), data)
this.dialogVisible = true
})
.finally(() => {
this.addLoading = false
})
},
handleSubmit() {
this.$refs.addFormRef.validate((valid) => {
if (!valid) return
this.addLoading = true
const payload = this.cleanPayload({ ...this.addForm })
const requestFn = this.isEdit ? updateSubject : addSubject
requestFn(payload)
.then(() => {
this.$message.success(this.isEdit ? '修改成功' : '保存成功')
this.dialogVisible = false
this.fetchList()
})
.finally(() => {
this.addLoading = false
})
})
},
handleCancel() {
this.dialogVisible = false
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
// ==================== 1. 顶部状态提示 ====================
.status-tip {
display: inline-block;
background: #dc3545;
color: #fff;
font-size: 12px;
padding: 3px 10px;
border-radius: 4px;
margin-bottom: 12px;
}
// ==================== 2. 查询条件区域 ====================
.search-card {
margin-bottom: 16px;
.query-field {
display: flex;
align-items: center;
margin-bottom: 14px;
min-height: 32px;
::v-deep(.el-checkbox) {
margin-right: 6px;
flex-shrink: 0;
}
.q-label {
width: 120px;
flex-shrink: 0;
font-size: 13px;
color: #303133;
text-align: right;
margin-right: 10px;
white-space: nowrap;
&.no-check {
margin-left: 24px;
}
}
.q-control {
flex: 1;
min-width: 0;
}
}
.notice {
font-size: 12px;
color: #f56c6c;
margin-top: 8px;
text-align: left;
}
}
// ==================== 3. 操作按钮区域 ====================
.action-card {
margin-bottom: 16px;
.action-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
// ==================== 4. 文件上传区域 ====================
.upload-card {
margin-bottom: 16px;
.upload-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
.upload-left {
display: flex;
align-items: center;
gap: 12px;
.file-name {
font-size: 12px;
color: #999;
&.has-file {
color: #303133;
}
}
}
.upload-right {
display: flex;
align-items: center;
gap: 12px;
}
}
}
// ==================== 5. 红色提示文字 ====================
.red-tip {
font-size: 14px;
color: #f56c6c;
margin-bottom: 16px;
line-height: 1.6;
}
// ==================== 6. 数据表格区域 ====================
.table-card {
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
::v-deep(.el-table .cell) {
font-size: 12px;
}
.text-danger {
color: #f56c6c;
}
}
// ==================== 新增/编辑弹窗 ====================
.add-dialog {
::v-deep(.el-dialog__body) {
max-height: 70vh;
overflow: auto;
}
}
.add-form {
.w-full {
width: 100%;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
@@ -0,0 +1,513 @@
<template>
<div class="app-container task-plan-page">
<!-- ==================== 页面标题 ==================== -->
<div class="page-title">教学任务计划管理</div>
<!-- ==================== 1. 查询条件区域 ==================== -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="任务名称">
<el-input
v-model="searchForm.rwmc"
placeholder="请输入任务名称"
clearable
style="width: 180px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="年度">
<el-select v-model="searchForm.nd" placeholder="请选择年度" clearable style="width: 140px">
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.zt" placeholder="请选择状态" clearable style="width: 140px">
<el-option
v-for="opt in statusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- ==================== 2. 操作区域 ==================== -->
<div class="list-toolbar">
<div class="left-group">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新建教学任务</el-button>
</div>
</div>
<!-- ==================== 3. 数据表格 ==================== -->
<el-card shadow="never" class="table-card">
<el-table v-loading="loading" :data="tableData" border stripe class="task-table">
<template slot="empty">
<span>无数据</span>
</template>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="rwmc" label="任务名称" width="250" align="center" show-overflow-tooltip />
<el-table-column prop="nd" label="年度" width="150" align="center" />
<el-table-column label="状态" width="100" align="center">
<template slot-scope="{ row }">
<el-tag :type="row.zt === '发布' ? 'success' : 'info'" size="small">
{{ row.zt || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fbsj" label="发布时间" width="180" align="center" :formatter="fmtDateTime" />
<el-table-column prop="jssj" label="结束时间" width="180" align="center" :formatter="fmtDateTime" />
<el-table-column prop="cjsj" label="创建时间" width="180" align="center" :formatter="fmtDateTime" />
<el-table-column prop="jcxqscsj" label="教材需求生成时间" align="center" :formatter="fmtDateTime" />
<el-table-column label="操作" width="300" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button
type="text"
size="small"
:disabled="row.zt === '发布'"
@click="handlePublish(row)"
>发布</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 4. 新增/修改对话框 ==================== -->
<el-dialog
:visible="dialogVisible"
:title="dialogTitle"
width="560px"
:close-on-click-modal="false"
@update:visible="val => dialogVisible = val"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px" class="add-form">
<el-form-item v-if="isEdit" label="编号" prop="bh">
<el-input v-model="form.bh" disabled placeholder="编号不可修改" />
</el-form-item>
<el-form-item v-else label="编号">
<el-input disabled placeholder="新增后由系统自动生成" />
</el-form-item>
<el-form-item label="任务名称" prop="rwmc">
<el-input v-model="form.rwmc" placeholder="请输入任务名称" clearable />
</el-form-item>
<el-form-item label="年度" prop="nd">
<el-select v-model="form.nd" placeholder="请选择年度" style="width: 100%">
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="zt">
<el-select v-model="form.zt" placeholder="请选择状态" style="width: 100%">
<el-option
v-for="opt in statusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="form.fbsj"
type="datetime"
value-format="yyyy-MM-ddTHH:mm:ss"
placeholder="请选择发布时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="结束时间">
<el-date-picker
v-model="form.jssj"
type="datetime"
value-format="yyyy-MM-ddTHH:mm:ss"
placeholder="请选择结束时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="教材需求生成时间">
<el-date-picker
v-model="form.jcxqscsj"
type="datetime"
value-format="yyyy-MM-ddTHH:mm:ss"
placeholder="请选择教材需求生成时间"
style="width: 100%"
/>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 5. 详情对话框 ==================== -->
<el-dialog
:visible="detailVisible"
title="教学任务详情"
width="620px"
:close-on-click-modal="false"
@update:visible="val => detailVisible = val"
>
<el-descriptions v-if="detailData.bh" :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ detailData.rwmc || '-' }}</el-descriptions-item>
<el-descriptions-item label="年度">{{ fmtVal(detailData.nd) }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.zt || '-' }}</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ fmtVal(detailData.fbsj) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ fmtVal(detailData.jssj) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ fmtVal(detailData.cjsj) }}</el-descriptions-item>
<el-descriptions-item label="教材需求生成时间">{{ fmtVal(detailData.jcxqscsj) }}</el-descriptions-item>
</el-descriptions>
<div v-else v-loading="detailLoading" class="detail-empty">加载中...</div>
<div slot="footer">
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
/**
* 教学任务计划管理(完整管理页)
* 对应菜单:teachOffice/taskPlan
* 提供 teachingTask 全套真实接口操作:分页查询 list、详情 get?bh=、新增 add、编辑 update、
* 发布 batchPublish?bh=(需先填写教研室任务书)、删除 delete?bh=(级联删除教研室任务书)
* 只读查询视图在 src/views/teachBusiness/teachingTask/index.vue(教学任务列表)提供,避免重复操作同一实体
*/
import {
listTeachingTask,
getTeachingTask,
addTeachingTask,
updateTeachingTask,
deleteTeachingTask,
publishTeachingTask
} from '@/api/teachBusiness/teachingTask'
import { listAllSemester } from '@/api/teachBusiness/semester'
export default {
name: 'TaskPlan',
data() {
return {
// ==================== 1. 查询条件 ====================
searchForm: {
rwmc: '',
nd: undefined,
zt: ''
},
yearOptions: [],
statusOptions: [
{ label: '未发布', value: '未发布' },
{ label: '发布', value: '发布' }
],
// ==================== 2. 表格数据 ====================
loading: false,
tableData: [],
pageNum: 1,
pageSize: 10,
total: 0,
// ==================== 3. 新增/修改表单 ====================
dialogVisible: false,
dialogTitle: '新建教学任务',
isEdit: false,
saving: false,
form: this.createEmptyForm(),
rules: {
rwmc: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
nd: [{ required: true, message: '请选择年度', trigger: 'change' }],
zt: [{ required: true, message: '请选择状态', trigger: 'change' }]
},
// ==================== 4. 详情 ====================
detailVisible: false,
detailLoading: false,
detailData: {}
}
},
created() {
this.loadYearOptions().then(() => this.fetchList())
},
methods: {
/* ---------- 年度下拉(数据来自 /semester/all,禁止硬编码) ---------- */
loadYearOptions() {
return listAllSemester().then(response => {
const list = response.data || []
const map = {}
list.forEach(item => {
if (item.nd !== null && item.nd !== undefined && item.nd !== '') map[item.nd] = item
})
const arr = Object.keys(map).sort((a, b) => String(b).localeCompare(String(a)))
this.yearOptions = arr
// 默认年度:优先当前学期(dqxq=true),否则取最新年度
const current = list.find(item => item.dqxq === true)
const defaultNd = (current && current.nd) || arr[0]
if (!this.searchForm.nd) this.searchForm.nd = defaultNd
return arr
}).catch(() => {
this.yearOptions = []
return []
})
},
/* ---------- 通用格式化 ---------- */
fmtDateTime(row, column, cellValue) {
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
return String(cellValue).replace('T', ' ').slice(0, 16)
},
fmtVal(val) {
if (val === null || val === undefined || val === '') return '-'
return String(val).replace('T', ' ').slice(0, 16)
},
/** 移除空值(''/null/undefined),保留 0 等有效值 */
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
/* ---------- 列表加载 ---------- */
fetchList() {
this.loading = true
const params = { pageNum: this.pageNum, pageSize: this.pageSize }
if (this.searchForm.rwmc && this.searchForm.rwmc.trim()) params.rwmc = this.searchForm.rwmc.trim()
if (this.searchForm.nd !== undefined && this.searchForm.nd !== null && this.searchForm.nd !== '') {
params.nd = this.searchForm.nd
}
if (this.searchForm.zt && this.searchForm.zt.trim()) params.zt = this.searchForm.zt.trim()
listTeachingTask(params).then(res => {
const data = (res && res.data) || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleQuery() {
this.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
/* ---------- 新增 / 修改 ---------- */
createEmptyForm() {
return {
bh: '',
rwmc: '',
nd: undefined,
zt: '未发布',
fbsj: '',
jssj: '',
jcxqscsj: ''
}
},
handleAdd() {
this.form = this.createEmptyForm()
if (!this.form.nd && this.searchForm.nd) this.form.nd = this.searchForm.nd
this.isEdit = false
this.dialogTitle = '新建教学任务'
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
handleEdit(row) {
this.form = {
bh: row.bh || '',
rwmc: row.rwmc || '',
nd: row.nd,
zt: row.zt || '未发布',
fbsj: row.fbsj || '',
jssj: row.jssj || '',
jcxqscsj: row.jcxqscsj || ''
}
this.isEdit = true
this.dialogTitle = '修改教学任务'
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) {
this.$message.warning('请完善必填项后再提交')
return
}
const payload = this.cleanPayload(this.buildPayload())
this.saving = true
const req = this.isEdit ? updateTeachingTask(payload) : addTeachingTask(payload)
req.then(() => {
this.$message.success(this.isEdit ? '修改成功' : '新建成功')
this.dialogVisible = false
this.fetchList()
}).catch(() => {}).finally(() => {
this.saving = false
})
})
},
/** 构建请求体:年度转数字以匹配后端 Integer,时间字段有值才传 */
buildPayload() {
const f = this.form
const payload = {}
if (this.isEdit && f.bh && String(f.bh).trim()) payload.bh = String(f.bh).trim()
if (f.rwmc && String(f.rwmc).trim()) payload.rwmc = String(f.rwmc).trim()
if (f.nd !== undefined && f.nd !== null && f.nd !== '') payload.nd = Number(f.nd)
if (f.zt && String(f.zt).trim()) payload.zt = String(f.zt).trim()
if (f.fbsj) payload.fbsj = f.fbsj
if (f.jssj) payload.jssj = f.jssj
if (f.jcxqscsj) payload.jcxqscsj = f.jcxqscsj
return payload
},
/* ---------- 详情 ---------- */
handleDetail(row) {
this.detailVisible = true
this.detailLoading = true
this.detailData = {}
getTeachingTask(row.bh).then(res => {
this.detailData = (res && res.data) || {}
this.detailLoading = false
}).catch(() => {
this.detailLoading = false
})
},
/* ---------- 删除 ---------- */
handleDelete(row) {
this.$confirm(`确定要删除「${row.rwmc || row.bh || '该记录'}」吗?删除将级联删除关联的教研室任务书。`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => deleteTeachingTask(row.bh)).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch(() => {})
},
/* ---------- 发布 ---------- */
handlePublish(row) {
this.$confirm(`确定要发布教学任务「${row.rwmc || row.bh || '该记录'}」吗?发布前请先填写关联的教研室任务书。`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => publishTeachingTask(row.bh)).then(() => {
this.$message.success('发布成功')
this.fetchList()
}).catch(() => {})
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.task-plan-page {
.page-title {
text-align: center;
font-size: 20px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
// ========== 1. 查询条件区域 ==========
.search-card {
margin-bottom: 16px;
.search-form {
.el-form-item {
margin-bottom: 0;
margin-right: 24px;
}
}
}
// ========== 2. 工具栏 ==========
.list-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
.left-group,
.right-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
}
// ========== 3. 数据表格区域 ==========
.table-card {
.task-table {
width: 100%;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.text-danger {
color: #f56c6c;
}
}
// ========== 4. 详情 ==========
.detail-empty {
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,388 @@
<template>
<div class="page-container">
<!-- 查询条件 -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="110px" class="search-form">
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="开始日期">
<el-date-picker v-model="searchForm.ksrq" type="date" value-format="yyyy-MM-dd" placeholder="选择开始日期"
class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期">
<el-date-picker v-model="searchForm.jsrq" type="date" value-format="yyyy-MM-dd" placeholder="选择结束日期"
class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="课程名称">
<el-input v-model="searchForm.kcmc" placeholder="请输入课程名称" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教研室">
<el-select v-model="searchForm.jys" placeholder="请选择教研室" clearable class="w-full">
<el-option v-for="item in jysOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="教学方法">
<el-select v-model="searchForm.jxff" placeholder="请选择教学方法" clearable class="w-full">
<el-option v-for="item in jxffOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="授课教员">
<el-select v-model="searchForm.skjy" placeholder="请选择授课教员" clearable class="w-full">
<el-option v-for="item in skjyOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="24">
<el-form-item label="改动情况">
<div class="change-options">
<el-radio-group v-model="searchForm.createSource">
<el-radio v-for="item in createSourceOptions" :key="item" :label="item">{{ item }}</el-radio>
</el-radio-group>
<el-checkbox-group v-model="searchForm.changeTypes">
<el-checkbox v-for="item in changeTypeOptions" :key="item" :label="item">{{ item }}</el-checkbox>
</el-checkbox-group>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="任务类别">
<el-select v-model="searchForm.rwlb" placeholder="请选择任务类别" clearable class="w-full">
<el-option v-for="item in rwlbOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训期班">
<el-input v-model="searchForm.pxqb" placeholder="请输入培训期班" clearable />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="rwlb" label="任务类别" width="130" align="center" show-overflow-tooltip />
<el-table-column prop="jyxm" label="主讲教员" width="100" align="center" />
<el-table-column prop="rq" label="日期" width="150" align="center">
<template slot-scope="scope">{{ scope.row.rq ? formatDate(scope.row.rq) : '-' }}</template>
</el-table-column>
<el-table-column label="节次" width="80" align="center">
<template slot-scope="scope">
{{ scope.row.qsjc && scope.row.jsjc ? `${scope.row.qsjc}-${scope.row.jsjc}` : (scope.row.qsjc || '-') }}
</template>
</el-table-column>
<el-table-column prop="kcmc" label="课程名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="pxqbxx" label="培训期班信息" min-width="140" show-overflow-tooltip />
<el-table-column label="人数" width="70" align="center">
<template slot-scope="scope">{{ scope.row.sdrs || scope.row.ydrs || '-' }}</template>
</el-table-column>
<el-table-column label="分组指导教员" width="120" align="center">
<template slot-scope="scope">{{ scope.row.fzyzdjy || '-' }}</template>
</el-table-column>
<el-table-column prop="jsfs" label="教学方法" width="100" align="center" />
<el-table-column prop="zt" label="日志状态" width="100" align="center" />
<el-table-column prop="gdqk" label="变动汇总" min-width="120" show-overflow-tooltip />
<el-table-column prop="jxrzcjfs" label="创建方式" width="100" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right" class-name="table-action-column">
<template slot-scope="scope">
<el-button type="text" @click="handleDetail(scope.row)">详细</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination :current-page="pageNum" :page-size="pageSize" :total="total" :page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper" background @current-change="handlePageChange"
@size-change="handleSizeChange" />
</div>
</el-card>
<!-- 日志详情对话框 -->
<el-dialog :visible.sync="detailVisible" title="教学日志详情" width="700px" destroy-on-close>
<div v-loading="detailLoading">
<el-descriptions v-if="!detailLoading" :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="统一编号">{{ detailData.tybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教员编号">{{ detailData.jybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教员姓名">{{ detailData.jyxm || '-' }}</el-descriptions-item>
<el-descriptions-item label="课程名称">{{ detailData.kcmc || '-' }}</el-descriptions-item>
<el-descriptions-item label="任务类别">{{ detailData.rwlb || '-' }}</el-descriptions-item>
<el-descriptions-item label="日期">{{ detailData.rq || '-' }}</el-descriptions-item>
<el-descriptions-item label="节次">
{{ detailData.qsjc && detailData.jsjc ? `${detailData.qsjc}-${detailData.jsjc}` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="教学方法">{{ detailData.jsfs || detailData.jxff || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建方式">{{ detailData.jxrzcjfs || '-' }}</el-descriptions-item>
<el-descriptions-item label="应到人数">{{ detailData.ydrs || '-' }}</el-descriptions-item>
<el-descriptions-item label="实到人数">{{ detailData.sdrs || '-' }}</el-descriptions-item>
<el-descriptions-item label="培训期班信息" :span="2">{{ detailData.pxqbxx || '-' }}</el-descriptions-item>
<el-descriptions-item label="授课地点" :span="2">{{ detailData.skdd || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.zt || '-' }}</el-descriptions-item>
<el-descriptions-item label="教研室">{{ detailData.jysmc || detailData.jys || '-' }}</el-descriptions-item>
<el-descriptions-item label="变动汇总" :span="2">{{ detailData.gdqk || '-' }}</el-descriptions-item>
<el-descriptions-item label="变动详细情况" :span="2">{{ detailData.gdxxqk || '-' }}</el-descriptions-item>
<el-descriptions-item label="退回意见" :span="2">{{ detailData.thyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.cjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="修改时间">{{ detailData.xgsj || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { formatDate } from '@/utils/index'
import { getAuditedTeachingLogList, getTeachingLogByBh } from '@/api/log'
export default {
name: 'AuditedView',
data() {
return {
// ==================== 查询表单 ====================
searchForm: {
ksrq: '',
jsrq: '',
kcmc: '',
jys: '',
jxff: '',
skjy: '',
createSource: '',
changeTypes: [],
rwlb: '',
pxqb: ''
},
jysOptions: ['教研室', '防空兵系', '通信工程系', '装甲兵系'],
jxffOptions: ['理论讲授', '讨论', '实操', '演示'],
skjyOptions: ['教务处', '张三', '李四', '王五'],
rwlbOptions: ['全军院校训练规划内任务', '其他任务'],
createSourceOptions: ['由教学计划创建', '已改动教学计划', '手动拟制'],
changeTypeOptions: [
'教员变动',
'时间变动',
'课程变动',
'授课方式变动',
'任务类别变动',
'期班变动',
'地点变动',
'课题变动'
],
// ==================== 数据表格 ====================
loading: false,
tableData: [],
selectedRows: [],
pageNum: 1,
pageSize: 20,
total: 0,
// ==================== 详情 ====================
detailVisible: false,
detailLoading: false,
detailData: {}
}
},
created() {
this.fetchList()
},
methods: {
handleSelectionChange(rows) {
this.selectedRows = rows
},
/** 根据已填写的查询值构建实际查询参数 */
buildSearchParams() {
const form = this.searchForm
const params = { pageNum: this.pageNum, pageSize: this.pageSize }
if (form.ksrq) params.ksrq = form.ksrq
if (form.jsrq) params.jsrq = form.jsrq
if (form.kcmc) params.kcmc = form.kcmc
if (form.jys) params.jys = form.jys
if (form.jxff) params.jxff = form.jxff
if (form.skjy) params.skjy = form.skjy
if (form.rwlb) params.rwlb = form.rwlb
if (form.pxqb) params.pxqb = form.pxqb
if (form.createSource) params.gdqk = form.createSource
if (form.changeTypes.length > 0) {
params.gdxxqk = form.changeTypes.join(',')
}
return params
},
extractListData(res) {
const data = res.data || res
const list = data.records || data.list || data.rows || []
const totalCount = data.total ?? data.totalCount ?? data.totalRows ?? 0
return { list, total: totalCount }
},
fetchList() {
this.loading = true
return getAuditedTeachingLogList(this.buildSearchParams())
.then((res) => {
if (res.code === 200 || res.code === 0) {
const { list, total } = this.extractListData(res)
this.tableData = list
this.total = total
}
})
.catch(() => {})
.finally(() => {
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
handleDetail(row) {
this.detailData = { ...row }
this.detailVisible = true
const bh = row.bh
if (!bh) return
this.detailLoading = true
getTeachingLogByBh(String(bh))
.then((res) => {
if ((res.code === 200 || res.code === 0) && res.data) {
this.detailData = res.data
}
})
.catch(() => {})
.finally(() => {
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.page-container {
padding: 4px 8px;
}
.search-card {
margin-bottom: 16px;
.search-form {
.w-full {
width: 100%;
}
::v-deep .el-row {
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
}
::v-deep .el-col {
border-right: 1px solid #ebeef5;
&:last-child {
border-right: none;
}
}
::v-deep .el-form-item__label {
white-space: normal;
line-height: 1.4;
text-align: left;
justify-content: flex-start;
}
.el-form-item {
margin-bottom: 0;
padding: 10px 12px;
}
.change-options {
display: flex;
flex-direction: column;
gap: 8px;
::v-deep .el-radio-group,
::v-deep .el-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
}
}
.search-tip {
padding: 6px 12px 0;
font-size: 12px;
color: #f56c6c;
}
.search-actions {
display: flex;
justify-content: flex-end;
padding: 12px;
border-top: 1px solid #ebeef5;
}
}
.table-card {
.el-table {
width: 100%;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding: 16px 0 0;
}
}
</style>
@@ -0,0 +1,542 @@
<template>
<div class="page-container">
<!-- 查询条件 -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="110px" class="search-form">
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="开始日期">
<el-date-picker v-model="searchForm.ksrq" type="date" value-format="yyyy-MM-dd" placeholder="选择开始日期"
class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期">
<el-date-picker v-model="searchForm.jsrq" type="date" value-format="yyyy-MM-dd" placeholder="选择结束日期"
class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="课程名称">
<el-input v-model="searchForm.kcmc" placeholder="请输入课程名称" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教研室">
<el-select v-model="searchForm.jys" placeholder="请选择教研室" clearable class="w-full">
<el-option v-for="item in jysOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="教学方法">
<el-select v-model="searchForm.jxff" placeholder="请选择教学方法" clearable class="w-full">
<el-option v-for="item in jxffOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="日志状态">
<el-select v-model="searchForm.rzzt" placeholder="请选择日志状态" clearable class="w-full">
<el-option v-for="item in rzztOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="授课教员">
<el-select v-model="searchForm.skjy" placeholder="请选择授课教员" clearable class="w-full">
<el-option v-for="item in skjyOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="培训期班">
<el-input v-model="searchForm.pxqb" placeholder="请输入培训期班" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="改动情况">
<div class="change-options">
<el-radio-group v-model="searchForm.changeType">
<el-radio v-for="item in changeTypeOptions" :key="item.value" :label="item.value">{{ item.label
}}</el-radio>
</el-radio-group>
<el-checkbox-group v-model="searchForm.changeTypes">
<el-checkbox v-for="item in changeTypeList" :key="item.value" :label="item.value">{{ item.label
}}</el-checkbox>
</el-checkbox-group>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务类别">
<el-select v-model="searchForm.rwlb" placeholder="请选择任务类别" clearable class="w-full">
<el-option v-for="item in rwlbOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="search-actions">
<div class="left-group">
<el-button type="danger" plain @click="handleDeleteSelected">删除所选</el-button>
<el-button type="primary" @click="handleBatchReport">批准上报所选</el-button>
<el-button @click="handleRecheck">重新检查通报变更情况</el-button>
<el-button icon="el-icon-download" @click="handleExport">导出到 Word</el-button>
<el-button icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
<el-button icon="el-icon-upload2" :loading="importing" @click="handleImportExcel">导入教学日志</el-button>
<el-button icon="el-icon-download" @click="handleDownloadTemplate">下载模板</el-button>
</div>
<el-button type="primary" @click="handleSearch">查询</el-button>
</div>
<!-- 隐藏的文件选择器用于导入 Excel -->
<input ref="importFileInput" type="file" accept=".xls,.xlsx" style="display: none" @change="handleImportFileChange" />
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="rwlb" label="库任务类别" width="120" align="center" />
<el-table-column prop="jyxm" label="主讲教员" width="100" align="center" />
<el-table-column prop="rq" label="日期" width="150" align="center">
<template slot-scope="scope">{{ scope.row.rq ? formatDate(scope.row.rq) : '-' }}</template>
</el-table-column>
<el-table-column label="节次" width="80" align="center">
<template slot-scope="scope">
{{ scope.row.qsjc && scope.row.jsjc ? `${scope.row.qsjc}-${scope.row.jsjc}` : (scope.row.qsjc || '-') }}
</template>
</el-table-column>
<el-table-column prop="kcmc" label="课程名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="pxqbxx" label="培训期班信息" min-width="140" show-overflow-tooltip />
<el-table-column label="人数" width="70" align="center">
<template slot-scope="scope">{{ scope.row.sdrs || scope.row.ydrs || '-' }}</template>
</el-table-column>
<el-table-column label="分组指导教员" width="120" align="center">
<template slot-scope="scope">{{ scope.row.fzyzdjy || '-' }}</template>
</el-table-column>
<el-table-column prop="jsfs" label="教学方法" width="100" align="center" />
<el-table-column prop="zt" label="日志状态" width="100" align="center" />
<el-table-column prop="gdqk" label="变动汇总" min-width="120" show-overflow-tooltip />
<el-table-column prop="jxrzcjfs" label="创建方式" width="100" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right" class-name="table-action-column">
<template slot-scope="scope">
<el-button type="text" @click="handleDetail(scope.row)">详细</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination :current-page="pageNum" :page-size="pageSize" :total="total" :page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper" background @current-change="handlePageChange"
@size-change="handleSizeChange" />
</div>
</el-card>
<!-- 日志详情对话框 -->
<el-dialog :visible.sync="detailVisible" title="教学日志详情" width="700px" destroy-on-close>
<div v-loading="detailLoading">
<el-descriptions v-if="!detailLoading" :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="统一编号">{{ detailData.tybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教员编号">{{ detailData.jybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教员姓名">{{ detailData.jyxm || '-' }}</el-descriptions-item>
<el-descriptions-item label="课程名称">{{ detailData.kcmc || '-' }}</el-descriptions-item>
<el-descriptions-item label="任务类别">{{ detailData.rwlb || '-' }}</el-descriptions-item>
<el-descriptions-item label="日期">{{ detailData.rq || '-' }}</el-descriptions-item>
<el-descriptions-item label="节次">
{{ detailData.qsjc && detailData.jsjc ? `${detailData.qsjc}-${detailData.jsjc}` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="教学方法">{{ detailData.jsfs || detailData.jxff || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建方式">{{ detailData.jxrzcjfs || '-' }}</el-descriptions-item>
<el-descriptions-item label="应到人数">{{ detailData.ydrs || '-' }}</el-descriptions-item>
<el-descriptions-item label="实到人数">{{ detailData.sdrs || '-' }}</el-descriptions-item>
<el-descriptions-item label="培训期班信息" :span="2">{{ detailData.pxqbxx || '-' }}</el-descriptions-item>
<el-descriptions-item label="授课地点" :span="2">{{ detailData.skdd || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.zt || '-' }}</el-descriptions-item>
<el-descriptions-item label="教研室">{{ detailData.jysmc || detailData.jys || '-' }}</el-descriptions-item>
<el-descriptions-item label="变动汇总" :span="2">{{ detailData.gdqk || '-' }}</el-descriptions-item>
<el-descriptions-item label="变动详细情况" :span="2">{{ detailData.gdxxqk || '-' }}</el-descriptions-item>
<el-descriptions-item label="退回意见" :span="2">{{ detailData.thyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.cjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="修改时间">{{ detailData.xgsj || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { formatDate } from '@/utils/index'
import {
getTeachingLogList,
submitTeachingLog,
exportTeachingLog,
importTeachingLogExcel,
downloadTeachingLogTemplate,
getTeachingLogByBh
} from '@/api/log'
export default {
name: 'LogView',
data() {
return {
// ==================== 查询表单 ====================
searchForm: {
ksrq: '',
jsrq: '',
kcmc: '',
jys: '',
jxff: '',
rzzt: '',
skjy: '',
pxqb: '',
changeType: '',
changeTypes: [],
rwlb: ''
},
jysOptions: ['教研室', '防空兵系', '通信工程系', '装甲兵系'],
jxffOptions: ['理论讲授', '讨论', '实操', '演示', '实验', '演习'],
skjyOptions: ['教务处', '张三', '李四', '王五', '赵六'],
rzztOptions: ['教务部门导入教员类', '管理员手工添加'],
rwlbOptions: ['全军院校训练规划内任务', '其他任务'],
changeTypeOptions: [
{ label: '与教学计划一致', value: '与教学计划一致' },
{ label: '已改动教学计划', value: '已改动教学计划' },
{ label: '手动拟制', value: '手动拟制' }
],
changeTypeList: [
{ label: '已改动教员', value: '已改动教员' },
{ label: '已改动时间', value: '已改动时间' },
{ label: '已改动课目', value: '已改动课目' },
{ label: '已改动内容', value: '已改动内容' },
{ label: '已改动教学方法', value: '已改动教学方法' },
{ label: '已改动场地', value: '已改动场地' },
{ label: '已改动期班', value: '已改动期班' }
],
// ==================== 数据表格 ====================
loading: false,
tableData: [],
selectedRows: [],
pageNum: 1,
pageSize: 20,
total: 0,
// ==================== 导入 ====================
importing: false,
// ==================== 详情 ====================
detailVisible: false,
detailLoading: false,
detailData: {}
}
},
created() {
this.fetchList()
},
methods: {
handleSelectionChange(rows) {
this.selectedRows = rows
},
/** 根据已填写的查询值构建实际查询参数 */
buildSearchParams() {
const form = this.searchForm
const params = { pageNum: this.pageNum, pageSize: this.pageSize }
if (form.ksrq) params.ksrq = form.ksrq
if (form.jsrq) params.jsrq = form.jsrq
if (form.kcmc) params.kcmc = form.kcmc
if (form.jys) params.jys = form.jys
if (form.jxff) params.jxff = form.jxff
if (form.rzzt) params.rzzt = form.rzzt
if (form.skjy) params.skjy = form.skjy
if (form.pxqb) params.pxqb = form.pxqb
if (form.rwlb) params.rwlb = form.rwlb
if (form.changeType) params.gdqk = form.changeType
if (form.changeTypes.length > 0) {
params.gdxxqk = form.changeTypes.join(',')
}
return params
},
/** 从响应中提取列表数据和总数(兼容 records / list / rows 等常见字段名) */
extractListData(res) {
const data = res.data || res
const list = data.records || data.list || data.rows || []
const totalCount = data.total ?? data.totalCount ?? data.totalRows ?? 0
return { list, total: totalCount }
},
fetchList() {
this.loading = true
return getTeachingLogList(this.buildSearchParams())
.then((res) => {
if (res.code === 200 || res.code === 0) {
const { list, total } = this.extractListData(res)
this.tableData = list
this.total = total
}
})
.catch(() => {
// 错误已由拦截器统一处理
})
.finally(() => {
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
handleRefresh() {
this.fetchList()
},
getSelectedBhs() {
const ids = this.selectedRows.map((row) => row.bh).filter(Boolean)
if (ids.length === 0) {
this.$message.warning('请先选择要操作的记录')
return []
}
return ids
},
handleDeleteSelected() {
// 后端暂未提供删除接口,仅作提示
this.$message.info('后端暂未提供该接口')
},
handleBatchReport() {
const ids = this.getSelectedBhs()
if (ids.length === 0) return
this.$confirm(`确定要上报选中的 ${ids.length} 条记录吗?`, '上报确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// 提交教学日志审核,逐条上报至「已上报」状态
let settled = Promise.resolve()
ids.forEach((bh) => {
settled = settled.then(() => submitTeachingLog(bh, '已上报'))
})
return settled
})
.then(() => {
this.$message.success(`已上报 ${ids.length} 条记录`)
this.fetchList()
})
.catch(() => {})
},
handleRecheck() {
// 后端暂未提供重新检查接口,仅作提示
this.$message.info('后端暂未提供该接口')
},
downloadBlob(blob, fileName) {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
},
handleExport() {
const params = this.buildSearchParams()
delete params.pageNum
delete params.pageSize
exportTeachingLog(params)
.then((blob) => {
this.downloadBlob(blob, `教学日志_${new Date().toISOString().slice(0, 10)}.xls`)
this.$message.success('导出成功')
})
.catch(() => {})
},
handleImportExcel() {
this.$refs.importFileInput && this.$refs.importFileInput.click()
},
handleImportFileChange(e) {
const files = e.target.files
if (!files || files.length === 0) return
const file = files[0]
this.importing = true
importTeachingLogExcel(file)
.then((res) => {
if (res.code === 200 || res.code === 0) {
this.$message.success(res.message || '导入成功')
this.fetchList()
} else {
this.$message.error(res.message || '导入失败')
}
})
.catch(() => {})
.finally(() => {
this.importing = false
if (this.$refs.importFileInput) {
this.$refs.importFileInput.value = ''
}
})
},
handleDownloadTemplate() {
downloadTeachingLogTemplate()
.then((blob) => {
this.downloadBlob(blob, '导入教学日志模板.xls')
this.$message.success('模板下载成功')
})
.catch(() => {})
},
handleDetail(row) {
this.detailData = { ...row }
this.detailVisible = true
const bh = row.bh
if (!bh) return
this.detailLoading = true
getTeachingLogByBh(String(bh))
.then((res) => {
if ((res.code === 200 || res.code === 0) && res.data) {
this.detailData = res.data
}
})
.catch(() => {
// 详情接口不可用时保留行数据兜底展示
})
.finally(() => {
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.page-container {
padding: 4px 8px;
}
.search-card {
margin-bottom: 16px;
.search-form {
.w-full {
width: 100%;
}
::v-deep .el-row {
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
}
::v-deep .el-col {
border-right: 1px solid #ebeef5;
&:last-child {
border-right: none;
}
}
::v-deep .el-form-item__label {
white-space: normal;
line-height: 1.4;
text-align: left;
justify-content: flex-start;
}
.el-form-item {
margin-bottom: 0;
padding: 10px 12px;
}
.change-options {
display: flex;
flex-direction: column;
gap: 8px;
::v-deep .el-radio-group,
::v-deep .el-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
}
}
.search-tip {
padding: 6px 12px 0;
font-size: 12px;
color: #f56c6c;
}
.search-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-top: 1px solid #ebeef5;
.left-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
}
}
.table-card {
.el-table {
width: 100%;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding: 16px 0 0;
}
}
</style>
@@ -0,0 +1,511 @@
<template>
<div class="page-container">
<!-- 查询条件 -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="110px" class="search-form">
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="开始日期">
<el-date-picker v-model="searchForm.startDate" type="date" value-format="yyyy-MM-dd" placeholder="选择开始日期"
class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期">
<el-date-picker v-model="searchForm.endDate" type="date" value-format="yyyy-MM-dd" placeholder="选择结束日期"
class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="课程名称">
<el-input v-model="searchForm.courseName" placeholder="请输入课程名称" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教学方法">
<el-select v-model="searchForm.teachMethod" placeholder="请选择教学方法" clearable class="w-full">
<el-option v-for="item in teachMethodOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="改动情况">
<el-select v-model="searchForm.change" placeholder="请选择改动情况" clearable class="w-full">
<el-option v-for="item in changeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务类别">
<el-select v-model="searchForm.taskCategory" placeholder="请选择任务类别" clearable class="w-full">
<el-option v-for="item in taskCategoryOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="教研室">
<el-select v-model="searchForm.dept" placeholder="请选择教研室" clearable class="w-full">
<el-option v-for="item in deptOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="授课教员">
<el-select v-model="searchForm.teacher" placeholder="请选择授课教员" clearable class="w-full">
<el-option v-for="item in teacherOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="0">
<el-col :span="12">
<el-form-item label="培训班">
<el-input v-model="searchForm.className" placeholder="请输入培训班" clearable />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="search-actions">
<div class="left-group">
<el-button type="danger" plain @click="handleDelete">删除所选</el-button>
<el-button type="primary" @click="handleAudit">审核通过所选</el-button>
<el-input v-model="remark" placeholder="退回原因/意见" clearable class="action-input" />
<el-button type="warning" plain @click="handleWithdraw">撤回重填所选</el-button>
<el-button @click="handleRecheck">重新检查所选变更情况</el-button>
<el-button icon="el-icon-download" @click="handleExport">导出到 Word</el-button>
</div>
<div class="right-group">
<el-button @click="handleReset">重置</el-button>
<el-button type="primary" @click="handleSearch">查询</el-button>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="rwlb" label="任务类别" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="jyxm" label="主讲教员" width="100" align="center" />
<el-table-column prop="rq" label="日期" width="150" align="center">
<template slot-scope="scope">{{ scope.row.rq ? formatDate(scope.row.rq) : '-' }}</template>
</el-table-column>
<el-table-column label="节次" width="80" align="center">
<template slot-scope="scope">
{{ scope.row.qsjc && scope.row.jsjc ? `${scope.row.qsjc}-${scope.row.jsjc}` : (scope.row.qsjc || '-') }}
</template>
</el-table-column>
<el-table-column prop="kcmc" label="课程名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="pxqbxx" label="培训期班信息" min-width="140" show-overflow-tooltip />
<el-table-column label="人数" width="70" align="center">
<template slot-scope="scope">{{ scope.row.sdrs || scope.row.ydrs || '-' }}</template>
</el-table-column>
<el-table-column label="分组指导教员" width="120" align="center">
<template slot-scope="scope">{{ scope.row.fzyzdjy || '-' }}</template>
</el-table-column>
<el-table-column prop="jsfs" label="教学方法" width="100" align="center" />
<el-table-column prop="zt" label="日志状态" width="100" align="center" />
<el-table-column prop="gdqk" label="变动汇总" min-width="120" show-overflow-tooltip />
<el-table-column prop="jxrzcjfs" label="创建方式" width="100" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right" class-name="table-action-column">
<template slot-scope="scope">
<el-button type="text" @click="handleDetail(scope.row)">详细</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination :current-page="pageNum" :page-size="pageSize" :total="total" :page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper" background @current-change="handlePageChange"
@size-change="handleSizeChange" />
</div>
</el-card>
<!-- 日志详情对话框 -->
<el-dialog :visible.sync="detailVisible" title="教学日志详情" width="700px" destroy-on-close>
<div v-loading="detailLoading">
<el-descriptions v-if="!detailLoading" :column="2" border>
<el-descriptions-item label="编号">{{ detailData.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="统一编号">{{ detailData.tybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教员编号">{{ detailData.jybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教员姓名">{{ detailData.jyxm || '-' }}</el-descriptions-item>
<el-descriptions-item label="课程名称">{{ detailData.kcmc || '-' }}</el-descriptions-item>
<el-descriptions-item label="任务类别">{{ detailData.rwlb || '-' }}</el-descriptions-item>
<el-descriptions-item label="日期">{{ detailData.rq || '-' }}</el-descriptions-item>
<el-descriptions-item label="节次">
{{ detailData.qsjc && detailData.jsjc ? `${detailData.qsjc}-${detailData.jsjc}` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="教学方法">{{ detailData.jsfs || detailData.jxff || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建方式">{{ detailData.jxrzcjfs || '-' }}</el-descriptions-item>
<el-descriptions-item label="应到人数">{{ detailData.ydrs || '-' }}</el-descriptions-item>
<el-descriptions-item label="实到人数">{{ detailData.sdrs || '-' }}</el-descriptions-item>
<el-descriptions-item label="培训期班信息" :span="2">{{ detailData.pxqbxx || '-' }}</el-descriptions-item>
<el-descriptions-item label="授课地点" :span="2">{{ detailData.skdd || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.zt || '-' }}</el-descriptions-item>
<el-descriptions-item label="教研室">{{ detailData.jysmc || detailData.jys || '-' }}</el-descriptions-item>
<el-descriptions-item label="变动汇总" :span="2">{{ detailData.gdqk || '-' }}</el-descriptions-item>
<el-descriptions-item label="变动详细情况" :span="2">{{ detailData.gdxxqk || '-' }}</el-descriptions-item>
<el-descriptions-item label="退回意见" :span="2">{{ detailData.thyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.cjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="修改时间">{{ detailData.xgsj || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { formatDate } from '@/utils/index'
import {
getReportedTeachingLogList,
approveTeachingLog,
rejectTeachingLog,
exportTeachingLog,
getTeachingLogByBh
} from '@/api/log'
export default {
name: 'ReportedView',
data() {
return {
// ==================== 查询表单 ====================
searchForm: {
startDate: '',
endDate: '',
courseName: '',
teachMethod: '',
change: '',
taskCategory: '',
dept: '',
teacher: '',
className: ''
},
teachMethodOptions: ['理论讲授', '实践教学', '研讨教学', '案例教学', '实操演练'],
changeOptions: [
'与教学计划一致',
'已改动教学计划',
'手动拟制',
'教员变动',
'时间变动',
'课程变动',
'授课方式变动',
'任务类别变动',
'期班变动',
'地点变动',
'课题变动'
],
taskCategoryOptions: ['全军院校训练规划内任务', '其他任务(含研究生导师指导)'],
deptOptions: ['教务处', '指挥系', '政治工作教研室', '军事基础教研室', '装备保障教研室'],
teacherOptions: ['教务处', '张三', '李四', '王五', '赵六', '孙七'],
// ==================== 数据表格 ====================
loading: false,
tableData: [],
selectedRows: [],
pageNum: 1,
pageSize: 20,
total: 0,
// ==================== 退回意见 ====================
remark: '',
// ==================== 详情 ====================
detailVisible: false,
detailLoading: false,
detailData: {}
}
},
created() {
this.fetchList()
},
methods: {
handleSelectionChange(rows) {
this.selectedRows = rows
},
/** 根据已填写的查询值构建实际查询参数 */
buildSearchParams() {
const form = this.searchForm
const params = { pageNum: this.pageNum, pageSize: this.pageSize }
if (form.startDate) params.ksrq = form.startDate
if (form.endDate) params.jsrq = form.endDate
if (form.courseName) params.kcmc = form.courseName
if (form.teachMethod) params.jxff = form.teachMethod
if (form.change) params.gdqk = form.change
if (form.taskCategory) params.rwlb = form.taskCategory
if (form.dept) params.jys = form.dept
if (form.teacher) params.skjy = form.teacher
if (form.className) params.pxqb = form.className
return params
},
extractListData(res) {
const data = res.data || res
const list = data.records || data.list || data.rows || []
const totalCount = data.total ?? data.totalCount ?? data.totalRows ?? 0
return { list, total: totalCount }
},
fetchList() {
this.loading = true
return getReportedTeachingLogList(this.buildSearchParams())
.then((res) => {
if (res.code === 200 || res.code === 0) {
const { list, total } = this.extractListData(res)
this.tableData = list
this.total = total
}
})
.catch(() => {})
.finally(() => {
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handleReset() {
this.searchForm = {
startDate: '',
endDate: '',
courseName: '',
teachMethod: '',
change: '',
taskCategory: '',
dept: '',
teacher: '',
className: ''
}
this.remark = ''
this.handleSearch()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
getSelectedBhs() {
const ids = this.selectedRows.map((row) => row.bh).filter(Boolean)
if (ids.length === 0) {
this.$message.warning('请先选择记录')
return []
}
return ids
},
handleDelete() {
// 后端暂未提供删除接口,仅作提示
this.$message.info('后端暂未提供该接口')
},
handleAudit() {
const ids = this.getSelectedBhs()
if (ids.length === 0) return
this.$confirm(`确定要审核通过选中的 ${ids.length} 条记录吗?`, '审核确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// 审核通过教学日志,逐条审核
let settled = Promise.resolve()
ids.forEach((bh) => {
settled = settled.then(() => approveTeachingLog(bh, ''))
})
return settled
})
.then(() => {
this.$message.success(`已审核通过 ${ids.length} 条记录`)
this.fetchList()
})
.catch(() => {})
},
handleWithdraw() {
const ids = this.getSelectedBhs()
if (ids.length === 0) return
if (!this.remark.trim()) {
this.$message.warning('请填写退回原因/意见')
return
}
const opinion = this.remark.trim()
let settled = Promise.resolve()
ids.forEach((bh) => {
settled = settled.then(() => rejectTeachingLog(bh, opinion))
})
this.$confirm(`确定要撤回重填选中的 ${ids.length} 条记录吗?`, '撤回确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => settled)
.then(() => {
this.$message.success(`已撤回 ${ids.length} 条记录`)
this.fetchList()
})
.catch(() => {})
},
handleRecheck() {
// 后端暂未提供重新检查接口,仅作提示
this.$message.info('后端暂未提供该接口')
},
downloadBlob(blob, fileName) {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
},
handleExport() {
const params = this.buildSearchParams()
delete params.pageNum
delete params.pageSize
exportTeachingLog(params)
.then((blob) => {
this.downloadBlob(blob, `已上报教学日志_${new Date().toISOString().slice(0, 10)}.xls`)
this.$message.success('导出成功')
})
.catch(() => {})
},
handleDetail(row) {
this.detailData = { ...row }
this.detailVisible = true
const bh = row.bh
if (!bh) return
this.detailLoading = true
getTeachingLogByBh(String(bh))
.then((res) => {
if ((res.code === 200 || res.code === 0) && res.data) {
this.detailData = res.data
}
})
.catch(() => {})
.finally(() => {
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.page-container {
padding: 4px 8px;
}
.search-card {
margin-bottom: 16px;
.search-form {
.w-full {
width: 100%;
}
::v-deep .el-row {
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
}
::v-deep .el-col {
border-right: 1px solid #ebeef5;
&:last-child {
border-right: none;
}
}
::v-deep .el-form-item__label {
white-space: normal;
line-height: 1.4;
text-align: left;
justify-content: flex-start;
}
.el-form-item {
margin-bottom: 0;
padding: 10px 12px;
}
}
.search-tip {
padding: 6px 12px 0;
font-size: 12px;
color: #f56c6c;
}
.search-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-top: 1px solid #ebeef5;
.left-group {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
.action-input {
width: 240px;
}
}
.right-group {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.table-card {
.el-table {
width: 100%;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding: 16px 0 0;
}
}
</style>
@@ -0,0 +1,51 @@
<template>
<div class="page-container">
<el-card shadow="never" class="tab-card">
<el-tabs v-model="activeTab" class="organ-tabs">
<el-tab-pane label="教学日志管理查询" name="query">
<log-view v-if="activeTab === 'query'" />
</el-tab-pane>
<el-tab-pane label="已上报机关教学日志" name="reported">
<reported-view v-if="activeTab === 'reported'" />
</el-tab-pane>
<el-tab-pane label="机关已审核教学日志" name="audited">
<audited-view v-if="activeTab === 'audited'" />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script>
import LogView from './components/logView.vue'
import ReportedView from './components/reportedView.vue'
import AuditedView from './components/auditedView.vue'
export default {
name: 'OrganLog',
components: { LogView, ReportedView, AuditedView },
data() {
return {
activeTab: 'query'
}
}
}
</script>
<style scoped lang="scss">
.page-container {
padding: 20px;
}
.tab-card {
.organ-tabs {
::v-deep .el-tabs__header {
margin-bottom: 8px;
}
::v-deep .el-tabs__content {
padding-top: 4px;
}
}
}
</style>
@@ -0,0 +1,106 @@
<template>
<div>
<el-dialog
v-bind="$attrs"
width="500px"
:close-on-click-modal="false"
:modal-append-to-body="false"
v-on="$listeners"
@open="onOpen"
@close="onClose"
>
<el-row :gutter="15">
<el-form
ref="elForm"
:model="formData"
:rules="rules"
size="medium"
label-width="100px"
>
<el-col :span="24">
<el-form-item label="生成类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio-button
v-for="(item, index) in typeOptions"
:key="index"
:label="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="showFileName" label="文件名" prop="fileName">
<el-input v-model="formData.fileName" placeholder="请输入文件名" clearable />
</el-form-item>
</el-col>
</el-form>
</el-row>
<div slot="footer">
<el-button @click="close">
取消
</el-button>
<el-button type="primary" @click="handleConfirm">
确定
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: ['showFileName'],
data() {
return {
formData: {
fileName: undefined,
type: 'file'
},
rules: {
fileName: [{
required: true,
message: '请输入文件名',
trigger: 'blur'
}],
type: [{
required: true,
message: '生成类型不能为空',
trigger: 'change'
}]
},
typeOptions: [{
label: '页面',
value: 'file'
}, {
label: '弹窗',
value: 'dialog'
}]
}
},
computed: {
},
watch: {},
mounted() {},
methods: {
onOpen() {
if (this.showFileName) {
this.formData.fileName = `${+new Date()}.vue`
}
},
onClose() {
},
close(e) {
this.$emit('update:visible', false)
},
handleConfirm() {
this.$refs.elForm.validate(valid => {
if (!valid) return
this.$emit('confirm', { ...this.formData })
this.close()
})
}
}
}
</script>
@@ -0,0 +1,100 @@
<script>
import draggable from 'vuedraggable'
import render from '@/utils/generator/render'
const components = {
itemBtns(h, element, index, parent) {
const { copyItem, deleteItem } = this.$listeners
return [
<span class="drawing-item-copy" title="复制" onClick={event => {
copyItem(element, parent); event.stopPropagation()
}}>
<i class="el-icon-copy-document" />
</span>,
<span class="drawing-item-delete" title="删除" onClick={event => {
deleteItem(index, parent); event.stopPropagation()
}}>
<i class="el-icon-delete" />
</span>
]
}
}
const layouts = {
colFormItem(h, element, index, parent) {
const { activeItem } = this.$listeners
let className = this.activeId === element.formId ? 'drawing-item active-from-item' : 'drawing-item'
if (this.formConf.unFocusedComponentBorder) className += ' unfocus-bordered'
return (
<el-col span={element.span} class={className}
nativeOnClick={event => { activeItem(element); event.stopPropagation() }}>
<el-form-item label-width={element.labelWidth ? `${element.labelWidth}px` : null}
label={element.label} required={element.required}>
<render key={element.renderKey} conf={element} onInput={ event => {
this.$set(element, 'defaultValue', event)
}} />
</el-form-item>
{components.itemBtns.apply(this, arguments)}
</el-col>
)
},
rowFormItem(h, element, index, parent) {
const { activeItem } = this.$listeners
const className = this.activeId === element.formId ? 'drawing-row-item active-from-item' : 'drawing-row-item'
let child = renderChildren.apply(this, arguments)
if (element.type === 'flex') {
child = <el-row type={element.type} justify={element.justify} align={element.align}>
{child}
</el-row>
}
return (
<el-col span={element.span}>
<el-row gutter={element.gutter} class={className}
nativeOnClick={event => { activeItem(element); event.stopPropagation() }}>
<span class="component-name">{element.componentName}</span>
<draggable list={element.children} animation={340} group="componentsGroup" class="drag-wrapper">
{child}
</draggable>
{components.itemBtns.apply(this, arguments)}
</el-row>
</el-col>
)
}
}
function renderChildren(h, element, index, parent) {
if (!Array.isArray(element.children)) return null
return element.children.map((el, i) => {
const layout = layouts[el.layout]
if (layout) {
return layout.call(this, h, el, i, element.children)
}
return layoutIsNotFound()
})
}
function layoutIsNotFound() {
throw new Error(`没有与${this.element.layout}匹配的layout`)
}
export default {
components: {
render,
draggable
},
props: [
'element',
'index',
'drawingList',
'activeId',
'formConf'
],
render(h) {
const layout = layouts[this.element.layout]
if (layout) {
return layout.call(this, h, this.element, this.index, this.drawingList)
}
return layoutIsNotFound()
}
}
</script>
@@ -0,0 +1,123 @@
<template>
<div class="icon-dialog">
<el-dialog
v-bind="$attrs"
width="980px"
:modal-append-to-body="false"
v-on="$listeners"
@open="onOpen"
@close="onClose"
>
<div slot="title">
选择图标
<el-input
v-model="key"
size="mini"
:style="{width: '260px'}"
placeholder="请输入图标名称"
prefix-icon="el-icon-search"
clearable
/>
</div>
<ul class="icon-ul">
<li
v-for="icon in iconList"
:key="icon"
:class="active===icon?'active-item':''"
@click="onSelect(icon)"
>
<i :class="icon" />
<div>{{ icon }}</div>
</li>
</ul>
</el-dialog>
</div>
</template>
<script>
import iconList from '@/utils/generator/icon.json'
const originList = iconList.map(name => `el-icon-${name}`)
export default {
inheritAttrs: false,
props: ['current'],
data() {
return {
iconList: originList,
active: null,
key: ''
}
},
watch: {
key(val) {
if (val) {
this.iconList = originList.filter(name => name.indexOf(val) > -1)
} else {
this.iconList = originList
}
}
},
methods: {
onOpen() {
this.active = this.current
this.key = ''
},
onClose() {},
onSelect(icon) {
this.active = icon
this.$emit('select', icon)
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="scss" scoped>
.icon-ul {
margin: 0;
padding: 0;
font-size: 0;
li {
list-style-type: none;
text-align: center;
font-size: 14px;
display: inline-block;
width: 16.66%;
box-sizing: border-box;
height: 108px;
padding: 15px 6px 6px 6px;
cursor: pointer;
overflow: hidden;
&:hover {
background: #f2f2f2;
}
&.active-item{
background: #e1f3fb;
color: #7a6df0
}
> i {
font-size: 30px;
line-height: 50px;
}
}
}
.icon-dialog {
::v-deep .el-dialog {
border-radius: 8px;
margin-bottom: 0;
margin-top: 4vh !important;
display: flex;
flex-direction: column;
max-height: 92vh;
overflow: hidden;
box-sizing: border-box;
.el-dialog__header {
padding-top: 14px;
}
.el-dialog__body {
margin: 0 20px 20px 20px;
padding: 0;
overflow: auto;
}
}
}
</style>
@@ -0,0 +1,944 @@
<template>
<div class="right-board">
<el-tabs v-model="currentTab" class="center-tabs">
<el-tab-pane label="组件属性" name="field" />
<el-tab-pane label="表单属性" name="form" />
</el-tabs>
<div class="field-box">
<a class="document-link" target="_blank" :href="documentLink" title="查看组件文档">
<i class="el-icon-link" />
</a>
<el-scrollbar class="right-scrollbar">
<!-- 组件属性 -->
<el-form v-show="currentTab==='field' && showField" size="small" label-width="90px">
<el-form-item v-if="activeData.changeTag" label="组件类型">
<el-select
v-model="activeData.tagIcon"
placeholder="请选择组件类型"
:style="{width: '100%'}"
@change="tagChange"
>
<el-option-group v-for="group in tagList" :key="group.label" :label="group.label">
<el-option
v-for="item in group.options"
:key="item.label"
:label="item.label"
:value="item.tagIcon"
>
<svg-icon class="node-icon" :icon-class="item.tagIcon" />
<span> {{ item.label }}</span>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item v-if="activeData.vModel!==undefined" label="字段名">
<el-input v-model="activeData.vModel" placeholder="请输入字段名(v-model" />
</el-form-item>
<el-form-item v-if="activeData.componentName!==undefined" label="组件名">
{{ activeData.componentName }}
</el-form-item>
<el-form-item v-if="activeData.label!==undefined" label="标题">
<el-input v-model="activeData.label" placeholder="请输入标题" />
</el-form-item>
<el-form-item v-if="activeData.placeholder!==undefined" label="占位提示">
<el-input v-model="activeData.placeholder" placeholder="请输入占位提示" />
</el-form-item>
<el-form-item v-if="activeData['start-placeholder']!==undefined" label="开始占位">
<el-input v-model="activeData['start-placeholder']" placeholder="请输入占位提示" />
</el-form-item>
<el-form-item v-if="activeData['end-placeholder']!==undefined" label="结束占位">
<el-input v-model="activeData['end-placeholder']" placeholder="请输入占位提示" />
</el-form-item>
<el-form-item v-if="activeData.span!==undefined" label="表单栅格">
<el-slider v-model="activeData.span" :max="24" :min="1" :marks="{12:''}" @change="spanChange" />
</el-form-item>
<el-form-item v-if="activeData.layout==='rowFormItem'" label="栅格间隔">
<el-input-number v-model="activeData.gutter" :min="0" placeholder="栅格间隔" />
</el-form-item>
<el-form-item v-if="activeData.layout==='rowFormItem'" label="布局模式">
<el-radio-group v-model="activeData.type">
<el-radio-button label="default" />
<el-radio-button label="flex" />
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData.justify!==undefined&&activeData.type==='flex'" label="水平排列">
<el-select v-model="activeData.justify" placeholder="请选择水平排列" :style="{width: '100%'}">
<el-option
v-for="(item, index) in justifyOptions"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="activeData.align!==undefined&&activeData.type==='flex'" label="垂直排列">
<el-radio-group v-model="activeData.align">
<el-radio-button label="top" />
<el-radio-button label="middle" />
<el-radio-button label="bottom" />
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData.labelWidth!==undefined" label="标签宽度">
<el-input v-model.number="activeData.labelWidth" type="number" placeholder="请输入标签宽度" />
</el-form-item>
<el-form-item v-if="activeData.style&&activeData.style.width!==undefined" label="组件宽度">
<el-input v-model="activeData.style.width" placeholder="请输入组件宽度" clearable />
</el-form-item>
<el-form-item v-if="activeData.vModel!==undefined" label="默认值">
<el-input
:value="setDefaultValue(activeData.defaultValue)"
placeholder="请输入默认值"
@input="onDefaultValueInput"
/>
</el-form-item>
<el-form-item v-if="activeData.tag==='el-checkbox-group'" label="至少应选">
<el-input-number
:value="activeData.min"
:min="0"
placeholder="至少应选"
@input="$set(activeData, 'min', $event?$event:undefined)"
/>
</el-form-item>
<el-form-item v-if="activeData.tag==='el-checkbox-group'" label="最多可选">
<el-input-number
:value="activeData.max"
:min="0"
placeholder="最多可选"
@input="$set(activeData, 'max', $event?$event:undefined)"
/>
</el-form-item>
<el-form-item v-if="activeData.prepend!==undefined" label="前缀">
<el-input v-model="activeData.prepend" placeholder="请输入前缀" />
</el-form-item>
<el-form-item v-if="activeData.append!==undefined" label="后缀">
<el-input v-model="activeData.append" placeholder="请输入后缀" />
</el-form-item>
<el-form-item v-if="activeData['prefix-icon']!==undefined" label="前图标">
<el-input v-model="activeData['prefix-icon']" placeholder="请输入前图标名称">
<el-button slot="append" icon="el-icon-thumb" @click="openIconsDialog('prefix-icon')">
选择
</el-button>
</el-input>
</el-form-item>
<el-form-item v-if="activeData['suffix-icon'] !== undefined" label="后图标">
<el-input v-model="activeData['suffix-icon']" placeholder="请输入后图标名称">
<el-button slot="append" icon="el-icon-thumb" @click="openIconsDialog('suffix-icon')">
选择
</el-button>
</el-input>
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="选项分隔符">
<el-input v-model="activeData.separator" placeholder="请输入选项分隔符" />
</el-form-item>
<el-form-item v-if="activeData.autosize !== undefined" label="最小行数">
<el-input-number v-model="activeData.autosize.minRows" :min="1" placeholder="最小行数" />
</el-form-item>
<el-form-item v-if="activeData.autosize !== undefined" label="最大行数">
<el-input-number v-model="activeData.autosize.maxRows" :min="1" placeholder="最大行数" />
</el-form-item>
<el-form-item v-if="activeData.min !== undefined" label="最小值">
<el-input-number v-model="activeData.min" placeholder="最小值" />
</el-form-item>
<el-form-item v-if="activeData.max !== undefined" label="最大值">
<el-input-number v-model="activeData.max" placeholder="最大值" />
</el-form-item>
<el-form-item v-if="activeData.step !== undefined" label="步长">
<el-input-number v-model="activeData.step" placeholder="步数" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-input-number'" label="精度">
<el-input-number v-model="activeData.precision" :min="0" placeholder="精度" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-input-number'" label="按钮位置">
<el-radio-group v-model="activeData['controls-position']">
<el-radio-button label="">
默认
</el-radio-button>
<el-radio-button label="right">
右侧
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData.maxlength !== undefined" label="最多输入">
<el-input v-model="activeData.maxlength" placeholder="请输入字符长度">
<template slot="append">
个字符
</template>
</el-input>
</el-form-item>
<el-form-item v-if="activeData['active-text'] !== undefined" label="开启提示">
<el-input v-model="activeData['active-text']" placeholder="请输入开启提示" />
</el-form-item>
<el-form-item v-if="activeData['inactive-text'] !== undefined" label="关闭提示">
<el-input v-model="activeData['inactive-text']" placeholder="请输入关闭提示" />
</el-form-item>
<el-form-item v-if="activeData['active-value'] !== undefined" label="开启值">
<el-input
:value="setDefaultValue(activeData['active-value'])"
placeholder="请输入开启值"
@input="onSwitchValueInput($event, 'active-value')"
/>
</el-form-item>
<el-form-item v-if="activeData['inactive-value'] !== undefined" label="关闭值">
<el-input
:value="setDefaultValue(activeData['inactive-value'])"
placeholder="请输入关闭值"
@input="onSwitchValueInput($event, 'inactive-value')"
/>
</el-form-item>
<el-form-item
v-if="activeData.type !== undefined && 'el-date-picker' === activeData.tag"
label="时间类型"
>
<el-select
v-model="activeData.type"
placeholder="请选择时间类型"
:style="{ width: '100%' }"
@change="dateTypeChange"
>
<el-option
v-for="(item, index) in dateOptions"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="activeData.name !== undefined" label="文件字段名">
<el-input v-model="activeData.name" placeholder="请输入上传文件字段名" />
</el-form-item>
<el-form-item v-if="activeData.accept !== undefined" label="文件类型">
<el-select
v-model="activeData.accept"
placeholder="请选择文件类型"
:style="{ width: '100%' }"
clearable
>
<el-option label="图片" value="image/*" />
<el-option label="视频" value="video/*" />
<el-option label="音频" value="audio/*" />
<el-option label="excel" value=".xls,.xlsx" />
<el-option label="word" value=".doc,.docx" />
<el-option label="pdf" value=".pdf" />
<el-option label="txt" value=".txt" />
</el-select>
</el-form-item>
<el-form-item v-if="activeData.fileSize !== undefined" label="文件大小">
<el-input v-model.number="activeData.fileSize" placeholder="请输入文件大小">
<el-select slot="append" v-model="activeData.sizeUnit" :style="{ width: '66px' }">
<el-option label="KB" value="KB" />
<el-option label="MB" value="MB" />
<el-option label="GB" value="GB" />
</el-select>
</el-input>
</el-form-item>
<el-form-item v-if="activeData.action !== undefined" label="上传地址">
<el-input v-model="activeData.action" placeholder="请输入上传地址" clearable />
</el-form-item>
<el-form-item v-if="activeData['list-type'] !== undefined" label="列表类型">
<el-radio-group v-model="activeData['list-type']" size="small">
<el-radio-button label="text">
text
</el-radio-button>
<el-radio-button label="picture">
picture
</el-radio-button>
<el-radio-button label="picture-card">
picture-card
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="activeData.buttonText !== undefined"
v-show="'picture-card' !== activeData['list-type']"
label="按钮文字"
>
<el-input v-model="activeData.buttonText" placeholder="请输入按钮文字" />
</el-form-item>
<el-form-item v-if="activeData['range-separator'] !== undefined" label="分隔符">
<el-input v-model="activeData['range-separator']" placeholder="请输入分隔符" />
</el-form-item>
<el-form-item v-if="activeData['picker-options'] !== undefined" label="时间段">
<el-input
v-model="activeData['picker-options'].selectableRange"
placeholder="请输入时间段"
/>
</el-form-item>
<el-form-item v-if="activeData.format !== undefined" label="时间格式">
<el-input
:value="activeData.format"
placeholder="请输入时间格式"
@input="setTimeValue($event)"
/>
</el-form-item>
<template v-if="['el-checkbox-group', 'el-radio-group', 'el-select'].indexOf(activeData.tag) > -1">
<el-divider>选项</el-divider>
<draggable
:list="activeData.options"
:animation="340"
group="selectItem"
handle=".option-drag"
>
<div v-for="(item, index) in activeData.options" :key="index" class="select-item">
<div class="select-line-icon option-drag">
<i class="el-icon-s-operation" />
</div>
<el-input v-model="item.label" placeholder="选项名" size="small" />
<el-input
placeholder="选项值"
size="small"
:value="item.value"
@input="setOptionValue(item, $event)"
/>
<div class="close-btn select-line-icon" @click="activeData.options.splice(index, 1)">
<i class="el-icon-remove-outline" />
</div>
</div>
</draggable>
<div style="margin-left: 20px;">
<el-button
style="padding-bottom: 0"
icon="el-icon-circle-plus-outline"
type="text"
@click="addSelectItem"
>
添加选项
</el-button>
</div>
<el-divider />
</template>
<template v-if="['el-cascader'].indexOf(activeData.tag) > -1">
<el-divider>选项</el-divider>
<el-form-item label="数据类型">
<el-radio-group v-model="activeData.dataType" size="small">
<el-radio-button label="dynamic">
动态数据
</el-radio-button>
<el-radio-button label="static">
静态数据
</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="activeData.dataType === 'dynamic'">
<el-form-item label="标签键名">
<el-input v-model="activeData.labelKey" placeholder="请输入标签键名" />
</el-form-item>
<el-form-item label="值键名">
<el-input v-model="activeData.valueKey" placeholder="请输入值键名" />
</el-form-item>
<el-form-item label="子级键名">
<el-input v-model="activeData.childrenKey" placeholder="请输入子级键名" />
</el-form-item>
</template>
<el-tree
v-if="activeData.dataType === 'static'"
draggable
:data="activeData.options"
node-key="id"
:expand-on-click-node="false"
:render-content="renderContent"
/>
<div v-if="activeData.dataType === 'static'" style="margin-left: 20px">
<el-button
style="padding-bottom: 0"
icon="el-icon-circle-plus-outline"
type="text"
@click="addTreeItem"
>
添加父级
</el-button>
</div>
<el-divider />
</template>
<el-form-item v-if="activeData.optionType !== undefined" label="选项样式">
<el-radio-group v-model="activeData.optionType">
<el-radio-button label="default">
默认
</el-radio-button>
<el-radio-button label="button">
按钮
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData['active-color'] !== undefined" label="开启颜色">
<el-color-picker v-model="activeData['active-color']" />
</el-form-item>
<el-form-item v-if="activeData['inactive-color'] !== undefined" label="关闭颜色">
<el-color-picker v-model="activeData['inactive-color']" />
</el-form-item>
<el-form-item v-if="activeData['allow-half'] !== undefined" label="允许半选">
<el-switch v-model="activeData['allow-half']" />
</el-form-item>
<el-form-item v-if="activeData['show-text'] !== undefined" label="辅助文字">
<el-switch v-model="activeData['show-text']" @change="rateTextChange" />
</el-form-item>
<el-form-item v-if="activeData['show-score'] !== undefined" label="显示分数">
<el-switch v-model="activeData['show-score']" @change="rateScoreChange" />
</el-form-item>
<el-form-item v-if="activeData['show-stops'] !== undefined" label="显示间断点">
<el-switch v-model="activeData['show-stops']" />
</el-form-item>
<el-form-item v-if="activeData.range !== undefined" label="范围选择">
<el-switch v-model="activeData.range" @change="rangeChange" />
</el-form-item>
<el-form-item
v-if="activeData.border !== undefined && activeData.optionType === 'default'"
label="是否带边框"
>
<el-switch v-model="activeData.border" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-color-picker'" label="颜色格式">
<el-select
v-model="activeData['color-format']"
placeholder="请选择颜色格式"
:style="{ width: '100%' }"
@change="colorFormatChange"
>
<el-option
v-for="(item, index) in colorFormatOptions"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="activeData.size !== undefined &&
(activeData.optionType === 'button' ||
activeData.border ||
activeData.tag === 'el-color-picker')"
label="选项尺寸"
>
<el-radio-group v-model="activeData.size">
<el-radio-button label="medium">
中等
</el-radio-button>
<el-radio-button label="small">
较小
</el-radio-button>
<el-radio-button label="mini">
迷你
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData['show-word-limit'] !== undefined" label="输入统计">
<el-switch v-model="activeData['show-word-limit']" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-input-number'" label="严格步数">
<el-switch v-model="activeData['step-strictly']" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="是否多选">
<el-switch v-model="activeData.props.props.multiple" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="展示全路径">
<el-switch v-model="activeData['show-all-levels']" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="可否筛选">
<el-switch v-model="activeData.filterable" />
</el-form-item>
<el-form-item v-if="activeData.clearable !== undefined" label="能否清空">
<el-switch v-model="activeData.clearable" />
</el-form-item>
<el-form-item v-if="activeData.showTip !== undefined" label="显示提示">
<el-switch v-model="activeData.showTip" />
</el-form-item>
<el-form-item v-if="activeData.multiple !== undefined" label="多选文件">
<el-switch v-model="activeData.multiple" />
</el-form-item>
<el-form-item v-if="activeData['auto-upload'] !== undefined" label="自动上传">
<el-switch v-model="activeData['auto-upload']" />
</el-form-item>
<el-form-item v-if="activeData.readonly !== undefined" label="是否只读">
<el-switch v-model="activeData.readonly" />
</el-form-item>
<el-form-item v-if="activeData.disabled !== undefined" label="是否禁用">
<el-switch v-model="activeData.disabled" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-select'" label="是否可搜索">
<el-switch v-model="activeData.filterable" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-select'" label="是否多选">
<el-switch v-model="activeData.multiple" @change="multipleChange" />
</el-form-item>
<el-form-item v-if="activeData.required !== undefined" label="是否必填">
<el-switch v-model="activeData.required" />
</el-form-item>
<template v-if="activeData.layoutTree">
<el-divider>布局结构树</el-divider>
<el-tree
:data="[activeData]"
:props="layoutTreeProps"
node-key="renderKey"
default-expand-all
draggable
>
<span slot-scope="{ node, data }">
<span class="node-label">
<svg-icon class="node-icon" :icon-class="data.tagIcon" />
{{ node.label }}
</span>
</span>
</el-tree>
</template>
<template v-if="activeData.layout === 'colFormItem' && activeData.tag !== 'el-button'">
<el-divider>正则校验</el-divider>
<div
v-for="(item, index) in activeData.regList"
:key="index"
class="reg-item"
>
<span class="close-btn" @click="activeData.regList.splice(index, 1)">
<i class="el-icon-close" />
</span>
<el-form-item label="表达式">
<el-input v-model="item.pattern" placeholder="请输入正则" />
</el-form-item>
<el-form-item label="错误提示" style="margin-bottom:0">
<el-input v-model="item.message" placeholder="请输入错误提示" />
</el-form-item>
</div>
<div style="margin-left: 20px">
<el-button icon="el-icon-circle-plus-outline" type="text" @click="addReg">
添加规则
</el-button>
</div>
</template>
</el-form>
<!-- 表单属性 -->
<el-form v-show="currentTab === 'form'" size="small" label-width="90px">
<el-form-item label="表单名">
<el-input v-model="formConf.formRef" placeholder="请输入表单名(ref" />
</el-form-item>
<el-form-item label="表单模型">
<el-input v-model="formConf.formModel" placeholder="请输入数据模型" />
</el-form-item>
<el-form-item label="校验模型">
<el-input v-model="formConf.formRules" placeholder="请输入校验模型" />
</el-form-item>
<el-form-item label="表单尺寸">
<el-radio-group v-model="formConf.size">
<el-radio-button label="medium">
中等
</el-radio-button>
<el-radio-button label="small">
较小
</el-radio-button>
<el-radio-button label="mini">
迷你
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="标签对齐">
<el-radio-group v-model="formConf.labelPosition">
<el-radio-button label="left">
左对齐
</el-radio-button>
<el-radio-button label="right">
右对齐
</el-radio-button>
<el-radio-button label="top">
顶部对齐
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="标签宽度">
<el-input-number v-model="formConf.labelWidth" placeholder="标签宽度" />
</el-form-item>
<el-form-item label="栅格间隔">
<el-input-number v-model="formConf.gutter" :min="0" placeholder="栅格间隔" />
</el-form-item>
<el-form-item label="禁用表单">
<el-switch v-model="formConf.disabled" />
</el-form-item>
<el-form-item label="表单按钮">
<el-switch v-model="formConf.formBtns" />
</el-form-item>
<el-form-item label="显示未选中组件边框">
<el-switch v-model="formConf.unFocusedComponentBorder" />
</el-form-item>
</el-form>
</el-scrollbar>
</div>
<treeNode-dialog :visible.sync="dialogVisible" title="添加选项" @commit="addNode" />
<icons-dialog :visible.sync="iconsVisible" :current="activeData[currentIconModel]" @select="setIcon" />
</div>
</template>
<script>
import draggable from 'vuedraggable'
import TreeNodeDialog from './TreeNodeDialog'
import { isNumberStr } from '@/utils/index'
import IconsDialog from './IconsDialog'
import {
inputComponents,
selectComponents
} from '@/utils/generator/config'
const dateTimeFormat = {
date: 'yyyy-MM-dd',
week: 'yyyy 第 WW 周',
month: 'yyyy-MM',
year: 'yyyy',
datetime: 'yyyy-MM-dd HH:mm:ss',
daterange: 'yyyy-MM-dd',
monthrange: 'yyyy-MM',
datetimerange: 'yyyy-MM-dd HH:mm:ss'
}
export default {
components: {
draggable,
TreeNodeDialog,
IconsDialog
},
props: ['showField', 'activeData', 'formConf'],
data() {
return {
currentTab: 'field',
currentNode: null,
dialogVisible: false,
iconsVisible: false,
currentIconModel: null,
dateTypeOptions: [
{
label: '日(date)',
value: 'date'
},
{
label: '周(week)',
value: 'week'
},
{
label: '月(month)',
value: 'month'
},
{
label: '年(year)',
value: 'year'
},
{
label: '日期时间(datetime)',
value: 'datetime'
}
],
dateRangeTypeOptions: [
{
label: '日期范围(daterange)',
value: 'daterange'
},
{
label: '月范围(monthrange)',
value: 'monthrange'
},
{
label: '日期时间范围(datetimerange)',
value: 'datetimerange'
}
],
colorFormatOptions: [
{
label: 'hex',
value: 'hex'
},
{
label: 'rgb',
value: 'rgb'
},
{
label: 'rgba',
value: 'rgba'
},
{
label: 'hsv',
value: 'hsv'
},
{
label: 'hsl',
value: 'hsl'
}
],
justifyOptions: [
{
label: 'start',
value: 'start'
},
{
label: 'end',
value: 'end'
},
{
label: 'center',
value: 'center'
},
{
label: 'space-around',
value: 'space-around'
},
{
label: 'space-between',
value: 'space-between'
}
],
layoutTreeProps: {
label(data) {
return data.componentName || `${data.label}: ${data.vModel}`
}
}
}
},
computed: {
documentLink() {
return (
this.activeData.document
|| 'https://element.eleme.cn/#/zh-CN/component/installation'
)
},
dateOptions() {
if (
this.activeData.type !== undefined
&& this.activeData.tag === 'el-date-picker'
) {
if (this.activeData['start-placeholder'] === undefined) {
return this.dateTypeOptions
}
return this.dateRangeTypeOptions
}
return []
},
tagList() {
return [
{
label: '输入型组件',
options: inputComponents
},
{
label: '选择型组件',
options: selectComponents
}
]
}
},
methods: {
addReg() {
this.activeData.regList.push({
pattern: '',
message: ''
})
},
addSelectItem() {
this.activeData.options.push({
label: '',
value: ''
})
},
addTreeItem() {
++this.idGlobal
this.dialogVisible = true
this.currentNode = this.activeData.options
},
renderContent(h, { node, data }) {
return (
<div class="custom-tree-node">
<span>{node.label}</span>
<span class="node-operation">
<i on-click={() => this.append(data)}
class="el-icon-plus"
title="添加"
></i>
<i on-click={() => this.remove(node, data)}
class="el-icon-delete"
title="删除"
></i>
</span>
</div>
)
},
append(data) {
if (!data.children) {
this.$set(data, 'children', [])
}
this.dialogVisible = true
this.currentNode = data.children
},
remove(node, data) {
const { parent } = node
const children = parent.data.children || parent.data
const index = children.findIndex(d => d.id === data.id)
children.splice(index, 1)
},
addNode(data) {
this.currentNode.push(data)
},
setOptionValue(item, val) {
item.value = isNumberStr(val) ? +val : val
},
setDefaultValue(val) {
if (Array.isArray(val)) {
return val.join(',')
}
if (['string', 'number'].indexOf(val) > -1) {
return val
}
if (typeof val === 'boolean') {
return `${val}`
}
return val
},
onDefaultValueInput(str) {
if (Array.isArray(this.activeData.defaultValue)) {
// 数组
this.$set(
this.activeData,
'defaultValue',
str.split(',').map(val => (isNumberStr(val) ? +val : val))
)
} else if (['true', 'false'].indexOf(str) > -1) {
// 布尔
this.$set(this.activeData, 'defaultValue', JSON.parse(str))
} else {
// 字符串和数字
this.$set(
this.activeData,
'defaultValue',
isNumberStr(str) ? +str : str
)
}
},
onSwitchValueInput(val, name) {
if (['true', 'false'].indexOf(val) > -1) {
this.$set(this.activeData, name, JSON.parse(val))
} else {
this.$set(this.activeData, name, isNumberStr(val) ? +val : val)
}
},
setTimeValue(val, type) {
const valueFormat = type === 'week' ? dateTimeFormat.date : val
this.$set(this.activeData, 'defaultValue', null)
this.$set(this.activeData, 'value-format', valueFormat)
this.$set(this.activeData, 'format', val)
},
spanChange(val) {
this.formConf.span = val
},
multipleChange(val) {
this.$set(this.activeData, 'defaultValue', val ? [] : '')
},
dateTypeChange(val) {
this.setTimeValue(dateTimeFormat[val], val)
},
rangeChange(val) {
this.$set(
this.activeData,
'defaultValue',
val ? [this.activeData.min, this.activeData.max] : this.activeData.min
)
},
rateTextChange(val) {
if (val) this.activeData['show-score'] = false
},
rateScoreChange(val) {
if (val) this.activeData['show-text'] = false
},
colorFormatChange(val) {
this.activeData.defaultValue = null
this.activeData['show-alpha'] = val.indexOf('a') > -1
this.activeData.renderKey = +new Date() // 更新renderKey,重新渲染该组件
},
openIconsDialog(model) {
this.iconsVisible = true
this.currentIconModel = model
},
setIcon(val) {
this.activeData[this.currentIconModel] = val
},
tagChange(tagIcon) {
let target = inputComponents.find(item => item.tagIcon === tagIcon)
if (!target) target = selectComponents.find(item => item.tagIcon === tagIcon)
this.$emit('tag-change', target)
}
}
}
</script>
<style lang="scss" scoped>
.right-board {
width: 350px;
position: absolute;
right: 0;
top: 0;
padding-top: 3px;
.field-box {
position: relative;
height: calc(100vh - 42px);
box-sizing: border-box;
overflow: hidden;
}
.el-scrollbar {
height: 100%;
}
}
.select-item {
display: flex;
border: 1px dashed #fff;
box-sizing: border-box;
& .close-btn {
cursor: pointer;
color: #f56c6c;
}
& .el-input + .el-input {
margin-left: 4px;
}
}
.select-item + .select-item {
margin-top: 4px;
}
.select-item.sortable-chosen {
border: 1px dashed #00875A;
}
.select-line-icon {
line-height: 32px;
font-size: 22px;
padding: 0 4px;
color: #777;
}
.option-drag {
cursor: move;
}
.time-range {
.el-date-editor {
width: 227px;
}
::v-deep .el-icon-time {
display: none;
}
}
.document-link {
position: absolute;
display: block;
width: 26px;
height: 26px;
top: 0;
left: 0;
cursor: pointer;
background: #00875A;
z-index: 1;
border-radius: 0 0 6px 0;
text-align: center;
line-height: 26px;
color: #fff;
font-size: 18px;
}
.node-label{
font-size: 14px;
}
.node-icon{
color: #bebfc3;
}
</style>
@@ -0,0 +1,148 @@
<template>
<div>
<el-dialog
v-bind="$attrs"
:close-on-click-modal="false"
:modal-append-to-body="false"
v-on="$listeners"
@open="onOpen"
@close="onClose"
>
<el-row :gutter="0">
<el-form
ref="elForm"
:model="formData"
:rules="rules"
size="small"
label-width="100px"
>
<el-col :span="24">
<el-form-item
label="选项名"
prop="label"
>
<el-input
v-model="formData.label"
placeholder="请输入选项名"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item
label="选项值"
prop="value"
>
<el-input
v-model="formData.value"
placeholder="请输入选项值"
clearable
>
<el-select
slot="append"
v-model="dataType"
:style="{width: '100px'}"
>
<el-option
v-for="(item, index) in dataTypeOptions"
:key="index"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
/>
</el-select>
</el-input>
</el-form-item>
</el-col>
</el-form>
</el-row>
<div slot="footer">
<el-button
type="primary"
@click="handleConfirm"
>
确定
</el-button>
<el-button @click="close">
取消
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { isNumberStr } from '@/utils/index'
export default {
components: {},
inheritAttrs: false,
props: [],
data() {
return {
id: 100,
formData: {
label: undefined,
value: undefined
},
rules: {
label: [
{
required: true,
message: '请输入选项名',
trigger: 'blur'
}
],
value: [
{
required: true,
message: '请输入选项值',
trigger: 'blur'
}
]
},
dataType: 'string',
dataTypeOptions: [
{
label: '字符串',
value: 'string'
},
{
label: '数字',
value: 'number'
}
]
}
},
computed: {},
watch: {
'formData.value': function (val) {
this.dataType = isNumberStr(val) ? 'number' : 'string'
}
},
created() {},
mounted() {},
methods: {
onOpen() {
this.formData = {
label: undefined,
value: undefined
}
},
onClose() {},
close() {
this.$emit('update:visible', false)
},
handleConfirm() {
this.$refs.elForm.validate(valid => {
if (!valid) return
if (this.dataType === 'number') {
this.formData.value = parseFloat(this.formData.value)
}
this.formData.id = this.id++
this.$emit('commit', this.formData)
this.close()
})
}
}
}
</script>
+775
View File
@@ -0,0 +1,775 @@
<template>
<div class="container">
<div class="left-board">
<div class="logo-wrapper">
<div class="logo">
<img :src="logo" alt="logo"> Form Generator
</div>
</div>
<el-scrollbar class="left-scrollbar">
<div class="components-list">
<div class="components-title">
<svg-icon icon-class="component" />输入型组件
</div>
<draggable
class="components-draggable"
:list="inputComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }"
:clone="cloneComponent"
draggable=".components-item"
:sort="false"
@end="onEnd"
>
<div
v-for="(element, index) in inputComponents" :key="index" class="components-item"
@click="addComponent(element)"
>
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" />选择型组件
</div>
<draggable
class="components-draggable"
:list="selectComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }"
:clone="cloneComponent"
draggable=".components-item"
:sort="false"
@end="onEnd"
>
<div
v-for="(element, index) in selectComponents"
:key="index"
class="components-item"
@click="addComponent(element)"
>
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" /> 布局型组件
</div>
<draggable
class="components-draggable" :list="layoutComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" @end="onEnd"
>
<div
v-for="(element, index) in layoutComponents" :key="index" class="components-item"
@click="addComponent(element)"
>
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</draggable>
</div>
</el-scrollbar>
</div>
<div class="center-board">
<div class="action-bar">
<el-button icon="el-icon-download" type="text" @click="download">
导出vue文件
</el-button>
<el-button class="copy-btn-main" icon="el-icon-document-copy" type="text" @click="copy">
复制代码
</el-button>
<el-button class="delete-btn" icon="el-icon-delete" type="text" @click="empty">
清空
</el-button>
</div>
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="formConf.gutter">
<el-form
:size="formConf.size"
:label-position="formConf.labelPosition"
:disabled="formConf.disabled"
:label-width="formConf.labelWidth + 'px'"
>
<draggable class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup">
<draggable-item
v-for="(element, index) in drawingList"
:key="element.renderKey"
:drawing-list="drawingList"
:element="element"
:index="index"
:active-id="activeId"
:form-conf="formConf"
@activeItem="activeFormItem"
@copyItem="drawingItemCopy"
@deleteItem="drawingItemDelete"
/>
</draggable>
<div v-show="!drawingList.length" class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
</div>
<right-panel
:active-data="activeData"
:form-conf="formConf"
:show-field="!!drawingList.length"
@tag-change="tagChange"
/>
<code-type-dialog
:visible.sync="dialogVisible"
title="选择生成类型"
:show-file-name="showFileName"
@confirm="generate"
/>
<input id="copyNode" type="hidden">
</div>
</template>
<script>
import draggable from 'vuedraggable'
import beautifier from 'js-beautify'
import ClipboardJS from 'clipboard'
import render from '@/utils/generator/render'
import RightPanel from './RightPanel'
import { inputComponents, selectComponents, layoutComponents, formConf } from '@/utils/generator/config'
import { beautifierConf, titleCase } from '@/utils/index'
import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html'
import { makeUpJs } from '@/utils/generator/js'
import { makeUpCss } from '@/utils/generator/css'
import { drawingDefaultValue, initDrawingDefaultValue, cleanDrawingDefaultValue } from '@/utils/generator/drawingDefault'
import logo from '@/assets/logo/logo.png'
import CodeTypeDialog from './CodeTypeDialog'
import DraggableItem from './DraggableItem'
let oldActiveId
let tempActiveData
let clipboard = null
export default {
components: {
draggable,
render,
RightPanel,
CodeTypeDialog,
DraggableItem
},
data() {
return {
logo,
idGlobal: 100,
formConf,
inputComponents,
selectComponents,
layoutComponents,
labelWidth: 100,
drawingList: drawingDefaultValue,
drawingData: {},
activeId: drawingDefaultValue[0].formId,
drawerVisible: false,
formData: {},
dialogVisible: false,
generateConf: null,
showFileName: false,
activeData: drawingDefaultValue[0]
}
},
beforeCreate() {
initDrawingDefaultValue()
},
created() {
// 防止 firefox 下 拖拽 会新打卡一个选项卡
document.body.ondrop = event => {
event.preventDefault()
event.stopPropagation()
}
},
watch: {
'activeData.label': function (val, oldVal) {
if (
this.activeData.placeholder === undefined
|| !this.activeData.tag
|| oldActiveId !== this.activeId
) {
return
}
this.activeData.placeholder = this.activeData.placeholder.replace(oldVal, '') + val
},
activeId: {
handler(val) {
oldActiveId = val
},
immediate: true
}
},
mounted() {
clipboard = new ClipboardJS('#copyNode', {
text: () => {
const codeStr = this.generateCode()
this.$notify({
title: '成功',
message: '代码已复制到剪切板,可粘贴。',
type: 'success'
})
return codeStr
}
})
clipboard.on('error', () => {
this.$message.error('代码复制失败')
})
},
beforeDestroy() {
clipboard.destroy()
},
methods: {
activeFormItem(element) {
this.activeData = element
this.activeId = element.formId
},
onEnd(obj) {
if (obj.from !== obj.to) {
this.activeData = tempActiveData
this.activeId = this.idGlobal
}
},
addComponent(item) {
const clone = this.cloneComponent(item)
this.drawingList.push(clone)
this.activeFormItem(clone)
},
cloneComponent(origin) {
const clone = JSON.parse(JSON.stringify(origin))
clone.formId = ++this.idGlobal
clone.span = formConf.span
clone.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
if (!clone.layout) clone.layout = 'colFormItem'
if (clone.layout === 'colFormItem') {
clone.vModel = `field${this.idGlobal}`
clone.placeholder !== undefined && (clone.placeholder += clone.label)
tempActiveData = clone
} else if (clone.layout === 'rowFormItem') {
delete clone.label
clone.componentName = `row${this.idGlobal}`
clone.gutter = this.formConf.gutter
tempActiveData = clone
}
return tempActiveData
},
AssembleFormData() {
this.formData = {
fields: JSON.parse(JSON.stringify(this.drawingList)),
...this.formConf
}
},
generate(data) {
const func = this[`exec${titleCase(this.operationType)}`]
this.generateConf = data
func && func(data)
},
execRun() {
this.AssembleFormData()
this.drawerVisible = true
},
execDownload(data) {
const codeStr = this.generateCode()
const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
this.$download.saveAs(blob, data.fileName)
},
execCopy() {
document.getElementById('copyNode').click()
},
empty() {
this.$confirm('确定要清空所有组件吗?', '提示', { type: 'warning' }).then(
() => {
this.drawingList = []
cleanDrawingDefaultValue()
}
)
},
drawingItemCopy(item, parent) {
let clone = JSON.parse(JSON.stringify(item))
clone = this.createIdAndKey(clone)
parent.push(clone)
this.activeFormItem(clone)
},
createIdAndKey(item) {
item.formId = ++this.idGlobal
item.renderKey = +new Date()
if (item.layout === 'colFormItem') {
item.vModel = `field${this.idGlobal}`
} else if (item.layout === 'rowFormItem') {
item.componentName = `row${this.idGlobal}`
}
if (Array.isArray(item.children)) {
item.children = item.children.map(childItem => this.createIdAndKey(childItem))
}
return item
},
drawingItemDelete(index, parent) {
parent.splice(index, 1)
this.$nextTick(() => {
const len = this.drawingList.length
if (len) {
this.activeFormItem(this.drawingList[len - 1])
}
})
},
generateCode() {
const { type } = this.generateConf
this.AssembleFormData()
const script = vueScript(makeUpJs(this.formData, type))
const html = vueTemplate(makeUpHtml(this.formData, type))
const css = cssStyle(makeUpCss(this.formData))
return beautifier.html(html + script + css, beautifierConf.html)
},
download() {
this.dialogVisible = true
this.showFileName = true
this.operationType = 'download'
},
run() {
this.dialogVisible = true
this.showFileName = false
this.operationType = 'run'
},
copy() {
this.dialogVisible = true
this.showFileName = false
this.operationType = 'copy'
},
tagChange(newTag) {
newTag = this.cloneComponent(newTag)
newTag.vModel = this.activeData.vModel
newTag.formId = this.activeId
newTag.span = this.activeData.span
delete this.activeData.tag
delete this.activeData.tagIcon
delete this.activeData.document
Object.keys(newTag).forEach(key => {
if (this.activeData[key] !== undefined
&& typeof this.activeData[key] === typeof newTag[key]) {
newTag[key] = this.activeData[key]
}
})
this.activeData = newTag
this.updateDrawingList(newTag, this.drawingList)
},
updateDrawingList(newTag, list) {
const index = list.findIndex(item => item.formId === this.activeId)
if (index > -1) {
list.splice(index, 1, newTag)
} else {
list.forEach(item => {
if (Array.isArray(item.children)) this.updateDrawingList(newTag, item.children)
})
}
}
}
}
</script>
<style lang='scss'>
.editor-tabs{
background: #121315;
.el-tabs__header{
margin: 0;
border-bottom-color: #121315;
.el-tabs__nav{
border-color: #121315;
}
}
.el-tabs__item{
height: 32px;
line-height: 32px;
color: #888a8e;
border-left: 1px solid #121315 !important;
background: #363636;
margin-right: 5px;
user-select: none;
}
.el-tabs__item.is-active{
background: #1e1e1e;
border-bottom-color: #1e1e1e!important;
color: #fff;
}
.el-icon-edit{
color: #f1fa8c;
}
.el-icon-document{
color: #a95812;
}
}
// home
.right-scrollbar {
.el-scrollbar__view {
padding: 12px 18px 15px 15px;
}
}
.left-scrollbar .el-scrollbar__wrap {
box-sizing: border-box;
overflow-x: hidden !important;
margin-bottom: 0 !important;
}
.center-tabs{
.el-tabs__header{
margin-bottom: 0!important;
}
.el-tabs__item{
width: 50%;
text-align: center;
}
.el-tabs__nav{
width: 100%;
}
}
.reg-item{
padding: 12px 6px;
background: #f8f8f8;
position: relative;
border-radius: 4px;
.close-btn{
position: absolute;
right: -6px;
top: -6px;
display: block;
width: 16px;
height: 16px;
line-height: 16px;
background: rgba(0, 0, 0, 0.2);
border-radius: 50%;
color: #fff;
text-align: center;
z-index: 1;
cursor: pointer;
font-size: 12px;
&:hover{
background: rgba(210, 23, 23, 0.5)
}
}
& + .reg-item{
margin-top: 18px;
}
}
.action-bar{
& .el-button+.el-button {
margin-left: 15px;
}
& i {
font-size: 20px;
vertical-align: middle;
position: relative;
top: -1px;
}
}
.custom-tree-node{
width: 100%;
font-size: 14px;
.node-operation{
float: right;
}
i[class*="el-icon"] + i[class*="el-icon"]{
margin-left: 6px;
}
.el-icon-plus{
color: #00875A;
}
.el-icon-delete{
color: #157a0c;
}
}
.left-scrollbar .el-scrollbar__view{
overflow-x: hidden;
}
.el-rate{
display: inline-block;
vertical-align: text-top;
}
.el-upload__tip{
line-height: 1.2;
}
$selectedColor: #f6f7ff;
$lighterBlue: #00875A;
.container {
position: relative;
width: 100%;
height: 100%;
}
.components-list {
padding: 8px;
box-sizing: border-box;
height: 100%;
.components-item {
display: inline-block;
width: 48%;
margin: 1%;
transition: transform 0ms !important;
}
}
.components-draggable{
padding-bottom: 20px;
}
.components-title{
font-size: 14px;
color: #222;
margin: 6px 2px;
.svg-icon{
color: #666;
font-size: 18px;
}
}
.components-body {
padding: 8px 10px;
background: $selectedColor;
font-size: 12px;
cursor: move;
border: 1px dashed $selectedColor;
border-radius: 3px;
.svg-icon{
color: #777;
font-size: 15px;
}
&:hover {
border: 1px dashed #787be8;
color: #787be8;
.svg-icon {
color: #787be8;
}
}
}
.left-board {
width: 260px;
position: absolute;
left: 0;
top: 0;
height: 100vh;
}
.left-scrollbar{
height: calc(100vh - 42px);
overflow: hidden;
}
.center-scrollbar {
height: calc(100vh - 42px);
overflow: hidden;
border-left: 1px solid #f1e8e8;
border-right: 1px solid #f1e8e8;
box-sizing: border-box;
}
.center-board {
height: 100vh;
width: auto;
margin: 0 350px 0 260px;
box-sizing: border-box;
}
.empty-info{
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: #ccb1ea;
letter-spacing: 4px;
}
.action-bar{
position: relative;
height: 42px;
text-align: right;
padding: 0 15px;
box-sizing: border-box;;
border: 1px solid #f1e8e8;
border-top: none;
border-left: none;
.delete-btn{
color: #F56C6C;
}
}
.logo-wrapper{
position: relative;
height: 42px;
background: #fff;
border-bottom: 1px solid #f1e8e8;
box-sizing: border-box;
}
.logo{
position: absolute;
left: 12px;
top: 6px;
line-height: 30px;
color: #00afff;
font-weight: 600;
font-size: 17px;
white-space: nowrap;
> img{
width: 30px;
height: 30px;
vertical-align: top;
}
.github{
display: inline-block;
vertical-align: sub;
margin-left: 15px;
> img{
height: 22px;
}
}
}
.center-board-row {
padding: 12px 12px 15px 12px;
box-sizing: border-box;
& > .el-form {
// 69 = 12+15+42
height: calc(100vh - 69px);
}
}
.drawing-board {
height: 100%;
position: relative;
.components-body {
padding: 0;
margin: 0;
font-size: 0;
}
.sortable-ghost {
position: relative;
display: block;
overflow: hidden;
&::before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: rgb(89, 89, 223);
z-index: 2;
}
}
.components-item.sortable-ghost {
width: 100%;
height: 60px;
background-color: $selectedColor;
}
.active-from-item {
& > .el-form-item{
background: $selectedColor;
border-radius: 6px;
}
& > .drawing-item-copy, & > .drawing-item-delete{
display: initial;
}
& > .component-name{
color: $lighterBlue;
}
}
.el-form-item{
margin-bottom: 15px;
}
}
.drawing-item{
position: relative;
cursor: move;
&.unfocus-bordered:not(.activeFromItem) > div:first-child {
border: 1px dashed #ccc;
}
.el-form-item{
padding: 12px 10px;
}
}
.drawing-row-item{
position: relative;
cursor: move;
box-sizing: border-box;
border: 1px dashed #ccc;
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
.drawing-row-item {
margin-bottom: 2px;
}
.el-col{
margin-top: 22px;
}
.el-form-item{
margin-bottom: 0;
}
.drag-wrapper{
min-height: 80px;
}
&.active-from-item{
border: 1px dashed $lighterBlue;
}
.component-name{
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: #bbb;
display: inline-block;
padding: 0 6px;
}
}
.drawing-item, .drawing-row-item{
&:hover {
& > .el-form-item{
background: $selectedColor;
border-radius: 6px;
}
& > .drawing-item-copy, & > .drawing-item-delete{
display: initial;
}
}
& > .drawing-item-copy, & > .drawing-item-delete{
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
}
& > .drawing-item-copy{
right: 56px;
border-color: $lighterBlue;
color: $lighterBlue;
background: #fff;
&:hover{
background: $lighterBlue;
color: #fff;
}
}
& > .drawing-item-delete{
right: 24px;
border-color: #F56C6C;
color: #F56C6C;
background: #fff;
&:hover{
background: #F56C6C;
color: #fff;
}
}
}
</style>
@@ -0,0 +1,60 @@
<template>
<el-form ref="basicInfoForm" :model="info" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-form-item label="表名称" prop="tableName">
<el-input placeholder="请输入仓库名称" v-model="info.tableName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表描述" prop="tableComment">
<el-input placeholder="请输入" v-model="info.tableComment" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实体类名称" prop="className">
<el-input placeholder="请输入" v-model="info.className" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作者" prop="functionAuthor">
<el-input placeholder="请输入" v-model="info.functionAuthor" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input type="textarea" :rows="3" v-model="info.remark"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
export default {
props: {
info: {
type: Object,
default: null
}
},
data() {
return {
rules: {
tableName: [
{ required: true, message: "请输入表名称", trigger: "blur" }
],
tableComment: [
{ required: true, message: "请输入表描述", trigger: "blur" }
],
className: [
{ required: true, message: "请输入实体类名称", trigger: "blur" }
],
functionAuthor: [
{ required: true, message: "请输入作者", trigger: "blur" }
]
}
}
}
}
</script>
@@ -0,0 +1,45 @@
<template>
<!-- 创建表 -->
<el-dialog title="创建表" :visible.sync="visible" width="800px" top="5vh" append-to-body>
<span>创建表语句(支持多个建表语句)</span>
<el-input type="textarea" :rows="10" placeholder="请输入文本" v-model="content"></el-input>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleCreateTable"> </el-button>
<el-button @click="visible = false"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { createTable } from "@/api/tool/gen"
export default {
data() {
return {
// 遮罩层
visible: false,
// 文本内容
content: ""
}
},
methods: {
// 显示弹框
show() {
this.visible = true
},
/** 创建按钮操作 */
handleCreateTable() {
if (this.content === "") {
this.$modal.msgError("请输入建表语句")
return
}
createTable({ sql: this.content, tplWebType: 'element-ui' }).then(res => {
this.$modal.msgSuccess(res.msg)
if (res.code === 200) {
this.visible = false
this.$emit("ok")
}
})
}
}
}
</script>
+230
View File
@@ -0,0 +1,230 @@
<template>
<el-card>
<el-tabs v-model="activeName">
<el-tab-pane label="基本信息" name="basic">
<basic-info-form ref="basicInfo" :info="info" />
</el-tab-pane>
<el-tab-pane label="字段信息" name="columnInfo">
<el-table ref="dragTable" :data="columns" row-key="columnId" :max-height="tableHeight">
<el-table-column label="序号" type="index" min-width="5%" class-name="allowDrag"/>
<el-table-column label="字段列名" prop="columnName" min-width="10%" :show-overflow-tooltip="true" class-name="allowDrag"/>
<el-table-column label="字段描述" min-width="10%">
<template slot-scope="scope">
<el-input v-model="scope.row.columnComment"></el-input>
</template>
</el-table-column>
<el-table-column
label="物理类型"
prop="columnType"
min-width="10%"
:show-overflow-tooltip="true"
/>
<el-table-column label="Java类型" min-width="11%">
<template slot-scope="scope">
<el-select v-model="scope.row.javaType">
<el-option label="Long" value="Long" />
<el-option label="String" value="String" />
<el-option label="Integer" value="Integer" />
<el-option label="Double" value="Double" />
<el-option label="BigDecimal" value="BigDecimal" />
<el-option label="Date" value="Date" />
<el-option label="Boolean" value="Boolean" />
</el-select>
</template>
</el-table-column>
<el-table-column label="java属性" min-width="10%">
<template slot-scope="scope">
<el-input v-model="scope.row.javaField"></el-input>
</template>
</el-table-column>
<el-table-column label="插入" min-width="5%">
<template slot-scope="scope">
<el-checkbox true-label="1" false-label="0" v-model="scope.row.isInsert"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="编辑" min-width="5%">
<template slot-scope="scope">
<el-checkbox true-label="1" false-label="0" v-model="scope.row.isEdit"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="列表" min-width="5%">
<template slot-scope="scope">
<el-checkbox true-label="1" false-label="0" v-model="scope.row.isList"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="查询" min-width="5%">
<template slot-scope="scope">
<el-checkbox true-label="1" false-label="0" v-model="scope.row.isQuery"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="查询方式" min-width="10%">
<template slot-scope="scope">
<el-select v-model="scope.row.queryType">
<el-option label="=" value="EQ" />
<el-option label="!=" value="NE" />
<el-option label=">" value="GT" />
<el-option label=">=" value="GTE" />
<el-option label="<" value="LT" />
<el-option label="<=" value="LTE" />
<el-option label="LIKE" value="LIKE" />
<el-option label="BETWEEN" value="BETWEEN" />
</el-select>
</template>
</el-table-column>
<el-table-column label="必填" min-width="5%">
<template slot-scope="scope">
<el-checkbox true-label="1" false-label="0" v-model="scope.row.isRequired"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="显示类型" min-width="12%">
<template slot-scope="scope">
<el-select v-model="scope.row.htmlType">
<el-option label="文本框" value="input" />
<el-option label="文本域" value="textarea" />
<el-option label="下拉框" value="select" />
<el-option label="单选框" value="radio" />
<el-option label="复选框" value="checkbox" />
<el-option label="日期控件" value="datetime" />
<el-option label="图片上传" value="imageUpload" />
<el-option label="文件上传" value="fileUpload" />
<el-option label="富文本控件" value="editor" />
</el-select>
</template>
</el-table-column>
<el-table-column label="字典类型" min-width="12%">
<template slot-scope="scope">
<el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
<el-option
v-for="dict in dictOptions"
:key="dict.dictType"
:label="dict.dictName"
:value="dict.dictType">
<span style="float: left">{{ dict.dictName }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ dict.dictType }}</span>
</el-option>
</el-select>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="生成信息" name="genInfo">
<gen-info-form ref="genInfo" :info="info" :tables="tables" :menus="menus"/>
</el-tab-pane>
</el-tabs>
<el-form label-width="100px">
<el-form-item style="text-align: center;margin-left:-100px;margin-top:10px;">
<el-button type="primary" @click="submitForm()">提交</el-button>
<el-button @click="close()">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script>
import { getGenTable, updateGenTable } from "@/api/tool/gen"
import { optionselect as getDictOptionselect } from "@/api/system/dict/type"
import { listMenu as getMenuTreeselect } from "@/api/system/menu"
import basicInfoForm from "./basicInfoForm"
import genInfoForm from "./genInfoForm"
import Sortable from 'sortablejs'
export default {
name: "GenEdit",
components: {
basicInfoForm,
genInfoForm
},
data() {
return {
// 选中选项卡的 name
activeName: "columnInfo",
// 表格的高度
tableHeight: document.documentElement.scrollHeight - 245 + "px",
// 表信息
tables: [],
// 表列信息
columns: [],
// 字典信息
dictOptions: [],
// 菜单信息
menus: [],
// 表详细信息
info: {}
}
},
created() {
const tableId = this.$route.params && this.$route.params.tableId
if (tableId) {
// 获取表详细信息
getGenTable(tableId).then(res => {
this.columns = res.data.rows
this.info = res.data.info
this.tables = res.data.tables
})
/** 查询字典下拉列表 */
getDictOptionselect().then(response => {
this.dictOptions = response.data
})
/** 查询菜单下拉列表 */
getMenuTreeselect().then(response => {
this.menus = this.handleTree(response.data, "menuId")
})
}
},
methods: {
/** 提交按钮 */
submitForm() {
const basicForm = this.$refs.basicInfo.$refs.basicInfoForm
const genForm = this.$refs.genInfo.$refs.genInfoForm
Promise.all([basicForm, genForm].map(this.getFormPromise)).then(res => {
const validateResult = res.every(item => !!item)
if (validateResult) {
const genTable = Object.assign({}, basicForm.model, genForm.model)
genTable.columns = this.columns
genTable.params = {
genView: genTable.view ? '1' : '0',
treeCode: genTable.treeCode,
treeName: genTable.treeName,
treeParentCode: genTable.treeParentCode,
parentMenuId: genTable.parentMenuId
}
updateGenTable(genTable).then(res => {
this.$modal.msgSuccess(res.msg)
if (res.code === 200) {
this.close()
}
})
} else {
this.$modal.msgError("表单校验未通过,请重新检查提交内容")
}
})
},
getFormPromise(form) {
return new Promise(resolve => {
form.validate(res => {
resolve(res)
})
})
},
/** 关闭按钮 */
close() {
const obj = { path: "/tool/gen", query: { t: Date.now(), pageNum: this.$route.query.pageNum } }
this.$tab.closeOpenPage(obj)
}
},
mounted() {
const el = this.$refs.dragTable.$el.querySelectorAll(".el-table__body-wrapper > table > tbody")[0]
const sortable = Sortable.create(el, {
handle: ".allowDrag",
onEnd: evt => {
const targetRow = this.columns.splice(evt.oldIndex, 1)[0]
this.columns.splice(evt.newIndex, 0, targetRow)
for (let index in this.columns) {
this.columns[index].sort = parseInt(index) + 1
}
}
})
}
}
</script>
+336
View File
@@ -0,0 +1,336 @@
<template>
<el-form ref="genInfoForm" :model="info" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-form-item prop="tplCategory">
<span slot="label">生成模板</span>
<el-select v-model="info.tplCategory" @change="tplSelectChange">
<el-option label="单表(增删改查)" value="crud" />
<el-option label="树表(增删改查)" value="tree" />
<el-option label="主子表(增删改查)" value="sub" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="tplWebType">
<span slot="label">前端类型</span>
<el-select v-model="info.tplWebType">
<el-option label="Vue2 Element UI 模版" value="element-ui" />
<el-option label="Vue3 Element Plus 模版" value="element-plus" />
<el-option label="Vue3 Element Plus TypeScript 模版" value="element-plus-typescript" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="packageName">
<span slot="label">
生成包路径
<el-tooltip content="生成在哪个java包下,例如 com.roomroot.system" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-input v-model="info.packageName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="moduleName">
<span slot="label">
生成模块名
<el-tooltip content="可理解为子系统名,例如 system" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-input v-model="info.moduleName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="businessName">
<span slot="label">
生成业务名
<el-tooltip content="可理解为功能英文名,例如 user" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-input v-model="info.businessName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="functionName">
<span slot="label">
生成功能名
<el-tooltip content="用作类描述,例如 用户" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-input v-model="info.functionName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="formColNum">
<span slot="label">
表单布局
<el-tooltip content="选择表单的栅格布局方式" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-select v-model="info.formColNum">
<el-option label="单列" :value="1" />
<el-option label="双列" :value="2" />
<el-option label="三列" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="genView">
<span slot="label">扩展功能</span>
<el-checkbox v-model="info.view">生成详情页</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="genType">
<span slot="label">
生成代码方式
<el-tooltip content="默认为zip压缩包下载,也可以自定义生成路径" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-radio v-model="info.genType" label="0">zip压缩包</el-radio>
<el-radio v-model="info.genType" label="1">自定义路径</el-radio>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<span slot="label">
上级菜单
<el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<treeselect
:append-to-body="true"
v-model="info.parentMenuId"
:options="menus"
:normalizer="normalizer"
:show-count="true"
placeholder="请选择系统菜单"
/>
</el-form-item>
</el-col>
<el-col :span="24" v-if="info.genType == '1'">
<el-form-item prop="genPath">
<span slot="label">
自定义路径
<el-tooltip content="填写磁盘绝对路径,若不填写,则生成到当前Web项目下" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-input v-model="info.genPath">
<el-dropdown slot="append">
<el-button type="primary">
最近路径快速选择
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="info.genPath = '/'">恢复默认的生成基础路径</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="info.tplCategory == 'tree'">
<h4 class="form-header">其他信息</h4>
<el-col :span="12">
<el-form-item>
<span slot="label">
树编码字段
<el-tooltip content="树显示的编码字段名, 如:dept_id" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-select v-model="info.treeCode" placeholder="请选择">
<el-option
v-for="(column, index) in info.columns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<span slot="label">
树父编码字段
<el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-select v-model="info.treeParentCode" placeholder="请选择">
<el-option
v-for="(column, index) in info.columns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<span slot="label">
树名称字段
<el-tooltip content="树节点的显示名称字段名, 如:dept_name" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-select v-model="info.treeName" placeholder="请选择">
<el-option
v-for="(column, index) in info.columns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="info.tplCategory == 'sub'">
<h4 class="form-header">关联信息</h4>
<el-col :span="12">
<el-form-item>
<span slot="label">
关联子表的表名
<el-tooltip content="关联子表的表名, 如:sys_user" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-select v-model="info.subTableName" placeholder="请选择" @change="subSelectChange">
<el-option
v-for="(table, index) in tables"
:key="index"
:label="table.tableName + '' + table.tableComment"
:value="table.tableName"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<span slot="label">
子表关联的外键名
<el-tooltip content="子表关联的外键名, 如:user_id" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</span>
<el-select v-model="info.subTableFkName" placeholder="请选择">
<el-option
v-for="(column, index) in subColumns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
export default {
components: { Treeselect },
props: {
info: {
type: Object,
default: null
},
tables: {
type: Array,
default: null
},
menus: {
type: Array,
default: []
}
},
data() {
return {
subColumns: [],
rules: {
tplCategory: [
{ required: true, message: "请选择生成模板", trigger: "blur" }
],
packageName: [
{ required: true, message: "请输入生成包路径", trigger: "blur" }
],
moduleName: [
{ required: true, message: "请输入生成模块名", trigger: "blur" }
],
businessName: [
{ required: true, message: "请输入生成业务名", trigger: "blur" }
],
functionName: [
{ required: true, message: "请输入生成功能名", trigger: "blur" }
]
}
}
},
watch: {
'info.subTableName': function(val) {
this.setSubTableColumns(val)
},
'info.tplWebType': function(val) {
if (val === '') {
this.info.tplWebType = "element-ui"
}
}
},
methods: {
/** 转换菜单数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.menuId,
label: node.menuName,
children: node.children
}
},
/** 选择子表名触发 */
subSelectChange(value) {
this.info.subTableFkName = ''
},
/** 选择生成模板触发 */
tplSelectChange(value) {
if (value !== 'sub') {
this.info.subTableName = ''
this.info.subTableFkName = ''
}
},
/** 设置关联外键 */
setSubTableColumns(value) {
for (var item in this.tables) {
const name = this.tables[item].tableName
if (value === name) {
this.subColumns = this.tables[item].columns
break
}
}
}
}
}
</script>
+120
View File
@@ -0,0 +1,120 @@
<template>
<!-- 导入表 -->
<el-dialog title="导入表" :visible.sync="visible" width="800px" top="5vh" append-to-body>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="表名称" prop="tableName">
<el-input
v-model="queryParams.tableName"
placeholder="请输入表名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
placeholder="请输入表描述"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row>
<el-table @row-click="clickRow" ref="table" :data="dbTableList" @selection-change="handleSelectionChange" height="260px">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="tableName" label="表名称" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="tableComment" label="表描述" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column prop="updateTime" label="更新时间"></el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-row>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleImportTable"> </el-button>
<el-button @click="visible = false"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { listDbTable, importTable } from "@/api/tool/gen"
export default {
data() {
return {
// 遮罩层
visible: false,
// 选中数组值
tables: [],
// 总条数
total: 0,
// 表数据
dbTableList: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
tableName: undefined,
tableComment: undefined
}
}
},
methods: {
// 显示弹框
show() {
this.getList()
this.visible = true
},
clickRow(row) {
this.$refs.table.toggleRowSelection(row)
},
// 多选框选中数据
handleSelectionChange(selection) {
this.tables = selection.map(item => item.tableName)
},
// 查询表数据
getList() {
listDbTable(this.queryParams).then(res => {
if (res.code === 200) {
this.dbTableList = res.rows
this.total = res.total
}
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
/** 导入按钮操作 */
handleImportTable() {
const tableNames = this.tables.join(",")
if (tableNames == "") {
this.$modal.msgError("请选择要导入的表")
return
}
importTable({ tables: tableNames, tplWebType: 'element-ui' }).then(res => {
this.$modal.msgSuccess(res.msg)
if (res.code === 200) {
this.visible = false
this.$emit("ok")
}
})
}
}
}
</script>
+386
View File
@@ -0,0 +1,386 @@
<template>
<div class="app-container tool-gen">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="表名称" prop="tableName">
<el-input
v-model="queryParams.tableName"
placeholder="请输入表名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
placeholder="请输入表描述"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-download"
size="mini"
:disabled="multiple"
@click="handleGenTable"
v-hasPermi="['tool:gen:code']"
>生成</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="openCreateTable"
v-hasRole="['admin']"
>创建</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-upload"
size="mini"
@click="openImportTable"
v-hasPermi="['tool:gen:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleEditTable"
v-hasPermi="['tool:gen:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['tool:gen:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table ref="tables" v-loading="loading" :data="tableList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange" :height="tableHeight">
<el-table-column type="selection" align="center" width="55"></el-table-column>
<el-table-column label="序号" type="index" width="50" align="center">
<template slot-scope="scope">
<span>{{(queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1}}</span>
</template>
</el-table-column>
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" width="140" />
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" width="140" />
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" width="140" />
<el-table-column label="创建时间" align="center" prop="createTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="160" />
<el-table-column label="更新时间" align="center" prop="updateTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="160" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
type="text"
size="small"
icon="el-icon-view"
@click="handlePreview(scope.row)"
v-hasPermi="['tool:gen:preview']"
>预览</el-button>
<el-button
type="text"
size="small"
icon="el-icon-edit"
@click="handleEditTable(scope.row)"
v-hasPermi="['tool:gen:edit']"
>编辑</el-button>
<el-button
type="text"
size="small"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['tool:gen:remove']"
>删除</el-button>
<el-button
type="text"
size="small"
icon="el-icon-refresh"
@click="handleSynchDb(scope.row)"
v-hasPermi="['tool:gen:edit']"
>同步</el-button>
<el-button
type="text"
size="small"
icon="el-icon-download"
@click="handleGenTable(scope.row)"
v-hasPermi="['tool:gen:code']"
>生成代码</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 预览界面 -->
<el-dialog :title="preview.title" :visible.sync="preview.open" width="80%" top="5vh" append-to-body class="scrollbar">
<el-tabs v-model="preview.activeName">
<el-tab-pane
v-for="(value, key) in preview.data"
:label="key.substring(key.lastIndexOf('/')+1,key.indexOf('.vm'))"
:name="key.substring(key.lastIndexOf('/')+1,key.indexOf('.vm'))"
:key="key"
>
<el-link :underline="false" icon="el-icon-document-copy" v-clipboard:copy="value" v-clipboard:success="clipboardSuccess" style="float:right">复制</el-link>
<pre><code class="hljs" v-html="highlightedCode(value, key)"></code></pre>
</el-tab-pane>
</el-tabs>
</el-dialog>
<import-table ref="import" @ok="handleQuery" />
<create-table ref="create" @ok="handleQuery" />
</div>
</template>
<script>
import { listTable, previewTable, delTable, genCode, synchDb } from "@/api/tool/gen"
import importTable from "./importTable"
import createTable from "./createTable"
import hljs from "highlight.js/lib/highlight"
import "highlight.js/styles/github-gist.css"
hljs.registerLanguage("java", require("highlight.js/lib/languages/java"))
hljs.registerLanguage("xml", require("highlight.js/lib/languages/xml"))
hljs.registerLanguage("html", require("highlight.js/lib/languages/xml"))
hljs.registerLanguage("vue", require("highlight.js/lib/languages/xml"))
hljs.registerLanguage("javascript", require("highlight.js/lib/languages/javascript"))
hljs.registerLanguage("typescript", require("highlight.js/lib/languages/typescript"))
hljs.registerLanguage("sql", require("highlight.js/lib/languages/sql"))
export default {
name: "Gen",
components: { importTable, createTable },
data() {
return {
// 遮罩层
loading: true,
// 唯一标识符
uniqueId: "",
// 选中数组
ids: [],
// 选中表数组
tableNames: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 表数据
tableList: [],
// 日期范围
dateRange: "",
// 默认排序
defaultSort: { prop: "createTime", order: "descending" },
// 表格高度
tableHeight: window.innerHeight - 240,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
tableName: undefined,
tableComment: undefined
},
// 预览参数
preview: {
open: false,
title: "代码预览",
data: {},
activeName: "domain.java"
}
}
},
created() {
this.queryParams.orderByColumn = this.defaultSort.prop
this.queryParams.isAsc = this.defaultSort.order
this.getList()
},
activated() {
const time = this.$route.query.t
if (time != null && time != this.uniqueId) {
this.uniqueId = time
this.queryParams.pageNum = Number(this.$route.query.pageNum)
this.getList()
}
},
mounted() {
this.getTableHeight()
window.addEventListener('resize', this.getTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.getTableHeight)
},
watch: {
showSearch() {
this.$nextTick(() => {
this.getTableHeight()
})
}
},
methods: {
getTableHeight() {
const offset = this.showSearch ? 240 : 190
this.tableHeight = window.innerHeight - offset
},
/** 查询表集合 */
getList() {
this.loading = true
listTable(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.tableList = response.rows
this.total = response.total
this.loading = false
}
)
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 生成代码操作 */
handleGenTable(row) {
const tableNames = row.tableName || this.tableNames
if (tableNames == "") {
this.$modal.msgError("请选择要生成的数据")
return
}
if (row.genType === "1") {
genCode(row.tableName).then(() => {
this.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath)
})
} else {
const zipName = Array.isArray(tableNames) ? "roomroot.zip" : tableNames + ".zip"
this.$download.zip("/tool/gen/batchGenCode?tables=" + tableNames, zipName)
}
},
/** 同步数据库操作 */
handleSynchDb(row) {
const tableName = row.tableName
this.$modal.confirm('确认要强制同步"' + tableName + '"表结构吗?').then(function() {
return synchDb(tableName)
}).then(() => {
this.$modal.msgSuccess("同步成功")
}).catch(() => {})
},
/** 打开导入表弹窗 */
openImportTable() {
this.$refs.import.show()
},
/** 打开创建表弹窗 */
openCreateTable() {
this.$refs.create.show()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm("queryForm")
this.queryParams.pageNum = 1
this.$refs.tables.sort(this.defaultSort.prop, this.defaultSort.order)
},
/** 预览按钮 */
handlePreview(row) {
previewTable(row.tableId).then(response => {
this.preview.data = response.data
this.preview.open = true
this.preview.activeName = "domain.java"
})
},
/** 高亮显示 */
highlightedCode(code, key) {
const vmName = key.substring(key.lastIndexOf("/") + 1, key.indexOf(".vm"))
var language = vmName.substring(vmName.indexOf(".") + 1, vmName.length)
const result = hljs.highlight(language, code || "", true)
return result.value || '&nbsp;'
},
/** 复制代码成功 */
clipboardSuccess() {
this.$modal.msgSuccess("复制成功")
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.tableId)
this.tableNames = selection.map(item => item.tableName)
this.single = selection.length != 1
this.multiple = !selection.length
},
/** 排序触发事件 */
handleSortChange(column, prop, order) {
this.queryParams.orderByColumn = column.prop
this.queryParams.isAsc = column.order
this.getList()
},
/** 修改按钮操作 */
handleEditTable(row) {
const tableId = row.tableId || this.ids[0]
const tableName = row.tableName || this.tableNames[0]
const params = { pageNum: this.queryParams.pageNum }
this.$tab.openPage("修改[" + tableName + "]生成配置", '/tool/gen-edit/index/' + tableId, params)
},
/** 删除按钮操作 */
handleDelete(row) {
const tableIds = row.tableId || this.ids
this.$modal.confirm('是否确认删除表编号为"' + tableIds + '"的数据项?').then(function() {
return delTable(tableIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
}
}
</script>
<style scoped>
.tool-gen ::v-deep .el-table ::-webkit-scrollbar {
display: none;
}
.tool-gen ::v-deep .el-table {
scrollbar-width: none;
-ms-overflow-style: none;
}
.tool-gen ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
.tool-gen ::v-deep .el-table__body-wrapper {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>
+15
View File
@@ -0,0 +1,15 @@
<template>
<i-frame :src="url" />
</template>
<script>
import iFrame from "@/components/iFrame/index"
export default {
name: "Swagger",
components: { iFrame },
data() {
return {
url: process.env.VUE_APP_BASE_API + "/swagger-ui/index.html"
}
}
}
</script>