accountInfo.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. <template>
  2. <div class="account-info-page">
  3. <h1 class="page-title">结算账户</h1>
  4. <section class="form-block">
  5. <h2 class="section-head">
  6. <span class="section-bar" aria-hidden="true" />
  7. 结算账户
  8. </h2>
  9. <el-form
  10. ref="formRef"
  11. class="account-form"
  12. :model="form"
  13. :rules="rules"
  14. label-width="120px"
  15. require-asterisk-position="right"
  16. label-position="right"
  17. >
  18. <el-form-item label="账户户名" prop="accountName">
  19. <el-input v-model="form.accountName" placeholder="请输入账户户名" clearable maxlength="64" style="max-width: 480px" />
  20. </el-form-item>
  21. <el-form-item label="账户类型" prop="accountType">
  22. <el-radio-group v-model="form.accountType" class="account-type-radio">
  23. <el-radio :value="BANK_ACCOUNT_TYPE_CORPORATE"> 对公账户 </el-radio>
  24. <el-radio :value="BANK_ACCOUNT_TYPE_PERSONAL"> 个人储蓄卡 </el-radio>
  25. </el-radio-group>
  26. </el-form-item>
  27. <el-form-item label="银行账号" prop="bankAccountNo">
  28. <div class="field-stack">
  29. <el-input
  30. v-model="form.bankAccountNo"
  31. placeholder="请输入开户银行账号"
  32. clearable
  33. maxlength="32"
  34. style="max-width: 480px"
  35. />
  36. <p class="field-tip">
  37. {{ bankAccountTip }}
  38. </p>
  39. </div>
  40. </el-form-item>
  41. <el-form-item label="开户银行" prop="bankCode">
  42. <div class="field-stack">
  43. <el-popover
  44. v-model:visible="bankPickerVisible"
  45. placement="bottom-start"
  46. :width="580"
  47. trigger="click"
  48. popper-class="bank-picker-popper"
  49. >
  50. <template #reference>
  51. <el-input
  52. readonly
  53. :model-value="selectedBankLabel"
  54. placeholder="请选择开户银行"
  55. style="max-width: 480px"
  56. class="bank-trigger-input"
  57. :class="{ 'is-focus-like': bankPickerVisible }"
  58. >
  59. <template #suffix>
  60. <el-icon class="el-select__caret">
  61. <ArrowDown />
  62. </el-icon>
  63. </template>
  64. </el-input>
  65. </template>
  66. <div class="bank-picker" @click.stop>
  67. <el-radio-group v-model="bankTab" size="small" class="bank-tab-group">
  68. <el-radio-button value="common"> 常用银行 </el-radio-button>
  69. <el-radio-button value="A-F"> ABCDEF </el-radio-button>
  70. <el-radio-button value="G-L"> GHJKL </el-radio-button>
  71. <el-radio-button value="M-P"> MNOP </el-radio-button>
  72. <el-radio-button value="Q-U"> QRSTU </el-radio-button>
  73. <el-radio-button value="V-Z"> VWXYZ </el-radio-button>
  74. </el-radio-group>
  75. <div class="bank-grid-wrap">
  76. <div class="bank-grid">
  77. <button
  78. v-for="b in filteredBanks"
  79. :key="b.code"
  80. type="button"
  81. class="bank-cell"
  82. :class="{ 'is-active': form.bankCode === b.code }"
  83. @click="selectBank(b)"
  84. >
  85. <span class="bank-logo" :style="{ background: b.color }">{{ b.logoText }}</span>
  86. <span class="bank-name">{{ b.shortName }}</span>
  87. </button>
  88. </div>
  89. </div>
  90. </div>
  91. </el-popover>
  92. </div>
  93. </el-form-item>
  94. </el-form>
  95. </section>
  96. <div class="footer-actions">
  97. <el-button @click="onBack"> 返回 </el-button>
  98. <el-button type="primary" class="btn-next" :loading="submitLoading" @click="onSubmit"> 确定 </el-button>
  99. </div>
  100. </div>
  101. </template>
  102. <script setup lang="ts">
  103. import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
  104. import { useRoute, useRouter } from "vue-router";
  105. import { ElMessage } from "element-plus";
  106. import type { FormInstance, FormRules } from "element-plus";
  107. import { ArrowDown } from "@element-plus/icons-vue";
  108. import { localGet, localSet } from "@/utils/index";
  109. const BUSINESS_DATA_CACHE_KEY = "businessData";
  110. const BANK_ACCOUNT_TYPE_CORPORATE = "BANK_ACCOUNT_TYPE_CORPORATE" as const;
  111. const BANK_ACCOUNT_TYPE_PERSONAL = "BANK_ACCOUNT_TYPE_PERSONAL" as const;
  112. const formRef = ref<FormInstance>();
  113. const submitLoading = ref(false);
  114. const bankPickerVisible = ref(false);
  115. const bankTab = ref("common");
  116. const route = useRoute();
  117. const router = useRouter();
  118. export interface BankItem {
  119. code: string;
  120. shortName: string;
  121. tabKey: "common" | "A-F" | "G-L" | "M-P" | "Q-U" | "V-Z";
  122. logoText: string;
  123. color: string;
  124. }
  125. /** 演示数据:常用 + 按首字母分组(与示意图「标签 + 网格」一致) */
  126. const BANK_LIST: BankItem[] = [
  127. { code: "ICBC", shortName: "工商银行", tabKey: "common", logoText: "工", color: "#c62828" },
  128. { code: "ABC", shortName: "农业银行", tabKey: "common", logoText: "农", color: "#2e7d32" },
  129. { code: "BOC", shortName: "中国银行", tabKey: "common", logoText: "中", color: "#b71c1c" },
  130. { code: "CCB", shortName: "建设银行", tabKey: "common", logoText: "建", color: "#1565c0" },
  131. { code: "COMM", shortName: "交通银行", tabKey: "common", logoText: "交", color: "#283593" },
  132. { code: "PSBC", shortName: "邮储银行", tabKey: "common", logoText: "邮", color: "#00695c" },
  133. { code: "CMB", shortName: "招商银行", tabKey: "common", logoText: "招", color: "#c62828" },
  134. { code: "SPDB", shortName: "浦发银行", tabKey: "common", logoText: "浦", color: "#0d47a1" },
  135. { code: "CIB", shortName: "兴业银行", tabKey: "common", logoText: "兴", color: "#1565c0" },
  136. { code: "CMBC", shortName: "民生银行", tabKey: "common", logoText: "民", color: "#4e342e" },
  137. { code: "HXB", shortName: "华夏银行", tabKey: "G-L", logoText: "华", color: "#d84315" },
  138. { code: "GDB", shortName: "广发银行", tabKey: "G-L", logoText: "广", color: "#c62828" },
  139. { code: "PAB", shortName: "平安银行", tabKey: "M-P", logoText: "平", color: "#f57f17" },
  140. { code: "CITIC", shortName: "中信银行", tabKey: "M-P", logoText: "信", color: "#c62828" },
  141. { code: "CEB", shortName: "光大银行", tabKey: "G-L", logoText: "光", color: "#6a1b9a" },
  142. { code: "BOSH", shortName: "上海银行", tabKey: "G-L", logoText: "上", color: "#283593" },
  143. { code: "BRCB", shortName: "北京银行", tabKey: "A-F", logoText: "京", color: "#c62828" },
  144. { code: "NJCB", shortName: "南京银行", tabKey: "M-P", logoText: "宁", color: "#c62828" },
  145. { code: "HZCB", shortName: "杭州银行", tabKey: "G-L", logoText: "杭", color: "#1565c0" },
  146. { code: "ASB", shortName: "鞍山银行", tabKey: "A-F", logoText: "鞍", color: "#37474f" },
  147. { code: "BSB", shortName: "包商银行", tabKey: "A-F", logoText: "包", color: "#455a64" },
  148. { code: "QDCCB", shortName: "青岛银行", tabKey: "M-P", logoText: "青", color: "#0d47a1" },
  149. { code: "WFCB", shortName: "潍坊银行", tabKey: "V-Z", logoText: "潍", color: "#1565c0" },
  150. { code: "WZCB", shortName: "温州银行", tabKey: "V-Z", logoText: "温", color: "#c62828" },
  151. { code: "XMBANK", shortName: "厦门银行", tabKey: "Q-U", logoText: "厦", color: "#00695c" },
  152. { code: "YTBANK", shortName: "烟台银行", tabKey: "V-Z", logoText: "烟", color: "#37474f" },
  153. { code: "ZZBANK", shortName: "郑州银行", tabKey: "V-Z", logoText: "郑", color: "#c62828" }
  154. ];
  155. const form = reactive({
  156. accountName: "",
  157. accountType: "BANK_ACCOUNT_TYPE_CORPORATE" as "BANK_ACCOUNT_TYPE_CORPORATE" | "BANK_ACCOUNT_TYPE_PERSONAL",
  158. bankAccountNo: "",
  159. bankCode: ""
  160. });
  161. const selectedBankLabel = computed(() => {
  162. if (!form.bankCode) return "";
  163. const b = BANK_LIST.find(x => x.code === form.bankCode);
  164. return b ? b.shortName : "";
  165. });
  166. const bankAccountTip = computed(() => {
  167. if (form.accountType === "BANK_ACCOUNT_TYPE_CORPORATE") {
  168. return "请务必填写与账户户名一致的对公银行账户。";
  169. }
  170. return "请务必填写与账户户名一致的个人储蓄卡账号。";
  171. });
  172. const tabToKey = computed<BankItem["tabKey"] | "common">(() => {
  173. const m: Record<string, BankItem["tabKey"] | "common"> = {
  174. common: "common",
  175. "A-F": "A-F",
  176. "G-L": "G-L",
  177. "M-P": "M-P",
  178. "Q-U": "Q-U",
  179. "V-Z": "V-Z"
  180. };
  181. return m[bankTab.value] ?? "common";
  182. });
  183. const filteredBanks = computed(() => {
  184. const k = tabToKey.value;
  185. if (k === "common") return BANK_LIST.filter(b => b.tabKey === "common");
  186. return BANK_LIST.filter(b => b.tabKey === k);
  187. });
  188. const rules: FormRules = {
  189. accountName: [{ required: true, message: "请输入账户户名", trigger: "blur" }],
  190. accountType: [{ required: true, message: "请选择账户类型", trigger: "change" }],
  191. bankAccountNo: [
  192. { required: true, message: "请输入银行账号", trigger: "blur" },
  193. { pattern: /^[\d\s-]{8,40}$/, message: "请输入有效的银行账号", trigger: "blur" }
  194. ],
  195. bankCode: [{ required: true, message: "请选择开户银行", trigger: "change" }]
  196. };
  197. function selectBank(b: BankItem) {
  198. form.bankCode = b.code;
  199. bankPickerVisible.value = false;
  200. formRef.value?.validateField("bankCode").catch(() => {});
  201. }
  202. function onBack() {
  203. router.push({ path: "/businessInfo/industryQualifications", query: { ...route.query } });
  204. }
  205. function onSaveDraft() {
  206. ElMessage.success("草稿已保存(可对接接口)");
  207. }
  208. /** 账户户名:subject_info.business_license_info.merchant_name */
  209. function readMerchantNameFromSubjectInfo(): string {
  210. const raw = localGet(BUSINESS_DATA_CACHE_KEY) as Record<string, unknown> | null | undefined;
  211. if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "";
  212. const si = raw.subject_info as Record<string, unknown> | undefined;
  213. if (!si || typeof si !== "object" || Array.isArray(si)) return "";
  214. const bli = si.business_license_info as Record<string, unknown> | undefined;
  215. if (!bli || typeof bli !== "object" || Array.isArray(bli)) return "";
  216. return String(bli.merchant_name ?? "").trim();
  217. }
  218. function resolveBankCodeFromAccountBank(accountBank: string): string {
  219. const s = accountBank.trim();
  220. if (!s) return "";
  221. const byCode = BANK_LIST.find(b => b.code === s);
  222. if (byCode) return byCode.code;
  223. const byShort = BANK_LIST.find(b => b.shortName === s);
  224. if (byShort) return byShort.code;
  225. return "";
  226. }
  227. /** 从缓存 bank_account_info 反显;无户名时用 subject_info 营业执照商户名 */
  228. function hydrateAccountFormFromBusinessDataCache() {
  229. const raw = localGet(BUSINESS_DATA_CACHE_KEY) as Record<string, unknown> | null | undefined;
  230. if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
  231. const m = readMerchantNameFromSubjectInfo();
  232. if (m) form.accountName = m;
  233. return;
  234. }
  235. const ba = raw.bank_account_info as Record<string, unknown> | undefined;
  236. if (!ba || typeof ba !== "object" || Array.isArray(ba)) {
  237. const m = readMerchantNameFromSubjectInfo();
  238. if (m) form.accountName = m;
  239. return;
  240. }
  241. const accName = String(ba.account_name ?? "").trim();
  242. form.accountName = accName || readMerchantNameFromSubjectInfo();
  243. const t = String(ba.bank_account_type ?? "").trim();
  244. if (t === BANK_ACCOUNT_TYPE_CORPORATE || t === BANK_ACCOUNT_TYPE_PERSONAL) {
  245. form.accountType = t;
  246. }
  247. form.bankAccountNo = String(ba.account_number ?? "").trim();
  248. const bankLabel = String(ba.account_bank ?? "").trim();
  249. form.bankCode = resolveBankCodeFromAccountBank(bankLabel);
  250. }
  251. onMounted(() => {
  252. hydrateAccountFormFromBusinessDataCache();
  253. nextTick(() => formRef.value?.clearValidate());
  254. });
  255. function mergeBankAccountIntoBusinessDataCache() {
  256. const prev = localGet(BUSINESS_DATA_CACHE_KEY);
  257. const base = prev && typeof prev === "object" && !Array.isArray(prev) ? { ...(prev as Record<string, unknown>) } : {};
  258. const prevBa =
  259. base.bank_account_info && typeof base.bank_account_info === "object" && !Array.isArray(base.bank_account_info)
  260. ? { ...(base.bank_account_info as Record<string, unknown>) }
  261. : {};
  262. const accountBankName = selectedBankLabel.value || String(form.bankCode || "").trim();
  263. base.bank_account_info = {
  264. ...prevBa,
  265. bank_account_type: form.accountType,
  266. account_bank: accountBankName,
  267. account_number: String(form.bankAccountNo || "")
  268. .replace(/\s+/g, "")
  269. .trim(),
  270. account_name: String(form.accountName || "").trim()
  271. };
  272. localSet(BUSINESS_DATA_CACHE_KEY, base);
  273. }
  274. async function onSubmit() {
  275. if (!formRef.value) return;
  276. submitLoading.value = true;
  277. try {
  278. await formRef.value.validate();
  279. mergeBankAccountIntoBusinessDataCache();
  280. await router.push({
  281. path: "/businessInfo/adminInfo",
  282. query: { ...route.query }
  283. });
  284. } catch {
  285. ElMessage.warning("请完善必填信息");
  286. } finally {
  287. submitLoading.value = false;
  288. }
  289. }
  290. watch(
  291. () => form.accountType,
  292. () => {
  293. formRef.value?.validateField("bankAccountNo").catch(() => {});
  294. }
  295. );
  296. </script>
  297. <style scoped lang="scss">
  298. $wechat-green: #07c160;
  299. .account-info-page {
  300. box-sizing: border-box;
  301. min-height: 100%;
  302. padding: 24px 32px 40px;
  303. margin: 0 auto;
  304. background: #ffffff;
  305. }
  306. .page-title {
  307. margin: 0 0 24px;
  308. font-size: 20px;
  309. font-weight: 600;
  310. color: #303133;
  311. }
  312. .form-block {
  313. margin-bottom: 24px;
  314. }
  315. .section-head {
  316. display: flex;
  317. align-items: center;
  318. margin: 0 0 20px;
  319. font-size: 16px;
  320. font-weight: 600;
  321. color: #303133;
  322. }
  323. .section-bar {
  324. display: inline-block;
  325. flex-shrink: 0;
  326. width: 4px;
  327. height: 16px;
  328. margin-right: 8px;
  329. background: $wechat-green;
  330. border-radius: 2px;
  331. }
  332. .account-form {
  333. :deep(.el-form-item__label) {
  334. font-weight: 500;
  335. color: #606266;
  336. }
  337. }
  338. .field-stack {
  339. width: 100%;
  340. }
  341. .field-tip {
  342. margin: 8px 0 0;
  343. font-size: 12px;
  344. line-height: 1.65;
  345. color: #909399;
  346. }
  347. .account-type-radio {
  348. :deep(.el-radio__input.is-checked .el-radio__inner) {
  349. background: $wechat-green;
  350. border-color: $wechat-green;
  351. }
  352. :deep(.el-radio__input.is-checked + .el-radio__label) {
  353. color: #303133;
  354. }
  355. }
  356. .bank-trigger-input {
  357. cursor: pointer;
  358. &.is-focus-like {
  359. :deep(.el-input__wrapper) {
  360. box-shadow: 0 0 0 1px $wechat-green inset;
  361. }
  362. }
  363. }
  364. .footer-actions {
  365. display: flex;
  366. align-items: center;
  367. justify-content: center;
  368. padding-top: 24px;
  369. margin-top: 16px;
  370. border-top: 1px solid #ebeef5;
  371. }
  372. .footer-center {
  373. display: flex;
  374. flex: 1;
  375. justify-content: center;
  376. }
  377. .btn-next {
  378. min-width: 100px;
  379. background: $wechat-green;
  380. border-color: $wechat-green;
  381. &:hover {
  382. background: #06ad56;
  383. border-color: #06ad56;
  384. }
  385. }
  386. </style>
  387. <style lang="scss">
  388. $wechat-green: #07c160;
  389. .bank-picker-popper {
  390. padding: 0 !important;
  391. }
  392. .bank-picker {
  393. .bank-tab-group {
  394. display: flex;
  395. flex-wrap: wrap;
  396. gap: 6px;
  397. padding: 8px 4px 4px;
  398. :deep(.el-radio-button__inner) {
  399. padding: 6px 10px;
  400. font-size: 12px;
  401. }
  402. :deep(.el-radio-button.is-active .el-radio-button__inner) {
  403. color: #ffffff;
  404. background: $wechat-green;
  405. border-color: $wechat-green;
  406. box-shadow: -1px 0 0 0 $wechat-green;
  407. }
  408. }
  409. .bank-grid-wrap {
  410. max-height: 280px;
  411. padding: 8px 4px 12px;
  412. overflow: auto;
  413. }
  414. .bank-grid {
  415. display: grid;
  416. grid-template-columns: repeat(3, 1fr);
  417. gap: 8px 12px;
  418. }
  419. .bank-cell {
  420. display: flex;
  421. gap: 8px;
  422. align-items: center;
  423. padding: 8px 10px;
  424. margin: 0;
  425. text-align: left;
  426. cursor: pointer;
  427. background: #ffffff;
  428. border: 1px solid transparent;
  429. border-radius: 6px;
  430. transition: background 0.15s;
  431. &:hover {
  432. background: #f5f7fa;
  433. }
  434. &.is-active {
  435. background: #e8f5e9;
  436. border-color: $wechat-green;
  437. }
  438. }
  439. .bank-logo {
  440. display: inline-flex;
  441. flex-shrink: 0;
  442. align-items: center;
  443. justify-content: center;
  444. width: 28px;
  445. height: 28px;
  446. font-size: 12px;
  447. font-weight: 600;
  448. color: #ffffff;
  449. border-radius: 4px;
  450. }
  451. .bank-name {
  452. font-size: 13px;
  453. line-height: 1.3;
  454. color: #303133;
  455. }
  456. }
  457. </style>