This commit is contained in:
cwchen 2025-11-19 10:21:43 +08:00
commit add4c8e032
17 changed files with 590 additions and 428 deletions

View File

@ -38,11 +38,11 @@
"file-saver": "2.0.5",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
"jquery": "^3.6.0",
"jquery-mousewheel": "^3.1.13",
"js-beautify": "1.13.0",
"js-cookie": "3.0.1",
"jsencrypt": "3.0.0-rc.1",
"jquery": "^3.6.0",
"jquery-mousewheel": "^3.1.13",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"luckysheet": "^2.1.13",

View File

@ -1,20 +1,20 @@
<template>
<div class="upload-container">
<el-upload
ref="upload"
class="upload-demo"
drag
action="#"
multiple
<el-upload
ref="upload"
class="upload-demo"
drag
action="#"
multiple
:show-file-list="true"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-change="handleFileChange"
:on-exceed="handleExceed"
:file-list="files"
:accept="accept"
:on-exceed="handleExceed"
:file-list="files"
:accept="accept"
:limit="limitUploadNum"
:auto-upload="autoUpload"
:auto-upload="autoUpload"
:http-request="customUpload"
>
<div class="upload-content">
@ -63,6 +63,8 @@ import {
uploadSmallFile,
uploadLargeFile,
} from '@/api/common/uploadFile.js'
import mammoth from 'mammoth'
import * as XLSX from 'xlsx'
//
const FILE_STATUS = {
@ -214,7 +216,7 @@ export default {
//
handleFileChange(file, fileList) {
if (this.shouldIgnoreFileChange(file.status)) return
this.files = this.formatFileList(fileList)
this.updatePreview(fileList)
},
@ -222,11 +224,11 @@ export default {
//
handleExceed(files, fileList) {
console.log('文件超出限制处理', files, fileList)
if (files.length > 0) {
//
this.$emit('del-file', { ...fileList[0], response: fileList[0].res })
//
this.files = []
const newFile = files[0]
@ -258,7 +260,7 @@ export default {
this.updatePreview(fileList)
this.files = this.formatFileList(fileList)
const delFileObj = this.findFileByRawFile(file.raw)
if (delFileObj) {
delFileObj.response = delFileObj.res
@ -273,7 +275,7 @@ export default {
async customUpload(options) {
const { file } = options
this.isUploading = true
const uploadFileObj = this.findFileByRawFile(file)
if (!uploadFileObj) {
this.handleError(new Error('文件对象不存在'), file)
@ -282,14 +284,14 @@ export default {
const fileUid = uploadFileObj.uid
const statusText = this.fileUploadRule.fields_json ? '识别中' : '上传中'
this.updateFileStatus(fileUid, FILE_STATUS.UPLOADING, statusText, null, 0)
this.$bus.$emit('startUpload', statusText)
try {
const formData = this.createFormData(file)
const res = await this.uploadFile(formData, file.size)
if (res.code === 200) {
this.handleSuccess(res, uploadFileObj)
} else {
@ -304,10 +306,26 @@ export default {
//
handleSuccess(response, file) {
this.$bus.$emit('endUpload')
this.updateFileStatus(file.uid, FILE_STATUS.SUCCESS, '', response.data, 100)
this.$emit('file-change', this.getCurrentFiles(), this.type)
this.isUploading = false
const fileExt = this.getFileExtension(file.name);
if (fileExt === 'docx') {
this.parseDocxFile(file.raw).then(text => {
// OCR
this.$emit('docx-content', text);
});
}
else if (fileExt === 'xlsx' || fileExt === 'xls') {
this.parseXlsxFile(file.raw).then(text => {
this.$emit('xlsx-content', text); // xlsx
}).catch(err => {
console.error('xlsx解析失败:', err);
});
}
this.$bus.$emit('endUpload')
this.updateFileStatus(file.uid, FILE_STATUS.SUCCESS, '', response.data, 100)
this.$emit('file-change', this.getCurrentFiles(), this.type)
this.isUploading = false
},
//
@ -318,6 +336,53 @@ export default {
this.isUploading = false
},
parseDocxFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
const arrayBuffer = e.target.result;
// 使mammothdocx
mammoth.extractRawText({ arrayBuffer })
.then(result => {
resolve(result.value); //
})
.catch(err => {
console.error('docx解析失败:', err);
reject(err);
});
};
reader.readAsArrayBuffer(file);
});
},
parseXlsxFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
try {
//
const data = new Uint8Array(e.target.result);
// Excel
const workbook = XLSX.read(data, { type: 'array' });
//
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// JSON100
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, raw: false });
//
const text = jsonData.map(row => row.join('\t')).join('\n');
resolve(text);
} catch (err) {
console.error('xlsx解析失败:', err);
reject(err);
}
};
//
reader.readAsArrayBuffer(file);
});
},
//
getFileExtension(filename) {
return filename.split('.').pop().toLowerCase()
@ -337,7 +402,7 @@ export default {
async uploadFile(formData, fileSize) {
const isLargeFile = fileSize > DEFAULT_FILE_SIZE
const hasOcr = !!this.fileUploadRule.fields_json
if (isLargeFile) {
return hasOcr ? await uploadLargeFileByOcr(formData) : await uploadLargeFile(formData)
} else {
@ -346,8 +411,8 @@ export default {
},
findFileByRawFile(rawFile) {
return this.files.find(item =>
item.raw === rawFile ||
return this.files.find(item =>
item.raw === rawFile ||
(item.name === rawFile.name && item.size === rawFile.size)
)
},
@ -369,10 +434,10 @@ export default {
}
const updatedFile = { ...this.files[fileIndex], status }
if (statusText) updatedFile.statusText = statusText
if (percentage !== null) updatedFile.percentage = Math.max(0, Math.min(100, percentage))
if (responseData) {
updatedFile.response = responseData
updatedFile.response.businessType = this.fileUploadRule?.fileUploadType
@ -455,7 +520,7 @@ export default {
isDocumentFile(file) {
if (!file || !file.name) return false
const fileExtension = this.getFileExtension(file.name)
return ['pdf', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtension) ||
return ['pdf', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtension) ||
(file && file.fileType === FILE_TYPES.DOCUMENT)
},
@ -468,8 +533,8 @@ export default {
if (fileList.length === 1 && fileList[0]) {
const file = fileList[0].raw || fileList[0]
if (this.isImageFile(file)) {
fileList[0].lsFilePath ?
this.generateImagePreviewFromPath(fileList[0]) :
fileList[0].lsFilePath ?
this.generateImagePreviewFromPath(fileList[0]) :
this.generateImagePreview(file)
} else if (this.isDocumentFile(file)) {
this.generateDocumentPreview(file)
@ -515,12 +580,12 @@ export default {
if (fileList.length > 0) {
const firstFile = fileList[0]
if (firstFile?.lsFilePath) {
this.isImageFile(firstFile) ?
this.generateImagePreviewFromPath(firstFile) :
this.isImageFile(firstFile) ?
this.generateImagePreviewFromPath(firstFile) :
this.generateDocumentPreview(firstFile)
} else if (firstFile?.raw) {
this.isImageFile(firstFile.raw) ?
this.generateImagePreview(firstFile.raw) :
this.isImageFile(firstFile.raw) ?
this.generateImagePreview(firstFile.raw) :
this.generateDocumentPreview(firstFile.raw)
} else {
this.clearPreview()

View File

@ -101,6 +101,8 @@ export default {
this.$refs.financialStatement.validate()
])
console.log('处理后的年份:', financeReportData);
// 2.
const formData = {
...financeReportData, // fileNamereportYear

View File

@ -105,7 +105,8 @@ export default {
name: item.fileName || '未知文件',
filePath: item.filePath || '',
lsFilePath: item.lsFilePath || '', //
fileType: item.fileType || '2' //
fileType: item.fileType || '2', //
id: item.sourceId + ''
}))
},

View File

@ -1,5 +1,4 @@
<template>
<!-- 模板部分保持不变 -->
<div>
<div class="basic-info-title">
<img src="@/assets/enterpriseLibrary/basic-info.png" alt="财务报告">
@ -13,17 +12,17 @@
label-width="110px"
label-position="top"
>
<!-- 财务报告上传 -->
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="财务报告附件" prop="fileList">
<UploadFile
:fileList="formatFileList()"
:fileList="formData.fileList"
:fileUploadRule="financeReportUploadRule"
@file-change="(files, type) => handleFileChange(files, type)"
@del-file="handleDelFile"
:uploadType="uploadType"
:maxFileTips="maxFileTips"
@docx-content="handleDocxContent"
:limitUploadNum="1"
type="finance_report"
:show-file-list="true"
@ -32,7 +31,6 @@
</el-col>
</el-row>
<!-- 文件名 -->
<el-row :gutter="24" class="info-row">
<el-col :span="8" class="info-col">
<el-form-item label="文件名" prop="fileName">
@ -46,7 +44,6 @@
</el-form-item>
</el-col>
<!-- 报告年份 -->
<el-col :span="8" class="info-col">
<el-form-item label="报告年份" prop="reportYear">
<el-date-picker
@ -73,7 +70,7 @@ export default {
dicts: ['identification_tag'],
data() {
return {
uploadType: 'pdf、doc、docx',
uploadType: 'docx',
maxFileTips: '100MB',
ocrRuleList: ['finance_report'],
fileUploadList: [],
@ -81,10 +78,10 @@ export default {
"报告年份": "reportYear"
},
formData: {
fileList: [], //
fileList: [],
fileName: '',
reportYear: '', // "2023"
delFileList: [] //
reportYear: '',
delFileList: []
},
rules: {
fileList: [
@ -96,7 +93,8 @@ export default {
reportYear: [
{ required: true, message: '请选择报告年份', trigger: 'change' }
]
}
},
isReplacingFile: false //
}
},
computed: {
@ -115,51 +113,95 @@ export default {
}
},
methods: {
//
setFormData(data) {
handleDocxContent(text) {
// OCRdocx
const mockOcrResult = {
status_code: 200,
data: {
chat_res: this.extractInfoFromDocx(text) //
}
};
// OCR
this.handleOcrResultWithMock(mockOcrResult);
},
// docx
extractInfoFromDocx(text) {
console.log("text", text);
// 2026:202620262026
const yearMatch = text.match(
/(年份|报告年份|年度)\s*[:]\s*(\d{4})|(\d{4})\s*(年|年度)/
);
console.log("yearMatch", yearMatch);
// +/
const year = yearMatch ? (yearMatch[2] || yearMatch[3]) : '';
return { "报告年份": year };
},
// OCR
handleOcrResultWithMock(ocrResult) {
if (ocrResult.status_code === 200) {
const chat_res = ocrResult.data?.chat_res;
if (chat_res && typeof chat_res === 'object') {
Object.keys(chat_res).forEach(key => {
const formField = this.ocrResultParams[key];
if (formField === 'reportYear' && chat_res[key]) {
const year = chat_res[key].replace(/[^\d]/g, '');
this.formData[formField] = year.length === 4 ? year : '';
}
});
this.$message.success('docx内容解析成功已自动填充报告年份');
this.$refs.financeReportForm.validateField('reportYear');
}
} else {
this.$message.error(`年份提取失败: ${ocrResult.status_msg || '未找到有效年份'}`);
}
},
setFormData(data) {
// uid
const financeFiles = Array.isArray(data.fileList)
? data.fileList.filter(file => file && file.businessType === 'finance_report')
: []
.map(file => ({
name: file.name || file.fileName || '未知文件',
filePath: file.filePath,
lsFilePath: file.lsFilePath,
fileType: file.fileType || '2',
businessType: 'finance_report'
}))
: [];
let reportYear = data.reportYear || '';
if (reportYear) {
if (typeof reportYear === 'string') {
reportYear = reportYear.split('-')[0];
}
if (typeof reportYear === 'number') {
reportYear = reportYear.toString();
}
}
this.formData = {
...data,
...(data || {}),
fileList: financeFiles,
fileName: financeFiles.length > 0 ? this.getFileName(financeFiles[0]) : '',
delFileList: []
delFileList: [],
reportYear: reportYear,
}
},
// UploadFile
formatFileList() {
return this.formData.fileList.map((file, index) => {
const fileExt = this.getFileExt(file)
return {
...file,
uid: file.uid || file.filePath || `${Date.now()}-${index}`,
name: this.getFileName(file), // 使
status: file.status || 'success',
percentage: file.percentage || 100,
fileType: file.fileType || '2'
}
})
},
//
getFileName(file) {
//
const fullName = file.name || file.fileName || `未命名文件.${this.getFileExt(file)}`
//
const fullName = file.name || file.fileName || '未知文件'
const lastDotIndex = fullName.lastIndexOf('.')
//
if (lastDotIndex > 0) {
return fullName.substring(0, lastDotIndex)
}
return fullName
return lastDotIndex > 0 ? fullName.substring(0, lastDotIndex) : fullName
},
//
getFileExt(file) {
if (!file) return ''
const fileName = file.name || file.fileName || ''
@ -167,37 +209,63 @@ export default {
return extMatch ? extMatch[1].toLowerCase() : ''
},
//
handleFileChange(files, type) {
if (type === 'finance_report') {
const markedFiles = files.map(file => ({
...file,
businessType: 'finance_report',
name: file.name || file.fileName || '未知文件'
}))
this.formData.fileList = markedFiles
if (type === 'finance_report' && files instanceof Array) {
if (files.length > 0 && files[0].response) {
this.isReplacingFile = false;
if (markedFiles.length > 0) {
const originalFileName = this.getFileName(markedFiles[0]) // 使
const safeFileName = originalFileName.replace(/--+/g, '-')
this.formData.fileName = safeFileName
this.$refs.financeReportForm.validateField('fileName')
this.$message.success(`文件名已自动提取:${safeFileName}`)
//
if (this.formData.fileList.length > 0) {
const oldFile = this.formData.fileList[0];
const delPath = oldFile?.response?.fileRes?.filePath || oldFile?.filePath || null;
if (delPath && !this.formData.delFileList.includes(delPath)) {
this.formData.delFileList.push(delPath);
}
}
const markedFiles = files.map(file => ({
...file,
businessType: 'finance_report'
})).slice(-1);
this.formData.fileList = markedFiles;
//
if (markedFiles.length > 0) {
const originalFileName = this.getFileName(markedFiles[0])
const safeFileName = originalFileName.replace(/--+/g, '-')
this.formData.fileName = safeFileName
this.$refs.financeReportForm.validateField('fileName')
}
//
const firstFile = markedFiles[0];
const fileExt = this.getFileExt(firstFile).toLowerCase(); //
if (['doc', 'docx'].includes(fileExt)) {
// doc/docxOCR
} else if (['pdf', 'jpg', 'jpeg', 'png'].includes(fileExt)) {
// pdf/OCR
this.handleOcrResult(markedFiles);
} else {
//
this.$message.warning('不支持的文件格式,无法解析年份');
}
} else if (!this.isReplacingFile) {
this.formData.fileList = [];
this.formData.fileName = '';
}
this.handleOcrResult(markedFiles)
}
},
//
handleDelFile(file) {
const filePath = file.filePath || file.response?.fileRes?.uploadPath
if (filePath && !this.formData.delFileList.includes(filePath)) {
this.formData.delFileList.push(filePath)
//
this.isReplacingFile = true;
const delPath = file?.response?.fileRes?.filePath || file?.filePath || null;
if (delPath && !this.formData.delFileList.includes(delPath)) {
this.formData.delFileList.push(delPath);
}
this.formData.fileList = []
this.formData.fileName = ''
this.formData.reportYear = ''
},
handleOcrResult(files) {
@ -264,6 +332,7 @@ export default {
reportYear: '',
delFileList: []
}
this.isReplacingFile = false;
},
addOcrRule() {
@ -292,7 +361,6 @@ export default {
</script>
<style scoped lang="scss">
/* 样式部分保持不变 */
.basic-info-title {
display: flex;
align-items: center;

View File

@ -12,12 +12,11 @@
label-width="110px"
label-position="top"
>
<!-- 财务报表上传 -->
<el-row :gutter="24" class="data-row">
<el-col :span="8" class="data-col">
<el-form-item label="财务报表附件" prop="fileList" class="data-item">
<UploadFile
:fileList="formatFileList()"
:fileList="formData.fileList"
:fileUploadRule="financialStatementUploadRule"
@file-change="(files, type) => handleFileChange(files, type)"
@del-file="handleDelFile"
@ -25,13 +24,13 @@
:maxFileTips="maxFileTips"
:limitUploadNum="1"
type="financial_statement"
@xlsx-content="handleXlsxContent"
:show-file-list="true"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 财务数据字段三列布局 -->
<el-row :gutter="24" class="data-row">
<el-col :span="8" class="data-col">
<el-form-item label="营业收入" prop="operatingIncome" class="data-item">
@ -146,7 +145,7 @@ export default {
dicts: ['identification_tag'],
data() {
return {
uploadType: 'pdf、doc、docx',
uploadType: 'xlsx、xls',
maxFileTips: '100MB',
ocrRuleList: ['financial_statement'],
fileUploadList: [],
@ -163,7 +162,7 @@ export default {
"资产负债率": "assetLiabilityRatio"
},
formData: {
fileList: [], //
fileList: [],
operatingIncome: '',
currentAssets: '',
currentLiabilities: '',
@ -174,7 +173,7 @@ export default {
totalLiabilities: '',
totalAssets: '',
assetLiabilityRatio: '',
delFileList: [] //
delFileList: []
},
rules: {
fileList: [
@ -213,7 +212,8 @@ export default {
{ required: true, message: '资产负债率为必填项', trigger: 'blur' },
{ pattern: /^\d+(\.\d+)?%?$/, message: '请输入有效的资产负债率如1或1%', trigger: 'blur' }
]
}
},
isReplacingFile: false //
}
},
computed: {
@ -232,43 +232,80 @@ export default {
}
},
methods: {
//
setFormData(data) {
// businessType=financial_statement
const financialFiles = Array.isArray(data.fileList)
? data.fileList.filter(file => file && file.businessType === 'financial_statement')
: []
this.formData = {
...data,
fileList: financialFiles, //
delFileList: [] //
handleXlsxContent(text) {
this.$bus.$emit('startUpload', '正在解析Excel财务数据')
try {
// Excel
const financialData = this.extractFinancialDataFromText(text);
//
this.fillFormWithFinancialData(financialData);
this.$message.success('Excel解析成功已自动填充财务数据');
} catch (error) {
this.$message.error(`Excel解析失败: ${error.message}`);
} finally {
this.$bus.$emit('endUpload');
}
},
// UploadFile
formatFileList() {
return this.formData.fileList.map((file, index) => {
//
const fileExt = this.getFileExt(file)
return {
...file,
//
uid: file.uid || file.filePath || `${Date.now()}-${index}`, //
name: this.getFileName(file), //
status: file.status || 'success', //
percentage: file.percentage || 100, // 100%
fileType: file.fileType || '2' //
//
extractFinancialDataFromText(text) {
const data = {};
const keywords = Object.keys(this.ocrResultParams); //
//
keywords.forEach(keyword => {
// ": "" "
const regex = new RegExp(`${keyword}\\s*[:]?\\s*([\\d,.%]+)`, 'i');
const match = text.match(regex);
if (match && match[1]) {
//
data[keyword] = match[1].replace(/,/g, '').trim();
}
})
});
return data;
},
//
fillFormWithFinancialData(financialData) {
Object.keys(financialData).forEach(keyword => {
const formField = this.ocrResultParams[keyword];
if (formField) {
//
this.formData[formField] = financialData[keyword].replace(/[^\d.%]/g, '');
}
});
//
this.$refs.financialStatementForm.validate();
},
setFormData(data) {
// uid
const financialFiles = Array.isArray(data.fileList)
? data.fileList.filter(file => file && file.businessType === 'financial_statement')
.map(file => ({
name: file.name || file.fileName || '未知文件',
filePath: file.filePath,
lsFilePath: file.lsFilePath,
fileType: file.fileType || '2',
businessType: 'financial_statement'
}))
: [];
this.formData = {
...(data || {}),
fileList: financialFiles,
delFileList: []
}
},
//
getFileName(file) {
return file.name || file.fileName || `未命名文件.${this.getFileExt(file)}`
return file.name || file.fileName || '未知文件'
},
//
getFileExt(file) {
if (!file) return ''
const fileName = file.name || file.fileName || ''
@ -276,25 +313,44 @@ export default {
return extMatch ? extMatch[1].toLowerCase() : ''
},
//
handleFileChange(files, type) {
if (type === 'financial_statement') {
// businessTypename
const markedFiles = files.map(file => ({
...file,
businessType: 'financial_statement',
name: file.name || file.fileName || '未知文件' // name
}))
this.formData.fileList = markedFiles //
this.handleOcrResult(markedFiles)
if (type === 'financial_statement' && files instanceof Array) {
//
if (files.length > 0 && files[0].response) { //
//
this.isReplacingFile = false;
//
if (this.formData.fileList.length > 0) {
const oldFile = this.formData.fileList[0];
const delPath = oldFile?.response?.fileRes?.filePath || oldFile?.filePath || null;
if (delPath && !this.formData.delFileList.includes(delPath)) {
this.formData.delFileList.push(delPath);
}
}
const markedFiles = files.map(file => ({
...file,
businessType: 'financial_statement'
})).slice(-1);
this.formData.fileList = markedFiles;
const fileExt = this.getFileExt(markedFiles[0]);
if (['xls', 'xlsx'].includes(fileExt)) {
// @xlsx-content
} else {
// ExcelOCR
this.handleOcrResult(markedFiles);
}
} else if (!this.isReplacingFile) {
this.formData.fileList = [];
}
}
},
// OCR
handleOcrResult(files) {
if (!files || !Array.isArray(files) || files.length === 0) return
//
this.$bus.$emit('startUpload', '正在识别财务数据')
try {
@ -307,7 +363,6 @@ export default {
Object.keys(chat_res).forEach(key => {
const formField = this.ocrResultParams[key]
if (formField && chat_res[key]) {
//
if (['operatingIncome', 'currentAssets', 'currentLiabilities', 'currentRatio',
'netProfit', 'shareholdersEquity', 'returnOnNetAssets', 'totalLiabilities',
'totalAssets', 'assetLiabilityRatio'].includes(formField)) {
@ -327,23 +382,20 @@ export default {
} catch (error) {
this.$message.error(`处理识别结果失败: ${error.message}`)
} finally {
//
this.$bus.$emit('endUpload')
}
},
//
handleDelFile(file) {
//
const filePath = file.filePath || file.response?.fileRes?.uploadPath
if (filePath && !this.formData.delFileList.includes(filePath)) {
this.formData.delFileList.push(filePath)
//
this.isReplacingFile = true;
const delPath = file?.response?.fileRes?.filePath || file?.filePath || null;
if (delPath && !this.formData.delFileList.includes(delPath)) {
this.formData.delFileList.push(delPath);
}
//
this.formData.fileList = []
},
//
validate() {
return new Promise((resolve, reject) => {
if (!this.$refs.financialStatementForm) {
@ -361,7 +413,6 @@ export default {
})
},
//
resetForm() {
if (this.$refs.financialStatementForm) {
this.$refs.financialStatementForm.resetFields()
@ -380,16 +431,15 @@ export default {
assetLiabilityRatio: '',
delFileList: []
}
this.isReplacingFile = false;
},
// OCR
addOcrRule() {
this.ocrRuleList.forEach(item => {
this.ocrRule(item)
})
},
// OCR
ocrRule(type) {
const foundItem = this.dict.type.identification_tag?.find(item => item.value === type)
if (!foundItem) {
@ -468,7 +518,6 @@ export default {
margin-top: 5px;
}
/* 优化上传组件样式,确保文件列表正确显示 */
::v-deep .upload-container {
::v-deep .el-upload-dragger {
height: 180px;

View File

@ -208,7 +208,8 @@ export default {
name: item.fileName || '未知文件',
filePath: item.filePath || '',
lsFilePath: item.lsFilePath || '', //
fileType: item.fileType || '2' //
fileType: item.fileType || '2', //
id: item.sourceId + ''
}))
},

View File

@ -139,18 +139,6 @@ export default {
this.$refs.otherInfo.validate()
])
// 2.
if (this.type === 'add' || (this.type === 'edit' && contractData.proName !== this.originalProName)) {
const isUnique = await checkProNameUnique({
proName: contractData.proName,
enterpriseId: this.enterpriseId,
performanceId: this.id || null
})
if (!isUnique) {
throw new Error('同一企业下项目名称已存在,请更换')
}
}
// 3.
const formData = {
...contractData,
@ -193,15 +181,15 @@ export default {
}
// 7.
// if (otherData.performanceFileList && otherData.performanceFileList.length) {
// const performanceFiles = otherData.performanceFileList.map(file =>
// file.response?.fileRes ? { ...file.response.fileRes, businessType: 'team_performance' } : null
// ).filter(Boolean)
// formData.files.push(...performanceFiles)
// }
// if (otherData.delFileList && otherData.delFileList.length) {
// formData.delFiles.push(...otherData.delFileList)
// }
if (otherData.performanceFileList && otherData.performanceFileList.length) {
const performanceFiles = otherData.performanceFileList.map(file =>
file.response?.fileRes ? { ...file.response.fileRes, businessType: 'team_performance' } : null
).filter(Boolean)
formData.files.push(...performanceFiles)
}
if (otherData.delFileList && otherData.delFileList.length) {
formData.delFiles.push(...otherData.delFileList)
}
// 8.
['fileList', 'completionReportFileList', 'bidNoticeFileList', 'performanceFileList', 'delFileList'].forEach(key => {
@ -242,6 +230,7 @@ export default {
const bidFiles = fileList.filter(f => f.businessType === 'bid_notice')
const teamFiles = fileList.filter(f => f.businessType === 'team_performance')
//
this.$refs.contractInfo.setFormData({
...performanceData,

View File

@ -12,12 +12,11 @@
label-width="110px"
label-position="top"
>
<!-- 竣工报告 -->
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="竣工报告附件" prop="completionReportFileList">
<UploadFile
:fileList="formatFileList('completionReportFileList')"
:fileList="formData.completionReportFileList"
:fileUploadRule="completionReportUploadRule"
@file-change="(files, type) => handleFileChange('completionReportFileList', files, type)"
@del-file="(file) => handleDelFile('completionReportFileList', file)"
@ -30,11 +29,10 @@
</el-form-item>
</el-col>
<!-- 中标通知书 -->
<el-col :span="8">
<el-form-item label="中标通知书附件" prop="bidNoticeFileList">
<UploadFile
:fileList="formatFileList('bidNoticeFileList')"
:fileList="formData.bidNoticeFileList"
:fileUploadRule="bidNoticeUploadRule"
@file-change="(files, type) => handleFileChange('bidNoticeFileList', files, type)"
@del-file="(file) => handleDelFile('bidNoticeFileList', file)"
@ -48,7 +46,6 @@
</el-col>
</el-row>
<!-- 时间信息 -->
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="开工时间" prop="startTime">
@ -119,6 +116,10 @@ export default {
bidNoticeFileList: [
{ required: true, message: '请上传中标通知书附件', trigger: 'change' }
]
},
replacingFiles: {
completionReportFileList: false,
bidNoticeFileList: false
}
}
},
@ -142,48 +143,43 @@ export default {
}
},
methods: {
//
setFormData(data) {
const completionFiles = Array.isArray(data.completionReportFileList)
? data.completionReportFileList.filter(file => file && file.businessType === 'completion_report')
: []
const bidFiles = Array.isArray(data.bidNoticeFileList)
? data.bidNoticeFileList.filter(file => file && file.businessType === 'bid_notice')
: []
// uid
const completionFiles = Array.isArray(data.fileList)
? data.fileList.filter(file => file && file.businessType === 'completion_report')
.map(file => ({
name: file.name || file.fileName || '未知文件',
filePath: file.filePath,
lsFilePath: file.lsFilePath,
fileType: file.fileType || '2',
businessType: 'completion_report'
}))
: [];
// uid
const bidFiles = Array.isArray(data.fileList)
? data.fileList.filter(file => file && file.businessType === 'bid_notice')
.map(file => ({
name: file.name || file.fileName || '未知文件',
filePath: file.filePath,
lsFilePath: file.lsFilePath,
fileType: file.fileType || '2',
businessType: 'bid_notice'
}))
: [];
this.formData = {
...data,
...(data || {}),
completionReportFileList: completionFiles,
bidNoticeFileList: bidFiles,
delFileList: []
}
},
//
formatFileList(field) {
return this.formData[field].map((file, index) => {
const fileExt = this.getFileExt(file)
const businessType = field === 'completionReportFileList' ? 'completion_report' : 'bid_notice'
return {
...file,
uid: file.uid || file.filePath || `${Date.now()}-${index}-${field}`,
name: this.getFileName(file),
status: file.status || 'success',
percentage: file.percentage || 100,
fileType: file.fileType || '2',
businessType
}
})
},
//
getFileName(file) {
const fullName = file.name || file.fileName || `未命名文件.${this.getFileExt(file)}`
const lastDotIndex = fullName.lastIndexOf('.')
return lastDotIndex > 0 ? fullName.substring(0, lastDotIndex) : fullName
return file.name || file.fileName || '未知文件'
},
//
getFileExt(file) {
if (!file) return ''
const fileName = file.name || file.fileName || ''
@ -191,22 +187,43 @@ export default {
return extMatch ? extMatch[1].toLowerCase() : ''
},
//
handleFileChange(field, files, type) {
const markedFiles = files.map(file => ({
...file,
businessType: type,
name: file.name || file.fileName || '未知文件'
}))
this.formData[field] = markedFiles
if (files instanceof Array) {
//
if (files.length > 0) {
//
this.replacingFiles[field] = false;
// OCR
if (field === 'completionReportFileList' && type === 'completion_report') {
this.handleOcrResult(markedFiles)
const markedFiles = files
.map(file => ({
...file,
businessType: type
}))
.slice(-1); // 1
//
if (this.formData[field].length > 0) {
const oldFile = this.formData[field][0];
const delPath = oldFile?.response?.fileRes?.filePath || oldFile?.filePath || null;
if (delPath && !this.formData.delFileList.includes(delPath)) {
this.formData.delFileList.push(delPath);
}
}
//
this.formData[field] = markedFiles;
// OCR
if (field === 'completionReportFileList' && type === 'completion_report' && markedFiles[0].response) {
this.handleOcrResult(markedFiles)
}
} else if (!this.replacingFiles[field]) {
//
this.formData[field] = [];
}
}
},
// OCR
handleOcrResult(files) {
if (!files?.length) return
@ -235,17 +252,16 @@ export default {
}
},
//
handleDelFile(field, file) {
const filePath = file.filePath || file.response?.fileRes?.uploadPath
if (filePath && !this.formData.delFileList.includes(filePath)) {
this.formData.delFileList.push(filePath)
//
this.replacingFiles[field] = true;
const delPath = file?.response?.fileRes?.filePath || file?.filePath || null;
if (delPath && !this.formData.delFileList.includes(delPath)) {
this.formData.delFileList.push(delPath);
}
//
this.formData[field] = []
},
//
disabledFutureDate(date) {
return date > new Date()
},
@ -265,7 +281,6 @@ export default {
}
},
//
validate() {
return new Promise((resolve, reject) => {
if (!this.$refs.completionForm) {
@ -288,7 +303,6 @@ export default {
})
},
//
resetForm() {
if (this.$refs.completionForm) {
this.$refs.completionForm.resetFields()
@ -300,9 +314,12 @@ export default {
completionTime: '',
delFileList: []
}
this.replacingFiles = {
completionReportFileList: false,
bidNoticeFileList: false
}
},
// OCR
addOcrRule() {
this.ocrRuleList.forEach(item => this.ocrRule(item))
},

View File

@ -112,7 +112,8 @@ export default {
name: item.fileName || '未知文件',
filePath: item.filePath || '',
lsFilePath: item.lsFilePath || '',
fileType: item.fileType || '2'
fileType: item.fileType || '2',
id: item.sourceId + ''
}))
},

View File

@ -16,7 +16,7 @@
<el-col :span="8">
<el-form-item label="合同附件" prop="fileList">
<UploadFile
:fileList="formatFileList()"
:fileList="formData.fileList"
:fileUploadRule="contractUploadRule"
@file-change="(files, type) => handleFileChange(files, type)"
@del-file="handleDelFile"
@ -24,6 +24,7 @@
:maxFileTips="maxFileTips"
type="contract"
:show-file-list="true"
:limitUploadNum="1"
/>
</el-form-item>
</el-col>
@ -42,7 +43,6 @@
</el-form-item>
</el-col>
<!-- 合同签订时间 -->
<el-col :span="8">
<el-form-item label="合同签订时间" prop="contractSigningTime">
<el-date-picker
@ -56,7 +56,6 @@
</el-form-item>
</el-col>
<!-- 合同金额 -->
<el-col :span="8">
<el-form-item label="合同金额" prop="contractAmount">
<el-input
@ -72,7 +71,6 @@
</el-col>
</el-row>
<!-- 建设地点 -->
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="建设地点" prop="constructionSite">
@ -81,12 +79,11 @@
placeholder="请输入建设地点"
class="form-control"
show-word-limit
maxlength="32"
maxlength="100"
></el-input>
</el-form-item>
</el-col>
<!-- 建设单位 -->
<el-col :span="8">
<el-form-item label="建设单位" prop="constructionUnit">
<el-input
@ -94,12 +91,11 @@
placeholder="请输入建设单位"
class="form-control"
show-word-limit
maxlength="32"
maxlength="100"
></el-input>
</el-form-item>
</el-col>
<!-- 建设单位联系方式 -->
<el-col :span="8">
<el-form-item label="建设单位联系方式" prop="constructionPhone">
<el-input
@ -137,14 +133,14 @@ export default {
"建设单位联系方式": "constructionPhone"
},
formData: {
fileList: [], //
fileList: [],
proName: '',
contractSigningTime: '',
contractAmount: '',
constructionSite: '',
constructionUnit: '',
constructionPhone: '',
delFileList: [] //
delFileList: []
},
rules: {
proName: [
@ -169,7 +165,8 @@ export default {
{ required: true, message: '请输入建设单位联系方式', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号码', trigger: 'blur' }
]
}
},
isReplacingFile: false //
}
},
computed: {
@ -188,43 +185,30 @@ export default {
}
},
methods: {
//
setFormData(data) {
// uid
const contractFiles = Array.isArray(data.fileList)
? data.fileList.filter(file => file && file.businessType === 'contract')
: []
.map(file => ({
name: file.name || file.fileName || '未知文件',
filePath: file.filePath,
lsFilePath: file.lsFilePath,
fileType: file.fileType || '2',
businessType: 'contract'
}))
: [];
this.formData = {
...data,
...(data || {}),
fileList: contractFiles,
delFileList: []
}
},
//
formatFileList() {
return this.formData.fileList.map((file, index) => {
const fileExt = this.getFileExt(file)
return {
...file,
uid: file.uid || file.filePath || `${Date.now()}-${index}`,
name: this.getFileName(file),
status: file.status || 'success',
percentage: file.percentage || 100,
fileType: file.fileType || '2',
businessType: 'contract'
}
})
},
//
getFileName(file) {
const fullName = file.name || file.fileName || `未命名文件.${this.getFileExt(file)}`
const lastDotIndex = fullName.lastIndexOf('.')
return lastDotIndex > 0 ? fullName.substring(0, lastDotIndex) : fullName
return file.name || file.fileName || '未知文件'
},
//
getFileExt(file) {
if (!file) return ''
const fileName = file.name || file.fileName || ''
@ -232,20 +216,31 @@ export default {
return extMatch ? extMatch[1].toLowerCase() : ''
},
//
handleFileChange(files, type) {
if (type === 'contract') {
const markedFiles = files.map(file => ({
...file,
businessType: 'contract',
name: file.name || file.fileName || '未知文件'
}))
this.formData.fileList = markedFiles
this.handleOcrResult(markedFiles)
if (type === 'contract' && files instanceof Array) {
//
if (files.length > 0) {
//
this.isReplacingFile = false;
const markedFiles = files.map(file => ({
...file,
businessType: 'contract'
})).slice(-1); // 1
this.formData.fileList = markedFiles;
// responseOCR
if (markedFiles.length > 0 && markedFiles[0].response) {
this.handleOcrResult(markedFiles);
}
} else if (!this.isReplacingFile) {
//
this.formData.fileList = [];
}
}
},
// OCR
handleOcrResult(files) {
if (!files || !Array.isArray(files) || files.length === 0) return
@ -259,7 +254,6 @@ export default {
const formField = this.ocrResultParams[key]
if (formField && chatRes[key]) {
let value = chatRes[key]
//
if (formField === 'contractAmount') {
value = value.replace(/[^\d.]/g, '')
}
@ -279,21 +273,20 @@ export default {
}
},
//
handleDelFile(file) {
const filePath = file.filePath || file.response?.fileRes?.uploadPath
if (filePath && !this.formData.delFileList.includes(filePath)) {
this.formData.delFileList.push(filePath)
//
this.isReplacingFile = true;
const delPath = file?.response?.fileRes?.filePath || file?.filePath || null;
if (delPath && !this.formData.delFileList.includes(delPath)) {
this.formData.delFileList.push(delPath);
}
this.formData.fileList = []
},
//
disabledFutureDate(date) {
return date > new Date()
},
//
validate() {
return new Promise((resolve, reject) => {
if (!this.$refs.contractForm) {
@ -311,7 +304,6 @@ export default {
})
},
//
resetForm() {
if (this.$refs.contractForm) {
this.$refs.contractForm.resetFields()
@ -326,16 +318,15 @@ export default {
constructionPhone: '',
delFileList: []
}
this.isReplacingFile = false
},
// OCR
addOcrRule() {
this.ocrRuleList.forEach(item => {
this.ocrRule(item)
})
},
// OCR
ocrRule(type) {
const foundItem = this.dict.type.identification_tag?.find(item => item.value === type)
if (!foundItem) {

View File

@ -153,7 +153,8 @@ export default {
name: item.fileName || '未知文件',
filePath: item.filePath || '', //
lsFilePath: item.lsFilePath || '', //
fileType: item.fileType || '2' // 2
fileType: item.fileType || '2', // 2
id: item.sourceId + ''
}))
}
}

View File

@ -13,22 +13,22 @@
label-position="top"
>
<!-- 团队业绩证明 -->
<!-- <el-row :gutter="24">-->
<!-- <el-col :span="8">-->
<!-- <el-form-item label="团队业绩证明" prop="performanceFileList">-->
<!-- <UploadFile-->
<!-- :fileList="formatFileList()"-->
<!-- @file-change="(files, type) => handleFileChange(files, type)"-->
<!-- @del-file="handleDelFile"-->
<!-- :uploadType="uploadType"-->
<!-- :maxFileTips="maxFileTips"-->
<!-- :limitUploadNum="3"-->
<!-- type="team_performance"-->
<!-- :show-file-list="true"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- </el-row>-->
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="团队业绩证明" prop="performanceFileList">
<UploadFile
ref="uploadFileComp"
:fileList="formData.performanceFileList"
@file-change="handleFileChange"
@del-file="handleDelFile"
:uploadType="uploadType"
:maxFileTips="maxFileTips"
:limitUploadNum="1"
type="team_performance"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 项目信息 -->
<el-row :gutter="24">
@ -107,6 +107,9 @@
<script>
import UploadFile from '@/views/common/UploadFile.vue'
//
const safeGetArray = (value) => Array.isArray(value) ? value : []
export default {
name: 'OtherInfo',
components: { UploadFile },
@ -125,8 +128,8 @@ export default {
proType: '',
voltageLevel: '',
projectOverview: '',
performanceFileList: [], //
delFileList: [] //
performanceFileList: [], // UploadFile
delFileList: [] // /
},
rules: {
projectManager: [
@ -143,7 +146,7 @@ export default {
{ max: 500, message: '项目概况不能超过500个字符', trigger: 'blur' }
],
performanceFileList: [
{ required: true, message: '请上传团队业绩证明', trigger: 'change' }
{ required: true, message: '请上传团队业绩证明', trigger: ['change', 'blur'] }
]
},
proTypeOptions: [],
@ -155,48 +158,24 @@ export default {
this.loadProjcetTypeDict()
},
methods: {
//
// getFileList
setFormData(data) {
const teamFiles = Array.isArray(data.performanceFileList)
? data.performanceFileList.filter(file => file && file.businessType === 'team_performance')
: []
const teamFiles = Array.isArray(data.fileList)
? data.fileList.filter(file => file && file.businessType === 'team_performance')
.map(file => ({
name: file.fileName,
filePath: file.filePath,
lsFilePath: file.lsFilePath,
fileType: file.fileType,
// uidUploadFile
}))
: [];
this.formData = {
...data,
performanceFileList: teamFiles,
delFileList: []
}
},
// UploadFile
formatFileList() {
return this.formData.performanceFileList.map((file, index) => {
const fileExt = this.getFileExt(file)
return {
...file,
uid: file.uid || file.filePath || `${Date.now()}-${index}`,
name: this.getFileName(file), //
status: file.status || 'success',
percentage: file.percentage || 100,
fileType: file.fileType || '2',
businessType: 'team_performance'
}
})
},
//
getFileName(file) {
const fullName = file.name || file.fileName || `未命名文件.${this.getFileExt(file)}`
const lastDotIndex = fullName.lastIndexOf('.')
return lastDotIndex > 0 ? fullName.substring(0, lastDotIndex) : fullName
},
//
getFileExt(file) {
if (!file) return ''
const fileName = file.name || file.fileName || ''
const extMatch = fileName.match(/\.([a-zA-Z0-9]+)$/)
return extMatch ? extMatch[1].toLowerCase() : ''
};
},
//
@ -222,32 +201,22 @@ export default {
})
},
//
// response
handleFileChange(files, type) {
if (type === 'team_performance') {
const markedFiles = files.map(file => ({
...file,
businessType: 'team_performance',
name: file.name || file.fileName || '未知文件'
}))
this.formData.performanceFileList = markedFiles
if (markedFiles.length > 0) {
this.$message.success(`成功上传${markedFiles.length}个团队业绩证明文件`)
if (type === 'team_performance' && files instanceof Array) {
//
if (files.length > 0 && files[0].response) {
this.formData.performanceFileList = files;
}
}
},
//
//
handleDelFile(file) {
const filePath = file.filePath || file.response?.fileRes?.uploadPath
if (filePath && !this.formData.delFileList.includes(filePath)) {
this.formData.delFileList.push(filePath)
const delPath = file?.response?.fileRes?.filePath || file?.filePath || null;
if (delPath) {
this.formData.delFileList.push(delPath);
}
//
this.formData.performanceFileList = this.formData.performanceFileList.filter(
item => item.uid !== file.uid
)
},
//
@ -270,17 +239,22 @@ export default {
//
resetForm() {
if (this.$refs.otherForm) {
this.$refs.otherForm.resetFields()
}
this.formData = {
projectManager: '',
proType: '',
voltageLevel: '',
projectOverview: '',
performanceFileList: [],
delFileList: []
}
this.$nextTick(() => {
if (this.$refs.uploadFileComp) {
this.$refs.uploadFileComp.clearFiles();
}
if (this.$refs.otherForm) {
this.$refs.otherForm.resetFields()
}
this.formData = {
projectManager: '',
proType: '',
voltageLevel: '',
projectOverview: '',
performanceFileList: [],
delFileList: []
}
});
}
}
}

View File

@ -11,18 +11,18 @@
class="detail-form"
>
<!-- <el-row :gutter="24">-->
<!-- <el-col :span="12">-->
<!-- <el-form-item label="团队业绩证明" class="form-item">-->
<!-- <FileOrImageDisplay-->
<!-- :label="''"-->
<!-- :file="form.fileList[0]"-->
<!-- :image-url="form.url"-->
<!-- />-->
<!-- <span v-if="form.fileList.length === 0" class="no-file-tip">无附件</span>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- </el-row>-->
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="团队业绩证明" class="form-item">
<FileOrImageDisplay
:label="''"
:file="form.fileList[0]"
:image-url="form.url"
/>
<span v-if="form.fileList.length === 0" class="no-file-tip">无附件</span>
</el-form-item>
</el-col>
</el-row>
<!-- 项目经理 + 项目类型使用输入框禁用 -->
<el-row :gutter="24">
@ -126,14 +126,15 @@ export default {
getFileList() {
// detailData
const rawFileList = Array.isArray(this.detailData.fileList) ? this.detailData.fileList : []
const performanceFiles = rawFileList.filter(file => file && file.businessType === 'performance')
const performanceFiles = rawFileList.filter(file => file && file.businessType === 'team_performance')
//
return performanceFiles.map(item => ({
name: item.fileName || '未知文件',
filePath: item.filePath || '', //
lsFilePath: item.lsFilePath || '', //
fileType: item.fileType || '2' // 2
fileType: item.fileType || '2', // 2
id: item.sourceId + ''
}))
}
}

View File

@ -106,7 +106,7 @@ export default {
dicts: ['identification_tag'],
data() {
return {
uploadType: 'doc、docx',
uploadType: 'jpg、png、pdf',
maxFileTips: '100MB',
ocrRuleList: ['qualification_certificate'], // OCR
fileUploadList: [],

View File

@ -221,7 +221,8 @@ export default {
name: item.fileName || item.name || '未知文件',
filePath: item.filePath || '',
lsFilePath: item.lsFilePath || item.fileUrl || '',
fileType: item.fileType || '2'
fileType: item.fileType || '2',
id: item.sourceId + ''
}))
this.form = {

View File

@ -45,7 +45,7 @@
<!-- 资质卡片网格 -->
<div class="qualification-grid">
<!-- 新增资质卡片 -->
<div class="grid-item">
<div class="qualification-card-wrapper">
<div
class="qualification-card create-card"
@click="handleAdd"
@ -59,7 +59,7 @@
</div>
<!-- 资质信息卡片及下方的过期提示 -->
<div class="grid-item" v-for="item in qualificationList" :key="item.qualificationId">
<div class="qualification-card-wrapper" v-for="item in qualificationList" :key="item.qualificationId">
<!-- 卡片本身 -->
<div class="qualification-card">
<div class="qualification-header">
@ -114,8 +114,8 @@
</div>
<!-- 过期提示 -->
<div class="expired-tag" v-if="item.isExpired">
<i class="el-icon-warning"></i> {{ item.certificateName || '证书' }}已过期
<div class="expired-tags" v-if="item.isExpired">
<span class="expired-text">证书已过期</span>
</div>
</div>
</div>
@ -220,7 +220,6 @@ export default {
methods: {
loadQualificationTypeDict() {
// getDicts qualification_type
this.getDicts('qualification_type').then(response => {
this.qualificationTypeOptions = response.data.map(item => ({
@ -459,36 +458,35 @@ export default {
.qualification-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
gap: 24px;
margin-bottom: 30px;
max-height: calc(100vh - 280px);
overflow-y: auto;
padding: 10px;
padding: 20px 6px 20px 0px;
align-items: start;
&::-webkit-scrollbar {
width: 8px;
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background: #a8a8a8;
background: rgba(0, 0, 0, 0.3);
}
}
}
//
.grid-item {
//
.qualification-card-wrapper {
display: flex;
flex-direction: column;
gap: 8px; //
}
.qualification-card {
@ -645,19 +643,22 @@ export default {
}
}
// qualification-card
.expired-tag {
padding: 6px 0;
text-align: center;
background: transparent; //
color: #ff4d4f; //
font-size: 12px;
font-weight: 500;
width: 100%; //
//
.expired-tags {
padding: 8px 12px;
flex-shrink: 0;
i {
margin-right: 5px;
.expired-text {
display: block;
color: #db3e29;
font-size: 14px;
line-height: 1.5;
word-break: break-all;
i {
margin-right: 5px;
font-size: 14px;
}
}
}