lanan-system-vue/src/views/base/chargeCompany/form/ChargeCompanyForm.vue

597 lines
20 KiB
Vue
Raw Normal View History

2025-10-21 18:37:28 +08:00
<template>
2025-10-22 14:10:47 +08:00
<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">支持多文件上传文件格式pdfdocdocxtxtjpgjpegpng大小不超过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>
<!-- 文件预览对话框 -->
2025-10-23 14:58:19 +08:00
<el-dialog :title="'文件预览(' + selectFile.fileName + ''" :visible.sync="isShowFile" width="70%" append-to- >
2025-10-22 14:10:47 +08:00
<!-- 全屏按钮 -->
<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>
2025-10-21 18:37:28 +08:00
</template>
<script>
2025-10-22 14:10:47 +08:00
import { getAccessToken } from "@/utils/auth";
import ImagePreview from '@/components/ImagePreview';
2025-10-21 18:37:28 +08:00
export default {
name: 'ChargeCompanyForm',
props: {
// 用于 v-model 的 value对象
value: {
type: Object,
default: () => ({})
},
// systemCode 允许字符串或数字
systemCode: {
type: [String, Number],
default: ''
}
},
2025-10-22 14:10:47 +08:00
components: {
ImagePreview
},
2025-10-21 18:37:28 +08:00
data() {
return {
// 本地表单数据 — 保持对象引用(方便 el-form resetFields
formData: {
id: null,
companyName: null,
contactPerson: null,
contactPhone: null,
address: null,
status: false,
systemCode: '', // 会在 created 中初始化
2025-10-22 14:10:47 +08:00
remark: null,
file: ''
2025-10-21 18:37:28 +08:00
},
2025-10-22 14:10:47 +08:00
// 上传文件列表
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: '',
2025-10-21 18:37:28 +08:00
// 防止同步回流的标志
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 传入的数据)变化并同步到本地表单
2025-10-22 14:10:47 +08:00
value: {
handler(newVal) {
// 标志:正在同步来自父组件的数据,避免触发 emit 回父组件
this.isSyncingFromParent = true
2025-10-21 18:37:28 +08:00
2025-10-22 14:10:47 +08:00
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)
2025-10-21 18:37:28 +08:00
2025-10-22 14:10:47 +08:00
// 处理文件列表
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
},
2025-10-21 18:37:28 +08:00
// 本地表单变化时向外 emit用于 v-model 双向绑定)
// 但如果是我们从父组件同步过来的改动,则不 emit避免回流
formData: {
handler(newVal) {
if (this.isSyncingFromParent) {
// 来自父组件的同步,不应再次 emit
return
}
// 发出副本,防止父组件直接修改传入对象引用
this.$emit('input', { ...newVal })
},
deep: true
}
},
2025-10-22 14:10:47 +08:00
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');
}
},
2025-10-21 18:37:28 +08:00
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,
2025-10-22 14:10:47 +08:00
remark: null,
file: ''
2025-10-21 18:37:28 +08:00
})
this.$set(this.formData, 'systemCode', keepSystemCode)
2025-10-22 14:10:47 +08:00
this.uploadFileList = []
2025-10-21 18:37:28 +08:00
this.$nextTick(() => {
this.clearValidate()
// 主动 emit 一次空的表单状态(如果需要通知父组件)
this.$emit('input', { ...this.formData })
})
},
2025-10-22 14:10:47 +08:00
// 文件上传前的钩子函数
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 })
},
2025-10-21 18:37:28 +08:00
// 外部主动传入数据设置到表单(不会改变 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)
2025-10-22 14:10:47 +08:00
// 处理文件列表
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 = []
}
2025-10-21 18:37:28 +08:00
this.$nextTick(() => {
this.isSyncingFromParent = false
})
2025-10-22 14:10:47 +08:00
},
// 判断文件是否为图片格式
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)
}
})
2025-10-21 18:37:28 +08:00
}
},
created() {
// 初始时确保 systemCode 使用 props 值(或空字符串)
this.$set(this.formData, 'systemCode', this.systemCode || '')
// 如果父组件通过 value 传入初始数据value watcher 的 immediate 会处理同步
}
}
</script>
<style scoped>
.el-form {
padding: 20px 10px 0 0;
}
2025-10-22 14:10:47 +08:00
2025-10-21 18:37:28 +08:00
.el-input,
.el-textarea {
width: 100%;
}
2025-10-22 14:10:47 +08:00
/* 预览容器样式 */
.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;
}
2025-10-21 18:37:28 +08:00
</style>