index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. <template>
  2. <div class="invoice-page">
  3. <!-- 顶部导航 -->
  4. <van-nav-bar title="自然人开票" fixed placeholder />
  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. <div class="bottom-bar">
  102. <van-button
  103. type="primary"
  104. block
  105. round
  106. color="linear-gradient(90deg, #ff7a00, #ffa94d)"
  107. class="next-btn"
  108. @click="handleNext"
  109. >
  110. 下一步
  111. </van-button>
  112. </div>
  113. <!-- ✅ 弹窗(放在最外层,不要嵌套第二个 template) -->
  114. <van-dialog
  115. v-model:show="showDialog"
  116. :show-cancel-button="false"
  117. :show-confirm-button="false"
  118. class="modern-dialog"
  119. close-on-click-overlay
  120. >
  121. <div class="dialog-content">
  122. <div class="dialog-title">申请成功</div>
  123. <div class="dialog-message">
  124. 您的开票信息已提交至税务部门审核,审核通过后通知您缴纳税款,请关注缴税信息。
  125. </div>
  126. <van-button
  127. type="primary"
  128. block
  129. round
  130. class="dialog-btn"
  131. color="linear-gradient(90deg, #ff7a00, #ffa94d)"
  132. @click="onConfirm"
  133. >
  134. 我知道了
  135. </van-button>
  136. </div>
  137. </van-dialog>
  138. </div>
  139. </template>
  140. <script setup lang="ts">
  141. import { useRouter } from 'vue-router'
  142. import { useDebounceFn } from '@/utils/util'
  143. import {
  144. getConfirmInvoiceInfoApi,
  145. getStatusApi,
  146. submitInvoiceApplyApi,
  147. } from '@/services/modules/invoiceInformation'
  148. import type { PushRecordIdRequest } from '@/services/modules/invoiceInformation/type.d.ts'
  149. import { reactive, onMounted, ref } from 'vue'
  150. import StepProgress from '@/components/StepProgress.vue'
  151. import { showToast } from 'vant'
  152. import { useUserStore } from '@/stores/modules/user'
  153. const userStore = useUserStore()
  154. const router = useRouter()
  155. const loading = ref(true)
  156. const invoiceInfo = ref<any>({})
  157. const params = reactive<PushRecordIdRequest>({
  158. pushRecordId: userStore.pushRecordId,
  159. })
  160. const getConfirmInvoiceInfo = async () => {
  161. try {
  162. loading.value = true
  163. const res = await getConfirmInvoiceInfoApi(params)
  164. if (res.code === 0) {
  165. invoiceInfo.value = res.data
  166. }
  167. } catch (err) {
  168. console.error('获取发票信息失败', err)
  169. } finally {
  170. loading.value = false
  171. }
  172. }
  173. const handleNext = useDebounceFn(async () => {
  174. const res = await getStatusApi(params)
  175. if (res.code === 0) {
  176. if (!res.data.eventStatus) {
  177. return router.push({
  178. path: '/face-recognition',
  179. })
  180. }
  181. // 当前账户没有身份照片,跳转上传证件页面
  182. if (!res.data.isIdImgReady) {
  183. router.push({
  184. path: '/identity-upload',
  185. })
  186. }
  187. }
  188. }, 1000)
  189. const showDialog = ref(false)
  190. const submitInvoiceApply = async () => {
  191. try {
  192. const res = await submitInvoiceApplyApi(params)
  193. if (res.code === 0 && res.data) {
  194. showDialog.value = true
  195. }
  196. } catch (err) {
  197. console.error('提交失败', err)
  198. showToast('提交失败,请稍后重试')
  199. }
  200. }
  201. const activeStep = ref(1)
  202. const onConfirm = () => {
  203. showDialog.value = false
  204. }
  205. const toDetail = () => {
  206. router.push({
  207. path: '/invoice-information/detail',
  208. })
  209. }
  210. onMounted(() => {
  211. getConfirmInvoiceInfo()
  212. const authFlag = sessionStorage.getItem('FACE_AUTH_DONE')
  213. if (authFlag === '1') {
  214. activeStep.value = 3
  215. sessionStorage.removeItem('FACE_AUTH_DONE') // 清除标记
  216. submitInvoiceApply()
  217. } else {
  218. activeStep.value = 1
  219. }
  220. })
  221. </script>
  222. <style scoped lang="scss">
  223. .invoice-page {
  224. background: #f5f6f8;
  225. min-height: 100vh;
  226. font-family:
  227. -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei',
  228. sans-serif;
  229. font-size: 3.8vw;
  230. .page-content {
  231. padding: 4vw 3.5vw;
  232. padding-bottom: 15vw;
  233. /* ---------- 卡片通用样式 ---------- */
  234. .card {
  235. background: #fff;
  236. border-radius: 14px;
  237. margin-bottom: 5vw;
  238. padding: 4vw 3.5vw;
  239. box-shadow:
  240. 0 4px 12px rgba(0, 0, 0, 0.06),
  241. 0 1px 3px rgba(0, 0, 0, 0.04);
  242. transition: all 0.3s ease;
  243. /* ---------- 骨架屏 Header ---------- */
  244. .card-header {
  245. display: flex;
  246. align-items: center;
  247. margin-bottom: 3vw;
  248. .dot {
  249. width: 6px;
  250. height: 28px;
  251. background: #ff7a00;
  252. border-radius: 4px;
  253. margin-right: 2vw;
  254. }
  255. .dot-skeleton {
  256. width: 6px;
  257. height: 28px;
  258. border-radius: 4px;
  259. margin-right: 2vw;
  260. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  261. background-size: 200% 100%;
  262. animation: skeleton-loading 1.2s ease-in-out infinite;
  263. }
  264. .title {
  265. font-size: 4vw;
  266. font-weight: 600;
  267. color: #333;
  268. }
  269. .title-skeleton {
  270. flex: 1;
  271. height: 22px;
  272. border-radius: 6px;
  273. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  274. background-size: 200% 100%;
  275. animation: skeleton-loading 1.2s ease-in-out infinite;
  276. }
  277. }
  278. /* ---------- 骨架屏 Body ---------- */
  279. .card-body {
  280. display: flex;
  281. flex-direction: column;
  282. gap: 3vw; // 🟢 关键间距:解决挤在一起的问题
  283. .info-skeleton {
  284. display: flex;
  285. justify-content: space-between;
  286. align-items: center;
  287. gap: 4vw;
  288. .label-skeleton {
  289. flex: 0 0 30%;
  290. height: 18px;
  291. border-radius: 6px;
  292. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  293. background-size: 200% 100%;
  294. animation: skeleton-loading 1.2s ease-in-out infinite;
  295. }
  296. .value-skeleton {
  297. flex: 1;
  298. height: 18px;
  299. border-radius: 6px;
  300. background: linear-gradient(90deg, #f2f2f2 25%, #e4e4e4 50%, #f2f2f2 75%);
  301. background-size: 200% 100%;
  302. animation: skeleton-loading 1.2s ease-in-out infinite;
  303. }
  304. }
  305. /* 真实内容样式 */
  306. .info-item {
  307. display: flex;
  308. justify-content: space-between;
  309. align-items: flex-start;
  310. font-size: 3.8vw;
  311. color: #555;
  312. line-height: 1.6;
  313. .label {
  314. flex: 0 0 28vw;
  315. color: #888;
  316. }
  317. .value {
  318. flex: 1;
  319. text-align: right;
  320. color: #222;
  321. word-break: break-all;
  322. }
  323. .highlight {
  324. color: #ff7a00;
  325. font-weight: 600;
  326. }
  327. .link {
  328. color: #ff7a00;
  329. cursor: pointer;
  330. font-weight: 500;
  331. }
  332. }
  333. }
  334. }
  335. }
  336. /* ---------- 底部按钮 ---------- */
  337. .bottom-bar {
  338. position: fixed;
  339. bottom: 4vw;
  340. left: 0;
  341. right: 0;
  342. width: 100%;
  343. .next-btn {
  344. font-size: 4.2vw;
  345. width: 90%;
  346. margin: 0 auto;
  347. height: 12vw;
  348. box-shadow: 0 4px 10px rgba(255, 128, 54, 0.25);
  349. }
  350. }
  351. /* ---------- 骨架屏动画 ---------- */
  352. @keyframes skeleton-loading {
  353. 0% {
  354. background-position: 200% 0;
  355. }
  356. 100% {
  357. background-position: -200% 0;
  358. }
  359. }
  360. }
  361. /* 修正版:留白 + 按钮不被挤压 */
  362. .modern-dialog {
  363. width: 80vw;
  364. border-radius: 5vw !important;
  365. background: #fff;
  366. box-shadow: 0 8px 28px rgba(0, 0, 0, 0.15);
  367. padding: 0; /* 外层不再加内边距,改到内容区 */
  368. overflow: visible; /* 允许按钮投影外扩 */
  369. text-align: center;
  370. animation: dialogPop 0.28s ease-out;
  371. box-sizing: border-box;
  372. .dialog-content {
  373. display: flex;
  374. flex-direction: column;
  375. align-items: center;
  376. padding: 6vw 5vw 5vw; /* ✅ 内容留白在这里 */
  377. gap: 5vw; /* 内容与按钮的竖向间距 */
  378. }
  379. .dialog-title {
  380. font-size: 4.4vw;
  381. font-weight: 700;
  382. color: #222;
  383. }
  384. .dialog-message {
  385. font-size: 3.6vw;
  386. line-height: 1.7;
  387. color: #555;
  388. word-break: break-word;
  389. }
  390. /* ✅ 独立按钮:有左右边距与圆角,不会被挤压 */
  391. .dialog-btn {
  392. width: calc(100% - 10vw); /* 等同左右各 5vw 的边距 */
  393. margin: 0 auto 2.5vw; /* 底部也留点气口 */
  394. height: 12vw;
  395. border-radius: 3vw;
  396. font-size: 4vw;
  397. font-weight: 600;
  398. display: inline-flex; /* 避免被拉伸成整块底边 */
  399. align-items: center;
  400. justify-content: center;
  401. border: none;
  402. box-shadow: 0 4px 12px rgba(255, 128, 54, 0.25);
  403. letter-spacing: 0.2vw;
  404. }
  405. }
  406. /* 蒙层与动画(可保留) */
  407. :deep(.van-overlay) {
  408. backdrop-filter: blur(8px);
  409. background: rgba(0, 0, 0, 0.25);
  410. animation: fadeIn 0.3s ease;
  411. }
  412. @keyframes dialogPop {
  413. from {
  414. transform: translateY(4vw) scale(0.9);
  415. opacity: 0;
  416. }
  417. to {
  418. transform: translateY(0) scale(1);
  419. opacity: 1;
  420. }
  421. }
  422. </style>