|
|
"use client";
|
|
|
|
|
|
import {
|
|
|
useCallback,
|
|
|
useEffect,
|
|
|
useMemo,
|
|
|
useRef,
|
|
|
useState,
|
|
|
type KeyboardEvent,
|
|
|
type MouseEvent,
|
|
|
} from "react";
|
|
|
import { CaretDown, FloppyDisk, X } from "@phosphor-icons/react";
|
|
|
import {
|
|
|
filterFlockSavedFreight,
|
|
|
loadFlockSavedFreight,
|
|
|
persistFlockSavedFreight,
|
|
|
removeFlockSavedFreight,
|
|
|
upsertFlockSavedFreight,
|
|
|
buildFlockSavedFreightPreset,
|
|
|
type FlockSavedFreightPreset,
|
|
|
type FlockSavedFreightSource,
|
|
|
} from "@/lib/flock/saved-freight-presets";
|
|
|
import { quoteInputCls } from "@/components/quote/quote-form-chrome";
|
|
|
|
|
|
const inputCls = quoteInputCls;
|
|
|
|
|
|
type Props = {
|
|
|
customerId: string;
|
|
|
disabled?: boolean;
|
|
|
/** 保存后递增,触发搜索列表刷新 */
|
|
|
revision?: number;
|
|
|
};
|
|
|
|
|
|
/** 顶部:搜索并选用常用货件 */
|
|
|
export function FlockSavedFreightSearch({
|
|
|
customerId,
|
|
|
disabled,
|
|
|
revision = 0,
|
|
|
onSelect,
|
|
|
}: Props & { onSelect: (preset: FlockSavedFreightPreset) => void }) {
|
|
|
const [presets, setPresets] = useState<FlockSavedFreightPreset[]>([]);
|
|
|
const [query, setQuery] = useState("");
|
|
|
const [open, setOpen] = useState(false);
|
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
const reload = useCallback(() => {
|
|
|
setPresets(loadFlockSavedFreight(customerId));
|
|
|
}, [customerId]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
reload();
|
|
|
}, [reload, revision]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
const onDoc = (e: globalThis.MouseEvent) => {
|
|
|
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
|
|
|
};
|
|
|
document.addEventListener("mousedown", onDoc);
|
|
|
return () => document.removeEventListener("mousedown", onDoc);
|
|
|
}, []);
|
|
|
|
|
|
const filtered = useMemo(
|
|
|
() => filterFlockSavedFreight(presets, query),
|
|
|
[presets, query],
|
|
|
);
|
|
|
|
|
|
const remove = (id: string, e: MouseEvent | KeyboardEvent) => {
|
|
|
e.stopPropagation();
|
|
|
e.preventDefault();
|
|
|
const next = removeFlockSavedFreight(presets, id);
|
|
|
persistFlockSavedFreight(customerId, next);
|
|
|
setPresets(next);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<div ref={rootRef} className="relative">
|
|
|
<input
|
|
|
disabled={disabled}
|
|
|
value={query}
|
|
|
onChange={(e) => {
|
|
|
setQuery(e.target.value);
|
|
|
setOpen(true);
|
|
|
}}
|
|
|
onFocus={() => setOpen(true)}
|
|
|
placeholder={
|
|
|
presets.length ? "搜索已保存的货件名称或描述" : "暂无常用货件,请先在货品描述处保存"
|
|
|
}
|
|
|
className={`${inputCls} pr-8`}
|
|
|
autoComplete="off"
|
|
|
/>
|
|
|
<CaretDown
|
|
|
size={14}
|
|
|
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-text-disabled"
|
|
|
/>
|
|
|
{open ? (
|
|
|
<ul className="absolute z-20 mt-1 max-h-56 w-full overflow-auto rounded-md border border-border bg-surface py-1 shadow-lg">
|
|
|
{filtered.length === 0 ? (
|
|
|
<li className="px-3 py-2 text-xs text-text-secondary">
|
|
|
{presets.length === 0
|
|
|
? "尚无保存记录"
|
|
|
: "无匹配货件"}
|
|
|
</li>
|
|
|
) : (
|
|
|
filtered.map((p) => (
|
|
|
<li key={p.id}>
|
|
|
<button
|
|
|
type="button"
|
|
|
disabled={disabled}
|
|
|
className="flex w-full items-start gap-2 px-3 py-2 text-left text-sm hover:bg-bg disabled:opacity-50"
|
|
|
onClick={() => {
|
|
|
onSelect(p);
|
|
|
setQuery(p.name);
|
|
|
setOpen(false);
|
|
|
}}
|
|
|
>
|
|
|
<span className="min-w-0 flex-1">
|
|
|
<span className="block font-medium text-text-primary">
|
|
|
{p.name}
|
|
|
</span>
|
|
|
{p.description && p.description !== p.name ? (
|
|
|
<span className="mt-0.5 block truncate text-xs text-text-secondary">
|
|
|
{p.description}
|
|
|
</span>
|
|
|
) : null}
|
|
|
</span>
|
|
|
<span
|
|
|
role="button"
|
|
|
tabIndex={0}
|
|
|
aria-label={`删除常用货件 ${p.name}`}
|
|
|
className="mt-0.5 shrink-0 rounded p-0.5 text-text-secondary hover:bg-[#FEE2E2] hover:text-[#EF4444]"
|
|
|
onClick={(e) => remove(p.id, e)}
|
|
|
onKeyDown={(e) => {
|
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
|
remove(p.id, e);
|
|
|
}
|
|
|
}}
|
|
|
>
|
|
|
<X size={14} />
|
|
|
</span>
|
|
|
</button>
|
|
|
</li>
|
|
|
))
|
|
|
)}
|
|
|
</ul>
|
|
|
) : null}
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 货品描述旁:保存当前货物行为常用货件 */
|
|
|
export function FlockSaveFreightButton({
|
|
|
customerId,
|
|
|
disabled,
|
|
|
source,
|
|
|
onSaved,
|
|
|
}: Props & {
|
|
|
source: FlockSavedFreightSource;
|
|
|
onSaved?: () => void;
|
|
|
}) {
|
|
|
const [open, setOpen] = useState(false);
|
|
|
const [name, setName] = useState("");
|
|
|
const [hint, setHint] = useState<string | null>(null);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (open) {
|
|
|
setName(source.description.trim());
|
|
|
setHint(null);
|
|
|
}
|
|
|
}, [open, source.description]);
|
|
|
|
|
|
const save = () => {
|
|
|
const preset = buildFlockSavedFreightPreset(source, name);
|
|
|
if (!preset) {
|
|
|
setHint("请先填写货品描述或货件名称");
|
|
|
return;
|
|
|
}
|
|
|
const list = loadFlockSavedFreight(customerId);
|
|
|
const next = upsertFlockSavedFreight(list, preset);
|
|
|
persistFlockSavedFreight(customerId, next);
|
|
|
setOpen(false);
|
|
|
setHint("已保存到常用货件");
|
|
|
onSaved?.();
|
|
|
window.setTimeout(() => setHint(null), 2000);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<div className="mt-2 space-y-2">
|
|
|
{!open ? (
|
|
|
<button
|
|
|
type="button"
|
|
|
disabled={disabled}
|
|
|
onClick={() => setOpen(true)}
|
|
|
className="inline-flex items-center gap-1 text-xs font-medium text-[#1890FF] hover:underline disabled:opacity-50"
|
|
|
>
|
|
|
<FloppyDisk size={14} />
|
|
|
存为常用货件
|
|
|
</button>
|
|
|
) : (
|
|
|
<div className="rounded-md border border-border bg-bg px-3 py-2">
|
|
|
<p className="mb-1.5 text-xs text-[#4B5563]">货件名称(便于搜索)</p>
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
<input
|
|
|
disabled={disabled}
|
|
|
value={name}
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
className={`${inputCls} !h-9 min-w-[12rem] flex-1 text-sm`}
|
|
|
placeholder="如:标准木托家电"
|
|
|
/>
|
|
|
<button
|
|
|
type="button"
|
|
|
disabled={disabled}
|
|
|
onClick={save}
|
|
|
className="h-9 rounded-md bg-[#1890FF] px-3 text-xs font-medium text-white hover:opacity-90 disabled:opacity-50"
|
|
|
>
|
|
|
保存
|
|
|
</button>
|
|
|
<button
|
|
|
type="button"
|
|
|
disabled={disabled}
|
|
|
onClick={() => setOpen(false)}
|
|
|
className="h-9 rounded-md px-2 text-xs text-text-secondary hover:bg-surface"
|
|
|
>
|
|
|
取消
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
{hint ? <p className="text-xs text-[#16A34A]">{hint}</p> : null}
|
|
|
</div>
|
|
|
);
|
|
|
}
|