| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114 |
- <template>
- <div class="manage-info-page">
- <h1 class="page-title">经营信息</h1>
- <el-form
- ref="formRef"
- class="manage-form"
- :model="form"
- :rules="rules"
- label-width="200px"
- require-asterisk-position="right"
- label-position="right"
- >
- <!-- 基础经营信息 -->
- <section class="form-block">
- <h2 class="section-head">
- <span class="section-bar" aria-hidden="true" />
- 经营信息
- </h2>
- <el-form-item prop="merchantShortName">
- <template #label>
- <span class="label-with-icon">
- 商户简称
- <el-tooltip placement="top" content="简称将展示给消费者,需与营业执照或品牌一致,勿使用无关或违规词汇。">
- <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
- </el-tooltip>
- </span>
- </template>
- <div class="field-stack">
- <el-input
- v-model="form.merchantShortName"
- placeholder="请输入商户简称"
- clearable
- maxlength="64"
- show-word-limit
- style="max-width: 480px"
- />
- <p class="field-tip">
- 1. 在支付完成页向买家展示,需与微信经营类目相关。<br />
- 2.简称要求:<br />
- 不支持单纯以人名来命名,若为个体户经营,可用“个体户+经营者名称”或“经营者名称+业务”命名,如个体户“张三”或“张三餐饮店”;<br />
- 不支持无实际意义的文案,如“XX”特约商户,“800”,“XX客服电话XXX”;
- </p>
- </div>
- </el-form-item>
- <el-form-item prop="servicePhone">
- <template #label>
- <span class="label-with-icon">
- 客服电话
- <el-tooltip placement="top" content="用于用户咨询与平台联系,请填写真实可接通号码。">
- <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
- </el-tooltip>
- </span>
- </template>
- <div class="field-stack">
- <el-input
- v-model="form.servicePhone"
- placeholder="请输入客服电话"
- clearable
- maxlength="20"
- style="max-width: 480px"
- />
- <p class="field-tip">
- 1.请填写真实、可接通的客服电话,以便用户咨询。<br />
- 2.请确保电话畅通,以便入驻平台回拨确认。<br />
- </p>
- </div>
- </el-form-item>
- <el-form-item label="经营场景" prop="scenarioBits" required>
- <div class="field-stack">
- <div class="scenario-checks">
- <el-checkbox v-model="form.scenarios.offline"> 线下场景 </el-checkbox>
- <el-checkbox v-model="form.scenarios.miniProgram"> 小程序 </el-checkbox>
- <el-checkbox v-model="form.scenarios.app"> APP </el-checkbox>
- <el-checkbox v-model="form.scenarios.wechatAccount"> 服务号或订阅号 </el-checkbox>
- <el-checkbox v-model="form.scenarios.website"> 互联网网站 </el-checkbox>
- <el-checkbox v-model="form.scenarios.wecom"> 企业微信 </el-checkbox>
- </div>
- <p class="field-tip">
- 请勾选实际售卖商品/提供服务场景(至少一项),以便为你开通需要的支付权限<br />
- 建议只勾选目前必须的尝尽,以便尽快通过入驻审核,其他支付权限你可在入驻后再根据实际需要发起申请
- </p>
- </div>
- </el-form-item>
- </section>
- <!-- 线下场景 -->
- <section v-if="form.scenarios.offline" class="form-block sub-section">
- <h2 class="section-head">
- <span class="section-bar" aria-hidden="true" />
- 线下场所
- <span class="section-intro"
- >你选择了“线下场所”场景,入驻成功后,服务商可帮商户发起付款码支付,JSAPI支付,同时系统将会对地址等信息进行核实</span
- >
- </h2>
- <el-form-item label="线下场所名称" prop="offlineVenueName">
- <el-input v-model="form.offlineVenueName" placeholder="请输入线下场所名称" clearable style="max-width: 480px" />
- </el-form-item>
- <el-form-item label="线下场所省市" prop="offlineProvinceCity">
- <el-select v-model="form.offlineProvince" placeholder="请选择省" filterable style="width: 200px">
- <el-option v-for="p in provinceOptions" :key="p.value" :label="p.label" :value="p.value" />
- </el-select>
- <el-select
- v-model="form.offlineCity"
- placeholder="请选择市"
- filterable
- style="width: 200px; margin-left: 12px"
- :disabled="!form.offlineProvince"
- >
- <el-option v-for="c in cityOptions" :key="c.value" :label="c.label" :value="c.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="线下场所地址" prop="offlineAddress">
- <div class="field-stack">
- <el-input
- v-model="form.offlineAddress"
- type="textarea"
- :rows="2"
- placeholder="请输入详细地址"
- maxlength="200"
- show-word-limit
- style="max-width: 480px"
- />
- <p class="field-tip">如有多个经营场所,请填写主要营业地址。</p>
- </div>
- </el-form-item>
- <el-form-item prop="offlineStorefrontUrls">
- <template #label>
- <span class="label-with-icon">
- 线下场所门头照片
- <el-tooltip placement="top" content="需清晰展示门头招牌与店名。">
- <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
- </el-tooltip>
- </span>
- </template>
- <div class="field-stack">
- <el-upload
- v-model:file-list="form.offlineStorefrontFileList"
- list-type="picture-card"
- :limit="5"
- accept=".jpg,.jpeg,.png,.bmp,image/jpeg,image/png,image/bmp"
- :before-upload="beforeImageUpload"
- :http-request="opt => handleMultiUpload(opt, 'storefront')"
- :on-remove="(f, fl) => onMultiRemove(f, fl, 'storefront')"
- >
- <el-icon><Plus /></el-icon>
- </el-upload>
- <p class="field-tip">
- 1.场景图片正面拍摄且清晰,完整,图片不得有遮挡;<br />
- 2.门店招牌清晰,招牌名称,文字可辨识,门框完整,且店面显示在营,若为停车场等无固定门头照片的经营场所,可上传岗亭/出入闸口。
- </p>
- </div>
- </el-form-item>
- <el-form-item prop="offlineInteriorUrls">
- <template #label>
- <span class="label-with-icon">
- 线下场所内部照片
- <el-tooltip placement="top" content="展示店内经营环境。">
- <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
- </el-tooltip>
- </span>
- </template>
- <div class="field-stack">
- <el-upload
- v-model:file-list="form.offlineInteriorFileList"
- list-type="picture-card"
- :limit="5"
- accept=".jpg,.jpeg,.png,.bmp,image/jpeg,image/png,image/bmp"
- :before-upload="beforeImageUpload"
- :http-request="opt => handleMultiUpload(opt, 'interior')"
- :on-remove="(f, fl) => onMultiRemove(f, fl, 'interior')"
- >
- <el-icon><Plus /></el-icon>
- </el-upload>
- <p class="field-tip">
- 1.场景图片正面拍摄且清晰,完整,图片不得有遮挡;<br />
- 2.门店招牌清晰,招牌名称,文字可辨识,门框完整,且店面显示在营,若为停车场等无固定门头照片的经营场所,可上传岗亭/出入闸口。
- </p>
- </div>
- </el-form-item>
- <el-form-item label="线下场所对应的服务号或公众号 APPID">
- <div class="field-stack">
- <el-input v-model="form.offlineMpAppId" placeholder="选填" clearable style="max-width: 480px" />
- <p class="field-tip">
- 1.可填写已认领的服务号或公众号(需时已认证的服务号,政府或媒体类型的公众号),小程序,应用的APPID.<br />
- 2.完成进件后,系统发起特约商户号与该APPID的绑定(即配置为sub_appid,可在发起支付时传入)<br />
- (1)若APPID主体与商家主体一致,则直接完成绑定;<br />
- (2)若APPID主体与商家主体不一致,则需商户提供主体变更证明材料,审核通过后完成绑定。
- </p>
- </div>
- </el-form-item>
- </section>
- <!-- 小程序 -->
- <section v-if="form.scenarios.miniProgram" class="form-block sub-section">
- <h2 class="section-head">
- <span class="section-bar" aria-hidden="true" />
- 小程序<span class="section-intro">你选择了“小程序”场景,入驻成功后,服务商可帮商户发起JSAPI支付</span>
- </h2>
- <el-form-item label="小程序 APPID" prop="miniProgramAppId">
- <div class="field-stack">
- <el-input v-model="form.miniProgramAppId" placeholder="请输入小程序 APPID" clearable style="max-width: 480px" />
- <p class="field-tip">
- 1.请填写已认证小程序的 APPID。<br />
- 2.完成进件后,系统发起特约商户号与该APPID的绑定(即配置为sub_appid,可在发起支付时传入)<br />
- (1)若APPID主体与商家主体一致,则直接完成绑定;<br />
- (2)若APPID主体与商家主体不一致,则需商户提供主体变更证明材料,审核通过后完成绑定。
- </p>
- </div>
- </el-form-item>
- <el-form-item label="小程序截图">
- <div class="field-stack">
- <el-upload
- v-model:file-list="form.miniProgramShotFileList"
- list-type="picture-card"
- :limit="5"
- accept=".jpg,.jpeg,.png,.bmp,image/jpeg,image/png,image/bmp"
- :before-upload="beforeImageUpload"
- :http-request="opt => handleMultiUpload(opt, 'miniShot')"
- :on-remove="(f, fl) => onMultiRemove(f, fl, 'miniShot')"
- >
- <el-icon><Plus /></el-icon>
- </el-upload>
- <p class="field-tip">请提供展示商品/服务的页面截图/设计稿(最多5张),若小程序胃建设完善或未上线请务必提供。</p>
- </div>
- </el-form-item>
- </section>
- <!-- APP -->
- <section v-if="form.scenarios.app" class="form-block sub-section">
- <h2 class="section-head">
- <span class="section-bar" aria-hidden="true" />
- APP<span class="section-intro">你选择了“APP”场景,入驻成功后,服务商可帮商户发起APP支付</span>
- </h2>
- <el-form-item label="AppID 来源" prop="appIdSource" required>
- <el-radio-group v-model="form.appIdSource">
- <el-radio value="provider"> 服务商应用 AppID </el-radio>
- <el-radio value="merchant"> 商家应用 AppID </el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item v-if="form.appIdSource === 'provider'" label="服务商应用 AppID" prop="providerAppId">
- <div class="field-stack">
- <el-select v-model="form.providerAppId" placeholder="请选择" filterable style="max-width: 480px">
- <el-option v-for="o in providerAppOptions" :key="o.value" :label="o.label" :value="o.value" />
- </el-select>
- </div>
- </el-form-item>
- <el-form-item prop="appShotUrls">
- <template #label>
- <span class="label-with-icon">
- APP 截图
- <el-tooltip placement="top" content="首页、商品列表、详情、支付等相关页面。">
- <el-icon class="info-icon" :size="16"><InfoFilled /></el-icon>
- </el-tooltip>
- </span>
- </template>
- <div class="field-stack">
- <el-upload
- v-model:file-list="form.appShotFileList"
- list-type="picture-card"
- :limit="5"
- accept=".jpg,.jpeg,.png,.bmp,image/jpeg,image/png,image/bmp"
- :before-upload="beforeImageUpload"
- :http-request="opt => handleMultiUpload(opt, 'appShot')"
- :on-remove="(f, fl) => onMultiRemove(f, fl, 'appShot')"
- >
- <el-icon><Plus /></el-icon>
- </el-upload>
- <p class="field-tip">请提供APP首页截图,尾页截图,应用内截图,支付页截图各1张</p>
- </div>
- </el-form-item>
- </section>
- </el-form>
- <div class="footer-actions">
- <el-button @click="onBack"> 返回 </el-button>
- <el-button type="primary" class="btn-next" :loading="submitLoading" @click="onSubmit"> 确定 </el-button>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import { ElMessage } from "element-plus";
- import type { FormInstance, FormRules } from "element-plus";
- import type { UploadRequestOptions, UploadUserFile } from "element-plus";
- import { Plus, InfoFilled } from "@element-plus/icons-vue";
- import { getUpload } from "@/api/modules/businessInfo";
- import { localGet, localSet } from "@/utils/index";
- import cityJson from "@/assets/json/city.json";
- const BUSINESS_DATA_CACHE_KEY = "businessData";
- const formRef = ref<FormInstance>();
- const submitLoading = ref(false);
- const route = useRoute();
- const router = useRouter();
- type UploadKind = "storefront" | "interior" | "miniShot" | "appShot";
- const form = reactive({
- merchantShortName: "",
- servicePhone: "",
- scenarios: {
- offline: true,
- miniProgram: true,
- app: true,
- wechatAccount: false,
- website: false,
- wecom: false
- },
- offlineVenueName: "",
- offlineProvince: "",
- offlineCity: "",
- offlineAddress: "",
- offlineStorefrontFileList: [] as UploadUserFile[],
- offlineStorefrontUrls: [] as string[],
- offlineInteriorFileList: [] as UploadUserFile[],
- offlineInteriorUrls: [] as string[],
- offlineMpAppId: "",
- miniProgramAppId: "",
- miniProgramShotFileList: [] as UploadUserFile[],
- miniProgramShotUrls: [] as string[],
- appIdSource: "provider" as "provider" | "merchant",
- providerAppId: "",
- merchantAppId: "",
- appShotFileList: [] as UploadUserFile[],
- appShotUrls: [] as string[],
- /** 用于校验至少选一种经营场景 */
- scenarioBits: "1"
- });
- /** 与进件文档 business_info.sales_info.sales_scenes_type 枚举对应 */
- function buildSalesScenesType(): string[] {
- const s = form.scenarios;
- const types: string[] = [];
- if (s.offline) types.push("SALES_SCENES_STORE");
- if (s.miniProgram) types.push("SALES_SCENES_MINI_PROGRAM");
- if (s.app) types.push("SALES_SCENES_APP");
- if (s.wechatAccount) types.push("SALES_SCENES_MP");
- if (s.website) types.push("SALES_SCENES_WEB");
- if (s.wecom) types.push("SALES_SCENES_WEWORK");
- return types;
- }
- /** 上传组件更新 file-list 时可能丢 response,用 uid 备份 media_id */
- const wechatMediaIdByFileUid = new Map<number, string>();
- function getMediaIdFromUploadFile(f: UploadUserFile): string {
- const tagged = String((f as UploadUserFile & { __wxMediaId?: string }).__wxMediaId ?? "").trim();
- if (tagged) return tagged;
- const uid = (f as UploadUserFile & { uid?: number }).uid;
- if (uid != null && wechatMediaIdByFileUid.has(uid)) {
- return wechatMediaIdByFileUid.get(uid) ?? "";
- }
- const r = (f as UploadUserFile & { response?: unknown }).response;
- if (r && typeof r === "object" && !Array.isArray(r)) {
- const o = r as Record<string, unknown>;
- const direct = String(o.media_id ?? o.mediaId ?? "").trim();
- if (direct) return direct;
- const nested = extractMediaUploadMeta(r).mediaId;
- if (nested) return nested;
- }
- return "";
- }
- /** 写入进件缓存的四处图片字段均为微信 media/upload 返回的 media_id */
- function collectMediaIdsForMerge(list: UploadUserFile[]): string[] {
- return list
- .filter(f => f.status === "success")
- .map(f => getMediaIdFromUploadFile(f))
- .filter(Boolean);
- }
- function mergeManageInfoIntoBusinessDataCache() {
- const prev = localGet(BUSINESS_DATA_CACHE_KEY);
- const base = prev && typeof prev === "object" && !Array.isArray(prev) ? { ...(prev as Record<string, unknown>) } : {};
- const prevBi =
- base.business_info && typeof base.business_info === "object" && !Array.isArray(base.business_info)
- ? { ...(base.business_info as Record<string, unknown>) }
- : {};
- const prevSales =
- prevBi.sales_info && typeof prevBi.sales_info === "object" && !Array.isArray(prevBi.sales_info)
- ? { ...(prevBi.sales_info as Record<string, unknown>) }
- : {};
- const sales_info: Record<string, unknown> = {
- ...prevSales,
- sales_scenes_type: buildSalesScenesType()
- };
- if (form.scenarios.offline) {
- sales_info.biz_store_info = {
- biz_store_name: String(form.offlineVenueName || "").trim(),
- biz_address_code: String(form.offlineCity || "").trim(),
- biz_store_address: String(form.offlineAddress || "").trim(),
- store_entrance_pic: collectMediaIdsForMerge(form.offlineStorefrontFileList),
- indoor_pic: collectMediaIdsForMerge(form.offlineInteriorFileList),
- biz_sub_appid: String(form.offlineMpAppId || "").trim()
- };
- } else {
- delete sales_info.biz_store_info;
- }
- if (form.scenarios.miniProgram) {
- sales_info.mini_program_info = {
- mini_program_sub_appid: String(form.miniProgramAppId || "").trim(),
- mini_program_pics: collectMediaIdsForMerge(form.miniProgramShotFileList)
- };
- } else {
- delete sales_info.mini_program_info;
- }
- if (form.scenarios.app) {
- sales_info.app_info = {
- app_appid: String(form.providerAppId || "").trim(),
- app_sub_appid: String(form.merchantAppId || "").trim(),
- app_pics: collectMediaIdsForMerge(form.appShotFileList)
- };
- } else {
- delete sales_info.app_info;
- }
- base.business_info = {
- ...prevBi,
- merchant_shortname: String(form.merchantShortName || "").trim(),
- service_phone: String(form.servicePhone || "").trim(),
- sales_info
- };
- localSet(BUSINESS_DATA_CACHE_KEY, base);
- }
- type CityJsonEntry = { name: string; adCode: string; cityCode: string; type?: string };
- type CityJsonRoot = { cityList: { letter: string; list: CityJsonEntry[] }[] };
- /** GB 行政区划代码前两位 → 省级名称(与 city.json 中 adCode 对应) */
- const PROVINCE_ROWS: [string, string][] = [
- ["11", "北京市"],
- ["12", "天津市"],
- ["13", "河北省"],
- ["14", "山西省"],
- ["15", "内蒙古自治区"],
- ["21", "辽宁省"],
- ["22", "吉林省"],
- ["23", "黑龙江省"],
- ["31", "上海市"],
- ["32", "江苏省"],
- ["33", "浙江省"],
- ["34", "安徽省"],
- ["35", "福建省"],
- ["36", "江西省"],
- ["37", "山东省"],
- ["41", "河南省"],
- ["42", "湖北省"],
- ["43", "湖南省"],
- ["44", "广东省"],
- ["45", "广西壮族自治区"],
- ["46", "海南省"],
- ["50", "重庆市"],
- ["51", "四川省"],
- ["52", "贵州省"],
- ["53", "云南省"],
- ["54", "西藏自治区"],
- ["61", "陕西省"],
- ["62", "甘肃省"],
- ["63", "青海省"],
- ["64", "宁夏回族自治区"],
- ["65", "新疆维吾尔自治区"],
- ["71", "台湾省"],
- ["81", "香港特别行政区"],
- ["82", "澳门特别行政区"]
- ];
- const PROVINCE_BY_PREFIX = Object.fromEntries(PROVINCE_ROWS) as Record<string, string>;
- const PROVINCE_PREFIX_ORDER = PROVINCE_ROWS.map(([k]) => k);
- function buildProvinceCityOptions(): {
- label: string;
- value: string;
- cities: { label: string; value: string }[];
- }[] {
- const root = cityJson as CityJsonRoot;
- const flat = root.cityList.flatMap(g => g.list);
- const bucket = new Map<string, Map<string, CityJsonEntry>>();
- for (const c of flat) {
- const digits = String(c.adCode ?? "").replace(/\D/g, "");
- const code = digits.length >= 6 ? digits.slice(-6) : digits.padStart(6, "0");
- if (code.length !== 6) continue;
- const prefix = code.slice(0, 2);
- if (!PROVINCE_BY_PREFIX[prefix]) continue;
- if (!bucket.has(prefix)) bucket.set(prefix, new Map());
- bucket.get(prefix)!.set(c.adCode, c);
- }
- const out = [...bucket.entries()].map(([prefix, cityMap]) => ({
- value: prefix,
- label: PROVINCE_BY_PREFIX[prefix],
- cities: [...cityMap.values()]
- .sort((a, b) => a.name.localeCompare(b.name, "zh-CN"))
- .map(ci => ({ label: ci.name, value: ci.adCode }))
- }));
- out.sort((a, b) => {
- const ia = PROVINCE_PREFIX_ORDER.indexOf(a.value);
- const ib = PROVINCE_PREFIX_ORDER.indexOf(b.value);
- return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib);
- });
- return out;
- }
- const provinceOptions = buildProvinceCityOptions();
- const cityOptions = ref<{ label: string; value: string }[]>([]);
- watch(
- () => form.offlineProvince,
- code => {
- form.offlineCity = "";
- const p = provinceOptions.find(x => x.value === code);
- cityOptions.value = p?.cities ?? [];
- }
- );
- const providerAppOptions = [
- { label: "请选择", value: "" },
- { label: "wxf5f1efe3a9f5012e", value: "wxf5f1efe3a9f5012e" }
- ];
- const scenarioChecked = computed(() => {
- const s = form.scenarios;
- return s.offline || s.miniProgram || s.app || s.wechatAccount || s.website || s.wecom;
- });
- watch(
- () => form.scenarios,
- () => {
- form.scenarioBits = scenarioChecked.value ? "1" : "";
- },
- { deep: true }
- );
- function asStringArray(v: unknown): string[] {
- if (!Array.isArray(v)) return [];
- return v.map(x => String(x ?? "").trim()).filter(Boolean);
- }
- /** 从 localStorage businessData.business_info 回显经营信息表单 */
- function hydrateManageFormFromBusinessDataCache() {
- const raw = localGet(BUSINESS_DATA_CACHE_KEY) as Record<string, unknown> | null | undefined;
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
- const bi = raw.business_info as Record<string, unknown> | undefined;
- if (!bi || typeof bi !== "object" || Array.isArray(bi)) return;
- form.merchantShortName = String(bi.merchant_shortname ?? "").trim();
- form.servicePhone = String(bi.service_phone ?? "").trim();
- let pendingOfflineCity = "";
- const si = bi.sales_info as Record<string, unknown> | undefined;
- if (si && typeof si === "object" && !Array.isArray(si)) {
- const sceneTypes = si.sales_scenes_type;
- if (Array.isArray(sceneTypes)) {
- const set = new Set(sceneTypes.map(t => String(t)));
- form.scenarios.offline = set.has("SALES_SCENES_STORE");
- form.scenarios.miniProgram = set.has("SALES_SCENES_MINI_PROGRAM");
- form.scenarios.app = set.has("SALES_SCENES_APP");
- form.scenarios.wechatAccount = set.has("SALES_SCENES_MP");
- form.scenarios.website = set.has("SALES_SCENES_WEB");
- form.scenarios.wecom = set.has("SALES_SCENES_WEWORK");
- form.scenarioBits = scenarioChecked.value ? "1" : "";
- }
- const bzs = si.biz_store_info as Record<string, unknown> | undefined;
- if (bzs && typeof bzs === "object" && !Array.isArray(bzs)) {
- form.offlineVenueName = String(bzs.biz_store_name ?? "").trim();
- const digits = String(bzs.biz_address_code ?? "").replace(/\D/g, "");
- const code6 = digits.length >= 6 ? digits.slice(-6) : digits;
- if (code6.length >= 2) {
- const prefix = code6.slice(0, 2);
- form.offlineProvince = prefix;
- const p = provinceOptions.find(x => x.value === prefix);
- cityOptions.value = p?.cities ?? [];
- if (code6.length >= 6) pendingOfflineCity = code6;
- }
- form.offlineAddress = String(bzs.biz_store_address ?? "").trim();
- const se = asStringArray(bzs.store_entrance_pic);
- const indoor = asStringArray(bzs.indoor_pic);
- form.offlineStorefrontUrls = [...se];
- form.offlineInteriorUrls = [...indoor];
- form.offlineStorefrontFileList = se.map(
- (url, i) =>
- ({
- name: `storefront-${i + 1}.jpg`,
- url,
- status: "success",
- response: { url, media_id: url, mediaId: url },
- __wxMediaId: url
- }) as UploadUserFile
- );
- form.offlineInteriorFileList = indoor.map(
- (url, i) =>
- ({
- name: `interior-${i + 1}.jpg`,
- url,
- status: "success",
- response: { url, media_id: url, mediaId: url },
- __wxMediaId: url
- }) as UploadUserFile
- );
- form.offlineMpAppId = String(bzs.biz_sub_appid ?? "").trim();
- }
- const mpi = si.mini_program_info as Record<string, unknown> | undefined;
- if (mpi && typeof mpi === "object" && !Array.isArray(mpi)) {
- form.miniProgramAppId = String(mpi.mini_program_sub_appid ?? "").trim();
- const pics = asStringArray(mpi.mini_program_pics);
- form.miniProgramShotUrls = [...pics];
- form.miniProgramShotFileList = pics.map(
- (url, i) =>
- ({
- name: `mini-${i + 1}.jpg`,
- url,
- status: "success",
- response: { url, media_id: url, mediaId: url },
- __wxMediaId: url
- }) as UploadUserFile
- );
- }
- const appInf = si.app_info as Record<string, unknown> | undefined;
- if (appInf && typeof appInf === "object" && !Array.isArray(appInf)) {
- form.providerAppId = String(appInf.app_appid ?? "").trim();
- form.merchantAppId = String(appInf.app_sub_appid ?? "").trim();
- form.appIdSource = form.providerAppId ? "provider" : "merchant";
- const pics = asStringArray(appInf.app_pics);
- form.appShotUrls = [...pics];
- form.appShotFileList = pics.map(
- (url, i) =>
- ({
- name: `app-${i + 1}.jpg`,
- url,
- status: "success",
- response: { url, media_id: url, mediaId: url },
- __wxMediaId: url
- }) as UploadUserFile
- );
- }
- }
- nextTick(() => {
- if (pendingOfflineCity) form.offlineCity = pendingOfflineCity;
- formRef.value?.clearValidate();
- });
- }
- onMounted(() => {
- hydrateManageFormFromBusinessDataCache();
- });
- const rules: FormRules = {
- merchantShortName: [{ required: true, message: "请输入商户简称", trigger: "blur" }],
- servicePhone: [
- { required: true, message: "请输入客服电话", trigger: "blur" },
- { pattern: /^[\d\-+\s]{5,20}$/, message: "请输入有效电话号码", trigger: "blur" }
- ],
- scenarioBits: [{ required: true, message: "请至少选择一种经营场景", trigger: "change" }],
- offlineVenueName: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.offline && !String(form.offlineVenueName).trim()) {
- cb(new Error("请输入线下场所名称"));
- } else cb();
- },
- trigger: "blur"
- }
- ],
- offlineProvinceCity: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.offline && (!form.offlineProvince || !form.offlineCity)) {
- cb(new Error("请选择线下场所省市"));
- } else cb();
- },
- trigger: "change"
- }
- ],
- offlineAddress: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.offline && !String(form.offlineAddress).trim()) {
- cb(new Error("请输入线下场所地址"));
- } else cb();
- },
- trigger: "blur"
- }
- ],
- offlineStorefrontUrls: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.offline && form.offlineStorefrontUrls.length < 1) {
- cb(new Error("请上传门头照片"));
- } else cb();
- },
- trigger: "change"
- }
- ],
- offlineInteriorUrls: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.offline && form.offlineInteriorUrls.length < 1) {
- cb(new Error("请上传内部照片"));
- } else cb();
- },
- trigger: "change"
- }
- ],
- miniProgramAppId: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.miniProgram && !String(form.miniProgramAppId).trim()) {
- cb(new Error("请输入小程序 APPID"));
- } else cb();
- },
- trigger: "blur"
- }
- ],
- providerAppId: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.app && form.appIdSource === "provider" && !String(form.providerAppId).trim()) {
- cb(new Error("请选择服务商应用 AppID"));
- } else cb();
- },
- trigger: "change"
- }
- ],
- appShotUrls: [
- {
- validator: (_r, _v, cb) => {
- if (form.scenarios.app && form.appShotUrls.length < 1) {
- cb(new Error("请上传 APP 截图"));
- } else cb();
- },
- trigger: "change"
- }
- ]
- };
- const MAX_IMG_MB = 20;
- function beforeImageUpload(file: File) {
- const name = file.name?.toLowerCase() || "";
- const okExt = /\.(jpe?g|png|bmp)$/i.test(name);
- const okMime = ["image/jpeg", "image/png", "image/bmp"].includes(file.type);
- if (!okExt && !okMime) {
- ElMessage.error("图片只支持 JPG、BMP、PNG 格式");
- return false;
- }
- if (file.size / 1024 / 1024 > MAX_IMG_MB) {
- ElMessage.error(`文件大小不能超过 ${MAX_IMG_MB}M`);
- return false;
- }
- return true;
- }
- function urlListKey(kind: UploadKind): "offlineStorefrontUrls" | "offlineInteriorUrls" | "miniProgramShotUrls" | "appShotUrls" {
- const map = {
- storefront: "offlineStorefrontUrls",
- interior: "offlineInteriorUrls",
- miniShot: "miniProgramShotUrls",
- appShot: "appShotUrls"
- } as const;
- return map[kind];
- }
- /** 解析 getUpload 响应:兼容 data 为字符串、双层 data、嵌套对象等 */
- function extractMediaUploadMeta(res: any): { fileUrl: string; mediaId: string; errMsg: string } {
- const errMsg = String(res?.msg ?? res?.message ?? res?.data?.msg ?? res?.data?.message ?? "").trim();
- const pickFromObject = (node: any): { fileUrl: string; mediaId: string } => {
- if (!node || typeof node !== "object" || Array.isArray(node)) return { fileUrl: "", mediaId: "" };
- const mediaId = String(node.media_id ?? node.mediaId ?? node.MediaId ?? "").trim();
- const fileUrl = String(node.url ?? node.fileUrl ?? node.mediaUrl ?? node.picUrl ?? "").trim();
- return { fileUrl, mediaId };
- };
- let root = res;
- const rawData = root?.data;
- if (rawData != null && typeof rawData === "string") {
- const s = rawData.trim();
- if (s.startsWith("{") || s.startsWith("[")) {
- try {
- root = { ...(root && typeof root === "object" ? root : {}), data: JSON.parse(s) };
- } catch {
- return { fileUrl: "", mediaId: s, errMsg };
- }
- } else if (s) {
- return { fileUrl: "", mediaId: s, errMsg };
- }
- }
- const candidates: any[] = [];
- const push = (x: any) => {
- if (x != null) candidates.push(x);
- };
- push(root);
- push(root?.data);
- if (root?.data != null && typeof root.data === "object" && !Array.isArray(root.data)) {
- push(root.data.data);
- for (const v of Object.values(root.data)) {
- if (v && typeof v === "object" && !Array.isArray(v)) push(v);
- }
- }
- let fileUrl = "";
- let mediaId = "";
- for (const c of candidates) {
- if (typeof c === "string" && c.trim()) {
- mediaId = c.trim();
- break;
- }
- if (!c || typeof c !== "object") continue;
- const p = pickFromObject(c);
- if (p.mediaId) {
- mediaId = p.mediaId;
- fileUrl = p.fileUrl || fileUrl;
- break;
- }
- for (const v of Object.values(c)) {
- if (v && typeof v === "object" && !Array.isArray(v)) {
- const p2 = pickFromObject(v);
- if (p2.mediaId) {
- mediaId = p2.mediaId;
- fileUrl = p2.fileUrl || fileUrl;
- break;
- }
- }
- }
- if (mediaId) break;
- }
- return { fileUrl, mediaId, errMsg };
- }
- async function handleMultiUpload(options: UploadRequestOptions, kind: UploadKind) {
- const uploadFileItem = options.file as UploadUserFile;
- const raw = uploadFileItem.raw || uploadFileItem;
- const file = raw instanceof File ? raw : null;
- if (!file) return;
- uploadFileItem.status = "uploading";
- try {
- const fd = new FormData();
- fd.append("file", file);
- const res: any = await getUpload(fd);
- const { fileUrl, mediaId, errMsg } = extractMediaUploadMeta(res);
- if (mediaId) {
- uploadFileItem.status = "success";
- if (fileUrl) uploadFileItem.url = fileUrl;
- uploadFileItem.response = { media_id: mediaId, url: fileUrl };
- const uid = (uploadFileItem as UploadUserFile & { uid?: number }).uid;
- if (uid != null) wechatMediaIdByFileUid.set(uid, mediaId);
- (uploadFileItem as UploadUserFile & { __wxMediaId?: string }).__wxMediaId = mediaId;
- } else {
- uploadFileItem.status = "fail";
- ElMessage.error(errMsg || "上传失败,未返回 media_id");
- }
- } catch {
- uploadFileItem.status = "fail";
- }
- syncUrlsFromFileList(kind);
- validateKind(kind);
- }
- function syncUrlsFromFileList(kind: UploadKind) {
- const key = urlListKey(kind);
- let list: UploadUserFile[] = [];
- if (kind === "storefront") list = form.offlineStorefrontFileList;
- else if (kind === "interior") list = form.offlineInteriorFileList;
- else if (kind === "miniShot") list = form.miniProgramShotFileList;
- else list = form.appShotFileList;
- form[key] = list
- .map(f => {
- const fu = f as UploadUserFile & { url?: string; response?: { url?: string } };
- const mid = getMediaIdFromUploadFile(f);
- const url = String(fu.url ?? fu.response?.url ?? "").trim();
- return mid || url;
- })
- .filter(Boolean) as string[];
- }
- function onMultiRemove(file: UploadUserFile, _fileList: UploadUserFile[], kind: UploadKind) {
- const uid = (file as UploadUserFile & { uid?: number }).uid;
- if (uid != null) wechatMediaIdByFileUid.delete(uid);
- delete (file as UploadUserFile & { __wxMediaId?: string }).__wxMediaId;
- syncUrlsFromFileList(kind);
- validateKind(kind);
- }
- function syncAllUploadUrlArrays() {
- syncUrlsFromFileList("storefront");
- syncUrlsFromFileList("interior");
- syncUrlsFromFileList("miniShot");
- syncUrlsFromFileList("appShot");
- }
- function assertActiveScenarioPicsHaveMediaId(): boolean {
- const groups: { label: string; list: UploadUserFile[] }[] = [];
- if (form.scenarios.offline) {
- groups.push({ label: "门头照片", list: form.offlineStorefrontFileList });
- groups.push({ label: "内部照片", list: form.offlineInteriorFileList });
- }
- if (form.scenarios.miniProgram) {
- groups.push({ label: "小程序截图", list: form.miniProgramShotFileList });
- }
- if (form.scenarios.app) {
- groups.push({ label: "APP 截图", list: form.appShotFileList });
- }
- for (const g of groups) {
- for (const f of g.list) {
- if (f.status !== "success") continue;
- if (!getMediaIdFromUploadFile(f)) {
- ElMessage.warning(`「${g.label}」须使用微信素材上传接口返回的 media_id,请删除后重新上传`);
- return false;
- }
- }
- }
- return true;
- }
- function validateKind(kind: UploadKind) {
- const map: Partial<Record<UploadKind, string>> = {
- storefront: "offlineStorefrontUrls",
- interior: "offlineInteriorUrls",
- appShot: "appShotUrls"
- };
- const f = map[kind];
- if (f) formRef.value?.validateField(f).catch(() => {});
- }
- function openScenarioGuide() {
- ElMessage.info("经营场景填写指引(可对接文档)");
- }
- function openStorefrontGuide() {
- ElMessage.info("门头照片拍摄指引");
- }
- function openAppidGuide() {
- ElMessage.info("公众号 APPID 查找说明");
- }
- function openMiniAppidGuide() {
- ElMessage.info("小程序 APPID 说明");
- }
- function openProviderAppGuide() {
- ElMessage.info("服务商应用说明");
- }
- function openMerchantAppGuide() {
- ElMessage.info("商家应用说明");
- }
- function openAppShotExample() {
- ElMessage.info("APP 截图示例");
- }
- function onBack() {
- router.push({ path: "/businessInfo/dataEntry", query: { ...route.query } });
- }
- function onSaveDraft() {
- ElMessage.success("草稿已保存(可对接接口)");
- }
- async function onSubmit() {
- if (!formRef.value) return;
- submitLoading.value = true;
- try {
- await formRef.value.validate();
- syncAllUploadUrlArrays();
- if (!assertActiveScenarioPicsHaveMediaId()) return;
- mergeManageInfoIntoBusinessDataCache();
- await router.push({
- path: "/businessInfo/industryQualifications",
- query: { ...route.query }
- });
- } catch {
- ElMessage.warning("请完善必填信息");
- } finally {
- submitLoading.value = false;
- }
- }
- </script>
- <style scoped lang="scss">
- $wechat-green: #07c160;
- .manage-info-page {
- box-sizing: border-box;
- min-height: 100%;
- padding: 24px 32px 40px;
- margin: 0 auto;
- background: #ffffff;
- }
- .page-title {
- margin: 0 0 24px;
- font-size: 20px;
- font-weight: 600;
- color: #303133;
- }
- .manage-form {
- :deep(.el-form-item__label) {
- font-weight: 500;
- color: #606266;
- }
- }
- .form-block {
- padding-bottom: 16px;
- margin-bottom: 28px;
- border-bottom: 1px solid #ebeef5;
- &:last-of-type {
- border-bottom: none;
- }
- }
- .sub-section {
- padding: 8px 16px 20px;
- margin-top: 8px;
- background: #fafafa;
- border: 1px solid #ebeef5;
- border-radius: 8px;
- }
- .section-head {
- display: flex;
- align-items: center;
- margin: 16px 0 20px;
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- .section-bar {
- display: inline-block;
- flex-shrink: 0;
- width: 4px;
- height: 16px;
- margin-right: 8px;
- background: $wechat-green;
- border-radius: 2px;
- }
- .section-intro {
- padding-left: 20px;
- font-size: 12px;
- color: #999999;
- }
- .field-stack {
- width: 100%;
- }
- .field-tip {
- margin: 8px 0 0;
- font-size: 12px;
- line-height: 1.65;
- color: #909399;
- }
- .scenario-checks {
- display: flex;
- flex-wrap: wrap;
- gap: 12px 20px;
- margin-bottom: 8px;
- }
- .label-with-icon {
- display: inline-flex;
- gap: 4px;
- align-items: center;
- }
- .info-icon {
- color: #909399;
- cursor: help;
- }
- :deep(.el-upload--picture-card) {
- --el-upload-picture-card-size: 120px;
- }
- :deep(.el-upload-list--picture-card .el-upload-list__item) {
- width: 120px;
- height: 120px;
- }
- .footer-actions {
- display: flex;
- align-items: center;
- justify-content: center;
- padding-top: 24px;
- margin-top: 16px;
- border-top: 1px solid #ebeef5;
- }
- .footer-center {
- display: flex;
- flex: 1;
- justify-content: center;
- }
- .btn-next {
- min-width: 100px;
- background: $wechat-green;
- border-color: $wechat-green;
- &:hover {
- background: #06ad56;
- border-color: #06ad56;
- }
- }
- </style>
|