|
|
@@ -96,6 +96,13 @@
|
|
|
<el-table-column prop="shop_name" label="门店名称" min-width="150">
|
|
|
<template #default="{ row }">{{ row.shop_name || "-" }}</template>
|
|
|
</el-table-column>
|
|
|
+ <el-table-column label="地区" min-width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="{ 'region-missing': !row.province_code && !row.city_code }">
|
|
|
+ {{ regionLabel(row) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
<el-table-column prop="shop_id" label="平台门店编号" min-width="140">
|
|
|
<template #default="{ row }">{{ row.shop_id || "-" }}</template>
|
|
|
</el-table-column>
|
|
|
@@ -200,6 +207,36 @@
|
|
|
<el-form-item label="门店名称" prop="shop_name">
|
|
|
<el-input v-model="form.shop_name" maxlength="128" show-word-limit />
|
|
|
</el-form-item>
|
|
|
+ <el-form-item label="省份" prop="province_code">
|
|
|
+ <!-- 两级都是必填(规范 261008 5.5)。取值只能来自这里给的下拉:
|
|
|
+ 编码要进上游报文,也是收款时按顾客 IP 挑店的匹配键,手敲一个错码
|
|
|
+ 就是一家永远挑不中的门店(上游没有更新接口,事后补不上) -->
|
|
|
+ <el-select
|
|
|
+ v-model="form.province_code"
|
|
|
+ placeholder="选择省份"
|
|
|
+ filterable
|
|
|
+ style="width: 100%"
|
|
|
+ @change="onProvinceChange"
|
|
|
+ >
|
|
|
+ <el-option v-for="p in provinceOptions" :key="p.code" :label="p.name" :value="p.code" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="城市" prop="city_code">
|
|
|
+ <el-select
|
|
|
+ v-model="form.city_code"
|
|
|
+ :disabled="!form.province_code"
|
|
|
+ :loading="cityLoading"
|
|
|
+ :placeholder="form.province_code ? '选择城市' : '请先选择省份'"
|
|
|
+ filterable
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option v-for="c in currentCities" :key="c.code" :label="c.name" :value="c.code" />
|
|
|
+ </el-select>
|
|
|
+ <div class="form-tip">
|
|
|
+ 支付宝对异地收款有风控:顾客付款时会自动用顾客所在地区的门店发起收款,
|
|
|
+ 所以每家门店都要有准确的地区。
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="createVisible = false">取消</el-button>
|
|
|
@@ -218,6 +255,7 @@
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="门店编码">{{ detail.third_shop_id }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="门店名称">{{ detail.shop_name || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="地区">{{ regionLabel(detail) }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="平台门店编号">
|
|
|
{{ detail.shop_id || "未返回(创建未成功)" }}
|
|
|
</el-descriptions-item>
|
|
|
@@ -247,13 +285,14 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { onMounted, reactive, ref } from "vue";
|
|
|
+import { computed, onMounted, reactive, ref } from "vue";
|
|
|
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
|
|
import dayjs from "dayjs";
|
|
|
import HarAPI, {
|
|
|
HAR_SHOP_STATUS_TAG,
|
|
|
HAR_SHOP_STATUS_TEXT,
|
|
|
type HarMerchantVO,
|
|
|
+ type HarRegionVO,
|
|
|
type HarShopCreateParams,
|
|
|
type HarShopVO,
|
|
|
} from "@/api/module_payment/har";
|
|
|
@@ -264,6 +303,75 @@ const enterpriseStore = useEnterpriseStore();
|
|
|
/** 本租户可见的商户配置 —— 建店时用哪一条由这里选 */
|
|
|
const merchantOptions = ref<HarMerchantVO[]>([]);
|
|
|
|
|
|
+/**
|
|
|
+ * 省市目录 —— 建店表单的两级选择器,同时用来把列表里的**码**显示成**名**。
|
|
|
+ *
|
|
|
+ * ★ 目录来自后端(`/payment/har/region/*`),**页面不存第二份省市表**:编码是按顾客 IP
|
|
|
+ * 挑店时比对的键,两份表一旦分叉,运营选出来的门店就会「永远挑不中」。
|
|
|
+ * ★ 市按省缓存:列表每页可能有多个省的门店,缓存住免得每行各打一次。
|
|
|
+ * 失败的省**不入缓存**(留着下次再试),成功的即使是空数组也入缓存(那是上游的答案)。
|
|
|
+ */
|
|
|
+const provinceOptions = ref<HarRegionVO[]>([]);
|
|
|
+const cityCache = reactive<Record<string, HarRegionVO[]>>({});
|
|
|
+const cityLoading = ref(false);
|
|
|
+/** 正在拉的城市所属省码 —— 拉 A 省时切到 B 省,A 的响应回来不能当成 B 的 */
|
|
|
+let loadingCityOf = "";
|
|
|
+
|
|
|
+async function ensureProvinces() {
|
|
|
+ if (provinceOptions.value.length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await HarAPI.regionProvinces();
|
|
|
+ provinceOptions.value = res.data.data || [];
|
|
|
+ } catch {
|
|
|
+ // 拉不到就保持空:选择器空着,用户看得见「没得选」,比给一份猜的表强
|
|
|
+ provinceOptions.value = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function ensureCities(code?: string | null) {
|
|
|
+ if (!code || cityCache[code]) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ loadingCityOf = code;
|
|
|
+ cityLoading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await HarAPI.regionCities(code);
|
|
|
+ cityCache[code] = res.data.data || [];
|
|
|
+ } catch {
|
|
|
+ // 不入缓存 ⇒ 重新选一次省可以再试(省码不认识时后端是 400,重试也还是 400,
|
|
|
+ // 但那是给人看的错,不该被这里吞成一个空下拉)
|
|
|
+ } finally {
|
|
|
+ if (loadingCityOf === code) {
|
|
|
+ cityLoading.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onProvinceChange(code: string) {
|
|
|
+ // 换省必须把市清掉:留着一个属于上一省的市码,提交出去就是「省 32 + 市 1101」,
|
|
|
+ // 后端按省查表会拒(正确),但用户看到的是自己明明选过城市
|
|
|
+ form.city_code = "";
|
|
|
+ ensureCities(code);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 行的地区显示名。码 → 名的对照取自目录;目录里没有就**如实回退到码本身**
|
|
|
+ * (不猜名字),两格都空则明说「未填」—— 2026-10-08 前建的店就是这一种,
|
|
|
+ * 它们在挑店时必然落选,页面若显示成有地区,运营会以为这家店还能收款。
|
|
|
+ */
|
|
|
+function regionLabel(row: HarShopVO) {
|
|
|
+ if (!row.province_code && !row.city_code) {
|
|
|
+ return "未填(不参与按地区收款)";
|
|
|
+ }
|
|
|
+ const province = provinceOptions.value.find((p) => p.code === row.province_code);
|
|
|
+ const city = (cityCache[row.province_code || ""] || []).find((c) => c.code === row.city_code);
|
|
|
+ return [province?.name || row.province_code, city?.name || row.city_code]
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(" / ");
|
|
|
+}
|
|
|
+
|
|
|
const list = ref<HarShopVO[]>([]);
|
|
|
const total = ref(0);
|
|
|
const pageNo = ref(1);
|
|
|
@@ -291,16 +399,27 @@ const defaultForm = (): HarShopCreateParams => ({
|
|
|
enterprise_id: "",
|
|
|
third_shop_id: "",
|
|
|
shop_name: "",
|
|
|
+ province_code: "",
|
|
|
+ city_code: "",
|
|
|
});
|
|
|
|
|
|
const form = reactive<HarShopCreateParams>(defaultForm());
|
|
|
|
|
|
+/** 表单里当前可选的城市(市级下拉) */
|
|
|
+const currentCities = computed(() =>
|
|
|
+ form.province_code ? cityCache[form.province_code] || [] : []
|
|
|
+);
|
|
|
+
|
|
|
const rules: FormRules = {
|
|
|
// 企业**非必填**:留空即按租户建店(enterprise_id 落 NULL 的租户级映射),
|
|
|
// 与 HarShopCreateDTO 上「选填」的注释、后端 HarShopService#create 的分流一致。
|
|
|
// 商户同理非必填:本租户只有一条配置时留空即为那条;有多条时后端会 400 要求指定。
|
|
|
third_shop_id: [{ required: true, message: "请填写门店编码", trigger: "blur" }],
|
|
|
shop_name: [{ required: true, message: "请填写门店名称", trigger: "blur" }],
|
|
|
+ // 地区两个都必填(规范 261008 5.5):这既是发上游的必填项,也是按顾客 IP 挑店的键。
|
|
|
+ // 门店建完在上游**没有更新接口** ⇒ 这里漏填/填错就是一家永远收不到款的门店。
|
|
|
+ province_code: [{ required: true, message: "请选择省份", trigger: "change" }],
|
|
|
+ city_code: [{ required: true, message: "请选择城市", trigger: "change" }],
|
|
|
};
|
|
|
|
|
|
const merchantLabel = (m: HarMerchantVO) =>
|
|
|
@@ -335,6 +454,11 @@ async function load() {
|
|
|
});
|
|
|
list.value = res.data.data?.items || res.data.data?.list || [];
|
|
|
total.value = res.data.data?.total || 0;
|
|
|
+ // 本页各行涉及的省各拉一次市(有缓存就不重复拉)——列表里要把码显示成名,
|
|
|
+ // 缺这一句时每行只能显示编码,运营得自己背 3201 是哪个市
|
|
|
+ ensureProvinces();
|
|
|
+ new Set(list.value.map((row) => row.province_code).filter((code): code is string => !!code))
|
|
|
+ .forEach((code) => ensureCities(code));
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
@@ -356,6 +480,8 @@ function handleReset() {
|
|
|
function openCreate() {
|
|
|
Object.assign(form, defaultForm());
|
|
|
createVisible.value = true;
|
|
|
+ // 目录是异步来的(也只在真正建店时用得上),别放进 onMounted 让列表页白等
|
|
|
+ ensureProvinces();
|
|
|
}
|
|
|
|
|
|
function handleCreateClosed() {
|
|
|
@@ -389,6 +515,9 @@ async function openDetail(row: HarShopVO) {
|
|
|
const res = await HarAPI.shopDetail(row.id as string);
|
|
|
detail.value = res.data.data;
|
|
|
detailVisible.value = true;
|
|
|
+ // 详情里也要把地区显示成名字(这家店可能不在本页列表里,缓存里未必有)
|
|
|
+ ensureProvinces();
|
|
|
+ ensureCities(detail.value?.province_code);
|
|
|
}
|
|
|
|
|
|
onMounted(async () => {
|
|
|
@@ -414,4 +543,10 @@ onMounted(async () => {
|
|
|
line-height: 1.6;
|
|
|
color: var(--el-text-color-secondary);
|
|
|
}
|
|
|
+
|
|
|
+/* 缺地区的门店在收款链接里必然落选(2026-10-08 前建的店补不上)——
|
|
|
+ 用警示色,别让它与「有地区」的普通文本长得一样 */
|
|
|
+.region-missing {
|
|
|
+ color: var(--el-color-danger);
|
|
|
+}
|
|
|
</style>
|