效果:
功能点:
- 放大/缩小按钮:每次缩放后自动滚动到顶部,确保用户总能看到图片的最顶部内容
- 重置功能:重置缩放后同时重置横向和纵向滚动位置
- 鼠标滚轮缩放:以鼠标位置为中心的智能缩放,缩放时保持鼠标指向的内容位置不变
代码:
<template>
<div
class="warp-card"
v-loading="isLoading"
element-loading-text="Loading..."
element-loading-svg-view-box="-10, -10, 50, 50"
element-loading-background="rgba(122, 122, 122, 0.8)"
>
<div class="breadcrumb">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<a type="primary" href="/homepage">Home</a>
</el-breadcrumb-item>
<el-breadcrumb-item
>Performance Management / Approval Form Details-{{
userId
}}</el-breadcrumb-item
>
</el-breadcrumb>
</div>
<div style="padding: 0px 10px 0px 10px">
<div
class="search-container"
style="margin: 10px 0; min-height: calc(-110px + 100vh)"
>
<div class="box-card">
<div class="performance-setting-container">
<div class="form-title">
Approval Form Details-{{ userId }}
<div
class="form-status"
:class="statusClass"
v-if="applicationStatusValue"
>
{{ applicationStatusValue }}
</div>
</div>
<el-divider class="custom-divider" />
<!-- 流程图 -->
<div class="process-section">
<div style="font-size: 16px; color: #333; font-weight: bold">
Process
</div>
<div class="process-steps">
<template v-for="(step, index) in stepsData" :key="index">
<div
:class="[
'step',
{
active: step.active && step.statusId !== 4,
rejected: step.active && step.statusId === 4,
disabled: hasRejectedBefore(index),
},
]"
>
<div class="step-icon">
<template v-if="step.active">
<el-icon v-if="step.statusId === 4"><Close /></el-icon>
<el-icon v-else><Check /></el-icon>
</template>
<span v-else style="color: #a8abb2">{{ index + 1 }}</span>
</div>
<div class="step-info">
<div class="step-name">{{ step.name }}</div>
<div class="step-user">{{ step.user }}</div>
<div class="step-time">{{ step.time }}</div>
</div>
</div>
<!-- 渲染连接线,最后一个不显示 -->
<div
v-if="index < stepsData.length - 1"
:class="[
'step-line',
stepsData[index].active && stepsData[index].statusId !== 4
? 'completed'
: '',
hasRejectedBefore(index + 1) ? 'disabled' : '',
]"
></div>
</template>
</div>
</div>
<el-divider class="custom-divider" />
<template v-if="rejectComment">
<div class="info-section">
<div style="display: flex; align-items: center">
<div style="font-size: 16px; color: #333; font-weight: bold">
Comments
</div>
</div>
<div class="comments-content">
<div
v-html="
processHtmlContent(
formatMessageWithLineBreaks(rejectComment)
)
"
></div>
</div>
</div>
<el-divider class="custom-divider" />
</template>
<!-- 个人信息 -->
<div class="info-section">
<h3>Personal Information</h3>
<el-row :gutter="20">
<el-col :span="24">
<div class="info-item">
<span class="label">Chinese Name:</span>
<span class="value">{{
personalInformationObj.chineseName
}}</span>
</div>
</el-col>
<!-- 在JM类型下不显示Role -->
<el-col :span="24" v-if="approvalType !== 'JM'">
<div class="info-item">
<span class="label">Role:</span>
<span class="value">{{ personalInformationObj.role }}</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<span class="label">Time:</span>
<span class="value"> {{ yearQuarterDisplay }}</span>
</div>
</el-col>
<!-- AE类型特有的字段 -->
<template
v-if="
approvalType === 'AE' ||
approvalType === 'DEJob' ||
approvalType === 'AEJob'
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">QC Pass Rate:</span>
<span class="value">
<span v-if="personalInformationObj.qcPassRate">
{{ personalInformationObj.qcPassRate }}%</span
>
<span v-else>0%</span>
</span>
</div>
</el-col>
</template>
<template
v-if="
approvalType === 'DE' ||
approvalType === 'AE' ||
approvalType === 'DEJob' ||
approvalType === 'JM'
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Paid Confirmed:</span>
<span class="value">
<a
v-if="personalInformationObj.paidConfirmed"
href="javascript:void(0)"
@click="handlePaidConfirmClick"
class="clickable-value"
>{{ personalInformationObj.paidConfirmed }}</a
>
<span v-else>0</span>
</span>
</div>
</el-col>
</template>
<template
v-if="
approvalType === 'DE' ||
approvalType === 'AE'
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Final Confirmed:</span>
<span class="value">
{{ personalInformationObj.finalConfirmed }}
</span>
</div>
</el-col>
</template>
<template
v-if="
approvalType === 'Other' ||
approvalType === 'DE' ||
approvalType === 'DM' ||
approvalType === 'PE' ||
approvalType === 'SI' ||
approvalType === 'JM'
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Journal:</span>
<span class="value">{{
personalInformationObj.journalName
}}</span>
</div>
</el-col>
</template>
<template
v-if="approvalType === 'DEJob' || approvalType === 'AEJob'"
>
<el-col :span="24">
<div class="info-item">
<span
class="label"
v-if="
approvalType === 'DEJob' || approvalType === 'AEJob'
"
>Total PI:</span
>
<span class="label" v-else>PI:</span>
<span class="value">{{ personalInformationObj.pi }}</span>
</div>
</el-col>
</template>
</el-row>
</div>
<el-divider class="custom-divider" />
<!-- 审批信息 -->
<div class="info-section">
<h3>Approval Information</h3>
<el-row :gutter="20">
<template v-if="approvalType === 'JM'">
<el-col :span="24">
<div class="info-item">
<span class="label">Journal Name:</span>
<span class="value">
{{ approvalInformationObj.journalName }}
</span>
</div>
</el-col>
</template>
<el-col :span="24">
<div class="info-item">
<span class="label">Commission Type:</span>
<span class="value">
{{ approvalInformationObj.commissionType }}
</span>
</div>
</el-col>
<template
v-if="
approvalType === 'PE' ||
approvalType === 'Other' ||
approvalType === 'SI' ||
approvalType === 'DEJob' ||
approvalType === 'AEJob'
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Category:</span>
<span class="value">
<el-select
v-model="selectedCategory"
placeholder="Select category"
style="width: 200px"
:disabled="true"
:title="selectedCategoryLabel"
>
<el-option
v-for="item in categoryList"
:key="item.id"
:label="item.value"
:value="item.id"
/>
</el-select>
</span>
</div>
</el-col>
</template>
<!-- PB类型特有的字段 -->
<template
v-if="approvalType === 'PE' || approvalType === 'Other'"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Paper ID:</span>
<span
class="value"
style="display: flex; align-items: flex-start"
v-if="personalInformationObj.paperId"
>
<el-link
type="primary"
@click="
showPaperDetails(
personalInformationObj.paperId,
'paper'
)
"
>
{{ personalInformationObj.paperId }}</el-link
>
<el-tooltip
v-if="hasCommission"
:content="tooltipContent"
placement="top"
:disabled="!tooltipContent"
popper-class="ebm-tooltip"
raw-content
>
<span
@mouseenter="
fetchInfo(personalInformationObj.paperId, 'paper')
"
style="
position: relative;
margin-left: 3px;
cursor: pointer;
"
>
<svg-icon icon-class="c" />
</span>
</el-tooltip>
</span>
<span class="value" v-else>-</span>
</div>
</el-col>
</template>
<template
v-if="approvalType === 'SI' || approvalType === 'Other'"
>
<el-col :span="24">
<div class="info-item">
<span class="label">SI ID:</span>
<span
class="value"
style="display: flex; align-items: flex-start"
v-if="approvalInformationObj.sectionId"
>
<el-link
type="primary"
@click="
showPaperDetails(
approvalInformationObj.sectionId,
'si'
)
"
>
{{ approvalInformationObj.sectionId }}</el-link
>
</span>
<span class="value" v-else>-</span>
<el-tooltip
v-if="sectionHasCommission"
:content="tooltipContent"
placement="top"
:disabled="!tooltipContent"
popper-class="ebm-tooltip"
raw-content
>
<span
@mouseenter="
fetchInfo(approvalInformationObj.sectionId, 'si')
"
style="
position: relative;
margin-left: 3px;
cursor: pointer;
"
>
<svg-icon icon-class="c" />
</span>
</el-tooltip>
</div>
</el-col>
</template>
<!-- SI类型特有的字段 -->
<template v-if="approvalType === 'SI'">
<el-col :span="24">
<div class="info-item">
<span class="label">SI Title:</span>
<span class="value">{{
approvalInformationObj.sectionTitle
}}</span>
</div>
</el-col>
<!-- <el-col :span="24">
<div class="info-item">
<span class="label">SI Published:</span>
<span class="value">{{
approvalInformationObj.siPublished
}}</span>
</div>
</el-col> -->
</template>
<!-- AE类型特有的字段 -->
<template
v-if="
approvalType === 'AE' ||
approvalType === 'DE' ||
approvalType === 'DM' ||
approvalType === 'JM'
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Calculation Formula:</span>
<span class="value">
<span
v-html="
removeFormulaPrefix(
approvalInformationObj.calculationFormula
)
"
></span
></span>
</div>
</el-col>
</template>
<template
v-if="
approvalType === 'PE' && [5, 6].includes(selectedCategory)
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Calculation formula:</span>
<span class="value">
<span
v-html="
removeFormulaPrefix(
approvalInformationObj.calculationFormula
)
"
></span
></span>
</div>
</el-col>
</template>
<!-- other类型特有的字段 (基于PB) -->
<template v-if="approvalType === 'Other'">
<el-col :span="24">
<div class="info-item">
<span class="label">Journal:</span>
<span class="value">
<el-select
v-model="selectedJournal"
placeholder="Select journal"
style="width: 200px"
:disabled="!showSave"
>
<el-option
v-for="item in journalOptions"
:key="item.journalId"
:label="item.abbreviation"
:value="item.journalId"
/>
</el-select>
</span>
</div>
</el-col>
<template v-if="approvalInformationObj.paymentAmount">
<el-col :span="24">
<div class="info-item">
<span class="label">SI Type:</span>
<span class="value">
{{ approvalInformationObj.sectionType }}
</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<span class="label">Article Type:</span>
<span class="value">
{{ approvalInformationObj.articleType }}
</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<span class="label">Payment Amount($):</span>
<span class="value">
{{ approvalInformationObj.paymentAmount }}
</span>
</div>
</el-col>
</template>
</template>
<template
v-if="
approvalType === 'PE' &&
[5, 6].includes(selectedCategory) &&
approvalInformationObj.paymentAmount
"
>
<el-col :span="24">
<div class="info-item">
<span class="label">Payment Amount($):</span>
<span class="value">
{{ approvalInformationObj.paymentAmount }}
</span>
</div>
</el-col>
</template>
<!-- JM类型特有的字段 -->
<template v-if="approvalType === 'JM'">
<el-col :span="24">
<div class="info-item">
<span class="label">Role:</span>
<span class="value">{{
personalInformationObj.role
}}</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<span class="label">Journal QC Pass Rate:</span>
<span class="value">
<span v-if="personalInformationObj.qcPassRate">
{{ personalInformationObj.qcPassRate }}%</span
>
<span v-else>0%</span>
</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<span class="label">Proportion:</span>
<span class="value"
>{{ approvalInformationObj.proportion }}%</span
>
</div>
</el-col>
</template>
<!-- DM类型特有的字段 -->
<template v-if="approvalType === 'DM'">
<el-col :span="24">
<div class="info-item">
<span class="label">DE total commission:</span>
<span class="value">{{
approvalInformationObj.totalCommission
}}</span>
</div>
</el-col>
</template>
<!-- 证明材料 - 仅PB和other类型显示 -->
<el-col
:span="24"
v-if="
approvalType === 'PE' ||
approvalType === 'Other' ||
approvalType === 'SI'
"
>
<div class="info-item" style="align-items: start">
<span class="label">Proof:</span>
<div class="two-col-container">
<!-- 动态渲染证明材料,保持原有的四个固定区域布局 -->
<div
class="item"
v-for="(proofItem, index) in getDisplayProofItems()"
:key="index"
>
<div class="proof-item">
<span
class="label"
:title="proofItem.title"
style="text-align: left"
>{{ proofItem.title }}:</span
>
<div class="upload-container">
<!-- 如果状态为pending或rejected且可编辑 -->
<template v-if="isProofEditable">
<!-- 图片上传类型 (type: 0) -->
<template v-if="proofItem.originalType === 0">
<div
:class="`upload-area ${
currentProofFocusArea === index
? 'active-upload'
: ''
}`"
@click="setActiveProofUploadArea(index)"
@mouseenter="showProofUploadHint(index, $event)"
@mouseleave="hideProofUploadHint()"
>
<el-upload
:class="`image-uploader proof-uploader-${index}`"
:action="uploadAction"
:show-file-list="false"
:on-success="(response: any, file: any) => onProofUploadSuccess(response, file, index)"
:before-upload="beforeProofUpload"
:http-request="(options: any) => customProofUploadHandler(options, index)"
:auto-upload="true"
list-type="picture-card"
>
<img
v-if="proofEditData[index]?.displayUrl"
:src="proofEditData[index].displayUrl"
class="upload-image upload-image-2"
/>
<div v-else class="upload-placeholder">
<i class="el-icon-plus"></i>
<div>Upload pictures</div>
</div>
</el-upload>
</div>
<!-- 显示已上传的多个图片 -->
<div
v-if="proofEditData[index]?.images?.length > 0"
class="uploaded-images"
>
<div
v-for="(image, imageIndex) in proofEditData[index]
.images"
:key="imageIndex"
class="image-item"
:class="{
active:
image.url === proofEditData[index].displayUrl,
}"
>
<img
:src="image.url"
class="thumbnail"
@click="
changeProofDisplayImage(index, image.url)
"
/>
<div class="image-actions">
<el-button
type="primary"
:icon="Search"
circle
@click.stop="previewImage(image.url, proofItem)"
title="preview"
/>
<el-button
type="danger"
:icon="Delete"
circle
@click.stop="
removeProofImage(index, imageIndex)
"
title="delete"
/>
</div>
</div>
</div>
</template>
<!-- 链接输入类型 (type: 1) -->
<template v-else-if="proofItem.originalType === 1">
<el-input
v-model="proofEditData[index].url"
type="textarea"
:rows="3"
:placeholder="`Please input ${proofItem.title} URL or text content`"
style="margin-top: 10px"
/>
</template>
</template>
<!-- 只读状态:保持原有展示逻辑 -->
<template v-else>
<!-- 如果是链接类型 -->
<div v-if="proofItem.type === 'link'">
<a
:href="proofItem.value"
target="_blank"
:title="proofItem.value"
class="custom-link"
>{{ proofItem.value }}</a
>
</div>
<!-- 如果是图片类型 -->
<div v-else class="image-display-area">
<div
v-if="
proofItem.images &&
proofItem.images.length > 0
"
class="images-container"
title="Preview Picture"
>
<img
v-for="(image, imgIndex) in proofItem.images"
:key="imgIndex"
:src="getImageUrl(image.fileId)"
class="upload-image"
@click="
previewImage(
getImageUrl(image.fileId),
proofItem
)
"
@error="handleImageError"
/>
</div>
<div v-else class="upload-placeholder">
<div>No images uploaded</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</el-col>
<el-col :span="24">
<div class="info-item" style="align-items: start">
<span class="label">Remark:</span>
<span class="value" style="width: 60%">
<el-input
v-model="approvalInformationObj.remark"
:rows="5"
type="textarea"
placeholder="Please input"
:disabled="!showSave"
/>
</span>
</div>
</el-col>
<!-- 只有approver的角色才显示 -->
<template v-if="showExchangeRate">
<el-col :span="24">
<div class="info-item">
<span class="label"><span style="color: red">*</span> USD Exchange Rate:</span>
<el-input
v-model="exchangeRateInput"
placeholder="Please input"
style="margin-left: 10px; width: 200px"
@input="handleExchangeRateInput"
@blur="calculateAmount"
:disabled="!showApprove"
clearable
></el-input>
</div>
</el-col>
</template>
<!-- 金额 -->
<template
v-if="
approvalType === 'PE' && [5, 6].includes(selectedCategory)
"
>
<el-col :span="24" v-if="exchangeRateInput">
<div class="amount-item">
<span class="label">Amount(¥):</span>
<el-input
v-model="approvalInformationObj.amount"
placeholder="250"
style="margin-left: 10px; width: 200px"
disabled
></el-input>
<!-- <el-button
v-if="showApprove"
type="primary"
link
style="margin: 0 10px"
@click="toggleEdit"
>
{{ isEditing ? "Save" : "Modify" }}
</el-button> -->
<!-- <div
v-if="approvalInformationObj.id"
class="amount-item remaining"
@click="showMatchingRulesDetails"
>
<span>Matching rules: </span>
<span class="quota"
>ID {{ approvalInformationObj.id }}</span
>
</div> -->
</div>
</el-col>
</template>
<template v-else>
<el-col :span="24">
<div class="amount-item">
<span class="label">Amount(¥):</span>
<el-input
v-model="approvalInformationObj.amount"
placeholder="250"
style="margin-left: 10px; width: 200px"
:disabled="!showApprove"
></el-input>
</div>
</el-col>
</template>
<!-- 匹配规则 -->
<el-col :span="24">
<div class="amount-item">
<span class="label">Matching rules:</span>
<span class="quota" @click="showMatchingRulesDetails"
>ID {{ approvalInformationObj.id }}</span
>
</div>
</el-col>
</el-row>
</div>
<!-- <el-divider /> -->
<!-- 按钮 -->
<div class="action-buttons">
<template v-if="showApprove">
<el-button type="primary" @click="handleApprove"
>Approve</el-button
>
</template>
<template v-if="showReject">
<el-button type="primary" @click="handleReject">Fail</el-button>
</template>
<template v-if="showSave">
<el-button type="primary" @click="handleSave">Save</el-button>
</template>
<template v-if="showSendToApprove">
<el-button type="primary" @click="handleSendToApproval"
>Send to Approve</el-button
>
</template>
</div>
<!-- <el-divider
v-if="showSave || showSendToApprove || showApprove || showReject"
/> -->
<!-- history notes -->
<div class="history-section">
<div class="table-header">
<h3>History Notes</h3>
<el-button
type="primary"
@click="handleAddNotes"
class="add-notes-btn"
>Add Notes</el-button
>
</div>
<el-table :data="notificationList" style="width: 100%" border>
<el-table-column
prop="createDate"
label="Created on"
width="180"
></el-table-column>
<el-table-column
prop="name"
label="Operator"
width="180"
></el-table-column>
<el-table-column label="Message">
<template #default="scope">
<!-- 如果包含图片,单独显示预览按钮 -->
<!-- <div
v-if="getImagesFromHtml(scope.row.message).length > 0"
class="image-preview-buttons"
>
<el-icon
v-for="(imageUrl, index) in getImagesFromHtml(
scope.row.message
)"
:key="index"
class="preview-icon"
@click="previewCommentImage(imageUrl)"
title="Preview Picture"
>
<Picture />
</el-icon>
</div> -->
<template
v-if="getImagesFromHtml(scope.row.message).length > 0"
>
<div
v-for="(imageUrl, index) in getImagesFromHtml(
scope.row.message
)"
:key="index"
@click="previewCommentImage(imageUrl)"
title="Preview Picture"
>
<div
v-html="
formatMessageWithLineBreaks(scope.row.message)
"
class="history-message history-message-2"
></div>
</div>
</template>
<template v-else>
<div
v-html="formatMessageWithLineBreaks(scope.row.message)"
class="history-message"
></div>
</template>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加拒绝弹窗 -->
<el-dialog
title="Rejection Comments"
v-model="rejectDialogVisible"
width="1000px"
:before-close="cancelModal"
>
<div class="reject-dialog-content">
<Editor2
styleVale="width: 100%;min-height:200px;"
v-model="rejectReason"
/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelModal">Cancel</el-button>
<el-button type="primary" @click="confirmReject">Confirm</el-button>
</span>
</template>
</el-dialog>
<!-- 添加笔记弹窗 -->
<div class="notes-modal">
<el-dialog
title="Add History Notes"
v-model="addNotesDialogVisible"
width="800"
:before-close="cancelModal"
class="addNotesModal"
>
<el-form ref="ruleFormRef" label-width="150px" size="default">
<el-form-item label="Content:">
<el-input
:rows="4"
placeholder="Please input"
type="textarea"
v-model="notesContent"
style="width: 500px"
/>
</el-form-item>
<!-- <el-form-item label="Upload:">
<div style="text-align: left">
<el-upload
ref="uploadNoteRef"
:http-request="uploadNoteFile"
class="upload-demo"
:on-exceed="handleExceed"
:on-remove="handleRemove"
:limit="1"
v-model:file-list="fileList"
>
<el-button size="small" type="primary">Upload</el-button>
<template #tip>
<div class="el-upload__tip">
Click the upload File button to begin upload [.doc, .zip,
.docx, .pdf, .jpg, .jpeg, .gif, .png]
</div>
</template>
</el-upload>
</div>
</el-form-item> -->
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelModal">Cancel</el-button>
<el-button
type="primary"
@click="saveNote(ruleFormRef)"
:loading="saveNoteLoading"
>Confirm</el-button
>
</span>
</template>
</el-dialog>
</div>
<!-- Matching rules弹窗 -->
<el-dialog class="rules-dialog" v-model="rulesDialogVisible" width="1000px">
<template #header>
<div class="custom-dialog-header">
<span class="dialog-title">Matching Rules Details</span>
</div>
</template>
<DescriptionList type="matchingRules" :data="performanceSettingData" />
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelModal">Closed</el-button>
</span>
</template>
</el-dialog>
<!-- Paper详情弹窗 -->
<el-dialog class="paper-dialog" v-model="paperDialogVisible" width="1200px">
<template #header>
<div class="custom-dialog-header">
<span class="dialog-title">Description Details</span>
</div>
</template>
<DescriptionList
:type="showDetailsType === 'paper' ? 'paperDetails' : 'siDetails'"
:data="paperDetailData"
/>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelModal">Closed</el-button>
</span>
</template>
</el-dialog>
<!-- 添加表格弹窗组件 -->
<TableDialog
v-model:visible="tableDialogVisible"
:title="tableDialogTitle"
:beginTime="personalInformationObj.beginTime"
:endTime="personalInformationObj.endTime"
:commissionTypeId="approvalInformationObj.commissionTypeId"
:email="personalInformationObj.email"
:jid="selectedJournal"
status="Paid"
:approvalType="approvalType"
@dialog-closed="handleDialogClosed"
/>
<!-- 图片预览弹窗 -->
<el-dialog
v-model="imagePreviewVisible"
title="Preview Picture"
width="90%"
:style="{ maxWidth: '1400px', minWidth: '800px' }"
append-to-body
>
<div class="image-preview-container" ref="previewContainer">
<div class="image-controls">
<el-button-group>
<el-button @click="zoomIn" size="small" :icon="ZoomIn" title="Zoom In">Zoom In</el-button>
<el-button @click="zoomOut" size="small" :icon="ZoomOut" title="Zoom Out">Zoom Out</el-button>
<el-button @click="resetZoom" size="small" :icon="Refresh" title="Reset">Reset</el-button>
<el-button @click="toggleFullscreen" size="small" :icon="FullScreen" title="Fullscreen">{{ isFullscreen ? 'Exit Fullscreen' : 'Fullscreen' }}</el-button>
</el-button-group>
<span class="zoom-indicator">{{ Math.round(imageScale * 100) }}%</span>
</div>
<div
class="image-wrapper"
@wheel="handleWheel"
ref="imageWrapper"
>
<img
:src="previewImageUrl"
alt="预览图片"
class="preview-image"
:style="imageStyle"
@error="handleImageError"
@load="handleImageLoad"
draggable="false"
/>
</div>
</div>
<template #footer>
<el-button @click="closePreview">Closed</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import {
onMounted,
reactive,
ref,
h,
getCurrentInstance,
onUnmounted,
computed,
watch,
nextTick,
} from "vue";
import {
ElMessage,
UploadRequestOptions,
genFileId,
ElMessageBox,
} from "element-plus";
import type { UploadProps, FormInstance } from "element-plus";
import { formatAmount } from "@/utils/format";
import { postNotification5 } from "@/api/developmentManagement/index";
import TableDialog from "@/views/performanceManagement/components/TableDialog.vue";
import DescriptionList from "@/views/performanceManagement/components/DescriptionList.vue";
import { Delete, Search, FullScreen, ZoomIn, ZoomOut, Refresh } from "@element-plus/icons-vue";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
import SvgIcon from "@/components/SvgIcon/index.vue";
import {
performanceApplicationDetails,
performanceSendToApprove,
performanceApprove,
performanceAppUpdate,
performanceSettingsByCommissionId,
performanceSettingsRuleId,
performanceCommissionDetailsByAritcleId,
performancePaperDetails,
} from "@/api/performanceManagement/index.ts";
import { removeFormulaPrefix } from "@/utils/format";
import Editor2 from "@/components/WangEditor/performanceManagementIndex.vue";
import { uploadFileApiNew } from "@/api/index";
import { CommissionType } from "@/types/performance";
const { proxy, ctx: that } = getCurrentInstance();
import { fetchSelect } from "@/api/index.ts";
interface PersonWorkload {
id: number;
name: string;
role: string;
proportion: string;
}
interface WorkloadGroup {
journal?: string;
people: PersonWorkload[];
}
interface HistoryNote {
id: number;
createdOn: string;
operator: string;
time: string;
journal: string;
content: string;
}
// 获取路由参数
const route = useRoute();
const approvalType = ref("AE");
const userId = computed(() => {
const id = route.query.id;
return id ? Number(id) : 0;
});
const approverEmail =
route.query.approverEmail || sessionStorage.getItem("userEmail");
const yearQuarterDisplay = computed(() => {
return personalInformationObj.year && personalInformationObj.quarter
? `${personalInformationObj.year} Q${personalInformationObj.quarter}`
: personalInformationObj.createTime;
});
// 从HTML内容中提取图片URL
const getImagesFromHtml = (htmlContent: string) => {
if (!htmlContent) return [];
// 创建一个DOM解析器来处理HTML
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, "text/html");
const images = doc.querySelectorAll("img");
const imageUrls: string[] = [];
images.forEach((img) => {
const src = img.getAttribute("src");
if (src) {
imageUrls.push(src);
}
});
return imageUrls;
};
// 处理富文本中的图片点击事件
const processHtmlContent = (htmlContent: string) => {
if (!htmlContent) return "";
// 使用正则表达式匹配img标签并添加点击事件和样式
const processedContent = htmlContent.replace(
/<img([^>]*?)>/gi,
(match, attributes) => {
// 为图片添加点击事件和样式类
const imgTag = `<img${attributes} class="comment-image clickable-image" onclick="handleCommentImageClick(this.src)" title="Preview Picture">`;
return imgTag;
}
);
return processedContent;
};
// 格式化消息内容,将换行符转换为HTML换行标签
const formatMessageWithLineBreaks = (message: string) => {
if (!message) return "";
// 将换行符 \n 转换为 <br> 标签
return message.replace(/\n/g, "<br>");
};
// Paid确认点击处理
const handlePaidConfirmClick = () => {
tableDialogVisible.value = true;
tableDialogTitle.value = "Paid Confirmed Details";
};
const hasCommission = ref(false);
const sectionHasCommission = ref(false);
// 表单数据
const assignmentTime = ref("2025 Q1");
const scopusUrl = ref("");
const identityUrl = ref("");
// 拒绝弹窗相关
const rejectDialogVisible = ref(false);
const rulesDialogVisible = ref(false);
const rejectReason = ref("");
// 添加笔记相关
const addNotesDialogVisible = ref(false);
const notesContent = ref("");
// 图片上传相关
const uploadAction = ref(""); // 实际使用时替换为后端上传接口
const invitationProofUrl = ref("");
const scopusProofUrl = ref("");
const referencesProofUrl = ref("");
const identityProofUrl = ref("");
const invitationImages = ref<{ url: string; name: string }[]>([]);
interface StepItem {
name: string;
user: string;
time: string;
status: string;
active?: boolean; // 新增可选属性
statusId?: number; // 修改为 number 类型
}
const stepsData = ref<StepItem[]>([
{
name: "申请",
user: "研究人员",
time: "2023-12-01 10:00",
status: "completed",
},
{
name: "初审",
user: "部门主管",
time: "2023-12-05 14:30",
status: "completed",
},
{
name: "复审",
user: "论文委员会",
time: "2023-12-10 09:15",
status: "completed",
},
{
name: "等审",
user: "最终审核",
time: "处理中",
status: "current",
},
]);
// 详情页个人信息
const personalInformationObj = reactive<any>({
chineseName: "",
role: "",
journalName: "",
year: "",
quarter: "",
createTime: "",
qcPassRate: "",
paidConfirmed: "",
finalConfirmed: '',
email: "",
pi: "",
beginTime: "",
endTime: "",
paperId: "",
});
// 详情页审批信息
const approvalInformationObj = reactive({
commissionType: "",
journalName: "",
calculationFormula: "",
remark: "",
amount: "",
id: "",
categoryId: null,
commissionTypeId: null,
sectionId: "",
sectionTitle: "",
siPublished: "",
sectionType: "",
articleType: "",
paymentAmount: "",
proportion: "",
totalCommission: "",
proofDtoList: [],
exchangeRate: "",
});
const isLoading = ref(false);
const showApprove = ref(false);
const showReject = ref(false);
const showSendToApprove = ref(false);
const showSave = ref(false);
const showExchangeRate = ref(false);
const applicationStatusValue = ref("");
const categoryList = ref([]);
const notificationList = ref([]);
const selectedJournal = ref();
const journalOptions = ref<any[]>([]);
const rejectComment = ref("");
// 汇率相关变量和方法
const exchangeRateInput = ref("");
// 处理汇率输入,限制最多两位小数
const handleExchangeRateInput = (value: string) => {
// 移除非数字和小数点的字符
let cleanValue = value.replace(/[^\d.]/g, "");
// 确保只有一个小数点
const parts = cleanValue.split(".");
if (parts.length > 2) {
cleanValue = parts[0] + "." + parts.slice(1).join("");
}
// 限制小数点后最多两位
if (parts.length === 2 && parts[1].length > 2) {
cleanValue = parts[0] + "." + parts[1].substring(0, 2);
}
exchangeRateInput.value = cleanValue;
approvalInformationObj.exchangeRate = cleanValue;
};
// 计算新的Amount值
const calculateAmount = () => {
const exchangeRate = parseFloat(exchangeRateInput.value);
const paymentAmount = parseFloat(approvalInformationObj.paymentAmount);
if (!isNaN(exchangeRate) && !isNaN(paymentAmount) && exchangeRate > 0) {
const newAmount = (exchangeRate * paymentAmount * 0.01).toFixed(2);
approvalInformationObj.amount = newAmount;
}
};
// 预览评论中的图片
const previewCommentImage = (imageUrl: string) => {
previewImageUrl.value = imageUrl;
imagePreviewVisible.value = true;
};
// 监听粘贴事件,实现粘贴上传图片
onMounted(() => {
document.addEventListener("paste", handlePaste);
document.addEventListener("paste", handleProofPasteEvent);
// 将预览函数挂载到window对象上,供HTML中的onclick调用
(window as any).previewCommentImage = previewCommentImage;
(window as any).handleCommentImageClick = (src: string) => {
previewCommentImage(src);
};
fetchSelect({
types: [1, 20],
}).then((res: any) => {
if (res.code === 2000) {
journalOptions.value = res.data.bases || [];
}
});
getDetailsData();
});
// 监听汇率变化,自动计算金额
watch(exchangeRateInput, (newValue) => {
if (newValue && approvalInformationObj.paymentAmount) {
calculateAmount();
}
});
// 计算当前选中category的label值,用于title属性
const selectedCategoryLabel = computed(() => {
if (!selectedCategory.value || !categoryList.value.length) {
return "";
}
const selectedItem = categoryList.value.find(
(item: any) => item.id === selectedCategory.value
);
return selectedItem ? selectedItem.value : "";
});
// 根据状态值返回对应的CSS类名
const statusClass = computed(() => {
if (!applicationStatusValue.value) return "";
const status = applicationStatusValue.value.toLowerCase();
if (status.includes("in review")) {
return "in-review";
} else if (status.includes("approved")) {
return "approved";
} else if (status.includes("pending")) {
return "pending";
} else if (status.includes("rejected")) {
return "rejected";
}
return "";
});
const getPerformanceSettingsByCommissionId = () => {
performanceSettingsByCommissionId({
commissionId: approvalInformationObj.commissionTypeId,
categoryId: selectedCategory.value || 0,
requestedId: personalInformationObj.paperId, // 文章id
sectionId: approvalInformationObj.sectionId,
journalId: selectedJournal.value,
beginTime: personalInformationObj.beginTime || null,
endTime: personalInformationObj.endTime || null,
}).then((res) => {
if (res.code === 2000 && res.data) {
categoryList.value = res.data.performanceCategoryEnumList || [];
approvalInformationObj.articleType = res.data.article?.articleType;
approvalInformationObj.paymentAmount = res.data.article?.apc;
calculateAmount();
}
});
};
const proofRelations = ref([]);
const handleDialogClosed = () => {
getDetailsData()
}
const getDetailsData = async () => {
isLoading.value = true;
await performanceApplicationDetails({
id: userId.value,
})
.then((res: any) => {
stepsData.value = [];
if (res.code === 2000) {
if (
res.data.application?.approvalFlows &&
res.data.application?.approvalFlows.length > 0
) {
res.data.application?.approvalFlows.forEach(
(item: any, index: number) => {
stepsData.value.push({
// name: item.stepDescription ? item.stepDescription : "申请",
// name: getStatusName(index),
name: getStatusName(item, index),
// user: item.approver.chineseName,
user: "",
time: item.approvalTime,
status: item.stepId,
active: item.active,
statusId: Number(item.statusId), // 确保转换为 number 类型
});
}
);
// 模块展示
approvalType.value = res.data.commissionType || "AE";
// 个人信息
personalInformationObj.chineseName =
res.data.application.user?.chineseName;
personalInformationObj.role = res.data.application.user?.roles
.map((item: any) => item.role)
.join(",");
personalInformationObj.journalName = res.data.application.user?.roles
.map((item: any) => item.journalName)
.join(",");
personalInformationObj.year = res.data.application.year;
personalInformationObj.quarter = res.data.application.quarter;
personalInformationObj.createTime = res.data.application.createTime;
personalInformationObj.beginTime = res.data.beginTime;
personalInformationObj.endTime = res.data.endTime;
personalInformationObj.qcPassRate = res.data.application.qcPassRate;
personalInformationObj.paidConfirmed =
res.data.application.paidConfirmed;
personalInformationObj.finalConfirmed = res.data.application.paidConfirmed - res.data.application.uncheckedNumber
personalInformationObj.email = res.data.application.email;
personalInformationObj.pi = res.data.application.pi || 0;
personalInformationObj.paperId = res.data.application?.paperId;
// 审批信息
approvalInformationObj.journalName =
res.data.application?.journalName;
approvalInformationObj.commissionType =
res.data.performanceSetting?.commissionTypeValue;
approvalInformationObj.calculationFormula =
res.data.performanceSetting?.amount;
approvalInformationObj.remark = res.data.application.remark;
approvalInformationObj.amount = res.data.application.amount;
selectedJournal.value = res.data.application?.jid;
approvalInformationObj.proportion = res.data.application?.proportion;
approvalInformationObj.totalCommission =
res.data.application?.totalCommission;
// 初始化汇率
exchangeRateInput.value = res.data.application.exchangeRate || "";
approvalInformationObj.id = res.data.performanceSetting?.id;
approvalInformationObj.categoryId =
res.data.performanceSetting?.categoryId;
approvalInformationObj.commissionTypeId =
res.data.performanceSetting?.commissionTypeId;
approvalInformationObj.sectionId = res.data.section?.sectionid;
approvalInformationObj.sectionTitle = res.data.section?.title;
approvalInformationObj.siPublished = res.data.section?.publishNumber;
approvalInformationObj.sectionType = res.data.section?.sectionType;
// 权限
showApprove.value = res.data.showApprove;
showReject.value = res.data.showReject;
showSendToApprove.value = res.data.showSendToApprove;
showSave.value = res.data.showSave;
showExchangeRate.value = res.data.showExchangeRate;
applicationStatusValue.value = res.data.applicationStatusValue;
notificationList.value = res.data.notificationList || [];
approvalInformationObj.proofDtoList = res.data.proofDtoList || [];
rejectComment.value = res.data.rejectComment;
proofRelations.value = res.data.application?.proofs;
hasCommission.value = res.data.application?.hasCommission || false;
sectionHasCommission.value =
res.data.application?.sectionHasCommission || false;
} else {
stepsData.value = [];
}
isLoading.value = false;
}
})
.catch(() => {
isLoading.value = false;
})
.finally(() => {
isLoading.value = false;
});
selectedCategory.value = approvalInformationObj.categoryId;
await getPerformanceSettingsByCommissionId();
// 初始化证明材料编辑数据
setTimeout(() => {
initializeProofEditData();
}, 100);
};
function getStatusName(item: any, index: number) {
return index == 0
? `申请:${item.approver.chineseName}`
: `${item.statusDescription}: ${item.approver.chineseName}`;
}
// 检查指定索引之前是否有被拒绝的步骤
const hasRejectedBefore = (index: number) => {
for (let i = 0; i < index && i < stepsData.value.length; i++) {
if (stepsData.value[i].active && stepsData.value[i].statusId === 4) {
return true;
}
}
return false;
};
// 组件卸载时移除事件监听
onUnmounted(() => {
document.removeEventListener("paste", handlePaste);
document.removeEventListener("paste", handleProofPasteEvent);
// 清理window上的函数
delete (window as any).previewCommentImage;
delete (window as any).handleCommentImageClick;
});
const selectedCategory = ref();
// 处理粘贴事件
const handlePaste = (event: ClipboardEvent) => {
const items = event.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
const file = items[i].getAsFile();
if (!file) continue;
// 根据当前焦点决定上传到哪个区域
if (rejectDialogVisible.value) {
handleImageFile(file, "reject");
} else if (addNotesDialogVisible.value) {
handleImageFile(file, "notes");
} else {
handleImageFile(file, "invitation");
}
// 阻止默认粘贴行为
event.preventDefault();
break;
}
}
};
// 上传前检查
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith("image/");
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isImage) {
ElMessage.error("Only image files can be uploaded.");
return false;
}
if (!isLt5M) {
ElMessage.error("The image size must not exceed 5MB.");
return false;
}
return true;
};
// 处理图片文件
const handleImageFile = (file: File, type: string) => {
if (!beforeUpload(file)) return;
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const imgUrl = reader.result as string;
if (type === "invitation") {
invitationProofUrl.value = imgUrl;
invitationImages.value.push({
url: imgUrl,
name: file.name,
});
} else if (type === "scopus") {
scopusProofUrl.value = imgUrl;
} else if (type === "references") {
referencesProofUrl.value = imgUrl;
} else if (type === "identity") {
identityProofUrl.value = imgUrl;
} else if (type === "reject") {
}
};
};
// 自定义上传函数
const customUploadInvitation = (options: any) => {
handleImageFile(options.file, "invitation");
};
const customUploadScopus = (options: any) => {
handleImageFile(options.file, "scopus");
};
const customUploadReferences = (options: any) => {
handleImageFile(options.file, "references");
};
const customUploadIdentity = (options: any) => {
handleImageFile(options.file, "identity");
};
const customUploadReject = (options: any) => {
handleImageFile(options.file, "reject");
};
const customUploadNotes = (options: any) => {
handleImageFile(options.file, "notes");
};
const fileList = ref([]);
const uploadNoteRef = ref(null);
const fileId = ref("");
const handleExceed: UploadProps["onExceed"] = (files) => {
uploadNoteRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
uploadNoteRef.value!.handleStart(file);
uploadNoteRef.value!.submit();
};
const handleRemove: UploadProps["onRemove"] = (file, uploadFiles) => {
fileId.value = "";
};
async function uploadNoteFile(options: UploadRequestOptions): Promise<any> {
const { data } = await uploadFileApiNew(options.file);
fileId.value = data.uploadFileData.id;
}
// 上传成功回调
const handleInvitationSuccess = (response: any, file: any) => {
// 实际项目中处理后端返回的URL
invitationProofUrl.value = URL.createObjectURL(file.raw);
};
const handleScopusSuccess = (response: any, file: any) => {
scopusProofUrl.value = URL.createObjectURL(file.raw);
};
const handleReferencesSuccess = (response: any, file: any) => {
referencesProofUrl.value = URL.createObjectURL(file.raw);
};
const handleIdentitySuccess = (response: any, file: any) => {
identityProofUrl.value = URL.createObjectURL(file.raw);
};
// 移除图片
const removeInvitationImage = (index: number) => {
invitationImages.value.splice(index, 1);
if (invitationImages.value.length === 0) {
invitationProofUrl.value = "";
} else {
invitationProofUrl.value = invitationImages.value[0].url;
}
};
// 预览图片
const previewImage = (url: string, item) => {
// console.log("previewImage", item);
// 使用 el-dialog 预览图片
previewImageUrl.value = url;
imagePreviewVisible.value = true;
};
const handleSave = () => {
// if (!invitationProofUrl.value) {
// return ElMessage.warning(
// "Please upload the proof materials of the invitation letter"
// );
// }
// 构建证明材料数据 - 如果是编辑模式,使用编辑后的数据
let finalProofRelations: any = proofRelations.value;
if (isProofEditable.value && Object.keys(proofEditData.value).length > 0) {
// 构建编辑后的证明材料数据
finalProofRelations = approvalInformationObj.proofDtoList
.map((proofType: any, index: number) => {
const data = proofEditData.value[index];
console.log('材料数据', data)
const isLinkType =
proofType.value?.toLowerCase().includes("url") ||
proofType.value?.toLowerCase().includes("link");
if (!isLinkType) {
// 图片类型:如果有多张图片,每张图片作为一条数据
if (data?.images?.length > 0) {
return data.images.map((image: any) => ({
fileId: image.fileId || null,
note: "",
fileType: proofType.id,
}));
} else {
// 没有图片时返回一条空数据
return [
{
fileId: null,
note: "",
fileType: proofType.id,
},
];
}
} else {
// 文本输入类型:只返回用户输入的文本,fileId为空
return [
{
fileId: null,
note: data?.url || "",
fileType: proofType.id,
},
];
}
})
.filter((item: any) => item !== null) // 过滤掉空值
.flat(); // 将嵌套数组展平
}
let params = {
id: userId.value,
email: personalInformationObj.email,
performanceSettingId: approvalInformationObj.id,
year: personalInformationObj.year,
quarter: personalInformationObj.quarter,
qcPassRate: personalInformationObj.qcPassRate,
paidConfirmed: personalInformationObj.paidConfirmed,
pi: personalInformationObj.pi,
amount: approvalInformationObj.amount,
paperId: personalInformationObj.paperId,
sectionId: approvalInformationObj.sectionId,
remark: approvalInformationObj.remark,
proofRelations: finalProofRelations,
jid: selectedJournal.value,
proportion: approvalInformationObj.proportion,
totalCommission: approvalInformationObj.totalCommission,
};
ElMessageBox.confirm("Confirm the approval of this application?", "Tips", {
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
type: "success",
})
.then(() => {
performanceAppUpdate(params).then((res) => {
if (res.code === 2000) {
getDetailsData();
ElMessage({
type: "success",
message: res.message,
});
}
});
})
.catch(() => {});
};
const handleSendToApproval = () => {
let params = {
id: userId.value,
email: personalInformationObj.email,
performanceSettingId: approvalInformationObj.id,
year: personalInformationObj.year,
quarter: personalInformationObj.quarter,
qcPassRate: personalInformationObj.qcPassRate,
paidConfirmed: personalInformationObj.paidConfirmed,
pi: personalInformationObj.pi,
amount: approvalInformationObj.amount,
paperId: personalInformationObj.paperId,
sectionId: approvalInformationObj.sectionId,
remark: approvalInformationObj.remark,
proofRelations: proofRelations.value,
jid: selectedJournal.value,
proportion: approvalInformationObj.proportion,
totalCommission: approvalInformationObj.totalCommission,
};
ElMessageBox.confirm("Are you sure send to approve ?", "Tip", {
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
type: "success",
})
.then(async () => {
await performanceAppUpdate(params).then((res) => {
if (res.code === 2000) {
getDetailsData();
ElMessage({
type: "success",
message: res.message,
});
performanceSendToApprove({
applicationId: userId.value,
}).then((res: any) => {
if (res.code === 2000) {
getDetailsData();
ElMessage({
type: "success",
message: res.message,
});
}
});
}
});
})
.catch(() => {});
};
// 审批和拒绝处理
const handleApprove = () => {
// 验证USD Exchange Rate必填(当显示汇率字段时)
if (showExchangeRate.value && !exchangeRateInput.value?.trim()) {
ElMessage.warning("USD Exchange Rate cannot be empty.");
return;
}
// if (!invitationProofUrl.value) {
// ElMessage.warning("请上传邀请函证明材料");
// return;
// }
ElMessageBox.confirm("Confirm the approval of this application?", "Tip", {
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
type: "success",
})
.then(() => {
let params = {
applicationId: userId.value,
approverEmail,
approved: true, // true 通过; false 拒绝
remark: approvalInformationObj.remark,
amount: approvalInformationObj.amount,
exchangeRate: approvalInformationObj.exchangeRate,
};
performanceApprove(params).then((res: any) => {
if (res.code === 2000) {
getDetailsData();
ElMessage({
type: "success",
message: res.message,
});
}
});
})
.catch(() => {});
};
const handleReject = () => {
rejectDialogVisible.value = true;
};
const confirmReject = () => {
// 验证USD Exchange Rate必填(当显示汇率字段时)
if (showExchangeRate.value && !exchangeRateInput.value?.trim()) {
ElMessage.warning("USD Exchange Rate cannot be empty.");
return;
}
if (!rejectReason.value) {
ElMessage.warning("Please enter rejection reason");
return;
}
ElMessageBox.confirm("Confirm to reject this application?", "Tip", {
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
type: "warning",
})
.then(() => {
let params = {
applicationId: userId.value,
approverEmail,
approved: false, // false 表示拒绝
remark: rejectReason.value,
amount: approvalInformationObj.amount,
exchangeRate: approvalInformationObj.exchangeRate,
};
performanceApprove(params)
.then((res: any) => {
if (res.code === 2000) {
getDetailsData();
rejectDialogVisible.value = false;
rejectReason.value = "";
ElMessage({
type: "success",
message: res.message,
});
}
})
.catch(() => {
ElMessage.error("Operation failed");
});
})
.catch(() => {});
};
const cancelModal = () => {
rejectDialogVisible.value = false;
rejectReason.value = "";
rulesDialogVisible.value = false;
addNotesDialogVisible.value = false;
notesContent.value = "";
paperDialogVisible.value = false;
};
// 添加笔记相关处理
const handleAddNotes = () => {
addNotesDialogVisible.value = true;
notesContent.value = "";
};
const ruleFormRef = ref();
const saveNoteLoading = ref(false);
const saveNote = () => {
const data = {
value: notesContent.value,
id: userId.value,
fileId: fileId.value,
};
if (!notesContent.value) {
ElMessage({
message: "History notes content is empty!",
type: "warning",
});
return;
}
saveNoteLoading.value = true;
postNotification5(data)
.then((res) => {
ElMessage({
message: "Successful operation!",
type: "success",
});
performanceApplicationDetails({
id: userId.value,
}).then((res: any) => {
if (res.code === 2000) {
notificationList.value = res.data.notificationList || [];
}
});
// 清理表单数据
fileList.value = [];
fileId.value = "";
proxy.$refs.uploadNoteRef.clearFiles();
notesContent.value = "";
})
.catch((error) => {
addNotesDialogVisible.value = false;
saveNoteLoading.value = false;
})
.finally(() => {
addNotesDialogVisible.value = false;
saveNoteLoading.value = false;
});
};
const isEditing = ref(false);
// 切换编辑状态
const toggleEdit = () => {
isEditing.value = !isEditing.value;
};
// 表格弹窗相关数据
const tableDialogVisible = ref(false);
const tableDialogTitle = ref("");
const performanceSettingData = ref(); // matching rules 弹窗数据
const showMatchingRulesDetails = () => {
rulesDialogVisible.value = true;
tableDialogTitle.value = "Matching Rules Details";
performanceSettingsRuleId({
id: approvalInformationObj.id,
}).then((res) => {
if (res.code === 2000) {
performanceSettingData.value = res.data.performanceSetting;
}
});
};
// Paper详情弹窗相关
const paperDialogVisible = ref(false);
const paperDetailData = ref();
const showDetailsType = ref();
const showPaperDetails = (paperId: string, nType: string) => {
if (!paperId) return;
showDetailsType.value = nType;
paperDialogVisible.value = true;
// 调用API获取Paper详情
performancePaperDetails({
articleId: paperId,
type: nType === "si" ? 1 : undefined,
})
.then((res: any) => {
if (res.code === 2000) {
paperDetailData.value =
nType === "paper" ? res.data.article : res.data.section;
} else {
ElMessage.error(res.message);
paperDetailData.value = null;
}
})
.catch(() => {
paperDetailData.value = null;
});
};
// 获取图片URL
const getImageUrl = (fileId: number) => {
if (!fileId) return "";
return `/api/poster/show/image/${fileId}`;
};
// 处理图片加载错误
const handleImageError = (event: Event) => {
const target = event.target as HTMLImageElement;
// target.src = "/src/assets/icons/no-image.svg"; // 使用默认图片或隐藏
// target.src = "";
target.src = new URL("@/assets/no-image.png", import.meta.url).href;
target.alt = "Image failed to load";
};
// 获取显示的proof项目,保持原有布局结构
const getDisplayProofItems = () => {
if (
!approvalInformationObj.proofDtoList ||
approvalInformationObj.proofDtoList.length === 0
) {
return [];
}
// 直接使用后端返回的数据,保持原有顺序
return approvalInformationObj.proofDtoList.map((proofItem: any) => {
const images =
proofItem.list?.filter((item: any) => item.fileId !== null) || [];
const links =
proofItem.list?.filter(
(item: any) => item.fileId === null && item.note
) || [];
// 判断是否为链接类型:有链接数据且没有图片数据,或者value中包含URL相关关键词
const isLinkType =
(links.length > 0 && images.length === 0) ||
proofItem.value?.toLowerCase().includes("url") ||
proofItem.value?.toLowerCase().includes("link");
return {
title: proofItem.value || "Proof Material",
type: isLinkType ? "link" : "image",
originalType: isLinkType ? 1 : 0, // 新增:保存原始类型用于编辑模式判断
value: links.length > 0 ? links[0].note : "",
images: images,
placeholder: isLinkType ? "No link provided" : "",
};
});
};
const goPage = (id: any) => {
// let routeData = router.resolve({
// path: "/papersMain/page",
// query: { id },
// });
// window.open(routeData.href, "_blank");
};
// 图片预览相关
const imagePreviewVisible = ref(false);
const previewImageUrl = ref("");
const imageScale = ref(1);
const isFullscreen = ref(false);
const previewContainer = ref();
const imageWrapper = ref();
const tooltipContent = ref("");
const fetchInfo = async (id, nType) => {
if (!id) {
tooltipContent.value = "No available";
return;
}
try {
tooltipContent.value = "Loading...";
const { code, data } = await performanceCommissionDetailsByAritcleId({
articleId: id,
type: nType === "si" ? 1 : undefined,
});
if (code === 2000) {
tooltipContent.value = data.details.commissionDetails
.map(
(item) =>
`${item.performanceApplicationName}:${item.commissionTypeName} ${item.stepId ? `(Step ${item.stepId}: Approved)` : ''}`
)
.join("<br>");
}
} catch {
tooltipContent.value = "Failed to load data";
}
};
// 新增编辑相关的数据和计算属性
const isProofEditable = computed(() => {
if (!applicationStatusValue.value || !showSave.value) return false;
const status = applicationStatusValue.value.toLowerCase();
return status.includes("pending") || status.includes("rejected");
});
// 编辑模式的证明材料数据结构
const proofEditData = ref<{
[key: number]: {
displayUrl?: string;
url?: string;
note?: string;
fileId?: number;
images?: { url: string; name: string; fileId?: number }[];
};
}>({});
// 当前焦点区域
const currentProofFocusArea = ref<any>(0);
// 是否显示焦点提示
const showProofFocusHint = ref(false);
// 焦点提示的位置
const proofFocusHintPosition = ref({ top: 0, left: 0 });
// 设置活跃上传区域
const setActiveProofUploadArea = (index: number) => {
currentProofFocusArea.value = index;
// 显示简短提示
showProofFocusHint.value = true;
setTimeout(() => {
showProofFocusHint.value = false;
}, 1500);
};
// 显示上传提示
const showProofUploadHint = (index: number, event: MouseEvent) => {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
proofFocusHintPosition.value = {
top: rect.top - 30,
left: rect.left + rect.width / 2 - 100,
};
// 临时更新焦点区域用于显示提示文本
const tempFocus = currentProofFocusArea.value;
currentProofFocusArea.value = index;
showProofFocusHint.value = true;
// 恢复原来的焦点区域
setTimeout(() => {
if (currentProofFocusArea.value === index && !showProofFocusHint.value) {
currentProofFocusArea.value = tempFocus;
}
}, 100);
};
// 隐藏上传提示
const hideProofUploadHint = () => {
showProofFocusHint.value = false;
};
// 处理证明材料粘贴事件
const handleProofPasteEvent = (event: ClipboardEvent) => {
const items = event.clipboardData?.items;
if (!items) return;
// 检查当前焦点元素,判断应该上传到哪个区域
const activeElement = document.activeElement;
let targetIndex = currentProofFocusArea.value;
let foundArea = false;
// 根据当前激活的元素判断上传区域
if (activeElement) {
// 尝试从当前元素向上查找上传容器
let element = activeElement as HTMLElement;
while (element && !foundArea) {
if (element.classList?.contains("upload-container")) {
// 查找proof-uploader的索引
const uploader = element.querySelector("[class*='proof-uploader-']");
if (uploader) {
const classList = Array.from(uploader.classList);
const uploaderClass = classList.find((cls) =>
cls.startsWith("proof-uploader-")
);
if (uploaderClass) {
targetIndex = parseInt(uploaderClass.split("-")[2]);
foundArea = true;
}
}
break;
}
// 直接检查当前元素是否是上传器
if (element.classList.value.includes("proof-uploader-")) {
const classList = Array.from(element.classList);
const uploaderClass = classList.find((cls) =>
cls.startsWith("proof-uploader-")
);
if (uploaderClass) {
targetIndex = parseInt(uploaderClass.split("-")[2]);
foundArea = true;
}
break;
}
element = element.parentElement as HTMLElement;
}
}
let imageFound = false;
// 处理所有粘贴的图片
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
const file = items[i].getAsFile();
if (!file) continue;
imageFound = true;
// 使用确定的目标区域上传图片
processProofImageFile(file, targetIndex);
}
}
// 只有找到图片时才阻止默认粘贴行为
if (imageFound) {
event.preventDefault();
// 更新当前焦点区域,以便下次粘贴时使用
currentProofFocusArea.value = targetIndex;
}
};
// 上传前检查
const beforeProofUpload = (file: File) => {
const isImage = file.type.startsWith("image/");
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isImage) {
ElMessage.error("Only image files can be uploaded!");
return false;
}
if (!isLt5M) {
ElMessage.error("The image size must not exceed 5MB.");
return false;
}
return true;
};
// 处理图片文件
const processProofImageFile = async (file: File, index: number) => {
if (!beforeProofUpload(file)) return;
// 初始化该索引的数据对象
if (!proofEditData.value[index]) {
proofEditData.value[index] = { images: [] };
}
if (!proofEditData.value[index].images) {
proofEditData.value[index].images = [];
}
try {
// 调用真实上传接口
const uploadResponse = await uploadFileApiNew(file);
if (uploadResponse.data && uploadResponse.data.uploadFileData) {
const fileId = uploadResponse.data.uploadFileData.id || "";
const imgUrl = URL.createObjectURL(file);
const imageData = {
url: imgUrl,
name: file.name,
fileId: fileId,
};
proofEditData.value[index].images!.push(imageData);
proofEditData.value[index].displayUrl = imgUrl;
proofEditData.value[index].fileId = fileId; // 记录当前显示图片的文件ID
ElMessage.success("Image uploaded successfully.");
} else {
ElMessage.error("Image upload failed.");
}
} catch (error) {
console.error("Upload error:", error);
ElMessage.error("Image upload failed.");
}
};
// 自定义上传函数
const customProofUploadHandler = (options: any, index: number) => {
processProofImageFile(options.file, index);
};
// 上传成功回调
const onProofUploadSuccess = async (response: any, file: any, index: number) => {
// 初始化该索引的数据对象
if (!proofEditData.value[index]) {
proofEditData.value[index] = { images: [] };
}
if (!proofEditData.value[index].images) {
proofEditData.value[index].images = [];
}
try {
// 调用真实上传接口
const uploadResponse = await uploadFileApiNew(file.raw);
if (uploadResponse.data && uploadResponse.data.uploadFileData) {
const fileId = uploadResponse.data.uploadFileData.id;
const url = URL.createObjectURL(file.raw);
proofEditData.value[index].images!.push({
url,
name: file.name,
fileId: fileId,
});
proofEditData.value[index].displayUrl = url;
proofEditData.value[index].fileId = fileId; // 记录当前显示图片的文件ID
ElMessage.success("Image uploaded successfully.");
} else {
ElMessage.error("Image upload failed.");
}
} catch (error) {
console.error("Upload error:", error);
ElMessage.error("Image upload failed.");
}
};
// 移除图片
const removeProofImage = (proofIndex: number, imageIndex: number) => {
if (proofEditData.value[proofIndex]?.images) {
proofEditData.value[proofIndex].images!.splice(imageIndex, 1);
if (proofEditData.value[proofIndex].images!.length === 0) {
proofEditData.value[proofIndex].displayUrl = "";
} else {
proofEditData.value[proofIndex].displayUrl =
proofEditData.value[proofIndex].images![0].url;
}
}
};
// 切换展示图片
const changeProofDisplayImage = (proofIndex: number, url: string) => {
if (proofEditData.value[proofIndex]) {
proofEditData.value[proofIndex].displayUrl = url;
// 找到对应图片的文件ID并更新
const selectedImage = proofEditData.value[proofIndex].images?.find(
(img) => img.url === url
);
if (selectedImage?.fileId) {
proofEditData.value[proofIndex].fileId = selectedImage.fileId;
}
}
};
// 初始化证明材料编辑数据
const initializeProofEditData = () => {
if (
!approvalInformationObj.proofDtoList ||
approvalInformationObj.proofDtoList.length === 0
) {
return;
}
approvalInformationObj.proofDtoList.forEach((proofItem: any, index: number) => {
const images =
proofItem.list?.filter((item: any) => item.fileId !== null) || [];
const links =
proofItem.list?.filter(
(item: any) => item.fileId === null && item.note
) || [];
// 判断是否为链接类型
const isLinkType =
(links.length > 0 && images.length === 0) ||
proofItem.value?.toLowerCase().includes("url") ||
proofItem.value?.toLowerCase().includes("link");
if (!proofEditData.value[index]) {
proofEditData.value[index] = {
images: [],
url: "",
displayUrl: "",
note: "",
fileId: undefined,
};
}
if (isLinkType) {
// 链接类型:回填文本内容
proofEditData.value[index].url = links.length > 0 ? links[0].note : "";
} else {
// 图片类型:回填图片数据
const imageData = images.map((img: any) => ({
url: getImageUrl(img.fileId),
name: `image_${img.fileId}`,
fileId: img.fileId,
}));
proofEditData.value[index].images = imageData;
if (imageData.length > 0) {
proofEditData.value[index].displayUrl = imageData[0].url;
proofEditData.value[index].fileId = imageData[0].fileId;
}
}
});
};
// 图片预览控制方法
const imageStyle = computed(() => ({
transform: `scale(${imageScale.value})`,
transition: 'transform 0.3s ease',
}));
// 放大图片
const zoomIn = () => {
imageScale.value = Math.min(imageScale.value * 1.2, 5);
// 缩放后保持顶部可见
nextTick(() => {
if (imageWrapper.value) {
imageWrapper.value.scrollTop = 0;
}
});
};
// 缩小图片
const zoomOut = () => {
imageScale.value = Math.max(imageScale.value / 1.2, 0.1);
// 缩放后保持顶部可见
nextTick(() => {
if (imageWrapper.value) {
imageWrapper.value.scrollTop = 0;
}
});
};
// 重置缩放
const resetZoom = () => {
imageScale.value = 1;
// 重置后滚动到顶部
nextTick(() => {
if (imageWrapper.value) {
imageWrapper.value.scrollTop = 0;
imageWrapper.value.scrollLeft = 0;
}
});
};
// 处理鼠标滚轮缩放
const handleWheel = (event: WheelEvent) => {
event.preventDefault();
const wrapper = imageWrapper.value;
if (!wrapper) return;
// 记录当前滚动位置和鼠标相对位置
const rect = wrapper.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const scrollLeft = wrapper.scrollLeft;
const scrollTop = wrapper.scrollTop;
// 计算鼠标在图片中的相对位置
const relativeX = (scrollLeft + mouseX) / imageScale.value;
const relativeY = (scrollTop + mouseY) / imageScale.value;
const oldScale = imageScale.value;
const delta = event.deltaY > 0 ? 0.9 : 1.1;
imageScale.value = Math.max(0.1, Math.min(5, imageScale.value * delta));
// 缩放完成后调整滚动位置,保持鼠标位置不变
nextTick(() => {
if (wrapper) {
const newScrollLeft = relativeX * imageScale.value - mouseX;
const newScrollTop = relativeY * imageScale.value - mouseY;
wrapper.scrollLeft = Math.max(0, newScrollLeft);
wrapper.scrollTop = Math.max(0, newScrollTop);
}
});
};
// 图片加载完成时重置状态
const handleImageLoad = () => {
resetZoom();
};
// 全屏切换
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
previewContainer.value?.requestFullscreen?.();
isFullscreen.value = true;
} else {
document.exitFullscreen?.();
isFullscreen.value = false;
}
};
// 关闭预览
const closePreview = () => {
if (isFullscreen.value) {
document.exitFullscreen?.();
isFullscreen.value = false;
}
imagePreviewVisible.value = false;
resetZoom();
};
// 监听全屏状态变化
document.addEventListener('fullscreenchange', () => {
isFullscreen.value = !!document.fullscreenElement;
});
</script>
<style lang="scss" scoped>
.box-card {
margin: 10px 0px;
min-height: 100vh;
background: #fff;
border-radius: 5px;
padding: 0px 2px 0px 2px;
box-shadow: 0 0 12px rgba(0, 0, 0, 0.12);
}
.performance-setting-container {
padding: 20px;
}
.form-title {
display: flex;
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
.form-status {
padding: 0 10px;
background-color: #f2f2f2;
margin-left: 10px;
border-radius: 5px;
font-weight: 100;
color: white;
&.in-review {
background-color: #d4b86a; /* 柔和黄色 */
}
&.approved {
background-color: #85ce61; /* 柔和绿色 */
}
&.pending {
background-color: #a6a9ad; /* 柔和灰色 */
}
&.rejected {
background-color: #f78989; /* 柔和红色 */
}
}
}
/* 流程样式 */
.process-section {
border-radius: 4px;
padding: 0 15px;
}
.process-steps {
display: flex;
align-items: center;
margin-top: 15px;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
flex: 1;
}
.step-icon {
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.step.completed .step-icon {
background-color: #67c23a;
color: white;
}
.step.current .step-icon {
background-color: #409eff;
color: white;
}
.step.active .step-icon {
background-color: #67c23a;
color: white;
}
.step.rejected .step-icon {
background-color: #f56c6c;
color: white;
}
.step.disabled .step-icon {
background-color: #e4e7ed;
color: #c0c4cc;
}
.step.disabled .step-info {
color: #c0c4cc;
}
.step-line {
height: 2px;
background-color: #f0f0f0;
flex: 1;
}
.step-line.completed {
background-color: #67c23a;
}
.step-line.disabled {
background-color: #e4e7ed;
}
.step-info {
text-align: center;
font-size: 12px;
}
.step-name {
font-weight: bold;
margin-bottom: 4px;
font-size: 18px;
}
.step-user,
.step-time {
color: #666;
}
/* 信息区域样式 */
.info-section {
// margin-bottom: 30px;
// border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
}
.history-section {
border-radius: 4px;
padding: 0 15px;
}
.info-section h3 {
margin-top: 0;
margin-bottom: 15px;
font-size: 16px;
color: #333;
}
.info-item {
margin-bottom: 15px;
display: flex;
align-items: center;
}
.label {
min-width: 155px;
// font-weight: bold;
color: #606266;
text-align: right;
}
.value {
color: #333;
margin-left: 10px;
}
.clickable-value {
color: #409eff;
cursor: pointer;
text-decoration: none;
}
.proof-item {
margin-bottom: 20px;
}
.upload-container {
margin-top: 10px;
}
.image-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 148px;
height: 148px;
display: inline-block;
}
.image-uploader:hover {
border-color: #409eff;
}
.upload-image {
width: 146px;
height: 146px;
display: block;
object-fit: cover;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #8c939d;
}
.uploaded-images {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
}
.image-item {
position: relative;
width: 80px;
height: 80px;
border: 2px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&.active {
border: 2px solid #409eff;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
&:hover {
border-color: #409eff;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
&:hover .image-actions {
opacity: 1;
}
}
.thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-actions {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s;
gap: 5px;
}
.image-item:hover .image-actions {
opacity: 1;
}
.image-actions .el-button {
color: white;
}
/* 金额部分样式 */
.amount-section {
margin-bottom: 30px;
}
.amount-item {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.modify-link {
justify-content: center;
}
.remaining {
justify-content: flex-end;
color: #3c8cbb;
margin-left: 10px;
cursor: pointer;
}
.readonly-textarea {
min-height: 80px;
padding: 8px 12px;
border: 1px solid #ebeef5;
border-radius: 4px;
background-color: #f8f8f8;
color: #606266;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.quota {
color: #3c8cbb;
// font-weight: bold;
margin-left: 10px;
cursor: pointer;
}
/* 按钮样式 */
.action-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
}
.el-button--primary.is-link {
color: #409eff;
}
.el-button--primary.is-link:hover {
color: #66b1ff;
}
.two-col-container {
display: flex;
flex-wrap: wrap;
gap: 16px; /* 子项之间的间距 */
// width: 100%;
width: 60%;
margin-left: 10px;
}
.item {
width: calc(50% - 8px); /* 50% 减去一半 gap 实现两列等宽 */
border: 1px dashed #ccc;
box-sizing: border-box;
padding: 12px;
// text-align: center;
border-radius: 6px;
background-color: #fff;
}
.el-divider--horizontal {
margin: 10px 0;
}
/* 拒绝弹窗样式 */
.reject-dialog-content {
padding: 0 20px;
}
.image-uploader ::v-deep .el-upload {
width: 148px;
height: 148px;
line-height: 146px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.add-notes-btn {
padding: 6px 12px;
font-size: 12px;
}
.notes-modal ::v-deep .el-dialog__header {
padding: 16px !important;
}
/* 自定义对话框标题头样式 */
.custom-dialog-header {
padding: 10px 0 0 0;
font-size: 18px;
font-weight: 500;
color: #303133;
}
.dialog-title {
line-height: 24px;
}
/* 保持原有样式,只添加必要的图片展示样式 */
.image-display-area {
width: 100%;
min-height: 100px;
}
.images-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-start;
align-items: flex-start;
}
.upload-image {
width: 80px;
height: 80px;
object-fit: cover;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.upload-image-2{
width: 100%;
height: 100%;
}
.upload-image:hover {
border-color: #409eff;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
/* Proof标题样式 - 超过两行显示省略号 */
.proof-item .label {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
max-height: 2.8em; /* 2行的高度 */
word-break: break-word;
min-width: auto;
}
.custom-link {
color: #1890ff;
text-decoration: underline;
display: inline-block;
max-width: 100%;
word-break: break-all;
overflow-wrap: break-word;
white-space: normal;
line-height: 1.4;
}
/* Comments 图片样式 */
.comments-content ::v-deep .comment-image {
max-width: 120px !important;
max-height: 80px !important;
width: auto;
height: auto;
object-fit: cover;
border-radius: 6px;
border: 2px solid #e4e7ed;
margin: 5px;
transition: all 0.3s ease;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.comments-content ::v-deep .comment-image:hover {
border-color: #409eff;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.comments-content ::v-deep .comment-image:active {
transform: scale(0.98);
}
/* History Notes 图片预览图标样式 */
.history-message ::v-deep .image-preview-icon {
display: inline-block;
margin-right: 8px;
font-size: 16px;
color: #409eff;
cursor: pointer;
vertical-align: middle;
padding: 2px 4px;
border-radius: 3px;
background-color: #f0f9ff;
border: 1px solid #409eff;
transition: all 0.3s ease;
}
.history-message ::v-deep .image-preview-icon:hover {
background-color: #409eff;
color: white;
transform: scale(1.1);
}
.history-message ::v-deep img {
margin-left: 5px;
max-width: 100%;
height: auto;
}
/* History Notes 表格中图片固定尺寸 */
.history-message ::v-deep img {
max-width: 150px !important;
max-height: 100px !important;
width: auto;
height: auto;
object-fit: cover;
border-radius: 4px;
border: 1px solid #dcdfe6;
cursor: pointer;
}
.history-message-2 ::v-deep img {
max-width: 50px !important;
max-height: 50px !important;
width: auto;
height: auto;
object-fit: cover;
border-radius: 4px;
border: 1px solid #dcdfe6;
cursor: pointer;
}
.history-message ::v-deep img:hover {
opacity: 0.8;
border-color: #409eff;
}
/* 图片预览按钮样式 */
.image-preview-buttons {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.preview-btn {
display: flex;
align-items: center;
gap: 4px;
}
.preview-btn .el-icon {
font-size: 14px;
}
/* 图片预览图标样式 */
.preview-icon {
font-size: 24px;
color: #409eff;
cursor: pointer;
margin-right: 8px;
padding: 4px;
border-radius: 4px;
transition: all 0.3s ease;
}
.preview-icon:hover {
background-color: #ecf5ff;
color: #337ecc;
transform: scale(1.1);
}
/* 图片预览弹窗样式 */
::v-deep(.image-preview-dialog) {
width: 80vw !important;
max-width: 1200px !important;
min-width: 800px !important;
}
::v-deep(.image-preview-dialog .el-message-box__content) {
padding: 20px !important;
}
::v-deep(.image-preview-dialog .el-message-box__message) {
padding: 0 !important;
}
/* 图片预览容器样式 */
.image-preview-container {
display: flex;
flex-direction: column;
height: 70vh;
user-select: none;
}
.image-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #ebeef5;
margin-bottom: 10px;
flex-shrink: 0;
}
.zoom-indicator {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.image-wrapper {
flex: 1;
overflow: auto;
position: relative;
background-color: #f5f7fa;
border-radius: 8px;
/* 优化滚动性能 */
scroll-behavior: smooth;
}
.preview-image {
display: block;
margin: 0 auto;
border-radius: 8px;
transform-origin: top center;
user-select: none;
/* 移除最大尺寸限制,确保图片可以正常缩放 */
max-width: none;
max-height: none;
width: auto;
height: auto;
/* 当图片小于容器时居中显示,当图片大于容器时从顶部开始显示 */
vertical-align: top;
}
/* 全屏状态下的样式调整 */
.image-preview-container:fullscreen {
height: 100vh;
background-color: #000;
padding: 20px;
}
.image-preview-container:fullscreen .image-wrapper {
background-color: #000;
}
.image-preview-container:fullscreen .image-controls {
background-color: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 8px;
padding: 15px;
border-bottom: 1px solid #666;
}
.image-preview-container:fullscreen .zoom-indicator {
color: white;
}
.custom-divider{
border-color: #f0f0f0;
}
/* 编辑模式相关样式 */
.upload-area {
position: relative;
border: 2px dashed transparent;
border-radius: 8px;
padding: 4px;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #409eff;
background-color: rgba(64, 158, 255, 0.05);
}
.upload-area.active-upload {
border-color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.uploaded-images {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
gap: 8px;
justify-content: flex-start;
align-items: flex-start;
}
.image-item.active {
border: 2px solid #409eff;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
</style>
核心部分:
<!-- 图片预览弹窗 -->
<el-dialog
v-model="imagePreviewVisible"
title="Preview Picture"
width="90%"
:style="{ maxWidth: '1400px', minWidth: '800px' }"
append-to-body
>
<div class="image-preview-container" ref="previewContainer">
<div class="image-controls">
<el-button-group>
<el-button @click="zoomIn" size="small" :icon="ZoomIn" title="Zoom In">Zoom In</el-button>
<el-button @click="zoomOut" size="small" :icon="ZoomOut" title="Zoom Out">Zoom Out</el-button>
<el-button @click="resetZoom" size="small" :icon="Refresh" title="Reset">Reset</el-button>
<el-button @click="toggleFullscreen" size="small" :icon="FullScreen" title="Fullscreen">{{ isFullscreen ? 'Exit Fullscreen' : 'Fullscreen' }}</el-button>
</el-button-group>
<span class="zoom-indicator">{{ Math.round(imageScale * 100) }}%</span>
</div>
<div
class="image-wrapper"
@wheel="handleWheel"
ref="imageWrapper"
>
<img
:src="previewImageUrl"
alt="预览图片"
class="preview-image"
:style="imageStyle"
@error="handleImageError"
@load="handleImageLoad"
draggable="false"
/>
</div>
</div>
<template #footer>
<el-button @click="closePreview">Closed</el-button>
</template>
</el-dialog>