lanan-system-vue/src/views/base/chargeCompany/form/ChargeCompanyForm.vue
2025-10-23 14:58:19 +08:00

597 lines
20 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>
<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>