Files
school-developer/src/views/finance/purchasingrequisition/implement.vue
2026-03-01 16:00:24 +08:00

704 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="implement-page">
<div class="implement-form">
<!-- 步骤一选择实施采购方式 -->
<div class="step-section">
<div class="step-header">
<span class="step-number" :class="{ completed: step1Completed && !isEditingStep1 }">1</span>
<span class="step-title">选择实施采购方式</span>
<el-tag v-if="step1Completed && !isEditingStep1" type="success" size="small">已完成</el-tag>
</div>
<div class="step-content">
<el-form-item label="实施采购方式" required>
<el-radio-group v-model="implementType" :disabled="step1Completed && !isEditingStep1">
<el-radio :label="IMPLEMENT_TYPE.SELF_ORGANIZED">自行组织采购</el-radio>
<el-radio :label="IMPLEMENT_TYPE.ENTRUST_AGENT">委托代理采购</el-radio>
</el-radio-group>
</el-form-item>
<!-- 步骤一未完成显示保存按钮 -->
<el-form-item v-if="!step1Completed">
<el-button type="primary" :loading="saveTypeSubmitting" :disabled="!implementType" @click="handleSaveImplementType">保存</el-button>
</el-form-item>
<!-- 步骤一已完成但正在编辑显示修改确认按钮 -->
<el-form-item v-else-if="isEditingStep1">
<el-button type="primary" :loading="saveTypeSubmitting" :disabled="!implementType" @click="handleReSaveImplementType">确认修改</el-button>
<el-button @click="cancelEditStep1">取消</el-button>
</el-form-item>
<!-- 步骤一已完成且未在编辑显示修改按钮 -->
<el-form-item v-else>
<el-button type="default" @click="startEditStep1">修改</el-button>
</el-form-item>
</div>
</div>
<!-- 步骤二分配代理仅委托代理采购且步骤一完成时显示 -->
<div v-if="showStep2" class="step-section">
<div class="step-header">
<span class="step-number" :class="{ completed: step2Completed }">2</span>
<span class="step-title">分配代理机构</span>
<el-tag v-if="step2Completed" type="success" size="small">已完成</el-tag>
</div>
<div class="step-content">
<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-tag v-if="applyRow?.agentSent === YES_NO.YES" type="success" style="margin-left: 8px;">已发送</el-tag>
<el-tag v-else type="info" style="margin-left: 8px;">未发送</el-tag>
</el-form-item>
<!-- 指定代理按钮未发送招标代理时可操作 -->
<el-form-item v-if="agentMode === 'designated' && canReassignAgent">
<el-button type="primary" :loading="assignAgentSubmitting" :disabled="!selectedAgentId" @click="handleAssignAgentDesignated">{{ step2Completed ? '重新指定' : '指定代理' }}</el-button>
</el-form-item>
<!-- 随机分配按钮未发送招标代理时可操作 -->
<el-form-item v-if="agentMode === 'random' && canReassignAgent">
<el-button type="primary" :loading="assignAgentSubmitting" @click="handleAssignAgentRandom">随机分配</el-button>
</el-form-item>
<!-- 发送招标代理按钮 -->
<el-form-item v-if="canSendToAgent">
<el-button type="success" :loading="sendToAgentSubmitting" @click="handleSendToAgent">发送招标代理</el-button>
</el-form-item>
<!-- 撤回招标代理按钮 -->
<el-form-item v-if="canRevokeAgent">
<el-button type="warning" :loading="revokeAgentSubmitting" @click="handleRevokeAgent">撤回</el-button>
</el-form-item>
</div>
</div>
</div>
<!-- 底部按钮编辑步骤一时隐藏 -->
<div v-if="!isEditingStep1" class="implement-footer">
<el-button @click="handleClose">取消</el-button>
<!-- 只有步骤一完成后才显示确定按钮 -->
<el-button v-if="step1Completed" type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">确定</el-button>
</div>
</div>
</template>
<script setup lang="ts" name="PurchasingImplement">
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { getDeptMembers, getObj, assignAgent, sendToAgent, revokeAgent, saveImplementType } from '/@/api/finance/purchasingrequisition'
import { getPage as getAgentPage } from '/@/api/finance/purchaseagent'
import { useMessage } from '/@/hooks/message'
import { Session } from '/@/utils/storage'
import * as orderVue from '/@/api/order/order-key-vue'
// ==================== 常量定义(与后端枚举保持一致) ====================
/** 部门审核角色编码:仅该角色下显示采购代表相关页面和功能,流转至部门审核时需填写采购代表 */
const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT'
/** 实施采购方式(与后端 ImplementTypeEnum 一致) */
const IMPLEMENT_TYPE = {
/** 自行组织采购 */
SELF_ORGANIZED: '1',
/** 委托代理采购 */
ENTRUST_AGENT: '2',
} as const
/** 采购形式(与后端 PurchaseModeEnum 一致) */
const PURCHASE_MODE = {
/** 部门自行采购 */
DEPT_SELF: '1',
/** 学校统一采购 */
SCHOOL_UNIFIED: '2',
} as const
/** 采购途径(与后端 PurchaseChannelEnum 一致) */
const PURCHASE_CHANNEL = {
/** 自行采购 */
SELF: '1',
/** 委托采购中心采购 */
ENTRUST_CENTER: '2',
} as const
/** 是否标识(与后端 CommonConstants.YES/NO 一致) */
const YES_NO = {
NO: '0',
YES: '1',
} as const
const roleCode = computed(() => Session.getRoleCode() || '')
const isDeptAuditRole = computed(() => roleCode.value === PURCHASE_DEPT_AUDIT_ROLE_CODE)
// 与编辑界面一致:支持流程 dynamic-link 传入 currJob/currElTab申请单 ID 优先取 currJob.orderId
const props = defineProps({
currJob: { type: Object, default: null },
currElTab: { type: Object, default: null }
})
const emit = defineEmits(['handleJob'])
/** 是否被流程 handle 页面通过 dynamic-link 嵌入 */
const isFlowEmbed = computed(() => !!props.currJob)
const route = useRoute()
/** 申请单 ID数值用于 getObj 等):与 add 一致,优先流程 currJob.orderId否则 route.query.id */
const applyId = computed(() => {
const raw = applyIdRaw.value
if (raw == null || raw === '') return null
const n = Number(raw)
return Number.isNaN(n) ? null : n
})
/** 申请单 ID 原始字符串(用于 getApplyFiles 的 purchaseId与编辑页一致避免类型/精度问题) */
const applyIdRaw = computed(() => {
if (props.currJob?.orderId != null && props.currJob?.orderId !== '') {
return String(props.currJob.orderId)
}
const id = route.query.id
return id != null && id !== '' ? String(id) : ''
})
const applyRow = ref<any>(null)
const implementType = ref<string>(IMPLEMENT_TYPE.SELF_ORGANIZED)
const implementSubmitting = ref(false)
/** 步骤控制 */
const step1Completed = ref(false)
const saveTypeSubmitting = ref(false)
const isEditingStep1 = ref(false)
const originalImplementType = ref<string>('')
const representorMode = ref<'single' | 'multi'>('single')
const representorTeacherNo = ref<string>('')
const representorsMulti = ref<string[]>([])
const deptMembers = ref<any[]>([])
/** 分配代理相关 */
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>('')
const sendToAgentSubmitting = ref(false)
const revokeAgentSubmitting = ref(false)
let rollInterval: ReturnType<typeof setInterval> | null = null
/** 是否可以分配代理:委托代理采购 且 (学校统一采购 或 部门自行采购且委托采购中心采购) 且 步骤一已完成 */
const canAssignAgent = computed(() => {
// 自行组织采购不需要分配代理
if (implementType.value !== IMPLEMENT_TYPE.ENTRUST_AGENT) return false
// 步骤一必须完成
if (!step1Completed.value) return false
const row = applyRow.value
if (!row) return false
// 学校统一采购 或 部门自行采购且委托采购中心采购
return row.purchaseMode === PURCHASE_MODE.SCHOOL_UNIFIED
|| (row.purchaseMode === PURCHASE_MODE.DEPT_SELF && row.purchaseChannel === PURCHASE_CHANNEL.ENTRUST_CENTER)
})
/** 是否显示步骤二:委托代理采购 且 步骤一已完成 且 不在编辑步骤一 */
const showStep2 = computed(() => {
return step1Completed.value && !isEditingStep1.value && implementType.value === IMPLEMENT_TYPE.ENTRUST_AGENT
})
/** 步骤二是否完成:已分配代理 */
const step2Completed = computed(() => {
return !!applyRow.value?.agentId
})
/** 是否可以发送招标代理:委托代理采购 且 已分配代理 且 未发送 */
const canSendToAgent = computed(() => {
// 自行组织采购不需要发送
if (implementType.value !== IMPLEMENT_TYPE.ENTRUST_AGENT) return false
const row = applyRow.value
if (!row) return false
// 已分配代理 且 未发送
return !!row.agentId && row.agentSent !== YES_NO.YES
})
/** 是否可以重新分配代理:未发送招标代理时可操作 */
const canReassignAgent = computed(() => {
const row = applyRow.value
if (!row) return false
// 未发送招标代理时可重新分配
return row.agentSent !== YES_NO.YES
})
/** 是否可以撤回招标代理:已发送招标代理时可撤回 */
const canRevokeAgent = computed(() => {
const row = applyRow.value
if (!row) return false
// 已分配代理 且 已发送招标代理时可撤回
return !!row.agentId && row.agentSent === YES_NO.YES
})
const loadAgentList = async () => {
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
}
}
/** 随机分配代理 - 滚动动画 */
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
}
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 handleSendToAgent = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return
}
sendToAgentSubmitting.value = true
try {
await sendToAgent(Number(id))
useMessage().success('已发送招标代理')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '发送招标代理失败')
} finally {
sendToAgentSubmitting.value = false
}
}
/** 撤回招标代理 */
const handleRevokeAgent = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return
}
revokeAgentSubmitting.value = true
try {
await revokeAgent(Number(id))
useMessage().success('已撤回招标代理')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '撤回招标代理失败')
} finally {
revokeAgentSubmitting.value = false
}
}
/** 步骤一:保存实施采购方式 */
const handleSaveImplementType = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return
}
if (!implementType.value) {
useMessage().warning('请选择实施采购方式')
return
}
saveTypeSubmitting.value = true
try {
await saveImplementType(Number(id), implementType.value)
useMessage().success('保存成功')
step1Completed.value = true
originalImplementType.value = implementType.value
// 流程嵌入场景:通知流程 Tab 已保存
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit)
}
// 如果是委托代理采购,加载代理列表
if (implementType.value === IMPLEMENT_TYPE.ENTRUST_AGENT) {
await loadAgentList()
}
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saveTypeSubmitting.value = false
}
}
/** 开始编辑步骤一 */
const startEditStep1 = () => {
originalImplementType.value = implementType.value
isEditingStep1.value = true
}
/** 取消编辑步骤一 */
const cancelEditStep1 = () => {
implementType.value = originalImplementType.value
isEditingStep1.value = false
}
/** 重新保存实施采购方式(修改后确认) */
const handleReSaveImplementType = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return
}
if (!implementType.value) {
useMessage().warning('请选择实施采购方式')
return
}
saveTypeSubmitting.value = true
try {
await saveImplementType(Number(id), implementType.value)
useMessage().success('修改成功')
isEditingStep1.value = false
originalImplementType.value = implementType.value
// 如果从委托代理采购改为自行组织采购,清空代理相关状态
if (implementType.value === IMPLEMENT_TYPE.SELF_ORGANIZED) {
assignedAgentName.value = ''
selectedAgentId.value = ''
}
// 如果改为委托代理采购,加载代理列表
if (implementType.value === IMPLEMENT_TYPE.ENTRUST_AGENT) {
await loadAgentList()
}
// 流程嵌入场景:通知流程 Tab 已保存
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit)
}
} catch (e: any) {
useMessage().error(e?.msg || '修改失败')
} finally {
saveTypeSubmitting.value = false
}
}
const isInIframe = () => typeof window !== 'undefined' && window.self !== window.top
const postMessage = (type: string, payload?: any) => {
if (typeof window !== 'undefined' && window.parent) {
window.parent.postMessage({ type, ...payload }, '*')
}
}
const loadData = async () => {
const id = applyId.value
if (!id) {
useMessage().warning('缺少申请单ID')
return
}
const needDeptMembers = isDeptAuditRole.value
try {
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 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
originalImplementType.value = row.implementType
// 已有 implementType 说明步骤一已完成
step1Completed.value = true
}
// 回显需求部门初审-采购代表人方式与人员
if (row?.representorTeacherNo) {
representorMode.value = 'single'
representorTeacherNo.value = row.representorTeacherNo ?? ''
representorsMulti.value = []
} else if (row?.representors) {
representorMode.value = 'multi'
representorTeacherNo.value = ''
const parts = typeof row.representors === 'string' ? row.representors.split(',') : []
representorsMulti.value = parts.map((s: string) => s.trim()).filter(Boolean)
} else {
representorTeacherNo.value = ''
representorsMulti.value = []
}
deptMembers.value = needDeptMembers && membersRes?.data ? membersRes.data : []
// 加载代理列表并回显已分配代理(委托代理采购时)
if (step1Completed.value && implementType.value === IMPLEMENT_TYPE.ENTRUST_AGENT) {
// 先判断是否可以分配代理(学校统一采购 或 部门自行采购且委托采购中心采购)
const canLoadAgent = row.purchaseMode === PURCHASE_MODE.SCHOOL_UNIFIED
|| (row.purchaseMode === PURCHASE_MODE.DEPT_SELF && row.purchaseChannel === PURCHASE_CHANNEL.ENTRUST_CENTER)
if (canLoadAgent) {
await loadAgentList()
// 回显已分配代理
if (row?.agentId) {
selectedAgentId.value = row.agentId
assignedAgentName.value = row.agentName || ''
}
}
}
} catch (_) {
applyRow.value = { id }
deptMembers.value = []
}
}
const handleClose = () => {
postMessage('purchasingimplement:close')
if (!isInIframe()) {
window.history.back()
}
}
const handleImplementSubmit = async () => {
const row = applyRow.value
if (!row?.id && !applyId.value) return
const id = row?.id ?? applyId.value
if (!id) return
// 步骤一未完成时,先保存步骤一
if (!step1Completed.value) {
if (!implementType.value) {
useMessage().warning('请选择实施采购方式')
return
}
// 自动保存步骤一
saveTypeSubmitting.value = true
try {
await saveImplementType(Number(id), implementType.value)
step1Completed.value = true
} catch (e: any) {
useMessage().error(e?.msg || '保存实施采购方式失败')
return
} finally {
saveTypeSubmitting.value = false
}
}
implementSubmitting.value = true
try {
useMessage().success('实施采购已保存')
postMessage('purchasingimplement:saved')
// 流程嵌入场景:通知流程当前 Tab 已保存
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit)
}
} catch (err: any) {
useMessage().error(err?.msg || '实施采购失败')
} finally {
implementSubmitting.value = false
}
}
/** 流程嵌入时供 handle.vue 调用的"保存"回调:与页面按钮保存逻辑保持一致 */
async function flowSubmitForm() {
await handleImplementSubmit()
}
// 流程切换工单时重新加载数据(与 add 编辑页一致)
watch(
() => props.currJob?.orderId ?? props.currJob?.id,
(newVal, oldVal) => {
if (newVal !== oldVal && applyId.value) {
loadData()
}
}
)
onMounted(async () => {
await loadData()
if (isInIframe()) {
document.documentElement.classList.add('iframe-mode')
document.body.classList.add('iframe-mode')
}
// 流程嵌入:注册 tab 保存回调,供审批页调用(与采购申请编辑页保持一致)
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsExist(props.currJob, props.currElTab.id)
await orderVue.currElTabIsView({}, props.currJob, props.currElTab.id, flowSubmitForm)
}
})
onUnmounted(() => {
// 清理滚动动画定时器
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
})
</script>
<style scoped lang="scss">
.implement-page {
padding: 20px;
min-height: 100%;
display: flex;
flex-direction: column;
}
.implement-form {
flex: 1;
.mb-2 {
margin-bottom: 8px;
}
}
.implement-form-tip {
margin-top: 12px;
padding: 8px 0;
}
.implement-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.step-section {
margin-bottom: 24px;
padding: 16px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.step-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--el-color-primary);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
&.completed {
background: var(--el-color-success);
}
}
.step-title {
font-weight: 500;
font-size: 16px;
color: var(--el-text-color-primary);
}
.step-content {
padding-left: 40px;
}
.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>