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.

218 lines
6.8 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.

/**
* 登录态查价表单视觉 chromeMotherShip / 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>
);
}