采购更新

This commit is contained in:
吴红兵
2026-02-28 16:47:22 +08:00
parent eafa255359
commit f5df8200aa
7 changed files with 406 additions and 287 deletions

View File

@@ -8,33 +8,37 @@
</el-radio-group>
</el-form-item>
<!-- 采购文件版本列表保留原文件多版本分别显示 -->
<el-divider content-position="left">采购文件版本</el-divider>
<div v-if="purchaseFileVersions.length" class="file-versions mb-2">
<el-table :data="purchaseFileVersions" border size="small" max-height="280">
<el-table-column type="index" label="版本" width="70" align="center">
<template #default="{ $index }">V{{ $index + 1 }}</template>
</el-table-column>
<el-table-column prop="fileTitle" label="文件名" min-width="180" show-overflow-tooltip />
<el-table-column prop="createBy" label="上传人" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="createTime" label="上传时间" width="165" align="center">
<template #default="{ row }">{{ formatCreateTime(row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleDownloadVersion(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="mb-2">可继续上传新版本保留原文件格式 doc/docx/pdf单文件不超过 5MB</div>
<upload-file
v-model="implementFileIds"
:limit="5"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: PURCHASE_FILE_TYPE }"
upload-file-url="/purchase/purchasingfiles/upload"
/>
<!-- 分配代理仅符合条件的申请单显示 -->
<template v-if="canAssignAgent">
<el-divider content-position="left">分配代理</el-divider>
<el-form-item label="分配方式">
<el-radio-group v-model="agentMode">
<el-radio label="designated">指定代理</el-radio>
<el-radio label="random">随机分配</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="agentMode === 'designated'" label="选择代理">
<el-select v-model="selectedAgentId" placeholder="请选择招标代理" filterable style="width: 100%" :loading="agentListLoading">
<el-option v-for="item in agentList" :key="item.id" :label="item.agentName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item v-if="agentMode === 'random'" label="随机结果">
<div class="agent-roller">
<span v-if="rollingAgentName" class="rolling">{{ rollingAgentName }}</span>
<span v-else-if="assignedAgentName" class="assigned">已分配{{ assignedAgentName }}</span>
<span v-else class="placeholder">点击下方按钮进行随机分配</span>
</div>
</el-form-item>
<el-form-item v-if="applyRow?.agentName" label="当前代理">
<el-tag>{{ applyRow.agentName }}</el-tag>
</el-form-item>
<el-form-item v-if="agentMode === 'designated'">
<el-button type="primary" :loading="assignAgentSubmitting" :disabled="!selectedAgentId" @click="handleAssignAgentDesignated">指定代理</el-button>
</el-form-item>
<el-form-item v-if="agentMode === 'random'">
<el-button type="primary" :loading="assignAgentSubmitting" :disabled="!!assignedAgentName" @click="handleAssignAgentRandom">随机分配</el-button>
</el-form-item>
</template>
<!-- 仅部门审核角色显示采购代表相关 -->
<template v-if="isDeptAuditRole">
@@ -59,37 +63,25 @@
</div>
<div class="implement-footer">
<el-button @click="handleClose">取消</el-button>
<template v-if="implementHasPurchaseFiles && !applyRow?.fileFlowInstId">
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">保存实施采购</el-button>
<el-button v-if="canStartFileFlow" type="success" :loading="startFileFlowSubmitting" @click="handleStartFileFlow">发起采购文件审批</el-button>
</template>
<template v-else>
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">确定</el-button>
</template>
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">确定</el-button>
</div>
</div>
</template>
<script setup lang="ts" name="PurchasingImplement">
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { implementApply, getApplyFiles, startFileFlow, getDeptMembers, getObj } from '/@/api/finance/purchasingrequisition'
import { getDeptMembers, getObj, assignAgent } from '/@/api/finance/purchasingrequisition'
import { getPage as getAgentPage } from '/@/api/finance/purchaseagent'
import { useMessage } from '/@/hooks/message'
import other from '/@/utils/other'
import UploadFile from '/@/components/Upload/index.vue'
import { Session } from '/@/utils/storage'
import * as orderVue from '/@/api/order/order-key-vue'
/** 部门审核角色编码:仅该角色下显示采购代表相关页面和功能,流转至部门审核时需填写采购代表 */
const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT'
/** 采购中心角色编码:可保存/发起实施采购,但不出现采购代表相关内容和接口 */
const PURCHASE_CENTER_ROLE_CODE = 'PURCHASE_CENTER'
const roleCode = computed(() => Session.getRoleCode() || '')
const isDeptAuditRole = computed(() => roleCode.value === PURCHASE_DEPT_AUDIT_ROLE_CODE)
const isPurchaseCenterRole = computed(() => roleCode.value === PURCHASE_CENTER_ROLE_CODE)
/** 可发起采购文件审批:部门审核(需填采购代表)、采购中心(不填采购代表) */
const canStartFileFlow = computed(() => isDeptAuditRole.value || isPurchaseCenterRole.value)
// 与编辑界面一致:支持流程 dynamic-link 传入 currJob/currElTab申请单 ID 优先取 currJob.orderId
const props = defineProps({
@@ -102,7 +94,6 @@ const emit = defineEmits(['handleJob'])
const isFlowEmbed = computed(() => !!props.currJob)
const route = useRoute()
const PURCHASE_FILE_TYPE = '130'
/** 申请单 ID数值用于 getObj 等):与 add 一致,优先流程 currJob.orderId否则 route.query.id */
const applyId = computed(() => {
@@ -122,10 +113,6 @@ const applyIdRaw = computed(() => {
})
const applyRow = ref<any>(null)
/** 已有采购文件版本列表(按 createTime 排序,用于展示与提交时一并关联) */
const purchaseFileVersions = ref<{ id: string; fileTitle?: string; createBy?: string; createTime?: string; remark?: string }[]>([])
/** 本次新上传的采购文件(仅新版本,不与已有版本混在一起) */
const implementFileIds = ref<string | string[]>([])
const implementType = ref<string>('1')
const implementSubmitting = ref(false)
@@ -133,28 +120,117 @@ const representorMode = ref<'single' | 'multi'>('single')
const representorTeacherNo = ref<string>('')
const representorsMulti = ref<string[]>([])
const deptMembers = ref<any[]>([])
const startFileFlowSubmitting = ref(false)
const implementHasPurchaseFiles = computed(() => {
if (purchaseFileVersions.value.length > 0) return true
const raw = implementFileIds.value
if (Array.isArray(raw)) return raw.length > 0
return !!raw
/** 分配代理相关 */
const agentMode = ref<'designated' | 'random'>('designated')
const selectedAgentId = ref<string>('')
const agentList = ref<any[]>([])
const agentListLoading = ref(false)
const assignAgentSubmitting = ref(false)
const rollingAgentName = ref<string>('')
const assignedAgentName = ref<string>('')
let rollInterval: ReturnType<typeof setInterval> | null = null
/** 是否可以分配代理:委托代理采购 且 (学校统一采购 或 部门自行采购且委托采购中心采购) */
const canAssignAgent = computed(() => {
// 自行组织采购不需要分配代理
if (implementType.value !== '2') return false
const row = applyRow.value
if (!row) return false
return row.purchaseMode === '2' || (row.purchaseMode === '0' && row.purchaseType === '4')
})
function formatCreateTime(t?: string) {
if (!t) return '-'
const d = new Date(t)
return isNaN(d.getTime()) ? t : d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
const loadAgentList = async () => {
if (!canAssignAgent.value) return
agentListLoading.value = true
try {
const res = await getAgentPage({ size: 500, current: 1 })
const records = res?.data?.records ?? res?.records ?? []
agentList.value = Array.isArray(records) ? records : []
} catch (_) {
agentList.value = []
} finally {
agentListLoading.value = false
}
}
function handleDownloadVersion(file: { remark?: string; fileTitle?: string }) {
if (!file?.remark) {
useMessage().warning('无法获取文件路径')
/** 随机分配代理 - 滚动动画 */
const startRollingAnimation = (finalAgentName: string) => {
if (agentList.value.length === 0) return
// 清除之前的动画
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
rollingAgentName.value = ''
assignedAgentName.value = ''
let currentIndex = 0
const totalDuration = 2000 // 总动画时间 2秒
const intervalTime = 80 // 滚动间隔
rollInterval = setInterval(() => {
rollingAgentName.value = agentList.value[currentIndex].agentName
currentIndex = (currentIndex + 1) % agentList.value.length
}, intervalTime)
// 2秒后停止并显示最终结果
setTimeout(() => {
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
rollingAgentName.value = ''
assignedAgentName.value = finalAgentName
}, totalDuration)
}
const handleAssignAgentRandom = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return
}
const url = `/purchase/purchasingfiles/download?fileName=${encodeURIComponent(file.remark)}&fileTitle=${encodeURIComponent(file.fileTitle || '采购文件')}`
other.downBlobFile(url, {}, file.fileTitle || '采购文件')
assignAgentSubmitting.value = true
try {
const res = await assignAgent(Number(id), 'random')
// 后端返回分配结果后,展示滚动动画
const finalAgentName = res?.data?.agentName || res?.agentName || ''
if (finalAgentName) {
startRollingAnimation(finalAgentName)
} else {
useMessage().success('随机分配代理成功')
await loadData()
}
} catch (e: any) {
useMessage().error(e?.msg || '随机分配代理失败')
} finally {
assignAgentSubmitting.value = false
}
}
const handleAssignAgentDesignated = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return
}
if (!selectedAgentId.value) {
useMessage().warning('请选择招标代理')
return
}
assignAgentSubmitting.value = true
try {
await assignAgent(Number(id), 'designated', selectedAgentId.value)
useMessage().success('指定代理成功')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '指定代理失败')
} finally {
assignAgentSubmitting.value = false
}
}
const isInIframe = () => typeof window !== 'undefined' && window.self !== window.top
@@ -173,21 +249,16 @@ const loadData = async () => {
}
const needDeptMembers = isDeptAuditRole.value
try {
const idStr = applyIdRaw.value || String(id)
const requests: [ReturnType<typeof getObj>, ReturnType<typeof getApplyFiles>, ReturnType<typeof getDeptMembers>?] = [
getObj(id),
getApplyFiles(idStr)
]
const requests: [ReturnType<typeof getObj>, ReturnType<typeof getDeptMembers>?] = [getObj(id)]
if (needDeptMembers) requests.push(getDeptMembers())
const results = await Promise.all(requests)
const detailRes = results[0]
const filesRes = results[1]
const membersRes = needDeptMembers ? results[2] : null
const membersRes = needDeptMembers ? results[1] : null
applyRow.value = detailRes?.data ? { ...detailRes.data, id: detailRes.data.id ?? id } : { id }
const row = applyRow.value
if (row?.implementType) implementType.value = row.implementType
// 回显需求部门初审-采购代表人方式与人员(与发起采购文件审批时保存的一致)
// 回显需求部门初审-采购代表人方式与人员
if (row?.representorTeacherNo) {
representorMode.value = 'single'
representorTeacherNo.value = row.representorTeacherNo ?? ''
@@ -201,19 +272,17 @@ const loadData = async () => {
representorTeacherNo.value = ''
representorsMulti.value = []
}
const list = filesRes?.data || []
const purchaseFiles = list.filter((f: any) => f.fileType === PURCHASE_FILE_TYPE)
purchaseFileVersions.value = purchaseFiles.map((f: any) => ({
id: String(f.id),
fileTitle: f.fileTitle || f.file_title || '采购文件',
createBy: f.createBy ?? f.create_by ?? '-',
createTime: f.createTime || f.create_time,
remark: f.remark
}))
deptMembers.value = needDeptMembers && membersRes?.data ? membersRes.data : []
// 加载代理列表并回显已分配代理
if (canAssignAgent.value) {
await loadAgentList()
if (row?.agentName) {
assignedAgentName.value = row.agentName
}
}
} catch (_) {
applyRow.value = { id }
purchaseFileVersions.value = []
deptMembers.value = []
}
}
@@ -234,27 +303,23 @@ const handleImplementSubmit = async () => {
useMessage().warning('请选择实施采购方式')
return
}
const existingIds = purchaseFileVersions.value.map((f) => f.id)
const raw = implementFileIds.value
const newIds: string[] = Array.isArray(raw)
? raw.map((x: any) => (typeof x === 'object' && x?.id ? x.id : x)).filter(Boolean)
: raw ? [String(raw)] : []
const fileIds = [...existingIds, ...newIds]
if (fileIds.length === 0) {
useMessage().warning('请至少上传一个采购文件')
return
// 仅部门审核角色校验采购代表人
if (isDeptAuditRole.value) {
if (representorMode.value === 'single' && !representorTeacherNo.value) {
useMessage().warning('请选择采购代表人')
return
}
if (representorMode.value === 'multi' && !representorsMulti.value?.length) {
useMessage().warning('请选择部门多人')
return
}
}
// 仅部门审核角色提交采购代表;采购中心保存时不传采购代表
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' && representorsMulti.value?.length ? representorsMulti.value.join(',') : undefined
implementSubmitting.value = true
try {
await implementApply(id, fileIds, implementType.value, single, multi)
// TODO: 调用保存接口
useMessage().success('实施采购已保存')
implementFileIds.value = []
await loadData()
postMessage('purchasingimplement:saved')
// 流程嵌入场景:通知流程当前 Tab 已保存,避免审批时提示“未保存”
// 流程嵌入场景:通知流程当前 Tab 已保存
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit)
}
@@ -265,40 +330,7 @@ const handleImplementSubmit = async () => {
}
}
const handleStartFileFlow = async () => {
const row = applyRow.value
const id = row?.id ?? applyId.value
if (!id) return
// 部门审核角色必须填写采购代表;采购中心不填采购代表
if (isDeptAuditRole.value) {
if (representorMode.value === 'single') {
if (!representorTeacherNo.value) {
useMessage().warning('请选择采购代表人')
return
}
} else {
if (!representorsMulti.value?.length) {
useMessage().warning('请选择部门多人')
return
}
}
}
startFileFlowSubmitting.value = true
try {
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' ? representorsMulti.value.join(',') : undefined
await startFileFlow(id, single, multi)
useMessage().success('已发起采购文件审批流程')
postMessage('purchasingimplement:submitSuccess')
await loadData()
} catch (err: any) {
useMessage().error(err?.msg || '发起失败')
} finally {
startFileFlowSubmitting.value = false
}
}
/** 流程嵌入时供 handle.vue 调用的“保存”回调:与页面按钮保存逻辑保持一致 */
/** 流程嵌入时供 handle.vue 调用的”保存”回调:与页面按钮保存逻辑保持一致 */
async function flowSubmitForm() {
await handleImplementSubmit()
}
@@ -325,6 +357,14 @@ onMounted(async () => {
await orderVue.currElTabIsView({}, props.currJob, props.currElTab.id, flowSubmitForm)
}
})
onUnmounted(() => {
// 清理滚动动画定时器
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
})
</script>
<style scoped lang="scss">
@@ -352,4 +392,35 @@ onMounted(async () => {
gap: 12px;
flex-wrap: wrap;
}
.agent-roller {
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;
}
.assigned {
font-size: 16px;
font-weight: 600;
color: var(--el-color-success);
}
.placeholder {
color: var(--el-text-color-placeholder);
}
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
</style>