UNPKG

@pisell/pisellos

Version:

一个可扩展的前端模块化SDK框架,支持插件系统

417 lines (415 loc) 18.1 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/solution/Sales/index.ts var Sales_exports = {}; __export(Sales_exports, { Sales: () => SalesImpl, SalesImpl: () => SalesImpl, default: () => Sales_default }); module.exports = __toCommonJS(Sales_exports); var import_dayjs = __toESM(require("dayjs")); var import_BaseModule = require("../../modules/BaseModule"); var import_types = require("./types"); __reExport(Sales_exports, require("./types"), module.exports); var SalesImpl = class extends import_BaseModule.BaseModule { constructor(name, version) { super(name, version); this.defaultName = "sales"; this.defaultVersion = "1.0.0"; this.defaultTimelineBucketMinutes = 15; this.isSolution = true; this.options = {}; this.otherParams = {}; } async initialize(core, options = {}) { this.core = core; this.options = options; this.otherParams = options.otherParams || {}; const appPlugin = this.core.getPlugin("app"); this.request = appPlugin == null ? void 0 : appPlugin.getApp().request; if (!this.request) { throw new Error("Sales 解决方案需要 request 插件支持"); } this.store = { currentDay: (0, import_dayjs.default)().startOf("day"), reservationList: [] }; await this.core.effects.emit(`${this.name}:onInited`, {}); } // ------------------------------------------------------------------------- // SalesModuleAPI 实现 // ------------------------------------------------------------------------- /** 获取当前 store 快照 */ getState() { return { ...this.store }; } /** 获取当前选择日期 */ getCurrentDay() { return this.store.currentDay; } /** 设置当前选择日期,会自动归一到当天 00:00:00 */ setCurrentDay(currentDay) { const normalized = (0, import_dayjs.default)(currentDay).startOf("day"); this.store.currentDay = normalized; this.core.effects.emit(import_types.SalesHooks.onCurrentDayChanged, { currentDay: normalized }); return normalized; } /** 获取当前预约列表 */ getReservationList() { return [...this.store.reservationList]; } /** 覆盖设置当前预约列表 */ setReservationList(reservationList) { this.store.reservationList = reservationList; this.core.effects.emit(import_types.SalesHooks.onReservationListChanged, { reservationList }); return reservationList; } /** 重置预约列表为空 */ resetReservationList() { this.store.reservationList = []; this.core.effects.emit(import_types.SalesHooks.onReservationListChanged, { reservationList: [] }); return []; } // ------------------------------------------------------------------------- // 生命周期 // ------------------------------------------------------------------------- async destroy() { await this.core.effects.emit(`${this.name}:onDestroy`, {}); super.destroy(); } /** * 时间轴粒度(分钟) * - 默认 15 分钟 * - 可通过 registerModule 时的 options.otherParams.timelineBucketMinutes 覆盖 */ getTimelineBucketMinutes() { var _a; const dynamicBucket = Number((_a = this.otherParams) == null ? void 0 : _a.timelineBucketMinutes); if (!Number.isFinite(dynamicBucket) || dynamicBucket <= 0) return this.defaultTimelineBucketMinutes; return Math.floor(dynamicBucket); } /** * 解析营业日统计窗口: * - 当 operating_day_boundary.type === 'start_time' * 使用「今天 + boundary.time」到「明天 + boundary.time」 * - 否则回退自然日(00:00 到次日 00:00) */ getTimelineRangeByOperatingBoundary() { var _a, _b, _c, _d, _e, _f, _g; const today = (0, import_dayjs.default)(); const defaultStart = today.startOf("day"); const defaultEndExclusive = defaultStart.add(1, "day"); const appPlugin = this.core.getPlugin("app"); const coreData = (_f = (_d = (_c = (_b = (_a = appPlugin == null ? void 0 : appPlugin.getApp()) == null ? void 0 : _a.data) == null ? void 0 : _b.store) == null ? void 0 : _c.getStore) == null ? void 0 : (_e = _d.call(_c)).getDataByModel) == null ? void 0 : _f.call(_e, "core"); const operatingDayBoundary = (_g = coreData == null ? void 0 : coreData.core) == null ? void 0 : _g.operating_day_boundary; if ((operatingDayBoundary == null ? void 0 : operatingDayBoundary.type) !== "start_time") { return { startAt: defaultStart, endExclusiveAt: defaultEndExclusive }; } const boundaryTime = String(operatingDayBoundary.time || "").trim(); const startAt = (0, import_dayjs.default)(`${today.format("YYYY-MM-DD")} ${boundaryTime}`); if (!startAt.isValid()) { return { startAt: defaultStart, endExclusiveAt: defaultEndExclusive }; } return { startAt, endExclusiveAt: startAt.add(1, "day") }; } /** * 统计时间轴每个时间片的预约数量。 * 算法使用差分数组,复杂度 O(n + s): * - n = booking 数量 * - s = 时间片数量 */ getTimelineHighlights(bookingList = [], startDateTime, endDateTime) { const bucketMinutes = this.getTimelineBucketMinutes(); const bucketMs = bucketMinutes * 60 * 1e3; const customStartAt = startDateTime ? (0, import_dayjs.default)(startDateTime) : (0, import_dayjs.default)(""); const customEndExclusiveAt = endDateTime ? (0, import_dayjs.default)(endDateTime) : (0, import_dayjs.default)(""); const hasValidCustomRange = customStartAt.isValid() && customEndExclusiveAt.isValid(); const { startAt, endExclusiveAt } = hasValidCustomRange ? { startAt: customStartAt, endExclusiveAt: customEndExclusiveAt } : this.getTimelineRangeByOperatingBoundary(); const startAtMs = startAt.valueOf(); const endExclusiveAtMs = endExclusiveAt.valueOf(); if (!startAt.isValid() || !endExclusiveAt.isValid() || endExclusiveAtMs <= startAtMs) return []; const slotCount = Math.ceil((endExclusiveAtMs - startAtMs) / bucketMs); if (slotCount <= 0) return []; const diff = new Int32Array(slotCount + 1); for (let i = 0; i < bookingList.length; i++) { const booking = bookingList[i]; const bookingStartAt = this.toBookingDateTime(booking.start_date, booking.start_time); const bookingEndAt = this.toBookingDateTime(booking.end_date, booking.end_time); if (!bookingStartAt.isValid() || !bookingEndAt.isValid()) continue; const overlapStartMs = Math.max(bookingStartAt.valueOf(), startAtMs); const overlapEndExclusiveMs = Math.min(bookingEndAt.valueOf() + 1, endExclusiveAtMs); if (overlapStartMs >= overlapEndExclusiveMs) continue; const startIdx = Math.max(0, Math.ceil((overlapStartMs - startAtMs) / bucketMs)); const endIdx = Math.min( slotCount - 1, Math.floor((overlapEndExclusiveMs - 1 - startAtMs) / bucketMs) ); if (startIdx > endIdx) continue; diff[startIdx] += 1; diff[endIdx + 1] -= 1; } const timeline = []; let runningCount = 0; for (let i = 0; i < slotCount; i++) { runningCount += diff[i]; timeline.push(runningCount); } return timeline; } /** dayjs 未启用插件时,手动封装 >= 判断 */ isSameOrAfter(left, right) { return left.isAfter(right) || left.isSame(right); } /** dayjs 未启用插件时,手动封装 <= 判断 */ isSameOrBefore(left, right) { return left.isBefore(right) || left.isSame(right); } /** * 将 booking 的日期/时间字段统一组装为 dayjs * - date + time 都有:按完整时间解析 * - 只有一个字段:按单字段解析(兼容历史数据) * - 都没有:返回 invalid */ toBookingDateTime(date, time) { if (!date && !time) return (0, import_dayjs.default)(""); if (date && time) return (0, import_dayjs.default)(`${date} ${time}`); return (0, import_dayjs.default)(date || time); } /** * 预约状态映射(面向 UI 的统一状态) * 说明:locked 逻辑后续会补业务规则,这里先保留分支占位。 */ getBookingStatus(appointmentStatus) { if (appointmentStatus === "started") return "occupied"; if (appointmentStatus === "new" || appointmentStatus === "confirmed" || appointmentStatus === "arrived") { return "reserved"; } if (appointmentStatus === "locked") return "locked"; return void 0; } getResourceId(resource) { return resource.id ?? resource.form_record_id ?? ""; } /** * 资源下 bookings 的返回裁剪策略: * 1) 优先返回当前时刻正在进行中的预约(允许并发返回多条) * 2) 若无 active,返回已超时且仍占用中的预约(允许并发多条) * 3) 若仍无,返回最近一组未来预约(同 start_time 的并发预约全部返回) * 4) 最后兜底返回一条(避免资源有历史预约时完全无反馈) */ pickBookingsForCurrentPoint(current, bookings) { if (bookings.length === 0) return []; const currentDayStart = current.startOf("day"); const currentDayEnd = current.endOf("day"); const dayScopedBookings = bookings.filter((booking) => { const startAt = this.toBookingDateTime(booking.start_date, booking.start_time); const endAt = this.toBookingDateTime(booking.end_date, booking.end_time); if (!startAt.isValid() || !endAt.isValid()) return false; return this.isSameOrBefore(startAt, currentDayEnd) && this.isSameOrAfter(endAt, currentDayStart); }); if (dayScopedBookings.length === 0) return []; const appendNextStartGroupIfNeeded = (selectedBookings) => { if (selectedBookings.length === 0) return selectedBookings; const shouldAppendNextGroup = selectedBookings.some( (booking) => booking.status === "reserved" || booking.status === "occupied" ); if (!shouldAppendNextGroup) return selectedBookings; const selectedStartValues = new Set( selectedBookings.map((booking) => this.toBookingDateTime(booking.start_date, booking.start_time)).filter((startAt) => startAt.isValid()).map((startAt) => startAt.valueOf()) ); const futureCandidates = dayScopedBookings.filter((booking) => { const startAt = this.toBookingDateTime(booking.start_date, booking.start_time); if (!startAt.isValid()) return false; return startAt.isAfter(current) && !selectedStartValues.has(startAt.valueOf()); }); if (futureCandidates.length === 0) return selectedBookings; const nextStartAt = this.toBookingDateTime( futureCandidates[0].start_date, futureCandidates[0].start_time ).valueOf(); const nextStartGroup = futureCandidates.filter((booking) => { const startAt = this.toBookingDateTime(booking.start_date, booking.start_time); return startAt.isValid() && startAt.valueOf() === nextStartAt; }); if (nextStartGroup.length === 0) return selectedBookings; return [...selectedBookings, ...nextStartGroup]; }; const activeBookings = dayScopedBookings.filter((booking) => { const startAt = this.toBookingDateTime(booking.start_date, booking.start_time); const endAt = this.toBookingDateTime(booking.end_date, booking.end_time); if (!startAt.isValid() || !endAt.isValid()) return false; return this.isSameOrAfter(current, startAt) && this.isSameOrBefore(current, endAt); }); if (activeBookings.length > 0) return appendNextStartGroupIfNeeded(activeBookings); const timeoutOccupied = dayScopedBookings.filter( (booking) => booking.status === "occupied" && booking.isTimeout ); if (timeoutOccupied.length > 0) return appendNextStartGroupIfNeeded(timeoutOccupied); const upcoming = dayScopedBookings.filter((booking) => { const startAt = this.toBookingDateTime(booking.start_date, booking.start_time); return startAt.isValid() && startAt.isAfter(current) && startAt.isSame(current, "day"); }); if (upcoming.length > 0) { const firstStartAt = this.toBookingDateTime(upcoming[0].start_date, upcoming[0].start_time).valueOf(); const upcomingStartGroup = upcoming.filter((booking) => { const startAt = this.toBookingDateTime(booking.start_date, booking.start_time); return startAt.valueOf() === firstStartAt; }); return appendNextStartGroupIfNeeded(upcomingStartGroup); } const fallback = dayScopedBookings[dayScopedBookings.length - 1]; return fallback ? [fallback] : []; } /** * 标准化单条 booking: * - 过滤终态(rejected/cancelled/completed) * - 注入 status / isTimeout / reserved_status */ normalizeMatchedBooking(current, booking) { const appointmentStatus = String(booking.appointment_status ?? booking.status ?? ""); if (appointmentStatus === "rejected" || appointmentStatus === "cancelled" || appointmentStatus === "completed") { return null; } const bookingStatus = this.getBookingStatus(appointmentStatus); const endAt = this.toBookingDateTime(booking.end_date, booking.end_time); const startAt = this.toBookingDateTime(booking.start_date, booking.start_time); const isTimeout = bookingStatus === "occupied" && endAt.isValid() && current.isAfter(endAt); const timeoutTime = isTimeout ? current.diff(endAt, "minute") : void 0; const progressPercent = (() => { if (bookingStatus !== "occupied") return 0; if (!startAt.isValid() || !endAt.isValid()) return 0; const totalMinutes = endAt.diff(startAt, "minute"); if (totalMinutes <= 0) return 0; const elapsedMinutes = current.diff(startAt, "minute"); if (elapsedMinutes <= 0) return 0; if (elapsedMinutes >= totalMinutes) return 100; return Math.floor(elapsedMinutes / totalMinutes * 100); })(); let reservedStatus; let lateTime; let remainingReserveTime; if (bookingStatus === "reserved" && startAt.isValid()) { if (current.isBefore(startAt)) { reservedStatus = "not_arrived"; remainingReserveTime = startAt.diff(current, "minute"); } else { reservedStatus = "late"; lateTime = Math.max(current.diff(startAt, "minute"), 0); } } return { ...booking, status: bookingStatus, isTimeout, timeoutTime, progressPercent, lateTime, reserved_status: reservedStatus, remainingReserveTime }; } async getResourceBookingList(currentTime, bookingList = []) { var _a; const current = (0, import_dayjs.default)(currentTime); if (!current.isValid()) return []; const resourceResponse = await this.request.get( "/shop/form/resource/page", { skip: 1, num: 999 }, // @ts-ignore { osServer: true, isShopApi: true } ); const resourceList = ((_a = resourceResponse == null ? void 0 : resourceResponse.data) == null ? void 0 : _a.list) ?? []; if (!Array.isArray(resourceList) || resourceList.length === 0) return []; const normalizedBookings = bookingList.map((booking) => this.normalizeMatchedBooking(current, booking)).filter((booking) => Boolean(booking)).sort((left, right) => { const leftStartAt = this.toBookingDateTime(left.start_date, left.start_time).valueOf(); const rightStartAt = this.toBookingDateTime(right.start_date, right.start_time).valueOf(); return leftStartAt - rightStartAt; }); const bookingMap = /* @__PURE__ */ new Map(); normalizedBookings.forEach((booking) => { if (!Array.isArray(booking.resources)) return; booking.resources.forEach((resource) => { const relationId = resource == null ? void 0 : resource.relation_id; if (relationId === void 0 || relationId === null) return; const key = String(relationId); const list = bookingMap.get(key) || []; list.push(booking); bookingMap.set(key, list); }); }); return resourceList.map((resource) => { const resourceId = this.getResourceId(resource); const matchedBookings = bookingMap.get(String(resourceId)) || []; const bookings = this.pickBookingsForCurrentPoint(current, matchedBookings); return { ...resource, resource_id: resourceId, bookings }; }); } }; var Sales_default = SalesImpl; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Sales, SalesImpl, ...require("./types") });