采购需求去掉横向滚动问题

This commit is contained in:
2026-06-26 13:29:09 +08:00
parent c6a3b6723f
commit 046f4c5e1b
6 changed files with 327 additions and 83 deletions

View File

@@ -53,40 +53,47 @@
</el-col>
</el-row>
<!-- 拨款单据附件 PDF 电子发票 -->
<!-- 拨款单据附件PDF 电子发票 / 单据图片可多文件 -->
<el-form-item label="拨款单据附件" prop="accessoryApplyIds">
<file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50"
:file-type="['pdf']" multiple
:file-type="['pdf', 'jpg', 'jpeg', 'png']" multiple
@delete="onFileDelete" />
<div class="hint-text">
仅支持 PDF 电子发票数电票 / 电子普通发票 / 电子专用发票上传后自动解析金额与明细<br/>
扫描件 / 图片票 / 纸质票请先在开票平台下载 PDF 原件再上传
可上传多份单据PDF 电子发票数电票/电子普通/专用发票走文字提取<br/>
单据图片jpg/png可不是发票样式如收据/车票/小票将自动调用 AI 识别金额与标题
</div>
</el-form-item>
<!-- OCR 识别中提示 -->
<!-- 识别中提示 -->
<div v-if="anyOcrLoading" class="ocr-thinking">
<i class="el-icon-loading"></i>
<span>正在解析发票 PDF</span>
<span>正在识别单据</span>
</div>
<!-- 发票明细条目表 -->
<!-- 单据明细条目表 -->
<div class="block-title">
拨款明细
<span class="block-title-hint">上传 PDF 后自动解析解析失败或无发票时可手动添加</span>
单据明细
<span class="block-title-hint">上传后自动识别标题与金额事由请手动填写可手动添加条目</span>
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addManualItem" style="margin-left:12px;vertical-align:middle">手动添加条目</el-button>
</div>
<div class="invoice-table" v-if="invoiceItems.length">
<div class="invoice-table-header">
<span class="col-reason">事由说明</span>
<span class="col-title">标题</span>
<span class="col-reason">事由</span>
<span class="col-amount">金额</span>
<span class="col-file">附件</span>
<span class="col-action"></span>
</div>
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
<el-input
v-model="item.itemName"
placeholder="识别标题(可编辑)"
size="small"
class="col-title"
/>
<el-input
v-model="item.reason"
:placeholder="item.itemName || '请填写事由'"
placeholder="请手动填写事由"
size="small"
class="col-reason"
/>
@@ -104,7 +111,7 @@
</el-tooltip>
<el-upload v-else :action="uploadFileUrl" :headers="uploadHeaders" :show-file-list="false"
:data="{ isPublic: 1 }" :on-success="(res) => onRowFileSuccess(res, idx)"
accept=".pdf" class="row-upload">
accept=".pdf,.jpg,.jpeg,.png" class="row-upload">
<el-button size="mini" type="text" icon="el-icon-paperclip"></el-button>
</el-upload>
</div>
@@ -375,26 +382,27 @@ export default {
const res = await ocrAppropriationInvoice(ossId)
if (res.code === 200 && res.data) {
const { items, totalAmount, sellerName, invoiceDate, invoiceType } = res.data
// 拼接发票头部信息作为事由前缀:发票类型 · 销售方 · 开票日期
// 发票头部信息类型 · 销售方 · 开票日期作为标题前缀PDF 票据才有
const prefix = [invoiceType, sellerName, invoiceDate].filter(Boolean).join(' · ')
if (items && items.length) {
const startIdx = this.invoiceItems.length
items.forEach((item, i) => {
const reason = [prefix, item.itemName].filter(Boolean).join(' / ')
// 标题 = 识别结果(图片单据直接为 AI 标题PDF 票据拼接发票头部)
const title = [prefix, item.itemName].filter(Boolean).join(' / ')
this.invoiceItems.push({
ossId: ossId,
itemName: item.itemName || '',
reason,
itemName: title,
reason: '', // 事由由用户手动填写
amount: item.amount || 0,
sortNo: startIdx + i
})
})
} else if (totalAmount) {
// 没有明细时用总金额创建一条,事由取发票头部信息
// 没有明细时用总金额创建一条,标题取发票头部信息
this.invoiceItems.push({
ossId: ossId,
itemName: sellerName || '',
reason: prefix || '',
itemName: prefix || sellerName || '',
reason: '',
amount: totalAmount,
sortNo: this.invoiceItems.length
})
@@ -408,10 +416,10 @@ export default {
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
}
} catch (e) {
console.error('[Invoice] 解析失败', e)
console.error('[Invoice] 识别失败', e)
const msg = (e && e.msg) || (e && e.message) || ''
this.$message.warning(
'发票解析失败,请确认上传的是开票平台下载的正规 PDF 电子发票(数电票 / 电子普通发票 / 电子专用发票)。' +
'单据识别失败,已为你添加空白条目,可手动填写标题、事由与金额。' +
(msg ? ' 错误信息:' + msg : '')
)
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
@@ -551,8 +559,8 @@ export default {
invoiceItems: this.invoiceItems.map((item, i) => ({
ossId: item.ossId || null,
sortNo: i,
itemName: item.itemName || '',
reason: item.reason || item.itemName || '',
itemName: item.itemName || '', // 标题
reason: item.reason || '', // 事由(手写)
amount: item.amount || 0
}))
}
@@ -688,11 +696,13 @@ export default {
&:last-child { border-bottom: none; }
}
.col-title { flex: 1; }
.col-reason { flex: 1; }
.col-amount { width: 140px; flex-shrink: 0; }
.col-file { width: 64px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.col-action { width: 32px; flex-shrink: 0; display: flex; justify-content: center; }
.invoice-table-header .col-title { flex: 1; }
.invoice-table-header .col-reason { flex: 1; }
.invoice-table-header .col-amount { width: 140px; }
.invoice-table-header .col-file { width: 64px; }

View File

@@ -26,11 +26,11 @@
<!-- 发票明细含附件下载 -->
<template v-if="detail.invoiceItems && detail.invoiceItems.length">
<div class="block-title">发票明细</div>
<div class="block-title">单据明细</div>
<el-card class="inner-card" shadow="never">
<el-table :data="detail.invoiceItems" border size="small" style="width:100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="itemName" label="项目名称" min-width="120" />
<el-table-column prop="itemName" label="标题" min-width="160" />
<el-table-column prop="reason" label="事由" min-width="160" />
<el-table-column prop="amount" label="金额(元)" width="110" align="right">
<template slot-scope="{ row }">¥{{ row.amount }}</template>

View File

@@ -53,40 +53,47 @@
</el-col>
</el-row>
<!-- 报销单据附件 PDF 电子发票 -->
<!-- 报销单据附件PDF 电子发票 / 单据图片可多文件 -->
<el-form-item label="报销单据附件" prop="accessoryApplyIds">
<file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50"
:file-type="['pdf']" multiple
:file-type="['pdf', 'jpg', 'jpeg', 'png']" multiple
@delete="onFileDelete" />
<div class="hint-text">
仅支持 PDF 电子发票数电票/电子普通发票/电子专用发票上传后自动解析金额与明细<br/>
扫描件 / 图片票 / 纸质票请先在开票平台下载 PDF 原件再上传
可上传多份单据PDF 电子发票数电票/电子普通/专用发票走文字提取<br/>
单据图片jpg/png可不是发票样式如收据/车票/小票将自动调用 AI 识别金额与标题
</div>
</el-form-item>
<!-- 发票解析中提示 -->
<!-- 识别中提示 -->
<div v-if="anyOcrLoading" class="ocr-thinking">
<i class="el-icon-loading"></i>
<span>正在解析发票 PDF</span>
<span>正在识别单据</span>
</div>
<!-- 发票明细条目表 -->
<!-- 单据明细条目表 -->
<div class="block-title">
发票明细
<span class="block-title-hint">上传 PDF 后自动解析解析失败或无发票时可手动添加</span>
单据明细
<span class="block-title-hint">上传后自动识别标题与金额事由请手动填写可手动添加条目</span>
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addManualItem" style="margin-left:12px;vertical-align:middle">手动添加条目</el-button>
</div>
<div class="invoice-table" v-if="invoiceItems.length">
<div class="invoice-table-header">
<span class="col-reason">事由说明</span>
<span class="col-title">标题</span>
<span class="col-reason">事由</span>
<span class="col-amount">金额</span>
<span class="col-file">附件</span>
<span class="col-action"></span>
</div>
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
<el-input
v-model="item.itemName"
placeholder="识别标题(可编辑)"
size="small"
class="col-title"
/>
<el-input
v-model="item.reason"
:placeholder="item.itemName || '请填写事由'"
placeholder="请手动填写事由"
size="small"
class="col-reason"
/>
@@ -104,7 +111,7 @@
</el-tooltip>
<el-upload v-else :action="uploadFileUrl" :headers="uploadHeaders" :show-file-list="false"
:data="{ isPublic: 1 }" :on-success="(res) => onRowFileSuccess(res, idx)"
accept=".pdf" class="row-upload">
accept=".pdf,.jpg,.jpeg,.png" class="row-upload">
<el-button size="mini" type="text" icon="el-icon-paperclip"></el-button>
</el-upload>
</div>
@@ -350,26 +357,27 @@ export default {
const res = await ocrReimburseInvoice(ossId)
if (res.code === 200 && res.data) {
const { items, totalAmount, sellerName, invoiceDate, invoiceType } = res.data
// 拼接发票头部信息作为事由前缀:发票类型 · 销售方 · 开票日期
// 发票头部信息类型 · 销售方 · 开票日期作为标题前缀PDF 票据才有
const prefix = [invoiceType, sellerName, invoiceDate].filter(Boolean).join(' · ')
if (items && items.length) {
const startIdx = this.invoiceItems.length
items.forEach((item, i) => {
const reason = [prefix, item.itemName].filter(Boolean).join(' / ')
// 标题 = 识别结果(图片单据直接为 AI 标题PDF 票据拼接发票头部)
const title = [prefix, item.itemName].filter(Boolean).join(' / ')
this.invoiceItems.push({
ossId: ossId,
itemName: item.itemName || '',
reason,
itemName: title,
reason: '', // 事由由用户手动填写
amount: item.amount || 0,
sortNo: startIdx + i
})
})
} else if (totalAmount) {
// 没有明细时用总金额创建一条,事由取发票头部信息
// 没有明细时用总金额创建一条,标题取发票头部信息
this.invoiceItems.push({
ossId: ossId,
itemName: sellerName || '',
reason: prefix || '',
itemName: prefix || sellerName || '',
reason: '',
amount: totalAmount,
sortNo: this.invoiceItems.length
})
@@ -383,10 +391,10 @@ export default {
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
}
} catch (e) {
console.error('[Invoice] 解析失败', e)
console.error('[Invoice] 识别失败', e)
const msg = (e && e.msg) || (e && e.message) || ''
this.$message.warning(
'发票解析失败,请确认上传的是开票平台下载的正规 PDF 电子发票(数电票 / 电子普通发票 / 电子专用发票)。' +
'单据识别失败,已为你添加空白条目,可手动填写标题、事由与金额。' +
(msg ? ' 错误信息:' + msg : '')
)
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
@@ -523,8 +531,8 @@ export default {
invoiceItems: this.invoiceItems.map((item, i) => ({
ossId: item.ossId || null,
sortNo: i,
itemName: item.itemName || '',
reason: item.reason || item.itemName || '',
itemName: item.itemName || '', // 标题
reason: item.reason || '', // 事由(手写)
amount: item.amount || 0
}))
}
@@ -661,6 +669,10 @@ export default {
&:last-child { border-bottom: none; }
}
.col-title {
flex: 1;
}
.col-reason {
flex: 1;
}
@@ -685,6 +697,7 @@ export default {
justify-content: center;
}
.invoice-table-header .col-title { flex: 1; }
.invoice-table-header .col-reason { flex: 1; }
.invoice-table-header .col-amount { width: 140px; }
.invoice-table-header .col-file { width: 64px; }

View File

@@ -17,11 +17,11 @@
<!-- 发票明细含附件下载 -->
<template v-if="detail.invoiceItems && detail.invoiceItems.length">
<div class="block-title">发票明细</div>
<div class="block-title">单据明细</div>
<el-card class="inner-card" shadow="never">
<el-table :data="detail.invoiceItems" border size="small" style="width:100%">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="itemName" label="项目名称" min-width="120" />
<el-table-column prop="itemName" label="标题" min-width="160" />
<el-table-column prop="reason" label="事由" min-width="160" />
<el-table-column prop="amount" label="金额(元)" width="110" align="right">
<template slot-scope="{ row }">¥{{ row.amount }}</template>

View File

@@ -66,13 +66,47 @@
<!-- 新增提示组件 -->
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="需求标题" align="center" prop="title" min-width="160" show-overflow-tooltip />
<el-table-column label="需求方" align="center" prop="requesterNickName" width="100" show-overflow-tooltip />
<el-table-column label="负责人" align="center" prop="ownerNickName" width="100" show-overflow-tooltip />
<el-table-column label="关联项目" align="center" prop="projectName" min-width="160" show-overflow-tooltip />
<el-table-column label="采购物料" align="left" min-width="240">
<el-table v-loading="loading" :data="requirementsList" style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="45" align="center" />
<el-table-column label="操作" align="center" width="130" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="showDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
v-if="scope.row.status === 1">完成</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-if="scope.row.status === 0">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
<el-table-column label="需求标题" align="center" prop="title" min-width="130">
<template slot-scope="{ row }">
<el-tooltip effect="dark" :content="row.title || ''" placement="top" :disabled="!row.title">
<div class="cell-2line">{{ row.title }}</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="需求方" align="center" prop="requesterNickName" width="80">
<template slot-scope="{ row }">
<el-tooltip effect="dark" :content="row.requesterNickName || ''" placement="top" :disabled="!row.requesterNickName">
<div class="cell-2line">{{ row.requesterNickName }}</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="负责人" align="center" prop="ownerNickName" width="80">
<template slot-scope="{ row }">
<el-tooltip effect="dark" :content="row.ownerNickName || ''" placement="top" :disabled="!row.ownerNickName">
<div class="cell-2line">{{ row.ownerNickName }}</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="关联项目" align="center" prop="projectName" min-width="130">
<template slot-scope="{ row }">
<el-tooltip effect="dark" :content="row.projectName || ''" placement="top" :disabled="!row.projectName">
<div class="cell-2line">{{ row.projectName }}</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="采购物料" align="left" min-width="170">
<template slot-scope="{ row }">
<template v-if="row.materials && row.materials.length">
<div v-for="m in row.materials" :key="m.id" class="mat-row">
@@ -85,24 +119,21 @@
<span v-else style="color:#c0c4cc;">未关联</span>
</template>
</el-table-column>
<el-table-column label="需求描述" align="center" prop="description" min-width="200" show-overflow-tooltip>
<el-table-column label="需求描述" align="center" prop="description" min-width="150">
<template slot-scope="{ row }">
<span v-if="row.description" class="copyable-text" @click="copyText(row.description)"
title="点击复制">{{ row.description }}</span>
<el-tooltip v-if="row.description" effect="dark" :content="row.description" placement="top">
<div class="cell-2line copyable-text" @click="copyText(row.description)" title="点击复制">{{ row.description }}</div>
</el-tooltip>
<span v-else style="color:#c0c4cc;"></span>
</template>
</el-table-column>
<el-table-column label="开始时间" align="center" prop="createTime" width="180">
<el-table-column label="起止时间" align="center" width="110">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
<div class="date-cell">{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</div>
<div class="date-cell deadline">{{ parseTime(scope.row.deadline, '{y}-{m}-{d}') }}</div>
</template>
</el-table-column>
<el-table-column label="截止日期" align="center" prop="deadline" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.deadline, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="剩余时间" align="center">
<el-table-column label="剩余时间" align="center" width="90">
<template slot-scope="scope">
<el-tag v-if="scope.row.status !== 2" :type="getRemainTagType(scope.row.deadline)"
:style="getRemainTagStyle(scope.row.deadline)">
@@ -131,7 +162,7 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="附件" align="center" prop="accessoryFiles" width="180">
<el-table-column label="附件" align="center" prop="accessoryFiles" width="120">
<template slot-scope="{ row }">
<template v-if="parseAccessoryFiles(row.accessoryFiles).length">
<el-tooltip v-for="f in parseAccessoryFiles(row.accessoryFiles)" :key="f.ossId"
@@ -145,16 +176,6 @@
<span v-else style="color:#c0c4cc;">无</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
v-if="scope.row.status === 1">完成</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
v-if="scope.row.status === 0">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="showDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@@ -857,6 +878,22 @@ export default {
</script>
<style lang="scss" scoped>
.cell-2line {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
line-height: 1.4;
max-height: 2.8em;
white-space: normal;
}
.date-cell {
font-size: 12px;
line-height: 1.6;
&.deadline { color: #e6a23c; }
}
.req-toolbar {
display: flex;
align-items: center;