import { createSharedComposable } from "@vueuse/core"; import * as z from "zod"; export const _useEventOrder = () => { const sectionIndex = ref(0); // Referer const REFERRER_MAP = { anadesign: "A&A Design 广告先生", dreamstudio: "Dream Studio", infinity: "Infinity Visuals", // 你可以自由扩充 } as const; const route = useRoute(); const refererKey = computed(() => { const raw = route.query.referer; return Array.isArray(raw) ? raw[0] : raw || ""; }); const refererNote = computed(() => { const key = refererKey.value as keyof typeof REFERRER_MAP; return key in REFERRER_MAP ? REFERRER_MAP[key] : ""; }); function formatLocalDate(date: Date): string { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } const EVENT_LOCATIONS = [ "永平区", "周边城市(Batu Pahat / Kluang)", "柔佛州境内", "柔佛州境外", ] as const; const eventLocationItems = ref([...EVENT_LOCATIONS]); const orderSchema = z.object({ contactName: z.string().min(1, "姓名不能为空"), // 用户可以选择是否使用同一联系方式 isSameContact: z.boolean().default(true), contactNumber: z.string().optional(), eventName: z.string().min(1, "活动名称不能为空"), eventDate: z .string() .refine((date) => { return !isNaN(Date.parse(date)); }, "无效的日期") .refine((date) => { const eventDate = new Date(date); const today = new Date(); // 仅按日期比较(忽略时间) eventDate.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0); // 活动日期必须是今天或未来的日期 return eventDate.getTime() >= today.getTime(); }, "活动日期必须是今天或未来的日期"), eventLocation: z.enum(EVENT_LOCATIONS), // 服务:竞标系统 biddingSystem: z.boolean().default(false), biddingSystemProvideImage: z.boolean().default(false).optional(), estimatedBidItemCount: z.string().optional(), // 服务:背景设计 backgroundDesign: z.boolean().default(false), backgroundType: z.enum(["static", "dynamic"]).optional(), backgroundWidthOverride: z.number().optional(), backgroundHeightOverride: z.number().optional(), // 服务:流程 PPT 设计 flowBackgroundDesign: z.boolean().default(false), backgroundSourceProvided: z.boolean().default(false), // 如果自己设计的,那么就没有这个额外收费 pptDesignQty: z .number() .min(1, "最少都要一张") .max(20, "太多了我来不及做设计"), // 服务:赞助商征信录设计 sponsorListDesign: z.boolean().default(false), }); type OrderForm = z.infer; const orderState = reactive({ contactName: "", isSameContact: true, contactNumber: "", eventName: "", eventDate: formatLocalDate( new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) ), // 默认一周后 eventLocation: EVENT_LOCATIONS[0], biddingSystem: false, biddingSystemProvideImage: false, estimatedBidItemCount: "30-40", backgroundDesign: false, backgroundType: "static", backgroundWidthOverride: 1920, backgroundHeightOverride: 1080, flowBackgroundDesign: false, backgroundSourceProvided: false, pptDesignQty: 1, sponsorListDesign: false, }); // --- Pricing logic (editable) --- const PRICE = { bidding: 550, // base for bidding system biddingProvideImageDiscount: 50, // discount if client provides all images biddingLocationTierAdjustment: { [EVENT_LOCATIONS[0]]: { adj: -100, reason: "同乡折扣" }, [EVENT_LOCATIONS[1]]: { adj: 0, reason: "" }, [EVENT_LOCATIONS[2]]: { adj: 200, reason: "含差旅费" }, [EVENT_LOCATIONS[3]]: { adj: 400, reason: "含差旅费" }, }, // biddingTierAdditions: [ // { range: [10, 20], add: 0 }, // { range: [20, 30], add: 200 }, // { range: [30, 40], add: 400 }, // { range: [40, 50], add: 600 }, // ], background: { static: 100, dynamic: 250, // highResSurcharge: 200, // if dimensions exceed 1920x1080 }, // 背景设计加急 // 静态背景:倒数 2 天 + 20,倒数 1 天 + 50 // 动态背景:倒数 5 天 + 50,倒数 1 天 + 100,中间 Lerp 计算 backgroundRush: (daysLeft: number, isDynamic: boolean) => { if (isDynamic) { if (daysLeft >= 5) return 0; if (daysLeft <= 1) return 100; return Math.round(((5 - daysLeft) / 4) * 50 + 50); } else { if (daysLeft >= 2) return 0; if (daysLeft <= 1) return 50; return Math.round(((2 - daysLeft) / 1) * 30 + 20); } }, flowPptDesign: { base: 90, perSlide: 10, slideAfterTen: 5, providedSourceDiscount: 0.5, // 20% off if background sources provided }, sponsorList: 100, } as const; // function parseRange(range?: string | null): [number, number] | null { // if (!range) return null; // const m = String(range).match(/(\d+)\s*[-~]\s*(\d+)/); // if (!m) return null; // return [Number(m[1]), Number(m[2])]; // } const priceBreakdown = computed(() => { const items: { label: string; amount: number; note?: string }[] = []; // Bidding system if (orderState.biddingSystem) { let amount = PRICE.bidding; amount -= orderState.biddingSystemProvideImage ? PRICE.biddingProvideImageDiscount : 0; // const r = parseRange(orderState.estimatedBidItemCount); // if (r) { // const tier = PRICE.biddingTierAdditions.find( // (t) => r[0] >= t.range[0] && r[1] <= t.range[1] // ); // if (tier) amount += tier.add; // } items.push({ label: "竞价系统", amount, note: `${orderState.estimatedBidItemCount} 件区间` + (orderState.biddingSystemProvideImage ? ",素材由客户提供" : ",包含素材拍摄") + ",图片处理,现场技术支持", }); const locationAdj = PRICE.biddingLocationTierAdjustment[orderState.eventLocation]; if (locationAdj.adj) { items.push({ label: locationAdj.reason, amount: locationAdj.adj, note: `活动地点:${orderState.eventLocation}`, }); } } // Stage background design if (orderState.backgroundDesign) { let amount = orderState.backgroundType === "dynamic" ? PRICE.background.dynamic : PRICE.background.static; const w = Number(orderState.backgroundWidthOverride || 1920); const h = Number(orderState.backgroundHeightOverride || 1080); // if (w > 1920 || h > 1080) amount += PRICE.background.highResSurcharge; items.push({ label: `舞台背景设计(${ orderState.backgroundType === "dynamic" ? "动效" : "静态" })`, amount, note: `${w}×${h}`, }); } // Background design rush fee if (orderState.backgroundDesign) { const eventDate = new Date(orderState.eventDate); const today = new Date(); // 计算剩余天数(向上取整) const diffTime = eventDate.getTime() - today.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const rushFee = PRICE.backgroundRush( diffDays, orderState.backgroundType === "dynamic" ); if (rushFee > 0) { items.push({ label: "背景设计加急费", amount: rushFee, note: `距离活动仅剩 ${diffDays} 天`, }); } } // Flow/PPT design if (orderState.flowBackgroundDesign) { const qty = Number(orderState.pptDesignQty || 0); // 如果客户有要求背景设计,Base Price 不算 let amount = orderState.backgroundDesign ? 0 : PRICE.flowPptDesign.base; if (qty > 0) { amount += Math.min(qty, 10) * PRICE.flowPptDesign.perSlide; if (qty > 10) { amount += (qty - 10) * PRICE.flowPptDesign.slideAfterTen; } } if (orderState.backgroundSourceProvided) { amount = Math.round( amount * (1 - PRICE.flowPptDesign.providedSourceDiscount) ); } items.push({ label: "流程/PPT设计", amount, note: `${qty} 张幻灯片设计` + (orderState.backgroundSourceProvided ? ",客户提供背景素材" : ""), }); } // Sponsor list if (orderState.sponsorListDesign) { items.push({ label: "赞助商位设计", amount: PRICE.sponsorList }); } return items; }); const estimatedTotal = computed(() => priceBreakdown.value.reduce((sum, i) => sum + (i.amount || 0), 0) ); const money = new Intl.NumberFormat("zh-CN", { style: "currency", currency: "MYR", maximumFractionDigits: 0, }); // 🚀 生成最终字符串 const buildMessage = computed(() => { const lines = priceBreakdown.value .map( (i) => `- ${i.label}: ${money.format(i.amount)}${ i.note ? `(${i.note})` : "" }` ) .join("\n"); return [ `[下单] 宴会大屏报价请求`, `姓名:${orderState.contactName}`, `电话:${orderState.contactNumber || "未填写"}`, `活动:${orderState.eventName}`, `日期:${orderState.eventDate}`, `地点:${orderState.eventLocation}`, ``, `服务明细:\n${lines || "无"}`, ``, `总计:${money.format(estimatedTotal.value)}`, `推荐单位:${refererNote.value || "无"}`, ].join("\n"); }); return { sectionIndex, eventLocationItems, orderSchema, orderState, priceBreakdown, estimatedTotal, buildMessage, money, }; }; export const useEventOrder = createSharedComposable(_useEventOrder);