e93e4b8f84
Co-authored-by: Cursor <cursoragent@cursor.com>
250 lines
8.0 KiB
TypeScript
250 lines
8.0 KiB
TypeScript
export type StoreDraftForm = {
|
|
regionCodes: string[];
|
|
cityId: string;
|
|
province: string;
|
|
city: string;
|
|
district: string;
|
|
name: string;
|
|
phone: 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[];
|
|
contractUrl: 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;
|
|
}
|
|
|
|
import { getDefaultPartnerRegionForm } from './china-region';
|
|
|
|
export const defaultStoreForm = (): StoreDraftForm => ({
|
|
...getDefaultPartnerRegionForm(),
|
|
cityId: '',
|
|
name: '',
|
|
phone: '',
|
|
address: '',
|
|
latitude: '',
|
|
longitude: '',
|
|
openTime: '10:00',
|
|
closeTime: '22:00',
|
|
dualHours: false,
|
|
openTime2: '17:00',
|
|
closeTime2: '21:00',
|
|
avgPrice: '',
|
|
categoryParentId: '',
|
|
categoryId: '',
|
|
intro: '',
|
|
benefitUsageRule: '',
|
|
coverUrl: '',
|
|
envPhotoUrls: ['', '', ''],
|
|
contractUrl: '',
|
|
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;
|
|
|
|
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<StoreDraftForm> | 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),
|
|
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),
|
|
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<StoreDraftForm, 'coverUrl' | 'envPhotoUrls' | 'contractUrl'>,
|
|
): 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} 张环境照片`;
|
|
if (!form.contractUrl.trim()) 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'
|
|
>,
|
|
): 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位手机号';
|
|
return null;
|
|
}
|