手写签名,线下评审,预审评委会

This commit is contained in:
jlzhangyx5
2025-07-11 08:44:50 +08:00
parent 87a7ecc309
commit 0a3fa905ca
12 changed files with 2183 additions and 25 deletions

View File

@ -38,6 +38,11 @@ export default [//评标
path: '/EvaRoom/Evaluation/projectManager/ReviewResults/Manager',
component: './Evaluation/projectManager/ReviewResults/Manager',
},
//评审结果-线下评审-项目经理
{
path: '/EvaRoom/Evaluation/projectManager/ReviewResults/ManagerEntry',
component: './Evaluation/projectManager/ReviewResults/ManagerEntry',
},
//评审结果-组长
{
path: '/EvaRoom/Evaluation/expert/ReviewResults/GroupLeader',

View File

@ -39,7 +39,7 @@ export default
//评委会设置 资审发售 项目经理
{
path: '/ProjectLayout/ZYuShen/Tender/ProjectManager/JudgingPanel',
component: './Tender/ProjectManager/JudgingPanel/List'
component: './Tender/ProjectManager/JudgingPanel/List/preIndex'
},
//澄清 资审发售 项目经理
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,31 @@
/**
* 手写签名用
*/
.container {
top: 10%;
left: 10%;
width: 100%;
height: 50vh;
}
.sigContainer {
width: 100%;
height: 100%;
margin: 0 auto;
background-color: #fff;
cursor: auto;
}
.sigPointer {
width: 100%;
height: 100%;
margin: 0 auto;
background-color: #fff;
cursor: url("./signPen/signPen.png"),Crosshair;
}
.sigPad {
width: 100%;
height: 100%;
background: #fff;
}

View File

@ -5,7 +5,7 @@ import styles from './index.less';
import { connect } from "dva";
import { routerRedux } from 'dva/router';
import React, { useState, useEffect, useReducer } from 'react';
import { getSessionUserData, getRoomId, getProMethod, getSessionRoleData, getIPassDecode, getDefId, getProId } from '@/utils/session';
import { getSessionUserData, getRoomId, getRoomStatus, getProMethod, getSessionRoleData, getIPassDecode, getDefId, getProId, getOfflineStatusById } from '@/utils/session';
import { getURLInformation } from '@/utils/CommonUtils';
import { getLeader, isShowResult, isShowCount, getErrorStatus, getRiskStatus, isShowRiskModal, saveConfirm, isLeaderConfirm } from './service';
import logo from '@/images/opening/logo.svg'
@ -34,8 +34,13 @@ const BiddingRoom = (props) => {
let data = getSessionUserData();
//获取比选一阶段二次项目,自定义流程,当前供应商
const isBxOneSecondCustom = sessionStorage.getItem("isBxOneSecondCustom");
//评审方式状态0-默认1-已确认配置分工及组长
const roomJuryConfigStatus = sessionStorage.getItem("roomJuryConfigStatus");
//评审方式0-线上、1-线下
const roomReviewMethod = sessionStorage.getItem("roomReviewMethod");
//获取评审室id
const roomId = getRoomId();
const roomStatus = getRoomStatus();
const [list, setList] = useState();
//风险提示文字弹窗控制
const [riskVisible, setRiskVisible] = useState(false);
@ -96,6 +101,28 @@ const BiddingRoom = (props) => {
path: "/EvaRoom/Evaluation/projectManager/ReviewResults/Manager",
text: "评审结果"
}]
//项目经理角色
let managerOffList = [
{
id: 1,
path: "/EvaRoom",
text: "基本信息"
},
{
id: 2,
path: "/EvaRoom/BiddingDocumentsDecrypt",
text: `${responseType}文件查看`
},
{
id: 3,
path: "/EvaRoom/Evaluation/BidControl/BidControlManager",
text: "风险点展示"
},
{
id: 8,
path: "/EvaRoom/Evaluation/projectManager/ReviewResults/ManagerEntry",
text: "评审结果录入"
}]
let JuryList = [
{
id: 1,
@ -179,13 +206,25 @@ const BiddingRoom = (props) => {
//评审结果页签点击事件
const onclick = async (path, id) => {
await getOfflineStatusById(roomId)
if ((role == 'ebtp-agency-project-manager' || role == 'ebtp-purchase') && id == 8) { //代理&采购经理进入评审结果
const success = await isClickResult();
if (success) {
history.push({ pathname: path });
setSelectedPath(path);
if (roomReviewMethod == 0){
const success = await isClickResult();
if (success) {
history.push({ pathname: path });
setSelectedPath(path);
} else {
message.info("未到评审结果环节,无法进入评审结果")
}
} else {
message.info("未到评审结果环节,无法进入评审结果")
console.log("roomJuryConfigStatus", roomJuryConfigStatus)
if (roomJuryConfigStatus == 1) {
history.push({ pathname: path });
setSelectedPath(path);
} else {
message.info("未到评审结果环节,无法进入评审结果")
}
}
} else if (role == 'ebtp-expert' && id == 8) {//专家进入评审结果
const success = await isClickResult();
@ -246,14 +285,14 @@ const BiddingRoom = (props) => {
if (res?.data == "Review") {
return "/EvaRoom/Evaluation/expert/ReviewResults/Jury"
} else {
const result = await isLeaderConfirm({ assessRoomId: roomId });//供应商股权关系-专家组长是否确认风险
if (result?.success && result?.data) {
//TODO zyx暂时不需要const result = await isLeaderConfirm({ assessRoomId: roomId });//供应商股权关系-专家组长是否确认风险
//TODO zyx暂时不需要if (result?.success && result?.data) {
return "/EvaRoom/Evaluation/expert/ReviewResults/GroupLeader"
} else {
setIsResult(true)
setRiskVisible(true)
return false;
}
//TODO zyx暂时不需要} else {
//TODO zyx暂时不需要 setIsResult(true)
//TODO zyx暂时不需要 setRiskVisible(true)
//TODO zyx暂时不需要 return false;
//TODO zyx暂时不需要}
}
} else {
return false
@ -347,7 +386,11 @@ const BiddingRoom = (props) => {
}
//代理&项目经理
if (role == "ebtp-agency-project-manager" || role == "ebtp-purchase") {//代理和采购经理
setList(managerList);
if(true){
setList(managerOffList);
}else {
setList(managerList);
}
} else if (role == "ebtp-expert") {//专家
setList(JuryList)
} else if (role == "ebtp-supplier") {//供应商

View File

@ -1,4 +1,4 @@
import { fetchManagerList,openBizassessroom,resetVerificationCode,fetchJuryList,fetchSupplierList
import { fetchManagerList,openBizassessroom,openOffBizassessroom,resetVerificationCode,fetchJuryList,fetchSupplierList
,fetchJuryMemInfo,updateJuryMemInfo,validateVerificationCode,fetchbidslist
,fetchPreSupplierList,fetchPreList,fetchPreListt,bxmultifetchPreListt,pushRedirectRe,getCheckedByRoomId,checkOpenBidSupplier,zmmultiOpenBizassessroom } from '../services/bidev';
import { message } from 'antd';
@ -43,6 +43,23 @@ export default {
}
callback();
},
// 开启线下评审室
*openOffBizassessroom({payload,callback}, { call, put }){
const response =yield call(openOffBizassessroom,payload)
if(response?.code==200){
// 查询招标代理列表
const params={
pageNo:1,
pageSize:10,
reviewMethod:1,
roomType: getURLInformation('roomType'),
tpId:getProId()//项目id
}
yield put({ type: 'fetchManagerList', payload: {...params} });
message.success("开启成功!")
}
callback();
},
// 开启评审室(多轮招募)
*zmmultiOpenBizassessroom({payload,callback}, { call, put }){
const response =yield call(zmmultiOpenBizassessroom,payload)

View File

@ -11,6 +11,8 @@ import { reviewReportExpertsConfirmed } from '../../Jury/service';
import { getDicData, getProMethod, getRoomId } from '@/utils/session';
import { btnAuthority } from '@/utils/authority';
import WebOffice0609, { WebOfficeRefProps } from '@/pages/webOffice/weboffice0609';
import styles from '@/assets/styles.module.less'
import SignaturePad from 'react-signature-canvas'
import SortEditableTable from './SortEditableTable';
import ReviewReportUpload from '@/utils/ReviewReportUpload';
import { ExclamationCircleOutlined } from '@ant-design/icons';
@ -111,6 +113,10 @@ const GroupLeader: React.FC = () => {
const [resultDisabledStatus, setResultDisabledStatus] = useState<boolean>(false);
//报价类型 true-百分比类型折扣率优惠率false-数值类型(总价,单价)
const [quotationMethod, setQuotationMethod] = useState<boolean>(false);
//评审报告确认无误弹出窗
const [visibleConfirm, setVisibleConfirm] = useState<boolean>(false);
//鼠标指针ref
const pointRef = useRef<any>()
//评价方法 eval_method_1 最低价法 eval_method_2 综合评估法
const [evalMethodDict, setEvalMethodDict] = useState<string>('');
//评审结果排序弹窗
@ -1446,6 +1452,142 @@ const GroupLeader: React.FC = () => {
setSpinning(false);
})
}
/**
* 评审报告确认无误(签字面板)
* zhoujianlong 2021.8.19
* @returns
*/
const modalConfirm = () => {
//保存loading
const [loading, setLoading] = useState<boolean>(false);
//关闭modal
const onCancel = () => {
init();
setVisibleConfirm(false);
}
//canvas ref
let sigPad: any = {}
//手写签名存储
const [saveTrimCanvas, setSaveTrimCanvas] = useState<string>('')
const canvasBase64 = reviewReportData?.expertList?.[0]?.expertSign
useEffect(() => {
if(canvasBase64 != undefined){
setSaveTrimCanvas(canvasBase64)
}
}, [canvasBase64])
/**
* 保存或发送调用接口
* @params expertSign 签名产生的base64值
* status true-保存 false-发送
*/
const toSaveOrSend = (expertSign: string,status: boolean) => {
let params = {
expertOpinion: 1,
reportId: reviewReportData.id,
expertSign: expertSign
};
if(status){ //判断是保存还是发送
params["status"] = 0
}
setLoading(true)
reviewReportExpertsConfirmed(params).then(res => {
if(res?.code == 200 && res?.success === true) {
commonMessage(res);
if(status) {} else {
onCancel();
}
}
}).finally(() => {
setLoading(false)
})
}
//提交 status true-保存 false-发送
const submit = (status: boolean) => {
let emptyStatus = sigPad.isEmpty();
if(emptyStatus) {
message.error("当前无签名内容,请先签名");
return;
}
//得到画布
let canvas = sigPad._canvas;
if (canvas.getContext) {
let ctx = canvas.getContext('2d');
// 将canvas的透明背景设置成白色
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for(var i = 0; i < imageData.data.length; i += 4) {
// 当该像素是透明的,则设置成白色
if(imageData.data[i + 3] == 0) {
imageData.data[i] = 255;
imageData.data[i + 1] = 255;
imageData.data[i + 2] = 255;
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
let imgUrl = sigPad.toDataURL('image/png',1);
setSaveTrimCanvas(imgUrl)
toSaveOrSend(imgUrl,status)
}
//clear
const clear = () => {
sigPad.clear();
}
//SignaturePad ref
const refRender = (ref: any) => {
sigPad = ref //存储ref
if(ref != null) {
ref?.clear()
ref?.fromDataURL(saveTrimCanvas)
}
}
return (
<>
<Modal
destroyOnClose
visible={visibleConfirm}
title="专家签字"
onCancel={onCancel}
maskClosable={false}
centered
width={'60%'}
bodyStyle={{
backgroundColor: '#f0f0f0',
userSelect: 'none', //用来禁止选中,复制
MozUserSelect: 'none',
WebkitUserSelect: 'none',
msUserSelect: 'none',
}}
footer={[
<Button key="send" type="primary" loading={loading} onClick={() => submit(false)}>
</Button>,
<Button key="submit" type="primary" loading={loading} onClick={() => submit(true)}>
</Button>,
<Button key="clear" loading={loading} onClick={() => clear()}>
</Button>,
<Button key="back" onClick={onCancel}>
</Button>,
<span style={{color: '#b30000',lineHeight: '32px'}}></span>
]}
>
<Spin spinning={loading} delay={300}>
<div className={styles.container}>
<div className={styles.sigContainer} ref={pointRef} onMouseDown={() => pointRef.current.className = styles.sigPointer} onMouseUp={() => pointRef.current.className = styles.sigContainer}>
<SignaturePad canvasProps={{className: styles.sigPad}} maxWidth={3.5} minWidth={1}
ref={refRender} penColor="#000"/>
</div>
</div>
</Spin>
</Modal>
</>
)
}
/**
* 评审报告存在问题
@ -1529,7 +1671,7 @@ const GroupLeader: React.FC = () => {
)
}
/**
* 评审报告确认无误
* 评审报告确认无误(无需手写签名)
*/
const beConfirmed = () => {
setReviewReportLoading(true);
@ -1544,6 +1686,14 @@ const GroupLeader: React.FC = () => {
})
}
//确认无误的手写签名判断方法
const toReportConfirm = () => {
if(String(reviewReportData?.signStatus) == '1') {//判断是否启用手写签名 1-是 0-否
setVisibleConfirm(true);
} else {
beConfirmed();
}
}
const saveTable = (data: any, config: any) => {
let form = config?.form;
form.validateFields().then((res: any) => {
@ -1606,11 +1756,12 @@ const GroupLeader: React.FC = () => {
value={supplierTableData}
onChange={setSupplierTableData}
scroll={{ x: 1300 }}
onRow={record => {
onRow={(record, index) => {
return {
// 鼠标移入行
onMouseEnter: () => {
setEditableRowKeys(resultDisabledStatus ? [] : [record.id])
setEditableRowKeys((resultDisabledStatus || (assessId == "1558990153220034560" && index == 0)) ? [] : [record.id])
},
onMouseLeave: () => {
setEditableRowKeys([]);
@ -1633,12 +1784,12 @@ const GroupLeader: React.FC = () => {
</Panel>
{isNotEmpty(reviewReportData.id) && (
<Panel header="评审报告确认" key="2">
<Row justify="end">
<Row justify="end" style={{marginTop: '-8px',marginBottom: '8px'}}>
<Col>
<Space>
{reportFileList.length == 0 ? null : <Button key="upload" onClick={() => setReportUploadVisible(true)}></Button>}
<Button type="primary" hidden={btnAuthority(['ebtp-expert'])} disabled={!reviewReportStatus} loading={reviewReportLoading} onClick={beConfirmed}></Button>
<Button type="primary" hidden={btnAuthority(['ebtp-expert'])} disabled={!reviewReportStatus} onClick={() => setVisibleIssue(true)}></Button>
<Button type="primary" hidden={btnAuthority(['ebtp-expert'])} key="confirm" disabled={!reviewReportStatus} loading={reviewReportLoading} onClick={() => toReportConfirm()}></Button>
<Button type="primary" hidden={btnAuthority(['ebtp-expert'])} key="problem" disabled={!reviewReportStatus} onClick={() => setVisibleIssue(true)}></Button>
</Space>
</Col>
</Row>
@ -1664,6 +1815,7 @@ const GroupLeader: React.FC = () => {
)}
</Collapse>
{modelIssue()}
{modalConfirm()}
{sortTableVisible && <SortEditableTable
modalVisible={sortTableVisible}
onCancel={() => setSortTableVisible(false)}

View File

@ -10,6 +10,8 @@ import Weboffice from "@/pages/webOffice/weboffice";
import { getDicData, getProMethod, getRoomId } from '@/utils/session';
import { btnAuthority } from '@/utils/authority';
import WebOffice0609, { WebOfficeRefProps } from '@/pages/webOffice/weboffice0609';
import styles from '@/assets/styles.module.less'
import SignaturePad from 'react-signature-canvas'
import ReviewReportUpload from '@/utils/ReviewReportUpload';
import { getFileListByBid } from '@/utils/DownloadUtils';
import MACAddressPrompt from '@/pages/Evaluation/BidControl/BidControlManager/components/MACAddressPrompt';
@ -53,6 +55,10 @@ const Jury: React.FC = () => {
const [ptcpMode, setPtcpMode] = useState<string>();
//报价类型 %-百分比类型(折扣率,优惠率)元-数值类型(总价,单价)
const [quotationMethod, setQuotationMethod] = useState<string>('元');
//评审报告确认无误弹出窗
const [visibleConfirm, setVisibleConfirm] = useState<boolean>(false);
//鼠标指针ref
const pointRef = useRef<any>()
//初始化显示字段
const candidateType = bidMethodDict == 'procurement_mode_1' || bidMethodDict == 'procurement_mode_2' ? '中标' : '中选';//初始化中标中选字段
//评审报告附件modal visible
@ -663,6 +669,143 @@ const Jury: React.FC = () => {
)
}
/**
* 评审报告确认无误(签字面板)
* zhoujianlong 2021.8.19
* @returns
*/
const modalConfirm = () => {
//保存loading
const [loading, setLoading] = useState<boolean>(false);
//关闭modal
const onCancel = () => {
init();
setVisibleConfirm(false);
}
//canvas ref
let sigPad: any = {}
//手写签名存储
const [saveTrimCanvas, setSaveTrimCanvas] = useState<string>('')
const canvasBase64 = reviewReportData?.expertList?.[0]?.expertSign
useEffect(() => {
if(canvasBase64 != undefined){
setSaveTrimCanvas(canvasBase64)
}
}, [canvasBase64])
/**
* 保存或发送调用接口
* @params expertSign 签名产生的base64值
* status true-保存 false-发送
*/
const toSaveOrSend = (expertSign: string,status: boolean) => {
let params = {
expertOpinion: 1,
reportId: reviewReportData.id,
expertSign: expertSign
};
if(status){ //判断是保存还是发送
params["status"] = 0
}
setLoading(true)
reviewReportExpertsConfirmed(params).then(res => {
if(res?.code == 200 && res?.success === true) {
commonMessage(res);
if(status) {} else {
onCancel();
}
}
}).finally(() => {
setLoading(false)
})
}
//提交 status true-保存 false-发送
const submit = (status: boolean) => {
let emptyStatus = sigPad.isEmpty();
if(emptyStatus) {
message.error("当前无签名内容,请先签名");
return;
}
//得到画布
let canvas = sigPad._canvas;
if (canvas.getContext) {
let ctx = canvas.getContext('2d');
// 将canvas的透明背景设置成白色
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for(var i = 0; i < imageData.data.length; i += 4) {
// 当该像素是透明的,则设置成白色
if(imageData.data[i + 3] == 0) {
imageData.data[i] = 255;
imageData.data[i + 1] = 255;
imageData.data[i + 2] = 255;
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
let imgUrl = sigPad.toDataURL('image/png',1);
setSaveTrimCanvas(imgUrl)
toSaveOrSend(imgUrl,status)
}
//clear
const clear = () => {
sigPad.clear();
}
//SignaturePad ref
const refRender = (ref: any) => {
sigPad = ref //存储ref
if(ref != null) {
ref?.clear()
ref?.fromDataURL(saveTrimCanvas)
}
}
return (
<>
<Modal
destroyOnClose
visible={visibleConfirm}
title="专家签字"
onCancel={onCancel}
maskClosable={false}
centered
width={'60%'}
bodyStyle={{
backgroundColor: '#f0f0f0',
userSelect: 'none', //用来禁止选中,复制
MozUserSelect: 'none',
WebkitUserSelect: 'none',
msUserSelect: 'none',
}}
footer={[
<Button key="send" type="primary" loading={loading} onClick={() => submit(false)}>
</Button>,
<Button key="submit" type="primary" loading={loading} onClick={() => submit(true)}>
</Button>,
<Button key="clear" loading={loading} onClick={() => clear()}>
</Button>,
<Button key="back" onClick={onCancel}>
</Button>,
<span style={{color: '#b30000',lineHeight: '32px'}}></span>
]}
>
<Spin spinning={loading} delay={300}>
<div className={styles.container}>
<div className={styles.sigContainer} ref={pointRef} onMouseDown={() => pointRef.current.className = styles.sigPointer} onMouseUp={() => pointRef.current.className = styles.sigContainer}>
<SignaturePad canvasProps={{className: styles.sigPad}} maxWidth={3.5} minWidth={1}
ref={refRender} penColor="#000"/>
</div>
</div>
</Spin>
</Modal>
</>
)
}
/**
* 评审报告 我有问题
*/
@ -783,7 +926,7 @@ const Jury: React.FC = () => {
)
}
/**
* 确认无误
* 确认无误(无需手写签名)
*/
const submitReviewReport = () => {
setReviewReportLoading(true);
@ -797,6 +940,15 @@ const Jury: React.FC = () => {
init();
})
}
//确认无误的手写签名判断方法
const toReportConfirm = () => {
console.log(String(reviewReportData?.signStatus))
if(String(reviewReportData?.signStatus) == '1') {//判断是否启用手写签名 1-是 0-否
setVisibleConfirm(true);
} else {
submitReviewReport();
}
}
/**
* 获取表头
*/
@ -844,12 +996,14 @@ const Jury: React.FC = () => {
{
(isNotEmpty(reviewReportData) && isNotEmpty(reviewReportData.id)) && (
<Panel header="评审报告确认" key="2">
<Row justify="end">
<Row justify="end" style={{marginTop: '-8px',marginBottom: '8px'}}>
<Col>
<Space>
{reportFileList.length == 0 ? null : <Button key="upload" onClick={() => setReportUploadVisible(true)}></Button>}
<Button type="primary" hidden={btnAuthority(["ebtp-expert"])} disabled={!reviewReportStatus} loading={reviewReportLoading} onClick={() => submitReviewReport()}></Button>
<Button type="primary" hidden={btnAuthority(["ebtp-expert"])} disabled={!reviewReportStatus} onClick={() => setVisibleProblem(true)}></Button>
{/* <Button type="primary" hidden={btnAuthority(["ebtp-expert"])} disabled={!reviewReportStatus} loading={reviewReportLoading} onClick={() => submitReviewReport()}>确认无误</Button>
<Button type="primary" hidden={btnAuthority(["ebtp-expert"])} disabled={!reviewReportStatus} onClick={() => setVisibleProblem(true)}>我有问题</Button> */}
<Button type="primary" hidden={btnAuthority(["ebtp-expert"])} key="confirm" disabled={!reviewReportStatus} loading={reviewReportLoading} onClick={() => toReportConfirm()}></Button>
<Button type="primary" hidden={btnAuthority(["ebtp-expert"])} key="problem" disabled={!reviewReportStatus} onClick={() => setVisibleProblem(true)}></Button>
</Space>
</Col>
</Row>
@ -876,6 +1030,7 @@ const Jury: React.FC = () => {
</Collapse>
{modelIssue()}
{modelProblem()}
{modalConfirm()}
{reportUploadVisible && <ReviewReportUpload
modalVisible={reportUploadVisible}
onCancel={() => setReportUploadVisible(false)}

View File

@ -0,0 +1,13 @@
import React from 'react';
import Manager from './components/Manager';
/**
* 项目经理
*/
const Index: React.FC = () => {
return (
<>
<Manager />
</>
);
}
export default Index;

View File

@ -51,3 +51,10 @@ export function finishFlow(id: any) {
export function getAssessRoomStatus(id: any) {
return request('/api/biz-service-ebtp-process/v1/bizassessroom/info/' + id);
}
/**
* 生成签名报告
* @param id
*/
export function saveSignPdfReport(id: any) {
return request('/api/biz-service-ebtp-evaluation/v1/review/report/signpdf/' + id);
}

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,11 @@ export async function openBizassessroom(params) {
return request(`/api/biz-service-ebtp-process/v1/bizassessroom/openRoom/${params.id}`,
{ method: 'POST' });
}
// 开启评审
export async function openOffBizassessroom(params) {
return request(`/api/biz-service-ebtp-process/v1/bizassessroom/openOffRoom/${params.id}`,
{ method: 'POST' });
}
// 开启评审多轮招募
export async function zmmultiOpenBizassessroom(params) {
return request(`/api/biz-service-ebtp-process/v1/assessroom/zmmulti/openRoom/${params.id}`,