Unverified Commit 5ffdbeb6 authored by LiangLiu's avatar LiangLiu Committed by GitHub
Browse files

deploy update (#355)

1、frontend vue+vite
2、share task & template
3、x264 rtc stream push
parent 39683e24
FROM lightx2v/lightx2v:25092201-cu128 AS base FROM node:alpine3.21 AS frontend_builder
COPY lightx2v /opt/lightx2v
RUN cd /opt/lightx2v/deploy/server/frontend \
&& npm install \
&& npm run build
FROM lightx2v:25092201-cu128 AS base
RUN mkdir /workspace/LightX2V RUN mkdir /workspace/LightX2V
WORKDIR /workspace/LightX2V WORKDIR /workspace/LightX2V
...@@ -8,3 +15,5 @@ COPY assets assets ...@@ -8,3 +15,5 @@ COPY assets assets
COPY configs configs COPY configs configs
COPY lightx2v lightx2v COPY lightx2v lightx2v
COPY lightx2v_kernel lightx2v_kernel COPY lightx2v_kernel lightx2v_kernel
COPY --from=frontend_builder /opt/lightx2v/deploy/server/frontend/dist lightx2v/deploy/server/frontend/dist
...@@ -2,6 +2,8 @@ import asyncio ...@@ -2,6 +2,8 @@ import asyncio
import base64 import base64
import io import io
import os import os
import subprocess
import tempfile
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
...@@ -136,21 +138,24 @@ def format_image_data(data, max_size=1280): ...@@ -136,21 +138,24 @@ def format_image_data(data, max_size=1280):
return output.getvalue() return output.getvalue()
def media_to_wav(data):
with tempfile.NamedTemporaryFile() as fin:
fin.write(data)
fin.flush()
cmd = ["ffmpeg", "-i", fin.name, "-f", "wav", "-acodec", "pcm_s16le", "-ar", "44100", "-ac", "2", "pipe:1"]
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
assert p.returncode == 0, f"media to wav failed: {p.stderr.decode()}"
return p.stdout
def format_audio_data(data): def format_audio_data(data):
if len(data) < 4: if len(data) < 4:
raise ValueError("Audio file too short") raise ValueError("Audio file too short")
try: data = media_to_wav(data)
waveform, sample_rate = torchaudio.load(io.BytesIO(data), num_frames=10) waveform, sample_rate = torchaudio.load(io.BytesIO(data), num_frames=10)
logger.info(f"load audio: {waveform.size()}, {sample_rate}") logger.info(f"load audio: {waveform.size()}, {sample_rate}")
assert waveform.size(0) > 0, "audio is empty" assert waveform.numel() > 0, "audio is empty"
assert sample_rate > 0, "audio sample rate is not valid" assert sample_rate > 0, "audio sample rate is not valid"
except Exception as e:
logger.warning(f"torchaudio failed to load audio, trying alternative method: {e}")
# check audio headers
audio_headers = [b"RIFF", b"ID3", b"\xff\xfb", b"\xff\xf3", b"\xff\xf2", b"OggS"]
if not any(data.startswith(header) for header in audio_headers):
logger.warning("Audio file doesn't have recognized header, but continuing...")
logger.info(f"Audio validation passed (alternative method), size: {len(data)} bytes")
return data return data
......
...@@ -97,7 +97,6 @@ class VAReader: ...@@ -97,7 +97,6 @@ class VAReader:
def start_ffmpeg_process_whep(self): def start_ffmpeg_process_whep(self):
"""Start gstream process read audio from stream""" """Start gstream process read audio from stream"""
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg",
"gst-launch-1.0", "gst-launch-1.0",
"-q", "-q",
"whepsrc", "whepsrc",
......
import queue import queue
import random
import signal import signal
import socket import socket
import subprocess import subprocess
...@@ -13,6 +12,12 @@ import torchaudio as ta ...@@ -13,6 +12,12 @@ import torchaudio as ta
from loguru import logger from loguru import logger
def pseudo_random(a, b):
x = str(time.time()).split(".")[1]
y = int(float("0." + x) * 1000000)
return a + (y % (b - a + 1))
class VARecorder: class VARecorder:
def __init__( def __init__(
self, self,
...@@ -23,13 +28,14 @@ class VARecorder: ...@@ -23,13 +28,14 @@ class VARecorder:
self.livestream_url = livestream_url self.livestream_url = livestream_url
self.fps = fps self.fps = fps
self.sample_rate = sample_rate self.sample_rate = sample_rate
self.audio_port = random.choice(range(32000, 40000)) self.audio_port = pseudo_random(32000, 40000)
self.video_port = self.audio_port + 1 self.video_port = self.audio_port + 1
logger.info(f"VARecorder audio port: {self.audio_port}, video port: {self.video_port}") logger.info(f"VARecorder audio port: {self.audio_port}, video port: {self.video_port}")
self.width = None self.width = None
self.height = None self.height = None
self.stoppable_t = None self.stoppable_t = None
self.realtime = True
# ffmpeg process for mix video and audio data and push to livestream # ffmpeg process for mix video and audio data and push to livestream
self.ffmpeg_process = None self.ffmpeg_process = None
...@@ -93,6 +99,7 @@ class VARecorder: ...@@ -93,6 +99,7 @@ class VARecorder:
self.video_conn, _ = self.video_socket.accept() self.video_conn, _ = self.video_socket.accept()
logger.info(f"Video connection established from {self.video_conn.getpeername()}") logger.info(f"Video connection established from {self.video_conn.getpeername()}")
fail_time, max_fail_time = 0, 10 fail_time, max_fail_time = 0, 10
packet_secs = 1.0 / self.fps
while True: while True:
try: try:
if self.video_queue is None: if self.video_queue is None:
...@@ -101,9 +108,18 @@ class VARecorder: ...@@ -101,9 +108,18 @@ class VARecorder:
if data is None: if data is None:
logger.info("Video thread received stop signal") logger.info("Video thread received stop signal")
break break
# Convert to numpy and scale to [0, 255], convert RGB to BGR for OpenCV/FFmpeg # Convert to numpy and scale to [0, 255], convert RGB to BGR for OpenCV/FFmpeg
if not self.realtime:
frames = (data * 255).clamp(0, 255).to(torch.uint8).cpu().numpy() frames = (data * 255).clamp(0, 255).to(torch.uint8).cpu().numpy()
self.video_conn.send(frames.tobytes()) self.video_conn.send(frames.tobytes())
else:
for i in range(data.shape[0]):
t0 = time.time()
frame = (data[i] * 255).clamp(0, 255).to(torch.uint8).cpu().numpy()
self.video_conn.send(frame.tobytes())
time.sleep(max(0, packet_secs - (time.time() - t0)))
fail_time = 0 fail_time = 0
except: # noqa except: # noqa
logger.error(f"Send video data error: {traceback.format_exc()}") logger.error(f"Send video data error: {traceback.format_exc()}")
...@@ -223,12 +239,22 @@ class VARecorder: ...@@ -223,12 +239,22 @@ class VARecorder:
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg", "ffmpeg",
"-re", "-re",
"-fflags",
"nobuffer",
"-analyzeduration",
"0",
"-probesize",
"32",
"-flush_packets",
"1",
"-f", "-f",
"s16le", "s16le",
"-ar", "-ar",
str(self.sample_rate), str(self.sample_rate),
"-ac", "-ac",
"1", "1",
"-ch_layout",
"mono",
"-i", "-i",
f"tcp://127.0.0.1:{self.audio_port}", f"tcp://127.0.0.1:{self.audio_port}",
"-f", "-f",
...@@ -278,6 +304,12 @@ class VARecorder: ...@@ -278,6 +304,12 @@ class VARecorder:
except Exception as e: except Exception as e:
logger.error(f"Failed to start FFmpeg: {e}") logger.error(f"Failed to start FFmpeg: {e}")
def start(self, width: int, height: int):
self.set_video_size(width, height)
duration = 1.0
self.pub_livestream(torch.zeros((int(self.fps * duration), height, width, 3), dtype=torch.float16), torch.zeros(int(self.sample_rate * duration), dtype=torch.float16))
time.sleep(duration)
def set_video_size(self, width: int, height: int): def set_video_size(self, width: int, height: int):
if self.width is not None and self.height is not None: if self.width is not None and self.height is not None:
assert self.width == width and self.height == height, "Video size already set" assert self.width == width and self.height == height, "Video size already set"
...@@ -291,6 +323,7 @@ class VARecorder: ...@@ -291,6 +323,7 @@ class VARecorder:
self.start_ffmpeg_process_whip() self.start_ffmpeg_process_whip()
else: else:
self.start_ffmpeg_process_local() self.start_ffmpeg_process_local()
self.realtime = False
self.audio_thread = threading.Thread(target=self.audio_worker) self.audio_thread = threading.Thread(target=self.audio_worker)
self.video_thread = threading.Thread(target=self.video_worker) self.video_thread = threading.Thread(target=self.video_worker)
self.audio_thread.start() self.audio_thread.start()
......
import ctypes
import queue
import threading
import time
import traceback
import numpy as np
import torch
import torchaudio as ta
from loguru import logger
from scipy.signal import resample
class VAX64Recorder:
def __init__(
self,
whip_shared_path: str,
livestream_url: str,
fps: float = 16.0,
sample_rate: int = 16000,
):
assert livestream_url.startswith("http"), "VAX64Recorder only support whip http livestream"
self.livestream_url = livestream_url
self.fps = fps
self.sample_rate = sample_rate
self.width = None
self.height = None
self.stoppable_t = None
# only enable whip shared api for whip http livestream
self.whip_shared_path = whip_shared_path
self.whip_shared_lib = None
self.whip_shared_handle = None
# queue for send data to whip shared api
self.queue = queue.Queue()
self.worker_thread = None
def worker(self):
try:
fail_time, max_fail_time = 0, 10
packet_secs = 1.0 / self.fps
audio_chunk = round(48000 * 2 / self.fps)
audio_samples = round(48000 / self.fps)
while True:
try:
if self.queue is None:
break
data = self.queue.get()
if data is None:
logger.info("Worker thread received stop signal")
break
audios, images = data
for i in range(images.shape[0]):
t0 = time.time()
cur_audio = audios[i * audio_chunk : (i + 1) * audio_chunk].flatten()
audio_ptr = cur_audio.ctypes.data_as(ctypes.POINTER(ctypes.c_int16))
self.whip_shared_lib.pushRawAudioFrame(self.whip_shared_handle, audio_ptr, audio_samples)
cur_video = images[i].flatten()
video_ptr = cur_video.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))
self.whip_shared_lib.pushRawVideoFrame(self.whip_shared_handle, video_ptr, self.width, self.height)
time.sleep(max(0, packet_secs - (time.time() - t0)))
fail_time = 0
except: # noqa
logger.error(f"Send audio data error: {traceback.format_exc()}")
fail_time += 1
if fail_time > max_fail_time:
logger.error(f"Audio push worker thread failed {fail_time} times, stopping...")
break
except: # noqa
logger.error(f"Audio push worker thread error: {traceback.format_exc()}")
finally:
logger.info("Audio push worker thread stopped")
def start_libx264_whip_shared_api(self):
self.whip_shared_lib = ctypes.CDLL(self.whip_shared_path)
# define function argtypes and restype
self.whip_shared_lib.initStream.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int, ctypes.c_int]
self.whip_shared_lib.initStream.restype = ctypes.c_void_p
self.whip_shared_lib.pushRawAudioFrame.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int16), ctypes.c_int]
self.whip_shared_lib.pushRawVideoFrame.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int, ctypes.c_int]
self.whip_shared_lib.destroyStream.argtypes = [ctypes.c_void_p]
whip_url = ctypes.c_char_p(self.livestream_url.encode("utf-8"))
self.whip_shared_handle = ctypes.c_void_p(self.whip_shared_lib.initStream(whip_url, 1, 1, 0))
logger.info(f"WHIP shared API initialized with handle: {self.whip_shared_handle}")
def convert_data(self, audios, images):
# Convert audio data to 16-bit integer format
audio_datas = np.clip(np.round(audios * 32767), -32768, 32767).astype(np.int16)
# Convert to numpy and scale to [0, 255], convert RGB to BGR for OpenCV/FFmpeg
image_datas = (images * 255).clamp(0, 255).to(torch.uint8).cpu().numpy()
logger.info(f"image_datas: {image_datas.shape} {image_datas.dtype} {image_datas.min()} {image_datas.max()}")
reample_audios = resample(audio_datas, int(len(audio_datas) * 48000 / self.sample_rate))
stereo_audios = np.stack([reample_audios, reample_audios], axis=-1).astype(np.int16).reshape(-1)
return stereo_audios, image_datas
def start(self, width: int, height: int):
self.set_video_size(width, height)
def set_video_size(self, width: int, height: int):
if self.width is not None and self.height is not None:
assert self.width == width and self.height == height, "Video size already set"
return
self.width = width
self.height = height
self.start_libx264_whip_shared_api()
self.worker_thread = threading.Thread(target=self.worker)
self.worker_thread.start()
# Publish ComfyUI Image tensor and audio tensor to livestream
def pub_livestream(self, images: torch.Tensor, audios: np.ndarray):
N, height, width, C = images.shape
M = audios.reshape(-1).shape[0]
assert C == 3, "Input must be [N, H, W, C] with C=3"
logger.info(f"Publishing video [{N}x{width}x{height}], audio: [{M}]")
audio_frames = round(M * self.fps / self.sample_rate)
if audio_frames != N:
logger.warning(f"Video and audio frames mismatch, {N} vs {audio_frames}")
self.set_video_size(width, height)
audio_datas, image_datas = self.convert_data(audios, images)
self.queue.put((audio_datas, image_datas))
logger.info(f"Published {N} frames and {M} audio samples")
self.stoppable_t = time.time() + M / self.sample_rate + 3
def stop(self, wait=True):
if wait and self.stoppable_t:
t = self.stoppable_t - time.time()
if t > 0:
logger.warning(f"Waiting for {t} seconds to stop ...")
time.sleep(t)
self.stoppable_t = None
# Send stop signals to queues
if self.queue:
self.queue.put(None)
# Wait for threads to finish
if self.worker_thread and self.worker_thread.is_alive():
self.worker_thread.join(timeout=5)
if self.worker_thread.is_alive():
logger.warning("Worker thread did not stop gracefully")
# Destroy WHIP shared API
if self.whip_shared_lib and self.whip_shared_handle:
self.whip_shared_lib.destroyStream(self.whip_shared_handle)
self.whip_shared_handle = None
self.whip_shared_lib = None
logger.warning("WHIP shared API destroyed")
def __del__(self):
self.stop()
def create_simple_video(frames=10, height=480, width=640):
video_data = []
for i in range(frames):
frame = np.zeros((height, width, 3), dtype=np.float32)
stripe_height = height // 8
colors = [
[1.0, 0.0, 0.0], # 红色
[0.0, 1.0, 0.0], # 绿色
[0.0, 0.0, 1.0], # 蓝色
[1.0, 1.0, 0.0], # 黄色
[1.0, 0.0, 1.0], # 洋红
[0.0, 1.0, 1.0], # 青色
[1.0, 1.0, 1.0], # 白色
[0.5, 0.5, 0.5], # 灰色
]
for j, color in enumerate(colors):
start_y = j * stripe_height
end_y = min((j + 1) * stripe_height, height)
frame[start_y:end_y, :] = color
offset = int((i / frames) * width)
frame = np.roll(frame, offset, axis=1)
frame = torch.tensor(frame, dtype=torch.float32)
video_data.append(frame)
return torch.stack(video_data, dim=0)
if __name__ == "__main__":
sample_rate = 16000
fps = 16
width = 352
height = 352
recorder = VAX64Recorder(
whip_shared_path="/data/nvme0/liuliang1/lightx2v/test_deploy/test_whip_so/src/libagora_go_whip.so",
livestream_url="https://reverse.st-oc-01.chielo.org/10.5.64.49:8000/rtc/v1/whip/?app=subscribe&stream=ll1&eip=10.120.114.82:8000",
fps=fps,
sample_rate=sample_rate,
)
recorder.start(width, height)
time.sleep(5)
audio_path = "/data/nvme0/liuliang1/lightx2v/test_deploy/media_test/mangzhong.wav"
audio_array, ori_sr = ta.load(audio_path)
audio_array = ta.functional.resample(audio_array.mean(0), orig_freq=ori_sr, new_freq=16000)
audio_array = audio_array.numpy().reshape(-1)
secs = audio_array.shape[0] // sample_rate
interval = 1
space = 10
i = 0
while i < space:
t0 = time.time()
logger.info(f"space {i} / {space} s")
cur_audio_array = np.zeros(int(interval * sample_rate), dtype=np.float32)
num_frames = int(interval * fps)
images = create_simple_video(num_frames, height, width)
recorder.pub_livestream(images, cur_audio_array)
i += interval
time.sleep(interval - (time.time() - t0))
started = True
i = 0
while i < secs:
t0 = time.time()
start = int(i * sample_rate)
end = int((i + interval) * sample_rate)
cur_audio_array = audio_array[start:end]
num_frames = int(interval * fps)
images = create_simple_video(num_frames, height, width)
logger.info(f"{i} / {secs} s")
if started:
logger.warning(f"start pub_livestream !!!!!!!!!!!!!!!!!!!!!!!")
started = False
recorder.pub_livestream(images, cur_audio_array)
i += interval
time.sleep(interval - (time.time() - t0))
recorder.stop()
...@@ -9,7 +9,7 @@ from contextlib import asynccontextmanager ...@@ -9,7 +9,7 @@ from contextlib import asynccontextmanager
import uvicorn import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, Response from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from loguru import logger from loguru import logger
...@@ -73,9 +73,9 @@ async def http_exception_handler(request: Request, exc: HTTPException): ...@@ -73,9 +73,9 @@ async def http_exception_handler(request: Request, exc: HTTPException):
static_dir = os.path.join(os.path.dirname(__file__), "static") static_dir = os.path.join(os.path.dirname(__file__), "static")
app.mount("/static", StaticFiles(directory=static_dir), name="static") app.mount("/static", StaticFiles(directory=static_dir), name="static")
# 添加icon目录的静态文件服务 # 添加assets目录的静态文件服务
icon_dir = os.path.join(os.path.dirname(__file__), "static", "icon") assets_dir = os.path.join(os.path.dirname(__file__), "static", "assets")
app.mount("/icon", StaticFiles(directory=icon_dir), name="icon") app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
security = HTTPBearer() security = HTTPBearer()
...@@ -299,7 +299,6 @@ async def api_v1_task_submit(request: Request, user=Depends(verify_user_access)) ...@@ -299,7 +299,6 @@ async def api_v1_task_submit(request: Request, user=Depends(verify_user_access))
@app.get("/api/v1/task/query") @app.get("/api/v1/task/query")
async def api_v1_task_query(request: Request, user=Depends(verify_user_access)): async def api_v1_task_query(request: Request, user=Depends(verify_user_access)):
try: try:
# 检查是否有task_ids参数(批量查询)
if "task_ids" in request.query_params: if "task_ids" in request.query_params:
task_ids = request.query_params["task_ids"].split(",") task_ids = request.query_params["task_ids"].split(",")
tasks = [] tasks = []
...@@ -492,16 +491,6 @@ async def api_v1_task_delete(request: Request, user=Depends(verify_user_access)) ...@@ -492,16 +491,6 @@ async def api_v1_task_delete(request: Request, user=Depends(verify_user_access))
if task["status"] not in FinishedStatus: if task["status"] not in FinishedStatus:
return error_response("Only finished tasks can be deleted", 400) return error_response("Only finished tasks can be deleted", 400)
# delete input files
for _, input_filename in task["inputs"].items():
await data_manager.delete_bytes(input_filename)
logger.info(f"Deleted input file: {input_filename}")
# delete output files
for _, output_filename in task["outputs"].items():
await data_manager.delete_bytes(output_filename)
logger.info(f"Deleted output file: {output_filename}")
# delete task record # delete task record
success = await task_manager.delete_task(task_id, user["user_id"]) success = await task_manager.delete_task(task_id, user["user_id"])
if success: if success:
...@@ -639,7 +628,8 @@ async def api_v1_monitor_metrics(): ...@@ -639,7 +628,8 @@ async def api_v1_monitor_metrics():
@app.get("/api/v1/template/asset_url/{template_type}/{filename}") @app.get("/api/v1/template/asset_url/{template_type}/{filename}")
async def api_v1_template_asset_url(template_type: str, filename: str, valid=Depends(verify_user_access)): async def api_v1_template_asset_url(template_type: str, filename: str):
"""get template asset URL - no authentication required"""
try: try:
url = await data_manager.presign_template_url(template_type, filename) url = await data_manager.presign_template_url(template_type, filename)
if url is None: if url is None:
...@@ -653,8 +643,8 @@ async def api_v1_template_asset_url(template_type: str, filename: str, valid=Dep ...@@ -653,8 +643,8 @@ async def api_v1_template_asset_url(template_type: str, filename: str, valid=Dep
# Template API endpoints # Template API endpoints
@app.get("/assets/template/{template_type}/{filename}") @app.get("/assets/template/{template_type}/{filename}")
async def assets_template(template_type: str, filename: str, valid=Depends(verify_user_access_from_query)): async def assets_template(template_type: str, filename: str):
"""get template file""" """get template file - no authentication required"""
try: try:
if not await data_manager.template_file_exists(template_type, filename): if not await data_manager.template_file_exists(template_type, filename):
return error_response(f"template file {template_type} {filename} not found", 404) return error_response(f"template file {template_type} {filename} not found", 404)
...@@ -695,8 +685,8 @@ async def assets_template(template_type: str, filename: str, valid=Depends(verif ...@@ -695,8 +685,8 @@ async def assets_template(template_type: str, filename: str, valid=Depends(verif
@app.get("/api/v1/template/list") @app.get("/api/v1/template/list")
async def api_v1_template_list(request: Request, valid=Depends(verify_user_access)): async def api_v1_template_list(request: Request):
"""get template file list (support pagination)""" """get template file list (support pagination) - no authentication required"""
try: try:
# check page params # check page params
page = int(request.query_params.get("page", 1)) page = int(request.query_params.get("page", 1))
...@@ -749,8 +739,8 @@ async def api_v1_template_list(request: Request, valid=Depends(verify_user_acces ...@@ -749,8 +739,8 @@ async def api_v1_template_list(request: Request, valid=Depends(verify_user_acces
@app.get("/api/v1/template/tasks") @app.get("/api/v1/template/tasks")
async def api_v1_template_tasks(request: Request, valid=Depends(verify_user_access)): async def api_v1_template_tasks(request: Request):
"""get template task list (support pagination)""" """get template task list (support pagination) - no authentication required"""
try: try:
# check page params # check page params
page = int(request.query_params.get("page", 1)) page = int(request.query_params.get("page", 1))
...@@ -773,11 +763,11 @@ async def api_v1_template_tasks(request: Request, valid=Depends(verify_user_acce ...@@ -773,11 +763,11 @@ async def api_v1_template_tasks(request: Request, valid=Depends(verify_user_acce
template_data = json.loads(bytes_data) template_data = json.loads(bytes_data)
template_data["task"]["model_cls"] = model_pipelines.outer_model_name(template_data["task"]["model_cls"]) template_data["task"]["model_cls"] = model_pipelines.outer_model_name(template_data["task"]["model_cls"])
all_categories.update(template_data["task"]["tags"]) all_categories.update(template_data["task"]["tags"])
if category is not None and category != "all" and category not in template_data["task"]["tags"]: if category and category not in template_data["task"]["tags"]:
continue continue
if search is not None and search not in template_data["task"]["params"]["prompt"] + template_data["task"]["params"]["negative_prompt"] + template_data["task"][ if search and search not in template_data["task"]["params"]["prompt"] + template_data["task"]["params"]["negative_prompt"] + template_data["task"]["model_cls"] + template_data["task"][
"model_cls" "stage"
] + template_data["task"]["stage"] + template_data["task"]["task_type"] + ",".join(template_data["task"]["tags"]): ] + template_data["task"]["task_type"] + ",".join(template_data["task"]["tags"]):
continue continue
all_templates.append(template_data["task"]) all_templates.append(template_data["task"])
except Exception as e: except Exception as e:
...@@ -800,6 +790,131 @@ async def api_v1_template_tasks(request: Request, valid=Depends(verify_user_acce ...@@ -800,6 +790,131 @@ async def api_v1_template_tasks(request: Request, valid=Depends(verify_user_acce
return error_response(str(e), 500) return error_response(str(e), 500)
@app.get("/api/v1/template/{template_id}")
async def api_v1_template_get(template_id: str, user=None):
try:
template_files = await data_manager.list_template_files("tasks")
template_files = [] if template_files is None else template_files
for template_file in template_files:
try:
bytes_data = await data_manager.load_template_file("tasks", template_file)
template_data = json.loads(bytes_data)
if template_data["task"]["task_id"] == template_id:
return template_data["task"]
except Exception as e:
logger.warning(f"Failed to load template file {template_file}: {e}")
continue
return error_response("Template not found", 404)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/api/v1/share/create")
async def api_v1_share_create(request: Request, user=Depends(verify_user_access)):
try:
params = await request.json()
task_id = params["task_id"]
valid_days = params.get("valid_days", 7)
auth_type = params.get("auth_type", "public")
auth_value = params.get("auth_value", "")
share_type = params.get("share_type", "task")
assert auth_type == "public", "Only public share is supported now"
if share_type == "template":
template = await api_v1_template_get(task_id, user)
assert isinstance(template, dict) and template["task_id"] == task_id, f"Template {task_id} not found"
else:
task = await task_manager.query_task(task_id, user["user_id"], only_task=True)
assert task, f"Task {task_id} not found"
if auth_type == "user_id":
assert auth_value != "", "Target user is required for auth_type = user_id"
target_user = await task_manager.query_user(auth_value)
assert target_user and target_user["user_id"] == auth_value, f"Target user {auth_value} not found"
share_id = await task_manager.create_share(task_id, user["user_id"], share_type, valid_days, auth_type, auth_value)
return {"share_id": share_id, "share_url": f"/share/{share_id}"}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/share/{share_id}")
async def api_v1_share_get(share_id: str):
try:
share_data = await task_manager.query_share(share_id)
assert share_data, f"Share {share_id} not found or expired or deleted"
task_id = share_data["task_id"]
share_type = share_data["share_type"]
assert share_data["auth_type"] == "public", "Only public share is supported now"
if share_type == "template":
task = await api_v1_template_get(task_id, None)
assert isinstance(task, dict) and task["task_id"] == task_id, f"Template {task_id} not found"
else:
task = await task_manager.query_task(task_id, only_task=True)
assert task, f"Task {task_id} not found"
user_info = await task_manager.query_user(share_data["user_id"])
username = user_info.get("username", "用户") if user_info else "用户"
share_info = {
"task_id": task_id,
"share_type": share_type,
"user_id": share_data["user_id"],
"username": username,
"task_type": task["task_type"],
"model_cls": task["model_cls"],
"stage": task["stage"],
"prompt": task["params"].get("prompt", ""),
"negative_prompt": task["params"].get("negative_prompt", ""),
"inputs": task["inputs"],
"outputs": task["outputs"],
"create_t": task["create_t"],
"valid_days": share_data["valid_days"],
"valid_t": share_data["valid_t"],
"auth_type": share_data["auth_type"],
"auth_value": share_data["auth_value"],
"output_video_url": None,
"input_urls": {},
}
for input_name, input_filename in task["inputs"].items():
if share_type == "template":
template_type = "images" if "image" in input_name else "audios"
input_url = await data_manager.presign_template_url(template_type, input_filename)
else:
input_url = await data_manager.presign_url(input_filename)
share_info["input_urls"][input_name] = input_url
for output_name, output_filename in task["outputs"].items():
if share_type == "template":
assert "video" in output_name, "Only video output is supported for template share"
output_url = await data_manager.presign_template_url("videos", output_filename)
else:
output_url = await data_manager.presign_url(output_filename)
share_info["output_video_url"] = output_url
return share_info
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
# 所有未知路由 fallback 到 index.html (必须在所有API路由之后)
@app.get("/{full_path:path}", response_class=HTMLResponse)
async def vue_fallback(full_path: str):
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return HTMLResponse("<h1>Frontend not found</h1>", status_code=404)
# ========================= # =========================
# Main Entry # Main Entry
# ========================= # =========================
......
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LightX2V</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 主要图标库 -->
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- 备用图标库 -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"
media="print" onload="this.media='all'">
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-bold-rounded/css/uicons-bold-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-regular-rounded/css/uicons-regular-rounded.css'>
<link href="/src/style.css" rel="stylesheet">
</head>
<body>
<div id="app">
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
{
"name": "my-project",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "my-project",
"version": "0.0.0",
"dependencies": {
"@flaticon/flaticon-uicons": "^3.3.1",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@tailwindcss/vite": "^4.1.13",
"pinia": "^3.0.3",
"tailwindcss": "^4.1.13",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
"integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
"integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
"integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
"integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
"integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
"integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
"integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
"integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
"integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
"integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
"integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
"integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
"integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
"integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
"integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
"integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
"integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
"integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
"integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
"integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
"integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
"integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
"integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
"integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
"integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
"integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@flaticon/flaticon-uicons": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@flaticon/flaticon-uicons/-/flaticon-uicons-3.3.1.tgz",
"integrity": "sha512-WN2zuECCdjuGBQrjzN0kpeSygzC5fgF8Q7pDR+FUuGtYWczSdIhIwoD+/fKBEfwqKfNIMZ1WouidevGQ4OJORg==",
"license": "SEE LICENSE IN LICENSE",
"optionalDependencies": {
"esbuild-linux-64": "^0.14.5"
}
},
"node_modules/@headlessui/vue": {
"version": "1.7.23",
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz",
"integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==",
"license": "MIT",
"dependencies": {
"@tanstack/vue-virtual": "^3.0.0-beta.60"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@heroicons/vue": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz",
"integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==",
"license": "MIT",
"peerDependencies": {
"vue": ">= 3"
}
},
"node_modules/@intlify/core-base": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.12",
"@intlify/shared": "11.1.12"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.12",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.29",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
"integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz",
"integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.2.tgz",
"integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.2.tgz",
"integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.2.tgz",
"integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.2.tgz",
"integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.2.tgz",
"integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.2.tgz",
"integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.2.tgz",
"integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.2.tgz",
"integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.2.tgz",
"integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.2.tgz",
"integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.2.tgz",
"integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.2.tgz",
"integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.2.tgz",
"integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.2.tgz",
"integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.2.tgz",
"integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.2.tgz",
"integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.2.tgz",
"integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.2.tgz",
"integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.2.tgz",
"integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.2.tgz",
"integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.2.tgz",
"integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tailwindcss/node": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz",
"integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.5.1",
"lightningcss": "1.30.1",
"magic-string": "^0.30.18",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.13"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz",
"integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.4",
"tar": "^7.4.3"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.13",
"@tailwindcss/oxide-darwin-arm64": "4.1.13",
"@tailwindcss/oxide-darwin-x64": "4.1.13",
"@tailwindcss/oxide-freebsd-x64": "4.1.13",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.13",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.13",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.13",
"@tailwindcss/oxide-linux-x64-musl": "4.1.13",
"@tailwindcss/oxide-wasm32-wasi": "4.1.13",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.13",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.13"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz",
"integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz",
"integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz",
"integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz",
"integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz",
"integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz",
"integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz",
"integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz",
"integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz",
"integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz",
"integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.5",
"@emnapi/runtime": "^1.4.5",
"@emnapi/wasi-threads": "^1.0.4",
"@napi-rs/wasm-runtime": "^0.2.12",
"@tybys/wasm-util": "^0.10.0",
"tslib": "^2.8.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
"integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz",
"integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz",
"integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.13",
"@tailwindcss/oxide": "4.1.13",
"tailwindcss": "4.1.13"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/vue-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
"integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"vue": "^2.7.0 || ^3.0.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
"integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.29"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
"integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
"@vue/shared": "3.5.21",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz",
"integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
"integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
"@vue/compiler-core": "3.5.21",
"@vue/compiler-dom": "3.5.21",
"@vue/compiler-ssr": "3.5.21",
"@vue/shared": "3.5.21",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.18",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz",
"integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz",
"integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz",
"integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz",
"integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.21",
"@vue/runtime-core": "3.5.21",
"@vue/shared": "3.5.21",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz",
"integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.21",
"@vue/shared": "3.5.21"
},
"peerDependencies": {
"vue": "3.5.21"
}
},
"node_modules/@vue/shared": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz",
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==",
"license": "MIT"
},
"node_modules/birpc": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz",
"integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"license": "MIT",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
"integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
"integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.10",
"@esbuild/android-arm": "0.25.10",
"@esbuild/android-arm64": "0.25.10",
"@esbuild/android-x64": "0.25.10",
"@esbuild/darwin-arm64": "0.25.10",
"@esbuild/darwin-x64": "0.25.10",
"@esbuild/freebsd-arm64": "0.25.10",
"@esbuild/freebsd-x64": "0.25.10",
"@esbuild/linux-arm": "0.25.10",
"@esbuild/linux-arm64": "0.25.10",
"@esbuild/linux-ia32": "0.25.10",
"@esbuild/linux-loong64": "0.25.10",
"@esbuild/linux-mips64el": "0.25.10",
"@esbuild/linux-ppc64": "0.25.10",
"@esbuild/linux-riscv64": "0.25.10",
"@esbuild/linux-s390x": "0.25.10",
"@esbuild/linux-x64": "0.25.10",
"@esbuild/netbsd-arm64": "0.25.10",
"@esbuild/netbsd-x64": "0.25.10",
"@esbuild/openbsd-arm64": "0.25.10",
"@esbuild/openbsd-x64": "0.25.10",
"@esbuild/openharmony-arm64": "0.25.10",
"@esbuild/sunos-x64": "0.25.10",
"@esbuild/win32-arm64": "0.25.10",
"@esbuild/win32-ia32": "0.25.10",
"@esbuild/win32-x64": "0.25.10"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/jiti": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz",
"integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-darwin-x64": "1.30.1",
"lightningcss-freebsd-x64": "1.30.1",
"lightningcss-linux-arm-gnueabihf": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-arm64-musl": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1",
"lightningcss-linux-x64-musl": "1.30.1",
"lightningcss-win32-arm64-msvc": "1.30.1",
"lightningcss-win32-x64-msvc": "1.30.1"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/magic-string": {
"version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rollup": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.2",
"@rollup/rollup-android-arm64": "4.52.2",
"@rollup/rollup-darwin-arm64": "4.52.2",
"@rollup/rollup-darwin-x64": "4.52.2",
"@rollup/rollup-freebsd-arm64": "4.52.2",
"@rollup/rollup-freebsd-x64": "4.52.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.2",
"@rollup/rollup-linux-arm-musleabihf": "4.52.2",
"@rollup/rollup-linux-arm64-gnu": "4.52.2",
"@rollup/rollup-linux-arm64-musl": "4.52.2",
"@rollup/rollup-linux-loong64-gnu": "4.52.2",
"@rollup/rollup-linux-ppc64-gnu": "4.52.2",
"@rollup/rollup-linux-riscv64-gnu": "4.52.2",
"@rollup/rollup-linux-riscv64-musl": "4.52.2",
"@rollup/rollup-linux-s390x-gnu": "4.52.2",
"@rollup/rollup-linux-x64-gnu": "4.52.2",
"@rollup/rollup-linux-x64-musl": "4.52.2",
"@rollup/rollup-openharmony-arm64": "4.52.2",
"@rollup/rollup-win32-arm64-msvc": "4.52.2",
"@rollup/rollup-win32-ia32-msvc": "4.52.2",
"@rollup/rollup-win32-x64-gnu": "4.52.2",
"@rollup/rollup-win32-x64-msvc": "4.52.2",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"license": "MIT",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tailwindcss": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tar": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.4.tgz",
"integrity": "sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/vite": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"lightningcss": "^1.21.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/vue": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz",
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.21",
"@vue/compiler-sfc": "3.5.21",
"@vue/runtime-dom": "3.5.21",
"@vue/server-renderer": "3.5.21",
"@vue/shared": "3.5.21"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue-i18n": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.12",
"@intlify/shared": "11.1.12",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
}
}
}
{
"name": "my-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@flaticon/flaticon-uicons": "^3.3.1",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@tailwindcss/vite": "^4.1.13",
"pinia": "^3.0.3",
"tailwindcss": "^4.1.13",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7"
}
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import router from './router'
import { init, handleLoginCallback, handleClickOutside, validateToken } from './utils/other'
import { initLanguage } from './utils/i18n'
import { startHintRotation, stopHintRotation } from './utils/other'
import { currentUser,
isLoading,
applyMobileStyles,
isLoggedIn,
loginLoading,
initLoading,
pollingInterval,
pollingTasks,
showAlert
} from './utils/other'
import { useI18n } from 'vue-i18n'
import Loading from './components/Loading.vue'
const { t, locale } = useI18n()
let source = null
// 页面加载时应用移动端样式
onMounted(() => {
applyMobileStyles();
window.addEventListener('resize', applyMobileStyles);
});
// 组件卸载时移除事件监听器
onUnmounted(() => {
window.removeEventListener('resize', applyMobileStyles);
});
// 生命周期:页面加载
onMounted(async () => {
// 1. 初始化语言
isLoading.value = true
await initLanguage()
initLoading.value = true
// 2. 启动提示滚动
startHintRotation()
// 3. 添加全局点击事件监听器
document.addEventListener('click', handleClickOutside)
try {
// 检查是否有登录回调参数
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
if (code) {
// 处理登录回调
isLoading.value = true
source = localStorage.getItem('loginSource')
await handleLoginCallback(code, source)
return
}
// 检查本地存储的登录状态
const savedToken = localStorage.getItem('accessToken')
const savedUser = localStorage.getItem('currentUser')
if (savedToken && savedUser) {
// 验证token是否过期
const isValidToken = await validateToken(savedToken)
if (isValidToken) {
currentUser.value = JSON.parse(savedUser)
isLoggedIn.value = true
await init();
console.log('用户已登录,初始化完成')
} else {
// Token已过期,清除本地存储
localStorage.removeItem('accessToken')
localStorage.removeItem('currentUser')
isLoggedIn.value = false
console.log('Token已过期')
showAlert('请重新登录', 'warning')
}
} else {
isLoggedIn.value = false
console.log('用户未登录')
}
} catch (error) {
console.error('初始化失败', error)
showAlert('初始化失败,请刷新页面重试', 'danger')
isLoggedIn.value = false
} finally {
loginLoading.value = false
initLoading.value = false
isLoading.value = false
}
// 6. 移动端样式适配
applyMobileStyles()
window.addEventListener('resize', applyMobileStyles)
})
// 生命周期:页面卸载
onUnmounted(() => {
// 清理轮询
if (pollingInterval.value) clearInterval(pollingInterval.value)
pollingTasks.value.clear()
// 清理提示滚动
stopHintRotation()
// 移除事件监听器
window.removeEventListener('resize', applyMobileStyles)
document.removeEventListener('click', handleClickOutside)
})
</script>
<template>
<router-view></router-view>
<!-- 全局路由跳转Loading覆盖层 -->
<div v-show="isLoading" class="fixed inset-0 bg-gradient-main flex items-center justify-center">
<Loading />
</div>
</template>
<script setup>
import { alert, getAlertClass, getAlertIconBgClass, getAlertIcon } from '../utils/other'
import { useI18n } from 'vue-i18n'
import { ref, onMounted, onUnmounted } from 'vue'
const { t, locale } = useI18n()
// 响应式变量控制Alert位置
const alertPosition = ref({ top: '1rem' })
// 防抖函数
let scrollTimeout = null
// 监听滚动事件,动态调整Alert位置
const handleScroll = () => {
// 清除之前的定时器
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}
// 设置新的定时器,防抖处理
scrollTimeout = setTimeout(() => {
const scrollY = window.scrollY
const viewportHeight = window.innerHeight
// 如果用户滚动了超过50px,将Alert显示在视口内
if (scrollY > 50) {
// 计算Alert应该显示的位置,确保在视口内可见
// 距离滚动位置20px,但不超过视口底部200px
const alertTop = Math.min(scrollY + 20, scrollY + viewportHeight - 200)
alertPosition.value = { top: `${alertTop}px` }
} else {
// 在页面顶部时,显示在固定位置
alertPosition.value = { top: '1rem' }
}
}, 10) // 10ms防抖延迟
}
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
// 初始化时也调用一次,确保位置正确
handleScroll()
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}
})
</script>
<template>
<!-- 增强的提示消息系统 -->
<div v-cloak>
<div v-if="alert.show"
class="fixed left-1/2 transform -translate-x-1/2 z-[9999] max-w-xs w-full px-4 transition-all duration-300 ease-out"
:style="alertPosition"
:class="getAlertClass(alert.type)">
<div
class="bg-gray-800/95 backdrop-blur-sm border border-gray-700/50 rounded-lg shadow-lg transition-all duration-300 ease-out">
<div class="flex items-center p-3">
<div class="flex-shrink-0 mr-3">
<div class="w-6 h-6 rounded-full flex items-center justify-center"
:class="getAlertIconBgClass(alert.type)">
<i :class="getAlertIcon(alert.type)" class="text-xs"></i>
</div>
</div>
<div class="flex-1">
<p class="text-xs font-medium text-gray-200">
{{ alert.message }}
</p>
</div>
<div class="flex-shrink-0 ml-2">
<button @click="alert.show = false"
class="w-5 h-5 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-200 hover:bg-gray-700/50 transition-all duration-200">
<i class="fas fa-times text-xs"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<template>
<div class="p-6">
<h2 class="text-xl font-bold text-white mb-4">音频预览测试</h2>
<!-- 测试音频模板预览 -->
<div class="mb-6">
<h3 class="text-lg text-white mb-2">音频模板预览测试</h3>
<div class="space-y-2">
<div v-for="template in audioTemplates" :key="template.filename"
class="flex items-center gap-4 p-3 bg-dark-light rounded-lg">
<span class="text-white">{{ template.filename }}</span>
<button @click="previewAudioTemplate(template)"
class="px-3 py-1 bg-laser-purple text-white rounded hover:bg-laser-purple/80">
预览
</button>
</div>
<div v-if="audioTemplates.length === 0" class="text-gray-400">
暂无音频模板
</div>
</div>
</div>
<!-- 测试音频历史预览 -->
<div class="mb-6">
<h3 class="text-lg text-white mb-2">音频历史预览测试</h3>
<div class="space-y-2">
<div v-for="history in audioHistory" :key="history.filename"
class="flex items-center gap-4 p-3 bg-dark-light rounded-lg">
<span class="text-white">{{ history.filename }}</span>
<button @click="previewAudioHistory(history)"
class="px-3 py-1 bg-laser-purple text-white rounded hover:bg-laser-purple/80">
预览
</button>
</div>
<div v-if="audioHistory.length === 0" class="text-gray-400">
暂无音频历史
</div>
</div>
</div>
<!-- 调试信息 -->
<div class="mt-6 p-4 bg-gray-800 rounded-lg">
<h3 class="text-lg text-white mb-2">调试信息</h3>
<div class="text-sm text-gray-300">
<p>音频模板数量: {{ audioTemplates.length }}</p>
<p>音频历史数量: {{ audioHistory.length }}</p>
<div v-if="audioTemplates.length > 0">
<p>第一个音频模板:</p>
<pre class="text-xs bg-gray-900 p-2 rounded mt-1">{{ JSON.stringify(audioTemplates[0], null, 2) }}</pre>
</div>
<div v-if="audioHistory.length > 0">
<p>第一个音频历史:</p>
<pre class="text-xs bg-gray-900 p-2 rounded mt-1">{{ JSON.stringify(audioHistory[0], null, 2) }}</pre>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
audioTemplates,
audioHistory,
previewAudioTemplate,
previewAudioHistory
} from '../utils/other'
</script>
<script setup>
import { confirmDialog,
showConfirmDialog} from '../utils/other'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<!-- 自定义确认对话框 -->
<div v-cloak>
<div v-if="confirmDialog.show" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<!-- 背景遮罩 -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="confirmDialog.show = false">
</div>
<!-- 对话框内容 -->
<div class="relative bg-dark-light border border-laser-purple/30 rounded-xl shadow-2xl max-w-md w-full mx-4 transform transition-all duration-300 ease-out"
:class="confirmDialog.show ? 'scale-100 opacity-100' : 'scale-95 opacity-0'">
<!-- 头部 -->
<div class="flex items-center justify-between p-6 border-b border-gray-700">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-red-500/20 rounded-full flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-red-400 text-lg"></i>
</div>
<h3 class="text-lg font-semibold text-white">{{ confirmDialog.title }}</h3>
</div>
<button @click="confirmDialog.show = false"
class="text-gray-400 hover:text-gray-300 transition-colors">
<i class="fas fa-times text-lg"></i>
</button>
</div>
<!-- 内容 -->
<div class="p-6">
<p class="text-gray-300 leading-relaxed mb-6">{{ confirmDialog.message }}</p>
<!-- 警告信息 -->
<div v-if="confirmDialog.warning"
class="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<i class="fas fa-info-circle text-red-400 mt-0.5"></i>
<div class="text-sm text-red-300">
<p class="font-medium mb-2">{{ confirmDialog.warning.title }}</p>
<ul class="space-y-1 text-xs">
<li v-for="item in confirmDialog.warning.items" :key="item"
class="flex items-center gap-2">
<i class="fas fa-minus text-red-400 text-xs"></i>
{{ item }}
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex gap-3 p-6 pt-0">
<button @click="confirmDialog.cancel()"
class="flex-1 px-4 py-2.5 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-all duration-200 font-medium">
{{ t('cancel') }}
</button>
<button @click="confirmDialog.confirm()"
class="flex-1 px-4 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-lg transition-all duration-200 font-medium flex items-center justify-center gap-2">
<i class="fas fa-trash text-sm"></i>
{{ confirmDialog.confirmText }}
</button>
</div>
</div>
</div>
</div>
</template>
<template>
<div class="relative inline-block text-left">
<Menu as="div">
<div>
<MenuButton
class="inline-flex w-full justify-center rounded-md bg-dark-light border border-laser-purple/30 px-4 py-3 text-sm font-medium text-white hover:bg-dark-light/80 hover:border-laser-purple/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-laser-purple/50 transition-all duration-200"
>
{{ selectedLabel || placeholder }}
<ChevronDownIcon
class="-mr-1 ml-2 h-5 w-5 text-laser-purple hover:text-gradient-primary transition-colors"
aria-hidden="true"
/>
</MenuButton>
</div>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems
class="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-700/50 rounded-md bg-dark-light border border-laser-purple/30 shadow-lg ring-1 ring-black/5 focus:outline-none z-50"
>
<div class="px-1 py-1">
<MenuItem v-for="item in items" :key="item.value" v-slot="{ active }">
<button
@click="selectItem(item)"
:class="[
active ? 'bg-laser-purple/20 text-white' : 'text-gray-300',
'group flex w-full items-center rounded-md px-3 py-2 text-sm transition-colors duration-200',
]"
>
<i v-if="item.icon" :class="[item.icon, 'mr-3 h-4 w-4 text-laser-purple']" aria-hidden="true"></i>
{{ item.label }}
<i v-if="selectedValue === item.value" class="fas fa-check ml-auto text-laser-purple"></i>
</button>
</MenuItem>
</div>
<div v-if="items.length === 0" class="px-1 py-1">
<div class="px-3 py-2 text-sm text-gray-500 text-center">
{{ emptyMessage }}
</div>
</div>
</MenuItems>
</transition>
</Menu>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { ChevronDownIcon } from '@heroicons/vue/20/solid'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// Props
const props = defineProps({
items: {
type: Array,
default: () => []
},
selectedValue: {
type: [String, Number],
default: ''
},
placeholder: {
type: String,
default: ''
},
emptyMessage: {
type: String,
default: ''
}
})
// Emits
const emit = defineEmits(['select-item'])
// Computed
const selectedLabel = computed(() => {
const selectedItem = props.items.find(item => item.value === props.selectedValue)
return selectedItem ? selectedItem.label : ''
})
// Methods
const selectItem = (item) => {
emit('select-item', item)
}
</script>
<template>
<!-- 浮动粒子背景 -->
<div class="floating-particles">
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
</div>
</template>
<style scoped type="text/tailwindcss">
.floating-particles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(193, 169, 255, 0.6);
border-radius: 50%;
animation: floatParticle 15s linear infinite;
}
.particle:nth-child(1) { left: 10%; animation-delay: 0s; }
.particle:nth-child(2) { left: 20%; animation-delay: 12s; }
.particle:nth-child(3) { left: 30%; animation-delay: 10s; }
.particle:nth-child(4) { left: 40%; animation-delay: 6s; }
.particle:nth-child(5) { left: 50%; animation-delay: 8s; }
.particle:nth-child(6) { left: 60%; animation-delay: 14s; }
.particle:nth-child(7) { left: 70%; animation-delay: 16s; }
.particle:nth-child(8) { left: 80%; animation-delay: 2s; }
.particle:nth-child(9) { left: 90%; animation-delay: 4s; }
@keyframes floatParticle {
0% {
transform: translateY(100vh) scale(0);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(-100px) scale(1);
opacity: 0;
}
}
</style>
<script setup>
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { watch, onMounted, computed, ref, nextTick, onUnmounted } from 'vue'
import ModelDropdown from './ModelDropdown.vue'
import MediaTemplate from './MediaTemplate.vue'
// Props
const props = defineProps({
query: {
type: Object,
default: () => ({})
}
})
const { t, locale } = useI18n()
const route = useRoute()
const router = useRouter()
// 当前显示的精选模版
const currentFeaturedTemplates = ref([])
// 屏幕尺寸响应式状态
const screenSize = ref('large') // 'small' 或 'large'
// 拖拽状态
const isDragOver = ref(false)
// 获取随机精选模版
const refreshRandomTemplates = async () => {
const randomTemplates = await getRandomFeaturedTemplates(10) // 获取10个模版
currentFeaturedTemplates.value = randomTemplates
}
// 随机列布局相关函数
const generateRandomColumnLayout = (templates) => {
if (!templates || templates.length === 0) return { columns: [], templates: [] }
// 响应式列数控制
const getColumnCount = () => {
if (screenSize.value === 'large') {
// 大屏幕:4-6列
return Math.floor(Math.random() * 2) + 4 // 4, 5, 6列
} else {
// 小屏幕:2-3列
return Math.floor(Math.random() * 2) + 2 // 2, 3列
}
}
const numColumns = getColumnCount()
// 生成随机列宽(总和为100%)
const columnWidths = []
let remainingWidth = 100
for (let i = 0; i < numColumns; i++) {
if (i === numColumns - 1) {
// 最后一列使用剩余宽度
columnWidths.push(remainingWidth)
} else {
// 随机宽度:20% 到 50%
const minWidth = 20
const maxWidth = Math.min(50, remainingWidth - (numColumns - i - 1) * minWidth)
const width = Math.random() * (maxWidth - minWidth) + minWidth
columnWidths.push(Math.round(width))
remainingWidth -= Math.round(width)
}
}
// 生成每列的起始位置(距离顶部的距离)
const columnStartPositions = []
for (let i = 0; i < numColumns; i++) {
// 随机起始位置:0% 到 20%
const startPosition = Math.random() * 20
columnStartPositions.push(Math.round(startPosition))
}
// 计算每列的起始left位置
const columnLeftPositions = []
let currentLeft = 0
for (let i = 0; i < numColumns; i++) {
columnLeftPositions.push(currentLeft)
currentLeft += columnWidths[i]
}
// 将模版分配到各列
const columnTemplates = Array.from({ length: numColumns }, () => [])
templates.forEach((template, index) => {
const columnIndex = index % numColumns
columnTemplates[columnIndex].push(template)
})
// 生成列配置
const columns = columnWidths.map((width, index) => ({
width: `${width}%`,
left: `${columnLeftPositions[index]}%`,
top: `${columnStartPositions[index]}%`,
templates: columnTemplates[index]
}))
return { columns, templates }
}
// 计算属性:带随机列布局的模版
const templatesWithRandomColumns = computed(() => {
return generateRandomColumnLayout(currentFeaturedTemplates.value)
})
// 屏幕尺寸监听器
const updateScreenSize = () => {
screenSize.value = window.innerWidth >= 1024 ? 'large' : 'small'
}
// 监听屏幕尺寸变化
let resizeHandler = null
import {
submitting,
templateLoading,
showTaskTypeMenu,
showModelMenu,
isRecording,
recordingDuration,
startRecording,
stopRecording,
formatRecordingDuration,
getCurrentForm,
getCurrentImagePreview,
getCurrentAudioPreview,
availableTaskTypes,
availableModelClasses,
currentTaskHints,
currentHintIndex,
selectedTaskId,
isCreationAreaExpanded,
isContracting,
expandCreationArea,
contractCreationArea,
handleImageUpload,
selectTask,
selectModel,
triggerImageUpload,
triggerAudioUpload,
removeImage,
removeAudio,
handleAudioUpload,
selectImageTemplate,
selectAudioTemplate,
previewAudioTemplate,
imageTemplates,
audioTemplates,
showImageTemplates,
showAudioTemplates,
mediaModalTab,
templatePaginationInfo,
templateCurrentPage,
templatePageInput,
imageHistory,
audioHistory,
showPromptModal,
promptModalTab,
submitTask,
goToTemplatePage,
jumpToTemplatePage,
getVisibleTemplatePages,
getTemplateFileUrl,
clearPrompt,
getTaskTypeIcon,
getTaskTypeName,
getPromptPlaceholder,
getHistoryImageUrl,
getCurrentImagePreviewUrl,
getCurrentAudioPreviewUrl,
handleAudioError,
getImageHistory,
getAudioHistory,
selectImageHistory,
selectAudioHistory,
previewAudioHistory,
clearImageHistory,
clearAudioHistory,
getAudioMimeType,
selectedModel,
// 精选模版相关
featuredTemplates,
featuredTemplatesLoading,
loadFeaturedTemplates,
getRandomFeaturedTemplates,
previewTemplateDetail,
useTemplate,
applyTemplateImage,
applyTemplateAudio,
playVideo,
pauseVideo,
toggleVideoPlay,
onVideoLoaded,
onVideoError,
onVideoEnded,
handleThumbnailError,
switchToInspirationView,
} from '../utils/other'
// 路由监听和URL同步
watch(() => route.query, (newQuery) => {
// 同步URL参数到组件状态
if (newQuery.taskType) {
// 根据URL参数设置任务类型
const taskType = newQuery.taskType
if (availableTaskTypes.value.some(type => type.value === taskType)) {
selectTask(taskType)
}
}
if (newQuery.model) {
// 根据URL参数设置模型
const model = newQuery.model
if (availableModelClasses.value.some(m => m.value === model)) {
selectModel(model)
}
}
if (newQuery.expanded === 'true') {
// 展开创建区域
expandCreationArea()
}
// 注意:分享数据导入功能已移至 Share.vue 中的 createSimilar 函数
// 这里不再需要处理分享数据导入
}, { immediate: true })
// 监听组件状态变化,同步到URL
watch([selectedTaskId, isCreationAreaExpanded, selectedModel], () => {
const query = {}
if (selectedTaskId.value) {
query.taskType = selectedTaskId.value
}
if (isCreationAreaExpanded.value) {
query.expanded = 'true'
}
if (selectedModel.value) {
query.model = selectedModel.value
}
// 更新URL但不触发路由监听
router.replace({ query })
})
// 组件挂载时初始化
onMounted(async () => {
// 确保URL参数正确同步
const query = route.query
if (query.taskType) {
selectTask(query.taskType)
}
if (query.model) {
selectModel(query.model)
}
if (query.expanded === 'true') {
expandCreationArea()
}
// 初始化屏幕尺寸
updateScreenSize()
// 添加屏幕尺寸监听器
resizeHandler = () => {
updateScreenSize()
}
window.addEventListener('resize', resizeHandler)
// 加载精选模版数据
await loadFeaturedTemplates(true)
// 获取随机精选模版
const randomTemplates = await getRandomFeaturedTemplates(10) // 获取10个模版
currentFeaturedTemplates.value = randomTemplates
})
// 拖拽处理函数
const handleDragOver = (e) => {
e.preventDefault()
e.stopPropagation()
}
const handleDragEnter = (e) => {
e.preventDefault()
e.stopPropagation()
isDragOver.value = true
}
const handleDragLeave = (e) => {
e.preventDefault()
e.stopPropagation()
// 只有当离开整个拖拽区域时才设置为false
if (!e.currentTarget.contains(e.relatedTarget)) {
isDragOver.value = false
}
}
const handleImageDrop = (e) => {
e.preventDefault()
e.stopPropagation()
isDragOver.value = false
const files = Array.from(e.dataTransfer.files)
const imageFile = files.find(file => file.type.startsWith('image/'))
if (imageFile) {
// 创建FileList对象来模拟input[type="file"]的change事件
const dataTransfer = new DataTransfer()
dataTransfer.items.add(imageFile)
const fileList = dataTransfer.files
// 创建模拟的change事件
const event = {
target: {
files: fileList
}
}
handleImageUpload(event)
showAlert('图片拖拽上传成功', 'success')
} else {
showAlert('请拖拽图片文件', 'warning')
}
}
const handleAudioDrop = (e) => {
e.preventDefault()
e.stopPropagation()
isDragOver.value = false
const files = Array.from(e.dataTransfer.files)
const audioFile = files.find(file => file.type.startsWith('audio/'))
if (audioFile) {
// 创建FileList对象来模拟input[type="file"]的change事件
const dataTransfer = new DataTransfer()
dataTransfer.items.add(audioFile)
const fileList = dataTransfer.files
// 创建模拟的change事件
const event = {
target: {
files: fileList
}
}
handleAudioUpload(event)
showAlert('音频拖拽上传成功', 'success')
} else {
showAlert('请拖拽音频文件', 'warning')
}
}
// 组件卸载时清理
onUnmounted(() => {
if (resizeHandler) {
window.removeEventListener('resize', resizeHandler)
}
})
</script>
<template>
<!-- 主内容区域 - 响应式布局 -->
<div class="flex-1 flex flex-col min-h-0 mobile-content main-scrollbar content-area">
<!-- 生成视频区域 -->
<div class="flex-1 flex flex-col">
<!-- 内容区域 -->
<div class="flex-1 p-6">
<!-- 任务创建面板 -->
<div class="max-w-4xl mx-auto" id="task-creator">
<!-- 合并的创作区域 -->
<div class="creation-area-container">
<div class="default-state-container">
<!-- 两个并列的下拉菜单 -->
<div class="flex justify-center gap-10 mb-6">
<!-- 任务类型下拉菜单 -->
<ModelDropdown
:available-models="availableTaskTypes.map(taskType => getTaskTypeName(taskType))"
:selected-model="getTaskTypeName(selectedTaskId)"
@select-model="selectTask"
/>
<!-- 模型选择下拉菜单 -->
<ModelDropdown
:available-models="availableModelClasses"
:selected-model="getCurrentForm().model_cls"
@select-model="selectModel"
/>
</div>
<!-- 默认状态:中心文字 -->
<div v-show="!isCreationAreaExpanded" class="flex flex-col items-center justify-center">
<div class="text-center">
<h2 class="text-4xl font-bold text-laser-purple mb-6">{{ t('whatDoYouWantToDo') }}</h2>
<!-- 动态滚动提示 -->
<div class="hint-container mb-8 pb-10">
<div class="hint-text text-gray-400 text-lg min-h-[60px] flex items-center justify-center">
<transition name="hint-fade" mode="out-in">
<p :key="currentHintIndex" class="text-center">
{{ currentTaskHints[currentHintIndex] }}
</p>
</transition>
</div>
<!-- 提示指示器 -->
<div class="flex justify-center mt-4 space-x-2">
<div v-for="(hint, index) in currentTaskHints" :key="index"
class="w-2 h-2 rounded-full transition-all duration-300"
:class="index === currentHintIndex ? 'bg-laser-purple' : 'bg-gray-600'">
</div>
</div>
</div>
</div>
<!-- 展开开关 -->
<div class="relative group cursor-pointer max-w-3/5" @click="expandCreationArea">
<button
class="relative w-full bg-dark-light/80 border border-laser-purple rounded-full pl-10 pr-10 py-6 text-base hover:border-laser-purple transition-all duration-300 resize-none hover:shadow-2xl"
>
<i class="fi fi-sr-cursor-finger-click text-lg text-gradient-icon transition-all duration-300 pointer-events-none group-hover:drop-shadow-[0_0_8px_rgba(168,85,247,0.6)] group-hover:animate-pulse"></i>
<span class="pl-2 text-base font-bold text-gradient-icon transition-all duration-300 pointer-events-none group-hover:drop-shadow-[0_0_8px_rgba(168,85,247,0.6)] group-hover:animate-pulse">{{ t('startCreatingVideo') }}</span>
</button>
<!-- 装饰性边框 -->
<div class="absolute inset-0 rounded-full border border-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
</div>
</div>
<!-- 展开状态:素材区域 -->
<div v-if="isCreationAreaExpanded" class="mb-8 prompt-input-section">
<!-- 中心文字 -->
<div class="text-center">
<h2 class="text-4xl font-bold text-laser-purple mb-6 animate-fade-in">{{ t('whatMaterialsDoYouNeed') }}</h2>
<p class="text-gray-400 text-lg mb-8 transition-all duration-300">
<span v-if="selectedTaskId === 't2v'"
class="inline-block animate-fade-in">{{ t('pleaseEnterTheMostDetailedVideoScript') }}</span>
<span v-else-if="selectedTaskId === 'i2v'"
class="inline-block animate-fade-in">{{ t('pleaseUploadAnImageAsTheFirstFrameOfTheVideoAndTheMostDetailedVideoScript') }}</span>
<span v-else-if="selectedTaskId === 's2v'"
class="inline-block animate-fade-in">{{ t('pleaseUploadARoleImageAnAudioAndTheGeneralVideoRequirements') }}</span>
<span v-else
class="inline-block animate-fade-in">选择任务类型开始创作您的视频</span>
</p>
</div>
<!-- 收缩开关 -->
<div
class="creation-area transition-all duration-500 ease-out max-w-10xl mx-auto"
@click.stop>
<!-- 收起按钮 -->
<div class="flex justify-center mb-4">
<button @click="contractCreationArea"
class="flex items-center gap-2 px-4 py-2 text-gray-400 hover:text-laser-purple transition-all duration-300 hover:bg-laser-purple/10 rounded-lg group"
:class="{ 'animate-pulse': isContracting }">
<i class="fas fa-compress-alt text-sm transition-transform duration-200 group-hover:scale-110"
:class="{ 'animate-spin': isContracting }"></i>
<span class="text-sm font-medium">
{{ t('collapseCreationArea') }}
</span>
<i class="fas fa-chevron-up text-xs transition-transform duration-200 group-hover:translate-y-[-2px]"
:class="{ 'animate-bounce': isContracting }"></i>
</button>
</div>
<div v-if="selectedTaskId === 'i2v' || selectedTaskId === 's2v'" class="upload-section">
<!-- 上传图片 -->
<div v-if="selectedTaskId === 'i2v' || selectedTaskId === 's2v'">
<!-- 图片历史和素材库 -->
<div class="flex justify-between items-center mb-2">
<label class="block text-sm text-gray-400 items-center">
{{ t('image') }}
</label>
</div>
<!-- 上传图片 -->
<div class="upload-area"
@drop="handleImageDrop"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
:class="{ 'drag-over': isDragOver }"
>
<!-- 默认上传界面 -->
<div v-if="!getCurrentImagePreview()" class="upload-content">
<p class="text-base text-white font-bold mb-4">{{ t('uploadImage') }}</p>
<p class="text-xs text-gray-400 mb-4">{{ t('supportedImageFormats') }}</p>
<div class="flex items-center justify-center space-x-6">
<div class="flex flex-col items-center space-y-2">
<button
class="w-12 h-12 flex items-center justify-center bg-white/15 text-white p-3 rounded-full transition-all duration-200 hover:scale-110 shadow-lg"
@click="triggerImageUpload"
:title="t('uploadImage')">
<i class="fas fa-upload text-lg"></i>
</button>
<span class="text-xs text-gray-300">{{ t('upload') }}</span>
</div>
<div class="flex flex-col items-center space-y-2">
<button
@click.stop="showImageTemplates = true; mediaModalTab = 'history'; getImageHistory()"
class="w-12 h-12 flex items-center justify-center bg-white/15 text-white p-3 rounded-full transition-all duration-200 hover:scale-110 shadow-lg"
:title="t('templates')">
<i class="fas fa-history text-lg"></i>
</button>
<span class="text-xs text-gray-300">{{ t('templates') }}</span>
</div>
</div>
</div>
<!-- 图片预览 -->
<div v-if="getCurrentImagePreview()" class="image-preview group">
<img :src="getCurrentImagePreviewUrl()" alt="t('previewImage')"
class="w-full h-full object-cover rounded-lg transition-all duration-300 group-hover:brightness-50">
<!-- 悬停时显示的操作按钮,位置在中下方 -->
<div
class="absolute inset-x-0 bottom-4 flex items-center justify-center opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-300">
<div class="flex space-x-3">
<button @click.stop="removeImage"
class="w-12 h-12 flex items-center justify-center bg-white/15 text-white p-3 rounded-full transition-all duration-200 hover:scale-110 shadow-lg"
:title="t('deleteImage')">
<i class="fas fa-trash text-lg"></i>
</button>
</div>
</div>
</div>
<input type="file" ref="imageInput" @change="handleImageUpload" accept="image/*"
style="display: none;">
</div>
</div>
<!-- 上传音频 -->
<div v-if="selectedTaskId === 's2v'">
<!-- 音频历史和素材库 -->
<div class="flex justify-between items-center mb-2">
<label class="block text-sm text-gray-400 flex items-center">
{{ t('audio') }}
</label>
</div>
<!-- 上传音频 -->
<div class="upload-area"
@drop="handleAudioDrop"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
:class="{ 'drag-over': isDragOver }"
>
<!-- 默认上传界面 -->
<div v-if="!getCurrentAudioPreview()" class="upload-content"
>
<p class="text-base text-white font-bold mb-4">{{ t('uploadAudio') }}</p>
<p class="text-xs text-gray-400 mb-4">{{ t('supportedAudioFormats') }}</p>
<div class="flex items-center justify-center space-x-6">
<div class="flex flex-col items-center space-y-2">
<button
@click.stop="showAudioTemplates = true; mediaModalTab = 'history'; getAudioHistory()"
class="w-12 h-12 flex items-center justify-center bg-white/15 text-white p-3 rounded-full transition-all duration-200 hover:scale-110 shadow-lg"
:title="t('templates')">
<i class="fas fa-history text-lg"></i>
</button>
<span class="text-xs text-gray-300">{{ t('templates') }}</span>
</div>
<div class="flex flex-col items-center space-y-2">
<button
class="w-12 h-12 flex items-center justify-center bg-white/15 text-white p-3 rounded-full transition-all duration-200 hover:scale-110 shadow-lg"
@click="triggerAudioUpload"
:title="t('uploadAudio')">
<i class="fas fa-upload text-lg"></i>
</button>
<span class="text-xs text-gray-300">{{ t('upload') }}</span>
</div>
<div class="flex flex-col items-center space-y-2">
<button @click.stop="isRecording ? stopRecording() : startRecording()"
class="w-12 h-12 flex items-center justify-center bg-white/15 text-white p-3 rounded-full transition-all duration-200 hover:scale-110 shadow-lg"
:title="isRecording ? t('stopRecording') : t('recordAudio')"
:class="{ 'bg-red-500/80': isRecording }">
<i class="fas fa-microphone text-lg" :class="{ 'animate-pulse': isRecording, 'text-red-500': isRecording }"></i>
</button>
<span class="text-xs text-gray-300">{{ isRecording ? formatRecordingDuration(recordingDuration) : t('recordAudio') }}</span>
</div>
</div>
</div>
<!-- 音频预览 -->
<div v-if="getCurrentAudioPreview()" class="audio-preview group">
<audio controls class="w-full h-full" @error="handleAudioError" @loadstart="console.log('音频开始加载')" @canplay="console.log('音频可以播放')">
<source :src="getCurrentAudioPreviewUrl()" :type="getAudioMimeType()" preload="metadata">
</audio>
<!-- 悬停时显示的操作按钮,位置在中下方 -->
<div
class="absolute inset-x-0 bottom-4 flex items-center justify-center opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-300">
<div class="flex space-x-3">
<button @click.stop="removeAudio"
class="w-12 h-12 flex items-center justify-center bg-white/15 text-white p-3 rounded-full transition-all duration-200 hover:scale-110 shadow-lg"
:title="t('deleteAudio')">
<i class="fas fa-trash text-lg"></i>
</button>
</div>
</div>
</div>
<input type="file" ref="audioInput" @change="handleAudioUpload" accept="audio/*"
style="display: none;">
</div>
</div>
</div>
<!-- 提示词输入区域 -->
<div class="flex justify-between items-center mb-2">
<label class="block text-sm text-gray-400 flex items-center">
{{ t('prompt') }}
<button @click="showPromptModal = true; promptModalTab = 'templates'"
class="text-xs text-gray-400 hover:text-gradient-primary transition-colors pl-3"
:title="t('promptTemplates')">
<i class="fas fa-lightbulb"></i>
</button>
</label>
<div class="text-xs text-gray-400">
{{ getCurrentForm().prompt?.length || 0 }} / 1000
</div>
</div>
<div class="relative group cursor-pointer">
<textarea v-model="getCurrentForm().prompt"
class="relative w-full bg-dark-light/80 border border-laser-purple/30 rounded-xl p-6 text-base min-h-[100px] focus:ring-1 transition-all duration-300 resize-none main-scrollbar focus:shadow-2x placeholder-gray-500"
:placeholder="getPromptPlaceholder()"
rows="4"
maxlength="500"
required></textarea>
<!-- 装饰性边框 -->
<div class="absolute inset-0 rounded-xl border border-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
</div>
<div class="flex justify-between items-center">
<button @click="clearPrompt"
class="flex items-center text-sm rounded-lg px-2 transition-all duration-200 text-gray-400 hover:text-laser-purple hover:bg-laser-purple/10 group">
<i class="fas fa-sync-alt text-sm mr-2 group-hover:rotate-180 transition-transform duration-300"></i>
{{ t('clear') }}
</button>
</div>
<!-- 提交按钮 -->
<div class="flex justify-center mt-6">
<button @click="submitTask" :disabled="submitting || templateLoading"
class="generate-button btn-primary"
:class="{ 'disabled': submitting || templateLoading }">
<i v-if="submitting" class="fas fa-spinner fa-spin text-xl mr-3"></i>
<i v-else-if="templateLoading" class="fas fa-spinner fa-spin text-xl mr-3"></i>
<i v-else class="fas fa-play text-xl mr-3"></i>
{{ submitting ? t('submitting') : templateLoading ? '模板加载中...' : t('generateVideo') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 精选模版区域 -->
<div v-if="currentFeaturedTemplates.length > 0" class="flex-1 flex flex-col min-h-0 border-t lg:border-t-0 lg:border-l border-gray-700/30">
<div class="flex-1 p-4 lg:p-6">
<!-- 控制区域 -->
<div class="flex-col flex items-center justify-center mb-4 lg:mb-6 gap-3 lg:gap-4">
<!-- 左侧:发现文字和随机按钮 -->
<div class="flex items-center gap-3 lg:gap-4">
<h2 class="text-2xl lg:text-3xl font-bold text-white">{{ t('discover') }}</h2>
<!-- 随机图标按钮 -->
<button @click="refreshRandomTemplates"
:disabled="featuredTemplatesLoading"
class="w-8 h-8 lg:w-10 lg:h-10 flex items-center justify-center bg-laser-purple/20 hover:bg-laser-purple/40 text-laser-purple rounded-full transition-all duration-300 hover:scale-110"
:title="t('refreshRandomTemplates')">
<i class="fas fa-random text-sm lg:text-lg"
:class="{ 'animate-spin': featuredTemplatesLoading }"></i>
</button>
</div>
<!-- 右侧:更多按钮 -->
<button @click="switchToInspirationView()"
class="flex items-center gap-2 px-3 py-2 text-gray-400 hover:text-white hover:bg-gray-600/20 rounded transition-all duration-200"
:title="t('viewMore')">
<span class="text-sm">{{ t('more') }}</span>
<i class="fas fa-arrow-right text-xs"></i>
</button>
</div>
<!-- 精选模版随机列布局 -->
<div class="relative min-h-[400px] lg:min-h-[600px]">
<!-- 随机列 -->
<div v-for="(column, columnIndex) in templatesWithRandomColumns.columns" :key="columnIndex"
class="absolute transition-all duration-500 animate-fade-in"
:style="{
width: column.width,
left: column.left,
top: column.top,
animationDelay: `${columnIndex * 0.2}s`
}">
<!-- 列内的模版卡片 -->
<div v-for="item in column.templates" :key="item.task_id"
class="mb-3 group relative bg-dark-light rounded-xl overflow-hidden border border-gray-700/50 hover:border-laser-purple/40 transition-all duration-300 hover:shadow-laser/20">
<!-- 视频缩略图区域 -->
<div class="cursor-pointer bg-gray-800 relative flex flex-col"
@click="previewTemplateDetail(item)"
:title="t('viewTemplateDetail')">
<!-- 视频预览 -->
<video v-if="item?.outputs?.output_video"
:src="getTemplateFileUrl(item.outputs.output_video,'videos')"
:poster="getTemplateFileUrl(item.inputs.input_image,'images')"
class="w-full h-auto object-contain group-hover:scale-105 transition-transform duration-300"
preload="auto" playsinline webkit-playsinline
@mouseenter="playVideo($event)" @mouseleave="pauseVideo($event)"
@loadeddata="onVideoLoaded($event)"
@ended="onVideoEnded($event)"
@error="onVideoError($event)"></video>
<!-- 图片缩略图 -->
<img v-else
:src="getTemplateFileUrl(item.inputs.input_image,'images')"
:alt="item.params?.prompt || '模板图片'"
class="w-full h-auto object-contain group-hover:scale-105 transition-transform duration-300"
@error="handleThumbnailError" />
<!-- 移动端播放按钮 -->
<button v-if="item?.outputs?.output_video"
@click.stop="toggleVideoPlay($event)"
class="md:hidden absolute bottom-3 left-1/2 transform -translate-x-1/2 w-10 h-10 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white hover:bg-black/70 transition-colors z-20">
<i class="fas fa-play text-sm"></i>
</button>
<!-- 悬浮操作按钮(下方居中,仅桌面端) -->
<div
class="hidden md:flex absolute bottom-3 left-1/2 transform -translate-x-1/2 items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10 w-full">
<div class="flex space-x-3 pointer-events-auto">
<button @click.stop="applyTemplateImage(item)"
class="w-10 h-10 rounded-full bg-laser-purple backdrop-blur-sm flex items-center justify-center text-white hover:bg-laser-purple transition-colors"
:title="t('applyImage')">
<i class="fas fa-image text-sm"></i>
</button>
<button @click.stop="applyTemplateAudio(item)"
class="w-10 h-10 rounded-full bg-laser-purple backdrop-blur-sm flex items-center justify-center text-white hover:bg-laser-purple transition-colors"
:title="t('applyAudio')">
<i class="fas fa-music text-sm"></i>
</button>
<button @click.stop="useTemplate(item)"
class="w-10 h-10 rounded-full bg-laser-purple backdrop-blur-sm flex items-center justify-center text-white hover:bg-laser-purple transition-colors"
:title="t('useTemplate')">
<i class="fas fa-clone text-sm"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<MediaTemplate />
</template>
<style scoped>
/* 生成按钮样式 - 简约大气 */
.generate-button {
padding: 1rem 3rem;
background: #8b5cf6;
border: none;
border-radius: 0.5rem;
color: white;
font-size: 1.125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 200px;
letter-spacing: 0.025em;
/* 简约阴影 */
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
.generate-button:hover:not(.disabled) {
background: #7c3aed;
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
}
.generate-button:active:not(.disabled) {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3);
}
.generate-button.disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
background: #6b7280;
box-shadow: 0 2px 8px rgba(107, 114, 128, 0.2);
}
.generate-button.disabled:hover {
transform: none;
box-shadow: 0 2px 8px rgba(107, 114, 128, 0.2);
}
/* 响应式设计 */
@media (max-width: 768px) {
.generate-button {
padding: 0.875rem 2.5rem;
font-size: 1rem;
min-width: 180px;
}
}
/* 拖拽样式 */
.upload-area.drag-over {
border-color: #8b5cf6 !important;
background: rgba(139, 92, 246, 0.1) !important;
transform: scale(1.02);
transition: all 0.3s ease;
}
.upload-area.drag-over .upload-content {
opacity: 0.7;
}
.upload-area.drag-over::before {
content: '拖拽文件到这里';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #8b5cf6;
font-size: 1.125rem;
font-weight: 600;
z-index: 10;
pointer-events: none;
}
</style>
<script setup>
import { showImageZoomModal, closeImageZoomModal, zoomedImageUrl } from '../utils/other'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<!-- 图片放大弹窗 -->
<div v-cloak>
<div v-if="showImageZoomModal" class="fixed inset-0 bg-black/80 z-60 flex items-center justify-center p-4"
@click="closeImageZoomModal">
<div class="relative max-w-4xl max-h-[90vh] bg-secondary rounded-xl overflow-hidden" @click.stop>
<div class="flex items-center justify-between p-4 border-b border-gray-700">
<h3 class="text-lg font-medium text-white">{{ t('imagePreview') }}</h3>
<button @click="closeImageZoomModal"
class="text-gray-400 hover:text-white transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="p-4">
<img :src="zoomedImageUrl" :alt="'图片预览'"
class="w-full h-auto max-h-[70vh] object-contain rounded-lg" />
</div>
</div>
</div>
</div>
</template>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment