| | |
| | | <template> |
| | | <view> |
| | | <view class="form-container"> |
| | | <form :modelValue="formData"> |
| | | <view class="form-group"> |
| | | <label class="form-label">项目名称:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.projName"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">质量要求:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.itemMod"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">检验方法:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.inspectionMethod"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">使用仪表:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.usingInstruments"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">检验数:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.levelNum"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">下限:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.minValue"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">标准值:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.standardValue"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">上限:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.maxValue"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">更新人:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.updater"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">更新时间:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.updateTime"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">预览结果:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.result"/> |
| | | </view> |
| | | <view class="form-group"> |
| | | <label class="form-label">不合格描述:</label> |
| | | <input class="form-input" disabled="true" type="text" v-model="formData.remarks"/> |
| | | </view> |
| | | <view class="form-group edit"> |
| | | <input class="form-input" style="color: red" disabled="true" type="text" |
| | | value="没有最大值和最小值时填写0(未通过检验)或1(通过检验)"/> |
| | | </view> |
| | | <view class="form-group edit"> |
| | | <label class="form-label">检测结果:</label> |
| | | <input class="form-input" type="number" v-model="formData.fcheckResu"/> |
| | | </view> |
| | | <button type="primary" v-if="tableData.length < formData.levelNum" @click="submit">保存</button> |
| | | </form> |
| | | <view class="container"> |
| | | <!-- 头部 --> |
| | | <view class="header"> |
| | | <view class="title">首检项目明细</view> |
| | | <view class="order-number">首检单号: {{formData.billNo}}</view> |
| | | </view> |
| | | <view> |
| | | <img v-if="isShowImg" :src="base64Image" style="width:100%" @click="previewImage"/> |
| | | |
| | | <!-- 标签栏 - 如果有多个检验项目可以切换 --> |
| | | <view class="tabs" v-if="tabs && tabs.length > 1"> |
| | | <view v-for="(tab, index) in tabs" :key="index" class="tab" :class="{active: currentTab === index}" |
| | | @tap="switchTab(index, tab.id)"> |
| | | {{ tab.projName }} |
| | | </view> |
| | | </view> |
| | | <view class="list-container"> |
| | | <uni-table ref="table" border emptyText="暂无更多数据"> |
| | | <uni-tr> |
| | | <uni-th align="center" style="color: #FFFFFF;background-color: lightskyblue;">编号</uni-th> |
| | | <uni-th align="center" style="color: #FFFFFF;background-color: lightskyblue;">判定标识</uni-th> |
| | | <uni-th align="center" style="color: #FFFFFF;background-color: lightskyblue;">检验结果</uni-th> |
| | | <uni-th width="150" align="center" style="color: #FFFFFF;background-color: lightskyblue;">修改</uni-th> |
| | | </uni-tr> |
| | | <uni-tr v-for="(item, index) in tableData" :key="index"> |
| | | <uni-td align="center"> |
| | | {{ index + 1 }} |
| | | </uni-td> |
| | | <uni-td align="center"> |
| | | <input class="form-input" disabled="true" type="text" v-model="item.fstand"/> |
| | | </uni-td> |
| | | <uni-td align="center"> |
| | | <input class="form-input" disabled="true" type="text" v-model="item.fcheckResu"/> |
| | | </uni-td> |
| | | <uni-td> |
| | | <view class="uni-group edit"> |
| | | <button type="warn" v-if="isNumber" @click="toDetail(item)">修改</button> |
| | | <button type="warn" v-if="!isNumber" @click="numberEdit(item)">{{ editResult(item.fcheckResu) }}</button> |
| | | |
| | | <view class="tab-content"> |
| | | <!-- 基本信息 --> |
| | | <view class="section"> |
| | | <view class="section-header">基本信息</view> |
| | | <view class="section-body"> |
| | | <view class="info-grid"> |
| | | <view class="info-item"> |
| | | <view class="info-label">项目名称</view> |
| | | <view class="info-value">{{ formData.projName }}</view> |
| | | </view> |
| | | </uni-td> |
| | | </uni-tr> |
| | | </uni-table> |
| | | </view> |
| | | <view class="info-item" v-if="formData.itemMod"> |
| | | <view class="info-label">质量要求</view> |
| | | <view class="info-value">{{ formData.itemMod }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.inspectionMethod"> |
| | | <view class="info-label">检验方法</view> |
| | | <view class="info-value">{{ formData.inspectionMethod }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.usingInstruments"> |
| | | <view class="info-label">使用仪表</view> |
| | | <view class="info-value">{{ formData.usingInstruments }}</view> |
| | | </view> |
| | | <view class="info-item"> |
| | | <view class="info-label">检验数</view> |
| | | <view class="info-value">{{ formData.levelNum }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.minValue"> |
| | | <view class="info-label">下限</view> |
| | | <view class="info-value">{{ formData.minValue }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.standardValue"> |
| | | <view class="info-label">标准值</view> |
| | | <view class="info-value">{{ formData.standardValue }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.maxValue"> |
| | | <view class="info-label">上限</view> |
| | | <view class="info-value">{{ formData.maxValue }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.mnum"> |
| | | <view class="info-label">开穴总数</view> |
| | | <view class="info-value">{{ formData.mnum }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.dnum"> |
| | | <view class="info-label">堵穴号</view> |
| | | <view class="info-value">{{ formData.dnum }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.updater"> |
| | | <view class="info-label">更新人</view> |
| | | <view class="info-value">{{ formData.updater }}</view> |
| | | </view> |
| | | <view class="info-item" v-if="formData.updateTime"> |
| | | <view class="info-label">更新时间</view> |
| | | <view class="info-value">{{ formData.updateTime }}</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 规格要求 --> |
| | | <view class="section" v-if="formData.specRequ"> |
| | | <view class="section-header">规格要求</view> |
| | | <view class="section-body"> |
| | | <text class="spec-text">{{ formData.specRequ }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 检验描述 --> |
| | | <view class="section" v-if="formData.itemMod"> |
| | | <view class="section-header">检验描述</view> |
| | | <view class="section-body"> |
| | | <text class="spec-text">{{ formData.itemMod }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 检验结果 --> |
| | | <view class="section"> |
| | | <view class="section-header">检验结果</view> |
| | | <view class="section-body"> |
| | | <view class="info-grid"> |
| | | <view class="info-item" v-if="formData.result"> |
| | | <view class="info-label">预览结果</view> |
| | | <view class="info-value">{{formData.result}}</view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="formData.result != null" class="result-preview"> |
| | | <view class="info-label">预览结果</view> |
| | | <view class="info-value">{{formData.result}}</view> |
| | | </view> |
| | | |
| | | <view v-if="formData.remarks != null" class="result-ng"> |
| | | <view class="info-label">不合格描述</view> |
| | | <view class="info-value danger">{{formData.remarks}}</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 结果录入 --> |
| | | <view class="section"> |
| | | <view class="section-header">检验结果录入</view> |
| | | <view class="section-body"> |
| | | <!-- 验证提示 --> |
| | | <view v-if="isUnqualifiedResult()" class="validation-hint"> |
| | | <view class="validation-icon">⚠️</view> |
| | | <view class="validation-text"> |
| | | <view class="validation-title">检验项目不合格,必须完成以下操作:</view> |
| | | <view class="validation-requirements"> |
| | | <view class="requirement-item" :class="{ 'completed': hasImage() }"> |
| | | <view class="requirement-icon">{{ hasImage() ? '✅' : '❌' }}</view> |
| | | <view class="requirement-text">上传图片</view> |
| | | </view> |
| | | <view class="requirement-item" :class="{ 'completed': hasRemarks() }"> |
| | | <view class="requirement-icon">{{ hasRemarks() ? '✅' : '❌' }}</view> |
| | | <view class="requirement-text">填写不良描述</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="input-group"> |
| | | <view class="input-wrapper"> |
| | | <button class="btn upload-btn" @tap="saveRemarks"> |
| | | <uni-icons type="compose" size="16" color="#fff"></uni-icons> |
| | | 不合格描述 |
| | | </button> |
| | | |
| | | <button class="btn attachment-btn" @tap="viewAttachmentInfo"> |
| | | <uni-icons type="folder" size="16" color="#fff"></uni-icons> |
| | | 查看附件信息 |
| | | </button> |
| | | |
| | | <input v-if="!isAllCompleted" |
| | | v-model="formData.fcheckResu" |
| | | type="text" |
| | | class="result-input" |
| | | placeholder="没有最大值和最小值时填写0(未通过检验)或1(通过检验)" |
| | | placeholder-class="placeholder" /> |
| | | <button v-if="!isAllCompleted" |
| | | style="margin: 0px;background-color: #3498db;color:#ffffff ;" class="btn primary-btn" |
| | | @tap="submit">保存结果</button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="plus-button"> |
| | | <button type="warn" @click="saveRemarks">添加不合格描述</button> |
| | | <!-- 操作按钮 --> |
| | | <view class="action-buttons"> |
| | | <button class="action-btn success tablet-upload-btn" @click="uploadImage"> |
| | | <view class="btn-content"> |
| | | <view class="btn-icon">📷</view> |
| | | <view class="btn-text">拍照/上传图片</view> |
| | | </view> |
| | | </button> |
| | | </view> |
| | | |
| | | <!-- 结果表格 --> |
| | | <view v-if="formData.levelNum > 0" class="table-container"> |
| | | <view class="table-header"> |
| | | <view class="th">编号</view> |
| | | <view class="th">穴号</view> |
| | | <view class="th">记录值</view> |
| | | <view class="th">检验结果<i style="color: rgb(0 212 68);" |
| | | v-if="isAllCompleted">(输入已完成)</i></view> |
| | | <view class="th">操作</view> |
| | | </view> |
| | | |
| | | <view v-for="(item, index) in displayTableData" :key="index" class="table-row"> |
| | | <view class="td">{{ index + 1 }}</view> |
| | | <view class="td">{{ getHoleNumber(index) }}</view> |
| | | <view class="td"> |
| | | <view class="record-value">{{ getRecordValue(item, index) }}</view> |
| | | </view> |
| | | <view class="td"> |
| | | <view :class="['result-badge', getResultClass(item.fcheckResu, item.fstand, index)]"> |
| | | {{ getResultText(item.fcheckResu, item.fstand, index) }} |
| | | </view> |
| | | </view> |
| | | <view class="td"> |
| | | <!-- 修改按钮 - 已填写后显示 --> |
| | | <button v-if="!isHoleBlocked(index) && item.fcheckResu && !isNumber" class="btn danger-btn" @tap="numberEdit(item)"> |
| | | {{ editResult(item.fcheckResu) }} |
| | | </button> |
| | | <button v-if="!isHoleBlocked(index) && item.fcheckResu && isNumber" class="btn danger-btn" @tap="toDetail(item)"> |
| | | 修改 |
| | | </button> |
| | | <!-- 状态显示 --> |
| | | <span v-if="isHoleBlocked(index)" class="blocked-text">已堵穴</span> |
| | | <span v-else-if="!item.fcheckResu" class="ready-text">可填写</span> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 图片预览 --> |
| | | <view v-if="isShowImg" class="section"> |
| | | <view class="section-header"> |
| | | <view class="section-title">相关图片</view> |
| | | <view class="section-actions"> |
| | | <button class="delete-image-btn" @click="deleteImage">删除图片</button> |
| | | </view> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="image-preview" @click="previewImage"> |
| | | <image :src="base64Image" mode="aspectFit" class="preview-image"/> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | |
| | | <!-- 修改不合格描述弹出框 --> |
| | | <view v-if="remarksPopup" class="overlay"> |
| | | <view class="popup"> |
| | | <h3>修改不合格描述</h3> |
| | | <form> |
| | | <view class="form-group"> |
| | | <label class="form-label">不合格描述:</label> |
| | | <input class="form-input" type="text" v-model="remarks"/> |
| | | <view class="popup-header"> |
| | | <h3>修改不合格描述</h3> |
| | | </view> |
| | | <view class="popup-content"> |
| | | <view class="input-group"> |
| | | <view class="input-label">不合格描述:</view> |
| | | <input class="input-field" type="text" v-model="remarks" placeholder="请输入不合格描述"/> |
| | | </view> |
| | | <button type="warn" @click="editRemarks">修改</button> |
| | | <button @click="remarksPopup = !remarksPopup">取消</button> |
| | | </form> |
| | | </view> |
| | | <view class="popup-actions"> |
| | | <button class="action-btn primary" @click="editRemarks">修改</button> |
| | | <button class="action-btn secondary" @click="remarksPopup = !remarksPopup">取消</button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | |
| | | <!-- 修改检验结果弹出框 --> |
| | | <view v-if="showPopup" class="overlay"> |
| | | <view class="popup"> |
| | | <h3>修改检验结果</h3> |
| | | <form :modelValue="editData"> |
| | | <view class="form-group"> |
| | | <label class="form-label">检验结果:</label> |
| | | <input class="form-input" type="text" v-model="editData.fcheckResu"/> |
| | | <view class="popup-header"> |
| | | <h3>修改检验结果</h3> |
| | | </view> |
| | | <view class="popup-content"> |
| | | <view class="input-group"> |
| | | <view class="input-label">检验结果:</view> |
| | | <input class="input-field" type="text" v-model="editData.fcheckResu" placeholder="请输入检验结果"/> |
| | | </view> |
| | | <button type="warn" @click="eidt">修改</button> |
| | | <button @click="showPopup = !showPopup">取消</button> |
| | | </form> |
| | | </view> |
| | | <view class="popup-actions"> |
| | | <button class="action-btn primary" @click="eidt">修改</button> |
| | | <button class="action-btn secondary" @click="showPopup = !showPopup">取消</button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 附件列表弹窗 --> |
| | | <view v-if="showAttachmentPopup" class="overlay"> |
| | | <view class="popup attachment-list-popup"> |
| | | <div class="attachment-popup-header"> |
| | | <h3 class="attachment-popup-title">附件列表</h3> |
| | | <button class="attachment-close-btn" @click="closeAttachmentPopup">关闭</button> |
| | | </div> |
| | | |
| | | <div class="attachment-popup-content"> |
| | | <div v-if="attachmentsLoading" class="attachment-loading"> |
| | | <div class="loading-spinner"></div> |
| | | <span class="loading-text">正在加载附件...</span> |
| | | </div> |
| | | |
| | | <div v-else-if="attachments.length === 0" class="attachment-empty"> |
| | | <div class="empty-icon">📁</div> |
| | | <div class="empty-text">暂无附件</div> |
| | | <div class="empty-hint">该物料暂未上传任何附件</div> |
| | | </div> |
| | | |
| | | <div v-else class="attachment-list"> |
| | | <div v-for="item in attachments" :key="item.id" class="attachment-item"> |
| | | <div class="attachment-info"> |
| | | <div class="file-type-badge" :class="getFileTypeClass(item.fattach)"> |
| | | {{ getFileTypeIcon(item.fattach) }} |
| | | </div> |
| | | <div class="attachment-details"> |
| | | <div class="attachment-name" @click="showAttachmentDetailDialog(item)"> |
| | | {{ item.fattach }} |
| | | </div> |
| | | <div class="attachment-meta"> |
| | | <span class="meta-type">{{ item.ftype || '未知类型' }}</span> |
| | | <span v-if="item.fversion" class="meta-version">v{{ item.fversion }}</span> |
| | | <span v-if="item.fdate" class="meta-date">{{ formatDate(item.fdate) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="attachment-actions"> |
| | | <button class="btn-secondary" @click="showAttachmentDetailDialog(item)">详情</button> |
| | | <button v-if="isPreviewable(item.fattach)" |
| | | class="btn-primary" |
| | | @click="previewFtpFile(item)">预览</button> |
| | | <button class="btn-success" @click="downloadAttachment(item)">下载</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 附件详情弹窗 --> |
| | | <view v-if="showAttachmentDetail" class="overlay"> |
| | | <view class="popup attachment-detail-popup"> |
| | | <div class="attachment-popup-header"> |
| | | <h3 class="attachment-popup-title">附件详情</h3> |
| | | <button class="attachment-close-btn" @click="closeAttachmentDetail">返回</button> |
| | | </div> |
| | | |
| | | <div class="attachment-popup-content"> |
| | | <div v-if="selectedAttachment" class="attachment-detail-content"> |
| | | <div class="attachment-detail-header"> |
| | | <div class="file-type-badge large" :class="getFileTypeClass(selectedAttachment.fattach)"> |
| | | {{ getFileTypeIcon(selectedAttachment.fattach) }} |
| | | </div> |
| | | <div class="attachment-detail-title"> |
| | | {{ selectedAttachment.fattach }} |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="attachment-detail-info"> |
| | | <div class="info-row"> |
| | | <div class="info-item"> |
| | | <text class="info-label">ID</text> |
| | | <text class="info-content">{{ Math.trunc(selectedAttachment.id) }}</text> |
| | | </div> |
| | | <div class="info-item"> |
| | | <text class="info-label">类型</text> |
| | | <text class="info-content">{{ selectedAttachment.ftype || '未知类型' }}</text> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="info-row" v-if="selectedAttachment.fversion"> |
| | | <div class="info-item"> |
| | | <text class="info-label">版本</text> |
| | | <text class="info-content">{{ selectedAttachment.fversion }}</text> |
| | | </div> |
| | | <div class="info-item" v-if="selectedAttachment.fdate"> |
| | | <text class="info-label">受控日期</text> |
| | | <text class="info-content">{{ formatDate(selectedAttachment.fdate) }}</text> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="info-row" v-if="selectedAttachment.createBy"> |
| | | <div class="info-item"> |
| | | <text class="info-label">上传人</text> |
| | | <text class="info-content">{{ selectedAttachment.createBy }}</text> |
| | | </div> |
| | | <div class="info-item" v-if="selectedAttachment.createDate"> |
| | | <text class="info-label">上传时间</text> |
| | | <text class="info-content">{{ formatDate(selectedAttachment.createDate) }}</text> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="attachment-detail-actions"> |
| | | <button v-if="isPreviewable(selectedAttachment.fattach)" |
| | | class="btn-primary" |
| | | @click="previewFtpFile(selectedAttachment)">预览</button> |
| | | <button class="btn-success" @click="downloadAttachment(selectedAttachment)">下载</button> |
| | | </div> |
| | | </div> |
| | | <div v-else class="attachment-detail-empty"> |
| | | <div class="empty-icon">❌</div> |
| | | <div class="empty-text">暂无附件信息</div> |
| | | </div> |
| | | </div> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 文件预览弹窗 --> |
| | | <view v-if="showFilePreviewPopup" class="overlay"> |
| | | <view class="popup file-preview-popup"> |
| | | <h3 class="file-preview-title">{{ previewTitle }}</h3> |
| | | <div class="file-preview-divider"></div> |
| | | <div class="file-preview-content"> |
| | | <!-- 文本内容预览 --> |
| | | <pre v-if="previewType === 'text'">{{ previewContent }}</pre> |
| | | |
| | | <!-- 图片内容预览 --> |
| | | <view v-else-if="previewType === 'image'" class="image-preview-container"> |
| | | <image |
| | | :src="previewContent" |
| | | mode="aspectFit" |
| | | class="preview-image-clickable" |
| | | @click="previewImageInPopup" |
| | | style="width: 100%; max-height: 400px; cursor: pointer;" |
| | | /> |
| | | <div class="image-zoom-hint">点击图片可放大查看</div> |
| | | </view> |
| | | |
| | | <!-- Excel 等 Office 文件提示 --> |
| | | <view v-else-if="previewType === 'excel'" class="unsupported-preview"> |
| | | <view class="unsupported-icon">📊</view> |
| | | <view class="unsupported-text">Excel 文件暂不支持在线预览</view> |
| | | <view class="unsupported-hint">请点击下载按钮获取完整文件</view> |
| | | </view> |
| | | |
| | | <!-- 不支持的文件类型 --> |
| | | <view v-else class="unsupported-preview"> |
| | | <view class="unsupported-icon">📄</view> |
| | | <view class="unsupported-text">此文件格式暂不支持预览</view> |
| | | <view class="unsupported-hint">请点击下载按钮获取完整文件</view> |
| | | </view> |
| | | </div> |
| | | <div class="file-preview-actions"> |
| | | <button v-if="previewType !== 'text'" class="file-preview-btn download-btn" @click="downloadPreviewFile">📥 下载文件</button> |
| | | <button class="file-preview-btn close-btn" @click="closeFilePreview">关闭</button> |
| | | </div> |
| | | </view> |
| | | </view> |
| | | |
| | | </view> |
| | | </template> |
| | | |
| | | |
| | | <script> |
| | | export default { |
| | | data() { |
| | |
| | | isShowImg: false, |
| | | remarks: "", |
| | | remarksPopup: false, |
| | | // ===== 新增LLJ样式相关数据 ===== |
| | | currentTab: 0, |
| | | tabs: [], |
| | | // ===== 附件相关数据 ===== |
| | | showAttachmentPopup: false, |
| | | showAttachmentDetail: false, |
| | | showFilePreviewPopup: false, |
| | | attachments: [], |
| | | attachmentsLoading: false, |
| | | selectedAttachment: null, |
| | | previewTitle: '', |
| | | previewContent: '', |
| | | previewType: '', |
| | | previewFileUrl: '' |
| | | } |
| | | }, |
| | | computed: { |
| | | // 根据穴号信息生成完整的显示数据 |
| | | displayTableData() { |
| | | const result = []; |
| | | |
| | | console.log('displayTableData - formData:', this.formData); |
| | | console.log('displayTableData - tableData:', this.tableData); |
| | | |
| | | // 如果有穴号信息,使用穴号信息生成数据 |
| | | if (this.formData.holeNumbers && this.formData.holeNumbers.length > 0) { |
| | | // 对数据库记录按创建时间排序,确保顺序稳定 |
| | | const sortedTableData = [...this.tableData].sort((a, b) => { |
| | | if (a.id && b.id) return a.id - b.id; |
| | | if (a.createDate && b.createDate) return new Date(a.createDate) - new Date(b.createDate); |
| | | return 0; |
| | | }); |
| | | |
| | | console.log('displayTableData - sortedTableData:', sortedTableData); |
| | | |
| | | this.formData.holeNumbers.forEach((holeInfo, index) => { |
| | | // 直接通过索引获取对应的数据库记录 |
| | | const actualData = sortedTableData[index]; |
| | | |
| | | if (actualData) { |
| | | // 如果有实际数据,使用实际数据 |
| | | result.push({ |
| | | ...actualData, |
| | | index: index, |
| | | holeInfo: holeInfo |
| | | }); |
| | | } else { |
| | | // 如果没有实际数据,创建空记录 |
| | | result.push({ |
| | | id: null, |
| | | fcheckResu: holeInfo.isBlocked ? "/" : null, |
| | | fstand: holeInfo.isBlocked ? "/" : null, |
| | | index: index, |
| | | pid: this.id, |
| | | gid: this.gid, |
| | | holeInfo: holeInfo |
| | | }); |
| | | } |
| | | }); |
| | | } else { |
| | | // 如果没有穴号信息,使用原来的逻辑 |
| | | const levelNum = this.formData.levelNum || 0; |
| | | |
| | | // 按照创建时间或ID排序,确保顺序稳定 |
| | | const sortedTableData = [...this.tableData].sort((a, b) => { |
| | | if (a.id && b.id) return a.id - b.id; |
| | | if (a.createDate && b.createDate) return new Date(a.createDate) - new Date(b.createDate); |
| | | return 0; |
| | | }); |
| | | |
| | | console.log('displayTableData - sortedTableData (no holes):', sortedTableData); |
| | | |
| | | for (let i = 0; i < levelNum; i++) { |
| | | const actualData = sortedTableData[i]; |
| | | |
| | | if (actualData) { |
| | | result.push({ |
| | | ...actualData, |
| | | index: i |
| | | }); |
| | | } else { |
| | | result.push({ |
| | | id: null, |
| | | fcheckResu: null, |
| | | fstand: null, |
| | | index: i, |
| | | pid: this.id, |
| | | gid: this.gid |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | console.log('displayTableData - result:', result); |
| | | return result; |
| | | }, |
| | | |
| | | // 判断是否所有检验都已完成 |
| | | isAllCompleted() { |
| | | // 只检查非堵穴的行是否都已填写 |
| | | return this.displayTableData.every(item => { |
| | | // 如果是堵穴,跳过检查 |
| | | if (item.holeInfo && item.holeInfo.isBlocked) { |
| | | return true; |
| | | } |
| | | // 非堵穴的行必须有检验结果 |
| | | return item.fcheckResu !== null && item.fcheckResu !== undefined; |
| | | }); |
| | | } |
| | | }, |
| | | methods: { |
| | | // 判断当前检验结果是否为不合格 |
| | | isUnqualifiedResult() { |
| | | if (!this.formData.fcheckResu) return false; |
| | | |
| | | // 如果有最大值和最小值,检查是否超出范围 |
| | | if (this.formData.maxValue && this.formData.minValue) { |
| | | return this.formData.fcheckResu < this.formData.minValue || |
| | | this.formData.fcheckResu > this.formData.maxValue; |
| | | } |
| | | |
| | | // 如果没有最大值和最小值,检查是否为0(不合格) |
| | | return this.formData.fcheckResu == 0; |
| | | }, |
| | | |
| | | // 判断是否已上传图片 |
| | | hasImage() { |
| | | return this.formData.imageData && this.formData.imageData.length > 0; |
| | | }, |
| | | |
| | | // 判断是否已填写不良描述 |
| | | hasRemarks() { |
| | | return this.formData.remarks && this.formData.remarks.trim() !== ''; |
| | | }, |
| | | |
| | | getResultClass(fcheckResu, fstand, index) { |
| | | // 检查是否被堵穴 |
| | | if (this.isHoleBlocked(index)) { |
| | | return 'BLOCKED'; |
| | | } |
| | | |
| | | // 如果没有记录值,显示未检验样式 |
| | | if (!fcheckResu) { |
| | | return 'PENDING'; |
| | | } |
| | | |
| | | // 优先根据fstand判断 |
| | | if (fstand === '√') { |
| | | return 'OK'; |
| | | } else if (fstand === '×') { |
| | | return 'NG'; |
| | | } else { |
| | | // 如果没有fstand,根据fcheckResu的值判断 |
| | | if (fcheckResu === '1' || fcheckResu === 1) { |
| | | return 'OK'; |
| | | } else if (fcheckResu === '0' || fcheckResu === 0) { |
| | | return 'NG'; |
| | | } else { |
| | | // 对于有上下限的数值检验,根据fcheckResu是否在范围内判断 |
| | | if (this.formData.maxValue && this.formData.minValue) { |
| | | const value = parseFloat(fcheckResu); |
| | | if (!isNaN(value)) { |
| | | if (value >= this.formData.minValue && value <= this.formData.maxValue) { |
| | | return 'OK'; |
| | | } else { |
| | | return 'NG'; |
| | | } |
| | | } |
| | | } |
| | | // 默认返回OK样式 |
| | | return 'OK'; |
| | | } |
| | | } |
| | | }, |
| | | |
| | | getResultText(fcheckResu, fstand, index) { |
| | | // 检查是否被堵穴 |
| | | if (this.isHoleBlocked(index)) { |
| | | return '/'; |
| | | } |
| | | |
| | | // 如果没有记录值,显示未检验 |
| | | if (!fcheckResu) { |
| | | return '未检验'; |
| | | } |
| | | |
| | | // 优先根据fstand判断 |
| | | if (fstand === '√') { |
| | | return '合格'; |
| | | } else if (fstand === '×') { |
| | | return '不合格'; |
| | | } else { |
| | | // 如果没有fstand,根据fcheckResu的值判断 |
| | | if (fcheckResu === '1' || fcheckResu === 1) { |
| | | return '合格'; |
| | | } else if (fcheckResu === '0' || fcheckResu === 0) { |
| | | return '不合格'; |
| | | } else { |
| | | // 对于有上下限的数值检验,根据fcheckResu是否在范围内判断 |
| | | if (this.formData.maxValue && this.formData.minValue) { |
| | | const value = parseFloat(fcheckResu); |
| | | if (!isNaN(value)) { |
| | | if (value >= this.formData.minValue && value <= this.formData.maxValue) { |
| | | return '合格'; |
| | | } else { |
| | | return '不合格'; |
| | | } |
| | | } |
| | | } |
| | | // 如果是有数值的检验结果,直接显示数值 |
| | | return fcheckResu || '未检测'; |
| | | } |
| | | } |
| | | }, |
| | | |
| | | // ===== 新增标签切换方法 ===== |
| | | switchTab(index, id) { |
| | | this.currentTab = index; |
| | | // 如果需要切换检验项目,可以在这里添加逻辑 |
| | | // this.gid = id; |
| | | // this.refreshResult(); |
| | | }, |
| | | |
| | | // ===== 穴号相关方法 ===== |
| | | getHoleNumber(index) { |
| | | const item = this.displayTableData[index]; |
| | | if (item && item.holeInfo) { |
| | | return item.holeInfo.holeNumber; |
| | | } |
| | | return index + 1; |
| | | }, |
| | | |
| | | getRecordValue(item, index) { |
| | | if (this.isHoleBlocked(index)) { |
| | | return '/'; |
| | | } |
| | | return item.fcheckResu || '无'; |
| | | }, |
| | | |
| | | isHoleBlocked(index) { |
| | | const item = this.displayTableData[index]; |
| | | if (item && item.holeInfo) { |
| | | return item.holeInfo.isBlocked; |
| | | } |
| | | return false; |
| | | }, |
| | | |
| | | |
| | | previewImage() { |
| | | uni.previewImage({ |
| | | urls: [this.base64Image], |
| | |
| | | } |
| | | }, |
| | | submit() { |
| | | |
| | | |
| | | let count = this.formData.levelNum; |
| | | let fstand = "√"; |
| | | |
| | | |
| | | //有最大值和最小值就根据是否符合标准值更新判定结果,没有最大值和最小值就根据是否通过检验判定结果 |
| | | if (this.formData.maxValue && this.formData.minValue) { |
| | | |
| | | |
| | | if (!this.formData.fcheckResu) { |
| | | this.$showMessage("请输入检验值"); |
| | | return; |
| | | } |
| | | |
| | | |
| | | if (this.formData.fcheckResu >= this.formData.minValue && this.formData.fcheckResu <= this.formData.maxValue) { |
| | | fstand = "√" |
| | | } else { |
| | |
| | | } |
| | | count = 1; |
| | | } else { |
| | | |
| | | |
| | | if (!this.formData.fcheckResu) { |
| | | this.formData.fcheckResu = 1 |
| | | } |
| | | |
| | | |
| | | if (this.formData.fcheckResu == 0 || this.formData.fcheckResu == 1) { |
| | | this.formData.isPass = this.formData.fcheckResu |
| | | // 根据fcheckResu设置fstand |
| | | if (this.formData.fcheckResu == 1) { |
| | | fstand = "√"; // 合格 |
| | | } else { |
| | | fstand = "×"; // 不合格 |
| | | } |
| | | } else { |
| | | this.$showMessage("无标准值时,检验结果只能为0或1!"); |
| | | return; |
| | | } |
| | | count = count - this.tableData.length; |
| | | |
| | | // 计算实际需要填写的数量(考虑穴号信息) |
| | | if (this.formData.holeNumbers && this.formData.holeNumbers.length > 0) { |
| | | // 如果有穴号信息,计算非堵穴的数量 |
| | | const nonBlockedCount = this.formData.holeNumbers.filter(hole => !hole.isBlocked).length; |
| | | count = nonBlockedCount; |
| | | } else { |
| | | // 否则使用levelNum |
| | | count = this.formData.levelNum || 1; |
| | | } |
| | | } |
| | | |
| | | // 验证不合格检验项目必须上传图片并填写不良描述 |
| | | if (fstand === "×" || this.formData.fcheckResu == 0) { |
| | | // 检查是否已上传图片 |
| | | if (!this.formData.imageData || this.formData.imageData.length === 0) { |
| | | this.$showMessage("检验项目不合格,必须上传图片!"); |
| | | return; |
| | | } |
| | | |
| | | // 检查是否已填写不良描述 |
| | | if (!this.formData.remarks || this.formData.remarks.trim() === '') { |
| | | this.$showMessage("检验项目不合格,必须填写不良描述!"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | this.formData.updater = this.$loginInfo.account; |
| | | |
| | | // 直接执行保存,不显示确认弹窗 |
| | | this.$post({ |
| | | url: "/SJ/SetQSItemDetail", |
| | | data: { |
| | | pid: this.id, |
| | | gid: this.gid, |
| | | fstand: fstand, |
| | | fcheckResu: this.formData.fcheckResu, |
| | | updateBy: this.formData.updater, |
| | | Fstand: fstand, |
| | | FcheckResu: this.formData.fcheckResu, |
| | | UpdateBy: this.formData.updater, |
| | | count: count |
| | | } |
| | | }).then(res => { |
| | |
| | | this.$showMessage("保存成功"); |
| | | this.refreshResult(); |
| | | }) |
| | | |
| | | |
| | | }, |
| | | refreshResult() { |
| | | // 先获取检验项目基本信息 |
| | | this.$post({ |
| | | url: "/SJ/getQSItems", |
| | | data: { |
| | | pid: this.gid, |
| | | id: this.id |
| | | } |
| | | }).then(res => { |
| | | this.formData = res.data.tbBillList[0]; |
| | | this.formData.billNo = this.billNo; |
| | | |
| | | console.log("获取到的 formData:", this.formData); |
| | | console.log("itemNo 值:", this.formData.itemNo); |
| | | |
| | | if (this.formData.imageData) { |
| | | this.isShowImg = true; |
| | | this.base64Image = 'data:image/jpeg;base64,' + this.formData.imageData; |
| | | } else { |
| | | this.isShowImg = false; |
| | | this.base64Image = ''; |
| | | } |
| | | if (this.formData.maxValue && this.formData.minValue && this.formData.standardValue) { |
| | | this.isNumber = true; |
| | | } |
| | | }) |
| | | |
| | | this.$post({ |
| | | url: "/SJ/getQSItemDetail", |
| | | data: { |
| | | pid: this.id, |
| | | gid: this.gid |
| | | } |
| | | |
| | | // 然后获取检验详情数据 |
| | | return this.$post({ |
| | | url: "/SJ/getQSItemDetail", |
| | | data: { |
| | | pid: this.id, |
| | | gid: this.gid |
| | | } |
| | | }); |
| | | }).then(res => { |
| | | this.tableData = res.data.tbBillList; |
| | | }) |
| | | console.log('刷新后的tableData:', this.tableData); |
| | | }).catch(error => { |
| | | console.error('刷新数据失败:', error); |
| | | }); |
| | | }, |
| | | toDetail(item) { |
| | | this.showPopup = !this.showPopup; |
| | | this.editData = item; |
| | | }, |
| | | eidt() { |
| | | |
| | | |
| | | if (!this.editData.fcheckResu) { |
| | | this.$showMessage("请输入检验结果"); |
| | | } |
| | | |
| | | |
| | | if (this.formData.fcheckResu == this.editData.fcheckResu) { |
| | | this.$showMessage("修改成功"); |
| | | return; |
| | | } |
| | | |
| | | |
| | | let fstand = "√"; |
| | | |
| | | |
| | | if (this.formData.maxValue && this.formData.minValue) { |
| | | |
| | | |
| | | if (!this.editData.fcheckResu) { |
| | | this.$showMessage("请输入检验值"); |
| | | return; |
| | | } |
| | | |
| | | |
| | | if (this.editData.fcheckResu >= this.formData.minValue && this.editData.fcheckResu <= this.formData.maxValue) { |
| | | this.editData.isPass = 1 |
| | | } else { |
| | |
| | | fstand = "×"; |
| | | } |
| | | } else { |
| | | |
| | | |
| | | if (!this.editData.fcheckResu) { |
| | | this.editData.fcheckResu = 1 |
| | | } |
| | | |
| | | |
| | | if (this.editData.fcheckResu == 0 || this.editData.fcheckResu == 1) { |
| | | if (this.editData.fcheckResu == 0) { |
| | | fstand = "×"; |
| | |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 验证不合格检验项目必须上传图片并填写不良描述 |
| | | if (fstand === "×" || this.editData.fcheckResu == 0) { |
| | | // 检查是否已上传图片 |
| | | if (!this.formData.imageData || this.formData.imageData.length === 0) { |
| | | this.$showMessage("检验项目不合格,必须上传图片!"); |
| | | return; |
| | | } |
| | | |
| | | // 检查是否已填写不良描述 |
| | | if (!this.formData.remarks || this.formData.remarks.trim() === '') { |
| | | this.$showMessage("检验项目不合格,必须填写不良描述!"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | this.editData.updater = this.$loginInfo.account; |
| | | |
| | | this.$post({ |
| | | url: "/SJ/UpdateQSItemDetail", |
| | | data: { |
| | | id: this.editData.id, |
| | | pid: this.id, |
| | | gid: this.gid, |
| | | fstand: fstand, |
| | | fcheckResu: this.editData.fcheckResu, |
| | | updateBy: this.editData.updater, |
| | | // 显示确认提示框 |
| | | uni.showModal({ |
| | | title: '确认修改', |
| | | content: '确定要修改检验结果吗?', |
| | | confirmText: '确定修改', |
| | | cancelText: '取消', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | // 用户确认后执行修改 |
| | | this.$post({ |
| | | url: "/SJ/UpdateQSItemDetail", |
| | | data: { |
| | | id: this.editData.id, |
| | | pid: this.id, |
| | | gid: this.gid, |
| | | fstand: fstand, |
| | | fcheckResu: this.editData.fcheckResu, |
| | | updateBy: this.editData.updater, |
| | | } |
| | | }).then(res => { |
| | | this.showPopup = !this.showPopup; |
| | | this.$showMessage("修改成功"); |
| | | this.refreshResult();//刷新页面 |
| | | }) |
| | | } |
| | | } |
| | | }).then(res => { |
| | | this.showPopup = !this.showPopup; |
| | | this.$showMessage("修改成功"); |
| | | this.refreshResult();//刷新页面 |
| | | }) |
| | | }); |
| | | }, |
| | | numberEdit(item) { |
| | | |
| | | |
| | | let fstand = "√"; |
| | | let fcheckResu = 1; |
| | | |
| | | |
| | | if (item.fcheckResu == '1') { |
| | | fstand = "×"; |
| | | fcheckResu = 0; |
| | | } |
| | | |
| | | // 验证不合格检验项目必须上传图片并填写不良描述 |
| | | if (fstand === "×" || fcheckResu == 0) { |
| | | // 检查是否已上传图片 |
| | | if (!this.formData.imageData || this.formData.imageData.length === 0) { |
| | | this.$showMessage("检验项目不合格,必须上传图片!"); |
| | | return; |
| | | } |
| | | |
| | | // 检查是否已填写不良描述 |
| | | if (!this.formData.remarks || this.formData.remarks.trim() === '') { |
| | | this.$showMessage("检验项目不合格,必须填写不良描述!"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | this.$post({ |
| | |
| | | } |
| | | }) |
| | | } |
| | | }, |
| | | |
| | | // ===== 附件相关方法 ===== |
| | | viewAttachmentInfo() { |
| | | this.showAttachmentPopup = true; |
| | | this.attachmentsLoading = true; |
| | | this.attachments = []; |
| | | |
| | | // 先获取物料编码,然后获取附件 |
| | | this.$post({ |
| | | url: "/SJ/GetPage", |
| | | data: { |
| | | pageIndex: 1, |
| | | limit: 1, |
| | | id: this.gid |
| | | } |
| | | }).then(res => { |
| | | let tbBillListElement = res.data.tbBillList[0]; |
| | | if (tbBillListElement && tbBillListElement.itemNo) { |
| | | console.log("获取到的物料编码:", tbBillListElement.itemNo); |
| | | |
| | | // 使用获取到的物料编码和项目名称调用附件接口 |
| | | return this.$post({ |
| | | url: "/SJ/getAttachments", |
| | | data: { |
| | | itemNo: tbBillListElement.itemNo, |
| | | projName: this.formData.projName, // 传递项目名称 |
| | | fromPage: 'Detail' // 标识来自Detail页面,需要过滤 |
| | | } |
| | | }); |
| | | } else { |
| | | throw new Error("未找到物料编码信息"); |
| | | } |
| | | }).then(res => { |
| | | this.attachmentsLoading = false; |
| | | if (res.status === 0) { |
| | | this.attachments = res.data.tbBillList; |
| | | // 为每个附件设置默认可用状态 |
| | | this.attachments.forEach((item, index) => { |
| | | this.$set(item, 'ftpAvailable', true); |
| | | this.$set(item, 'checking', false); |
| | | }); |
| | | } else if (res.status === 1 && res.message === "该检验单未上传附件信息!") { |
| | | uni.showToast({ title: res.message, icon: "none" }); |
| | | } else { |
| | | uni.showToast({ title: "获取附件失败", icon: "none" }); |
| | | } |
| | | }).catch(err => { |
| | | this.attachmentsLoading = false; |
| | | console.error("获取附件失败:", err); |
| | | uni.showToast({ title: "获取附件失败,请重试", icon: "none" }); |
| | | }); |
| | | }, |
| | | |
| | | closeAttachmentPopup() { |
| | | this.showAttachmentPopup = false; |
| | | }, |
| | | |
| | | showAttachmentDetailDialog(item) { |
| | | this.selectedAttachment = item; |
| | | this.showAttachmentPopup = false; |
| | | this.showAttachmentDetail = true; |
| | | }, |
| | | |
| | | closeAttachmentDetail() { |
| | | this.showAttachmentDetail = false; |
| | | this.selectedAttachment = null; |
| | | this.showAttachmentPopup = true; |
| | | }, |
| | | |
| | | isPreviewable(filename) { |
| | | if (!filename) return false; |
| | | const ext = filename.trim().split('.').pop().toLowerCase(); |
| | | return [ |
| | | 'pdf', |
| | | 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', |
| | | 'txt', 'log', 'md', |
| | | 'doc', 'docx', |
| | | 'xls', 'xlsx', |
| | | 'ppt', 'pptx', |
| | | 'csv' |
| | | ].includes(ext); |
| | | }, |
| | | |
| | | // 获取文件类型图标 |
| | | getFileTypeIcon(filename) { |
| | | if (!filename) return '📄'; |
| | | const ext = filename.trim().split('.').pop().toLowerCase(); |
| | | const iconMap = { |
| | | 'pdf': '📕', |
| | | 'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'bmp': '🖼️', 'webp': '🖼️', |
| | | 'txt': '📝', 'log': '📝', 'md': '📝', |
| | | 'doc': '📘', 'docx': '📘', |
| | | 'xls': '📊', 'xlsx': '📊', |
| | | 'ppt': '📙', 'pptx': '📙', |
| | | 'csv': '📊', |
| | | 'zip': '📦', 'rar': '📦', '7z': '📦', |
| | | 'dwg': '🏗️', 'dxf': '🏗️' |
| | | }; |
| | | return iconMap[ext] || '📄'; |
| | | }, |
| | | |
| | | // 获取文件类型CSS类 |
| | | getFileTypeClass(filename) { |
| | | if (!filename) return 'file-unknown'; |
| | | const ext = filename.trim().split('.').pop().toLowerCase(); |
| | | const classMap = { |
| | | 'pdf': 'file-pdf', |
| | | 'jpg': 'file-image', 'jpeg': 'file-image', 'png': 'file-image', 'gif': 'file-image', 'bmp': 'file-image', 'webp': 'file-image', |
| | | 'txt': 'file-text', 'log': 'file-text', 'md': 'file-text', |
| | | 'doc': 'file-word', 'docx': 'file-word', |
| | | 'xls': 'file-excel', 'xlsx': 'file-excel', |
| | | 'ppt': 'file-powerpoint', 'pptx': 'file-powerpoint', |
| | | 'csv': 'file-excel', |
| | | 'zip': 'file-archive', 'rar': 'file-archive', '7z': 'file-archive', |
| | | 'dwg': 'file-cad', 'dxf': 'file-cad' |
| | | }; |
| | | return classMap[ext] || 'file-unknown'; |
| | | }, |
| | | |
| | | // 格式化日期 |
| | | formatDate(dateString) { |
| | | if (!dateString) return ''; |
| | | try { |
| | | const date = new Date(dateString); |
| | | return date.toLocaleDateString('zh-CN', { |
| | | year: 'numeric', |
| | | month: '2-digit', |
| | | day: '2-digit', |
| | | hour: '2-digit', |
| | | minute: '2-digit' |
| | | }); |
| | | } catch (e) { |
| | | return dateString; |
| | | } |
| | | }, |
| | | |
| | | downloadAttachment(item) { |
| | | const fileName = item.fattach.replace(/[\s\u3000\r\n]+/g, '').trim(); |
| | | const downloadUrl = this.$store.state.serverInfo.serverAPI + "/SJ/DownloadFtpFile?itemNo=" + |
| | | encodeURIComponent(item.itemNo) + "&fileName=" + encodeURIComponent(fileName) + |
| | | "&ftpServer=" + encodeURIComponent(this.$store.state.serverInfo.ftpServer) + |
| | | "&projName=" + encodeURIComponent(this.formData.projName || ''); |
| | | |
| | | uni.downloadFile({ |
| | | url: downloadUrl, |
| | | success: (res) => { |
| | | if (res.statusCode === 200) { |
| | | uni.showToast({ title: '下载成功', icon: 'success' }); |
| | | } else { |
| | | uni.showToast({ title: '下载失败', icon: 'none' }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('下载失败:', err); |
| | | uni.showToast({ title: '下载失败,请重试', icon: 'none' }); |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | previewFtpFile(item) { |
| | | const fileName = item.fattach.replace(/[\s\u3000\r\n]+/g, '').trim(); |
| | | const fileExt = fileName.split('.').pop().toLowerCase(); |
| | | |
| | | if (!this.isPreviewable(fileName)) { |
| | | uni.showModal({ |
| | | title: '不支持预览', |
| | | content: '该文件类型不支持在线预览,请下载后查看', |
| | | showCancel: false |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const previewUrl = this.$store.state.serverInfo.serverAPI + "/SJ/PreviewFtpFile?itemNo=" + |
| | | encodeURIComponent(item.itemNo) + "&fileName=" + encodeURIComponent(fileName) + |
| | | "&ftpServer=" + encodeURIComponent(this.$store.state.serverInfo.ftpServer) + |
| | | "&projName=" + encodeURIComponent(this.formData.projName || ''); |
| | | |
| | | if (['pdf'].includes(fileExt)) { |
| | | this.previewPdfFile(previewUrl, fileName); |
| | | } else if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(fileExt)) { |
| | | this.previewImageFile(previewUrl, fileName); |
| | | } else if (['txt'].includes(fileExt)) { |
| | | this.previewTextFile(previewUrl, fileName); |
| | | } else { |
| | | this.previewOfficeFile(previewUrl, fileName); |
| | | } |
| | | }, |
| | | |
| | | previewPdfFile(url, fileName) { |
| | | this.previewTitle = fileName; |
| | | this.previewContent = url; |
| | | this.previewType = 'pdf'; |
| | | this.previewFileUrl = url; |
| | | this.showFilePreviewPopup = true; |
| | | this.showAttachmentDetail = false; |
| | | }, |
| | | |
| | | previewImageFile(url, fileName) { |
| | | this.previewTitle = fileName; |
| | | this.previewContent = url; |
| | | this.previewType = 'image'; |
| | | this.previewFileUrl = url; |
| | | this.showFilePreviewPopup = true; |
| | | this.showAttachmentDetail = false; |
| | | }, |
| | | |
| | | previewTextFile(url, fileName) { |
| | | this.previewTitle = fileName; |
| | | this.previewType = 'text'; |
| | | this.previewFileUrl = url; |
| | | this.showFilePreviewPopup = true; |
| | | this.showAttachmentDetail = false; |
| | | |
| | | uni.request({ |
| | | url: url, |
| | | method: 'GET', |
| | | success: (res) => { |
| | | this.previewContent = res.data; |
| | | }, |
| | | fail: (err) => { |
| | | this.previewContent = '预览失败,请下载后查看'; |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | previewOfficeFile(url, fileName) { |
| | | this.previewTitle = fileName; |
| | | this.previewContent = ''; |
| | | this.previewType = 'excel'; |
| | | this.previewFileUrl = url; |
| | | this.showFilePreviewPopup = true; |
| | | this.showAttachmentDetail = false; |
| | | }, |
| | | |
| | | closeFilePreview() { |
| | | this.showFilePreviewPopup = false; |
| | | this.showAttachmentDetail = true; |
| | | }, |
| | | |
| | | downloadPreviewFile() { |
| | | if (this.previewFileUrl && this.selectedAttachment) { |
| | | this.downloadAttachment(this.selectedAttachment); |
| | | } |
| | | }, |
| | | |
| | | previewImageInPopup() { |
| | | // 在新窗口中打开图片进行放大查看 |
| | | if (this.previewContent) { |
| | | uni.previewImage({ |
| | | urls: [this.previewContent], |
| | | current: this.previewContent |
| | | }); |
| | | } |
| | | }, |
| | | |
| | | // 图片上传方法 |
| | | uploadImage() { |
| | | uni.showActionSheet({ |
| | | itemList: ['拍照', '从相册选择'], |
| | | success: (res) => { |
| | | if (res.tapIndex === 0) { |
| | | this.chooseImageWithSource('camera'); |
| | | } else if (res.tapIndex === 1) { |
| | | this.chooseImageWithSource('album'); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | // 选择图片来源 |
| | | chooseImageWithSource(source) { |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ['compressed'], |
| | | sourceType: [source], |
| | | success: (res) => { |
| | | const tempFilePath = res.tempFilePaths[0]; |
| | | if (source === 'camera') { |
| | | // 拍照后显示预览确认 |
| | | this.showImagePreview(tempFilePath); |
| | | } else { |
| | | // 相册选择直接上传 |
| | | this.uploadImageToServer(tempFilePath); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('选择图片失败:', err); |
| | | this.$showMessage('选择图片失败'); |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | // 显示图片预览 |
| | | showImagePreview(tempFilePath) { |
| | | uni.previewImage({ |
| | | urls: [tempFilePath], |
| | | current: tempFilePath, |
| | | success: () => { |
| | | // 预览后询问是否上传 |
| | | uni.showModal({ |
| | | title: '确认上传', |
| | | content: '是否上传此图片?', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | this.uploadImageToServer(tempFilePath); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | // 上传图片到服务器 |
| | | uploadImageToServer(tempFilePath) { |
| | | uni.showLoading({ |
| | | title: '上传中...' |
| | | }); |
| | | |
| | | uni.uploadFile({ |
| | | url: this.$store.state.serverInfo.serverAPI + '/SJ/UploadImageToPicture', |
| | | filePath: tempFilePath, |
| | | name: 'file', |
| | | formData: { |
| | | id: this.id.toString(), |
| | | gid: this.gid.toString(), |
| | | billNo: this.billNo, |
| | | createBy: this.$loginInfo.account |
| | | }, |
| | | success: (res) => { |
| | | uni.hideLoading(); |
| | | try { |
| | | const result = JSON.parse(res.data); |
| | | if (result.status === 0) { |
| | | this.$showMessage('图片上传成功'); |
| | | this.refreshResult(); // 刷新页面数据 |
| | | } else { |
| | | this.$showMessage(result.message || '上传失败'); |
| | | } |
| | | } catch (e) { |
| | | this.$showMessage('上传失败,请重试'); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | uni.hideLoading(); |
| | | console.error('上传失败:', err); |
| | | this.$showMessage('上传失败,请重试'); |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | // 删除图片 |
| | | deleteImage() { |
| | | uni.showModal({ |
| | | title: '确认删除', |
| | | content: '确定要删除此图片吗?', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | this.deleteImageFromServer(); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | // 从服务器删除图片 |
| | | deleteImageFromServer() { |
| | | uni.showLoading({ |
| | | title: '删除中...' |
| | | }); |
| | | |
| | | this.$post({ |
| | | url: '/SJ/DeleteImageFromPicture', |
| | | data: { |
| | | id: this.id |
| | | } |
| | | }).then(res => { |
| | | uni.hideLoading(); |
| | | if (res.status === 0) { |
| | | this.$showMessage('图片删除成功'); |
| | | this.refreshResult(); // 刷新页面数据 |
| | | } else { |
| | | this.$showMessage(res.message || '删除失败'); |
| | | } |
| | | }).catch(err => { |
| | | uni.hideLoading(); |
| | | console.error('删除失败:', err); |
| | | this.$showMessage('删除失败,请重试'); |
| | | }); |
| | | } |
| | | }, |
| | | onLoad(options) { |
| | | //options中包含了url附带的参数 |
| | | let params = options; |
| | | |
| | | |
| | | this.id = params["id"]; |
| | | this.billNo = params["billNo"]; |
| | | this.gid = params["gid"]; |
| | | |
| | | |
| | | this.refreshResult(); |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style> |
| | | |
| | | .form-group { |
| | | display: flex; |
| | | align-items: center; |
| | | border: 1px solid #c9c9c9; |
| | | background-color: #d4d4d4; |
| | | |
| | | <style lang="scss"> |
| | | $primary-color: #409EFF; |
| | | $success-color: #67C23A; |
| | | $danger-color: #F56C6C; |
| | | $border-color: #DCDFE6; |
| | | $bg-color: #f5f7fa; |
| | | |
| | | .container { |
| | | padding: 20px; |
| | | background-color: #fff; |
| | | } |
| | | |
| | | .form-label { |
| | | margin-bottom: 0; |
| | | padding: 5px; |
| | | } |
| | | |
| | | .form-input { |
| | | flex: 1; |
| | | margin-bottom: 0; |
| | | padding: 5px; |
| | | } |
| | | |
| | | /* 默认样式 */ |
| | | .list-container { |
| | | height: calc(100vh - 750px); |
| | | /* 设置列表容器的高度为剩余空间,并减去表单容器的高度 */ |
| | | overflow-y: auto; |
| | | /* 允许列表容器垂直滚动 */ |
| | | padding: 10px; |
| | | /* 可选:添加一些内边距,使列表内容更美观 */ |
| | | } |
| | | |
| | | /* 在小屏幕设备上,重置高度为适应屏幕 */ |
| | | @media (max-width: 768px) { |
| | | .list-container { |
| | | height: calc(100vh - 485px); |
| | | /* 适当调整高度 */ |
| | | |
| | | .header { |
| | | padding: 20px; |
| | | border-bottom: 1px solid $border-color; |
| | | background: linear-gradient(90deg, #f0f7ff, #e1f0ff); |
| | | |
| | | .title { |
| | | font-size: 24px; |
| | | color: #333; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .order-number { |
| | | color: #666; |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | |
| | | .form-container { |
| | | /* 设置表单容器的高度,使其可以滚动 */ |
| | | overflow-y: auto; |
| | | /* 允许表单容器垂直滚动 */ |
| | | padding: 10px; |
| | | /* 可选:添加一些内边距,使表单内容更美观 */ |
| | | |
| | | .tabs { |
| | | display: flex; |
| | | background-color: $bg-color; |
| | | border-bottom: 1px solid $border-color; |
| | | |
| | | .tab { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 12px 0; |
| | | border-right: 1px solid $border-color; |
| | | color: #666; |
| | | transition: all 0.3s; |
| | | |
| | | &:last-child { |
| | | border-right: none; |
| | | } |
| | | |
| | | &.active { |
| | | background-color: #fff; |
| | | color: $primary-color; |
| | | font-weight: bold; |
| | | position: relative; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | height: 2px; |
| | | background-color: $primary-color; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .section { |
| | | margin: 20px 0; |
| | | border: 1px solid $border-color; |
| | | border-radius: 4px; |
| | | |
| | | &-header { |
| | | padding: 12px 16px; |
| | | background-color: $bg-color; |
| | | border-bottom: 1px solid $border-color; |
| | | font-weight: bold; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | &-title { |
| | | flex: 1; |
| | | } |
| | | |
| | | &-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | &-body { |
| | | padding: 16px; |
| | | } |
| | | } |
| | | |
| | | .info-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, 1fr); |
| | | gap: 16px; |
| | | |
| | | .info-item { |
| | | margin-bottom: 12px; |
| | | |
| | | .info-label { |
| | | color: #909399; |
| | | font-size: 14px; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .info-value { |
| | | color: #333; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .input-group { |
| | | margin: 16px 0; |
| | | |
| | | .input-wrapper { |
| | | display: flex; |
| | | gap: 12px; |
| | | |
| | | .result-input { |
| | | flex: 1; |
| | | height: 45px; |
| | | padding: 0 12px; |
| | | border: 1px solid $border-color; |
| | | border-radius: 4px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .upload-btn { |
| | | background-color: #909399; |
| | | color: #fff; |
| | | padding: 10px 12px; |
| | | margin: 0; |
| | | font-size: 14px; |
| | | min-height: 44px; |
| | | white-space: nowrap; |
| | | flex-shrink: 0; |
| | | min-width: 80px; |
| | | max-width: 120px; |
| | | flex: 1; |
| | | } |
| | | |
| | | .attachment-btn { |
| | | background-color: #17a2b8; |
| | | color: #fff; |
| | | padding: 10px 12px; |
| | | margin: 0; |
| | | font-size: 14px; |
| | | min-height: 44px; |
| | | white-space: nowrap; |
| | | flex-shrink: 0; |
| | | min-width: 80px; |
| | | max-width: 120px; |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .table-container { |
| | | border: 1px solid $border-color; |
| | | border-radius: 4px; |
| | | margin-top: 20px; |
| | | |
| | | .table-header { |
| | | display: flex; |
| | | background-color: $bg-color; |
| | | border-bottom: 1px solid $border-color; |
| | | |
| | | .th { |
| | | flex: 1; |
| | | padding: 12px; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | |
| | | .table-row { |
| | | display: flex; |
| | | border-bottom: 1px solid $border-color; |
| | | padding: 12px; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .td { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .result-badge { |
| | | display: inline-block; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | font-weight: bold; |
| | | |
| | | &.OK { |
| | | background-color: rgba($success-color, 0.1); |
| | | color: $success-color; |
| | | } |
| | | |
| | | &.NG { |
| | | background-color: rgba($danger-color, 0.1); |
| | | color: $danger-color; |
| | | } |
| | | |
| | | &.PENDING { |
| | | background-color: rgba(#E6A23C, 0.1); |
| | | color: #E6A23C; |
| | | } |
| | | |
| | | &.BLOCKED { |
| | | background-color: rgba(#909399, 0.1); |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .record-value { |
| | | font-family: 'Courier New', monospace; |
| | | font-weight: 500; |
| | | color: #333; |
| | | padding: 2px 4px; |
| | | background-color: #f8f9fa; |
| | | border-radius: 3px; |
| | | border: 1px solid #e9ecef; |
| | | } |
| | | |
| | | .no-data-text { |
| | | color: #E6A23C; |
| | | font-size: 12px; |
| | | font-style: italic; |
| | | } |
| | | |
| | | .blocked-text { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | font-style: italic; |
| | | } |
| | | |
| | | .waiting-text { |
| | | color: #E6A23C; |
| | | font-size: 12px; |
| | | font-style: italic; |
| | | } |
| | | |
| | | .ready-text { |
| | | color: #67C23A; |
| | | font-size: 12px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | |
| | | .spec-text { |
| | | font-size: 14px; |
| | | color: #333; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .result-preview { |
| | | margin: 16px 0; |
| | | padding: 12px; |
| | | background-color: #f8f9fa; |
| | | border-radius: 4px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | |
| | | .info-label { |
| | | color: #909399; |
| | | font-size: 14px; |
| | | min-width: 80px; |
| | | } |
| | | |
| | | .info-value { |
| | | color: #333; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .result-ng { |
| | | margin: 16px 0; |
| | | padding: 12px; |
| | | background-color: rgba($danger-color, 0.1); |
| | | border-radius: 4px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | |
| | | .info-label { |
| | | color: $danger-color; |
| | | font-size: 14px; |
| | | min-width: 80px; |
| | | } |
| | | |
| | | .info-value { |
| | | color: $danger-color; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .action-buttons { |
| | | margin-top: 20px; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 12px; |
| | | |
| | | .btn { |
| | | padding: 8px 20px; |
| | | border-radius: 4px; |
| | | |
| | | &.primary-btn { |
| | | background-color: $primary-color; |
| | | color: #fff; |
| | | } |
| | | |
| | | &.cancel-btn { |
| | | background-color: #909399; |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .danger { |
| | | color: $danger-color; |
| | | } |
| | | |
| | | .overlay { |
| | | position: fixed; |
| | | top: 0; |
| | |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | z-index: 2000; |
| | | } |
| | | |
| | | |
| | | .popup { |
| | | background-color: #fff; |
| | | padding: 0; |
| | | border: none; |
| | | box-shadow: 0 8px 32px rgba(60,60,60,0.18); |
| | | width: 68vw; |
| | | max-width: 600px; |
| | | border-radius: 16px; |
| | | overflow: hidden; |
| | | position: relative; |
| | | z-index: 2001; |
| | | max-height: 80vh; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .popup-header { |
| | | padding: 20px; |
| | | border: 1px solid #ccc; |
| | | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
| | | width: 68vw; /* 设置宽度为视口宽度的80% */ |
| | | height: 25vh; /* 设置高度为视口高度的80% */ |
| | | border-bottom: 1px solid #eee; |
| | | |
| | | h3 { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #2c3e50; |
| | | margin: 0; |
| | | } |
| | | |
| | | .popup-subtitle { |
| | | font-size: 14px; |
| | | color: #7f8c8d; |
| | | margin: 5px 0 0 0; |
| | | } |
| | | } |
| | | |
| | | .popup-content { |
| | | padding: 20px; |
| | | |
| | | .input-group { |
| | | margin-bottom: 15px; |
| | | |
| | | .input-label { |
| | | font-size: 14px; |
| | | color: #7f8c8d; |
| | | margin-right: 10px; |
| | | min-width: 80px; |
| | | } |
| | | |
| | | .input-field { |
| | | flex: 1; |
| | | padding: 8px 12px; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | font-size: 14px; |
| | | background-color: white; |
| | | } |
| | | } |
| | | |
| | | .input-hint { |
| | | margin-top: 8px; |
| | | padding: 8px 12px; |
| | | background-color: #f8f9fa; |
| | | border-radius: 4px; |
| | | border-left: 3px solid #409EFF; |
| | | |
| | | text { |
| | | font-size: 12px; |
| | | color: #666; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .popup-actions { |
| | | padding: 20px; |
| | | border-top: 1px solid #eee; |
| | | display: flex; |
| | | gap: 10px; |
| | | justify-content: flex-end; |
| | | |
| | | .action-btn { |
| | | padding: 12px 20px; |
| | | border: none; |
| | | border-radius: 6px; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | cursor: pointer; |
| | | transition: all 0.2s; |
| | | |
| | | &.primary { |
| | | background-color: #3498db; |
| | | color: white; |
| | | } |
| | | |
| | | &.secondary { |
| | | background-color: #95a5a6; |
| | | color: white; |
| | | } |
| | | |
| | | &:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 2px 8px rgba(0,0,0,0.15); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /* 图片预览 */ |
| | | .image-preview { |
| | | text-align: center; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .preview-image { |
| | | max-width: 100%; |
| | | max-height: 300px; |
| | | border-radius: 4px; |
| | | border: 1px solid #ddd; |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (max-width: 768px) { |
| | | .info-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .input-group { |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .input-label { |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .input-wrapper { |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .upload-btn, |
| | | .attachment-btn { |
| | | min-width: 70px; |
| | | max-width: 100px; |
| | | padding: 8px 10px; |
| | | font-size: 13px; |
| | | min-height: 40px; |
| | | } |
| | | } |
| | | |
| | | /* 附件相关样式 */ |
| | | .attachment-list-popup { |
| | | width: 80vw; |
| | | max-width: 800px; |
| | | max-height: 85vh; |
| | | } |
| | | |
| | | .attachment-detail-popup { |
| | | width: 70vw; |
| | | max-width: 600px; |
| | | } |
| | | |
| | | .attachment-popup-header { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #eee; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | background-color: white; |
| | | } |
| | | |
| | | .attachment-popup-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #2c3e50; |
| | | margin: 0; |
| | | } |
| | | |
| | | .attachment-close-btn { |
| | | padding: 8px 16px; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | background-color: white; |
| | | font-size: 14px; |
| | | transition: all 0.2s; |
| | | color: #2c3e50; |
| | | } |
| | | |
| | | .attachment-close-btn:hover { |
| | | background-color: #f8f9fa; |
| | | } |
| | | |
| | | .attachment-popup-content { |
| | | padding: 16px; |
| | | max-height: 60vh; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | /* 加载状态 */ |
| | | .attachment-loading { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 40px 20px; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .loading-spinner { |
| | | width: 32px; |
| | | height: 32px; |
| | | border: 3px solid #f3f3f3; |
| | | border-top: 3px solid #3498db; |
| | | border-radius: 50%; |
| | | animation: spin 1s linear infinite; |
| | | } |
| | | |
| | | @keyframes spin { |
| | | 0% { transform: rotate(0deg); } |
| | | 100% { transform: rotate(360deg); } |
| | | } |
| | | |
| | | .loading-text { |
| | | font-size: 14px; |
| | | color: #7f8c8d; |
| | | } |
| | | |
| | | /* 空状态 */ |
| | | .attachment-empty { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 40px 20px; |
| | | gap: 12px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .empty-icon { |
| | | font-size: 36px; |
| | | opacity: 0.6; |
| | | } |
| | | |
| | | .empty-text { |
| | | font-size: 16px; |
| | | color: #7f8c8d; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .empty-hint { |
| | | font-size: 14px; |
| | | color: #95a5a6; |
| | | } |
| | | |
| | | /* 附件列表布局 */ |
| | | .attachment-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .attachment-item { |
| | | background-color: white; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
| | | overflow: hidden; |
| | | transition: all 0.3s; |
| | | border: 1px solid #eee; |
| | | } |
| | | |
| | | .attachment-item:hover { |
| | | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .attachment-info { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #eee; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .file-type-badge { |
| | | width: 40px; |
| | | height: 40px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 20px; |
| | | background: #f8f9fa; |
| | | border: 2px solid #e9ecef; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .file-type-badge.large { |
| | | width: 56px; |
| | | height: 56px; |
| | | font-size: 28px; |
| | | } |
| | | |
| | | .file-type-badge.file-pdf { background: #ffe6e6; border-color: #ffcccc; } |
| | | .file-type-badge.file-image { background: #e6f3ff; border-color: #cce7ff; } |
| | | .file-type-badge.file-text { background: #e6ffe6; border-color: #ccffcc; } |
| | | .file-type-badge.file-word { background: #e6f0ff; border-color: #cce0ff; } |
| | | .file-type-badge.file-excel { background: #e6ffe6; border-color: #ccffcc; } |
| | | .file-type-badge.file-powerpoint { background: #fff0e6; border-color: #ffe0cc; } |
| | | .file-type-badge.file-archive { background: #f0e6ff; border-color: #e0ccff; } |
| | | .file-type-badge.file-cad { background: #e6fff0; border-color: #ccffe0; } |
| | | .file-type-badge.file-unknown { background: #f5f5f5; border-color: #e0e0e0; } |
| | | |
| | | .attachment-details { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .attachment-name { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #2c3e50; |
| | | cursor: pointer; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | margin-bottom: 8px; |
| | | transition: color 0.2s; |
| | | } |
| | | |
| | | .attachment-name:hover { |
| | | color: #3498db; |
| | | } |
| | | |
| | | .attachment-meta { |
| | | display: flex; |
| | | gap: 16px; |
| | | font-size: 12px; |
| | | color: #95a5a6; |
| | | } |
| | | |
| | | .meta-type { |
| | | background-color: #ecf0f1; |
| | | padding: 2px 6px; |
| | | border-radius: 10px; |
| | | color: #7f8c8d; |
| | | } |
| | | |
| | | .meta-version { |
| | | background-color: #e8f5e8; |
| | | padding: 2px 6px; |
| | | border-radius: 10px; |
| | | color: #2e7d32; |
| | | } |
| | | |
| | | .meta-date { |
| | | background-color: #fff3e0; |
| | | padding: 2px 6px; |
| | | border-radius: 10px; |
| | | color: #f57c00; |
| | | } |
| | | |
| | | .attachment-actions { |
| | | padding: 12px 16px; |
| | | border-top: 1px solid #eee; |
| | | display: flex; |
| | | gap: 8px; |
| | | background-color: #f8f9fa; |
| | | } |
| | | |
| | | /* 按钮样式 */ |
| | | .btn-secondary { |
| | | padding: 8px 16px; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | background-color: white; |
| | | font-size: 14px; |
| | | transition: all 0.2s; |
| | | color: #2c3e50; |
| | | flex: 1; |
| | | } |
| | | |
| | | .btn-secondary:hover { |
| | | background-color: #f8f9fa; |
| | | } |
| | | |
| | | .btn-primary { |
| | | padding: 8px 16px; |
| | | border: 1px solid #3498db; |
| | | border-radius: 4px; |
| | | background-color: #3498db; |
| | | color: white; |
| | | font-size: 14px; |
| | | transition: all 0.2s; |
| | | flex: 1; |
| | | } |
| | | |
| | | .btn-primary:hover { |
| | | background-color: #2980b9; |
| | | } |
| | | |
| | | .btn-success { |
| | | padding: 8px 16px; |
| | | border: 1px solid #2ecc71; |
| | | border-radius: 4px; |
| | | background-color: #2ecc71; |
| | | color: white; |
| | | font-size: 14px; |
| | | transition: all 0.2s; |
| | | flex: 1; |
| | | } |
| | | |
| | | .btn-success:hover { |
| | | background-color: #27ae60; |
| | | } |
| | | |
| | | /* 附件详情样式 */ |
| | | .attachment-detail-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 16px; |
| | | border-bottom: 1px solid #eee; |
| | | } |
| | | |
| | | .attachment-detail-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #2c3e50; |
| | | flex: 1; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .attachment-detail-info { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .info-row { |
| | | display: flex; |
| | | margin-bottom: 12px; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .info-item { |
| | | flex: 1; |
| | | } |
| | | |
| | | .info-label { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: #7f8c8d; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .info-content { |
| | | font-size: 14px; |
| | | color: #2c3e50; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .attachment-detail-actions { |
| | | padding: 12px 16px; |
| | | border-top: 1px solid #eee; |
| | | display: flex; |
| | | gap: 8px; |
| | | background-color: #f8f9fa; |
| | | } |
| | | |
| | | /* 文件预览弹窗样式 */ |
| | | .file-preview-popup { |
| | | width: 80vw; |
| | | max-width: 800px; |
| | | max-height: 80vh; |
| | | } |
| | | |
| | | .file-preview-title { |
| | | padding: 20px; |
| | | margin: 0; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #2c3e50; |
| | | border-bottom: 1px solid #eee; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .file-preview-content { |
| | | padding: 20px; |
| | | max-height: 60vh; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .file-preview-content pre { |
| | | white-space: pre-wrap; |
| | | word-wrap: break-word; |
| | | font-family: 'Courier New', monospace; |
| | | font-size: 12px; |
| | | line-height: 1.4; |
| | | background: #f8f9fa; |
| | | padding: 15px; |
| | | border-radius: 4px; |
| | | border: 1px solid #e9ecef; |
| | | } |
| | | |
| | | .image-preview-container { |
| | | text-align: center; |
| | | } |
| | | |
| | | .image-zoom-hint { |
| | | margin-top: 10px; |
| | | font-size: 12px; |
| | | color: #7f8c8d; |
| | | } |
| | | |
| | | .unsupported-preview { |
| | | text-align: center; |
| | | padding: 40px 20px; |
| | | } |
| | | |
| | | .unsupported-icon { |
| | | font-size: 48px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .unsupported-text { |
| | | font-size: 16px; |
| | | color: #7f8c8d; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .unsupported-hint { |
| | | font-size: 14px; |
| | | color: #95a5a6; |
| | | } |
| | | |
| | | .file-preview-actions { |
| | | padding: 15px 20px; |
| | | border-top: 1px solid #eee; |
| | | display: flex; |
| | | gap: 10px; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .file-preview-btn { |
| | | padding: 8px 16px; |
| | | border-radius: 4px; |
| | | border: none; |
| | | font-size: 14px; |
| | | cursor: pointer; |
| | | transition: all 0.2s; |
| | | } |
| | | |
| | | .file-preview-btn.download-btn { |
| | | background: #2ecc71; |
| | | color: white; |
| | | } |
| | | |
| | | .file-preview-btn.close-btn { |
| | | background: #95a5a6; |
| | | color: white; |
| | | } |
| | | |
| | | .file-preview-btn:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 2px 8px rgba(0,0,0,0.15); |
| | | } |
| | | |
| | | .edit { |
| | | background-color: white; |
| | | /* 验证提示样式 */ |
| | | .validation-hint { |
| | | background: linear-gradient(135deg, #ffe6e6 0%, #ffcccc 100%); |
| | | border: 2px solid #ff6b6b; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | margin-bottom: 20px; |
| | | display: flex; |
| | | align-items: flex-start; |
| | | gap: 12px; |
| | | box-shadow: 0 2px 8px rgba(255, 107, 107, 0.2); |
| | | } |
| | | |
| | | .validation-icon { |
| | | font-size: 24px; |
| | | flex-shrink: 0; |
| | | margin-top: 2px; |
| | | } |
| | | |
| | | .validation-text { |
| | | flex: 1; |
| | | } |
| | | |
| | | .validation-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #d63031; |
| | | margin-bottom: 12px; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .validation-requirements { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .requirement-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 8px 12px; |
| | | background: rgba(255, 255, 255, 0.7); |
| | | border-radius: 6px; |
| | | border: 1px solid rgba(255, 107, 107, 0.3); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .requirement-item.completed { |
| | | background: rgba(46, 204, 113, 0.1); |
| | | border-color: rgba(46, 204, 113, 0.3); |
| | | } |
| | | |
| | | .requirement-icon { |
| | | font-size: 16px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .requirement-text { |
| | | font-size: 13px; |
| | | color: #2c3e50; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .requirement-item.completed .requirement-text { |
| | | color: #27ae60; |
| | | } |
| | | |
| | | /* 删除图片按钮样式 */ |
| | | .delete-image-btn { |
| | | padding: 6px 12px; |
| | | border: 1px solid #e74c3c; |
| | | border-radius: 4px; |
| | | background-color: #e74c3c; |
| | | color: white; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | cursor: pointer; |
| | | transition: all 0.2s; |
| | | } |
| | | |
| | | .delete-image-btn:hover { |
| | | background-color: #c0392b; |
| | | border-color: #c0392b; |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | /* 上传图片按钮样式 */ |
| | | .tablet-upload-btn { |
| | | background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%); |
| | | border: none; |
| | | border-radius: 8px; |
| | | padding: 12px 20px; |
| | | color: white; |
| | | font-weight: 600; |
| | | font-size: 14px; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-height: 48px; |
| | | } |
| | | |
| | | .tablet-upload-btn:hover { |
| | | background: linear-gradient(135deg, #27ae60 0%, #229954 100%); |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 6px 16px rgba(46, 204, 113, 0.4); |
| | | } |
| | | |
| | | .btn-content { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .btn-icon { |
| | | font-size: 18px; |
| | | } |
| | | |
| | | .btn-text { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (min-width: 768px) { |
| | | .section-body { |
| | | padding: 40px; |
| | | } |
| | | |
| | | .validation-hint { |
| | | padding: 25px; |
| | | margin-bottom: 30px; |
| | | } |
| | | |
| | | .validation-title { |
| | | font-size: 18px; |
| | | } |
| | | |
| | | .requirement-item { |
| | | padding: 12px 18px; |
| | | } |
| | | |
| | | .requirement-text { |
| | | font-size: 15px; |
| | | } |
| | | } |
| | | </style> |