import { getDefaultPartnerRegionForm } from './china-region'; import { isStoreContactPhone, STORE_CONTACT_PHONE_HINT } from '@dukang/domain'; export type StoreDraftForm = { regionCodes: string[]; cityId: string; province: string; city: string; district: string; name: string; /** 门店登录手机号(老板主账号) */ phone: string; /** 对外联系电话(店长);可与登录号不同 */ contactPhone: string; address: string; /** 门店坐标(定位或地理编码) */ latitude: string; longitude: string; openTime: string; closeTime: string; /** 是否启用第二段营业时间 */ dualHours: boolean; openTime2: string; closeTime2: string; /** 人均费用(选填) */ avgPrice: string; categoryParentId: string; categoryId: string; intro: string; /** 好客权益券使用规则 */ benefitUsageRule: string; coverUrl: string; envPhotoUrls: string[]; /** 签约合同,支持多张照片 / PDF */ contractUrls: string[]; bankAccountName: string; bankAccountNo: string; bankBranch: string; /** 门店套餐(拓店第 4 步,可空) */ packages: import('@dukang/shared-types').StorePackageItemDto[]; }; export type StoreDraft = { step: number; form: StoreDraftForm; }; export const STORE_DRAFT_KEY = 'partner_store_draft_v2'; export function storeDraftKey(accountId?: string): string { return accountId ? `${STORE_DRAFT_KEY}_${accountId}` : STORE_DRAFT_KEY; } export const defaultStoreForm = (): StoreDraftForm => ({ ...getDefaultPartnerRegionForm(), cityId: '', name: '', phone: '', contactPhone: '', address: '', latitude: '', longitude: '', openTime: '10:00', closeTime: '22:00', dualHours: false, openTime2: '17:00', closeTime2: '21:00', avgPrice: '', categoryParentId: '', categoryId: '', intro: '', benefitUsageRule: '', coverUrl: '', envPhotoUrls: ['', '', ''], contractUrls: [], bankAccountName: '', bankAccountNo: '', bankBranch: '', packages: [], }); const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/; const PHONE_RE = /^1[3-9]\d{9}$/; const BANK_RE = /^\d{16,19}$/; function timeToMinutes(hhmm: string): number { const [h, m] = hhmm.split(':').map(Number); return h * 60 + m; } export const MIN_ENV_PHOTO_COUNT = 3; /** 签约合同最多可上传的照片 / PDF 数量 */ export const MAX_CONTRACT_COUNT = 20; export function normalizeStringArray(urls: unknown, minLen: number): string[] { const arr = Array.isArray(urls) ? urls.map((u) => String(u ?? '')) : []; while (arr.length < minLen) arr.push(''); return arr; } export function addEnvPhotoSlot(urls: string[]): string[] { return [...urls, '']; } export function normalizeStoreDraftForm(raw: Partial | null | undefined): StoreDraftForm { const base = defaultStoreForm(); if (!raw) return base; return { ...base, ...raw, regionCodes: Array.isArray(raw.regionCodes) ? raw.regionCodes.map(String) : base.regionCodes, cityId: String(raw.cityId ?? base.cityId), phone: String(raw.phone ?? base.phone), contactPhone: String(raw.contactPhone ?? '').trim() || String(raw.phone ?? '').trim() || base.contactPhone, latitude: raw.latitude != null && raw.latitude !== '' ? String(raw.latitude) : base.latitude, longitude: raw.longitude != null && raw.longitude !== '' ? String(raw.longitude) : base.longitude, openTime: String(raw.openTime ?? base.openTime), closeTime: String(raw.closeTime ?? base.closeTime), dualHours: Boolean(raw.dualHours), openTime2: String(raw.openTime2 ?? base.openTime2), closeTime2: String(raw.closeTime2 ?? base.closeTime2), avgPrice: String(raw.avgPrice ?? base.avgPrice), envPhotoUrls: normalizeStringArray(raw.envPhotoUrls, MIN_ENV_PHOTO_COUNT), // 兼容旧草稿:单个 contractUrl 迁移为数组 contractUrls: (() => { const list = Array.isArray(raw.contractUrls) ? raw.contractUrls : [(raw as { contractUrl?: unknown }).contractUrl]; const seen = new Set(); const out: string[] = []; for (const item of list) { const url = String(item ?? '').trim(); if (!url || seen.has(url)) continue; seen.add(url); out.push(url); if (out.length >= MAX_CONTRACT_COUNT) break; } return out; })(), packages: Array.isArray(raw.packages) ? raw.packages.map((p, i) => ({ name: String((p as { name?: string }).name ?? ''), price: String((p as { price?: string | number }).price ?? ''), dishes: String((p as { dishes?: string }).dishes ?? ''), usableTime: (p as { usableTime?: string }).usableTime ?? '', otherNotes: (p as { otherNotes?: string }).otherNotes ?? '', sortOrder: i, })) : [], }; } export function loadStoreDraft(accountId?: string): StoreDraft | null { try { const key = storeDraftKey(accountId); let raw = localStorage.getItem(key); if (!raw && accountId) { raw = localStorage.getItem(STORE_DRAFT_KEY); if (raw) { localStorage.setItem(key, raw); localStorage.removeItem(STORE_DRAFT_KEY); } } if (!raw) return null; const parsed = JSON.parse(raw) as StoreDraft; return { step: Number(parsed.step) || 0, form: normalizeStoreDraftForm(parsed.form), }; } catch { return null; } } export function saveStoreDraft(draft: StoreDraft, accountId?: string) { localStorage.setItem(storeDraftKey(accountId), JSON.stringify(draft)); } export function clearStoreDraft(accountId?: string) { localStorage.removeItem(storeDraftKey(accountId)); } /** 清除当前账号及遗留的全局录店草稿 */ export function clearAllStoreDrafts(accountId?: string) { clearStoreDraft(accountId); localStorage.removeItem(STORE_DRAFT_KEY); } export function validateStoreStep1( form: Pick< StoreDraftForm, | 'regionCodes' | 'cityId' | 'name' | 'address' | 'openTime' | 'closeTime' | 'dualHours' | 'openTime2' | 'closeTime2' | 'avgPrice' | 'categoryId' | 'intro' | 'benefitUsageRule' >, ): string | null { if (!form.regionCodes || form.regionCodes.length < 3) return '请选择省 / 市 / 区县'; if (!form.cityId) return '所选地区未匹配到开城城市,请联系总部配置开城区划'; if (!form.name.trim()) return '请填写门店名称'; if (!form.address.trim()) return '请填写详细地址'; if (!form.openTime.trim()) return '请填写营业开始时间'; if (!TIME_RE.test(form.openTime.trim())) return '营业开始时间格式须为 HH:MM'; if (!form.closeTime.trim()) return '请填写营业结束时间'; if (!TIME_RE.test(form.closeTime.trim())) return '营业结束时间格式须为 HH:MM'; if (timeToMinutes(form.openTime.trim()) >= timeToMinutes(form.closeTime.trim())) { return '营业结束时间须晚于开始时间'; } if (form.dualHours) { if (!form.openTime2.trim() || !form.closeTime2.trim()) return '请完整填写第二段营业时间'; if (!TIME_RE.test(form.openTime2.trim()) || !TIME_RE.test(form.closeTime2.trim())) { return '第二段时间格式须为 HH:MM'; } if (timeToMinutes(form.openTime2.trim()) >= timeToMinutes(form.closeTime2.trim())) { return '第二段结束时间须晚于开始时间'; } if (timeToMinutes(form.closeTime.trim()) >= timeToMinutes(form.openTime2.trim())) { return '第二段开始时间须晚于第一段结束时间'; } } if (form.avgPrice.trim()) { const n = Number(form.avgPrice); if (Number.isNaN(n) || n < 0) return '人均费用须为非负数字'; } if (!form.categoryId.trim()) return '请选择店铺类型'; if (form.intro.trim()) { const len = form.intro.trim().length; if (len < 2 || len > 500) return '门店简介须为 2~500 字'; } if (form.benefitUsageRule.trim().length > 1000) { return '好客权益券使用规则最多 1000 字'; } return null; } export function validateStoreStep2( form: Pick, ): string | null { if (!form.coverUrl.trim()) return '请上传门头照'; const envCount = form.envPhotoUrls.filter((u) => u.trim()).length; if (envCount < MIN_ENV_PHOTO_COUNT) return `请上传至少 ${MIN_ENV_PHOTO_COUNT} 张环境照片`; const contractCount = (form.contractUrls ?? []).filter((u) => u.trim()).length; if (contractCount < 1) return '请上传签约合同'; return null; } export function patchEnvPhotoAt(urls: string[], index: number, url: string): string[] { const envPhotoUrls = [...urls]; while (envPhotoUrls.length <= index) envPhotoUrls.push(''); envPhotoUrls[index] = url; return envPhotoUrls; } export function validateStoreStep3( form: Pick< StoreDraftForm, 'bankAccountName' | 'bankAccountNo' | 'bankBranch' | 'phone' | 'contactPhone' >, ): string | null { if (!form.bankAccountName.trim()) return '请填写户主姓名'; if (!form.bankAccountNo.trim()) return '请填写银行卡号'; if (!BANK_RE.test(form.bankAccountNo.replace(/\s/g, ''))) return '银行卡号须为 16~19 位数字'; if (!form.bankBranch.trim()) return '请填写开户支行'; if (!form.phone.trim()) return '请填写门店登录手机号'; if (!PHONE_RE.test(form.phone.trim())) return '门店登录手机号须为11位手机号'; if (!form.contactPhone.trim()) return '请填写联系电话'; if (!isStoreContactPhone(form.contactPhone.trim())) return STORE_CONTACT_PHONE_HINT; return null; }