Add product/channel billing condition editors, place-order additional services, billing simulator, forecast-to-inbound fee delta and margin warnings, insurance-aware claim tips, OMS preset dict seeds, and related AI skills/system pages. Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
/**
|
||
* 表头批量/筛选护栏 + 订单列表默认时间窗(前后端应对齐)
|
||
*/
|
||
import dayjs, { type Dayjs } from 'dayjs'
|
||
|
||
/** 批量单号 / IN 值上限 */
|
||
export const MAX_HEADER_BATCH_VALUES = 500
|
||
|
||
/** 单个筛选值最大字符数 */
|
||
export const MAX_HEADER_FILTER_VALUE_LEN = 128
|
||
|
||
/** filters[] 最大条数(前端组装时裁剪) */
|
||
export const MAX_HEADER_FILTERS = 20
|
||
|
||
/** 订单列表默认仅查近 N 天(无精确单号时) */
|
||
export const ORDER_LIST_DEFAULT_DAYS = 90
|
||
|
||
/** 导出最大行数(与后端一致) */
|
||
export const ORDER_EXPORT_MAX_ROWS = 10000
|
||
|
||
export function clampBatchValues(values: string[], max = MAX_HEADER_BATCH_VALUES): {
|
||
values: string[]
|
||
truncated: boolean
|
||
originalCount: number
|
||
} {
|
||
const unique = Array.from(
|
||
new Set(
|
||
values
|
||
.map((v) => String(v || '').trim().slice(0, MAX_HEADER_FILTER_VALUE_LEN))
|
||
.filter(Boolean),
|
||
),
|
||
)
|
||
if (unique.length <= max) {
|
||
return { values: unique, truncated: false, originalCount: unique.length }
|
||
}
|
||
return {
|
||
values: unique.slice(0, max),
|
||
truncated: true,
|
||
originalCount: unique.length,
|
||
}
|
||
}
|
||
|
||
export function normalizeBatchInputText(text: string, max = MAX_HEADER_BATCH_VALUES): {
|
||
value: string
|
||
truncated: boolean
|
||
originalCount: number
|
||
keptCount: number
|
||
} {
|
||
const lines = String(text || '')
|
||
.split(/[\n\r]+/)
|
||
.flatMap((line) => line.split(/[,,\t;;]/))
|
||
.map((v) => v.trim())
|
||
.filter(Boolean)
|
||
const { values, truncated, originalCount } = clampBatchValues(lines, max)
|
||
return {
|
||
value: values.join(','),
|
||
truncated,
|
||
originalCount,
|
||
keptCount: values.length,
|
||
}
|
||
}
|
||
|
||
export function buildDefaultCreateTimeRange(
|
||
days = ORDER_LIST_DEFAULT_DAYS,
|
||
): [Dayjs, Dayjs] {
|
||
const end = dayjs().endOf('day')
|
||
const start = dayjs()
|
||
.subtract(Math.max(days, 1) - 1, 'day')
|
||
.startOf('day')
|
||
return [start, end]
|
||
}
|
||
|
||
export function formatCreateTimeRangeParam(
|
||
range: (Date | string | Dayjs)[] | undefined | null,
|
||
): { createTimeStart: string; createTimeEnd: string } | null {
|
||
if (!range || range.length < 2 || !range[0] || !range[1]) return null
|
||
return {
|
||
createTimeStart: dayjs(range[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||
createTimeEnd: dayjs(range[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||
}
|
||
}
|
||
|
||
/** 是否与「默认近 N 天」窗口一致(允许差 1 分钟内) */
|
||
export function isDefaultCreateTimeRange(
|
||
range: (Date | string | Dayjs)[] | undefined | null,
|
||
days = ORDER_LIST_DEFAULT_DAYS,
|
||
): boolean {
|
||
if (!range || range.length < 2 || !range[0] || !range[1]) return false
|
||
const [defStart, defEnd] = buildDefaultCreateTimeRange(days)
|
||
const start = dayjs(range[0])
|
||
const end = dayjs(range[1])
|
||
return (
|
||
Math.abs(start.startOf('day').diff(defStart, 'minute')) < 2 &&
|
||
Math.abs(end.endOf('day').diff(defEnd, 'minute')) < 2
|
||
)
|
||
}
|