597 lines
20 KiB
Vue
597 lines
20 KiB
Vue
<template>
|
||
<div>
|
||
<el-form ref="form" :model="formData" :rules="rules" label-width="120px">
|
||
<el-form-item label="单位名称" prop="companyName">
|
||
<el-input v-model="formData.companyName" placeholder="请输入单位名称" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="联系人" prop="contactPerson">
|
||
<el-input v-model="formData.contactPerson" placeholder="请输入联系人" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="联系电话" prop="contactPhone">
|
||
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="单位地址" prop="address">
|
||
<el-input v-model="formData.address" placeholder="请输入单位地址" />
|
||
</el-form-item>
|
||
<!--
|
||
<el-form-item label="状态" prop="status">
|
||
<el-switch v-model="formData.status" active-text="启用" inactive-text="禁用" />
|
||
</el-form-item> -->
|
||
|
||
<el-form-item label="文件上传" prop="file">
|
||
<el-upload class="upload-demo" :action="uploadFileUrl" :multiple="true" :before-upload="beforeUpload"
|
||
:file-list="uploadFileList" :on-success="handleUploadSuccess" :on-error="handleUploadError"
|
||
:on-remove="handleRemove" :headers="headers" name="file" accept=".pdf,.doc,.docx,.txt,.jpg,.jpeg,.png"
|
||
:on-preview="previewFile">
|
||
<el-button size="small" type="primary">点击上传</el-button>
|
||
<div slot="tip" class="el-upload__tip">支持多文件上传,文件格式:pdf、doc、docx、txt、jpg、jpeg、png,大小不超过20MB</div>
|
||
</el-upload>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="备注" prop="remark">
|
||
<el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 文件预览对话框 -->
|
||
<el-dialog :title="'文件预览(' + selectFile.fileName + ')'" :visible.sync="isShowFile" width="70%" append-to- >
|
||
<!-- 全屏按钮 -->
|
||
<el-button
|
||
class="fullscreen-btn"
|
||
icon="el-icon-full-screen"
|
||
size="mini"
|
||
@click="toggleFullScreen"
|
||
style="position: absolute; top: 10px; right: 100px; z-index: 10;"
|
||
>
|
||
全屏
|
||
</el-button>
|
||
<div class="preview-container" ref="previewContainer">
|
||
<!-- 音频预览 -->
|
||
<audio v-if="isAudioType" class="preview-iframe" controls>
|
||
<source :src="getPreviewFilePath(selectFile)"/>
|
||
</audio>
|
||
<!-- 图片预览 -->
|
||
<img
|
||
v-if="isImageType"
|
||
:src="getPreviewFilePath(selectFile)"
|
||
class="preview-iframe"
|
||
style="max-width: 100%; max-height: 80vh; object-fit: contain;"
|
||
>
|
||
<!-- Office文档预览 -->
|
||
<iframe
|
||
v-if="!isAudioType && !isImageType && !isPdfType && !isTxtType"
|
||
:src="fileUrl"
|
||
frameborder="0"
|
||
class="preview-iframe"
|
||
>
|
||
</iframe>
|
||
<!-- PDF和TXT预览 -->
|
||
<iframe
|
||
v-if="isPdfType || isTxtType"
|
||
:src="getPreviewFilePath(selectFile)"
|
||
frameborder="0"
|
||
class="preview-iframe"
|
||
>
|
||
</iframe>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getAccessToken } from "@/utils/auth";
|
||
import ImagePreview from '@/components/ImagePreview';
|
||
|
||
export default {
|
||
name: 'ChargeCompanyForm',
|
||
props: {
|
||
// 用于 v-model 的 value(对象)
|
||
value: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// systemCode 允许字符串或数字
|
||
systemCode: {
|
||
type: [String, Number],
|
||
default: ''
|
||
}
|
||
},
|
||
components: {
|
||
ImagePreview
|
||
},
|
||
data() {
|
||
return {
|
||
// 本地表单数据 — 保持对象引用(方便 el-form resetFields)
|
||
formData: {
|
||
id: null,
|
||
companyName: null,
|
||
contactPerson: null,
|
||
contactPhone: null,
|
||
address: null,
|
||
status: false,
|
||
systemCode: '', // 会在 created 中初始化
|
||
remark: null,
|
||
file: ''
|
||
},
|
||
// 上传文件列表
|
||
uploadFileList: [],
|
||
// 文件预览地址
|
||
viewFileUrl: process.env.VUE_APP_FILE_API,
|
||
// 文件上传地址
|
||
uploadFileUrl: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload",
|
||
// 上传请求头
|
||
headers: { Authorization: "Bearer " + getAccessToken() },
|
||
// 正在上传的文件数量
|
||
uploadingCount: 0,
|
||
// 文件预览相关
|
||
isShowFile: false,
|
||
selectFile: {
|
||
fileName: '',
|
||
filePath: '',
|
||
isImage: false
|
||
},
|
||
fileUrl: '',
|
||
|
||
// 防止同步回流的标志
|
||
isSyncingFromParent: false,
|
||
rules: {
|
||
companyName: [
|
||
{ required: true, message: '单位名称不能为空', trigger: 'blur' }
|
||
],
|
||
contactPerson: [
|
||
{ required: true, message: '联系人不能为空', trigger: 'blur' }
|
||
],
|
||
contactPhone: [
|
||
{ required: true, message: '联系电话不能为空', trigger: 'blur' },
|
||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||
]
|
||
}
|
||
}
|
||
},
|
||
watch: {
|
||
// 当外部传入 systemCode 变化时,更新本地 field(外部 systemCode 优先)
|
||
systemCode: {
|
||
handler(newVal) {
|
||
// 使用 $set 保证响应式(Vue2)
|
||
this.$set(this.formData, 'systemCode', newVal === undefined || newVal === null ? '' : newVal)
|
||
},
|
||
immediate: true
|
||
},
|
||
|
||
// 监听 value(父组件通过 v-model 传入的数据)变化并同步到本地表单
|
||
value: {
|
||
handler(newVal) {
|
||
// 标志:正在同步来自父组件的数据,避免触发 emit 回父组件
|
||
this.isSyncingFromParent = true
|
||
|
||
if (newVal && Object.keys(newVal).length > 0) {
|
||
// 保持 systemCode 优先为当前本地 systemCode(外部 systemCode Prop 会单独处理)
|
||
const keepSystemCode = this.formData.systemCode
|
||
|
||
// 使用 Object.assign 保持 formData 对象引用,避免意外破坏 el-form 的绑定
|
||
Object.assign(this.formData, { ...newVal })
|
||
|
||
// 恢复/保持本地 systemCode(如果你需要让 value 中的 systemCode 覆盖,改这里)
|
||
this.$set(this.formData, 'systemCode', keepSystemCode)
|
||
|
||
// 处理文件列表
|
||
if (this.formData.file) {
|
||
const fileNames = this.formData.file.split(',')
|
||
this.uploadFileList = fileNames.map((path, index) => {
|
||
// 提取文件名
|
||
const fileName = this.extractFileName(path);
|
||
// 为每个文件生成唯一的uid
|
||
const uid = `local_${index}_${Date.now()}`
|
||
// 如果路径已经包含viewFileUrl,则直接使用
|
||
const url = path.includes(this.viewFileUrl) ? path : (this.viewFileUrl + path);
|
||
return {
|
||
uid,
|
||
name: fileName,
|
||
fileName: fileName,
|
||
url: url,
|
||
filePath: path, // 保存原始路径用于预览
|
||
status: 'success',
|
||
isImage: this.isImageExtension(fileName)
|
||
}
|
||
})
|
||
} else {
|
||
this.uploadFileList = []
|
||
}
|
||
} else {
|
||
// 如果 newVal 为空对象或 null,则只清空可编辑字段(保持 systemCode)
|
||
const keepSystemCode = this.formData.systemCode
|
||
Object.assign(this.formData, {
|
||
id: null,
|
||
companyName: null,
|
||
contactPerson: null,
|
||
contactPhone: null,
|
||
address: null,
|
||
status: false,
|
||
remark: null,
|
||
file: ''
|
||
})
|
||
this.$set(this.formData, 'systemCode', keepSystemCode)
|
||
this.uploadFileList = []
|
||
}
|
||
|
||
// 等下一个 tick 再允许 emit(保证同步完成后不回流)
|
||
this.$nextTick(() => {
|
||
this.isSyncingFromParent = false
|
||
})
|
||
},
|
||
immediate: true,
|
||
deep: true
|
||
},
|
||
// 本地表单变化时向外 emit(用于 v-model 双向绑定)
|
||
// 但如果是我们从父组件同步过来的改动,则不 emit(避免回流)
|
||
formData: {
|
||
handler(newVal) {
|
||
if (this.isSyncingFromParent) {
|
||
// 来自父组件的同步,不应再次 emit
|
||
return
|
||
}
|
||
// 发出副本,防止父组件直接修改传入对象引用
|
||
this.$emit('input', { ...newVal })
|
||
},
|
||
deep: true
|
||
}
|
||
},
|
||
computed: {
|
||
// 判断是否为图片类型
|
||
isImageType() {
|
||
if (!this.selectFile.fileName) return false;
|
||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp'];
|
||
const extension = this.selectFile.fileName.substring(this.selectFile.fileName.lastIndexOf('.')).toLowerCase();
|
||
return imageExtensions.includes(extension);
|
||
},
|
||
// 判断是否为音频类型
|
||
isAudioType() {
|
||
if (!this.selectFile.fileName) return false;
|
||
const audioExtensions = ['.mp3', '.wav', '.ogg', '.flac', '.aac'];
|
||
const extension = this.selectFile.fileName.substring(this.selectFile.fileName.lastIndexOf('.')).toLowerCase();
|
||
return audioExtensions.includes(extension);
|
||
},
|
||
// 判断是否为PDF类型
|
||
isPdfType() {
|
||
if (!this.selectFile.fileName) return false;
|
||
return this.selectFile.fileName.toLowerCase().endsWith('.pdf');
|
||
},
|
||
// 判断是否为TXT类型
|
||
isTxtType() {
|
||
if (!this.selectFile.fileName) return false;
|
||
return this.selectFile.fileName.toLowerCase().endsWith('.txt');
|
||
}
|
||
},
|
||
methods: {
|
||
// 验证表单:callback 接收 (valid, fields)
|
||
validate(callback) {
|
||
if (!this.$refs.form) {
|
||
const error = new Error('form ref not found')
|
||
if (typeof callback === 'function') callback(false, error)
|
||
return Promise.reject(error)
|
||
}
|
||
// Element UI validate 支持回调或 Promise
|
||
return new Promise((resolve, reject) => {
|
||
this.$refs.form.validate((valid, fields) => {
|
||
if (typeof callback === 'function') callback(valid, fields)
|
||
if (valid) resolve(true)
|
||
else reject(fields)
|
||
})
|
||
})
|
||
},
|
||
|
||
// 重置表单验证与字段(保留 systemCode)
|
||
resetFields() {
|
||
if (!this.$refs.form) return
|
||
// resetFields 会根据 model 将字段恢复为 model 中的初始值
|
||
this.$refs.form.resetFields()
|
||
// 重新确保 systemCode 设置为 props 的值(或空字符串)
|
||
this.$set(this.formData, 'systemCode', this.systemCode || '')
|
||
},
|
||
|
||
// 清除校验提示
|
||
clearValidate() {
|
||
if (!this.$refs.form) return
|
||
this.$refs.form.clearValidate()
|
||
},
|
||
|
||
// 彻底重置 formData(保持 systemCode)
|
||
resetFormData() {
|
||
const keepSystemCode = this.systemCode || this.formData.systemCode || ''
|
||
Object.assign(this.formData, {
|
||
id: null,
|
||
companyName: null,
|
||
contactPerson: null,
|
||
contactPhone: null,
|
||
address: null,
|
||
status: false,
|
||
remark: null,
|
||
file: ''
|
||
})
|
||
this.$set(this.formData, 'systemCode', keepSystemCode)
|
||
this.uploadFileList = []
|
||
|
||
this.$nextTick(() => {
|
||
this.clearValidate()
|
||
// 主动 emit 一次空的表单状态(如果需要通知父组件)
|
||
this.$emit('input', { ...this.formData })
|
||
})
|
||
},
|
||
|
||
// 文件上传前的钩子函数
|
||
beforeUpload(file) {
|
||
// 文件大小限制
|
||
const isLt20M = file.size / 1024 / 1024 < 20 // 限制20MB
|
||
if (!isLt20M) {
|
||
this.$message.error('上传文件大小不能超过 20MB!')
|
||
return false
|
||
}
|
||
|
||
// 显示上传中加载状态
|
||
if (this.uploadingCount === 0) {
|
||
this.$modal.loading("正在上传文件,请稍候...")
|
||
}
|
||
this.uploadingCount++
|
||
return true
|
||
},
|
||
|
||
// 文件上传成功处理
|
||
handleUploadSuccess(res, file, fileList) {
|
||
this.uploadingCount--
|
||
|
||
// 检查响应数据格式
|
||
console.log('上传成功响应:', res);
|
||
|
||
// 使用原始文件名显示
|
||
const originalFileName = file.name || '未知文件名';
|
||
|
||
// 查找当前上传的文件在列表中的索引
|
||
const index = this.uploadFileList.findIndex(item => item.uid === file.uid)
|
||
if (index > -1) {
|
||
// 使用服务器返回的文件路径
|
||
const filePath = res && res.code === 0 && res.data ? res.data : file.name
|
||
|
||
// 更新文件列表
|
||
this.uploadFileList[index].url = filePath
|
||
this.uploadFileList[index].status = 'success'
|
||
this.uploadFileList[index].name = originalFileName // 使用原始文件名显示
|
||
this.uploadFileList[index].fileName = originalFileName // 添加fileName属性用于显示
|
||
this.uploadFileList[index].filePath = filePath // 添加filePath属性用于预览
|
||
this.uploadFileList[index].isImage = this.isImageExtension(originalFileName) // 判断是否为图片类型
|
||
} else {
|
||
// 如果在列表中找不到,添加到列表
|
||
const filePath = res && res.code === 0 && res.data ? res.data : file.name
|
||
this.uploadFileList.push({
|
||
uid: file.uid,
|
||
name: originalFileName,
|
||
fileName: originalFileName,
|
||
url: filePath,
|
||
filePath: filePath,
|
||
status: 'success',
|
||
isImage: this.isImageExtension(originalFileName)
|
||
})
|
||
}
|
||
|
||
// 每次文件上传成功后立即更新file字段
|
||
this.updateFilePaths()
|
||
|
||
// 所有文件上传完成后关闭加载
|
||
if (this.uploadingCount === 0) {
|
||
this.$modal.closeLoading()
|
||
}
|
||
},
|
||
|
||
// 文件上传失败处理
|
||
handleUploadError(err, file, fileList) {
|
||
this.uploadingCount--
|
||
this.$message.error('文件上传失败,请重试')
|
||
|
||
if (this.uploadingCount === 0) {
|
||
this.$modal.closeLoading()
|
||
}
|
||
},
|
||
|
||
// 文件移除时的处理函数
|
||
handleRemove(file, fileList) {
|
||
this.uploadFileList = fileList
|
||
this.updateFilePaths()
|
||
},
|
||
|
||
// 更新文件路径字符串(以逗号分隔)
|
||
updateFilePaths() {
|
||
// 使用上传成功后返回的文件路径
|
||
const paths = this.uploadFileList
|
||
.filter(file => file && (file.status === 'success' || file.url)) // 只处理已成功上传的文件
|
||
.map(file => {
|
||
// 确保使用正确的文件路径,优先使用url
|
||
const filePath = file.url || file.name || ''
|
||
|
||
// 如果路径包含viewFileUrl,则去掉前缀
|
||
if (filePath && this.viewFileUrl && filePath.includes(this.viewFileUrl)) {
|
||
return filePath.replace(this.viewFileUrl, '')
|
||
}
|
||
return filePath
|
||
})
|
||
|
||
// 设置文件路径,确保总是一个字符串(即使为空)
|
||
const filePathStr = paths.join(',') || ''
|
||
this.$set(this.formData, 'file', filePathStr)
|
||
|
||
console.log('更新后的file字段值:', filePathStr)
|
||
|
||
// 主动触发formData的watch,确保父组件能接收到最新的file值
|
||
this.$emit('input', { ...this.formData })
|
||
},
|
||
|
||
// 外部主动传入数据设置到表单(不会改变 systemCode)
|
||
setFormData(data = {}) {
|
||
if (!data) return
|
||
const keepSystemCode = this.formData.systemCode
|
||
// 阻止在同步过程中 emit
|
||
this.isSyncingFromParent = true
|
||
Object.assign(this.formData, { ...data })
|
||
this.$set(this.formData, 'systemCode', keepSystemCode)
|
||
|
||
// 处理文件列表
|
||
if (this.formData.file) {
|
||
const fileNames = this.formData.file.split(',')
|
||
this.uploadFileList = fileNames.map((path, index) => {
|
||
// 提取文件名
|
||
const fileName = this.extractFileName(path);
|
||
// 为每个文件生成唯一的uid
|
||
const uid = `local_${index}_${Date.now()}`
|
||
// 如果路径已经包含viewFileUrl,则直接使用
|
||
const url = path.includes(this.viewFileUrl) ? path : (this.viewFileUrl + path);
|
||
return {
|
||
uid,
|
||
name: fileName,
|
||
fileName: fileName,
|
||
url: url,
|
||
filePath: path, // 保存原始路径用于预览
|
||
status: 'success',
|
||
isImage: this.isImageExtension(fileName)
|
||
}
|
||
})
|
||
} else {
|
||
this.uploadFileList = []
|
||
}
|
||
|
||
this.$nextTick(() => {
|
||
this.isSyncingFromParent = false
|
||
})
|
||
},
|
||
// 判断文件是否为图片格式
|
||
isImageExtension(fileName) {
|
||
if (!fileName) return false;
|
||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp'];
|
||
const extension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
|
||
return imageExtensions.includes(extension);
|
||
},
|
||
// 获取预览文件路径
|
||
getPreviewFilePath(file) {
|
||
if (!file || !file.filePath && !file.url) return '';
|
||
|
||
const filePath = file.filePath || file.url || '';
|
||
// 如果是完整的http链接,直接返回
|
||
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
||
return filePath;
|
||
}
|
||
// 否则拼接完整的图片预览URL
|
||
return `${this.viewFileUrl || process.env.VUE_APP_BASE_API}${filePath}`;
|
||
},
|
||
// 预览文件
|
||
previewFile(file) {
|
||
console.log('预览文件:', file);
|
||
// 设置选中的文件信息
|
||
this.selectFile = {
|
||
fileName: file.name || file.fileName || '未知文件',
|
||
filePath: file.url || file.filePath || '',
|
||
isImage: file.isImage || this.isImageExtension(file.name || file.fileName || '')
|
||
};
|
||
|
||
// 对于Office文档,使用Office Online预览
|
||
if (!this.isImageExtension(this.selectFile.fileName) && !this.isPdfType && !this.isTxtType && !this.isAudioType) {
|
||
const fileUrl = this.getPreviewFilePath(this.selectFile);
|
||
this.fileUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fileUrl)}`;
|
||
}
|
||
|
||
// 显示预览对话框
|
||
this.isShowFile = true;
|
||
},
|
||
// 全屏切换
|
||
toggleFullScreen() {
|
||
const container = this.$refs.previewContainer;
|
||
if (!container) return;
|
||
|
||
if (!document.fullscreenElement) {
|
||
container.requestFullscreen().catch(err => {
|
||
console.error(`全屏切换失败: ${err.message}`);
|
||
});
|
||
} else {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen();
|
||
}
|
||
}
|
||
},
|
||
// 从路径中提取文件名
|
||
extractFileName(path) {
|
||
if (!path) return '未知文件';
|
||
// 移除URL前缀
|
||
let cleanPath = path;
|
||
if (cleanPath.includes('http://') || cleanPath.includes('https://')) {
|
||
const urlObj = new URL(cleanPath);
|
||
cleanPath = urlObj.pathname;
|
||
}
|
||
// 提取文件名
|
||
const parts = cleanPath.split('/');
|
||
return parts[parts.length - 1] || '未知文件';
|
||
},
|
||
|
||
// 表单提交前确保file字段已正确设置
|
||
ensureFileField() {
|
||
this.updateFilePaths()
|
||
return this.formData.file
|
||
},
|
||
|
||
// 表单提交
|
||
submitForm() {
|
||
this.$refs.form.validate(valid => {
|
||
if (valid) {
|
||
// 确保file字段已正确设置
|
||
this.ensureFileField()
|
||
this.$emit('submit', this.formData)
|
||
}
|
||
})
|
||
}
|
||
},
|
||
created() {
|
||
// 初始时确保 systemCode 使用 props 值(或空字符串)
|
||
this.$set(this.formData, 'systemCode', this.systemCode || '')
|
||
// 如果父组件通过 value 传入初始数据,value watcher 的 immediate 会处理同步
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.el-form {
|
||
padding: 20px 10px 0 0;
|
||
}
|
||
|
||
.el-input,
|
||
.el-textarea {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 预览容器样式 */
|
||
.preview-container {
|
||
width: 100%;
|
||
height: 70vh;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.preview-iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* 全屏按钮样式 */
|
||
.fullscreen-btn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 100px;
|
||
z-index: 10;
|
||
}
|
||
|
||
/* 修复预览对话框中的样式问题 */
|
||
:deep(.el-dialog__body) {
|
||
padding: 30px 20px 20px;
|
||
position: relative;
|
||
}
|
||
</style>
|