index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <!-- 发票管理页面 -->
  2. <template>
  3. <div class="page-container">
  4. <header class="page-head">
  5. <h1>发票管理</h1>
  6. <p class="page-desc">统一管理开票、作废、下载与发票详情查询。</p>
  7. </header>
  8. <section class="panel">
  9. <div class="panel-top">
  10. <h2>发票列表</h2>
  11. </div>
  12. <div class="filter-block">
  13. <el-form :model="searchForm" class="filter-form" @submit.prevent>
  14. <div class="filter-grid">
  15. <el-form-item label="发票号">
  16. <el-input v-model="searchForm.invoiceNo" placeholder="发票号" clearable />
  17. </el-form-item>
  18. <el-form-item label="客户名称">
  19. <el-input v-model="searchForm.clientName" placeholder="客户名称" clearable />
  20. </el-form-item>
  21. <el-form-item label="发票类型">
  22. <el-select v-model="searchForm.invoiceType" placeholder="请选择类型" clearable>
  23. <el-option label="全部" value="" />
  24. <el-option label="普通发票" value="normal" />
  25. <el-option label="增值税发票" value="vat" />
  26. </el-select>
  27. </el-form-item>
  28. <el-form-item label="状态">
  29. <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
  30. <el-option label="全部" value="" />
  31. <el-option label="待开具" value="pending" />
  32. <el-option label="已开具" value="issued" />
  33. <el-option label="已作废" value="void" />
  34. </el-select>
  35. </el-form-item>
  36. </div>
  37. <div class="filter-actions-row">
  38. <span class="filter-hint">按发票号、客户、类型与状态快速检索。</span>
  39. <div class="filter-actions-end">
  40. <el-button class="btn-ghost" @click="handleReset">重置</el-button>
  41. <el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
  42. </div>
  43. </div>
  44. </el-form>
  45. </div>
  46. <div class="list-body list-wrap">
  47. <el-table :data="tableData" v-loading="loading" stripe class="list-table">
  48. <el-table-column prop="invoiceNo" label="发票号" min-width="180" />
  49. <el-table-column prop="orderNo" label="订单编号" min-width="180" />
  50. <el-table-column prop="clientName" label="客户名称" min-width="150" />
  51. <el-table-column prop="invoiceType" label="发票类型" min-width="120">
  52. <template #default="{ row }">
  53. <el-tag size="small">{{ row.invoiceType === 'normal' ? '普通发票' : '增值税发票' }}</el-tag>
  54. </template>
  55. </el-table-column>
  56. <el-table-column prop="title" label="发票抬头" min-width="150" show-overflow-tooltip />
  57. <el-table-column prop="invoiceAmount" label="发票金额(元)" min-width="120">
  58. <template #default="{ row }">
  59. <span class="amount-text">¥{{ row.invoiceAmount }}</span>
  60. </template>
  61. </el-table-column>
  62. <el-table-column prop="status" label="状态" min-width="100">
  63. <template #default="{ row }">
  64. <el-tag :type="getStatusType(row.status)" size="small">{{ getStatusText(row.status) }}</el-tag>
  65. </template>
  66. </el-table-column>
  67. <el-table-column prop="createAt" label="创建时间" min-width="180" />
  68. <el-table-column label="操作" min-width="180" fixed="right">
  69. <template #default="{ row }">
  70. <el-button class="btn-op-item primary" @click="handleView(row)">查看</el-button>
  71. <el-button v-if="row.status === 'pending'" class="btn-op-item" @click="handleIssue(row)">开具</el-button>
  72. <el-button v-if="row.status === 'issued'" class="btn-op-item" @click="handleDownload(row)">下载</el-button>
  73. <el-button v-if="row.status === 'pending'" class="btn-op-item" @click="handleVoid(row)">作废</el-button>
  74. </template>
  75. </el-table-column>
  76. </el-table>
  77. <div class="pagination list-pager-bar">
  78. <el-pagination
  79. v-model:current-page="pagination.page"
  80. v-model:page-size="pagination.pageSize"
  81. :total="pagination.total"
  82. :page-sizes="[10, 20, 50]"
  83. layout="total, sizes, prev, pager, next"
  84. @size-change="loadData"
  85. @current-change="loadData"
  86. />
  87. </div>
  88. </div>
  89. </section>
  90. <!-- 查看详情弹窗 -->
  91. <el-dialog v-model="viewVisible" title="发票详情" width="700px" class="invoice-dialog">
  92. <el-descriptions :column="2" border>
  93. <el-descriptions-item label="发票号">{{ viewData.invoiceNo }}</el-descriptions-item>
  94. <el-descriptions-item label="订单编号">{{ viewData.orderNo }}</el-descriptions-item>
  95. <el-descriptions-item label="客户名称">{{ viewData.clientName }}</el-descriptions-item>
  96. <el-descriptions-item label="发票类型">
  97. <el-tag size="small">{{ viewData.invoiceType === 'normal' ? '普通发票' : '增值税发票' }}</el-tag>
  98. </el-descriptions-item>
  99. <el-descriptions-item label="发票抬头" :span="2">{{ viewData.title }}</el-descriptions-item>
  100. <el-descriptions-item label="发票金额(元)">
  101. <span class="amount-text">¥{{ viewData.invoiceAmount }}</span>
  102. </el-descriptions-item>
  103. <el-descriptions-item label="税率">{{ viewData.taxRate }}%</el-descriptions-item>
  104. <el-descriptions-item label="税额">¥{{ viewData.taxAmount }}</el-descriptions-item>
  105. <el-descriptions-item label="发票状态">
  106. <el-tag :type="getStatusType(viewData.status)" size="small">{{ getStatusText(viewData.status) }}</el-tag>
  107. </el-descriptions-item>
  108. <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="税号">{{ viewData.taxNo }}</el-descriptions-item>
  109. <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="开户银行">{{ viewData.bank }}</el-descriptions-item>
  110. <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="银行账号">{{ viewData.bankAccount }}</el-descriptions-item>
  111. <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="开票地址">{{ viewData.address }}</el-descriptions-item>
  112. <el-descriptions-item v-if="viewData.invoiceType === 'vat'" label="电话">{{ viewData.phone }}</el-descriptions-item>
  113. <el-descriptions-item label="开具时间">{{ viewData.issuedAt || '-' }}</el-descriptions-item>
  114. <el-descriptions-item label="开具人">{{ viewData.issuedByName || '-' }}</el-descriptions-item>
  115. <el-descriptions-item label="备注" :span="2">{{ viewData.remark || '-' }}</el-descriptions-item>
  116. <el-descriptions-item label="创建时间">{{ viewData.createAt }}</el-descriptions-item>
  117. </el-descriptions>
  118. <template #footer>
  119. <el-button class="btn-ghost" @click="viewVisible = false">关闭</el-button>
  120. <el-button v-if="viewData.status === 'issued'" class="btn-main" type="primary" @click="handleDownload(viewData)">下载发票</el-button>
  121. </template>
  122. </el-dialog>
  123. <!-- 开具发票弹窗 -->
  124. <el-dialog v-model="issueVisible" title="开具发票" width="600px" class="invoice-dialog">
  125. <el-form :model="issueForm" label-width="100px">
  126. <el-form-item label="发票文件">
  127. <el-input v-model="issueForm.invoiceUrl" placeholder="请输入发票文件URL或上传发票文件" />
  128. </el-form-item>
  129. <el-form-item label="发票号码">
  130. <el-input v-model="issueForm.invoiceNo" placeholder="请输入发票号码" />
  131. </el-form-item>
  132. <el-form-item label="开票日期">
  133. <el-date-picker v-model="issueForm.issueDate" type="date" placeholder="选择开票日期" value-format="YYYY-MM-DD" />
  134. </el-form-item>
  135. </el-form>
  136. <template #footer>
  137. <el-button class="btn-ghost" @click="issueVisible = false">取消</el-button>
  138. <el-button class="btn-main" type="primary" @click="confirmIssue" :loading="submitLoading">确定开具</el-button>
  139. </template>
  140. </el-dialog>
  141. <!-- 作废确认弹窗 -->
  142. <el-dialog v-model="voidVisible" title="确认作废" width="400px" class="invoice-dialog">
  143. <p style="text-align: center; padding: 20px 0;">确定要作废这张发票吗?</p>
  144. <template #footer>
  145. <el-button class="btn-ghost" @click="voidVisible = false">取消</el-button>
  146. <el-button class="btn-main" type="danger" @click="confirmVoid" :loading="submitLoading">确认作废</el-button>
  147. </template>
  148. </el-dialog>
  149. </div>
  150. </template>
  151. <script setup lang="ts">
  152. import { ref, reactive, onMounted } from 'vue'
  153. import { ElMessage, ElMessageBox } from 'element-plus'
  154. import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
  155. import { fetchInvoiceList, fetchInvoiceDetail, fetchIssueInvoice, fetchVoidInvoice, fetchDownloadInvoice } from '@/api/business'
  156. defineOptions({ name: 'InvoiceManagement' })
  157. const searchForm = reactive({
  158. invoiceNo: '',
  159. clientName: '',
  160. invoiceType: '',
  161. status: ''
  162. })
  163. const tableData = ref<any[]>([])
  164. const loading = ref(false)
  165. const pagination = reactive({
  166. page: 1,
  167. pageSize: 10,
  168. total: 0
  169. })
  170. const viewVisible = ref(false)
  171. const viewData = ref<any>({})
  172. const issueVisible = ref(false)
  173. const issueForm = reactive({
  174. id: '',
  175. invoiceUrl: '',
  176. invoiceNo: '',
  177. issueDate: ''
  178. })
  179. const voidVisible = ref(false)
  180. const voidData = ref<any>({})
  181. const submitLoading = ref(false)
  182. const getStatusType = (status: string) => {
  183. const map: Record<string, string> = {
  184. pending: 'warning',
  185. issued: 'success',
  186. void: 'info'
  187. }
  188. return map[status] || 'info'
  189. }
  190. const getStatusText = (status: string) => {
  191. const map: Record<string, string> = {
  192. pending: '待开具',
  193. issued: '已开具',
  194. void: '已作废'
  195. }
  196. return map[status] || status
  197. }
  198. const loadData = async () => {
  199. loading.value = true
  200. try {
  201. const res = await fetchInvoiceList({
  202. page: pagination.page,
  203. pageSize: pagination.pageSize,
  204. invoiceNo: searchForm.invoiceNo || undefined,
  205. clientName: searchForm.clientName || undefined,
  206. invoiceType: searchForm.invoiceType || undefined,
  207. status: searchForm.status || undefined
  208. })
  209. tableData.value = res.list
  210. pagination.total = res.total
  211. } catch (error) {
  212. console.error('加载数据失败:', error)
  213. } finally {
  214. loading.value = false
  215. }
  216. }
  217. const handleSearch = () => {
  218. pagination.page = 1
  219. loadData()
  220. }
  221. const handleReset = () => {
  222. searchForm.invoiceNo = ''
  223. searchForm.clientName = ''
  224. searchForm.invoiceType = ''
  225. searchForm.status = ''
  226. pagination.page = 1
  227. loadData()
  228. }
  229. const handleView = async (row: any) => {
  230. try {
  231. const res = await fetchInvoiceDetail(row.id)
  232. viewData.value = res
  233. viewVisible.value = true
  234. } catch (error) {
  235. console.error('加载详情失败:', error)
  236. }
  237. }
  238. const handleIssue = (row: any) => {
  239. issueForm.id = row.id
  240. issueForm.invoiceUrl = ''
  241. issueForm.invoiceNo = ''
  242. issueForm.issueDate = ''
  243. issueVisible.value = true
  244. }
  245. const confirmIssue = async () => {
  246. if (!issueForm.invoiceUrl && !issueForm.invoiceNo) {
  247. ElMessage.warning('请填写发票文件URL或发票号码')
  248. return
  249. }
  250. submitLoading.value = true
  251. try {
  252. await withFullscreenLoading(async () => {
  253. await fetchIssueInvoice(issueForm.id, issueForm.invoiceUrl || undefined)
  254. ElMessage.success('发票开具成功')
  255. issueVisible.value = false
  256. loadData()
  257. }, fullscreenLoadingText.save)
  258. } catch (error) {
  259. console.error('开具失败:', error)
  260. ElMessage.error('开具失败,请重试')
  261. } finally {
  262. submitLoading.value = false
  263. }
  264. }
  265. const handleVoid = (row: any) => {
  266. voidData.value = row
  267. voidVisible.value = true
  268. }
  269. const confirmVoid = async () => {
  270. submitLoading.value = true
  271. try {
  272. await withFullscreenLoading(async () => {
  273. await fetchVoidInvoice(voidData.value.id)
  274. ElMessage.success('发票已作废')
  275. voidVisible.value = false
  276. loadData()
  277. }, fullscreenLoadingText.delete)
  278. } catch (error) {
  279. console.error('作废失败:', error)
  280. } finally {
  281. submitLoading.value = false
  282. }
  283. }
  284. const handleDownload = async (row: any) => {
  285. try {
  286. const res = await fetchDownloadInvoice(row.id)
  287. // 如果返回的是文件URL,直接打开
  288. if (res.url) {
  289. window.open(res.url, '_blank')
  290. } else {
  291. // 否则提示开发中
  292. ElMessage.info('发票文件下载中...')
  293. }
  294. } catch (error) {
  295. console.error('下载失败:', error)
  296. // 如果 API 不可用,使用备选方案
  297. if (row.invoiceUrl) {
  298. window.open(row.invoiceUrl, '_blank')
  299. } else {
  300. ElMessage.error('发票文件不存在,请稍后重试')
  301. }
  302. }
  303. }
  304. onMounted(() => {
  305. loadData()
  306. })
  307. </script>
  308. <style lang="scss" scoped>
  309. .amount-text {
  310. color: #dc2626;
  311. font-weight: 600;
  312. }
  313. </style>