This commit is contained in:
shijing 2026-08-03 13:57:30 +08:00
commit fd05785952
13 changed files with 416 additions and 31 deletions

View File

@ -341,6 +341,12 @@ export default {
},
mlogbw: {
files: {
name: "产出附件",
req: async function (id, data) {
return await http.put(`${config.API_URL}/wpm/mlogbw/${id}/files/`, data);
},
},
list: {
name: "列表",
req: async function (data) {

View File

@ -0,0 +1,77 @@
<template>
<span>
<el-button link type="primary" :disabled="!row.id" @click="open">
附件{{ fileCount }}
</el-button>
<el-dialog
v-model="visible"
:title="`${row.number || ''} 产出附件`"
width="620px"
append-to-body
destroy-on-close
>
<sc-upload-filed
v-model="fileIds"
:file_="sourceFiles"
:disabled="disabled"
:multiple="true"
tip="支持文件、图片和视频,可一次选择多个文件"
@uploading-change="uploading = $event"
>
<el-button v-if="!disabled" type="primary">上传文件</el-button>
</sc-upload-filed>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
<el-button v-if="!disabled" type="primary" :loading="saving" :disabled="uploading" @click="save">保存</el-button>
</template>
</el-dialog>
</span>
</template>
<script>
export default {
name: "OutputMaterialFiles",
props: {
row: { type: Object, required: true },
disabled: { type: Boolean, default: false },
},
data() {
return {
visible: false,
saving: false,
uploading: false,
fileIds: [],
sourceFiles: [],
};
},
computed: {
fileCount() {
return (this.row.files_ || []).length;
},
},
methods: {
open() {
this.sourceFiles = [...(this.row.files_ || [])];
this.fileIds = this.sourceFiles.map((file) => file.id);
this.uploading = false;
this.visible = true;
},
async save() {
if (this.uploading) {
this.$message.warning("请等待文件上传完成");
return;
}
this.saving = true;
try {
const result = await this.$API.wpm.mlogbw.files.req(this.row.id, { files: this.fileIds });
this.row.files = result.files;
this.row.files_ = result.files_;
this.visible = false;
this.$message.success("附件已保存");
} finally {
this.saving = false;
}
},
},
};
</script>

View File

@ -26,6 +26,11 @@
</template>
</el-upload>
<span style="display:none!important"><el-input v-model="value"></el-input></span>
<xt-file-preview
v-model="previewVisible"
:files="defaultFileList"
:initial-index="previewIndex"
/>
</div>
</template>
@ -53,7 +58,9 @@
data() {
return {
value: "",
defaultFileList: []
defaultFileList: [],
previewVisible: false,
previewIndex: 0,
}
},
watch:{
@ -151,7 +158,9 @@
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
},
handlePreview(uploadFile){
window.open(uploadFile.url)
const index = this.defaultFileList.indexOf(uploadFile)
this.previewIndex = index >= 0 ? index : 0
this.previewVisible = true
},
request(param){
var apiObj = config.apiObjFile;

View File

@ -25,6 +25,11 @@
<div v-if="tip" class="el-upload__tip">{{tip}}</div>
</template>
</el-upload>
<xt-file-preview
v-model="previewVisible"
:files="defaultFileList"
:initial-index="previewIndex"
/>
</div>
</template>
@ -52,7 +57,10 @@
},
data() {
return {
defaultFileList: []
defaultFileList: [],
uploadingCount: 0,
previewVisible: false,
previewIndex: 0,
}
},
watch:{
@ -100,14 +108,23 @@
arr.forEach(item => {
if(item){
_arr.push({
id: item.id,
name: item.name,
url: item.path
url: item.path || item.url,
path: item.path || item.url,
mime: item.mime,
type: item.type,
status: "success",
})
}
})
return _arr
},
before(file){
if (file.size <= 0) {
this.$message.warning("不能上传空文件");
return false;
}
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
@ -121,7 +138,10 @@
}
file.name = res.name
file.url = res.path
file.path = res.path
file.id = res.id
file.mime = res.mime
file.type = res.type
},
error(err){
this.$notify.error({
@ -142,7 +162,9 @@
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
},
handlePreview(uploadFile){
window.open(uploadFile.url)
const index = this.defaultFileList.indexOf(uploadFile)
this.previewIndex = index >= 0 ? index : 0
this.previewVisible = true
},
request(param){
var apiObj = config.apiObjFile;
@ -154,6 +176,8 @@
for (const key in param.data) {
data.append(key, param.data[key]);
}
this.uploadingCount += 1
this.$emit('uploading-change', true)
apiObj.post(data, {
onUploadProgress: e => {
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
@ -164,6 +188,9 @@
param.onSuccess(res)
}).catch(err => {
param.onError(err)
}).finally(() => {
this.uploadingCount = Math.max(0, this.uploadingCount - 1)
this.$emit('uploading-change', this.uploadingCount > 0)
})
}
}

View File

@ -25,7 +25,7 @@
</template>
<template #file="{ file }">
<div class="sc-upload-list-item">
<el-image class="el-upload-list__item-thumbnail" :src="file.url" fit="cover" :preview-src-list="preview" :initial-index="preview.findIndex(n=>n==file.url)" hide-on-click-modal append-to-body :z-index="9999">
<el-image class="el-upload-list__item-thumbnail" :src="file.url" fit="cover" @click="handlePreview(file)">
<template #placeholder>
<div class="sc-upload-multiple-image-slot">
Loading...
@ -42,6 +42,11 @@
</template>
</el-upload>
<span style="display:none!important"><el-input v-model="value"></el-input></span>
<xt-file-preview
v-model="previewVisible"
:files="defaultFileList"
:initial-index="previewIndex"
/>
</div>
</template>
@ -69,7 +74,9 @@
data(){
return {
value: "",
defaultFileList: []
defaultFileList: [],
previewVisible: false,
previewIndex: 0,
}
},
watch:{
@ -235,7 +242,9 @@
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
},
handlePreview(uploadFile){
window.open(uploadFile.url)
const index = this.defaultFileList.indexOf(uploadFile)
this.previewIndex = index >= 0 ? index : 0
this.previewVisible = true
},
request(param){
var apiObj = config.apiObj;

View File

@ -0,0 +1,228 @@
<template>
<span class="xt-file-preview-trigger">
<slot name="trigger" :open="open"></slot>
<el-dialog
v-model="visible"
:title="currentFile.name || '文件预览'"
width="90%"
top="5vh"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<div class="xt-file-preview">
<div v-if="!currentFile.url" class="xt-file-preview__empty">
文件地址不存在
</div>
<img
v-else-if="previewType === 'image'"
class="xt-file-preview__image"
:src="currentFile.url"
:alt="currentFile.name"
/>
<video
v-else-if="previewType === 'video'"
class="xt-file-preview__media"
:src="currentFile.url"
controls
preload="metadata"
></video>
<audio
v-else-if="previewType === 'audio'"
class="xt-file-preview__audio"
:src="currentFile.url"
controls
preload="metadata"
></audio>
<iframe
v-else-if="previewType === 'pdf'"
class="xt-file-preview__pdf"
:src="currentFile.url"
:title="currentFile.name"
></iframe>
<div v-else class="xt-file-preview__unsupported">
<div class="xt-file-preview__file-name">{{ currentFile.name }}</div>
<div>该文件格式不支持浏览器内预览请下载源文件查看</div>
</div>
</div>
<template #footer>
<div class="xt-file-preview__footer">
<div>
<el-button v-if="fileList.length > 1" :disabled="currentIndex <= 0" @click="previous">
上一个
</el-button>
<span v-if="fileList.length > 1" class="xt-file-preview__counter">
{{ currentIndex + 1 }} / {{ fileList.length }}
</span>
<el-button v-if="fileList.length > 1" :disabled="currentIndex >= fileList.length - 1" @click="next">
下一个
</el-button>
</div>
<div>
<el-button :disabled="!currentFile.url" @click="download">下载源文件</el-button>
<el-button type="primary" @click="visible = false">关闭</el-button>
</div>
</div>
</template>
</el-dialog>
</span>
</template>
<script>
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
const VIDEO_EXTENSIONS = ["mp4", "webm", "ogg", "mov", "m4v"];
const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac"];
export default {
name: "XtFilePreview",
props: {
modelValue: { type: Boolean, default: undefined },
files: { type: Array, default: () => [] },
initialIndex: { type: Number, default: 0 },
},
emits: ["update:modelValue"],
data() {
return {
currentIndex: 0,
internalVisible: false,
};
},
computed: {
visible: {
get() {
return this.modelValue === undefined ? this.internalVisible : this.modelValue;
},
set(value) {
this.internalVisible = value;
this.$emit("update:modelValue", value);
},
},
fileList() {
return this.files.map((file) => this.normalizeFile(file));
},
currentFile() {
return this.fileList[this.currentIndex] || {};
},
previewType() {
const mime = (this.currentFile.mime || "").toLowerCase();
const extension = this.extensionOf(this.currentFile.name || this.currentFile.url || "");
if (mime.startsWith("image/") || IMAGE_EXTENSIONS.includes(extension)) return "image";
if (mime.startsWith("video/") || VIDEO_EXTENSIONS.includes(extension)) return "video";
if (mime.startsWith("audio/") || AUDIO_EXTENSIONS.includes(extension)) return "audio";
if (mime === "application/pdf" || extension === "pdf") return "pdf";
return "unsupported";
},
},
watch: {
modelValue(value) {
if (value) this.resetIndex();
},
initialIndex() {
if (this.modelValue) this.resetIndex();
},
files() {
this.resetIndex(this.currentIndex);
},
},
methods: {
normalizeFile(file) {
const response = file?.response || {};
return {
id: file?.id || response.id,
name: file?.name || response.name || "未命名文件",
url: file?.path || file?.url || response.path || response.url || "",
mime: file?.mime || file?.raw?.type || response.mime || "",
};
},
extensionOf(value) {
const cleanValue = value.split(/[?#]/)[0];
const dotIndex = cleanValue.lastIndexOf(".");
return dotIndex >= 0 ? cleanValue.slice(dotIndex + 1).toLowerCase() : "";
},
resetIndex(index = this.initialIndex) {
const maxIndex = Math.max(0, this.files.length - 1);
this.currentIndex = Math.min(Math.max(0, index), maxIndex);
},
open(index = this.initialIndex) {
this.resetIndex(index);
this.visible = true;
},
previous() {
if (this.currentIndex > 0) this.currentIndex -= 1;
},
next() {
if (this.currentIndex < this.fileList.length - 1) this.currentIndex += 1;
},
download() {
if (!this.currentFile.url) return;
const link = document.createElement("a");
link.href = this.currentFile.url;
link.download = this.currentFile.name || "download";
link.target = "_blank";
link.rel = "noopener";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
},
};
</script>
<style scoped>
.xt-file-preview {
display: flex;
align-items: center;
justify-content: center;
min-height: 420px;
height: 70vh;
background: var(--el-fill-color-light);
overflow: hidden;
}
.xt-file-preview__image,
.xt-file-preview__media {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.xt-file-preview__audio {
width: min(600px, 90%);
}
.xt-file-preview__pdf {
width: 100%;
height: 100%;
border: 0;
background: #fff;
}
.xt-file-preview__empty,
.xt-file-preview__unsupported {
padding: 32px;
text-align: center;
color: var(--el-text-color-secondary);
}
.xt-file-preview__file-name {
margin-bottom: 12px;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
word-break: break-all;
}
.xt-file-preview__footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.xt-file-preview__counter {
display: inline-block;
min-width: 70px;
text-align: center;
color: var(--el-text-color-secondary);
}
</style>

View File

@ -30,17 +30,23 @@
<el-table-column label="物料类别" prop="material_cate" width="200" show-overflow-tooltip></el-table-column>
<el-table-column label="调查表" prop="survery_form" width="100" show-overflow-tooltip>
<template #default="scope">
<el-link v-if="scope.row.survery_form_" type="text" :href="scope.row.survery_form_.path" target="_blank" style="color:blue" @click.stop="">下载</el-link>
<xt-file-preview v-if="scope.row.survery_form_" :files="[scope.row.survery_form_]">
<template #trigger="{ open }"><el-link type="primary" @click.stop="open()">预览</el-link></template>
</xt-file-preview>
</template>
</el-table-column>
<el-table-column label="营业执照" prop="business_license" width="100" show-overflow-tooltip>
<template #default="scope">
<el-link v-if="scope.row.business_license_" type="text" :href="scope.row.business_license_.path" target="_blank" style="color:blue" @click.stop="">下载</el-link>
<xt-file-preview v-if="scope.row.business_license_" :files="[scope.row.business_license_]">
<template #trigger="{ open }"><el-link type="primary" @click.stop="open()">预览</el-link></template>
</xt-file-preview>
</template>
</el-table-column>
<el-table-column label="质量证书" prop="quality_certificate" show-overflow-tooltip>
<template #default="scope">
<el-link v-if="scope.row.quality_certificate_" type="text" :href="scope.row.quality_certificate_.path" target="_blank" style="color:blue" @click.stop="">下载</el-link>
<xt-file-preview v-if="scope.row.quality_certificate_" :files="[scope.row.quality_certificate_]">
<template #trigger="{ open }"><el-link type="primary" @click.stop="open()">预览</el-link></template>
</xt-file-preview>
</template>
</el-table-column>
</scTable>
@ -70,4 +76,4 @@ const exportCols = [
{ header: "物料名称", key: "material_name", wch: 20 },
{ header: "物料类别", key: "material_cate", wch: 20 },
]
</script>
</script>

View File

@ -22,9 +22,13 @@
</el-table-column>
<el-table-column label="文件" prop="files" >
<template #default="scope">
<div v-for="item in scope.row.files_" :key="item.id">
<el-link style="font-size:12px" type="primary" :href="item.path" target="_blank">{{item.name}}</el-link>
</div>
<xt-file-preview :files="scope.row.files_">
<template #trigger="{ open }">
<div v-for="(item, index) in scope.row.files_" :key="item.id">
<el-link style="font-size:12px" type="primary" @click.stop="open(index)">{{item.name}}</el-link>
</div>
</template>
</xt-file-preview>
</template>
</el-table-column>
<!-- <el-table-column label="操作" fixed="right" align="left" width="170">
@ -108,4 +112,4 @@ export default {
},
},
};
</script>
</script>

View File

@ -29,10 +29,14 @@
</el-table-column>
<el-table-column label="文件资料" prop="name">
<template #default="scope">
<div v-for="item in scope.row.files_" :key="item.id">
<el-link style="font-size:12px" type="primary" :href="item.path" target="_blank">
{{ item.name }}</el-link>
</div>
<xt-file-preview :files="scope.row.files_">
<template #trigger="{ open }">
<div v-for="(item, index) in scope.row.files_" :key="item.id">
<el-link style="font-size:12px" type="primary" @click.stop="open(index)">
{{ item.name }}</el-link>
</div>
</template>
</xt-file-preview>
</template>
</el-table-column>
</scTable>

View File

@ -146,15 +146,15 @@
</el-table-column>
<el-table-column label="文件资料" prop="name">
<template #default="scope">
<div v-for="item in scope.row.files_" :key="item.id">
<el-link
style="font-size: 12px"
type="primary"
:href="item.path"
target="_blank"
>{{ item.name }}</el-link
>
</div>
<xt-file-preview :files="scope.row.files_">
<template #trigger="{ open }">
<div v-for="(item, index) in scope.row.files_" :key="item.id">
<el-link style="font-size: 12px" type="primary" @click.stop="open(index)">
{{ item.name }}
</el-link>
</div>
</template>
</xt-file-preview>
</template>
</el-table-column>

View File

@ -233,6 +233,11 @@
<el-input v-else v-model="scope.row.note" placeholder="备注"></el-input>
</template>
</el-table-column>
<el-table-column v-if="mode == 'outs'" label="附件" width="100px" align="center">
<template #default="scope">
<output-material-files :row="scope.row" :disabled="isSubmit" />
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" fixed="right" v-if="!isSubmit">
<template #default="scope">
<el-button
@ -429,7 +434,10 @@
</el-drawer>
</template>
<script>
import OutputMaterialFiles from "@/components/OutputMaterialFiles.vue";
export default {
components: { OutputMaterialFiles },
props: {
mlogb: {
type: String,

View File

@ -63,6 +63,7 @@
<span style="color:#f56c6c;">{{ getDefectCount(item.defect_name) }}</span>
</th>
<th class="w_80" v-show="isColVisible('note')">备注</th>
<th class="w_100">附件</th>
<th class="w_150 sticky-cell sticky-right-action" v-if="!isSubmit">操作</th>
</tr>
</thead>
@ -128,7 +129,10 @@
<td class="w_80 " v-show="isColVisible('note')">
<input v-if="row.isEdit" v-model="row.note" placeholder="备注">
<span v-else style="width: 100%;height: 100%;display: inline-block;">{{ row.note }}</span>
</td>
</td>
<td class="w_100">
<output-material-files :row="row" :disabled="isSubmit" />
</td>
<td
class="w_150 sticky-cell sticky-right-action operation-cell"
:class="{ 'is-menu-open': openedActionMenu === actionMenuKey(row, index) }"
@ -323,8 +327,9 @@
<script>
import { ElLoading } from "element-plus";
import scFileImport from "@/components/scFileImport";
import OutputMaterialFiles from "@/components/OutputMaterialFiles.vue";
export default {
components: {scFileImport},
components: { scFileImport, OutputMaterialFiles },
props: {
qct: {
type: String,

View File

@ -4,6 +4,7 @@ import ehsSelect from './components/ehsSelect/select'
import ehsTableSelect from './components/ehsSelect/tableSelect'
import xtSelect from './components/xtSelect/index.vue'
import xtScanInput from './components/xtScanInput/index.vue'
import xtFilePreview from './components/xtFilePreview/index.vue'
export default {
install(app) {
app.component('ehsUserSelect', ehsUserSelect);
@ -12,5 +13,6 @@ export default {
app.component('ehsTableSelect', ehsTableSelect);
app.component('xtSelect', xtSelect);
app.component('xtScanInput', xtScanInput);
app.component('xtFilePreview', xtFilePreview);
}
}