This commit is contained in:
guochunsi
2026-01-07 18:33:03 +08:00
parent bb06c81997
commit 9e3e775b0f
11 changed files with 277 additions and 155 deletions

View File

@@ -89,3 +89,4 @@ const handleCommand = (command: string) => {
} }
</script> </script>

View File

@@ -1,150 +1,85 @@
<template> <template>
<!-- 组件不占据任何布局空间所有预览组件都是 teleported --> <div style="width: 100%; height: 100%;">
<div style="display: none;"> <!-- 图片直接使用原始地址展示保持原有行为 -->
<!-- 图片直接使用 el-image-viewer 全屏预览 --> <viewer :images="[authSrc]" v-if="!showIframe">
<el-image-viewer <img
v-if="!showIframe && imageSrc && imagePreviewVisible" v-if="!showIframe"
:url-list="[imageSrc]" ref="imgRef"
:teleported="true" :width="imgWidth ? imgWidth : '100%;'"
hide-on-click-modal :height="imgHeight ? imgHeight : '100%;'"
@close="imagePreviewVisible = false" :src="authSrc"
/> />
</viewer>
<!-- PDF dialog 中显 --> <!-- PDF通过 iframe + token 的请求展 -->
<el-dialog <iframe
v-if="showIframe" v-if="showIframe"
v-model="pdfDialogVisible" ref="authIframeRef"
:title="dialogTitle || '文件预览'" style="width: 100%; height: 100%;"
append-to-body />
width="90%"
class="pdf-preview-dialog"
>
<iframe ref="authIframeRef" :style="{ width: '100%', height: pdfIframeHeight }" />
<!-- <div class="pdf-iframe-wrapper">
<iframe
ref="authIframeRef"
:style="{
width: '100%',
height: pdfIframeHeight,
border: 'none',
display: 'block'
}"
/>
</div> -->
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, nextTick, computed, onUnmounted } from 'vue'; import { ref, onMounted, nextTick } from 'vue'
import { ElImageViewer } from 'element-plus'; import { Session } from '/@/utils/storage'
import { Session } from "/@/utils/storage";
// 定义 props
const props = defineProps<{ const props = defineProps<{
authSrc: string; authSrc: string
imgWidth?: string; imgWidth?: string
imgHeight?: string; imgHeight?: string
dialogTitle?: string; }>()
}>();
// 定义响应式数据 const showIframe = ref(false)
const showIframe = ref(false); const authIframeRef = ref<HTMLIFrameElement | null>(null)
const imageSrc = ref<string>(''); const imgRef = ref<HTMLImageElement | null>(null)
const imagePreviewVisible = ref(false);
const pdfDialogVisible = ref(false);
const authIframeRef = ref<HTMLIFrameElement | null>(null);
const windowHeight = ref(window.innerHeight);
// 计算 PDF iframe 的合适高度(优先使用外部传入的 imgHeight否则根据窗口高度动态计算 // 携带 token 请求 img/pdf 的 src
const pdfIframeHeight = computed(() => { const getImgSrcByToken = (src?: string) => {
// 如果外部传入了 imgHeight优先使用 const targetSrc = src || props.authSrc
if (props.imgHeight) {
return props.imgHeight; if (targetSrc.indexOf('.pdf') >= 0) {
// PDF通过 iframe 展示
showIframe.value = true
nextTick(() => {
const tenantId = Session.getTenant()
const iframe = authIframeRef.value
if (!iframe) return
const request = new XMLHttpRequest()
request.responseType = 'blob'
request.open('get', targetSrc, true)
request.setRequestHeader('Authorization', 'Bearer ' + Session.getToken())
request.setRequestHeader('TENANT-ID', tenantId)
request.onreadystatechange = () => {
if (request.readyState === XMLHttpRequest.DONE && request.status === 200) {
const binaryData: BlobPart[] = []
binaryData.push(request.response)
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }))
iframe.onload = () => {
URL.revokeObjectURL(iframe.src)
}
}
}
request.send(null)
})
} else {
// 图片:保持原有行为(直接使用 authSrc不做 token 转发)
showIframe.value = false
// 如需带 token 加载图片,可参考被注释的旧逻辑在此扩展
} }
// 否则根据窗口高度动态计算dialog header 约 50pxpadding 约 40px留一些余量 }
return `${windowHeight.value - 120}px`;
});
// 监听窗口大小变化 const refreshImg = (src?: string) => {
const handleResize = () => { getImgSrcByToken(src)
windowHeight.value = window.innerHeight; }
};
onMounted(() => { onMounted(() => {
window.addEventListener('resize', handleResize); getImgSrcByToken()
getImgSrcByToken(); })
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
// 携带token请求img的src
const getImgSrcByToken = (src?: string) => {
if (props.authSrc.indexOf(".pdf") >= 0) {
showIframe.value = true;
pdfDialogVisible.value = true;
nextTick(() => {
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const iframe = authIframeRef.value;
if (!iframe) return;
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
const binaryData: BlobPart[] = [];
binaryData.push(request.response);
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
iframe.onload = () => {
URL.revokeObjectURL(iframe.src);
};
}
};
request.send(null);
});
} else {
// 图片处理逻辑:加载后直接打开预览
showIframe.value = false;
pdfDialogVisible.value = false;
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
imageSrc.value = URL.createObjectURL(request.response);
imagePreviewVisible.value = true;
}
};
request.send(null);
}
};
// 刷新图片
const refreshImg = (src?: string) => {
getImgSrcByToken(src);
};
// 暴露方法供外部调用
defineExpose({ defineExpose({
refreshImg refreshImg,
}); })
</script> </script>
<style scoped>
.pdf-preview-dialog :deep(.el-dialog__body) {
padding: 20px !important;
overflow-y: hidden !important;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<!-- 组件不占据任何布局空间所有预览组件都是 teleported -->
<div style="display: none;">
<!-- 图片直接使用 el-image-viewer 全屏预览 -->
<el-image-viewer
v-if="!showIframe && imageSrc && imagePreviewVisible"
:url-list="[imageSrc]"
:teleported="true"
hide-on-click-modal
@close="imagePreviewVisible = false"
/>
<!-- PDF dialog 中显示 -->
<el-dialog
v-if="showIframe"
v-model="pdfDialogVisible"
:title="dialogTitle || '文件预览'"
append-to-body
width="90%"
class="pdf-preview-dialog"
>
<iframe ref="authIframeRef" :style="{ width: '100%', height: pdfIframeHeight }" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed, onUnmounted } from 'vue';
import { ElImageViewer } from 'element-plus';
import { Session } from "/@/utils/storage";
// 定义 props
const props = defineProps<{
authSrc: string;
imgWidth?: string;
imgHeight?: string;
dialogTitle?: string;
}>();
// 定义响应式数据
const showIframe = ref(false);
const imageSrc = ref<string>('');
const imagePreviewVisible = ref(false);
const pdfDialogVisible = ref(false);
const authIframeRef = ref<HTMLIFrameElement | null>(null);
const windowHeight = ref(window.innerHeight);
// 计算 PDF iframe 的合适高度(优先使用外部传入的 imgHeight否则根据窗口高度动态计算
const pdfIframeHeight = computed(() => {
// 如果外部传入了 imgHeight优先使用
if (props.imgHeight) {
return props.imgHeight;
}
// 否则根据窗口高度动态计算dialog header 约 50pxpadding 约 40px留一些余量
return `${windowHeight.value - 120}px`;
});
// 监听窗口大小变化
const handleResize = () => {
windowHeight.value = window.innerHeight;
};
onMounted(() => {
window.addEventListener('resize', handleResize);
getImgSrcByToken();
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
// 携带token请求img的src
const getImgSrcByToken = (src?: string) => {
if (props.authSrc.indexOf(".pdf") >= 0) {
showIframe.value = true;
pdfDialogVisible.value = true;
nextTick(() => {
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const iframe = authIframeRef.value;
if (!iframe) return;
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
const binaryData: BlobPart[] = [];
binaryData.push(request.response);
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
iframe.onload = () => {
URL.revokeObjectURL(iframe.src);
};
}
};
request.send(null);
});
} else {
// 图片处理逻辑:加载后直接打开预览
showIframe.value = false;
pdfDialogVisible.value = false;
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
imageSrc.value = URL.createObjectURL(request.response);
imagePreviewVisible.value = true;
}
};
request.send(null);
}
};
// 刷新图片
const refreshImg = (src?: string) => {
getImgSrcByToken(src);
};
// 暴露方法供外部调用
defineExpose({
refreshImg
});
</script>
<style scoped>
.pdf-preview-dialog :deep(.el-dialog__body) {
padding: 20px !important;
overflow-y: hidden !important;
}
</style>

View File

@@ -343,7 +343,41 @@ export function useTableColumns(
} }
if (extracted.length > 0) { if (extracted.length > 0) {
columns.value = extracted // 合并列配置:
// - 第一次提取时直接赋值
// - 后续提取时与已有列做并集,避免因为列被隐藏导致配置丢失,
// 从而在“列设置”弹窗中看不到已隐藏的列
if (columns.value.length === 0) {
columns.value = extracted
} else {
const keyOf = (col: ColumnConfig) => col.prop || col.label
const map = new Map<string, ColumnConfig>()
// 先放入已有列
columns.value.forEach(col => {
const key = keyOf(col)
if (key) {
map.set(key, { ...col })
}
})
// 再合并本次提取的列更新宽度、fixed 等信息,避免旧配置过时)
extracted.forEach(col => {
const key = keyOf(col)
if (!key) return
const exist = map.get(key)
if (exist) {
map.set(key, {
...exist,
...col,
})
} else {
map.set(key, { ...col })
}
})
columns.value = Array.from(map.values())
}
// 初始化可见列 // 初始化可见列
if (storageKey) { if (storageKey) {

View File

@@ -167,7 +167,7 @@
/> />
<!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 --> <!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 -->
<auth-img <preview-file
v-for="src in imgUrl" v-for="src in imgUrl"
:key="src.title" :key="src.title"
:authSrc="src.url" :authSrc="src.url"
@@ -202,7 +202,7 @@ const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNa
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue')) const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue')) const DataForm = defineAsyncComponent(() => import('./form.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue')) const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const authImg = defineAsyncComponent(() => import('/@/components/tools/auth-img.vue')) const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
// 审核状态选项(独立定义,防止其他页面修改时被波及) // 审核状态选项(独立定义,防止其他页面修改时被波及)
import type { StateOption } from '/@/components/AuditState/index.vue' import type { StateOption } from '/@/components/AuditState/index.vue'

View File

@@ -193,7 +193,7 @@
/> />
<!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 --> <!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 -->
<auth-img <preview-file
v-for="src in imgUrl" v-for="src in imgUrl"
:key="src.title" :key="src.title"
:authSrc="src.url" :authSrc="src.url"
@@ -229,7 +229,7 @@ const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNa
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue')) const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue')) const DataForm = defineAsyncComponent(() => import('./form.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue')) const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const authImg = defineAsyncComponent(() => import('/@/components/tools/auth-img.vue')) const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
// 审核状态选项(独立定义,防止其他页面修改时被波及) // 审核状态选项(独立定义,防止其他页面修改时被波及)
import type { StateOption } from '/@/components/AuditState/index.vue' import type { StateOption } from '/@/components/AuditState/index.vue'

View File

@@ -163,7 +163,7 @@
/> />
<!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 --> <!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 -->
<auth-img <preview-file
v-for="src in imgUrl" v-for="src in imgUrl"
:key="src.title" :key="src.title"
:authSrc="src.url" :authSrc="src.url"
@@ -197,7 +197,7 @@ const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNa
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue')) const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue')) const DataForm = defineAsyncComponent(() => import('./form.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue')) const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const authImg = defineAsyncComponent(() => import('/@/components/tools/auth-img.vue')) const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
// 审核状态选项(独立定义,防止其他页面修改时被波及) // 审核状态选项(独立定义,防止其他页面修改时被波及)
import type { StateOption } from '/@/components/AuditState/index.vue' import type { StateOption } from '/@/components/AuditState/index.vue'

View File

@@ -158,7 +158,7 @@
/> />
<!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 --> <!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 -->
<auth-img <preview-file
v-for="src in imgUrl" v-for="src in imgUrl"
:key="src.title" :key="src.title"
:authSrc="src.url" :authSrc="src.url"
@@ -190,7 +190,7 @@ const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNa
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue')) const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue')) const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue')) const DataForm = defineAsyncComponent(() => import('./form.vue'))
const authImg = defineAsyncComponent(() => import('/@/components/tools/auth-img.vue')) const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
// 使用 Pinia store // 使用 Pinia store
const userInfoStore = useUserInfo() const userInfoStore = useUserInfo()

View File

@@ -202,7 +202,7 @@
/> />
<!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 --> <!-- 材料预览图片直接显示PDF 在组件内部 dialog 中显示 -->
<auth-img <preview-file
v-for="src in imgUrl" v-for="src in imgUrl"
:key="src.title" :key="src.title"
:authSrc="src.url" :authSrc="src.url"
@@ -241,7 +241,7 @@ const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/i
const MultiDialog = defineAsyncComponent(() => import('/@/views/professional/teacherbase/multiDialog.vue')) const MultiDialog = defineAsyncComponent(() => import('/@/views/professional/teacherbase/multiDialog.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue')) const DataForm = defineAsyncComponent(() => import('./form.vue'))
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue')) const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const authImg = defineAsyncComponent(() => import('/@/components/tools/auth-img.vue')) const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
// 使用 Pinia store // 使用 Pinia store
const userInfoStore = useUserInfo() const userInfoStore = useUserInfo()

View File

@@ -1449,7 +1449,7 @@
const PoliticsDialog = defineAsyncComponent(() => import('./politics-dialog.vue')); const PoliticsDialog = defineAsyncComponent(() => import('./politics-dialog.vue'));
const RelationDialog = defineAsyncComponent(() => import('./relation-dialog.vue')); const RelationDialog = defineAsyncComponent(() => import('./relation-dialog.vue'));
const StatusLockDialog = defineAsyncComponent(() => import('./status-lock-dialog.vue')); const StatusLockDialog = defineAsyncComponent(() => import('./status-lock-dialog.vue'));
const ActionDropdown = defineAsyncComponent(() => import('./action-dropdown.vue')); const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'));
const TableColumnControl = defineAsyncComponent(() => import('/@/components/TableColumnControl/index.vue')); const TableColumnControl = defineAsyncComponent(() => import('/@/components/TableColumnControl/index.vue'));
const TableColumn = defineAsyncComponent(() => import('/@/components/TableColumn/index.vue')); const TableColumn = defineAsyncComponent(() => import('/@/components/TableColumn/index.vue'));
const TableColumnProvider = defineAsyncComponent(() => import('/@/components/TableColumn/Provider.vue')); const TableColumnProvider = defineAsyncComponent(() => import('/@/components/TableColumn/Provider.vue'));

View File

@@ -367,7 +367,7 @@
<el-tag disabled>{{waitShenheForm.form.deptName}}</el-tag> <el-tag disabled>{{waitShenheForm.form.deptName}}</el-tag>
</el-form-item> </el-form-item>
<el-form-item label="现二级部门*"> <el-form-item label="现二级部门" prop="newDeptCode">
<el-select v-model="newSecDeptCode" @change="getDeptListByParent" placeholder="请选择二级部门" style="width: 100%"> <el-select v-model="newSecDeptCode" @change="getDeptListByParent" placeholder="请选择二级部门" style="width: 100%">
<el-option v-for="item in secDeptList" <el-option v-for="item in secDeptList"
:key="item.deptCode" :key="item.deptCode"
@@ -392,9 +392,8 @@
v-model="waitShenheForm.form.changeDate" v-model="waitShenheForm.form.changeDate"
type="date" type="date"
placeholder="选择日期" placeholder="选择日期"
format="yyyy-MM-dd" format="YYYY-MM-DD"
value-format="yyyy-MM-dd HH:mm:ss" value-format="YYYY-MM-DD"
style="width: 100%"
/> />
</el-form-item> </el-form-item>
@@ -461,9 +460,8 @@
v-model="waitShenheForm.form.changeTime" v-model="waitShenheForm.form.changeTime"
type="date" type="date"
placeholder="选择日期" placeholder="选择日期"
format="yyyy-MM-dd" format="YYYY-MM-DD"
value-format="yyyy-MM-dd HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/> />
</el-form-item> </el-form-item>
@@ -490,7 +488,9 @@
import { getTeacherCertificateList } from '/@/api/professional/rsbase/professionalteachercertificateconf' import { getTeacherCertificateList } from '/@/api/professional/rsbase/professionalteachercertificateconf'
import { getAllTypeList } from '/@/api/professional/rsbase/professionalacademiceducationtypeconfig' import { getAllTypeList } from '/@/api/professional/rsbase/professionalacademiceducationtypeconfig'
import { getQualificationList } from '/@/api/professional/rsbase/academicqualificationsconfig' import { getQualificationList } from '/@/api/professional/rsbase/academicqualificationsconfig'
import { getDegreeList } from '/@/api/professional/rsbase/professionalacademicdegreeconfig' import { getDegreeList } from '/@/api/professional/rsbase/professionalacademicdegreeconfig'
import { addObj as addStationChangeObj } from '/@/api/professional/professionaluser/professionalteacherstationchange'
import { addObj as addPartyChangeObj } from '/@/api/professional/professionaluser/professionalpartychange'
import global from '/@/components/tools/commondict.vue' import global from '/@/components/tools/commondict.vue'
import { Session } from '/@/utils/storage' import { Session } from '/@/utils/storage'
@@ -556,7 +556,8 @@
} }
const stationChangeRules = { const stationChangeRules = {
changeDate: [{ required: true, message: '请选择日期', trigger: 'change' }], newDeptCode: [{ required: true, message: '请选择要调入的二级部门', trigger: 'change' }],
changeDate: [{ required: true, message: '请选择调令日期', trigger: 'change' }],
pos: [{ required: true, message: '请选择岗位类型', trigger: 'change' }] pos: [{ required: true, message: '请选择岗位类型', trigger: 'change' }]
} }
@@ -870,7 +871,17 @@
try { try {
await messageBox.confirm('确认提交?') await messageBox.confirm('确认提交?')
const res = await updateOtherInfo(waitShenheForm.form) let res
if (val === 5) {
// 人员调动:使用人员调动接口
res = await addStationChangeObj(waitShenheForm.form)
} else if (val === 6) {
// 党员调动:使用党员调动接口
res = await addPartyChangeObj(waitShenheForm.form)
} else {
// 其他类型:走通用“其他信息”更新接口
res = await updateOtherInfo(waitShenheForm.form)
}
if (res.data === '-1') { if (res.data === '-1') {
message.warning("当前不允许提交") message.warning("当前不允许提交")
} else { } else {