采购更新

This commit is contained in:
吴红兵
2026-02-28 16:47:22 +08:00
parent eafa255359
commit f5df8200aa
7 changed files with 406 additions and 287 deletions

View File

@@ -188,6 +188,20 @@ export function getDeptMembers() {
}); });
} }
/**
* 保存采购代表(指定单人或部门多人)
* @param id 采购申请ID
* @param representorTeacherNo 指定采购代表人单人用户ID或工号
* @param representors 部门多人由系统抽取多人用户ID或工号逗号分隔
*/
export function saveRepresentor(id: number, representorTeacherNo?: string, representors?: string) {
return request({
url: '/purchase/purchasingapply/saveRepresentor',
method: 'post',
data: { id, representorTeacherNo, representors }
});
}
/** /**
* 文件归档按文件类型打包下载该申请单下所有附件的下载地址GET 请求,浏览器直接下载 zip * 文件归档按文件类型打包下载该申请单下所有附件的下载地址GET 请求,浏览器直接下载 zip
* @param purchaseId 采购申请ID * @param purchaseId 采购申请ID

View File

@@ -169,6 +169,7 @@ interface FileItem {
url?: string; url?: string;
uid?: number; uid?: number;
id?: string; // 文件ID id?: string; // 文件ID
fileTitle?: string; // 文件标题(用于显示)
} }
interface UploadFileItem { interface UploadFileItem {

View File

@@ -17,20 +17,12 @@
<!-- 上传履约验收模版 --> <!-- 上传履约验收模版 -->
<el-col :span="12" class="mb20"> <el-col :span="12" class="mb20">
<el-form-item label="履约验收模版" prop="templateFileIds"> <el-form-item label="履约验收文件" prop="templateFileIds">
<div class="upload-with-template"> <upload-file v-model="templateFiles" :limit="1" :file-type="['doc', 'docx', 'pdf']" :data="{ purchaseId: purchaseId || '', fileType: '110' }" upload-file-url="/purchase/purchasingfiles/upload" :disabled="readonly" />
<UploadFile <el-link v-if="!readonly" type="primary" :href="lyysTemplateUrl" :download="lyysTemplateDownloadName" target="_blank" style="margin-top: 8px; display: inline-flex; align-items: center;">
v-model="templateFileIdsStr" <el-icon><Download /></el-icon>
:limit="1" <span style="margin-left: 4px;">下载{{ lyysTemplateLabel }}</span>
:data="{ purchaseId: purchaseId || '', fileType: '110' }" </el-link>
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="readonly"
/>
<el-link type="primary" :href="lyysTemplateUrl" :download="lyysTemplateDownloadName" target="_blank" style="margin-top: 8px; display: inline-flex; align-items: center;">
<el-icon><Download /></el-icon>
<span style="margin-left: 4px;">下载{{ lyysTemplateLabel }}</span>
</el-link>
</div>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -44,9 +36,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue' import { ref, reactive, computed, watch, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { Download } from '@element-plus/icons-vue' import { Download } from '@element-plus/icons-vue'
import UploadFile from "/@/components/Upload/index.vue";
/** 项目类型 A:货物 B:工程 C:服务 */ /** 项目类型 A:货物 B:工程 C:服务 */
const LYYS_TEMPLATE_MAP: Record<string, { label: string }> = { const LYYS_TEMPLATE_MAP: Record<string, { label: string }> = {
@@ -55,22 +48,20 @@ const LYYS_TEMPLATE_MAP: Record<string, { label: string }> = {
C: { label: '履约验收表模板(服务)' }, C: { label: '履约验收表模板(服务)' },
} }
const props = withDefaults( const props = defineProps<{
defineProps<{ modelValue: Record<string, any>
modelValue: Record<string, any> readonly?: boolean
readonly?: boolean purchaseId?: string
purchaseId?: string /** 项目类型 A:货物 B:工程 C:服务,用于模版下载 */
/** 项目类型 A:货物 B:工程 C:服务,用于模版下载 */ projectType?: string
projectType?: string batchNum?: number
batchNum?: number }>()
}>(),
{ readonly: false, purchaseId: '', projectType: 'A', batchNum: 1 }
)
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const templateFileIdsStr = ref('') // 文件对象数组用于上传组件显示包含id和fileTitle
const templateFiles = ref<any[]>([])
const projectTypeKey = computed(() => (props.projectType === 'A' || props.projectType === 'B' || props.projectType === 'C' ? props.projectType : 'A')) const projectTypeKey = computed(() => (props.projectType === 'A' || props.projectType === 'B' || props.projectType === 'C' ? props.projectType : 'A'))
const lyysTemplateLabel = computed(() => LYYS_TEMPLATE_MAP[projectTypeKey.value]?.label || LYYS_TEMPLATE_MAP.A.label) const lyysTemplateLabel = computed(() => LYYS_TEMPLATE_MAP[projectTypeKey.value]?.label || LYYS_TEMPLATE_MAP.A.label)
@@ -82,27 +73,57 @@ const form = reactive({
acceptDate: '', acceptDate: '',
templateFileIds: [] as string[], templateFileIds: [] as string[],
remark: '', remark: '',
...props.modelValue,
}) })
// 确保acceptType始终为'2' // 从外部数据初始化(仅在挂载和 modelValue 引用变化时执行)
watch(() => props.modelValue, (val) => { const initData = () => {
Object.assign(form, val || {}) const val = props.modelValue
form.acceptType = '2' // 强制上传模式 if (!val) return
}, { deep: true })
watch(form, () => emit('update:modelValue', { ...form }), { deep: true }) form.acceptType = '2'
form.acceptDate = val.acceptDate || ''
form.remark = val.remark || ''
watch(templateFileIdsStr, (s) => { // 处理文件数据:支持 _templateFiles 数组或 templateFileIds
const arr = s ? s.split(',').map((x: string) => x.trim()).filter(Boolean) : [] if (val._templateFiles && Array.isArray(val._templateFiles)) {
if (JSON.stringify(form.templateFileIds) !== JSON.stringify(arr)) { templateFiles.value = val._templateFiles.map((f: any) => ({
form.templateFileIds = arr id: f.id,
fileTitle: f.fileTitle,
name: f.fileTitle || '',
url: '',
}))
form.templateFileIds = val._templateFiles.map((f: any) => f.id)
} else if (val.templateFileIds) {
if (typeof val.templateFileIds === 'string') {
const ids = val.templateFileIds.split(',').filter(Boolean)
templateFiles.value = ids.map((id: string) => ({ id: id.trim(), name: '', url: '' }))
form.templateFileIds = ids
} else if (Array.isArray(val.templateFileIds)) {
templateFiles.value = val.templateFileIds.map((item: any) => {
if (typeof item === 'string') return { id: item, name: '', url: '' }
return { id: item.id, fileTitle: item.fileTitle, name: item.fileTitle || '', url: '' }
})
form.templateFileIds = val.templateFileIds.map((item: any) => typeof item === 'string' ? item : item.id).filter(Boolean)
}
} else {
templateFiles.value = []
form.templateFileIds = []
} }
}
// 挂载时初始化
onMounted(() => {
initData()
}) })
watch(() => form.templateFileIds, (arr) => { // 监听文件变化,更新 form.templateFileIds
if (Array.isArray(arr) && arr.length) templateFileIdsStr.value = arr.join(',') watch(templateFiles, (files) => {
}, { immediate: true, deep: true }) if (Array.isArray(files)) {
form.templateFileIds = files.map((f: any) => f.id).filter(Boolean)
} else {
form.templateFileIds = []
}
}, { deep: true })
const rules: FormRules = { const rules: FormRules = {
acceptDate: [{ required: true, message: '请选择验收日期', trigger: 'change' }], acceptDate: [{ required: true, message: '请选择验收日期', trigger: 'change' }],
@@ -110,16 +131,19 @@ const rules: FormRules = {
const validate = () => formRef.value?.validate() const validate = () => formRef.value?.validate()
defineExpose({ validate, form }) // 获取当前表单数据(供父组件调用)
const getFormData = () => ({
acceptType: form.acceptType,
acceptDate: form.acceptDate,
templateFileIds: [...form.templateFileIds],
remark: form.remark,
})
defineExpose({ validate, form, getFormData, initData })
</script> </script>
<style scoped> <style scoped>
.mb20 { .mb20 {
margin-bottom: 20px; margin-bottom: 20px;
} }
.upload-with-template {
display: flex;
flex-direction: column;
gap: 8px;
}
</style> </style>

View File

@@ -59,7 +59,7 @@
v-show="String(b.batch) === activeTab" v-show="String(b.batch) === activeTab"
:key="b.id" :key="b.id"
:ref="(el) => setBatchFormRef(b.batch, el)" :ref="(el) => setBatchFormRef(b.batch, el)"
v-model="batchForms[b.batch]" :model-value="batchForms[b.batch]"
:readonly="false" :readonly="false"
:purchase-id="String(purchaseId)" :purchase-id="String(purchaseId)"
:project-type="acceptProjectType" :project-type="acceptProjectType"
@@ -221,11 +221,29 @@ const loadBatchDetails = async () => {
// 仅当该期在服务端有验收日期时才视为已保存 // 仅当该期在服务端有验收日期时才视为已保存
const hasSaved = !!d.accept.acceptDate const hasSaved = !!d.accept.acceptDate
batchSavedFlags.value[b.batch] = hasSaved batchSavedFlags.value[b.batch] = hasSaved
// 优先使用 templateFiles包含id和fileTitle否则降级使用 templateFileIds
let fileIdsStr = ''
if (d.accept.templateFiles && d.accept.templateFiles.length > 0) {
// 使用 templateFiles格式为 {id: string, fileTitle: string}[]
fileIdsStr = d.accept.templateFiles.map((f: any) => f.id).join(',')
} else if (d.accept.templateFileIds) {
// 降级使用 templateFileIds
const fileIds = d.accept.templateFileIds
fileIdsStr = Array.isArray(fileIds) ? fileIds.join(',') : (fileIds || '')
}
batchForms[b.batch] = { batchForms[b.batch] = {
acceptType: '2', // 固定为上传模式 acceptType: '2', // 固定为上传模式
acceptDate: d.accept.acceptDate || '', acceptDate: d.accept.acceptDate || '',
remark: d.accept.remark || '', remark: d.accept.remark || '',
templateFileIds: d.accept.templateFileIds || [], templateFileIds: fileIdsStr,
// 保存文件信息用于显示
_templateFiles: d.accept.templateFiles || [],
}
// 通知子组件初始化数据
await nextTick()
const batchFormRef = batchFormRefMap.value[b.batch]
if (batchFormRef?.initData) {
batchFormRef.initData()
} }
} }
} catch (_) {} } catch (_) {}
@@ -275,23 +293,39 @@ const saveCurrentBatch = async () => {
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value) const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
if (!b?.id) return if (!b?.id) return
const form = batchForms[curBatch]
if (!form) return
if (!form.acceptDate) { // 从子组件获取表单数据
const formData = batchFormRef?.getFormData?.() || batchFormRef?.form
if (!formData) return
if (!formData.acceptDate) {
useMessage().error('请选择验收日期') useMessage().error('请选择验收日期')
return return
} }
// templateFileIds: 提取ID数组
let fileIds: string[] = []
if (formData.templateFileIds) {
if (Array.isArray(formData.templateFileIds)) {
fileIds = formData.templateFileIds.map((item: any) => {
if (typeof item === 'string') return item
if (item && item.id) return item.id
return null
}).filter(Boolean)
} else if (typeof formData.templateFileIds === 'string') {
fileIds = formData.templateFileIds.split(',').map((s: string) => s.trim()).filter(Boolean)
}
}
saving.value = true saving.value = true
try { try {
await updateBatch({ await updateBatch({
id: b.id, id: b.id,
purchaseId: String(purchaseId.value), purchaseId: String(purchaseId.value),
acceptType: '2', // 固定为上传模式 acceptType: '2', // 固定为上传模式
acceptDate: form.acceptDate, acceptDate: formData.acceptDate,
remark: form.remark, remark: formData.remark,
templateFileIds: form.templateFileIds || [], templateFileIds: fileIds,
}) })
useMessage().success('保存成功') useMessage().success('保存成功')
batchSavedFlags.value[curBatch] = true batchSavedFlags.value[curBatch] = true

View File

@@ -211,10 +211,9 @@
<el-radio-group v-model="dataForm.purchaseMode" :disabled="schoolUnifiedPurchaseFormDisabled"> <el-radio-group v-model="dataForm.purchaseMode" :disabled="schoolUnifiedPurchaseFormDisabled">
<el-radio v-for="item in purchaseModeSchoolList" :key="item.value" :label="item.value">{{ item.label }}</el-radio> <el-radio v-for="item in purchaseModeSchoolList" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group> </el-radio-group>
<!-- <div v-if="schoolUnifiedPurchaseFormDefault != null" class="template-note mt5"><el-text type="info" size="small">根据预算金额与是否集采由系统自动选择</el-text></div> -->
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8" class="mb12" v-if="isDeptPurchase || isFlowEmbed"> <el-col :span="8" class="mb12" >
<el-form-item label="采购方式" prop="purchaseType" :required="!isDeptPurchase"> <el-form-item label="采购方式" prop="purchaseType" :required="!isDeptPurchase">
<el-select v-model="dataForm.purchaseType" placeholder="请选择采购方式" clearable :disabled="(isFlowEmbed && isPurchaseCenter) ? false : (isAutoSelectPurchaseTypeUnion || flowFieldDisabled('purchaseType'))" style="width: 100%"> <el-select v-model="dataForm.purchaseType" placeholder="请选择采购方式" clearable :disabled="(isFlowEmbed && isPurchaseCenter) ? false : (isAutoSelectPurchaseTypeUnion || flowFieldDisabled('purchaseType'))" style="width: 100%">
<el-option v-for="item in purchaseTypeUnionList" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in purchaseTypeUnionList" :key="item.value" :label="item.label" :value="item.value" />

View File

@@ -8,33 +8,37 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<!-- 采购文件版本列表保留原文件多版本分别显示 --> <!-- 分配代理仅符合条件的申请单显示 -->
<el-divider content-position="left">采购文件版本</el-divider> <template v-if="canAssignAgent">
<div v-if="purchaseFileVersions.length" class="file-versions mb-2"> <el-divider content-position="left">分配代理</el-divider>
<el-table :data="purchaseFileVersions" border size="small" max-height="280"> <el-form-item label="分配方式">
<el-table-column type="index" label="版本" width="70" align="center"> <el-radio-group v-model="agentMode">
<template #default="{ $index }">V{{ $index + 1 }}</template> <el-radio label="designated">指定代理</el-radio>
</el-table-column> <el-radio label="random">随机分配</el-radio>
<el-table-column prop="fileTitle" label="文件名" min-width="180" show-overflow-tooltip /> </el-radio-group>
<el-table-column prop="createBy" label="上传人" width="100" align="center" show-overflow-tooltip /> </el-form-item>
<el-table-column prop="createTime" label="上传时间" width="165" align="center"> <el-form-item v-if="agentMode === 'designated'" label="选择代理">
<template #default="{ row }">{{ formatCreateTime(row.createTime) }}</template> <el-select v-model="selectedAgentId" placeholder="请选择招标代理" filterable style="width: 100%" :loading="agentListLoading">
</el-table-column> <el-option v-for="item in agentList" :key="item.id" :label="item.agentName" :value="item.id" />
<el-table-column label="操作" width="90" align="center" fixed="right"> </el-select>
<template #default="{ row }"> </el-form-item>
<el-button type="primary" link size="small" @click="handleDownloadVersion(row)">下载</el-button> <el-form-item v-if="agentMode === 'random'" label="随机结果">
</template> <div class="agent-roller">
</el-table-column> <span v-if="rollingAgentName" class="rolling">{{ rollingAgentName }}</span>
</el-table> <span v-else-if="assignedAgentName" class="assigned">已分配{{ assignedAgentName }}</span>
</div> <span v-else class="placeholder">点击下方按钮进行随机分配</span>
<div class="mb-2">可继续上传新版本保留原文件格式 doc/docx/pdf单文件不超过 5MB</div> </div>
<upload-file </el-form-item>
v-model="implementFileIds" <el-form-item v-if="applyRow?.agentName" label="当前代理">
:limit="5" <el-tag>{{ applyRow.agentName }}</el-tag>
:file-type="['doc', 'docx', 'pdf']" </el-form-item>
:data="{ fileType: PURCHASE_FILE_TYPE }" <el-form-item v-if="agentMode === 'designated'">
upload-file-url="/purchase/purchasingfiles/upload" <el-button type="primary" :loading="assignAgentSubmitting" :disabled="!selectedAgentId" @click="handleAssignAgentDesignated">指定代理</el-button>
/> </el-form-item>
<el-form-item v-if="agentMode === 'random'">
<el-button type="primary" :loading="assignAgentSubmitting" :disabled="!!assignedAgentName" @click="handleAssignAgentRandom">随机分配</el-button>
</el-form-item>
</template>
<!-- 仅部门审核角色显示采购代表相关 --> <!-- 仅部门审核角色显示采购代表相关 -->
<template v-if="isDeptAuditRole"> <template v-if="isDeptAuditRole">
@@ -59,37 +63,25 @@
</div> </div>
<div class="implement-footer"> <div class="implement-footer">
<el-button @click="handleClose">取消</el-button> <el-button @click="handleClose">取消</el-button>
<template v-if="implementHasPurchaseFiles && !applyRow?.fileFlowInstId"> <el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">确定</el-button>
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">保存实施采购</el-button>
<el-button v-if="canStartFileFlow" type="success" :loading="startFileFlowSubmitting" @click="handleStartFileFlow">发起采购文件审批</el-button>
</template>
<template v-else>
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">确定</el-button>
</template>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts" name="PurchasingImplement"> <script setup lang="ts" name="PurchasingImplement">
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { implementApply, getApplyFiles, startFileFlow, getDeptMembers, getObj } from '/@/api/finance/purchasingrequisition' import { getDeptMembers, getObj, assignAgent } from '/@/api/finance/purchasingrequisition'
import { getPage as getAgentPage } from '/@/api/finance/purchaseagent'
import { useMessage } from '/@/hooks/message' import { useMessage } from '/@/hooks/message'
import other from '/@/utils/other'
import UploadFile from '/@/components/Upload/index.vue'
import { Session } from '/@/utils/storage' import { Session } from '/@/utils/storage'
import * as orderVue from '/@/api/order/order-key-vue' import * as orderVue from '/@/api/order/order-key-vue'
/** 部门审核角色编码:仅该角色下显示采购代表相关页面和功能,流转至部门审核时需填写采购代表 */ /** 部门审核角色编码:仅该角色下显示采购代表相关页面和功能,流转至部门审核时需填写采购代表 */
const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT' const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT'
/** 采购中心角色编码:可保存/发起实施采购,但不出现采购代表相关内容和接口 */
const PURCHASE_CENTER_ROLE_CODE = 'PURCHASE_CENTER'
const roleCode = computed(() => Session.getRoleCode() || '') const roleCode = computed(() => Session.getRoleCode() || '')
const isDeptAuditRole = computed(() => roleCode.value === PURCHASE_DEPT_AUDIT_ROLE_CODE) const isDeptAuditRole = computed(() => roleCode.value === PURCHASE_DEPT_AUDIT_ROLE_CODE)
const isPurchaseCenterRole = computed(() => roleCode.value === PURCHASE_CENTER_ROLE_CODE)
/** 可发起采购文件审批:部门审核(需填采购代表)、采购中心(不填采购代表) */
const canStartFileFlow = computed(() => isDeptAuditRole.value || isPurchaseCenterRole.value)
// 与编辑界面一致:支持流程 dynamic-link 传入 currJob/currElTab申请单 ID 优先取 currJob.orderId // 与编辑界面一致:支持流程 dynamic-link 传入 currJob/currElTab申请单 ID 优先取 currJob.orderId
const props = defineProps({ const props = defineProps({
@@ -102,7 +94,6 @@ const emit = defineEmits(['handleJob'])
const isFlowEmbed = computed(() => !!props.currJob) const isFlowEmbed = computed(() => !!props.currJob)
const route = useRoute() const route = useRoute()
const PURCHASE_FILE_TYPE = '130'
/** 申请单 ID数值用于 getObj 等):与 add 一致,优先流程 currJob.orderId否则 route.query.id */ /** 申请单 ID数值用于 getObj 等):与 add 一致,优先流程 currJob.orderId否则 route.query.id */
const applyId = computed(() => { const applyId = computed(() => {
@@ -122,10 +113,6 @@ const applyIdRaw = computed(() => {
}) })
const applyRow = ref<any>(null) const applyRow = ref<any>(null)
/** 已有采购文件版本列表(按 createTime 排序,用于展示与提交时一并关联) */
const purchaseFileVersions = ref<{ id: string; fileTitle?: string; createBy?: string; createTime?: string; remark?: string }[]>([])
/** 本次新上传的采购文件(仅新版本,不与已有版本混在一起) */
const implementFileIds = ref<string | string[]>([])
const implementType = ref<string>('1') const implementType = ref<string>('1')
const implementSubmitting = ref(false) const implementSubmitting = ref(false)
@@ -133,28 +120,117 @@ const representorMode = ref<'single' | 'multi'>('single')
const representorTeacherNo = ref<string>('') const representorTeacherNo = ref<string>('')
const representorsMulti = ref<string[]>([]) const representorsMulti = ref<string[]>([])
const deptMembers = ref<any[]>([]) const deptMembers = ref<any[]>([])
const startFileFlowSubmitting = ref(false)
const implementHasPurchaseFiles = computed(() => { /** 分配代理相关 */
if (purchaseFileVersions.value.length > 0) return true const agentMode = ref<'designated' | 'random'>('designated')
const raw = implementFileIds.value const selectedAgentId = ref<string>('')
if (Array.isArray(raw)) return raw.length > 0 const agentList = ref<any[]>([])
return !!raw const agentListLoading = ref(false)
const assignAgentSubmitting = ref(false)
const rollingAgentName = ref<string>('')
const assignedAgentName = ref<string>('')
let rollInterval: ReturnType<typeof setInterval> | null = null
/** 是否可以分配代理:委托代理采购 且 (学校统一采购 或 部门自行采购且委托采购中心采购) */
const canAssignAgent = computed(() => {
// 自行组织采购不需要分配代理
if (implementType.value !== '2') return false
const row = applyRow.value
if (!row) return false
return row.purchaseMode === '2' || (row.purchaseMode === '0' && row.purchaseType === '4')
}) })
function formatCreateTime(t?: string) { const loadAgentList = async () => {
if (!t) return '-' if (!canAssignAgent.value) return
const d = new Date(t) agentListLoading.value = true
return isNaN(d.getTime()) ? t : d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) 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
}
} }
function handleDownloadVersion(file: { remark?: string; fileTitle?: string }) { /** 随机分配代理 - 滚动动画 */
if (!file?.remark) { const startRollingAnimation = (finalAgentName: string) => {
useMessage().warning('无法获取文件路径') if (agentList.value.length === 0) return
// 清除之前的动画
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
rollingAgentName.value = ''
assignedAgentName.value = ''
let currentIndex = 0
const totalDuration = 2000 // 总动画时间 2秒
const intervalTime = 80 // 滚动间隔
rollInterval = setInterval(() => {
rollingAgentName.value = agentList.value[currentIndex].agentName
currentIndex = (currentIndex + 1) % agentList.value.length
}, intervalTime)
// 2秒后停止并显示最终结果
setTimeout(() => {
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
rollingAgentName.value = ''
assignedAgentName.value = finalAgentName
}, totalDuration)
}
const handleAssignAgentRandom = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return return
} }
const url = `/purchase/purchasingfiles/download?fileName=${encodeURIComponent(file.remark)}&fileTitle=${encodeURIComponent(file.fileTitle || '采购文件')}` assignAgentSubmitting.value = true
other.downBlobFile(url, {}, file.fileTitle || '采购文件') try {
const res = await assignAgent(Number(id), 'random')
// 后端返回分配结果后,展示滚动动画
const finalAgentName = res?.data?.agentName || res?.agentName || ''
if (finalAgentName) {
startRollingAnimation(finalAgentName)
} else {
useMessage().success('随机分配代理成功')
await loadData()
}
} catch (e: any) {
useMessage().error(e?.msg || '随机分配代理失败')
} finally {
assignAgentSubmitting.value = false
}
}
const handleAssignAgentDesignated = async () => {
const id = applyRow.value?.id ?? applyId.value
if (!id) {
useMessage().warning('无法获取申请单ID')
return
}
if (!selectedAgentId.value) {
useMessage().warning('请选择招标代理')
return
}
assignAgentSubmitting.value = true
try {
await assignAgent(Number(id), 'designated', selectedAgentId.value)
useMessage().success('指定代理成功')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '指定代理失败')
} finally {
assignAgentSubmitting.value = false
}
} }
const isInIframe = () => typeof window !== 'undefined' && window.self !== window.top const isInIframe = () => typeof window !== 'undefined' && window.self !== window.top
@@ -173,21 +249,16 @@ const loadData = async () => {
} }
const needDeptMembers = isDeptAuditRole.value const needDeptMembers = isDeptAuditRole.value
try { try {
const idStr = applyIdRaw.value || String(id) const requests: [ReturnType<typeof getObj>, ReturnType<typeof getDeptMembers>?] = [getObj(id)]
const requests: [ReturnType<typeof getObj>, ReturnType<typeof getApplyFiles>, ReturnType<typeof getDeptMembers>?] = [
getObj(id),
getApplyFiles(idStr)
]
if (needDeptMembers) requests.push(getDeptMembers()) if (needDeptMembers) requests.push(getDeptMembers())
const results = await Promise.all(requests) const results = await Promise.all(requests)
const detailRes = results[0] const detailRes = results[0]
const filesRes = results[1] const membersRes = needDeptMembers ? results[1] : null
const membersRes = needDeptMembers ? results[2] : null
applyRow.value = detailRes?.data ? { ...detailRes.data, id: detailRes.data.id ?? id } : { id } applyRow.value = detailRes?.data ? { ...detailRes.data, id: detailRes.data.id ?? id } : { id }
const row = applyRow.value const row = applyRow.value
if (row?.implementType) implementType.value = row.implementType if (row?.implementType) implementType.value = row.implementType
// 回显需求部门初审-采购代表人方式与人员(与发起采购文件审批时保存的一致) // 回显需求部门初审-采购代表人方式与人员
if (row?.representorTeacherNo) { if (row?.representorTeacherNo) {
representorMode.value = 'single' representorMode.value = 'single'
representorTeacherNo.value = row.representorTeacherNo ?? '' representorTeacherNo.value = row.representorTeacherNo ?? ''
@@ -201,19 +272,17 @@ const loadData = async () => {
representorTeacherNo.value = '' representorTeacherNo.value = ''
representorsMulti.value = [] representorsMulti.value = []
} }
const list = filesRes?.data || []
const purchaseFiles = list.filter((f: any) => f.fileType === PURCHASE_FILE_TYPE)
purchaseFileVersions.value = purchaseFiles.map((f: any) => ({
id: String(f.id),
fileTitle: f.fileTitle || f.file_title || '采购文件',
createBy: f.createBy ?? f.create_by ?? '-',
createTime: f.createTime || f.create_time,
remark: f.remark
}))
deptMembers.value = needDeptMembers && membersRes?.data ? membersRes.data : [] deptMembers.value = needDeptMembers && membersRes?.data ? membersRes.data : []
// 加载代理列表并回显已分配代理
if (canAssignAgent.value) {
await loadAgentList()
if (row?.agentName) {
assignedAgentName.value = row.agentName
}
}
} catch (_) { } catch (_) {
applyRow.value = { id } applyRow.value = { id }
purchaseFileVersions.value = []
deptMembers.value = [] deptMembers.value = []
} }
} }
@@ -234,27 +303,23 @@ const handleImplementSubmit = async () => {
useMessage().warning('请选择实施采购方式') useMessage().warning('请选择实施采购方式')
return return
} }
const existingIds = purchaseFileVersions.value.map((f) => f.id) // 仅部门审核角色校验采购代表人
const raw = implementFileIds.value if (isDeptAuditRole.value) {
const newIds: string[] = Array.isArray(raw) if (representorMode.value === 'single' && !representorTeacherNo.value) {
? raw.map((x: any) => (typeof x === 'object' && x?.id ? x.id : x)).filter(Boolean) useMessage().warning('请选择采购代表人')
: raw ? [String(raw)] : [] return
const fileIds = [...existingIds, ...newIds] }
if (fileIds.length === 0) { if (representorMode.value === 'multi' && !representorsMulti.value?.length) {
useMessage().warning('请至少上传一个采购文件') useMessage().warning('请选择部门多人')
return return
}
} }
// 仅部门审核角色提交采购代表;采购中心保存时不传采购代表
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' && representorsMulti.value?.length ? representorsMulti.value.join(',') : undefined
implementSubmitting.value = true implementSubmitting.value = true
try { try {
await implementApply(id, fileIds, implementType.value, single, multi) // TODO: 调用保存接口
useMessage().success('实施采购已保存') useMessage().success('实施采购已保存')
implementFileIds.value = []
await loadData()
postMessage('purchasingimplement:saved') postMessage('purchasingimplement:saved')
// 流程嵌入场景:通知流程当前 Tab 已保存,避免审批时提示“未保存” // 流程嵌入场景:通知流程当前 Tab 已保存
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) { if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit) orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit)
} }
@@ -265,40 +330,7 @@ const handleImplementSubmit = async () => {
} }
} }
const handleStartFileFlow = async () => { /** 流程嵌入时供 handle.vue 调用的”保存”回调:与页面按钮保存逻辑保持一致 */
const row = applyRow.value
const id = row?.id ?? applyId.value
if (!id) return
// 部门审核角色必须填写采购代表;采购中心不填采购代表
if (isDeptAuditRole.value) {
if (representorMode.value === 'single') {
if (!representorTeacherNo.value) {
useMessage().warning('请选择采购代表人')
return
}
} else {
if (!representorsMulti.value?.length) {
useMessage().warning('请选择部门多人')
return
}
}
}
startFileFlowSubmitting.value = true
try {
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' ? representorsMulti.value.join(',') : undefined
await startFileFlow(id, single, multi)
useMessage().success('已发起采购文件审批流程')
postMessage('purchasingimplement:submitSuccess')
await loadData()
} catch (err: any) {
useMessage().error(err?.msg || '发起失败')
} finally {
startFileFlowSubmitting.value = false
}
}
/** 流程嵌入时供 handle.vue 调用的“保存”回调:与页面按钮保存逻辑保持一致 */
async function flowSubmitForm() { async function flowSubmitForm() {
await handleImplementSubmit() await handleImplementSubmit()
} }
@@ -325,6 +357,14 @@ onMounted(async () => {
await orderVue.currElTabIsView({}, props.currJob, props.currElTab.id, flowSubmitForm) await orderVue.currElTabIsView({}, props.currJob, props.currElTab.id, flowSubmitForm)
} }
}) })
onUnmounted(() => {
// 清理滚动动画定时器
if (rollInterval) {
clearInterval(rollInterval)
rollInterval = null
}
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -352,4 +392,35 @@ onMounted(async () => {
gap: 12px; gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.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> </style>

View File

@@ -292,63 +292,52 @@
<!-- 实施采购iframe 嵌入 implement.vue供列表与流程页面使用 --> <!-- 实施采购iframe 嵌入 implement.vue供列表与流程页面使用 -->
<ImplementForm ref="implementFormRef" @refresh="getDataList" /> <ImplementForm ref="implementFormRef" @refresh="getDataList" />
<!-- 分配代理弹窗 --> <!-- 采购文件审核弹窗 -->
<DocAuditDialog ref="docAuditDialogRef" @refresh="getDataList" />
<!-- 采购代表弹窗 -->
<el-dialog <el-dialog
v-model="assignAgentDialogVisible" v-model="representorDialogVisible"
title="分配代理" title="设置采购代表"
width="420px" width="500px"
destroy-on-close destroy-on-close
@close="assignAgentForm.agentId = ''; assignAgentForm.mode = 'designated'"
> >
<el-form label-width="90px"> <el-form label-width="100px">
<el-form-item label="分配方式"> <el-form-item label="选择方式">
<el-radio-group v-model="assignAgentForm.mode"> <el-radio-group v-model="representorForm.mode">
<el-radio label="designated">指定</el-radio> <el-radio label="single">指定采购代表人</el-radio>
<el-radio label="random">随机</el-radio> <el-radio label="multi">部门多人系统抽取</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="assignAgentForm.mode === 'designated'" label="招标代理"> <el-form-item v-if="representorForm.mode === 'single'" label="采购代表人">
<el-select <el-select
v-model="assignAgentForm.agentId" v-model="representorForm.teacherNo"
placeholder="请选择招标代理" placeholder="请选择"
clearable clearable
filterable filterable
style="width: 100%" style="width: 100%"
:loading="agentListLoading"
> >
<el-option v-for="item in agentList" :key="item.id" :label="item.agentName" :value="item.id" /> <el-option v-for="m in representorDeptMembers" :key="m.userId || m.teacherNo || m.id" :label="m.realName || m.name || m.teacherNo" :value="m.userId || m.teacherNo || m.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="assignAgentCurrentRow?.agentName" label="当前代理"> <el-form-item v-else label="部门多人">
<span>{{ assignAgentCurrentRow.agentName }}</span> <el-select
v-model="representorForm.multiIds"
multiple
placeholder="请选择多人,系统将自动抽取一人"
clearable
filterable
style="width: 100%"
>
<el-option v-for="m in representorDeptMembers" :key="m.userId || m.teacherNo || m.id" :label="m.realName || m.name || m.teacherNo" :value="m.userId || m.teacherNo || m.id" />
</el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span> <el-button @click="representorDialogVisible = false">取消</el-button>
<el-button @click="assignAgentDialogVisible = false">取消</el-button> <el-button type="primary" :loading="representorSubmitting" @click="handleSaveRepresentor">确定</el-button>
<el-button
v-if="assignAgentForm.mode === 'random'"
type="primary"
:loading="assignAgentSubmitting"
@click="handleAssignAgentRandom"
>
随机分配
</el-button>
<el-button
v-else
type="primary"
:loading="assignAgentSubmitting"
:disabled="!assignAgentForm.agentId"
@click="handleAssignAgentDesignated"
>
确定
</el-button>
</span>
</template> </template>
</el-dialog> </el-dialog>
<!-- 采购文件审核弹窗 -->
<DocAuditDialog ref="docAuditDialogRef" @refresh="getDataList" />
</div> </div>
</template> </template>
@@ -356,14 +345,19 @@
import { ref, reactive, defineAsyncComponent, onMounted, computed } from 'vue' import { ref, reactive, defineAsyncComponent, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table"; import { BasicTableProps, useTable } from "/@/hooks/table";
import { getPage, delObj, submitObj, getArchiveDownloadUrl, getApplyTemplateDownloadUrl, getFileApplyTemplateDownloadUrl, assignAgent } from "/@/api/finance/purchasingrequisition"; import { getPage, delObj, submitObj, getArchiveDownloadUrl, getApplyTemplateDownloadUrl, getFileApplyTemplateDownloadUrl, getDeptMembers, saveRepresentor } from "/@/api/finance/purchasingrequisition";
import { getPage as getAgentPage } from '/@/api/finance/purchaseagent';
import { useMessage, useMessageBox } from "/@/hooks/message"; import { useMessage, useMessageBox } from "/@/hooks/message";
import { useAuth } from '/@/hooks/auth'; import { useAuth } from '/@/hooks/auth';
import { getDicts } from '/@/api/admin/dict'; import { getDicts } from '/@/api/admin/dict';
import { getTree } from '/@/api/finance/purchasingcategory'; import { getTree } from '/@/api/finance/purchasingcategory';
import { List, Document, DocumentCopy, Search, Collection, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning, DocumentChecked, Edit, Delete, Upload, FolderOpened, Download } from '@element-plus/icons-vue' import { List, Document, DocumentCopy, Search, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning, DocumentChecked, Edit, Delete, Upload, FolderOpened, Download, User } from '@element-plus/icons-vue'
import other from '/@/utils/other' import other from '/@/utils/other'
import { Session } from '/@/utils/storage'
// 角色常量
const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT'
const roleCode = computed(() => Session.getRoleCode() || '')
const isDeptAuditRole = computed(() => roleCode.value === PURCHASE_DEPT_AUDIT_ROLE_CODE)
// 引入组件 // 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue')); const FormDialog = defineAsyncComponent(() => import('./form.vue'));
@@ -400,72 +394,54 @@ const currFlowCommentType = ref<'apply' | 'file'>('apply')
const implementFormRef = ref() const implementFormRef = ref()
/** 分配代理弹窗 */ /** 采购代表弹窗 */
const assignAgentDialogVisible = ref(false) const representorDialogVisible = ref(false)
const assignAgentCurrentRow = ref<any>(null) const representorCurrentRow = ref<any>(null)
const assignAgentForm = reactive({ mode: 'designated' as 'designated' | 'random', agentId: '' }) const representorForm = reactive({ mode: 'single' as 'single' | 'multi', teacherNo: '', multiIds: [] as string[] })
const agentList = ref<any[]>([]) const representorDeptMembers = ref<any[]>([])
const agentListLoading = ref(false) const representorSubmitting = ref(false)
const assignAgentSubmitting = ref(false)
const openAssignAgentDialog = async (row: any) => { const openRepresentorDialog = async (row: any) => {
assignAgentCurrentRow.value = row representorCurrentRow.value = row
assignAgentForm.mode = 'designated' representorForm.mode = 'single'
assignAgentForm.agentId = '' representorForm.teacherNo = ''
assignAgentDialogVisible.value = true representorForm.multiIds = []
agentListLoading.value = true representorDialogVisible.value = true
try { try {
const res = await getAgentPage({ size: 500, current: 1 }) const res = await getDeptMembers()
const records = res?.data?.records ?? res?.records ?? [] representorDeptMembers.value = res?.data || []
agentList.value = Array.isArray(records) ? records : []
} catch (_) { } catch (_) {
agentList.value = [] representorDeptMembers.value = []
} finally {
agentListLoading.value = false
} }
} }
const handleAssignAgentRandom = async () => { const handleSaveRepresentor = async () => {
const row = assignAgentCurrentRow.value const row = representorCurrentRow.value
const id = row?.id ?? row?.purchaseId const id = row?.id ?? row?.purchaseId
if (id == null || id === '') { if (id == null || id === '') {
useMessage().warning('无法获取申请单ID') useMessage().warning('无法获取申请单ID')
return return
} }
assignAgentSubmitting.value = true if (representorForm.mode === 'single' && !representorForm.teacherNo) {
try { useMessage().warning('请选择采购代表人')
await assignAgent(Number(id), 'random')
useMessage().success('随机分配代理成功')
assignAgentDialogVisible.value = false
getDataList()
} catch (e: any) {
useMessage().error(e?.msg || '随机分配代理失败')
} finally {
assignAgentSubmitting.value = false
}
}
const handleAssignAgentDesignated = async () => {
const row = assignAgentCurrentRow.value
const id = row?.id ?? row?.purchaseId
if (id == null || id === '') {
useMessage().warning('无法获取申请单ID')
return return
} }
if (!assignAgentForm.agentId) { if (representorForm.mode === 'multi' && !representorForm.multiIds.length) {
useMessage().warning('请选择招标代理') useMessage().warning('请选择部门多人')
return return
} }
assignAgentSubmitting.value = true representorSubmitting.value = true
try { try {
await assignAgent(Number(id), 'designated', assignAgentForm.agentId) const teacherNo = representorForm.mode === 'single' ? representorForm.teacherNo : undefined
useMessage().success('指定代理成功') const multiIds = representorForm.mode === 'multi' ? representorForm.multiIds.join(',') : undefined
assignAgentDialogVisible.value = false await saveRepresentor(Number(id), teacherNo, multiIds)
useMessage().success('保存采购代表成功')
representorDialogVisible.value = false
getDataList() getDataList()
} catch (e: any) { } catch (e: any) {
useMessage().error(e?.msg || '指定代理失败') useMessage().error(e?.msg || '保存采购代表失败')
} finally { } finally {
assignAgentSubmitting.value = false representorSubmitting.value = false
} }
} }
@@ -648,18 +624,18 @@ const getActionMenuItems = (row: any) => {
icon: Download, icon: Download,
visible: () => true, visible: () => true,
}, },
{
command: 'representor',
label: '采购代表',
icon: User,
visible: () => isDeptAuditRole.value,
},
// { // {
// command: 'downloadFileApply', // command: 'downloadFileApply',
// label: '下载文件审批表', // label: '下载文件审批表',
// icon: Download, // icon: Download,
// visible: () => true, // visible: () => true,
// }, // },
{
command: 'assignAgent',
label: '分配代理',
icon: Collection,
visible: () => row?.purchaseMode === '2' || (row?.purchaseMode === '0' && row?.purchaseType === '4'),
}
// { // {
// command: 'docAudit', // command: 'docAudit',
// label: '采购文件审核', // label: '采购文件审核',
@@ -703,12 +679,12 @@ const handleMoreCommand = (command: string, row: any) => {
case 'downloadFileApply': case 'downloadFileApply':
handleDownloadFileApply(row); handleDownloadFileApply(row);
break; break;
case 'assignAgent':
openAssignAgentDialog(row);
break;
case 'docAudit': case 'docAudit':
handleDocAudit(row); handleDocAudit(row);
break; break;
case 'representor':
openRepresentorDialog(row);
break;
} }
}; };