index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <template>
  2. <div class="invoice-page">
  3. <!-- 顶部导航 -->
  4. <van-nav-bar title="自然人开票" fixed placeholder safe-area-inset-top />
  5. <div class="page-content">
  6. <!-- 骨架屏:加载中显示 -->
  7. <template v-if="loading">
  8. <div class="card" v-for="i in 4" :key="i">
  9. <div class="card-header">
  10. <div class="dot-skeleton"></div>
  11. <div class="title-skeleton"></div>
  12. </div>
  13. <div class="card-body">
  14. <div class="info-skeleton" v-for="j in 3" :key="j">
  15. <div class="label-skeleton"></div>
  16. <div class="value-skeleton"></div>
  17. </div>
  18. </div>
  19. </div>
  20. </template>
  21. <!-- 加载完成后显示真实内容 -->
  22. <template v-else>
  23. <div class="card">
  24. <StepProgress :activeStep="activeStep" />
  25. </div>
  26. <!-- 卡片:申请人信息 -->
  27. <div class="card">
  28. <div class="card-header">
  29. <span class="dot"></span>
  30. <span class="title">申请人信息</span>
  31. </div>
  32. <div class="card-body">
  33. <div class="info-item">
  34. <span class="label">姓名</span>
  35. <span class="value">{{ invoiceInfo?.sellerName }}</span>
  36. </div>
  37. <div class="info-item">
  38. <span class="label">身份证号</span>
  39. <span class="value">{{ invoiceInfo?.sellerId }}</span>
  40. </div>
  41. </div>
  42. </div>
  43. <!-- 发票抬头 -->
  44. <div class="card">
  45. <div class="card-header">
  46. <span class="dot"></span>
  47. <span class="title">发票抬头</span>
  48. </div>
  49. <div class="card-body">
  50. <div class="info-item">
  51. <span class="label">公司名称</span>
  52. <span class="value">{{ invoiceInfo?.buyerName }}</span>
  53. </div>
  54. <div class="info-item">
  55. <span class="label">统一信用代码</span>
  56. <span class="value">{{ invoiceInfo?.buyerId }}</span>
  57. </div>
  58. </div>
  59. </div>
  60. <!-- 发票信息 -->
  61. <div class="card">
  62. <div class="card-header">
  63. <span class="dot"></span>
  64. <span class="title">发票信息</span>
  65. </div>
  66. <div class="card-body">
  67. <div class="info-item">
  68. <span class="label">应税发生地</span>
  69. <span class="value">{{
  70. [invoiceInfo?.province, invoiceInfo?.city, invoiceInfo?.district]
  71. .filter(Boolean)
  72. .join(' ')
  73. }}</span>
  74. </div>
  75. <div class="info-item">
  76. <span class="label">发票类别</span>
  77. <span class="value">{{ invoiceInfo?.category }}</span>
  78. </div>
  79. <div class="info-item">
  80. <span class="label">税前金额</span>
  81. <span class="value highlight">{{ invoiceInfo?.amount }}</span>
  82. </div>
  83. </div>
  84. </div>
  85. <!-- 缴税信息 -->
  86. <div class="card">
  87. <div class="card-header">
  88. <span class="dot"></span>
  89. <span class="title">缴税信息</span>
  90. </div>
  91. <div class="card-body">
  92. <div class="info-item">
  93. <span class="label">应缴税信息</span>
  94. <span class="value link" @click="toDetail">查看详情 ></span>
  95. </div>
  96. </div>
  97. </div>
  98. </template>
  99. </div>
  100. <!-- 底部操作按钮 -->
  101. <!-- 底部操作按钮 -->
  102. <div class="bottom-bar">
  103. <van-button type="primary" plain round class="reject-btn btn" @click="rejectAndReturn">
  104. 拒绝并退回
  105. </van-button>
  106. <van-button
  107. type="primary"
  108. :disabled="btnDisabled"
  109. round
  110. class="next-btn btn"
  111. @click="handleNextDebounced"
  112. >
  113. 下一步
  114. </van-button>
  115. </div>
  116. <!-- 拒绝并退回 -->
  117. <ModernDialog
  118. v-model:show="rejectAndReturnDialog"
  119. title="提示"
  120. message="确认拒绝后,信息会同步给项目执行人员"
  121. cancelText="返回开票页"
  122. @cancel="rejectAndReturnDialog = false"
  123. confirmText="确认拒绝"
  124. @confirm="rejectAndReturnDialogConfirm"
  125. />
  126. </div>
  127. </template>
  128. <script setup lang="ts">
  129. import { ref, reactive, onMounted } from 'vue'
  130. import { showToast, showSuccessToast } from 'vant'
  131. import { useRouter } from 'vue-router'
  132. import { useDebounceFn } from '@/utils/util'
  133. import StepProgress from '@/components/StepProgress.vue'
  134. import {
  135. getConfirmInvoiceInfoApi,
  136. invoiceRecordInvalidateApi,
  137. } from '@/services/modules/invoiceInformation'
  138. import type { PushRecordIdRequest } from '@/services/modules/invoiceInformation/type.d.ts'
  139. import { useUserStore } from '@/stores/modules/user'
  140. import ModernDialog from '@/components/ModernDialog.vue'
  141. // ✅ 使用封装好的 Hook
  142. import { useInvoice } from '@/hooks/useInvoice'
  143. // --- 初始化 Hooks ---
  144. const { getStatus, submitInvoiceApply, getFaceAuthResult, btnDisabled, statusMap, faceAuthResult } =
  145. useInvoice()
  146. // --- 其余本页面独有逻辑 ---
  147. const userStore = useUserStore()
  148. const router = useRouter()
  149. const loading = ref(true)
  150. const invoiceInfo = ref<any>({})
  151. const activeStep = ref(1)
  152. // 请求参数
  153. const params = reactive<PushRecordIdRequest>({
  154. pushRecordId: userStore.pushRecordId,
  155. })
  156. // 获取发票确认信息
  157. const getConfirmInvoiceInfo = async () => {
  158. try {
  159. loading.value = true
  160. const res = await getConfirmInvoiceInfoApi(params)
  161. if (res.code === 0) {
  162. invoiceInfo.value = res.data
  163. }
  164. } catch (err: any) {
  165. const { message } = err
  166. showToast(message)
  167. } finally {
  168. loading.value = false
  169. }
  170. }
  171. /**
  172. * 下一步逻辑
  173. * 使用 hooks 提供的状态进行判断
  174. */
  175. const handleNext = async () => {
  176. // 未上传身份证,跳转上传页
  177. if (!statusMap.value.isIdImgReady) {
  178. return router.push({ path: '/identity-upload' })
  179. }
  180. // 已处理状态,跳转人脸识别
  181. if (userStore.needFaceId && !faceAuthResult.value) {
  182. return router.push({ path: '/face-recognition' })
  183. }
  184. // 其他情况,直接提交
  185. await submitInvoiceApply()
  186. }
  187. const rejectAndReturnDialog = ref(false)
  188. // 拒绝并退回
  189. const rejectAndReturn = () => {
  190. rejectAndReturnDialog.value = true
  191. }
  192. const rejectAndReturnDialogConfirm = async () => {
  193. const res = await invoiceRecordInvalidateApi(params)
  194. if (res.code === 0 && res.data) {
  195. showSuccessToast('提交成功')
  196. setTimeout(() => {
  197. router.replace({
  198. path: '/login',
  199. query: {
  200. pushRecordId: userStore.pushRecordId,
  201. },
  202. })
  203. userStore.LogOut()
  204. }, 1500)
  205. }
  206. }
  207. /** 防抖包装 */
  208. const handleNextDebounced = useDebounceFn(handleNext, 1000)
  209. /** 跳转缴税详情页 */
  210. const toDetail = () => {
  211. router.push({ path: '/invoice-information/detail' })
  212. }
  213. /** 生命周期 */
  214. onMounted(async () => {
  215. await Promise.all([getStatus(), getConfirmInvoiceInfo(), getFaceAuthResult()])
  216. })
  217. </script>
  218. <style scoped lang="scss">
  219. .invoice-page {
  220. background: #f5f6f8;
  221. min-height: 100vh;
  222. font-family:
  223. -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei',
  224. sans-serif;
  225. font-size: 3.8vw;
  226. .page-content {
  227. padding: 4vw 3.5vw;
  228. padding-bottom: 15vw;
  229. /* ---------- 卡片通用样式 ---------- */
  230. .card {
  231. background: #fff;
  232. border-radius: 14px;
  233. margin-bottom: 5vw;
  234. padding: 4vw 3.5vw;
  235. box-shadow:
  236. 0 4px 12px rgba(0, 0, 0, 0.06),
  237. 0 1px 3px rgba(0, 0, 0, 0.04);
  238. transition: all 0.3s ease;
  239. /* ---------- 骨架屏 Header ---------- */
  240. .card-header {
  241. display: flex;
  242. align-items: center;
  243. margin-bottom: 3vw;
  244. .dot {
  245. width: 6px;
  246. height: 28px;
  247. background: #0072f8;
  248. border-radius: 4px;
  249. margin-right: 2vw;
  250. }
  251. .dot-skeleton {
  252. width: 6px;
  253. height: 28px;
  254. border-radius: 4px;
  255. margin-right: 2vw;
  256. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  257. background-size: 200% 100%;
  258. animation: skeleton-loading 1.2s ease-in-out infinite;
  259. }
  260. .title {
  261. font-size: 4vw;
  262. font-weight: 600;
  263. color: #333;
  264. }
  265. .title-skeleton {
  266. flex: 1;
  267. height: 22px;
  268. border-radius: 6px;
  269. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  270. background-size: 200% 100%;
  271. animation: skeleton-loading 1.2s ease-in-out infinite;
  272. }
  273. }
  274. /* ---------- 骨架屏 Body ---------- */
  275. .card-body {
  276. display: flex;
  277. flex-direction: column;
  278. gap: 3vw; // 🟢 关键间距:解决挤在一起的问题
  279. .info-skeleton {
  280. display: flex;
  281. justify-content: space-between;
  282. align-items: center;
  283. gap: 4vw;
  284. .label-skeleton {
  285. flex: 0 0 30%;
  286. height: 18px;
  287. border-radius: 6px;
  288. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  289. background-size: 200% 100%;
  290. animation: skeleton-loading 1.2s ease-in-out infinite;
  291. }
  292. .value-skeleton {
  293. flex: 1;
  294. height: 18px;
  295. border-radius: 6px;
  296. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  297. background-size: 200% 100%;
  298. animation: skeleton-loading 1.2s ease-in-out infinite;
  299. }
  300. }
  301. /* 真实内容样式 */
  302. .info-item {
  303. display: flex;
  304. justify-content: space-between;
  305. align-items: flex-start;
  306. font-size: 3.8vw;
  307. color: #555;
  308. line-height: 1.6;
  309. .label {
  310. flex: 0 0 28vw;
  311. color: #888;
  312. }
  313. .value {
  314. flex: 1;
  315. text-align: right;
  316. color: #222;
  317. word-break: break-all;
  318. }
  319. .highlight {
  320. color: #0072f8;
  321. font-weight: 600;
  322. }
  323. .link {
  324. color: #0072f8;
  325. cursor: pointer;
  326. font-weight: 500;
  327. }
  328. }
  329. }
  330. }
  331. }
  332. /* ---------- 底部按钮 ---------- */
  333. .bottom-bar {
  334. position: fixed;
  335. bottom: 0;
  336. left: 0;
  337. right: 0;
  338. z-index: 1000;
  339. padding: 3vw 4vw 4vw;
  340. background: #f7f9fc;
  341. box-shadow: 0 -2vw 6vw rgba(0, 0, 0, 0.08);
  342. display: flex;
  343. gap: 3vw;
  344. /* ---------- 通用按钮 ---------- */
  345. .btn {
  346. flex: 1;
  347. min-width: 0; // ⭐ 关键:防止被挤出
  348. height: 12vw;
  349. font-size: 4.2vw;
  350. border-radius: 6vw;
  351. white-space: normal; // 覆盖 van-button 默认 nowrap
  352. }
  353. /* ---------- 左侧按钮(次操作) ---------- */
  354. .reject-btn {
  355. background: #ffffff;
  356. color: #3a7afe;
  357. border: 1px solid #3a7afe;
  358. }
  359. /* ---------- 右侧按钮(主操作) ---------- */
  360. .next-btn {
  361. color: #fff;
  362. background: linear-gradient(90deg, #0072f8 0%, #3a9fff 100%);
  363. border: none;
  364. }
  365. /* ---------- 禁用态兜底(Vant) ---------- */
  366. :deep(.van-button--disabled) {
  367. opacity: 0.6;
  368. }
  369. }
  370. /* ---------- 骨架屏动画 ---------- */
  371. @keyframes skeleton-loading {
  372. 0% {
  373. background-position: 200% 0;
  374. }
  375. 100% {
  376. background-position: -200% 0;
  377. }
  378. }
  379. }
  380. </style>