<template>
|
<view class="sales-account">
|
<!-- 使用通用页面头部组件 -->
|
<PageHeader title="开票台账"
|
@back="goBack" />
|
<!-- 搜索和筛选区域(保持与销售台账风格一致) -->
|
<view class="search-section">
|
<view class="search-bar">
|
<view class="search-input">
|
<up-input class="search-text"
|
placeholder="请输入客户名称/合同号搜索"
|
v-model="searchForm.searchText"
|
@change="handleQuery"
|
clearable />
|
</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="ledgerList.length > 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">
|
<up-button type="primary"
|
size="small"
|
class="action-btn"
|
:disabled="item.invoicePerson !== userStore.nickName"
|
@click="openEdit(item)">
|
编辑
|
</up-button>
|
<up-button type="error"
|
size="small"
|
plain
|
class="action-btn"
|
:disabled="item.invoicePerson !== userStore.nickName"
|
@click="handleDelete(item)">
|
删除
|
</up-button>
|
<!-- <up-button-->
|
<!-- size="small"-->
|
<!-- plain-->
|
<!-- class="action-btn"-->
|
<!-- v-if="item.invoiceFileName"-->
|
<!-- @click="openFileActions(item.commonFiles || [])"-->
|
<!-- >-->
|
<!-- 查看附件-->
|
<!-- </up-button>-->
|
<!-- <up-button-->
|
<!-- type="primary"-->
|
<!-- size="small"-->
|
<!-- class="action-btn"-->
|
<!-- v-else-->
|
<!-- :disabled="item.invoicePerson !== userStore.nickName"-->
|
<!-- @click="openUpload(item)"-->
|
<!-- >-->
|
<!-- 上传-->
|
<!-- </up-button>-->
|
</view>
|
</view>
|
</view>
|
</view>
|
<view v-else
|
class="no-data">
|
<text>暂无开票台账数据</text>
|
</view>
|
<!-- 筛选弹窗 -->
|
<up-popup v-model="showFilter"
|
mode="bottom"
|
round><up-transition>
|
<view class="filter-popup">
|
<up-cell-group title="筛选条件"
|
inset>
|
<up-input label="开票日期"
|
readonly
|
placeholder="请选择日期范围"
|
@click="showInvoiceRange = true"
|
v-model="invoiceRangeLabel" />
|
<up-input label="录入日期"
|
readonly
|
@click="showCreateDatePicker = true"
|
v-model="searchForm.createTimeStart" />
|
<view class="switch-row">
|
<text class="switch-label">不显示有发票行</text>
|
<up-switch v-model="searchForm.status"
|
size="20" />
|
</view>
|
</up-cell-group>
|
<view class="filter-actions">
|
<up-button @click="resetFilter">重置</up-button>
|
<up-button type="primary"
|
@click="confirmFilter">确定</up-button>
|
</view>
|
</view>
|
</up-transition></up-popup>
|
<!-- 日历:开票日期范围 -->
|
<up-popup v-model="showInvoiceRange"
|
mode="bottom"><up-transition>
|
<up-datetime-picker mode="date"
|
type="range"
|
title="选择开票日期范围"
|
@confirm="onInvoiceRangeConfirm"
|
@cancel="showInvoiceRange = false" />
|
</up-transition></up-popup>
|
<!-- 日期:录入日期 -->
|
<up-popup v-model="showCreateDatePicker"
|
mode="bottom"><up-transition>
|
<up-datetime-picker mode="date"
|
type="selector"
|
v-model="currentCreateDate"
|
title="选择录入日期"
|
@confirm="onCreateDateConfirm"
|
@cancel="showCreateDatePicker = false" />
|
</up-transition></up-popup>
|
<!-- 单行上传弹窗(无表单) -->
|
<up-popup v-model="showUpload"
|
mode="bottom"
|
round><up-transition>
|
<view class="upload-container">
|
<up-cell-group title="上传附件(仅支持 pdf,最大10MB,最多10个)"
|
inset>
|
<up-upload accept="pdf"
|
multiple
|
:maxCount="10"
|
:afterRead="afterReadRowUpload"
|
:beforeRead="beforeReadPdf">
|
<up-button type="primary">点击上传</up-button>
|
</up-upload>
|
<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>
|
<up-button size="mini"
|
type="error"
|
plain
|
@click="removeUploaded(idx)">移除</up-button>
|
</view>
|
</view>
|
</up-cell-group>
|
<view class="filter-actions">
|
<up-button @click="showUpload = false">取消</up-button>
|
<up-button type="primary"
|
@click="confirmUpload">确认</up-button>
|
</view>
|
</view>
|
</up-transition></up-popup>
|
<!-- 附件列表选择 -->
|
<up-action-sheet v-model="showFileSheet"
|
:actions="fileActions"
|
cancel-text="取消"
|
close-on-click-action
|
@select="onSelectFile">
|
<view class="up-action-sheet__cancel"
|
@click="showFileSheet = false">
|
取消
|
</view>
|
</up-action-sheet>
|
</view>
|
</template>
|
|
<script setup>
|
import { reactive, ref } from "vue";
|
import dayjs from "dayjs";
|
import PageHeader from "@/components/PageHeader.vue";
|
import useUserStore from "@/store/modules/user";
|
import { getToken } from "@/utils/auth";
|
import config from "@/config.js";
|
import {
|
commitFile,
|
delInvoiceLedgerByRegProductId,
|
registrationProductPage,
|
} from "@/api/salesManagement/invoiceLedger.js";
|
import { onShow } from "@dcloudio/uni-app";
|
|
const showToast = message => {
|
uni.showToast({
|
title: message,
|
icon: "none",
|
});
|
};
|
const showLoadingToast = message => {
|
uni.showLoading({
|
title: message,
|
mask: true,
|
});
|
};
|
const closeToast = () => {
|
uni.hideLoading();
|
};
|
|
const userStore = useUserStore();
|
|
// 列表与查询
|
const ledgerList = ref([]);
|
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("加载中...");
|
const { invoiceDate, ...rest } = searchForm;
|
const res = await registrationProductPage({ ...rest, ...page });
|
// 兼容不同返回结构
|
ledgerList.value = res?.data?.records || res?.records || res?.data || [];
|
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 = ({ selectedValues }) => {
|
try {
|
const start = dayjs(selectedValues[0]).format("YYYY-MM-DD");
|
const end = dayjs(selectedValues[1]).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("处理中...");
|
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("提交中...");
|
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("正在上传...");
|
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("下载中...");
|
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("打开失败");
|
}
|
};
|
|
onShow(() => {
|
getList();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/styles/sales-common.scss";
|
|
// 开票台账特有样式
|
.filter-popup {
|
padding: 12px 12px 20px;
|
}
|
|
.switch-row {
|
padding: 12px 16px;
|
}
|
|
.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>
|