<template>
|
<view class="sales-account">
|
<!-- 页面头部 -->
|
<van-nav-bar
|
title="开票台账"
|
left-text="返回"
|
left-arrow
|
@click-left="goBack"
|
fixed
|
placeholder
|
/>
|
|
<!-- 搜索和筛选区域(保持与销售台账风格一致) -->
|
<view class="search-filter-section">
|
<view class="search-bar">
|
<view class="search-input">
|
<input
|
class="search-text"
|
placeholder="客户名称/销售合同号"
|
v-model="searchForm.searchText"
|
confirm-type="search"
|
@confirm="handleQuery"
|
/>
|
</view>
|
<!-- <view class="filter-button" @click="showFilter = true">-->
|
<!-- <up-icon name="list" size="24" color="#999"></up-icon>-->
|
<!-- </view>-->
|
<view class="filter-button" @click="handleQuery">
|
<up-icon name="search" size="24" color="#999"></up-icon>
|
</view>
|
</view>
|
</view>
|
|
<!-- 列表区域 -->
|
<view class="ledger-list" v-if="total > 0">
|
<view v-for="(item, index) in ledgerList" :key="index">
|
<view class="ledger-item">
|
<view class="item-header">
|
<view class="item-left">
|
<view class="document-icon">
|
<up-icon name="file-text" size="16" color="#ffffff"></up-icon>
|
</view>
|
<text class="item-id">{{ item.salesContractNo }}</text>
|
</view>
|
</view>
|
<up-divider></up-divider>
|
<view class="item-details">
|
<view class="detail-row">
|
<text class="detail-label">客户名称</text>
|
<text class="detail-value">{{ item.customerName }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">客户合同号</text>
|
<text class="detail-value">{{ item.customerContractNo }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">项目</text>
|
<text class="detail-value">{{ item.projectName }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">产品大类</text>
|
<text class="detail-value">{{ item.productCategory }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">规格型号</text>
|
<text class="detail-value">{{ item.specificationModel }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">发票号</text>
|
<text class="detail-value">{{ item.invoiceNo || '-' }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">发票金额(元)</text>
|
<text class="detail-value highlight">{{ formatAmount(item.invoiceTotal) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">税率(%)</text>
|
<text class="detail-value">{{ item.taxRate }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">录入人</text>
|
<text class="detail-value">{{ item.invoicePerson }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">录入日期</text>
|
<text class="detail-value">{{ formatDateTime(item.createTime) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">开票日期</text>
|
<text class="detail-value">{{ item.invoiceDate || '-' }}</text>
|
</view>
|
</view>
|
<view class="action-buttons">
|
<van-button
|
type="primary"
|
size="small"
|
class="action-btn"
|
:disabled="item.invoicePerson !== userStore.nickName"
|
@click="openEdit(item)"
|
>
|
编辑
|
</van-button>
|
<van-button
|
type="danger"
|
size="small"
|
class="action-btn"
|
:disabled="item.invoicePerson !== userStore.nickName"
|
@click="handleDelete(item)"
|
>
|
删除
|
</van-button>
|
<van-button
|
type="default"
|
size="small"
|
class="action-btn"
|
v-if="item.invoiceFileName"
|
@click="openFileActions(item.commonFiles || [])"
|
>
|
查看附件
|
</van-button>
|
<van-button
|
type="primary"
|
size="small"
|
plain
|
class="action-btn"
|
v-else
|
:disabled="item.invoicePerson !== userStore.nickName"
|
@click="openUpload(item)"
|
>
|
上传
|
</van-button>
|
</view>
|
</view>
|
</view>
|
</view>
|
<view v-else class="no-data">
|
<text>暂无开票台账数据</text>
|
</view>
|
|
<!-- 筛选弹窗 -->
|
<van-popup v-model:show="showFilter" position="bottom" round>
|
<view class="filter-popup">
|
<van-cell-group title="筛选条件" inset>
|
<van-field
|
label="开票日期"
|
readonly
|
@click="showInvoiceRange = true"
|
:placeholder="invoiceRangeLabel || '请选择日期范围'"
|
/>
|
<van-field
|
label="录入日期"
|
readonly
|
@click="showCreateDatePicker = true"
|
:placeholder="searchForm.createTimeStart || '请选择录入日期'"
|
/>
|
<view class="switch-row">
|
<text class="switch-label">不显示有发票行</text>
|
<van-switch v-model="searchForm.status" size="20" />
|
</view>
|
</van-cell-group>
|
<view class="filter-actions">
|
<van-button @click="resetFilter">重置</van-button>
|
<van-button type="primary" @click="confirmFilter">确定</van-button>
|
</view>
|
</view>
|
</van-popup>
|
|
<!-- 日历:开票日期范围 -->
|
<van-popup v-model:show="showInvoiceRange" position="bottom">
|
<van-calendar
|
title="选择开票日期范围"
|
type="range"
|
color="#2979ff"
|
@confirm="onInvoiceRangeConfirm"
|
@cancel="showInvoiceRange = false"
|
/>
|
</van-popup>
|
|
<!-- 日期:录入日期 -->
|
<van-popup v-model:show="showCreateDatePicker" position="bottom">
|
<van-date-picker
|
v-model="currentCreateDate"
|
title="选择录入日期"
|
@confirm="onCreateDateConfirm"
|
@cancel="showCreateDatePicker = false"
|
/>
|
</van-popup>
|
|
|
|
<!-- 单行上传弹窗(无表单) -->
|
<van-popup v-model:show="showUpload" position="bottom" round>
|
<view class="upload-container">
|
<van-cell-group title="上传附件(仅支持 pdf,最大10MB,最多10个)" inset>
|
<van-uploader
|
accept="*"
|
multiple
|
:max-count="10"
|
:after-read="afterReadRowUpload"
|
:before-read="beforeReadPdf"
|
/>
|
<view class="uploaded-list" v-if="fileList.length">
|
<view class="uploaded-item" v-for="(f, idx) in fileList" :key="idx">
|
<text class="file-name">{{ f.name || getFileNameFromUrl(f.url) }}</text>
|
<van-button size="mini" type="danger" plain @click="removeUploaded(idx)">移除</van-button>
|
</view>
|
</view>
|
</van-cell-group>
|
<view class="filter-actions">
|
<van-button @click="showUpload = false">取消</van-button>
|
<van-button type="primary" @click="confirmUpload">确认</van-button>
|
</view>
|
</view>
|
</van-popup>
|
|
<!-- 附件列表选择 -->
|
<van-action-sheet v-model:show="showFileSheet" :actions="fileActions" cancel-text="取消" close-on-click-action @select="onSelectFile" />
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted } from 'vue'
|
import dayjs from 'dayjs'
|
import { showToast, showLoadingToast, closeToast } from 'vant'
|
import useUserStore from '@/store/modules/user'
|
import { getToken } from '@/utils/auth'
|
import config from '@/config.js'
|
import {
|
registrationProductPage,
|
commitFile,
|
delInvoiceLedgerByRegProductId
|
} from '@/api/salesManagement/invoiceLedger.js'
|
|
const userStore = useUserStore()
|
|
// 列表与查询
|
const ledgerList = ref([])
|
const total = ref(0)
|
const page = reactive({ current: -1, size: -1 })
|
const searchForm = reactive({
|
searchText: '',
|
status: false,
|
createTimeStart: ''
|
})
|
|
// 顶部交互
|
const showFilter = ref(false)
|
const showInvoiceRange = ref(false)
|
const showCreateDatePicker = ref(false)
|
const invoiceRangeLabel = ref('')
|
const currentCreateDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()])
|
|
const currentId = ref('')
|
const fileList = ref([]) // 行上传或通用上传列表
|
|
// 行上传弹窗
|
const showUpload = ref(false)
|
|
// 附件查看
|
const showFileSheet = ref(false)
|
const fileActions = ref([])
|
let currentFilesToOpen = []
|
|
const formatAmount = (val) => {
|
if (val === undefined || val === null || val === '') return '0.00'
|
const num = Number(val)
|
if (Number.isNaN(num)) return '0.00'
|
return num.toFixed(2)
|
}
|
const formatDateTime = (val) => {
|
if (!val) return ''
|
return dayjs(val).format('YYYY-MM-DD HH:mm:ss')
|
}
|
|
const goBack = () => {
|
uni.navigateBack()
|
}
|
|
const handleQuery = () => {
|
getList()
|
}
|
|
const getList = async () => {
|
try {
|
showLoadingToast({ message: '加载中...' })
|
const { invoiceDate, ...rest } = searchForm
|
const res = await registrationProductPage({ ...rest, ...page })
|
// 兼容不同返回结构
|
const records = res?.data?.records || res?.records || res?.data || []
|
const totalVal = res?.data?.total || res?.total || records.length || 0
|
ledgerList.value = records
|
total.value = totalVal
|
closeToast()
|
} catch (e) {
|
closeToast()
|
showToast('获取列表失败')
|
}
|
}
|
|
// 筛选逻辑
|
const resetFilter = () => {
|
searchForm.searchText = ''
|
searchForm.status = false
|
const start = dayjs().startOf('month').format('YYYY-MM-DD')
|
const end = dayjs().endOf('month').format('YYYY-MM-DD')
|
searchForm.invoiceDate = [start, end]
|
searchForm.invoiceDateStart = start
|
searchForm.invoiceDateEnd = end
|
searchForm.createTimeStart = ''
|
invoiceRangeLabel.value = ''
|
}
|
const confirmFilter = () => {
|
showFilter.value = false
|
getList()
|
}
|
const onInvoiceRangeConfirm = (e) => {
|
// e 为 [start, end] 的 Date 对象或字符串,uni-app 下 Vant Calendar 返回时间戳数组
|
try {
|
let start, end
|
if (Array.isArray(e)) {
|
const [s, ed] = e
|
start = dayjs(s).format('YYYY-MM-DD')
|
end = dayjs(ed).format('YYYY-MM-DD')
|
} else if (e && e.detail && Array.isArray(e.detail)) {
|
const [s, ed] = e.detail
|
start = dayjs(s).format('YYYY-MM-DD')
|
end = dayjs(ed).format('YYYY-MM-DD')
|
}
|
searchForm.invoiceDateStart = start
|
searchForm.invoiceDateEnd = end
|
invoiceRangeLabel.value = `${start} 至 ${end}`
|
showInvoiceRange.value = false
|
} catch (err) {
|
showInvoiceRange.value = false
|
}
|
}
|
const onCreateDateConfirm = ({ selectedValues }) => {
|
try {
|
searchForm.createTimeStart = selectedValues.join('-')
|
currentCreateDate.value = selectedValues
|
showCreateDatePicker.value = false
|
} catch (err) {
|
showCreateDatePicker.value = false
|
}
|
}
|
|
// 编辑逻辑改为跳转新页面
|
const openEdit = (row) => {
|
try {
|
uni.setStorageSync('invoiceLedgerEditRow', JSON.stringify(row))
|
uni.navigateTo({ url: '/pages/sales/invoiceLedger/detail' })
|
} catch (e) {
|
showToast('跳转失败')
|
}
|
}
|
|
// 删除
|
const handleDelete = (row) => {
|
uni.showModal({
|
title: '删除确认',
|
content: '该发票台账将被删除,是否确认删除?',
|
success: async (res) => {
|
if (res.confirm) {
|
try {
|
showLoadingToast({ message: '处理中...' })
|
await delInvoiceLedgerByRegProductId(row.id)
|
closeToast()
|
showToast('删除成功')
|
getList()
|
} catch (e) {
|
closeToast()
|
showToast('删除失败,请重试')
|
}
|
}
|
}
|
})
|
}
|
|
// 行上传
|
const openUpload = (row) => {
|
currentId.value = row.id
|
fileList.value = []
|
showUpload.value = true
|
}
|
const confirmUpload = async () => {
|
try {
|
const payload = { fileList: fileList.value, id: currentId.value }
|
showLoadingToast({ message: '提交中...' })
|
await commitFile(payload)
|
closeToast()
|
showToast('提交成功')
|
showUpload.value = false
|
fileList.value = []
|
currentId.value = ''
|
getList()
|
} catch (e) {
|
closeToast()
|
showToast('提交失败,请重试')
|
}
|
}
|
|
// 上传相关
|
const beforeReadPdf = (file) => {
|
// 兼容多文件
|
const files = Array.isArray(file) ? file : [file]
|
for (const f of files) {
|
const sizeOk = f.size <= 10 * 1024 * 1024
|
const ext = (f.name || '').split('.').pop()?.toLowerCase()
|
if (ext !== 'pdf') {
|
showToast('仅支持pdf文件')
|
return false
|
}
|
if (!sizeOk) {
|
showToast('上传文件大小不能超过10MB')
|
return false
|
}
|
}
|
return true
|
}
|
|
const uploadSingleFile = async (fileObj) => {
|
return new Promise((resolve, reject) => {
|
showLoadingToast({ message: '正在上传...' })
|
uni.uploadFile({
|
url: config.baseUrl + '/invoiceLedger/uploadFile',
|
filePath: fileObj.url || fileObj.file?.path || fileObj.tempFilePath,
|
name: 'file',
|
header: { Authorization: 'Bearer ' + getToken() },
|
success: (res) => {
|
closeToast()
|
try {
|
const data = JSON.parse(res.data || '{}')
|
if (data.code === 200) {
|
resolve(data.data)
|
} else {
|
reject(new Error(data.msg || '上传失败'))
|
}
|
} catch (err) {
|
reject(err)
|
}
|
},
|
fail: (err) => {
|
closeToast()
|
reject(err)
|
}
|
})
|
})
|
}
|
|
const afterReadEditUpload = async (file) => {
|
try {
|
const files = Array.isArray(file) ? file : file?.file ? [file] : [file]
|
for (const f of files) {
|
const uploaded = await uploadSingleFile(f)
|
fileList.value.push(uploaded)
|
}
|
showToast('上传成功')
|
} catch (e) {
|
showToast('上传失败')
|
}
|
}
|
|
const afterReadRowUpload = async (file) => {
|
try {
|
const files = Array.isArray(file) ? file : file?.file ? [file] : [file]
|
for (const f of files) {
|
const uploaded = await uploadSingleFile(f)
|
fileList.value.push(uploaded)
|
}
|
showToast('上传成功')
|
} catch (e) {
|
showToast('上传失败')
|
}
|
}
|
|
const removeUploaded = (index) => {
|
fileList.value.splice(index, 1)
|
}
|
|
const getFileNameFromUrl = (url) => {
|
try {
|
if (!url) return ''
|
return decodeURIComponent(url.split('/').pop())
|
} catch (e) {
|
return url
|
}
|
}
|
|
// 附件查看
|
const openFileActions = (commonFiles) => {
|
currentFilesToOpen = commonFiles || []
|
fileActions.value = (commonFiles || []).map((f, idx) => ({ name: getFileNameFromUrl(f.url || ''), index: idx }))
|
showFileSheet.value = true
|
}
|
const onSelectFile = async (action) => {
|
try {
|
const item = currentFilesToOpen[action.index]
|
if (!item || !item.url) return
|
showLoadingToast({ message: '下载中...' })
|
uni.downloadFile({
|
url: item.url,
|
success: (res) => {
|
closeToast()
|
if (res.statusCode === 200) {
|
uni.openDocument({ filePath: res.tempFilePath })
|
} else {
|
showToast('下载失败')
|
}
|
},
|
fail: () => {
|
closeToast()
|
showToast('下载失败')
|
}
|
})
|
} catch (e) {
|
closeToast()
|
showToast('打开失败')
|
}
|
}
|
|
onMounted(() => {
|
getList()
|
})
|
</script>
|
|
<style scoped lang="scss">
|
.u-divider {
|
margin: 0 !important;
|
}
|
.sales-account {
|
min-height: 100vh;
|
background: #f8f9fa;
|
position: relative;
|
}
|
|
.search-filter-section {
|
padding: 10px 20px;
|
background: #ffffff;
|
}
|
|
.search-bar {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
}
|
|
.search-input {
|
flex: 1;
|
background: #f5f5f5;
|
border-radius: 24px;
|
padding: 10px 16px;
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.search-text {
|
flex: 1;
|
font-size: 14px;
|
color: #333;
|
background: transparent;
|
border: none;
|
outline: none;
|
}
|
|
.search-text::placeholder {
|
color: #999;
|
}
|
|
.filter-button {
|
width: 40px;
|
height: 40px;
|
border-radius: 8px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.ledger-list {
|
padding: 20px;
|
}
|
|
.ledger-item {
|
background: #ffffff;
|
border-radius: 12px;
|
margin-bottom: 16px;
|
overflow: hidden;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
padding: 0 16px;
|
}
|
|
.item-header {
|
padding: 16px 0;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.item-left {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.document-icon {
|
width: 24px;
|
height: 24px;
|
background: #2979ff;
|
border-radius: 4px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.item-id {
|
font-size: 14px;
|
color: #333;
|
font-weight: 500;
|
}
|
|
.item-details {
|
padding: 16px 0;
|
}
|
|
.detail-row {
|
display: flex;
|
align-items: flex-end;
|
justify-content: space-between;
|
margin-bottom: 8px;
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
}
|
|
.detail-label {
|
font-size: 12px;
|
color: #777777;
|
min-width: 60px;
|
}
|
|
.detail-value {
|
font-size: 12px;
|
color: #000000;
|
text-align: right;
|
flex: 1;
|
margin-left: 16px;
|
}
|
|
.detail-value.highlight {
|
color: #2979ff;
|
font-weight: 500;
|
}
|
|
.no-data {
|
padding: 40px 0;
|
text-align: center;
|
color: #999;
|
}
|
|
.action-buttons {
|
display: flex;
|
gap: 12px;
|
padding: 0 0 16px 0;
|
justify-content: space-between;
|
}
|
|
.action-btn {
|
flex: 1;
|
}
|
|
.filter-popup {
|
padding: 12px 12px 20px;
|
}
|
|
.switch-row {
|
padding: 12px 16px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.switch-label {
|
font-size: 14px;
|
color: #333;
|
}
|
|
.filter-actions {
|
display: flex;
|
gap: 12px;
|
padding: 12px 16px 16px;
|
justify-content: space-between;
|
}
|
|
.edit-container {
|
padding-bottom: 5rem;
|
}
|
|
.uploaded-list {
|
padding: 8px 16px 0 16px;
|
}
|
|
.uploaded-item {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 8px 0;
|
border-bottom: 1px solid #f5f5f5;
|
}
|
|
.file-name {
|
font-size: 12px;
|
color: #333;
|
margin-right: 8px;
|
flex: 1;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.tip-text {
|
padding: 4px 16px 0 16px;
|
font-size: 12px;
|
color: #888;
|
}
|
|
.footer-btns {
|
position: fixed;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: #fff;
|
display: flex;
|
justify-content: space-around;
|
align-items: center;
|
padding: 0.75rem 0;
|
box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
|
z-index: 1000;
|
}
|
.cancel-btn {
|
font-weight: 400;
|
font-size: 1rem;
|
color: #FFFFFF;
|
width: 6.375rem;
|
background: #C7C9CC;
|
box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
|
border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
|
}
|
.save-btn {
|
font-weight: 400;
|
font-size: 1rem;
|
color: #FFFFFF;
|
width: 14rem;
|
background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
|
box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
|
border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
|
}
|
</style>
|