2026-02-12 21:38:29 +08:00

370 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="step-units">
<el-form
ref="formRef"
:model="localFormData"
:rules="rules"
label-position="right"
>
<!-- 参与单位列表 -->
<div class="section-header">
<h3>参与单位列表</h3>
<p class="description">添加本项目涉及的参与单位信息</p>
</div>
<el-table
:data="localFormData.participatingUnits"
style="width: 100%"
border
stripe
class="units-table"
>
<el-table-column prop="index" label="序号" width="80" align="center">
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column label="单位Logo" width="120" align="center">
<template #default="{ row, $index }">
<el-upload
class="logo-uploader"
:show-file-list="false"
:before-upload="(file) => handleLogoUpload(file, $index)"
accept="image/*"
>
<div v-if="row.logoUrl" class="logo-preview">
<img :src="row.logoUrl" class="logo-image" />
<div class="logo-overlay">
<el-icon><Camera /></el-icon>
<span>更换</span>
</div>
</div>
<div v-else class="logo-placeholder">
<el-icon><Plus /></el-icon>
<span>上传Logo</span>
</div>
</el-upload>
</template>
</el-table-column>
<el-table-column label="单位名称" min-width="200" align="center">
<template #default="{ row, $index }">
<el-form-item
:prop="`participatingUnits.${$index}.unitName`"
:rules="unitNameRules"
style="margin-bottom: 0"
>
<el-input
v-model="row.unitName"
placeholder="请输入单位名称"
maxlength="100"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="单位角色" min-width="150" align="center">
<template #default="{ row, $index }">
<el-form-item
:prop="`participatingUnits.${$index}.role`"
:rules="roleRules"
style="margin-bottom: 0"
>
<el-select v-model="row.role" placeholder="请选择角色">
<el-option label="主办单位" value="host" />
<el-option label="协办单位" value="coorganizer" />
<el-option label="支持单位" value="supporter" />
<el-option label="合作单位" value="partner" />
<el-option label="赞助单位" value="sponsor" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ $index }">
<div class="action-buttons">
<el-button
type="danger"
size="small"
@click="handleDeleteUnit($index)"
:disabled="localFormData.participatingUnits.length <= 1"
>
<el-icon><Delete /></el-icon>
</el-button>
<el-tooltip content="删除该单位" placement="top">
<span :class="{ 'disabled-tip': localFormData.participatingUnits.length > 1 }"></span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<!-- 添加单位按钮 -->
<div class="add-unit-action">
<el-button type="primary" @click="handleAddUnit" plain>
<el-icon><Plus /></el-icon>
添加单位
</el-button>
</div>
<!-- 验证提示 -->
<el-alert
v-if="localFormData.participatingUnits.length === 0"
title="提示"
type="warning"
:closable="false"
class="validation-alert"
>
请至少添加一个参与单位
</el-alert>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Delete, Plus, Camera } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 定义 props
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
// 定义 emits
const emit = defineEmits(['update:modelValue'])
// 表单引用
const formRef = ref(null)
// 本地表单数据副本
const localFormData = reactive({
participatingUnits: []
})
// 初始化本地数据
// localFormData.participatingUnits = JSON.parse(JSON.stringify(props.modelValue.participatingUnits || []))
// 监听本地数据变化,同步到父组件
watch(localFormData, (newVal) => {
emit('update:modelValue', newVal)
}, { deep: true })
// 监听父组件数据变化,同步到本地
watch(() => props.modelValue, (newVal) => {
if (newVal && newVal.participatingUnits) {
localFormData.participatingUnits = JSON.parse(JSON.stringify(newVal.participatingUnits))
}
}, { deep: true })
// 单位名称验证规则
const unitNameRules = [
{ required: true, message: '请输入单位名称', trigger: 'blur' },
{ min: 2, max: 100, message: '单位名称长度为2-100个字符', trigger: 'blur' }
]
// 角色验证规则
const roleRules = [
{ required: true, message: '请选择单位角色', trigger: 'change' }
]
// 表单验证规则
const rules = reactive({
participatingUnits: [
{
type: 'array',
required: true,
message: '请至少添加一个参与单位',
trigger: 'change',
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少添加一个参与单位'))
} else {
// 检查是否所有行都填写完整
const incomplete = value.some(unit => !unit.unitName || !unit.role)
if (incomplete) {
callback(new Error('请完善所有单位信息'))
} else {
callback()
}
}
}
}
]
})
// 处理 Logo 上传
const handleLogoUpload = (file, index) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
// 使用 FileReader 读取文件并转换为 base64
const reader = new FileReader()
reader.onload = (e) => {
const base64Url = e.target.result
localFormData.participatingUnits[index].logoUrl = base64Url
ElMessage.success('Logo 上传成功!')
}
reader.readAsDataURL(file)
return false // 阻止默认上传行为
}
// 添加单位
const handleAddUnit = () => {
localFormData.participatingUnits.push({
unitName: '',
role: '',
logoUrl: ''
})
}
// 删除单位
const handleDeleteUnit = (index) => {
localFormData.participatingUnits.splice(index, 1)
}
// 暴露验证方法给父组件
const validate = async () => {
if (!formRef.value) return false
try {
await formRef.value.validate()
return true
} catch (error) {
return false
}
}
defineExpose({
validate
})
</script>
<style scoped>
.step-units {
padding: 0 20px;
}
.section-header {
margin-bottom: 20px;
}
.section-header h3 {
font-size: 18px;
color: #303133;
margin-bottom: 8px;
}
.section-header .description {
font-size: 14px;
color: #909399;
}
.units-table {
margin-bottom: 20px;
}
/* Logo 上传样式 */
.logo-uploader {
display: inline-block;
}
.logo-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 8px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.logo-uploader .el-upload:hover {
border-color: #409eff;
}
.logo-placeholder {
width: 80px;
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #8c939d;
font-size: 12px;
}
.logo-placeholder .el-icon {
font-size: 24px;
margin-bottom: 4px;
}
.logo-preview {
width: 80px;
height: 80px;
position: relative;
}
.logo-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.logo-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
font-size: 12px;
border-radius: 8px;
}
.logo-preview:hover .logo-overlay {
display: flex;
}
.logo-overlay .el-icon {
font-size: 20px;
margin-bottom: 4px;
}
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
.add-unit-action {
display: flex;
justify-content: center;
margin-top: 20px;
}
.validation-alert {
margin-top: 20px;
}
</style>