|
|
/**
|
|
|
* 登录态查价表单视觉 chrome(MotherShip / Flock 共用)
|
|
|
* 主色仅局部 #1890FF,不改全局 primary token
|
|
|
*/
|
|
|
"use client";
|
|
|
|
|
|
import { useId, useState, type ReactNode } from "react";
|
|
|
import { CaretDown, Info, Warning } from "@phosphor-icons/react";
|
|
|
|
|
|
/** 查价表单局部主色 */
|
|
|
export const QUOTE_ACCENT = "#1890FF";
|
|
|
export const QUOTE_ACCENT_HOVER = "#1580E0";
|
|
|
export const QUOTE_TITLE = "#1F2937";
|
|
|
export const QUOTE_SECTION = "#4B5563";
|
|
|
export const QUOTE_REQUIRED = "#EF4444";
|
|
|
|
|
|
export const quoteInputCls =
|
|
|
"h-10 w-full rounded-md border border-border bg-surface px-3 text-sm text-text-primary transition-colors focus:border-[#1890FF] focus:outline-none focus:ring-2 focus:ring-[#1890FF]/20 disabled:opacity-50";
|
|
|
|
|
|
export const quoteInputErrCls =
|
|
|
"h-10 w-full rounded-md border border-[#EF4444] bg-surface px-3 text-sm text-text-primary focus:border-[#EF4444] focus:outline-none focus:ring-2 focus:ring-[#EF4444]/20 disabled:opacity-50";
|
|
|
|
|
|
export const quoteCtaCls =
|
|
|
"inline-flex h-10 min-w-[48px] items-center justify-center gap-2 rounded-md bg-[#1890FF] px-5 text-sm font-semibold text-white transition-colors hover:bg-[#1580E0] focus:outline-none focus:ring-2 focus:ring-[#1890FF]/30 disabled:cursor-not-allowed disabled:opacity-50";
|
|
|
|
|
|
export const quoteAddRowCls =
|
|
|
"inline-flex h-10 items-center justify-center gap-1.5 rounded-md border border-border bg-surface px-3 text-sm font-medium text-text-primary transition-transform hover:-translate-y-0.5 hover:border-[#1890FF]/40 hover:text-[#1890FF] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:translate-y-0";
|
|
|
|
|
|
export function QuoteRequiredMark() {
|
|
|
return (
|
|
|
<span className="ml-0.5 text-[#EF4444]" aria-hidden>
|
|
|
*
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** ● 步骤1 → ○ 步骤2 → ○ 步骤3 */
|
|
|
export function QuoteStepProgress({ current = 1 }: { current?: 1 | 2 | 3 }) {
|
|
|
const steps = [
|
|
|
{ n: 1 as const, label: "步骤1:填写基础信息" },
|
|
|
{ n: 2 as const, label: "步骤2:确认价格" },
|
|
|
{ n: 3 as const, label: "步骤3:提交" },
|
|
|
];
|
|
|
return (
|
|
|
<nav
|
|
|
aria-label="查价进度"
|
|
|
className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-[#4B5563]"
|
|
|
>
|
|
|
{steps.map((s, i) => {
|
|
|
const active = s.n === current;
|
|
|
const done = s.n < current;
|
|
|
return (
|
|
|
<span key={s.n} className="inline-flex items-center gap-2">
|
|
|
{i > 0 ? (
|
|
|
<span className="text-text-disabled" aria-hidden>
|
|
|
→
|
|
|
</span>
|
|
|
) : null}
|
|
|
<span
|
|
|
className={
|
|
|
active
|
|
|
? "font-semibold text-[#1890FF]"
|
|
|
: done
|
|
|
? "text-[#1F2937]"
|
|
|
: "text-text-disabled"
|
|
|
}
|
|
|
>
|
|
|
<span aria-hidden>{active || done ? "●" : "○"} </span>
|
|
|
{s.label}
|
|
|
</span>
|
|
|
</span>
|
|
|
);
|
|
|
})}
|
|
|
</nav>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export function QuotePageTitle({ children }: { children: ReactNode }) {
|
|
|
return (
|
|
|
<h3 className="text-[20px] font-bold leading-snug tracking-tight text-[#1F2937]">
|
|
|
{children}
|
|
|
</h3>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export function QuoteSectionTitle({ children }: { children: ReactNode }) {
|
|
|
return (
|
|
|
<h4 className="border-l-[3px] border-[#1890FF] pl-2 text-[16px] font-semibold leading-6 text-[#4B5563]">
|
|
|
{children}
|
|
|
</h4>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** ⓘ 悬停 Tooltip(全程 span,可放在 label/行内;tip 勿传 div/p) */
|
|
|
export function QuoteFieldTooltip({
|
|
|
tip,
|
|
|
label = "说明",
|
|
|
wide = false,
|
|
|
position = "above",
|
|
|
}: {
|
|
|
tip: ReactNode;
|
|
|
label?: string;
|
|
|
/** 长文案加宽 */
|
|
|
wide?: boolean;
|
|
|
position?: "above" | "below";
|
|
|
}) {
|
|
|
const tipId = useId();
|
|
|
const posCls =
|
|
|
position === "below"
|
|
|
? "top-full left-0 mt-1.5 translate-x-0"
|
|
|
: "bottom-full left-1/2 mb-1.5 -translate-x-1/2";
|
|
|
const widthCls = wide
|
|
|
? "w-[min(22rem,calc(100vw-2rem))]"
|
|
|
: "w-56";
|
|
|
return (
|
|
|
<span className="group relative inline-flex align-middle">
|
|
|
<span
|
|
|
tabIndex={0}
|
|
|
aria-describedby={tipId}
|
|
|
aria-label={label}
|
|
|
className="inline-flex cursor-help text-text-disabled outline-none hover:text-[#1890FF] focus-visible:text-[#1890FF]"
|
|
|
onClick={(e) => {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
}}
|
|
|
>
|
|
|
<Info size={14} weight="regular" aria-hidden />
|
|
|
</span>
|
|
|
<span
|
|
|
id={tipId}
|
|
|
role="tooltip"
|
|
|
className={[
|
|
|
"pointer-events-none absolute z-40 hidden max-h-[min(70vh,28rem)] overflow-y-auto rounded-md bg-[#1F2937] px-3 py-2 text-left text-[11px] leading-relaxed text-white shadow-modal group-hover:block group-focus-within:block",
|
|
|
posCls,
|
|
|
widthCls,
|
|
|
].join(" ")}
|
|
|
>
|
|
|
{tip}
|
|
|
</span>
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** Tooltip 内:加粗标题 + 说明列表(仅用 span,避免嵌套 p/div 破坏外层 p) */
|
|
|
export function QuoteTipDefs({
|
|
|
intro,
|
|
|
items,
|
|
|
}: {
|
|
|
intro?: string;
|
|
|
items: ReadonlyArray<{ title: string; body: string }>;
|
|
|
}) {
|
|
|
return (
|
|
|
<span className="block space-y-2">
|
|
|
{intro ? (
|
|
|
<span className="block italic text-white/90">{intro}</span>
|
|
|
) : null}
|
|
|
{items.map((it) => (
|
|
|
<span key={it.title} className="block">
|
|
|
<span className="font-semibold">{it.title}</span>
|
|
|
<span className="text-white/90"> {it.body}</span>
|
|
|
</span>
|
|
|
))}
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 可展开「注意事项」 */
|
|
|
export function QuoteNoticeCollapse({
|
|
|
title = "注意事项",
|
|
|
children,
|
|
|
defaultOpen = false,
|
|
|
}: {
|
|
|
title?: string;
|
|
|
children: ReactNode;
|
|
|
defaultOpen?: boolean;
|
|
|
}) {
|
|
|
const [open, setOpen] = useState(defaultOpen);
|
|
|
return (
|
|
|
<div className="rounded-md border border-warning/30 bg-warning/5">
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={() => setOpen((v) => !v)}
|
|
|
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-xs font-medium text-[#4B5563]"
|
|
|
>
|
|
|
<span className="inline-flex items-center gap-1.5">
|
|
|
<Warning size={14} className="shrink-0 text-warning" weight="fill" />
|
|
|
{title}
|
|
|
</span>
|
|
|
<CaretDown
|
|
|
size={14}
|
|
|
className={[
|
|
|
"shrink-0 text-text-secondary transition-transform",
|
|
|
open ? "rotate-180" : "",
|
|
|
].join(" ")}
|
|
|
/>
|
|
|
</button>
|
|
|
{open ? (
|
|
|
<div className="border-t border-warning/20 px-3 py-2 text-xs leading-relaxed text-text-secondary">
|
|
|
{children}
|
|
|
</div>
|
|
|
) : null}
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 校验失败浮动提示 */
|
|
|
export function QuoteFloatHint({ message }: { message: string | null }) {
|
|
|
if (!message) return null;
|
|
|
return (
|
|
|
<p
|
|
|
role="alert"
|
|
|
className="rounded-md border border-[#EF4444]/30 bg-[#EF4444]/5 px-3 py-2 text-sm text-[#EF4444] shadow-card"
|
|
|
>
|
|
|
{message}
|
|
|
</p>
|
|
|
);
|
|
|
}
|