This commit is contained in:
wyuu 2026-07-16 18:08:54 +08:00
parent 23b6d65f0c
commit 2f325c122c
14 changed files with 266 additions and 301 deletions

View File

@ -410,6 +410,7 @@ aside {
line-height: 30px;
padding: 0 !important;
background-image: none !important;
color: #000;
}
.vxe-footer--column {
@ -430,8 +431,6 @@ aside {
border-top: 1px solid #ccc !important;
}
.vxe-header--row th {
height: 30px !important;
line-height: 30px;

View File

@ -188,7 +188,7 @@
.el-table__body td {
font-size: .8125rem;
color: #000;
}
@ -376,6 +376,7 @@
.col--ellipsis {
height: 1.875rem !important;
line-height: 1.875rem !important;
color: #000;
}
.el-checkbox {
@ -398,17 +399,17 @@
}
// 滚动条
.mytable-style ::-webkit-scrollbar {
.vxe-table ::-webkit-scrollbar {
width: .6rem;
height: .5rem;
}
.mytable-style ::-webkit-scrollbar-thumb {
.vxe-table ::-webkit-scrollbar-thumb {
background-color: #817e7e;
cursor: pointer;
}
.mytable-style ::-webkit-scrollbar-thumb:hover {
.vxe-table ::-webkit-scrollbar-thumb:hover {
background-color: #817e7e;
cursor: pointer;
}

View File

@ -0,0 +1,109 @@
<template>
<div v-if="visible" class="print-mask">
<div class="mask-card">
<div class="card-header">
<div class="title">打印报告进度</div>
<el-button icon="Close" circle size="small" v-if="progress == 100" @click="closeMask" />
</div>
<div class="tip-text">{{ ybh }} {{ desc }}</div>
<el-progress :percentage="progress" :stroke-width="15" striped />
</div>
</div>
</template>
<script setup lang="ts">
import { classCom } from '@/utils/classCom';
const props = defineProps<{
// websocket原始data字符串
wsData?: string
}>()
const visible = ref(false)
const ybh = ref('')
const desc = ref('')
const progress = ref(0)
const succCount = ref(0)
const failCount = ref(0)
const errorMsg = ref('')
// 解析ws数据
const parseWsData = (dataStr: string) => {
try {
const data = JSON.parse(dataStr)
ybh.value = data.ybh
desc.value = data.desc
progress.value = data.progress
succCount.value = data.succCount
failCount.value = data.failCount
errorMsg.value = data.errorMsg
visible.value = true
// 100%完成后打印PDF
if (data.progress == 100) {
classCom.printBase64PDF(data.pdfBase64)
}
} catch (err) {
console.error('打印ws数据解析失败', err)
}
}
watch(
() => props.wsData,
(val) => {
if (val) parseWsData(val)
},
{ immediate: true }
)
// 对外暴露手动关闭方法
const closeMask = () => {
visible.value = false
}
defineExpose({ closeMask })
</script>
<style scoped lang="scss">
.print-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.mask-card {
background: #fff;
width: 460px;
padding: 24px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
// 标题行布局
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16;
.title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
}
}
.tip-text {
color: #4e5969;
margin-bottom: 20px;
line-height: 1.7;
font-size: 14px;
margin-top: 20px;
}
.finish-text {
margin-top: 20px;
text-align: center;
font-size: 15px;
color: #00b42a;
font-weight: 500;
}
}
}
</style>

View File

@ -40,7 +40,7 @@
:row-config="{ isHover: true, keyField: 'value', height: 30, isCurrent: true }" :data="filterData"
:current-row="currentRow" @cell-click="currentChange"
:tooltip-config="{ appendToBody: true, zIndex: 3000 }" :show-overflow="true"
:scroll-y="{ enabled: true, rSize: 30, height: '100%' }">
:scroll-y="{ enabled: true, rSize: 30, height: '100%', oSize: 20, gt: 30, }">
<vxe-table-column v-for="field in props.fields" :field="field.prop" :title="field.label" align="center"
:width="field.width" min-width="150" />
</vxe-table>

View File

@ -1,10 +1,10 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar @setLayout="setLayout" />
<navbar @setLayout="setLayout" />
<tags-view v-if="needTagsView" />
</div>
<app-main />
@ -21,7 +21,10 @@ import defaultSettings from '@/settings'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import { initWebSocket } from '@/utils/webSocket'
import { useCommonStore } from '@/store/modules/commonStore'
const mbStore = useCommonStore();
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
@ -60,11 +63,17 @@ const settingRef = ref(null);
function setLayout() {
settingRef.value.openSetting();
}
onMounted(() => {
// 初始化socket
const url = `ws://47.97.125.165:8904/ws/msgserver/${mbStore.fingerprint}`
initWebSocket({ fullUrl: url })
})
</script>
<style lang="scss" scoped>
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
.app-wrapper {
@include clearfix;

View File

@ -10,6 +10,9 @@ import {
removeToken
} from '@/utils/auth'
import defAva from '@/assets/images/profile.jpg'
import {
closeWebSocket
} from '@/utils/webSocket'
const useUserStore = defineStore(
'user', {
@ -96,6 +99,7 @@ const useUserStore = defineStore(
this.roles = []
this.permissions = []
removeToken()
closeWebSocket()
resolve()
}).catch(error => {
reject(error)

View File

@ -1,10 +1,7 @@
// WebSocket连接配置
type WebSocketOptions = {
import emitter from '@/utils/mitt';
type WebSocketInternalOptions = {
fullUrl: string;
onMessage?: (data: string) => void;
onOpen?: () => void;
onError?: (error: Event) => void;
onClose?: () => void;
reconnectInterval?: number;
};
@ -12,74 +9,71 @@ let websocket: WebSocket | null = null;
let reconnectTimer: any = null;
let isManualClose = false;
// 初始化WebSocket连接
export function initWebSocket(options: WebSocketOptions) {
const {
fullUrl,
onMessage,
onOpen,
onError,
onClose,
reconnectInterval = 3000
} = options;
/** 初始化WebSocket */
export function initWebSocket(options: WebSocketInternalOptions) {
const { fullUrl, reconnectInterval = 3000 } = options;
// 关闭旧连接
// 关闭已有连接
if (websocket) {
websocket.close();
}
// 检查浏览器支持性
if (!('WebSocket' in window)) {
console.error('当前浏览器不支持WebSocket');
return;
}
// 创建WebSocket连接
try {
websocket = new WebSocket(fullUrl);
// 连接成功回调
// 连接成功
websocket.onopen = () => {
clearTimeout(reconnectTimer!);
isManualClose = false;
onOpen?.();
console.log('WebSocket 连接成功');
};
// 接收消息回调
// 接收消息,自动解析按 msgType 分发事件
websocket.onmessage = (event) => {
onMessage?.(event.data);
try {
const rawMsg = JSON.parse(event.data);
const msgType = rawMsg.msgType;
emitter.emit(msgType, rawMsg);
} catch (err) {
console.error('WebSocket消息解析失败:', err);
}
};
// 错误回调
// 连接异常
websocket.onerror = (error) => {
onError?.(error);
console.error('WebSocket 连接异常:', error);
startReconnect(() => initWebSocket(options), reconnectInterval);
};
// 关闭回调
// 连接关闭
websocket.onclose = () => {
onClose?.();
console.log('WebSocket 连接关闭');
if (!isManualClose) {
startReconnect(() => initWebSocket(options), reconnectInterval);
}
};
} catch (error) {
console.error('WebSocket连接创建失败:', error);
console.error('WebSocket 创建失败:', error);
startReconnect(() => initWebSocket(options), reconnectInterval);
}
}
// 发送消息
/** 发送ws消息 */
export function sendWebSocketMessage(message: any): boolean {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify(message));
return true;
}
console.error('WebSocket未连接,无法发送消息');
console.error('WebSocket未连接,发送失败');
return false;
}
// 手动关闭WebSocket
/** 手动关闭ws(登出时调用) */
export function closeWebSocket(): void {
isManualClose = true;
if (reconnectTimer) {
@ -89,15 +83,15 @@ export function closeWebSocket(): void {
websocket = null;
}
// 启动重连
/** 获取当前连接状态 */
export function getWebSocketState(): number | null {
return websocket?.readyState || null;
}
// 内部重连私有方法
function startReconnect(connectCallback: () => void, interval: number): void {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
reconnectTimer = setTimeout(connectCallback, interval);
}
// 获取当前连接状态
export function getWebSocketState(): number | null {
return websocket?.readyState || null;
}

View File

@ -131,7 +131,6 @@ import { listitemclass } from "@/api/regional/dict/itemclass";
import { classCom } from '@/utils/classCom';
import { useGroupTableData } from '@/utils/groupHelper'
const { proxy } = getCurrentInstance();
// import { initWebSocket, sendWebSocketMessage, closeWebSocket, getWebSocketState } from '@/utils/webSocket';
const aotuSize = classCom.useAutoSize();
const labpat = ref({

View File

@ -70,7 +70,7 @@
</el-row>
<div class="bottom-card mt10">
<el-table :data="tableData" border style="width: 100%" height="100%" v-loading="loading">
<el-table :data="tableData" border style="width: 100%" height="100%" v-loading="loading" show-overflow-tooltip>
<el-table-column label="样本号" prop="ybh" width="120" align="center" />
<el-table-column label="病人姓名" prop="brxm" width="120" align="center" />
<el-table-column label="病人类型" prop="brly" width="120" align="center">
@ -92,6 +92,9 @@
</el-table-column>
</el-table>
</div>
<!-- 打印进度遮罩 -->
<PrintReportMask ref="printMaskRef" :ws-data="printWsData" />
</div>
</template>
@ -103,6 +106,8 @@ import { queryBatchPrintService, batchPrintAll } from '@/api/liswork/batch/index
import { getDictData, formatDict, dictData } from '@/hooks'
import { useCommonStore } from '@/store/modules/commonStore';
import { ElMessage, ElNotification } from 'element-plus';
import PrintReportMask from '@/components/PrintReportMask/index.vue'
import emitter from '@/utils/mitt';
import { classCom } from '@/utils/classCom';
const mbStore = useCommonStore();
@ -126,6 +131,8 @@ const queryForm = reactive({
jgbz2: 1,
});
const printWsData = ref('');
const printLoading = ref(false);
const loading = ref(false);
@ -171,10 +178,8 @@ const printHandle = () => {
printLoading.value = true
batchPrintAll(queryForm).then((res) => {
printLoading.value = false
if (res.code == 0) {
classCom.printBase64PDF(res.data.base64data)
tableData.value = res.data.printflag
}
}).finally(() => {
printLoading.value = false
})
}
@ -198,6 +203,18 @@ const getYqConfig = () => {
})
}
// 监听打印消息处理函数
const handlePrintMsg = (msg) => {
console.log('打印进度消息', msg)
const info = JSON.parse(msg.data)
// 匹配表格,更新cp_msg
const matchrow: any = tableData.value.find((item: any) => item.ybh == info.ybh)
if (matchrow) {
matchrow.cp_msg = info.errorMsg
}
// 传递给进度弹窗
printWsData.value = msg.data
}
onMounted(() => {
getComDicts({ zdlb: 'GROUP' }).then((res: any) => {
@ -210,8 +227,13 @@ onMounted(() => {
getDictData('PT', 'DP')
});
// 监听打印消息
emitter.on('printReport', handlePrintMsg)
});
onBeforeUnmount(() => {
emitter.off('printReport', handlePrintMsg)
})
</script>

View File

@ -185,7 +185,6 @@ import PatientQueryForm from './components/searchListFrom.vue'
import { listUser } from '@/api/system/user.js'
import dayjs from 'dayjs'
const aotuSize = classCom.useAutoSize();
import { initWebSocket, sendWebSocketMessage, closeWebSocket, getWebSocketState } from '@/utils/webSocket';
const previewShow = ref(false);
const lbFlag = ref(false);
@ -413,7 +412,6 @@ const yqHandleChange = () => {
mbStore.setLxsrList([]);
getSysList()
fetchlabPatList()
sendMessage()
}
const instrGroupFields = ref([
{ prop: 'zddh', label: '代号', width: 80, enablePinyinSearch: true },
@ -791,7 +789,6 @@ const getParams = () => {
getSysList()
getYqConfig()
fetchlabPatList()
sendMessage()
// 读取后清空 state 刷新/回退不重复读取
history.replaceState({}, document.title);
}
@ -827,43 +824,15 @@ onActivated(() => {
getParams()
})
const initSocket = () => {
initWebSocket({
fullUrl: `ws://47.97.125.165:8904/ws/instr/${mbStore.fingerprint}`,
// fullUrl: `ws://192.168.1.151:9801/ws/instr/${mbStore.fingerprint}`,
onOpen: () => {
console.log('WebSocket连接成功');
},
onMessage: (data) => {
console.log(`收到消息: `, JSON.parse(data));
},
onError: (error) => {
console.log(`连接错误: ${error.type}`);
},
onClose: () => {
console.log('WebSocket连接已关闭');
},
reconnectInterval: 3000 // 重连间隔(毫秒)
});
}
// 页面卸载时清理
onUnmounted(() => {
// closeWebSocket();
});
// 发送消息
const sendMessage = () => {
return
const isSuccess = sendWebSocketMessage({ yq: queryParams.value.yq });
if (isSuccess) {
console.log(`发送消息`);
} else {
console.log('发送失败:连接未建立');
}
};
</script>
<style scoped lang="scss">

View File

@ -152,7 +152,6 @@ import BatchCode from './components/batchCode.vue';
import { listUser } from '@/api/system/user.js'
const mbStore = useCommonStore();
const aotuSize = classCom.useAutoSize();
import { initWebSocket, sendWebSocketMessage, closeWebSocket, getWebSocketState } from '@/utils/webSocket';
import dayjs from 'dayjs';
const previewShow = ref(false);
const lbFlag = ref(false);
@ -311,7 +310,6 @@ const yqHandleChange = () => {
mbStore.setLxsrList([]);
getSysList()
fetchlabPatList()
sendMessage()
}
const instrGroupFields = ref([
{ prop: 'zddh', label: '代号', width: 80, enablePinyinSearch: true },
@ -691,7 +689,6 @@ const getParams = () => {
getSysList()
getYqConfig()
fetchlabPatList()
sendMessage()
// 读取后清空 state 刷新/回退不重复读取
history.replaceState({}, document.title);
}
@ -716,50 +713,21 @@ onMounted(async () => {
HOS: toRaw(dictRefs.HOS.value) || [],
ST: toRaw(dictRefs.ST.value) || [],
};
// 初始化Socket
// initSocket()
});
onActivated(() => {
getParams()
})
const initSocket = () => {
initWebSocket({
fullUrl: `ws://47.97.125.165:8904/ws/instr/${mbStore.fingerprint}`,
onOpen: () => {
console.log('WebSocket连接成功');
},
onMessage: (data) => {
console.log(`收到消息: `, JSON.parse(data));
},
onError: (error) => {
console.log(`连接错误: ${error.type}`);
},
onClose: () => {
console.log('WebSocket连接已关闭');
},
reconnectInterval: 3000 // 重连间隔(毫秒)
});
}
// 页面卸载时清理
onUnmounted(() => {
// closeWebSocket();
});
// 发送消息
const sendMessage = () => {
return
const isSuccess = sendWebSocketMessage({ yq: queryParams.value.yq });
if (isSuccess) {
console.log(`发送消息`);
} else {
console.log('发送失败:连接未建立');
}
};
</script>
<style scoped lang="scss">

View File

@ -55,7 +55,7 @@
<el-table-column :label="`${menuname}编号`" align="center" prop="paramId" v-if="false" />
<el-table-column :label="`${menuname}类别`" align="center" prop="paramType" width="100" show-overflow-tooltip>
<template #default="scope">
<span class="stats-type-cell" :style="{
<span class="stats-type-cell" :style="{
backgroundColor: getparamTypeColor(scope.row.paramType),
color: '#fff'
}">
@ -124,19 +124,10 @@
<el-input v-model="form.paramName" :placeholder="`请输入${menuname}名称`" />
</el-form-item>
<el-form-item :label="`${menuname}默认值`" prop="paramDefvalue">
<el-autocomplete
v-model="form.paramDefvalue"
:fetch-suggestions="queryDefaultSuggestions"
placeholder="请输入/选择${menuname}默认值"
clearable
style="width: 100%"
>
<!-- 下拉选项自定义展示 -->
<template #default="{ item }">
<span>{{ item.label }}</span>
<span style="color:#999; margin-left:10px;">{{ item.value }}</span>
</template>
</el-autocomplete>
<el-select v-model="form.paramDefvalue" :placeholder="`请选择${menuname}默认值`" style="width: 100%" clearable>
<el-option v-for="item in defaultOptions" :key="item.value" :label="item.label + ' ' + item.value"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="`${menuname}框体类型`" prop="paramOptiontype">
<el-select v-model="form.paramOptiontype" :placeholder="`请选择${menuname}筛选类型`" style="width: 100%">
@ -225,13 +216,13 @@ const copyTemplateOptions = ref([])
// 报表类别选项
const paramTypeOptions = ref([
{ label: "统计条件", value: "0" ,color: "#3282F6"},
{ label: "显示列", value: "1" , color: "#67C23A"},
{ label: "副标题", value: "2" , color: "#FF443F"},
{ label: "汇总栏", value: "3" , color: "#FF443F"},
{ label: "子查询", value: "4" , color: "#FF443F"},
{ label: "逻辑条件", value: "5" , color: "#909399"},
{ label: "计算列", value: "6" , color: "#C2C024" }
{ label: "统计条件", value: "0", color: "#3282F6" },
{ label: "显示列", value: "1", color: "#67C23A" },
{ label: "副标题", value: "2", color: "#FF443F" },
{ label: "汇总栏", value: "3", color: "#FF443F" },
{ label: "子查询", value: "4", color: "#FF443F" },
{ label: "逻辑条件", value: "5", color: "#909399" },
{ label: "计算列", value: "6", color: "#C2C024" }
]);
@ -252,7 +243,7 @@ const defaultOptions = ref([
const queryDefaultSuggestions = (queryString, callback) => {
// 根据输入文本过滤预设选项,无输入则展示全部
const suggestions = defaultOptions.value.filter(item =>
item.label.includes(queryString) || item.value.includes(queryString)
item.label.includes(queryString) || item.value.includes(queryString)
);
callback(suggestions);
};
@ -567,21 +558,14 @@ function getTypes(statsCode0) {
.compact-form {
--el-form-item-margin-bottom: 8px;
line-height: 1.4;
/* line-height: 1.4; */
.el-form-item {
margin-bottom: 8px;
}
}
:deep(.compact-form .el-form-item__label) {
padding: 0 8px 0 0;
line-height: 1.4;
}
:deep(.compact-form .el-form-item__content) {
line-height: 1.4;
}
/* 对话框底部按钮区 */
:deep(.compact-dialog .dialog-footer) {
@ -593,6 +577,7 @@ function getTypes(statsCode0) {
:deep(.compact-form .el-form-item:last-of-type .el-textarea__inner) {
min-height: 60px;
}
.stats-type-cell {
width: 100%;
height: 100%;

View File

@ -92,9 +92,9 @@ import { ref, watch, getCurrentInstance, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getCodeImg, getInfo, getLocalconfig, login } from "@/api/login";
import Cookies from "js-cookie";
import { useFingerprint } from '@/utils/useFingerprint';
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from '@/store/modules/user';
import { useCommonStore } from '@/store/modules/commonStore'
const mbStore = useCommonStore();
// 初始化响应式变量
@ -208,8 +208,8 @@ const getCookie = () => {
};
// 获取本地配置
const Localconfig = () => {
getLocalconfig().then(res => {
const Localconfig = (localid) => {
getLocalconfig({ localid }).then(res => {
if (res) {
const { localconfig, hosplist, oem: oemData } = res;
mbStore.setDefaultConfig(localconfig)
@ -222,6 +222,19 @@ const Localconfig = () => {
});
};
// 获取指纹并加载配置
const { getFingerprint } = useFingerprint();
const refreshFingerprint = async () => {
try {
const res = await getFingerprint();
loginForm.value.loginParam.localid = res.visitorId
Localconfig(res.visitorId);
} catch (err) {
console.error("获取指纹失败:", err);
Localconfig("default"); // 兜底使用默认ID
}
};
// Tab切换事件(移除activeName手动赋值,避免递归)
const handleClick = (tab) => {
@ -233,7 +246,7 @@ const handleClick = (tab) => {
};
Localconfig();
refreshFingerprint();
getCode();
</script>

View File

@ -57,6 +57,10 @@
<el-time-picker v-model="item.paramDefvalue" value-format="HH:mm:ss" format="HH:mm:ss"
style="width:100px" />
</template>
<template v-if="item.paramOptiontype == '9'">
<el-time-picker v-model="item.paramDefvalue" type="datetime" value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss" style="width:100px" />
</template>
</el-form-item>
</template>
</div>
@ -86,46 +90,22 @@
<div class="table-container bb_table" :style="{ height: `calc(100% - ${statsTitle ? '70px' : '15px'})` }">
<vxe-table :data="tableData" border v-loading="loading" height="100%"
:tooltip-config="{ appendToBody: true, zIndex: 3000 }" show-footer :footer-method="getSummaries"
:scroll-y="{ enabled: true, rSize: 30, height: '100%', }" show-footer-overflow>
<vxe-column v-for="col in tableColumns" :key="col.prop" :title="col.label" :field="col.prop" width="120"
show-overflow align="center">
:scroll-y="{ enabled: true, rSize: 30, height: '100%', oSize: 20, gt: 30, }" show-footer-overflow>
<vxe-column v-for="col in tableColumns" :key="col.prop" :title="col.label" :field="col.prop"
:width="col.paramWidth" show-overflow align="center">
<template #default="scope">
{{ col.dictType ? formatDict(scope.row[col.prop], col.dictType) : scope.row[col.prop] }}
<template v-if="col.paramOptiontype == 0">
<el-checkbox :model-value="scope.row[col.prop]" true-value="1" false-value="0" />
</template>
<template v-else>
{{ col.dictType ? formatDict(scope.row[col.prop], col.dictType) : scope.row[col.prop] }}
</template>
</template>
</vxe-column>
</vxe-table>
</div>
</div>
</div>
<!-- 视觉移出屏幕,页面看不见,但DOM完整存在用于iframe打印 -->
<div class="print-only-wrap" id="printWrap">
<div class="title" v-if="!statsTitle">{{ formatDict(userStore.sysLoginParam.loginYLJG, 'HOS') }}</div>
<div class="content">{{ statsTitle }}</div>
<!-- 原生完整table,无滚动、无虚拟DOM,所有数据全部渲染 -->
<table class="print-table" border="1" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th v-for="col in tableColumns" :key="col.prop">{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIdx) in tableData" :key="rowIdx">
<td v-for="col in tableColumns" :key="col.prop" align="center">
{{ col.dictType ? formatDict(row[col.prop], col.dictType) : row[col.prop] }}
</td>
</tr>
</tbody>
<!-- 合计行,和vxe合计逻辑完全一致 -->
<tfoot>
<tr>
<td v-for="(col, colIdx) in tableColumns" :key="col.prop" align="center">
{{ getPrintFooterText(col, colIdx) }}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</template>
@ -210,29 +190,44 @@ const getSummaries = ({ columns, data }) => {
return [footerData];
};
// 记录统计项目勾选先后顺序,存储 paramCode
const checkOrder = ref([])
// 处理checkbox选中
const handleCheckBoxChange = (val) => {
// val:当前所有已勾选 paramCode 数组
// 第一步:维护勾选顺序数组 checkOrder
// 1. 过滤掉已取消勾选的项
checkOrder.value = checkOrder.value.filter(code => val.includes(code))
// 2. 新增本次刚勾选、不在顺序数组里的项,追加到末尾(记录点击先后)
val.forEach(code => {
if (!checkOrder.value.includes(code)) {
checkOrder.value.push(code)
}
})
// 基础列:保留,不随勾选删除
const baseCols = tableColumns.value.filter(col => {
// 判断是否是统计项目复选框对应的列:checkBoxs里存在该paramCode就是可选列
const isCheckItem = checkBoxs.value.some(item => item.paramCode === col.prop)
return !isCheckItem
})
// 2. 获取当前勾选的所有可选列配置
const selectedCheckCols = checkBoxs.value
.filter(item => val.includes(item.paramCode))
// 2. 按【用户勾选先后顺序】生成选中列
const selectedCheckCols = checkOrder.value
.map(code => checkBoxs.value.find(item => item.paramCode === code))
.filter(Boolean) // 过滤找不到的脏数据
.map(item => ({
key: item.paramCode,
label: item.paramName,
prop: item.paramCode,
dictType: item.remark,
paramSequence: 0
paramSequence: 0,
paramWidth: item.paramWidth,
paramOptiontype: item.paramOptiontype
}))
// 3. 合并数组:勾选项放最前面 + 原有基础列在后
// 3. 合并数组:按勾选顺序的勾选项放最前面 + 原有基础列在后
tableColumns.value = [...selectedCheckCols, ...baseCols]
let seq = 1
tableColumns.value.forEach((col, idx) => {
if (col.paramSequence === 0) {
@ -240,7 +235,6 @@ const handleCheckBoxChange = (val) => {
}
})
};
// 查询
const search = () => {
originalColumn.value.forEach(item => {
@ -279,95 +273,7 @@ const getPrintFooterText = (col, idx) => {
// iframe打印核心方法
const print = () => {
return;
// 获取隐藏打印容器完整HTML
const printDom = document.getElementById('printWrap')
const printHtml = printDom.outerHTML
// 打印页面内置样式(统一红边框、行高、打印A4横向、合计加粗)
const printStyle = `
<style>
@page {
size: A4 landscape;
margin: 8mm;
}
html,body {
padding: 0;
margin: 0;
}
.title {
font-size: 20px;
text-align: center;
font-weight: 600;
margin-bottom: 6px;
}
.content {
font-size: 16px;
text-align: center;
margin-bottom: 12px;
font-weight: 600;
}
.print-table {
width: 100%;
border-collapse: collapse;
}
.print-table th,
.print-table td {
border: 1px solid #ccc;
height: 30px;
line-height: 30px;
font-size: 12px;
padding: 2px 4px;
}
.print-table tfoot td {
font-size: 13px;
font-weight: 700;
}
tr {
page-break-inside: avoid;
}
/* 打印渲染完整颜色边框 */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
</style>
`
// 1. 创建隐藏iframe
const iframe = document.createElement('iframe')
iframe.style.width = '0px'
iframe.style.height = '0px'
iframe.style.position = 'absolute'
iframe.style.left = '-9999px'
document.body.appendChild(iframe)
// 2. 写入完整打印页面内容
const iframeDoc = iframe.contentDocument
iframeDoc.open()
iframeDoc.write(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>报表打印</title>
${printStyle}
</head>
<body>
${printHtml}
</body>
</html>
`)
iframeDoc.close()
// 3. 等待DOM渲染完成后打印
setTimeout(() => {
iframe.contentWindow.print()
// 打印弹窗关闭后销毁iframe
setTimeout(() => {
document.body.removeChild(iframe)
}, 1000)
}, 600)
// classCom.printBase64PDF()
}
@ -386,6 +292,8 @@ const getParams = () => {
prop: item.paramCode,
dictType: item.remark,
paramSequence: item.paramSequence,
paramWidth: item.paramWidth,
paramOptiontype: item.paramOptiontype,
}
}) //显示列 (item.paramType == '1' || item.paramType == '6') && item.paramSequence > 0
@ -416,22 +324,6 @@ onMounted(() => {
})
</script>
<style scoped lang="scss">
@media print {
.no-print {
display: none !important;
}
}
// 打印专用容器:屏幕视觉隐藏,DOM保留
.print-only-wrap {
position: absolute;
left: -9999px;
top: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
@ -525,6 +417,7 @@ onMounted(() => {
.table-container {
padding: 10px;
min-height: 0;
}
.tips {