<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-button type="primary" @click="addFile" v-hasPermi="['qualityManage2:list:add']">生成质量手册</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="fileName" align="center">
|
<template #default="scope">
|
<el-button link type="primary" @click="initFile(scope.row)">
|
{{ scope.row.companyName + scope.row.qualityName + '.docx' }}
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
<template #default="scope">
|
<el-button link type="primary" @click="initFile(scope.row)" v-hasPermi="['qualityManage2:list:edit']">下载
|
</el-button>
|
<el-button link type="primary" @click="openDialog('add',scope.row)" v-hasPermi="['qualityManage2:list:add']">
|
上传
|
</el-button>
|
<el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['qualityManage2:list:del']">删除
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="orgTreeBox" id="org-tree-container">
|
<vue3-tree-org
|
:data="deptTree"
|
:horizontal="false"
|
:props="treeProps"
|
:toolBar="tools"
|
:label-style="labelStyle"
|
center
|
/>
|
</div>
|
<pagination
|
v-show="total > 0"
|
:total="total"
|
v-model:page="queryParams.pageNum"
|
v-model:limit="queryParams.pageSize"
|
@pagination="getList"
|
/>
|
<edit-dialog ref="dialogRef" @getList=getList></edit-dialog>
|
<el-dialog
|
v-model="dialogVisible"
|
width="750px"
|
:before-close="handleClose"
|
:close-on-press-escape="false"
|
:close-on-click-modal="false"
|
>
|
<el-form :model="dialogForm" size="default" ref="formRef" :rules="formRules" label-width="150px">
|
<el-form-item label="编号:" prop="number">
|
<el-input v-model.trim="dialogForm.number" placeholder="手册编号"></el-input>
|
</el-form-item>
|
<el-form-item label="版本号:" prop="versionNum">
|
<el-input v-model.trim="dialogForm.versionNum" placeholder="版本号"></el-input>
|
</el-form-item>
|
<el-form-item label="文件状态:" prop="fileStatus">
|
<el-input v-model.trim="dialogForm.fileStatus" placeholder="文件状态"></el-input>
|
</el-form-item>
|
<el-form-item label="发放号:" prop="grantNum">
|
<el-input v-model.trim="dialogForm.grantNum" placeholder="发放号"></el-input>
|
</el-form-item>
|
<el-form-item label="编制:" prop="fictionName">
|
<el-select clearable v-model="dialogForm.fictionName" filterable placeholder="编制" style="width: 100%">
|
<el-option
|
v-for="item in userList"
|
:key="item.name"
|
:label="item.name"
|
:value="item.name"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="审核:" prop="checkName">
|
<el-select clearable v-model="dialogForm.checkName" filterable placeholder="审核" style="width: 100%">
|
<el-option
|
v-for="item in userList"
|
:key="item.name"
|
:label="item.name"
|
:value="item.name"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="批准:" prop="ratifyName">
|
<el-select clearable v-model="dialogForm.ratifyName" filterable placeholder="批准" style="width: 100%">
|
<el-option
|
v-for="item in userList"
|
:key="item.name"
|
:label="item.name"
|
:value="item.name"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="发布日期:" prop="releaseDate">
|
<el-date-picker
|
v-model="dialogForm.releaseDate"
|
type="date"
|
placeholder="请选择日期"
|
value-format="YYYY-MM-DD"
|
/>
|
</el-form-item>
|
<el-form-item label="实施日期:" prop="executionDate">
|
<el-date-picker
|
v-model="dialogForm.executionDate"
|
type="date"
|
placeholder="请选择日期"
|
value-format="YYYY-MM-DD"
|
/>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleClose" size="default">取 消</el-button>
|
<el-button type="primary" @click="onSubmitAdd" size="default" v-preReClick>确认</el-button>
|
</div>
|
</template>
|
</el-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 {generateWordDocument} from './components/exportDoc.js'
|
import editDialog from './components/editDialog.vue'
|
import html2canvas from 'html2canvas'
|
import {saveAs} from 'file-saver'
|
import useUserStore from "@/store/modules/user";
|
import {
|
addStandardQuality,
|
delStandardQuality,
|
getStandardDetail,
|
getStandardQuality
|
} from "@/api/standardSys/standardSys";
|
import {getDepart, getDistribution, getFunctionalRemarkList, getSysClause} from "@/api/orgStructure/depart";
|
import {getEmployeeRecords} from "@/api/onlineEducation/user";
|
|
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: [],
|
deptList: {
|
id: 0,
|
deptName: "",
|
children: []
|
},
|
treeProps: {
|
label: 'label'
|
},
|
tools: {
|
scale: false, restore: false, expand: false, zoom: false, fullscreen: false
|
},
|
labelStyle: {
|
border: '1px solid #ccc',
|
background: 'rgba(0,0,0,0)'
|
},
|
companyList: [],
|
isAdmin: false,
|
companyInfo: {},
|
caluseList: [],
|
originDeptList: [],
|
dialogVisible: false,
|
dialogForm: {
|
qualityName: '质量手册',
|
companyId: null,
|
number: '',
|
versionNum: '',
|
fileStatus: '',
|
grantNum: '',
|
fictionName: '',
|
checkName: '',
|
ratifyName: '',
|
releaseDate: '',
|
executionDate: ''
|
},
|
formRules: {},
|
userList: [],
|
deptTree: {
|
id: 0,
|
label: "",
|
children: []
|
}
|
});
|
|
const {
|
queryParams,
|
total,
|
dataList,
|
deptList,
|
treeProps,
|
tools,
|
labelStyle,
|
companyList,
|
isAdmin,
|
companyInfo,
|
caluseList,
|
dialogVisible,
|
dialogForm,
|
formRules,
|
userList,
|
deptTree
|
} = toRefs(data);
|
const userInfo = ref()
|
const formRef = ref()
|
onMounted(async () => {
|
await getSysClauseList()
|
if (userStore.roles.includes('admin')) {
|
data.isAdmin = true
|
await getCompanyList()
|
} else {
|
data.isAdmin = false
|
data.queryParams.companyId = userStore.companyId
|
}
|
await getList()
|
})
|
|
onUnmounted(() => {
|
|
})
|
const handleClose = () => {
|
data.dialogForm = {
|
qualityName: '质量手册',
|
companyId: null,
|
number: '',
|
versionNum: '',
|
fileStatus: '',
|
grantNum: '',
|
fictionName: '',
|
checkName: '',
|
ratifyName: '',
|
releaseDate: '',
|
executionDate: ''
|
}
|
data.dialogVisible = false
|
}
|
|
const onSubmitAdd = async () => {
|
const valid = await formRef.value.validate()
|
if (valid) {
|
const res = await addStandardQuality(data.dialogForm)
|
if (res.code == 200) {
|
ElMessage.success(res.message)
|
await handleClose()
|
await reset()
|
} else {
|
ElMessage.warning(res.message)
|
}
|
}
|
}
|
|
const getSysClauseList = async () => {
|
const res = await getSysClause()
|
if (res.code == 200) {
|
data.caluseList = res.data.map(i => {
|
return {
|
clauseNum: i.clauseNum,
|
content: i.name
|
}
|
})
|
} else {
|
ElMessage.warning(res.message)
|
}
|
}
|
const getUserList = async (companyId)=> {
|
const res = await getEmployeeRecords({companyId: companyId})
|
if(res.code == 200){
|
data.userList = res.data ? res.data :[]
|
}else{
|
ElMessage.warning(res.message)
|
}
|
}
|
const getList = async () => {
|
loading.value = true
|
const res = await getStandardQuality(data.queryParams)
|
if (res.code == 200) {
|
data.dataList = res.data.data || []
|
data.total = res.data.total
|
} else {
|
ElMessage.warning(res.message)
|
}
|
loading.value = false
|
}
|
|
const getDeptData = async (val) => {
|
await getDeptList(val)
|
const res = await getDistribution({companyId: val.companyId})
|
if (res.code == 200) {
|
for (let item of data.caluseList) {
|
const sameNum = data.companyInfo.duties.filter(i => i.clauseNum == item.clauseNum).map(j => {
|
return {
|
deptId: j.deptId,
|
chooseLab: j.chooseLab
|
}
|
}
|
)
|
for (let i of sameNum) {
|
item[i.deptId] = i.chooseLab ? (i.chooseLab == 1 ? '●' : '○') : '○'
|
}
|
}
|
} else {
|
ElMessage.warning(res.message)
|
}
|
}
|
const getDeptList = async (val) => {
|
const res = await getDepart({responsType: 1, companyId: val.companyId})
|
if (res.code == 200) {
|
data.originDeptList = JSON.parse(JSON.stringify(res.data))
|
data.deptList.deptName = val.companyName
|
data.deptTree.label = val.companyName
|
data.deptList.children = proxy.handleTree(res.data, "deptId")
|
} else {
|
ElMessage.warning(res.message)
|
}
|
}
|
const addFile = async () => {
|
if (data.queryParams.companyId) {
|
data.dialogForm.companyId = data.queryParams.companyId
|
const res = await getStandardQuality(data.queryParams)
|
if (res.code == 200) {
|
const val = res.data.data[0]
|
Object.keys(data.dialogForm).forEach(key => {
|
if (key in val) {
|
data.dialogForm[key] = val[key]
|
}
|
})
|
} else {
|
ElMessage.warning(res.message)
|
}
|
await getUserList(data.queryParams.companyId)
|
data.dialogVisible = true
|
} else {
|
ElMessage.warning('请先选择对应的企业')
|
return
|
}
|
}
|
|
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 getInfo = async (val) => {
|
const res = await getStandardDetail({companyId: val.companyId});
|
if (res.code === 200) {
|
|
// 调试 proclaim 数据
|
if (!res.data || (res.data.companyIndustryTemplates.length == 0 && res.data.companyQualityPolicies.length == 0 && res.data.companySummaries.length == 0 && res.data.sysFunctionalDistributions
|
.length == 0 && res.data.treeSelects.length == 0)) {
|
loading.value = false
|
return Promise.reject(new Error('该企业暂无质量数据'))
|
}
|
data.deptTree.children = res.data.treeSelects || []
|
data.companyInfo.summaries = res.data.companySummaries ? res.data.companySummaries[0]?.companySummary : []
|
data.companyInfo.policies = res.data.companyQualityPolicies ? res.data.companyQualityPolicies[0]?.policy : []
|
const duties = transToTableData(res.data.sysFunctionalDistributions, data.originDeptList)
|
data.companyInfo.allDepts = duties.allDepts
|
data.companyInfo.clauses = duties.clauses
|
data.companyInfo.temps = res.data.companyIndustryTemplates?.map((item, index) => {
|
return {
|
index: index + 1,
|
templateName: item.templateName
|
}
|
}) || []
|
data.companyInfo.productServiceImages = res.data.productServiceDatas ? await processImagesToBase64(res.data.productServiceDatas) : []
|
data.companyInfo.proclaim1 = res.data.proclaim.find(i=>i.type == 1)?.content || ''
|
data.companyInfo.sign1 = await urlToBase64(res.data.proclaim.find(i=>i.type == 1)?.sign) || ''
|
data.companyInfo.updateTime1 = res.data.proclaim.find(i=>i.type == 1 && i.status == 1)?.updateTime?.substring(0,10) || ''
|
data.companyInfo.proclaim2 = res.data.proclaim.find(i=>i.type == 2)?.content || ''
|
data.companyInfo.sign2 = await urlToBase64(res.data.proclaim.find(i=>i.type == 2)?.sign) || ''
|
data.companyInfo.updateTime2 = res.data.proclaim.find(i=>i.type == 2 && i.status == 1)?.updateTime?.substring(0,10) || ''
|
data.companyInfo.proclaim3 = res.data.proclaim.find(i=>i.type == 3)?.content || ''
|
data.companyInfo.sign3 = await urlToBase64(res.data.proclaim.find(i=>i.type == 3)?.sign) || ''
|
data.companyInfo.updateTime3 = res.data.proclaim.find(i=>i.type == 3 && i.status == 1)?.updateTime?.substring(0,10) || ''
|
data.companyInfo.proclaim4 = res.data.proclaim.find(i=>i.type == 4)?.content || ''
|
data.companyInfo.sign4 = await urlToBase64(res.data.proclaim.find(i=>i.type == 4)?.sign) || ''
|
data.companyInfo.updateTime4 = res.data.proclaim.find(i=>i.type == 4 && i.status == 1)?.updateTime?.substring(0,10) || ''
|
} else {
|
ElMessage.warning(res.message)
|
}
|
}
|
|
const getRemarksList = async (companyId) => {
|
const res = await getFunctionalRemarkList({companyId: companyId})
|
if (res.code == 200) {
|
const originRemark = res.data
|
for (let item of originRemark) {
|
const foundObj = data.caluseList.find(i => i.clauseNum == item.clauseNum)
|
if (foundObj) {
|
foundObj.remark = item.remark
|
}
|
}
|
data.companyInfo.remarks = originRemark.map(i => {
|
return `${i.clauseNum}:${i.remark}`
|
}).join(';')
|
} else {
|
ElMessage.warning(res.message)
|
}
|
}
|
|
const transToTableData = (duties, deptList) => {
|
// 步骤1:获取所有唯一的部门和条款编号
|
// const allDeptNames = [...new Set(duties.map(item => item.deptName))];
|
const allDepts = deptList
|
const allClauseNums = [...new Set([
|
...data.caluseList.map(c => c.clauseNum),
|
...duties.map(d => d.clauseNum)
|
])];
|
|
// 步骤2:为每个条款生成完整的部门数据(缺失数据默认 chooseLab: 0)
|
const mergeValues = ['7.1', '8.2', '8.3', '8.4', '8.5', '9.1']
|
const processedClauses = allClauseNums.map(clauseNum => {
|
const clauseContent = data.caluseList.find(c => c.clauseNum === clauseNum)?.content || "";
|
// 为当前条款生成所有部门的数据(确保每个部门都有值)
|
const deptValues = allDepts.map(dept => {
|
const matchedDept = duties.find(
|
item => item.clauseNum === clauseNum && item.deptId === dept.deptId
|
)
|
if (dept.deptType == '0') {
|
return matchedDept ? (matchedDept.chooseLab == 1 ? '●' : '○') : '○'
|
} else {
|
return matchedDept ? (matchedDept.chooseLab == 1 ? '▲' : '○') : '○'
|
}
|
});
|
return {
|
clauseNum,
|
content: clauseContent,
|
deptValues: !mergeValues.includes(clauseNum) ? deptValues : deptValues.map(i => '')
|
}
|
})
|
// 最终数据结构
|
return {
|
clauses: processedClauses,
|
allDepts // 用于生成表头
|
};
|
}
|
|
// 新增:将图片URL转换为Base64
|
async function urlToBase64(imageUrl) {
|
if (!imageUrl || imageUrl.trim() === '') {
|
console.log('urlToBase64: 接收到空URL');
|
return '';
|
}
|
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(imagePaths) {
|
if (!imagePaths || imagePaths.length === 0) return [];
|
|
const processed = [];
|
for (const path of imagePaths) {
|
try {
|
// 如果是网络路径,先下载
|
const base64 = await urlToBase64(path);
|
processed.push(base64);
|
} catch (error) {
|
console.error('处理图片失败:', path, error);
|
processed.push(''); // 或保持原始路径
|
}
|
}
|
return processed;
|
}
|
|
const initFile = async (val) => {
|
data.companyInfo = {}
|
loading.value = true
|
if (val.filePath !== '') {
|
window.open(import.meta.env.VITE_APP_BASE_API + '/' + val.filePath)
|
} else {
|
try {
|
await getDeptList(val)
|
await getInfo(val)
|
await getRemarksList(val.companyId)
|
// 2. 等待DOM更新完成
|
await nextTick();
|
|
// 3. 捕获组织结构图图片
|
const orgChartImage = await captureElementToImage('org-tree-container');
|
data.companyInfo.orgChart = orgChartImage
|
data.companyInfo.companyName = val.companyName
|
data.companyInfo.number = val.number
|
data.companyInfo.versionNum = val.versionNum
|
data.companyInfo.fileStatus = val.fileStatus
|
data.companyInfo.grantNum = val.grantNum
|
data.companyInfo.fictionName = val.fictionName
|
data.companyInfo.checkName = val.checkName
|
data.companyInfo.ratifyName = val.ratifyName
|
data.companyInfo.releaseDate = val.releaseDate?.substring(0,10)
|
data.companyInfo.executionDate = val.executionDate?.substring(0,10)
|
const templatePath = '/qualityFile1.docx'
|
await generateWordDocument(
|
templatePath,
|
data.companyInfo,
|
val.companyName + '质量手册.docx'
|
);
|
loading.value = false
|
} catch (error) {
|
console.error('操作失败:', error);
|
ElMessage.warning(error.message || '操作失败');
|
}
|
}
|
}
|
|
const captureElementToImage = async (elementId) => {
|
const html2canvas = (await import('html2canvas')).default;
|
const element = document.getElementById(elementId);
|
if (!element) throw new Error('找不到组织结构图容器');
|
|
return await html2canvas(element, {
|
scale: 4,
|
useCORS: true,
|
allowTaint: true,
|
backgroundColor: 'rgba(0,0,0,0)' // 确保背景是白色
|
}).then(canvas => canvas.toDataURL('image/png'));
|
}
|
|
const openDialog = (type, value) => {
|
dialogRef.value.openDialog(type, value);
|
}
|
|
/** 重置新增的表单以及其他数据 */
|
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 delStandardQuality({standardizedQualityId: val.id})
|
if (res.code == 200) {
|
ElMessage.success('数据删除成功')
|
await getList()
|
} else {
|
ElMessage.warning(res.message)
|
}
|
})
|
}
|
|
</script>
|
<style lang="scss">
|
.orgTreeBox {
|
width: 1200px;
|
height: 1200px;
|
position: absolute; /* 或 fixed */
|
left: -9999px; /* 移出可视区域 */
|
pointer-events: none; /* 禁止交互 */
|
z-index: -1;
|
}
|
</style>
|