mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-12 03:48:55 +08:00
feat(appointment): 预约商品发布改为可视化配置,并接入表单设计器
商家端补齐预约规则、时段库存与系统表单设计;运营端增加预约工单与订单设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
135
seller/src/constants/appointment.js
Normal file
135
seller/src/constants/appointment.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 预约商品(APPOINTMENT_GOODS)— 商家端常量
|
||||
*/
|
||||
export const APPOINTMENT_GOODS_TYPE = "APPOINTMENT_GOODS";
|
||||
|
||||
export const SERVICE_MODE = {
|
||||
IN_STORE: "IN_STORE",
|
||||
ON_SITE: "ON_SITE",
|
||||
};
|
||||
|
||||
export const SERVICE_MODE_LABEL = {
|
||||
IN_STORE: "到店服务",
|
||||
ON_SITE: "上门服务",
|
||||
};
|
||||
|
||||
export const SLOT_DIVISION_MODE = {
|
||||
AUTO: "AUTO",
|
||||
CUSTOM: "CUSTOM",
|
||||
};
|
||||
|
||||
export const BOOKABLE_DATE_MODE = {
|
||||
EVERY_DAY: "EVERY_DAY",
|
||||
CUSTOM: "CUSTOM",
|
||||
};
|
||||
|
||||
export const ADVANCE_MODE = {
|
||||
NONE: "NONE",
|
||||
REQUIRE_HOURS: "REQUIRE_HOURS",
|
||||
};
|
||||
|
||||
export const CANCEL_MODE = {
|
||||
NOT_ALLOWED: "NOT_ALLOWED",
|
||||
ALLOW_BEFORE_HOURS: "ALLOW_BEFORE_HOURS",
|
||||
};
|
||||
|
||||
export const FORM_SUBMIT_MODE = {
|
||||
PER_APPOINTMENT: "PER_APPOINTMENT",
|
||||
PER_ORDER: "PER_ORDER",
|
||||
};
|
||||
|
||||
export const WORK_ORDER_STATUS = {
|
||||
PENDING_CLAIM: "PENDING_CLAIM",
|
||||
WAIT_VERIFY: "WAIT_VERIFY",
|
||||
CLAIMED: "CLAIMED",
|
||||
IN_SERVICE: "IN_SERVICE",
|
||||
SERVICE_ENDED: "SERVICE_ENDED",
|
||||
NO_SHOW: "NO_SHOW",
|
||||
REFUNDED: "REFUNDED",
|
||||
};
|
||||
|
||||
export const WORK_ORDER_STATUS_LABEL = {
|
||||
PENDING_CLAIM: "待领取",
|
||||
WAIT_VERIFY: "待核销",
|
||||
CLAIMED: "已领取",
|
||||
IN_SERVICE: "服务中",
|
||||
SERVICE_ENDED: "服务结束",
|
||||
NO_SHOW: "逾期未到店",
|
||||
REFUNDED: "已退款",
|
||||
};
|
||||
|
||||
export const WEEKDAY_OPTIONS = [
|
||||
{ label: "周一", value: 1 },
|
||||
{ label: "周二", value: 2 },
|
||||
{ label: "周三", value: 3 },
|
||||
{ label: "周四", value: 4 },
|
||||
{ label: "周五", value: 5 },
|
||||
{ label: "周六", value: 6 },
|
||||
{ label: "周日", value: 7 },
|
||||
];
|
||||
|
||||
export function isAppointmentGoods(goodsType) {
|
||||
return goodsType === APPOINTMENT_GOODS_TYPE;
|
||||
}
|
||||
|
||||
export function defaultAppointmentSettings() {
|
||||
return {
|
||||
serviceModes: [SERVICE_MODE.IN_STORE],
|
||||
showRemainingQty: true,
|
||||
bookableDateMode: BOOKABLE_DATE_MODE.EVERY_DAY,
|
||||
bookableDays: 1,
|
||||
bookableCustomStartDate: "",
|
||||
bookableCustomEndDate: "",
|
||||
bookableWeekdays: [1, 2, 3, 4, 5, 6, 7],
|
||||
advanceMode: ADVANCE_MODE.NONE,
|
||||
advanceHours: 1,
|
||||
cancelMode: CANCEL_MODE.ALLOW_BEFORE_HOURS,
|
||||
cancelAheadHours: 2,
|
||||
formSubmitMode: FORM_SUBMIT_MODE.PER_APPOINTMENT,
|
||||
systemFormId: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultSlotDivision() {
|
||||
return {
|
||||
slotDivisionMode: SLOT_DIVISION_MODE.AUTO,
|
||||
serviceDurationMinutes: 60,
|
||||
autoStartTime: "08:00",
|
||||
autoEndTime: "18:00",
|
||||
confirmedSlots: [],
|
||||
customDrafts: [{ startTime: "08:00", endTime: "09:00" }],
|
||||
};
|
||||
}
|
||||
|
||||
export function slotKey(slot) {
|
||||
return `${slot.startTime}-${slot.endTime}`;
|
||||
}
|
||||
|
||||
export function parseFormAnswerGroups(formAnswerJson) {
|
||||
if (!formAnswerJson) return [];
|
||||
let data = formAnswerJson;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const fields = Array.isArray(data?.fields) ? data.fields : [];
|
||||
const answers = Array.isArray(data?.answers) ? data.answers : [];
|
||||
return answers
|
||||
.map((group) => {
|
||||
const map = group && typeof group === "object" && !Array.isArray(group) ? group : {};
|
||||
const source = fields.length
|
||||
? fields
|
||||
: Object.keys(map).map((key) => ({ fieldKey: key, fieldLabel: key }));
|
||||
return source
|
||||
.map((field) => ({
|
||||
fieldKey: field.fieldKey,
|
||||
fieldLabel: field.fieldLabel || field.fieldKey,
|
||||
value: map[field.fieldKey] ?? "",
|
||||
}))
|
||||
.filter((row) => row.value !== "" && row.value != null);
|
||||
})
|
||||
.filter((rows) => rows.length);
|
||||
}
|
||||
@@ -5,14 +5,20 @@
|
||||
* @date 2026-08-02
|
||||
*/
|
||||
export const E_COUPON_GOODS_TYPE = "E_COUPON";
|
||||
export const APPOINTMENT_GOODS_TYPE = "APPOINTMENT_GOODS";
|
||||
|
||||
export function isECoupon(goodsType) {
|
||||
return goodsType === E_COUPON_GOODS_TYPE;
|
||||
}
|
||||
|
||||
export function isAppointmentGoods(goodsType) {
|
||||
return goodsType === APPOINTMENT_GOODS_TYPE;
|
||||
}
|
||||
|
||||
export function goodsTypeLabel(goodsType) {
|
||||
const map = {
|
||||
E_COUPON: "电子卡券",
|
||||
APPOINTMENT_GOODS: "预约商品",
|
||||
VIRTUAL_GOODS: "虚拟商品",
|
||||
PHYSICAL_GOODS: "实物商品",
|
||||
};
|
||||
@@ -21,6 +27,7 @@ export function goodsTypeLabel(goodsType) {
|
||||
|
||||
export function goodsTypeTagType(goodsType) {
|
||||
if (goodsType === E_COUPON_GOODS_TYPE) return "warning";
|
||||
if (goodsType === APPOINTMENT_GOODS_TYPE) return "success";
|
||||
if (goodsType === "VIRTUAL_GOODS") return "info";
|
||||
return "";
|
||||
}
|
||||
|
||||
327
seller/src/constants/systemForm.js
Normal file
327
seller/src/constants/systemForm.js
Normal file
@@ -0,0 +1,327 @@
|
||||
/** 预约系统表单 · 字段类型与 schema(§9.1.1.2) */
|
||||
|
||||
export const FORM_FIELD_TYPE = {
|
||||
CHECKBOX: "CHECKBOX",
|
||||
CITY: "CITY",
|
||||
RADIO: "RADIO",
|
||||
DATE: "DATE",
|
||||
DATE_RANGE: "DATE_RANGE",
|
||||
SELECT: "SELECT",
|
||||
TEXT: "TEXT",
|
||||
TIME: "TIME",
|
||||
TIME_RANGE: "TIME_RANGE",
|
||||
RICH_TEXT: "RICH_TEXT",
|
||||
IMAGE: "IMAGE",
|
||||
};
|
||||
|
||||
export const FORM_FIELD_PALETTE = [
|
||||
{ type: FORM_FIELD_TYPE.CHECKBOX, label: "多选框" },
|
||||
{ type: FORM_FIELD_TYPE.CITY, label: "城市" },
|
||||
{ type: FORM_FIELD_TYPE.RADIO, label: "单选框" },
|
||||
{ type: FORM_FIELD_TYPE.DATE, label: "日期" },
|
||||
{ type: FORM_FIELD_TYPE.DATE_RANGE, label: "日期范围" },
|
||||
{ type: FORM_FIELD_TYPE.SELECT, label: "下拉框" },
|
||||
{ type: FORM_FIELD_TYPE.TEXT, label: "文本框" },
|
||||
{ type: FORM_FIELD_TYPE.TIME, label: "时间" },
|
||||
{ type: FORM_FIELD_TYPE.TIME_RANGE, label: "时间范围" },
|
||||
{ type: FORM_FIELD_TYPE.RICH_TEXT, label: "富文本" },
|
||||
{ type: FORM_FIELD_TYPE.IMAGE, label: "图片" },
|
||||
];
|
||||
|
||||
export const FORM_FIELD_LABEL = Object.fromEntries(
|
||||
FORM_FIELD_PALETTE.map((item) => [item.type, item.label])
|
||||
);
|
||||
|
||||
export const CITY_LEVEL = {
|
||||
PROVINCE_CITY: "PROVINCE_CITY",
|
||||
PROVINCE_CITY_DISTRICT: "PROVINCE_CITY_DISTRICT",
|
||||
PROVINCE_CITY_DISTRICT_STREET: "PROVINCE_CITY_DISTRICT_STREET",
|
||||
};
|
||||
|
||||
export const TEXT_CONTENT_TYPE = {
|
||||
TEXT: "TEXT",
|
||||
MOBILE: "MOBILE",
|
||||
ID_CARD: "ID_CARD",
|
||||
EMAIL: "EMAIL",
|
||||
NUMBER: "NUMBER",
|
||||
};
|
||||
|
||||
export const OPTION_MAX = 20;
|
||||
/** 标题/选项/提示语均最多十字 */
|
||||
export const FIELD_LABEL_MAX = 10;
|
||||
export const OPTION_LABEL_MAX = 10;
|
||||
export const HINT_TEXT_MAX = 10;
|
||||
export const RADIO_OPTION_MAX_LEN = OPTION_LABEL_MAX;
|
||||
|
||||
export function createFieldKey() {
|
||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `f_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function defaultOptions() {
|
||||
return [
|
||||
{ label: "选项一", value: createFieldKey() },
|
||||
{ label: "选项二", value: createFieldKey() },
|
||||
];
|
||||
}
|
||||
|
||||
export function createField(type) {
|
||||
const fieldType = String(type || FORM_FIELD_TYPE.TEXT).toUpperCase();
|
||||
const base = {
|
||||
field_key: createFieldKey(),
|
||||
field_type: fieldType,
|
||||
field_label: FORM_FIELD_LABEL[fieldType] || "文本框",
|
||||
required: true,
|
||||
};
|
||||
switch (fieldType) {
|
||||
case FORM_FIELD_TYPE.CHECKBOX:
|
||||
return { ...base, options: defaultOptions() };
|
||||
case FORM_FIELD_TYPE.RADIO:
|
||||
return {
|
||||
field_key: base.field_key,
|
||||
field_type: fieldType,
|
||||
field_label: "单选框",
|
||||
options: defaultOptions(),
|
||||
default_option_index: 0, // 默认选项一
|
||||
};
|
||||
case FORM_FIELD_TYPE.SELECT:
|
||||
return {
|
||||
...base,
|
||||
field_label: "下拉框",
|
||||
options: defaultOptions(),
|
||||
default_option_index: 0,
|
||||
};
|
||||
case FORM_FIELD_TYPE.CITY:
|
||||
return {
|
||||
...base,
|
||||
field_label: "城市",
|
||||
city_level: CITY_LEVEL.PROVINCE_CITY_DISTRICT_STREET,
|
||||
hint_text: "请选择",
|
||||
};
|
||||
case FORM_FIELD_TYPE.DATE:
|
||||
return {
|
||||
...base,
|
||||
field_label: "日期",
|
||||
default_visibility: "SHOW",
|
||||
default_date_mode: "CURRENT_DATE",
|
||||
default_date: null,
|
||||
hint_text: "请选择",
|
||||
};
|
||||
case FORM_FIELD_TYPE.DATE_RANGE:
|
||||
return {
|
||||
...base,
|
||||
field_label: "日期范围",
|
||||
default_visibility: "SHOW",
|
||||
default_date_mode: "CURRENT_DATE",
|
||||
default_date_start: null,
|
||||
default_date_end: null,
|
||||
hint_text: "请选择",
|
||||
};
|
||||
case FORM_FIELD_TYPE.TIME:
|
||||
return {
|
||||
...base,
|
||||
field_label: "时间",
|
||||
default_visibility: "SHOW",
|
||||
default_time_mode: "CURRENT_TIME",
|
||||
default_time: null,
|
||||
hint_text: "请选择",
|
||||
};
|
||||
case FORM_FIELD_TYPE.TIME_RANGE:
|
||||
return {
|
||||
...base,
|
||||
field_label: "时间范围",
|
||||
default_visibility: "SHOW",
|
||||
default_time_mode: "CURRENT_TIME",
|
||||
default_time_start: null,
|
||||
default_time_end: null,
|
||||
hint_text: "请选择",
|
||||
};
|
||||
case FORM_FIELD_TYPE.TEXT:
|
||||
return {
|
||||
...base,
|
||||
field_label: "文本框",
|
||||
content_type: TEXT_CONTENT_TYPE.TEXT,
|
||||
default_value: "",
|
||||
hint_text: "请填写",
|
||||
};
|
||||
case FORM_FIELD_TYPE.RICH_TEXT:
|
||||
return {
|
||||
field_key: base.field_key,
|
||||
field_type: fieldType,
|
||||
field_label: "富文本",
|
||||
rich_text_content: "",
|
||||
};
|
||||
case FORM_FIELD_TYPE.IMAGE:
|
||||
return {
|
||||
...base,
|
||||
field_label: "上传图片",
|
||||
max_upload_count: 1,
|
||||
};
|
||||
default:
|
||||
return { ...base, field_type: FORM_FIELD_TYPE.TEXT, content_type: TEXT_CONTENT_TYPE.TEXT, hint_text: "请填写" };
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeType(raw) {
|
||||
return String(raw || FORM_FIELD_TYPE.TEXT)
|
||||
.toUpperCase()
|
||||
.replace(/-/g, "_");
|
||||
}
|
||||
|
||||
function normalizeOptions(rawOptions) {
|
||||
if (!Array.isArray(rawOptions) || !rawOptions.length) {
|
||||
return defaultOptions();
|
||||
}
|
||||
return rawOptions.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return { label: item, value: createFieldKey() };
|
||||
}
|
||||
return {
|
||||
label: item?.label ?? item?.name ?? "",
|
||||
value: item?.value || createFieldKey(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeField(raw, index = 0) {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return createField(FORM_FIELD_TYPE.TEXT);
|
||||
}
|
||||
const type = normalizeType(raw.field_type || raw.fieldType || raw.type);
|
||||
const field = createField(type);
|
||||
field.field_key = raw.field_key || raw.fieldKey || raw.id || raw.key || field.field_key;
|
||||
field.field_label = raw.field_label || raw.fieldLabel || raw.label || raw.title || field.field_label;
|
||||
if (raw.required != null) {
|
||||
field.required = !!raw.required;
|
||||
}
|
||||
if (raw.hint_text != null || raw.hintText != null || raw.hint != null || raw.placeholder != null) {
|
||||
field.hint_text = raw.hint_text || raw.hintText || raw.hint || raw.placeholder || "";
|
||||
}
|
||||
if (field.options) {
|
||||
field.options = normalizeOptions(raw.options);
|
||||
}
|
||||
if (raw.default_option_index != null || raw.defaultOptionIndex != null) {
|
||||
field.default_option_index = Number(raw.default_option_index ?? raw.defaultOptionIndex) || 0;
|
||||
}
|
||||
if (raw.city_level || raw.cityLevel) {
|
||||
field.city_level = raw.city_level || raw.cityLevel;
|
||||
}
|
||||
if (raw.content_type || raw.contentType) {
|
||||
field.content_type = raw.content_type || raw.contentType;
|
||||
}
|
||||
if (raw.default_value != null || raw.defaultValue != null) {
|
||||
field.default_value = raw.default_value ?? raw.defaultValue ?? "";
|
||||
}
|
||||
if (raw.default_visibility || raw.defaultVisibility) {
|
||||
field.default_visibility = raw.default_visibility || raw.defaultVisibility;
|
||||
}
|
||||
if (raw.default_date_mode || raw.defaultDateMode) {
|
||||
field.default_date_mode = raw.default_date_mode || raw.defaultDateMode;
|
||||
}
|
||||
if (raw.default_date || raw.defaultDate) {
|
||||
field.default_date = raw.default_date || raw.defaultDate;
|
||||
}
|
||||
if (raw.default_date_start || raw.defaultDateStart) {
|
||||
field.default_date_start = raw.default_date_start || raw.defaultDateStart;
|
||||
}
|
||||
if (raw.default_date_end || raw.defaultDateEnd) {
|
||||
field.default_date_end = raw.default_date_end || raw.defaultDateEnd;
|
||||
}
|
||||
if (raw.default_time_mode || raw.defaultTimeMode) {
|
||||
field.default_time_mode = raw.default_time_mode || raw.defaultTimeMode;
|
||||
}
|
||||
if (raw.default_time || raw.defaultTime) {
|
||||
field.default_time = raw.default_time || raw.defaultTime;
|
||||
}
|
||||
if (raw.default_time_start || raw.defaultTimeStart) {
|
||||
field.default_time_start = raw.default_time_start || raw.defaultTimeStart;
|
||||
}
|
||||
if (raw.default_time_end || raw.defaultTimeEnd) {
|
||||
field.default_time_end = raw.default_time_end || raw.defaultTimeEnd;
|
||||
}
|
||||
if (raw.rich_text_content != null || raw.richTextContent != null || raw.content != null) {
|
||||
field.rich_text_content = raw.rich_text_content || raw.richTextContent || raw.content || "";
|
||||
}
|
||||
if (raw.max_upload_count != null || raw.maxUploadCount != null) {
|
||||
field.max_upload_count = Math.min(10, Math.max(1, Number(raw.max_upload_count ?? raw.maxUploadCount) || 1));
|
||||
}
|
||||
field.sort_order = raw.sort_order ?? raw.sortOrder ?? index;
|
||||
return field;
|
||||
}
|
||||
|
||||
export function parseFormSchema(schemaJson) {
|
||||
if (!schemaJson) {
|
||||
return [];
|
||||
}
|
||||
let data = schemaJson;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const list = Array.isArray(data) ? data : data?.fields || [];
|
||||
return list.map((item, index) => normalizeField(item, index));
|
||||
}
|
||||
|
||||
export function serializeFormSchema(fields) {
|
||||
return JSON.stringify(
|
||||
(fields || []).map((field, index) => ({
|
||||
...field,
|
||||
sort_order: index,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
export function countFormFields(schemaJson) {
|
||||
return parseFormSchema(schemaJson).length;
|
||||
}
|
||||
|
||||
export function addOption(options, { radio } = {}) {
|
||||
const list = [...(options || [])];
|
||||
if (list.length >= OPTION_MAX) {
|
||||
return list;
|
||||
}
|
||||
list.push({
|
||||
label: radio ? "" : `选项${list.length + 1}`,
|
||||
value: createFieldKey(),
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
export function removeOption(options, index) {
|
||||
const list = [...(options || [])];
|
||||
if (index < 2 || list.length <= 2) {
|
||||
return list;
|
||||
}
|
||||
list.splice(index, 1);
|
||||
return list;
|
||||
}
|
||||
|
||||
export function batchAddOptions(options, text, { radio } = {}) {
|
||||
const list = [...(options || [])];
|
||||
const lines = String(text || "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => line.slice(0, OPTION_LABEL_MAX));
|
||||
for (const label of lines) {
|
||||
if (list.length >= OPTION_MAX) {
|
||||
break;
|
||||
}
|
||||
list.push({ label, value: createFieldKey() });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function hasRequiredSwitch(type) {
|
||||
// 单选、富文本不配必填
|
||||
return ![FORM_FIELD_TYPE.RADIO, FORM_FIELD_TYPE.RICH_TEXT].includes(type);
|
||||
}
|
||||
|
||||
export function hasOptions(type) {
|
||||
return [FORM_FIELD_TYPE.CHECKBOX, FORM_FIELD_TYPE.RADIO, FORM_FIELD_TYPE.SELECT].includes(type);
|
||||
}
|
||||
Reference in New Issue
Block a user