|
|
@@ -1,61 +1,173 @@
|
|
|
<template>
|
|
|
<div class="app-container">
|
|
|
- <el-alert type="info" :closable="false" style="margin-bottom: 12px" :title="MODE_HINT[mode]" />
|
|
|
+ <!-- ==================== 第一屏就是列表 ====================
|
|
|
+ 这一页从「一张 41 字段的大表单」改成了「列表打头 + 每行一个下一步」:
|
|
|
+ 操作员进来先看见自己有哪些商户、各自卡在哪一步、下一步该点哪个按钮。
|
|
|
+ 大表单还在,但只在点「新建商户」/「改资料」时才展开。 -->
|
|
|
+ <el-card shadow="never">
|
|
|
+ <div class="list-head">
|
|
|
+ <div>
|
|
|
+ <h3 class="list-head__title">我的商户</h3>
|
|
|
+ <p class="list-head__sub">
|
|
|
+ 在这里创建商户、把资料交给支付渠道审核 —— 审核通过后就能收款。
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" @click="openCreate">+ 新建商户</el-button>
|
|
|
+ </div>
|
|
|
+ <el-form :inline="true" @submit.prevent>
|
|
|
+ <el-form-item label="搜索">
|
|
|
+ <el-input
|
|
|
+ v-model="query.keyword"
|
|
|
+ placeholder="商户号 / 商户名"
|
|
|
+ clearable
|
|
|
+ style="width: 260px"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleSearch">查询</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- ==================== 上一次操作的结果(常驻到下一次操作) ====================
|
|
|
+ toast 会自己消失,而这几条是「下一步该干嘛」的依据,必须留得住。 -->
|
|
|
|
|
|
- <!-- 商户号是**建号腿**唯一的产出:toast 会自己消失,这里**常驻**到下一次提交 -->
|
|
|
- <el-alert v-if="createdMchNo" type="success" :closable="false" style="margin-bottom: 12px">
|
|
|
+ <!-- 建号成功:本页唯一的产出是商户号,而**下一步是提交审核** —— 两件事一起说 -->
|
|
|
+ <el-alert v-if="createdMchNo" type="success" :closable="false" style="margin: 12px 0">
|
|
|
<template #title>
|
|
|
- 提交成功,商户号:
|
|
|
- <strong class="mch-no">{{ createdMchNo }}</strong>
|
|
|
+ 商户已建好,商户号:<strong class="mch-no">{{ createdMchNo }}</strong>
|
|
|
+ —— 下一步:点它那一行的「提交审核」。
|
|
|
</template>
|
|
|
<template #default>
|
|
|
- <!-- 建号后当场发现填错,是更新腿最常见的入口 —— 这里少一次「回头找商户号」 -->
|
|
|
- <el-button link type="primary" @click="startUpdateFor(createdMchNo)">
|
|
|
+ <el-button link type="primary" @click="openUpdate(createdMchNo)">
|
|
|
资料填错了?改这个商户
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-alert>
|
|
|
|
|
|
- <!-- 更新腿**不回**商户号(`Result<Void>`),所以这里显示的是**请求里那个**,不是响应里的 -->
|
|
|
- <el-alert v-if="updatedMchNo" type="success" :closable="false" style="margin-bottom: 12px">
|
|
|
+ <!-- 资料已改(更新腿**不回**商户号,`Result<Void>`)⇒ 这里显示的是**请求里那个** -->
|
|
|
+ <el-alert v-if="updatedMchNo" type="success" :closable="false" style="margin: 12px 0">
|
|
|
<template #title>
|
|
|
- 已提交更新:
|
|
|
- <strong class="mch-no">{{ updatedMchNo }}</strong>
|
|
|
- —— 表单里留空的项,上游原值未变。
|
|
|
+ 已提交更新:<strong class="mch-no">{{ updatedMchNo }}</strong>
|
|
|
+ —— 表单里留空的项,渠道那边的原值未变。
|
|
|
</template>
|
|
|
</el-alert>
|
|
|
|
|
|
- <!-- 信封说成功、却没有商户号:既不能说成功(拿不到号),也不能说失败(可能已经建出来了) -->
|
|
|
+ <!-- 审核已提交 -->
|
|
|
<el-alert
|
|
|
- v-if="submitNoMchNo"
|
|
|
+ v-if="committedMchNo"
|
|
|
+ type="success"
|
|
|
+ :closable="false"
|
|
|
+ style="margin: 12px 0"
|
|
|
+ :title="`已提交审核:${committedMchNo} —— 审核通过后就能收款了。`"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- ★★ 409:**上游已经收到这次提交了**,只是本地状态没同步上。
|
|
|
+ 这条**不能**报成「提交失败」:操作员会再点一次,而平台那边会再收一次、且无法回滚。
|
|
|
+ 后端文案逐字含「不得当作失败直接重试」,这里把它翻成一句行动指令。 -->
|
|
|
+ <el-alert
|
|
|
+ v-if="commitSyncFailedMchNo"
|
|
|
type="warning"
|
|
|
:closable="false"
|
|
|
show-icon
|
|
|
- style="margin-bottom: 12px"
|
|
|
- title="提交已被受理,但响应里没有商户号 —— 请先核对是否已建号,直接重提会再建一个商户。"
|
|
|
+ style="margin: 12px 0"
|
|
|
+ :title="`已提交给支付渠道(${commitSyncFailedMchNo}),但本地状态没同步上。请**刷新列表**核对后再操作 —— 直接重试会让平台再收一次。`"
|
|
|
/>
|
|
|
|
|
|
+ <!-- 信封说成功、却没有商户号:既不能说成功(拿不到号),也不能说失败(可能已经建出来了) -->
|
|
|
<el-alert
|
|
|
- v-if="cascadeFailed"
|
|
|
- type="error"
|
|
|
+ v-if="submitNoMchNo"
|
|
|
+ type="warning"
|
|
|
:closable="false"
|
|
|
show-icon
|
|
|
- style="margin-bottom: 12px"
|
|
|
- title="省市区 / 行业类目 未能加载,这两组级联为空时无法完成填写"
|
|
|
+ style="margin: 12px 0"
|
|
|
+ title="提交已被受理,但响应里没有商户号 —— 请先核对是否已建号,直接重提会再建一个商户。"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-card shadow="never" style="margin-top: 12px">
|
|
|
+ <el-table v-loading="loading" :data="list" border stripe>
|
|
|
+ <el-table-column prop="mch_no" label="商户号" min-width="170" />
|
|
|
+ <el-table-column label="商户名" min-width="200">
|
|
|
+ <template #default="{ row }">{{ row.merchant_name || "—" }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="进度" width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tooltip :disabled="!!progressOf(row)" :content="row.review_status || '(空)'">
|
|
|
+ <el-tag :type="progressOf(row)?.tag ?? 'info'" size="small">
|
|
|
+ {{ progressOf(row)?.text ?? "状态未知" }}
|
|
|
+ </el-tag>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <!-- ★ 每行**恰好一个主按钮**,由进度判定(`actionsFor` 是唯一那张表)——
|
|
|
+ 操作员不需要在多个动作之间做选择。非主按钮只在「还差另一条腿走不完」时出现。 -->
|
|
|
+ <el-table-column label="操作" width="230" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button
|
|
|
+ v-for="a in actionsFor(row)"
|
|
|
+ :key="a.text"
|
|
|
+ type="primary"
|
|
|
+ :link="!a.primary"
|
|
|
+ @click="a.run(row)"
|
|
|
+ >
|
|
|
+ {{ a.text }}
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <!-- 空态:这是**唯一**引导新建的地方之外的第二处(另一处是右上角那颗按钮) -->
|
|
|
+ <template #empty>
|
|
|
+ <el-empty description="还没有商户">
|
|
|
+ <el-button type="primary" @click="openCreate">新建商户</el-button>
|
|
|
+ </el-empty>
|
|
|
+ </template>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="query.pageNo"
|
|
|
+ v-model:page-size="query.pageSize"
|
|
|
+ :total="total"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ style="margin-top: 12px; justify-content: flex-end"
|
|
|
+ @change="loadList"
|
|
|
+ />
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- ==================== 大表单抽屉(新建 / 改资料) ====================
|
|
|
+ ★ 模式由**打开它的那颗按钮**决定,不给操作员一个「建号/更新」单选 ——
|
|
|
+ 那是给开发者看的开关,选错了的代价是把资料发去改别人的商户。
|
|
|
+ ★ `destroy-on-close`:每次打开都是**全新的表单 DOM**;表单模型在页面作用域里,
|
|
|
+ 由 `prepareForm` 显式复位(两者各管一段,缺一不可)。 -->
|
|
|
+ <el-drawer
|
|
|
+ v-model="formVisible"
|
|
|
+ :title="mode === 'create' ? '新建商户' : `修改资料:${form.mch_no}`"
|
|
|
+ size="80%"
|
|
|
+ destroy-on-close
|
|
|
>
|
|
|
- <template #default>
|
|
|
- <el-button link type="primary" @click="reloadCascades">重新加载</el-button>
|
|
|
- </template>
|
|
|
- </el-alert>
|
|
|
+ <el-alert
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ style="margin-bottom: 12px"
|
|
|
+ :title="MODE_HINT[mode]"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-if="cascadeFailed"
|
|
|
+ type="error"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ style="margin-bottom: 12px"
|
|
|
+ title="省市区 / 行业类目 未能加载,这两组级联为空时无法完成填写"
|
|
|
+ >
|
|
|
+ <template #default>
|
|
|
+ <el-button link type="primary" @click="reloadCascades">重新加载</el-button>
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
|
|
|
- <el-card shadow="never">
|
|
|
- <!-- ★ 两种模式**共用**这一张表单、这一套级联。不另起页面/弹窗:更新腿的字段集是
|
|
|
+ <!-- ★ 两种模式**共用**这一张表单、这一套级联。不另起一份:更新腿的字段集是
|
|
|
建号腿那张表**减去两个**(见 `UPDATE_EXCLUDED_KEYS`),另起一份必然漂移。 -->
|
|
|
- <el-radio-group v-model="mode" style="margin-bottom: 16px" @change="switchMode">
|
|
|
- <el-radio-button value="create">建号(新建商户)</el-radio-button>
|
|
|
- <el-radio-button value="update">更新(改已有商户资料)</el-radio-button>
|
|
|
- </el-radio-group>
|
|
|
-
|
|
|
<el-form
|
|
|
ref="formRef"
|
|
|
:model="form"
|
|
|
@@ -64,7 +176,7 @@
|
|
|
label-position="right"
|
|
|
v-loading="submitting"
|
|
|
>
|
|
|
- <!-- 商户号:**只有更新模式**有 —— 建号时它由服务端产出,不是输入 -->
|
|
|
+ <!-- 商户号:**只有改资料模式**显示它 —— 新建时它由服务端产出,不是输入 -->
|
|
|
<el-form-item v-if="mode === 'update'" label="商户号" prop="mch_no">
|
|
|
<el-input
|
|
|
v-model="form.mch_no"
|
|
|
@@ -73,7 +185,7 @@
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
|
|
|
- <!-- 商户类别:**只有建号模式**有 —— 更新页的参数表里压根没有它
|
|
|
+ <!-- 商户类别:**只有新建模式**有 —— 更新页的参数表里压根没有它
|
|
|
(`merchant_type` 不在那张表里 ⇒ 建号腿「一个 category 派生两个输出」在更新腿不成立) -->
|
|
|
<el-form-item v-if="mode === 'create'" label="商户类别" prop="category">
|
|
|
<el-select v-model="form.category" placeholder="请选择商户类别" style="width: 320px">
|
|
|
@@ -255,11 +367,68 @@
|
|
|
<div class="form-actions">
|
|
|
<el-button @click="handleReset">重置</el-button>
|
|
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">
|
|
|
- {{ mode === "update" ? "提交更新" : "提交进件" }}
|
|
|
+ {{ mode === "update" ? "保存修改" : "创建商户" }}
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</el-form>
|
|
|
- </el-card>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- ==================== 提交审核(6 个字段) ====================
|
|
|
+ 键名逐字 = `LsyCommitMerchantInfoDTO` 的字段名经全局 SNAKE_CASE 的结果,
|
|
|
+ 详见 `api/module_payment/onboarding.ts` 的 `commitOnboardingMerchant`。 -->
|
|
|
+ <el-dialog v-model="commitVisible" title="提交审核" width="600px">
|
|
|
+ <el-alert type="info" :closable="false" style="margin-bottom: 12px">
|
|
|
+ <template #title>
|
|
|
+ 把商户号
|
|
|
+ <strong class="mch-no">{{ commitRow?.mch_no }}</strong>
|
|
|
+ 的资料提交给支付渠道审核。六项都是必填。
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+
|
|
|
+ <el-form ref="commitFormRef" :model="commitForm" :rules="commitRules" label-width="140px">
|
|
|
+ <el-form-item :label="COMMIT_LABELS.mcht_aptitude" prop="mcht_aptitude">
|
|
|
+ <!-- ★ 十二个码值是**照抄**上游资质表的原文(见 `LsyCommitMerchantInfoDTO` 的 javadoc)。
|
|
|
+ `filterable allow-create` 是给「上游哪天加码」留的口子 —— 纯下拉会把新码堵死在页面上。 -->
|
|
|
+ <el-select
|
|
|
+ v-model="commitForm.mcht_aptitude"
|
|
|
+ placeholder="请选择商户资质"
|
|
|
+ filterable
|
|
|
+ allow-create
|
|
|
+ default-first-option
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="a in APTITUDE_OPTIONS"
|
|
|
+ :key="a.value"
|
|
|
+ :label="`${a.value} ${a.label}`"
|
|
|
+ :value="a.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="COMMIT_LABELS.mcht_aptitude_note" prop="mcht_aptitude_note">
|
|
|
+ <el-input v-model="commitForm.mcht_aptitude_note" placeholder="资质说明,例如「无」" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="COMMIT_LABELS.store_loca" prop="store_loca">
|
|
|
+ <el-input v-model="commitForm.store_loca" placeholder="门店所在位置" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="COMMIT_LABELS.mcht_landuse" prop="mcht_landuse">
|
|
|
+ <el-input v-model="commitForm.mcht_landuse" placeholder="营业用地,例如「自有」/「租赁」" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="COMMIT_LABELS.longitude" prop="longitude">
|
|
|
+ <el-input v-model="commitForm.longitude" placeholder="例如 116.397" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="COMMIT_LABELS.latitude" prop="latitude">
|
|
|
+ <el-input v-model="commitForm.latitude" placeholder="例如 39.908" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="commitVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="commitSubmitting" @click="submitCommit">
|
|
|
+ 提交审核
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -275,14 +444,17 @@ defineOptions({
|
|
|
import { computed, onMounted, reactive, ref } from "vue";
|
|
|
import {
|
|
|
MERCHANT_CATEGORY_OPTIONS,
|
|
|
+ commitOnboardingMerchant,
|
|
|
createOnboardingMerchant,
|
|
|
getOnboardingAreas,
|
|
|
getOnboardingMcc,
|
|
|
+ getOnboardingPage,
|
|
|
updateOnboardingMerchant,
|
|
|
uploadOnboardingImage,
|
|
|
type OnboardingArea,
|
|
|
type OnboardingLevel,
|
|
|
type OnboardingMcc,
|
|
|
+ type OnboardingMerchantRow,
|
|
|
} from "@/api/module_payment/onboarding";
|
|
|
import SingleImageUpload from "@/components/Upload/SingleImageUpload.vue";
|
|
|
|
|
|
@@ -297,6 +469,266 @@ import SingleImageUpload from "@/components/Upload/SingleImageUpload.vue";
|
|
|
const UPLOAD_MAX_MB = 2;
|
|
|
const UPLOAD_ACCEPT = ".jpg,.jpeg,.png,.bmp";
|
|
|
|
|
|
+// ==================== 列表 ====================
|
|
|
+
|
|
|
+const query = reactive({ pageNo: 1, pageSize: 20, keyword: "" });
|
|
|
+const list = ref<OnboardingMerchantRow[]>([]);
|
|
|
+const total = ref(0);
|
|
|
+const loading = ref(false);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 读法照已上线的 `collection/merchant/index.vue`:`res.data.data?.list` / `?.total`
|
|
|
+ * (响应拦截器**不解包** ⇒ 两层 `.data`;分页字段名是 `list`/`total`,不是 `records`/`rows`)。
|
|
|
+ */
|
|
|
+async function loadList() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getOnboardingPage({ ...query });
|
|
|
+ list.value = res.data.data?.list || [];
|
|
|
+ total.value = res.data.data?.total || 0;
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleSearch() {
|
|
|
+ query.pageNo = 1;
|
|
|
+ loadList();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 进度 → 文案。**四个键逐字 = `LsyMerchantReviewStatus` 的四个字面值**,别在这里造第五个词。
|
|
|
+ *
|
|
|
+ * ★ 前三个键是**本地词表**(`PENDING` 由建号写、`SUBMITTED` 由提交审核的记号写、
|
|
|
+ * `APPROVED`/`REJECTED` 由审核结果回调写)。上游自己有另一套 `reviewed_status` 码值,
|
|
|
+ * 本页**不碰** —— 上游那套在同一页里有互相冲突的 0–6 两张表,映射谁都是在猜
|
|
|
+ * (见 `LsyOnboardingService.reportQuery` 的 javadoc)。
|
|
|
+ * ★ 表里没有的值 ⇒ 显示「状态未知」,并把原始值放进 tooltip(不静默当成某一档)。
|
|
|
+ */
|
|
|
+const PROGRESS: Record<string, { text: string; tag: "success" | "info" | "warning" | "danger" }> = {
|
|
|
+ PENDING: { text: "待提交审核", tag: "warning" },
|
|
|
+ SUBMITTED: { text: "审核中", tag: "info" },
|
|
|
+ APPROVED: { text: "可以收款了", tag: "success" },
|
|
|
+ REJECTED: { text: "审核未通过", tag: "danger" },
|
|
|
+};
|
|
|
+
|
|
|
+function progressOf(row: OnboardingMerchantRow) {
|
|
|
+ return PROGRESS[row.review_status ?? ""] ?? null;
|
|
|
+}
|
|
|
+
|
|
|
+interface RowAction {
|
|
|
+ text: string;
|
|
|
+ primary: boolean;
|
|
|
+ run: (row: OnboardingMerchantRow) => void;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 每行该显示哪几个按钮 —— **本页唯一一处「下一步是什么」的判定**。
|
|
|
+ *
|
|
|
+ * ★ 只有一条规则:**恰好一个主按钮**(由进度决定,操作员不需要选),
|
|
|
+ * 外加**至多一个**文字按钮,且它只在「非『可以收款了』」时出现。
|
|
|
+ *
|
|
|
+ * 为什么非「可以收款了」的行都要留一个「提交审核」入口:提交审核是把资料送审的**唯一**
|
|
|
+ * 一条腿。少了它,「审核未通过」的行改完资料就再也走不到送审那一步 —— 按钮会一直停在
|
|
|
+ * 「改资料」上,操作员只能反复点它。
|
|
|
+ * ★ 「可以收款了」**不给**重新提交:那一档再送一次没有意义,而误点一次的代价是一次
|
|
|
+ * 不可回滚的上游调用。
|
|
|
+ * ★ 主按钮的取法:卡在「待提交审核」的下一步是**送审**;其余各档的下一步都是**先改资料**
|
|
|
+ * (审核中/被驳回时,不改就再送一次几乎必然还是同一个结果)。
|
|
|
+ */
|
|
|
+function actionsFor(row: OnboardingMerchantRow): RowAction[] {
|
|
|
+ const submit: RowAction = { text: "提交审核", primary: false, run: openCommit };
|
|
|
+ const edit: RowAction = { text: "改资料", primary: false, run: (r) => openUpdate(r.mch_no) };
|
|
|
+
|
|
|
+ switch (row.review_status) {
|
|
|
+ case "PENDING":
|
|
|
+ return [{ ...submit, primary: true }, edit];
|
|
|
+ case "APPROVED":
|
|
|
+ return [{ ...edit, primary: true }];
|
|
|
+ case "SUBMITTED":
|
|
|
+ case "REJECTED":
|
|
|
+ return [{ ...edit, primary: true }, submit];
|
|
|
+ default:
|
|
|
+ // 表里没有的值:不猜下一步是送审还是改资料,给保守的那条(改资料),
|
|
|
+ // 并保留送审入口,免得这一行变成死路。
|
|
|
+ return [{ ...edit, primary: true }, submit];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 打开两张表单 ====================
|
|
|
+
|
|
|
+const formVisible = ref(false);
|
|
|
+/** 大表单的模式 —— **只由打开它的那颗按钮决定**,页面上没有切换开关。 */
|
|
|
+const mode = ref<"create" | "update">("create");
|
|
|
+
|
|
|
+function openCreate() {
|
|
|
+ mode.value = "create";
|
|
|
+ prepareForm();
|
|
|
+ formVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function openUpdate(mchNo: string) {
|
|
|
+ mode.value = "update";
|
|
|
+ prepareForm();
|
|
|
+ // ★ 只预填 `mch_no`,**不**把上一次表单里那一堆值带过去:更新是增量的,
|
|
|
+ // 带过去等于「按上次填的再覆盖一遍」,而不是「只改要改的那一项」。
|
|
|
+ form.mch_no = mchNo;
|
|
|
+ formVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 每次打开抽屉前复位。`el-drawer` 的 `destroy-on-close` 换掉的是 **DOM**,
|
|
|
+ * 而 `form` 是页面作用域里的 `reactive` ⇒ 两件事各管一段,缺一不可。
|
|
|
+ *
|
|
|
+ * ★ 复位**结果提示**:那是「上一次那个动作」的结论,留着会让人以为这次已经做过了。
|
|
|
+ * ★ 级联**只拉一次**:`cascadesLoaded` 之后复用(下拉的选项与表单无关,重进页面才该重拉)。
|
|
|
+ * 失败时的重试入口是抽屉里那条红色提示上的「重新加载」。
|
|
|
+ */
|
|
|
+function prepareForm() {
|
|
|
+ Object.assign(form, emptyForm());
|
|
|
+ activeSections.value = SECTIONS.map((s) => s.key);
|
|
|
+ resetCascadePicks();
|
|
|
+ createdMchNo.value = "";
|
|
|
+ updatedMchNo.value = "";
|
|
|
+ submitNoMchNo.value = false;
|
|
|
+ if (!cascadesLoaded.value) {
|
|
|
+ cascadesLoaded.value = true;
|
|
|
+ loadProvinces();
|
|
|
+ loadMccLevel1();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 提交审核 ====================
|
|
|
+
|
|
|
+const commitVisible = ref(false);
|
|
|
+const commitRow = ref<OnboardingMerchantRow | null>(null);
|
|
|
+const commitFormRef = ref();
|
|
|
+const commitSubmitting = ref(false);
|
|
|
+const committedMchNo = ref("");
|
|
|
+const commitSyncFailedMchNo = ref("");
|
|
|
+
|
|
|
+/**
|
|
|
+ * 六个业务字段的**线上键名逐字**(`LsyCommitMerchantInfoDTO` 的字段名经全局 SNAKE_CASE)。
|
|
|
+ * 中括号里是页面上给操作员看的中文 —— 前五个逐字 = 服务端 `requireText` 报错时用的标签。
|
|
|
+ *
|
|
|
+ * ★ 第六个(`latitude`)**两处措辞不同**:服务端那侧逐字是「营业地址**维度**」
|
|
|
+ * (上游说明列的错字,那边按「标签逐字取说明列」的纪律照抄,`LsyOnboardingService` 里有注释)。
|
|
|
+ * 页面这里用正确的「纬度」—— 本页的必填校验会先拦住,所以操作员通常看不到服务端那句话;
|
|
|
+ * 万一看到,指的是同一个框。
|
|
|
+ */
|
|
|
+const COMMIT_LABELS: Record<string, string> = {
|
|
|
+ mcht_aptitude: "商户资质",
|
|
|
+ mcht_aptitude_note: "商户资质备注",
|
|
|
+ store_loca: "门店所在位置",
|
|
|
+ mcht_landuse: "营业用地",
|
|
|
+ longitude: "营业地址经度",
|
|
|
+ latitude: "营业地址纬度",
|
|
|
+};
|
|
|
+
|
|
|
+/** 提交审核报文的字段清单 —— 与 `COMMIT_LABELS` 同键集,**一处派生、一处校验**。 */
|
|
|
+const COMMIT_KEYS = Object.keys(COMMIT_LABELS);
|
|
|
+
|
|
|
+function emptyCommit(): Record<string, string> {
|
|
|
+ const f: Record<string, string> = {};
|
|
|
+ for (const k of COMMIT_KEYS) f[k] = "";
|
|
|
+ return f;
|
|
|
+}
|
|
|
+
|
|
|
+const commitForm = reactive<Record<string, string>>(emptyCommit());
|
|
|
+
|
|
|
+/**
|
|
|
+ * 六项**全部必填**(上游那页参数表逐个标了「必填=是」,服务端 `requireText` 六条一一对应)。
|
|
|
+ * 这里不像大表单那样「只钉一条」,因为那边是**按类别分档**的必填矩阵(抄一份就是两个真值来源),
|
|
|
+ * 而这边是**无条件**的六条,且标签与服务端逐字相同。
|
|
|
+ * `trigger` 带上 `change`:`mcht_aptitude` 是下拉,只挂 `blur` 永远不会触发。
|
|
|
+ */
|
|
|
+const commitRules = Object.fromEntries(
|
|
|
+ COMMIT_KEYS.map((k) => [
|
|
|
+ k,
|
|
|
+ [{ required: true, message: `请填写${COMMIT_LABELS[k]}`, trigger: ["blur", "change"] }],
|
|
|
+ ])
|
|
|
+);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 商户资质的十二个码值 —— **照抄** `LsyCommitMerchantInfoDTO` 的 javadoc(那边逐字抄自上游参数表)。
|
|
|
+ * ★ 只送 `value`(码),`label` 只用于显示 —— 与 `MERCHANT_CATEGORY_OPTIONS` 同一条纪律。
|
|
|
+ * ★ 本仓**不校验**这张表(上游加码是预期内的事),所以下拉开了 `allow-create`。
|
|
|
+ */
|
|
|
+const APTITUDE_OPTIONS = [
|
|
|
+ { value: "01", label: "央企、国企" },
|
|
|
+ { value: "02", label: "央企国企子公司" },
|
|
|
+ { value: "03", label: "上市公司" },
|
|
|
+ { value: "04", label: "上市公司子公司" },
|
|
|
+ { value: "05", label: "全国连锁企业" },
|
|
|
+ { value: "06", label: "当地连锁企业" },
|
|
|
+ { value: "07", label: "一般商户" },
|
|
|
+ { value: "08", label: "五星级及以上或同等级商务酒店" },
|
|
|
+ { value: "09", label: "四星级或同等级商务酒店" },
|
|
|
+ { value: "10", label: "准四星酒店" },
|
|
|
+ { value: "11", label: "三星酒店" },
|
|
|
+ { value: "12", label: "二星级及以下酒店宾馆" },
|
|
|
+] as const;
|
|
|
+
|
|
|
+function openCommit(row: OnboardingMerchantRow) {
|
|
|
+ commitRow.value = row;
|
|
|
+ Object.assign(commitForm, emptyCommit());
|
|
|
+ committedMchNo.value = "";
|
|
|
+ commitSyncFailedMchNo.value = "";
|
|
|
+ commitVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 提交审核报文 —— **空白整个省掉,不送空串**(见 `api/module_payment/onboarding.ts` 文件头 ★★:
|
|
|
+ * 后端只剔 `null`,而空串会进上游签名原文并照发)。六项都被页面校验过,正常不会有空白;
|
|
|
+ * 真漏了也只会落到服务端那条按中文标签报出来的 400,而不是把空串发上去。
|
|
|
+ */
|
|
|
+function buildCommitBody(): Record<string, string> {
|
|
|
+ const body: Record<string, string> = { mch_no: commitRow.value?.mch_no ?? "" };
|
|
|
+ for (const k of COMMIT_KEYS) {
|
|
|
+ const v = (commitForm[k] ?? "").trim();
|
|
|
+ if (v !== "") body[k] = v;
|
|
|
+ }
|
|
|
+ return body;
|
|
|
+}
|
|
|
+
|
|
|
+async function submitCommit() {
|
|
|
+ try {
|
|
|
+ await commitFormRef.value?.validate();
|
|
|
+ } catch {
|
|
|
+ // 校验没过:`el-form-item` 已就地标红,不再弹 toast
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ commitSubmitting.value = true;
|
|
|
+ committedMchNo.value = "";
|
|
|
+ commitSyncFailedMchNo.value = "";
|
|
|
+ try {
|
|
|
+ await commitOnboardingMerchant(buildCommitBody());
|
|
|
+ committedMchNo.value = commitRow.value?.mch_no ?? "";
|
|
|
+ commitVisible.value = false;
|
|
|
+ } catch (e: any) {
|
|
|
+ // ★★ 409 是**后端特意造出来**的那一条:上游已经收到这次提交了,只是本地状态没同步上
|
|
|
+ // (`LeshouyingOnboardingController.markSubmittedOrSayWhy`)。它**不是**失败 ——
|
|
|
+ // 当成失败处理会让操作员原地再点一次,而平台那边会再收一次、且无法回滚。
|
|
|
+ // ⇒ 按「已提交」对待:关掉弹窗、刷新列表、把这句话留在页面上。
|
|
|
+ // ★ 读法:拦截器对信封失败走的是 `Promise.reject(response)`,所以 `e` 就是那个
|
|
|
+ // AxiosResponse ⇒ 这里读 `e.data.code`(**不是** `e.response.data.code`,那是网络层错误的形状)。
|
|
|
+ // 非网络层的错误(超时等)reject 的是 `Error`,没有 `.data` ⇒ `?.` 兜住。
|
|
|
+ if (e?.data?.code === 409) {
|
|
|
+ commitSyncFailedMchNo.value = commitRow.value?.mch_no ?? "";
|
|
|
+ commitVisible.value = false;
|
|
|
+ }
|
|
|
+ // 其余失败:文案由拦截器统一弹(必填缺失 / 非本租户 404 / 上游失败 502 都在 `msg` 里)
|
|
|
+ // ⇒ 页面**不重复弹**,也不把它当成功往下走。这里只是不让 rejection 逃逸成未处理的拒绝。
|
|
|
+ } finally {
|
|
|
+ commitSubmitting.value = false;
|
|
|
+ // 两条路(成功 / 409)都要刷新列表 —— 列表上那一列进度正是这次操作的结果
|
|
|
+ await loadList();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 大表单的字段清单 ====================
|
|
|
+
|
|
|
/** 一个字段:`key` 逐字 = 线上报文键名(snake_case),`label` 逐字 = 服务端那张中文标签表。 */
|
|
|
interface Field {
|
|
|
key: string;
|
|
|
@@ -325,7 +757,7 @@ const MCC_FIELD: Field = { key: "merchant_mcc_code", label: "商户行业类目c
|
|
|
const UPLOAD_HINT = "支持 jpg/jpeg/png/bmp、单张不超过 2MB";
|
|
|
|
|
|
/**
|
|
|
- * ★★ 本页的**唯一**一张字段清单:分区折叠的渲染、以及建号报文的键白名单,都从它推导
|
|
|
+ * ★★ 本页大表单的**唯一**一张字段清单:分区折叠的渲染、以及建号报文的键白名单,都从它推导
|
|
|
* (见 `CREATE_KEYS`)—— 两处各写一份必然漂移。
|
|
|
*
|
|
|
* `label` **逐字**抄自服务端那张中文标签表(覆盖该页 62 个参数、javadoc 明说用途之一是
|
|
|
@@ -470,18 +902,16 @@ const UPDATE_EXCLUDED_KEYS: readonly string[] = ["merchant_name_business", "leas
|
|
|
const UPDATE_KEYS: string[] = CREATE_KEYS.filter((k) => !UPDATE_EXCLUDED_KEYS.includes(k));
|
|
|
|
|
|
/**
|
|
|
- * 本页两种模式 —— **同一张表、同一套级联、两个出口**(`/create` 与 `/update`)。
|
|
|
+ * 大表单两种模式 —— **同一张表、同一套级联、两个出口**(`/create` 与 `/update`)。
|
|
|
*
|
|
|
- * ★ 不另起一个页面或弹窗:更新腿的字段集就是建号腿那张表减去两个,另起一份必然漂移,
|
|
|
+ * ★ 不另起一份:更新腿的字段集就是建号腿那张表减去两个,另起一份必然漂移,
|
|
|
* 而漂移的症状是「同一份资料在两处能填的格不一样」且不报错。
|
|
|
*/
|
|
|
-const mode = ref<"create" | "update">("create");
|
|
|
-
|
|
|
const MODE_HINT: Record<"create" | "update", string> = {
|
|
|
create:
|
|
|
- "按所选商户类别填写资料,提交后由服务端建号。必填项按类别分档,漏填时服务端会在提交后一次列全(按中文字段名)—— 照那份清单补齐即可。",
|
|
|
+ "按所选商户类别填写资料,提交后由服务端建号。必填项按类别分档,漏填时服务端会在提交后一次列全(按中文字段名)—— 照那份清单补齐即可。建好后**下一步是提交审核**。",
|
|
|
update:
|
|
|
- "更新已有商户的上游资料。★ 上游是增量更新:这里留空 = 不改这一项,不是清空 —— 只想改哪几项就只填哪几项;下拉与图片同理,没动过的不会被提交。",
|
|
|
+ "修改已有商户在渠道那边的资料。★ 渠道是增量更新:这里留空 = 不改这一项,不是清空 —— 只想改哪几项就只填哪几项;下拉与图片同理,没动过的不会被提交。",
|
|
|
};
|
|
|
|
|
|
/** 更新模式下该分区要渲染的字段 —— `SECTIONS` 仍是**唯一**那张表,这里只是投影。 */
|
|
|
@@ -492,7 +922,7 @@ function visibleFields(s: Section): Field[] {
|
|
|
}
|
|
|
|
|
|
function emptyForm(): Record<string, string> {
|
|
|
- // `mch_no` 只有更新模式用得上(建号时它是服务端的**产出**);不填它 ⇒ 建号报文不带这个键
|
|
|
+ // `mch_no` 只有改资料模式用得上(新建时它是服务端的**产出**);不填它 ⇒ 建号报文不带这个键
|
|
|
const f: Record<string, string> = { category: "", mch_no: "" };
|
|
|
for (const key of CREATE_KEYS) {
|
|
|
f[key] = "";
|
|
|
@@ -512,12 +942,14 @@ const submitNoMchNo = ref(false);
|
|
|
*/
|
|
|
const updatedMchNo = ref("");
|
|
|
const cascadeFailed = ref(false);
|
|
|
+/** 级联只在**第一次**打开抽屉时拉;之后复用(下拉选项与表单无关)。 */
|
|
|
+const cascadesLoaded = ref(false);
|
|
|
|
|
|
/** 默认**全部展开**(理由见模板上的注释)。 */
|
|
|
const activeSections = ref<string[]>(SECTIONS.map((s) => s.key));
|
|
|
|
|
|
/**
|
|
|
- * ★ 页面**只**钉 `category` 一条规则,**不做**必填矩阵的副本。
|
|
|
+ * ★ 大表单页面**只**钉 `category` 一条规则,**不做**必填矩阵的副本。
|
|
|
*
|
|
|
* 为什么只钉它:类别为空时服务端报的缺失项是两个**派生**字段(商户类型 / 账户类型),
|
|
|
* 它们 DTO 上没有、页面上也没有输入框 ⇒ 操作者会看到两个自己从未见过的名字。
|
|
|
@@ -571,6 +1003,18 @@ const mcc3Name = computed(
|
|
|
() => mccOptions3.value.find((m) => m.mcc_code === mcc3.value)?.mcc_name || ""
|
|
|
);
|
|
|
|
|
|
+/** 复位两组级联的**选项与已选**(表单复位时必调,理由见 `handleReset`)。 */
|
|
|
+function resetCascadePicks() {
|
|
|
+ cityOptions.value = [];
|
|
|
+ countyOptions.value = [];
|
|
|
+ mcc1.value = "";
|
|
|
+ mcc2.value = "";
|
|
|
+ mcc3.value = "";
|
|
|
+ mccOptions2.value = [];
|
|
|
+ mccOptions3.value = [];
|
|
|
+ clearLeaf();
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 拉一次级联腿。
|
|
|
*
|
|
|
@@ -597,7 +1041,7 @@ async function loadCascade<T>(fetcher: () => Promise<unknown>): Promise<T[] | nu
|
|
|
async function loadProvinces() {
|
|
|
// ★ `level="1"` ⇒ **字面上就不给 `parentCode` 这个键**(不是给空串、也不是给 undefined)。
|
|
|
// ★ 空值兜底(`?? []`)只针对**失败**(`null`);行政区这三级的「空列表」没有「末级」语义,
|
|
|
- // 不像类目那样会被读成结论,所以这里不必再区分。
|
|
|
+ // 不像类目那样会被读成结论,所以这里不必再区分。
|
|
|
provinceOptions.value =
|
|
|
(await loadCascade<OnboardingArea>(() => getOnboardingAreas({ level: "1" }))) ?? [];
|
|
|
}
|
|
|
@@ -768,7 +1212,7 @@ async function uploadOnboardingFile(fd: FormData): Promise<string> {
|
|
|
return url;
|
|
|
}
|
|
|
|
|
|
-// ==================== 提交 ====================
|
|
|
+// ==================== 大表单提交 ====================
|
|
|
|
|
|
/**
|
|
|
* ★ 空白字段**整个省掉**,不送空串:后端只剔 `null`,而**空串会进上游签名原文并照发**
|
|
|
@@ -791,8 +1235,8 @@ function buildCreateBody(): Record<string, string> {
|
|
|
* ★ 与 `buildCreateBody` 共用「空白字段整个省掉」这条判据(理由见上)。**不共用函数**是因为
|
|
|
* 两个出口的键集不同,而且这次的「省掉」还多一层语义:上游是**增量更新**,
|
|
|
* 留空 = 保持原值 ⇒ 省掉正是正确行为,不是「少发了」。
|
|
|
- * ★★ 切模式**不清表单**(见 `switchMode`)⇒ `form` 里此刻可能还留着建号模式打进去的
|
|
|
- * `merchant_name_business` / `lease_contract` 值。`UPDATE_KEYS` 不含它们,**物理上发不出去** ——
|
|
|
+ * ★★ `form` 里此刻可能还留着上一次新建时打进去的 `merchant_name_business` / `lease_contract` 值
|
|
|
+ * (`prepareForm` 确实会清,但**不要依赖它**)。`UPDATE_KEYS` 不含这两个键,**物理上发不出去** ——
|
|
|
* 这就是「白名单」而非 `{ ...form }` 的意义:那道 500 由白名单挡,不靠「用户没填过」这个假设。
|
|
|
*/
|
|
|
function buildUpdateBody(): Record<string, string> {
|
|
|
@@ -820,11 +1264,11 @@ async function handleSubmit() {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 更新腿。与建号腿的三处**有意不同**:
|
|
|
+ * 改资料腿。与新建腿的三处**有意不同**:
|
|
|
* ① 应答是 `Result<Void>` ⇒ **没有**商户号可读,也就没有「没回商户号」那条告警
|
|
|
- * (那条的语义是「本页唯一的产出没了」,在更新腿没有对应物);
|
|
|
+ * (那条的语义是「本页唯一的产出没了」,在这条腿上没有对应物);
|
|
|
* ② 商户号是**输入**(`form.mch_no`)而不是产出,提示里显示的是请求里的那个;
|
|
|
- * ③ 提交完**不清表单**(与建号腿一致)⇒ 一条常驻提示说明「哪几项没被提交」,
|
|
|
+ * ③ 提交完**不清表单**(与新建腿一致)⇒ 一条常驻提示说明「哪几项没被提交」,
|
|
|
* 因为从表单上看不出「留空 = 不改」。
|
|
|
*/
|
|
|
async function submitUpdate() {
|
|
|
@@ -832,9 +1276,11 @@ async function submitUpdate() {
|
|
|
updatedMchNo.value = "";
|
|
|
try {
|
|
|
await updateOnboardingMerchant(buildUpdateBody());
|
|
|
- // ★ 这里**不读** `res.data?.data?.mch_no` —— 更新腿回的是 `Result<Void>`,`data` 是 `null`,
|
|
|
+ // ★ 这里**不读** `res.data?.data?.mch_no` —— 这条腿回的是 `Result<Void>`,`data` 是 `null`,
|
|
|
// 读了只会拿到 `undefined` 并把成功显示成「拿不到商户号」。
|
|
|
updatedMchNo.value = form.mch_no.trim();
|
|
|
+ formVisible.value = false;
|
|
|
+ await loadList();
|
|
|
} catch {
|
|
|
// 失败文案由拦截器统一弹(两道封盖的 400、非本租户的 404、上游失败的 502 都在 `msg` 里)
|
|
|
// ⇒ 页面**不重复弹**,也不把它当成功往下走。这里只是不让 rejection 逃逸成未处理的 Promise 拒绝。
|
|
|
@@ -856,6 +1302,8 @@ async function submitCreate() {
|
|
|
// 但**不**当失败往下走:那会诱导重提,而商户很可能已经建出来了 ⇒ 落成一条常驻提示,
|
|
|
// 让人先去核对。这就是为什么它不走上面那个 `catch`(那条路的语义是「这次没成」)。
|
|
|
submitNoMchNo.value = createdMchNo.value === "";
|
|
|
+ formVisible.value = false;
|
|
|
+ await loadList();
|
|
|
} catch {
|
|
|
// 失败文案由拦截器统一弹(必填缺失清单就在 `msg` 里)⇒ 页面**不重复弹**,
|
|
|
// 也不把它当成功往下走。这里只是不让 rejection 逃逸成未处理的 Promise 拒绝。
|
|
|
@@ -867,56 +1315,36 @@ async function submitCreate() {
|
|
|
function handleReset() {
|
|
|
Object.assign(form, emptyForm());
|
|
|
// 级联的**选项**是独立的 ref、不随表单模型复位,得单独清
|
|
|
- cityOptions.value = [];
|
|
|
- countyOptions.value = [];
|
|
|
- mcc1.value = "";
|
|
|
- mcc2.value = "";
|
|
|
- mcc3.value = "";
|
|
|
- mccOptions2.value = [];
|
|
|
- mccOptions3.value = [];
|
|
|
- clearLeaf();
|
|
|
+ resetCascadePicks();
|
|
|
createdMchNo.value = "";
|
|
|
submitNoMchNo.value = false;
|
|
|
updatedMchNo.value = "";
|
|
|
formRef.value?.clearValidate();
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 切模式:**不动**表单里已填的值 —— 41 个框,误点一下清空是不可逆的。
|
|
|
- * 代价是建号模式填的 `merchant_name_business` / `lease_contract` 会留在模型里,
|
|
|
- * 但那条路径由 `buildUpdateBody` 的**键白名单**堵死(不靠「用户没填过」这个假设)。
|
|
|
- *
|
|
|
- * ★ 只清**上一次提交的结果提示**:那是「上一次那个动作」的结论,
|
|
|
- * 留着会让人以为刚切换到的这个模式已经提交过了。
|
|
|
- */
|
|
|
-function switchMode() {
|
|
|
- createdMchNo.value = "";
|
|
|
- submitNoMchNo.value = false;
|
|
|
- updatedMchNo.value = "";
|
|
|
- formRef.value?.clearValidate();
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 从「刚建完号」那条常驻提示跳进更新模式,并带上商户号 —— 建号后当场发现填错,
|
|
|
- * 是更新腿最常见的入口,这里省掉一次「回头把商户号抄过来」。
|
|
|
- *
|
|
|
- * ★ 只预填 `mch_no`,**不**把建号表单里那一堆值带过去:更新是增量的,
|
|
|
- * 带过去等于「按建号时填的再覆盖一遍」,而不是「只改填错的那一项」。
|
|
|
- */
|
|
|
-function startUpdateFor(mchNo: string) {
|
|
|
- mode.value = "update";
|
|
|
- form.mch_no = mchNo;
|
|
|
- switchMode();
|
|
|
-}
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- // 两组级联与表单无关,进页面就拉(失败由 `loadCascade` 落成页面上那条常驻提示)
|
|
|
- loadProvinces();
|
|
|
- loadMccLevel1();
|
|
|
-});
|
|
|
+onMounted(loadList);
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
+.list-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ &__title {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__sub {
|
|
|
+ margin: 4px 0 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
.section-title {
|
|
|
font-weight: 600;
|
|
|
}
|