@pisell/pisellos
Version:
一个可扩展的前端模块化SDK框架,支持插件系统
417 lines (415 loc) • 18.1 kB
JavaScript
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")
});