From 9674af17d8e7ad8f85c9df3b89d99a71d7e39268 Mon Sep 17 00:00:00 2001
From: 祖安之光 <11848914+light-of-zuan@user.noreply.gitee.com>
Date: 星期三, 03 十二月 2025 13:11:09 +0800
Subject: [PATCH] 修改新增
---
src/views/work/productAndService/components/editDialog.vue | 229 ++++++++++++++++++++
src/api/standardSys/standardSys.js | 24 ++
src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/index.vue | 52 ++++
src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/components/exportDoc.js | 117 ++++++++++
public/qualityFile.docx | 0
src/views/work/productAndService/index.vue | 179 ++++++++++++++++
6 files changed, 596 insertions(+), 5 deletions(-)
diff --git a/public/qualityFile.docx b/public/qualityFile.docx
index d2b504a..451edd4 100644
--- a/public/qualityFile.docx
+++ b/public/qualityFile.docx
Binary files differ
diff --git a/src/api/standardSys/standardSys.js b/src/api/standardSys/standardSys.js
index 62d9aaf..0fbb12d 100644
--- a/src/api/standardSys/standardSys.js
+++ b/src/api/standardSys/standardSys.js
@@ -103,3 +103,27 @@
method: 'get'
})
}
+
+export function getProductServiceList(params) {
+ return request({
+ url: '/system/productService/selectProductServiceList',
+ method: 'get',
+ params: params
+ })
+}
+
+export function saveProductService(data) {
+ return request({
+ url: '/system/productService/saveProductService',
+ method: 'post',
+ data: data
+ })
+}
+
+export function delProductService(params) {
+ return request({
+ url: '/system/productService/delProductService',
+ method: 'get',
+ params: params
+ })
+}
diff --git a/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/components/exportDoc.js b/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/components/exportDoc.js
index 41a294d..b027f15 100644
--- a/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/components/exportDoc.js
+++ b/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/components/exportDoc.js
@@ -271,12 +271,103 @@
}
return bytes.buffer;
}
+
+function getDimensionsFromBase64Sync(base64Str) {
+ try {
+ // 去除 data:image/...;base64, 前缀
+ const base64Data = base64Str.replace(/^data:image\/\w+;base64,/, '');
+
+ // 将base64转换为二进制
+ const binaryString = atob(base64Data);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+
+ return parseImageDimensions(bytes);
+ } catch (error) {
+ console.warn('解析图片尺寸失败:', error);
+ return { width: 600, height: 400 };
+ }
+}
+
+function parseImageDimensions(bytes) {
+ if (bytes.length < 8) {
+ return { width: 600, height: 400 };
+ }
+
+ // 检查 PNG 格式 (89 50 4E 47 0D 0A 1A 0A)
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 &&
+ bytes[2] === 0x4E && bytes[3] === 0x47) {
+ return parsePNGDimensions(bytes);
+ }
+
+ // 检查 JPEG 格式 (FF D8)
+ if (bytes[0] === 0xFF && bytes[1] === 0xD8) {
+ return parseJPEGDimensions(bytes);
+ }
+
+ return { width: 600, height: 400 };
+}
+
+// 解析 PNG 尺寸
+function parsePNGDimensions(bytes) {
+ // PNG 的宽高在 IHDR chunk 中(偏移 16-24 字节)
+ if (bytes.length >= 24) {
+ const view = new DataView(bytes.buffer);
+ const width = view.getUint32(16, false); // 大端序
+ const height = view.getUint32(20, false);
+ return { width, height };
+ }
+ return { width: 600, height: 400 };
+}
+
+// 解析 JPEG 尺寸
+function parseJPEGDimensions(bytes) {
+ let i = 2; // 跳过 FFD8
+
+ while (i < bytes.length - 1) {
+ // JPEG 标记开始
+ if (bytes[i] === 0xFF) {
+ const marker = bytes[i + 1];
+
+ // SOF0, SOF1, SOF2 (Start of Frame markers)
+ if ((marker >= 0xC0 && marker <= 0xC3) ||
+ (marker >= 0xC5 && marker <= 0xC7) ||
+ (marker >= 0xC9 && marker <= 0xCB) ||
+ (marker >= 0xCD && marker <= 0xCF)) {
+
+ if (i + 7 < bytes.length) {
+ const height = (bytes[i + 5] << 8) | bytes[i + 6];
+ const width = (bytes[i + 7] << 8) | bytes[i + 8];
+ return { width, height };
+ }
+ break;
+ }
+
+ // 跳过当前段
+ const length = (bytes[i + 2] << 8) | bytes[i + 3];
+ i += length + 2;
+ } else {
+ i++;
+ }
+ }
+
+ return { width: 600, height: 400 };
+}
+
const imageOptions = {
getImage(tagValue) {
return base64Parser(tagValue);
},
getSize(img, tagValue, tagName, context) {
- return [600, 600];
+ const dimensions = getDimensionsFromBase64Sync(tagValue);
+ const { width, height } = dimensions;
+ const targetWidth = 600;
+ const scale = targetWidth / width;
+ let targetHeight = height * scale;
+ targetHeight = Math.max(100, Math.min(700, targetHeight));
+ return [targetWidth, Math.round(targetHeight)];
},
};
@@ -309,11 +400,27 @@
// data.departmentsTable = processTableHtml(tableHtml);
// }
- // 生成表格XML
- // if (data.clauses && data.duties) {
- // data.tableXML = generateTableXML(data.clauses, data.duties);
- // }
+ if (data.productServiceImages && Array.isArray(data.productServiceImages)) {
+ // 确保是纯 base64 字符串数组
+ data.productServiceImageArray = data.productServiceImages.map(item =>
+ typeof item === 'object' ? item.image : item
+ ).filter(img => img && typeof img === 'string');
+ // 为前10张图片创建单独的图片变量
+ data.productServiceImageArray.slice(0, 10).forEach((img, index) => {
+ data[`productServiceImage${index + 1}`] = img;
+ });
+
+ // 创建带元数据的对象数组
+ data.productServiceImageObjects = data.productServiceImageArray.map((img, index) => ({
+ image: img, // 这个字段会作为图片插入
+ index: index + 1,
+ description: `产品和服务实现过程图 ${index + 1}`
+ }));
+
+ data.productServiceCount = data.productServiceImageArray.length;
+ data.hasProductServiceImages = data.productServiceImageArray.length > 0;
+ }
// 处理富文本字段(如果有)
if (data.summaries && typeof data.summaries === 'string') {
data.summaries = processRichText(data.summaries);
diff --git a/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/index.vue b/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/index.vue
index da8a49b..7035b54 100644
--- a/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/index.vue
+++ b/src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/index.vue
@@ -243,6 +243,7 @@
templateName: item.templateName
}
}) || []
+ data.companyInfo.productServiceImages = res.data.productServiceDatas ? await processImagesToBase64(res.data.productServiceDatas): []
}else{
ElMessage.warning(res.message)
}
@@ -303,6 +304,57 @@
};
}
+// 新增:将图片URL转换为Base64
+async function urlToBase64(imageUrl) {
+ return new Promise((resolve, reject) => {
+ // 如果是相对路径,添加基础URL
+ let fullUrl = imageUrl;
+ if (!imageUrl.startsWith('http')) {
+ fullUrl = import.meta.env.VITE_APP_BASE_API + '/' + imageUrl;
+ }
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', fullUrl, true);
+ xhr.responseType = 'blob';
+
+ xhr.onload = function() {
+ if (this.status === 200) {
+ const blob = this.response;
+ const reader = new FileReader();
+ reader.onloadend = function() {
+ resolve(reader.result); // 返回base64字符串
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(blob);
+ } else {
+ reject(new Error(`图片加载失败: ${this.status}`));
+ }
+ };
+ xhr.onerror = reject;
+ xhr.send();
+ });
+}
+
+// 新增:处理图片数组
+async function processImagesToBase64(imageUrls) {
+ try {
+ const base64Images = [];
+ for (const url of imageUrls) {
+ try {
+ const base64 = await urlToBase64(url);
+ base64Images.push(base64);
+ } catch (error) {
+ console.warn(`无法加载图片 ${url}:`, error);
+ // 可以添加一个占位图片或跳过
+ base64Images.push('');
+ }
+ }
+ return base64Images;
+ } catch (error) {
+ console.error('处理图片失败:', error);
+ return [];
+ }
+}
+
const initFile = async (val) => {
data.companyInfo = {}
loading.value = true
diff --git a/src/views/work/productAndService/components/editDialog.vue b/src/views/work/productAndService/components/editDialog.vue
new file mode 100644
index 0000000..0a0a7ed
--- /dev/null
+++ b/src/views/work/productAndService/components/editDialog.vue
@@ -0,0 +1,229 @@
+<template>
+ <div class="notice">
+ <el-dialog
+ v-model="dialogVisible"
+ :title="state.title"
+ width="700px"
+ :before-close="handleClose"
+ :close-on-press-escape="false"
+ :close-on-click-modal="false"
+ >
+ <el-form :model="state.form" size="default" ref="superRef" :rules="state.formRules" label-width="200px" >
+ <el-form-item v-if="state.isAdmin" label="单位:" prop="companyId">
+ <el-select v-model="state.form.companyId" placeholder="请选择" clearable>
+ <el-option
+ v-for="item in state.companyList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id">
+ </el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="产品和服务实现过程图:" prop="fileUrl">
+ <el-upload accept="image/*" :action="state.uploadUrl" :disabled="state.title =='查看'" :headers="state.header" method="post" :on-success="(res, uploadFile)=>handleAvatarSuccess(res, uploadFile)" :on-exceed="showTip" :limit='state.fileLimit' v-model:file-list="state.fileList" :before-upload="picSize" :on-remove="(file, uploadFiles)=>handleRemove(file, uploadFiles)" >
+ <el-button type="primary">点击上传</el-button>
+ <template #tip>
+ <div class="el-upload__tip">支持上传图片格式,尺寸小于30M,最多可上传15份</div>
+ </template>
+ </el-upload>
+ </el-form-item>
+ </el-form>
+ <template #footer v-if="state.title !='查看'">
+ <span class="dialog-footer">
+ <el-button @click="handleClose" size="default">取 消</el-button>
+ <el-button type="primary" @click="onSubmit" size="default" v-preReClick>确认</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+<script setup>
+import {reactive, ref, toRefs, defineEmits, nextTick, onMounted} from 'vue'
+import {ElMessage} from "element-plus";
+import {addUser, editUser, getUserById, resetPwd} from "@/api/onlineEducation/user"
+import {Base64} from "js-base64"
+import {getCompany} from "@/api/onlineEducation/company";
+import {addIndustryTemp, updateIndustryTemp, updateInfoPlatforms} from "@/api/staffManage/staff";
+import {getToken} from "@/utils/auth";
+import {delPic} from "@/api/onlineEducation/banner";
+import {saveProductService, saveStandardTemp, updateStandardTemp} from "@/api/standardSys/standardSys";
+
+const emit = defineEmits(["getList"]);
+const dialogVisible = ref(false)
+const superRef = ref()
+const checkFiles = (rule, value, callback) => {
+ if (state.fileList.length == 0) {
+ callback(new Error('请上传文件'))
+ } else {
+ callback()
+ }
+}
+const state = reactive({
+ title: '',
+ form: {
+ id: null,
+ fileUrl: [],
+ companyId: null
+ },
+ formRules:{
+ companyId: [{ required: true, message: '请选择单位', trigger: 'blur' }],
+ fileUrl: [{ required: true, validator: checkFiles, trigger: 'blur' }]
+ },
+ isAdmin: false,
+ companyList: [],
+ uploadUrl: import.meta.env.VITE_APP_BASE_API + '/system/common/uploadFile',
+ header: {
+ Authorization: getToken()
+ },
+ fileLimit: 15,
+ fileList: []
+})
+onMounted(() => {
+
+});
+
+const openDialog = async (type, value,companyId, isAdmin, companyList) => {
+ state.isAdmin = isAdmin
+ if(isAdmin){
+ state.companyList = companyList
+ }
+ state.title = type === 'add' ? '新增' : type ==='edit' ? '编辑' : '查看'
+ state.form.companyId = companyId
+ if(state.title == '编辑'||state.title == '查看'){
+ Object.keys(state.form).forEach(key => {
+ if (key in value) {
+ state.form[key] = value[key]
+ }
+ })
+ if(!value.fileUrl || value.fileUrl == ''){
+ state.form.fileUrl = []
+ }else{
+ const certificatePaths = value.fileUrl.split(',')
+ state.form.fileUrl = certificatePaths
+ state.fileList = certificatePaths.map((path, index) => {
+ const fileName = path.split('/').pop() || `文件${index + 1}`
+ return {
+ name: fileName,
+ url: path,
+ response: {
+ data: {
+ path: path,
+ fileName: fileName
+ }
+ },
+ status: 'success',
+ uid: Date.now() + index
+ }
+ })
+ }
+ }
+ dialogVisible.value = true
+}
+
+
+const onSubmit = async () => {
+ const valid = await superRef.value.validate();
+ if(valid){
+ if(state.title == '新增'){
+ const {id,...data} = state.form
+ data.fileUrl = state.fileList
+ .filter(file => file.response?.data?.path)
+ .map(file => file.response.data.path).join(',')
+ const res = await saveProductService(data)
+ if(res.code == 200){
+ ElMessage.success(res.message)
+ emit('getList')
+ handleClose()
+ dialogVisible.value = false;
+ }else{
+ ElMessage.warning(res.message)
+ }
+ }else{
+ state.form.fileUrl = state.fileList
+ .filter(file => file.response?.data?.path)
+ .map(file => file.response.data.path)
+ .join(',')
+ const res = await saveProductService(state.form)
+ if(res.code == 200){
+ ElMessage.success(res.message)
+ emit('getList')
+ handleClose()
+ dialogVisible.value = false;
+ }else{
+ ElMessage.warning(res.message)
+ }
+ }
+ }
+}
+
+const handleAvatarSuccess = (response, uploadFile) => {
+ if(response.code === 200){
+ // 设置文件显示名称
+ uploadFile.name = response.data.fileName || `文件${state.fileList.length}`
+ uploadFile.url = response.data.url || response.data.path
+ ElMessage.success('文件上传成功')
+ } else {
+ const index = state.fileList.findIndex(file => file.uid === uploadFile.uid)
+ if (index > -1) {
+ state.fileList.splice(index, 1)
+ }
+ ElMessage.error(response.message || '文件上传失败')
+ }
+}
+
+const showTip =()=>{
+ ElMessage({
+ type: 'warning',
+ message: '超出文件上传数量'
+ });
+}
+const picSize = async (rawFile) => {
+ if(rawFile.size / 1024 / 1024 > 30){
+ ElMessage({
+ type: 'warning',
+ message: '文件大小不能超过30M'
+ });
+ return false
+ }
+};
+const handleRemove = async (file, uploadFiles) => {
+ try {
+ if (file.response?.data?.path) {
+ await delPic({ path: file.response.data.path })
+ ElMessage.success('文件删除成功')
+ }
+ } catch (error) {
+ ElMessage.error('文件删除失败')
+ }
+}
+
+const handleClose = () => {
+ state.form = {
+ id: null,
+ fileUrl: [],
+ companyId: null
+ }
+ state.fileList = []
+ superRef.value.clearValidate();
+ superRef.value.resetFields()
+ dialogVisible.value = false;
+}
+
+defineExpose({
+ openDialog
+});
+
+</script>
+
+<style scoped lang="scss">
+.notice{
+ :deep(.el-form .el-form-item__label) {
+ font-size: 15px;
+ }
+ .file {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+</style>
diff --git a/src/views/work/productAndService/index.vue b/src/views/work/productAndService/index.vue
new file mode 100644
index 0000000..60c9676
--- /dev/null
+++ b/src/views/work/productAndService/index.vue
@@ -0,0 +1,179 @@
+<template>
+ <div class="app-container">
+ <div style="display: flex;justify-content: space-between">
+ <el-form :inline="true" style="display: flex;align-items: center;flex-wrap: wrap;" >
+ <el-form-item>
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="openDialog('add',{})"
+ >新增</el-button>
+ </el-form-item>
+ <el-form-item v-if="isAdmin" label="单位:" >
+ <el-select v-model="data.queryParams.companyId" placeholder="请选择" clearable>
+ <el-option
+ v-for="item in companyList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id">
+ </el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item >
+ <el-button v-if="isAdmin" type="primary" @click="getList">查询</el-button>
+ <el-button v-if="isAdmin" type="primary" plain @click="reset">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <!-- 表格数据 -->
+ <el-table v-loading="loading" :data="dataList" :border="true">
+ <el-table-column label="序号" type="index" align="center" width="80"/>
+ <el-table-column label="产品和服务实现过程图" prop="fileUrl" align="center">
+ <template #default="scope">
+ <div class="demo-image__preview" v-if="scope.row.fileUrl && scope.row.fileUrl !== ''">
+ <el-image
+ v-for="(pic,index) in scope.row.fileUrl.split(',')"
+ style="width: 100px; height: 100px;margin-right: 10px"
+ :src= "baseUrl + '/' + pic"
+ :zoom-rate="1.2"
+ :max-scale="7"
+ :min-scale="0.2"
+ :preview-src-list="[baseUrl + '/' + pic]"
+ :initial-index="0"
+ fit="cover"
+ :preview-teleported=true
+ />
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" width="120">
+ <template #default="scope">
+ <el-button link type="primary" @click="openDialog('edit',scope.row)">编辑</el-button>
+ <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+<!-- <div id="docx-preview-container" style="width: 100%; height: auto; border: 1px solid #ccc;"></div>-->
+ <edit-dialog ref="dialogRef" @getList=getList></edit-dialog>
+ </div>
+</template>
+
+<script setup>
+import {getCurrentInstance, onMounted, onUnmounted, reactive, ref, toRefs} from "vue";
+import {ElMessage, ElMessageBox} from "element-plus";
+import {delCompany, getCompany} from "@/api/onlineEducation/company";
+import Cookies from "js-cookie";
+import editDialog from './components/editDialog.vue'
+import useUserStore from "@/store/modules/user";
+import {
+ getStandardTemp,
+ delStandardTemp,
+ getProductServiceList,
+ delProductService
+} from "@/api/standardSys/standardSys";
+import { renderAsync } from "docx-preview";
+
+const userStore = useUserStore()
+const { proxy } = getCurrentInstance();
+const loading = ref(false);
+const dialogRef = ref();
+const data = reactive({
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ companyId: null
+ },
+ total: 0,
+ dataList: [],
+ companyList: [],
+ isAdmin: false,
+ baseUrl: import.meta.env.VITE_APP_BASE_API
+});
+
+const { queryParams, total, dataList,companyList, isAdmin, baseUrl } = toRefs(data);
+const userInfo = ref()
+onMounted(async ()=>{
+ if(userStore.roles.includes('admin')){
+ data.isAdmin = true
+ await getCompanyList()
+ }else{
+ data.isAdmin = false
+ data.queryParams.companyId = userStore.companyId
+ }
+ await getList()
+})
+
+onUnmounted(()=>{
+
+})
+
+const getList = async () => {
+ loading.value = true
+ const res = await getProductServiceList(data.queryParams)
+ if(res.code == 200){
+ data.dataList = res.data.list || []
+ data.total = res.data.total
+ }else{
+ ElMessage.warning(res.message)
+ }
+ loading.value = false
+}
+
+const getCompanyList = async ()=>{
+ const queryParams = {
+ pageNum: 1,
+ pageSize: 999
+ }
+ const res = await getCompany(queryParams)
+ if (res.code == 200) {
+ data.companyList = res.data.list?res.data.list:[]
+ // data.queryParams.companyId = data.companyList[0].id
+ } else {
+ ElMessage.warning(res.message)
+ }
+}
+
+const openDialog = (type, value) => {
+ dialogRef.value.openDialog(type, value, data.queryParams.companyId, data.isAdmin, data.companyList);
+}
+
+/** 重置新增的表单以及其他数据 */
+const reset= async()=> {
+ data.queryParams = {
+ pageNum: 1,
+ pageSize: 10,
+ companyId: null
+ }
+ await getCompanyList()
+ await getList()
+}
+const handleDelete = (val) => {
+ ElMessageBox.confirm(
+ '确定删除此条数据?',
+ '提示',
+ {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning',
+ })
+ .then( async() => {
+ const res = await delProductService({id: val.id})
+ if(res.code == 200){
+ ElMessage.success('数据删除成功')
+ await getList()
+ }else{
+ ElMessage.warning(res.message)
+ }
+ })
+}
+
+</script>
--
Gitblit v1.9.2