rebuild all

This commit is contained in:
吴红兵
2026-03-02 10:06:48 +08:00
parent c8aaeee298
commit 4167a2a94d
12 changed files with 836 additions and 4 deletions

View 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'
});
}

View File

@@ -271,4 +271,39 @@ export function finalizeDoc(data: any) {
method: 'post', method: 'post',
data 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
});
} }

View File

@@ -33,7 +33,7 @@ export function getPage(params?: any) {
* 通过id查询 * 通过id查询
* @param id ID * @param id ID
*/ */
export function getObj(id: number) { export function getObj(id: string | number) {
return request({ return request({
url: '/purchase/purchasingapply/' + id, url: '/purchase/purchasingapply/' + id,
method: 'get' method: 'get'
@@ -123,7 +123,7 @@ export function saveImplementType(id: number | string, implementType: string) {
return request({ return request({
url: '/purchase/purchasingapply/save-implement-type', url: '/purchase/purchasingapply/save-implement-type',
method: 'post', method: 'post',
data: { id: Number(id), implementType } data: { id: id, implementType }
}); });
} }

View File

@@ -11,6 +11,7 @@ import { staticRoutes, notFoundAndNoPower } from '/@/router/route';
import { initBackEndControlRoutes } from '/@/router/backEnd'; import { initBackEndControlRoutes } from '/@/router/backEnd';
import { flowConfig } from "/@/flow/designer/config/flow-config"; import { flowConfig } from "/@/flow/designer/config/flow-config";
import { replaceRouterRoute } from "/@/flow/support/extend"; import { replaceRouterRoute } from "/@/flow/support/extend";
import { NextLoading } from '/@/utils/loading';
/** /**
* 1、前端控制路由时isRequestRoutes 为 false需要写 roles需要走 setFilterRoute 方法。 * 1、前端控制路由时isRequestRoutes 为 false需要写 roles需要走 setFilterRoute 方法。
@@ -148,6 +149,7 @@ router.beforeEach(async (to, from, next) => {
// 路由加载后 // 路由加载后
router.afterEach(() => { router.afterEach(() => {
NProgress.done(); NProgress.done();
NextLoading.done();
}); });
// 导出路由 // 导出路由

View File

@@ -99,6 +99,14 @@ export const staticRoutes: Array<RouteRecordRaw> = [
isAuth: true, isAuth: true,
}, },
}, },
{
path: '/purchase/purchasingrequisition/add',
name: 'purchase.purchasingrequisition.add',
component: () => import('/@/views/purchase/purchasingrequisition/add.vue'),
meta: {
isAuth: true,
},
},
...staticRoutesFlow ...staticRoutesFlow
]; ];

View File

@@ -40,7 +40,7 @@
{{ t('sysmenu.permission') }} {{ t('sysmenu.permission') }}
<tip content="对应后台接口@PreAuthorize注解入参字符串"></tip> <tip content="对应后台接口@PreAuthorize注解入参字符串"></tip>
</template> </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>
<el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder"> <el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder">
<el-input-number v-model="state.ruleForm.sortOrder" :min="0" controls-position="right"/> <el-input-number v-model="state.ruleForm.sortOrder" :min="0" controls-position="right"/>

View File

@@ -72,6 +72,17 @@
<org-selector v-model:orgList="assetAdminList" type="user" :multiple="false" @update:orgList="onAssetAdminChange" /> <org-selector v-model:orgList="assetAdminList" type="user" :multiple="false" @update:orgList="onAssetAdminChange" />
</el-form-item> </el-form-item>
</el-col> </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-row>
</el-form> </el-form>
</template> </template>
@@ -113,6 +124,7 @@ const form = reactive({
purchaserName: '', purchaserName: '',
assetAdminId: '', assetAdminId: '',
assetAdminName: '', assetAdminName: '',
transactionAmount: null,
...props.modelValue, ...props.modelValue,
}) })

View File

@@ -186,6 +186,7 @@ const loadData = async () => {
totalPhases: config.common.totalPhases || 1, totalPhases: config.common.totalPhases || 1,
supplierName: config.common.supplierName || '', supplierName: config.common.supplierName || '',
supplierContact: config.common.supplierContact || '', supplierContact: config.common.supplierContact || '',
transactionAmount: config.common.transactionAmount || null,
}) })
} }
} }
@@ -275,6 +276,7 @@ const saveCommonConfig = async () => {
purchaserName: String(form.purchaserName ?? ''), purchaserName: String(form.purchaserName ?? ''),
assetAdminId: String(form.assetAdminId ?? ''), assetAdminId: String(form.assetAdminId ?? ''),
assetAdminName: String(form.assetAdminName ?? ''), assetAdminName: String(form.assetAdminName ?? ''),
transactionAmount: form.transactionAmount ?? null,
}) })
useMessage().success('保存成功') useMessage().success('保存成功')
await loadData() await loadData()
@@ -353,6 +355,7 @@ const DEFAULT_COMMON_FORM = {
purchaserName: '', purchaserName: '',
assetAdminId: '', assetAdminId: '',
assetAdminName: '', assetAdminName: '',
transactionAmount: null,
} }
/** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */ /** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */

View File

@@ -1204,7 +1204,7 @@ const handleCancel = () => {
async function loadDetail(applyId: string | number) { async function loadDetail(applyId: string | number) {
if (!applyId) return; if (!applyId) return;
try { try {
const res = await getObj(Number(applyId)); const res = await getObj(String(applyId));
const detail = res?.data; const detail = res?.data;
if (detail && typeof detail === 'object') { if (detail && typeof detail === 'object') {
Object.assign(dataForm, { Object.assign(dataForm, {

View File

@@ -127,6 +127,119 @@
<el-tab-pane label="审核记录" name="audit"> <el-tab-pane label="审核记录" name="audit">
<AuditRecordList :apply-id="applyId" ref="auditRecordListRef" /> <AuditRecordList :apply-id="applyId" ref="auditRecordListRef" />
</el-tab-pane> </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> </el-tabs>
<!-- 操作区域 --> <!-- 操作区域 -->
@@ -244,9 +357,17 @@ import {
submitToAsset as submitToAssetApi, submitToAsset as submitToAssetApi,
finalizeDoc as finalizeDocApi finalizeDoc as finalizeDocApi
} from '/@/api/purchase/docProcess' } from '/@/api/purchase/docProcess'
import {
getNoticeByApplyId,
saveNotice,
publishNotice,
canSendNotice
} from '/@/api/purchase/bidOpeningNotice'
import type { UploadInstance, UploadProps, UploadUserFile } from 'element-plus' import type { UploadInstance, UploadProps, UploadUserFile } from 'element-plus'
import { Document } from '@element-plus/icons-vue'
const AuditRecordList = defineAsyncComponent(() => import('./AuditRecordList.vue')) const AuditRecordList = defineAsyncComponent(() => import('./AuditRecordList.vue'))
const ReviewerSetting = defineAsyncComponent(() => import('./ReviewerSetting.vue'))
const props = defineProps<{ const props = defineProps<{
mode: 'agent' | 'audit' mode: 'agent' | 'audit'
@@ -297,6 +418,33 @@ const supplyUploadForm = ref({
fileRemark: '' 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(() => { const dialogTitle = computed(() => {
return props.mode === 'agent' ? `处理项目 - ${rowData.value.purchaseNo || ''}` : '招标文件审核' 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 isConfirming = computed(() => statusField.value === 'ASSET_CONFIRMING')
const isCompleted = computed(() => statusField.value === 'COMPLETED') 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 uploadAction = computed(() => {
const baseUrl = import.meta.env.VITE_API_URL || '' const baseUrl = import.meta.env.VITE_API_URL || ''
@@ -365,6 +519,9 @@ const open = async (row: any) => {
fileList.value = [] fileList.value = []
uploadedFileData.value = null uploadedFileData.value = null
// 重置开标通知表单
resetBidOpeningForm()
// 获取申请ID兼容 id 和 applyId 两种字段名) // 获取申请ID兼容 id 和 applyId 两种字段名)
applyId.value = row.applyId || row.id applyId.value = row.applyId || row.id
@@ -380,6 +537,10 @@ const open = async (row: any) => {
loadRequirementFiles() loadRequirementFiles()
// 加载招标文件 // 加载招标文件
loadDocList() loadDocList()
// 加载开标通知(如果是招标代理模式且状态为已完成)
if (props.mode === 'agent') {
loadBidOpeningNotice()
}
} }
const loadRequirementFiles = async () => { 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 = () => { const handleClose = () => {
visible.value = false visible.value = false
} }
@@ -841,6 +1150,11 @@ const getFileTypeLabel = (type: string) => {
return labelMap[type] || type return labelMap[type] || type
} }
// 采购代表设置保存成功回调
const handleReviewerSaved = () => {
emit('refresh')
}
defineExpose({ open }) defineExpose({ open })
</script> </script>
@@ -868,4 +1182,29 @@ defineExpose({ open })
.mt-4 { .mt-4 {
margin-top: 16px; 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> </style>

View File

@@ -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>

View File

@@ -316,6 +316,7 @@ const handleRevokeAgent = async () => {
const handleSaveImplementType = async () => { const handleSaveImplementType = async () => {
const id = applyRow.value?.id ?? applyId.value const id = applyRow.value?.id ?? applyId.value
if (!id) { if (!id) {
useMessage().warning('无法获取申请单ID') useMessage().warning('无法获取申请单ID')
return return
@@ -326,6 +327,7 @@ const handleSaveImplementType = async () => {
} }
saveTypeSubmitting.value = true saveTypeSubmitting.value = true
try { try {
await saveImplementType(id, implementType.value) await saveImplementType(id, implementType.value)
useMessage().success('保存成功') useMessage().success('保存成功')
step1Completed.value = true step1Completed.value = true