rebuild all
This commit is contained in:
64
src/api/purchase/bidOpeningNotice.ts
Normal file
64
src/api/purchase/bidOpeningNotice.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, cyweb All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
*/
|
||||
|
||||
import request from '/@/utils/request';
|
||||
|
||||
/**
|
||||
* 根据采购申请ID获取开标通知
|
||||
* @param applyId 采购申请ID
|
||||
*/
|
||||
export function getNoticeByApplyId(applyId: string | number) {
|
||||
return request({
|
||||
url: '/purchase/bidopeningnotice/' + applyId,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存开标通知(草稿)
|
||||
* @param data 开标通知数据
|
||||
*/
|
||||
export function saveNotice(data: any) {
|
||||
return request({
|
||||
url: '/purchase/bidopeningnotice/save',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布开标通知
|
||||
* @param data 开标通知数据
|
||||
*/
|
||||
export function publishNotice(data: any) {
|
||||
return request({
|
||||
url: '/purchase/bidopeningnotice/publish',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否可以发送开标通知
|
||||
* @param applyId 采购申请ID
|
||||
*/
|
||||
export function canSendNotice(applyId: string | number) {
|
||||
return request({
|
||||
url: '/purchase/bidopeningnotice/can-send/' + applyId,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
@@ -272,3 +272,38 @@ export function finalizeDoc(data: any) {
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取采购代表设置信息
|
||||
* @param applyId 采购申请ID
|
||||
*/
|
||||
export function getReviewerSetting(applyId: string | number) {
|
||||
return request({
|
||||
url: `/purchase/purchasingdoc/reviewer/${applyId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置采购代表
|
||||
* @param data 设置信息
|
||||
*/
|
||||
export function setReviewerSetting(data: any) {
|
||||
return request({
|
||||
url: '/purchase/purchasingdoc/reviewer/set',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机抽取采购代表
|
||||
* @param data 候选人列表
|
||||
*/
|
||||
export function randomSelectReviewer(data: any) {
|
||||
return request({
|
||||
url: '/purchase/purchasingdoc/reviewer/random',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export function getPage(params?: any) {
|
||||
* 通过id查询
|
||||
* @param id ID
|
||||
*/
|
||||
export function getObj(id: number) {
|
||||
export function getObj(id: string | number) {
|
||||
return request({
|
||||
url: '/purchase/purchasingapply/' + id,
|
||||
method: 'get'
|
||||
@@ -123,7 +123,7 @@ export function saveImplementType(id: number | string, implementType: string) {
|
||||
return request({
|
||||
url: '/purchase/purchasingapply/save-implement-type',
|
||||
method: 'post',
|
||||
data: { id: Number(id), implementType }
|
||||
data: { id: id, implementType }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { staticRoutes, notFoundAndNoPower } from '/@/router/route';
|
||||
import { initBackEndControlRoutes } from '/@/router/backEnd';
|
||||
import { flowConfig } from "/@/flow/designer/config/flow-config";
|
||||
import { replaceRouterRoute } from "/@/flow/support/extend";
|
||||
import { NextLoading } from '/@/utils/loading';
|
||||
|
||||
/**
|
||||
* 1、前端控制路由时:isRequestRoutes 为 false,需要写 roles,需要走 setFilterRoute 方法。
|
||||
@@ -148,6 +149,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
// 路由加载后
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
NextLoading.done();
|
||||
});
|
||||
|
||||
// 导出路由
|
||||
|
||||
@@ -99,6 +99,14 @@ export const staticRoutes: Array<RouteRecordRaw> = [
|
||||
isAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/purchase/purchasingrequisition/add',
|
||||
name: 'purchase.purchasingrequisition.add',
|
||||
component: () => import('/@/views/purchase/purchasingrequisition/add.vue'),
|
||||
meta: {
|
||||
isAuth: true,
|
||||
},
|
||||
},
|
||||
|
||||
...staticRoutesFlow
|
||||
];
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{{ t('sysmenu.permission') }}
|
||||
<tip content="对应后台接口@PreAuthorize注解入参字符串"></tip>
|
||||
</template>
|
||||
<el-input v-model="state.ruleForm.permission" maxlength="30" :placeholder="$t('sysmenu.inputPermissionTip')"/>
|
||||
<el-input v-model="state.ruleForm.permission" maxlength="100" :placeholder="$t('sysmenu.inputPermissionTip')"/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder">
|
||||
<el-input-number v-model="state.ruleForm.sortOrder" :min="0" controls-position="right"/>
|
||||
|
||||
@@ -72,6 +72,17 @@
|
||||
<org-selector v-model:orgList="assetAdminList" type="user" :multiple="false" @update:orgList="onAssetAdminChange" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8" class="mb20" v-if="form.hasContract === '0'">
|
||||
<el-form-item label="成交金额" prop="transactionAmount">
|
||||
<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>
|
||||
@@ -113,6 +124,7 @@ const form = reactive({
|
||||
purchaserName: '',
|
||||
assetAdminId: '',
|
||||
assetAdminName: '',
|
||||
transactionAmount: null,
|
||||
...props.modelValue,
|
||||
})
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ const loadData = async () => {
|
||||
totalPhases: config.common.totalPhases || 1,
|
||||
supplierName: config.common.supplierName || '',
|
||||
supplierContact: config.common.supplierContact || '',
|
||||
transactionAmount: config.common.transactionAmount || null,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -275,6 +276,7 @@ const saveCommonConfig = async () => {
|
||||
purchaserName: String(form.purchaserName ?? ''),
|
||||
assetAdminId: String(form.assetAdminId ?? ''),
|
||||
assetAdminName: String(form.assetAdminName ?? ''),
|
||||
transactionAmount: form.transactionAmount ?? null,
|
||||
})
|
||||
useMessage().success('保存成功')
|
||||
await loadData()
|
||||
@@ -353,6 +355,7 @@ const DEFAULT_COMMON_FORM = {
|
||||
purchaserName: '',
|
||||
assetAdminId: '',
|
||||
assetAdminName: '',
|
||||
transactionAmount: null,
|
||||
}
|
||||
|
||||
/** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */
|
||||
|
||||
@@ -1204,7 +1204,7 @@ const handleCancel = () => {
|
||||
async function loadDetail(applyId: string | number) {
|
||||
if (!applyId) return;
|
||||
try {
|
||||
const res = await getObj(Number(applyId));
|
||||
const res = await getObj(String(applyId));
|
||||
const detail = res?.data;
|
||||
if (detail && typeof detail === 'object') {
|
||||
Object.assign(dataForm, {
|
||||
|
||||
@@ -127,6 +127,119 @@
|
||||
<el-tab-pane label="审核记录" name="audit">
|
||||
<AuditRecordList :apply-id="applyId" ref="auditRecordListRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 采购代表设置(仅需求部门审核时显示) -->
|
||||
<el-tab-pane v-if="canSetReviewer" label="采购代表设置" name="reviewer">
|
||||
<ReviewerSetting :apply-id="applyId" @saved="handleReviewerSaved" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 开标通知 - 仅招标代理模式且状态为已完成时显示 -->
|
||||
<el-tab-pane v-if="showBidOpeningNotice" label="开标通知" name="bidOpening">
|
||||
<div class="bid-opening-form" v-loading="bidOpeningLoading">
|
||||
<el-form :model="bidOpeningForm" :rules="bidOpeningRules" ref="bidOpeningFormRef" label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目名称">
|
||||
<el-input v-model="rowData.projectName" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开标时间" prop="openTime">
|
||||
<el-date-picker
|
||||
v-model="bidOpeningForm.openTime"
|
||||
type="datetime"
|
||||
placeholder="请选择开标时间"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开标地点" prop="openLocation">
|
||||
<el-input v-model="bidOpeningForm.openLocation" placeholder="请输入开标地点" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="公示网址">
|
||||
<el-input v-model="bidOpeningForm.publicUrl" placeholder="请输入公示网址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="代理联系人" prop="agentContactName">
|
||||
<el-input v-model="bidOpeningForm.agentContactName" placeholder="请输入代理联系人姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" prop="agentContactPhone">
|
||||
<el-input v-model="bidOpeningForm.agentContactPhone" placeholder="请输入代理联系人电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标确认函">
|
||||
<el-upload
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:data="{ fileType: '131', purchaseId: applyId }"
|
||||
:on-success="(res: any, file: any) => handleBidFileSuccess(res, file, 'bidConfirmationLetter')"
|
||||
:before-upload="beforeUpload"
|
||||
:show-file-list="false"
|
||||
accept=".doc,.docx,.pdf,.jpg,.jpeg,.png">
|
||||
<el-button type="primary" icon="Upload">上传文件</el-button>
|
||||
</el-upload>
|
||||
<div v-if="bidOpeningForm.bidConfirmationLetter" class="file-info">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>已上传</span>
|
||||
<el-button type="primary" link @click="viewFile(bidOpeningForm.bidConfirmationLetter)">查看</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="授权书">
|
||||
<el-upload
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:data="{ fileType: '132', purchaseId: applyId }"
|
||||
:on-success="(res: any, file: any) => handleBidFileSuccess(res, file, 'authorizationLetter')"
|
||||
:before-upload="beforeUpload"
|
||||
:show-file-list="false"
|
||||
accept=".doc,.docx,.pdf,.jpg,.jpeg,.png">
|
||||
<el-button type="primary" icon="Upload">上传文件</el-button>
|
||||
</el-upload>
|
||||
<div v-if="bidOpeningForm.authorizationLetter" class="file-info">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>已上传</span>
|
||||
<el-button type="primary" link @click="viewFile(bidOpeningForm.authorizationLetter)">查看</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="bidOpeningForm.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24" class="form-actions">
|
||||
<el-button type="info" :loading="bidOpeningSaving" @click="handleSaveBidOpeningDraft">保存草稿</el-button>
|
||||
<el-button type="success" :loading="bidOpeningPublishing" @click="handlePublishBidOpening">发布通知</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<!-- 已发布提示 -->
|
||||
<el-alert v-if="bidOpeningNoticeStatus === 'PUBLISHED'" type="success" :closable="false" class="mt-4">
|
||||
<template #title>
|
||||
<span>开标通知已于 {{ bidOpeningForm.publishTime }} 发布</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
@@ -244,9 +357,17 @@ import {
|
||||
submitToAsset as submitToAssetApi,
|
||||
finalizeDoc as finalizeDocApi
|
||||
} from '/@/api/purchase/docProcess'
|
||||
import {
|
||||
getNoticeByApplyId,
|
||||
saveNotice,
|
||||
publishNotice,
|
||||
canSendNotice
|
||||
} from '/@/api/purchase/bidOpeningNotice'
|
||||
import type { UploadInstance, UploadProps, UploadUserFile } from 'element-plus'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
|
||||
const AuditRecordList = defineAsyncComponent(() => import('./AuditRecordList.vue'))
|
||||
const ReviewerSetting = defineAsyncComponent(() => import('./ReviewerSetting.vue'))
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'agent' | 'audit'
|
||||
@@ -297,6 +418,33 @@ const supplyUploadForm = ref({
|
||||
fileRemark: ''
|
||||
})
|
||||
|
||||
// 开标通知相关
|
||||
const bidOpeningFormRef = ref()
|
||||
const bidOpeningLoading = ref(false)
|
||||
const bidOpeningSaving = ref(false)
|
||||
const bidOpeningPublishing = ref(false)
|
||||
const bidOpeningNoticeStatus = ref('')
|
||||
const bidOpeningForm = ref({
|
||||
id: '',
|
||||
applyId: '',
|
||||
projectName: '',
|
||||
openTime: '',
|
||||
openLocation: '',
|
||||
agentContactName: '',
|
||||
agentContactPhone: '',
|
||||
publicUrl: '',
|
||||
bidConfirmationLetter: '',
|
||||
authorizationLetter: '',
|
||||
remarks: '',
|
||||
publishTime: ''
|
||||
})
|
||||
const bidOpeningRules = {
|
||||
openTime: [{ required: true, message: '请选择开标时间', trigger: 'change' }],
|
||||
openLocation: [{ required: true, message: '请输入开标地点', trigger: 'blur' }],
|
||||
agentContactName: [{ required: true, message: '请输入代理联系人姓名', trigger: 'blur' }],
|
||||
agentContactPhone: [{ required: true, message: '请输入代理联系人电话', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 弹窗标题
|
||||
const dialogTitle = computed(() => {
|
||||
return props.mode === 'agent' ? `处理项目 - ${rowData.value.purchaseNo || ''}` : '招标文件审核'
|
||||
@@ -336,6 +484,12 @@ const isReviewing = computed(() => ['ASSET_REVIEWING', 'DEPT_REVIEWING', 'AUDIT_
|
||||
const isConfirming = computed(() => statusField.value === 'ASSET_CONFIRMING')
|
||||
const isCompleted = computed(() => statusField.value === 'COMPLETED')
|
||||
|
||||
// 是否显示开标通知Tab(招标代理模式且状态为已完成)
|
||||
const showBidOpeningNotice = computed(() => props.mode === 'agent' && isCompleted.value)
|
||||
|
||||
// 是否显示采购代表设置Tab(需求部门审核中且有提交权限)
|
||||
const canSetReviewer = computed(() => statusField.value === 'DEPT_REVIEWING' && availableActions.value.includes('submitToAsset'))
|
||||
|
||||
// 上传配置
|
||||
const uploadAction = computed(() => {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || ''
|
||||
@@ -365,6 +519,9 @@ const open = async (row: any) => {
|
||||
fileList.value = []
|
||||
uploadedFileData.value = null
|
||||
|
||||
// 重置开标通知表单
|
||||
resetBidOpeningForm()
|
||||
|
||||
// 获取申请ID(兼容 id 和 applyId 两种字段名)
|
||||
applyId.value = row.applyId || row.id
|
||||
|
||||
@@ -380,6 +537,10 @@ const open = async (row: any) => {
|
||||
loadRequirementFiles()
|
||||
// 加载招标文件
|
||||
loadDocList()
|
||||
// 加载开标通知(如果是招标代理模式且状态为已完成)
|
||||
if (props.mode === 'agent') {
|
||||
loadBidOpeningNotice()
|
||||
}
|
||||
}
|
||||
|
||||
const loadRequirementFiles = async () => {
|
||||
@@ -801,6 +962,154 @@ const submitFinalize = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 开标通知相关方法 ====================
|
||||
|
||||
/**
|
||||
* 重置开标通知表单
|
||||
*/
|
||||
const resetBidOpeningForm = () => {
|
||||
bidOpeningForm.value = {
|
||||
id: '',
|
||||
applyId: '',
|
||||
projectName: '',
|
||||
openTime: '',
|
||||
openLocation: '',
|
||||
agentContactName: '',
|
||||
agentContactPhone: '',
|
||||
publicUrl: '',
|
||||
bidConfirmationLetter: '',
|
||||
authorizationLetter: '',
|
||||
remarks: '',
|
||||
publishTime: ''
|
||||
}
|
||||
bidOpeningNoticeStatus.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载开标通知
|
||||
*/
|
||||
const loadBidOpeningNotice = async () => {
|
||||
if (!applyId.value) return
|
||||
bidOpeningLoading.value = true
|
||||
try {
|
||||
const res = await getNoticeByApplyId(applyId.value)
|
||||
if (res?.code === 0 || res?.code === 200) {
|
||||
const data = res.data
|
||||
if (data) {
|
||||
bidOpeningForm.value = {
|
||||
id: data.id || '',
|
||||
applyId: data.applyId || applyId.value,
|
||||
projectName: data.projectName || rowData.value.projectName || '',
|
||||
openTime: data.openTime || '',
|
||||
openLocation: data.openLocation || '',
|
||||
agentContactName: data.agentContactName || '',
|
||||
agentContactPhone: data.agentContactPhone || '',
|
||||
publicUrl: data.publicUrl || '',
|
||||
bidConfirmationLetter: data.bidConfirmationLetter || '',
|
||||
authorizationLetter: data.authorizationLetter || '',
|
||||
remarks: data.remarks || '',
|
||||
publishTime: data.publishTime || ''
|
||||
}
|
||||
bidOpeningNoticeStatus.value = data.status || ''
|
||||
} else {
|
||||
bidOpeningForm.value.applyId = applyId.value
|
||||
bidOpeningForm.value.projectName = rowData.value.projectName || ''
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误,可能是第一次创建
|
||||
} finally {
|
||||
bidOpeningLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开标通知文件上传成功回调
|
||||
*/
|
||||
const handleBidFileSuccess = (response: any, file: any, field: string) => {
|
||||
if (response?.code === 0 || response?.code === 200) {
|
||||
bidOpeningForm.value[field] = response.data.remark || response.data.filePath
|
||||
useMessage().success('文件上传成功')
|
||||
} else {
|
||||
useMessage().error(response?.msg || '文件上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看文件
|
||||
*/
|
||||
const viewFile = (url: string) => {
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存开标通知草稿
|
||||
*/
|
||||
const handleSaveBidOpeningDraft = async () => {
|
||||
try {
|
||||
await bidOpeningFormRef.value?.validate()
|
||||
} catch {
|
||||
useMessage().warning('请填写必填项')
|
||||
return
|
||||
}
|
||||
|
||||
bidOpeningSaving.value = true
|
||||
try {
|
||||
const res = await saveNotice({
|
||||
...bidOpeningForm.value,
|
||||
applyId: applyId.value
|
||||
})
|
||||
if (res?.code === 0 || res?.code === 200) {
|
||||
useMessage().success('保存成功')
|
||||
await loadBidOpeningNotice()
|
||||
} else {
|
||||
useMessage().error(res?.msg || '保存失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
useMessage().error(e?.msg || '保存失败')
|
||||
} finally {
|
||||
bidOpeningSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布开标通知
|
||||
*/
|
||||
const handlePublishBidOpening = async () => {
|
||||
try {
|
||||
await bidOpeningFormRef.value?.validate()
|
||||
} catch {
|
||||
useMessage().warning('请填写必填项')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await useMessageBox().confirm('确定要发布开标通知吗?发布后将无法修改。')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
bidOpeningPublishing.value = true
|
||||
try {
|
||||
const res = await publishNotice({
|
||||
...bidOpeningForm.value,
|
||||
applyId: applyId.value
|
||||
})
|
||||
if (res?.code === 0 || res?.code === 200) {
|
||||
useMessage().success('发布成功')
|
||||
await loadBidOpeningNotice()
|
||||
} else {
|
||||
useMessage().error(res?.msg || '发布失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
useMessage().error(e?.msg || '发布失败')
|
||||
} finally {
|
||||
bidOpeningPublishing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
@@ -841,6 +1150,11 @@ const getFileTypeLabel = (type: string) => {
|
||||
return labelMap[type] || type
|
||||
}
|
||||
|
||||
// 采购代表设置保存成功回调
|
||||
const handleReviewerSaved = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
@@ -868,4 +1182,29 @@ defineExpose({ open })
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.bid-opening-form {
|
||||
padding: 20px;
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
color: #606266;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,367 @@
|
||||
<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-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>
|
||||
|
||||
<!-- 随机抽取模式 -->
|
||||
<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="抽取结果">
|
||||
<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>
|
||||
<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 { getReviewerSetting, setReviewerSetting, randomSelectReviewer } from '/@/api/purchase/docProcess'
|
||||
import orgSelector from '/@/components/OrgSelector/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
applyId: string | number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
// 常量
|
||||
const SELECT_MODE = {
|
||||
DESIGNATED: 'DESIGNATED',
|
||||
RANDOM: 'RANDOM',
|
||||
} as const
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
selectMode: 'DESIGNATED' as 'DESIGNATED' | 'RANDOM',
|
||||
teacherNo: '',
|
||||
teacherName: '',
|
||||
candidates: [] as Array<{ teacherNo: string; teacherName: string }>
|
||||
})
|
||||
|
||||
// 当前采购代表(已有设置)
|
||||
const currentRepresentor = ref({
|
||||
teacherNo: '',
|
||||
teacherName: ''
|
||||
})
|
||||
|
||||
// 用户选择相关
|
||||
const selectedUserList = ref<any[]>([])
|
||||
const candidateUserList = ref<any[]>([])
|
||||
|
||||
// 候选人列表
|
||||
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 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
|
||||
}
|
||||
})
|
||||
|
||||
// 处理选择方式变化
|
||||
const handleSelectModeChange = () => {
|
||||
formData.value.teacherNo = ''
|
||||
formData.value.teacherName = ''
|
||||
formData.value.candidates = []
|
||||
selectedUserList.value = []
|
||||
candidateUserList.value = []
|
||||
candidates.value = []
|
||||
selectedCandidate.value = { teacherNo: '', teacherName: '' }
|
||||
rollingName.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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 处理候选人变化
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 随机抽取动画
|
||||
const startRollingAnimation = (finalCandidate: { teacherNo: string; teacherName: string }) => {
|
||||
if (candidates.value.length === 0) return
|
||||
|
||||
if (rollInterval) {
|
||||
clearInterval(rollInterval)
|
||||
rollInterval = null
|
||||
}
|
||||
|
||||
rollingName.value = ''
|
||||
rolling.value = true
|
||||
|
||||
let currentIndex = 0
|
||||
const totalDuration = 2000
|
||||
const intervalTime = 80
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 执行随机抽取
|
||||
const handleRandomSelect = async () => {
|
||||
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 || '随机抽取失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const handleSave = async () => {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
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 || ''
|
||||
}
|
||||
formData.value.selectMode = data.selectMode || SELECT_MODE.DESIGNATED
|
||||
formData.value.teacherNo = data.teacherNo || ''
|
||||
formData.value.teacherName = data.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.selectMode === SELECT_MODE.RANDOM && data.teacherNo) {
|
||||
selectedCandidate.value = {
|
||||
teacherNo: data.teacherNo,
|
||||
teacherName: data.teacherName || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 忽略错误
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (rollInterval) {
|
||||
clearInterval(rollInterval)
|
||||
rollInterval = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reviewer-setting {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.random-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;
|
||||
}
|
||||
|
||||
.selected {
|
||||
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>
|
||||
@@ -316,6 +316,7 @@ const handleRevokeAgent = async () => {
|
||||
const handleSaveImplementType = async () => {
|
||||
|
||||
const id = applyRow.value?.id ?? applyId.value
|
||||
|
||||
if (!id) {
|
||||
useMessage().warning('无法获取申请单ID')
|
||||
return
|
||||
@@ -326,6 +327,7 @@ const handleSaveImplementType = async () => {
|
||||
}
|
||||
saveTypeSubmitting.value = true
|
||||
try {
|
||||
|
||||
await saveImplementType(id, implementType.value)
|
||||
useMessage().success('保存成功')
|
||||
step1Completed.value = true
|
||||
|
||||
Reference in New Issue
Block a user