You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/components/flock/flock-saved-freight-control...

232 lines
7.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"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>
);
}