spring
2025-08-05 7acc4367b3320e2a49265112fc8051f285a870bf
文件预览组件封装使用
已修改4个文件
已添加1个文件
359 ■■■■ 文件已修改
package.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/element-ui.scss 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/filePreview/index.vue 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/fileList.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -17,6 +17,8 @@
  },
  "dependencies": {
    "@element-plus/icons-vue": "2.3.1",
    "@vue-office/docx": "^1.6.3",
    "@vue-office/excel": "^1.7.14",
    "@vueup/vue-quill": "1.2.0",
    "@vueuse/core": "10.11.0",
    "axios": "0.28.1",
src/assets/styles/element-ui.scss
@@ -56,18 +56,18 @@
  padding: 0 !important;
}
.el-dialog__header {
  background: #F5F6F7;
  background: #f5f6f7;
  padding: 12px 16px;
  border-radius: 8px 8px 0 0;
}
.el-dialog__title {
  font-weight: 400;
  font-size: 16px;
  color: #2E3033;
  color: #2e3033;
}
.el-dialog__body {
  padding: 16px 40px 0 40px;
  max-height: 680px;
  max-height: 90vh;
  overflow-y: auto;
}
.el-dialog__footer {
@@ -79,14 +79,14 @@
  border-radius: 8px;
}
.el-message-box__header {
  background: #F5F6F7;
  background: #f5f6f7;
  padding: 12px 16px;
  border-radius: 8px 8px 0 0;
}
.el-message-box__title {
  font-weight: 400;
  font-size: 16px;
  color: #2E3033;
  color: #2e3033;
}
.el-message-box__content {
  padding: 16px 40px 0 40px;
@@ -108,7 +108,7 @@
.el-table__expanded-cell {
  padding: 0 !important;
  .el-table__header-wrapper {
    background-color: #F5F8FF !important;
    background-color: #f5f8ff !important;
  }
}
@@ -127,7 +127,7 @@
// dropdown
.el-dropdown-menu {
  a {
    display: block
    display: block;
  }
}
@@ -149,6 +149,6 @@
  display: none;
}
.el-dropdown .el-dropdown-link{
.el-dropdown .el-dropdown-link {
  color: var(--el-color-primary) !important;
}
}
src/components/filePreview/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,201 @@
<template>
  <el-dialog v-model="dialogVisible" title="预览" width="100%" fullscreen align-center :before-close="handleClose" append-to-body>
    <div>
      <!-- å›¾ç‰‡é¢„览 -->
      <div v-if="isImage">
        <img :src="imgUrl" alt="Image Preview" />
      </div>
      <!-- PDF预览提示 -->
      <div v-if="isPdf" style="height: 100vh; display: flex; align-items: center; justify-content: center;">
        <p>正在准备PDF预览...</p>
      </div>
      <!-- Word文档预览 -->
      <div v-if="isDoc">
        <p v-if="!isDocShow">文档无法直接预览,请下载查看。</p>
        <a :href="fileUrl" v-if="!isDocShow">下载文件</a>
        <vue-office-docx
          v-else
          :src="fileUrl"
          style="height: 100vh;"
          @rendered="renderedHandler"
          @error="errorHandler"
        />
      </div>
      <!-- Excel文档预览 -->
      <div v-if="isXls">
        <p v-if="!isDocShow">文档无法直接预览,请下载查看。</p>
        <a :href="fileUrl" v-if="!isDocShow">下载文件</a>
        <vue-office-excel
          v-else
          :src="fileUrl"
          :options="options"
          style="height: 100vh;"
          @rendered="renderedHandler"
          @error="errorHandler"
        />
      </div>
      <!-- åŽ‹ç¼©æ–‡ä»¶å¤„ç† -->
      <div v-if="isZipOrRar">
        <p>压缩文件无法直接预览,请下载查看。</p>
        <a :href="fileUrl">下载文件</a>
      </div>
      <!-- ä¸æ”¯æŒçš„æ ¼å¼ -->
      <div v-if="!isSupported">
        <p>不支持的文件格式</p>
      </div>
    </div>
  </el-dialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance, watch } from 'vue';
import VueOfficeDocx from '@vue-office/docx';
import '@vue-office/docx/lib/index.css';
import VueOfficeExcel from '@vue-office/excel';
import '@vue-office/excel/lib/index.css';
// å“åº”式变量
const fileUrl = ref('')
const dialogVisible = ref(false)
const { proxy } = getCurrentInstance();
const javaApi = proxy.javaApi;
// æ–‡æ¡£é¢„览状态
const isDocShow = ref(true);
const imgUrl = ref('');
const options = ref({
  xls: false,
  minColLength: 0,
  minRowLength: 0,
  widthOffset: 10,
  heightOffset: 10,
  beforeTransformData: (workbookData) => workbookData,
  transformData: (workbookData) => workbookData,
});
// è®¡ç®—属性 - åˆ¤æ–­æ–‡ä»¶ç±»åž‹
const isImage = computed(() => {
  const state = /\.(jpg|jpeg|png|gif)$/i.test(fileUrl.value);
  if (state) {
    imgUrl.value = fileUrl.value.replaceAll('word', 'img');
  }
  return state;
});
const isPdf = computed(() => {
  return /\.pdf$/i.test(fileUrl.value);
});
const isDoc = computed(() => {
  return /\.(doc|docx)$/i.test(fileUrl.value);
});
const isXls = computed(() => {
  const state = /\.(xls|xlsx)$/i.test(fileUrl.value);
  if (state) {
    options.value.xls = /\.(xls)$/i.test(fileUrl.value);
  }
  return state;
});
const isZipOrRar = computed(() => {
  return /\.(zip|rar)$/i.test(fileUrl.value);
});
const isSupported = computed(() => {
  return isImage.value || isPdf.value || isDoc.value || isXls.value || isZipOrRar.value;
});
// åŠ¨æ€åˆ›å»ºa标签并跳转预览PDF
const previewPdf = (url) => {
  // åˆ›å»ºa标签
  const link = document.createElement('a');
  // è®¾ç½®PDF文件URL
  link.href = url;
  // åœ¨æ–°æ ‡ç­¾é¡µæ‰“å¼€
  link.target = '_blank';
  // å®‰å…¨å±žæ€§ï¼Œé˜²æ­¢æ–°é¡µé¢è®¿é—®åŽŸé¡µé¢
  link.rel = 'noopener noreferrer';
  // å¯é€‰ï¼šè®¾ç½®é“¾æŽ¥æ–‡æœ¬
  link.textContent = '预览PDF';
  // å°†a标签添加到页面(部分浏览器要求必须在DOM中)
  document.body.appendChild(link);
  // è§¦å‘点击事件
  link.click();
  // ç§»é™¤a标签,清理DOM
  document.body.removeChild(link);
};
// ç›‘听PDF状态变化,自动触发跳转
watch(
  () => isPdf.value,
  (newVal) => {
    // å½“确认是PDF且文件URL有效时
    if (newVal && fileUrl.value) {
      // å…³é—­å¯¹è¯æ¡†
      dialogVisible.value = false;
      // åŠ ä¸ªå°å»¶è¿Ÿç¡®ä¿çŠ¶æ€æ›´æ–°å®Œæˆ
      setTimeout(() => {
        previewPdf(fileUrl.value);
        fileUrl.value = '';
      }, 100);
    }
  }
);
// æ–¹æ³•定义
const renderedHandler = () => {
  console.log("渲染完成");
  isDocShow.value = true;
  resetStyle();
};
const errorHandler = () => {
  console.log("渲染失败");
  isDocShow.value = false;
};
const open = (url) => {
  fileUrl.value = javaApi + url;
  dialogVisible.value = true;
};
const handleClose = () => {
  dialogVisible.value = false;
};
const resetStyle = () => {
  const elements = document.querySelectorAll('[style*="pt"]');
  for (const element of elements) {
    const style = element.getAttribute('style');
    if (style) {
      element.setAttribute('style', style.replace(/pt/g, 'px'));
    }
  }
};
// æš´éœ²open方法供外部调用
defineExpose({
  open
})
</script>
<style scoped>
img {
  max-width: 100%;
  display: block;
  margin: 0 auto;
}
.oneLine {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
</style>
src/main.js
@@ -1,68 +1,81 @@
import { createApp } from 'vue'
import { createApp } from "vue";
import Cookies from 'js-cookie'
import Cookies from "js-cookie";
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import locale from 'element-plus/es/locale/lang/zh-cn'
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import "element-plus/theme-chalk/dark/css-vars.css";
import locale from "element-plus/es/locale/lang/zh-cn";
import '@/assets/styles/index.scss' // global css
import "@/assets/styles/index.scss"; // global css
import App from './App'
import store from './store'
import router from './router'
import directive from './directive' // directive
import App from "./App";
import store from "./store";
import router from "./router";
import directive from "./directive"; // directive
// æ³¨å†ŒæŒ‡ä»¤
import plugins from './plugins' // plugins
import { download } from '@/utils/request'
import plugins from "./plugins"; // plugins
import { download } from "@/utils/request";
// svg图标
import 'virtual:svg-icons-register'
import SvgIcon from '@/components/SvgIcon'
import elementIcons from '@/components/SvgIcon/svgicon'
import "virtual:svg-icons-register";
import SvgIcon from "@/components/SvgIcon";
import elementIcons from "@/components/SvgIcon/svgicon";
import './permission' // permission control
import "./permission"; // permission control
import { useDict } from '@/utils/dict'
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
import { useDict } from "@/utils/dict";
import {
  parseTime,
  resetForm,
  addDateRange,
  handleTree,
  selectDictLabel,
  selectDictLabels,
} from "@/utils/ruoyi";
// åˆ†é¡µç»„ä»¶
import Pagination from '@/components/Pagination'
import Pagination from "@/components/Pagination";
// è‡ªå®šä¹‰è¡¨æ ¼å·¥å…·ç»„ä»¶
import RightToolbar from '@/components/RightToolbar'
import RightToolbar from "@/components/RightToolbar";
// å¯Œæ–‡æœ¬ç»„ä»¶
import Editor from "@/components/Editor"
import Editor from "@/components/Editor";
// æ–‡ä»¶ä¸Šä¼ ç»„ä»¶
import FileUpload from "@/components/FileUpload"
import FileUpload from "@/components/FileUpload";
// å›¾ç‰‡ä¸Šä¼ ç»„ä»¶
import ImageUpload from "@/components/ImageUpload"
import ImageUpload from "@/components/ImageUpload";
// å›¾ç‰‡é¢„览组件
import ImagePreview from "@/components/ImagePreview"
import ImagePreview from "@/components/ImagePreview";
// å­—典标签组件
import DictTag from '@/components/DictTag'
import DictTag from "@/components/DictTag";
// è¡¨æ ¼ç»„ä»¶
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { getToken } from "@/utils/auth";
import {calculateTaxExclusiveTotalPrice, summarizeTable,calculateTaxIncludeTotalPrice} from "@/utils/summarizeTable.js";
import {
  calculateTaxExclusiveTotalPrice,
  summarizeTable,
  calculateTaxIncludeTotalPrice,
} from "@/utils/summarizeTable.js";
const app = createApp(App)
const app = createApp(App);
// å…¨å±€æ–¹æ³•挂载
app.config.globalProperties.useDict = useDict
app.config.globalProperties.download = download
app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.summarizeTable = summarizeTable
app.config.globalProperties.calculateTaxExclusiveTotalPrice = calculateTaxExclusiveTotalPrice
app.config.globalProperties.calculateTaxIncludeTotalPrice = calculateTaxIncludeTotalPrice
app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels
app.config.globalProperties.javaApi = 'http://114.132.189.42:8078'
app.config.globalProperties.useDict = useDict;
app.config.globalProperties.download = download;
app.config.globalProperties.parseTime = parseTime;
app.config.globalProperties.resetForm = resetForm;
app.config.globalProperties.summarizeTable = summarizeTable;
app.config.globalProperties.calculateTaxExclusiveTotalPrice =
  calculateTaxExclusiveTotalPrice;
app.config.globalProperties.calculateTaxIncludeTotalPrice =
  calculateTaxIncludeTotalPrice;
app.config.globalProperties.handleTree = handleTree;
app.config.globalProperties.addDateRange = addDateRange;
app.config.globalProperties.selectDictLabel = selectDictLabel;
app.config.globalProperties.selectDictLabels = selectDictLabels;
app.config.globalProperties.javaApi = "http://114.132.189.42:8099";
app.config.globalProperties.HaveJson = (val) => {
  return JSON.parse(JSON.stringify(val));
};
@@ -71,29 +84,29 @@
};
// å…¨å±€ç»„件挂载
app.component('DictTag', DictTag)
app.component('Pagination', Pagination)
app.component('FileUpload', FileUpload)
app.component('ImageUpload', ImageUpload)
app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar)
app.component('Editor', Editor)
app.component('PIMTable', PIMTable)
app.component("DictTag", DictTag);
app.component("Pagination", Pagination);
app.component("FileUpload", FileUpload);
app.component("ImageUpload", ImageUpload);
app.component("ImagePreview", ImagePreview);
app.component("RightToolbar", RightToolbar);
app.component("Editor", Editor);
app.component("PIMTable", PIMTable);
app.use(router)
app.use(store)
app.use(plugins)
app.use(elementIcons)
app.component('svg-icon', SvgIcon)
app.use(router);
app.use(store);
app.use(plugins);
app.use(elementIcons);
app.component("svg-icon", SvgIcon);
directive(app)
directive(app);
// ä½¿ç”¨element-plus å¹¶ä¸”设置全局的大小
app.use(ElementPlus, {
  locale: locale,
  // æ”¯æŒ large、default、small
  size: Cookies.get('size') || 'default'
})
app._context.components.ElDialog.props.closeOnClickModal.default = false
  size: Cookies.get("size") || "default",
});
app._context.components.ElDialog.props.closeOnClickModal.default = false;
app.mount('#app')
app.mount("#app");
src/views/salesManagement/salesLedger/fileList.vue
@@ -1,22 +1,26 @@
<template>
  <el-dialog v-model="dialogVisible" title="附件" width="30%" :before-close="handleClose">
  <el-dialog v-model="dialogVisible" title="附件" width="40%" :before-close="handleClose">
    <el-table :data="tableData" border height="40vh">
      <el-table-column label="附件名称" prop="name" min-width="400" show-overflow-tooltip />
      <el-table-column fixed="right" label="操作" width="100" align="center">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">下载</el-button>
          <el-button link type="primary" size="small" @click="lookFile(scope.row)">预览</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-dialog>
  <filePreview ref="filePreviewRef" />
</template>
<script setup>
import { ref } from 'vue'
import filePreview from '@/components/filePreview/index.vue'
const dialogVisible = ref(false)
const tableData = ref([])
const { proxy } = getCurrentInstance();
const filePreviewRef = ref()
const handleClose = () => {
  dialogVisible.value = false
}
@@ -28,6 +32,9 @@
  proxy.$download.name(row.url);
}
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
defineExpose({
  open
})