解决API文档和路由配置合并冲突

This commit is contained in:
李旭明 committed 2025-08-04 11:19:52 +08:00
commit 79a96a7dfd
662 files changed
+127608 -205195

No files matched your search

@@ -0,0 +1,748 @@
<template>
<div class="performance-indicators">
<el-alert
title="绩效指标设置"
type="info"
show-icon
description="请按需要填写各类绩效指标,未使用的指标可以不填写。分值占比总和必须等于100%。"
:closable="false"
style="margin-bottom: 20px"
/>
<!-- 产出指标 -->
<el-card class="indicator-category" style="margin-bottom: 20px">
<template #header>
<div class="category-header">
<el-icon><TrendCharts /></el-icon>
<span>产出指标</span>
</div>
</template>
<!-- 数量指标 -->
<div class="indicator-item">
<h4 class="indicator-title">数量指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.quantity.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.quantity.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.quantity.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.quantity.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.quantity.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 质量指标 -->
<div class="indicator-item">
<h4 class="indicator-title">质量指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.quality.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.quality.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.quality.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.quality.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.quality.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 时效指标 -->
<div class="indicator-item">
<h4 class="indicator-title">时效指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.timeliness.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.timeliness.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.timeliness.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.timeliness.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.timeliness.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 成本指标 -->
<div class="indicator-item">
<h4 class="indicator-title">成本指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.cost.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.cost.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.cost.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.cost.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.cost.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<!-- 效益指标 -->
<el-card class="indicator-category" style="margin-bottom: 20px">
<template #header>
<div class="category-header">
<el-icon><Money /></el-icon>
<span>效益指标</span>
</div>
</template>
<!-- 经济效益指标 -->
<div class="indicator-item">
<h4 class="indicator-title">经济效益指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.economic.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.economic.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.economic.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.economic.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.economic.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 社会效益指标 -->
<div class="indicator-item">
<h4 class="indicator-title">社会效益指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.social.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.social.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.social.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.social.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.social.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 生态效益指标 -->
<div class="indicator-item">
<h4 class="indicator-title">生态效益指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.ecological.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.ecological.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.ecological.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.ecological.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.ecological.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<!-- 满意度指标 -->
<el-card class="indicator-category" style="margin-bottom: 20px">
<template #header>
<div class="category-header">
<el-icon><Star /></el-icon>
<span>满意度指标</span>
</div>
</template>
<!-- 服务对象满意度指标 -->
<div class="indicator-item">
<h4 class="indicator-title">服务对象满意度指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.service_satisfaction.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.service_satisfaction.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.service_satisfaction.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.service_satisfaction.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.service_satisfaction.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<!-- 分值占比统计 -->
<el-alert
:title="`当前分值占比总和:${totalWeight}%`"
:type="totalWeight === 100 ? 'success' : totalWeight === 0 ? 'info' : 'warning'"
show-icon
:description="totalWeight === 100 ? '分值占比设置正确' : totalWeight === 0 ? '请填写绩效指标的分值占比' : '分值占比总和必须等于100%'"
:closable="false"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch } from 'vue'
import { TrendCharts, Money, Star } from '@element-plus/icons-vue'
interface PerformanceIndicator {
indicatorType: string
indicatorTypeName: string
goalName: string
targetValue: string
unit: string
weightPercentage: number
description: string
measurementMethod: string
dataSource: string
isQuantitative: boolean
sortOrder: number
status: string
}
export default defineComponent({
name: 'PerformanceIndicators',
components: {
TrendCharts,
Money,
Star
},
props: {
modelValue: {
type: Array,
default: () => []
},
readonly: {
type: Boolean,
default: false
}
},
emits: ['update:modelValue', 'change'],
setup(props, { emit }) {
// 创建默认指标对象
const createIndicator = (type: string, typeName: string): PerformanceIndicator => ({
indicatorType: type,
indicatorTypeName: typeName,
goalName: '',
targetValue: '',
unit: '',
weightPercentage: 0,
description: '',
measurementMethod: '',
dataSource: '',
isQuantitative: true,
sortOrder: 0,
status: '1'
})
// 8种固定的指标类型
const indicators = ref({
quantity: createIndicator('quantity', '数量指标'),
quality: createIndicator('quality', '质量指标'),
timeliness: createIndicator('timeliness', '时效指标'),
cost: createIndicator('cost', '成本指标'),
economic: createIndicator('economic', '经济效益指标'),
social: createIndicator('social', '社会效益指标'),
ecological: createIndicator('ecological', '生态效益指标'),
service_satisfaction: createIndicator('service_satisfaction', '服务对象满意度指标')
})
// 计算总分值占比
const totalWeight = computed(() => {
let total = 0
Object.values(indicators.value).forEach(indicator => {
total += indicator.weightPercentage || 0
})
return Math.round(total * 10) / 10
})
// 收集有效的指标数据
const collectValidIndicators = () => {
const validIndicators: PerformanceIndicator[] = []
let sortOrder = 0
Object.values(indicators.value).forEach(indicator => {
// 只有填写了目标名称的指标才被认为是有效的
if (indicator.goalName?.trim()) {
validIndicators.push({
...indicator,
sortOrder: sortOrder++
})
}
})
return validIndicators
}
// 发出变化事件
const emitChange = () => {
const validIndicators = collectValidIndicators()
emit('update:modelValue', validIndicators)
emit('change', validIndicators)
}
// 从传入的数据初始化指标
const initializeFromData = (data: any[]) => {
if (!data || data.length === 0) return
// 重置所有指标
indicators.value = {
quantity: createIndicator('quantity', '数量指标'),
quality: createIndicator('quality', '质量指标'),
timeliness: createIndicator('timeliness', '时效指标'),
cost: createIndicator('cost', '成本指标'),
economic: createIndicator('economic', '经济效益指标'),
social: createIndicator('social', '社会效益指标'),
ecological: createIndicator('ecological', '生态效益指标'),
service_satisfaction: createIndicator('service_satisfaction', '服务对象满意度指标')
}
// 将数据填入对应的指标类型
data.forEach(item => {
if (indicators.value[item.indicatorType]) {
indicators.value[item.indicatorType] = { ...item }
}
})
}
// 监听外部数据变化
watch(() => props.modelValue, (newValue) => {
initializeFromData(newValue)
}, { deep: true, immediate: true })
// 校验方法
const validate = () => {
const validIndicators = collectValidIndicators()
const errors = []
if (validIndicators.length === 0) {
errors.push('请至少填写一个绩效指标')
}
validIndicators.forEach((indicator, index) => {
if (!indicator.goalName?.trim()) {
errors.push(`第${index + 1}个指标未填写绩效目标名称`)
}
if (!indicator.targetValue?.trim()) {
errors.push(`第${index + 1}个指标未填写目标值`)
}
if (!indicator.weightPercentage || indicator.weightPercentage <= 0) {
errors.push(`第${index + 1}个指标未设置分值占比`)
}
})
if (Math.abs(totalWeight.value - 100) > 0.1) {
errors.push(`分值占比总和必须等于100%,当前为${totalWeight.value}%`)
}
if (errors.length > 0) {
return { valid: false, message: errors.join(';') }
}
return { valid: true }
}
// 获取所有有效指标数据
const getAllIndicators = () => {
return collectValidIndicators()
}
return {
indicators,
totalWeight,
emitChange,
validate,
getAllIndicators
}
}
})
</script>
<style scoped lang="scss">
.performance-indicators {
.category-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
font-size: 16px;
.el-icon {
font-size: 18px;
}
}
.indicator-item {
margin-bottom: 20px;
padding: 15px;
background-color: #fafafa;
border-radius: 6px;
border-left: 4px solid #409eff;
&:last-child {
margin-bottom: 0;
}
}
.indicator-title {
margin: 0 0 15px 0;
font-size: 14px;
font-weight: bold;
color: #606266;
}
.el-form-item {
margin-bottom: 10px;
}
}
</style>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+280
View File
@@ -0,0 +1,280 @@
<template>
<div class="app-container">
<el-card>
<template #header>
<div class="card-header">
<span>项目预算申报详情</span>
<el-tag :type="getStatusType(detail.approvalStatus)" class="ml10">
{{ getStatusText(detail.approvalStatus) }}
</el-tag>
</div>
</template>
<!-- 申报摘要 -->
<application-summary :application="detail" />
<!-- 审批流程 -->
<el-divider content-position="left">审批流程</el-divider>
<el-timeline>
<el-timeline-item
v-for="(item, index) in approvalTimeline"
:key="index"
:type="item.type"
:timestamp="item.timestamp"
>
<div class="timeline-content">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
<span class="timeline-user" v-if="item.user">{{ item.user }}</span>
</div>
</el-timeline-item>
</el-timeline>
<!-- 操作日志 -->
<el-divider content-position="left">操作日志</el-divider>
<el-table :data="operationLogs" style="width: 100%">
<el-table-column prop="operation" label="操作" width="120"/>
<el-table-column prop="operator" label="操作人" width="100"/>
<el-table-column prop="operateTime" label="操作时间" width="180">
<template #default="scope">
{{ parseTime(scope.row.operateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip/>
</el-table>
<!-- 操作按钮 -->
<div class="operation-buttons">
<el-button
v-if="detail.approvalStatus === 'draft'"
type="primary"
@click="handleEdit"
v-hasPermi="['cw:application:edit']"
>编辑</el-button>
<el-button
v-if="detail.approvalStatus === 'draft'"
type="success"
@click="handleSubmit"
v-hasPermi="['cw:application:submit']"
>提交</el-button>
<el-button
v-if="detail.approvalStatus === 'pending'"
type="warning"
@click="handleWithdraw"
v-hasPermi="['cw:application:withdraw']"
>撤回</el-button>
<el-button @click="goBack">返回</el-button>
</div>
</el-card>
</div>
</template>
<script setup name="CwApplicationDetail">
import { ref, getCurrentInstance, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getProjectApplication, submitApplication, withdrawApplication } from "@/api/cw/projectApplication";
import ApplicationSummary from "./summary.vue";
import { parseTime } from "@/utils/ruoyi";
console.log('=== detail.vue 组件开始加载 ===');
const { proxy } = getCurrentInstance();
const route = useRoute();
const router = useRouter();
const detail = ref({});
const approvalTimeline = ref([]);
const operationLogs = ref([]);
console.log('=== detail.vue 组件初始化完成 ===');
console.log('route对象:', route);
console.log('初始查询参数:', route.query);
/** 获取状态类型 */
function getStatusType(status) {
const statusMap = {
'draft': 'info',
'pending': 'warning',
'approving': 'primary',
'approved': 'success',
'rejected': 'danger',
'withdrawn': 'info',
'cancelled': 'info'
};
return statusMap[status] || 'info';
}
/** 获取状态文本 */
function getStatusText(status) {
const statusMap = {
'draft': '草稿',
'pending': '待审批',
'approving': '审批中',
'approved': '已通过',
'rejected': '已拒绝',
'withdrawn': '已撤回',
'cancelled': '已取消'
};
return statusMap[status] || '未知';
}
/** 编辑按钮操作 */
function handleEdit() {
router.push("/cw/application/form?id=" + detail.value.id);
}
/** 提交按钮操作 */
function handleSubmit() {
proxy.$modal.confirm('是否确认提交申报编号为"' + detail.value.applicationNo + '"的数据项?').then(function() {
return submitApplication(detail.value.id);
}).then(() => {
getDetail();
proxy.$modal.msgSuccess("提交成功");
}).catch(() => {});
}
/** 撤回按钮操作 */
function handleWithdraw() {
proxy.$modal.confirm('是否确认撤回申报编号为"' + detail.value.applicationNo + '"的数据项?').then(function() {
return withdrawApplication(detail.value.id);
}).then(() => {
getDetail();
proxy.$modal.msgSuccess("撤回成功");
}).catch(() => {});
}
/** 返回按钮操作 */
function goBack() {
router.push("/cw/application");
}
/** 获取详情数据 */
function getDetail() {
const id = route.query.id;
console.log('=== 开始获取详情数据 ===');
console.log('URL参数中的ID:', id);
console.log('完整query参数:', route.query);
if (id) {
console.log('开始调用API:getProjectApplication, ID:', id);
getProjectApplication(id).then(response => {
console.log('=== API调用成功 ===');
console.log('API响应:', response);
console.log('响应代码:', response.code);
console.log('响应数据:', response.data);
detail.value = response.data;
// 调试日志:查看返回的数据
console.log('=== 项目申报详情数据 ===');
console.log('完整数据:', response.data);
console.log('绩效指标数据:', response.data.performanceIndicators);
console.log('绩效指标数量:', response.data.performanceIndicators ? response.data.performanceIndicators.length : 0);
// 模拟审批流程数据
approvalTimeline.value = [
{
type: 'success',
timestamp: detail.value.createTime,
title: '申报提交',
description: '申报人提交预算申报',
user: detail.value.applicantName
}
];
if (detail.value.submitTime) {
approvalTimeline.value.push({
type: 'primary',
timestamp: detail.value.submitTime,
title: '提交审批',
description: '申报已提交,等待部门审批',
user: detail.value.applicantName
});
}
if (detail.value.approveTime && detail.value.approvalStatus === 'approved') {
approvalTimeline.value.push({
type: 'success',
timestamp: detail.value.approveTime,
title: '审批通过',
description: '所有审批节点已通过',
user: '系统管理员'
});
}
// 模拟操作日志数据
operationLogs.value = [
{
operation: '创建申报',
operator: detail.value.applicantName,
operateTime: detail.value.createTime,
remark: '创建项目预算申报'
}
];
if (detail.value.submitTime) {
operationLogs.value.push({
operation: '提交审批',
operator: detail.value.applicantName,
operateTime: detail.value.submitTime,
remark: '提交预算申报进入审批流程'
});
}
}).catch(error => {
console.error('=== API调用失败 ===');
console.error('错误信息:', error);
console.error('错误响应:', error.response);
proxy.$modal.msgError('获取申报详情失败:' + (error.message || '未知错误'));
});
} else {
console.error('=== URL参数错误 ===');
console.error('缺少ID参数,无法获取详情数据');
proxy.$modal.msgError('缺少申报ID参数');
}
}
onMounted(() => {
console.log('=== detail.vue 页面已挂载 ===');
console.log('当前路由:', route);
console.log('当前路由路径:', route.path);
console.log('当前路由查询参数:', route.query);
getDetail();
});
</script>
<style scoped>
.card-header {
display: flex;
align-items: center;
}
.ml10 {
margin-left: 10px;
}
.timeline-content h4 {
margin: 0 0 8px 0;
font-size: 16px;
color: #303133;
}
.timeline-content p {
margin: 0 0 8px 0;
color: #606266;
font-size: 14px;
}
.timeline-user {
color: #909399;
font-size: 12px;
}
.operation-buttons {
margin-top: 30px;
text-align: center;
}
.operation-buttons .el-button {
margin: 0 10px;
}
</style>
File diff suppressed because it is too large. Load diff
+784
View File
@@ -0,0 +1,784 @@
<template>
<div class="page-container">
<!-- 统计卡片区域 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="6">
<div class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ totalCount }}</div>
<div class="stat-label">总申报数</div>
</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Check /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ approvedCount }}</div>
<div class="stat-label">已通过</div>
</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ pendingCount }}</div>
<div class="stat-label">审批中</div>
</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><EditPen /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ draftCount }}</div>
<div class="stat-label">草稿</div>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 搜索区域 -->
<el-card class="button-card" v-show="showSearch">
<template #header>
<div class="card-header">
<span class="toolbar-title">项目申报查询</span>
</div>
</template>
<el-form :model="queryParams" ref="queryRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="申报编号" prop="applicationNo">
<el-input
v-model="queryParams.applicationNo"
placeholder="请输入申报编号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="queryParams.projectName"
placeholder="请输入项目名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目负责人" prop="principal">
<el-input
v-model="queryParams.principal"
placeholder="请输入项目负责人"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<el-option label="草稿" value="draft"/>
<el-option label="已提交" value="submitted"/>
<el-option label="审批中" value="approving"/>
<el-option label="已通过" value="approved"/>
<el-option label="已拒绝" value="rejected"/>
<el-option label="已退回" value="returned"/>
<el-option label="已取消" value="cancelled"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="预算年度" prop="year">
<el-date-picker
v-model="queryParams.year"
type="year"
placeholder="选择年份"
value-format="YYYY"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="18">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮区域 -->
<el-card class="button-card">
<template #header>
<div class="card-header">
<span class="toolbar-title">项目申报管理</span>
<div class="toolbar-actions">
<el-button
type="primary"
icon="Plus"
@click="handleAdd"
v-hasPermi="['cw:application:add']"
>新增申报</el-button>
<el-button
type="success"
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['cw:application:edit']"
>修改</el-button>
<el-button
type="danger"
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['cw:application:remove']"
>删除</el-button>
<el-button
type="info"
icon="Upload"
@click="handleSubmit"
:disabled="multiple"
v-hasPermi="['cw:application:submit']"
>提交审批</el-button>
<el-button
type="warning"
icon="Download"
@click="handleExport"
v-hasPermi="['cw:application:export']"
>导出</el-button>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</div>
</div>
</template>
</el-card>
<!-- 数据表格区域 -->
<el-card class="table-card">
<div class="table-container">
<el-table
v-loading="loading"
:data="applicationList"
@selection-change="handleSelectionChange"
class="scroll-table"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="申报编号" align="center" prop="applicationNo" min-width="140" />
<el-table-column label="项目名称" align="center" prop="projectName" min-width="180" show-overflow-tooltip />
<el-table-column label="项目负责人" align="center" prop="principal" min-width="100" />
<el-table-column label="所属部门" align="center" prop="departmentName" min-width="120" show-overflow-tooltip />
<el-table-column label="预算年度" align="center" prop="year" width="100" />
<el-table-column label="项目总投资" align="center" prop="totalInvestment" min-width="120">
<template #default="scope">
<span class="amount-text">{{ formatAmount(scope.row.totalInvestment) }}</span>
</template>
</el-table-column>
<el-table-column label="审批状态" align="center" prop="approvalStatus" width="100">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.approvalStatus)"
effect="light"
>
{{ getStatusText(scope.row.approvalStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" min-width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="280" class-name="small-padding fixed-width">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看详情" placement="top">
<el-button
type="primary"
icon="View"
circle
size="small"
@click="handleView(scope.row)"
v-hasPermi="['cw:application:query']"
/>
</el-tooltip>
<el-tooltip content="编辑" placement="top" v-if="scope.row.approvalStatus === 'draft'">
<el-button
type="success"
icon="Edit"
circle
size="small"
@click="handleUpdate(scope.row)"
v-hasPermi="['cw:application:edit']"
/>
</el-tooltip>
<el-tooltip content="提交审批" placement="top" v-if="scope.row.approvalStatus === 'draft'">
<el-button
type="warning"
icon="Upload"
circle
size="small"
@click="handleSubmit(scope.row)"
v-hasPermi="['cw:application:submit']"
/>
</el-tooltip>
<el-tooltip content="撤回" placement="top" v-if="scope.row.approvalStatus === 'submitted'">
<el-button
type="info"
icon="RefreshLeft"
circle
size="small"
@click="handleWithdraw(scope.row)"
v-hasPermi="['cw:application:withdraw']"
/>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="['draft', 'cancelled'].includes(scope.row.approvalStatus)">
<el-button
type="danger"
icon="Delete"
circle
size="small"
@click="handleDelete(scope.row)"
v-hasPermi="['cw:application:remove']"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</el-card>
<!-- 添加或修改项目申报对话框 -->
<el-dialog :title="title" v-model="open" width="1200px" append-to-body>
<ApplicationForm ref="applicationFormRef" :form-data="form" @submit="submitForm" />
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
<!-- 查看详情对话框 -->
<el-dialog title="项目申报详情" v-model="viewOpen" width="1200px" append-to-body>
<ApplicationSummary :application-id="currentId" />
<template #footer>
<div class="dialog-footer">
<el-button @click="viewOpen = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import { listProjectApplication, getProjectApplication, delProjectApplication, addProjectApplication, updateProjectApplication, submitApplication, getApplicationStatistics } from "@/api/cw/projectApplication"
import ApplicationForm from "./form.vue"
import ApplicationSummary from "./summary.vue"
export default {
name: "ProjectApplication",
components: {
ApplicationForm,
ApplicationSummary
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 项目申报表格数据
applicationList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否显示查看对话框
viewOpen: false,
// 当前查看的ID
currentId: null,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
applicationNo: null,
projectName: null,
principal: null,
approvalStatus: null,
year: null
},
// 表单参数
form: {},
// 表单校验
rules: {},
// 统计数据
totalCount: 0,
approvedCount: 0,
pendingCount: 0,
draftCount: 0
}
},
created() {
this.getList()
this.getStatistics()
},
methods: {
/** 查询项目申报列表 */
getList() {
this.loading = true
listProjectApplication(this.queryParams).then(response => {
this.applicationList = response.rows
this.total = response.total
this.loading = false
})
},
/** 获取统计数据 */
getStatistics() {
// 调用专门的统计API接口
const year = this.queryParams.year ? parseInt(this.queryParams.year) : null
getApplicationStatistics(year).then(response => {
const data = response.data
this.totalCount = data.totalCount || 0
this.approvedCount = data.approvedCount || 0
this.pendingCount = data.approvingCount || 0 // 注意:后端返回的是approvingCount
this.draftCount = data.draftCount || 0
}).catch(error => {
console.error('获取统计数据失败:', error)
// 如果API失败,使用本地计算作为后备方案
this.totalCount = this.applicationList.length
this.approvedCount = this.applicationList.filter(item => item.approvalStatus === 'approved').length
this.pendingCount = this.applicationList.filter(item => item.approvalStatus === 'approving').length
this.draftCount = this.applicationList.filter(item => item.approvalStatus === 'draft').length
})
},
/** 取消按钮 */
cancel() {
this.open = false
this.reset()
},
/** 表单重置 */
reset() {
this.form = {
id: null,
applicationNo: null,
projectName: null,
principal: null,
year: new Date().getFullYear(),
departmentId: null,
departmentName: null,
projectCategory: null,
implementationPeriod: null,
projectBackground: null,
projectObjective: null,
projectScope: null,
projectContent: null,
implementationPlan: null,
expectedResults: null,
riskAssessment: null,
totalPerformanceGoal: null,
currentYearGoal: null,
totalInvestment: 0,
totalMunicipalFiscal: 0,
totalSelfFunding: 0,
totalOtherFunding: 0,
currentYearTotal: 0,
currentYearMunicipal: 0,
currentYearSelf: 0,
currentYearOther: 0,
nextYearTotal: 0,
nextYearMunicipal: 0,
nextYearSelf: 0,
nextYearOther: 0,
futureYearTotal: 0,
futureYearMunicipal: 0,
futureYearSelf: 0,
futureYearOther: 0,
performanceIndicators: [],
approvalStatus: 'draft'
}
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
this.getStatistics() // 搜索时也更新统计数据
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryRef")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加项目申报"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id = row.id || this.ids
getProjectApplication(id).then(response => {
this.form = response.data
this.open = true
this.title = "修改项目申报"
})
},
/** 查看详情 */
handleView(row) {
this.currentId = row.id
this.viewOpen = true
},
/** 提交按钮 */
submitForm() {
this.$refs["applicationFormRef"].validate((valid) => {
if (valid) {
if (this.form.id != null) {
updateProjectApplication(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
this.getStatistics() // 修改后更新统计
})
} else {
addProjectApplication(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
this.getStatistics() // 新增后更新统计
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids
this.$modal.confirm('是否确认删除项目申报编号为"' + ids + '"的数据项?').then(function() {
return delProjectApplication(ids)
}).then(() => {
this.getList()
this.getStatistics() // 删除后更新统计
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 提交审批 */
handleSubmit(row) {
const applications = row?.applicationNo ? [row.applicationNo] : this.applicationList.filter(item => this.ids.includes(item.id)).map(item => item.applicationNo)
this.$modal.confirm('是否确认提交申报编号为"' + applications.join('", "') + '"的数据项?').then(() => {
return submitApplication(row?.id || this.ids)
}).then(() => {
this.getList()
this.getStatistics() // 提交后更新统计
this.$modal.msgSuccess("提交成功")
}).catch(() => {})
},
/** 撤回申请 */
handleWithdraw(row) {
this.$modal.confirm('是否确认撤回该申报?').then(() => {
// 调用撤回接口
this.$modal.msgSuccess("撤回成功")
this.getList()
this.getStatistics() // 撤回后更新统计
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('cw/application/export', {
...this.queryParams
}, `application_${new Date().getTime()}.xlsx`)
},
/** 格式化金额 */
formatAmount(amount) {
if (!amount) return '0.00'
return parseFloat(amount).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
},
/** 获取状态标签类型 */
getStatusTagType(status) {
const statusMap = {
draft: 'info',
submitted: 'warning',
approving: 'warning',
approved: 'success',
rejected: 'danger',
returned: 'warning',
cancelled: 'info'
}
return statusMap[status] || 'info'
},
/** 获取状态文本 */
getStatusText(status) {
const statusMap = {
draft: '草稿',
submitted: '已提交',
approving: '审批中',
approved: '已通过',
rejected: '已拒绝',
returned: '已退回',
cancelled: '已取消'
}
return statusMap[status] || '未知'
}
}
}
</script>
<style scoped lang="scss">
/* 继承后勤管理模块的样式风格 */
.page-container {
padding: 20px;
/* 统计卡片样式 */
.stats-row {
margin-bottom: 20px;
}
.stat-card {
border-radius: 12px;
transition: all 0.3s ease;
height: 90px;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
padding: 5px 16px;
height: 100%;
box-sizing: border-box;
.stat-icon {
font-size: 28px;
margin-right: 15px;
opacity: 0.9;
}
.stat-info {
flex: 1;
.stat-value {
font-size: 24px;
font-weight: bold;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
opacity: 0.8;
line-height: 1;
}
}
}
&.stat-card-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
&.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
}
/* 卡片样式 */
.button-card, .table-card {
margin-bottom: 20px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
:deep(.el-card__body) {
padding: 20px;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
&::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 12px;
}
}
.toolbar-actions {
display: flex;
gap: 12px;
align-items: center;
}
}
/* 表格样式 */
.table-container {
width: 100%;
overflow-x: auto;
}
.scroll-table {
width: 100%;
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
white-space: nowrap;
.el-button {
width: 32px !important;
height: 32px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
}
/* 金额文本样式 */
.amount-text {
font-weight: 600;
color: #e74c3c;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 0 0 0;
margin-top: 16px;
}
/* 对话框样式 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-container {
padding: 10px;
.stats-row {
.stat-card {
margin-bottom: 10px;
}
}
.toolbar-actions {
flex-direction: column;
gap: 8px !important;
}
}
}
</style>
+505
View File
@@ -0,0 +1,505 @@
<template>
<div class="application-summary" v-loading="loading">
<!-- 基本信息 -->
<el-card class="summary-card">
<template #header>
<div class="card-header">
<span>项目基本信息</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="申报编号">
{{ currentApplication.applicationNo || '系统自动生成' }}
</el-descriptions-item>
<el-descriptions-item label="项目名称">
{{ currentApplication.projectName }}
</el-descriptions-item>
<el-descriptions-item label="项目负责人">
{{ currentApplication.principal }}
</el-descriptions-item>
<el-descriptions-item label="联系电话">
{{ currentApplication.contactPhone }}
</el-descriptions-item>
<el-descriptions-item label="预算年度">
{{ currentApplication.year }}
</el-descriptions-item>
<el-descriptions-item label="项目周期">
{{ formatDateRange(currentApplication.startDate, currentApplication.endDate) }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 项目特征 -->
<el-card class="summary-card">
<template #header>
<div class="card-header">
<span>项目特征</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="8">
<el-tag :type="currentApplication.targetSme ? 'success' : 'info'">
小微企业发展: {{ currentApplication.targetSme ? '是' : '否' }}
</el-tag>
</el-col>
<el-col :span="8">
<el-tag :type="currentApplication.targetMicroSme ? 'success' : 'info'">
微小企业成长: {{ currentApplication.targetMicroSme ? '是' : '否' }}
</el-tag>
</el-col>
<el-col :span="8">
<el-tag :type="currentApplication.importEquipment ? 'success' : 'info'">
进口设备: {{ currentApplication.importEquipment ? '是' : '否' }}
</el-tag>
</el-col>
</el-row>
</el-card>
<!-- 项目内容 -->
<el-card class="summary-card" v-if="hasProjectContent">
<template #header>
<div class="card-header">
<span>项目内容</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="申报理由" v-if="currentApplication.applicationReason">
<div class="content-text">{{ currentApplication.applicationReason }}</div>
</el-descriptions-item>
<el-descriptions-item label="建设内容" v-if="currentApplication.constructionContent">
<div class="content-text">{{ currentApplication.constructionContent }}</div>
</el-descriptions-item>
<el-descriptions-item label="总体绩效目标" v-if="currentApplication.totalPerformanceGoal">
<div class="content-text">{{ currentApplication.totalPerformanceGoal }}</div>
</el-descriptions-item>
<el-descriptions-item label="当年完成目标" v-if="currentApplication.currentYearGoal">
<div class="content-text">{{ currentApplication.currentYearGoal }}</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 资金预算汇总 -->
<el-card class="summary-card">
<template #header>
<div class="card-header">
<span>资金预算汇总</span>
<el-tag type="warning" size="small">单位:万元</el-tag>
</div>
</template>
<!-- 总体预算 -->
<div class="budget-section">
<h4>总体预算</h4>
<el-row :gutter="20" class="budget-row">
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">总投资</div>
<div class="budget-value highlight">{{ formatAmount(currentApplication.totalInvestment) }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">市财政资金</div>
<div class="budget-value">{{ formatAmount(currentApplication.totalMunicipalFiscal) }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">单位自筹</div>
<div class="budget-value">{{ formatAmount(currentApplication.totalSelfFunding) }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">其他资金</div>
<div class="budget-value">{{ formatAmount(currentApplication.totalOtherFunding) }}</div>
</div>
</el-col>
</el-row>
</div>
<!-- 分年度预算 -->
<div class="budget-section">
<h4>分年度预算明细</h4>
<el-table :data="budgetBreakdown" border>
<el-table-column prop="year" label="年度" width="120" align="center"/>
<el-table-column prop="total" label="总投资" align="right">
<template #default="scope">
{{ formatAmount(scope.row.total) }}
</template>
</el-table-column>
<el-table-column prop="municipal" label="市财政资金" align="right">
<template #default="scope">
{{ formatAmount(scope.row.municipal) }}
</template>
</el-table-column>
<el-table-column prop="self" label="单位自筹" align="right">
<template #default="scope">
{{ formatAmount(scope.row.self) }}
</template>
</el-table-column>
<el-table-column prop="other" label="其他资金" align="right">
<template #default="scope">
{{ formatAmount(scope.row.other) }}
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<!-- 绩效指标设置 -->
<el-card class="summary-card" v-if="hasPerformanceIndicators">
<template #header>
<div class="card-header">
<span>绩效指标设置</span>
<el-tag type="warning" size="small">分值占比总和:{{ performanceIndicatorTotal }}%</el-tag>
</div>
</template>
<el-table :data="validPerformanceIndicators" border style="width: 100%">
<el-table-column label="指标类型" width="120" align="center">
<template #default="scope">
<el-tag>{{ scope.row.indicatorTypeName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绩效目标名称" min-width="200">
<template #default="scope">
<div class="content-text">{{ scope.row.goalName }}</div>
</template>
</el-table-column>
<el-table-column label="目标值" width="120" align="center">
<template #default="scope">
<div class="content-text">{{ scope.row.targetValue }}</div>
</template>
</el-table-column>
<el-table-column label="分值占比" width="100" align="center">
<template #default="scope">
<el-tag type="primary">{{ parseFloat(scope.row.weightPercentage) || 0 }}%</el-tag>
</template>
</el-table-column>
<el-table-column label="详细说明" min-width="150">
<template #default="scope">
<div class="content-text" v-if="scope.row.description">{{ scope.row.description }}</div>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 备注信息 -->
<el-card class="summary-card" v-if="currentApplication.remark">
<template #header>
<div class="card-header">
<span>备注信息</span>
</div>
</template>
<div class="content-text">{{ currentApplication.remark }}</div>
</el-card>
</div>
</template>
<script setup>
import { computed, ref, watch, onMounted } from 'vue';
import { getProjectApplication } from '@/api/cw/projectApplication';
defineOptions({
name: 'ApplicationSummary'
});
const props = defineProps({
application: {
type: Object,
default: () => ({})
},
applicationId: {
type: [String, Number],
default: null
}
});
// 内部数据状态
const internalApplication = ref({});
const loading = ref(false);
// 计算属性:优先使用传入的application,如果没有则使用内部获取的数据
const currentApplication = computed(() => {
// 如果直接传入了application对象且不为空,则使用它
if (props.application && Object.keys(props.application).length > 0) {
console.log('=== 使用传入的application对象 ===');
console.log('application数据:', props.application);
return props.application;
}
// 否则使用内部获取的数据
console.log('=== 使用内部获取的application数据 ===');
console.log('internalApplication数据:', internalApplication.value);
return internalApplication.value;
});
// 获取申报详情数据
const fetchApplicationData = async (id) => {
if (!id) return;
console.log('=== ApplicationSummary 开始获取数据 ===');
console.log('applicationId:', id);
loading.value = true;
try {
const response = await getProjectApplication(id);
console.log('=== ApplicationSummary API调用成功 ===');
console.log('API响应:', response);
console.log('响应数据:', response.data);
console.log('响应数据类型:', typeof response.data);
// 特别检查绩效指标字段
if (response.data) {
console.log('=== API响应中的绩效指标检查 ===');
console.log('response.data.performanceIndicators:', response.data.performanceIndicators);
console.log('performanceIndicators类型:', typeof response.data.performanceIndicators);
console.log('是否为数组:', Array.isArray(response.data.performanceIndicators));
// 检查所有可能的绩效指标字段名
console.log('可能的字段名检查:');
console.log('- performanceIndicators:', response.data.performanceIndicators);
console.log('- performance_indicators:', response.data.performance_indicators);
console.log('- indicators:', response.data.indicators);
console.log('- performanceIndicatorList:', response.data.performanceIndicatorList);
// 列出所有字段
console.log('响应数据的所有字段:', Object.keys(response.data));
}
internalApplication.value = response.data || {};
} catch (error) {
console.error('=== ApplicationSummary API调用失败 ===');
console.error('错误信息:', error);
internalApplication.value = {};
} finally {
loading.value = false;
}
};
// 监听applicationId变化
watch(() => props.applicationId, (newId) => {
if (newId && (!props.application || Object.keys(props.application).length === 0)) {
fetchApplicationData(newId);
}
}, { immediate: true });
// 组件挂载时检查是否需要获取数据
onMounted(() => {
console.log('=== ApplicationSummary 组件挂载 ===');
console.log('props.application:', props.application);
console.log('props.applicationId:', props.applicationId);
// 如果没有传入application但有applicationId,则获取数据
if (props.applicationId && (!props.application || Object.keys(props.application).length === 0)) {
fetchApplicationData(props.applicationId);
}
});
// 判断是否有项目内容
const hasProjectContent = computed(() => {
const app = currentApplication.value;
return app.applicationReason ||
app.constructionContent ||
app.totalPerformanceGoal ||
app.currentYearGoal;
});
// 判断是否有绩效指标
const hasPerformanceIndicators = computed(() => {
const app = currentApplication.value;
console.log('=== 绩效指标详细检查 ===');
console.log('完整app对象:', app);
console.log('app.performanceIndicators类型:', typeof app.performanceIndicators);
console.log('app.performanceIndicators值:', app.performanceIndicators);
console.log('是否为数组:', Array.isArray(app.performanceIndicators));
if (app.performanceIndicators) {
console.log('绩效指标数组长度:', app.performanceIndicators.length);
console.log('绩效指标数组内容:', JSON.stringify(app.performanceIndicators, null, 2));
} else {
console.log('绩效指标数据为空或未定义');
}
const hasIndicators = app.performanceIndicators && app.performanceIndicators.length > 0;
console.log('最终判断 - 是否有绩效指标:', hasIndicators);
return hasIndicators;
});
// 计算绩效指标总分值占比
const performanceIndicatorTotal = computed(() => {
const app = currentApplication.value;
if (!app.performanceIndicators || app.performanceIndicators.length === 0) {
return 0;
}
const total = app.performanceIndicators.reduce((sum, indicator) => {
const weight = parseFloat(indicator.weightPercentage) || 0;
return sum + weight;
}, 0);
return Math.round(total * 100) / 100; // 保留两位小数
});
// 过滤掉无效的绩效指标(分值占比为0的)
const validPerformanceIndicators = computed(() => {
const app = currentApplication.value;
console.log('=== validPerformanceIndicators 计算 ===');
console.log('app.performanceIndicators:', app.performanceIndicators);
if (!app.performanceIndicators) {
console.log('没有绩效指标数据,返回空数组');
return [];
}
const validIndicators = app.performanceIndicators.filter(indicator => {
const weight = parseFloat(indicator.weightPercentage) || 0;
console.log(`指标 ${indicator.goalName}: 权重=${indicator.weightPercentage}, 解析后权重=${weight}, 是否有效=${weight > 0}`);
return weight > 0;
});
console.log('有效绩效指标数量:', validIndicators.length);
console.log('有效绩效指标详情:', validIndicators);
return validIndicators;
});
// 分年度预算明细
const budgetBreakdown = computed(() => {
const app = currentApplication.value;
const currentYear = app.year || new Date().getFullYear();
console.log('=== 预算数据检查 ===');
console.log('application数据:', app);
console.log('当年预算字段:', {
currentYearTotal: app.currentYearTotal,
currentYearMunicipal: app.currentYearMunicipal,
currentYearSelf: app.currentYearSelf,
currentYearOther: app.currentYearOther
});
return [
{
year: `${currentYear}年`,
total: app.currentYearTotal || 0,
municipal: app.currentYearMunicipal || 0,
self: app.currentYearSelf || 0,
other: app.currentYearOther || 0
},
{
year: `${parseInt(currentYear) + 1}年`,
total: app.nextYearTotal || 0,
municipal: app.nextYearMunicipal || 0,
self: app.nextYearSelf || 0,
other: app.nextYearOther || 0
},
{
year: '以后年度',
total: app.futureYearTotal || 0,
municipal: app.futureYearMunicipal || 0,
self: app.futureYearSelf || 0,
other: app.futureYearOther || 0
}
].filter(item => item.total > 0); // 只显示有预算的年度
});
// 格式化金额
const formatAmount = (amount) => {
if (!amount || amount === 0) return '0.00';
return Number(amount).toFixed(2);
};
// 格式化日期范围
const formatDateRange = (startDate, endDate) => {
if (!startDate && !endDate) return '未设置';
if (!startDate) return `截止 ${endDate}`;
if (!endDate) return `${startDate} 开始`;
return `${startDate} 至 ${endDate}`;
};
</script>
<style scoped>
.application-summary {
padding: 0;
}
.summary-card {
margin-bottom: 20px;
}
.summary-card:last-child {
margin-bottom: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
}
.content-text {
line-height: 1.6;
word-break: break-word;
white-space: pre-wrap;
}
.budget-section {
margin-bottom: 24px;
}
.budget-section:last-child {
margin-bottom: 0;
}
.budget-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
border-left: 4px solid #409eff;
padding-left: 12px;
}
.budget-row {
margin-bottom: 16px;
}
.budget-item {
text-align: center;
padding: 16px;
border: 1px solid #ebeef5;
border-radius: 4px;
background-color: #fafafa;
}
.budget-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.budget-value {
font-size: 18px;
font-weight: 600;
color: #333;
}
.budget-value.highlight {
color: #409eff;
font-size: 20px;
}
.el-tag {
margin-right: 8px;
}
:deep(.el-descriptions__label) {
width: 120px;
font-weight: 500;
}
:deep(.el-table td, .el-table th) {
text-align: center;
}
:deep(.el-table .cell) {
padding: 0 8px;
}
</style>
+869
View File
@@ -0,0 +1,869 @@
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="申报编号" prop="applicationNo">
<el-input v-model="queryParams.applicationNo" placeholder="请输入申报编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申报人" prop="applicantName">
<el-input v-model="queryParams.applicantName" placeholder="请输入申报人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<template v-if="cw_approval_status && cw_approval_status.length > 0">
<el-option v-for="dict in cw_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</template>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="审批人" prop="approverName">
<el-input v-model="queryParams.approverName" placeholder="请输入审批人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="提交时间">
<el-date-picker
v-model="daterangeSubmitTime"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleQuery"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 快捷操作按钮 -->
<el-row :gutter="10" class="mb-[10px]">
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showPendingApprovals">
<el-icon class="quick-icon"><Clock /></el-icon>
<span>待我审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showMyApprovals">
<el-icon class="quick-icon"><User /></el-icon>
<span>我已审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showDepartmentApprovals">
<el-icon class="quick-icon"><OfficeBuilding /></el-icon>
<span>部门审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showApprovalStatistics">
<el-icon class="quick-icon"><DataAnalysis /></el-icon>
<span>审批统计</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 表格区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Check"
:disabled="multiple"
@click="handleBatchApprove"
v-hasPermi="['cw:application:approve']"
>批量通过</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Close"
:disabled="multiple"
@click="handleBatchReject"
v-hasPermi="['cw:application:approve']"
>批量拒绝</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['cw:approvalRecord:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch="showSearch" @update:showSearch="showSearch = $event" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table
v-loading="loading"
border
:data="approvalList"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
style="width: 100%"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="申报编号" align="center" prop="applicationNo" width="140" show-overflow-tooltip />
<el-table-column label="项目名称" align="center" prop="projectName" min-width="200" show-overflow-tooltip />
<el-table-column label="申报人" align="center" prop="applicantName" width="100" />
<el-table-column label="申报部门" align="center" prop="deptName" width="120" show-overflow-tooltip />
<el-table-column label="总投资(万元)" align="center" prop="totalInvestment" width="120">
<template #default="scope">
<span class="amount-text">{{ formatAmount(scope.row.totalInvestment) }}</span>
</template>
</el-table-column>
<el-table-column label="当前审批人" align="center" prop="currentApproverName" width="120" />
<el-table-column label="审批状态" align="center" prop="approvalStatus" width="100">
<template #default="scope">
<dict-tag :options="cw_approval_status" :value="scope.row.approvalStatus" v-if="cw_approval_status && cw_approval_status.length > 0" />
</template>
</el-table-column>
<el-table-column label="提交时间" align="center" prop="submitTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.submitTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="审批时间" align="center" prop="approvalTime" width="180">
<template #default="scope">
<span>{{ scope.row.approvalTime ? parseTime(scope.row.approvalTime, '{y}-{m}-{d} {h}:{i}') : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleView(scope.row)"></el-button>
</el-tooltip>
<!-- 待审批状态可以进行审批操作 -->
<template v-if="scope.row.approvalStatus === 'submitted' || scope.row.approvalStatus === 'approving'">
<el-tooltip content="通过" placement="top">
<el-button
link
type="success"
icon="Check"
@click="handleApprove(scope.row)"
v-hasPermi="['cw:application:approve']"
></el-button>
</el-tooltip>
<el-tooltip content="拒绝" placement="top">
<el-button
link
type="danger"
icon="Close"
@click="handleReject(scope.row)"
v-hasPermi="['cw:application:approve']"
></el-button>
</el-tooltip>
<el-tooltip content="退回" placement="top">
<el-button
link
type="warning"
icon="RefreshLeft"
@click="handleReturn(scope.row)"
v-hasPermi="['cw:application:approve']"
></el-button>
</el-tooltip>
</template>
<!-- 所有状态都可以查看审批记录 -->
<el-tooltip content="审批记录" placement="top">
<el-button link type="primary" icon="Document" @click="handleApprovalHistory(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page="queryParams.pageNum"
:limit="queryParams.pageSize"
@update:page="queryParams.pageNum = $event"
@update:limit="queryParams.pageSize = $event"
@pagination="getList"
/>
</el-card>
<!-- 审批对话框 -->
<el-dialog
v-model="approvalDialog.visible"
:title="approvalDialog.title"
width="800px"
append-to-body
@close="closeApprovalDialog"
>
<el-form ref="approvalFormRef" :model="approvalForm" :rules="approvalRules" label-width="120px">
<!-- 申报信息展示 -->
<el-form-item label="申报信息">
<el-descriptions :column="2" border>
<el-descriptions-item label="申报编号">{{ currentApplication.applicationNo }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ currentApplication.projectName }}</el-descriptions-item>
<el-descriptions-item label="申报人">{{ currentApplication.applicantName }}</el-descriptions-item>
<el-descriptions-item label="申报部门">{{ currentApplication.deptName }}</el-descriptions-item>
<el-descriptions-item label="总投资">{{ formatAmount(currentApplication.totalInvestment) }} 万元</el-descriptions-item>
<el-descriptions-item label="预算年度">{{ currentApplication.year }}</el-descriptions-item>
</el-descriptions>
</el-form-item>
<!-- 审批决定 -->
<el-form-item label="审批决定" prop="approvalResult">
<el-radio-group v-model="approvalForm.approvalResult">
<el-radio value="approved">
<el-icon class="approve-icon"><Check /></el-icon>
通过
</el-radio>
<el-radio value="rejected">
<el-icon class="reject-icon"><Close /></el-icon>
拒绝
</el-radio>
<el-radio value="returned">
<el-icon class="return-icon"><RefreshLeft /></el-icon>
退回
</el-radio>
</el-radio-group>
</el-form-item>
<!-- 审批意见 -->
<el-form-item label="审批意见" prop="approvalComment">
<el-input
v-model="approvalForm.approvalComment"
type="textarea"
:rows="4"
placeholder="请输入审批意见(必填)"
maxlength="500"
show-word-limit
/>
</el-form-item>
<!-- 下一审批人(如果需要) -->
<el-form-item label="下一审批人" prop="nextApprover" v-if="showNextApprover">
<el-select v-model="approvalForm.nextApprover" placeholder="请选择下一审批人" style="width: 100%">
<el-option
v-for="approver in approverList"
:key="approver.userId"
:label="approver.userName"
:value="approver.userId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeApprovalDialog">取消</el-button>
<el-button type="primary" @click="submitApproval" :loading="approvalSubmitting">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog
v-model="detailDialog.visible"
:title="detailDialog.title"
width="90%"
:close-on-click-modal="false"
>
<application-summary :application="currentApplication" />
<template #footer>
<div class="dialog-footer">
<el-button
v-if="canApprove(currentApplication)"
type="primary"
@click="approveFromDetail"
>审批</el-button>
<el-button @click="detailDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 审批记录对话框 -->
<el-dialog
v-model="historyDialog.visible"
title="审批记录"
width="900px"
append-to-body
>
<el-timeline>
<el-timeline-item
v-for="(record, index) in approvalHistory"
:key="index"
:icon="getHistoryIcon(record.approvalResult)"
:type="getHistoryType(record.approvalResult)"
:timestamp="parseTime(record.approvalTime, '{y}-{m}-{d} {h}:{i}')"
placement="top"
>
<el-card shadow="hover">
<div class="history-header">
<span class="approver-name">{{ record.approverName }}</span>
<el-tag :type="getStatusTagType(record.approvalResult)">
{{ getApprovalResultText(record.approvalResult) }}
</el-tag>
</div>
<div class="history-content" v-if="record.approvalComment">
<p>{{ record.approvalComment }}</p>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<template #footer>
<div class="dialog-footer">
<el-button @click="historyDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {
listApprovalRecord,
getApprovalRecord,
approveApplication,
batchApproveApplication,
getApprovalHistory,
getPendingApprovals,
getMyApprovals,
getDepartmentApprovals,
getAvailableApprovers,
exportApprovalRecord
} from "@/api/cw/approvalRecord";
import { useDict } from "@/utils/dict";
import ApplicationSummary from "../application/summary.vue";
export default {
name: 'CwApprovalRecord',
components: {
ApplicationSummary
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 审批列表
approvalList: [],
// 时间范围
daterangeSubmitTime: [],
// 动画配置
animate: {
searchAnimate: {
enter: 'animate__animated animate__fadeIn',
leave: 'animate__animated animate__fadeOut'
}
},
// 字典数据
cw_approval_status: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
applicationNo: '',
projectName: '',
applicantName: '',
approvalStatus: '',
approverName: '',
submitTimeBegin: '',
submitTimeEnd: ''
},
// 审批对话框
approvalDialog: {
visible: false,
title: ''
},
// 详情对话框
detailDialog: {
visible: false,
title: ''
},
// 历史记录对话框
historyDialog: {
visible: false
},
// 当前申报信息
currentApplication: {},
// 审批表单
approvalForm: {
applicationId: null,
approvalResult: '',
approvalComment: '',
nextApprover: null
},
// 审批表单验证
approvalRules: {
approvalResult: [
{ required: true, message: '请选择审批结果', trigger: 'change' }
],
approvalComment: [
{ required: true, message: '请输入审批意见', trigger: 'blur' },
{ min: 5, max: 500, message: '审批意见长度在 5 到 500 个字符', trigger: 'blur' }
]
},
// 审批提交状态
approvalSubmitting: false,
// 审批人列表
approverList: [],
// 审批历史
approvalHistory: []
};
},
computed: {
// 是否显示下一审批人选择
showNextApprover() {
return this.approvalForm.approvalResult === 'approved';
}
},
created() {
this.loadDictData();
this.getList();
},
methods: {
/** 加载字典数据 */
async loadDictData() {
try {
const dictData = useDict('cw_approval_status');
// 监听字典数据变化
this.$watch(() => dictData.cw_approval_status, (newVal) => {
this.cw_approval_status = newVal || [];
}, { immediate: true });
} catch (error) {
console.error('加载审批状态字典数据失败:', error);
}
},
/** 查询审批列表 */
getList() {
this.loading = true;
// 处理时间范围
this.queryParams.submitTimeBegin = this.daterangeSubmitTime[0] || '';
this.queryParams.submitTimeEnd = this.daterangeSubmitTime[1] || '';
listApprovalRecord(this.queryParams).then(response => {
this.approvalList = response.rows;
this.total = response.total;
this.loading = false;
}).catch(error => {
console.error('获取审批列表失败:', error);
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.daterangeSubmitTime = [];
this.$refs.queryFormRef.resetFields();
this.handleQuery();
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 快捷操作 */
showPendingApprovals() {
this.queryParams = {
...this.queryParams,
approvalStatus: 'submitted',
currentApprover: 'current'
};
this.handleQuery();
},
showMyApprovals() {
this.queryParams = {
...this.queryParams,
approverName: 'current'
};
this.handleQuery();
},
showDepartmentApprovals() {
this.queryParams = {
...this.queryParams,
deptId: 'current'
};
this.handleQuery();
},
showApprovalStatistics() {
this.$router.push('/cw/approvalRecord/statistics');
},
/** 查看详情 */
handleView(row) {
const id = row.id || row.applicationId;
getApprovalRecord(id).then(response => {
this.currentApplication = response.data;
this.detailDialog.visible = true;
this.detailDialog.title = "申报详情";
}).catch(error => {
console.error('获取详情失败:', error);
this.$modal.msgError('获取详情失败');
});
},
/** 从详情页审批 */
approveFromDetail() {
this.detailDialog.visible = false;
this.handleApprove(this.currentApplication);
},
/** 单个审批通过 */
handleApprove(row) {
this.openApprovalDialog('approved', row, '审批通过');
},
/** 单个审批拒绝 */
handleReject(row) {
this.openApprovalDialog('rejected', row, '审批拒绝');
},
/** 单个审批退回 */
handleReturn(row) {
this.openApprovalDialog('returned', row, '审批退回');
},
/** 批量审批通过 */
handleBatchApprove() {
if (this.ids.length === 0) {
this.$modal.msgWarning('请选择要审批的记录');
return;
}
this.openBatchApprovalDialog('approved', '批量审批通过');
},
/** 批量审批拒绝 */
handleBatchReject() {
if (this.ids.length === 0) {
this.$modal.msgWarning('请选择要审批的记录');
return;
}
this.openBatchApprovalDialog('rejected', '批量审批拒绝');
},
/** 打开审批对话框 */
openApprovalDialog(result, row, title) {
this.currentApplication = row;
this.approvalForm = {
applicationId: row.id,
approvalResult: result,
approvalComment: '',
nextApprover: null
};
// 如果是通过状态,加载可选审批人
if (result === 'approved') {
this.loadAvailableApprovers(row.id);
}
this.approvalDialog.visible = true;
this.approvalDialog.title = title;
},
/** 打开批量审批对话框 */
openBatchApprovalDialog(result, title) {
this.approvalForm = {
applicationIds: this.ids,
approvalResult: result,
approvalComment: '',
nextApprover: null
};
this.approvalDialog.visible = true;
this.approvalDialog.title = title;
},
/** 加载可选审批人 */
loadAvailableApprovers(applicationId) {
getAvailableApprovers(applicationId).then(response => {
this.approverList = response.data || [];
}).catch(error => {
console.error('获取审批人列表失败:', error);
this.approverList = [];
});
},
/** 关闭审批对话框 */
closeApprovalDialog() {
this.approvalDialog.visible = false;
this.approvalForm = {
applicationId: null,
approvalResult: '',
approvalComment: '',
nextApprover: null
};
this.approverList = [];
if (this.$refs.approvalFormRef) {
this.$refs.approvalFormRef.resetFields();
}
},
/** 提交审批 */
submitApproval() {
this.$refs.approvalFormRef.validate(valid => {
if (valid) {
this.approvalSubmitting = true;
const apiCall = this.approvalForm.applicationIds
? batchApproveApplication(this.approvalForm)
: approveApplication(this.approvalForm);
apiCall.then(response => {
this.$modal.msgSuccess("审批成功");
this.closeApprovalDialog();
this.getList();
}).catch(error => {
console.error('审批失败:', error);
this.$modal.msgError('审批失败');
}).finally(() => {
this.approvalSubmitting = false;
});
}
});
},
/** 查看审批历史 */
handleApprovalHistory(row) {
getApprovalHistory(row.id || row.applicationId).then(response => {
this.approvalHistory = response.data || [];
this.historyDialog.visible = true;
}).catch(error => {
console.error('获取审批历史失败:', error);
this.$modal.msgError('获取审批历史失败');
// 显示模拟数据
this.approvalHistory = [
{
approverName: '系统',
approvalResult: 'submitted',
approvalComment: '申报材料已提交,等待审批',
approvalTime: new Date().toISOString(),
stepName: '申报提交',
stepOrder: 1
}
];
this.historyDialog.visible = true;
});
},
/** 导出 */
handleExport() {
exportApprovalRecord(this.queryParams).then(() => {
this.$modal.msgSuccess('导出成功');
}).catch(error => {
console.error('导出失败:', error);
this.$modal.msgError('导出失败');
});
},
/** 判断是否可以审批 */
canApprove(row) {
return row.approvalStatus === 'submitted' || row.approvalStatus === 'approving';
},
/** 格式化金额 */
formatAmount(amount) {
return amount ? amount.toFixed(2) : '0.00';
},
/** 获取历史记录图标 */
getHistoryIcon(result) {
const iconMap = {
'submitted': 'Upload',
'approved': 'Check',
'rejected': 'Close',
'returned': 'RefreshLeft'
};
return iconMap[result] || 'Document';
},
/** 获取历史记录类型 */
getHistoryType(result) {
const typeMap = {
'submitted': 'primary',
'approved': 'success',
'rejected': 'danger',
'returned': 'warning'
};
return typeMap[result] || 'info';
},
/** 获取状态标签类型 */
getStatusTagType(status) {
const statusMap = {
'submitted': 'warning',
'approved': 'success',
'rejected': 'danger',
'returned': 'warning'
};
return statusMap[status] || 'info';
},
/** 获取审批结果文本 */
getApprovalResultText(result) {
const textMap = {
'submitted': '已提交',
'approved': '通过',
'rejected': '拒绝',
'returned': '退回'
};
return textMap[result] || result;
}
}
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: calc(100vh - 84px);
}
// 快捷操作卡片
.quick-action-card {
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid #e4e7ed;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #409EFF;
}
.quick-action {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
.quick-icon {
font-size: 28px;
color: #409EFF;
margin-bottom: 8px;
}
span {
font-size: 14px;
color: #606266;
font-weight: 500;
}
}
}
// 表格卡片
.button-card.table-card {
border-radius: 8px;
border: 1px solid #e4e7ed;
}
// 金额文本
.amount-text {
font-weight: 600;
color: #E6A23C;
}
// 审批对话框样式
.approve-icon {
color: #67C23A;
margin-right: 4px;
}
.reject-icon {
color: #F56C6C;
margin-right: 4px;
}
.return-icon {
color: #E6A23C;
margin-right: 4px;
}
// 审批历史样式
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.approver-name {
font-weight: 600;
color: #2c3e50;
}
}
.history-content {
color: #606266;
line-height: 1.6;
p {
margin: 0;
}
}
// 响应式设计
@media (max-width: 768px) {
.quick-action {
padding: 15px;
.quick-icon {
font-size: 24px;
}
span {
font-size: 12px;
}
}
}
</style>
+319
View File
@@ -0,0 +1,319 @@
<template>
<div class="config-detail">
<!-- 基本信息 -->
<el-card class="detail-card mb-4">
<template #header>
<div class="card-header">
<el-icon><InfoFilled /></el-icon>
<span>基本信息</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="配置名称">
{{ config.configName }}
</el-descriptions-item>
<el-descriptions-item label="审批类型">
<el-tag :type="getApprovalTypeTag(config.approvalType)">
{{ getApprovalTypeText(config.approvalType) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="适用范围">
{{ getScopeText(config.applicableScope) }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="config.status === '1' ? 'success' : 'danger'">
{{ config.status === '1' ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="排序">
{{ config.orderNum }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ parseTime(config.createTime, '{y}-{m}-{d} {h}:{i}') }}
</el-descriptions-item>
</el-descriptions>
<div v-if="config.description" class="mt-4">
<h4 class="section-title">配置描述</h4>
<p class="description-text">{{ config.description }}</p>
</div>
</el-card>
<!-- 审批流程 -->
<el-card class="detail-card" v-if="config.steps && config.steps.length > 0">
<template #header>
<div class="card-header">
<el-icon><Share /></el-icon>
<span>审批流程</span>
<el-tag type="info" size="small">共 {{ config.steps.length }} 个步骤</el-tag>
</div>
</template>
<el-steps :active="config.steps.length" finish-status="success" align-center>
<el-step
v-for="(step, index) in config.steps"
:key="index"
:title="step.stepName"
:description="`${getApprovalModeText(step.approvalMode)} | ${step.timeoutHours}小时`"
/>
</el-steps>
<div class="steps-detail mt-6">
<div
v-for="(step, index) in config.steps"
:key="index"
class="step-detail-item"
>
<el-card shadow="hover" class="step-detail-card">
<template #header>
<div class="step-header">
<span class="step-number">步骤 {{ index + 1 }}</span>
<span class="step-name">{{ step.stepName }}</span>
<el-tag :type="getModeTagType(step.approvalMode)" size="small">
{{ getApprovalModeText(step.approvalMode) }}
</el-tag>
</div>
</template>
<el-row :gutter="20">
<el-col :span="8">
<div class="step-info">
<label class="info-label">审批方式:</label>
<span>{{ getApprovalModeText(step.approvalMode) }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="step-info">
<label class="info-label">超时时间:</label>
<span>{{ step.timeoutHours }} 小时</span>
</div>
</el-col>
<el-col :span="8">
<div class="step-info">
<label class="info-label">审批人数:</label>
<span>{{ step.approvers ? step.approvers.length : 0 }} 人</span>
</div>
</el-col>
</el-row>
<div class="approvers-section mt-4" v-if="step.approvers && step.approvers.length > 0">
<label class="info-label">审批人员:</label>
<div class="approvers-list">
<el-tag
v-for="(approver, approverIndex) in step.approverNames"
:key="approverIndex"
class="approver-tag"
>
{{ approver }}
</el-tag>
</div>
</div>
<div v-else-if="config.approvalType === 'custom'" class="custom-approver-notice">
<el-alert
title="自选审批人"
description="申报人在提交时自行选择审批人"
type="info"
show-icon
:closable="false"
/>
</div>
</el-card>
</div>
</div>
</el-card>
<!-- 无流程步骤时的提示 -->
<el-card class="detail-card" v-else>
<el-empty description="暂无配置的审批流程步骤" />
</el-card>
</div>
</template>
<script>
export default {
name: 'ApproverConfigDetail',
props: {
config: {
type: Object,
default: () => ({})
}
},
methods: {
/** 获取审批类型标签 */
getApprovalTypeTag(type) {
const typeMap = {
'fixed': 'success',
'role': 'warning',
'dept': 'info',
'custom': 'primary'
};
return typeMap[type] || 'info';
},
/** 获取审批类型文本 */
getApprovalTypeText(type) {
const textMap = {
'fixed': '固定审批人',
'role': '角色审批',
'dept': '部门审批',
'custom': '自选审批人'
};
return textMap[type] || type;
},
/** 获取适用范围文本 */
getScopeText(scope) {
const scopeMap = {
'all': '全部',
'dept': '部门',
'project_type': '项目类型',
'amount_range': '金额范围'
};
return scopeMap[scope] || scope;
},
/** 获取审批方式文本 */
getApprovalModeText(mode) {
const modeMap = {
'single': '单人审批',
'multi_sign': '多人会签',
'multi_or': '多人或签'
};
return modeMap[mode] || mode;
},
/** 获取审批方式标签类型 */
getModeTagType(mode) {
const typeMap = {
'single': 'primary',
'multi_sign': 'success',
'multi_or': 'warning'
};
return typeMap[mode] || 'info';
}
}
};
</script>
<style lang="scss" scoped>
.config-detail {
.detail-card {
border-radius: 8px;
border: 1px solid #e4e7ed;
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #2c3e50;
}
}
.section-title {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #2c3e50;
}
.description-text {
margin: 0;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
color: #606266;
line-height: 1.6;
}
.steps-detail {
.step-detail-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.step-detail-card {
border: 1px solid #e4e7ed;
.step-header {
display: flex;
align-items: center;
gap: 12px;
.step-number {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.step-name {
font-weight: 600;
color: #2c3e50;
flex: 1;
}
}
}
.step-info {
display: flex;
align-items: center;
margin-bottom: 8px;
.info-label {
font-weight: 600;
color: #909399;
margin-right: 8px;
min-width: 80px;
}
}
.approvers-section {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
.info-label {
font-weight: 600;
color: #909399;
margin-bottom: 8px;
display: block;
}
.approvers-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
.approver-tag {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
}
}
}
.custom-approver-notice {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
}
}
.mb-4 {
margin-bottom: 16px;
}
.mt-4 {
margin-top: 16px;
}
.mt-6 {
margin-top: 24px;
}
</style>
+890
View File
@@ -0,0 +1,890 @@
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="queryParams.configName" placeholder="请输入配置名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审批类型" prop="approvalType">
<el-select v-model="queryParams.approvalType" placeholder="请选择审批类型" clearable style="width: 100%">
<el-option label="固定审批人" value="fixed" />
<el-option label="角色审批" value="role" />
<el-option label="部门审批" value="dept" />
<el-option label="自选审批人" value="custom" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 快捷操作按钮 -->
<el-row :gutter="10" class="mb-[10px]">
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="handleAdd">
<el-icon class="quick-icon"><Plus /></el-icon>
<span>新增配置</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showFlowDesigner">
<el-icon class="quick-icon"><SetUp /></el-icon>
<span>流程设计</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showTemplates">
<el-icon class="quick-icon"><Document /></el-icon>
<span>配置模板</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showTestFlow">
<el-icon class="quick-icon"><Promotion /></el-icon>
<span>流程测试</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 表格区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['cw:approverConfig:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['cw:approverConfig:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['cw:approverConfig:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Sort" @click="toggleExpandAll">展开/折叠</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['cw:approverConfig:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table
v-loading="loading"
border
:data="configList"
row-key="id"
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="配置名称" align="left" prop="configName" min-width="200" show-overflow-tooltip />
<el-table-column label="审批类型" align="center" prop="approvalType" width="120">
<template #default="scope">
<el-tag :type="getApprovalTypeTag(scope.row.approvalType)">
{{ getApprovalTypeText(scope.row.approvalType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="步骤数量" align="center" prop="stepCount" width="100">
<template #default="scope">
<el-badge :value="scope.row.stepCount" class="step-badge" />
</template>
</el-table-column>
<el-table-column label="审批人" align="center" prop="approverNames" min-width="180" show-overflow-tooltip>
<template #default="scope">
<el-tag
v-for="(name, index) in scope.row.approverNames"
:key="index"
size="small"
class="mr-1"
>
{{ name }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="适用范围" align="center" prop="applicableScope" width="120">
<template #default="scope">
<el-tag type="info" size="small">{{ scope.row.applicableScope }}</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="orderNum" width="80" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="handleStatusChange(scope.row)"
v-hasPermi="['cw:approverConfig:edit']"
/>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="240" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleDetail(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="编辑配置" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['cw:approverConfig:edit']"></el-button>
</el-tooltip>
<el-tooltip content="复制配置" placement="top">
<el-button link type="success" icon="CopyDocument" @click="handleCopy(scope.row)" v-hasPermi="['cw:approverConfig:add']"></el-button>
</el-tooltip>
<el-tooltip content="流程图" placement="top">
<el-button link type="warning" icon="Share" @click="handleViewFlow(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['cw:approverConfig:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<!-- 添加或修改审批人配置对话框 -->
<el-dialog v-model="dialog.visible" :title="dialog.title" width="900px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="form.configName" placeholder="请输入配置名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审批类型" prop="approvalType">
<el-select v-model="form.approvalType" placeholder="请选择审批类型" style="width: 100%" @change="handleTypeChange">
<el-option label="固定审批人" value="fixed" />
<el-option label="角色审批" value="role" />
<el-option label="部门审批" value="dept" />
<el-option label="自选审批人" value="custom" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="适用范围" prop="applicableScope">
<el-select v-model="form.applicableScope" placeholder="请选择适用范围" style="width: 100%">
<el-option label="全部" value="all" />
<el-option label="部门" value="dept" />
<el-option label="项目类型" value="project_type" />
<el-option label="金额范围" value="amount_range" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序" prop="orderNum">
<el-input-number v-model="form.orderNum" :min="0" :max="999" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="配置描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入配置描述" />
</el-form-item>
</el-col>
</el-row>
<!-- 审批步骤配置 -->
<el-form-item label="审批步骤">
<el-card shadow="never" class="steps-card">
<template #header>
<div class="steps-header">
<span>审批流程步骤</span>
<el-button type="primary" size="small" icon="Plus" @click="addStep">添加步骤</el-button>
</div>
</template>
<div v-if="form.steps && form.steps.length > 0" class="steps-container">
<div
v-for="(step, index) in form.steps"
:key="index"
class="step-item"
>
<el-card shadow="hover" class="step-card">
<template #header>
<div class="step-header">
<span class="step-title">
<el-icon><Position /></el-icon>
步骤 {{ index + 1 }}: {{ step.stepName || '未命名步骤' }}
</span>
<div class="step-actions">
<el-button type="text" size="small" @click="moveStepUp(index)" :disabled="index === 0">
<el-icon><ArrowUp /></el-icon>
</el-button>
<el-button type="text" size="small" @click="moveStepDown(index)" :disabled="index === form.steps.length - 1">
<el-icon><ArrowDown /></el-icon>
</el-button>
<el-button type="text" size="small" @click="removeStep(index)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="步骤名称" :prop="`steps.${index}.stepName`" :rules="stepRules.stepName">
<el-input v-model="step.stepName" placeholder="请输入步骤名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="审批方式" :prop="`steps.${index}.approvalMode`" :rules="stepRules.approvalMode">
<el-select v-model="step.approvalMode" placeholder="请选择审批方式" style="width: 100%">
<el-option label="单人审批" value="single" />
<el-option label="多人会签" value="multi_sign" />
<el-option label="多人或签" value="multi_or" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="超时时间(小时)" :prop="`steps.${index}.timeoutHours`">
<el-input-number v-model="step.timeoutHours" :min="1" :max="720" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="审批人设置" :prop="`steps.${index}.approvers`" :rules="stepRules.approvers">
<div class="approvers-container">
<!-- 固定审批人 -->
<div v-if="form.approvalType === 'fixed'">
<el-select
v-model="step.approvers"
multiple
placeholder="请选择审批人"
style="width: 100%"
@change="updateApproverNames"
>
<el-option
v-for="user in userList"
:key="user.userId"
:label="user.nickName"
:value="user.userId"
/>
</el-select>
</div>
<!-- 角色审批 -->
<div v-else-if="form.approvalType === 'role'">
<el-select
v-model="step.approvers"
multiple
placeholder="请选择审批角色"
style="width: 100%"
>
<el-option
v-for="role in roleList"
:key="role.roleId"
:label="role.roleName"
:value="role.roleId"
/>
</el-select>
</div>
<!-- 部门审批 -->
<div v-else-if="form.approvalType === 'dept'">
<el-tree-select
v-model="step.approvers"
:data="deptTreeData"
:props="{ value: 'id', label: 'label', children: 'children' }"
multiple
placeholder="请选择审批部门"
style="width: 100%"
/>
</div>
<!-- 自选审批人 -->
<div v-else-if="form.approvalType === 'custom'">
<el-alert
title="自选审批人模式"
description="申报人在提交时可以自行选择审批人,此处无需配置具体人员"
type="info"
show-icon
:closable="false"
/>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
</el-card>
</div>
</div>
<el-empty v-else description="暂无审批步骤,请添加步骤" />
</el-card>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialog.visible" :title="detailDialog.title" width="900px" append-to-body>
<approver-config-detail :config="currentConfig" />
<template #footer>
<div class="dialog-footer">
<el-button @click="detailDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 流程图对话框 -->
<el-dialog v-model="flowDialog.visible" title="审批流程图" width="1200px" append-to-body>
<div ref="flowChartRef" style="height: 500px; border: 1px solid #e4e7ed;"></div>
<template #footer>
<div class="dialog-footer">
<el-button @click="flowDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {
listApproverConfig,
getApproverConfig,
addApproverConfig,
updateApproverConfig,
delApproverConfig
} from "@/api/cw/projectApplication";
export default {
name: 'CwApproverConfig',
dicts: ['sys_normal_disable'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 配置列表
configList: [],
// 是否展开,默认全部展开
isExpandAll: true,
// 弹出层标题
dialog: {
visible: false,
title: ''
},
// 详情弹窗
detailDialog: {
visible: false,
title: ''
},
// 流程图弹窗
flowDialog: {
visible: false
},
// 当前配置数据
currentConfig: {},
// 表单参数
form: {},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
configName: '',
approvalType: '',
status: ''
},
// 表单校验
rules: {
configName: [
{ required: true, message: "配置名称不能为空", trigger: "blur" },
{ min: 2, max: 50, message: "配置名称长度在 2 到 50 个字符", trigger: "blur" }
],
approvalType: [
{ required: true, message: "审批类型不能为空", trigger: "change" }
],
applicableScope: [
{ required: true, message: "适用范围不能为空", trigger: "change" }
]
},
// 步骤表单校验
stepRules: {
stepName: [
{ required: true, message: "步骤名称不能为空", trigger: "blur" }
],
approvalMode: [
{ required: true, message: "审批方式不能为空", trigger: "change" }
],
approvers: [
{ required: true, message: "请设置审批人", trigger: "change" }
]
},
// 用户列表
userList: [],
// 角色列表
roleList: [],
// 部门树数据
deptTreeData: []
};
},
created() {
this.getList();
this.loadBasicData();
},
methods: {
/** 查询审批人配置列表 */
getList() {
this.loading = true;
listApproverConfig(this.queryParams).then(response => {
this.configList = this.handleTree(response.data, "id", "parentId");
this.total = response.total;
this.loading = false;
});
},
/** 加载基础数据 */
loadBasicData() {
// 加载用户列表
// listUser().then(response => {
// this.userList = response.rows;
// });
// 加载角色列表
// listRole().then(response => {
// this.roleList = response.rows;
// });
// 加载部门树
// listDept().then(response => {
// this.deptTreeData = this.handleTree(response.data, "deptId");
// });
},
/** 取消按钮 */
cancel() {
this.dialog.visible = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
id: null,
configName: null,
approvalType: null,
applicableScope: 'all',
orderNum: 0,
description: null,
status: "1",
steps: []
};
if (this.$refs.formRef) {
this.$refs.formRef.resetFields();
}
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.$refs.queryFormRef.resetFields();
this.handleQuery();
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 展开/折叠操作 */
toggleExpandAll() {
this.isExpandAll = !this.isExpandAll;
this.toggleExpandAllTree(this.configList, this.isExpandAll);
},
/** 快捷操作 */
showFlowDesigner() {
this.$modal.msgInfo("流程设计器功能开发中...");
},
showTemplates() {
this.$modal.msgInfo("配置模板功能开发中...");
},
showTestFlow() {
this.$modal.msgInfo("流程测试功能开发中...");
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.dialog.visible = true;
this.dialog.title = "添加审批人配置";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row?.id || this.ids[0];
getApproverConfig(id).then(response => {
this.form = response.data;
this.dialog.visible = true;
this.dialog.title = "修改审批人配置";
});
},
/** 复制配置 */
handleCopy(row) {
getApproverConfig(row.id).then(response => {
this.form = {
...response.data,
id: null,
configName: response.data.configName + '_副本'
};
this.dialog.visible = true;
this.dialog.title = "复制审批人配置";
});
},
/** 查看详情 */
handleDetail(row) {
getApproverConfig(row.id).then(response => {
this.currentConfig = response.data;
this.detailDialog.visible = true;
this.detailDialog.title = "配置详情";
});
},
/** 查看流程图 */
handleViewFlow(row) {
this.flowDialog.visible = true;
this.$nextTick(() => {
this.renderFlowChart(row);
});
},
/** 状态变更 */
handleStatusChange(row) {
let text = row.status === "1" ? "启用" : "停用";
this.$modal.confirm('确认要"' + text + '""' + row.configName + '"配置吗?').then(() => {
return updateApproverConfig(row);
}).then(() => {
this.$modal.msgSuccess(text + "成功");
}).catch(() => {
row.status = row.status === "0" ? "1" : "0";
});
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row?.id ? [row.id] : this.ids;
const configs = row?.configName ? [row.configName] : this.configList.filter(item => this.ids.includes(item.id)).map(item => item.configName);
this.$modal.confirm('是否确认删除配置名称为"' + configs.join('", "') + '"的数据项?').then(() => {
return delApproverConfig(ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 提交按钮 */
submitForm() {
this.$refs.formRef.validate(valid => {
if (valid) {
if (this.form.id != null) {
updateApproverConfig(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.dialog.visible = false;
this.getList();
});
} else {
addApproverConfig(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.dialog.visible = false;
this.getList();
});
}
}
});
},
/** 审批类型变更 */
handleTypeChange(value) {
// 清空已配置的审批人
if (this.form.steps) {
this.form.steps.forEach(step => {
step.approvers = [];
});
}
},
/** 添加步骤 */
addStep() {
if (!this.form.steps) {
this.form.steps = [];
}
this.form.steps.push({
stepName: `审批步骤${this.form.steps.length + 1}`,
approvalMode: 'single',
timeoutHours: 24,
approvers: []
});
},
/** 删除步骤 */
removeStep(index) {
this.form.steps.splice(index, 1);
},
/** 上移步骤 */
moveStepUp(index) {
if (index > 0) {
const temp = this.form.steps[index];
this.form.steps[index] = this.form.steps[index - 1];
this.form.steps[index - 1] = temp;
}
},
/** 下移步骤 */
moveStepDown(index) {
if (index < this.form.steps.length - 1) {
const temp = this.form.steps[index];
this.form.steps[index] = this.form.steps[index + 1];
this.form.steps[index + 1] = temp;
}
},
/** 更新审批人名称 */
updateApproverNames() {
// 更新审批人名称用于显示
},
/** 导出按钮操作 */
handleExport() {
this.download('cw/approverConfig/export', {
...this.queryParams
}, `approver_config_${new Date().getTime()}.xlsx`);
},
/** 渲染流程图 */
renderFlowChart(row) {
// 使用流程图库渲染流程图
this.$modal.msgInfo("流程图功能开发中...");
},
/** 获取审批类型标签 */
getApprovalTypeTag(type) {
const typeMap = {
'fixed': 'success',
'role': 'warning',
'dept': 'info',
'custom': 'primary'
};
return typeMap[type] || 'info';
},
/** 获取审批类型文本 */
getApprovalTypeText(type) {
const textMap = {
'fixed': '固定审批人',
'role': '角色审批',
'dept': '部门审批',
'custom': '自选审批人'
};
return textMap[type] || type;
}
}
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: calc(100vh - 84px);
}
// 快捷操作卡片
.quick-action-card {
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid #e4e7ed;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #409EFF;
}
.quick-action {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
.quick-icon {
font-size: 28px;
color: #409EFF;
margin-bottom: 8px;
}
span {
font-size: 14px;
color: #606266;
font-weight: 500;
}
}
}
// 表格卡片
.button-card.table-card {
border-radius: 8px;
border: 1px solid #e4e7ed;
}
// 步骤徽章
.step-badge {
:deep(.el-badge__content) {
background-color: #409EFF;
}
}
// 步骤配置卡片
.steps-card {
border: 1px solid #e4e7ed;
.steps-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #2c3e50;
}
}
.steps-container {
.step-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.step-card {
border: 1px solid #e4e7ed;
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
.step-title {
display: flex;
align-items: center;
font-weight: 600;
color: #2c3e50;
.el-icon {
margin-right: 8px;
color: #409EFF;
}
}
.step-actions {
display: flex;
align-items: center;
gap: 4px;
}
}
}
}
// 审批人容器
.approvers-container {
width: 100%;
}
// 标签样式
.mr-1 {
margin-right: 8px;
margin-bottom: 4px;
}
// 响应式设计
@media (max-width: 768px) {
.quick-action {
padding: 15px;
.quick-icon {
font-size: 24px;
}
span {
font-size: 12px;
}
}
}
</style>
+700
View File
@@ -0,0 +1,700 @@
<template>
<div class="p-2 page-container">
<!-- 快捷功能按钮 -->
<el-row :gutter="10" class="mb-[10px]">
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="handleAdd">
<el-icon class="quick-icon"><Plus /></el-icon>
<span>新增申报</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showMyApplications">
<el-icon class="quick-icon"><User /></el-icon>
<span>我的申报</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showPendingApplications">
<el-icon class="quick-icon"><Clock /></el-icon>
<span>待审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showStatistics">
<el-icon class="quick-icon"><DataAnalysis /></el-icon>
<span>统计分析</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 统计卡片区域 -->
<div class="mb-[10px]">
<el-row :gutter="16">
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Files /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.totalCount }}</div>
<div class="stat-label">申报总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.approvingCount }}</div>
<div class="stat-label">审批中</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.approvedCount }}</div>
<div class="stat-label">已通过</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-danger">
<div class="stat-content">
<div class="stat-icon">
<el-icon><CircleClose /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.rejectedCount }}</div>
<div class="stat-label">已拒绝</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Money /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ formatAmount(statistics.totalAmount) }}</div>
<div class="stat-label">总金额(万元)</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Timer /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.avgProcessDays || 0 }}</div>
<div class="stat-label">平均周期(天)</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.monthlyNew || 0 }}</div>
<div class="stat-label">本月新增</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Finished /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.monthlyApproved || 0 }}</div>
<div class="stat-label">本月通过</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 图表区域 -->
<el-row :gutter="16" class="mb-[10px]">
<!-- 月度趋势图表 -->
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24">
<el-card shadow="never" class="chart-card">
<template #header>
<div class="card-header">
<span>月度申报趋势</span>
<el-select v-model="selectedYear" @change="handleYearChange" style="width: 120px;">
<el-option
v-for="year in yearOptions"
:key="year"
:label="year + '年'"
:value="year"
/>
</el-select>
</div>
</template>
<div ref="trendChart" style="height: 350px;"></div>
</el-card>
</el-col>
<!-- 状态分布图表 -->
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
<el-card shadow="never" class="chart-card">
<template #header>
<span>状态分布</span>
</template>
<div ref="statusChart" style="height: 350px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 部门统计表格 -->
<el-card shadow="never" class="table-card">
<template #header>
<div class="card-header">
<span>部门统计详情</span>
<el-button type="primary" icon="Download" @click="handleExportDeptStatistics">导出部门统计</el-button>
</div>
</template>
<el-table
:data="statistics.deptStatistics"
border
style="width: 100%"
v-loading="statisticsLoading"
>
<el-table-column prop="deptName" label="部门名称" align="center" min-width="120" show-overflow-tooltip />
<el-table-column prop="totalCount" label="申报数量" align="center" width="100" />
<el-table-column prop="approvingCount" label="审批中" align="center" width="100">
<template #default="scope">
<el-tag type="warning" size="small">{{ scope.row.approvingCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="approvedCount" label="已通过" align="center" width="100">
<template #default="scope">
<el-tag type="success" size="small">{{ scope.row.approvedCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="rejectedCount" label="已拒绝" align="center" width="100">
<template #default="scope">
<el-tag type="danger" size="small">{{ scope.row.rejectedCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="总金额(万元)" align="center" width="120">
<template #default="scope">
<span class="amount-text">{{ formatAmount(scope.row.totalAmount) }}</span>
</template>
</el-table-column>
<el-table-column prop="approvedAmount" label="已批金额(万元)" align="center" width="130">
<template #default="scope">
<span class="amount-text success">{{ formatAmount(scope.row.approvedAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="审批率" align="center" width="100">
<template #default="scope">
<el-progress
:percentage="calculateApprovalRate(scope.row)"
:status="getProgressStatus(scope.row)"
:show-text="false"
/>
<span class="rate-text">{{ calculateApprovalRate(scope.row) }}%</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120" fixed="right">
<template #default="scope">
<el-tooltip content="查看部门详情" placement="top">
<el-button link type="primary" icon="View" @click="viewDepartmentDetail(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="导出部门数据" placement="top">
<el-button link type="info" icon="Download" @click="exportDepartmentData(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus, User, Clock, DataAnalysis, Files, CircleCheck, CircleClose,
Money, Timer, TrendCharts, Finished
} from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import { getApplicationStatistics, getStatisticsByDept, getStatisticsByMonth } from '@/api/cw/projectApplication'
import { parseTime } from '@/utils/ruoyi'
const router = useRouter()
// 响应式数据
const statistics = reactive({
totalCount: 0,
approvingCount: 0,
approvedCount: 0,
rejectedCount: 0,
totalAmount: 0,
avgProcessDays: 0,
monthlyNew: 0,
monthlyApproved: 0,
deptStatistics: [] as any[]
})
const selectedYear = ref(new Date().getFullYear())
const yearOptions = ref<number[]>([])
const statisticsLoading = ref(false)
// 图表引用
const trendChart = ref()
const statusChart = ref()
let trendChartInstance: echarts.ECharts | null = null
let statusChartInstance: echarts.ECharts | null = null
// 生成年份选项
const generateYearOptions = () => {
const currentYear = new Date().getFullYear()
for (let i = currentYear - 4; i <= currentYear + 1; i++) {
yearOptions.value.push(i)
}
}
// 快捷操作
const handleAdd = () => {
router.push('/cw/application')
}
const showMyApplications = () => {
router.push({ path: '/cw/application', query: { tab: 'my' } })
}
const showPendingApplications = () => {
router.push({ path: '/cw/application', query: { tab: 'pending' } })
}
const showStatistics = () => {
router.push('/cw/application/summary')
}
// 格式化金额
const formatAmount = (amount: number | undefined): string => {
if (!amount) return '0.00'
return amount.toFixed(2)
}
// 计算审批率
const calculateApprovalRate = (row: any): number => {
if (!row.totalCount || row.totalCount === 0) return 0
return Math.round((row.approvedCount / row.totalCount) * 100)
}
// 获取进度条状态
const getProgressStatus = (row: any): string => {
const rate = calculateApprovalRate(row)
if (rate >= 80) return 'success'
if (rate >= 60) return 'warning'
return 'exception'
}
// 查看部门详情
const viewDepartmentDetail = (row: any) => {
router.push({
path: '/cw/application',
query: { deptId: row.deptId, deptName: row.deptName }
})
}
// 导出部门数据
const exportDepartmentData = (row: any) => {
ElMessage.success(`导出部门【${row.deptName}】数据功能开发中...`)
}
// 导出部门统计
const handleExportDeptStatistics = () => {
ElMessage.success('导出部门统计功能开发中...')
}
// 获取统计数据
const loadStatistics = async () => {
try {
statisticsLoading.value = true
const [statsRes, deptRes] = await Promise.all([
getApplicationStatistics(selectedYear.value),
getStatisticsByDept()
])
if (statsRes.code === 200) {
Object.assign(statistics, statsRes.data)
}
if (deptRes.code === 200) {
statistics.deptStatistics = deptRes.data || []
}
} catch (error) {
console.error('获取统计数据失败:', error)
ElMessage.error('获取统计数据失败')
} finally {
statisticsLoading.value = false
}
}
// 年份变化处理
const handleYearChange = () => {
loadStatistics()
loadTrendChart()
}
// 加载趋势图表
const loadTrendChart = async () => {
try {
const response = await getStatisticsByMonth(selectedYear.value)
if (response.code === 200) {
renderTrendChart(response.data)
}
} catch (error) {
console.error('获取月度统计失败:', error)
}
}
// 渲染趋势图表
const renderTrendChart = (data: any[]) => {
if (!trendChartInstance) return
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
const applicationCounts = new Array(12).fill(0)
const approvedCounts = new Array(12).fill(0)
data.forEach(item => {
const monthIndex = item.month - 1
if (monthIndex >= 0 && monthIndex < 12) {
applicationCounts[monthIndex] = item.applicationCount || 0
approvedCounts[monthIndex] = item.approvedCount || 0
}
})
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: ['申报数量', '通过数量']
},
xAxis: {
type: 'category',
data: months
},
yAxis: {
type: 'value'
},
series: [
{
name: '申报数量',
type: 'line',
data: applicationCounts,
smooth: true,
itemStyle: { color: '#409EFF' }
},
{
name: '通过数量',
type: 'line',
data: approvedCounts,
smooth: true,
itemStyle: { color: '#67C23A' }
}
]
}
trendChartInstance.setOption(option)
}
// 渲染状态分布图
const renderStatusChart = () => {
if (!statusChartInstance) return
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '申报状态',
type: 'pie',
radius: ['40%', '70%'],
data: [
{ value: statistics.approvingCount, name: '审批中', itemStyle: { color: '#E6A23C' } },
{ value: statistics.approvedCount, name: '已通过', itemStyle: { color: '#67C23A' } },
{ value: statistics.rejectedCount, name: '已拒绝', itemStyle: { color: '#F56C6C' } }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
statusChartInstance.setOption(option)
}
// 初始化图表
const initCharts = async () => {
await nextTick()
if (trendChart.value) {
trendChartInstance = echarts.init(trendChart.value)
loadTrendChart()
}
if (statusChart.value) {
statusChartInstance = echarts.init(statusChart.value)
renderStatusChart()
}
// 监听窗口大小变化
window.addEventListener('resize', () => {
trendChartInstance?.resize()
statusChartInstance?.resize()
})
}
// 组件挂载
onMounted(() => {
generateYearOptions()
loadStatistics()
initCharts()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: calc(100vh - 84px);
}
// 快捷操作卡片
.quick-action-card {
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid #e4e7ed;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #409EFF;
}
.quick-action {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
.quick-icon {
font-size: 28px;
color: #409EFF;
margin-bottom: 8px;
}
span {
font-size: 14px;
color: #606266;
font-weight: 500;
}
}
}
// 统计卡片
.stat-card {
border: none;
border-radius: 8px;
overflow: hidden;
&.stat-card-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.stat-card-warning {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
color: white;
}
&.stat-card-danger {
background: linear-gradient(135deg, #ff6b6b 0%, #ffa726 100%);
color: white;
}
&.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #2c3e50;
}
:deep(.el-card__body) {
padding: 0;
}
}
.stat-content {
display: flex;
align-items: center;
padding: 20px;
.stat-icon {
font-size: 32px;
margin-right: 16px;
opacity: 0.8;
}
.stat-info {
.stat-value {
font-size: 24px;
font-weight: bold;
line-height: 1.2;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
opacity: 0.9;
line-height: 1;
}
}
}
// 图表卡片
.chart-card {
border-radius: 8px;
border: 1px solid #e4e7ed;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #2c3e50;
}
}
// 表格卡片
.table-card {
border-radius: 8px;
border: 1px solid #e4e7ed;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #2c3e50;
}
.amount-text {
font-weight: 600;
color: #606266;
&.success {
color: #67C23A;
}
}
.rate-text {
font-size: 12px;
margin-left: 8px;
color: #909399;
}
}
// 动画效果
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.quick-action-card,
.stat-card,
.chart-card,
.table-card {
animation: fadeInUp 0.6s ease forwards;
}
// 响应式设计
@media (max-width: 768px) {
.quick-action {
padding: 15px;
.quick-icon {
font-size: 24px;
}
span {
font-size: 12px;
}
}
.stat-content {
padding: 15px;
.stat-icon {
font-size: 24px;
margin-right: 12px;
}
.stat-info .stat-value {
font-size: 20px;
}
}
}
</style>
+261
View File
@@ -0,0 +1,261 @@
<template>
<div class="image-preview-test">
<el-card header="图片预览组件测试">
<el-form :model="testForm" label-width="120px">
<el-form-item label="图片源:">
<el-input v-model="testForm.src" placeholder="输入OSS ID或图片URL" clearable />
</el-form-item>
<el-form-item label="模式:">
<el-radio-group v-model="testForm.isOssId">
<el-radio :label="true">OSS ID模式</el-radio>
<el-radio :label="false">直接URL模式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="调试模式:">
<el-switch v-model="testForm.debug" />
</el-form-item>
<el-form-item label="尺寸:">
<el-input v-model="testForm.width" placeholder="宽度" style="width: 100px" />
<span style="margin: 0 10px">×</span>
<el-input v-model="testForm.height" placeholder="高度" style="width: 100px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="testImage">测试图片</el-button>
<el-button @click="checkOssId" :loading="checkingOssId">检查OSS ID</el-button>
<el-button @click="clearTest">清除</el-button>
<el-button @click="testDefaultOssId">测试默认OSS ID</el-button>
</el-form-item>
</el-form>
<el-divider content-position="left">预览结果</el-divider>
<div class="preview-container">
<ImagePreview
v-if="showPreview"
:src="testForm.src"
:is-oss-id="testForm.isOssId"
:width="testForm.width || 200"
:height="testForm.height || 200"
:debug="testForm.debug"
/>
<div v-else class="no-preview">
<el-icon><picture /></el-icon>
<p>请输入图片源进行测试</p>
</div>
</div>
<el-divider content-position="left">调试指南</el-divider>
<div class="debug-guide">
<el-alert title="调试步骤" type="info" show-icon :closable="false">
<template #default>
<p>1. 打开浏览器开发者工具 → Console 面板</p>
<p>2. 开启"调试模式"可以看到详细的执行日志</p>
<p>3. 观察 [ImagePreview Debug] 开头的日志信息</p>
<p>4. 查看是否有错误信息(红色日志)</p>
</template>
</el-alert>
<el-card style="margin-top: 15px" header="常见问题排查">
<el-collapse>
<el-collapse-item title="1. OSS接口调用失败" name="1">
<p><strong>现象:</strong>控制台显示 "获取OSS URL失败"</p>
<p><strong>排查:</strong>检查OSS ID是否正确,后端接口是否正常</p>
<p><strong>解决:</strong>确认 /resource/oss/listByIds/{ossId} 接口可以正常访问</p>
</el-collapse-item>
<el-collapse-item title="2. 图片请求权限问题" name="2">
<p><strong>现象:</strong>控制台显示 401/403 错误</p>
<p><strong>排查:</strong>检查token是否有效,clientId是否正确</p>
<p><strong>解决:</strong>确认request拦截器正确添加了Authorization头</p>
</el-collapse-item>
<el-collapse-item title="3. blob URL创建失败" name="3">
<p><strong>现象:</strong>控制台显示 "无效的图片格式"</p>
<p><strong>排查:</strong>检查图片URL返回的内容类型</p>
<p><strong>解决:</strong>确认返回的是真实图片数据,不是HTML错误页面</p>
</el-collapse-item>
<el-collapse-item title="4. CORS跨域问题" name="4">
<p><strong>现象:</strong>控制台显示CORS相关错误</p>
<p><strong>排查:</strong>检查图片服务器的CORS配置</p>
<p><strong>解决:</strong>配置后端代理或修改CORS策略</p>
</el-collapse-item>
</el-collapse>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import ImagePreview from '@/components/ImagePreview/index.vue';
import { Picture } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { listByIds } from '@/api/system/oss';
const testForm = ref({
src: '',
isOssId: true,
debug: true,
width: '200',
height: '200'
});
const checkingOssId = ref(false);
const showPreview = computed(() => {
return testForm.value.src && testForm.value.src.trim() !== '';
});
const testImage = () => {
if (!testForm.value.src) {
ElMessage.warning('请输入图片源');
return;
}
console.log('='.repeat(50));
console.log('开始测试图片预览组件');
console.log('测试参数:', {
src: testForm.value.src,
isOssId: testForm.value.isOssId,
debug: testForm.value.debug,
width: testForm.value.width,
height: testForm.value.height
});
console.log('='.repeat(50));
};
const checkOssId = async () => {
if (!testForm.value.src) {
ElMessage.warning('请输入OSS ID');
return;
}
if (!testForm.value.isOssId) {
ElMessage.warning('请切换到OSS ID模式进行检查');
return;
}
checkingOssId.value = true;
try {
console.log('='.repeat(50));
console.log('检查OSS ID:', testForm.value.src);
const response = await listByIds(testForm.value.src);
// RuoYi框架的响应格式:{code, msg, data},实际数据在data字段中
console.log('API响应原始数据:', response);
const ossList = response.data || response;
console.log('OSS检查结果:', {
dataLength: ossList?.length || 0,
data: ossList
});
if (!ossList || ossList.length === 0) {
ElMessage.error(`OSS ID ${testForm.value.src} 在数据库中不存在`);
console.error('OSS ID不存在:', testForm.value.src);
} else {
const ossFile = ossList[0];
ElMessage.success(`OSS ID ${testForm.value.src} 存在`);
console.log('OSS文件信息:', {
ossId: ossFile.ossId,
fileName: ossFile.fileName,
originalName: ossFile.originalName,
fileSuffix: ossFile.fileSuffix,
url: ossFile.url,
createTime: ossFile.createTime,
service: ossFile.service
});
// 检查文件类型
if (ossFile.fileSuffix && !ossFile.fileSuffix.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp|bmp)$/)) {
ElMessage.warning(`文件类型 ${ossFile.fileSuffix} 可能不是图片格式`);
}
}
console.log('='.repeat(50));
} catch (error) {
console.error('检查OSS ID失败:', error);
ElMessage.error(`检查OSS ID失败: ${error.message}`);
} finally {
checkingOssId.value = false;
}
};
const clearTest = () => {
testForm.value.src = '';
console.clear();
ElMessage.success('已清除测试数据和控制台日志');
};
const testDefaultOssId = () => {
// 这里可以设置一个测试用的OSS ID
testForm.value.src = '1'; // 请替换为实际存在的OSS ID
testForm.value.isOssId = true;
testForm.value.debug = true;
ElMessage.info('已设置默认测试OSS ID,请根据实际情况修改');
};
</script>
<style lang="scss" scoped>
.image-preview-test {
padding: 20px;
.preview-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 250px;
border: 2px dashed #dcdfe6;
border-radius: 8px;
background-color: #fafafa;
.no-preview {
text-align: center;
color: #909399;
.el-icon {
font-size: 48px;
margin-bottom: 10px;
}
p {
margin: 0;
font-size: 14px;
}
}
}
.debug-guide {
.el-alert {
:deep(.el-alert__content) {
p {
margin: 5px 0;
line-height: 1.5;
}
}
}
.el-collapse {
:deep(.el-collapse-item__content) {
p {
margin: 8px 0;
line-height: 1.6;
strong {
color: #409eff;
}
}
}
}
}
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<template>
<div class="image-upload-test">
<el-card header="ImageUpload组件测试 - 认证图片回显和预览">
<el-form :model="form" label-width="120px">
<el-form-item label="测试数据1">
<el-button @click="loadTestData1" type="primary">加载单个OSS ID</el-button>
<span style="margin-left: 10px; color: #666;">测试单个OSS文件回显</span>
</el-form-item>
<el-form-item label="测试数据2">
<el-button @click="loadTestData2" type="primary">加载多个OSS ID</el-button>
<span style="margin-left: 10px; color: #666;">测试多个OSS文件回显</span>
</el-form-item>
<el-form-item label="测试数据3">
<el-button @click="loadTestData3" type="primary">加载URL格式</el-button>
<span style="margin-left: 10px; color: #666;">测试直接URL回显</span>
</el-form-item>
<el-form-item label="清空数据">
<el-button @click="clearData" type="danger">清空</el-button>
<span style="margin-left: 10px; color: #666;">清空所有图片数据</span>
</el-form-item>
<el-form-item label="当前值">
<el-input v-model="form.images" type="textarea" :rows="3" readonly />
</el-form-item>
<el-form-item label="图片上传">
<ImageUpload v-model="form.images" :limit="5" />
</el-form-item>
</el-form>
<div class="test-info">
<h3>测试说明</h3>
<ul>
<li><strong>功能验证</strong>:
<ol>
<li>界面打开时历史图片是否正常显示(通过认证接口获取blob URL)</li>
<li>点击图片预览是否正常工作</li>
<li>上传新图片功能是否正常</li>
<li>删除图片功能是否正常</li>
</ol>
</li>
<li><strong>技术要点</strong>:
<ul>
<li>使用 Promise.all 并行处理多个文件的blob URL转换</li>
<li>缓存机制避免重复请求相同图片的认证数据</li>
<li>降级处理:认证失败时使用原始URL</li>
<li>完整的blob URL生命周期管理,防止内存泄漏</li>
</ul>
</li>
<li><strong>错误处理</strong>:如果图片无法显示,检查控制台错误信息</li>
</ul>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import ImageUpload from '@/components/ImageUpload/index.vue';
const form = reactive({
images: ''
});
// 测试数据1:单个OSS ID
const loadTestData1 = () => {
form.images = '1951584287279034370'; // 示例OSS ID,实际使用时需要替换为有效的OSS ID
};
// 测试数据2:多个OSS ID
const loadTestData2 = () => {
form.images = '1951584287279034370,1951584287279034371'; // 示例多个OSS ID
};
// 测试数据3:URL格式
const loadTestData3 = () => {
form.images = '/resource/oss/download/1951584287279034370'; // 示例OSS下载URL
};
// 清空数据
const clearData = () => {
form.images = '';
};
</script>
<style lang="scss" scoped>
.image-upload-test {
padding: 20px;
.test-info {
margin-top: 30px;
padding: 20px;
background-color: #f5f5f5;
border-radius: 4px;
h3 {
margin-top: 0;
color: #333;
}
ul, ol {
margin-left: 20px;
li {
margin-bottom: 8px;
line-height: 1.6;
}
}
strong {
color: #409EFF;
}
}
}
</style>
+68 -42
View File
@@ -80,9 +80,9 @@
>拒绝</el-button
>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:hallReservation:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -216,11 +216,12 @@
<el-form-item label="报告厅图片">
<div class="view-hall-images">
<div class="image-gallery" v-if="viewHallImages.length > 0">
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
<img :src="image" class="gallery-image" @error="handleImageError" />
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item">
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
<div class="image-overlay">
<el-icon class="preview-icon"><ZoomIn /></el-icon>
</div>
</div> -->
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
</div>
</div>
<div v-else class="no-images-placeholder">
@@ -283,11 +284,12 @@
</div>
<div class="hall-images" v-if="getHallImagesByName(hallName).length > 0">
<div class="image-gallery">
<div v-for="(image, index) in getHallImagesByName(hallName)" :key="index" class="image-item" @click="previewImage(image, index)">
<img :src="image" class="gallery-image" @error="handleImageError" />
<div v-for="(image, index) in getHallImagesByName(hallName)" :key="index" class="image-item">
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
<div class="image-overlay">
<el-icon class="preview-icon"><ZoomIn /></el-icon>
</div>
</div> -->
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
</div>
</div>
</div>
@@ -418,6 +420,8 @@ import { listHallInfo } from '@/api/hq/hallInfo';
import { HallReservationVO, HallReservationQuery, HallReservationForm } from '@/api/hq/hallReservation/types';
import { HallInfoVO } from '@/api/hq/hallInfo/types';
import { useUserStore } from '@/store/modules/user';
import { listByIds } from '@/api/system/oss';
import { OssVO } from '@/api/system/oss/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { hall_reserv_status } = toRefs<any>(proxy?.useDict('hall_reserv_status'));
@@ -426,6 +430,7 @@ const userStore = useUserStore();
const hallReservationList = ref<HallReservationVO[]>([]);
const hallList = ref<HallInfoVO[]>([]);
const viewHallImages = ref<string[]>([]);
const hallImagesCache = ref<Map<string, string[]>>(new Map()); // 缓存报告厅图片URL
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
@@ -510,49 +515,75 @@ const getHallList = async () => {
// 调试信息:检查每个报告厅的图片信息
console.log('报告厅列表数据:', hallList.value);
hallList.value.forEach((hall, index) => {
console.log(`报告厅 ${index + 1} - ${hall.hallName}:`, {
id: hall.id,
hallImage: hall.hallImage,
images: getHallImages(hall.hallImage || '')
});
});
// 异步加载所有报告厅的图片URL
await loadAllHallImages();
} catch (error) {
console.error('获取报告厅列表失败:', error);
hallList.value = [];
}
};
/** 处理报告厅图片 - 可能有多张图片,用逗号分隔 */
const getHallImages = (imageUrl: string): string[] => {
if (!imageUrl || imageUrl.trim() === '') return [];
// 支持多种分隔符:逗号、分号、空格
const separators = /[,;|\\s]+/;
const images = imageUrl
.split(separators)
.map((url) => url.trim())
.filter((url) => url && url !== 'null' && url !== 'undefined');
/** 加载所有报告厅的图片URL */
const loadAllHallImages = async () => {
const imagePromises = hallList.value.map(async (hall) => {
if (hall.hallImage) {
try {
const images = await getHallImages(hall.hallImage);
hallImagesCache.value.set(hall.hallName, images);
console.log(`报告厅 ${hall.hallName} 图片加载完成:`, images);
} catch (error) {
console.error(`报告厅 ${hall.hallName} 图片加载失败:`, error);
hallImagesCache.value.set(hall.hallName, []);
}
} else {
hallImagesCache.value.set(hall.hallName, []);
}
});
console.log('处理图片URL:', imageUrl, '结果:', images);
return images;
await Promise.all(imagePromises);
console.log('所有报告厅图片加载完成');
};
/** 处理报告厅图片 - 通过OSS ID获取真实URL */
const getHallImages = async (ossIds: string): Promise<string[]> => {
if (!ossIds || ossIds.trim() === '') return [];
try {
// 支持多种分隔符:逗号、分号、空格
const separators = /[,;|\\s]+/;
const idArray = ossIds
.split(separators)
.map((id) => id.trim())
.filter((id) => id && id !== 'null' && id !== 'undefined');
if (idArray.length === 0) return [];
console.log('处理OSS ID:', ossIds, 'ID数组:', idArray);
// 调用API获取真实的图片URL
// const response = await listByIds(idArray.join(','));
// const ossList: OssVO[] = response.data || [];
// // 提取URL列表
// const urls = ossList.map((oss) => oss.url).filter((url) => url);
// console.log('获取到的图片URL:', urls);
return idArray;
} catch (error) {
console.error('获取图片URL失败:', error);
return [];
}
};
/** 获取记录对应的报告厅图片 */
const getRecordHallImages = (record: HallReservationVO): string[] => {
const selectedHall = hallList.value.find((hall) => hall.hallName === record.hallName);
if (selectedHall && selectedHall.hallImage) {
return getHallImages(selectedHall.hallImage);
}
return [];
return hallImagesCache.value.get(record.hallName) || [];
};
/** 根据报告厅名称获取图片 */
const getHallImagesByName = (hallName: string): string[] => {
const selectedHall = hallList.value.find((hall) => hall.hallName === hallName);
if (selectedHall && selectedHall.hallImage) {
return getHallImages(selectedHall.hallImage);
}
return [];
return hallImagesCache.value.get(hallName) || [];
};
/** 获取选中记录中的唯一报告厅名称 */
@@ -698,12 +729,7 @@ const handleView = async (row: HallReservationVO) => {
// 查找对应的报告厅并显示图片
if (viewForm.value.hallName) {
const selectedHall = hallList.value.find((hall) => hall.hallName === viewForm.value.hallName);
if (selectedHall && selectedHall.hallImage) {
viewHallImages.value = getHallImages(selectedHall.hallImage);
} else {
viewHallImages.value = [];
}
viewHallImages.value = hallImagesCache.value.get(viewForm.value.hallName) || [];
} else {
viewHallImages.value = [];
}
+4 -2
View File
@@ -44,9 +44,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:hallInfo:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:hallInfo:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -254,6 +254,7 @@ const { queryParams, form, rules } = toRefs(data);
const getList = async () => {
loading.value = true;
const res = await listHallInfo(queryParams.value);
console.log('res', res);
hallInfoList.value = res.rows;
total.value = res.total;
loading.value = false;
@@ -311,6 +312,7 @@ const handleUpdate = async (row?: HallInfoVO) => {
/** 提交按钮 */
const submitForm = () => {
console.log('form.value', form.value);
hallInfoFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
@@ -68,9 +68,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:hallReservation:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:hallReservation:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -210,15 +210,18 @@
{{ displayHallDescription(hall.hallDescription) }}
</div>
<div class="hall-images">
<template v-if="getHallImages(hall.hallImage || '').length > 0">
<img
v-for="(image, index) in getHallImages(hall.hallImage || '')"
<template v-if="hall.hallImage && hall.hallImage.length > 0">
<!-- <img
v-for="(image, index) in hall.imageUrls"
:key="index"
:src="image"
class="hall-image"
@error="handleImageError"
@load="handleImageLoad"
/>
/> -->
<div v-for="(image, index) in hall.hallImage" :key="index">
<image-preview :src="image" :width="40" :height="40" :isOssId="true" />
</div>
</template>
<span v-else class="no-image-text">暂无图片</span>
</div>
@@ -230,11 +233,12 @@
<el-form-item label="报告厅图片">
<div class="selected-hall-images">
<div class="image-gallery" v-if="selectedHallImages.length > 0">
<div v-for="(image, index) in selectedHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
<img :src="image" class="gallery-image" @error="handleImageError" />
<div v-for="(image, index) in selectedHallImages" :key="index">
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
<div class="image-overlay">
<el-icon class="preview-icon"><ZoomIn /></el-icon>
</div>
</div> -->
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
</div>
</div>
<div v-else class="no-images-placeholder">
@@ -298,11 +302,12 @@
<el-form-item label="报告厅图片">
<div class="view-hall-images">
<div class="image-gallery" v-if="viewHallImages.length > 0">
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
<img :src="image" class="gallery-image" @error="handleImageError" />
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item">
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
<div class="image-overlay">
<el-icon class="preview-icon"><ZoomIn /></el-icon>
</div>
</div> -->
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
</div>
</div>
<div v-else class="no-images-placeholder">
@@ -338,7 +343,7 @@
<el-avatar :size="32" :src="auditor.avatar">{{ auditor.nickName?.substring(0, 1) }}</el-avatar>
<div class="auditor-info">
<div class="auditor-name">{{ auditor.nickName || auditor.userName }}</div>
<div class="auditor-dept">{{ auditor.dept?.deptName }}</div>
<div class="auditor-dept">{{ auditor?.phonenumber }}</div>
</div>
</div>
<div v-if="auditorList.length === 0" class="no-auditor">暂无可审核人员</div>
@@ -525,6 +530,29 @@ const getHallList = async () => {
hallStatus: '1'
}); // 获取正常状态的报告厅
hallList.value = res.rows || [];
// for (const hall of hallList.value) {
// if (hall.hallImage) {
// try {
// const imageUrls = await getHallImages(hall.hallImage);
// hall.imageUrls = imageUrls;
// } catch (error) {
// console.error(`获取报告厅 ${hall.hallName} 图片失败:`, error);
// hall.imageUrls = [];
// }
// } else {
// hall.imageUrls = [];
// }
// }
//hall.hallImage.split(',')
hallList.value.forEach((hall) => {
//加一些判断
if (hall.hallImage) {
hall.hallImage = hall.hallImage.split(',');
} else {
hall.hallImage = [];
}
});
// 为每个报告厅获取图片URL
// 调试信息:检查每个报告厅的图片信息
console.log('报告厅列表数据:', hallList.value);
@@ -532,7 +560,7 @@ const getHallList = async () => {
console.log(`报告厅 ${index + 1} - ${hall.hallName}:`, {
id: hall.id,
hallImage: hall.hallImage,
images: getHallImages(hall.hallImage || '')
imageUrls: hall.imageUrls
});
});
} catch (error) {
@@ -547,24 +575,62 @@ const handleHallChange = (hallName: string) => {
if (selectedHall) {
form.value.hallId = selectedHall.id;
// 更新选中的报告厅图片
selectedHallImages.value = getHallImages(selectedHall.hallImage || '');
selectedHallImages.value = selectedHall.hallImage || [];
} else {
selectedHallImages.value = [];
}
};
/** 处理报告厅图片 - 可能有多张图片,用逗号分隔 */
const getHallImages = (imageUrl: string): string[] => {
/** 处理报告厅图片 - 支持OSS ID和直接URL格式 */
const getHallImages = async (imageUrl: string): Promise<string[]> => {
if (!imageUrl || imageUrl.trim() === '') return [];
// 支持多种分隔符:逗号、分号、空格
const separators = /[,;|\s]+/;
const images = imageUrl
const items = imageUrl
.split(separators)
.map((url) => url.trim())
.filter((url) => url && url !== 'null' && url !== 'undefined');
.map((item) => item.trim())
.filter((item) => item && item !== 'null' && item !== 'undefined');
console.log('处理图片URL:', imageUrl, '结果:', images);
return images;
console.log('处理图片信息:', imageUrl, '结果:', items);
if (items.length === 0) return [];
// 判断是否为数字(OSS ID)还是URL
const isOssId = items.every((item) => /^\d+$/.test(item));
if (isOssId) {
// 如果是OSS ID,需要调用接口获取URL
console.log('检测到OSS ID格式,需要调用接口获取URL');
try {
// 动态导入OSS模块
const { listByIds } = await import('@/api/system/oss');
// 批量获取OSS信息
const ossIdsParam = items.join(',');
const res = await listByIds(ossIdsParam);
const ossList = res.data || [];
// 获取图片URL列表
const imageUrls: string[] = [];
for (const ossInfo of ossList) {
if (ossInfo.url) {
imageUrls.push(ossInfo.url);
}
}
console.log('获取到的图片URL:', imageUrls);
return imageUrls;
} catch (error) {
console.error('获取OSS信息失败:', error);
return [];
}
} else {
// 如果是URL格式,直接返回
console.log('检测到URL格式,直接使用');
return items;
}
};
/** 处理图片加载成功 */
@@ -744,7 +810,7 @@ const handleView = async (row: HallReservationVO) => {
if (viewForm.value.hallName) {
const selectedHall = hallList.value.find((hall) => hall.hallName === viewForm.value.hallName);
if (selectedHall && selectedHall.hallImage) {
viewHallImages.value = getHallImages(selectedHall.hallImage);
viewHallImages.value = selectedHall.hallImage;
} else {
viewHallImages.value = [];
}
@@ -787,8 +853,9 @@ const handleUpdate = async (row?: HallReservationVO) => {
// 如果有报告厅名称,找到对应的报告厅并显示图片
if (form.value.hallName) {
const selectedHall = hallList.value.find((hall) => hall.hallName === form.value.hallName);
console.log('selectedHall', selectedHall);
if (selectedHall && selectedHall.hallImage) {
selectedHallImages.value = getHallImages(selectedHall.hallImage);
selectedHallImages.value = selectedHall.hallImage;
}
}
@@ -0,0 +1,356 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="sc_showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="sc_queryFormRef" :model="sc_queryParams" :inline="true">
<!-- <el-form-item label="删除标志" prop="deleted">
<el-select v-model="queryParams.deleted" placeholder="请选择删除标志" clearable>
<el-option v-for="dict in sys_item_delete" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item> -->
<el-form-item label="分类编码" prop="categoryCode">
<el-input v-model="sc_queryParams.categoryCode" placeholder="请输入分类编码" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="分类名称" prop="categoryName">
<el-input v-model="sc_queryParams.categoryName" placeholder="请输入分类名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<!-- <el-form-item label="分类描述" prop="categoryDesc">
<el-input v-model="sc_queryParams.categoryDesc" placeholder="请输入分类描述" clearable @keyup.enter="handleQuery" />
</el-form-item> -->
<!-- <el-form-item label="排序" prop="sortOrder">
<el-input v-model="sc_queryParams.sortOrder" placeholder="请输入排序" clearable @keyup.enter="handleQuery" />
</el-form-item> -->
<el-form-item label="状态" prop="status">
<el-select v-model="sc_queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in sys_item_enable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['hq:satCategory:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Edit" :disabled="sc_single" @click="handleUpdate()" v-hasPermi="['hq:satCategory:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="sc_multiple" @click="handleDelete()" v-hasPermi="['hq:satCategory:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:satCategory:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="sc_showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<div class="table-container">
<el-table v-loading="sc_loading" border :data="sc_satCategoryList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="主键ID" align="center" prop="id" v-if="true" /> -->
<!-- <el-table-column label="删除标志" align="center" prop="deleted">
<template #default="scope">
<dict-tag :options="sys_item_delete" :value="scope.row.deleted"/>
</template>
</el-table-column> -->
<el-table-column label="分类编码" align="center" prop="categoryCode" />
<el-table-column label="分类名称" align="center" prop="categoryName" />
<el-table-column label="分类描述" align="center" prop="categoryDesc" />
<!-- <el-table-column label="排序" align="center" prop="sortOrder" /> -->
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_item_enable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['hq:satCategory:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['hq:satCategory:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-container">
<pagination
v-show="sc_total > 0"
:total="sc_total"
v-model:page="sc_queryParams.pageNum"
v-model:limit="sc_queryParams.pageSize"
@pagination="getList"
/>
</div>
</el-card>
<!-- 添加或修改题目分类对话框 -->
<el-dialog :title="sc_dialog.title" v-model="sc_dialog.visible" width="500px" append-to-body>
<el-form ref="sc_satCategoryFormRef" :model="sc_form" :rules="sc_rules" label-width="80px">
<!-- <el-form-item label="删除标志" prop="deleted">
<el-select v-model="form.deleted" placeholder="请选择删除标志">
<el-option v-for="dict in sys_item_delete" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
</el-select>
</el-form-item> -->
<el-form-item label="分类编码" prop="categoryCode">
<el-input v-model="sc_form.categoryCode" placeholder="请输入分类编码" />
</el-form-item>
<el-form-item label="分类名称" prop="categoryName">
<el-input v-model="sc_form.categoryName" placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="分类描述" prop="categoryDesc">
<el-input v-model="sc_form.categoryDesc" type="textarea" resize="none" :autosize="false" :rows="4" placeholder="请输入内容" />
</el-form-item>
<!-- <el-form-item label="排序" prop="sortOrder">
<el-input v-model="sc_form.sortOrder" placeholder="请输入排序" />
</el-form-item> -->
<el-form-item label="状态" prop="status">
<el-select v-model="sc_form.status" placeholder="请选择状态">
<el-option v-for="dict in sys_item_enable" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="sc_form.remark" type="textarea" resize="none" :autosize="false" :rows="4" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="sc_buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SatCategory" lang="ts">
import { ref, reactive, toRefs, getCurrentInstance, onMounted } from 'vue';
import type { ComponentInternalInstance, ElFormInstance } from 'vue';
import { listSatCategory, getSatCategory, delSatCategory, addSatCategory, updateSatCategory } from '@/api/hq/satCategory';
import { SatCategoryVO, SatCategoryQuery, SatCategoryForm } from '@/api/hq/satCategory/types';
// 类型定义
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<T, Q> {
form: T;
queryParams: Q;
rules: Record<string, any>;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_item_enable, sys_item_delete } = toRefs<any>(proxy?.useDict('sys_item_enable', 'sys_item_delete'));
const sc_satCategoryList = ref<SatCategoryVO[]>([]);
const sc_buttonLoading = ref(false);
const sc_loading = ref(true);
const sc_showSearch = ref(true);
const sc_ids = ref<Array<string | number>>([]);
const sc_single = ref(true);
const sc_multiple = ref(true);
const sc_total = ref(0);
const sc_queryFormRef = ref<ElFormInstance>();
const sc_satCategoryFormRef = ref<ElFormInstance>();
const sc_dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const sc_initFormData: SatCategoryForm = {
id: undefined,
deleted: undefined,
categoryCode: undefined,
categoryName: undefined,
categoryDesc: undefined,
sortOrder: undefined,
status: '0',
remark: undefined
};
const sc_data = reactive<PageData<SatCategoryForm, SatCategoryQuery>>({
form: { ...sc_initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
deleted: undefined,
categoryCode: undefined,
categoryName: undefined,
categoryDesc: undefined,
sortOrder: undefined,
status: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
categoryCode: [{ required: true, message: '分类编码不能为空', trigger: 'blur' }],
categoryName: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }]
}
});
const { queryParams: sc_queryParams, form: sc_form, rules: sc_rules } = toRefs(sc_data);
/** 查询题目分类列表 */
const getList = async () => {
sc_loading.value = true;
const res = await listSatCategory(sc_queryParams.value);
sc_satCategoryList.value = res.rows;
sc_total.value = res.total;
sc_loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
sc_dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
sc_form.value = { ...sc_initFormData };
sc_satCategoryFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
sc_queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
sc_queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: SatCategoryVO[]) => {
sc_ids.value = selection.map((item) => item.id);
sc_single.value = selection.length != 1;
sc_multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
sc_dialog.visible = true;
sc_dialog.title = '添加题目分类';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: SatCategoryVO) => {
reset();
const _id = row?.id || sc_ids.value[0];
const res = await getSatCategory(_id);
Object.assign(sc_form.value, res.data);
sc_dialog.visible = true;
sc_dialog.title = '修改题目分类';
};
/** 提交按钮 */
const submitForm = () => {
sc_satCategoryFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
sc_buttonLoading.value = true;
if (sc_form.value.id) {
await updateSatCategory(sc_form.value).finally(() => (sc_buttonLoading.value = false));
} else {
await addSatCategory(sc_form.value).finally(() => (sc_buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
sc_dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: SatCategoryVO) => {
const _ids = row?.id || sc_ids.value;
let confirmMessage = '';
if (row) {
// 单个删除
confirmMessage = `是否确认删除题目分类\"${row.categoryName}(${row.categoryCode})\"的数据项?\n\n【提示】本操作为逻辑删除,数据不会被物理移除,可在后台恢复。`;
} else {
// 批量删除
const selectedRows = sc_satCategoryList.value.filter((item) => sc_ids.value.includes(item.id));
if (selectedRows.length === 1) {
confirmMessage = `是否确认删除题目分类\"${selectedRows[0].categoryName}(${selectedRows[0].categoryCode})\"的数据项?\n\n【提示】本操作为逻辑删除,数据不会被物理移除,可在后台恢复。`;
} else {
const categoryNames = selectedRows.map((item) => `${item.categoryName}(${item.categoryCode})`).join('、');
confirmMessage = `是否确认删除题目分类\"${categoryNames}\"的数据项?\n\n【提示】本操作为逻辑删除,数据不会被物理移除,可在后台恢复。`;
}
}
await proxy?.$modal.confirm(confirmMessage).finally(() => (sc_loading.value = false));
await delSatCategory(_ids);
proxy?.$modal.msgSuccess('已逻辑删除');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'hq/satCategory/export',
{
...sc_queryParams.value
},
`satCategory_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 表格主体 */
.table-container {
margin-bottom: 0;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
</style>
+2 -2
View File
@@ -47,9 +47,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:satCategory:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:satCategory:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
+2 -2
View File
@@ -39,9 +39,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:satDetail:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:satDetail:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
+2 -2
View File
@@ -44,9 +44,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:satOption:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:satOption:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -0,0 +1,738 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="sq_proxy?.animate.searchAnimate.enter" :leave-active-class="sq_proxy?.animate.searchAnimate.leave">
<div ref="sq_searchRef" v-show="sq_showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="sq_queryFormRef" :model="sq_queryParams" :inline="true">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="题目标题" prop="questionTitle">
<el-input v-model="sq_queryParams.questionTitle" placeholder="请输入题目标题" clearable @keyup.enter="sq_handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属分类" prop="categoryId">
<el-select v-model="sq_queryParams.categoryId" placeholder="请选择所属分类" clearable @change="sq_handleQuery">
<el-option v-for="category in sq_satCategoryList" :key="category.id" :label="category.categoryName" :value="category.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="题目类型" prop="questionType">
<el-select v-model="sq_queryParams.questionType" placeholder="请选择题目类型" clearable>
<el-option v-for="dict in sq_sys_question_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="sq_queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in sq_sys_item_enable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="sq_handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="sq_resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row ref="sq_toolbarRef" :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="sq_handleAdd" v-hasPermi="['hq:satQuestion:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Edit" :disabled="sq_single" @click="sq_handleUpdate()" v-hasPermi="['hq:satQuestion:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="sq_multiple" @click="sq_handleDelete()" v-hasPermi="['hq:satQuestion:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="sq_handleExport" v-hasPermi="['hq:satQuestion:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch="sq_showSearch" @queryTable="sq_getList"></right-toolbar>
</el-row>
</template>
<div class="table-container">
<el-table v-loading="sq_loading" border :data="sq_satQuestionList" @selection-change="sq_handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="题目标题" align="center" prop="questionTitle" width="200">
<template #default="scope">
<el-tooltip
effect="dark"
:content="scope.row.questionTitle"
placement="top"
:show-after="100"
:hide-after="100"
popper-style="max-width: 300px; max-height: 200px;"
>
<div class="text-ellipsis">
{{ scope.row.questionTitle }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="所属分类" align="center" prop="categoryId">
<template #default="scope">
{{ sq_satCategoryAllList.find((cat) => cat.id == scope.row.categoryId)?.categoryName || '--' }}
</template>
</el-table-column>
<el-table-column label="分值" align="center" prop="score">
<template #default="scope">
{{ scope.row.score }}
</template>
</el-table-column>
<el-table-column label="题目类型" align="center" prop="questionType">
<template #default="scope">
<dict-tag :options="sq_sys_question_category" :value="scope.row.questionType" />
</template>
</el-table-column>
<el-table-column label="是否必填" align="center" prop="isRequired">
<template #default="scope">
<dict-tag :options="sq_sys_item_required" :value="scope.row.isRequired" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sq_sys_item_enable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" width="200">
<template #default="scope">
<el-tooltip
effect="dark"
:content="scope.row.remark"
placement="top"
:show-after="100"
:hide-after="100"
popper-style="max-width: 300px; max-height: 200px;"
>
<div class="text-ellipsis">
{{ scope.row.remark }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="设置选项" placement="top" v-if="scope.row.questionType === '1' || scope.row.questionType === '2'">
<el-button link type="success" icon="List" @click="sq_handleOptionManage(scope.row)" v-hasPermi="['hq:satOption:list']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="sq_handleUpdate(scope.row)" v-hasPermi="['hq:satQuestion:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="sq_handleDelete(scope.row)" v-hasPermi="['hq:satQuestion:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-container">
<pagination
v-show="sq_total > 0"
:total="sq_total"
:page="sq_queryParams.pageNum"
:limit="sq_queryParams.pageSize"
@pagination="sq_getList"
/>
</div>
</el-card>
<!-- 添加或修改题库对话框 -->
<el-dialog :title="sq_dialog.title" v-model="sq_dialog.visible" width="500px" append-to-body>
<el-form ref="sq_satQuestionFormRef" :model="sq_form" :rules="sq_rules" label-width="80px">
<el-form-item label="题目标题" prop="questionTitle">
<el-input v-model="sq_form.questionTitle" type="textarea" resize="none" :autosize="false" :rows="4" placeholder="请输入题目标题" />
</el-form-item>
<el-form-item label="所属分类" prop="categoryId">
<el-select v-model="sq_form.categoryId" placeholder="请选择所属分类">
<el-option v-for="category in sq_satCategoryList" :key="category.id" :label="category.categoryName" :value="category.id" />
</el-select>
</el-form-item>
<el-form-item label="题目类型" prop="questionType">
<el-select v-model="sq_form.questionType" placeholder="请选择题目类型">
<el-option v-for="dict in sq_sys_question_category" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="是否必填" prop="isRequired">
<el-select v-model="sq_form.isRequired" placeholder="请选择是否必填">
<el-option v-for="dict in sq_sys_item_required" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="sq_form.status" placeholder="请选择状态">
<el-option v-for="dict in sq_sys_item_enable" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="sq_form.remark" type="textarea" resize="none" :autosize="false" :rows="4" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="sq_buttonLoading" type="primary" @click="sq_submitForm">确 定</el-button>
<el-button @click="sq_cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 选项管理对话框 -->
<el-dialog :title="`管理题目【${sq_optionDialog.question?.questionTitle}】的选项`" v-model="sq_optionDialog.visible" width="850px" append-to-body>
<div>
<el-button type="primary" icon="Plus" @click="sq_addOption" size="small" class="mb-2">新增选项</el-button>
<el-button type="success" icon="Menu" @click="sq_addDefaultOptions" size="small" class="mb-2 ml-2">默认选项</el-button>
<el-table :data="sq_optionDialog.options" border size="small">
<el-table-column label="内容" prop="optionContent" width="295">
<template #default="scope">
<el-input v-model="scope.row.optionContent" placeholder="请输入选项内容" size="small" />
</template>
</el-table-column>
<el-table-column label="分值" prop="optionScore" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionScore" :min="0" :max="100" size="small" />
</template>
</el-table-column>
<el-table-column label="排序" prop="optionOrder" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionOrder" :min="0" :max="99" size="small" />
</template>
</el-table-column>
<el-table-column label="默认" prop="isDefault" width="90">
<template #default="scope">
<el-switch v-model="scope.row.isDefault" active-value="Y" inactive-value="N" />
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button icon="Delete" type="danger" size="small" @click="sq_removeOption(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="sq_saveOptions" :loading="sq_optionDialog.saveLoading">保存</el-button>
<el-button @click="sq_optionDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SqSatQuestion" lang="ts">
import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
import { listSatQuestion, getSatQuestion, delSatQuestion, addSatQuestion, updateSatQuestion } from '@/api/hq/satQuestion';
import { SatQuestionVO, SatQuestionQuery, SatQuestionForm } from '@/api/hq/satQuestion/types';
import { listSatCategory, listAllSatCategory } from '@/api/hq/satCategory';
import { SatCategoryVO } from '@/api/hq/satCategory/types';
import { nextTick } from 'vue';
import { listSatOption, addSatOption, updateSatOption, delSatOption } from '@/api/hq/satOption';
// 定义类型接口
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<T, Q> {
form: T;
queryParams: Q;
rules: any;
}
const { proxy: sq_proxy } = getCurrentInstance() as any;
const {
sys_question_category: sq_sys_question_category,
sys_item_enable: sq_sys_item_enable,
sys_item_required: sq_sys_item_required,
sys_item_delete: sq_sys_item_delete
} = toRefs<any>(sq_proxy?.useDict('sys_question_category', 'sys_item_enable', 'sys_item_required', 'sys_item_delete'));
const sq_satQuestionList = ref<SatQuestionVO[]>([]);
const sq_satCategoryList = ref<SatCategoryVO[]>([]);
const sq_satCategoryAllList = ref<SatCategoryVO[]>([]);
const sq_buttonLoading = ref(false);
const sq_loading = ref(true);
const sq_showSearch = ref(true);
const sq_ids = ref<Array<string | number>>([]);
const sq_single = ref(true);
const sq_multiple = ref(true);
const sq_total = ref(0);
const sq_queryFormRef = ref<any>();
const sq_satQuestionFormRef = ref<any>();
const sq_dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const sq_optionDialog = reactive({
visible: false,
question: null as SatQuestionVO | null,
options: [] as any[],
saveLoading: false
});
const sq_initFormData: SatQuestionForm = {
id: undefined,
deleted: undefined,
questionTitle: undefined,
questionContent: undefined,
categoryId: undefined,
questionType: '1',
isRequired: 1,
sortOrder: undefined,
status: '0',
remark: undefined
};
const sq_data = reactive<PageData<SatQuestionForm, SatQuestionQuery>>({
form: { ...sq_initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
deleted: undefined,
questionTitle: undefined,
questionContent: undefined,
categoryId: undefined,
questionType: undefined,
isRequired: undefined,
sortOrder: undefined,
status: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
questionTitle: [{ required: true, message: '题目标题不能为空', trigger: 'blur' }],
categoryId: [{ required: true, message: '所属分类不能为空', trigger: 'blur' }]
}
});
const { queryParams: sq_queryParams, form: sq_form, rules: sq_rules } = toRefs(sq_data);
/** 查询题库列表 */
const sq_getList = async () => {
sq_loading.value = true;
const res = await listSatQuestion(sq_queryParams.value);
sq_satQuestionList.value = res.rows;
sq_total.value = res.total;
sq_loading.value = false;
};
/** 获取分类列表 */
const sq_getCategoryList = async () => {
const res = await listSatCategory({
pageNum: 1,
pageSize: 1000,
status: '0'
});
sq_satCategoryList.value = res.rows.sort((a, b) => a.sortOrder - b.sortOrder);
};
/** 获取全部分类列表(包括已删除的) */
const sq_getAllCategoryList = async () => {
const res = await listAllSatCategory({
pageNum: 1,
pageSize: 1000
});
sq_satCategoryAllList.value = res.data.sort((a, b) => a.sortOrder - b.sortOrder);
};
/** 取消按钮 */
const sq_cancel = () => {
sq_reset();
sq_dialog.visible = false;
};
/** 表单重置 */
const sq_reset = () => {
sq_form.value = { ...sq_initFormData };
sq_satQuestionFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const sq_handleQuery = () => {
sq_queryParams.value.pageNum = 1;
sq_getList();
};
/** 重置按钮操作 */
const sq_resetQuery = () => {
sq_queryFormRef.value?.resetFields();
sq_handleQuery();
};
/** 多选框选中数据 */
const sq_handleSelectionChange = (selection: SatQuestionVO[]) => {
sq_ids.value = selection.map((item) => item.id);
sq_single.value = selection.length != 1;
sq_multiple.value = !selection.length;
};
/** 新增按钮操作 */
const sq_handleAdd = () => {
sq_reset();
sq_dialog.visible = true;
sq_dialog.title = '添加题库';
};
/** 修改按钮操作 */
const sq_handleUpdate = async (row?: SatQuestionVO) => {
sq_reset();
const _id = row?.id || sq_ids.value[0];
const res = await getSatQuestion(_id);
Object.assign(sq_form.value, res.data);
sq_dialog.visible = true;
sq_dialog.title = '修改题库';
};
/** 提交按钮 */
const sq_submitForm = () => {
sq_satQuestionFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
sq_buttonLoading.value = true;
if (sq_form.value.id) {
await updateSatQuestion(sq_form.value).finally(() => (sq_buttonLoading.value = false));
} else {
await addSatQuestion(sq_form.value).finally(() => (sq_buttonLoading.value = false));
}
sq_proxy?.$modal.msgSuccess('操作成功');
sq_dialog.visible = false;
await sq_getList();
}
});
};
/** 删除按钮操作 */
const sq_handleDelete = async (row?: SatQuestionVO) => {
const _ids = row?.id || sq_ids.value;
let confirmMessage = '';
if (row) {
confirmMessage = `是否确认删除题目"${row.questionTitle}"?`;
} else {
confirmMessage = `是否确认删除选中的${sq_ids.value.length}个题目?`;
}
await sq_proxy?.$modal.confirm(confirmMessage).finally(() => (sq_loading.value = false));
await delSatQuestion(_ids);
sq_proxy?.$modal.msgSuccess('删除成功');
await sq_getList();
};
/** 导出按钮操作 */
const sq_handleExport = () => {
sq_proxy?.download(
'hq/satQuestion/export',
{
...sq_queryParams.value
},
`satQuestion_${new Date().getTime()}.xlsx`
);
};
const sq_toolbarRef = ref<HTMLElement>();
const sq_searchRef = ref<HTMLElement>();
const sq_handleOptionManage = async (row: SatQuestionVO) => {
sq_optionDialog.visible = true;
sq_optionDialog.question = row;
sq_proxy?.$modal.loading('正在加载选项数据...');
try {
const res = await listSatOption({
questionId: row.id,
pageNum: 1,
pageSize: 100
});
sq_optionDialog.options = res.rows.map((item) => ({
id: item.id,
questionId: item.questionId,
optionContent: item.optionContent,
optionScore: item.optionScore,
optionOrder: item.optionOrder,
isDefault: item.isDefault || 'N',
tenantId: item.tenantId,
createBy: item.createBy,
createTime: item.createTime,
updateBy: item.updateBy,
updateTime: item.updateTime,
deleted: item.deleted
}));
sq_optionDialog.options.sort((a, b) => a.optionOrder - b.optionOrder);
} catch (error) {
console.error('加载选项失败:', error);
sq_optionDialog.options = [];
sq_proxy?.$modal.msgError('选项数据加载失败,请重试');
} finally {
sq_proxy?.$modal.closeLoading();
}
};
const sq_addOption = () => {
sq_optionDialog.options.push({
id: undefined,
questionId: sq_optionDialog.question?.id,
optionContent: '',
optionScore: 0,
optionOrder: sq_optionDialog.options.length + 1,
isDefault: 'N',
tenantId: '',
createBy: '',
createTime: '',
updateBy: '',
updateTime: '',
deleted: '0'
});
nextTick(() => {
// 可选:自动聚焦新行
});
};
const sq_addDefaultOptions = () => {
const defaultOptions = [
{
optionContent: '非常满意',
optionScore: 5,
optionOrder: 0,
isDefault: 'Y'
},
{
optionContent: '满意',
optionScore: 3,
optionOrder: 1,
isDefault: 'N'
},
{
optionContent: '一般满意',
optionScore: 1,
optionOrder: 2,
isDefault: 'N'
},
{
optionContent: '不满意',
optionScore: 0,
optionOrder: 3,
isDefault: 'N'
}
];
defaultOptions.forEach((option) => {
sq_optionDialog.options.push({
id: undefined,
questionId: sq_optionDialog.question?.id,
optionContent: option.optionContent,
optionScore: option.optionScore,
optionOrder: option.optionOrder,
isDefault: option.isDefault,
tenantId: '',
createBy: '',
createTime: '',
updateBy: '',
updateTime: '',
deleted: '0'
});
});
sq_proxy?.$modal.msgSuccess('已添加默认选项');
};
const sq_removeOption = (index: number) => {
sq_optionDialog.options.splice(index, 1);
};
const sq_saveOptions = async () => {
if (!sq_optionDialog.question?.id) {
sq_proxy?.$modal.msgError('题目ID不能为空');
return;
}
if (sq_optionDialog.saveLoading) {
return;
}
sq_optionDialog.saveLoading = true;
sq_proxy?.$modal.loading('正在保存选项数据...');
try {
const res = await listSatOption({
questionId: sq_optionDialog.question.id,
pageNum: 1,
pageSize: 100
});
const existingOptions = res.rows || [];
const promises = [];
for (const option of sq_optionDialog.options) {
if (!option.optionContent || option.optionContent.trim() === '') {
continue;
}
const params = {
id: option.id,
questionId: sq_optionDialog.question.id,
optionContent: option.optionContent,
optionScore: option.optionScore || 0,
optionOrder: option.optionOrder || 1,
status: '0',
isDefault: option.isDefault || 'N'
};
if (option.id) {
promises.push(updateSatOption(params));
} else {
promises.push(addSatOption(params));
}
}
const currentOptionIds = sq_optionDialog.options.filter((opt) => opt.id).map((opt) => opt.id);
const optionsToDelete = existingOptions.filter((opt) => !currentOptionIds.includes(opt.id)).map((opt) => opt.id);
if (optionsToDelete.length > 0) {
promises.push(delSatOption(optionsToDelete));
}
await Promise.all(promises);
const validOptions = sq_optionDialog.options.filter((opt) => opt.optionContent && opt.optionContent.trim() !== '');
let score = 0;
if (sq_optionDialog.question.questionType === '1') {
score = Math.max(...validOptions.map((opt) => Number(opt.optionScore) || 0));
} else if (sq_optionDialog.question.questionType === '2') {
score = validOptions.reduce((sum, opt) => sum + (Number(opt.optionScore) || 0), 0);
}
if (score > 0) {
await updateSatQuestion({
id: sq_optionDialog.question.id,
categoryId: sq_optionDialog.question.categoryId,
questionType: sq_optionDialog.question.questionType,
isRequired: sq_optionDialog.question.isRequired,
sortOrder: sq_optionDialog.question.sortOrder,
status: sq_optionDialog.question.status,
remark: sq_optionDialog.question.remark,
questionTitle: sq_optionDialog.question.questionTitle,
questionContent: sq_optionDialog.question.questionContent,
score: score
});
}
await sq_getList();
sq_proxy?.$modal.msgSuccess('保存成功');
const currentQuestion = { ...sq_optionDialog.question };
const optRes = await listSatOption({
questionId: currentQuestion.id,
pageNum: 1,
pageSize: 100
});
sq_optionDialog.options = optRes.rows.map((item) => ({
id: item.id,
questionId: item.questionId,
optionContent: item.optionContent,
optionScore: item.optionScore,
optionOrder: item.optionOrder,
isDefault: item.isDefault || 'N',
tenantId: item.tenantId,
createBy: item.createBy,
createTime: item.createTime,
updateBy: item.updateBy,
updateTime: item.updateTime,
deleted: item.deleted
}));
sq_optionDialog.options.sort((a, b) => a.optionOrder - b.optionOrder);
} catch (error) {
console.error('保存选项失败:', error);
sq_proxy?.$modal.msgError('保存失败,请重试');
} finally {
sq_proxy?.$modal.closeLoading();
sq_optionDialog.saveLoading = false;
}
};
onMounted(() => {
sq_getList();
sq_getCategoryList();
sq_getAllCategoryList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 表格容器 */
.table-container {
margin-bottom: 0;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 文字省略样式 */
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
display: inline-block;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
/* 左边距类 */
.ml-2 {
margin-left: 8px;
}
</style>
+41 -41
View File
@@ -70,9 +70,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:satQuestion:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:satQuestion:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -212,46 +212,46 @@
</div>
</template>
</el-dialog>
</div>
<el-dialog :title="`管理题目【${optionDialog.question?.questionTitle}】的选项`" v-model="optionDialog.visible" width="850px" append-to-body>
<div>
<el-button type="primary" icon="Plus" @click="addOption" size="small" class="mb-2">新增选项</el-button>
<el-button type="success" icon="Menu" @click="addDefaultOptions" size="small" class="mb-2 ml-2">默认选项</el-button>
<el-table :data="optionDialog.options" border size="small">
<el-table-column label="内容" prop="optionContent" width="295">
<template #default="scope">
<el-input v-model="scope.row.optionContent" placeholder="请输入选项内容" size="small" />
</template>
</el-table-column>
<el-table-column label="分值" prop="optionScore" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionScore" :min="0" :max="100" size="small" />
</template>
</el-table-column>
<el-table-column label="排序" prop="optionOrder" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionOrder" :min="0" :max="99" size="small" />
</template>
</el-table-column>
<el-table-column label="默认" prop="isDefault" width="90">
<template #default="scope">
<el-switch v-model="scope.row.isDefault" active-value="Y" inactive-value="N" />
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button icon="Delete" type="danger" size="small" @click="removeOption(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="saveOptions" :loading="optionDialog.saveLoading">保存</el-button>
<el-button @click="optionDialog.visible = false">关闭</el-button>
<el-dialog :title="`管理题目【${optionDialog.question?.questionTitle}】的选项`" v-model="optionDialog.visible" width="850px" append-to-body>
<div>
<el-button type="primary" icon="Plus" @click="addOption" size="small" class="mb-2">新增选项</el-button>
<el-button type="success" icon="Menu" @click="addDefaultOptions" size="small" class="mb-2 ml-2">默认选项</el-button>
<el-table :data="optionDialog.options" border size="small">
<el-table-column label="内容" prop="optionContent" width="295">
<template #default="scope">
<el-input v-model="scope.row.optionContent" placeholder="请输入选项内容" size="small" />
</template>
</el-table-column>
<el-table-column label="分值" prop="optionScore" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionScore" :min="0" :max="100" size="small" />
</template>
</el-table-column>
<el-table-column label="排序" prop="optionOrder" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionOrder" :min="0" :max="99" size="small" />
</template>
</el-table-column>
<el-table-column label="默认" prop="isDefault" width="90">
<template #default="scope">
<el-switch v-model="scope.row.isDefault" active-value="Y" inactive-value="N" />
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button icon="Delete" type="danger" size="small" @click="removeOption(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-dialog>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="saveOptions" :loading="optionDialog.saveLoading">保存</el-button>
<el-button @click="optionDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SatQuestion" lang="ts">
+55 -19
View File
@@ -214,9 +214,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['pm:repairOrder:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['pm:repairOrder:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -562,13 +562,15 @@
<div class="upload-file-card">
<!-- 缩略图区域 -->
<div class="upload-file-thumb">
<el-image
<!-- {{ file.url }} -->
<image-preview v-if="isImageFile(file) && file.url" :src="file.url" class="upload-thumb-image" :isOssId="true" />
<!-- <el-image
v-if="isImageFile(file) && file.url"
:src="file.url"
fit="cover"
:preview-src-list="[file.url]"
class="upload-thumb-image"
/>
/> -->
<div v-else class="upload-file-placeholder">
<el-icon><Document /></el-icon>
<span class="file-ext">{{ getFileExt(file.name) }}</span>
@@ -822,14 +824,15 @@
<div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-card">
<!-- 附件缩略图 -->
<div class="attachment-thumbnail-wrapper">
<el-image
<!-- <el-image
v-if="attachment.fileType === 'image'"
:src="attachment.filePath"
:preview-src-list="getImagePreviewList()"
fit="cover"
class="attachment-image"
lazy
/>
/> -->
<image-preview v-if="attachment.fileType === 'image'" :src="attachment.filePath" class="attachment-image" :isOssId="true" />
<div v-else class="attachment-file-icon">
<el-icon :size="28">
<Document v-if="attachment.fileType === 'document'" />
@@ -857,14 +860,14 @@
<!-- 附件操作 -->
<div class="attachment-card-actions">
<el-button-group size="small">
<el-button
<!-- <el-button
type="primary"
@click="previewAttachment(attachment)"
:disabled="!canPreview(attachment)"
:title="canPreview(attachment) ? '预览' : '不支持预览'"
>
<el-icon><View /></el-icon>
</el-button>
</el-button> -->
<el-button type="success" @click="downloadAttachment(attachment)" title="下载">
<el-icon><Download /></el-icon>
</el-button>
@@ -1230,6 +1233,7 @@ import { getInfo } from '@/api/login'; // 添加获取用户详细信息的API
import { getToken } from '@/utils/auth'; // 修复:添加getToken导入
import { vLoading, ElLoading } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import request from '@/utils/request';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -1271,7 +1275,7 @@ const workerOptions = ref<Array<{ value: string | number; label: string; userNam
// 附件相关数据
const attachmentList = ref<any[]>([]);
const uploadAction = ref('/dev-api/hq/pm/attachment/upload'); // 修复:使用正确的API路径
const uploadAction = ref(import.meta.env.VITE_APP_BASE_API + '/hq/pm/attachment/upload'); // 修复:使用正确的API路径
const uploadHeaders = ref({
'Authorization': `Bearer ${getToken() || ''}`,
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
@@ -1826,7 +1830,10 @@ const submitForm = () => {
buttonLoading.value = true;
try {
let orderId: string | number;
// todo form.attachementCount的数量等于附件栏中附件的数量
console.log('form.value', form.value);
console.log('attachmentList 长度:', attachmentList.value.length);
form.value.attachmentCount=attachmentList.value.length
if (form.value.id) {
await updateRepairOrder(form.value);
orderId = form.value.id;
@@ -2330,7 +2337,9 @@ const handleUploadSuccess = (response: any, file: any) => {
if (response.code === 200) {
proxy?.$modal.msgSuccess(`文件 ${file.name} 上传成功`);
// todo
console.log('orderDetail', orderDetail.value);
// orderDetail.value.attachmentCount++;
// 立即记录附件数据
const attachmentData = response.data;
if (attachmentData && attachmentData.id) {
@@ -2415,12 +2424,28 @@ const handleDownloadAttachment = async (file: any) => {
target: document.body
});
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
// const response = await fetch(downloadUrl);
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// 通过fetch获取文件内容,强制下载
// const response = await req('/resource/oss/download/' + attachment.filePath);
const response = await request({
url: '/resource/oss/download/' + downloadUrl,
method: 'get',
responseType: 'blob',
timeout: 300000 // 300秒超时
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
const blob = await response.blob();
// const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
@@ -2713,12 +2738,23 @@ const downloadAttachment = async (attachment: any) => {
});
// 通过fetch获取文件内容,强制下载
const response = await fetch(attachment.filePath);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
// const response = await req('/resource/oss/download/' + attachment.filePath);
const response = await request({
url: '/resource/oss/download/' + attachment.filePath,
method: 'get',
responseType: 'blob',
timeout: 300000 // 300秒超时
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
const blob = await response.blob();
// const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
// 创建下载链接
+2 -2
View File
@@ -68,9 +68,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:venue:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:venue:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
+2 -2
View File
@@ -97,9 +97,9 @@
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:reservation:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['hq:reservation:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
+2 -2
View File
@@ -93,10 +93,10 @@
<template #icon><Search /></template>
查询
</el-button>
<el-button @click="exportReport">
<!-- <el-button @click="exportReport">
<template #icon><Download /></template>
导出报表
</el-button>
</el-button> -->
</el-form-item>
</el-form>
</el-card>
@@ -1,147 +0,0 @@
<template>
<el-dialog
:title="mode === 'add' ? '新增' + getTypeLabel(type) : '编辑' + getTypeLabel(type)"
v-model="visible"
width="500px"
append-to-body
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<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 label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watchEffect, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'add'
},
type: {
type: String,
default: 'school'
},
admin: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:modelValue', 'submit']);
// 计算属性:对话框可见性
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
// 表单和校验
const formRef = ref(null);
const form = reactive({
id: null,
name: '',
phone: '',
sex: 0,
remark: ''
});
// 表单校验规则
const rules = reactive({
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
});
// 监听传入的admin数据,更新表单
watchEffect(() => {
if (props.admin) {
form.id = props.admin.id;
form.name = props.admin.name;
form.phone = props.admin.phone;
form.sex = props.admin.sex;
form.remark = props.admin.remark;
}
});
// 根据类型获取标签文本
function getTypeLabel(type) {
switch (type) {
case 'school':
return '全校管理员';
case 'custom':
return '自定义管理员';
case 'teacher':
return '班主任';
default:
return '管理员';
}
}
// 提交表单
function submitForm() {
formRef.value?.validate((valid) => {
if (valid) {
// 这里模拟保存
emit('submit', {
id: form.id,
name: form.name,
phone: form.phone,
sex: form.sex,
remark: form.remark,
scope: props.admin?.scope || ''
});
ElMessage.success('保存成功');
visible.value = false;
}
});
}
// 取消
function cancel() {
visible.value = false;
}
// 暴露组件以修复导出问题
defineExpose({
submitForm,
cancel
});
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>
@@ -1,165 +0,0 @@
<template>
<el-dialog :title="getScopeDialogTitle(type)" v-model="visible" width="600px" append-to-body destroy-on-close>
<el-form :model="form" label-width="100px">
<el-form-item :label="getScopeName(type)">
<div v-if="type === 'custom'">
<el-checkbox-group v-model="form.selectedScopes">
<el-checkbox v-for="item in scopeOptions.custom" :key="item.id" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
<div v-else-if="type === 'teacher'">
<el-checkbox-group v-model="form.selectedScopes">
<el-checkbox v-for="item in scopeOptions.teacher" :key="item.id" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
admin: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: 'custom'
}
});
const emit = defineEmits(['update:modelValue', 'submit']);
// 计算属性:对话框可见性
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
// 范围选项(模拟数据)
const scopeOptions = {
custom: [
{ id: 1, name: '高一年级' },
{ id: 2, name: '高二年级' },
{ id: 3, name: '高三年级' },
{ id: 4, name: '初一年级' },
{ id: 5, name: '初二年级' },
{ id: 6, name: '初三年级' }
],
teacher: [
{ id: 101, name: '高一(1)班' },
{ id: 102, name: '高一(2)班' },
{ id: 103, name: '高一(3)班' },
{ id: 104, name: '高二(1)班' },
{ id: 105, name: '高二(2)班' },
{ id: 106, name: '高三(1)班' },
{ id: 107, name: '高三(2)班' }
]
};
// 表单数据
const form = reactive({
selectedScopes: [] as number[]
});
// 获取范围对话框标题
function getScopeDialogTitle(type: string) {
const name = props.admin?.name || '';
switch (type) {
case 'custom':
return `为【${name}】分配管理范围`;
case 'teacher':
return `为【${name}】分配班级`;
default:
return `为【${name}】分配范围`;
}
}
// 获取范围名称
function getScopeName(type: string) {
switch (type) {
case 'custom':
return '管理范围';
case 'teacher':
return '所管理班级';
default:
return '管理范围';
}
}
// 提交表单
function submitForm() {
// 组装要提交的数据
const selectedIds = form.selectedScopes;
let scopeName = '';
// 根据类型获取选中的名称
if (type.value === 'custom') {
scopeName = scopeOptions.custom
.filter((item) => selectedIds.includes(item.id))
.map((item) => item.name)
.join(', ');
} else if (type.value === 'teacher') {
scopeName = scopeOptions.teacher
.filter((item) => selectedIds.includes(item.id))
.map((item) => item.name)
.join(', ');
}
// 提交
emit('submit', {
sourceIds: selectedIds,
scopeName: scopeName
});
ElMessage.success('分配成功');
visible.value = false;
}
// 取消
function cancel() {
visible.value = false;
}
// 从props获取类型的响应式引用
const type = computed(() => props.type);
// 暴露组件以修复导出问题
defineExpose({
submitForm,
cancel
});
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.el-checkbox-group {
display: flex;
flex-wrap: wrap;
}
.el-checkbox {
margin-right: 15px;
margin-bottom: 10px;
}
</style>
@@ -1,713 +0,0 @@
<template>
<div class="p-2">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" style="">
<el-card shadow="hover">
<el-input v-model="deptName" placeholder="请输入院系名称" prefix-icon="Search" clearable />
<el-tree
ref="deptTreeRef"
class="mt-2"
node-key="id"
:data="deptOptions"
:props="{ label: 'label', children: 'children' }"
:expand-on-click-node="false"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</el-col>
<el-col :lg="20" :xs="24">
<transition enter-active-class="animate__animated animate__fadeIn" leave-active-class="animate__animated animate__fadeOut">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" inline>
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="学生状态" clearable>
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" style="width: 308px">
<el-date-picker
v-model="dateRange"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="hover">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button :disabled="single" icon="Edit" @click="handleUpdate()"> 修改 </el-button>
</el-col>
<el-col :span="1.5">
<el-button :disabled="multiple" icon="Delete" @click="handleDelete()"> 删除 </el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown class="mt-[1px]">
<el-button>
更多
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
<el-dropdown-item icon="Top" @click="handleImport">导入数据</el-dropdown-item>
<el-dropdown-item icon="Download" @click="handleExport">导出数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
<right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="studentList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column v-if="columns[0].visible" key="studentId" label="学生编号" align="center" prop="studentId" />
<el-table-column v-if="columns[1].visible" key="studentNo" label="学号" align="center" prop="studentNo" :show-overflow-tooltip="true" />
<el-table-column
v-if="columns[2].visible"
key="studentName"
label="姓名"
align="center"
prop="studentName"
:show-overflow-tooltip="true"
/>
<el-table-column v-if="columns[3].visible" key="deptName" label="院系" align="center" prop="deptName" :show-overflow-tooltip="true" />
<el-table-column v-if="columns[4].visible" key="majorName" label="专业" align="center" prop="majorName" :show-overflow-tooltip="true" />
<el-table-column v-if="columns[5].visible" key="className" label="班级" align="center" prop="className" :show-overflow-tooltip="true" />
<el-table-column v-if="columns[6].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber" width="120" />
<el-table-column v-if="columns[7].visible" key="status" label="状态" align="center">
<template #default="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 v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160">
<template #default="scope">
<span>{{ scope.row.createTime }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="240" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="重置密码" placement="top">
<el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="详细信息" placement="top">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
<!-- 添加或修改学生配置对话框 -->
<el-dialog v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
<el-form ref="studentFormRef" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="form.studentName" placeholder="请输入学生姓名" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入昵称" maxlength="30" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="归属院系" prop="deptId">
<el-tree-select
v-model="form.deptId"
:data="deptOptions"
:props="{ value: 'id', label: 'label', children: 'children' }"
value-key="id"
placeholder="请选择归属院系"
check-strictly
@change="handleDeptChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="majorId">
<el-select v-model="form.majorId" placeholder="请选择专业" @change="handleMajorChange">
<el-option v-for="item in majorOptions" :key="item.majorId" :label="item.majorName" :value="item.majorId"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="班级" prop="classId">
<el-select v-model="form.classId" placeholder="请选择班级">
<el-option v-for="item in classOptions" :key="item.classId" :label="item.className" :value="item.classId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号" maxlength="18" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item v-if="form.studentId == undefined" label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="请输入学号" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.studentId == undefined" label="密码" prop="password">
<el-input v-model="form.password" placeholder="请输入密码" type="password" maxlength="20" show-password />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="学生性别">
<el-select v-model="form.sex" placeholder="请选择">
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" type="textarea" placeholder="请输入地址"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel()">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 学生详情对话框 -->
<el-dialog v-model="detailDialog.visible" :title="detailDialog.title" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学生姓名">{{ detailForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="学号">{{ detailForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="院系">{{ detailForm.deptName }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ detailForm.majorName }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ detailForm.className }}</el-descriptions-item>
<el-descriptions-item label="性别">
{{ detailForm.sex === '0' ? '男' : detailForm.sex === '1' ? '女' : '未知' }}
</el-descriptions-item>
<el-descriptions-item label="手机号码">{{ detailForm.phonenumber }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ detailForm.email }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detailForm.idCard }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="detailForm.status === '0' ? 'success' : 'danger'">
{{ detailForm.status === '0' ? '正常' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="地址" :span="2">{{ detailForm.address }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailForm.remark }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="detailDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 学生导入对话框 -->
<el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload">
<i-ep-upload-filled />
</el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="text-center el-upload__tip">
<div class="el-upload__tip"><el-checkbox v-model="upload.updateSupport" />是否更新已经存在的学生数据</div>
<span>仅允许导入xls、xlsx格式文件。</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import api from '@/api/studentMsg/index';
import { StudentForm, StudentQuery, StudentVO } from '@/api/studentMsg/types';
import { useRouter } from 'vue-router';
import { getCurrentInstance, ref, reactive, toRefs, watchEffect, onMounted } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import { to } from 'await-to-js';
import type { UploadFile, UploadInstance } from 'element-plus';
import type { FormInstance, TreeInstance } from 'element-plus';
interface ComponentInternalInstance {
proxy?: any;
}
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const sys_normal_disable = ref([
{ value: '0', label: '正常' },
{ value: '1', label: '停用' }
]);
const sys_user_sex = ref([
{ value: '0', label: '男' },
{ value: '1', label: '女' },
{ value: '2', label: '未知' }
]);
const studentList = ref<StudentVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<number | string>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const dateRange = ref<[string, string]>(['', '']);
const deptName = ref('');
const deptOptions = ref([]);
const majorOptions = ref([]);
const classOptions = ref([]);
// 列显隐信息
const columns = ref([
{ key: 0, label: `学生编号`, visible: false },
{ key: 1, label: `学号`, visible: true },
{ key: 2, label: `姓名`, visible: true },
{ key: 3, label: `院系`, visible: true },
{ key: 4, label: `专业`, visible: true },
{ key: 5, label: `班级`, visible: true },
{ key: 6, label: `手机号码`, visible: true },
{ key: 7, label: `状态`, visible: true },
{ key: 8, label: `创建时间`, visible: true }
]);
const deptTreeRef = ref<TreeInstance>();
const queryFormRef = ref<FormInstance>();
const studentFormRef = ref<FormInstance>();
const uploadRef = ref<UploadInstance>();
const initPassword = ref('123456');
const dialog = reactive({
visible: false,
title: ''
});
const detailDialog = reactive({
visible: false,
title: '学生详情'
});
const detailForm = reactive<StudentVO>({
studentId: '',
studentNo: '',
studentName: ''
});
// 导入参数
const upload = reactive({
// 是否显示弹出层
open: false,
// 弹出层标题
title: '',
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的数据
updateSupport: false,
// 设置上传的请求头部
headers: { Authorization: 'Bearer ' },
// 上传的地址
url: '/api/studentMsg/student/importData'
});
const initFormData: StudentForm = {
studentId: undefined,
deptId: undefined,
studentNo: '',
studentName: '',
nickName: '',
password: '',
phonenumber: '',
email: '',
idCard: '',
sex: '0',
status: '0',
majorId: undefined,
classId: undefined,
address: '',
remark: ''
};
const form = reactive<StudentForm>({
...initFormData
});
const rules = reactive({
studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }],
studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }],
password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
deptId: [{ required: true, message: '归属院系不能为空', trigger: 'blur' }],
majorId: [{ required: true, message: '专业不能为空', trigger: 'blur' }],
classId: [{ required: true, message: '班级不能为空', trigger: 'blur' }],
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }],
idCard: [{ pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '请输入正确的身份证号码', trigger: 'blur' }]
});
const queryParams = reactive<StudentQuery>({
pageNum: 1,
pageSize: 10,
studentName: '',
studentNo: '',
phonenumber: '',
status: '',
deptId: undefined
});
/** 通过条件过滤节点 */
const filterNode = (value: string, data: any) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/** 根据名称筛选部门树 */
watchEffect(
() => {
deptTreeRef.value?.filter(deptName.value);
},
{ flush: 'post' }
);
/** 查询学生列表 */
const getList = async () => {
loading.value = true;
const res = await api.listStudent(queryParams);
loading.value = false;
studentList.value = res.data.data.rows;
total.value = res.data.data.total;
};
/** 查询部门下拉树结构 */
const getDeptTree = async () => {
try {
const res = await api.XgDeptTreeSelect(101);
// XgDeptTreeSelect返回的数据结构与deptTreeSelect不同,需要进行适当处理
if (res.data && Array.isArray(res.data)) {
deptOptions.value = res.data;
} else {
console.warn('部门树数据格式不符合预期');
// 回退到默认处理
const backup = await api.deptTreeSelect();
deptOptions.value = backup.data.data;
}
} catch (error) {
console.error('获取部门树失败:', error);
// 如果API调用失败,可以使用默认的deptTreeSelect作为备选
const backup = await api.deptTreeSelect();
deptOptions.value = backup.data.data;
}
};
/** 节点单击事件 */
const handleNodeClick = (data: any) => {
queryParams.deptId = data.id;
handleQuery();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
dateRange.value = ['', ''];
queryFormRef.value?.resetFields();
queryParams.pageNum = 1;
queryParams.deptId = undefined;
deptTreeRef.value?.setCurrentKey(undefined);
handleQuery();
};
/** 删除按钮操作 */
const handleDelete = async (row?: StudentVO) => {
const studentIds = row?.studentId || ids.value;
const [err] = await to(ElMessageBox.confirm('是否确认删除学生编号为"' + studentIds + '"的数据项?', '提示'));
if (!err) {
await api.delStudent(studentIds);
await getList();
ElMessage.success('删除成功');
}
};
/** 学生状态修改 */
const handleStatusChange = async (row: StudentVO) => {
const text = row.status === '0' ? '启用' : '停用';
try {
await ElMessageBox.confirm('确认要"' + text + '""' + row.studentName + '"学生吗?', '提示');
await api.changeStudentStatus(row.studentId, row.status);
ElMessage.success(text + '成功');
} catch (err) {
row.status = row.status === '0' ? '1' : '0';
}
};
/** 查看详情按钮操作 */
const handleDetail = async (row: StudentVO) => {
Object.assign(detailForm, row);
detailDialog.visible = true;
};
/** 重置密码按钮操作 */
const handleResetPwd = async (row: StudentVO) => {
const [err, res] = await to(
ElMessageBox.prompt('请输入"' + row.studentName + '"的新密码', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
inputPattern: /^.{6,20}$/,
inputErrorMessage: '密码长度必须介于 6 和 20 之间'
})
);
if (!err && res) {
// 实际项目中需要调用重置密码API
ElMessage.success('修改成功,新密码是:' + res.value);
}
};
/** 选择条数 */
const handleSelectionChange = (selection: StudentVO[]) => {
ids.value = selection.map((item) => item.studentId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 导入按钮操作 */
const handleImport = () => {
upload.title = '学生导入';
upload.open = true;
};
/** 导出按钮操作 */
const handleExport = () => {
// 实际项目中需要调用导出API
ElMessage.success('导出成功');
};
/** 下载模板操作 */
const importTemplate = () => {
// 实际项目中需要调用下载模板API
ElMessage.success('下载模板成功');
};
/**文件上传中处理 */
const handleFileUploadProgress = () => {
upload.isUploading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = (response: any) => {
upload.open = false;
upload.isUploading = false;
uploadRef.value?.clearFiles();
ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
dangerouslyUseHTMLString: true
});
getList();
};
/** 提交上传文件 */
const submitFileForm = () => {
uploadRef.value?.submit();
};
/** 重置操作表单 */
const reset = () => {
Object.assign(form, initFormData);
studentFormRef.value?.resetFields();
majorOptions.value = [];
classOptions.value = [];
};
/** 取消按钮 */
const cancel = () => {
dialog.visible = false;
reset();
};
/** 新增按钮操作 */
const handleAdd = async () => {
reset();
dialog.visible = true;
dialog.title = '新增学生';
form.password = initPassword.value;
};
/** 修改按钮操作 */
const handleUpdate = async (row?: StudentForm) => {
reset();
const studentId = row?.studentId || ids.value[0];
const { data } = await api.getStudent(studentId);
dialog.visible = true;
dialog.title = '修改学生';
Object.assign(form, data.data.student);
majorOptions.value = data.data.majors;
classOptions.value = data.data.classes;
};
/** 院系变更事件 */
const handleDeptChange = (deptId: number) => {
form.majorId = undefined;
form.classId = undefined;
majorOptions.value = [];
classOptions.value = [];
// 根据院系ID过滤专业选项
const majors = api.getStudent().then((res) => {
return res.data.data.majors.filter((item: any) => item.deptId === deptId);
});
majors.then((data) => {
majorOptions.value = data;
});
};
/** 专业变更事件 */
const handleMajorChange = (majorId: number) => {
form.classId = undefined;
classOptions.value = [];
// 根据专业ID过滤班级选项
const classes = api.getStudent().then((res) => {
return res.data.data.classes.filter((item: any) => item.majorId === majorId);
});
classes.then((data) => {
classOptions.value = data;
});
};
/** 提交按钮 */
const submitForm = () => {
studentFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (form.studentId !== undefined) {
await api.updateStudent(form);
} else {
await api.addStudent(form);
}
ElMessage.success('操作成功');
dialog.visible = false;
await getList();
}
});
};
/**
* 关闭学生弹窗
*/
const closeDialog = () => {
dialog.visible = false;
reset();
};
onMounted(() => {
getDeptTree(); // 初始化部门数据
getList(); // 初始化列表数据
});
</script>
+93 -9
View File
@@ -63,7 +63,13 @@
<el-table-column label="显示顺序" prop="roleSort" width="100" />
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
<el-switch
v-model="scope.row.status"
active-value="0"
inactive-value="1"
:disabled="isSystemRoleInTable(scope.row)"
@change="handleStatusChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime">
@@ -77,14 +83,36 @@
<el-tooltip v-if="scope.row.roleId !== 1" content="修改" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="删除" placement="top">
<el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
<el-tooltip
v-if="scope.row.roleId !== 1"
:content="isSystemRoleInTable(scope.row) ? '系统内置角色不可删除,请前往【学生信息管理】->【班级干部职务配置】界面进行操作' : '删除'"
placement="top"
>
<el-button
v-hasPermi="['system:role:remove']"
link
type="primary"
icon="Delete"
:disabled="isSystemRoleInTable(scope.row)"
@click="handleDelete(scope.row)"
></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" @click="handleAuthUser(scope.row)"></el-button>
<el-tooltip
v-if="scope.row.roleId !== 1"
:content="isSystemRoleInTable(scope.row) ? '系统内置角色不可分配用户,请前往学生信息管理-班级干部信息界面进行操作' : '分配用户'"
placement="top"
>
<el-button
v-hasPermi="['system:role:edit']"
link
type="primary"
icon="User"
:disabled="isSystemRoleInTable(scope.row)"
@click="handleAuthUser(scope.row)"
></el-button>
</el-tooltip>
</template>
</el-table-column>
@@ -100,9 +128,18 @@
</el-card>
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<el-alert
v-if="isSystemRole"
title="系统角色提示"
type="warning"
description="此角色为系统内置角色,角色名称、权限字符、角色顺序、状态等基础信息不可修改,如需修改请前往【学生信息管理】->【班级干部职务配置】"
show-icon
:closable="false"
class="mb-4"
/>
<el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
<el-input v-model="form.roleName" placeholder="请输入角色名称" :disabled="isSystemRole" />
</el-form-item>
<el-form-item prop="roleKey">
<template #label>
@@ -113,13 +150,13 @@
权限字符
</span>
</template>
<el-input v-model="form.roleKey" placeholder="请输入权限字符" />
<el-input v-model="form.roleKey" placeholder="请输入权限字符" :disabled="isSystemRole" />
</el-form-item>
<el-form-item label="角色顺序" prop="roleSort">
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" :disabled="isSystemRole" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio-group v-model="form.status" :disabled="isSystemRole">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
@@ -192,6 +229,8 @@
</template>
<script setup name="Role" lang="ts">
import { ref, reactive, computed, toRefs, nextTick, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
import { useRouter } from 'vue-router';
import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from '@/api/system/role';
import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
import { RoleVO, RoleForm, RoleQuery, DeptTreeOption } from '@/api/system/role/types';
@@ -269,6 +308,25 @@ const dialog = reactive<DialogOption>({
title: ''
});
/** 判断是否为系统角色(权限字符以XT-BJZW-开头) */
const isSystemRole = computed(() => {
if (form.value.roleKey && form.value.roleKey.trim() !== '') {
return form.value.roleKey.startsWith('XT-BJZW-');
} else {
return false;
}
});
/** 判断表格中的角色是否为系统角色 */
const isSystemRoleInTable = (row: RoleVO) => {
if (row.roleKey && row.roleKey.trim() !== '') {
return row.roleKey.startsWith('XT-BJZW-');
} else {
return false;
}
// return row.roleKey && row.roleKey.trim() !== '' && row.roleKey.startsWith('XT-BJZW-');
};
/**
* 查询角色列表
*/
@@ -297,6 +355,12 @@ const resetQuery = () => {
};
/**删除按钮操作 */
const handleDelete = async (row?: RoleVO) => {
// 检查是否为系统角色
if (row && isSystemRoleInTable(row)) {
proxy?.$modal.msgError('系统内置角色不可删除,请前往学生信息管理-学生干部职务配置界面进行操作');
return;
}
const roleids = row?.roleId || ids.value;
await proxy?.$modal.confirm('是否确认删除角色编号为' + roleids + '数据项目');
await delRole(roleids);
@@ -323,6 +387,14 @@ const handleSelectionChange = (selection: RoleVO[]) => {
/** 角色状态修改 */
const handleStatusChange = async (row: RoleVO) => {
// 检查是否为系统角色
if (isSystemRoleInTable(row)) {
proxy?.$modal.msgError('系统内置角色状态不可修改,请前往学生信息管理-班级干部职务配置界面进行操作');
// 恢复原状态
row.status = row.status === '0' ? '1' : '0';
return;
}
const text = row.status === '0' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?');
@@ -335,6 +407,12 @@ const handleStatusChange = async (row: RoleVO) => {
/** 分配用户 */
const handleAuthUser = (row: RoleVO) => {
// 检查是否为系统角色
if (isSystemRoleInTable(row)) {
proxy?.$modal.msgError('系统内置角色不可分配用户,请前往学生信息管理-学生干部职务配置界面进行操作');
return;
}
router.push('/system/role-auth/user/' + row.roleId);
};
@@ -362,12 +440,17 @@ const reset = () => {
deptExpand.value = true;
deptNodeAll.value = false;
form.value = { ...initForm };
// 确保roleKey被明确重置为空字符串
form.value.roleKey = '';
roleFormRef.value?.resetFields();
};
/** 添加角色 */
const handleAdd = () => {
reset();
// 确保roleKey被正确重置为空字符串
form.value.roleKey = '';
console.log('新增角色 - roleKey:', `"${form.value.roleKey}"`, 'isSystemRole:', isSystemRole.value);
getMenuTreeselect();
dialog.visible = true;
dialog.title = '添加角色';
@@ -379,6 +462,7 @@ const handleUpdate = async (row?: RoleVO) => {
const { data } = await getRole(roleId);
Object.assign(form.value, data);
form.value.roleSort = Number(form.value.roleSort);
console.log('修改角色 - roleKey:', `"${form.value.roleKey}"`, 'isSystemRole:', isSystemRole.value);
const res = await getRoleMenuTreeselect(roleId);
dialog.title = '修改角色';
dialog.visible = true;
+146 -15
View File
@@ -1,16 +1,16 @@
<template>
<div class="p-2">
<el-row :gutter="20">
<div style="display: flex; gap: 16px">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" style="">
<div style="width: 360px">
<el-card shadow="hover">
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
<el-tree
ref="deptTreeRef"
class="mt-2"
class="mt-2 dept-tree"
node-key="id"
:data="deptOptions"
:props="{ label: 'label', children: 'children' } as any"
:props="{ label: 'label', children: 'children' }"
:expand-on-click-node="false"
:filter-node-method="filterNode"
highlight-current
@@ -18,8 +18,8 @@
@node-click="handleNodeClick"
/>
</el-card>
</el-col>
<el-col :lg="20" :xs="24">
</div>
<div style="flex: 1; min-width: 0; /* 防止内容撑开容器 */">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
@@ -63,13 +63,17 @@
<template #header>
<el-row :gutter="10">
<el-col :span="1.5">
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增(教职工)</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-has-permi="['system:user:edit']" :disabled="single" icon="Edit" @click="handleUpdate()"> 修改 </el-button>
<el-button v-has-permi="['system:user:edit']" :disabled="single || isSelectedStudentUser" icon="Edit" @click="handleUpdate()">
修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-has-permi="['system:user:remove']" :disabled="multiple" icon="Delete" @click="handleDelete()"> 删除 </el-button>
<el-button v-has-permi="['system:user:remove']" :disabled="multiple || hasSelectedStudentUser" icon="Delete" @click="handleDelete()">
删除
</el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown class="mt-[1px]">
@@ -100,7 +104,13 @@
<el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber" width="120" />
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
<el-switch
v-model="scope.row.status"
active-value="0"
inactive-value="1"
:disabled="scope.row.accountType === '2'"
@change="handleStatusChange(scope.row)"
></el-switch>
</template>
</el-table-column>
@@ -112,10 +122,10 @@
<el-table-column label="操作" fixed="right" width="280" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top">
<el-tooltip v-if="scope.row.userId !== 1 && scope.row.accountType !== '2'" content="修改" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top">
<el-tooltip v-if="scope.row.userId !== 1 && scope.row.accountType !== '2'" content="删除" placement="top">
<el-button v-hasPermi="['system:user:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
</el-tooltip>
@@ -126,6 +136,11 @@
<el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)"></el-button>
</el-tooltip>
<!-- 学生用户显示提示信息 -->
<el-tooltip v-if="scope.row.accountType === '2'" content="学生用户不允许操作" placement="top">
<span class="text-gray-400">学生用户</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
@@ -138,8 +153,8 @@
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
</div>
</div>
<!-- 添加或修改用户配置对话框 -->
<el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
@@ -155,7 +170,7 @@
<el-tree-select
v-model="form.deptId"
:data="enabledDeptOptions"
:props="{ value: 'id', label: 'label', children: 'children' } as any"
:props="{ value: 'id', label: 'label', children: 'children' }"
value-key="id"
placeholder="请选择归属部门"
check-strictly
@@ -316,6 +331,20 @@ const enabledDeptOptions = ref<DeptTreeVO[]>([]);
const initPassword = ref<string>('');
const postOptions = ref<PostVO[]>([]);
const roleOptions = ref<RoleVO[]>([]);
// 选中的用户相关计算属性
const selectedUsers = ref<UserVO[]>([]);
// 计算属性:是否选中了学生用户
const isSelectedStudentUser = computed(() => {
return selectedUsers.value.length === 1 && selectedUsers.value[0].accountType === '2';
});
// 计算属性:选中的用户中是否包含学生用户
const hasSelectedStudentUser = computed(() => {
return selectedUsers.value.some((user) => user.accountType === '2');
});
/*** 用户导入参数 */
const upload = reactive<ImportOption>({
// 是否显示弹出层(用户导入)
@@ -489,6 +518,19 @@ const resetQuery = () => {
/** 删除按钮操作 */
const handleDelete = async (row?: UserVO) => {
const userIds = row?.userId || ids.value;
// 检查是否是学生用户
if (row && row.accountType === '2') {
proxy?.$modal.msgError('学生用户不允许删除操作');
return;
}
// 检查批量删除时是否包含学生用户
if (!row && hasSelectedStudentUser.value) {
proxy?.$modal.msgError('选中的用户中包含学生用户,不允许删除操作');
return;
}
const [err] = await to(proxy?.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any);
if (!err) {
await api.delUser(userIds);
@@ -499,6 +541,14 @@ const handleDelete = async (row?: UserVO) => {
/** 用户状态修改 */
const handleStatusChange = async (row: UserVO) => {
// 检查是否是学生用户
if (row.accountType === '2') {
// 恢复原状态
row.status = row.status === '0' ? '1' : '0';
proxy?.$modal.msgError('学生用户不允许修改状态');
return;
}
const text = row.status === '0' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?');
@@ -510,12 +560,24 @@ const handleStatusChange = async (row: UserVO) => {
};
/** 跳转角色分配 */
const handleAuthRole = (row: UserVO) => {
// 检查是否是学生用户
// if (row.accountType === '2') {
// proxy?.$modal.msgError('学生用户不允许分配角色操作');
// return;
// }
const userId = row.userId;
router.push('/system/user-auth/role/' + userId);
};
/** 重置密码按钮操作 */
const handleResetPwd = async (row: UserVO) => {
// 检查是否是学生用户
// if (row.accountType === '2') {
// proxy?.$modal.msgError('学生用户不允许重置密码操作');
// return;
// }
const [err, res] = await to(
ElMessageBox.prompt('请输入"' + row.userName + '"的新密码', '提示', {
confirmButtonText: '确定',
@@ -538,6 +600,7 @@ const handleResetPwd = async (row: UserVO) => {
/** 选择条数 */
const handleSelectionChange = (selection: UserVO[]) => {
selectedUsers.value = selection;
ids.value = selection.map((item) => item.userId);
single.value = selection.length != 1;
multiple.value = !selection.length;
@@ -607,6 +670,18 @@ const handleAdd = async () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: UserForm) => {
// 检查是否是学生用户
if (row && (row as UserVO).accountType === '2') {
proxy?.$modal.msgError('学生用户不允许修改操作');
return;
}
// 检查选中的用户是否是学生用户
if (!row && isSelectedStudentUser.value) {
proxy?.$modal.msgError('学生用户不允许修改操作');
return;
}
reset();
const userId = row?.userId || ids.value[0];
const { data } = await api.getUser(userId);
@@ -664,3 +739,59 @@ async function handleDeptChange(value: number | string) {
form.value.postIds = [];
}
</script>
<style scoped>
/* 部门树组件样式 */
.dept-tree {
height: 600px;
overflow-y: auto;
overflow-x: auto;
width: 330px; /* 固定宽度,超出时显示滚动条 */
min-width: 330px;
}
/* 强制覆盖el-tree的默认样式 */
.dept-tree :deep(.el-tree-node) {
white-space: nowrap !important;
width: 100% !important;
min-width: 300px !important;
}
.dept-tree :deep(.el-tree-node__content) {
white-space: nowrap !important;
width: 100% !important;
min-width: 300px !important;
overflow: visible !important;
text-overflow: unset !important; /* 移除省略号 */
max-width: none !important;
padding-right: 20px !important; /* 增加右侧内边距,确保hover效果完整显示 */
}
/* 树节点hover效果优化 */
.dept-tree :deep(.el-tree-node__content:hover) {
background-color: var(--el-tree-node-hover-bg-color, #f5f7fa) !important;
}
.dept-tree :deep(.el-tree-node__label) {
white-space: nowrap !important;
display: inline-block !important;
flex-shrink: 0 !important;
max-width: none !important;
overflow: visible !important;
text-overflow: unset !important; /* 移除省略号效果 */
width: auto !important;
}
.dept-tree :deep(.el-tree-node__expand-icon) {
flex-shrink: 0 !important;
}
/* 确保树容器内部元素都不被截断 */
.dept-tree :deep(.el-tree-node__children) {
width: 100% !important;
}
.dept-tree :deep(.el-tree-node > .el-tree-node__children) {
width: 100% !important;
}
</style>
@@ -266,6 +266,8 @@ const handleStartWorkFlow = async (data: LeaveForm) => {
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// hzdyjs-qj 使用的流程变量
qjts: data.leaveDays,
// leave2/6 使用的流程变量
leaveDays: data.leaveDays,
// leave4/5 使用的流程变量
@@ -72,9 +72,9 @@
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['sslxx:dormBuilding:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -173,10 +173,10 @@ import {
updateDormBuilding,
listCampusOptions,
listAreaTreeData
} from '@/api/xgss/sslxx/dormBuilding';
import { DormBuildingVO, DormBuildingQuery, DormBuildingForm } from '@/api/xgss/sslxx/dormBuilding/types';
import { DormCampusVO } from '@/api/xgss/xqxx/dormCampus/types';
import { DormAreaVO } from '@/api/xgss/ssqxx/dormArea/types';
} from '@/api/xg/xgss/sslxx/dormBuilding';
import { DormBuildingVO, DormBuildingQuery, DormBuildingForm } from '@/api/xg/xgss/sslxx/dormBuilding/types';
import { DormCampusVO } from '@/api/xg/xgss/xqxx/dormCampus/types';
import { DormAreaVO } from '@/api/xg/xgss/ssqxx/dormArea/types';
import { getCurrentInstance, ComponentInternalInstance, ref, reactive, toRefs, onMounted } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -50,9 +50,9 @@
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['ssqxx:dormArea:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -112,10 +112,10 @@
</template>
<script setup name="DormArea" lang="ts">
import { listDormArea, getDormArea, delDormArea, addDormArea, updateDormArea } from '@/api/xgss/ssqxx/dormArea';
import { DormAreaVO, DormAreaQuery, DormAreaForm } from '@/api/xgss/ssqxx/dormArea/types';
import { listDormCampus } from '@/api/xgss/xqxx/dormCampus';
import type { DormCampusVO } from '@/api/xgss/xqxx/dormCampus/types';
import { listDormArea, getDormArea, delDormArea, addDormArea, updateDormArea } from '@/api/xg/xgss/ssqxx/dormArea';
import { DormAreaVO, DormAreaQuery, DormAreaForm } from '@/api/xg/xgss/ssqxx/dormArea/types';
import { listDormCampus } from '@/api/xg/xgss/xqxx/dormCampus';
import type { DormCampusVO } from '@/api/xg/xgss/xqxx/dormCampus/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -35,9 +35,9 @@
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['xqxx:dormCampus:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -94,8 +94,8 @@
</template>
<script setup name="DormCampus" lang="ts">
import { listDormCampus, getDormCampus, delDormCampus, addDormCampus, updateDormCampus } from '@/api/xgss/xqxx/dormCampus';
import { DormCampusVO, DormCampusQuery, DormCampusForm } from '@/api/xgss/xqxx/dormCampus/types';
import { listDormCampus, getDormCampus, delDormCampus, addDormCampus, updateDormCampus } from '@/api/xg/xgss/xqxx/dormCampus';
import { DormCampusVO, DormCampusQuery, DormCampusForm } from '@/api/xg/xgss/xqxx/dormCampus/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -38,8 +38,8 @@
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormBedForm } from '@/api/xgss/dormBed/types';
import { addDormBed, updateDormBed, getDormBed } from '@/api/xgss/dormBed';
import { DormBedForm } from '@/api/xg/xgss/dormBed/types';
import { addDormBed, updateDormBed, getDormBed } from '@/api/xg/xgss/dormBed';
const props = defineProps<{
modelValue: boolean;
@@ -50,8 +50,8 @@
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormRoomForm, DormRoomVO } from '@/api/xgss/dormRoom/types';
import { addDormRoom, updateDormRoom } from '@/api/xgss/dormRoom';
import { DormRoomForm, DormRoomVO } from '@/api/xg/xgss/dormRoom/types';
import { addDormRoom, updateDormRoom } from '@/api/xg/xgss/dormRoom';
const props = defineProps<{
modelValue: boolean;
@@ -213,12 +213,12 @@
import { ref, computed, reactive, onMounted, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { InfoFilled, Search } from '@element-plus/icons-vue';
import { DormTreeNode, BedInfo, DormQuery, BedForm } from '@/api/xgss/xsssfjgl/types';
import { DormRoomVO, DormRoomQuery } from '@/api/xgss/dormRoom/types';
import { getDormTree } from '@/api/xgss/xsssfjgl';
import { listDormBed, delDormBed, addDormBed } from '@/api/xgss/dormBed';
import type { DormBedQuery } from '@/api/xgss/dormBed/types';
import { listDormRoom, delDormRoom, addDormRoom } from '@/api/xgss/dormRoom';
import { DormTreeNode, BedInfo, DormQuery, BedForm } from '@/api/xg/xgss/xsssfjgl/types';
import { DormRoomVO, DormRoomQuery } from '@/api/xg/xgss/dormRoom/types';
import { getDormTree } from '@/api/xg/xgss/xsssfjgl';
import { listDormBed, delDormBed, addDormBed } from '@/api/xg/xgss/dormBed';
import type { DormBedQuery } from '@/api/xg/xgss/dormBed/types';
import { listDormRoom, delDormRoom, addDormRoom } from '@/api/xg/xgss/dormRoom';
// 引入子组件
import RoomDialog from './components/RoomDialog.vue';
@@ -38,8 +38,8 @@
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormBedForm } from '@/api/xgss/dormBed/types';
import { addDormBed, updateDormBed, getDormBed } from '@/api/xgss/dormBed';
import { DormBedForm } from '@/api/xg/xgss/dormBed/types';
import { addDormBed, updateDormBed, getDormBed } from '@/api/xg/xgss/dormBed';
const props = defineProps<{
modelValue: boolean;
@@ -50,8 +50,8 @@
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormRoomForm, DormRoomVO } from '@/api/xgss/dormRoom/types';
import { addDormRoom, updateDormRoom } from '@/api/xgss/dormRoom';
import { DormRoomForm, DormRoomVO } from '@/api/xg/xgss/dormRoom/types';
import { addDormRoom, updateDormRoom } from '@/api/xg/xgss/dormRoom';
const props = defineProps<{
modelValue: boolean;
@@ -256,11 +256,11 @@
<script setup name="DormRoomTreeManagement" lang="ts">
import { ref, computed, reactive, onMounted, nextTick, watch, getCurrentInstance } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import type { DormTreeNode, BedInfo } from '@/api/xgss/xsssfjgl/types';
import type { DormRoomVO } from '@/api/xgss/dormRoom/types';
import { getDormTree } from '@/api/xgss/xsssfjgl';
import { listDormBed, delDormBed, getDormBed } from '@/api/xgss/dormBed';
import { listDormRoom, delDormRoom } from '@/api/xgss/dormRoom';
import type { DormTreeNode, BedInfo } from '@/api/xg/xgss/xsssfjgl/types';
import type { DormRoomVO } from '@/api/xg/xgss/dormRoom/types';
import { getDormTree } from '@/api/xg/xgss/xsssfjgl';
import { listDormBed, delDormBed, getDormBed } from '@/api/xg/xgss/dormBed';
import { listDormRoom, delDormRoom } from '@/api/xg/xgss/dormRoom';
// 引入子组件
import RoomDialog from './components/RoomDialog.vue';
@@ -47,8 +47,8 @@
<script lang="ts">
import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue';
import { getTeacherList } from '@/api/xgss/xsssgly/dormManager';
import { addDormManager, updateDormManager } from '@/api/xgss/ssglyxx/dormManager/index';
import { getTeacherList } from '@/api/xg/xgss/xsssgly/dormManager';
import { addDormManager, updateDormManager } from '@/api/xg/xgss/ssglyxx/dormManager/index';
import { ElMessage } from 'element-plus';
export default defineComponent({
@@ -82,11 +82,11 @@
<script lang="ts">
import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue';
import { listDormCampus } from '@/api/xgss/xqxx/dormCampus';
import { getAreaTree } from '@/api/xgss/ssqxx/dormArea';
import { getBuildingTree } from '@/api/xgss/sslxx/dormBuilding';
import type { TreeSelectVo } from '@/api/xgss/ssqxx/dormArea/types';
import type { BuildingTreeSelectVo } from '@/api/xgss/sslxx/dormBuilding/types';
import { listDormCampus } from '@/api/xg/xgss/xqxx/dormCampus';
import { getAreaTree } from '@/api/xg/xgss/ssqxx/dormArea';
import { getBuildingTree } from '@/api/xg/xgss/sslxx/dormBuilding';
import type { TreeSelectVo } from '@/api/xg/xgss/ssqxx/dormArea/types';
import type { BuildingTreeSelectVo } from '@/api/xg/xgss/sslxx/dormBuilding/types';
export default defineComponent({
name: 'ScopeDialog',
File renamed without changes.
@@ -0,0 +1,612 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="queryParams.batchName" placeholder="请输入批次名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['XgStuColBatch:stuColBatch:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['XgStuColBatch:stuColBatch:edit']"
>修改</el-button
>
</el-col>
<!-- <el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete()"
v-hasPermi="['XgStuColBatch:stuColBatch:remove']"
>删除</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['XgStuColBatch:stuColBatch:export']">导出</el-button>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="stuColBatchList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="批次ID" align="center" prop="id" v-if="true" />
<el-table-column label="批次名称" align="center" prop="batchName" />
<el-table-column label="批次描述" align="center" prop="description" />
<el-table-column label="开始时间" align="center" prop="startTime" width="200">
<template #default="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="200">
<template #default="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
{{ scope.row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="收集类型" align="center" prop="collectionType">
<template #default="scope">
<el-tag :type="scope.row.collectionType === '1' ? 'primary' : 'info'">
{{ scope.row.collectionType === '1' ? '新学期收集' : '其他' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-right">
<template #default="scope">
<el-tooltip content="查看详细" placement="top">
<el-button
link
type="info"
plain
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:query']"
></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
plain
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="收集范围" placement="top">
<el-button
link
type="warning"
plain
icon="Setting"
@click="handleScope(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:scope']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
plain
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改学生信息收集批次对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="stuColBatchFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="form.batchName" placeholder="请输入批次名称" />
</el-form-item>
<el-form-item label="批次描述" prop="description">
<el-input v-model="form.description" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
clearable
style="width: 100%"
v-model="form.startTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择开始时间"
>
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
clearable
style="width: 100%"
v-model="form.endTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择结束时间"
>
</el-date-picker>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item label="收集类型" prop="collectionType">
<el-select v-model="form.collectionType" placeholder="请选择收集类型">
<el-option label="新学期收集" value="1" />
<el-option label="其他" value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 收集范围设置对话框 -->
<el-dialog title="设置收集范围" v-model="scopeDialog.visible" width="500px" append-to-body @close="handleScopeDialogClose">
<div class="scope-content">
<el-tree
ref="scopeTreeRef"
:data="deptTreeData"
show-checkbox
default-expand-all
node-key="id"
:props="{ children: 'children', label: 'label' }"
:check-strictly="false"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button :loading="scopeButtonLoading" type="primary" @click="submitScope">确 定</el-button>
<el-button @click="cancelScope">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看详细对话框 -->
<el-dialog title="批次详细信息" v-model="viewDialog.visible" width="800px" append-to-body>
<el-tabs v-model="viewActiveTab" type="border-card">
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border>
<el-descriptions-item label="批次ID">{{ viewForm.id }}</el-descriptions-item>
<el-descriptions-item label="批次名称">{{ viewForm.batchName }}</el-descriptions-item>
<el-descriptions-item label="批次描述" :span="2">{{ viewForm.description || '暂无描述' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(viewForm.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(viewForm.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewForm.status === '0' ? 'success' : 'danger'">
{{ viewForm.status === '0' ? '正常' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="收集类型">
<el-tag :type="viewForm.collectionType === '1' ? 'primary' : 'info'">
{{ viewForm.collectionType === '1' ? '新学期收集' : '其他' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(viewForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ parseTime(viewForm.updateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="权限范围" name="scope">
<div class="scope-detail-content">
<div v-if="scopeDetailList.length === 0" class="no-data">
<el-empty description="暂无权限范围配置" />
</div>
<div v-else>
<el-alert title="权限范围说明" type="info" :closable="false" show-icon class="mb-3">
<template #default> 该批次配置了 {{ scopeDetailList.length }} 个班级的收集权限,具体如下: </template>
</el-alert>
<el-table :data="scopeDetailList" border stripe>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="学院名称" prop="collegeName" align="center" />
<el-table-column label="专业名称" prop="majorName" align="center" />
<el-table-column label="班级名称" prop="className" align="center" />
<el-table-column label="配置时间" align="center" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="StuColBatch" lang="ts">
import { listStuColBatch, getStuColBatch, delStuColBatch, addStuColBatch, updateStuColBatch } from '@/api/xg/xsjcxx/XgStuColBatch/stuColBatch';
import { StuColBatchVO, StuColBatchQuery, StuColBatchForm } from '@/api/xg/xsjcxx/XgStuColBatch/stuColBatch/types';
import { setScope, getScope, getScopeDetail } from '@/api/xg/xsjcxx/stuColBatchClass/stuColBatchClass';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const stuColBatchList = ref<StuColBatchVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const stuColBatchFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 收集范围弹窗相关数据
const scopeDialog = reactive({
visible: false
});
const scopeButtonLoading = ref(false);
const currentBatch = ref<StuColBatchVO | null>(null);
const deptTreeData = ref<any[]>([]);
const checkedDeptIds = ref<number[]>([]);
const scopeTreeRef = ref();
// 查看详细弹窗相关数据
const viewDialog = reactive({
visible: false
});
const viewForm = ref<StuColBatchVO>({} as StuColBatchVO);
const viewActiveTab = ref('basic');
const scopeDetailList = ref<any[]>([]);
const initFormData: StuColBatchForm = {
id: undefined,
batchName: undefined,
description: undefined,
startTime: new Date()
.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
.replace(/\//g, '-'),
endTime: undefined,
status: '0',
collectionType: '1'
};
const data = reactive<PageData<StuColBatchForm, StuColBatchQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
batchName: undefined,
status: undefined,
collectionType: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '批次ID不能为空', trigger: 'blur' }],
batchName: [{ required: true, message: '批次名称不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
collectionType: [{ required: true, message: '收集类型 1 - 新学期收集 2 - 其他 不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询学生信息收集批次列表 */
const getList = async () => {
loading.value = true;
const res = await listStuColBatch(queryParams.value);
stuColBatchList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
stuColBatchFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StuColBatchVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加学生信息收集批次';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: StuColBatchVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getStuColBatch(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改学生信息收集批次';
};
/** 提交按钮 */
const submitForm = () => {
stuColBatchFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateStuColBatch(form.value).finally(() => (buttonLoading.value = false));
} else {
await addStuColBatch(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: StuColBatchVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除学生信息收集批次编号为"' + row?.batchName + '"的数据项?').finally(() => (loading.value = false));
await delStuColBatch(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'XgStuColBatch/stuColBatch/export',
{
...queryParams.value
},
`stuColBatch_${new Date().getTime()}.xlsx`
);
};
/** 收集范围按钮操作 */
const handleScope = async (row: StuColBatchVO) => {
currentBatch.value = row;
// 先清空勾选状态
checkedDeptIds.value = [];
await loadDeptTree();
await loadCurrentScope(row.id);
scopeDialog.visible = true;
// 使用nextTick确保DOM更新完成后再设置勾选状态
await nextTick();
if (scopeTreeRef.value && checkedDeptIds.value.length > 0) {
scopeTreeRef.value.setCheckedKeys(checkedDeptIds.value);
}
};
/** 加载部门树数据 */
const loadDeptTree = async () => {
try {
loading.value = true;
const response = await request({
url: '/system/user/deptTree',
method: 'get',
params: {
belongDeptId: 101
}
});
if (response && response.code === 200) {
deptTreeData.value = response.data || [];
}
} catch (error) {
console.error('获取部门树数据失败:', error);
ElMessage.error('获取范围选项失败');
} finally {
loading.value = false;
}
};
/** 加载当前批次的收集范围 */
const loadCurrentScope = async (batchId: string | number) => {
try {
checkedDeptIds.value = [];
const response = await getScope(batchId);
if (response && response.code === 200) {
checkedDeptIds.value = response.data || [];
} else {
checkedDeptIds.value = [];
}
} catch (error) {
console.error('加载收集范围失败:', error);
checkedDeptIds.value = [];
}
};
/** 构建树形数据 */
const buildTree = (data: any[]) => {
// 如果数据已经是树形结构,直接转换格式
const convertToTreeNode = (item: any): any => {
return {
id: item.deptId,
label: item.deptName,
children: item.children ? item.children.map(convertToTreeNode) : []
};
};
return data.map(convertToTreeNode);
};
/** 提交收集范围 */
const submitScope = async () => {
if (!currentBatch.value) return;
try {
scopeButtonLoading.value = true;
const checkedNodes = scopeTreeRef.value.getCheckedNodes();
const checkedIds = checkedNodes.map((node: any) => node.id);
// 调用后端API设置收集范围
const response = await setScope(currentBatch.value.id, checkedIds);
if (response && response.code === 200) {
ElMessage.success(response.msg || '收集范围设置成功');
scopeDialog.visible = false;
// 提交成功后清空状态
handleScopeDialogClose();
} else {
ElMessage.error(response?.msg || '设置收集范围失败');
}
} catch (error) {
console.error('设置收集范围失败:', error);
ElMessage.error('设置收集范围失败');
} finally {
scopeButtonLoading.value = false;
}
};
/** 取消收集范围设置 */
const cancelScope = () => {
scopeDialog.visible = false;
currentBatch.value = null;
checkedDeptIds.value = [];
// 清空树形组件的勾选状态
if (scopeTreeRef.value) {
scopeTreeRef.value.setCheckedKeys([]);
}
};
/** 处理收集范围弹窗关闭 */
const handleScopeDialogClose = () => {
// 清空相关状态
currentBatch.value = null;
checkedDeptIds.value = [];
// 清空树形组件的勾选状态
if (scopeTreeRef.value) {
scopeTreeRef.value.setCheckedKeys([]);
}
};
/** 查看详细按钮操作 */
const handleView = async (row: StuColBatchVO) => {
try {
// 获取批次基本信息
const res = await getStuColBatch(row.id);
viewForm.value = res.data;
// 获取权限范围详细信息
await loadScopeDetail(row.id);
viewDialog.visible = true;
viewActiveTab.value = 'basic';
} catch (error) {
console.error('获取批次详情失败:', error);
proxy?.$modal.msgError('获取批次详情失败');
}
};
/** 加载权限范围详细信息 */
const loadScopeDetail = async (batchId: string | number) => {
try {
const response = await getScopeDetail(batchId);
if (response && response.code === 200) {
scopeDetailList.value = response.data || [];
} else {
scopeDetailList.value = [];
}
} catch (error) {
console.error('加载权限范围详情失败:', error);
scopeDetailList.value = [];
}
};
/** 取消查看详细 */
const cancelView = () => {
viewDialog.visible = false;
viewForm.value = {} as StuColBatchVO;
scopeDetailList.value = [];
viewActiveTab.value = 'basic';
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.scope-detail-content {
padding: 10px 0;
}
.no-data {
text-align: center;
padding: 40px 0;
}
.mb-3 {
margin-bottom: 12px;
}
</style>
@@ -59,9 +59,9 @@
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['classMsg:classMsg:export']">导出</el-button>
</el-col>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -74,19 +74,22 @@
<el-table-column label="学院名称" align="center" prop="collegeName" />
<el-table-column label="专业名称" align="center" prop="majorName" />
<el-table-column label="班级名称" align="center" prop="className" />
<el-table-column label="班级类型" align="center" prop="classType">
<el-table-column label="学历层次" align="center" prop="classType">
<template #default="scope">
<el-tag :type="getClassTypeTag(scope.row.classType)">{{ formatClassType(scope.row.classType) }}</el-tag>
<!-- <el-tag :type="getClassTypeTag(scope.row.classType)">{{ formatClassType(scope.row.classType) }}</el-tag> -->
<dict-tag :options="zsjy_education_level" :value="scope.row.classType" />
</template>
</el-table-column>
<el-table-column label="班主任" align="center" prop="classTeacherName" />
<el-table-column label="辅导员" align="center" prop="counselorName" />
<el-table-column label="年级" align="center" prop="grade" />
<el-table-column label="入学年份" align="center" prop="enrollmentYear" />
<el-table-column label="学制(年)" align="center" prop="schoolSystem" />
<el-table-column label="毕业年份" align="center" prop="graduationYear" />
<el-table-column label="班级状态" align="center" prop="classStatus">
<template #default="scope">
<el-tag :type="getClassStatusTag(scope.row.classStatus)">{{ formatClassStatus(scope.row.classStatus) }}</el-tag>
<!-- <el-tag :type="getClassStatusTag(scope.row.classStatus)">{{ formatClassStatus(scope.row.classStatus) }}</el-tag> -->
<dict-tag :options="xg_class_status" :value="scope.row.classStatus" />
</template>
</el-table-column>
<el-table-column label="备注信息" align="center" prop="remark" />
@@ -156,13 +159,14 @@
<el-form-item label="班级名称" prop="className">
<el-input v-model="form.className" placeholder="请输入班级名称" />
</el-form-item>
<el-form-item label="班级类型" prop="classType">
<el-select v-model="form.classType" placeholder="请选择班级类型" style="width: 100%">
<el-option label="中级工" :value="1" />
<el-form-item label="学历层级" prop="classType">
<el-select v-model="form.classType" placeholder="请选择学历层级" style="width: 100%">
<el-option v-for="dict in zsjy_education_level" :key="dict.value" :label="dict.label" :value="dict.value" />
<!-- <el-option label="中级工" :value="1" />
<el-option label="高级工" :value="2" />
<el-option label="预备技师" :value="3" />
<el-option label="技师" :value="4" />
<el-option label="中高职一体" :value="5" />
<el-option label="中高职一体" :value="5" /> -->
</el-select>
</el-form-item>
<el-form-item label="年级" prop="grade">
@@ -171,6 +175,9 @@
<el-form-item label="入学年份" prop="enrollmentYear">
<el-input v-model="form.enrollmentYear" placeholder="请输入入学年份" />
</el-form-item>
<el-form-item label="学制(年)" prop="schoolSystem">
<el-input v-model="form.schoolSystem" placeholder="请输入学制(年)" />
</el-form-item>
<el-form-item label="毕业年份" prop="graduationYear">
<el-input v-model="form.graduationYear" placeholder="请输入毕业年份" />
</el-form-item>
@@ -206,8 +213,9 @@
</el-form-item>
<el-form-item label="是否已毕业" prop="isGraduation">
<el-select v-model="form.isGraduation" placeholder="请选择是否已毕业" style="width: 100%">
<el-option label="未毕业" :value="0" />
<el-option label="已毕业" :value="1" />
<!-- <el-option label="未毕业" :value="0" />
<el-option label="已毕业" :value="1" /> -->
<el-option v-for="dict in is_graduation" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="毕业日期" prop="graduationDate" v-if="false">
@@ -280,10 +288,11 @@
</el-form-item>
<el-form-item label="班级状态" prop="classStatus" v-if="false">
<el-select v-model="form.classStatus" placeholder="请选择班级状态" style="width: 100%">
<el-option label="正常" :value="1" />
<!-- <el-option label="正常" :value="1" />
<el-option label="暂停" :value="2" />
<el-option label="毕业" :value="3" />
<el-option label="解散" :value="4" />
<el-option label="解散" :value="4" /> -->
<el-option v-for="dict in xg_class_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="班级描述" prop="classDescription">
@@ -404,9 +413,9 @@
</template>
<script setup name="ClassMsg" lang="ts">
import { listClassMsg, getClassMsg, delClassMsg, addClassMsg, updateClassMsg } from '@/api/classMsg/classMsg';
import { listColleges, collegeTree, listMajorsByCollegeId } from '@/api/classMsg/college';
import { ClassMsgVO, ClassMsgQuery, ClassMsgForm, SysDeptVo } from '@/api/classMsg/types';
import { listClassMsg, getClassMsg, delClassMsg, addClassMsg, updateClassMsg } from '@/api/xg/xsjcxx/classmsg/classMsg';
import { listColleges, collegeTree, listMajorsByCollegeId } from '@/api/xg/xsjcxx/classmsg/college';
import { ClassMsgVO, ClassMsgQuery, ClassMsgForm, SysDeptVo } from '@/api/xg/xsjcxx/classmsg/types';
import { ComponentInternalInstance, ref, reactive, toRefs, onMounted, getCurrentInstance, watch } from 'vue';
import type { FormInstance as ElFormInstance } from 'element-plus';
import { getActiveUserList } from '@/api/xg/sysUser'; // 添加用户API导入
@@ -423,7 +432,55 @@ interface PageData<T, Q> {
rules: Record<string, any[]>;
}
interface ComponentInternalInstance {
proxy?: any;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const {
xg_class_status,
is_graduation,
wenhuachengdu,
xg_political_status,
sys_user_sex,
xg_nation,
sys_question_category,
sys_item_enable,
sys_item_required,
sys_item_delete,
house_hold_register_type,
marital_status,
sys_yesno,
xg_family_economic,
xg_blood_type,
disability_type,
zsjy_education_level,
xg_training_mode,
admission_category,
student_status
} = toRefs<any>(
proxy?.useDict(
'xg_class_status',
'is_graduation',
'wenhuachengdu',
'xg_political_status',
'sys_user_sex',
'xg_nation',
'sys_question_category',
'sys_item_enable',
'sys_item_required',
'sys_item_delete',
'house_hold_register_type',
'marital_status',
'sys_yesno',
'xg_family_economic',
'xg_blood_type',
'disability_type',
'zsjy_education_level',
'xg_training_mode',
'admission_category',
'student_status'
)
);
const classMsgList = ref<ClassMsgVO[]>([]);
const buttonLoading = ref(false);
@@ -454,9 +511,10 @@ const initFormData: ClassMsgForm = {
majorName: undefined,
classCode: undefined,
className: undefined,
classType: undefined,
classType: '4',
grade: undefined,
enrollmentYear: undefined,
schoolSystem: undefined,
graduationYear: undefined,
trainingType: undefined,
degreeType: undefined,
@@ -471,7 +529,7 @@ const initFormData: ClassMsgForm = {
deputyMonitorId: undefined,
deputyMonitorName: undefined,
classStatus: 1,
isGraduation: 0,
isGraduation: '0',
graduationDate: undefined,
plannedCapacity: undefined,
currentStudentCount: undefined,
@@ -514,7 +572,12 @@ const data = reactive<PageData<ClassMsgForm, ClassMsgQuery>>({
classCode: [{ required: true, message: '班级代码不能为空', trigger: 'blur' }],
className: [{ required: true, message: '班级名称不能为空', trigger: 'blur' }],
grade: [{ required: true, message: '年级不能为空', trigger: 'blur' }],
enrollmentYear: [{ required: true, message: '入学年份不能为空', trigger: 'blur' }]
enrollmentYear: [{ required: true, message: '入学年份不能为空', trigger: 'blur' }],
majorId: [{ required: true, message: '专业不能为空', trigger: 'blur' }],
classType: [{ required: true, message: '学历层级不能为空', trigger: 'blur' }],
schoolSystem: [{ required: true, message: '学制(年)不能为空', trigger: 'blur' }],
graduationYear: [{ required: true, message: '毕业年份不能为空', trigger: 'blur' }],
collegeId: [{ required: true, message: '学院不能为空', trigger: 'blur' }]
}
});
@@ -661,17 +724,12 @@ const handleUpdate = async (row?: ClassMsgVO) => {
// 确保classType是数字类型
if (form.value.classType !== undefined) {
form.value.classType = Number(form.value.classType);
form.value.classType = String(form.value.classType);
}
// 确保classStatus是数字类型
if (form.value.classStatus !== undefined) {
form.value.classStatus = Number(form.value.classStatus);
}
// 确保isGraduation是数字类型
// 确保isGraduation是字符串类型
if (form.value.isGraduation !== undefined) {
form.value.isGraduation = Number(form.value.isGraduation);
form.value.isGraduation = String(form.value.isGraduation);
}
// 获取该学院下的专业列表
@@ -0,0 +1,889 @@
<template>
<div class="audit-dialog">
<el-dialog v-model="visible" :title="title" width="80%" append-to-body :before-close="handleClose" top="5vh" class="audit-dialog-container">
<div class="layout-container">
<!-- 部门树 -->
<div class="left-panel">
<el-card shadow="hover">
<el-input v-model="deptName" placeholder="请输入院系名称" prefix-icon="Search" clearable />
<el-tree
ref="deptTreeRef"
class="mt-2 dept-tree"
node-key="id"
:data="deptOptions"
:props="{ label: 'label', children: 'children', disabled: 'disabled' }"
:expand-on-click-node="false"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</div>
<!-- 右侧内容区域 -->
<div class="right-panel">
<!-- 搜索表单 -->
<div class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="手机号码" prop="phone">
<el-input v-model="queryParams.phone" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="提交状态" prop="submitStatus">
<el-select v-model="queryParams.submitStatus" placeholder="提交状态" clearable>
<!-- 3-审核中,4-已驳回,5-已锁定 -->
<el-option label="审核中" value="3" />
<el-option label="已驳回" value="4" />
<el-option label="已锁定" value="5" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
<!-- 学生信息表格 -->
<el-card shadow="hover">
<!-- <template #header>
<div class="card-header">
<span>学生信息审核 - {{ batchInfo?.batchName || '未知批次' }}</span>
<div class="header-actions">
<el-button type="success" icon="Check" :disabled="!hasSelected" @click="handleBatchApprove"> 批量通过 </el-button>
<el-button type="danger" icon="Close" :disabled="!hasSelected" @click="handleBatchReject"> 批量拒绝 </el-button>
</div>
</div>
</template> -->
<el-table v-loading="loading" border :data="studentList" @selection-change="handleSelectionChange" class="right-table">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学号" align="center" prop="studentNo" :show-overflow-tooltip="true" />
<el-table-column label="姓名" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="院系" align="center" prop="collegeName" :show-overflow-tooltip="true" />
<el-table-column label="专业" align="center" prop="majorName" :show-overflow-tooltip="true" />
<el-table-column label="班级" align="center" prop="className" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" align="center" prop="phone" width="120" />
<el-table-column label="提交状态" align="center" width="120">
<template #default="scope">
<el-tag :type="getSubmitStatusType(scope.row.infoStatus)">
{{ getSubmitStatusLabel(scope.row.infoStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="提交时间" align="center" prop="createTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-right" fixed="right">
<template #default="scope">
<el-tooltip content="审核" placement="top">
<el-button link type="primary" icon="View" @click="handleAuditStudent(scope.row)" :disabled="scope.row.submitStatus === '0'">
审核
</el-button>
</el-tooltip>
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="Document" @click="handleViewDetail(scope.row)"> 详情 </el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page="queryParams.pageNum" :limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</div>
<!-- <template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关 闭</el-button>
</div>
</template> -->
</el-dialog>
<!-- 学生审核详情弹窗 -->
<el-dialog v-model="auditDetailDialog.visible" :title="auditDetailDialog.title" width="800px" append-to-body>
<div v-if="currentAuditStudent">
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ currentAuditStudent.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ currentAuditStudent.name }}</el-descriptions-item>
<el-descriptions-item label="院系">{{ currentAuditStudent.collegeName }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ currentAuditStudent.majorName }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ currentAuditStudent.className }}</el-descriptions-item>
<el-descriptions-item label="手机号码">{{ currentAuditStudent.phone }}</el-descriptions-item>
<el-descriptions-item label="提交状态" :span="2">
<el-tag :type="getSubmitStatusType(currentAuditStudent.submitStatus)">
{{ getSubmitStatusLabel(currentAuditStudent.submitStatus) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="提交时间" :span="2">
{{ parseTime(currentAuditStudent.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</el-descriptions-item>
</el-descriptions>
<div class="mt-4">
<el-form ref="auditFormRef" :model="auditForm" label-width="100px">
<el-form-item label="审核结果" prop="auditResult">
<el-radio-group v-model="auditForm.auditResult">
<el-radio label="2">通过</el-radio>
<el-radio label="3">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见" prop="auditRemark">
<el-input v-model="auditForm.auditRemark" type="textarea" :rows="4" placeholder="请输入审核意见" />
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button :loading="auditLoading" type="primary" @click="submitAudit">确定审核</el-button>
<el-button @click="auditDetailDialog.visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 学生信息详情弹窗 -->
<component
:is="StuMsgDialogComponent"
:visible="stuMsgDialogVisible"
:title="stuMsgDialogTitle"
:batchInfo="stuMsgBatchInfo"
@update:visible="stuMsgDialogVisible = $event"
@confirm="handleStuMsgConfirm"
v-if="StuMsgDialogComponent"
/>
<!-- 学生详情查看弹窗 -->
<component
:is="StuMsgDetailDialogComponent"
:visible="stuMsgDetailDialogVisible"
:title="stuMsgDetailDialogTitle"
:batchInfo="stuMsgDetailBatchInfo"
@update:visible="stuMsgDetailDialogVisible = $event"
v-if="StuMsgDetailDialogComponent"
/>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, ComponentInternalInstance, ref, reactive, computed, watch, onMounted, nextTick, defineAsyncComponent } from 'vue';
import ElMessage from 'element-plus/es/components/message/index';
import ElMessageBox from 'element-plus/es/components/message-box/index';
// @ts-ignore
import request from '@/utils/request';
// 动态加载StuMsgDialog组件
const StuMsgDialogComponent = defineAsyncComponent(() => import('./stuMsgDialog.vue'));
// 动态加载学生详情弹窗组件
const StuMsgDetailDialogComponent = defineAsyncComponent(() => import('./stuMsgDetailDialog.vue'));
interface StudentAuditInfo {
id: string | number;
studentNo: string;
name: string;
collegeName: string;
majorName: string;
className: string;
phone: string;
submitStatus: string;
submitTime: string;
auditResult?: string;
auditRemark?: string;
auditTime?: string;
}
interface QueryParams {
pageNum: number;
pageSize: number;
name: string;
studentNo: string;
phone: string;
submitStatus: string;
deptId?: number;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// Props
const props = defineProps<{
visible: boolean;
title: string;
batchInfo: any;
}>();
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean];
confirm: [data: any];
}>();
// 响应式数据
const visible = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
});
const loading = ref(false);
const total = ref(0);
const studentList = ref<StudentAuditInfo[]>([]);
const selectedStudents = ref<StudentAuditInfo[]>([]);
const deptName = ref('');
const deptOptions = ref<any[]>([]);
const currentSelectedNode = ref<any>(null);
// 计算属性
const hasSelected = computed(() => selectedStudents.value.length > 0);
// 查询参数
const queryParams = reactive<QueryParams>({
pageNum: 1,
pageSize: 10,
name: '',
studentNo: '',
phone: '',
submitStatus: '',
deptId: undefined
});
// 审核详情弹窗
const auditDetailDialog = reactive({
visible: false,
title: '学生信息审核'
});
const currentAuditStudent = ref<StudentAuditInfo | null>(null);
const auditLoading = ref(false);
const auditForm = reactive({
auditResult: '2',
auditRemark: ''
});
// Refs
const deptTreeRef = ref();
const queryFormRef = ref();
const auditFormRef = ref();
// 学生信息详情弹窗相关数据
const stuMsgDialogVisible = ref(false);
const stuMsgDialogTitle = ref('学生信息审核');
const stuMsgBatchInfo = ref<any>(null);
// 学生详情查看弹窗相关数据
const stuMsgDetailDialogVisible = ref(false);
const stuMsgDetailDialogTitle = ref('学生信息详情查看');
const stuMsgDetailBatchInfo = ref<any>(null);
/** 通过条件过滤节点 */
const filterNode = (value: string, data: any) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/** 获取提交状态标签 */
const getSubmitStatusLabel = (status: string) => {
// 3-审核中,4-已驳回,5-已锁定
const statusMap: Record<string, string> = {
'3': '审核中',
'4': '已驳回',
'5': '已锁定'
};
return statusMap[status] || status;
};
/** 获取提交状态标签类型 */
const getSubmitStatusType = (status: string): 'success' | 'info' | 'warning' | 'danger' => {
const typeMap: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
'3': 'info',
'5': 'success',
'4': 'danger'
};
return typeMap[status] || 'info';
};
/** 格式化时间 */
const parseTime = (time: any, pattern?: string): string => {
if (!time) return '';
let date: Date;
if (typeof time === 'object') {
date = time;
} else {
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
time = parseInt(time);
}
if (typeof time === 'number' && time.toString().length === 10) {
time = time * 1000;
}
date = new Date(time);
}
const formatObj: Record<string, number> = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
};
const time_str = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
return time_str.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key];
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value];
}
return value.toString().padStart(2, '0');
});
};
/** 查询学生列表 */
const getList = async () => {
if (!props.batchInfo?.id) return;
// 如果没有选择部门,则不查询
if (!queryParams.deptId) {
ElMessage.warning('请先选择部门');
return;
}
loading.value = true;
try {
const params = {
batchId: props.batchInfo.id,
deptId: queryParams.deptId,
name: queryParams.name || undefined,
studentNo: queryParams.studentNo || undefined,
phone: queryParams.phone || undefined,
infoStatus: queryParams.submitStatus || undefined,
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize
};
const res = await request({
url: '/studentMsgBatch/studentMsgBatch/listByDept',
method: 'get',
params
});
if (res && res.code === 200) {
studentList.value = res.rows || [];
total.value = res.total || 0;
} else {
ElMessage.error(res?.msg || '获取学生列表失败');
studentList.value = [];
total.value = 0;
}
} catch (error) {
console.error('获取学生列表失败:', error);
ElMessage.error('获取学生列表失败');
studentList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
/** 查询部门下拉树结构 */
const getDeptTree = async () => {
try {
if (!props.batchInfo?.id) {
console.warn('批次ID不存在,无法获取部门树');
return;
}
const res = await request({
url: `/stuColBatchClass/stuColBatchClass/getBatchAuthScope/${props.batchInfo.id}`,
method: 'get'
});
if (res && res.code === 200 && res.data && Array.isArray(res.data)) {
deptOptions.value = res.data;
// 设置根节点为默认选中节点
setDefaultRootNode(res.data);
} else {
console.warn('部门树数据格式不符合预期:', res);
deptOptions.value = [];
}
} catch (error) {
console.error('获取部门树失败:', error);
deptOptions.value = [];
}
};
/** 设置默认根节点并触发查询 */
const setDefaultRootNode = (treeData: any[]) => {
if (treeData && treeData.length > 0) {
// 找到第一个可用的根节点(不是disabled的节点)
const findFirstAvailableNode = (nodes: any[]): any => {
for (const node of nodes) {
if (!node.disabled) {
return node;
}
if (node.children && node.children.length > 0) {
const childNode = findFirstAvailableNode(node.children);
if (childNode) {
return childNode;
}
}
}
return null;
};
const rootNode = findFirstAvailableNode(treeData);
if (rootNode) {
// 设置当前选中的节点
currentSelectedNode.value = rootNode;
queryParams.deptId = rootNode.id;
// 设置树形控件的当前选中项
setTimeout(() => {
deptTreeRef.value?.setCurrentKey(rootNode.id);
}, 100);
// 触发数据查询
getList();
} else {
// 如果没有找到可用节点,则直接查询全部数据
getList();
}
} else {
// 如果没有树数据,则直接查询全部数据
getList();
}
};
/** 节点单击事件 */
const handleNodeClick = (data: any) => {
// alert(1);
// console.log('handleNodeClick', data);
// 只有班级节点才可以点击(disabled=false的节点)
// if (data.disabled) {
// ElMessage.warning('没有该节点权限,请联系管理员');
// return;
// }
// 保存当前选中的节点信息
currentSelectedNode.value = data;
queryParams.deptId = data.id;
handleQuery();
};
// /** 节点单击事件 */
// const handleNodeClick = (data: any) => {
// currentSelectedNode.value = data;
// queryParams.deptId = data.id;
// handleQuery();
// };
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.pageNum = 1;
queryParams.name = '';
queryParams.studentNo = '';
queryParams.phone = '';
queryParams.submitStatus = '';
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StudentAuditInfo[]) => {
selectedStudents.value = selection;
};
/** 审核学生 */
const handleAuditStudent = (row: StudentAuditInfo) => {
// 打开学生信息详情弹窗进行审核
stuMsgBatchInfo.value = {
batchId: props.batchInfo?.id,
userId: row.id,
...props.batchInfo
};
stuMsgDialogTitle.value = `学生信息审核 - ${row.name}`;
stuMsgDialogVisible.value = true;
};
/** 查看学生详情 */
const handleViewDetail = (row: StudentAuditInfo) => {
// 设置详情弹窗数据
stuMsgDetailBatchInfo.value = {
id: props.batchInfo?.id,
batchId: props.batchInfo?.id,
userId: row.id,
studentId: row.id,
batchName: props.batchInfo?.batchName,
...props.batchInfo
};
stuMsgDetailDialogTitle.value = `学生信息详情 - ${row.name}`;
stuMsgDetailDialogVisible.value = true;
};
/** 批量通过 */
const handleBatchApprove = async () => {
if (selectedStudents.value.length === 0) {
ElMessage.warning('请选择要审核的学生');
return;
}
try {
await ElMessageBox.confirm('确认批量通过选中的学生信息?', '提示');
loading.value = true;
const studentIds = selectedStudents.value.map((item) => item.id);
const res = await request({
url: '/stuColBatchClass/stuColBatchClass/batchAudit',
method: 'post',
data: {
studentIds,
auditResult: '2',
auditRemark: '批量审核通过'
}
});
if (res && res.code === 200) {
ElMessage.success('批量审核成功');
await getList();
} else {
ElMessage.error(res?.msg || '批量审核失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('批量审核失败:', error);
ElMessage.error('批量审核失败');
}
} finally {
loading.value = false;
}
};
/** 批量拒绝 */
const handleBatchReject = async () => {
if (selectedStudents.value.length === 0) {
ElMessage.warning('请选择要审核的学生');
return;
}
try {
const { value: remark } = await ElMessageBox.prompt('请输入拒绝理由', '批量拒绝', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入拒绝理由',
inputValidator: (val) => {
if (!val || val.trim() === '') {
return '拒绝理由不能为空';
}
return true;
}
});
loading.value = true;
const studentIds = selectedStudents.value.map((item) => item.id);
const res = await request({
url: '/stuColBatchClass/stuColBatchClass/batchAudit',
method: 'post',
data: {
studentIds,
auditResult: '3',
auditRemark: remark
}
});
if (res && res.code === 200) {
ElMessage.success('批量审核成功');
await getList();
} else {
ElMessage.error(res?.msg || '批量审核失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('批量审核失败:', error);
ElMessage.error('批量审核失败');
}
} finally {
loading.value = false;
}
};
/** 提交审核 */
const submitAudit = async () => {
if (!currentAuditStudent.value) return;
auditFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
try {
auditLoading.value = true;
const res = await request({
url: '/stuColBatchClass/stuColBatchClass/auditStudent',
method: 'post',
data: {
studentId: currentAuditStudent.value!.id,
auditResult: auditForm.auditResult,
auditRemark: auditForm.auditRemark
}
});
if (res && res.code === 200) {
ElMessage.success('审核成功');
auditDetailDialog.visible = false;
await getList();
} else {
ElMessage.error(res?.msg || '审核失败');
}
} catch (error) {
console.error('审核失败:', error);
ElMessage.error('审核失败');
} finally {
auditLoading.value = false;
}
}
});
};
/** 关闭弹窗 */
const handleClose = () => {
visible.value = false;
// 重置数据
studentList.value = [];
selectedStudents.value = [];
queryParams.pageNum = 1;
queryParams.name = '';
queryParams.studentNo = '';
queryParams.phone = '';
queryParams.submitStatus = '';
currentSelectedNode.value = null;
};
// 监听弹窗显示状态
watch(
() => props.visible,
(newVal) => {
if (newVal && props.batchInfo?.id) {
// 弹窗打开时初始化数据
getDeptTree().then(() => {
getList();
});
}
}
);
// 监听部门名称筛选
watch(deptName, (val) => {
deptTreeRef.value?.filter(val);
});
/** 学生信息确认回调 */
const handleStuMsgConfirm = (data: any) => {
console.log('学生信息已更新:', data);
ElMessage.success('学生信息已更新');
// 刷新学生列表
getList();
};
</script>
<style scoped>
/* 审核弹窗样式 */
.audit-dialog :deep(.el-dialog) {
margin-top: 5vh !important;
margin-bottom: 5vh !important;
height: 700px;
max-height: 700px;
display: flex;
flex-direction: column;
}
.audit-dialog :deep(.el-dialog__body) {
flex: 1;
padding: 15px 20px;
overflow: hidden;
height: calc(100% - 120px);
}
/* 主布局容器 */
.layout-container {
display: flex;
gap: 20px;
height: 600px;
overflow: hidden;
}
/* 左侧面板 - 固定宽度300px */
.left-panel {
width: 350px;
flex-shrink: 0;
overflow: hidden;
}
/* 左侧面板内的卡片容器 */
.left-panel .el-card {
min-width: 330px;
height: 600px;
display: flex;
flex-direction: column;
}
.left-panel .el-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
gap: 10px;
overflow: hidden;
}
/* 部门树组件样式 */
.dept-tree {
height: 520px;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
/* 强制覆盖el-tree的默认样式 */
.dept-tree :deep(.el-tree-node) {
white-space: nowrap !important;
}
.dept-tree :deep(.el-tree-node__content) {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
padding-right: 10px !important;
}
.dept-tree :deep(.el-tree-node__content:hover) {
background-color: var(--el-tree-node-hover-bg-color, #f5f7fa) !important;
}
.dept-tree :deep(.el-tree-node__label) {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
flex: 1 !important;
}
/* 右侧面板 - 占据剩余空间 */
.right-panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
height: 600px;
overflow: hidden;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
/* 右侧面板内容区域样式 */
.right-panel .mb-\[10px\] {
flex-shrink: 0;
margin-bottom: 10px;
}
.right-panel .el-card:last-child {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.right-panel .el-card:last-child :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 15px;
}
.right-panel .el-table {
flex: 1;
margin-bottom: 10px;
}
/* 右侧表格样式 */
.right-table {
height: 420px;
}
.right-table :deep(.el-table__body-wrapper) {
overflow-y: auto;
max-height: 350px;
}
/* 卡片头部样式 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.header-actions {
display: flex;
gap: 10px;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.layout-container {
flex-direction: column;
}
.left-panel {
width: 100%;
}
.left-panel .el-card {
height: 400px;
}
.dept-tree {
height: 320px;
}
}
</style>
@@ -0,0 +1,773 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="queryParams.batchName" placeholder="请输入批次名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['XgStuColBatch:stuColBatch:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['XgStuColBatch:stuColBatch:edit']"
>修改</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete()"
v-hasPermi="['XgStuColBatch:stuColBatch:remove']"
>删除</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['XgStuColBatch:stuColBatch:export']">导出</el-button>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="stuColBatchList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="批次ID" align="center" prop="id" v-if="true" />
<el-table-column label="批次名称" align="center" prop="batchName" />
<el-table-column label="批次描述" align="center" prop="description" />
<el-table-column label="开始时间" align="center" prop="startTime" width="200">
<template #default="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="200">
<template #default="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
{{ scope.row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="收集类型" align="center" prop="collectionType">
<template #default="scope">
<el-tag :type="scope.row.collectionType === '1' ? 'primary' : 'info'">
{{ scope.row.collectionType === '1' ? '新学期收集' : '其他' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-right">
<template #default="scope">
<el-tooltip content="查看详细" placement="top">
<el-button link type="info" plain icon="View" @click="handleView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="审核管理" placement="top">
<el-button link type="warning" plain icon="Document" @click="handleAudit(scope.row)"></el-button>
</el-tooltip>
<!-- <el-tooltip content="信息完善" placement="top" v-if="isBatchValid(scope.row)">
<el-button link type="success" plain icon="Edit" @click="handleInfoComplete(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="查看提交历史" placement="top">
<el-button link type="info" plain icon="Clock" @click="handleViewHistory(scope.row)"></el-button>
</el-tooltip> -->
<!-- <el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
plain
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="收集范围" placement="top">
<el-button
link
type="warning"
plain
icon="Setting"
@click="handleScope(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:scope']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
plain
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:remove']"
></el-button>
</el-tooltip> -->
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改学生信息收集批次对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="stuColBatchFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="form.batchName" placeholder="请输入批次名称" />
</el-form-item>
<el-form-item label="批次描述" prop="description">
<el-input v-model="form.description" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
clearable
style="width: 100%"
v-model="form.startTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择开始时间"
>
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
clearable
style="width: 100%"
v-model="form.endTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择结束时间"
>
</el-date-picker>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item label="收集类型" prop="collectionType">
<el-select v-model="form.collectionType" placeholder="请选择收集类型">
<el-option label="新学期收集" value="1" />
<el-option label="其他" value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 收集范围设置对话框 -->
<el-dialog title="设置收集范围" v-model="scopeDialog.visible" width="500px" append-to-body @close="handleScopeDialogClose">
<div class="scope-content">
<el-tree
ref="scopeTreeRef"
:data="deptTreeData"
show-checkbox
default-expand-all
node-key="id"
:props="{ children: 'children', label: 'label' }"
:check-strictly="false"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button :loading="scopeButtonLoading" type="primary" @click="submitScope">确 定</el-button>
<el-button @click="cancelScope">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看详细对话框 -->
<el-dialog title="批次详细信息" v-model="viewDialog.visible" width="800px" append-to-body>
<el-tabs v-model="viewActiveTab" type="border-card">
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border>
<el-descriptions-item label="批次ID">{{ viewForm.id }}</el-descriptions-item>
<el-descriptions-item label="批次名称">{{ viewForm.batchName }}</el-descriptions-item>
<el-descriptions-item label="批次描述" :span="2">{{ viewForm.description || '暂无描述' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(viewForm.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(viewForm.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewForm.status === '0' ? 'success' : 'danger'">
{{ viewForm.status === '0' ? '正常' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="收集类型">
<el-tag :type="viewForm.collectionType === '1' ? 'primary' : 'info'">
{{ viewForm.collectionType === '1' ? '新学期收集' : '其他' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(viewForm.createTime || '', '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ parseTime(viewForm.updateTime || '', '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- <el-tab-pane label="权限范围" name="scope">
<div class="scope-detail-content">
<div v-if="scopeDetailList.length === 0" class="no-data">
<el-empty description="暂无权限范围配置" />
</div>
<div v-else>
<el-alert title="权限范围说明" type="info" :closable="false" show-icon class="mb-3">
<template #default> 该批次配置了 {{ scopeDetailList.length }} 个班级的收集权限,具体如下: </template>
</el-alert>
<el-table :data="scopeDetailList" border stripe>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="学院名称" prop="collegeName" align="center" />
<el-table-column label="专业名称" prop="majorName" align="center" />
<el-table-column label="班级名称" prop="className" align="center" />
<el-table-column label="配置时间" align="center" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-tab-pane> -->
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 学生信息完善弹窗 -->
<StuMsgDialog
v-model:visible="stuMsgDialog.visible"
:title="stuMsgDialog.title"
:batch-info="stuMsgDialog.batchInfo"
@confirm="handleStuMsgConfirm"
@update:visible="handleStuMsgClose"
/>
<!-- 学生信息提交历史详情弹窗 -->
<StuMsgDetailDialog
v-model:visible="historyDialog.visible"
:title="historyDialog.title"
:batch-info="historyDialog.batchInfo"
@update:visible="handleHistoryDialogClose"
/>
<!-- 审核管理弹窗 -->
<AuditDialog
v-model:visible="auditDialog.visible"
:title="auditDialog.title"
:batch-info="auditDialog.batchInfo"
@update:visible="handleAuditDialogClose"
@confirm="handleAuditConfirm"
/>
</div>
</template>
<script setup name="StuColBatch" lang="ts">
import { getCurrentInstance, ComponentInternalInstance, ref, reactive, toRefs, onMounted, nextTick } from 'vue';
import { getStuColBatch, delStuColBatch, addStuColBatch, updateStuColBatch } from '@/api/xg/xsjcxx/stuMsgAudit';
import { StuColBatchVO, StuColBatchQuery, StuColBatchForm } from '@/api/xg/xsjcxx/stuMsgAudit/types';
import { setScope, getScope, getScopeDetail } from '@/api/xg/xsjcxx/stuColBatchClass/stuColBatchClass';
import request from '@/utils/request';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance as ElFormInstance } from 'element-plus';
import StuMsgDialog from './stuMsgDialog.vue';
import StuMsgDetailDialog from './stuMsgDetailDialog.vue';
import AuditDialog from './auditDialog.vue';
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<T, Q> {
form: T;
queryParams: Q;
rules: Record<string, any[]>;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const stuColBatchList = ref<StuColBatchVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const stuColBatchFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 收集范围弹窗相关数据
const scopeDialog = reactive({
visible: false
});
const scopeButtonLoading = ref(false);
const currentBatch = ref<StuColBatchVO | null>(null);
const deptTreeData = ref<any[]>([]);
const checkedDeptIds = ref<number[]>([]);
const scopeTreeRef = ref();
// 查看详细弹窗相关数据
const viewDialog = reactive({
visible: false
});
const viewForm = ref<StuColBatchVO>({} as StuColBatchVO);
const viewActiveTab = ref('basic');
const scopeDetailList = ref<any[]>([]);
// 学生信息完善弹窗相关数据
const stuMsgDialog = reactive({
visible: false,
title: '学生信息完善',
batchInfo: null as StuColBatchVO | null
});
// 学生信息提交历史详情弹窗相关数据
const historyDialog = reactive({
visible: false,
title: '学生信息提交历史',
batchInfo: null as StuColBatchVO | null
});
// 审核管理弹窗相关数据
const auditDialog = reactive({
visible: false,
title: '审核管理',
batchInfo: null as StuColBatchVO | null
});
const initFormData: StuColBatchForm = {
id: undefined,
batchName: undefined,
description: undefined,
startTime: new Date()
.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
.replace(/\//g, '-'),
endTime: undefined,
status: '0',
collectionType: '1'
};
const data = reactive<PageData<StuColBatchForm, StuColBatchQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
batchName: undefined,
status: undefined,
collectionType: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '批次ID不能为空', trigger: 'blur' }],
batchName: [{ required: true, message: '批次名称不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
collectionType: [{ required: true, message: '收集类型 1 - 新学期收集 2 - 其他 不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询学生信息收集批次列表 */
const getList = async () => {
loading.value = true;
console.log('queryParams.value.batchName', queryParams.value.batchName);
try {
const res = await request({
url: '/stuColBatchClass/stuColBatchClass/getTeacherRelatedBatches',
method: 'get',
params: {
batchName: queryParams.value.batchName || ''
}
});
if (res && res.code === 200) {
// 将返回的数据转换为表格需要的格式
stuColBatchList.value = res.data || [];
total.value = res.data ? res.data.length : 0;
} else {
stuColBatchList.value = [];
total.value = 0;
proxy?.$modal.msgError(res?.msg || '获取数据失败');
}
} catch (error) {
console.error('获取学生可参与批次信息失败:', error);
stuColBatchList.value = [];
total.value = 0;
proxy?.$modal.msgError('获取数据失败');
} finally {
loading.value = false;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
stuColBatchFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StuColBatchVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加学生信息收集批次';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: StuColBatchVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getStuColBatch(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改学生信息收集批次';
};
/** 提交按钮 */
const submitForm = () => {
stuColBatchFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateStuColBatch(form.value).finally(() => (buttonLoading.value = false));
} else {
await addStuColBatch(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: StuColBatchVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除学生信息收集批次编号为"' + row?.batchName + '"的数据项?').finally(() => (loading.value = false));
await delStuColBatch(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'XgStuColBatch/stuColBatch/export',
{
...queryParams.value
},
`stuColBatch_${new Date().getTime()}.xlsx`
);
};
/** 收集范围按钮操作 */
const handleScope = async (row: StuColBatchVO) => {
currentBatch.value = row;
// 先清空勾选状态
checkedDeptIds.value = [];
await loadDeptTree();
await loadCurrentScope(row.id);
scopeDialog.visible = true;
// 使用nextTick确保DOM更新完成后再设置勾选状态
await nextTick();
if (scopeTreeRef.value && checkedDeptIds.value.length > 0) {
scopeTreeRef.value.setCheckedKeys(checkedDeptIds.value);
}
};
/** 加载部门树数据 */
const loadDeptTree = async () => {
try {
loading.value = true;
const response = await request({
url: '/system/user/deptTree',
method: 'get',
params: {
belongDeptId: 101
}
});
if (response && response.code === 200) {
deptTreeData.value = response.data || [];
}
} catch (error) {
console.error('获取部门树数据失败:', error);
ElMessage.error('获取范围选项失败');
} finally {
loading.value = false;
}
};
/** 加载当前批次的收集范围 */
const loadCurrentScope = async (batchId: string | number) => {
try {
checkedDeptIds.value = [];
const response = await getScope(batchId);
if (response && response.code === 200) {
checkedDeptIds.value = response.data || [];
} else {
checkedDeptIds.value = [];
}
} catch (error) {
console.error('加载收集范围失败:', error);
checkedDeptIds.value = [];
}
};
/** 构建树形数据 */
const buildTree = (data: any[]) => {
// 如果数据已经是树形结构,直接转换格式
const convertToTreeNode = (item: any): any => {
return {
id: item.deptId,
label: item.deptName,
children: item.children ? item.children.map(convertToTreeNode) : []
};
};
return data.map(convertToTreeNode);
};
/** 提交收集范围 */
const submitScope = async () => {
if (!currentBatch.value) return;
try {
scopeButtonLoading.value = true;
const checkedNodes = scopeTreeRef.value.getCheckedNodes();
const checkedIds = checkedNodes.map((node: any) => node.id);
// 调用后端API设置收集范围
const response = await setScope(currentBatch.value.id, checkedIds);
if (response && response.code === 200) {
ElMessage.success(response.msg || '收集范围设置成功');
scopeDialog.visible = false;
// 提交成功后清空状态
handleScopeDialogClose();
} else {
ElMessage.error(response?.msg || '设置收集范围失败');
}
} catch (error) {
console.error('设置收集范围失败:', error);
ElMessage.error('设置收集范围失败');
} finally {
scopeButtonLoading.value = false;
}
};
/** 取消收集范围设置 */
const cancelScope = () => {
scopeDialog.visible = false;
currentBatch.value = null;
checkedDeptIds.value = [];
// 清空树形组件的勾选状态
if (scopeTreeRef.value) {
scopeTreeRef.value.setCheckedKeys([]);
}
};
/** 处理收集范围弹窗关闭 */
const handleScopeDialogClose = () => {
// 清空相关状态
currentBatch.value = null;
checkedDeptIds.value = [];
// 清空树形组件的勾选状态
if (scopeTreeRef.value) {
scopeTreeRef.value.setCheckedKeys([]);
}
};
/** 查看详细按钮操作 */
const handleView = async (row: StuColBatchVO) => {
try {
// 获取批次基本信息
const res = await getStuColBatch(row?.id);
viewForm.value = res.data;
// 获取权限范围详细信息
// await loadScopeDetail(row.id);
viewDialog.visible = true;
viewActiveTab.value = 'basic';
} catch (error) {
console.error('获取批次详情失败:', error);
proxy?.$modal.msgError('获取批次详情失败');
}
};
/** 加载权限范围详细信息 */
const loadScopeDetail = async (batchId: string | number) => {
try {
const response = await getScopeDetail(batchId);
if (response && response.code === 200) {
scopeDetailList.value = response.data || [];
} else {
scopeDetailList.value = [];
}
} catch (error) {
console.error('加载权限范围详情失败:', error);
scopeDetailList.value = [];
}
};
/** 取消查看详细 */
const cancelView = () => {
viewDialog.visible = false;
viewForm.value = {} as StuColBatchVO;
scopeDetailList.value = [];
viewActiveTab.value = 'basic';
};
/** 信息完善按钮操作 */
const handleInfoComplete = (row: StuColBatchVO) => {
console.log('信息完善按钮点击,批次ID:', row.id, '批次名称:', row.batchName);
stuMsgDialog.batchInfo = row;
stuMsgDialog.title = `信息完善 - ${row.batchName}`;
stuMsgDialog.visible = true;
};
/** 学生信息完善弹窗确认 */
const handleStuMsgConfirm = (formData: any) => {
console.log('学生信息完善提交:', formData);
// TODO: 调用API保存学生信息
ElMessage.success('学生信息完善成功');
};
/** 关闭学生信息完善弹窗 */
const handleStuMsgClose = () => {
stuMsgDialog.visible = false;
stuMsgDialog.batchInfo = null;
};
/** 查看提交历史按钮操作 */
const handleViewHistory = (row: StuColBatchVO) => {
console.log('查看提交历史按钮点击,批次ID:', row.id, '批次名称:', row.batchName);
historyDialog.batchInfo = row;
historyDialog.title = `学生信息提交历史 - ${row.batchName}`;
historyDialog.visible = true;
};
/** 关闭学生信息提交历史详情弹窗 */
const handleHistoryDialogClose = () => {
historyDialog.visible = false;
historyDialog.batchInfo = null;
};
/** 审核管理按钮操作 */
const handleAudit = (row: StuColBatchVO) => {
console.log('审核管理按钮点击,批次ID:', row.id, '批次名称:', row.batchName);
auditDialog.batchInfo = row;
auditDialog.title = `审核管理 - ${row.batchName}`;
auditDialog.visible = true;
};
/** 审核弹窗确认 */
const handleAuditConfirm = (data: any) => {
console.log('审核弹窗确认:', data);
ElMessage.success('审核操作成功');
};
/** 关闭审核管理弹窗 */
const handleAuditDialogClose = () => {
auditDialog.visible = false;
auditDialog.batchInfo = null;
};
/** 检查批次是否有效(在有效时间内且状态正常) */
const isBatchValid = (row: StuColBatchVO): boolean => {
// 检查状态是否为正常
if (row.status !== '0') {
return false;
}
// 检查是否在有效时间内
const now = new Date();
const startTime = new Date(row.startTime);
const endTime = new Date(row.endTime);
return now >= startTime && now <= endTime;
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.scope-detail-content {
padding: 10px 0;
}
.no-data {
text-align: center;
padding: 40px 0;
}
.mb-3 {
margin-bottom: 12px;
}
</style>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,732 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="queryParams.batchName" placeholder="请输入批次名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['XgStuColBatch:stuColBatch:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['XgStuColBatch:stuColBatch:edit']"
>修改</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete()"
v-hasPermi="['XgStuColBatch:stuColBatch:remove']"
>删除</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['XgStuColBatch:stuColBatch:export']">导出</el-button>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="stuColBatchList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="批次ID" align="center" prop="batchId" v-if="true" />
<el-table-column label="批次名称" align="center" prop="batchName" />
<el-table-column label="批次描述" align="center" prop="description" />
<el-table-column label="开始时间" align="center" prop="startTime" width="200">
<template #default="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="200">
<template #default="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
{{ scope.row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="收集类型" align="center" prop="collectionType">
<template #default="scope">
<el-tag :type="scope.row.collectionType === '1' ? 'primary' : 'info'">
{{ scope.row.collectionType === '1' ? '新学期收集' : '其他' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-right">
<template #default="scope">
<el-tooltip content="查看详细" placement="top">
<el-button link type="info" plain icon="View" @click="handleView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="信息完善" placement="top" v-if="isBatchValid(scope.row)">
<el-button link type="success" plain icon="Edit" @click="handleInfoComplete(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="查看提交历史" placement="top">
<el-button link type="info" plain icon="Clock" @click="handleViewHistory(scope.row)"></el-button>
</el-tooltip>
<!-- <el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
plain
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="收集范围" placement="top">
<el-button
link
type="warning"
plain
icon="Setting"
@click="handleScope(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:scope']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
plain
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['XgStuColBatch:stuColBatch:remove']"
></el-button>
</el-tooltip> -->
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改学生信息收集批次对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="stuColBatchFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="form.batchName" placeholder="请输入批次名称" />
</el-form-item>
<el-form-item label="批次描述" prop="description">
<el-input v-model="form.description" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
clearable
style="width: 100%"
v-model="form.startTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择开始时间"
>
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
clearable
style="width: 100%"
v-model="form.endTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择结束时间"
>
</el-date-picker>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item label="收集类型" prop="collectionType">
<el-select v-model="form.collectionType" placeholder="请选择收集类型">
<el-option label="新学期收集" value="1" />
<el-option label="其他" value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 收集范围设置对话框 -->
<el-dialog title="设置收集范围" v-model="scopeDialog.visible" width="500px" append-to-body @close="handleScopeDialogClose">
<div class="scope-content">
<el-tree
ref="scopeTreeRef"
:data="deptTreeData"
show-checkbox
default-expand-all
node-key="id"
:props="{ children: 'children', label: 'label' }"
:check-strictly="false"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button :loading="scopeButtonLoading" type="primary" @click="submitScope">确 定</el-button>
<el-button @click="cancelScope">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看详细对话框 -->
<el-dialog title="批次详细信息" v-model="viewDialog.visible" width="800px" append-to-body>
<el-tabs v-model="viewActiveTab" type="border-card">
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border>
<el-descriptions-item label="批次ID">{{ viewForm.id }}</el-descriptions-item>
<el-descriptions-item label="批次名称">{{ viewForm.batchName }}</el-descriptions-item>
<el-descriptions-item label="批次描述" :span="2">{{ viewForm.description || '暂无描述' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(viewForm.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(viewForm.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewForm.status === '0' ? 'success' : 'danger'">
{{ viewForm.status === '0' ? '正常' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="收集类型">
<el-tag :type="viewForm.collectionType === '1' ? 'primary' : 'info'">
{{ viewForm.collectionType === '1' ? '新学期收集' : '其他' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(viewForm.createTime || '', '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ parseTime(viewForm.updateTime || '', '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- <el-tab-pane label="权限范围" name="scope">
<div class="scope-detail-content">
<div v-if="scopeDetailList.length === 0" class="no-data">
<el-empty description="暂无权限范围配置" />
</div>
<div v-else>
<el-alert title="权限范围说明" type="info" :closable="false" show-icon class="mb-3">
<template #default> 该批次配置了 {{ scopeDetailList.length }} 个班级的收集权限,具体如下: </template>
</el-alert>
<el-table :data="scopeDetailList" border stripe>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="学院名称" prop="collegeName" align="center" />
<el-table-column label="专业名称" prop="majorName" align="center" />
<el-table-column label="班级名称" prop="className" align="center" />
<el-table-column label="配置时间" align="center" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-tab-pane> -->
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 学生信息完善弹窗 -->
<StuMsgDialog
v-model:visible="stuMsgDialog.visible"
:title="stuMsgDialog.title"
:batch-info="stuMsgDialog.batchInfo"
@confirm="handleStuMsgConfirm"
@update:visible="handleStuMsgClose"
/>
<!-- 学生信息提交历史详情弹窗 -->
<StuMsgDetailDialog
v-model:visible="historyDialog.visible"
:title="historyDialog.title"
:batch-info="historyDialog.batchInfo"
@update:visible="handleHistoryDialogClose"
/>
</div>
</template>
<script setup name="StuColBatch" lang="ts">
import { getCurrentInstance, ComponentInternalInstance, ref, reactive, toRefs, onMounted, nextTick } from 'vue';
import { getStuColBatch, delStuColBatch, addStuColBatch, updateStuColBatch } from '@/api/xg/xsjcxx/stuMsgSupply';
import { StuColBatchVO, StuColBatchQuery, StuColBatchForm } from '@/api/xg/xsjcxx/stuMsgSupply/types';
import { setScope, getScope, getScopeDetail } from '@/api/xg/xsjcxx/stuColBatchClass/stuColBatchClass';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';
import type { FormInstance as ElFormInstance } from 'element-plus';
import StuMsgDialog from './stuMsgDialog.vue';
import StuMsgDetailDialog from './stuMsgDetailDialog.vue';
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<T, Q> {
form: T;
queryParams: Q;
rules: Record<string, any[]>;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const stuColBatchList = ref<StuColBatchVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const stuColBatchFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 收集范围弹窗相关数据
const scopeDialog = reactive({
visible: false
});
const scopeButtonLoading = ref(false);
const currentBatch = ref<StuColBatchVO | null>(null);
const deptTreeData = ref<any[]>([]);
const checkedDeptIds = ref<number[]>([]);
const scopeTreeRef = ref();
// 查看详细弹窗相关数据
const viewDialog = reactive({
visible: false
});
const viewForm = ref<StuColBatchVO>({} as StuColBatchVO);
const viewActiveTab = ref('basic');
const scopeDetailList = ref<any[]>([]);
// 学生信息完善弹窗相关数据
const stuMsgDialog = reactive({
visible: false,
title: '学生信息完善',
batchInfo: null as StuColBatchVO | null
});
// 学生信息提交历史详情弹窗相关数据
const historyDialog = reactive({
visible: false,
title: '学生信息提交历史',
batchInfo: null as StuColBatchVO | null
});
const initFormData: StuColBatchForm = {
id: undefined,
batchName: undefined,
description: undefined,
startTime: new Date()
.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
.replace(/\//g, '-'),
endTime: undefined,
status: '0',
collectionType: '1'
};
const data = reactive<PageData<StuColBatchForm, StuColBatchQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
batchName: undefined,
status: undefined,
collectionType: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '批次ID不能为空', trigger: 'blur' }],
batchName: [{ required: true, message: '批次名称不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
collectionType: [{ required: true, message: '收集类型 1 - 新学期收集 2 - 其他 不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询学生信息收集批次列表 */
const getList = async () => {
loading.value = true;
console.log('queryParams.value.batchName', queryParams.value.batchName);
try {
const res = await request({
url: '/stuColBatchClass/stuColBatchClass/getStudentAvailableBatches',
method: 'get',
params: {
batchName: queryParams.value.batchName || ''
}
});
if (res && res.code === 200) {
// 将返回的数据转换为表格需要的格式
stuColBatchList.value = res.data || [];
total.value = res.data ? res.data.length : 0;
} else {
stuColBatchList.value = [];
total.value = 0;
proxy?.$modal.msgError(res?.msg || '获取数据失败');
}
} catch (error) {
console.error('获取学生可参与批次信息失败:', error);
stuColBatchList.value = [];
total.value = 0;
proxy?.$modal.msgError('获取数据失败');
} finally {
loading.value = false;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
stuColBatchFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StuColBatchVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加学生信息收集批次';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: StuColBatchVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getStuColBatch(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改学生信息收集批次';
};
/** 提交按钮 */
const submitForm = () => {
stuColBatchFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateStuColBatch(form.value).finally(() => (buttonLoading.value = false));
} else {
await addStuColBatch(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: StuColBatchVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除学生信息收集批次编号为"' + row?.batchName + '"的数据项?').finally(() => (loading.value = false));
await delStuColBatch(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'XgStuColBatch/stuColBatch/export',
{
...queryParams.value
},
`stuColBatch_${new Date().getTime()}.xlsx`
);
};
/** 收集范围按钮操作 */
const handleScope = async (row: StuColBatchVO) => {
currentBatch.value = row;
// 先清空勾选状态
checkedDeptIds.value = [];
await loadDeptTree();
await loadCurrentScope(row.id);
scopeDialog.visible = true;
// 使用nextTick确保DOM更新完成后再设置勾选状态
await nextTick();
if (scopeTreeRef.value && checkedDeptIds.value.length > 0) {
scopeTreeRef.value.setCheckedKeys(checkedDeptIds.value);
}
};
/** 加载部门树数据 */
const loadDeptTree = async () => {
try {
loading.value = true;
const response = await request({
url: '/system/user/deptTree',
method: 'get',
params: {
belongDeptId: 101
}
});
if (response && response.code === 200) {
deptTreeData.value = response.data || [];
}
} catch (error) {
console.error('获取部门树数据失败:', error);
ElMessage.error('获取范围选项失败');
} finally {
loading.value = false;
}
};
/** 加载当前批次的收集范围 */
const loadCurrentScope = async (batchId: string | number) => {
try {
checkedDeptIds.value = [];
const response = await getScope(batchId);
if (response && response.code === 200) {
checkedDeptIds.value = response.data || [];
} else {
checkedDeptIds.value = [];
}
} catch (error) {
console.error('加载收集范围失败:', error);
checkedDeptIds.value = [];
}
};
/** 构建树形数据 */
const buildTree = (data: any[]) => {
// 如果数据已经是树形结构,直接转换格式
const convertToTreeNode = (item: any): any => {
return {
id: item.deptId,
label: item.deptName,
children: item.children ? item.children.map(convertToTreeNode) : []
};
};
return data.map(convertToTreeNode);
};
/** 提交收集范围 */
const submitScope = async () => {
if (!currentBatch.value) return;
try {
scopeButtonLoading.value = true;
const checkedNodes = scopeTreeRef.value.getCheckedNodes();
const checkedIds = checkedNodes.map((node: any) => node.id);
// 调用后端API设置收集范围
const response = await setScope(currentBatch.value.id, checkedIds);
if (response && response.code === 200) {
ElMessage.success(response.msg || '收集范围设置成功');
scopeDialog.visible = false;
// 提交成功后清空状态
handleScopeDialogClose();
} else {
ElMessage.error(response?.msg || '设置收集范围失败');
}
} catch (error) {
console.error('设置收集范围失败:', error);
ElMessage.error('设置收集范围失败');
} finally {
scopeButtonLoading.value = false;
}
};
/** 取消收集范围设置 */
const cancelScope = () => {
scopeDialog.visible = false;
currentBatch.value = null;
checkedDeptIds.value = [];
// 清空树形组件的勾选状态
if (scopeTreeRef.value) {
scopeTreeRef.value.setCheckedKeys([]);
}
};
/** 处理收集范围弹窗关闭 */
const handleScopeDialogClose = () => {
// 清空相关状态
currentBatch.value = null;
checkedDeptIds.value = [];
// 清空树形组件的勾选状态
if (scopeTreeRef.value) {
scopeTreeRef.value.setCheckedKeys([]);
}
};
/** 查看详细按钮操作 */
const handleView = async (row: StuColBatchVO) => {
try {
// 获取批次基本信息
const res = await getStuColBatch(row?.batchId);
viewForm.value = res.data;
// 获取权限范围详细信息
// await loadScopeDetail(row.id);
viewDialog.visible = true;
viewActiveTab.value = 'basic';
} catch (error) {
console.error('获取批次详情失败:', error);
proxy?.$modal.msgError('获取批次详情失败');
}
};
/** 加载权限范围详细信息 */
const loadScopeDetail = async (batchId: string | number) => {
try {
const response = await getScopeDetail(batchId);
if (response && response.code === 200) {
scopeDetailList.value = response.data || [];
} else {
scopeDetailList.value = [];
}
} catch (error) {
console.error('加载权限范围详情失败:', error);
scopeDetailList.value = [];
}
};
/** 取消查看详细 */
const cancelView = () => {
viewDialog.visible = false;
viewForm.value = {} as StuColBatchVO;
scopeDetailList.value = [];
viewActiveTab.value = 'basic';
};
/** 信息完善按钮操作 */
const handleInfoComplete = (row: StuColBatchVO) => {
console.log('信息完善按钮点击,批次ID:', row.id, '批次名称:', row.batchName);
stuMsgDialog.batchInfo = row;
stuMsgDialog.title = `信息完善 - ${row.batchName}`;
stuMsgDialog.visible = true;
};
/** 学生信息完善弹窗确认 */
const handleStuMsgConfirm = (formData: any) => {
console.log('学生信息完善提交:', formData);
// TODO: 调用API保存学生信息
ElMessage.success('学生信息完善成功');
};
/** 关闭学生信息完善弹窗 */
const handleStuMsgClose = () => {
stuMsgDialog.visible = false;
stuMsgDialog.batchInfo = null;
};
/** 查看提交历史按钮操作 */
const handleViewHistory = (row: StuColBatchVO) => {
console.log('查看提交历史按钮点击,批次ID:', row.id, '批次名称:', row.batchName);
historyDialog.batchInfo = row;
historyDialog.title = `学生信息提交历史 - ${row.batchName}`;
historyDialog.visible = true;
};
/** 关闭学生信息提交历史详情弹窗 */
const handleHistoryDialogClose = () => {
historyDialog.visible = false;
historyDialog.batchInfo = null;
};
/** 检查批次是否有效(在有效时间内且状态正常) */
const isBatchValid = (row: StuColBatchVO): boolean => {
// 检查状态是否为正常
if (row.status !== '0') {
return false;
}
// 检查是否在有效时间内
const now = new Date();
const startTime = new Date(row.startTime);
const endTime = new Date(row.endTime);
return now >= startTime && now <= endTime;
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.scope-detail-content {
padding: 10px 0;
}
.no-data {
text-align: center;
padding: 40px 0;
}
.mb-3 {
margin-bottom: 12px;
}
</style>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+202
View File
@@ -0,0 +1,202 @@
# 学生花名册管理功能实现说明
## 功能概述
学生花名册管理界面用于展示和管理学生信息,支持高级复杂检索、导出、查看等操作。
## 文件结构
```
web/src/views/xg/xsjcxx/stuRoster/
├── index.vue # 主界面文件
├── components/ # 组件目录
│ ├── AdvancedSearchDialog.vue # 高级检索弹窗
│ └── StudentDetailDialog.vue # 学生详情弹窗
├── test.vue # 测试页面
└── README.md # 说明文档
```
## 功能特性
### 1. 主界面 (index.vue)
#### 布局设计
- **左右布局模式**:左侧固定宽高的部门树,右侧内容区域
- **部门树功能**:
- 支持纵向和横向滚动条
- 支持多选功能
- 支持搜索过滤
- 清空选择功能
#### 检索功能
- **基础检索**:学号、学生姓名、手机号码、状态四个检索条件
- **高级检索**:弹出专门弹窗配置检索条件
- **检索按钮**:搜索、重置、高级检索
#### 数据展示
- **双视图模式**:列表视图和卡片视图
- **列表视图**:标准表格展示,支持列显示控制
- **卡片视图**:卡片式布局,每行4个卡片
- **分页功能**:支持分页显示
#### 操作功能
- **导出数据**:支持数据导出功能
- **查看详情**:查看学生完整信息
- **列控制**:通过right-toolbar控制列显示
### 2. 高级检索弹窗 (AdvancedSearchDialog.vue)
#### 检索字段覆盖
支持 `xg_student_msg` 表中所有字段的检索功能:
**基础信息**
- 身份证号、英文姓名、曾用名、性别
- 出生日期(时间范围)、民族、籍贯、政治面貌
- 户籍所在地、家庭地址、现住址、邮政编码
- 电子邮箱、QQ号码、微信号、婚姻状况
**学籍信息**
- 入学年份、入学日期(时间范围)、录取类别
- 学制、学历层次、培养方式、年级、户口类型
**家庭信息**
- 父亲信息:姓名、电话、工作单位
- 母亲信息:姓名、电话、工作单位
- 监护人信息:姓名、电话、关系
- 经济信息:家庭年收入、人均收入、收入来源、经济状况、成员数量
- 紧急联系人信息
**特殊标识**
- 贫困生、少数民族、残疾、孤儿、留守儿童、独生子女、低保、助学贷款等标识
- 宗教信仰、生源地区
**健康信息**
- 身高、体重(数值范围)
- 血型、健康状况、特殊体质、病史、过敏史
- 残疾类型、残疾等级
- 特长爱好、获奖情况、违纪记录、社会活动
**时间范围**
- 创建时间、更新时间(时间范围)
#### 检索特性
- **文本检索**:采用like模式
- **下拉选择**:支持多选
- **时间范围**:支持开始时间到结束时间的跨度检索
- **数值范围**:支持最小值和最大值范围检索
- **每行4个**:检索条件每4个放一行
### 3. 学生详情弹窗 (StudentDetailDialog.vue)
#### Tab页设计
参考 `stuMsgDetailDialog.vue` 结构,分为5个Tab页:
**基础信息**
- 学生照片和基本信息布局
- 学号、身份证号、姓名、英文姓名、曾用名、性别
- 出生日期、民族、籍贯、政治面貌
- 手机号码、电子邮箱、QQ号码、微信号、婚姻状况
- 宗教信仰、户籍所在地、家庭地址、现住址、邮政编码
- 生源地区、毕业院校、户口类型
**学籍信息**
- 学籍状态、入学年份、入学日期、录取类别
- 学制、学历层次、培养方式、年级、是否住校
- 学院、专业、班级、辅导员、班主任
- 宿舍楼栋、宿舍房间号、银行卡号、开户银行
**家庭信息**
- 父亲信息:姓名、电话、工作单位
- 母亲信息:姓名、电话、工作单位
- 监护人信息:姓名、电话、关系
- 经济信息:家庭年收入、人均收入、收入来源、经济状况、成员数量
- 紧急联系人信息
**特殊标识**
- 各种特殊标识的状态展示(使用Tag标签)
- 助学贷款金额、残疾类型、残疾等级
**健康信息**
- 身高、体重、血型
- 健康状况、特殊体质或疾病、病史、过敏史
- 特长爱好、获奖情况、违纪记录、社会活动
#### 展示特性
- **只读模式**:所有字段都是禁用状态,仅用于查看
- **状态标签**:使用不同颜色的Tag标签展示状态
- **照片展示**:学生照片展示
- **加载状态**:数据加载时显示loading状态
### 4. 测试页面 (test.vue)
#### 测试功能
- **基础功能测试**:查询条件输入和查询功能
- **模拟数据展示**:表格形式展示模拟数据
- **弹窗测试**:高级检索和详情弹窗的测试
- **状态展示**:学生状态的Tag标签展示
## 技术实现
### 数据模型
基于 `xg_student_msg` 表结构,包含:
- 基础信息字段(学号、姓名、性别等)
- 学籍信息字段(状态、入学年份等)
- 家庭信息字段(父母信息、经济状况等)
- 特殊标识字段(贫困生、少数民族等)
- 健康信息字段(身高、体重、病史等)
### 组件设计
- **主界面组件**:负责整体布局和主要功能
- **高级检索组件**:独立的弹窗组件,负责复杂检索
- **详情展示组件**:独立的弹窗组件,负责信息展示
- **测试组件**:用于功能测试和验证
### 状态管理
- **查询参数**:基础查询参数和高级查询参数
- **视图状态**:列表视图和卡片视图切换
- **弹窗状态**:各种弹窗的显示控制
- **选择状态**:部门树的多选状态
### 模拟数据
目前使用模拟数据进行展示,包含:
- 3个示例学生数据
- 完整的字段信息
- 不同的状态值
## 待完善功能
### API对接
- 需要对接真实的学生信息API
- 实现数据的增删改查功能
- 实现导出功能
### 权限控制
- 根据用户权限控制功能访问
- 实现数据权限过滤
### 性能优化
- 大数据量时的分页优化
- 检索性能优化
- 组件懒加载
### 用户体验
- 更多的交互动画
- 响应式布局优化
- 移动端适配
## 使用说明
1. **访问路径**:`/xg/xsjcxx/stuRoster`
2. **测试页面**:`/xg/xsjcxx/stuRoster/test`
3. **主要功能**:
- 左侧选择部门
- 右侧进行检索
- 查看学生详情
- 导出数据
## 开发规范
- 遵循Vue 3 Composition API规范
- 使用TypeScript进行类型检查
- 遵循Element Plus设计规范
- 组件化开发,保持代码可维护性
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,891 @@
<template>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="1100px" append-to-body @close="handleClose">
<el-tabs
v-model="activeTab"
type="border-card"
v-loading="dialogLoading"
element-loading-text="正在加载数据,请稍候..."
element-loading-background="rgba(0, 0, 0, 0.3)"
>
<!-- 基础信息Tab -->
<el-tab-pane label="基础信息" name="basic">
<el-form ref="studentFormRef" :model="form" label-width="100px">
<!-- 学生照片和基本信息布局 -->
<el-row :gutter="20">
<el-col :span="16">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="请输入学号" maxlength="32" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号" maxlength="18" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="50" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="英文姓名" prop="nameEnglish">
<el-input v-model="form.nameEnglish" placeholder="请输入英文姓名" maxlength="100" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="曾用名" prop="formerName">
<el-input v-model="form.formerName" placeholder="请输入曾用名" maxlength="50" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="form.gender" placeholder="请选择性别" disabled>
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="出生日期" prop="birthDate">
<el-date-picker
v-model="form.birthDate"
type="date"
placeholder="请选择出生日期"
value-format="YYYY-MM-DD"
disabled
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="民族" prop="nationality">
<el-input v-model="form.nationality" placeholder="请输入民族" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="籍贯" prop="nativePlace">
<el-input v-model="form.nativePlace" placeholder="请输入籍贯" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="政治面貌" prop="politicalStatus">
<el-input v-model="form.politicalStatus" placeholder="请输入政治面貌" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号码" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号码" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入电子邮箱" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="QQ号码" prop="qq">
<el-input v-model="form.qq" placeholder="请输入QQ号码" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="微信号" prop="wechat">
<el-input v-model="form.wechat" placeholder="请输入微信号" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="婚姻状况" prop="maritalStatus">
<el-select v-model="form.maritalStatus" placeholder="请选择婚姻状况" disabled>
<el-option v-for="dict in marital_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="宗教信仰" prop="religion">
<el-input v-model="form.religion" placeholder="请输入宗教信仰" disabled />
</el-form-item>
</el-col>
</el-row>
</el-col>
<el-col :span="8">
<el-form-item prop="photoUrl">
<template #label>
<div class="photo-label-container">
<span>学生照片</span>
</div>
</template>
<div class="photo-container">
<el-avatar :size="120" :src="form.photoUrl || '/src/assets/images/profile.jpg'" />
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 地址信息 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="户籍所在地" prop="hukouLocation">
<el-input v-model="form.hukouLocation" type="textarea" :rows="2" placeholder="请输入户籍所在地" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="家庭地址" prop="homeAddress">
<el-input v-model="form.homeAddress" type="textarea" :rows="2" placeholder="请输入家庭地址" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="现住址" prop="currentAddress">
<el-input v-model="form.currentAddress" type="textarea" :rows="2" placeholder="请输入现住址" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="邮政编码" prop="postalCode">
<el-input v-model="form.postalCode" placeholder="请输入邮政编码" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="生源地区" prop="sourceRegion">
<el-input v-model="form.sourceRegion" placeholder="请输入生源地区" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="毕业院校" prop="graduateSchool">
<el-input v-model="form.graduateSchool" placeholder="请输入毕业院校" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="户口类型" prop="householdRegisterType">
<el-input v-model="form.householdRegisterType" placeholder="请输入户口类型" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<!-- 学籍信息Tab -->
<el-tab-pane label="学籍信息" name="academic">
<el-form ref="academicFormRef" :model="form" label-width="120px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="学籍状态" prop="studentStatus">
<el-tag :type="getStatusType(form.studentStatus)">
{{ getStudentStatusLabel(form.studentStatus) }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="入学年份" prop="enrollmentYear">
<el-input v-model="form.enrollmentYear" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="入学日期" prop="enrollmentDate">
<el-date-picker
v-model="form.enrollmentDate"
type="date"
placeholder="请选择入学日期"
value-format="YYYY-MM-DD"
disabled
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="录取类别" prop="admissionCategory">
<el-input v-model="form.admissionCategory" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="学制" prop="schoolSystem">
<el-input v-model="form.schoolSystem" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="学历层次" prop="academicLevel">
<el-input v-model="form.academicLevel" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="培养方式" prop="trainingType">
<el-input v-model="form.trainingType" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="年级" prop="grade">
<el-input v-model="form.grade" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="是否住校" prop="isBoarder">
<el-tag :type="form.isBoarder === '1' ? 'success' : 'info'">
{{ form.isBoarder === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="学院" prop="collegeName">
<el-input v-model="form.collegeName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="专业" prop="majorName">
<el-input v-model="form.majorName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="班级" prop="className">
<el-input v-model="form.className" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="辅导员" prop="counselorName">
<el-input v-model="form.counselorName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="班主任" prop="classTeacherName">
<el-input v-model="form.classTeacherName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="宿舍楼栋" prop="dormitoryBuilding">
<el-input v-model="form.dormitoryBuilding" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="宿舍房间号" prop="dormitoryRoom">
<el-input v-model="form.dormitoryRoom" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="银行卡号" prop="bankCardNo">
<el-input v-model="form.bankCardNo" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="开户银行" prop="bankName">
<el-input v-model="form.bankName" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<!-- 家庭信息Tab -->
<el-tab-pane label="家庭信息" name="family">
<el-form ref="familyFormRef" :model="form" label-width="120px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="父亲姓名" prop="fatherName">
<el-input v-model="form.fatherName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="父亲电话" prop="fatherPhone">
<el-input v-model="form.fatherPhone" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="父亲工作单位" prop="fatherWorkUnit">
<el-input v-model="form.fatherWorkUnit" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="母亲姓名" prop="motherName">
<el-input v-model="form.motherName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="母亲电话" prop="motherPhone">
<el-input v-model="form.motherPhone" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="母亲工作单位" prop="motherWorkUnit">
<el-input v-model="form.motherWorkUnit" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="监护人姓名" prop="guardianName">
<el-input v-model="form.guardianName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="监护人电话" prop="guardianPhone">
<el-input v-model="form.guardianPhone" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="监护人关系" prop="guardianRelation">
<el-input v-model="form.guardianRelation" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="家庭年收入" prop="familyIncome">
<el-input v-model="form.familyIncome" disabled>
<template #append>元</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="人均收入" prop="perCapitaIncome">
<el-input v-model="form.perCapitaIncome" disabled>
<template #append>元</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="收入来源" prop="incomeSource">
<el-input v-model="form.incomeSource" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="家庭经济状况" prop="familyEconomicStatus">
<el-input v-model="form.familyEconomicStatus" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="家庭成员数量" prop="familyMembers">
<el-input v-model="form.familyMembers" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="紧急联系人" prop="emergencyContactName">
<el-input v-model="form.emergencyContactName" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="紧急联系人电话" prop="emergencyContactPhone">
<el-input v-model="form.emergencyContactPhone" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="紧急联系人关系" prop="emergencyContactRelation">
<el-input v-model="form.emergencyContactRelation" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<!-- 特殊标识Tab -->
<el-tab-pane label="特殊标识" name="special">
<el-form ref="specialFormRef" :model="form" label-width="120px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="是否贫困生" prop="isPoorStudent">
<el-tag :type="form.isPoorStudent === '1' ? 'warning' : 'info'">
{{ form.isPoorStudent === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否少数民族" prop="isMinority">
<el-tag :type="form.isMinority === '1' ? 'success' : 'info'">
{{ form.isMinority === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否残疾" prop="isDisabled">
<el-tag :type="form.isDisabled === '1' ? 'danger' : 'info'">
{{ form.isDisabled === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否孤儿" prop="isOrphan">
<el-tag :type="form.isOrphan === '1' ? 'danger' : 'info'">
{{ form.isOrphan === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="是否留守儿童" prop="isLeftBehindChild">
<el-tag :type="form.isLeftBehindChild === '1' ? 'warning' : 'info'">
{{ form.isLeftBehindChild === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否独生子女" prop="isSingleChild">
<el-tag :type="form.isSingleChild === '1' ? 'success' : 'info'">
{{ form.isSingleChild === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否低保" prop="isLowIncome">
<el-tag :type="form.isLowIncome === '1' ? 'warning' : 'info'">
{{ form.isLowIncome === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否有助学贷款" prop="hasLoan">
<el-tag :type="form.hasLoan === '1' ? 'warning' : 'info'">
{{ form.hasLoan === '1' ? '是' : '否' }}
</el-tag>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="助学贷款金额" prop="loanAmount">
<el-input v-model="form.loanAmount" disabled>
<template #append>元</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="残疾类型" prop="disabilityType">
<el-input v-model="form.disabilityType" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="残疾等级" prop="disabilityLevel">
<el-input v-model="form.disabilityLevel" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<!-- 健康信息Tab -->
<el-tab-pane label="健康信息" name="health">
<el-form ref="healthFormRef" :model="form" label-width="120px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="身高" prop="height">
<el-input v-model="form.height" disabled>
<template #append>cm</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="体重" prop="weight">
<el-input v-model="form.weight" disabled>
<template #append>kg</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="血型" prop="bloodType">
<el-input v-model="form.bloodType" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="健康状况" prop="healthStatus">
<el-input v-model="form.healthStatus" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="特殊体质或疾病" prop="specialCondition">
<el-input v-model="form.specialCondition" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病史" prop="medicalHistory">
<el-input v-model="form.medicalHistory" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="过敏史" prop="allergies">
<el-input v-model="form.allergies" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="特长爱好" prop="specialties">
<el-input v-model="form.specialties" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="获奖情况" prop="awards">
<el-input v-model="form.awards" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="违纪记录" prop="violations">
<el-input v-model="form.violations" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="社会活动" prop="socialActivities">
<el-input v-model="form.socialActivities" type="textarea" :rows="3" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue';
// Props
interface Props {
visible: boolean;
studentId: number | null;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
studentId: null
});
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
// 响应式数据
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
const dialogTitle = ref('学生详情');
const activeTab = ref('basic');
const dialogLoading = ref(false);
// 表单数据
const form = reactive({
// 基础信息
id: null as number | null,
studentNo: '',
idCard: '',
name: '',
nameEnglish: '',
formerName: '',
gender: '',
birthDate: '',
nationality: '',
nativePlace: '',
politicalStatus: '',
hukouLocation: '',
homeAddress: '',
currentAddress: '',
postalCode: '',
photoUrl: '',
phone: '',
email: '',
qq: '',
wechat: '',
maritalStatus: '',
religion: '',
sourceRegion: '',
graduateSchool: '',
householdRegisterType: '',
// 学籍信息
studentStatus: '',
enrollmentYear: '',
enrollmentDate: '',
admissionCategory: '',
schoolSystem: '',
academicLevel: '',
trainingType: '',
grade: '',
isBoarder: '',
collegeName: '',
majorName: '',
className: '',
counselorName: '',
classTeacherName: '',
dormitoryBuilding: '',
dormitoryRoom: '',
bankCardNo: '',
bankName: '',
// 家庭信息
fatherName: '',
fatherPhone: '',
fatherWorkUnit: '',
motherName: '',
motherPhone: '',
motherWorkUnit: '',
guardianName: '',
guardianPhone: '',
guardianRelation: '',
familyIncome: '',
perCapitaIncome: '',
incomeSource: '',
familyEconomicStatus: '',
familyMembers: '',
emergencyContactName: '',
emergencyContactPhone: '',
emergencyContactRelation: '',
// 特殊标识
isPoorStudent: '',
isMinority: '',
isDisabled: '',
isOrphan: '',
isLeftBehindChild: '',
isSingleChild: '',
isLowIncome: '',
hasLoan: '',
loanAmount: '',
disabilityType: '',
disabilityLevel: '',
// 健康信息
height: '',
weight: '',
bloodType: '',
healthStatus: '',
specialCondition: '',
medicalHistory: '',
allergies: '',
specialties: '',
awards: '',
violations: '',
socialActivities: ''
});
// 字典数据
const sys_user_sex = ref([
{ value: '0', label: '男' },
{ value: '1', label: '女' }
]);
const marital_status = ref([
{ value: '1', label: '未婚' },
{ value: '2', label: '已婚' },
{ value: '3', label: '离异' },
{ value: '4', label: '丧偶' }
]);
// 模拟数据
const mockStudentData = {
id: 1,
studentNo: '2021001',
idCard: '110101200001011234',
name: '张三',
nameEnglish: 'Zhang San',
formerName: '',
gender: '0',
birthDate: '2000-01-01',
nationality: '汉族',
nativePlace: '北京市',
politicalStatus: '共青团员',
hukouLocation: '北京市朝阳区',
homeAddress: '北京市朝阳区某某街道某某小区',
currentAddress: '北京市海淀区某某大学宿舍',
postalCode: '100000',
photoUrl: '',
phone: '13800138001',
email: 'zhangsan@example.com',
qq: '123456789',
wechat: 'zhangsan123',
maritalStatus: '1',
religion: '',
sourceRegion: '北京市',
graduateSchool: '北京市第一中学',
householdRegisterType: '非农业',
// 学籍信息
studentStatus: '1',
enrollmentYear: '2021',
enrollmentDate: '2021-09-01',
admissionCategory: '统招',
schoolSystem: '4',
academicLevel: '本科',
trainingType: '全日制',
grade: '2021',
isBoarder: '1',
collegeName: '计算机学院',
majorName: '软件工程',
className: '软件2101班',
counselorName: '李老师',
classTeacherName: '王老师',
dormitoryBuilding: '1号楼',
dormitoryRoom: '101',
bankCardNo: '6222021234567890123',
bankName: '中国工商银行',
// 家庭信息
fatherName: '张父',
fatherPhone: '13900139001',
fatherWorkUnit: '某某公司',
motherName: '张母',
motherPhone: '13900139002',
motherWorkUnit: '某某单位',
guardianName: '张父',
guardianPhone: '13900139001',
guardianRelation: '父亲',
familyIncome: '120000',
perCapitaIncome: '40000',
incomeSource: '工资收入',
familyEconomicStatus: '一般',
familyMembers: '3',
emergencyContactName: '张父',
emergencyContactPhone: '13900139001',
emergencyContactRelation: '父亲',
// 特殊标识
isPoorStudent: '0',
isMinority: '0',
isDisabled: '0',
isOrphan: '0',
isLeftBehindChild: '0',
isSingleChild: '1',
isLowIncome: '0',
hasLoan: '0',
loanAmount: '0',
disabilityType: '',
disabilityLevel: '',
// 健康信息
height: '175',
weight: '65',
bloodType: 'A型',
healthStatus: '良好',
specialCondition: '',
medicalHistory: '',
allergies: '',
specialties: '篮球、编程',
awards: '校级优秀学生',
violations: '',
socialActivities: '学生会成员、志愿者'
};
// 方法
const handleClose = () => {
dialogVisible.value = false;
};
const getStatusType = (status: string) => {
const statusMap: Record<string, string> = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'info'
};
return statusMap[status] || 'info';
};
const getStudentStatusLabel = (status: string) => {
const statusMap: Record<string, string> = {
'1': '在籍',
'2': '休学',
'3': '复学',
'4': '退学',
'5': '毕业'
};
return statusMap[status] || '未知';
};
const loadStudentData = async (studentId: number) => {
dialogLoading.value = true;
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
// 使用模拟数据
Object.assign(form, mockStudentData);
} catch (error) {
console.error('加载学生数据失败:', error);
} finally {
dialogLoading.value = false;
}
};
// 监听studentId变化
watch(() => props.studentId, (newId) => {
if (newId && dialogVisible.value) {
loadStudentData(newId);
}
});
// 监听弹窗显示状态
watch(() => props.visible, (visible) => {
if (visible && props.studentId) {
loadStudentData(props.studentId);
}
});
</script>
<style scoped>
.photo-label-container {
display: flex;
align-items: center;
gap: 8px;
}
.photo-container {
display: flex;
justify-content: center;
align-items: center;
height: 120px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
background-color: #fafafa;
}
.dialog-footer {
text-align: right;
}
.el-tag {
margin-right: 8px;
}
</style>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,301 @@
<template>
<el-dialog
:title="mode === 'add' ? '新增' + getTypeLabel(type) : '编辑' + getTypeLabel(type)"
v-model="visible"
width="500px"
append-to-body
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="姓名" prop="name">
<el-select
v-model="form.name"
filterable
placeholder="请选择教师姓名"
@change="handleTeacherChange"
:disabled="mode === 'edit'"
style="width: 100%"
>
<el-option v-for="item in teacherOptions" :key="item.id" :label="item.managerName" :value="item.managerName">
<div>
<span> {{ item.managerName }} </span>
<span style="color: #8492a6; font-size: 13px"> ({{ item.phone }}) ({{ item.userId }}) </span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<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 label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watchEffect, computed, onMounted } from 'vue';
import { ElMessage, type FormInstance } from 'element-plus';
import request from '@/utils/request';
/**
* 教师信息数据结构
*/
interface TeacherData {
/** 教师ID */
userId: string | number;
/** 管理员姓名 */
managerName: string;
/** 手机号码 */
phone: string;
/** 性别(0男 1女) */
gender: string;
}
/**
* 获取可用的教职工信息列表
* @param managerType 管理员类型(1 全校管理员 2 自定义管理员 3 班主任)
*/
const listAvailableTeachers = (managerType: string) => {
return request({
url: '/xgStudentManager/studentManager/listAvailableTeachers',
method: 'get',
params: { managerType }
});
};
/**
* 新增宿舍管理员信息
*/
const addStudentManager = (data: any) => {
return request({
url: '/xgStudentManager/studentManager',
method: 'post',
data: data
});
};
/**
* 修改宿舍管理员信息
*/
const updateStudentManager = (data: any) => {
return request({
url: '/xgStudentManager/studentManager',
method: 'put',
data: data
});
};
// 在setup函数中引入这种方式提示弹窗
const message = (type: 'success' | 'error' | 'info', text: string) => {
if (type === 'success') {
ElMessage.success(text);
} else if (type === 'error') {
ElMessage.error(text);
} else {
ElMessage.info(text);
}
};
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'add'
},
type: {
type: String,
default: 'school'
},
admin: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:modelValue', 'submit']);
// 教师选项列表
const teacherOptions = ref<TeacherData[]>([]);
// 计算属性:对话框可见性
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
// 表单和校验
const formRef = ref<FormInstance>();
const form = reactive({
id: null,
userId: null,
name: '',
phone: '',
sex: 0,
remark: ''
});
// 表单校验规则
const rules = {
name: [{ required: true, message: '请选择姓名', trigger: 'change' }],
phone: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
};
// 获取管理员类型对应的值
const getManagerType = (type: string): string => {
switch (type) {
case 'school':
return '1'; // 全校管理员
case 'custom':
return '2'; // 自定义管理员
case 'teacher':
return '3'; // 班主任
default:
return '1';
}
};
// 加载可用教师列表
const loadTeachers = async () => {
try {
const managerType = getManagerType(props.type);
const res = await listAvailableTeachers(managerType);
if (res.data) {
teacherOptions.value = res.data;
}
} catch (error) {
console.error('获取可用教师列表失败', error);
}
};
// 选择教师后的处理
const handleTeacherChange = (value: string) => {
const selectedTeacher = teacherOptions.value.find((item) => item.managerName === value);
if (selectedTeacher) {
console.log(selectedTeacher.gender === '0');
// 自动填充手机号码和性别
form.phone = selectedTeacher.phone || '';
form.sex = selectedTeacher.gender === '0' ? 0 : 1; // 0男 1女
form.userId = selectedTeacher.userId;
}
};
// 监听传入的admin数据,更新表单
watchEffect(() => {
if (props.admin) {
form.id = props.admin.id;
form.userId = props.admin.userId;
form.name = props.admin.managerName;
form.phone = props.admin.phone;
form.sex = props.admin.gender === '0' ? 0 : 1;
form.remark = props.admin.remark;
}
});
// 监听对话框可见性变化,当打开时加载教师列表
watchEffect(() => {
if (visible.value && props.mode === 'add') {
loadTeachers();
}
});
// 组件挂载时加载教师列表
onMounted(() => {
if (props.mode === 'add') {
loadTeachers();
}
});
// 根据类型获取标签文本
function getTypeLabel(type: string): string {
switch (type) {
case 'school':
return '全校管理员';
case 'custom':
return '自定义管理员';
case 'teacher':
return '班主任';
default:
return '管理员';
}
}
// 提交表单
async function submitForm() {
formRef.value?.validate(async (valid: boolean) => {
if (valid) {
try {
const formData = {
id: form.id,
managerName: form.name,
userId: form.userId,
phone: form.phone,
gender: form.sex,
remark: form.remark,
status: '0',
// scope: props.admin?.scope || '',
managerType: getManagerType(props.type),
managerLevel: getManagerType(props.type)
};
let result;
if (props.mode === 'add') {
// 调用添加接口
result = await addStudentManager(formData);
} else {
// 调用更新接口
result = await updateStudentManager(formData);
}
if (result.code === 200) {
message('success', props.mode === 'add' ? '添加成功' : '更新成功');
// 通知父组件处理成功
emit('submit', formData);
visible.value = false;
} else {
message('error', result.msg || '操作失败');
}
} catch (error: any) {
message('error', '操作失败: ' + (error.message || '未知错误'));
}
}
});
}
// 取消
function cancel() {
visible.value = false;
}
// 暴露组件以修复导出问题
defineExpose({
submitForm,
cancel
});
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>
@@ -0,0 +1,271 @@
<template>
<el-dialog :title="getScopeDialogTitle(type)" v-model="visible" width="600px" append-to-body destroy-on-close>
<el-form :model="form" label-width="100px">
<el-form-item :label="getScopeName(type)">
<div v-if="loading" class="loading-container">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
<div v-else-if="type === 'custom'">
<el-tree
:data="scopeOptions.custom"
show-checkbox
node-key="id"
:default-checked-keys="form.selectedScopes"
:props="{ label: 'label', children: 'children' }"
@check="onTreeCheck"
default-expand-all
style="
width: 100%;
min-width: 100%;
overflow-x: auto;
overflow-y: auto;
/* border: 1px solid #ebeef5; */
border-radius: 4px;
padding: 10px;
box-sizing: border-box;
"
/>
</div>
<div v-else-if="type === 'teacher'">
<el-checkbox-group v-model="form.selectedScopes">
<el-checkbox v-for="item in scopeOptions.teacher" :key="item.id" :label="item.id">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm" :loading="loading">确 定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import axios from 'axios';
import request from '@/utils/request';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
admin: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: 'custom'
}
});
const emit = defineEmits(['update:modelValue', 'submit']);
// 计算属性:对话框可见性
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
// 范围选项(从API获取)
const scopeOptions = reactive({
custom: [] as any[],
teacher: [] as any[]
});
// 加载状态
const loading = ref(false);
// 表单数据
const form = reactive({
selectedScopes: [] as number[]
});
// 获取部门树数据
async function fetchDeptTree() {
try {
loading.value = true;
const response = await request({
url: '/system/user/deptTree',
method: 'get',
params: {
belongDeptId: 101
}
});
if (response && response.code === 200) {
// custom类型直接使用树形结构
if (props.type === 'custom') {
scopeOptions.custom = response.data || [];
} else if (props.type === 'teacher') {
// teacher类型仍可用扁平化
scopeOptions.teacher = flattenTreeData(response.data || []);
}
}
} catch (error) {
console.error('获取部门树数据失败:', error);
ElMessage.error('获取范围选项失败');
} finally {
loading.value = false;
}
}
// 将树形数据扁平化
function flattenTreeData(treeData: any[]): any[] {
const result: any[] = [];
function traverse(nodes: any[]) {
nodes.forEach((node) => {
result.push({
id: node.id,
label: node.label
});
if (node.children && node.children.length > 0) {
traverse(node.children);
}
});
}
traverse(treeData);
return result;
}
// 新增:获取历史范围数据
async function fetchSelectedScopes(managerId: number | string) {
if (!managerId) return;
try {
const res = await request({
url: '/xgStudentManagerSource/studentManagerSource/list',
method: 'get',
params: { managerId }
});
if (res && res.code === 200) {
// 提取rows中的authSource字段作为已选范围ID
form.selectedScopes = (res.rows || []).map((item: any) => item.authSource);
}
} catch (e) {
form.selectedScopes = [];
}
}
// 监听对话框显示状态,自动加载数据
watch(visible, async (newVal) => {
if (newVal) {
const managerId = props.admin?.managerId || props.admin?.id;
await fetchDeptTree();
await fetchSelectedScopes(managerId);
} else {
// 对话框关闭时清空选择
form.selectedScopes = [];
}
});
// el-tree勾选事件,确保能正确获取所有勾选节点的id
function onTreeCheck(checkedNodes: any, info: { checkedKeys: any[]; halfCheckedKeys: any[] }) {
form.selectedScopes = info.checkedKeys;
}
// 获取范围对话框标题
function getScopeDialogTitle(type: string) {
const name = props.admin?.managerName || '';
switch (type) {
case 'custom':
return `为【${name}】分配管理范围`;
case 'teacher':
return `为【${name}】分配班级`;
default:
return `为【${name}】分配范围`;
}
}
// 获取范围名称
function getScopeName(type: string) {
switch (type) {
case 'custom':
return '管理范围';
case 'teacher':
return '所管理班级';
default:
return '管理范围';
}
}
// 提交表单
async function submitForm() {
// 组装要提交的数据
const selectedIds = form.selectedScopes;
console.log(selectedIds);
const managerId = props.admin?.managerId || props.admin?.id; // 兼容不同字段
if (!managerId) {
ElMessage.error('管理员ID缺失,无法提交');
return;
}
loading.value = true;
try {
await request({
url: '/xgStudentManagerSource/studentManagerSource/grantDeptAuth',
method: 'post',
params: { managerId },
data: selectedIds
});
ElMessage.success('分配成功');
visible.value = false;
emit('submit', {
sourceIds: selectedIds
});
} catch (error) {
ElMessage.error('分配失败,请重试');
} finally {
loading.value = false;
}
}
// 取消
function cancel() {
visible.value = false;
}
// 从props获取类型的响应式引用
const type = computed(() => props.type);
// 暴露组件以修复导出问题
defineExpose({
submitForm,
cancel,
fetchDeptTree
});
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.el-checkbox-group {
display: flex;
flex-wrap: wrap;
}
.el-checkbox {
margin-right: 15px;
margin-bottom: 10px;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #909399;
}
.loading-container .el-icon {
margin-right: 8px;
font-size: 16px;
}
</style>
@@ -8,7 +8,7 @@
<el-card shadow="hover">
<el-form :inline="true" :model="schoolSearchForm" class="search-form">
<el-form-item label="姓名">
<el-input v-model="schoolSearchForm.name" placeholder="请输入姓名" clearable @keyup.enter="handleSearch('school')" />
<el-input v-model="schoolSearchForm.managerName" placeholder="请输入姓名" clearable @keyup.enter="handleSearch('school')" />
</el-form-item>
<el-form-item label="手机号码">
<el-input v-model="schoolSearchForm.phone" placeholder="请输入手机号码" clearable @keyup.enter="handleSearch('school')" />
@@ -30,11 +30,11 @@
</el-row>
</template>
<el-table :data="filteredAdmins('school')" border>
<el-table-column prop="name" label="姓名" align="center" />
<el-table-column prop="managerName" label="姓名" align="center" />
<el-table-column prop="phone" label="手机号码" align="center" />
<el-table-column prop="sex" label="性别" align="center">
<el-table-column prop="gender" label="性别" align="center">
<template #default="scope">
{{ scope.row.sex === 0 ? '男' : '女' }}
{{ scope.row.gender == 0 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" align="center" />
@@ -57,13 +57,13 @@
<el-card shadow="hover">
<el-form :inline="true" :model="customSearchForm" class="search-form">
<el-form-item label="姓名">
<el-input v-model="customSearchForm.name" placeholder="请输入姓名" clearable @keyup.enter="handleSearch('custom')" />
<el-input v-model="customSearchForm.managerName" placeholder="请输入姓名" clearable @keyup.enter="handleSearch('custom')" />
</el-form-item>
<el-form-item label="手机号码">
<el-input v-model="customSearchForm.phone" placeholder="请输入手机号码" clearable @keyup.enter="handleSearch('custom')" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch('custom')">搜索</el-button>
<el-button type="primary" icon="Search" @click="fetchCustomAdminsWithScope('custom')">搜索</el-button>
<el-button icon="Refresh" @click="handleReset('custom')">重置</el-button>
</el-form-item>
</el-form>
@@ -78,14 +78,30 @@
</el-row>
</template>
<el-table :data="filteredAdmins('custom')" border>
<el-table-column prop="name" label="姓名" align="center" />
<el-table-column prop="managerName" label="姓名" align="center" />
<el-table-column prop="phone" label="手机号码" align="center" />
<el-table-column prop="sex" label="性别" align="center">
<el-table-column prop="gender" label="性别" align="center">
<template #default="scope">
{{ scope.row.sex === 0 ? '男' : '女' }}
{{ scope.row.gender == 0 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="scope" label="管理范围" align="center">
<template #default="scope">
<el-tooltip
v-if="scope.row.authSourceNames && scope.row.authSourceNames.length > 2"
:content="scope.row.authSourceNames.join(',')"
placement="top"
>
<span>
{{ scope.row.authSourceNames.slice(0, 2).join(',')
}}{{ scope.row.authSourceNames.length > 2 ? ' 等' + scope.row.authSourceNames.length + '个' : '' }}
</span>
</el-tooltip>
<span v-else>
{{ scope.row.authSourceNames?.join(',') || '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="scope" label="管理范围" align="center" />
<el-table-column prop="remark" label="备注" align="center" />
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
<template #default="scope">
@@ -109,7 +125,7 @@
<el-card shadow="hover">
<el-form :inline="true" :model="teacherSearchForm" class="search-form">
<el-form-item label="姓名">
<el-input v-model="teacherSearchForm.name" placeholder="请输入姓名" clearable @keyup.enter="handleSearch('teacher')" />
<el-input v-model="teacherSearchForm.managerName" placeholder="请输入姓名" clearable @keyup.enter="handleSearch('teacher')" />
</el-form-item>
<el-form-item label="手机号码">
<el-input v-model="teacherSearchForm.phone" placeholder="请输入手机号码" clearable @keyup.enter="handleSearch('teacher')" />
@@ -122,13 +138,13 @@
</el-card>
</div>
<el-card shadow="never">
<template #header>
<!-- <template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="onAdd('teacher')">新增班主任</el-button>
</el-col>
</el-row>
</template>
</template> -->
<el-table :data="filteredAdmins('teacher')" border>
<el-table-column prop="name" label="姓名" align="center" />
<el-table-column prop="phone" label="手机号码" align="center" />
@@ -137,9 +153,9 @@
{{ scope.row.sex === 0 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="scope" label="所管理班级" align="center" />
<el-table-column prop="className" label="所管理班级" align="center" />
<el-table-column prop="remark" label="备注" align="center" />
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
<!-- <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="编辑" placement="top">
<el-button link type="primary" plain icon="Edit" @click="onEdit('teacher', scope.row)" />
@@ -151,7 +167,7 @@
<el-button link type="danger" plain icon="Delete" @click="onDelete('teacher', scope.row)" />
</el-tooltip>
</template>
</el-table-column>
</el-table-column> -->
</el-table>
</el-card>
</el-tab-pane>
@@ -163,23 +179,35 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted, watch } from 'vue';
import AdminDialog from './components/AdminDialog.vue';
import ScopeDialog from './components/ScopeDialog.vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listStudentManager, delStudentManager, listActiveClassTeachers } from '@/api/xg/xsjcxx/xgStudentManager/studentManager';
import type { StudentManagerQuery } from '@/api/xg/xsjcxx/xgStudentManager/studentManager/types';
import service from '@/utils/request';
// 暴露组件以修复导出问题
defineExpose({});
// 导入改为使用模拟数据
// import { getAdminList, deleteAdmin, assignScope, getScopeNames } from '@/api/studentMsg/authmsg';
const activeTab = ref('school');
// 各tab独立检索表单
const schoolSearchForm = reactive({ name: '', phone: '' });
const customSearchForm = reactive({ name: '', phone: '' });
const teacherSearchForm = reactive({ name: '', phone: '' });
const schoolSearchForm = reactive<StudentManagerQuery>({
managerName: '',
phone: '',
managerType: '1',
pageNum: 1,
pageSize: 1000
});
const customSearchForm = reactive<StudentManagerQuery>({
managerName: '',
phone: '',
managerType: '2',
pageNum: 1,
pageSize: 1000
});
const teacherSearchForm = reactive({ managerName: '', phone: '' });
// 管理员数据
const schoolAdmins = ref([]);
@@ -192,46 +220,38 @@ const currentAdmin = ref<any>(null);
const scopeDialogVisible = ref(false);
const scopeType = ref('');
// 模拟数据
const mockData = {
school: [
{ id: 1, name: '张校长', phone: '13800138001', sex: 0, remark: '全校管理员' },
{ id: 2, name: '李主任', phone: '13800138002', sex: 1, remark: '教务处负责人' }
],
custom: [
{ id: 3, name: '王组长', phone: '13800138003', sex: 0, scope: '高一年级', remark: '年级组长' },
{ id: 4, name: '赵主管', phone: '13800138004', sex: 1, scope: '高二年级', remark: '年级负责人' }
],
teacher: [
{ id: 5, name: '刘老师', phone: '13800138005', sex: 0, scope: '高一(1)班', remark: '数学教师' },
{ id: 6, name: '陈老师', phone: '13800138006', sex: 1, scope: '高一(2)班', remark: '语文教师' }
]
};
// 班主任模拟数据 (预留,后续完善)
const mockTeacherData = [
{ id: 5, name: '刘老师', phone: '13800138005', sex: 0, scope: '高一(1)班', remark: '数学教师' },
{ id: 6, name: '陈老师', phone: '13800138006', sex: 1, scope: '高一(2)班', remark: '语文教师' }
];
// 获取管理员列表
async function fetchAdmins(type: string) {
// 实际项目中这里会调用API
// 此处使用模拟数据
try {
// 模拟API检索过滤
const searchForm = getSearchForm(type);
const filteredData = mockData[type].filter((item) => {
const nameMatch = !searchForm.name || item.name.includes(searchForm.name);
const phoneMatch = !searchForm.phone || item.phone.includes(searchForm.phone);
return nameMatch && phoneMatch;
});
// 更新对应类型的管理员数据
switch (type) {
case 'school':
schoolAdmins.value = filteredData;
break;
case 'custom':
customAdmins.value = filteredData;
break;
case 'teacher':
teacherAdmins.value = filteredData;
break;
if (type === 'school' || type === 'custom') {
// 调用API获取全校管理员或自定义管理员数据
const managerType = type === 'school' ? '1' : '2';
const res = await listStudentManager({
...searchForm,
managerType
});
// 更新对应类型的管理员数据
if (type === 'school') {
schoolAdmins.value = res.rows || [];
} else {
customAdmins.value = res.rows || [];
}
} else if (type === 'teacher') {
// 通过API获取班主任数据
const params = {
phone: searchForm.phone,
name: searchForm.managerName // teacherSearchForm 实际只有 managerName 字段
};
const res = await listActiveClassTeachers(params);
teacherAdmins.value = res.data || [];
}
} catch (error) {
console.error('获取管理员列表失败:', error);
@@ -252,9 +272,34 @@ function getSearchForm(type: string) {
}
}
// 获取自定义管理员及其管理范围
async function fetchCustomAdminsWithScope() {
await fetchAdmins('custom');
// 遍历 customAdmins,异步获取每个管理员的authSource
for (const admin of customAdmins.value) {
try {
const res = await service({
url: '/xgStudentManagerSource/studentManagerSource/authSourceNames',
method: 'get',
params: { managerId: admin.id }
});
admin.authSourceNames = res?.data || [];
} catch (e) {
admin.authSourceNames = [];
}
}
}
// 监听tab切换,切到custom时自动加载管理范围
watch(activeTab, (val) => {
if (val === 'custom') {
fetchCustomAdminsWithScope();
}
});
onMounted(() => {
fetchAdmins('school');
fetchAdmins('custom');
fetchCustomAdminsWithScope();
fetchAdmins('teacher');
});
@@ -274,17 +319,31 @@ function onEdit(type: string, admin: any) {
async function onDelete(type: string, admin: any) {
// 二次确认
ElMessageBox.confirm(`确定要删除管理员【${admin.name}】吗?`, '提示', {
ElMessageBox.confirm(`确定要删除管理员【${admin.name || admin.managerName}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
// 模拟删除成功
mockData[type] = mockData[type].filter((item) => item.id !== admin.id);
ElMessage.success('删除成功');
fetchAdmins(type); // 删除后刷新
if (type === 'school' || type === 'custom') {
// 调用删除API
const res = await delStudentManager(admin.id);
if (res.code === 200) {
ElMessage.success('删除成功');
fetchAdmins(type);
} else {
ElMessage.error(res.msg || '删除失败');
}
} else {
// 班主任暂时使用模拟数据
ElMessage.success('删除成功');
mockTeacherData.splice(
mockTeacherData.findIndex((item) => item.id === admin.id),
1
);
fetchAdmins(type);
}
} catch (e) {
ElMessage.error('删除失败');
}
@@ -301,36 +360,23 @@ function onScope(type: string, admin: any) {
}
function onDialogSubmit(admin: any) {
// 模拟添加或编辑操作
if (dialogMode.value === 'add') {
// 生成新ID
const newId = Math.max(...mockData[activeTab.value].map((item) => item.id), 0) + 1;
admin.id = newId;
mockData[activeTab.value].push(admin);
} else {
// 编辑
const index = mockData[activeTab.value].findIndex((item) => item.id === admin.id);
if (index !== -1) {
mockData[activeTab.value][index] = admin;
}
}
// 表单已在对话框组件内处理API调用
// 刷新列表数据
fetchAdmins(activeTab.value);
if (activeTab.value === 'custom') {
fetchCustomAdminsWithScope();
} else {
fetchAdmins(activeTab.value);
}
dialogVisible.value = false;
}
async function onScopeSubmit(scope: any) {
try {
// 模拟分配范围
const index = mockData[scopeType.value].findIndex((item) => item.id === currentAdmin.value.id);
if (index !== -1) {
mockData[scopeType.value][index].scope = scope.scopeName;
}
// 范围分配已在对话框组件内处理API调用
// 成功后刷新数据并提示
ElMessage.success('管理范围分配成功');
fetchAdmins(scopeType.value);
fetchCustomAdminsWithScope();
scopeDialogVisible.value = false;
} catch (error) {
ElMessage.error('管理范围分配失败');
@@ -353,6 +399,9 @@ function getListByType(type: string) {
// 过滤
function filteredAdmins(type: string) {
if (type === 'custom') {
return customAdmins.value;
}
return getListByType(type).value;
}
@@ -363,19 +412,23 @@ function handleSearch(type: string) {
function handleReset(type: string) {
switch (type) {
case 'school':
schoolSearchForm.name = '';
schoolSearchForm.managerName = '';
schoolSearchForm.phone = '';
break;
case 'custom':
customSearchForm.name = '';
customSearchForm.managerName = '';
customSearchForm.phone = '';
break;
case 'teacher':
teacherSearchForm.name = '';
teacherSearchForm.managerName = '';
teacherSearchForm.phone = '';
break;
}
fetchAdmins(type);
if (type === 'custom') {
fetchCustomAdminsWithScope();
} else {
fetchAdmins(type);
}
}
</script>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,490 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<!-- <el-form-item label="删除标志" prop="deleted" v-if="false">
<el-input v-model="queryParams.deleted" placeholder="请输入删除标志" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="职务编码" prop="positionCode" v-if="false">
<el-input v-model="queryParams.positionCode" placeholder="请输入职务编码" clearable @keyup.enter="handleQuery" />
</el-form-item> -->
<el-form-item label="职务名称" prop="positionName">
<el-input v-model="queryParams.positionName" placeholder="请输入职务名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学生职务类别" prop="positionCategory">
<el-select v-model="queryParams.positionCategory" placeholder="请选择学生职务类别" clearable>
<el-option v-for="dict in stu_position_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学生职务级别" prop="positionLevel">
<el-select v-model="queryParams.positionLevel" placeholder="请选择学生职务级别" clearable>
<el-option v-for="dict in stu_position_level" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学生职务类型" prop="positionType">
<el-select v-model="queryParams.positionType" placeholder="请选择学生职务类型" clearable>
<el-option v-for="dict in stu_position_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<!-- <el-form-item label="职责描述" prop="responsibilities" v-if="false">
<el-input v-model="queryParams.responsibilities" placeholder="请输入职责描述" clearable @keyup.enter="handleQuery" />
</el-form-item> -->
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in sys_item_enable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<!-- <el-form-item label="排序顺序" prop="sortOrder" v-if="false">
<el-input v-model="queryParams.sortOrder" placeholder="请输入排序顺序" clearable @keyup.enter="handleQuery" />
</el-form-item> -->
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['xgClassPosDict:xgClassPosDict:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['xgClassPosDict:xgClassPosDict:edit']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete()"
v-hasPermi="['xgClassPosDict:xgClassPosDict:remove']"
>删除</el-button
>
</el-col>
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['xgClassPosDict:xgClassPosDict:export']"
>导出</el-button
>
</el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="xgClassPosDictList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="id" v-if="true" />
<!-- <el-table-column label="删除标志" align="center" prop="deleted" /> -->
<el-table-column label="职务编码" align="center" prop="positionCode" />
<el-table-column label="职务名称" align="center" prop="positionName" />
<el-table-column label="学生职务类别" align="center" prop="positionCategory">
<template #default="scope">
<dict-tag :options="stu_position_category" :value="scope.row.positionCategory" />
</template>
</el-table-column>
<el-table-column label="学生职务级别" align="center" prop="positionLevel">
<template #default="scope">
<dict-tag :options="stu_position_level" :value="scope.row.positionLevel" />
</template>
</el-table-column>
<el-table-column label="学生职务类型" align="center" prop="positionType">
<template #default="scope">
<dict-tag :options="stu_position_type" :value="scope.row.positionType" />
</template>
</el-table-column>
<el-table-column label="职责描述" align="center" prop="responsibilities" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_item_enable" :value="scope.row.status" />
</template>
</el-table-column>
<!-- <el-table-column label="排序顺序" align="center" prop="sortOrder" /> -->
<el-table-column label="备注信息" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['xgClassPosDict:xgClassPosDict:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['xgClassPosDict:xgClassPosDict:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改班级干部职务配置对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="xgClassPosDictFormRef" :model="form" :rules="rules" label-width="110px">
<!-- <el-form-item label="删除标志" prop="deleted">
<el-input v-model="form.deleted" placeholder="请输入删除标志" />
</el-form-item> -->
<el-form-item label="职务名称" prop="positionName">
<el-input v-model="form.positionName" placeholder="请输入职务名称" />
</el-form-item>
<el-form-item label="职务编码" prop="positionCode">
<el-input
v-model="form.positionCode"
:placeholder="form.id ? '职务编码不可修改' : '请输入职务编码后缀(如:001、班长、副班长等)'"
:disabled="!!form.id"
@input="handlePositionCodeInput"
>
<template #prepend>XT-BJZW-</template>
</el-input>
<div class="form-tip">
<el-text type="info" size="small">
<el-icon><InfoFilled /></el-icon>
<span v-if="form.id">职务编码创建后不可修改</span>
<span v-else>编码格式:XT-BJZW- + 自定义后缀(如:XT-BJZW-001、XT-BJZW-班长)</span>
</el-text>
</div>
</el-form-item>
<el-form-item label="学生职务类别" prop="positionCategory">
<el-select v-model="form.positionCategory" placeholder="请选择学生职务类别">
<el-option v-for="dict in stu_position_category" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="学生职务级别" prop="positionLevel">
<el-select v-model="form.positionLevel" placeholder="请选择学生职务级别">
<el-option v-for="dict in stu_position_level" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
</el-select>
</el-form-item>
<el-form-item label="学生职务类型" prop="positionType">
<el-select v-model="form.positionType" placeholder="请选择学生职务类型">
<el-option v-for="dict in stu_position_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="职责描述" prop="responsibilities">
<el-input v-model="form.responsibilities" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option v-for="dict in sys_item_enable" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="排序顺序" prop="sortOrder">
<el-input v-model="form.sortOrder" placeholder="请输入排序顺序" />
</el-form-item> -->
<el-form-item label="备注信息" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="XgClassPosDict" lang="ts">
import { InfoFilled } from '@element-plus/icons-vue';
import {
listXgClassPosDict,
getXgClassPosDict,
delXgClassPosDict,
addXgClassPosDict,
updateXgClassPosDict
} from '@/api/xg/xsjcxx/xgClassPosDict/xgClassPosDict';
import { XgClassPosDictVO, XgClassPosDictQuery, XgClassPosDictForm } from '@/api/xg/xsjcxx/xgClassPosDict/xgClassPosDict/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { stu_position_level, stu_position_type, sys_item_enable, stu_position_category } = toRefs<any>(
proxy?.useDict('stu_position_level', 'stu_position_type', 'sys_item_enable', 'stu_position_category')
);
const xgClassPosDictList = ref<XgClassPosDictVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const xgClassPosDictFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: XgClassPosDictForm = {
id: undefined,
deleted: undefined,
positionCode: undefined,
positionName: undefined,
positionCategory: 'class', // 默认选择:班级干部
positionLevel: 1, // 默认选择:班长级别
positionType: 'regular', // 默认选择:常规职务
responsibilities: undefined,
status: '0', // 默认选择:启用状态
sortOrder: undefined,
remark: undefined
};
const data = reactive<PageData<XgClassPosDictForm, XgClassPosDictQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
deleted: undefined,
positionCode: undefined,
positionName: undefined,
positionCategory: undefined,
positionLevel: undefined,
positionType: undefined,
responsibilities: undefined,
status: undefined,
sortOrder: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
positionCode: [
{
required: (form: any) => !form.id,
message: '职务编码不能为空',
trigger: 'blur'
},
{
pattern: /^XT-BJZW-.+/,
message: '职务编码必须以XT-BJZW-开头,后面需要补充编码后缀',
trigger: 'blur'
}
],
positionName: [{ required: true, message: '职务名称不能为空', trigger: 'blur' }],
positionCategory: [{ required: true, message: '学生职务类别:class-班级干部,party-党团干部,other-其他不能为空', trigger: 'change' }],
positionLevel: [{ required: true, message: '学生职务级别:1-班长,2-副班长,3-委员,9-普通成员不能为空', trigger: 'change' }],
positionType: [{ required: true, message: '学生职务类型:regular-常规职务,special-特殊职务,temporary-临时职务不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态:0-启用,1-停用不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询班级干部职务配置列表 */
const getList = async () => {
loading.value = true;
const res = await listXgClassPosDict(queryParams.value);
xgClassPosDictList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
xgClassPosDictFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: XgClassPosDictVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加班级干部职务配置';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: XgClassPosDictVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getXgClassPosDict(_id);
Object.assign(form.value, res.data);
// 确保职务编码格式正确
if (form.value.positionCode && !form.value.positionCode.startsWith('XT-BJZW-')) {
form.value.positionCode = `XT-BJZW-${form.value.positionCode}`;
}
dialog.visible = true;
dialog.title = '修改班级干部职务配置';
};
/** 提交按钮 */
const submitForm = () => {
xgClassPosDictFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateXgClassPosDict(form.value).finally(() => (buttonLoading.value = false));
} else {
await addXgClassPosDict(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: XgClassPosDictVO) => {
const _ids = row?.id || ids.value;
// 根据删除类型生成不同的确认提示
let confirmMessage = '';
if (row) {
// 单个删除
confirmMessage = `是否确认删除班级干部职务配置"${row.positionName}"?`;
} else {
// 批量删除
confirmMessage = `是否确认删除选中的 ${ids.value.length} 条班级干部职务配置数据?`;
}
await proxy?.$modal.confirm(confirmMessage).finally(() => (loading.value = false));
await delXgClassPosDict(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'xgClassPosDict/xgClassPosDict/export',
{
...queryParams.value
},
`xgClassPosDict_${new Date().getTime()}.xlsx`
);
};
/** 处理职务编码输入 */
const handlePositionCodeInput = (value: string) => {
// 如果是修改模式,不允许修改职务编码
if (form.value.id) {
return;
}
// 如果用户输入的是完整编码(包含前缀),直接使用
if (value.startsWith('XT-BJZW-')) {
form.value.positionCode = value;
} else {
// 如果用户只输入后缀,自动拼接前缀
const suffix = value.replace(/^XT-BJZW-/, ''); // 移除可能的前缀
form.value.positionCode = `XT-BJZW-${suffix}`;
}
};
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
.page-container {
.table-container {
.tree-table {
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
}
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
/* 表单提示信息样式 */
.form-tip {
margin-top: 4px;
.el-text {
display: flex;
align-items: center;
gap: 4px;
.el-icon {
font-size: 12px;
}
}
}
</style>
File diff suppressed because it is too large. Load diff
+236
View File
@@ -0,0 +1,236 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="search">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="请假天数" prop="startLeaveDays">
<el-input v-model="queryParams.startLeaveDays" placeholder="请输入请假天数" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item prop="endLeaveDays"> 至 </el-form-item>
<el-form-item prop="endLeaveDays">
<el-input v-model="queryParams.endLeaveDays" placeholder="请输入请假天数" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button v-hasPermi="['workflow:leave:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['workflow:leave:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="leaveList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="false" label="主键" align="center" prop="id" />
<el-table-column label="请假类型" align="center">
<template #default="scope">
<el-tag>{{ options.find((e) => e.value === scope.row.leaveType)?.label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="开始时间" align="center" prop="startDate">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.startDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endDate">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.endDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="请假天数" align="center" prop="leaveDays" />
<el-table-column label="请假原因" align="center" prop="remark" />
<el-table-column align="center" label="流程状态" min-width="70">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="162">
<template #default="scope">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
<el-button v-hasPermi="['workflow:leave:edit']" size="small" type="primary" icon="Edit" @click="handleUpdate(scope.row)"
>修改</el-button
>
</el-col>
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
<el-button v-hasPermi="['workflow:leave:remove']" size="small" type="primary" icon="Delete" @click="handleDelete(scope.row)"
>删除</el-button
>
</el-col>
</el-row>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
</el-col>
<el-col :span="1.5" v-if="scope.row.status === 'waiting'">
<el-button size="small" type="primary" icon="Notification" @click="handleCancelProcessApply(scope.row.id)">撤销</el-button>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="Leave" lang="ts">
import { delLeave, listLeave } from '@/api/workflow/leave';
import { cancelProcessApply } from '@/api/workflow/instance';
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
const leaveList = ref<LeaveVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const options = [
{
value: '1',
label: '事假'
},
{
value: '2',
label: '调休'
},
{
value: '3',
label: '病假'
},
{
value: '4',
label: '婚假'
}
];
const queryFormRef = ref<ElFormInstance>();
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
startLeaveDays: undefined,
endLeaveDays: undefined
},
rules: {}
});
const { queryParams } = toRefs(data);
/** 查询请假列表 */
const getList = async () => {
loading.value = true;
const res = await listLeave(queryParams.value);
leaveList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: LeaveVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
query: {
type: 'add'
}
});
};
/** 修改按钮操作 */
const handleUpdate = (row?: LeaveVO) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
query: {
id: row.id,
type: 'update'
}
});
};
/** 查看按钮操作 */
const handleView = (row?: LeaveVO) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
query: {
id: row.id,
type: 'view'
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: LeaveVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除请假编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delLeave(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'workflow/leave/export',
{
...queryParams.value
},
`leave_${new Date().getTime()}.xlsx`
);
};
/** 撤销按钮操作 */
const handleCancelProcessApply = async (id: string) => {
await proxy?.$modal.confirm('是否确认撤销当前单据?');
loading.value = true;
const data = {
businessId: id,
message: '申请人撤销流程!'
};
await cancelProcessApply(data).finally(() => (loading.value = false));
await getList();
proxy?.$modal.msgSuccess('撤销成功');
};
onMounted(() => {
getList();
});
</script>
+314
View File
@@ -0,0 +1,314 @@
<template>
<div class="p-2">
<el-card shadow="never">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<el-card shadow="never" style="height: 78vh; overflow-y: auto">
<el-form ref="leaveFormRef" v-loading="loading" :disabled="routeParams.type === 'view'" :model="form" :rules="rules" label-width="80px">
<el-form-item label="请假类型" prop="leaveType">
<el-select v-model="form.leaveType" placeholder="请选择请假类型" style="width: 100%">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="请假时间" required>
<el-date-picker
v-model="leaveTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="To"
start-placeholder="开始时间"
end-placeholder="结束时间"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
@change="changeLeaveTime()"
/>
</el-form-item>
<el-form-item label="请假天数" prop="leaveDays">
<el-input v-model="form.leaveDays" disabled type="number" placeholder="请输入请假天数" />
</el-form-item>
<el-form-item label="请假原因" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入请假原因" />
</el-form-item>
</el-form>
</el-card>
<!-- 提交组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 审批记录 -->
<approvalRecord ref="approvalRecordRef" />
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" :before-close="handleClose" width="500">
<el-select v-model="flowCode" placeholder="Select" style="width: 240px">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="submitFlow()"> 确认 </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Leave" lang="ts">
import { addLeave, getLeave, updateLeave } from '@/api/workflow/leave';
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
import { startWorkFlow } from '@/api/workflow/task';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { AxiosResponse } from 'axios';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const buttonLoading = ref(false);
const loading = ref(true);
const leaveTime = ref<Array<string>>([]);
//路由参数
const routeParams = ref<Record<string, any>>({});
const options = [
{
value: '1',
label: '事假'
},
{
value: '2',
label: '调休'
},
{
value: '3',
label: '病假'
},
{
value: '4',
label: '婚假'
}
];
const flowCodeOptions = [
{
value: 'hzdyjs-qj',
label: '学生请假流程-杭州第一技师'
},
{
value: 'leave1',
label: '请假申请-普通'
},
{
value: 'leave2',
label: '请假申请-排他网关'
},
{
value: 'leave3',
label: '请假申请-并行网关'
},
{
value: 'leave4',
label: '请假申请-会签'
},
{
value: 'leave5',
label: '请假申请-并行会签网关'
},
{
value: 'leave6',
label: '请假申请-排他并行会签'
}
];
const flowCode = ref<string>('');
const dialogVisible = reactive<DialogOption>({
visible: false,
title: '流程定义'
});
//提交组件
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
//审批记录组件
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
const leaveFormRef = ref<ElFormInstance>();
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: '',
variables: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData: LeaveForm = {
id: undefined,
leaveType: undefined,
startDate: undefined,
endDate: undefined,
leaveDays: undefined,
remark: undefined,
status: undefined
};
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
startLeaveDays: undefined,
endLeaveDays: undefined
},
rules: {
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
leaveType: [{ required: true, message: '请假类型不能为空', trigger: 'blur' }],
leaveTime: [{ required: true, message: '请假时间不能为空', trigger: 'blur' }],
leaveDays: [{ required: true, message: '请假天数不能为空', trigger: 'blur' }]
}
});
const handleClose = () => {
dialogVisible.visible = false;
flowCode.value = '';
buttonLoading.value = false;
};
const { form, rules } = toRefs(data);
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
leaveTime.value = [];
leaveFormRef.value?.resetFields();
};
const changeLeaveTime = () => {
const startDate = new Date(leaveTime.value[0]).getTime();
const endDate = new Date(leaveTime.value[1]).getTime();
const diffInMilliseconds = endDate - startDate;
form.value.leaveDays = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24)) + 1;
};
/** 获取详情 */
const getInfo = () => {
loading.value = true;
buttonLoading.value = false;
nextTick(async () => {
const res = await getLeave(routeParams.value.id);
Object.assign(form.value, res.data);
console.log('res.data:', res.data);
console.log('form.value:', form.value);
// 为了确保 Element Plus 日期区间组件能够正确渲染,需要直接替换绑定数组引用
// 同时将后端返回的日期统一格式化为组件要求的 "YYYY-MM-DD HH:mm:ss"
// 后端仅返回 "YYYY-MM-DD",需要为日期补齐时间,符合 value-format="YYYY-MM-DD HH:mm:ss"
const ensureDateTime = (dateStr: string | undefined, endFlag = false) => {
if (!dateStr) return '';
// 如果已包含时间段,直接返回
if (/\d{2}:\d{2}:\d{2}$/.test(dateStr)) {
return dateStr;
}
return endFlag ? `${dateStr} 23:59:59` : `${dateStr} 00:00:00`;
};
const start = ensureDateTime(form.value.startDate, false);
const end = ensureDateTime(form.value.endDate, true);
leaveTime.value = [start, end];
loading.value = false;
buttonLoading.value = false;
});
};
/** 提交按钮 */
const submitForm = (status: string) => {
if (leaveTime.value.length === 0) {
proxy?.$modal.msgError('请假时间不能为空');
return;
}
try {
leaveFormRef.value?.validate(async (valid: boolean) => {
form.value.startDate = leaveTime.value[0];
form.value.endDate = leaveTime.value[1];
if (valid) {
buttonLoading.value = true;
let res: AxiosResponse<LeaveVO>;
if (form.value.id) {
res = await updateLeave(form.value).finally(() => (buttonLoading.value = false));
} else {
res = await addLeave(form.value).finally(() => (buttonLoading.value = false));
}
form.value = res.data;
if (status === 'draft') {
buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
} else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
flowCode.value = flowCodeOptions[0].value;
dialogVisible.visible = true;
return;
}
//说明启动过先随意穿个参数
if (flowCode.value === '' || flowCode.value === null) {
flowCode.value = 'xx';
}
await handleStartWorkFlow(res.data);
}
}
});
} finally {
buttonLoading.value = false;
}
};
const submitFlow = async () => {
handleStartWorkFlow(form.value);
dialogVisible.visible = false;
};
//提交申请
const handleStartWorkFlow = async (data: LeaveForm) => {
try {
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
//流程变量
taskVariables.value = {
// hzdyjs-qj 使用的流程变量
qjts: data.leaveDays,
// leave2/6 使用的流程变量
leaveDays: data.leaveDays,
// leave4/5 使用的流程变量
userList: ['1', '3', '4']
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);
if (submitVerifyRef.value) {
buttonLoading.value = false;
submitVerifyRef.value.openDialog(resp.data.taskId);
}
} finally {
buttonLoading.value = false;
}
};
//审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
//提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
//审批
const approvalVerifyOpen = async () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
reset();
loading.value = false;
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
getInfo();
}
});
});
</script>
+517
View File
@@ -0,0 +1,517 @@
<template>
<div class="p-2 page-container">
<!-- 搜索表单 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="企业名称" prop="enterpriseName">
<el-input v-model="queryParams.enterpriseName" placeholder="请输入企业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属行业" prop="industry">
<el-select v-model="queryParams.industry" placeholder="请选择所属行业" clearable style="width: 100%">
<el-option v-for="dict in zsjy_enterprise_industry" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="企业规模" prop="enterpriseScale">
<el-select v-model="queryParams.enterpriseScale" placeholder="请选择企业规模" clearable style="width: 100%">
<el-option v-for="dict in zsjy_enterprise_scale" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="企业状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择企业状态" clearable style="width: 100%">
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 操作按钮和表格 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['zsjy:enterprise:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['zsjy:enterprise:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['zsjy:enterprise:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['zsjy:enterprise:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 数据表格 -->
<el-table v-loading="loading" border :data="enterpriseList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="企业名称" align="center" prop="enterpriseName" min-width="150" show-overflow-tooltip />
<el-table-column label="所属行业" align="center" prop="industry" width="120">
<template #default="scope">
<dict-tag :options="zsjy_enterprise_industry" :value="scope.row.industry" />
</template>
</el-table-column>
<el-table-column label="企业规模" align="center" prop="enterpriseScale" width="120">
<template #default="scope">
<dict-tag :options="zsjy_enterprise_scale" :value="scope.row.enterpriseScale" />
</template>
</el-table-column>
<el-table-column label="联系人" align="center" prop="contactName" width="100" show-overflow-tooltip />
<el-table-column label="联系电话" align="center" prop="contactPhone" width="120" show-overflow-tooltip />
<el-table-column label="企业邮箱" align="center" prop="email" min-width="150" show-overflow-tooltip />
<el-table-column label="企业地址" align="center" prop="address" min-width="200" show-overflow-tooltip />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" show-overflow-tooltip />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
size="default"
style="font-size: 16px; margin-right: 8px; color: blue"
@click="handleView(scope.row)"
v-hasPermi="['zsjy:enterprise:query']"
></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
icon="Edit"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleUpdate(scope.row)"
v-hasPermi="['zsjy:enterprise:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
type="danger"
icon="Delete"
size="default"
style="font-size: 16px"
@click="handleDelete(scope.row)"
v-hasPermi="['zsjy:enterprise:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 添加或修改企业信息对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="1000px" append-to-body>
<el-form ref="enterpriseFormRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="企业名称" prop="enterpriseName">
<el-input v-model="form.enterpriseName" placeholder="请输入企业名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属行业" prop="industry">
<el-select v-model="form.industry" placeholder="请选择所属行业" style="width: 100%">
<el-option v-for="dict in zsjy_enterprise_industry" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="企业规模" prop="enterpriseScale">
<el-select v-model="form.enterpriseScale" placeholder="请选择企业规模" style="width: 100%">
<el-option v-for="dict in zsjy_enterprise_scale" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人姓名" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入联系人姓名" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="企业邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入企业邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="企业地址" prop="address">
<el-input v-model="form.address" placeholder="请输入企业地址" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="主营业务" prop="mainBusiness">
<el-input
v-model="form.mainBusiness"
type="textarea"
placeholder="请输入主营业务"
:autosize="{ minRows: 3, maxRows: 5 }"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注信息"
:autosize="{ minRows: 2, maxRows: 4 }"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看企业信息对话框 -->
<el-dialog title="企业信息详情" v-model="viewDialog.visible" width="1000px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="企业名称">{{ viewForm.enterpriseName }}</el-descriptions-item>
<el-descriptions-item label="所属行业">
<dict-tag :options="zsjy_enterprise_industry" :value="viewForm.industry" />
</el-descriptions-item>
<el-descriptions-item label="企业规模">
<dict-tag :options="zsjy_enterprise_scale" :value="viewForm.enterpriseScale" />
</el-descriptions-item>
<el-descriptions-item label="联系人姓名">{{ viewForm.contactName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewForm.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="企业邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="企业地址" span="2">{{ viewForm.address }}</el-descriptions-item>
<el-descriptions-item label="主营业务" span="2">{{ viewForm.mainBusiness }}</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :options="sys_normal_disable" :value="viewForm.status" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ viewForm.createTime }}</el-descriptions-item>
<el-descriptions-item label="备注" span="2">{{ viewForm.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="viewDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Enterprise" lang="ts">
import { listEnterprise, getEnterprise, delEnterprise, addEnterprise, updateEnterprise } from '@/api/zsjy/enterprise'
import { EnterpriseVO, EnterpriseForm, EnterpriseQuery } from '@/api/zsjy/types'
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const enterpriseList = ref<EnterpriseVO[]>([])
const buttonLoading = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref<Array<string | number>>([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const queryFormRef = ref<ElFormInstance>()
const enterpriseFormRef = ref<ElFormInstance>()
// 字典数据
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'))
const { zsjy_enterprise_industry } = toRefs<any>(proxy?.useDict('zsjy_enterprise_industry'))
const { zsjy_enterprise_scale } = toRefs<any>(proxy?.useDict('zsjy_enterprise_scale'))
const initFormData: EnterpriseForm = {
id: undefined,
enterpriseName: '',
industry: '',
mainBusiness: '',
enterpriseScale: '',
contactName: '',
contactPhone: '',
email: '',
address: '',
status: '0',
remark: ''
}
const data = reactive<PageData<EnterpriseForm, EnterpriseQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
enterpriseName: '',
industry: '',
enterpriseScale: '',
status: ''
},
rules: {
enterpriseName: [{ required: true, message: '企业名称不能为空', trigger: 'blur' }],
industry: [{ required: true, message: '所属行业不能为空', trigger: 'change' }],
mainBusiness: [{ required: true, message: '主营业务不能为空', trigger: 'blur' }],
enterpriseScale: [{ required: true, message: '企业规模不能为空', trigger: 'change' }],
contactName: [{ required: true, message: '联系人姓名不能为空', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '联系电话不能为空', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
{ required: true, message: '企业邮箱不能为空', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
address: [{ required: true, message: '企业地址不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
})
const { queryParams, form, rules } = toRefs(data)
// 对话框相关
const dialog = reactive<DialogOption>({
visible: false,
title: ''
})
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
})
const viewForm = ref<EnterpriseVO>({} as EnterpriseVO)
/** 查询企业信息列表 */
const getList = async () => {
loading.value = true
const res = await listEnterprise(queryParams.value)
enterpriseList.value = res.rows
total.value = res.total
loading.value = false
}
/** 取消按钮 */
const cancel = () => {
reset()
dialog.visible = false
}
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData }
enterpriseFormRef.value?.resetFields()
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: EnterpriseVO[]) => {
ids.value = selection.map(item => item.id)
single.value = selection.length !== 1
multiple.value = !selection.length
}
/** 新增按钮操作 */
const handleAdd = () => {
reset()
dialog.visible = true
dialog.title = '添加企业信息'
}
/** 修改按钮操作 */
const handleUpdate = async (row?: EnterpriseVO) => {
reset()
const id = row?.id || ids.value[0]
const res = await getEnterprise(id)
Object.assign(form.value, res.data)
dialog.visible = true
dialog.title = '修改企业信息'
}
/** 查看详情 */
const handleView = async (row: EnterpriseVO) => {
const res = await getEnterprise(row.id)
viewForm.value = res.data
viewDialog.visible = true
}
/** 提交按钮 */
const submitForm = () => {
enterpriseFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true
if (form.value.id) {
await updateEnterprise(form.value).finally(() => (buttonLoading.value = false))
} else {
await addEnterprise(form.value).finally(() => (buttonLoading.value = false))
}
proxy?.$modal.msgSuccess('操作成功')
dialog.visible = false
await getList()
}
})
}
/** 删除按钮操作 */
const handleDelete = async (row?: EnterpriseVO) => {
const idList = row?.id || ids.value
await proxy?.$modal.confirm('是否确认删除企业信息编号为"' + idList + '"的数据项?')
await delEnterprise(idList)
proxy?.$modal.msgSuccess('删除成功')
await getList()
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'zsjy/enterprise/export',
{
...queryParams.value
},
`enterprise_${new Date().getTime()}.xlsx`
)
}
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
// 继承后勤管理的样式
.page-container {
.search-btn-container {
text-align: right;
}
.button-card {
margin-bottom: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.table-card {
margin-bottom: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.pagination-container {
text-align: right;
padding-top: 20px;
}
.dialog-footer {
text-align: right;
}
// 表格适应性样式
:deep(.el-table) {
.el-table__cell {
padding: 8px 0;
}
}
// 响应式表格
:deep(.el-table__body-wrapper) {
overflow-x: auto;
}
// 表格列宽自适应
.el-table {
width: 100%;
table-layout: auto;
}
}
// 描述列表样式优化
:deep(.el-descriptions) {
.el-descriptions__label {
font-weight: 600;
color: #606266;
width: 120px;
}
.el-descriptions__content {
color: #303133;
}
}
</style>
Loaded 100 of 662 files, more files were not shown because too many files have changed in this diff. Show more