|
|
@@ -25,6 +25,7 @@ import {
|
|
|
CopyOutlined,
|
|
|
EyeOutlined,
|
|
|
PictureOutlined,
|
|
|
+ PlayCircleOutlined,
|
|
|
MobileOutlined,
|
|
|
} from "@ant-design/icons";
|
|
|
import type { AiOrderV2Item, AiOrderV2Params } from "@/api/aiOrderV2";
|
|
|
@@ -78,6 +79,16 @@ function normalizeResultText(val?: string) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function isVideoUrl(url?: string): boolean {
|
|
|
+ if (!url) return false;
|
|
|
+ try {
|
|
|
+ const pathname = new URL(url).pathname;
|
|
|
+ return /\.(mp4|webm|mov|avi|mkv|flv|m4v|3gp)$/i.test(pathname);
|
|
|
+ } catch {
|
|
|
+ return /\.(mp4|webm|mov|avi|mkv|flv|m4v|3gp)([?#]|$)/i.test(url);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function normalizeUrl(raw: string) {
|
|
|
let val = raw.trim();
|
|
|
val = val.replace(/\\+/g, "");
|
|
|
@@ -116,6 +127,33 @@ function parseResultImages(result?: string): string[] {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+interface VideoItem {
|
|
|
+ url: string;
|
|
|
+ thumbnailUrl?: string;
|
|
|
+}
|
|
|
+
|
|
|
+function parseResultVideos(result?: string): VideoItem[] {
|
|
|
+ if (!result) return [];
|
|
|
+ try {
|
|
|
+ const payload = JSON.parse(result) as Record<string, unknown>;
|
|
|
+ if (!payload || typeof payload !== "object") return [];
|
|
|
+ let videosRaw = payload.videos;
|
|
|
+ if (typeof videosRaw === "string") {
|
|
|
+ videosRaw = JSON.parse(videosRaw) as unknown;
|
|
|
+ }
|
|
|
+ if (!Array.isArray(videosRaw)) return [];
|
|
|
+ return videosRaw
|
|
|
+ .filter((v): v is Record<string, unknown> => v && typeof v === "object" && typeof v.url === "string")
|
|
|
+ .map((v) => ({
|
|
|
+ url: normalizeUrl(v.url as string),
|
|
|
+ thumbnailUrl: typeof v.thumbnailUrl === "string" ? normalizeUrl(v.thumbnailUrl) : undefined,
|
|
|
+ }))
|
|
|
+ .filter((v) => /^https?:\/\//i.test(v.url));
|
|
|
+ } catch {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function CopyableId({
|
|
|
text,
|
|
|
label,
|
|
|
@@ -278,13 +316,30 @@ export function AiOrderV2Page() {
|
|
|
width: 260,
|
|
|
render: (_, r) => (
|
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
|
- <Avatar
|
|
|
- shape="square"
|
|
|
- size={40}
|
|
|
- src={r.styleCoverImg}
|
|
|
- icon={<PictureOutlined />}
|
|
|
- style={{ flexShrink: 0 }}
|
|
|
- />
|
|
|
+ {isVideoUrl(r.styleCoverImg) ? (
|
|
|
+ <div
|
|
|
+ className="relative shrink-0 overflow-hidden rounded bg-black"
|
|
|
+ style={{ width: 40, height: 40 }}
|
|
|
+ >
|
|
|
+ <video
|
|
|
+ src={r.styleCoverImg}
|
|
|
+ muted
|
|
|
+ preload="metadata"
|
|
|
+ className="h-full w-full object-cover"
|
|
|
+ />
|
|
|
+ <div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
|
+ <PlayCircleOutlined className="text-white" style={{ fontSize: 16 }} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <Avatar
|
|
|
+ shape="square"
|
|
|
+ size={40}
|
|
|
+ src={r.styleCoverImg}
|
|
|
+ icon={<PictureOutlined />}
|
|
|
+ style={{ flexShrink: 0 }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
<div className="min-w-0">
|
|
|
<div className="text-sm truncate" title={r.styleName}>
|
|
|
{r.styleName || "—"}
|
|
|
@@ -433,24 +488,46 @@ export function AiOrderV2Page() {
|
|
|
render: (val?: string) => {
|
|
|
if (!showVal(val)) return <span className="text-gray-300">—</span>;
|
|
|
const images = parseResultImages(val);
|
|
|
- if (images.length > 0) {
|
|
|
+ const videos = parseResultVideos(val);
|
|
|
+ if (images.length > 0 || videos.length > 0) {
|
|
|
return (
|
|
|
- <Image.PreviewGroup>
|
|
|
- <div className="flex items-center gap-1">
|
|
|
- {images.slice(0, 3).map((url, idx) => (
|
|
|
- <Image
|
|
|
- key={`${url}-${idx}`}
|
|
|
- src={url}
|
|
|
- width={42}
|
|
|
- height={42}
|
|
|
- style={{ objectFit: "cover", borderRadius: 6 }}
|
|
|
- />
|
|
|
- ))}
|
|
|
- {images.length > 3 ? (
|
|
|
- <Tag style={{ margin: 0, fontSize: 11 }}>+{images.length - 3}</Tag>
|
|
|
- ) : null}
|
|
|
- </div>
|
|
|
- </Image.PreviewGroup>
|
|
|
+ <div className="flex items-center gap-1">
|
|
|
+ {images.length > 0 && (
|
|
|
+ <Image.PreviewGroup>
|
|
|
+ {images.slice(0, 3).map((url, idx) => (
|
|
|
+ <Image
|
|
|
+ key={`img-${idx}`}
|
|
|
+ src={url}
|
|
|
+ width={42}
|
|
|
+ height={42}
|
|
|
+ style={{ objectFit: "cover", borderRadius: 6 }}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </Image.PreviewGroup>
|
|
|
+ )}
|
|
|
+ {videos.slice(0, 2).map((v, idx) => (
|
|
|
+ <a
|
|
|
+ key={`vid-${idx}`}
|
|
|
+ href={v.url}
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="relative block shrink-0 overflow-hidden rounded-md bg-black"
|
|
|
+ style={{ width: 42, height: 42 }}
|
|
|
+ >
|
|
|
+ {v.thumbnailUrl ? (
|
|
|
+ <img src={v.thumbnailUrl} className="h-full w-full object-cover" />
|
|
|
+ ) : (
|
|
|
+ <video src={v.url} muted preload="metadata" className="h-full w-full object-cover" />
|
|
|
+ )}
|
|
|
+ <div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
|
+ <PlayCircleOutlined className="text-white" style={{ fontSize: 16 }} />
|
|
|
+ </div>
|
|
|
+ </a>
|
|
|
+ ))}
|
|
|
+ {images.length + videos.length > 3 ? (
|
|
|
+ <Tag style={{ margin: 0, fontSize: 11 }}>+{images.length + videos.length - 3}</Tag>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
);
|
|
|
}
|
|
|
return (
|
|
|
@@ -672,21 +749,43 @@ export function AiOrderV2Page() {
|
|
|
<Card size="small" title={t("aiOrderV2.fieldResult")} style={{ marginTop: 12 }}>
|
|
|
{showVal(detailData.result) ? (() => {
|
|
|
const images = parseResultImages(detailData.result);
|
|
|
- if (images.length > 0) {
|
|
|
+ const videos = parseResultVideos(detailData.result);
|
|
|
+ if (images.length > 0 || videos.length > 0) {
|
|
|
return (
|
|
|
- <Image.PreviewGroup>
|
|
|
- <div className="flex flex-wrap gap-2">
|
|
|
- {images.map((url, idx) => (
|
|
|
- <Image
|
|
|
- key={`${url}-${idx}`}
|
|
|
- src={url}
|
|
|
- width={120}
|
|
|
- height={120}
|
|
|
- style={{ objectFit: "cover", borderRadius: 8 }}
|
|
|
- />
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </Image.PreviewGroup>
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ {images.length > 0 && (
|
|
|
+ <Image.PreviewGroup>
|
|
|
+ {images.map((url, idx) => (
|
|
|
+ <Image
|
|
|
+ key={`img-${idx}`}
|
|
|
+ src={url}
|
|
|
+ width={120}
|
|
|
+ height={120}
|
|
|
+ style={{ objectFit: "cover", borderRadius: 8 }}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </Image.PreviewGroup>
|
|
|
+ )}
|
|
|
+ {videos.map((v, idx) => (
|
|
|
+ <a
|
|
|
+ key={`vid-${idx}`}
|
|
|
+ href={v.url}
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="group relative block shrink-0 overflow-hidden rounded-lg bg-black"
|
|
|
+ style={{ width: 120, height: 120 }}
|
|
|
+ >
|
|
|
+ {v.thumbnailUrl ? (
|
|
|
+ <img src={v.thumbnailUrl} className="h-full w-full object-cover" />
|
|
|
+ ) : (
|
|
|
+ <video src={v.url} muted preload="metadata" className="h-full w-full object-cover" />
|
|
|
+ )}
|
|
|
+ <div className="absolute inset-0 flex items-center justify-center bg-black/30 transition-colors group-hover:bg-black/40">
|
|
|
+ <PlayCircleOutlined className="text-white" style={{ fontSize: 32 }} />
|
|
|
+ </div>
|
|
|
+ </a>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
);
|
|
|
}
|
|
|
return (
|