593 lines
17 KiB
Vue
593 lines
17 KiB
Vue
<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>
|