<!-- 图片生成页的大图查看器，负责图片切换、标记、作为参考图和下载。 -->
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { ArrowLeft, Download, Image, Pencil, Plus, Trash2, X } from "@/icons";
import { creativeApi } from "@/api/creative";
import { downloadMedia } from "@/utils/download";
import { useToastStore } from "@/stores/toast";

export interface ProductViewerImage {
  id: string;
  mediaId?: string;
  url: string;
  prompt?: string;
  filename?: string;
}
const props = defineProps<{ modelValue: boolean; images: ProductViewerImage[]; initialId?: string; referenceBusy?: boolean }>();
const emit = defineEmits<{ "update:modelValue": [boolean]; reference: [File] }>();
const toast = useToastStore();
const index = ref(0);
const imageUrl = ref("");
const image = ref<HTMLImageElement>();
const canvas = ref<HTMLCanvasElement>();
const editing = ref(false);
const tool = ref<"brush" | "rect" | "arrow">("brush");
const color = ref("#ef5b50");
const size = ref(4);
const drawing = ref(false);
const start = ref({ x: 0, y: 0 });
const beforeShape = ref<ImageData>();
const history = ref<string[]>([""]);
const historyIndex = ref(0);
const hasMarks = ref(false);
let objectUrl = "";
const current = computed(() => props.images[index.value]);

function close() { emit("update:modelValue", false); }
function reset() {
  const found = props.images.findIndex((item) => item.id === props.initialId);
  index.value = found >= 0 ? found : 0;
  editing.value = false; history.value = [""]; historyIndex.value = 0; hasMarks.value = false;
  void loadImage();
}
async function loadImage() {
  if (objectUrl) URL.revokeObjectURL(objectUrl);
  objectUrl = ""; imageUrl.value = "";
  const source = current.value;
  if (!source) return;
  try {
    if (source.mediaId) { const blob = await creativeApi.media(source.mediaId); objectUrl = URL.createObjectURL(blob); imageUrl.value = objectUrl; }
    else imageUrl.value = source.url;
  } catch { imageUrl.value = source.url; toast.error("图片读取失败"); }
}
function resizeCanvas() {
  if (!image.value || !canvas.value) return;
  canvas.value.width = image.value.clientWidth; canvas.value.height = image.value.clientHeight;
  hasMarks.value = false;
}
function context() { const ctx = canvas.value?.getContext("2d"); if (!ctx) return null; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = color.value; ctx.lineWidth = size.value; return ctx; }
function point(event: PointerEvent) { const rect = canvas.value!.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top }; }
function pointerDown(event: PointerEvent) { if (!editing.value || !canvas.value) return; start.value = point(event); drawing.value = true; beforeShape.value = context()?.getImageData(0, 0, canvas.value.width, canvas.value.height); canvas.value.setPointerCapture(event.pointerId); if (tool.value === "brush") { const ctx = context(); ctx?.beginPath(); ctx?.moveTo(start.value.x, start.value.y); } }
function pointerMove(event: PointerEvent) { if (!drawing.value) return; const ctx = context(); if (!ctx || !canvas.value) return; const end = point(event); if (tool.value === "brush") { ctx.lineTo(end.x, end.y); ctx.stroke(); return; } if (beforeShape.value) ctx.putImageData(beforeShape.value, 0, 0); if (tool.value === "rect") ctx.strokeRect(start.value.x, start.value.y, end.x - start.value.x, end.y - start.value.y); else drawArrow(ctx, start.value.x, start.value.y, end.x, end.y); }
function pointerUp(event: PointerEvent) { if (!drawing.value) return; pointerMove(event); drawing.value = false; beforeShape.value = undefined; commitHistory(); }
function drawArrow(ctx: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number) { const angle = Math.atan2(y2 - y1, x2 - x1); const head = Math.max(10, size.value * 3); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x2 - head * Math.cos(angle - Math.PI / 6), y2 - head * Math.sin(angle - Math.PI / 6)); ctx.moveTo(x2, y2); ctx.lineTo(x2 - head * Math.cos(angle + Math.PI / 6), y2 - head * Math.sin(angle + Math.PI / 6)); ctx.stroke(); }
function empty() { const data = canvas.value?.getContext("2d")?.getImageData(0, 0, canvas.value!.width, canvas.value!.height).data; return !data || !data.some((value) => value !== 0); }
function syncMarks() { hasMarks.value = !empty(); }
function commitHistory() { if (!canvas.value) return; history.value = history.value.slice(0, historyIndex.value + 1); history.value.push(canvas.value.toDataURL()); historyIndex.value = history.value.length - 1; syncMarks(); }
function restore(data: string) { const layer = canvas.value; const ctx = layer?.getContext("2d"); if (!layer || !ctx) return; ctx.clearRect(0, 0, layer.width, layer.height); if (!data) { syncMarks(); return; } const saved = new Image(); saved.onload = () => { ctx.drawImage(saved, 0, 0, layer.width, layer.height); syncMarks(); }; saved.src = data; }
function undo() { if (historyIndex.value > 0) { historyIndex.value--; restore(history.value[historyIndex.value]); } }
function redo() { if (historyIndex.value < history.value.length - 1) { historyIndex.value++; restore(history.value[historyIndex.value]); } }
function clearMarks() { if (!canvas.value || empty()) return; canvas.value.getContext("2d")?.clearRect(0, 0, canvas.value.width, canvas.value.height); commitHistory(); }
function move(delta: number) { index.value = (index.value + delta + props.images.length) % props.images.length; editing.value = false; history.value = [""]; historyIndex.value = 0; hasMarks.value = false; void loadImage(); }
function composeFile() { return new Promise<File>((resolve, reject) => { if (!image.value || !canvas.value) return reject(new Error("图片尚未加载完成")); const output = document.createElement("canvas"); output.width = image.value.naturalWidth; output.height = image.value.naturalHeight; const ctx = output.getContext("2d"); if (!ctx) return reject(new Error("图片编辑器初始化失败")); try { ctx.drawImage(image.value, 0, 0, output.width, output.height); ctx.drawImage(canvas.value, 0, 0, output.width, output.height); } catch { return reject(new Error("图片读取失败")); } output.toBlob((blob) => blob ? resolve(new File([blob], `reference-${current.value.id}.png`, { type: "image/png" })) : reject(new Error("图片合成失败")), "image/png"); }); }
async function makeReference() { if (!current.value || props.referenceBusy) return; try { emit("reference", await composeFile()); } catch (error) { toast.error(error instanceof Error ? error.message : "图片读取失败"); } }
async function download() { if (!current.value) return; try { await downloadMedia(current.value.mediaId, current.value.url, current.value.filename || `图片生成-${current.value.id}.png`); } catch { toast.error("下载失败"); } }
watch(() => props.modelValue, (value) => { if (value) { reset(); void nextTick(resizeCanvas); } });
watch(() => props.initialId, () => { if (props.modelValue) reset(); });
onBeforeUnmount(() => { if (objectUrl) URL.revokeObjectURL(objectUrl); });
</script>

<template>
  <Teleport to="body">
    <div v-if="modelValue && current" class="product-image-viewer" @click.self="close">
      <header><strong>图片生成</strong><span>{{ index + 1 }} / {{ images.length }}</span><button type="button" @click="close"><X :size="22" /></button></header>
      <button v-if="images.length > 1" class="nav prev" type="button" @click="move(-1)"><ArrowLeft :size="24" /></button>
      <main><div class="image-stack"><img ref="image" :src="imageUrl" alt="大图预览" @load="resizeCanvas" /><canvas ref="canvas" :class="{ editing }" @pointerdown="pointerDown" @pointermove="pointerMove" @pointerup="pointerUp" @pointercancel="pointerUp" /></div></main>
      <button v-if="images.length > 1" class="nav next" type="button" @click="move(1)"><ArrowLeft class="next-icon" :size="24" /></button>
      <div v-if="editing" class="mark-toolbar"><button type="button" :class="{ active: tool === 'brush' }" @click="tool = 'brush'"><Pencil :size="17" /></button><button type="button" :class="{ active: tool === 'rect' }" @click="tool = 'rect'"><Plus :size="17" /></button><button type="button" :class="{ active: tool === 'arrow' }" @click="tool = 'arrow'"><ArrowLeft class="next-icon" :size="17" /></button><input v-model="color" type="color" /><input v-model.number="size" type="range" min="2" max="14" /><button type="button" :disabled="historyIndex === 0" @click="undo"><ArrowLeft :size="17" /></button><button type="button" :disabled="historyIndex >= history.length - 1" @click="redo"><ArrowLeft class="next-icon" :size="17" /></button><button type="button" @click="clearMarks"><Trash2 :size="17" /></button></div>
      <footer><button type="button" :class="{ active: editing }" @click="editing = !editing"><Pencil :size="17" />标记</button><button type="button" :disabled="referenceBusy" @click="makeReference"><Image :size="17" />{{ referenceBusy ? '处理中' : '作为参考图' }}</button><button type="button" @click="download"><Download :size="17" />下载</button></footer>
    </div>
  </Teleport>
</template>

<style scoped>
.next-icon{transform:rotate(180deg)}
.product-image-viewer{position:fixed;inset:0;z-index:3000;display:grid;grid-template-rows:58px minmax(0,1fr) 78px;color:var(--web-text-primary);background:var(--web-media-overlay)}
header{display:flex;align-items:center;justify-content:space-between;padding:0 22px}header span{margin-left:10px;color:#e8eef4;font-size:12px}header button,.nav{display:inline-flex;align-items:center;justify-content:center;color:#e8eef4;background:rgba(25,31,36,.82);border:1px solid rgba(255,255,255,.45);border-radius:8px;cursor:pointer}header button{width:36px;height:36px}
main{display:flex;align-items:center;justify-content:center;overflow:hidden}.image-stack{position:relative;max-width:calc(100vw - 150px);max-height:calc(100vh - 190px)}.image-stack img{display:block;max-width:100%;max-height:calc(100vh - 190px);object-fit:contain;user-select:none}.image-stack canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.image-stack canvas.editing{pointer-events:auto;cursor:crosshair}.nav{position:fixed;top:50%;width:44px;height:58px;transform:translateY(-50%)}.nav.prev{left:22px}.nav.next{right:22px}
.mark-toolbar,footer{position:fixed;left:50%;display:flex;align-items:center;gap:6px;padding:7px;background:rgba(248,251,253,.96);border:1px solid #a9c2d8;border-radius:12px;box-shadow:0 8px 22px rgba(0,0,0,.2);transform:translateX(-50%)}.mark-toolbar{bottom:76px}.mark-toolbar button,footer button{height:38px;padding:0 11px;gap:5px;color:#5b7183;background:#f8fbfd;border:1px solid #b6cbdc;border-radius:8px;font-size:15px}.mark-toolbar button.active,footer button.active{color:#fff;background:#5d8fbb;border-color:#5d8fbb}.mark-toolbar input[type=color]{width:30px;height:30px;padding:0;border:0;border-radius:50%;overflow:hidden}.mark-toolbar input[type=range]{width:88px;accent-color:#5d8fbb}.mark-toolbar button:disabled,footer button:disabled{color:#9aaab6;background:#f1f4f6;opacity:.72;cursor:not-allowed}footer{bottom:12px}button:disabled{cursor:not-allowed}
@media(max-width:700px){.nav{width:36px}.nav.prev{left:8px}.nav.next{right:8px}.image-stack{max-width:calc(100vw - 70px)}.mark-toolbar{max-width:calc(100vw - 20px);overflow:auto}.product-image-viewer footer button{padding:0 10px;font-size:15px}}
</style>
