门店开通流程
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { codeToText, regionData } from 'element-china-area-data';
|
||||
|
||||
export { regionData as CHINA_REGION_OPTIONS };
|
||||
|
||||
export type OpenCityRef = {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
partnerId?: string | null;
|
||||
};
|
||||
export type ParsedChinaRegion = {
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
provinceCode: string;
|
||||
cityCode: string;
|
||||
districtCode: string;
|
||||
};
|
||||
|
||||
export function districtCodeToCityCode(districtCode: string): string {
|
||||
if (districtCode.length < 6) return districtCode;
|
||||
return `${districtCode.slice(0, 4)}00`;
|
||||
}
|
||||
|
||||
export function parseRegionCodes(codes?: string[]): ParsedChinaRegion | null {
|
||||
if (!codes || codes.length < 3) return null;
|
||||
const [provinceCode, cityCode, districtCode] = codes;
|
||||
const province = codeToText[provinceCode];
|
||||
const city = codeToText[cityCode];
|
||||
const district = codeToText[districtCode];
|
||||
if (!province || !city || !district) return null;
|
||||
return { province, city, district, provinceCode, cityCode, districtCode };
|
||||
}
|
||||
|
||||
export function formatRegionLabel(region: ParsedChinaRegion): string {
|
||||
return `${region.province} / ${region.city} / ${region.district}`;
|
||||
}
|
||||
|
||||
export function matchOpenCityId(cities: OpenCityRef[], districtCode: string): string | undefined {
|
||||
const cityCode = districtCodeToCityCode(districtCode);
|
||||
return (
|
||||
cities.find((c) => c.code === cityCode)?.id
|
||||
?? cities.find((c) => c.code === districtCode)?.id
|
||||
?? cities.find((c) => districtCode.startsWith(c.code.slice(0, 4)))?.id
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRegionBinding(codes: string[], cities: OpenCityRef[]) {
|
||||
const region = parseRegionCodes(codes);
|
||||
if (!region) return null;
|
||||
const cityId = matchOpenCityId(cities, region.districtCode);
|
||||
const matchedCity = cityId ? cities.find((c) => c.id === cityId) : undefined;
|
||||
return {
|
||||
region,
|
||||
cityId,
|
||||
matchedCity,
|
||||
cityCode: districtCodeToCityCode(region.districtCode),
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
export type StoreDraftForm = {
|
||||
regionCodes: string[];
|
||||
cityId: string;
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
district: string;
|
||||
address: string;
|
||||
intro: string;
|
||||
accountPhone: string;
|
||||
accountName: string;
|
||||
coverUrl: string;
|
||||
envPhotoUrls: string[];
|
||||
contractUrl: string;
|
||||
bankAccountName: string;
|
||||
bankAccountNo: string;
|
||||
bankBranch: string;
|
||||
@@ -14,27 +23,55 @@ export type StoreDraft = {
|
||||
form: StoreDraftForm;
|
||||
};
|
||||
|
||||
export const STORE_DRAFT_KEY = 'partner_store_draft_v1';
|
||||
export const STORE_DRAFT_KEY = 'partner_store_draft_v2';
|
||||
|
||||
export const defaultStoreForm = (): StoreDraftForm => ({
|
||||
regionCodes: [],
|
||||
cityId: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
district: '金水区',
|
||||
address: '',
|
||||
intro: '',
|
||||
accountPhone: '',
|
||||
accountName: '',
|
||||
coverUrl: '',
|
||||
envPhotoUrls: ['', '', ''],
|
||||
contractUrl: '',
|
||||
bankAccountName: '',
|
||||
bankAccountNo: '',
|
||||
bankBranch: '',
|
||||
});
|
||||
|
||||
function normalizeStringArray(raw: unknown, length: number): string[] {
|
||||
if (!Array.isArray(raw)) return Array.from({ length }, () => '');
|
||||
const items = raw.map((item) => String(item ?? ''));
|
||||
while (items.length < length) items.push('');
|
||||
return items.slice(0, length);
|
||||
}
|
||||
|
||||
function normalizeForm(raw: Record<string, unknown>): StoreDraftForm {
|
||||
const base = defaultStoreForm();
|
||||
const regionCodes = Array.isArray(raw.regionCodes)
|
||||
? raw.regionCodes.map((code) => String(code))
|
||||
: base.regionCodes;
|
||||
return {
|
||||
regionCodes,
|
||||
cityId: String(raw.cityId ?? base.cityId),
|
||||
province: String(raw.province ?? base.province),
|
||||
city: String(raw.city ?? base.city),
|
||||
district: String(raw.district ?? base.district),
|
||||
name: String(raw.name ?? base.name),
|
||||
phone: String(raw.phone ?? base.phone),
|
||||
district: String(raw.district ?? base.district),
|
||||
address: String(raw.address ?? base.address),
|
||||
intro: String(raw.intro ?? base.intro),
|
||||
accountPhone: String(raw.accountPhone ?? base.accountPhone),
|
||||
accountName: String(raw.accountName ?? base.accountName),
|
||||
coverUrl: String(raw.coverUrl ?? base.coverUrl),
|
||||
envPhotoUrls: normalizeStringArray(raw.envPhotoUrls, 3),
|
||||
contractUrl: String(raw.contractUrl ?? base.contractUrl),
|
||||
bankAccountName: String(raw.bankAccountName ?? base.bankAccountName),
|
||||
bankAccountNo: String(raw.bankAccountNo ?? base.bankAccountNo),
|
||||
bankBranch: String(raw.bankBranch ?? base.bankBranch),
|
||||
@@ -69,11 +106,14 @@ export function clearStoreDraft() {
|
||||
const PHONE_RE = /^1\d{10}$/;
|
||||
const BANK_RE = /^\d{16,19}$/;
|
||||
|
||||
export function validateStoreStep1(form: StoreDraftForm): string | null {
|
||||
export function validateStoreStep1(
|
||||
form: Pick<StoreDraftForm, 'regionCodes' | 'cityId' | 'name' | 'phone' | 'address' | 'intro'>,
|
||||
): string | null {
|
||||
if (!form.regionCodes || form.regionCodes.length < 3) return '请选择省 / 市 / 区县';
|
||||
if (!form.cityId) return '所选地区未匹配到开城城市,请联系总部配置开城区划';
|
||||
if (!form.name.trim()) return '请填写门店名称';
|
||||
if (!form.phone.trim()) return '请填写联系电话';
|
||||
if (!PHONE_RE.test(form.phone.trim())) return '联系电话须为11位手机号';
|
||||
if (!form.district.trim()) return '请填写区县';
|
||||
if (!form.address.trim()) return '请填写详细地址';
|
||||
if (form.intro.trim()) {
|
||||
const len = form.intro.trim().length;
|
||||
@@ -82,7 +122,9 @@ export function validateStoreStep1(form: StoreDraftForm): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateStoreStep3(form: StoreDraftForm): string | null {
|
||||
export function validateStoreStep3(
|
||||
form: Pick<StoreDraftForm, 'bankAccountName' | 'bankAccountNo' | 'bankBranch'>,
|
||||
): string | null {
|
||||
if (!form.bankAccountName.trim()) return '请填写户主姓名';
|
||||
if (!form.bankAccountNo.trim()) return '请填写银行卡号';
|
||||
if (!BANK_RE.test(form.bankAccountNo.replace(/\s/g, ''))) return '银行卡号须为 16~19 位数字';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export type StoreStatusValue = 'OPEN' | 'PAUSED' | 'CLOSED';
|
||||
|
||||
export function storeStatusLabel(status: string): string {
|
||||
const s = String(status).toUpperCase();
|
||||
if (s === 'OPEN') return '营业中';
|
||||
if (s === 'PAUSED') return '暂时闭店';
|
||||
if (s === 'CLOSED') return '关闭';
|
||||
return status;
|
||||
}
|
||||
|
||||
export function storeStatusPillClass(status: string): string {
|
||||
const s = String(status).toUpperCase();
|
||||
if (s === 'OPEN') return 'partner-status-pill--open';
|
||||
if (s === 'PAUSED') return 'partner-status-pill--paused';
|
||||
return 'partner-status-pill--closed';
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { apiBase, request } from './api';
|
||||
|
||||
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
|
||||
|
||||
export type UploadFileResult = {
|
||||
url: string;
|
||||
ossKey: string;
|
||||
bucket: string;
|
||||
mock: boolean;
|
||||
};
|
||||
|
||||
/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */
|
||||
export async function uploadFileToOss(
|
||||
file: File,
|
||||
options: { bizType: string; mediaType?: OssMediaType },
|
||||
): Promise<UploadFileResult> {
|
||||
const mediaType = options.mediaType ?? (file.type.startsWith('video/') ? 'VIDEO' : 'IMAGE');
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('bizType', options.bizType);
|
||||
formData.append('mediaType', mediaType);
|
||||
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const headers: Record<string, string> = { 'X-Client-App': 'PARTNER_H5' };
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${apiBase}/common/resources/upload`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.code === 401) {
|
||||
localStorage.removeItem('accessToken');
|
||||
throw new Error('未登录');
|
||||
}
|
||||
if (json.code !== 0) throw new Error(json.message || '上传失败');
|
||||
|
||||
const data = json.data as UploadFileResult;
|
||||
return {
|
||||
url: data.url,
|
||||
ossKey: data.ossKey,
|
||||
bucket: data.bucket,
|
||||
mock: data.mock,
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenCityOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
province: string;
|
||||
};
|
||||
|
||||
export async function fetchPartnerCities(): Promise<OpenCityOption[]> {
|
||||
return request<OpenCityOption[]>('PARTNER_H5', '/partner/stores/cities');
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createWeixinSdk, isWechatEnv } from '@dukang/weixin-sdk';
|
||||
|
||||
export const weixinSdk = createWeixinSdk({
|
||||
apiBase: '/api/v1',
|
||||
clientApp: 'PARTNER_H5',
|
||||
getAccessToken: () => localStorage.getItem('accessToken'),
|
||||
});
|
||||
|
||||
export { isWechatEnv };
|
||||
Reference in New Issue
Block a user