Files
school-developer/src/views/purchase/purchasingrequisition/implement.vue
2026-03-09 15:22:19 +08:00

593 lines
17 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>
</template>
<script setup lang="ts" name="PurchasingImplement">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { ElMessageBox } from 'element-plus';
import { getObj, assignAgent, sendToAgent, revokeAgent, saveImplementType } from '/@/api/purchase/purchasingrequisition';
import { getPage as getAgentPage } from '/@/api/purchase/purchaseagent';
import { useMessage } from '/@/hooks/message';
import { Session } from '/@/utils/storage';
// ==================== 常量定义(与后端枚举保持一致) ====================
/** 实施采购方式(与后端 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 emit = defineEmits<{
(e: 'close'): void;
(e: 'saved'): void;
}>();
/** 申请单 ID */
const applyId = ref<string | number | null>(null);
const applyRow = ref<any>(null);
const implementType = ref<string>(IMPLEMENT_TYPE.SELF_ORGANIZED);
/** 步骤控制 */
const step1Completed = ref(false);
const saveTypeSubmitting = ref(false);
const isEditingStep1 = ref(false);
const originalImplementType = ref<string>('');
/** 分配代理相关 */
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 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;
const intervalTime = 80;
rollInterval = setInterval(() => {
rollingAgentName.value = agentList.value[currentIndex].agentName;
currentIndex = (currentIndex + 1) % agentList.value.length;
}, intervalTime);
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(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(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;
}
// 确认弹窗
try {
await ElMessageBox.confirm('是否确认发送至招标代理启动招标文件审核流程?', '确认发送', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
});
} catch {
// 用户取消
return;
}
sendToAgentSubmitting.value = true;
try {
await sendToAgent(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(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(id, implementType.value);
useMessage().success('保存成功');
step1Completed.value = true;
originalImplementType.value = implementType.value;
emit('saved');
// 如果是委托代理采购,加载代理列表
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(id, implementType.value);
useMessage().success('修改成功');
isEditingStep1.value = false;
originalImplementType.value = implementType.value;
emit('saved');
// 如果从委托代理采购改为自行组织采购,清空代理相关状态
if (implementType.value === IMPLEMENT_TYPE.SELF_ORGANIZED) {
assignedAgentName.value = '';
selectedAgentId.value = '';
}
// 如果改为委托代理采购,加载代理列表
if (implementType.value === IMPLEMENT_TYPE.ENTRUST_AGENT) {
await loadAgentList();
}
} catch (e: any) {
useMessage().error(e?.msg || '修改失败');
} finally {
saveTypeSubmitting.value = false;
}
};
const loadData = async () => {
const id = applyId.value;
if (!id) {
useMessage().warning('缺少申请单ID');
return;
}
try {
const detailRes = await getObj(id);
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;
step1Completed.value = true;
}
// 加载代理列表并回显已分配代理(委托代理采购时)
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 (_) {
console.log(_);
}
};
/** 打开弹窗 */
const open = async (row: { id: string | number }) => {
applyId.value = row?.id ?? null;
applyRow.value = null;
implementType.value = IMPLEMENT_TYPE.SELF_ORGANIZED;
step1Completed.value = false;
isEditingStep1.value = false;
agentMode.value = 'designated';
selectedAgentId.value = '';
agentList.value = [];
assignedAgentName.value = '';
rollingAgentName.value = '';
await loadData();
};
/** 确定 */
const handleConfirm = 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(id, implementType.value);
step1Completed.value = true;
emit('saved');
} catch (e: any) {
useMessage().error(e?.msg || '保存实施采购方式失败');
return;
} finally {
saveTypeSubmitting.value = false;
}
}
useMessage().success('实施采购已保存');
emit('close');
};
onUnmounted(() => {
if (rollInterval) {
clearInterval(rollInterval);
rollInterval = null;
}
});
defineExpose({
open,
handleConfirm,
});
</script>
<style scoped lang="scss">
.implement-page {
padding: 0;
min-height: 100%;
display: flex;
flex-direction: column;
}
.implement-form {
flex: 1;
.mb-2 {
margin-bottom: 8px;
}
}
.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>