This commit is contained in:
吴红兵
2026-03-07 12:35:45 +08:00
parent 271710e870
commit b997b3ba48
423 changed files with 79612 additions and 91574 deletions

View File

@@ -1,280 +1,236 @@
<template>
<el-dialog
v-model="visible"
title="文件归档"
width="900px"
destroy-on-close
append-to-body
>
<template #header>
<div class="dialog-header">
<span class="dialog-title">
<el-icon><FolderOpened /></el-icon>
文件归档 - {{ purchaseNo || purchaseId }}
</span>
<el-button
type="primary"
:loading="downloading"
@click="handleDownloadAll"
>
<el-icon><Download /></el-icon>
下载全部文件
</el-button>
</div>
</template>
<el-dialog v-model="visible" title="文件归档" width="900px" destroy-on-close append-to-body>
<template #header>
<div class="dialog-header">
<span class="dialog-title">
<el-icon><FolderOpened /></el-icon>
文件归档 - {{ purchaseNo || purchaseId }}
</span>
<el-button type="primary" :loading="downloading" @click="handleDownloadAll">
<el-icon><Download /></el-icon>
下载全部文件
</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="fileList"
stripe
border
max-height="500px"
class="file-table"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="fileTitle" label="文件名称" min-width="220" show-overflow-tooltip>
<template #default="{ row }">
<div class="file-name">
<el-icon class="file-icon"><Document /></el-icon>
<span>{{ row.fileTitle }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="fileTypeDesc" label="文件类型" width="160" align="center">
<template #default="{ row }">
<el-tag type="info">{{ row.fileTypeDesc || '未知类型' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
link
icon="View"
@click="handlePreview(row)"
>
预览
</el-button>
<el-button
type="primary"
link
icon="Download"
@click="handleDownloadFile(row)"
>
下载
</el-button>
</template>
</el-table-column>
</el-table>
<el-table v-loading="loading" :data="fileList" stripe border max-height="500px" class="file-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="fileTitle" label="文件名称" min-width="220" show-overflow-tooltip>
<template #default="{ row }">
<div class="file-name">
<el-icon class="file-icon"><Document /></el-icon>
<span>{{ row.fileTitle }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="fileTypeDesc" label="文件类型" width="160" align="center">
<template #default="{ row }">
<el-tag type="info">{{ row.fileTypeDesc || '未知类型' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link icon="View" @click="handlePreview(row)"> 预览 </el-button>
<el-button type="primary" link icon="Download" @click="handleDownloadFile(row)"> 下载 </el-button>
</template>
</el-table-column>
</el-table>
<div v-if="!loading && fileList.length === 0" class="empty-tip">
<el-empty description="暂无文件" />
</div>
<div v-if="!loading && fileList.length === 0" class="empty-tip">
<el-empty description="暂无文件" />
</div>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
<!-- PDF预览弹窗 -->
<el-dialog
v-model="previewVisible"
title="文件预览"
width="90%"
top="5vh"
destroy-on-close
append-to-body
class="preview-dialog"
>
<div class="preview-container">
<iframe
v-if="previewUrl"
:src="previewUrl"
class="preview-iframe"
frameborder="0"
/>
</div>
</el-dialog>
<!-- PDF预览弹窗 -->
<el-dialog v-model="previewVisible" title="文件预览" width="90%" top="5vh" destroy-on-close append-to-body class="preview-dialog">
<div class="preview-container">
<iframe v-if="previewUrl" :src="previewUrl" class="preview-iframe" frameborder="0" />
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import { FolderOpened, Download, Document } from '@element-plus/icons-vue'
import { useMessage } from '/@/hooks/message'
import { listDownloadUrls, getArchiveDownloadUrl, downloadFileById, previewFileById } from '/@/api/purchase/purchasingrequisition'
import other from '/@/utils/other'
import { ref, computed, onUnmounted } from 'vue';
import { FolderOpened, Download, Document } from '@element-plus/icons-vue';
import { useMessage } from '/@/hooks/message';
import { listDownloadUrls, getArchiveDownloadUrl, downloadFileById, previewFileById } from '/@/api/purchase/purchasingrequisition';
import other from '/@/utils/other';
interface FileItem {
id: string
fileTitle: string
fileType: string
fileTypeDesc: string
downloadUrl: string
id: string;
fileTitle: string;
fileType: string;
fileTypeDesc: string;
downloadUrl: string;
}
const visible = ref(false)
const loading = ref(false)
const downloading = ref(false)
const previewVisible = ref(false)
const previewUrl = ref('')
const previewLoading = ref(false)
const purchaseId = ref('')
const purchaseNo = ref('')
const fileList = ref<FileItem[]>([])
const visible = ref(false);
const loading = ref(false);
const downloading = ref(false);
const previewVisible = ref(false);
const previewUrl = ref('');
const previewLoading = ref(false);
const purchaseId = ref('');
const purchaseNo = ref('');
const fileList = ref<FileItem[]>([]);
const open = async (id: string, no?: string) => {
purchaseId.value = id || ''
purchaseNo.value = no || ''
visible.value = true
await loadFileList()
}
purchaseId.value = id || '';
purchaseNo.value = no || '';
visible.value = true;
await loadFileList();
};
const loadFileList = async () => {
if (!purchaseId.value) {
useMessage().warning('无法获取采购申请ID')
return
}
loading.value = true
try {
const res = await listDownloadUrls(purchaseId.value)
if (res && res.data) {
fileList.value = res.data as FileItem[]
} else {
fileList.value = []
}
} catch (err: any) {
useMessage().error(err?.msg || '获取文件列表失败')
fileList.value = []
} finally {
loading.value = false
}
}
if (!purchaseId.value) {
useMessage().warning('无法获取采购申请ID');
return;
}
loading.value = true;
try {
const res = await listDownloadUrls(purchaseId.value);
if (res && res.data) {
fileList.value = res.data as FileItem[];
} else {
fileList.value = [];
}
} catch (err: any) {
useMessage().error(err?.msg || '获取文件列表失败');
fileList.value = [];
} finally {
loading.value = false;
}
};
const isPdfFile = (fileName: string): boolean => {
if (!fileName) return false
const ext = fileName.toLowerCase().split('.').pop()
return ext === 'pdf'
}
if (!fileName) return false;
const ext = fileName.toLowerCase().split('.').pop();
return ext === 'pdf';
};
const handlePreview = async (row: FileItem) => {
if (!row.id) {
useMessage().warning('文件ID不存在')
return
}
if (!row.id) {
useMessage().warning('文件ID不存在');
return;
}
if (!isPdfFile(row.fileTitle)) {
useMessage().info('仅支持PDF格式文件预览将为您下载文件')
handleDownloadFile(row)
return
}
if (!isPdfFile(row.fileTitle)) {
useMessage().info('仅支持PDF格式文件预览将为您下载文件');
handleDownloadFile(row);
return;
}
previewLoading.value = true
previewVisible.value = true
previewUrl.value = ''
previewLoading.value = true;
previewVisible.value = true;
previewUrl.value = '';
try {
const res = await previewFileById(row.id)
const blob = res as unknown as Blob
const url = window.URL.createObjectURL(blob)
previewUrl.value = url
} catch (err: any) {
useMessage().error(err?.msg || '预览失败')
previewVisible.value = false
} finally {
previewLoading.value = false
}
}
try {
const res = await previewFileById(row.id);
const blob = res as unknown as Blob;
const url = window.URL.createObjectURL(blob);
previewUrl.value = url;
} catch (err: any) {
useMessage().error(err?.msg || '预览失败');
previewVisible.value = false;
} finally {
previewLoading.value = false;
}
};
const handleDownloadFile = async (row: FileItem) => {
if (!row.id) {
useMessage().warning('文件ID不存在')
return
}
try {
const res = await downloadFileById(row.id)
const blob = res as unknown as Blob
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = row.fileTitle || 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (err: any) {
useMessage().error(err?.msg || '下载失败')
}
}
if (!row.id) {
useMessage().warning('文件ID不存在');
return;
}
try {
const res = await downloadFileById(row.id);
const blob = res as unknown as Blob;
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = row.fileTitle || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (err: any) {
useMessage().error(err?.msg || '下载失败');
}
};
const handleDownloadAll = () => {
if (!purchaseId.value) {
useMessage().warning('无法获取采购申请ID')
return
}
downloading.value = true
try {
const url = getArchiveDownloadUrl(purchaseId.value)
const fileName = `归档_${purchaseNo.value || purchaseId.value}.zip`
other.downBlobFile(url, {}, fileName)
} finally {
setTimeout(() => {
downloading.value = false
}, 500)
}
}
if (!purchaseId.value) {
useMessage().warning('无法获取采购申请ID');
return;
}
downloading.value = true;
try {
const url = getArchiveDownloadUrl(purchaseId.value);
const fileName = `归档_${purchaseNo.value || purchaseId.value}.zip`;
other.downBlobFile(url, {}, fileName);
} finally {
setTimeout(() => {
downloading.value = false;
}, 500);
}
};
onUnmounted(() => {
if (previewUrl.value) {
window.URL.revokeObjectURL(previewUrl.value)
}
})
if (previewUrl.value) {
window.URL.revokeObjectURL(previewUrl.value);
}
});
defineExpose({
open
})
open,
});
</script>
<style scoped lang="scss">
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.dialog-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
}
.file-table {
width: 100%;
width: 100%;
}
.file-name {
display: flex;
align-items: center;
gap: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.file-icon {
color: #409eff;
color: #409eff;
}
.empty-tip {
padding: 40px 0;
padding: 40px 0;
}
.preview-container {
width: 100%;
height: calc(90vh - 120px);
width: 100%;
height: calc(90vh - 120px);
}
.preview-iframe {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
</style>

View File

@@ -9,7 +9,12 @@
</div>
</template>
<el-alert title="仅支持上传PDF格式文件每个类型仅能上传1个文件如需更新请先删除原文件" type="info" :closable="false" style="margin-bottom: 16px" />
<el-alert
title="仅支持上传PDF格式文件每个类型仅能上传1个文件如需更新请先删除原文件"
type="info"
:closable="false"
style="margin-bottom: 16px"
/>
<el-empty v-if="uploadedFileTypes.length === 0" description="该采购申请暂无上传材料" />
@@ -82,12 +87,7 @@ const fileTypeList: FileTypeItem[] = [
// 根据已上传文件类型过滤显示列表排除履约验收110和采购文件130
const displayedFileTypes = computed(() => {
return fileTypeList.filter(
(item) =>
uploadedFileTypes.value.includes(item.value) &&
item.value !== '110' &&
item.value !== '130'
);
return fileTypeList.filter((item) => uploadedFileTypes.value.includes(item.value) && item.value !== '110' && item.value !== '130');
});
const open = async (id: string, no?: string) => {
@@ -149,13 +149,13 @@ const handleSubmit = async () => {
uploadedFileTypes.value
.filter((ft) => ft !== '110' && ft !== '130')
.forEach((fileType) => {
const files = fileMap[fileType] || [];
files.forEach((file) => {
if (file.id) {
allFileIds.push(file.id);
}
const files = fileMap[fileType] || [];
files.forEach((file) => {
if (file.id) {
allFileIds.push(file.id);
}
});
});
});
if (allFileIds.length === 0) {
useMessage().warning('请至少保留或上传一个文件');

View File

@@ -1,188 +1,201 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px" >
<el-row :gutter="24">
<!-- <el-col :span="12" class="mb20">-->
<!-- <el-form-item label="验收日期" prop="acceptDate">-->
<!-- <el-date-picker-->
<!-- v-model="form.acceptDate"-->
<!-- type="date"-->
<!-- placeholder="请选择"-->
<!-- format="YYYY-MM-DD"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- style="width: 100%"-->
<!-- :disabled="readonly"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px">
<el-row :gutter="24">
<!-- <el-col :span="12" class="mb20">-->
<!-- <el-form-item label="验收日期" prop="acceptDate">-->
<!-- <el-date-picker-->
<!-- v-model="form.acceptDate"-->
<!-- type="date"-->
<!-- placeholder="请选择"-->
<!-- format="YYYY-MM-DD"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- style="width: 100%"-->
<!-- :disabled="readonly"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- 上传履约验收模版 -->
<el-col :span="12" class="mb20">
<el-form-item label="履约验收文件" prop="templateFileIds" :required="true">
<upload-file v-model="templateFiles" :limit="1" :file-type="['pdf']" :data="{ purchaseId: purchaseId || '', fileType: '110' }" upload-file-url="/purchase/purchasingfiles/upload" :disabled="readonly" />
<el-link v-if="!readonly" type="primary" @click="handleDownloadTemplate" style="margin-top: 8px; display: inline-flex; align-items: center;">
<el-icon><Download /></el-icon>
<span style="margin-left: 4px;">下载{{ lyysTemplateLabel }}</span>
</el-link>
</el-form-item>
</el-col>
<!-- 上传履约验收模版 -->
<el-col :span="12" class="mb20">
<el-form-item label="履约验收文件" prop="templateFileIds" :required="true">
<upload-file
v-model="templateFiles"
:limit="1"
:file-type="['pdf']"
:data="{ purchaseId: purchaseId || '', fileType: '110' }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="readonly"
/>
<el-link v-if="!readonly" type="primary" @click="handleDownloadTemplate" style="margin-top: 8px; display: inline-flex; align-items: center">
<el-icon><Download /></el-icon>
<span style="margin-left: 4px">下载{{ lyysTemplateLabel }}</span>
</el-link>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入" :disabled="readonly" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入" :disabled="readonly" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { Download } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import UploadFile from "/@/components/Upload/index.vue";
import { downloadTemplate } from "/@/api/purchase/purchasingAccept";
import { ref, reactive, computed, watch, onMounted } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { Download } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import UploadFile from '/@/components/Upload/index.vue';
import { downloadTemplate } from '/@/api/purchase/purchasingAccept';
/** 项目类型 A:货物 B:工程 C:服务 */
const LYYS_TEMPLATE_MAP: Record<string, { label: string }> = {
A: { label: '履约验收表模板(货物)' },
B: { label: '履约验收表模板(工程)' },
C: { label: '履约验收表模板(服务)' },
}
A: { label: '履约验收表模板(货物)' },
B: { label: '履约验收表模板(工程)' },
C: { label: '履约验收表模板(服务)' },
};
const props = defineProps<{
modelValue: Record<string, any>
readonly?: boolean
purchaseId?: string
/** 项目类型 A:货物 B:工程 C:服务,用于模版下载 */
projectType?: string
/** 预算金额,用于判断模板目录 */
budget?: number
batchNum?: number
}>()
modelValue: Record<string, any>;
readonly?: boolean;
purchaseId?: string;
/** 项目类型 A:货物 B:工程 C:服务,用于模版下载 */
projectType?: string;
/** 预算金额,用于判断模板目录 */
budget?: number;
batchNum?: number;
}>();
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue']);
const formRef = ref<FormInstance>()
const formRef = ref<FormInstance>();
// 文件对象数组用于上传组件显示包含id和fileTitle
const templateFiles = ref<any[]>([])
const templateFiles = ref<any[]>([]);
const projectTypeKey = computed(() => (props.projectType === 'A' || props.projectType === 'B' || props.projectType === 'C' ? props.projectType : 'A'))
const projectTypeKey = computed(() =>
props.projectType === 'A' || props.projectType === 'B' || props.projectType === 'C' ? props.projectType : 'A'
);
const lyysTemplateLabel = computed(() => {
const amountLabel = (props.budget && props.budget >= 50000) ? '(≥5万)' : '(<5万)'
return LYYS_TEMPLATE_MAP[projectTypeKey.value]?.label + amountLabel || LYYS_TEMPLATE_MAP.A.label + amountLabel
})
const lyysTemplateDownloadName = computed(() => `${lyysTemplateLabel.value}.docx`)
const amountLabel = props.budget && props.budget >= 50000 ? '(≥5万)' : '(<5万)';
return LYYS_TEMPLATE_MAP[projectTypeKey.value]?.label + amountLabel || LYYS_TEMPLATE_MAP.A.label + amountLabel;
});
const lyysTemplateDownloadName = computed(() => `${lyysTemplateLabel.value}.docx`);
/** 下载模板 - 调用后台接口 */
const handleDownloadTemplate = async () => {
if (!props.purchaseId) {
ElMessage.warning('采购ID不存在')
return
}
try {
const res = await downloadTemplate(props.purchaseId)
const blob = res as unknown as Blob
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = lyysTemplateDownloadName.value
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (e: any) {
ElMessage.error(e?.message || '下载模板失败')
}
}
if (!props.purchaseId) {
ElMessage.warning('采购ID不存在');
return;
}
try {
const res = await downloadTemplate(props.purchaseId);
const blob = res as unknown as Blob;
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = lyysTemplateDownloadName.value;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (e: any) {
ElMessage.error(e?.message || '下载模板失败');
}
};
const form = reactive({
acceptType: '2', // 固定为上传模式
acceptDate: '',
templateFileIds: [] as string[],
remark: '',
})
acceptType: '2', // 固定为上传模式
acceptDate: '',
templateFileIds: [] as string[],
remark: '',
});
// 从外部数据初始化(仅在挂载和 modelValue 引用变化时执行)
const initData = () => {
const val = props.modelValue
if (!val) return
const val = props.modelValue;
if (!val) return;
form.acceptType = '2'
form.acceptDate = val.acceptDate || ''
form.remark = val.remark || ''
form.acceptType = '2';
form.acceptDate = val.acceptDate || '';
form.remark = val.remark || '';
// 处理文件数据:支持 _templateFiles 数组或 templateFileIds
if (val._templateFiles && Array.isArray(val._templateFiles)) {
templateFiles.value = val._templateFiles.map((f: any) => ({
id: f.id,
fileTitle: f.fileTitle,
name: f.fileTitle || '',
url: '',
}))
form.templateFileIds = val._templateFiles.map((f: any) => f.id)
} else if (val.templateFileIds) {
if (typeof val.templateFileIds === 'string') {
const ids = val.templateFileIds.split(',').filter(Boolean)
templateFiles.value = ids.map((id: string) => ({ id: id.trim(), name: '', url: '' }))
form.templateFileIds = ids
} else if (Array.isArray(val.templateFileIds)) {
templateFiles.value = val.templateFileIds.map((item: any) => {
if (typeof item === 'string') return { id: item, name: '', url: '' }
return { id: item.id, fileTitle: item.fileTitle, name: item.fileTitle || '', url: '' }
})
form.templateFileIds = val.templateFileIds.map((item: any) => typeof item === 'string' ? item : item.id).filter(Boolean)
}
} else {
templateFiles.value = []
form.templateFileIds = []
}
}
// 处理文件数据:支持 _templateFiles 数组或 templateFileIds
if (val._templateFiles && Array.isArray(val._templateFiles)) {
templateFiles.value = val._templateFiles.map((f: any) => ({
id: f.id,
fileTitle: f.fileTitle,
name: f.fileTitle || '',
url: '',
}));
form.templateFileIds = val._templateFiles.map((f: any) => f.id);
} else if (val.templateFileIds) {
if (typeof val.templateFileIds === 'string') {
const ids = val.templateFileIds.split(',').filter(Boolean);
templateFiles.value = ids.map((id: string) => ({ id: id.trim(), name: '', url: '' }));
form.templateFileIds = ids;
} else if (Array.isArray(val.templateFileIds)) {
templateFiles.value = val.templateFileIds.map((item: any) => {
if (typeof item === 'string') return { id: item, name: '', url: '' };
return { id: item.id, fileTitle: item.fileTitle, name: item.fileTitle || '', url: '' };
});
form.templateFileIds = val.templateFileIds.map((item: any) => (typeof item === 'string' ? item : item.id)).filter(Boolean);
}
} else {
templateFiles.value = [];
form.templateFileIds = [];
}
};
// 挂载时初始化
onMounted(() => {
initData()
})
initData();
});
// 监听文件变化,更新 form.templateFileIds
watch(templateFiles, (files) => {
if (Array.isArray(files)) {
form.templateFileIds = files.map((f: any) => f.id).filter(Boolean)
} else {
form.templateFileIds = []
}
}, { deep: true })
watch(
templateFiles,
(files) => {
if (Array.isArray(files)) {
form.templateFileIds = files.map((f: any) => f.id).filter(Boolean);
} else {
form.templateFileIds = [];
}
},
{ deep: true }
);
const rules: FormRules = {
templateFileIds: [
{
validator: (_rule: any, value: any, callback: (e?: Error) => void) => {
if (!value || (Array.isArray(value) && value.length === 0) || (typeof value === 'string' && !value.trim())) {
callback(new Error('请上传履约验收文件'))
return
}
callback()
},
trigger: 'change'
},
],
}
templateFileIds: [
{
validator: (_rule: any, value: any, callback: (e?: Error) => void) => {
if (!value || (Array.isArray(value) && value.length === 0) || (typeof value === 'string' && !value.trim())) {
callback(new Error('请上传履约验收文件'));
return;
}
callback();
},
trigger: 'change',
},
],
};
const validate = () => formRef.value?.validate()
const validate = () => formRef.value?.validate();
// 获取当前表单数据(供父组件调用)
const getFormData = () => ({
acceptType: form.acceptType,
acceptDate: form.acceptDate,
templateFileIds: [...form.templateFileIds],
remark: form.remark,
})
acceptType: form.acceptType,
acceptDate: form.acceptDate,
templateFileIds: [...form.templateFileIds],
remark: form.remark,
});
defineExpose({ validate, form, getFormData, initData })
defineExpose({ validate, form, getFormData, initData });
</script>
<style scoped>
.mb20 {
margin-bottom: 20px;
margin-bottom: 20px;
}
</style>

View File

@@ -1,356 +1,351 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-row :gutter="24">
<el-col :span="8" class="mb20">
<el-form-item label="项目名称">
<el-input :model-value="projectName || form.projectName" readonly placeholder="-" disabled />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="需求部门">
<el-input :model-value="deptName || form.deptName" readonly placeholder="-" disabled/>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否签订合同" prop="hasContract">
<el-radio-group v-model="form.hasContract">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.hasContract === '1'">
<el-form-item label="合同" prop="contractId">
<el-select
v-model="form.contractId"
placeholder="请选择合同"
clearable
filterable
style="width: 100%"
:loading="contractLoading"
@visible-change="onContractSelectVisibleChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName || item.contractNo || item.id"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否分期验收" prop="isInstallment">
<el-radio-group v-model="form.isInstallment">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.isInstallment === '1'">
<el-form-item label="分期次数" prop="totalPhases">
<el-input-number v-model="form.totalPhases" :min="1" :max="99" placeholder="请输入" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="供应商名称" prop="supplierName">
<el-input v-model="form.supplierName" placeholder="选择合同后自动带出" clearable />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="资产管理员" prop="assetAdminId">
<el-select
v-model="form.assetAdminId"
placeholder="请输入姓名或工号搜索"
filterable
remote
clearable
reserve-keyword
:remote-method="searchAssetAdmin"
:loading="assetAdminLoading"
style="width: 100%"
@change="onAssetAdminChange"
>
<el-option
v-for="item in assetAdminOptions"
:key="item.teacherNo"
:label="(item.commonDeptName ? item.commonDeptName + ' - ' : '') + (item.realName || item.name) + ' (' + item.teacherNo + ')'"
:value="item.teacherNo"
>
<span>{{ item.commonDeptName ? item.commonDeptName + ' - ' : '' }}{{ item.realName || item.name }}</span>
<span style="color: #999; font-size: 12px; margin-left: 8px;">{{ item.teacherNo }}</span>
</el-option>
</el-select>
<div class="field-note">如入固定资产必填</div>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="采购人员" prop="purchaserId">
<el-select
v-model="form.purchaserId"
placeholder="请输入姓名或工号搜索"
filterable
remote
clearable
reserve-keyword
:remote-method="searchPurchaser"
:loading="purchaserLoading"
style="width: 100%"
@change="onPurchaserChange"
>
<el-option
v-for="item in purchaserOptions"
:key="item.teacherNo"
:label="(item.commonDeptName ? item.commonDeptName + ' - ' : '') + (item.realName || item.name) + ' (' + item.teacherNo + ')'"
:value="item.teacherNo"
>
<span>{{ item.commonDeptName ? item.commonDeptName + ' - ' : '' }}{{ item.realName || item.name }}</span>
<span style="color: #999; font-size: 12px; margin-left: 8px;">{{ item.teacherNo }}</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-row :gutter="24">
<el-col :span="8" class="mb20">
<el-form-item label="项目名称">
<el-input :model-value="projectName || form.projectName" readonly placeholder="-" disabled />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="需求部门">
<el-input :model-value="deptName || form.deptName" readonly placeholder="-" disabled />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否签订合同" prop="hasContract">
<el-radio-group v-model="form.hasContract">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.hasContract === '1'">
<el-form-item label="合同" prop="contractId">
<el-select
v-model="form.contractId"
placeholder="请选择合同"
clearable
filterable
style="width: 100%"
:loading="contractLoading"
@visible-change="onContractSelectVisibleChange"
>
<el-option v-for="item in contractOptions" :key="item.id" :label="item.contractName || item.contractNo || item.id" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否分期验收" prop="isInstallment">
<el-radio-group v-model="form.isInstallment">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.isInstallment === '1'">
<el-form-item label="分期次数" prop="totalPhases">
<el-input-number v-model="form.totalPhases" :min="1" :max="99" placeholder="请输入" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="供应商名称" prop="supplierName">
<el-input v-model="form.supplierName" placeholder="选择合同后自动带出" clearable />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="资产管理员" prop="assetAdminId">
<el-select
v-model="form.assetAdminId"
placeholder="请输入姓名或工号搜索"
filterable
remote
clearable
reserve-keyword
:remote-method="searchAssetAdmin"
:loading="assetAdminLoading"
style="width: 100%"
@change="onAssetAdminChange"
>
<el-option
v-for="item in assetAdminOptions"
:key="item.teacherNo"
:label="(item.commonDeptName ? item.commonDeptName + ' - ' : '') + (item.realName || item.name) + ' (' + item.teacherNo + ')'"
:value="item.teacherNo"
>
<span>{{ item.commonDeptName ? item.commonDeptName + ' - ' : '' }}{{ item.realName || item.name }}</span>
<span style="color: #999; font-size: 12px; margin-left: 8px">{{ item.teacherNo }}</span>
</el-option>
</el-select>
<div class="field-note">如入固定资产必填</div>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="采购人员" prop="purchaserId">
<el-select
v-model="form.purchaserId"
placeholder="请输入姓名或工号搜索"
filterable
remote
clearable
reserve-keyword
:remote-method="searchPurchaser"
:loading="purchaserLoading"
style="width: 100%"
@change="onPurchaserChange"
>
<el-option
v-for="item in purchaserOptions"
:key="item.teacherNo"
:label="(item.commonDeptName ? item.commonDeptName + ' - ' : '') + (item.realName || item.name) + ' (' + item.teacherNo + ')'"
:value="item.teacherNo"
>
<span>{{ item.commonDeptName ? item.commonDeptName + ' - ' : '' }}{{ item.realName || item.name }}</span>
<span style="color: #999; font-size: 12px; margin-left: 8px">{{ item.teacherNo }}</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.hasContract === '0'">
<el-form-item label="成交金额" prop="transactionAmount" :required="form.hasContract === '0'">
<el-input-number
v-model="form.transactionAmount"
:min="0"
:precision="2"
placeholder="请输入成交金额"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-col :span="8" class="mb20" v-if="form.hasContract === '0'">
<el-form-item label="成交金额" prop="transactionAmount" :required="form.hasContract === '0'">
<el-input-number v-model="form.transactionAmount" :min="0" :precision="2" placeholder="请输入成交金额" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { getContracts, searchTeachers } from '/@/api/purchase/purchasingrequisition'
import { ref, reactive, watch, onMounted } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { getContracts, searchTeachers } from '/@/api/purchase/purchasingrequisition';
const props = defineProps<{
modelValue: Record<string, any>
projectName?: string
deptName?: string
/** 采购申请ID用于拉取合同列表 */
purchaseId?: string | number
/** 每次打开弹窗时变化,用于强制重置内部 form */
resetKey?: number
}>()
modelValue: Record<string, any>;
projectName?: string;
deptName?: string;
/** 采购申请ID用于拉取合同列表 */
purchaseId?: string | number;
/** 每次打开弹窗时变化,用于强制重置内部 form */
resetKey?: number;
}>();
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue']);
const formRef = ref<FormInstance>()
const contractOptions = ref<any[]>([])
const contractLoading = ref(false)
const contractLoaded = ref(false)
const formRef = ref<FormInstance>();
const contractOptions = ref<any[]>([]);
const contractLoading = ref(false);
const contractLoaded = ref(false);
// 采购人员相关
const purchaserOptions = ref<any[]>([])
const purchaserLoading = ref(false)
const purchaserOptions = ref<any[]>([]);
const purchaserLoading = ref(false);
// 资产管理员相关
const assetAdminOptions = ref<any[]>([])
const assetAdminLoading = ref(false)
const assetAdminOptions = ref<any[]>([]);
const assetAdminLoading = ref(false);
const form = reactive({
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
projectName: '',
deptName: '',
supplierName: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
transactionAmount: null,
...props.modelValue,
})
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
projectName: '',
deptName: '',
supplierName: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
transactionAmount: null,
...props.modelValue,
});
const syncFormFromModel = (val: Record<string, any> | undefined) => {
Object.assign(form, val || {})
// 加载已选人员信息用于回显
if (form.purchaserId && form.purchaserName) {
purchaserOptions.value = [{ teacherNo: form.purchaserId, realName: form.purchaserName, name: form.purchaserName }]
}
if (form.assetAdminId && form.assetAdminName) {
assetAdminOptions.value = [{ teacherNo: form.assetAdminId, realName: form.assetAdminName, name: form.assetAdminName }]
}
}
Object.assign(form, val || {});
// 加载已选人员信息用于回显
if (form.purchaserId && form.purchaserName) {
purchaserOptions.value = [{ teacherNo: form.purchaserId, realName: form.purchaserName, name: form.purchaserName }];
}
if (form.assetAdminId && form.assetAdminName) {
assetAdminOptions.value = [{ teacherNo: form.assetAdminId, realName: form.assetAdminName, name: form.assetAdminName }];
}
};
const loadContractOptions = async () => {
if (contractLoaded.value || contractLoading.value) return
if (form.hasContract !== '1') return
contractLoading.value = true
try {
const res = await getContracts(props.purchaseId ? { id: props.purchaseId } : {})
const list = res?.data
contractOptions.value = Array.isArray(list) ? list : []
contractLoaded.value = true
// 回显时:列表中含当前合同,用其供应商名称填充(若尚未有值)
if (form.contractId) {
const c = contractOptions.value.find((it: any) => it.id === form.contractId)
if (c?.supplierName) form.supplierName = c.supplierName
}
} catch (_) {
contractOptions.value = []
} finally {
contractLoading.value = false
}
}
if (contractLoaded.value || contractLoading.value) return;
if (form.hasContract !== '1') return;
contractLoading.value = true;
try {
const res = await getContracts(props.purchaseId ? { id: props.purchaseId } : {});
const list = res?.data;
contractOptions.value = Array.isArray(list) ? list : [];
contractLoaded.value = true;
// 回显时:列表中含当前合同,用其供应商名称填充(若尚未有值)
if (form.contractId) {
const c = contractOptions.value.find((it: any) => it.id === form.contractId);
if (c?.supplierName) form.supplierName = c.supplierName;
}
} catch (_) {
contractOptions.value = [];
} finally {
contractLoading.value = false;
}
};
const onContractSelectVisibleChange = (visible: boolean) => {
if (visible && form.hasContract === '1' && contractOptions.value.length === 0) {
loadContractOptions()
}
}
if (visible && form.hasContract === '1' && contractOptions.value.length === 0) {
loadContractOptions();
}
};
const searchPurchaser = async (query: string) => {
if (!query) {
purchaserOptions.value = []
return
}
purchaserLoading.value = true
try {
const res = await searchTeachers(query)
purchaserOptions.value = res?.data || []
} catch (_) {
purchaserOptions.value = []
} finally {
purchaserLoading.value = false
}
}
if (!query) {
purchaserOptions.value = [];
return;
}
purchaserLoading.value = true;
try {
const res = await searchTeachers(query);
purchaserOptions.value = res?.data || [];
} catch (_) {
purchaserOptions.value = [];
} finally {
purchaserLoading.value = false;
}
};
const searchAssetAdmin = async (query: string) => {
if (!query) {
assetAdminOptions.value = []
return
}
assetAdminLoading.value = true
try {
const res = await searchTeachers(query)
assetAdminOptions.value = res?.data || []
} catch (_) {
assetAdminOptions.value = []
} finally {
assetAdminLoading.value = false
}
}
if (!query) {
assetAdminOptions.value = [];
return;
}
assetAdminLoading.value = true;
try {
const res = await searchTeachers(query);
assetAdminOptions.value = res?.data || [];
} catch (_) {
assetAdminOptions.value = [];
} finally {
assetAdminLoading.value = false;
}
};
const onPurchaserChange = (teacherNo: string) => {
if (!teacherNo) {
form.purchaserId = ''
form.purchaserName = ''
return
}
const selected = purchaserOptions.value.find((item: any) => item.teacherNo === teacherNo)
if (selected) {
form.purchaserId = selected.teacherNo
form.purchaserName = selected.realName || selected.name
}
}
if (!teacherNo) {
form.purchaserId = '';
form.purchaserName = '';
return;
}
const selected = purchaserOptions.value.find((item: any) => item.teacherNo === teacherNo);
if (selected) {
form.purchaserId = selected.teacherNo;
form.purchaserName = selected.realName || selected.name;
}
};
const onAssetAdminChange = (teacherNo: string) => {
if (!teacherNo) {
form.assetAdminId = ''
form.assetAdminName = ''
return
}
const selected = assetAdminOptions.value.find((item: any) => item.teacherNo === teacherNo)
if (selected) {
form.assetAdminId = selected.teacherNo
form.assetAdminName = selected.realName || selected.name
}
}
if (!teacherNo) {
form.assetAdminId = '';
form.assetAdminName = '';
return;
}
const selected = assetAdminOptions.value.find((item: any) => item.teacherNo === teacherNo);
if (selected) {
form.assetAdminId = selected.teacherNo;
form.assetAdminName = selected.realName || selected.name;
}
};
watch(
() => props.modelValue,
(val) => {
syncFormFromModel(val)
// 回显已有合同ID时主动加载合同列表以便下拉显示合同名称后端已排除"其他申请"的合同,当前申请合同会在列表中)
if (form.hasContract === '1' && form.contractId && props.purchaseId && !contractLoaded.value && !contractLoading.value) {
loadContractOptions()
}
},
{ deep: true, immediate: true }
)
() => props.modelValue,
(val) => {
syncFormFromModel(val);
// 回显已有合同ID时主动加载合同列表以便下拉显示合同名称后端已排除"其他申请"的合同,当前申请合同会在列表中)
if (form.hasContract === '1' && form.contractId && props.purchaseId && !contractLoaded.value && !contractLoading.value) {
loadContractOptions();
}
},
{ deep: true, immediate: true }
);
// resetKey 变化时强制用 modelValue 覆盖内部 form并重置合同列表以便重新拉取
watch(() => props.resetKey, () => {
syncFormFromModel(props.modelValue)
contractLoaded.value = false
contractOptions.value = []
})
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
watch(
() => props.resetKey,
() => {
syncFormFromModel(props.modelValue);
contractLoaded.value = false;
contractOptions.value = [];
}
);
watch(form, () => emit('update:modelValue', { ...form }), { deep: true });
watch(() => form.hasContract, (val) => {
if (val === '1') {
contractLoaded.value = false
loadContractOptions()
} else {
contractOptions.value = []
contractLoaded.value = false
}
// hasContract 变化时触发 transactionAmount 校验
formRef.value?.validateField('transactionAmount')
})
watch(
() => form.hasContract,
(val) => {
if (val === '1') {
contractLoaded.value = false;
loadContractOptions();
} else {
contractOptions.value = [];
contractLoaded.value = false;
}
// hasContract 变化时触发 transactionAmount 校验
formRef.value?.validateField('transactionAmount');
}
);
// 选择合同后,自动带出合同供应商名称
watch(
() => form.contractId,
(val) => {
if (!val) {
form.supplierName = ''
return
}
const c = contractOptions.value.find((it: any) => it.id === val)
if (c && c.supplierName) {
form.supplierName = c.supplierName
}
}
)
() => form.contractId,
(val) => {
if (!val) {
form.supplierName = '';
return;
}
const c = contractOptions.value.find((it: any) => it.id === val);
if (c && c.supplierName) {
form.supplierName = c.supplierName;
}
}
);
onMounted(() => {
if (form.hasContract === '1') {
loadContractOptions()
}
})
if (form.hasContract === '1') {
loadContractOptions();
}
});
const rules: FormRules = {
hasContract: [{ required: true, message: '请选择是否签订合同', trigger: 'change' }],
isInstallment: [{ required: true, message: '请选择是否分期验收', trigger: 'change' }],
totalPhases: [{ required: true, message: '请输入分期次数', trigger: 'blur' }],
transactionAmount: [
{
validator: (rule: any, value: any, callback: any) => {
// 未签订合同时,成交金额为必填
if (form.hasContract === '0' && (value === null || value === undefined || value === '')) {
callback(new Error('未签订合同时,成交金额为必填'))
} else {
callback()
}
},
trigger: 'blur',
},
],
}
hasContract: [{ required: true, message: '请选择是否签订合同', trigger: 'change' }],
isInstallment: [{ required: true, message: '请选择是否分期验收', trigger: 'change' }],
totalPhases: [{ required: true, message: '请输入分期次数', trigger: 'blur' }],
transactionAmount: [
{
validator: (rule: any, value: any, callback: any) => {
// 未签订合同时,成交金额为必填
if (form.hasContract === '0' && (value === null || value === undefined || value === '')) {
callback(new Error('未签订合同时,成交金额为必填'));
} else {
callback();
}
},
trigger: 'blur',
},
],
};
const validate = () => formRef.value?.validate()
const validate = () => formRef.value?.validate();
defineExpose({ validate, form })
defineExpose({ validate, form });
</script>
<style scoped>
.mb20 {
margin-bottom: 20px;
margin-bottom: 20px;
}
.field-note {
font-size: 12px;
color: #999;
margin-top: 4px;
font-size: 12px;
color: #999;
margin-top: 4px;
}
</style>

View File

@@ -1,475 +1,456 @@
<template>
<el-dialog
v-model="visible"
title="履约验收"
width="75%"
:close-on-click-modal="false"
destroy-on-close
class="purchasing-accept-modal"
@close="handleClose"
>
<div v-loading="loading" class="modal-body" :key="String(purchaseId)">
<div class="main-tabs">
<div class="main-tab-nav">
<div
class="main-tab-item"
:class="{ active: mainTab === 'common' }"
@click="mainTab = 'common'"
>
公共信息
</div>
<div
class="main-tab-item"
:class="{ active: mainTab === 'batch' }"
@click="mainTab = 'batch'"
>
{{ commonForm?.isInstallment === '0' ? '验收' : '分期验收' }}{{ commonForm?.isInstallment !== '0' && batches.length > 0 ? ` (${batches.length})` : '' }}
</div>
</div>
<div class="main-tab-content">
<div v-show="mainTab === 'common'" class="tab-content">
<AcceptCommonForm
:key="`${purchaseId}-${openToken}`"
:reset-key="openToken"
ref="commonFormRef"
v-model="commonForm"
:purchase-id="purchaseId"
:project-name="applyInfo?.projectName"
:dept-name="applyInfo?.deptName"
/>
</div>
<div v-show="mainTab === 'batch'" class="tab-content">
<div v-if="batches.length > 0">
<div v-show="commonForm?.isInstallment !== '0'" class="batch-tabs">
<div
v-for="b in batches"
:key="b.id"
class="batch-tab-item"
:class="{ active: String(b.batch) === activeTab, disabled: !canEditBatch(b.batch) }"
@click="canEditBatch(b.batch) && (activeTab = String(b.batch))"
>
<span>{{ b.batch }}</span>
<el-tag v-if="isBatchCompleted(b)" type="success" size="small">已填</el-tag>
<el-tag v-else-if="!canEditBatch(b.batch)" type="info" size="small">需先完成上一期</el-tag>
</div>
</div>
<div class="batch-panel">
<AcceptBatchForm
v-for="b in batches"
v-show="String(b.batch) === activeTab"
:key="b.id"
:ref="(el) => setBatchFormRef(b.batch, el)"
:model-value="batchForms[b.batch]"
:readonly="false"
:purchase-id="String(purchaseId)"
:project-type="acceptProjectType"
:budget="applyInfo?.budget"
:batch-num="b.batch"
/>
</div>
</div>
<div v-else class="tip-box">
<el-alert type="info" :closable="false" show-icon>
请先在公共信息中填写并点击保存公共配置系统将按分期次数自动生成验收批次
</el-alert>
</div>
</div>
</div>
</div>
</div>
<el-dialog
v-model="visible"
title="履约验收"
width="75%"
:close-on-click-modal="false"
destroy-on-close
class="purchasing-accept-modal"
@close="handleClose"
>
<div v-loading="loading" class="modal-body" :key="String(purchaseId)">
<div class="main-tabs">
<div class="main-tab-nav">
<div class="main-tab-item" :class="{ active: mainTab === 'common' }" @click="mainTab = 'common'">公共信息</div>
<div class="main-tab-item" :class="{ active: mainTab === 'batch' }" @click="mainTab = 'batch'">
{{ commonForm?.isInstallment === '0' ? '验收' : '分期验收'
}}{{ commonForm?.isInstallment !== '0' && batches.length > 0 ? ` (${batches.length})` : '' }}
</div>
</div>
<div class="main-tab-content">
<div v-show="mainTab === 'common'" class="tab-content">
<AcceptCommonForm
:key="`${purchaseId}-${openToken}`"
:reset-key="openToken"
ref="commonFormRef"
v-model="commonForm"
:purchase-id="purchaseId"
:project-name="applyInfo?.projectName"
:dept-name="applyInfo?.deptName"
/>
</div>
<div v-show="mainTab === 'batch'" class="tab-content">
<div v-if="batches.length > 0">
<div v-show="commonForm?.isInstallment !== '0'" class="batch-tabs">
<div
v-for="b in batches"
:key="b.id"
class="batch-tab-item"
:class="{ active: String(b.batch) === activeTab, disabled: !canEditBatch(b.batch) }"
@click="canEditBatch(b.batch) && (activeTab = String(b.batch))"
>
<span>{{ b.batch }}</span>
<el-tag v-if="isBatchCompleted(b)" type="success" size="small">已填</el-tag>
<el-tag v-else-if="!canEditBatch(b.batch)" type="info" size="small">需先完成上一期</el-tag>
</div>
</div>
<div class="batch-panel">
<AcceptBatchForm
v-for="b in batches"
v-show="String(b.batch) === activeTab"
:key="b.id"
:ref="(el) => setBatchFormRef(b.batch, el)"
:model-value="batchForms[b.batch]"
:readonly="false"
:purchase-id="String(purchaseId)"
:project-type="acceptProjectType"
:budget="applyInfo?.budget"
:batch-num="b.batch"
/>
</div>
</div>
<div v-else class="tip-box">
<el-alert type="info" :closable="false" show-icon>
请先在公共信息中填写并点击保存公共配置系统将按分期次数自动生成验收批次
</el-alert>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<span>
<el-button @click="handleClose"> </el-button>
<el-button
v-if="mainTab === 'common' || batches.length === 0"
type="primary"
@click="saveCommonConfig"
:loading="saving"
>
保存公共配置
</el-button>
<el-button
v-else-if="mainTab === 'batch' && activeBatchId"
type="primary"
@click="saveCurrentBatch"
:loading="saving"
>
保存第{{ activeTab }}
</el-button>
</span>
</template>
</el-dialog>
<template #footer>
<span>
<el-button @click="handleClose"> </el-button>
<el-button v-if="mainTab === 'common' || batches.length === 0" type="primary" @click="saveCommonConfig" :loading="saving">
保存公共配置
</el-button>
<el-button v-else-if="mainTab === 'batch' && activeBatchId" type="primary" @click="saveCurrentBatch" :loading="saving">
保存第{{ activeTab }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import {
saveCommonConfig as apiSaveCommonConfig,
getCommonConfigWithBatches,
updateBatch,
getDetail,
} from '/@/api/purchase/purchasingAccept'
import AcceptCommonForm from './AcceptCommonForm.vue'
import AcceptBatchForm from './AcceptBatchForm.vue'
import { ref, reactive, computed, nextTick } from 'vue';
import { useMessage } from '/@/hooks/message';
import { saveCommonConfig as apiSaveCommonConfig, getCommonConfigWithBatches, updateBatch, getDetail } from '/@/api/purchase/purchasingAccept';
import AcceptCommonForm from './AcceptCommonForm.vue';
import AcceptBatchForm from './AcceptBatchForm.vue';
const emit = defineEmits(['refresh'])
const emit = defineEmits(['refresh']);
const visible = ref(false)
const loading = ref(false)
const saving = ref(false)
const purchaseId = ref<string | number>('')
const applyInfo = ref<any>(null)
const rowProjectType = ref<string>('A')
const batches = ref<any[]>([])
const mainTab = ref('common')
const activeTab = ref('1')
const commonFormRef = ref()
const batchFormRefMap = ref<Record<number, any>>({})
const visible = ref(false);
const loading = ref(false);
const saving = ref(false);
const purchaseId = ref<string | number>('');
const applyInfo = ref<any>(null);
const rowProjectType = ref<string>('A');
const batches = ref<any[]>([]);
const mainTab = ref('common');
const activeTab = ref('1');
const commonFormRef = ref();
const batchFormRefMap = ref<Record<number, any>>({});
/** 使用 ref 并在每次打开时替换整个对象,确保子组件能感知引用变化并清空 */
const commonForm = ref<Record<string, any>>({})
const commonForm = ref<Record<string, any>>({});
/** 每次打开自增,用于强制 AcceptCommonForm 重新挂载,确保公共信息彻底清空 */
const openToken = ref(0)
const batchForms = reactive<Record<number, any>>({})
const openToken = ref(0);
const batchForms = reactive<Record<number, any>>({});
/** 记录哪些期已保存到服务器,用于控制”下一期可填”:只有上一期已保存才允许填下一期 */
const batchSavedFlags = ref<Record<number, boolean>>({})
const batchSavedFlags = ref<Record<number, boolean>>({});
const setBatchFormRef = (batch: number, el: any) => {
if (el) batchFormRefMap.value[batch] = el
}
if (el) batchFormRefMap.value[batch] = el;
};
const activeBatchId = computed(() => {
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
return b?.id || ''
})
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value);
return b?.id || '';
});
/** 项目类型 A:货物 B:工程 C:服务,用于批次表单模版下载 */
const acceptProjectType = computed(() => applyInfo.value?.projectType || rowProjectType.value || 'A')
const acceptProjectType = computed(() => applyInfo.value?.projectType || rowProjectType.value || 'A');
/** 是否允许编辑该期:第 1 期始终可编辑;第 N 期仅当第 1N-1 期均已保存后才可编辑 */
const canEditBatch = (batch: number) => {
if (batch === 1) return true
for (let i = 1; i < batch; i++) {
if (!batchSavedFlags.value[i]) return false
}
return true
}
if (batch === 1) return true;
for (let i = 1; i < batch; i++) {
if (!batchSavedFlags.value[i]) return false;
}
return true;
};
/** 该期是否已保存(用于 tab 上显示“已填”标签) */
const isBatchCompleted = (b: any) => {
return !!batchSavedFlags.value[b.batch]
}
return !!batchSavedFlags.value[b.batch];
};
const isBatchCompletedByIdx = (batch: number) => {
return !!batchSavedFlags.value[batch]
}
return !!batchSavedFlags.value[batch];
};
const loadData = async () => {
if (!purchaseId.value) return
const currentId = String(purchaseId.value)
loading.value = true
try {
const configRes = await getCommonConfigWithBatches(currentId)
// 防止快速切换:若已打开其他申请单,忽略本次结果
if (String(purchaseId.value) !== currentId) return
if (!purchaseId.value) return;
const currentId = String(purchaseId.value);
loading.value = true;
try {
const configRes = await getCommonConfigWithBatches(currentId);
// 防止快速切换:若已打开其他申请单,忽略本次结果
if (String(purchaseId.value) !== currentId) return;
const config = configRes?.data
const config = configRes?.data;
if (config?.common) {
applyInfo.value = config.common
// 采购人员和资产管理员始终回填
commonForm.value.purchaserId = config.common.purchaserId || ''
commonForm.value.purchaserName = config.common.purchaserName || ''
commonForm.value.assetAdminId = config.common.assetAdminId || ''
commonForm.value.assetAdminName = config.common.assetAdminName || ''
// 其他字段仅当存在已保存批次时回填
if (config?.batches?.length) {
Object.assign(commonForm.value, {
hasContract: config.common.hasContract || '0',
contractId: config.common.contractId || '',
isInstallment: config.common.isInstallment || '0',
totalPhases: config.common.totalPhases || 1,
supplierName: config.common.supplierName || '',
transactionAmount: config.common.transactionAmount || null,
})
}
}
if (config?.common) {
applyInfo.value = config.common;
// 采购人员和资产管理员始终回填
commonForm.value.purchaserId = config.common.purchaserId || '';
commonForm.value.purchaserName = config.common.purchaserName || '';
commonForm.value.assetAdminId = config.common.assetAdminId || '';
commonForm.value.assetAdminName = config.common.assetAdminName || '';
if (config?.batches?.length) {
batches.value = config.batches.sort((a: any, b: any) => (a.batch || 0) - (b.batch || 0))
activeTab.value = String(batches.value[0]?.batch || '1')
mainTab.value = 'batch'
for (const b of batches.value) {
if (!batchForms[b.batch]) batchForms[b.batch] = {}
}
await loadBatchDetails()
if (String(purchaseId.value) !== currentId) return
} else {
batches.value = []
}
} catch (e: any) {
useMessage().error(e?.msg || '加载失败')
} finally {
loading.value = false
}
}
// 其他字段仅当存在已保存批次时回填
if (config?.batches?.length) {
Object.assign(commonForm.value, {
hasContract: config.common.hasContract || '0',
contractId: config.common.contractId || '',
isInstallment: config.common.isInstallment || '0',
totalPhases: config.common.totalPhases || 1,
supplierName: config.common.supplierName || '',
transactionAmount: config.common.transactionAmount || null,
});
}
}
if (config?.batches?.length) {
batches.value = config.batches.sort((a: any, b: any) => (a.batch || 0) - (b.batch || 0));
activeTab.value = String(batches.value[0]?.batch || '1');
mainTab.value = 'batch';
for (const b of batches.value) {
if (!batchForms[b.batch]) batchForms[b.batch] = {};
}
await loadBatchDetails();
if (String(purchaseId.value) !== currentId) return;
} else {
batches.value = [];
}
} catch (e: any) {
useMessage().error(e?.msg || '加载失败');
} finally {
loading.value = false;
}
};
const loadBatchDetails = async () => {
for (const b of batches.value) {
batchSavedFlags.value[b.batch] = false
}
for (const b of batches.value) {
try {
const res = await getDetail(String(purchaseId.value), b.batch)
const d = res?.data
if (d?.accept) {
// 仅当该期在服务端有验收日期时才视为已保存
const hasSaved = !!d.accept.acceptDate
batchSavedFlags.value[b.batch] = hasSaved
// 优先使用 templateFiles包含id和fileTitle否则降级使用 templateFileIds
let fileIdsStr = ''
if (d.accept.templateFiles && d.accept.templateFiles.length > 0) {
// 使用 templateFiles格式为 {id: string, fileTitle: string}[]
fileIdsStr = d.accept.templateFiles.map((f: any) => f.id).join(',')
} else if (d.accept.templateFileIds) {
// 降级使用 templateFileIds
const fileIds = d.accept.templateFileIds
fileIdsStr = Array.isArray(fileIds) ? fileIds.join(',') : (fileIds || '')
}
batchForms[b.batch] = {
acceptType: '2', // 固定为上传模式
acceptDate: d.accept.acceptDate || '',
remark: d.accept.remark || '',
templateFileIds: fileIdsStr,
// 保存文件信息用于显示
_templateFiles: d.accept.templateFiles || [],
}
// 通知子组件初始化数据
await nextTick()
const batchFormRef = batchFormRefMap.value[b.batch]
if (batchFormRef?.initData) {
batchFormRef.initData()
}
}
} catch (_) {}
}
}
for (const b of batches.value) {
batchSavedFlags.value[b.batch] = false;
}
for (const b of batches.value) {
try {
const res = await getDetail(String(purchaseId.value), b.batch);
const d = res?.data;
if (d?.accept) {
// 仅当该期在服务端有验收日期时才视为已保存
const hasSaved = !!d.accept.acceptDate;
batchSavedFlags.value[b.batch] = hasSaved;
// 优先使用 templateFiles包含id和fileTitle否则降级使用 templateFileIds
let fileIdsStr = '';
if (d.accept.templateFiles && d.accept.templateFiles.length > 0) {
// 使用 templateFiles格式为 {id: string, fileTitle: string}[]
fileIdsStr = d.accept.templateFiles.map((f: any) => f.id).join(',');
} else if (d.accept.templateFileIds) {
// 降级使用 templateFileIds
const fileIds = d.accept.templateFileIds;
fileIdsStr = Array.isArray(fileIds) ? fileIds.join(',') : fileIds || '';
}
batchForms[b.batch] = {
acceptType: '2', // 固定为上传模式
acceptDate: d.accept.acceptDate || '',
remark: d.accept.remark || '',
templateFileIds: fileIdsStr,
// 保存文件信息用于显示
_templateFiles: d.accept.templateFiles || [],
};
// 通知子组件初始化数据
await nextTick();
const batchFormRef = batchFormRefMap.value[b.batch];
if (batchFormRef?.initData) {
batchFormRef.initData();
}
}
} catch (_) {}
}
};
const saveCommonConfig = async () => {
const formRef = commonFormRef.value
const valid = await formRef?.validate?.().catch(() => false)
if (!valid) return
// 直接从子组件 form 读取,确保拿到用户填写的最新值(避免 v-model 同步延迟)
const form = formRef?.form || commonForm.value
const isInstallment = form.isInstallment === '1' || form.isInstallment === 1
if (isInstallment && (!form.totalPhases || form.totalPhases < 1)) {
useMessage().error('请填写分期次数')
return
}
saving.value = true
try {
await apiSaveCommonConfig({
purchaseId: String(purchaseId.value),
hasContract: form.hasContract ?? '0',
contractId: form.contractId ?? '',
isInstallment: form.isInstallment ?? '0',
totalPhases: isInstallment ? (Number(form.totalPhases) || 1) : 1,
supplierName: String(form.supplierName ?? ''),
purchaserId: String(form.purchaserId ?? ''),
purchaserName: String(form.purchaserName ?? ''),
assetAdminId: String(form.assetAdminId ?? ''),
assetAdminName: String(form.assetAdminName ?? ''),
transactionAmount: form.transactionAmount ?? null,
})
useMessage().success('保存成功')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saving.value = false
}
}
const formRef = commonFormRef.value;
const valid = await formRef?.validate?.().catch(() => false);
if (!valid) return;
// 直接从子组件 form 读取,确保拿到用户填写的最新值(避免 v-model 同步延迟)
const form = formRef?.form || commonForm.value;
const isInstallment = form.isInstallment === '1' || form.isInstallment === 1;
if (isInstallment && (!form.totalPhases || form.totalPhases < 1)) {
useMessage().error('请填写分期次数');
return;
}
saving.value = true;
try {
await apiSaveCommonConfig({
purchaseId: String(purchaseId.value),
hasContract: form.hasContract ?? '0',
contractId: form.contractId ?? '',
isInstallment: form.isInstallment ?? '0',
totalPhases: isInstallment ? Number(form.totalPhases) || 1 : 1,
supplierName: String(form.supplierName ?? ''),
purchaserId: String(form.purchaserId ?? ''),
purchaserName: String(form.purchaserName ?? ''),
assetAdminId: String(form.assetAdminId ?? ''),
assetAdminName: String(form.assetAdminName ?? ''),
transactionAmount: form.transactionAmount ?? null,
});
useMessage().success('保存成功');
await loadData();
} catch (e: any) {
useMessage().error(e?.msg || '保存失败');
} finally {
saving.value = false;
}
};
const saveCurrentBatch = async () => {
const curBatch = Number(activeTab.value)
const batchFormRef = batchFormRefMap.value[curBatch]
const valid = await batchFormRef?.validate?.().catch(() => false)
if (!valid) return
const curBatch = Number(activeTab.value);
const batchFormRef = batchFormRefMap.value[curBatch];
const valid = await batchFormRef?.validate?.().catch(() => false);
if (!valid) return;
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
if (!b?.id) return
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value);
if (!b?.id) return;
// 从子组件获取表单数据
const formData = batchFormRef?.getFormData?.() || batchFormRef?.form
if (!formData) return
// 从子组件获取表单数据
const formData = batchFormRef?.getFormData?.() || batchFormRef?.form;
if (!formData) return;
// acceptDate is now optional - removed the validation check
// acceptDate is now optional - removed the validation check
// templateFileIds: 提取ID数组
let fileIds: string[] = []
if (formData.templateFileIds) {
if (Array.isArray(formData.templateFileIds)) {
fileIds = formData.templateFileIds.map((item: any) => {
if (typeof item === 'string') return item
if (item && item.id) return item.id
return null
}).filter(Boolean)
} else if (typeof formData.templateFileIds === 'string') {
fileIds = formData.templateFileIds.split(',').map((s: string) => s.trim()).filter(Boolean)
}
}
// templateFileIds: 提取ID数组
let fileIds: string[] = [];
if (formData.templateFileIds) {
if (Array.isArray(formData.templateFileIds)) {
fileIds = formData.templateFileIds
.map((item: any) => {
if (typeof item === 'string') return item;
if (item && item.id) return item.id;
return null;
})
.filter(Boolean);
} else if (typeof formData.templateFileIds === 'string') {
fileIds = formData.templateFileIds
.split(',')
.map((s: string) => s.trim())
.filter(Boolean);
}
}
saving.value = true
try {
await updateBatch({
id: b.id,
purchaseId: String(purchaseId.value),
acceptType: '2', // 固定为上传模式
acceptDate: formData.acceptDate,
remark: formData.remark,
templateFileIds: fileIds,
})
useMessage().success('保存成功')
batchSavedFlags.value[curBatch] = true
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saving.value = false
}
}
saving.value = true;
try {
await updateBatch({
id: b.id,
purchaseId: String(purchaseId.value),
acceptType: '2', // 固定为上传模式
acceptDate: formData.acceptDate,
remark: formData.remark,
templateFileIds: fileIds,
});
useMessage().success('保存成功');
batchSavedFlags.value[curBatch] = true;
await loadData();
} catch (e: any) {
useMessage().error(e?.msg || '保存失败');
} finally {
saving.value = false;
}
};
const handleClose = () => {
visible.value = false
emit('refresh')
}
visible.value = false;
emit('refresh');
};
const DEFAULT_COMMON_FORM = {
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
supplierName: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
transactionAmount: null,
}
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
supplierName: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
transactionAmount: null,
};
/** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */
const resetAllToDefault = () => {
openToken.value++
commonForm.value = { ...DEFAULT_COMMON_FORM }
applyInfo.value = null
mainTab.value = 'common'
activeTab.value = '1'
batchFormRefMap.value = {}
batches.value = []
Object.keys(batchForms).forEach((k) => delete batchForms[Number(k)])
batchSavedFlags.value = {}
}
openToken.value++;
commonForm.value = { ...DEFAULT_COMMON_FORM };
applyInfo.value = null;
mainTab.value = 'common';
activeTab.value = '1';
batchFormRefMap.value = {};
batches.value = [];
Object.keys(batchForms).forEach((k) => delete batchForms[Number(k)]);
batchSavedFlags.value = {};
};
const open = async (row: any) => {
purchaseId.value = row?.id ?? ''
rowProjectType.value = row?.projectType || 'A'
purchaseId.value = row?.id ?? '';
rowProjectType.value = row?.projectType || 'A';
// 1. 先将弹窗内所有内容恢复为初始空值
resetAllToDefault()
// 1. 先将弹窗内所有内容恢复为初始空值
resetAllToDefault();
// 2. 显示弹窗并开启 loading避免接口返回前展示旧数据
visible.value = true
loading.value = true
// 2. 显示弹窗并开启 loading避免接口返回前展示旧数据
visible.value = true;
loading.value = true;
// 3. 等待 Vue 完成渲染,确保子组件已接收并展示空值
await nextTick()
await nextTick()
// 3. 等待 Vue 完成渲染,确保子组件已接收并展示空值
await nextTick();
await nextTick();
// 4. 再进行接口查询并覆盖
await loadData()
}
// 4. 再进行接口查询并覆盖
await loadData();
};
defineExpose({ open })
defineExpose({ open });
</script>
<style scoped>
.modal-body {
padding: 0;
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
padding: 0;
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
}
.main-tab-nav {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid var(--el-border-color);
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid var(--el-border-color);
}
.main-tab-item {
padding: 12px 20px;
cursor: pointer;
color: var(--el-text-color-regular);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
padding: 12px 20px;
cursor: pointer;
color: var(--el-text-color-regular);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
}
.main-tab-item:hover {
color: var(--el-color-primary);
color: var(--el-color-primary);
}
.main-tab-item.active {
color: var(--el-color-primary);
font-weight: 600;
border-bottom-color: var(--el-color-primary);
color: var(--el-color-primary);
font-weight: 600;
border-bottom-color: var(--el-color-primary);
}
.main-tab-content {
padding-top: 4px;
padding-top: 4px;
}
.tab-content {
min-height: 200px;
display: block;
min-height: 200px;
display: block;
}
.tip-box {
padding: 20px 0;
padding: 20px 0;
}
.batch-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.batch-tab-item {
padding: 8px 16px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
padding: 8px 16px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.batch-tab-item:hover:not(.disabled) {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.batch-tab-item.active {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #fff;
background: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #fff;
}
.batch-tab-item.disabled {
cursor: not-allowed;
opacity: 0.6;
cursor: not-allowed;
opacity: 0.6;
}
.batch-panel {
min-height: 200px;
min-height: 200px;
}
</style>
<style>
/* 弹窗横向滚动修复,需非 scoped 以影响 el-dialog */
.purchasing-accept-modal .el-dialog__body {
overflow-x: hidden;
overflow-x: hidden;
}
</style>

View File

@@ -1,229 +1,206 @@
<template>
<el-dialog
v-model="state.visible"
:title="state.title"
width="700px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
@close="handleClose">
<el-form
ref="formRef"
:model="state.formData"
:rules="rules"
label-width="140px">
<el-form-item label="合同编号" prop="contractNo">
<el-input
v-model="state.formData.contractNo"
placeholder="请输入合同编号"
:disabled="state.operation === 'view'" />
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
<el-input
v-model="state.formData.contractName"
placeholder="请输入合同名称"
:disabled="state.operation === 'view'" />
</el-form-item>
<el-form-item label="合同金额(元)" prop="money">
<el-input-number
v-model="state.formData.money"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入合同金额"
style="width: 100%"
:disabled="state.operation === 'view'" />
</el-form-item>
<el-form-item label="是否需要招标" prop="isBidding">
<el-radio-group
v-model="state.formData.isBidding"
:disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否需要法律顾问" prop="isLegalAdviser">
<el-radio-group
v-model="state.formData.isLegalAdviser"
:disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="state.formData.isLegalAdviser === '1'"
label="法律顾问意见"
prop="legalAdviserOpinion">
<el-input
v-model="state.formData.legalAdviserOpinion"
type="textarea"
:rows="3"
placeholder="请输入法律顾问意见"
:disabled="state.operation === 'view'" />
</el-form-item>
<el-form-item label="是否涉及多个部门" prop="isDepts">
<el-radio-group
v-model="state.formData.isDepts"
:disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否全校合同" prop="isSchool">
<el-radio-group
v-model="state.formData.isSchool"
:disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input
v-model="state.formData.remarks"
type="textarea"
:rows="2"
placeholder="请输入备注"
:disabled="state.operation === 'view'" />
</el-form-item>
</el-form>
<template #footer v-if="state.operation !== 'view'">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="state.loading">确定</el-button>
</template>
<template #footer v-else>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
<el-dialog
v-model="state.visible"
:title="state.title"
width="700px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
@close="handleClose"
>
<el-form ref="formRef" :model="state.formData" :rules="rules" label-width="140px">
<el-form-item label="合同编号" prop="contractNo">
<el-input v-model="state.formData.contractNo" placeholder="请输入合同编号" :disabled="state.operation === 'view'" />
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
<el-input v-model="state.formData.contractName" placeholder="请输入合同名称" :disabled="state.operation === 'view'" />
</el-form-item>
<el-form-item label="合同金额(元)" prop="money">
<el-input-number
v-model="state.formData.money"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入合同金额"
style="width: 100%"
:disabled="state.operation === 'view'"
/>
</el-form-item>
<el-form-item label="是否需要招标" prop="isBidding">
<el-radio-group v-model="state.formData.isBidding" :disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否需要法律顾问" prop="isLegalAdviser">
<el-radio-group v-model="state.formData.isLegalAdviser" :disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="state.formData.isLegalAdviser === '1'" label="法律顾问意见" prop="legalAdviserOpinion">
<el-input
v-model="state.formData.legalAdviserOpinion"
type="textarea"
:rows="3"
placeholder="请输入法律顾问意见"
:disabled="state.operation === 'view'"
/>
</el-form-item>
<el-form-item label="是否涉及多个部门" prop="isDepts">
<el-radio-group v-model="state.formData.isDepts" :disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否全校合同" prop="isSchool">
<el-radio-group v-model="state.formData.isSchool" :disabled="state.operation === 'view'">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="state.formData.remarks" type="textarea" :rows="2" placeholder="请输入备注" :disabled="state.operation === 'view'" />
</el-form-item>
</el-form>
<template #footer v-if="state.operation !== 'view'">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="state.loading">确定</el-button>
</template>
<template #footer v-else>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ContractDialog">
import { ref, reactive, computed } from 'vue'
import { addObj, editObj } from "/@/api/purchase/purchasingcontract";
import { useMessage } from "/@/hooks/message";
import { ref, reactive, computed } from 'vue';
import { addObj, editObj } from '/@/api/purchase/purchasingcontract';
import { useMessage } from '/@/hooks/message';
const emit = defineEmits(['refresh']);
const formRef = ref()
const formRef = ref();
const state = reactive({
visible: false,
loading: false,
operation: 'add' as 'add' | 'edit' | 'view',
title: computed(() => {
return state.operation === 'add' ? '新增合同' : state.operation === 'edit' ? '编辑合同' : '合同详情';
}),
formData: {
id: '',
contractNo: '',
contractName: '',
money: 0,
purchaseId: '',
isBidding: '0',
isLegalAdviser: '0',
legalAdviserOpinion: '',
isDepts: '0',
isSchool: '0',
remarks: ''
},
purchaseNo: ''
visible: false,
loading: false,
operation: 'add' as 'add' | 'edit' | 'view',
title: computed(() => {
return state.operation === 'add' ? '新增合同' : state.operation === 'edit' ? '编辑合同' : '合同详情';
}),
formData: {
id: '',
contractNo: '',
contractName: '',
money: 0,
purchaseId: '',
isBidding: '0',
isLegalAdviser: '0',
legalAdviserOpinion: '',
isDepts: '0',
isSchool: '0',
remarks: '',
},
purchaseNo: '',
});
const rules = {
contractNo: [{ required: true, message: '请输入合同编号', trigger: 'blur' }],
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
money: [{ required: true, message: '请输入合同金额', trigger: 'blur' }],
isBidding: [{ required: true, message: '请选择是否需要招标', trigger: 'change' }],
isLegalAdviser: [{ required: true, message: '请选择是否需要法律顾问', trigger: 'change' }],
isDepts: [{ required: true, message: '请选择是否涉及多部门', trigger: 'change' }],
isSchool: [{ required: true, message: '请选择是否全校合同', trigger: 'change' }]
contractNo: [{ required: true, message: '请输入合同编号', trigger: 'blur' }],
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
money: [{ required: true, message: '请输入合同金额', trigger: 'blur' }],
isBidding: [{ required: true, message: '请选择是否需要招标', trigger: 'change' }],
isLegalAdviser: [{ required: true, message: '请选择是否需要法律顾问', trigger: 'change' }],
isDepts: [{ required: true, message: '请选择是否涉及多部门', trigger: 'change' }],
isSchool: [{ required: true, message: '请选择是否全校合同', trigger: 'change' }],
};
const openDialog = (purchaseApply: any, contract: any) => {
state.visible = true;
state.loading = false;
state.purchaseNo = purchaseApply?.purchaseNo || '';
state.formData.purchaseId = purchaseApply?.id || purchaseApply?.purchaseId || '';
if (contract && contract.id) {
state.operation = 'edit';
state.formData = {
id: contract.id,
contractNo: contract.contractNo || '',
contractName: contract.contractName || '',
money: contract.money || 0,
purchaseId: contract.purchaseId || state.formData.purchaseId,
isBidding: contract.isBidding || '0',
isLegalAdviser: contract.isLegalAdviser || '0',
legalAdviserOpinion: contract.legalAdviserOpinion || '',
isDepts: contract.isDepts || '0',
isSchool: contract.isSchool || '0',
remarks: contract.remarks || ''
};
} else {
state.operation = 'add';
state.formData = {
id: '',
contractNo: '',
contractName: '',
money: 0,
purchaseId: state.formData.purchaseId,
isBidding: '0',
isLegalAdviser: '0',
legalAdviserOpinion: '',
isDepts: '0',
isSchool: '0',
remarks: ''
};
}
state.visible = true;
state.loading = false;
state.purchaseNo = purchaseApply?.purchaseNo || '';
state.formData.purchaseId = purchaseApply?.id || purchaseApply?.purchaseId || '';
if (contract && contract.id) {
state.operation = 'edit';
state.formData = {
id: contract.id,
contractNo: contract.contractNo || '',
contractName: contract.contractName || '',
money: contract.money || 0,
purchaseId: contract.purchaseId || state.formData.purchaseId,
isBidding: contract.isBidding || '0',
isLegalAdviser: contract.isLegalAdviser || '0',
legalAdviserOpinion: contract.legalAdviserOpinion || '',
isDepts: contract.isDepts || '0',
isSchool: contract.isSchool || '0',
remarks: contract.remarks || '',
};
} else {
state.operation = 'add';
state.formData = {
id: '',
contractNo: '',
contractName: '',
money: 0,
purchaseId: state.formData.purchaseId,
isBidding: '0',
isLegalAdviser: '0',
legalAdviserOpinion: '',
isDepts: '0',
isSchool: '0',
remarks: '',
};
}
};
const handleSubmit = async () => {
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) return;
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) return;
try {
state.loading = true;
if (state.operation === 'add') {
await addObj(state.formData);
useMessage().success('新增成功');
} else {
await editObj(state.formData);
useMessage().success('修改成功');
}
handleClose();
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg || '操作失败');
} finally {
state.loading = false;
}
try {
state.loading = true;
if (state.operation === 'add') {
await addObj(state.formData);
useMessage().success('新增成功');
} else {
await editObj(state.formData);
useMessage().success('修改成功');
}
handleClose();
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg || '操作失败');
} finally {
state.loading = false;
}
};
const handleClose = () => {
state.visible = false;
formRef.value?.resetFields();
state.formData = {
id: '',
contractNo: '',
contractName: '',
money: 0,
purchaseId: '',
isBidding: '0',
isLegalAdviser: '0',
legalAdviserOpinion: '',
isDepts: '0',
isSchool: '0',
remarks: ''
};
state.visible = false;
formRef.value?.resetFields();
state.formData = {
id: '',
contractNo: '',
contractName: '',
money: 0,
purchaseId: '',
isBidding: '0',
isLegalAdviser: '0',
legalAdviserOpinion: '',
isDepts: '0',
isSchool: '0',
remarks: '',
};
};
defineExpose({
openDialog
openDialog,
});
</script>
<style scoped>
:deep(.el-dialog__body) {
padding: 20px;
padding: 20px;
}
</style>
</style>

View File

@@ -1,65 +1,69 @@
<template>
<el-table :data="records" stripe v-loading="loading" max-height="400">
<el-table-column prop="operateTypeDesc" label="操作类型" width="100" align="center">
<template #default="scope">
<el-tag :type="getOperateTypeStyle(scope.row.operateType)">
{{ scope.row.operateTypeDesc }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operateRoleDesc" label="操作角色" width="100" />
<el-table-column prop="operateByName" label="操作人" width="100" />
<el-table-column prop="currentVersion" label="文件版本" width="80" align="center" />
<el-table-column prop="remark" label="批注意见" min-width="200" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.remark || '-' }}
</template>
</el-table-column>
<el-table-column prop="operateTime" label="操作时间" width="160" />
</el-table>
<el-table :data="records" stripe v-loading="loading" max-height="400">
<el-table-column prop="operateTypeDesc" label="操作类型" width="100" align="center">
<template #default="scope">
<el-tag :type="getOperateTypeStyle(scope.row.operateType)">
{{ scope.row.operateTypeDesc }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operateRoleDesc" label="操作角色" width="100" />
<el-table-column prop="operateByName" label="操作人" width="100" />
<el-table-column prop="currentVersion" label="文件版本" width="80" align="center" />
<el-table-column prop="remark" label="批注意见" min-width="200" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.remark || '-' }}
</template>
</el-table-column>
<el-table-column prop="operateTime" label="操作时间" width="160" />
</el-table>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { getAuditRecords } from '/@/api/purchase/docProcess'
import { ref, watch } from 'vue';
import { getAuditRecords } from '/@/api/purchase/docProcess';
const props = defineProps<{
applyId: number | string
}>()
applyId: number | string;
}>();
const records = ref<any[]>([])
const loading = ref(false)
const records = ref<any[]>([]);
const loading = ref(false);
const loadRecords = async () => {
if (!props.applyId) return
loading.value = true
try {
const res = await getAuditRecords(props.applyId)
records.value = res.data || []
} catch (e) {
records.value = []
} finally {
loading.value = false
}
}
if (!props.applyId) return;
loading.value = true;
try {
const res = await getAuditRecords(props.applyId);
records.value = res.data || [];
} catch (e) {
records.value = [];
} finally {
loading.value = false;
}
};
const refresh = () => {
loadRecords()
}
loadRecords();
};
const getOperateTypeStyle = (type: string) => {
const styleMap: Record<string, string> = {
'UPLOAD': 'primary',
'CONFIRM': 'success',
'RETURN': 'warning',
'COMPLETE': 'success'
}
return styleMap[type] || 'info'
}
const styleMap: Record<string, string> = {
UPLOAD: 'primary',
CONFIRM: 'success',
RETURN: 'warning',
COMPLETE: 'success',
};
return styleMap[type] || 'info';
};
watch(() => props.applyId, () => {
loadRecords()
}, { immediate: true })
watch(
() => props.applyId,
() => {
loadRecords();
},
{ immediate: true }
);
defineExpose({ refresh })
</script>
defineExpose({ refresh });
</script>

View File

@@ -1,402 +1,397 @@
<template>
<div class="reviewer-setting">
<el-form :model="formData" label-width="100px" v-loading="loading">
<!-- 当前采购代表 -->
<el-form-item label="当前代表" v-if="currentRepresentor.teacherName">
<el-tag type="success">{{ currentRepresentor.teacherName }} ({{ currentRepresentor.teacherNo }})</el-tag>
<el-tag v-if="currentRepresentor.representorType" type="info" style="margin-left: 8px;">{{ getRepresentorTypeLabel(currentRepresentor.representorType) }}</el-tag>
</el-form-item>
<div class="reviewer-setting">
<el-form :model="formData" label-width="100px" v-loading="loading">
<!-- 当前采购代表 -->
<el-form-item label="当前代表" v-if="currentRepresentor.teacherName">
<el-tag type="success">{{ currentRepresentor.teacherName }} ({{ currentRepresentor.teacherNo }})</el-tag>
<el-tag v-if="currentRepresentor.representorType" type="info" style="margin-left: 8px">{{
getRepresentorTypeLabel(currentRepresentor.representorType)
}}</el-tag>
</el-form-item>
<!-- 选择方式 -->
<el-form-item label="选择方式">
<el-radio-group v-model="formData.selectMode" @change="handleSelectModeChange">
<el-radio label="DESIGNATED">指定人员</el-radio>
<el-radio label="RANDOM">随机抽取</el-radio>
</el-radio-group>
</el-form-item>
<!-- 选择方式 -->
<el-form-item label="选择方式">
<el-radio-group v-model="formData.selectMode" @change="handleSelectModeChange">
<el-radio label="DESIGNATED">指定人员</el-radio>
<el-radio label="RANDOM">随机抽取</el-radio>
</el-radio-group>
</el-form-item>
<!-- 指定人员模式 -->
<el-form-item v-if="formData.selectMode === 'DESIGNATED'" label="选择人员" required>
<org-selector
v-model:orgList="selectedUserList"
type="user"
:multiple="false"
@update:orgList="handleUserChange" />
</el-form-item>
<!-- 指定人员模式 -->
<el-form-item v-if="formData.selectMode === 'DESIGNATED'" label="选择人员" required>
<org-selector v-model:orgList="selectedUserList" type="user" :multiple="false" @update:orgList="handleUserChange" />
</el-form-item>
<!-- 随机抽取模式 -->
<template v-if="formData.selectMode === 'RANDOM'">
<el-form-item label="选择候选人" required>
<org-selector
v-model:orgList="candidateUserList"
type="user"
:multiple="true"
@update:orgList="handleCandidateChange" />
</el-form-item>
<!-- 随机抽取模式 -->
<template v-if="formData.selectMode === 'RANDOM'">
<el-form-item label="选择候选人" required>
<org-selector v-model:orgList="candidateUserList" type="user" :multiple="true" @update:orgList="handleCandidateChange" />
</el-form-item>
<!-- 候选人列表 -->
<el-form-item label="候选人列表" v-if="candidates.length > 0">
<el-table :data="candidates" stripe size="small" max-height="200">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="teacherNo" label="工号" width="120" />
<el-table-column prop="teacherName" label="姓名" />
</el-table>
</el-form-item>
<!-- 候选人列表 -->
<el-form-item label="候选人列表" v-if="candidates.length > 0">
<el-table :data="candidates" stripe size="small" max-height="200">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="teacherNo" label="工号" width="120" />
<el-table-column prop="teacherName" label="姓名" />
</el-table>
</el-form-item>
<!-- 随机抽取结果 -->
<el-form-item label="抽取结果">
<div class="random-roller">
<span v-if="rollingName" class="rolling">{{ rollingName }}</span>
<span v-else-if="selectedCandidate.teacherName" class="selected">
已抽取{{ selectedCandidate.teacherName }} ({{ selectedCandidate.teacherNo }})
</span>
<span v-else class="placeholder">点击下方按钮进行随机抽取</span>
</div>
</el-form-item>
<!-- 随机抽取结果 -->
<el-form-item label="抽取结果">
<div class="random-roller">
<span v-if="rollingName" class="rolling">{{ rollingName }}</span>
<span v-else-if="selectedCandidate.teacherName" class="selected">
已抽取{{ selectedCandidate.teacherName }} ({{ selectedCandidate.teacherNo }})
</span>
<span v-else class="placeholder">点击下方按钮进行随机抽取</span>
</div>
</el-form-item>
<!-- 随机抽取按钮 -->
<el-form-item v-if="candidates.length > 1">
<el-button type="primary" :loading="rolling" @click="handleRandomSelect">
{{ rolling ? '抽取中...' : '随机抽取' }}
</el-button>
</el-form-item>
</template>
<!-- 随机抽取按钮 -->
<el-form-item v-if="candidates.length > 1">
<el-button type="primary" :loading="rolling" @click="handleRandomSelect">
{{ rolling ? '抽取中...' : '随机抽取' }}
</el-button>
</el-form-item>
</template>
<!-- 人员类型选择 -->
<el-form-item label="人员类型">
<el-select v-model="formData.representorType" placeholder="请选择人员类型" clearable style="width: 200px;">
<el-option
v-for="item in representorTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<!-- 人员类型选择 -->
<el-form-item label="人员类型">
<el-select v-model="formData.representorType" placeholder="请选择人员类型" clearable style="width: 200px">
<el-option v-for="item in representorTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<!-- 保存按钮 -->
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="!canSave" @click="handleSave">
保存设置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 保存按钮 -->
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="!canSave" @click="handleSave"> 保存设置 </el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts" name="ReviewerSetting">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { useDict } from '/@/hooks/dict'
import { getReviewerSetting, setReviewerSetting, randomSelectReviewer } from '/@/api/purchase/docProcess'
import orgSelector from '/@/components/OrgSelector/index.vue'
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useMessage } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { getReviewerSetting, setReviewerSetting, randomSelectReviewer } from '/@/api/purchase/docProcess';
import orgSelector from '/@/components/OrgSelector/index.vue';
const props = defineProps<{
applyId: string | number
}>()
applyId: string | number;
}>();
const emit = defineEmits<{
(e: 'saved'): void
}>()
(e: 'saved'): void;
}>();
// 常量
const SELECT_MODE = {
DESIGNATED: 'DESIGNATED',
RANDOM: 'RANDOM',
} as const
DESIGNATED: 'DESIGNATED',
RANDOM: 'RANDOM',
} as const;
// 字典数据
const { PURCHASE_REPRESENTOR_TYPE: representorTypeDict } = useDict('PURCHASE_REPRESENTOR_TYPE')
const { PURCHASE_REPRESENTOR_TYPE: representorTypeDict } = useDict('PURCHASE_REPRESENTOR_TYPE');
// 人员类型选项
const representorTypeOptions = computed(() => {
return (representorTypeDict.value || []).map((item: any) => ({
value: item.value,
label: item.label
}))
})
return (representorTypeDict.value || []).map((item: any) => ({
value: item.value,
label: item.label,
}));
});
// 表单数据
const formData = ref({
selectMode: 'DESIGNATED' as 'DESIGNATED' | 'RANDOM',
teacherNo: '',
teacherName: '',
candidates: [] as Array<{ teacherNo: string; teacherName: string }>,
representorType: ''
})
selectMode: 'DESIGNATED' as 'DESIGNATED' | 'RANDOM',
teacherNo: '',
teacherName: '',
candidates: [] as Array<{ teacherNo: string; teacherName: string }>,
representorType: '',
});
// 当前采购代表(已有设置)
const currentRepresentor = ref({
teacherNo: '',
teacherName: '',
representorType: ''
})
teacherNo: '',
teacherName: '',
representorType: '',
});
// 用户选择相关
const selectedUserList = ref<any[]>([])
const candidateUserList = ref<any[]>([])
const selectedUserList = ref<any[]>([]);
const candidateUserList = ref<any[]>([]);
// 候选人列表
const candidates = ref<Array<{ teacherNo: string; teacherName: string }>>([])
const candidates = ref<Array<{ teacherNo: string; teacherName: string }>>([]);
// 随机抽取相关
const rolling = ref(false)
const rollingName = ref('')
const selectedCandidate = ref<{ teacherNo: string; teacherName: string }>({ teacherNo: '', teacherName: '' })
let rollInterval: ReturnType<typeof setInterval> | null = null
const rolling = ref(false);
const rollingName = ref('');
const selectedCandidate = ref<{ teacherNo: string; teacherName: string }>({ teacherNo: '', teacherName: '' });
let rollInterval: ReturnType<typeof setInterval> | null = null;
// 加载状态
const loading = ref(false)
const saving = ref(false)
const loading = ref(false);
const saving = ref(false);
// 是否可以保存
const canSave = computed(() => {
if (formData.value.selectMode === SELECT_MODE.DESIGNATED) {
return formData.value.teacherNo && formData.value.teacherName
} else {
return formData.value.teacherNo && formData.value.teacherName && candidates.value.length > 0
}
})
if (formData.value.selectMode === SELECT_MODE.DESIGNATED) {
return formData.value.teacherNo && formData.value.teacherName;
} else {
return formData.value.teacherNo && formData.value.teacherName && candidates.value.length > 0;
}
});
// 处理选择方式变化
const handleSelectModeChange = () => {
formData.value.teacherNo = ''
formData.value.teacherName = ''
formData.value.candidates = []
selectedUserList.value = []
candidateUserList.value = []
candidates.value = []
selectedCandidate.value = { teacherNo: '', teacherName: '' }
rollingName.value = ''
}
formData.value.teacherNo = '';
formData.value.teacherName = '';
formData.value.candidates = [];
selectedUserList.value = [];
candidateUserList.value = [];
candidates.value = [];
selectedCandidate.value = { teacherNo: '', teacherName: '' };
rollingName.value = '';
};
// 获取人员类型标签
const getRepresentorTypeLabel = (value: string): string => {
const option = representorTypeOptions.value.find((item: any) => item.value === value)
return option ? option.label : value
}
const option = representorTypeOptions.value.find((item: any) => item.value === value);
return option ? option.label : value;
};
// 处理指定人员变化
const handleUserChange = (list: any[]) => {
if (list && list.length > 0) {
const user = list[0]
formData.value.teacherNo = user.username || user.userName || ''
formData.value.teacherName = user.name || user.realName || ''
} else {
formData.value.teacherNo = ''
formData.value.teacherName = ''
}
}
if (list && list.length > 0) {
const user = list[0];
formData.value.teacherNo = user.username || user.userName || '';
formData.value.teacherName = user.name || user.realName || '';
} else {
formData.value.teacherNo = '';
formData.value.teacherName = '';
}
};
// 处理候选人变化
const handleCandidateChange = (list: any[]) => {
candidates.value = (list || []).map(user => ({
teacherNo: user.username || user.userName || '',
teacherName: user.name || user.realName || ''
}))
// 重置已选结果
if (candidates.value.length > 0) {
selectedCandidate.value = { ...candidates.value[0] }
formData.value.teacherNo = selectedCandidate.value.teacherNo
formData.value.teacherName = selectedCandidate.value.teacherName
} else {
selectedCandidate.value = { teacherNo: '', teacherName: '' }
formData.value.teacherNo = ''
formData.value.teacherName = ''
}
}
candidates.value = (list || []).map((user) => ({
teacherNo: user.username || user.userName || '',
teacherName: user.name || user.realName || '',
}));
// 重置已选结果
if (candidates.value.length > 0) {
selectedCandidate.value = { ...candidates.value[0] };
formData.value.teacherNo = selectedCandidate.value.teacherNo;
formData.value.teacherName = selectedCandidate.value.teacherName;
} else {
selectedCandidate.value = { teacherNo: '', teacherName: '' };
formData.value.teacherNo = '';
formData.value.teacherName = '';
}
};
// 随机抽取动画
const startRollingAnimation = (finalCandidate: { teacherNo: string; teacherName: string }) => {
if (candidates.value.length === 0) return
if (candidates.value.length === 0) return;
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
if (rollInterval) {
clearInterval(rollInterval);
rollInterval = null;
}
rollingName.value = ''
rolling.value = true
rollingName.value = '';
rolling.value = true;
let currentIndex = 0
const totalDuration = 2000
const intervalTime = 80
let currentIndex = 0;
const totalDuration = 2000;
const intervalTime = 80;
rollInterval = setInterval(() => {
rollingName.value = candidates.value[currentIndex].teacherName
currentIndex = (currentIndex + 1) % candidates.value.length
}, intervalTime)
rollInterval = setInterval(() => {
rollingName.value = candidates.value[currentIndex].teacherName;
currentIndex = (currentIndex + 1) % candidates.value.length;
}, intervalTime);
setTimeout(() => {
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
rolling.value = false
rollingName.value = ''
selectedCandidate.value = finalCandidate
formData.value.teacherNo = finalCandidate.teacherNo
formData.value.teacherName = finalCandidate.teacherName
}, totalDuration)
}
setTimeout(() => {
if (rollInterval) {
clearInterval(rollInterval);
rollInterval = null;
}
rolling.value = false;
rollingName.value = '';
selectedCandidate.value = finalCandidate;
formData.value.teacherNo = finalCandidate.teacherNo;
formData.value.teacherName = finalCandidate.teacherName;
}, totalDuration);
};
// 执行随机抽取
const handleRandomSelect = async () => {
if (candidates.value.length < 2) {
useMessage().warning('请至少选择2位候选人')
return
}
if (candidates.value.length < 2) {
useMessage().warning('请至少选择2位候选人');
return;
}
rolling.value = true
try {
const res = await randomSelectReviewer({
applyId: props.applyId,
selectMode: SELECT_MODE.RANDOM,
candidates: candidates.value
})
const result = res?.data || res
if (result?.teacherNo) {
startRollingAnimation({
teacherNo: result.teacherNo,
teacherName: result.teacherName || ''
})
}
} catch (e: any) {
rolling.value = false
useMessage().error(e?.msg || '随机抽取失败')
}
}
rolling.value = true;
try {
const res = await randomSelectReviewer({
applyId: props.applyId,
selectMode: SELECT_MODE.RANDOM,
candidates: candidates.value,
});
const result = res?.data || res;
if (result?.teacherNo) {
startRollingAnimation({
teacherNo: result.teacherNo,
teacherName: result.teacherName || '',
});
}
} catch (e: any) {
rolling.value = false;
useMessage().error(e?.msg || '随机抽取失败');
}
};
// 保存设置
const handleSave = async () => {
if (!canSave.value) {
useMessage().warning('请完善设置信息')
return
}
if (!canSave.value) {
useMessage().warning('请完善设置信息');
return;
}
saving.value = true
try {
const params: any = {
applyId: props.applyId,
selectMode: formData.value.selectMode,
teacherNo: formData.value.teacherNo,
teacherName: formData.value.teacherName,
representorType: formData.value.representorType
}
saving.value = true;
try {
const params: any = {
applyId: props.applyId,
selectMode: formData.value.selectMode,
teacherNo: formData.value.teacherNo,
teacherName: formData.value.teacherName,
representorType: formData.value.representorType,
};
if (formData.value.selectMode === SELECT_MODE.RANDOM) {
params.candidates = candidates.value
}
if (formData.value.selectMode === SELECT_MODE.RANDOM) {
params.candidates = candidates.value;
}
await setReviewerSetting(params)
useMessage().success('保存成功')
emit('saved')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saving.value = false
}
}
await setReviewerSetting(params);
useMessage().success('保存成功');
emit('saved');
await loadData();
} catch (e: any) {
useMessage().error(e?.msg || '保存失败');
} finally {
saving.value = false;
}
};
// 加载数据
const loadData = async () => {
if (!props.applyId) return
if (!props.applyId) return;
loading.value = true
try {
const res = await getReviewerSetting(props.applyId)
const data = res?.data || res
if (data) {
currentRepresentor.value = {
teacherNo: data.teacherNo || '',
teacherName: data.teacherName || '',
representorType: data.representorType || ''
}
formData.value.selectMode = data.selectMode || SELECT_MODE.DESIGNATED
formData.value.teacherNo = data.teacherNo || ''
formData.value.teacherName = data.teacherName || ''
formData.value.representorType = data.representorType || ''
loading.value = true;
try {
const res = await getReviewerSetting(props.applyId);
const data = res?.data || res;
if (data) {
currentRepresentor.value = {
teacherNo: data.teacherNo || '',
teacherName: data.teacherName || '',
representorType: data.representorType || '',
};
formData.value.selectMode = data.selectMode || SELECT_MODE.DESIGNATED;
formData.value.teacherNo = data.teacherNo || '';
formData.value.teacherName = data.teacherName || '';
formData.value.representorType = data.representorType || '';
if (data.candidateList && data.candidateList.length > 0) {
candidates.value = data.candidateList
formData.value.candidates = data.candidateList
// 回显候选人
candidateUserList.value = data.candidateList.map((c: any) => ({
username: c.teacherNo,
userName: c.teacherNo,
name: c.teacherName,
realName: c.teacherName
}))
}
if (data.candidateList && data.candidateList.length > 0) {
candidates.value = data.candidateList;
formData.value.candidates = data.candidateList;
// 回显候选人
candidateUserList.value = data.candidateList.map((c: any) => ({
username: c.teacherNo,
userName: c.teacherNo,
name: c.teacherName,
realName: c.teacherName,
}));
}
if (data.teacherNo && data.selectMode === SELECT_MODE.DESIGNATED) {
selectedUserList.value = [{
username: data.teacherNo,
userName: data.teacherNo,
name: data.teacherName,
realName: data.teacherName
}]
}
if (data.teacherNo && data.selectMode === SELECT_MODE.DESIGNATED) {
selectedUserList.value = [
{
username: data.teacherNo,
userName: data.teacherNo,
name: data.teacherName,
realName: data.teacherName,
},
];
}
if (data.selectMode === SELECT_MODE.RANDOM && data.teacherNo) {
selectedCandidate.value = {
teacherNo: data.teacherNo,
teacherName: data.teacherName || ''
}
}
}
} catch (_) {
// 忽略错误
} finally {
loading.value = false
}
}
if (data.selectMode === SELECT_MODE.RANDOM && data.teacherNo) {
selectedCandidate.value = {
teacherNo: data.teacherNo,
teacherName: data.teacherName || '',
};
}
}
} catch (_) {
// 忽略错误
} finally {
loading.value = false;
}
};
onMounted(() => {
loadData()
})
loadData();
});
onUnmounted(() => {
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
})
if (rollInterval) {
clearInterval(rollInterval);
rollInterval = null;
}
});
</script>
<style scoped lang="scss">
.reviewer-setting {
padding: 16px;
padding: 16px;
}
.random-roller {
padding: 12px 16px;
background: var(--el-fill-color-light);
border-radius: 4px;
min-height: 40px;
display: flex;
align-items: center;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-radius: 4px;
min-height: 40px;
display: flex;
align-items: center;
.rolling {
font-size: 16px;
font-weight: 500;
color: var(--el-color-primary);
animation: blink 0.1s infinite;
}
.rolling {
font-size: 16px;
font-weight: 500;
color: var(--el-color-primary);
animation: blink 0.1s infinite;
}
.selected {
font-size: 16px;
font-weight: 600;
color: var(--el-color-success);
}
.selected {
font-size: 16px;
font-weight: 600;
color: var(--el-color-success);
}
.placeholder {
color: var(--el-text-color-placeholder);
}
.placeholder {
color: var(--el-text-color-placeholder);
}
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
</style>
</style>

View File

@@ -1,252 +1,241 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 搜索表单卡片 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="采购编号" prop="purchaseNo">
<el-input
v-model="state.queryForm.purchaseNo"
placeholder="请输入采购编号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="state.queryForm.projectName"
placeholder="请输入项目名称"
clearable
style="width: 200px" />
</el-form-item>
<!-- 审核状态筛选 - 仅审核模式显示 -->
<el-form-item v-if="mode === 'audit'" label="审核状态" prop="docAuditStatus">
<el-select
v-model="state.queryForm.docAuditStatus"
placeholder="请选择审核状态"
clearable
style="width: 200px">
<el-option label="待上传" value="PENDING_UPLOAD" />
<el-option label="草稿" value="DRAFT" />
<el-option label="资产管理处审核中" value="ASSET_REVIEWING" />
<el-option label="需求部门审核中" value="DEPT_REVIEWING" />
<el-option label="内审部门审核中" value="AUDIT_REVIEWING" />
<el-option label="资产管理处确认中" value="ASSET_CONFIRMING" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已退回" value="RETURNED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 搜索表单卡片 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="采购编号" prop="purchaseNo">
<el-input v-model="state.queryForm.purchaseNo" placeholder="请输入采购编号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="state.queryForm.projectName" placeholder="请输入项目名称" clearable style="width: 200px" />
</el-form-item>
<!-- 审核状态筛选 - 仅审核模式显示 -->
<el-form-item v-if="mode === 'audit'" label="审核状态" prop="docAuditStatus">
<el-select v-model="state.queryForm.docAuditStatus" placeholder="请选择审核状态" clearable style="width: 200px">
<el-option label="待上传" value="PENDING_UPLOAD" />
<el-option label="草稿" value="DRAFT" />
<el-option label="资产管理处审核中" value="ASSET_REVIEWING" />
<el-option label="需求部门审核中" value="DEPT_REVIEWING" />
<el-option label="内审部门审核中" value="AUDIT_REVIEWING" />
<el-option label="资产管理处确认中" value="ASSET_CONFIRMING" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已退回" value="RETURNED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon">
<component :is="titleIcon" />
</el-icon>
{{ pageTitle }}
</span>
<div class="header-actions">
<right-toolbar v-model:showSearch="showSearch" class="ml10" @queryTable="getDataList" />
</div>
</div>
</template>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon">
<component :is="titleIcon" />
</el-icon>
{{ pageTitle }}
</span>
<div class="header-actions">
<right-toolbar v-model:showSearch="showSearch" class="ml10" @queryTable="getDataList" />
</div>
</div>
</template>
<!-- 表格 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
</el-table-column>
<el-table-column prop="purchaseNo" label="采购编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<!-- 需求部门 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="deptName" label="需求部门" min-width="150" show-overflow-tooltip />
<!-- 预算金额 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="budget" label="预算金额(元)" width="120" align="right">
<template #default="scope">
{{ scope.row.budget ? Number(scope.row.budget).toLocaleString() : '-' }}
</template>
</el-table-column>
<!-- 文件状态 -->
<el-table-column :prop="statusProp" label="文件状态" width="140" align="center">
<template #default="scope">
<el-tag :type="getStatusType(getRowStatus(scope.row))">
{{ getStatusLabel(getRowStatus(scope.row)) }}
</el-tag>
</template>
</el-table-column>
<!-- 当前版本 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="currentDocVersion" label="当前版本" width="100" align="center">
<template #default="scope">
{{ scope.row.currentDocVersion || '-' }}
</template>
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button type="primary" link icon="View" @click="handleProcess(scope.row)">
{{ actionLabel }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 表格 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
>
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
</el-table-column>
<el-table-column prop="purchaseNo" label="采购编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<!-- 需求部门 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="deptName" label="需求部门" min-width="150" show-overflow-tooltip />
<!-- 预算金额 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="budget" label="预算金额(元)" width="120" align="right">
<template #default="scope">
{{ scope.row.budget ? Number(scope.row.budget).toLocaleString() : '-' }}
</template>
</el-table-column>
<!-- 文件状态 -->
<el-table-column :prop="statusProp" label="文件状态" width="140" align="center">
<template #default="scope">
<el-tag :type="getStatusType(getRowStatus(scope.row))">
{{ getStatusLabel(getRowStatus(scope.row)) }}
</el-tag>
</template>
</el-table-column>
<!-- 当前版本 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="currentDocVersion" label="当前版本" width="100" align="center">
<template #default="scope">
{{ scope.row.currentDocVersion || '-' }}
</template>
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button type="primary" link icon="View" @click="handleProcess(scope.row)">
{{ actionLabel }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-if="state.pagination && state.pagination.total && state.pagination.total > 0"
:total="state.pagination.total"
:current="state.pagination.current"
:size="state.pagination.size"
@sizeChange="sizeChangeHandle"
@currentChange="currentChangeHandle"
/>
</el-card>
</div>
<!-- 分页 -->
<pagination
v-if="state.pagination && state.pagination.total && state.pagination.total > 0"
:total="state.pagination.total"
:current="state.pagination.current"
:size="state.pagination.size"
@sizeChange="sizeChangeHandle"
@currentChange="currentChangeHandle"
/>
</el-card>
</div>
<!-- 处理弹窗 -->
<!-- {{mode}}-->
<DocProcessDialog ref="docProcessDialogRef" :mode="mode" @refresh="getDataList" />
</div>
<!-- 处理弹窗 -->
<!-- {{mode}}-->
<DocProcessDialog ref="docProcessDialogRef" :mode="mode" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PurchasingDocProcess">
import { ref, reactive, computed, defineAsyncComponent, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { getDocProcessList } from "/@/api/purchase/docProcess";
import { Search, DocumentCopy, DocumentChecked, List } from '@element-plus/icons-vue'
import { ref, reactive, computed, defineAsyncComponent, onMounted } from 'vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { getDocProcessList } from '/@/api/purchase/docProcess';
import { Search, DocumentCopy, DocumentChecked, List } from '@element-plus/icons-vue';
import { useUserInfo } from '/@/stores/userInfo';
// 引入组件
const DocProcessDialog = defineAsyncComponent(() => import('./DocProcessDialog.vue'));
const userInfoStore = useUserInfo()
const docProcessDialogRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const userInfoStore = useUserInfo();
const docProcessDialogRef = ref();
const searchFormRef = ref();
const showSearch = ref(true);
// 从用户角色自动判断模式
const mode = computed(() => {
const roleCodes = userInfoStore.userInfos.roleCodes || [];
// 有 PURCHASE_AGENT 角色则显示代理模式
if (roleCodes.includes('PURCHASE_AGENT')) {
return 'agent';
}
// 其他情况显示审核模式
return 'audit';
})
const roleCodes = userInfoStore.userInfos.roleCodes || [];
// 有 PURCHASE_AGENT 角色则显示代理模式
if (roleCodes.includes('PURCHASE_AGENT')) {
return 'agent';
}
// 其他情况显示审核模式
return 'audit';
});
// 页面标题
const pageTitle = computed(() => {
return mode.value === 'agent' ? '招标代理工作台' : '招标文件审核'
})
return mode.value === 'agent' ? '招标代理工作台' : '招标文件审核';
});
// 标题图标
const titleIcon = computed(() => {
return mode.value === 'agent' ? DocumentCopy : DocumentChecked
})
return mode.value === 'agent' ? DocumentCopy : DocumentChecked;
});
// 操作按钮标签
const actionLabel = computed(() => {
return mode.value === 'agent' ? '处理' : '审核'
})
return mode.value === 'agent' ? '处理' : '审核';
});
// 状态字段
const statusProp = computed(() => {
return mode.value === 'agent' ? 'status' : 'docAuditStatus'
})
return mode.value === 'agent' ? 'status' : 'docAuditStatus';
});
// 查询表单
const getQueryForm = () => {
const base: any = {
purchaseNo: '',
projectName: '',
}
if (mode.value === 'audit') {
base.docAuditStatus = ''
}
return base
}
const base: any = {
purchaseNo: '',
projectName: '',
};
if (mode.value === 'audit') {
base.docAuditStatus = '';
}
return base;
};
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: (params?: any) => getDocProcessList(mode.value, params),
queryForm: getQueryForm(),
createdIsNeed: true
pageList: (params?: any) => getDocProcessList(mode.value, params),
queryForm: getQueryForm(),
createdIsNeed: true,
});
const { getDataList, tableStyle, sizeChangeHandle, currentChangeHandle } = useTable(state);
const handleReset = () => {
searchFormRef.value?.resetFields();
getDataList();
searchFormRef.value?.resetFields();
getDataList();
};
// 获取行状态(兼容两种字段名)
const getRowStatus = (row: any) => {
return mode.value === 'agent' ? row.status : row.docAuditStatus
}
return mode.value === 'agent' ? row.status : row.docAuditStatus;
};
const getStatusType = (status: string) => {
const typeMap: Record<string, string> = {
'PENDING_UPLOAD': 'info',
'DRAFT': 'info',
'ASSET_REVIEWING': 'warning',
'DEPT_REVIEWING': 'warning',
'AUDIT_REVIEWING': 'warning',
'ASSET_CONFIRMING': 'primary',
'COMPLETED': 'success',
'RETURNED': 'danger'
};
return typeMap[status] || 'info';
const typeMap: Record<string, string> = {
PENDING_UPLOAD: 'info',
DRAFT: 'info',
ASSET_REVIEWING: 'warning',
DEPT_REVIEWING: 'warning',
AUDIT_REVIEWING: 'warning',
ASSET_CONFIRMING: 'primary',
COMPLETED: 'success',
RETURNED: 'danger',
};
return typeMap[status] || 'info';
};
const getStatusLabel = (status: string) => {
const labelMap: Record<string, string> = {
'PENDING_UPLOAD': '待上传',
'DRAFT': '草稿',
'ASSET_REVIEWING': '资产管理处审核中',
'DEPT_REVIEWING': '需求部门审核中',
'AUDIT_REVIEWING': '内审部门审核中',
'ASSET_CONFIRMING': '资产管理处确认中',
'COMPLETED': '已完成',
'RETURNED': '已退回'
};
return labelMap[status] || '-';
const labelMap: Record<string, string> = {
PENDING_UPLOAD: '待上传',
DRAFT: '草稿',
ASSET_REVIEWING: '资产管理处审核中',
DEPT_REVIEWING: '需求部门审核中',
AUDIT_REVIEWING: '内审部门审核中',
ASSET_CONFIRMING: '资产管理处确认中',
COMPLETED: '已完成',
RETURNED: '已退回',
};
return labelMap[status] || '-';
};
const handleProcess = (row: any) => {
docProcessDialogRef.value?.open(row);
docProcessDialogRef.value?.open(row);
};
// 监听路由参数变化,重新加载数据
onMounted(() => {
// 重置查询表单
state.queryForm = getQueryForm()
// 重置查询表单
state.queryForm = getQueryForm();
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>
</style>

View File

@@ -1,120 +1,115 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="90%"
:close-on-click-modal="false"
destroy-on-close
class="form-iframe-dialog"
@close="handleClose"
>
<div class="form-iframe-content">
<iframe
ref="iframeRef"
:src="iframeSrc"
frameborder="0"
class="form-iframe"
/>
</div>
</el-dialog>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="90%"
:close-on-click-modal="false"
destroy-on-close
class="form-iframe-dialog"
@close="handleClose"
>
<div class="form-iframe-content">
<iframe ref="iframeRef" :src="iframeSrc" frameborder="0" class="form-iframe" />
</div>
</el-dialog>
</template>
<script setup lang="ts" name="PurchasingRequisitionForm">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch } from 'vue';
const props = defineProps<{
dictData?: Record<string, any>
}>()
dictData?: Record<string, any>;
}>();
const emit = defineEmits<{
(e: 'refresh'): void
}>()
(e: 'refresh'): void;
}>();
const visible = ref(false)
const iframeRef = ref<HTMLIFrameElement>()
const mode = ref<'add' | 'edit' | 'view'>('add')
const rowId = ref<string | number>('')
const visible = ref(false);
const iframeRef = ref<HTMLIFrameElement>();
const mode = ref<'add' | 'edit' | 'view'>('add');
const rowId = ref<string | number>('');
const dialogTitle = computed(() => {
const titles = {
add: '新增采购申请',
edit: '编辑采购申请',
view: '查看采购申请',
}
return titles[mode.value] || titles.add
})
const titles = {
add: '新增采购申请',
edit: '编辑采购申请',
view: '查看采购申请',
};
return titles[mode.value] || titles.add;
});
const iframeSrc = computed(() => {
const baseUrl = window.location.origin + window.location.pathname
let src = `${baseUrl}#/purchase/purchasingrequisition/add`
if (mode.value !== 'add' && rowId.value) {
src += `?mode=${mode.value}&id=${rowId.value}`
}
return src
})
const baseUrl = window.location.origin + window.location.pathname;
let src = `${baseUrl}#/purchase/purchasingrequisition/add`;
if (mode.value !== 'add' && rowId.value) {
src += `?mode=${mode.value}&id=${rowId.value}`;
}
return src;
});
const handleClose = () => {
visible.value = false
window.removeEventListener('message', handleMessage)
}
visible.value = false;
window.removeEventListener('message', handleMessage);
};
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'purchasingrequisition:submitSuccess') {
handleClose()
emit('refresh')
} else if (event.data?.type === 'purchasingrequisition:close') {
handleClose()
}
}
if (event.data?.type === 'purchasingrequisition:submitSuccess') {
handleClose();
emit('refresh');
} else if (event.data?.type === 'purchasingrequisition:close') {
handleClose();
}
};
const openDialog = (openMode: 'add' | 'edit' | 'view', row?: any) => {
mode.value = openMode
rowId.value = row?.id ?? ''
visible.value = true
window.addEventListener('message', handleMessage)
}
mode.value = openMode;
rowId.value = row?.id ?? '';
visible.value = true;
window.addEventListener('message', handleMessage);
};
watch(visible, (val) => {
if (!val) {
window.removeEventListener('message', handleMessage)
}
})
if (!val) {
window.removeEventListener('message', handleMessage);
}
});
defineExpose({
openDialog,
})
openDialog,
});
</script>
<style scoped lang="scss">
.form-iframe-content {
width: 100%;
height: 70vh;
min-height: 500px;
max-height: calc(100vh - 200px);
position: relative;
overflow: hidden;
width: 100%;
height: 70vh;
min-height: 500px;
max-height: calc(100vh - 200px);
position: relative;
overflow: hidden;
.form-iframe {
width: 100%;
height: 100%;
min-height: 500px;
border: none;
display: block;
}
.form-iframe {
width: 100%;
height: 100%;
min-height: 500px;
border: none;
display: block;
}
}
</style>
<style>
.form-iframe-dialog.el-dialog {
display: flex;
flex-direction: column;
max-height: 90vh;
margin-top: 5vh !important;
display: flex;
flex-direction: column;
max-height: 90vh;
margin-top: 5vh !important;
}
.form-iframe-dialog .el-dialog__body {
padding: 20px;
overflow: hidden;
flex: 1;
min-height: 0;
padding: 20px;
overflow: hidden;
flex: 1;
min-height: 0;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +1,73 @@
<template>
<el-dialog
v-model="visible"
title="实施采购"
width="800px"
:close-on-click-modal="false"
destroy-on-close
@close="handleClose"
>
<ImplementContent
ref="implementContentRef"
@close="handleContentClose"
@saved="handleContentSaved"
/>
<el-dialog v-model="visible" title="实施采购" width="800px" :close-on-click-modal="false" destroy-on-close @close="handleClose">
<ImplementContent ref="implementContentRef" @close="handleContentClose" @saved="handleContentSaved" />
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="confirming" @click="handleConfirm">确定</el-button>
</div>
</template>
</el-dialog>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="confirming" @click="handleConfirm">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ImplementForm">
import { ref, nextTick } from 'vue'
import ImplementContent from './implement.vue'
import { ref, nextTick } from 'vue';
import ImplementContent from './implement.vue';
const emit = defineEmits<{
(e: 'refresh'): void
}>()
(e: 'refresh'): void;
}>();
const visible = ref(false)
const implementContentRef = ref<InstanceType<typeof ImplementContent>>()
const confirming = ref(false)
const pendingRow = ref<{ id: string | number } | null>(null)
const visible = ref(false);
const implementContentRef = ref<InstanceType<typeof ImplementContent>>();
const confirming = ref(false);
const pendingRow = ref<{ id: string | number } | null>(null);
const handleClose = () => {
visible.value = false
}
visible.value = false;
};
const handleContentClose = () => {
visible.value = false
emit('refresh')
}
visible.value = false;
emit('refresh');
};
const handleContentSaved = () => {
emit('refresh')
}
emit('refresh');
};
const handleConfirm = async () => {
confirming.value = true
try {
await implementContentRef.value?.handleConfirm()
visible.value = false
emit('refresh')
} finally {
confirming.value = false
}
}
confirming.value = true;
try {
await implementContentRef.value?.handleConfirm();
visible.value = false;
emit('refresh');
} finally {
confirming.value = false;
}
};
const openDialog = async (row: { id: string | number }) => {
pendingRow.value = row
visible.value = true
// 等待 dialog 及内部组件挂载完成后再调用 open
await nextTick()
if (pendingRow.value) {
implementContentRef.value?.open(pendingRow.value)
pendingRow.value = null
}
}
pendingRow.value = row;
visible.value = true;
// 等待 dialog 及内部组件挂载完成后再调用 open
await nextTick();
if (pendingRow.value) {
implementContentRef.value?.open(pendingRow.value);
pendingRow.value = null;
}
};
defineExpose({
openDialog,
})
openDialog,
});
</script>
<style scoped lang="scss">
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
</style>

File diff suppressed because it is too large Load Diff