Merge pull request #870 from zanllp/feature/topicSearch
feat: experimental natural-language categorization & search (persistent scope, clustering, retrieval)pull/876/head
commit
3ef90cface
48
.env.example
48
.env.example
|
|
@ -29,15 +29,15 @@ IIB_DB_FILE_BACKUP_MAX=8
|
|||
# The default value is 'auto', which will be determined based on the command-line parameters used to start sd-webui (such as --server-name, --share, --listen).
|
||||
IIB_ACCESS_CONTROL=auto
|
||||
|
||||
# This variable is used to define a list of allowed paths for the application to access when access control mode is enabled.
|
||||
# This variable is used to define a list of allowed paths for the application to access when access control mode is enabled.
|
||||
# It can be set to a comma-separated string of file paths or directory paths, representing the resources that are allowed to be accessed by the application.
|
||||
# In addition, if sd_webui_config or sd_webui_dir has been configured, or if you're running this repository as an extension of sd-webui,
|
||||
# In addition, if sd_webui_config or sd_webui_dir has been configured, or if you're running this repository as an extension of sd-webui,
|
||||
# you can use the following shortcuts (txt2img, img2img, extra, save) as values for the ALLOWED_PATHS variable.
|
||||
# IIB_ACCESS_CONTROL_ALLOWED_PATHS=save,extra,/output ...etc
|
||||
|
||||
# This variable is used to control fine-grained access control for different types of requests, but only if access control mode is enabled.
|
||||
# It can be set to a string value that represents a specific permission or set of permissions, such as "read-only", "write-only", "read-write", or "no-access".
|
||||
# This variable can be used to restrict access to certain API endpoints or data sources based on the permissions required by the user.
|
||||
# This variable is used to control fine-grained access control for different types of requests, but only if access control mode is enabled.
|
||||
# It can be set to a string value that represents a specific permission or set of permissions, such as "read-only", "write-only", "read-write", or "no-access".
|
||||
# This variable can be used to restrict access to certain API endpoints or data sources based on the permissions required by the user.
|
||||
# IIB_ACCESS_CONTROL_PERMISSION=read-write
|
||||
|
||||
|
||||
|
|
@ -47,3 +47,41 @@ IIB_ACCESS_CONTROL=auto
|
|||
# Due to the high performance cost of parsing this type of file, it is disabled by default.
|
||||
# Set to 'true' to enable it.
|
||||
IIB_ENABLE_SD_WEBUI_STEALTH_PARSER=false
|
||||
|
||||
|
||||
# ---------------------------- AI / EMBEDDINGS ----------------------------
|
||||
# OpenAI-compatible API base url.
|
||||
# - Keep it generic here (do NOT write any provider-specific or personal info into this example file).
|
||||
# - Typical format: https://your-openai-compatible-host/v1
|
||||
# - This project uses it for BOTH embeddings (/embeddings) and topic-title chat (/chat/completions).
|
||||
OPENAI_BASE_URL=
|
||||
|
||||
# OpenAI-compatible API key (Bearer token).
|
||||
# - Put your real key in x.env (or .env) only, never commit it.
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# Embedding model id (OpenAI-compatible).
|
||||
# - Used for clustering images by prompt semantics.
|
||||
# - Recommended: start with a small/cheap embedding model; switch to a larger one only if clustering quality is insufficient.
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
# Default chat model id (OpenAI-compatible).
|
||||
# - Used by the generic /ai_chat endpoint (if you use it) and as a fallback default.
|
||||
# - If your provider applies content filtering that sometimes returns empty output, try switching to another chat model here.
|
||||
AI_MODEL=gpt-4o-mini
|
||||
|
||||
# Topic title model id (OpenAI-compatible).
|
||||
# - Used ONLY for generating short cluster titles/keywords (topic naming).
|
||||
# - If not set, it falls back to AI_MODEL.
|
||||
# - Tip: if a model frequently violates "JSON only" or gets truncated, switch this model first.
|
||||
TOPIC_TITLE_MODEL=gpt-4o-mini
|
||||
|
||||
# Prompt normalization before embedding/clustering (remove boilerplate but keep distinctiveness).
|
||||
# - Removes common "quality / camera / resolution" boilerplate from prompts before embedding.
|
||||
# - Keep enabled for better clustering; disable only for debugging.
|
||||
IIB_PROMPT_NORMALIZE=1
|
||||
|
||||
# Prompt normalization mode:
|
||||
# - 'balanced' (recommended): remove generic boilerplate but keep some discriminative style words.
|
||||
# - 'theme_only' (aggressive): focus more on subject/theme nouns; may lose some stylistic distinctiveness.
|
||||
IIB_PROMPT_NORMALIZE_MODE=balanced
|
||||
|
|
|
|||
|
|
@ -20,3 +20,6 @@ plugins/*
|
|||
!plugins/.gitkeep
|
||||
test_data/*
|
||||
.DS_Store
|
||||
iib_output
|
||||
iib.db-shm
|
||||
iib.db-wal
|
||||
|
|
|
|||
79
README-zh.md
79
README-zh.md
|
|
@ -159,4 +159,83 @@ https://user-images.githubusercontent.com/25872019/230768207-daab786b-d4ab-489f-
|
|||
|
||||
<img width="768" alt="image" src="https://user-images.githubusercontent.com/25872019/230064879-c95866ac-999d-4d4b-87ea-3e38c8479415.png">
|
||||
|
||||
## 自然语言分类&搜索(实验性)
|
||||
|
||||
这个功能用于把图片按**提示词语义相似度**自动分组(主题),并支持用一句自然语言做**语义检索**(类似 RAG 的召回阶段)。
|
||||
它是实验性功能:效果强依赖模型与提示词质量,适合快速找回/整理生成图片。
|
||||
|
||||
### 使用方式(面向使用者)
|
||||
|
||||
1. 打开首页「**自然语言分类&搜索(实验性)**」
|
||||
2. 点击「范围」选择要处理的文件夹(可多选,来源于 QuickMovePaths)
|
||||
3. **归类**:点「刷新」会在所选范围内生成主题列表(标题会按前端语言输出)
|
||||
4. **搜索**:输入一句话点「搜索」,会自动打开结果页(TopK 相似图片)
|
||||
|
||||
> 选择的范围会持久化到后端 KV(`app_fe_setting["topic_search_scope"]`),下次打开会自动恢复并自动刷新一次结果。
|
||||
|
||||
### 接口(给高级用户/二次开发)
|
||||
|
||||
- **构建/刷新向量**:`POST /infinite_image_browsing/db/build_iib_output_embeddings`
|
||||
- 入参:`folder`, `model`, `force`, `batch_size`, `max_chars`
|
||||
- **归类(聚类)**:`POST /infinite_image_browsing/db/cluster_iib_output_job_start`,然后轮询 `GET /infinite_image_browsing/db/cluster_iib_output_job_status?job_id=...`
|
||||
- 入参:`folder_paths`(必填,数组)、`threshold`, `min_cluster_size`, `force_embed`, `title_model`, `force_title`, `use_title_cache`, `assign_noise_threshold`, `lang`
|
||||
- **语义检索(RAG 召回)**:`POST /infinite_image_browsing/db/search_iib_output_by_prompt`
|
||||
- 入参:`query`, `folder_paths`(必填,数组)、`top_k`, `min_score`, `ensure_embed`, `model`, `max_chars`
|
||||
|
||||
### 原理(简单版)
|
||||
|
||||
- **1)提示词抽取与清洗**
|
||||
- 从 `image.exif` 中抽取提示词文本(只取 `Negative prompt:` 之前)
|
||||
- 可选做“语义清洗”:去掉无意义的高频模板词(画质/摄影参数等),更聚焦主题语义(见 `IIB_PROMPT_NORMALIZE*`)
|
||||
- **2)向量化(Embedding)**
|
||||
- 调用 OpenAI 兼容的 `/embeddings` 得到向量
|
||||
- 写入 SQLite 表 `image_embedding`(增量更新,避免重复花费)
|
||||
- **3)主题聚类**
|
||||
- 用“簇向量求和方向”的增量聚类(近似在线聚类),再把高相似簇做一次合并(减少同主题被切碎)
|
||||
- 可选把小簇成员重新分配到最相近的大簇,降低噪声
|
||||
- **4)主题命名(LLM)**
|
||||
- 对每个簇取代表提示词样本,调用 `/chat/completions` 生成短标题与关键词
|
||||
- 通过 tool/function calling 强制结构化输出(JSON),并写入 `topic_title_cache`
|
||||
- **5)语义检索**
|
||||
- 把用户 query 向量化,然后和范围内所有图片向量做余弦相似度排序,返回 TopK
|
||||
|
||||
### 缓存与增量更新
|
||||
|
||||
#### 1)向量缓存(`image_embedding`)
|
||||
|
||||
- **存储位置**:SQLite 表 `image_embedding`(以 `image_id` 为主键)
|
||||
- **增量跳过条件**:满足以下条件则跳过重新向量化:
|
||||
- `model` 相同
|
||||
- `text_hash` 相同
|
||||
- 已存在 `vec`
|
||||
- **“重新向量化”的缓存键**:`text_hash = sha256(f"{normalize_version}:{prompt_text}")`
|
||||
- `prompt_text`:用于 embedding 的最终文本(抽取 + 可选清洗)
|
||||
- `normalize_version`:由代码对清洗规则/模式计算出的**指纹**(不允许用户用环境变量手动覆盖)
|
||||
- **强制刷新**:在 `build_iib_output_embeddings` 传 `force=true`,或在 `cluster_iib_output_job_start` 传 `force_embed=true`
|
||||
|
||||
#### 2)标题缓存(`topic_title_cache`)
|
||||
|
||||
- **存储位置**:SQLite 表 `topic_title_cache`(主键 `cluster_hash`)
|
||||
- **命中条件**:`use_title_cache=true` 且 `force_title=false` 时复用历史标题/关键词
|
||||
- **缓存键 `cluster_hash` 包含**:
|
||||
- 成员图片 id(排序后)
|
||||
- embedding `model`、`threshold`、`min_cluster_size`
|
||||
- `title_model`、输出语言 `lang`
|
||||
- 语义清洗指纹(`normalize_version`)与清洗模式
|
||||
- **强制重新生成标题**:`force_title=true`
|
||||
|
||||
### 配置(环境变量)
|
||||
|
||||
所有 AI 调用都基于 **OpenAI 兼容** 的服务:
|
||||
|
||||
- **`OPENAI_BASE_URL`**:例如 `https://your-host/v1`
|
||||
- **`OPENAI_API_KEY`**:你的 Key
|
||||
- **`EMBEDDING_MODEL`**:用于聚类的 embedding 模型
|
||||
- **`AI_MODEL`**:默认 chat 模型(兜底默认)
|
||||
- **`TOPIC_TITLE_MODEL`**:用于主题标题的 chat 模型(不配则回退到 `AI_MODEL`)
|
||||
- **`IIB_PROMPT_NORMALIZE`**:`1/0` 是否开启提示词清洗
|
||||
- **`IIB_PROMPT_NORMALIZE_MODE`**:`balanced`(推荐)/ `theme_only`(更激进)
|
||||
|
||||
> 注意:AI 调用**没有 mock 兜底**。只要服务端/模型返回异常或不符合约束,就会直接报错,避免产生“看似能跑但其实不可信”的结果。
|
||||
|
||||
|
||||
|
|
|
|||
79
README.md
79
README.md
|
|
@ -176,3 +176,82 @@ https://user-images.githubusercontent.com/25872019/230768207-daab786b-d4ab-489f-
|
|||
### Dark mode
|
||||
|
||||
<img width="768" alt="image" src="https://user-images.githubusercontent.com/25872019/230064879-c95866ac-999d-4d4b-87ea-3e38c8479415.png">
|
||||
|
||||
## Natural Language Categorization & Search (Experimental)
|
||||
|
||||
This feature groups images by **semantic similarity of prompts** and supports **natural-language retrieval** (similar to the retrieval stage in RAG).
|
||||
It’s experimental: results depend on the embedding/chat models and the quality of prompt metadata.
|
||||
|
||||
### How to Use (for end users)
|
||||
|
||||
1. Open **“Natural Language Categorization & Search (Experimental)”** from the startup page
|
||||
2. Click **Scope** and select one or more folders (from QuickMovePaths)
|
||||
3. **Categorize**: click **Refresh** to generate topic cards for the selected scope
|
||||
4. **Search**: type a natural-language query and click **Search** (auto-opens the result grid)
|
||||
|
||||
> The selected scope is persisted in backend KV: `app_fe_setting["topic_search_scope"]`. Next time it will auto-restore and auto-refresh once.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- **Build/refresh embeddings**: `POST /infinite_image_browsing/db/build_iib_output_embeddings`
|
||||
- Request: `folder`, `model`, `force`, `batch_size`, `max_chars`
|
||||
- **Cluster (categorize)**: `POST /infinite_image_browsing/db/cluster_iib_output_job_start` then poll `GET /infinite_image_browsing/db/cluster_iib_output_job_status?job_id=...`
|
||||
- Request: `folder_paths` (required, array), `threshold`, `min_cluster_size`, `force_embed`, `title_model`, `force_title`, `use_title_cache`, `assign_noise_threshold`, `lang`
|
||||
- **Prompt retrieval (RAG-like)**: `POST /infinite_image_browsing/db/search_iib_output_by_prompt`
|
||||
- Request: `query`, `folder_paths` (required, array), `top_k`, `min_score`, `ensure_embed`, `model`, `max_chars`
|
||||
|
||||
### How it Works (simple explanation)
|
||||
|
||||
- **1) Prompt extraction & normalization**
|
||||
- Reads `image.exif` and keeps content before `Negative prompt:`
|
||||
- Optionally removes “boilerplate” terms (quality/photography parameters, etc.) to focus on topic semantics (`IIB_PROMPT_NORMALIZE*`)
|
||||
- **2) Embeddings**
|
||||
- Calls OpenAI-compatible `/embeddings`
|
||||
- Stores vectors in SQLite table `image_embedding` (incremental, to avoid repeated costs)
|
||||
- **3) Clustering**
|
||||
- Online centroid-sum clustering, plus a post-merge step for highly similar clusters
|
||||
- Optionally reassigns members of small clusters into the closest large cluster to reduce noise
|
||||
- **4) Title generation (LLM)**
|
||||
- Calls `/chat/completions` with tool/function calling to force structured JSON output
|
||||
- Stores titles/keywords in SQLite table `topic_title_cache`
|
||||
- **5) Retrieval**
|
||||
- Embeds the query and ranks images in the selected scope by cosine similarity, returning TopK
|
||||
|
||||
### Caching & Incremental Updates
|
||||
|
||||
#### 1) Embedding cache (`image_embedding`)
|
||||
|
||||
- **Where**: table `image_embedding` (keyed by `image_id`)
|
||||
- **Skip rule (incremental update)**: an image is skipped if:
|
||||
- same `model`
|
||||
- same `text_hash`
|
||||
- existing `vec` is present
|
||||
- **Re-vectorization cache key**: `text_hash = sha256(f"{normalize_version}:{prompt_text}")`
|
||||
- `prompt_text` is the extracted + (optionally) normalized text used for embeddings
|
||||
- `normalize_version` is a **code-derived fingerprint** of normalization rules/mode (not user-configurable)
|
||||
- **Force rebuild**: pass `force=true` to `build_iib_output_embeddings` or `force_embed=true` to `cluster_iib_output_job_start`
|
||||
|
||||
#### 2) Title cache (`topic_title_cache`)
|
||||
|
||||
- **Where**: table `topic_title_cache` keyed by `cluster_hash`
|
||||
- **Hit rule**: when `use_title_cache=true` and `force_title=false`, titles/keywords are reused
|
||||
- **Cache key (`cluster_hash`) includes**:
|
||||
- member image IDs (sorted)
|
||||
- embedding `model`, `threshold`, `min_cluster_size`
|
||||
- `title_model`, output `lang`
|
||||
- normalization fingerprint (`normalize_version`) and mode
|
||||
- **Force title regeneration**: `force_title=true`
|
||||
|
||||
### Configuration (Environment Variables)
|
||||
|
||||
All calls use an **OpenAI-compatible** provider:
|
||||
|
||||
- **`OPENAI_BASE_URL`**: e.g. `https://your-host/v1`
|
||||
- **`OPENAI_API_KEY`**: your API key
|
||||
- **`EMBEDDING_MODEL`**: embeddings model used for clustering
|
||||
- **`AI_MODEL`**: default chat model (fallback)
|
||||
- **`TOPIC_TITLE_MODEL`**: chat model used for cluster titles (falls back to `AI_MODEL`)
|
||||
- **`IIB_PROMPT_NORMALIZE`**: `1/0` enable prompt normalization
|
||||
- **`IIB_PROMPT_NORMALIZE_MODE`**: `balanced` (recommended) / `theme_only`
|
||||
|
||||
> Note: There is **no mock fallback** for AI calls. If the provider/model fails or returns invalid output, the API will return an error directly.
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ Promise.resolve().then(async () => {
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Infinite Image Browsing</title>
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-5ed9cd5a.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-c290c403.css">
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8b1d4076.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-d385cc4f.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -7,4 +7,7 @@ pillow-avif-plugin
|
|||
imageio
|
||||
av>=14,<15
|
||||
lxml
|
||||
filetype
|
||||
filetype
|
||||
requests
|
||||
numpy
|
||||
hnswlib
|
||||
|
|
@ -6,7 +6,6 @@ from pathlib import Path
|
|||
import shutil
|
||||
import sqlite3
|
||||
|
||||
|
||||
from scripts.iib.dir_cover_cache import get_top_4_media_info
|
||||
from scripts.iib.tool import (
|
||||
get_created_date_by_stat,
|
||||
|
|
@ -48,6 +47,7 @@ from PIL import Image
|
|||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import hashlib
|
||||
from contextlib import closing
|
||||
from scripts.iib.db.datamodel import (
|
||||
DataBase,
|
||||
ExtraPathType,
|
||||
|
|
@ -58,9 +58,10 @@ from scripts.iib.db.datamodel import (
|
|||
ExtraPath,
|
||||
FileInfoDict,
|
||||
Cursor,
|
||||
GlobalSetting
|
||||
GlobalSetting,
|
||||
)
|
||||
from scripts.iib.db.update_image_data import update_image_data, rebuild_image_index, add_image_data_single
|
||||
from scripts.iib.topic_cluster import mount_topic_cluster_routes
|
||||
from scripts.iib.logger import logger
|
||||
from scripts.iib.seq import seq
|
||||
import urllib.parse
|
||||
|
|
@ -73,6 +74,13 @@ try:
|
|||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
import requests
|
||||
import dotenv
|
||||
|
||||
|
||||
|
||||
# 加载环境变量
|
||||
dotenv.load_dotenv()
|
||||
|
||||
index_html_path = get_data_file_path("vue/dist/index.html") if is_exe_ver else os.path.join(cwd, "vue/dist/index.html") # 在app.py也被使用
|
||||
|
||||
|
|
@ -83,6 +91,17 @@ secret_key = os.getenv("IIB_SECRET_KEY")
|
|||
if secret_key:
|
||||
print("Secret key loaded successfully. ")
|
||||
|
||||
# AI 配置
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
||||
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
AI_MODEL = os.getenv("AI_MODEL", "gpt-4o-mini")
|
||||
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
|
||||
|
||||
print(f"AI Model: {AI_MODEL or 'Not configured'}")
|
||||
print(f"OpenAI Base URL: {OPENAI_BASE_URL}")
|
||||
print(f"OpenAI API Key: {'Configured' if OPENAI_API_KEY else 'Not configured'}")
|
||||
print(f"Embedding Model: {EMBEDDING_MODEL or 'Not configured'}")
|
||||
|
||||
WRITEABLE_PERMISSIONS = ["read-write", "write-only"]
|
||||
|
||||
is_api_writeable = not (os.getenv("IIB_ACCESS_CONTROL_PERMISSION")) or (
|
||||
|
|
@ -376,6 +395,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
)
|
||||
async def delete_files(req: DeleteFilesReq):
|
||||
conn = DataBase.get_conn()
|
||||
|
||||
for path in req.file_paths:
|
||||
check_path_trust(path)
|
||||
try:
|
||||
|
|
@ -390,10 +410,12 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
shutil.rmtree(path)
|
||||
else:
|
||||
close_video_file_reader(path)
|
||||
os.remove(path)
|
||||
txt_path = get_img_geninfo_txt_path(path)
|
||||
|
||||
os.remove(path)
|
||||
if txt_path:
|
||||
os.remove(txt_path)
|
||||
|
||||
img = DbImg.get(conn, os.path.normpath(path))
|
||||
if img:
|
||||
logger.info("delete file: %s", path)
|
||||
|
|
@ -409,6 +431,8 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
)
|
||||
raise HTTPException(400, detail=error_msg)
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
class CreateFoldersReq(BaseModel):
|
||||
dest_folder: str
|
||||
|
||||
|
|
@ -606,6 +630,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
img.thumbnail((int(w), int(h)))
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
img.save(cache_path, "webp")
|
||||
# print(f"Image cache generated: {path}")
|
||||
|
||||
# 返回缓存文件
|
||||
return FileResponse(
|
||||
|
|
@ -1216,6 +1241,17 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
ImageTag.remove(conn, image_id=req.img_id, tag_id=req.tag_id)
|
||||
|
||||
|
||||
# ===== 主题聚类 / Embedding(拆分到独立模块,减少 api.py 体积)=====
|
||||
mount_topic_cluster_routes(
|
||||
app=app,
|
||||
db_api_base=db_api_base,
|
||||
verify_secret=verify_secret,
|
||||
write_permission_required=write_permission_required,
|
||||
openai_base_url=OPENAI_BASE_URL,
|
||||
openai_api_key=OPENAI_API_KEY,
|
||||
embedding_model=EMBEDDING_MODEL,
|
||||
ai_model=AI_MODEL,
|
||||
)
|
||||
|
||||
|
||||
class ExtraPathModel(BaseModel):
|
||||
|
|
@ -1293,3 +1329,48 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
update_extra_paths(conn = DataBase.get_conn())
|
||||
rebuild_image_index(search_dirs = get_img_search_dirs() + mem["extra_paths"])
|
||||
|
||||
|
||||
# AI 相关路由
|
||||
class AIChatRequest(BaseModel):
|
||||
messages: List[dict]
|
||||
temperature: Optional[float] = 0.7
|
||||
max_tokens: Optional[int] = None
|
||||
stream: Optional[bool] = False
|
||||
|
||||
@app.post(f"{api_base}/ai-chat", dependencies=[Depends(verify_secret), Depends(write_permission_required)])
|
||||
async def ai_chat(req: AIChatRequest):
|
||||
"""通用AI聊天接口,转发到OpenAI兼容API"""
|
||||
if not OPENAI_API_KEY:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"model": AI_MODEL,
|
||||
"messages": req.messages,
|
||||
"temperature": req.temperature,
|
||||
"stream": req.stream
|
||||
}
|
||||
if req.max_tokens:
|
||||
payload["max_tokens"] = req.max_tokens
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {OPENAI_API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{OPENAI_BASE_URL}/chat/completions",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
|
||||
return response.json()
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"AI API request failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"AI API request failed: {str(e)}")
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from contextlib import closing
|
|||
import os
|
||||
import threading
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
|
||||
class FileInfoDict(TypedDict):
|
||||
|
|
@ -80,6 +81,10 @@ class DataBase:
|
|||
ExtraPath.create_table(conn)
|
||||
DirCoverCache.create_table(conn)
|
||||
GlobalSetting.create_table(conn)
|
||||
ImageEmbedding.create_table(conn)
|
||||
ImageEmbeddingFail.create_table(conn)
|
||||
TopicTitleCache.create_table(conn)
|
||||
TopicClusterCache.create_table(conn)
|
||||
finally:
|
||||
conn.commit()
|
||||
clz.num += 1
|
||||
|
|
@ -187,6 +192,13 @@ class Image:
|
|||
@classmethod
|
||||
def remove(cls, conn: Connection, image_id: int) -> None:
|
||||
with closing(conn.cursor()) as cur:
|
||||
# Manual cascade delete to avoid leaving orphan rows in related tables.
|
||||
# NOTE: SQLite foreign key constraints are often disabled by default unless
|
||||
# PRAGMA foreign_keys=ON is set. We still delete related rows explicitly
|
||||
# so deletion works regardless of FK settings and keeps DB clean.
|
||||
cur.execute("DELETE FROM image_embedding WHERE image_id = ?", (int(image_id),))
|
||||
cur.execute("DELETE FROM image_embedding_fail WHERE image_id = ?", (int(image_id),))
|
||||
cur.execute("DELETE FROM image_tag WHERE image_id = ?", (int(image_id),))
|
||||
cur.execute("DELETE FROM image WHERE id = ?", (image_id,))
|
||||
conn.commit()
|
||||
|
||||
|
|
@ -197,6 +209,16 @@ class Image:
|
|||
with closing(conn.cursor()) as cur:
|
||||
try:
|
||||
placeholders = ",".join("?" * len(image_ids))
|
||||
# Manual cascade delete for related tables.
|
||||
# Keep this in sync with tables referencing image.id.
|
||||
cur.execute(
|
||||
f"DELETE FROM image_embedding WHERE image_id IN ({placeholders})",
|
||||
image_ids,
|
||||
)
|
||||
cur.execute(
|
||||
f"DELETE FROM image_embedding_fail WHERE image_id IN ({placeholders})",
|
||||
image_ids,
|
||||
)
|
||||
cur.execute(
|
||||
f"DELETE FROM image_tag WHERE image_id IN ({placeholders})",
|
||||
image_ids,
|
||||
|
|
@ -301,6 +323,328 @@ class Image:
|
|||
return images
|
||||
|
||||
|
||||
class ImageEmbedding:
|
||||
"""
|
||||
Store embeddings for image prompt text.
|
||||
|
||||
Notes:
|
||||
- vec is stored as float32 bytes (little-endian), compatible with Python's array('f').
|
||||
- text_hash is used to skip recomputation when prompt text doesn't change.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create_table(cls, conn: Connection):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS image_embedding (
|
||||
image_id INTEGER PRIMARY KEY,
|
||||
model TEXT NOT NULL,
|
||||
dim INTEGER NOT NULL,
|
||||
text_hash TEXT NOT NULL,
|
||||
vec BLOB NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (image_id) REFERENCES image(id)
|
||||
)"""
|
||||
)
|
||||
cur.execute(
|
||||
"CREATE INDEX IF NOT EXISTS image_embedding_idx_model_hash ON image_embedding(model, text_hash)"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def compute_text_hash(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def get_by_image_ids(cls, conn: Connection, image_ids: List[int]):
|
||||
if not image_ids:
|
||||
return {}
|
||||
placeholders = ",".join("?" * len(image_ids))
|
||||
query = f"SELECT image_id, model, dim, text_hash, vec, updated_at FROM image_embedding WHERE image_id IN ({placeholders})"
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(query, image_ids)
|
||||
rows = cur.fetchall()
|
||||
res = {}
|
||||
for row in rows:
|
||||
res[row[0]] = {
|
||||
"image_id": row[0],
|
||||
"model": row[1],
|
||||
"dim": row[2],
|
||||
"text_hash": row[3],
|
||||
"vec": row[4],
|
||||
"updated_at": row[5],
|
||||
}
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def upsert(
|
||||
cls,
|
||||
conn: Connection,
|
||||
image_id: int,
|
||||
model: str,
|
||||
dim: int,
|
||||
text_hash: str,
|
||||
vec_blob: bytes,
|
||||
updated_at: Optional[str] = None,
|
||||
):
|
||||
updated_at = updated_at or datetime.now().isoformat()
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""INSERT INTO image_embedding (image_id, model, dim, text_hash, vec, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(image_id) DO UPDATE SET
|
||||
model = excluded.model,
|
||||
dim = excluded.dim,
|
||||
text_hash = excluded.text_hash,
|
||||
vec = excluded.vec,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(image_id, model, dim, text_hash, vec_blob, updated_at),
|
||||
)
|
||||
|
||||
|
||||
class ImageEmbeddingFail:
|
||||
"""
|
||||
Cache embedding failures per image+model+text_hash to avoid repeatedly hitting the API
|
||||
for known-failing inputs. This helps keep clustering/search usable by skipping bad items.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create_table(cls, conn: Connection):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS image_embedding_fail (
|
||||
image_id INTEGER NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
text_hash TEXT NOT NULL,
|
||||
error TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY(image_id, model, text_hash)
|
||||
)"""
|
||||
)
|
||||
cur.execute("CREATE INDEX IF NOT EXISTS image_embedding_fail_idx_model ON image_embedding_fail(model)")
|
||||
|
||||
@classmethod
|
||||
def get_by_image_ids(cls, conn: Connection, image_ids: List[int], model: str) -> Dict[int, Dict]:
|
||||
if not image_ids:
|
||||
return {}
|
||||
ids = [int(x) for x in image_ids]
|
||||
placeholders = ",".join(["?"] * len(ids))
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
f"SELECT image_id, text_hash, error, updated_at FROM image_embedding_fail WHERE model = ? AND image_id IN ({placeholders})",
|
||||
(str(model), *ids),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
out: Dict[int, Dict] = {}
|
||||
for image_id, text_hash, error, updated_at in rows or []:
|
||||
out[int(image_id)] = {
|
||||
"text_hash": str(text_hash or ""),
|
||||
"error": str(error or ""),
|
||||
"updated_at": str(updated_at or ""),
|
||||
}
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def upsert(
|
||||
cls,
|
||||
conn: Connection,
|
||||
*,
|
||||
image_id: int,
|
||||
model: str,
|
||||
text_hash: str,
|
||||
error: str,
|
||||
updated_at: Optional[str] = None,
|
||||
):
|
||||
updated_at = updated_at or datetime.now().isoformat()
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""INSERT INTO image_embedding_fail (image_id, model, text_hash, error, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(image_id, model, text_hash) DO UPDATE SET
|
||||
error = excluded.error,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(int(image_id), str(model), str(text_hash), str(error or "")[:600], str(updated_at)),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, conn: Connection, *, image_id: int, model: str):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute("DELETE FROM image_embedding_fail WHERE image_id = ? AND model = ?", (int(image_id), str(model)))
|
||||
|
||||
|
||||
class TopicTitleCache:
|
||||
"""
|
||||
Cache cluster titles/keywords to avoid repeated LLM calls.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create_table(cls, conn: Connection):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS topic_title_cache (
|
||||
cluster_hash TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
keywords TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)"""
|
||||
)
|
||||
cur.execute(
|
||||
"CREATE INDEX IF NOT EXISTS topic_title_cache_idx_model ON topic_title_cache(model)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get(cls, conn: Connection, cluster_hash: str):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"SELECT title, keywords, model, updated_at FROM topic_title_cache WHERE cluster_hash = ?",
|
||||
(cluster_hash,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
title, keywords, model, updated_at = row
|
||||
try:
|
||||
kw = json.loads(keywords) if isinstance(keywords, str) else []
|
||||
except Exception:
|
||||
kw = []
|
||||
if not isinstance(kw, list):
|
||||
kw = []
|
||||
return {"title": title, "keywords": kw, "model": model, "updated_at": updated_at}
|
||||
|
||||
@classmethod
|
||||
def upsert(
|
||||
cls,
|
||||
conn: Connection,
|
||||
cluster_hash: str,
|
||||
title: str,
|
||||
keywords: List[str],
|
||||
model: str,
|
||||
updated_at: Optional[str] = None,
|
||||
):
|
||||
updated_at = updated_at or datetime.now().isoformat()
|
||||
kw = json.dumps([str(x) for x in (keywords or [])], ensure_ascii=False)
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""INSERT INTO topic_title_cache (cluster_hash, title, keywords, model, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(cluster_hash) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
keywords = excluded.keywords,
|
||||
model = excluded.model,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(cluster_hash, title, kw, model, updated_at),
|
||||
)
|
||||
|
||||
|
||||
class TopicClusterCache:
|
||||
"""
|
||||
Persist the final clustering result (clusters/noise) to avoid re-clustering when:
|
||||
- embeddings haven't changed (by max(updated_at) & count), and
|
||||
- clustering parameters are unchanged.
|
||||
|
||||
This is intentionally lightweight:
|
||||
- result is stored as JSON text
|
||||
- caller defines cache_key (sha1 over params + folders + normalize version + lang, etc.)
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create_table(cls, conn: Connection):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS topic_cluster_cache (
|
||||
cache_key TEXT PRIMARY KEY,
|
||||
folders TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
params TEXT NOT NULL,
|
||||
embeddings_count INTEGER NOT NULL,
|
||||
embeddings_max_updated_at TEXT NOT NULL,
|
||||
result TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)"""
|
||||
)
|
||||
cur.execute("CREATE INDEX IF NOT EXISTS topic_cluster_cache_idx_model ON topic_cluster_cache(model)")
|
||||
|
||||
@classmethod
|
||||
def get(cls, conn: Connection, cache_key: str):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"SELECT folders, model, params, embeddings_count, embeddings_max_updated_at, result, updated_at FROM topic_cluster_cache WHERE cache_key = ?",
|
||||
(cache_key,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
folders, model, params, embeddings_count, embeddings_max_updated_at, result, updated_at = row
|
||||
try:
|
||||
folders_obj = json.loads(folders) if isinstance(folders, str) else []
|
||||
except Exception:
|
||||
folders_obj = []
|
||||
try:
|
||||
params_obj = json.loads(params) if isinstance(params, str) else {}
|
||||
except Exception:
|
||||
params_obj = {}
|
||||
try:
|
||||
result_obj = json.loads(result) if isinstance(result, str) else None
|
||||
except Exception:
|
||||
result_obj = None
|
||||
return {
|
||||
"cache_key": cache_key,
|
||||
"folders": folders_obj if isinstance(folders_obj, list) else [],
|
||||
"model": str(model),
|
||||
"params": params_obj if isinstance(params_obj, dict) else {},
|
||||
"embeddings_count": int(embeddings_count or 0),
|
||||
"embeddings_max_updated_at": str(embeddings_max_updated_at or ""),
|
||||
"result": result_obj,
|
||||
"updated_at": str(updated_at or ""),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def upsert(
|
||||
cls,
|
||||
conn: Connection,
|
||||
*,
|
||||
cache_key: str,
|
||||
folders: List[str],
|
||||
model: str,
|
||||
params: Dict,
|
||||
embeddings_count: int,
|
||||
embeddings_max_updated_at: str,
|
||||
result: Dict,
|
||||
updated_at: Optional[str] = None,
|
||||
):
|
||||
updated_at = updated_at or datetime.now().isoformat()
|
||||
folders_s = json.dumps([str(x) for x in (folders or [])], ensure_ascii=False)
|
||||
params_s = json.dumps(params or {}, ensure_ascii=False, sort_keys=True)
|
||||
result_s = json.dumps(result or {}, ensure_ascii=False)
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"""INSERT INTO topic_cluster_cache
|
||||
(cache_key, folders, model, params, embeddings_count, embeddings_max_updated_at, result, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(cache_key) DO UPDATE SET
|
||||
folders = excluded.folders,
|
||||
model = excluded.model,
|
||||
params = excluded.params,
|
||||
embeddings_count = excluded.embeddings_count,
|
||||
embeddings_max_updated_at = excluded.embeddings_max_updated_at,
|
||||
result = excluded.result,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
cache_key,
|
||||
folders_s,
|
||||
str(model),
|
||||
params_s,
|
||||
int(embeddings_count or 0),
|
||||
str(embeddings_max_updated_at or ""),
|
||||
result_s,
|
||||
updated_at,
|
||||
),
|
||||
)
|
||||
|
||||
class Tag:
|
||||
def __init__(self, name: str, score: int, type: str, count=0, color = ""):
|
||||
self.name = name
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ from concurrent.futures import ThreadPoolExecutor
|
|||
import time
|
||||
from PIL import Image
|
||||
|
||||
def generate_image_cache(dirs, size:str, verbose=False):
|
||||
def generate_image_cache(dirs: List[str], size:str, verbose=True):
|
||||
dirs = [r"C:\Users\zanllp\Desktop\repo\Z-Image-Turbo\client"]
|
||||
start_time = time.time()
|
||||
cache_base_dir = get_cache_dir()
|
||||
|
||||
verbose=True
|
||||
def process_image(item):
|
||||
if '\\node_modules\\' in item.path:
|
||||
return
|
||||
if item.is_dir():
|
||||
verbose and print(f"Processing directory: {item.path}")
|
||||
for sub_item in os.scandir(item.path):
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -31,6 +31,7 @@ declare module '@vue/runtime-core' {
|
|||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
import{d as E,bC as $,r as f,m as M,_ as T,a as c,an as W,h as m,c as v,P as z}from"./index-5ed9cd5a.js";var G=["prefixCls","name","id","type","disabled","readonly","tabindex","autofocus","value","required"],H={prefixCls:String,name:String,id:String,type:String,defaultChecked:{type:[Boolean,Number],default:void 0},checked:{type:[Boolean,Number],default:void 0},disabled:Boolean,tabindex:{type:[Number,String]},readonly:Boolean,autofocus:Boolean,value:z.any,required:Boolean};const L=E({compatConfig:{MODE:3},name:"Checkbox",inheritAttrs:!1,props:$(H,{prefixCls:"rc-checkbox",type:"checkbox",defaultChecked:!1}),emits:["click","change"],setup:function(a,d){var t=d.attrs,h=d.emit,g=d.expose,o=f(a.checked===void 0?a.defaultChecked:a.checked),i=f();M(function(){return a.checked},function(){o.value=a.checked}),g({focus:function(){var e;(e=i.value)===null||e===void 0||e.focus()},blur:function(){var e;(e=i.value)===null||e===void 0||e.blur()}});var l=f(),x=function(e){if(!a.disabled){a.checked===void 0&&(o.value=e.target.checked),e.shiftKey=l.value;var r={target:c(c({},a),{},{checked:e.target.checked}),stopPropagation:function(){e.stopPropagation()},preventDefault:function(){e.preventDefault()},nativeEvent:e};a.checked!==void 0&&(i.value.checked=!!a.checked),h("change",r),l.value=!1}},C=function(e){h("click",e),l.value=e.shiftKey};return function(){var n,e=a.prefixCls,r=a.name,s=a.id,p=a.type,b=a.disabled,K=a.readonly,P=a.tabindex,B=a.autofocus,S=a.value,N=a.required,_=T(a,G),q=t.class,D=t.onFocus,j=t.onBlur,w=t.onKeydown,A=t.onKeypress,F=t.onKeyup,y=c(c({},_),t),O=Object.keys(y).reduce(function(k,u){return(u.substr(0,5)==="aria-"||u.substr(0,5)==="data-"||u==="role")&&(k[u]=y[u]),k},{}),R=W(e,q,(n={},m(n,"".concat(e,"-checked"),o.value),m(n,"".concat(e,"-disabled"),b),n)),V=c(c({name:r,id:s,type:p,readonly:K,disabled:b,tabindex:P,class:"".concat(e,"-input"),checked:!!o.value,autofocus:B,value:S},O),{},{onChange:x,onClick:C,onFocus:D,onBlur:j,onKeydown:w,onKeypress:A,onKeyup:F,required:N});return v("span",{class:R},[v("input",c({ref:i},V),null),v("span",{class:"".concat(e,"-inner")},null)])}}});export{L as V};
|
||||
import{d as E,bC as $,r as f,m as M,_ as T,a as c,an as W,h as m,c as v,P as z}from"./index-8b1d4076.js";var G=["prefixCls","name","id","type","disabled","readonly","tabindex","autofocus","value","required"],H={prefixCls:String,name:String,id:String,type:String,defaultChecked:{type:[Boolean,Number],default:void 0},checked:{type:[Boolean,Number],default:void 0},disabled:Boolean,tabindex:{type:[Number,String]},readonly:Boolean,autofocus:Boolean,value:z.any,required:Boolean};const L=E({compatConfig:{MODE:3},name:"Checkbox",inheritAttrs:!1,props:$(H,{prefixCls:"rc-checkbox",type:"checkbox",defaultChecked:!1}),emits:["click","change"],setup:function(a,d){var t=d.attrs,h=d.emit,g=d.expose,o=f(a.checked===void 0?a.defaultChecked:a.checked),i=f();M(function(){return a.checked},function(){o.value=a.checked}),g({focus:function(){var e;(e=i.value)===null||e===void 0||e.focus()},blur:function(){var e;(e=i.value)===null||e===void 0||e.blur()}});var l=f(),x=function(e){if(!a.disabled){a.checked===void 0&&(o.value=e.target.checked),e.shiftKey=l.value;var r={target:c(c({},a),{},{checked:e.target.checked}),stopPropagation:function(){e.stopPropagation()},preventDefault:function(){e.preventDefault()},nativeEvent:e};a.checked!==void 0&&(i.value.checked=!!a.checked),h("change",r),l.value=!1}},C=function(e){h("click",e),l.value=e.shiftKey};return function(){var n,e=a.prefixCls,r=a.name,s=a.id,p=a.type,b=a.disabled,K=a.readonly,P=a.tabindex,B=a.autofocus,S=a.value,N=a.required,_=T(a,G),q=t.class,D=t.onFocus,j=t.onBlur,w=t.onKeydown,A=t.onKeypress,F=t.onKeyup,y=c(c({},_),t),O=Object.keys(y).reduce(function(k,u){return(u.substr(0,5)==="aria-"||u.substr(0,5)==="data-"||u==="role")&&(k[u]=y[u]),k},{}),R=W(e,q,(n={},m(n,"".concat(e,"-checked"),o.value),m(n,"".concat(e,"-disabled"),b),n)),V=c(c({name:r,id:s,type:p,readonly:K,disabled:b,tabindex:P,class:"".concat(e,"-input"),checked:!!o.value,autofocus:B,value:S},O),{},{onChange:x,onClick:C,onFocus:D,onBlur:j,onKeydown:w,onKeypress:A,onKeyup:F,required:N});return v("span",{class:R},[v("input",c({ref:i},V),null),v("span",{class:"".concat(e,"-inner")},null)])}}});export{L as V};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{d as a,U as t,V as s,c as n,cD as _,a0 as o}from"./index-5ed9cd5a.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),s("div",c,[n(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};
|
||||
import{d as a,U as t,V as s,c as n,cN as _,a0 as o}from"./index-8b1d4076.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),s("div",c,[n(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-aea581a5]{background:var(--zp-secondary-background);position:relative;height:var(--pane-max-height)}.action-bar[data-v-aea581a5]{display:flex;align-items:center;user-select:none;gap:6px;padding:6px 8px}.title[data-v-aea581a5]{font-weight:700;max-width:40vw}.file-list[data-v-aea581a5]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 44px);width:100%}.no-res-hint[data-v-aea581a5]{height:calc(var(--pane-max-height) - 44px);display:flex;align-items:center;flex-direction:column;justify-content:center}.no-res-hint .hint[data-v-aea581a5]{font-size:1.2em;opacity:.7}.preview-switch[data-v-aea581a5]{position:fixed;bottom:24px;right:24px;display:flex;gap:8px;font-size:36px;user-select:none}.disable[data-v-aea581a5]{opacity:.3;pointer-events:none}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.full-screen-menu[data-v-50c80b83]{position:fixed;z-index:9999;background:var(--zp-primary-background);padding:8px 16px;box-shadow:0 0 4px var(--zp-secondary);border-radius:4px}.full-screen-menu .tags-container[data-v-50c80b83]{margin:4px 0}.full-screen-menu .tags-container .tag[data-v-50c80b83]{margin-right:4px;margin-bottom:4px;padding:2px 16px;border-radius:4px;display:inline-block;cursor:pointer;font-weight:700;transition:.5s all ease;border:2px solid var(--tag-color);color:var(--tag-color);background:var(--zp-primary-background);user-select:none}.full-screen-menu .tags-container .tag.selected[data-v-50c80b83]{background:var(--tag-color);color:#fff}.full-screen-menu .container[data-v-50c80b83]{height:100%;display:flex;overflow:hidden;flex-direction:column}.full-screen-menu .gen-info[data-v-50c80b83]{flex:1;word-break:break-all;white-space:pre-line;overflow:auto;z-index:1;padding-top:4px;position:relative}.full-screen-menu .gen-info code[data-v-50c80b83]{font-size:.9em;display:block;padding:4px;background:var(--zp-primary-background);border-radius:4px;margin-right:20px;white-space:pre-wrap;word-break:break-word;line-height:1.78em}.full-screen-menu .gen-info code[data-v-50c80b83] .natural-text{margin:.5em 0;line-height:1.6em;text-align:justify;color:var(--zp-primary)}.full-screen-menu .gen-info code[data-v-50c80b83] .short-tag{word-break:break-all;white-space:nowrap}.full-screen-menu .gen-info code[data-v-50c80b83] span.tag{background:var(--zp-secondary-variant-background);color:var(--zp-primary);padding:2px 4px;border-radius:6px;margin-right:6px;margin-top:4px;line-height:1.3em;display:inline-block}.full-screen-menu .gen-info code[data-v-50c80b83] .has-parentheses.tag{background:rgba(255,100,100,.14)}.full-screen-menu .gen-info code[data-v-50c80b83] span.tag:hover{background:rgba(120,0,0,.15)}.full-screen-menu .gen-info table[data-v-50c80b83]{font-size:1em;border-radius:4px;border-collapse:separate;margin-bottom:3em}.full-screen-menu .gen-info table tr td[data-v-50c80b83]:first-child{white-space:nowrap}.full-screen-menu .gen-info table td[data-v-50c80b83]{padding-right:14px;padding-left:4px;border-bottom:1px solid var(--zp-secondary);border-collapse:collapse}.full-screen-menu .gen-info .info-tags .info-tag[data-v-50c80b83]{display:inline-block;overflow:hidden;border-radius:4px;margin-right:8px;border:2px solid var(--zp-primary)}.full-screen-menu .gen-info .info-tags .name[data-v-50c80b83]{background-color:var(--zp-primary);color:var(--zp-primary-background);padding:4px;border-bottom-right-radius:4px}.full-screen-menu .gen-info .info-tags .value[data-v-50c80b83]{padding:4px}.full-screen-menu.unset-size[data-v-50c80b83]{width:unset!important;height:unset!important}.full-screen-menu .mouse-sensor[data-v-50c80b83]{position:absolute;bottom:0;right:0;transform:rotate(90deg);cursor:se-resize;z-index:1;background:var(--zp-primary-background);border-radius:2px}.full-screen-menu .mouse-sensor>*[data-v-50c80b83]{font-size:18px;padding:4px}.full-screen-menu .action-bar[data-v-50c80b83]{display:flex;align-items:center;user-select:none;gap:4px}.full-screen-menu .action-bar .icon[data-v-50c80b83]{font-size:1.5em;padding:2px 4px;border-radius:4px}.full-screen-menu .action-bar .icon[data-v-50c80b83]:hover{background:var(--zp-secondary-variant-background)}.full-screen-menu .action-bar>*[data-v-50c80b83]{flex-wrap:wrap}.full-screen-menu.lr[data-v-50c80b83]{top:var(--3eebcda7)!important;right:0!important;bottom:0!important;left:100vw!important;height:unset!important;width:var(--3e27f0da)!important;transition:left ease .3s}.full-screen-menu.lr.always-on[data-v-50c80b83],.full-screen-menu.lr.mouse-in[data-v-50c80b83]{left:var(--37c6591e)!important}.tag-alpha-item[data-v-50c80b83]{display:flex;margin-top:4px}.tag-alpha-item h4[data-v-50c80b83]{width:32px;flex-shrink:0}.sort-tag-switch[data-v-50c80b83]{display:inline-block;padding-right:16px;padding-left:8px;cursor:pointer;user-select:none}.sort-tag-switch span[data-v-50c80b83]{transition:all ease .3s;transform:scale(1.2)}.sort-tag-switch:hover span[data-v-50c80b83]{transform:scale(1.3)}.lr-layout-control[data-v-50c80b83]{display:flex;align-items:center;gap:16px;padding:4px 8px;flex-wrap:wrap;border-radius:2px;border-left:3px solid var(--zp-luminous);background-color:var(--zp-secondary-background)}.lr-layout-control .ctrl-item[data-v-50c80b83]{display:flex;align-items:center;gap:4px;flex-wrap:nowrap}.select-actions[data-v-b04c3508]>:not(:last-child){margin-right:4px}.float-panel[data-v-b04c3508]{position:absolute;bottom:32px;right:32px;background:var(--zp-primary-background);border-radius:4px;z-index:1000;padding:8px;box-shadow:0 0 4px var(--zp-secondary)}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
.full-screen-menu[data-v-c7e0b9b7]{position:fixed;z-index:9999;background:var(--zp-primary-background);padding:8px 16px;box-shadow:0 0 4px var(--zp-secondary);border-radius:4px}.full-screen-menu .tags-container[data-v-c7e0b9b7]{margin:4px 0}.full-screen-menu .tags-container .tag[data-v-c7e0b9b7]{margin-right:4px;margin-bottom:4px;padding:2px 16px;border-radius:4px;display:inline-block;cursor:pointer;font-weight:700;transition:.5s all ease;border:2px solid var(--tag-color);color:var(--tag-color);background:var(--zp-primary-background);user-select:none}.full-screen-menu .tags-container .tag.selected[data-v-c7e0b9b7]{background:var(--tag-color);color:#fff}.full-screen-menu .container[data-v-c7e0b9b7]{height:100%;display:flex;overflow:hidden;flex-direction:column}.full-screen-menu .gen-info[data-v-c7e0b9b7]{flex:1;word-break:break-all;white-space:pre-line;overflow:auto;z-index:1;padding-top:4px;position:relative}.full-screen-menu .gen-info code[data-v-c7e0b9b7]{font-size:.9em;display:block;padding:4px;background:var(--zp-primary-background);border-radius:4px;margin-right:20px;white-space:pre-wrap;word-break:break-word;line-height:1.78em}.full-screen-menu .gen-info code[data-v-c7e0b9b7] .natural-text{margin:.5em 0;line-height:1.6em;text-align:justify;color:var(--zp-primary)}.full-screen-menu .gen-info code[data-v-c7e0b9b7] .short-tag{word-break:break-all;white-space:nowrap}.full-screen-menu .gen-info code[data-v-c7e0b9b7] span.tag{background:var(--zp-secondary-variant-background);color:var(--zp-primary);padding:2px 4px;border-radius:6px;margin-right:6px;margin-top:4px;line-height:1.3em;display:inline-block}.full-screen-menu .gen-info code[data-v-c7e0b9b7] .has-parentheses.tag{background:rgba(255,100,100,.14)}.full-screen-menu .gen-info code[data-v-c7e0b9b7] span.tag:hover{background:rgba(120,0,0,.15)}.full-screen-menu .gen-info table[data-v-c7e0b9b7]{font-size:1em;border-radius:4px;border-collapse:separate;margin-bottom:3em}.full-screen-menu .gen-info table tr td[data-v-c7e0b9b7]:first-child{white-space:nowrap;vertical-align:top}.full-screen-menu .gen-info table.extra-meta-table .extra-meta-value[data-v-c7e0b9b7]{display:block;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:.85em;background:var(--zp-secondary-variant-background);padding:8px;border-radius:4px}.full-screen-menu .gen-info table td[data-v-c7e0b9b7]{padding-right:14px;padding-left:4px;border-bottom:1px solid var(--zp-secondary);border-collapse:collapse}.full-screen-menu .gen-info .info-tags .info-tag[data-v-c7e0b9b7]{display:inline-block;overflow:hidden;border-radius:4px;margin-right:8px;border:2px solid var(--zp-primary)}.full-screen-menu .gen-info .info-tags .name[data-v-c7e0b9b7]{background-color:var(--zp-primary);color:var(--zp-primary-background);padding:4px;border-bottom-right-radius:4px}.full-screen-menu .gen-info .info-tags .value[data-v-c7e0b9b7]{padding:4px}.full-screen-menu.unset-size[data-v-c7e0b9b7]{width:unset!important;height:unset!important}.full-screen-menu .mouse-sensor[data-v-c7e0b9b7]{position:absolute;bottom:0;right:0;transform:rotate(90deg);cursor:se-resize;z-index:1;background:var(--zp-primary-background);border-radius:2px}.full-screen-menu .mouse-sensor>*[data-v-c7e0b9b7]{font-size:18px;padding:4px}.full-screen-menu .action-bar[data-v-c7e0b9b7]{display:flex;align-items:center;user-select:none;gap:4px}.full-screen-menu .action-bar .icon[data-v-c7e0b9b7]{font-size:1.5em;padding:2px 4px;border-radius:4px}.full-screen-menu .action-bar .icon[data-v-c7e0b9b7]:hover{background:var(--zp-secondary-variant-background)}.full-screen-menu .action-bar>*[data-v-c7e0b9b7]{flex-wrap:wrap}.full-screen-menu.lr[data-v-c7e0b9b7]{top:var(--b7cd59ce)!important;right:0!important;bottom:0!important;left:100vw!important;height:unset!important;width:var(--0e09e1cc)!important;transition:left ease .3s}.full-screen-menu.lr.always-on[data-v-c7e0b9b7],.full-screen-menu.lr.mouse-in[data-v-c7e0b9b7]{left:var(--62228ae0)!important}.tag-alpha-item[data-v-c7e0b9b7]{display:flex;margin-top:4px}.tag-alpha-item h4[data-v-c7e0b9b7]{width:32px;flex-shrink:0}.sort-tag-switch[data-v-c7e0b9b7]{display:inline-block;padding-right:16px;padding-left:8px;cursor:pointer;user-select:none}.sort-tag-switch span[data-v-c7e0b9b7]{transition:all ease .3s;transform:scale(1.2)}.sort-tag-switch:hover span[data-v-c7e0b9b7]{transform:scale(1.3)}.lr-layout-control[data-v-c7e0b9b7]{display:flex;align-items:center;gap:16px;padding:4px 8px;flex-wrap:wrap;border-radius:2px;border-left:3px solid var(--zp-luminous);background-color:var(--zp-secondary-background)}.lr-layout-control .ctrl-item[data-v-c7e0b9b7]{display:flex;align-items:center;gap:4px;flex-wrap:nowrap}.select-actions[data-v-b04c3508]>:not(:last-child){margin-right:4px}.float-panel[data-v-b04c3508]{position:absolute;bottom:32px;right:32px;background:var(--zp-primary-background);border-radius:4px;z-index:1000;padding:8px;box-shadow:0 0 4px var(--zp-secondary)}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{bU as i,b1 as t,e0 as f,bL as n}from"./index-5ed9cd5a.js";function u(e,s,r){if(!i(r))return!1;var a=typeof s;return(a=="number"?t(r)&&f(s,r.length):a=="string"&&s in r)?n(r[s],e):!1}export{u as i};
|
||||
import{bV as i,b1 as t,e5 as f,bM as n}from"./index-8b1d4076.js";function u(e,s,r){if(!i(r))return!1;var a=typeof s;return(a=="number"?t(r)&&f(s,r.length):a=="string"&&s in r)?n(r[s],e):!1}export{u as i};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as z,a1 as B,cE as $,cb as S,U as _,V as w,W as f,c as l,a3 as d,X as p,Y as c,a4 as s,a2 as A,af as E,cF as R,cG as y,z as V,B as x,ak as T,a0 as U}from"./index-5ed9cd5a.js";import{_ as N}from"./index-4e015155.js";import{u as L,a as G,f as H,F as O,d as W}from"./FileItem-2ecfe4d5.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-47577760.js";/* empty css */import"./_isIterateeCall-cd370691.js";import"./index-7cbf21fe.js";const j={class:"actions-panel actions"},q={class:"item"},P={key:0,class:"file-list"},Q={class:"hint"},X=z({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(Y){const{stackViewEl:b}=L().toRefs(),{itemSize:h,gridItems:D,cellWidth:g}=G(),i=B(),m=H(),{selectdFiles:o}=$(m),r=S(),v=async e=>{const t=R(e);t&&m.addFiles(t.nodes)},C=async()=>{r.pushAction(async()=>{const e=await y.value.post("/zip",{paths:o.value.map(u=>u.fullpath),compress:i.batchDownloadCompress,pack_only:!1},{responseType:"blob"}),t=window.URL.createObjectURL(new Blob([e.data])),a=document.createElement("a");a.href=t,a.setAttribute("download",`iib_${new Date().toLocaleString()}.zip`),document.body.appendChild(a),a.click()})},I=async()=>{r.pushAction(async()=>{await y.value.post("/zip",{paths:o.value.map(e=>e.fullpath),compress:i.batchDownloadCompress,pack_only:!0},{responseType:"blob"}),V.success(x("success"))})},F=e=>{o.value.splice(e,1)};return(e,t)=>{const a=T,u=N;return _(),w("div",{class:"container",ref_key:"stackViewEl",ref:b,onDrop:v},[f("div",j,[l(a,{onClick:t[0]||(t[0]=n=>s(m).selectdFiles=[])},{default:d(()=>[p(c(e.$t("clear")),1)]),_:1}),f("div",q,[p(c(e.$t("compressFile"))+": ",1),l(u,{checked:s(i).batchDownloadCompress,"onUpdate:checked":t[1]||(t[1]=n=>s(i).batchDownloadCompress=n)},null,8,["checked"])]),l(a,{onClick:I,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("packOnlyNotDownload")),1)]),_:1},8,["loading"]),l(a,{onClick:C,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("zipDownload")),1)]),_:1},8,["loading"])]),s(o).length?(_(),A(s(W),{key:1,ref:"scroller",class:"file-list",items:s(o).slice(),"item-size":s(h).first,"key-field":"fullpath","item-secondary-size":s(h).second,gridItems:s(D)},{default:d(({item:n,index:k})=>[l(O,{idx:k,file:n,"cell-width":s(g),"enable-close-icon":"",onCloseIconClick:J=>F(k),"full-screen-preview-image-url":s(E)(n),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","onCloseIconClick","full-screen-preview-image-url"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):(_(),w("div",P,[f("p",Q,c(e.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const oe=U(X,[["__scopeId","data-v-a2642a17"]]);export{oe as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{d as F,a1 as B,cO as $,cc as S,U as _,V as w,W as f,c as l,a3 as d,X as p,Y as c,a4 as s,a2 as A,af as R,cP as V,cQ as y,z as x,B as E,ak as T,a0 as U}from"./index-8b1d4076.js";import{_ as N}from"./index-19cfb514.js";import{u as L,a as O,f as H,F as P,d as Q}from"./FileItem-5c27aa5d.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js";/* empty css */import"./index-fd0b9b75.js";import"./_isIterateeCall-4f946453.js";import"./index-404f2353.js";import"./index-133a27d3.js";const W={class:"actions-panel actions"},j={class:"item"},q={key:0,class:"file-list"},G={class:"hint"},X=F({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(Y){const{stackViewEl:D}=L().toRefs(),{itemSize:h,gridItems:b,cellWidth:g}=O(),i=B(),m=H(),{selectdFiles:a}=$(m),r=S(),v=async e=>{const t=V(e);t&&m.addFiles(t.nodes)},C=async()=>{r.pushAction(async()=>{const e=await y.value.post("/zip",{paths:a.value.map(u=>u.fullpath),compress:i.batchDownloadCompress,pack_only:!1},{responseType:"blob"}),t=window.URL.createObjectURL(new Blob([e.data])),o=document.createElement("a");o.href=t,o.setAttribute("download",`iib_${new Date().toLocaleString()}.zip`),document.body.appendChild(o),o.click()})},I=async()=>{r.pushAction(async()=>{await y.value.post("/zip",{paths:a.value.map(e=>e.fullpath),compress:i.batchDownloadCompress,pack_only:!0},{responseType:"blob"}),x.success(E("success"))})},z=e=>{a.value.splice(e,1)};return(e,t)=>{const o=T,u=N;return _(),w("div",{class:"container",ref_key:"stackViewEl",ref:D,onDrop:v},[f("div",W,[l(o,{onClick:t[0]||(t[0]=n=>s(m).selectdFiles=[])},{default:d(()=>[p(c(e.$t("clear")),1)]),_:1}),f("div",j,[p(c(e.$t("compressFile"))+": ",1),l(u,{checked:s(i).batchDownloadCompress,"onUpdate:checked":t[1]||(t[1]=n=>s(i).batchDownloadCompress=n)},null,8,["checked"])]),l(o,{onClick:I,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("packOnlyNotDownload")),1)]),_:1},8,["loading"]),l(o,{onClick:C,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("zipDownload")),1)]),_:1},8,["loading"])]),s(a).length?(_(),A(s(Q),{key:1,ref:"scroller",class:"file-list",items:s(a).slice(),"item-size":s(h).first,"key-field":"fullpath","item-secondary-size":s(h).second,gridItems:s(b)},{default:d(({item:n,index:k})=>[l(P,{idx:k,file:n,"cell-width":s(g),"enable-close-icon":"",onCloseIconClick:J=>z(k),"full-screen-preview-image-url":s(R)(n),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","onCloseIconClick","full-screen-preview-image-url"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):(_(),w("div",q,[f("p",G,c(e.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const le=U(X,[["__scopeId","data-v-a2642a17"]]);export{le as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{u as w,a as y,F as k,d as x}from"./FileItem-5c27aa5d.js";import{d as h,a1 as F,c9 as b,r as D,bh as I,bl as C,U as V,V as E,c,a3 as z,a4 as e,af as S,cP as B,cR as R,a0 as A}from"./index-8b1d4076.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js";/* empty css */import"./index-fd0b9b75.js";import"./_isIterateeCall-4f946453.js";import"./index-404f2353.js";import"./index-133a27d3.js";const K=h({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,m=F(),{stackViewEl:d}=w().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=y(),g=b(),a=D(o.files??[]),_=async s=>{const l=B(s);o.allowDragAndDrop&&l&&(a.value=R([...a.value,...l.nodes]))},v=s=>{a.value.splice(s,1)};return I(()=>{m.pageFuncExportMap.set(o.paneKey,{getFiles:()=>C(a.value),setFiles:s=>a.value=s})}),(s,l)=>(V(),E("div",{class:"container",ref_key:"stackViewEl",ref:d,onDrop:_},[c(e(x),{ref:"scroller",class:"file-list",items:a.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:z(({item:t,index:r})=>{var n;return[c(k,{idx:r,file:t,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:T=>v(r),"full-screen-preview-image-url":e(S)(t),"extra-tags":(n=t==null?void 0:t.tags)==null?void 0:n.map(e(g).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const W=A(K,[["__scopeId","data-v-f35f4802"]]);export{W as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as w,a as y,F as k,d as x}from"./FileItem-2ecfe4d5.js";import{d as F,a1 as h,c8 as b,r as D,bh as I,bl as C,U as V,V as E,c,a3 as z,a4 as e,af as S,cF as B,cH as R,a0 as A}from"./index-5ed9cd5a.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-47577760.js";/* empty css */import"./_isIterateeCall-cd370691.js";import"./index-7cbf21fe.js";const H=F({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,d=h(),{stackViewEl:m}=w().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=y(),g=b(),a=D(o.files??[]),_=async s=>{const l=B(s);o.allowDragAndDrop&&l&&(a.value=R([...a.value,...l.nodes]))},v=s=>{a.value.splice(s,1)};return I(()=>{d.pageFuncExportMap.set(o.paneKey,{getFiles:()=>C(a.value),setFiles:s=>a.value=s})}),(s,l)=>(V(),E("div",{class:"container",ref_key:"stackViewEl",ref:m,onDrop:_},[c(e(x),{ref:"scroller",class:"file-list",items:a.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:z(({item:t,index:r})=>{var n;return[c(k,{idx:r,file:t,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:K=>v(r),"full-screen-preview-image-url":e(S)(t),"extra-tags":(n=t==null?void 0:t.tags)==null?void 0:n.map(e(g).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const N=A(H,[["__scopeId","data-v-f35f4802"]]);export{N as default};
|
||||
|
|
@ -1 +1 @@
|
|||
import{am as F,r as g,l as P,k as A,O as b,G as R,cb as q,cm as O,cr as z}from"./index-5ed9cd5a.js";import{u as L,a as Q,b as j,e as H}from"./FileItem-2ecfe4d5.js";import{a as T,b as U,c as W}from"./MultiSelectKeep-68ce9bb5.js";import{u as B}from"./useGenInfoDiff-b31dce1f.js";let K=0;const V=()=>++K,X=(n,i,{dataUpdateStrategy:l="replace"}={})=>{const o=F([""]),c=g(!1),t=g(),a=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(b((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>A(void 0,void 0,void 0,function*(){if(a.value||c.value&&typeof e>"u")return!1;a.value=!0;const s=V();f.value=s;try{let r;if(typeof e=="number"){if(r=o[e],typeof r!="string")return!1}else r=o[o.length-1];const h=yield n(r);if(v.has(s))return v.delete(s),!1;w(i(h));const u=h.cursor;if((e===o.length-1||typeof e!="number")&&(c.value=!u.has_next,u.has_next)){const m=u.next_cursor||u.next;b(typeof m=="string"),o.push(m)}}finally{f.value===s&&(a.value=!1)}return!0}),p=()=>{v.add(f.value),a.value=!1},x=(e=!1)=>A(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&p(),b(!a.value),o.splice(0,o.length,""),a.value=!1,t.value=void 0,c.value=!1,s&&(yield d())}),I=()=>({next:()=>A(void 0,void 0,void 0,function*(){if(a.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return P({abort:p,load:c,next:d,res:t,loading:a,cursorStack:o,reset:x,[Symbol.asyncIterator]:I,iter:{[Symbol.asyncIterator]:I}})},se=n=>F(X(n,i=>i.files,{dataUpdateStrategy:"merge"})),ne=n=>{const i=F(new Set),l=R(()=>(n.res??[]).filter(y=>!i.has(y.fullpath))),o=q(),{stackViewEl:c,multiSelectedIdxs:t,stack:a,scroller:f,props:v}=L({images:l}).toRefs(),{itemSize:w,gridItems:d,cellWidth:p,onScroll:x}=Q({fetchNext:()=>n.next()}),{showMenuIdx:I}=j(),{onFileDragStart:e,onFileDragEnd:s}=T(),{showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C}=U({openNext:O}),{previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G}=W({loadNext:()=>n.next()}),J=async(y,S,N)=>{a.value=[{curr:"",files:l.value}],await m(y,S,N)};H("removeFiles",async({paths:y})=>{y.forEach(S=>i.add(S))});const k=()=>{z(l.value)};return{images:l,scroller:f,queue:o,iter:n,onContextMenuClickU:J,stackViewEl:c,previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G,itemSize:w,gridItems:d,showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C,showMenuIdx:I,multiSelectedIdxs:t,onFileDragStart:e,onFileDragEnd:s,cellWidth:p,onScroll:x,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!n.load;)await n.next();k()},props:v,...B()}};export{se as c,ne as u};
|
||||
import{am as F,r as g,l as P,k as A,O as b,G as R,cc as q,cn as O,cs as z}from"./index-8b1d4076.js";import{u as L,a as Q,b as j,e as H}from"./FileItem-5c27aa5d.js";import{a as T,b as U,c as W}from"./MultiSelectKeep-4ce030ff.js";import{u as B}from"./useGenInfoDiff-068a10f2.js";let K=0;const V=()=>++K,X=(n,i,{dataUpdateStrategy:l="replace"}={})=>{const o=F([""]),c=g(!1),t=g(),a=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(b((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>A(void 0,void 0,void 0,function*(){if(a.value||c.value&&typeof e>"u")return!1;a.value=!0;const s=V();f.value=s;try{let r;if(typeof e=="number"){if(r=o[e],typeof r!="string")return!1}else r=o[o.length-1];const h=yield n(r);if(v.has(s))return v.delete(s),!1;w(i(h));const u=h.cursor;if((e===o.length-1||typeof e!="number")&&(c.value=!u.has_next,u.has_next)){const m=u.next_cursor||u.next;b(typeof m=="string"),o.push(m)}}finally{f.value===s&&(a.value=!1)}return!0}),p=()=>{v.add(f.value),a.value=!1},x=(e=!1)=>A(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&p(),b(!a.value),o.splice(0,o.length,""),a.value=!1,t.value=void 0,c.value=!1,s&&(yield d())}),I=()=>({next:()=>A(void 0,void 0,void 0,function*(){if(a.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return P({abort:p,load:c,next:d,res:t,loading:a,cursorStack:o,reset:x,[Symbol.asyncIterator]:I,iter:{[Symbol.asyncIterator]:I}})},se=n=>F(X(n,i=>i.files,{dataUpdateStrategy:"merge"})),ne=n=>{const i=F(new Set),l=R(()=>(n.res??[]).filter(y=>!i.has(y.fullpath))),o=q(),{stackViewEl:c,multiSelectedIdxs:t,stack:a,scroller:f,props:v}=L({images:l}).toRefs(),{itemSize:w,gridItems:d,cellWidth:p,onScroll:x}=Q({fetchNext:()=>n.next()}),{showMenuIdx:I}=j(),{onFileDragStart:e,onFileDragEnd:s}=T(),{showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C}=U({openNext:O}),{previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G}=W({loadNext:()=>n.next()}),J=async(y,S,N)=>{a.value=[{curr:"",files:l.value}],await m(y,S,N)};H("removeFiles",async({paths:y})=>{y.forEach(S=>i.add(S))});const k=()=>{z(l.value)};return{images:l,scroller:f,queue:o,iter:n,onContextMenuClickU:J,stackViewEl:c,previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G,itemSize:w,gridItems:d,showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C,showMenuIdx:I,multiSelectedIdxs:t,onFileDragStart:e,onFileDragEnd:s,cellWidth:p,onScroll:x,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!n.load;)await n.next();k()},props:v,...B()}};export{se as c,ne as u};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{cw as j,ay as z,d as K,j as U,du as $,w as g,r as b,G as S,m as A,u as D,o as E,az as G,h as d,c as s,a as C,aw as H,bf as L,g as _,dv as W,P as u,dw as x}from"./index-8b1d4076.js";var R=z("small","default"),q=function(){return{id:String,prefixCls:String,size:u.oneOf(R),disabled:{type:Boolean,default:void 0},checkedChildren:u.any,unCheckedChildren:u.any,tabindex:u.oneOfType([u.string,u.number]),autofocus:{type:Boolean,default:void 0},loading:{type:Boolean,default:void 0},checked:u.oneOfType([u.string,u.number,u.looseBool]),checkedValue:u.oneOfType([u.string,u.number,u.looseBool]).def(!0),unCheckedValue:u.oneOfType([u.string,u.number,u.looseBool]).def(!1),onChange:{type:Function},onClick:{type:Function},onKeydown:{type:Function},onMouseup:{type:Function},"onUpdate:checked":{type:Function},onBlur:Function,onFocus:Function}},J=K({compatConfig:{MODE:3},name:"ASwitch",__ANT_SWITCH:!0,inheritAttrs:!1,props:q(),slots:["checkedChildren","unCheckedChildren"],setup:function(n,r){var o=r.attrs,y=r.slots,B=r.expose,l=r.emit,m=U();$(function(){g(!("defaultChecked"in o),"Switch","'defaultChecked' is deprecated, please use 'v-model:checked'"),g(!("value"in o),"Switch","`value` is not validate prop, do you mean `checked`?")});var h=b(n.checked!==void 0?n.checked:o.defaultChecked),f=S(function(){return h.value===n.checkedValue});A(function(){return n.checked},function(){h.value=n.checked});var v=D("switch",n),c=v.prefixCls,F=v.direction,T=v.size,i=b(),w=function(){var e;(e=i.value)===null||e===void 0||e.focus()},V=function(){var e;(e=i.value)===null||e===void 0||e.blur()};B({focus:w,blur:V}),E(function(){G(function(){n.autofocus&&!n.disabled&&i.value.focus()})});var k=function(e,t){n.disabled||(l("update:checked",e),l("change",e,t),m.onFieldChange())},I=function(e){l("blur",e)},N=function(e){w();var t=f.value?n.unCheckedValue:n.checkedValue;k(t,e),l("click",t,e)},M=function(e){e.keyCode===x.LEFT?k(n.unCheckedValue,e):e.keyCode===x.RIGHT&&k(n.checkedValue,e),l("keydown",e)},O=function(e){var t;(t=i.value)===null||t===void 0||t.blur(),l("mouseup",e)},P=S(function(){var a;return a={},d(a,"".concat(c.value,"-small"),T.value==="small"),d(a,"".concat(c.value,"-loading"),n.loading),d(a,"".concat(c.value,"-checked"),f.value),d(a,"".concat(c.value,"-disabled"),n.disabled),d(a,c.value,!0),d(a,"".concat(c.value,"-rtl"),F.value==="rtl"),a});return function(){var a;return s(W,{insertExtraNode:!0},{default:function(){return[s("button",C(C(C({},H(n,["prefixCls","checkedChildren","unCheckedChildren","checked","autofocus","checkedValue","unCheckedValue","id","onChange","onUpdate:checked"])),o),{},{id:(a=n.id)!==null&&a!==void 0?a:m.id.value,onKeydown:M,onClick:N,onBlur:I,onMouseup:O,type:"button",role:"switch","aria-checked":h.value,disabled:n.disabled||n.loading,class:[o.class,P.value],ref:i}),[s("div",{class:"".concat(c.value,"-handle")},[n.loading?s(L,{class:"".concat(c.value,"-loading-icon")},null):null]),s("span",{class:"".concat(c.value,"-inner")},[f.value?_(y,n,"checkedChildren"):_(y,n,"unCheckedChildren")])])]}})}}});const X=j(J);export{X as _};
|
||||
|
|
@ -1 +1 @@
|
|||
import{d as w,bC as A,av as D,cz as j,az as k,n as B,cA as V,cB as y,e as $,c as a,_ as T,h as r,a as P,cC as M,P as b}from"./index-5ed9cd5a.js";var O=["class","style"],W=function(){return{prefixCls:String,spinning:{type:Boolean,default:void 0},size:String,wrapperClassName:String,tip:b.any,delay:Number,indicator:b.any}},p=null;function q(t,n){return!!t&&!!n&&!isNaN(Number(n))}function G(t){var n=t.indicator;p=typeof n=="function"?n:function(){return a(n,null,null)}}const H=w({compatConfig:{MODE:3},name:"ASpin",inheritAttrs:!1,props:A(W(),{size:"default",spinning:!0,wrapperClassName:""}),setup:function(){return{originalUpdateSpinning:null,configProvider:D("configProvider",j)}},data:function(){var n=this.spinning,e=this.delay,i=q(n,e);return{sSpinning:n&&!i}},created:function(){this.originalUpdateSpinning=this.updateSpinning,this.debouncifyUpdateSpinning(this.$props)},mounted:function(){this.updateSpinning()},updated:function(){var n=this;k(function(){n.debouncifyUpdateSpinning(),n.updateSpinning()})},beforeUnmount:function(){this.cancelExistingSpin()},methods:{debouncifyUpdateSpinning:function(n){var e=n||this.$props,i=e.delay;i&&(this.cancelExistingSpin(),this.updateSpinning=B(this.originalUpdateSpinning,i))},updateSpinning:function(){var n=this.spinning,e=this.sSpinning;e!==n&&(this.sSpinning=n)},cancelExistingSpin:function(){var n=this.updateSpinning;n&&n.cancel&&n.cancel()},renderIndicator:function(n){var e="".concat(n,"-dot"),i=V(this,"indicator");return i===null?null:(Array.isArray(i)&&(i=i.length===1?i[0]:i),y(i)?$(i,{class:e}):p&&y(p())?$(p(),{class:e}):a("span",{class:"".concat(e," ").concat(n,"-dot-spin")},[a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null)]))}},render:function(){var n,e,i,o=this.$props,f=o.size,x=o.prefixCls,h=o.tip,d=h===void 0?(n=(e=this.$slots).tip)===null||n===void 0?void 0:n.call(e):h,C=o.wrapperClassName,l=this.$attrs,v=l.class,N=l.style,_=T(l,O),S=this.configProvider,U=S.getPrefixCls,z=S.direction,s=U("spin",x),u=this.sSpinning,E=(i={},r(i,s,!0),r(i,"".concat(s,"-sm"),f==="small"),r(i,"".concat(s,"-lg"),f==="large"),r(i,"".concat(s,"-spinning"),u),r(i,"".concat(s,"-show-text"),!!d),r(i,"".concat(s,"-rtl"),z==="rtl"),r(i,v,!!v),i),m=a("div",P(P({},_),{},{style:N,class:E}),[this.renderIndicator(s),d?a("div",{class:"".concat(s,"-text")},[d]):null]),g=M(this);if(g&&g.length){var c,I=(c={},r(c,"".concat(s,"-container"),!0),r(c,"".concat(s,"-blur"),u),c);return a("div",{class:["".concat(s,"-nested-loading"),C]},[u&&a("div",{key:"loading"},[m]),a("div",{class:I,key:"container"},[g])])}return m}});export{H as S,G as s};
|
||||
import{d as w,bC as D,av as A,cJ as j,az as k,n as V,cK as B,cL as y,e as $,c as a,_ as M,h as r,a as P,cM as T,P as b}from"./index-8b1d4076.js";var J=["class","style"],K=function(){return{prefixCls:String,spinning:{type:Boolean,default:void 0},size:String,wrapperClassName:String,tip:b.any,delay:Number,indicator:b.any}},p=null;function L(t,n){return!!t&&!!n&&!isNaN(Number(n))}function W(t){var n=t.indicator;p=typeof n=="function"?n:function(){return a(n,null,null)}}const q=w({compatConfig:{MODE:3},name:"ASpin",inheritAttrs:!1,props:D(K(),{size:"default",spinning:!0,wrapperClassName:""}),setup:function(){return{originalUpdateSpinning:null,configProvider:A("configProvider",j)}},data:function(){var n=this.spinning,e=this.delay,i=L(n,e);return{sSpinning:n&&!i}},created:function(){this.originalUpdateSpinning=this.updateSpinning,this.debouncifyUpdateSpinning(this.$props)},mounted:function(){this.updateSpinning()},updated:function(){var n=this;k(function(){n.debouncifyUpdateSpinning(),n.updateSpinning()})},beforeUnmount:function(){this.cancelExistingSpin()},methods:{debouncifyUpdateSpinning:function(n){var e=n||this.$props,i=e.delay;i&&(this.cancelExistingSpin(),this.updateSpinning=V(this.originalUpdateSpinning,i))},updateSpinning:function(){var n=this.spinning,e=this.sSpinning;e!==n&&(this.sSpinning=n)},cancelExistingSpin:function(){var n=this.updateSpinning;n&&n.cancel&&n.cancel()},renderIndicator:function(n){var e="".concat(n,"-dot"),i=B(this,"indicator");return i===null?null:(Array.isArray(i)&&(i=i.length===1?i[0]:i),y(i)?$(i,{class:e}):p&&y(p())?$(p(),{class:e}):a("span",{class:"".concat(e," ").concat(n,"-dot-spin")},[a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null)]))}},render:function(){var n,e,i,o=this.$props,f=o.size,x=o.prefixCls,h=o.tip,d=h===void 0?(n=(e=this.$slots).tip)===null||n===void 0?void 0:n.call(e):h,N=o.wrapperClassName,l=this.$attrs,v=l.class,_=l.style,C=M(l,J),S=this.configProvider,U=S.getPrefixCls,z=S.direction,s=U("spin",x),u=this.sSpinning,E=(i={},r(i,s,!0),r(i,"".concat(s,"-sm"),f==="small"),r(i,"".concat(s,"-lg"),f==="large"),r(i,"".concat(s,"-spinning"),u),r(i,"".concat(s,"-show-text"),!!d),r(i,"".concat(s,"-rtl"),z==="rtl"),r(i,v,!!v),i),m=a("div",P(P({},C),{},{style:_,class:E}),[this.renderIndicator(s),d?a("div",{class:"".concat(s,"-text")},[d]):null]),g=T(this);if(g&&g.length){var c,I=(c={},r(c,"".concat(s,"-container"),!0),r(c,"".concat(s,"-blur"),u),c);return a("div",{class:["".concat(s,"-nested-loading"),N]},[u&&a("div",{key:"loading"},[m]),a("div",{class:I,key:"container"},[g])])}return m}});export{q as S,W as s};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{d as F,u as S,G as k,an as j,h as d,c as s,aq as U,e6 as W,r as q,bh as G,Z as V,dv as Z,P as N,cb as z}from"./index-8b1d4076.js";var H=function(){return{prefixCls:String,checked:{type:Boolean,default:void 0},onChange:{type:Function},onClick:{type:Function},"onUpdate:checked":Function}},J=F({compatConfig:{MODE:3},name:"ACheckableTag",props:H(),setup:function(e,i){var l=i.slots,r=i.emit,g=S("tag",e),u=g.prefixCls,o=function(C){var v=e.checked;r("update:checked",!v),r("change",!v),r("click",C)},p=k(function(){var a;return j(u.value,(a={},d(a,"".concat(u.value,"-checkable"),!0),d(a,"".concat(u.value,"-checkable-checked"),e.checked),a))});return function(){var a;return s("span",{class:p.value,onClick:o},[(a=l.default)===null||a===void 0?void 0:a.call(l)])}}});const b=J;var K=new RegExp("^(".concat(U.join("|"),")(-inverse)?$")),L=new RegExp("^(".concat(W.join("|"),")$")),Q=function(){return{prefixCls:String,color:{type:String},closable:{type:Boolean,default:!1},closeIcon:N.any,visible:{type:Boolean,default:void 0},onClose:{type:Function},"onUpdate:visible":Function,icon:N.any}},f=F({compatConfig:{MODE:3},name:"ATag",props:Q(),slots:["closeIcon","icon"],setup:function(e,i){var l=i.slots,r=i.emit,g=i.attrs,u=S("tag",e),o=u.prefixCls,p=u.direction,a=q(!0);G(function(){e.visible!==void 0&&(a.value=e.visible)});var C=function(t){t.stopPropagation(),r("update:visible",!1),r("close",t),!t.defaultPrevented&&e.visible===void 0&&(a.value=!1)},v=k(function(){var n=e.color;return n?K.test(n)||L.test(n):!1}),E=k(function(){var n;return j(o.value,(n={},d(n,"".concat(o.value,"-").concat(e.color),v.value),d(n,"".concat(o.value,"-has-color"),e.color&&!v.value),d(n,"".concat(o.value,"-hidden"),!a.value),d(n,"".concat(o.value,"-rtl"),p.value==="rtl"),n))});return function(){var n,t,h,m=e.icon,R=m===void 0?(n=l.icon)===null||n===void 0?void 0:n.call(l):m,y=e.color,_=e.closeIcon,P=_===void 0?(t=l.closeIcon)===null||t===void 0?void 0:t.call(l):_,x=e.closable,w=x===void 0?!1:x,B=function(){return w?P?s("span",{class:"".concat(o.value,"-close-icon"),onClick:C},[P]):s(z,{class:"".concat(o.value,"-close-icon"),onClick:C},null):null},O={backgroundColor:y&&!v.value?y:void 0},I=R||null,T=(h=l.default)===null||h===void 0?void 0:h.call(l),A=I?s(V,null,[I,s("span",null,[T])]):T,D="onClick"in g,$=s("span",{class:E.value,style:O},[A,B()]);return D?s(Z,null,{default:function(){return[$]}}):$}}});f.CheckableTag=b;f.install=function(c){return c.component(f.name,f),c.component(b.name,b),c};const Y=f;export{Y as _};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{cv as P,ay as z,d as K,j as U,di as $,w,r as b,G as S,m as A,u as D,o as E,az as G,h as d,c as s,a as C,aw as H,bf as L,g as _,dj as W,P as c,dk as x}from"./index-5ed9cd5a.js";var R=z("small","default"),q=function(){return{id:String,prefixCls:String,size:c.oneOf(R),disabled:{type:Boolean,default:void 0},checkedChildren:c.any,unCheckedChildren:c.any,tabindex:c.oneOfType([c.string,c.number]),autofocus:{type:Boolean,default:void 0},loading:{type:Boolean,default:void 0},checked:c.oneOfType([c.string,c.number,c.looseBool]),checkedValue:c.oneOfType([c.string,c.number,c.looseBool]).def(!0),unCheckedValue:c.oneOfType([c.string,c.number,c.looseBool]).def(!1),onChange:{type:Function},onClick:{type:Function},onKeydown:{type:Function},onMouseup:{type:Function},"onUpdate:checked":{type:Function},onBlur:Function,onFocus:Function}},J=K({compatConfig:{MODE:3},name:"ASwitch",__ANT_SWITCH:!0,inheritAttrs:!1,props:q(),slots:["checkedChildren","unCheckedChildren"],setup:function(n,r){var o=r.attrs,y=r.slots,B=r.expose,l=r.emit,m=U();$(function(){w(!("defaultChecked"in o),"Switch","'defaultChecked' is deprecated, please use 'v-model:checked'"),w(!("value"in o),"Switch","`value` is not validate prop, do you mean `checked`?")});var h=b(n.checked!==void 0?n.checked:o.defaultChecked),f=S(function(){return h.value===n.checkedValue});A(function(){return n.checked},function(){h.value=n.checked});var v=D("switch",n),u=v.prefixCls,F=v.direction,T=v.size,i=b(),g=function(){var e;(e=i.value)===null||e===void 0||e.focus()},V=function(){var e;(e=i.value)===null||e===void 0||e.blur()};B({focus:g,blur:V}),E(function(){G(function(){n.autofocus&&!n.disabled&&i.value.focus()})});var k=function(e,t){n.disabled||(l("update:checked",e),l("change",e,t),m.onFieldChange())},I=function(e){l("blur",e)},N=function(e){g();var t=f.value?n.unCheckedValue:n.checkedValue;k(t,e),l("click",t,e)},M=function(e){e.keyCode===x.LEFT?k(n.unCheckedValue,e):e.keyCode===x.RIGHT&&k(n.checkedValue,e),l("keydown",e)},O=function(e){var t;(t=i.value)===null||t===void 0||t.blur(),l("mouseup",e)},j=S(function(){var a;return a={},d(a,"".concat(u.value,"-small"),T.value==="small"),d(a,"".concat(u.value,"-loading"),n.loading),d(a,"".concat(u.value,"-checked"),f.value),d(a,"".concat(u.value,"-disabled"),n.disabled),d(a,u.value,!0),d(a,"".concat(u.value,"-rtl"),F.value==="rtl"),a});return function(){var a;return s(W,{insertExtraNode:!0},{default:function(){return[s("button",C(C(C({},H(n,["prefixCls","checkedChildren","unCheckedChildren","checked","autofocus","checkedValue","unCheckedValue","id","onChange","onUpdate:checked"])),o),{},{id:(a=n.id)!==null&&a!==void 0?a:m.id.value,onKeydown:M,onClick:N,onBlur:I,onMouseup:O,type:"button",role:"switch","aria-checked":h.value,disabled:n.disabled||n.loading,class:[o.class,j.value],ref:i}),[s("div",{class:"".concat(u.value,"-handle")},[n.loading?s(L,{class:"".concat(u.value,"-loading-icon")},null):null]),s("span",{class:"".concat(u.value,"-inner")},[f.value?_(y,n,"checkedChildren"):_(y,n,"unCheckedChildren")])])]}})}}});const X=P(J);export{X as _};
|
||||
|
|
@ -1 +1 @@
|
|||
import{r as F,o as P,cw as K,av as L,G as l,ax as T,ay as $,d as I,u as B,cx as _,b as y,bk as V,cy as A,an as E,h as c,c as M,a as G}from"./index-5ed9cd5a.js";const W=function(){var o=F(!1);return P(function(){o.value=K()}),o};var D=Symbol("rowContextKey"),k=function(r){T(D,r)},U=function(){return L(D,{gutter:l(function(){}),wrap:l(function(){}),supportFlexGap:l(function(){})})};$("top","middle","bottom","stretch");$("start","end","center","space-around","space-between");var q=function(){return{align:String,justify:String,prefixCls:String,gutter:{type:[Number,Array,Object],default:0},wrap:{type:Boolean,default:void 0}}},H=I({compatConfig:{MODE:3},name:"ARow",props:q(),setup:function(r,N){var m=N.slots,v=B("row",r),d=v.prefixCls,h=v.direction,j,x=F({xs:!0,sm:!0,md:!0,lg:!0,xl:!0,xxl:!0,xxxl:!0}),w=W();P(function(){j=_.subscribe(function(e){var t=r.gutter||0;(!Array.isArray(t)&&y(t)==="object"||Array.isArray(t)&&(y(t[0])==="object"||y(t[1])==="object"))&&(x.value=e)})}),V(function(){_.unsubscribe(j)});var S=l(function(){var e=[0,0],t=r.gutter,n=t===void 0?0:t,s=Array.isArray(n)?n:[n,0];return s.forEach(function(i,b){if(y(i)==="object")for(var a=0;a<A.length;a++){var p=A[a];if(x.value[p]&&i[p]!==void 0){e[b]=i[p];break}}else e[b]=i||0}),e});k({gutter:S,supportFlexGap:w,wrap:l(function(){return r.wrap})});var R=l(function(){var e;return E(d.value,(e={},c(e,"".concat(d.value,"-no-wrap"),r.wrap===!1),c(e,"".concat(d.value,"-").concat(r.justify),r.justify),c(e,"".concat(d.value,"-").concat(r.align),r.align),c(e,"".concat(d.value,"-rtl"),h.value==="rtl"),e))}),O=l(function(){var e=S.value,t={},n=e[0]>0?"".concat(e[0]/-2,"px"):void 0,s=e[1]>0?"".concat(e[1]/-2,"px"):void 0;return n&&(t.marginLeft=n,t.marginRight=n),w.value?t.rowGap="".concat(e[1],"px"):s&&(t.marginTop=s,t.marginBottom=s),t});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});const Y=H;function J(o){return typeof o=="number"?"".concat(o," ").concat(o," auto"):/^\d+(\.\d+)?(px|em|rem|%)$/.test(o)?"0 0 ".concat(o):o}var Q=function(){return{span:[String,Number],order:[String,Number],offset:[String,Number],push:[String,Number],pull:[String,Number],xs:{type:[String,Number,Object],default:void 0},sm:{type:[String,Number,Object],default:void 0},md:{type:[String,Number,Object],default:void 0},lg:{type:[String,Number,Object],default:void 0},xl:{type:[String,Number,Object],default:void 0},xxl:{type:[String,Number,Object],default:void 0},xxxl:{type:[String,Number,Object],default:void 0},prefixCls:String,flex:[String,Number]}};const Z=I({compatConfig:{MODE:3},name:"ACol",props:Q(),setup:function(r,N){var m=N.slots,v=U(),d=v.gutter,h=v.supportFlexGap,j=v.wrap,x=B("col",r),w=x.prefixCls,S=x.direction,R=l(function(){var e,t=r.span,n=r.order,s=r.offset,i=r.push,b=r.pull,a=w.value,p={};return["xs","sm","md","lg","xl","xxl","xxxl"].forEach(function(g){var f,u={},C=r[g];typeof C=="number"?u.span=C:y(C)==="object"&&(u=C||{}),p=G(G({},p),{},(f={},c(f,"".concat(a,"-").concat(g,"-").concat(u.span),u.span!==void 0),c(f,"".concat(a,"-").concat(g,"-order-").concat(u.order),u.order||u.order===0),c(f,"".concat(a,"-").concat(g,"-offset-").concat(u.offset),u.offset||u.offset===0),c(f,"".concat(a,"-").concat(g,"-push-").concat(u.push),u.push||u.push===0),c(f,"".concat(a,"-").concat(g,"-pull-").concat(u.pull),u.pull||u.pull===0),c(f,"".concat(a,"-rtl"),S.value==="rtl"),f))}),E(a,(e={},c(e,"".concat(a,"-").concat(t),t!==void 0),c(e,"".concat(a,"-order-").concat(n),n),c(e,"".concat(a,"-offset-").concat(s),s),c(e,"".concat(a,"-push-").concat(i),i),c(e,"".concat(a,"-pull-").concat(b),b),e),p)}),O=l(function(){var e=r.flex,t=d.value,n={};if(t&&t[0]>0){var s="".concat(t[0]/2,"px");n.paddingLeft=s,n.paddingRight=s}if(t&&t[1]>0&&!h.value){var i="".concat(t[1]/2,"px");n.paddingTop=i,n.paddingBottom=i}return e&&(n.flex=J(e),j.value===!1&&!n.minWidth&&(n.minWidth=0)),n});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});export{Z as C,Y as R};
|
||||
import{r as F,o as P,cx as K,av as L,G as i,ax as T,ay as I,d as $,u as B,cy as _,b as y,bk as V,cz as A,an as E,h as c,c as M,a as G}from"./index-8b1d4076.js";const W=function(){var o=F(!1);return P(function(){o.value=K()}),o};var D=Symbol("rowContextKey"),k=function(r){T(D,r)},U=function(){return L(D,{gutter:i(function(){}),wrap:i(function(){}),supportFlexGap:i(function(){})})};I("top","middle","bottom","stretch");I("start","end","center","space-around","space-between");var q=function(){return{align:String,justify:String,prefixCls:String,gutter:{type:[Number,Array,Object],default:0},wrap:{type:Boolean,default:void 0}}},z=$({compatConfig:{MODE:3},name:"ARow",props:q(),setup:function(r,N){var m=N.slots,v=B("row",r),d=v.prefixCls,h=v.direction,j,x=F({xs:!0,sm:!0,md:!0,lg:!0,xl:!0,xxl:!0,xxxl:!0}),S=W();P(function(){j=_.subscribe(function(e){var t=r.gutter||0;(!Array.isArray(t)&&y(t)==="object"||Array.isArray(t)&&(y(t[0])==="object"||y(t[1])==="object"))&&(x.value=e)})}),V(function(){_.unsubscribe(j)});var w=i(function(){var e=[0,0],t=r.gutter,n=t===void 0?0:t,s=Array.isArray(n)?n:[n,0];return s.forEach(function(l,b){if(y(l)==="object")for(var a=0;a<A.length;a++){var p=A[a];if(x.value[p]&&l[p]!==void 0){e[b]=l[p];break}}else e[b]=l||0}),e});k({gutter:w,supportFlexGap:S,wrap:i(function(){return r.wrap})});var R=i(function(){var e;return E(d.value,(e={},c(e,"".concat(d.value,"-no-wrap"),r.wrap===!1),c(e,"".concat(d.value,"-").concat(r.justify),r.justify),c(e,"".concat(d.value,"-").concat(r.align),r.align),c(e,"".concat(d.value,"-rtl"),h.value==="rtl"),e))}),O=i(function(){var e=w.value,t={},n=e[0]>0?"".concat(e[0]/-2,"px"):void 0,s=e[1]>0?"".concat(e[1]/-2,"px"):void 0;return n&&(t.marginLeft=n,t.marginRight=n),S.value?t.rowGap="".concat(e[1],"px"):s&&(t.marginTop=s,t.marginBottom=s),t});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});const X=z;function H(o){return typeof o=="number"?"".concat(o," ").concat(o," auto"):/^\d+(\.\d+)?(px|em|rem|%)$/.test(o)?"0 0 ".concat(o):o}var J=function(){return{span:[String,Number],order:[String,Number],offset:[String,Number],push:[String,Number],pull:[String,Number],xs:{type:[String,Number,Object],default:void 0},sm:{type:[String,Number,Object],default:void 0},md:{type:[String,Number,Object],default:void 0},lg:{type:[String,Number,Object],default:void 0},xl:{type:[String,Number,Object],default:void 0},xxl:{type:[String,Number,Object],default:void 0},xxxl:{type:[String,Number,Object],default:void 0},prefixCls:String,flex:[String,Number]}};const Y=$({compatConfig:{MODE:3},name:"ACol",props:J(),setup:function(r,N){var m=N.slots,v=U(),d=v.gutter,h=v.supportFlexGap,j=v.wrap,x=B("col",r),S=x.prefixCls,w=x.direction,R=i(function(){var e,t=r.span,n=r.order,s=r.offset,l=r.push,b=r.pull,a=S.value,p={};return["xs","sm","md","lg","xl","xxl","xxxl"].forEach(function(g){var f,u={},C=r[g];typeof C=="number"?u.span=C:y(C)==="object"&&(u=C||{}),p=G(G({},p),{},(f={},c(f,"".concat(a,"-").concat(g,"-").concat(u.span),u.span!==void 0),c(f,"".concat(a,"-").concat(g,"-order-").concat(u.order),u.order||u.order===0),c(f,"".concat(a,"-").concat(g,"-offset-").concat(u.offset),u.offset||u.offset===0),c(f,"".concat(a,"-").concat(g,"-push-").concat(u.push),u.push||u.push===0),c(f,"".concat(a,"-").concat(g,"-pull-").concat(u.pull),u.pull||u.pull===0),c(f,"".concat(a,"-rtl"),w.value==="rtl"),f))}),E(a,(e={},c(e,"".concat(a,"-").concat(t),t!==void 0),c(e,"".concat(a,"-order-").concat(n),n),c(e,"".concat(a,"-offset-").concat(s),s),c(e,"".concat(a,"-push-").concat(l),l),c(e,"".concat(a,"-pull-").concat(b),b),e),p)}),O=i(function(){var e=r.flex,t=d.value,n={};if(t&&t[0]>0){var s="".concat(t[0]/2,"px");n.paddingLeft=s,n.paddingRight=s}if(t&&t[1]>0&&!h.value){var l="".concat(t[1]/2,"px");n.paddingTop=l,n.paddingBottom=l}return e&&(n.flex=H(e),j.value===!1&&!n.minWidth&&(n.minWidth=0)),n});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});export{Y as C,X as R};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.ant-tag{box-sizing:border-box;margin:0 8px 0 0;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";display:inline-block;height:auto;padding:0 7px;font-size:12px;line-height:20px;white-space:nowrap;background:#fafafa;border:1px solid #d9d9d9;border-radius:2px;opacity:1;transition:all .3s}.ant-tag,.ant-tag a,.ant-tag a:hover{color:#000000d9}.ant-tag>a:first-child:last-child{display:inline-block;margin:0 -8px;padding:0 8px}.ant-tag-close-icon{margin-left:3px;color:#00000073;font-size:10px;cursor:pointer;transition:all .3s}.ant-tag-close-icon:hover{color:#000000d9}.ant-tag-has-color{border-color:transparent}.ant-tag-has-color,.ant-tag-has-color a,.ant-tag-has-color a:hover,.ant-tag-has-color .anticon-close,.ant-tag-has-color .anticon-close:hover{color:#fff}.ant-tag-checkable{background-color:transparent;border-color:transparent;cursor:pointer}.ant-tag-checkable:not(.ant-tag-checkable-checked):hover{color:#d03f0a}.ant-tag-checkable:active,.ant-tag-checkable-checked{color:#fff}.ant-tag-checkable-checked{background-color:#d03f0a}.ant-tag-checkable:active{background-color:#ab2800}.ant-tag-hidden{display:none}.ant-tag-pink{color:#c41d7f;background:#fff0f6;border-color:#ffadd2}.ant-tag-pink-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-magenta{color:#c41d7f;background:#fff0f6;border-color:#ffadd2}.ant-tag-magenta-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-red{color:#cf1322;background:#fff1f0;border-color:#ffa39e}.ant-tag-red-inverse{color:#fff;background:#f5222d;border-color:#f5222d}.ant-tag-volcano{color:#d4380d;background:#fff2e8;border-color:#ffbb96}.ant-tag-volcano-inverse{color:#fff;background:#fa541c;border-color:#fa541c}.ant-tag-orange{color:#d46b08;background:#fff7e6;border-color:#ffd591}.ant-tag-orange-inverse{color:#fff;background:#fa8c16;border-color:#fa8c16}.ant-tag-yellow{color:#d4b106;background:#feffe6;border-color:#fffb8f}.ant-tag-yellow-inverse{color:#fff;background:#fadb14;border-color:#fadb14}.ant-tag-gold{color:#d48806;background:#fffbe6;border-color:#ffe58f}.ant-tag-gold-inverse{color:#fff;background:#faad14;border-color:#faad14}.ant-tag-cyan{color:#08979c;background:#e6fffb;border-color:#87e8de}.ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2}.ant-tag-lime{color:#7cb305;background:#fcffe6;border-color:#eaff8f}.ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911}.ant-tag-green{color:#389e0d;background:#f6ffed;border-color:#b7eb8f}.ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a}.ant-tag-blue{color:#096dd9;background:#e6f7ff;border-color:#91d5ff}.ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff}.ant-tag-geekblue{color:#1d39c4;background:#f0f5ff;border-color:#adc6ff}.ant-tag-geekblue-inverse{color:#fff;background:#2f54eb;border-color:#2f54eb}.ant-tag-purple{color:#531dab;background:#f9f0ff;border-color:#d3adf7}.ant-tag-purple-inverse{color:#fff;background:#722ed1;border-color:#722ed1}.ant-tag-success{color:#52c41a;background:#f6ffed;border-color:#b7eb8f}.ant-tag-processing{color:#d03f0a;background:#fff1e6;border-color:#f7ae83}.ant-tag-error{color:#ff4d4f;background:#fff2f0;border-color:#ffccc7}.ant-tag-warning{color:#faad14;background:#fffbe6;border-color:#ffe58f}.ant-tag>.anticon+span,.ant-tag>span+.anticon{margin-left:7px}.ant-tag.ant-tag-rtl{margin-right:0;margin-left:8px;direction:rtl;text-align:right}.ant-tag-rtl .ant-tag-close-icon{margin-right:3px;margin-left:0}.ant-tag-rtl.ant-tag>.anticon+span,.ant-tag-rtl.ant-tag>span+.anticon{margin-right:7px;margin-left:0}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{d as x,a1 as $,aK as g,cI as b,r as w,U as p,V as i,W as a,c as r,a3 as d,X as u,Y as n,Z as B,a8 as I,a4 as m,y as V,z as _,B as v,aj as W,ak as D,cJ as N,a0 as R}from"./index-5ed9cd5a.js";/* empty css */const F={class:"container"},K={class:"actions"},L={class:"uni-desc"},U={class:"snapshot"},j=x({__name:"index",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(q){const h=$(),t=g(),f=e=>{h.tabList=V(e.tabs)},k=b(async e=>{await N(`workspace_snapshot_${e.id}`),t.snapshots=t.snapshots.filter(c=>c.id!==e.id),_.success(v("deleteSuccess"))}),o=w(""),y=async()=>{if(!o.value){_.error(v("nameRequired"));return}const e=t.createSnapshot(o.value);await t.addSnapshot(e),_.success(v("saveCompleted"))};return(e,c)=>{const C=W,l=D;return p(),i("div",F,[a("div",K,[r(C,{value:o.value,"onUpdate:value":c[0]||(c[0]=s=>o.value=s),placeholder:e.$t("name"),style:{"max-width":"300px"}},null,8,["value","placeholder"]),r(l,{type:"primary",onClick:y},{default:d(()=>[u(n(e.$t("saveWorkspaceSnapshot")),1)]),_:1})]),a("p",L,n(e.$t("WorkspaceSnapshotDesc")),1),a("ul",U,[(p(!0),i(B,null,I(m(t).snapshots,s=>(p(),i("li",{key:s.id},[a("div",null,[a("span",null,n(s.name),1)]),a("div",null,[r(l,{onClick:S=>f(s)},{default:d(()=>[u(n(e.$t("restore")),1)]),_:2},1032,["onClick"]),r(l,{onClick:S=>m(k)(s)},{default:d(()=>[u(n(e.$t("remove")),1)]),_:2},1032,["onClick"])])]))),128))])])}}});const E=R(j,[["__scopeId","data-v-2c44013c"]]);export{E as default};
|
||||
import{d as x,a1 as $,aK as g,cS as b,r as w,U as p,V as i,W as a,c as r,a3 as d,X as u,Y as n,Z as B,a8 as I,a4 as m,y as V,z as _,B as v,aj as W,ak as D,cT as N,a0 as R}from"./index-8b1d4076.js";/* empty css */const F={class:"container"},K={class:"actions"},L={class:"uni-desc"},T={class:"snapshot"},U=x({__name:"index",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(j){const h=$(),t=g(),f=e=>{h.tabList=V(e.tabs)},k=b(async e=>{await N(`workspace_snapshot_${e.id}`),t.snapshots=t.snapshots.filter(c=>c.id!==e.id),_.success(v("deleteSuccess"))}),o=w(""),y=async()=>{if(!o.value){_.error(v("nameRequired"));return}const e=t.createSnapshot(o.value);await t.addSnapshot(e),_.success(v("saveCompleted"))};return(e,c)=>{const C=W,l=D;return p(),i("div",F,[a("div",K,[r(C,{value:o.value,"onUpdate:value":c[0]||(c[0]=s=>o.value=s),placeholder:e.$t("name"),style:{"max-width":"300px"}},null,8,["value","placeholder"]),r(l,{type:"primary",onClick:y},{default:d(()=>[u(n(e.$t("saveWorkspaceSnapshot")),1)]),_:1})]),a("p",L,n(e.$t("WorkspaceSnapshotDesc")),1),a("ul",T,[(p(!0),i(B,null,I(m(t).snapshots,s=>(p(),i("li",{key:s.id},[a("div",null,[a("span",null,n(s.name),1)]),a("div",null,[r(l,{onClick:S=>f(s)},{default:d(()=>[u(n(e.$t("restore")),1)]),_:2},1032,["onClick"]),r(l,{onClick:S=>m(k)(s)},{default:d(()=>[u(n(e.$t("remove")),1)]),_:2},1032,["onClick"])])]))),128))])])}}});const A=R(U,[["__scopeId","data-v-2c44013c"]]);export{A as default};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{ck as e,cl as i,cm as r,cn as a,b1 as n}from"./index-5ed9cd5a.js";function o(s,t){return e(i(s,t,r),s+"")}function b(s){return a(s)&&n(s)}export{o as b,b as i};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{cl as e,cm as i,cn as r,co as a,b1 as n}from"./index-8b1d4076.js";function c(s,t){return e(i(s,t,r),s+"")}function b(s){return a(s)&&n(s)}export{c as b,b as i};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.ant-slider{box-sizing:border-box;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";position:relative;height:12px;margin:10px 6px;padding:4px 0;cursor:pointer;touch-action:none}.ant-slider-vertical{width:12px;height:100%;margin:6px 10px;padding:0 4px}.ant-slider-vertical .ant-slider-rail{width:4px;height:100%}.ant-slider-vertical .ant-slider-track{width:4px}.ant-slider-vertical .ant-slider-handle{margin-top:-6px;margin-left:-5px}.ant-slider-vertical .ant-slider-mark{top:0;left:12px;width:18px;height:100%}.ant-slider-vertical .ant-slider-mark-text{left:4px;white-space:nowrap}.ant-slider-vertical .ant-slider-step{width:4px;height:100%}.ant-slider-vertical .ant-slider-dot{top:auto;left:2px;margin-bottom:-4px}.ant-slider-tooltip .ant-tooltip-inner{min-width:unset}.ant-slider-rtl.ant-slider-vertical .ant-slider-handle{margin-right:-5px;margin-left:0}.ant-slider-rtl.ant-slider-vertical .ant-slider-mark{right:12px;left:auto}.ant-slider-rtl.ant-slider-vertical .ant-slider-mark-text{right:4px;left:auto}.ant-slider-rtl.ant-slider-vertical .ant-slider-dot{right:2px;left:auto}.ant-slider-with-marks{margin-bottom:28px}.ant-slider-rail{position:absolute;width:100%;height:4px;background-color:#f5f5f5;border-radius:2px;transition:background-color .3s}.ant-slider-track{position:absolute;height:4px;background-color:#f7ae83;border-radius:2px;transition:background-color .3s}.ant-slider-handle{position:absolute;width:14px;height:14px;margin-top:-5px;background-color:#fff;border:solid 2px #f7ae83;border-radius:50%;box-shadow:0;cursor:pointer;transition:border-color .3s,box-shadow .6s,transform .3s cubic-bezier(.18,.89,.32,1.28)}.ant-slider-handle-dragging.ant-slider-handle-dragging.ant-slider-handle-dragging{border-color:#d9653b;box-shadow:0 0 0 5px #d03f0a1f}.ant-slider-handle:focus{border-color:#d9653b;outline:none;box-shadow:0 0 0 5px #d03f0a1f}.ant-slider-handle.ant-tooltip-open{border-color:#d03f0a}.ant-slider:hover .ant-slider-rail{background-color:#e1e1e1}.ant-slider:hover .ant-slider-track{background-color:#eb8857}.ant-slider:hover .ant-slider-handle:not(.ant-tooltip-open){border-color:#eb8857}.ant-slider-mark{position:absolute;top:14px;left:0;width:100%;font-size:14px}.ant-slider-mark-text{position:absolute;display:inline-block;color:#00000073;text-align:center;word-break:keep-all;cursor:pointer;user-select:none}.ant-slider-mark-text-active{color:#000000d9}.ant-slider-step{position:absolute;width:100%;height:4px;background:transparent}.ant-slider-dot{position:absolute;top:-2px;width:8px;height:8px;margin-left:-4px;background-color:#fff;border:2px solid #f0f0f0;border-radius:50%;cursor:pointer}.ant-slider-dot:first-child{margin-left:-4px}.ant-slider-dot:last-child{margin-left:-4px}.ant-slider-dot-active{border-color:#e89f85}.ant-slider-disabled{cursor:not-allowed}.ant-slider-disabled .ant-slider-rail{background-color:#f5f5f5!important}.ant-slider-disabled .ant-slider-track{background-color:#00000040!important}.ant-slider-disabled .ant-slider-handle,.ant-slider-disabled .ant-slider-dot{background-color:#fff;border-color:#00000040!important;box-shadow:none;cursor:not-allowed}.ant-slider-disabled .ant-slider-mark-text,.ant-slider-disabled .ant-slider-dot{cursor:not-allowed!important}.ant-slider-rtl{direction:rtl}.ant-slider-rtl .ant-slider-mark{right:0;left:auto}.ant-slider-rtl .ant-slider-dot,.ant-slider-rtl .ant-slider-dot:first-child{margin-right:-4px;margin-left:0}.ant-slider-rtl .ant-slider-dot:last-child{margin-right:-4px;margin-left:0}.num-input[data-v-55978858]{display:flex}.num-input .slide[data-v-55978858]{flex:1;min-width:128px;max-width:256px;margin-left:8px}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
vue/dist/assets/numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js
vendored
Normal file
1
vue/dist/assets/numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{d as Z,a1 as ee,r as F,J as te,K as le,o as ie,U as v,V as N,c as i,a4 as e,W as g,a3 as n,X as k,Y as u,a5 as R,L as se,a6 as ae,af as oe,ag as $,$ as A,a2 as ne,z as w,B as re,cU as ce,cV as de,ak as ue,ai as me,T as fe,a0 as pe}from"./index-8b1d4076.js";import{u as ve,c as ge,a as ke,F as we,d as he}from"./FileItem-5c27aa5d.js";import{a as Ce,b as Se,c as _e,M as Ie,o as z,L as ye,R as xe,f as be}from"./MultiSelectKeep-4ce030ff.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js";/* empty css */import"./index-fd0b9b75.js";import"./_isIterateeCall-4f946453.js";import"./index-404f2353.js";import"./index-133a27d3.js";import"./shortcut-d7b854eb.js";import"./Checkbox-8b8e8d31.js";import"./index-19cfb514.js";const Ve={class:"refresh-button"},Me={class:"hint"},Te={key:0,class:"preview-switch"},Fe=Z({__name:"randomImage",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(Ne){const B=ee(),m=F(!1),l=F([]),r=l,h=te(`${le}randomImageSettingNotificationShown`,!1),P=()=>{h.value||(w.info({content:re("randomImageSettingNotification"),duration:6,key:"randomImageSetting"}),h.value=!0)},f=async()=>{try{m.value=!0;const s=await ce();s.length===0&&w.warn("No data, please generate index in image search page first"),l.value=s}finally{m.value=!1,_()}},C=()=>{if(l.value.length===0){w.warn("没有图片可以浏览");return}z(l.value,o.value||0)};ie(()=>{f(),setTimeout(()=>{P()},2e3)});const{stackViewEl:U,multiSelectedIdxs:p,stack:K,scroller:L}=ve({images:l}).toRefs(),{onClearAllSelected:D,onSelectAll:E,onReverseSelect:G}=ge();Ce();const{itemSize:S,gridItems:O,cellWidth:W,onScroll:_}=ke(),{showGenInfo:c,imageGenInfo:I,q,onContextMenuClick:H,onFileItemClick:J}=Se({openNext:de}),{previewIdx:o,previewing:y,onPreviewVisibleChange:Q,previewImgMove:x,canPreview:b}=_e(),V=async(s,t,d)=>{K.value=[{curr:"",files:l.value}],await H(s,t,d)};return(s,t)=>{var M;const d=ue,X=me,Y=fe;return v(),N("div",{class:"container",ref_key:"stackViewEl",ref:U},[i(Ie,{show:!!e(p).length||e(B).keepMultiSelect,onClearAllSelected:e(D),onSelectAll:e(E),onReverseSelect:e(G)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),g("div",Ve,[i(d,{onClick:f,onTouchstart:R(f,["prevent"]),type:"primary",loading:m.value,shape:"round"},{default:n(()=>[k(u(s.$t("shuffle")),1)]),_:1},8,["onTouchstart","loading"]),i(d,{onClick:C,onTouchstart:R(C,["prevent"]),type:"default",disabled:!((M=l.value)!=null&&M.length),shape:"round"},{default:n(()=>[k(u(s.$t("tiktokView")),1)]),_:1},8,["onTouchstart","disabled"])]),i(Y,{visible:e(c),"onUpdate:visible":t[1]||(t[1]=a=>ae(c)?c.value=a:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=a=>c.value=!1)},{cancelText:n(()=>[]),default:n(()=>[i(X,{active:"",loading:!e(q).isIdle},{default:n(()=>[g("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=a=>e(se)(e(I)))},[g("div",Me,u(s.$t("doubleClickToCopy")),1),k(" "+u(e(I)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),i(e(he),{ref_key:"scroller",ref:L,class:"file-list",items:l.value.slice(),"item-size":e(S).first,"key-field":"fullpath","item-secondary-size":e(S).second,gridItems:e(O),onScroll:e(_)},{default:n(({item:a,index:T})=>[i(we,{idx:T,file:a,"cell-width":e(W),"full-screen-preview-image-url":e(r)[e(o)]?e(oe)(e(r)[e(o)]):"",onContextMenuClick:V,onPreviewVisibleChange:e(Q),"is-selected-mutil-files":e(p).length>1,selected:e(p).includes(T),onFileItemClick:e(J),onTiktokView:(Re,j)=>e(z)(l.value,j)},null,8,["idx","file","cell-width","full-screen-preview-image-url","onPreviewVisibleChange","is-selected-mutil-files","selected","onFileItemClick","onTiktokView"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"]),e(y)?(v(),N("div",Te,[i(e(ye),{onClick:t[3]||(t[3]=a=>e(x)("prev")),class:$({disable:!e(b)("prev")})},null,8,["class"]),i(e(xe),{onClick:t[4]||(t[4]=a=>e(x)("next")),class:$({disable:!e(b)("next")})},null,8,["class"])])):A("",!0),e(y)&&e(r)&&e(r)[e(o)]?(v(),ne(be,{key:1,file:e(r)[e(o)],idx:e(o),onContextMenuClick:V},null,8,["file","idx"])):A("",!0)],512)}}});const We=pe(Fe,[["__scopeId","data-v-49082269"]]);export{We as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as Z,a1 as ee,r as F,J as te,K as le,o as ie,U as v,V as N,c as i,a4 as e,W as g,a3 as n,X as k,Y as u,a5 as R,L as se,a6 as ae,af as oe,ag as $,$ as A,a2 as ne,z as w,B as re,cK as ce,cL as de,ak as ue,ai as me,T as fe,a0 as pe}from"./index-5ed9cd5a.js";import{u as ve,c as ge,a as ke,F as we,d as he}from"./FileItem-2ecfe4d5.js";import{a as Ce,b as Se,c as _e,M as Ie,o as z,L as ye,R as xe,f as be}from"./MultiSelectKeep-68ce9bb5.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-47577760.js";/* empty css */import"./_isIterateeCall-cd370691.js";import"./index-7cbf21fe.js";import"./shortcut-bf073698.js";import"./Checkbox-bbe5a1a5.js";import"./index-4e015155.js";const Ve={class:"refresh-button"},Me={class:"hint"},Te={key:0,class:"preview-switch"},Fe=Z({__name:"randomImage",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(Ne){const B=ee(),m=F(!1),l=F([]),r=l,h=te(`${le}randomImageSettingNotificationShown`,!1),K=()=>{h.value||(w.info({content:re("randomImageSettingNotification"),duration:6,key:"randomImageSetting"}),h.value=!0)},f=async()=>{try{m.value=!0;const s=await ce();s.length===0&&w.warn("No data, please generate index in image search page first"),l.value=s}finally{m.value=!1,_()}},C=()=>{if(l.value.length===0){w.warn("没有图片可以浏览");return}z(l.value,o.value||0)};ie(()=>{f(),setTimeout(()=>{K()},2e3)});const{stackViewEl:L,multiSelectedIdxs:p,stack:P,scroller:U}=ve({images:l}).toRefs(),{onClearAllSelected:D,onSelectAll:E,onReverseSelect:G}=ge();Ce();const{itemSize:S,gridItems:O,cellWidth:W,onScroll:_}=ke(),{showGenInfo:c,imageGenInfo:I,q,onContextMenuClick:H,onFileItemClick:J}=Se({openNext:de}),{previewIdx:o,previewing:y,onPreviewVisibleChange:Q,previewImgMove:x,canPreview:b}=_e(),V=async(s,t,d)=>{P.value=[{curr:"",files:l.value}],await H(s,t,d)};return(s,t)=>{var M;const d=ue,X=me,Y=fe;return v(),N("div",{class:"container",ref_key:"stackViewEl",ref:L},[i(Ie,{show:!!e(p).length||e(B).keepMultiSelect,onClearAllSelected:e(D),onSelectAll:e(E),onReverseSelect:e(G)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),g("div",Ve,[i(d,{onClick:f,onTouchstart:R(f,["prevent"]),type:"primary",loading:m.value,shape:"round"},{default:n(()=>[k(u(s.$t("shuffle")),1)]),_:1},8,["onTouchstart","loading"]),i(d,{onClick:C,onTouchstart:R(C,["prevent"]),type:"default",disabled:!((M=l.value)!=null&&M.length),shape:"round"},{default:n(()=>[k(u(s.$t("tiktokView")),1)]),_:1},8,["onTouchstart","disabled"])]),i(Y,{visible:e(c),"onUpdate:visible":t[1]||(t[1]=a=>ae(c)?c.value=a:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=a=>c.value=!1)},{cancelText:n(()=>[]),default:n(()=>[i(X,{active:"",loading:!e(q).isIdle},{default:n(()=>[g("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=a=>e(se)(e(I)))},[g("div",Me,u(s.$t("doubleClickToCopy")),1),k(" "+u(e(I)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),i(e(he),{ref_key:"scroller",ref:U,class:"file-list",items:l.value.slice(),"item-size":e(S).first,"key-field":"fullpath","item-secondary-size":e(S).second,gridItems:e(O),onScroll:e(_)},{default:n(({item:a,index:T})=>[i(we,{idx:T,file:a,"cell-width":e(W),"full-screen-preview-image-url":e(r)[e(o)]?e(oe)(e(r)[e(o)]):"",onContextMenuClick:V,onPreviewVisibleChange:e(Q),"is-selected-mutil-files":e(p).length>1,selected:e(p).includes(T),onFileItemClick:e(J),onTiktokView:(Re,j)=>e(z)(l.value,j)},null,8,["idx","file","cell-width","full-screen-preview-image-url","onPreviewVisibleChange","is-selected-mutil-files","selected","onFileItemClick","onTiktokView"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"]),e(y)?(v(),N("div",Te,[i(e(ye),{onClick:t[3]||(t[3]=a=>e(x)("prev")),class:$({disable:!e(b)("prev")})},null,8,["class"]),i(e(xe),{onClick:t[4]||(t[4]=a=>e(x)("next")),class:$({disable:!e(b)("next")})},null,8,["class"])])):A("",!0),e(y)&&e(r)&&e(r)[e(o)]?(v(),ne(be,{key:1,file:e(r)[e(o)],idx:e(o),onContextMenuClick:V},null,8,["file","idx"])):A("",!0)],512)}}});const Ge=pe(Fe,[["__scopeId","data-v-49082269"]]);export{Ge as default};
|
||||
|
|
@ -1 +1 @@
|
|||
import{R as y,C as v}from"./index-eec830e6.js";import{cv as f,c as d,A as P,d as w,U as o,V as c,W as r,Z as S,a8 as V,aG as O,a3 as R,X as u,Y as p,a4 as b,ak as $,a0 as x,R as H,J as _,K as m}from"./index-5ed9cd5a.js";const A=f(y),E=f(v);var L={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3-15.4 12.3-16.6 35.4-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8z"}}]},name:"pushpin",theme:"filled"};const C=L;function h(t){for(var e=1;e<arguments.length;e++){var s=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(s);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(s).filter(function(i){return Object.getOwnPropertyDescriptor(s,i).enumerable}))),n.forEach(function(i){N(t,i,s[i])})}return t}function N(t,e,s){return e in t?Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[e]=s,t}var l=function(e,s){var n=h({},e,s.attrs);return d(P,h({},n,{icon:C}),null)};l.displayName="PushpinFilled";l.inheritAttrs=!1;const z=l,F={class:"record-container"},k={style:{flex:"1"}},I={class:"rec-actions"},B=["onClick"],J=w({__name:"HistoryRecord",props:{records:{}},emits:["reuseRecord"],setup(t){return(e,s)=>{const n=$;return o(),c("div",null,[r("ul",F,[(o(!0),c(S,null,V(e.records.getRecords(),i=>(o(),c("li",{key:i.id,class:"record"},[r("div",k,[O(e.$slots,"default",{record:i},void 0,!0)]),r("div",I,[d(n,{onClick:g=>e.$emit("reuseRecord",i),type:"primary"},{default:R(()=>[u(p(e.$t("restore")),1)]),_:2},1032,["onClick"]),r("div",{class:"pin",onClick:g=>e.records.switchPin(i)},[d(b(z)),u(" "+p(e.records.isPinned(i)?e.$t("unpin"):e.$t("pin")),1)],8,B)])]))),128))])])}}});const q=x(J,[["__scopeId","data-v-834a248f"]]);class a{constructor(e=128,s=[],n=[]){this.maxLength=e,this.records=s,this.pinnedValues=n}isPinned(e){return this.pinnedValues.some(s=>s.id===e.id)}add(e){this.records.length>=this.maxLength&&this.records.pop(),this.records.unshift({...e,id:H()+Date.now(),time:new Date().toLocaleString()})}pin(e){const s=this.records.findIndex(n=>n.id===e.id);s!==-1&&this.records.splice(s,1),this.pinnedValues.push(e)}unpin(e){const s=this.pinnedValues.findIndex(n=>n.id===e.id);s!==-1&&this.pinnedValues.splice(s,1),this.records.unshift(e)}switchPin(e){this.isPinned(e)?this.unpin(e):this.pin(e)}getRecords(){return[...this.pinnedValues,...this.records]}getPinnedValues(){return this.pinnedValues}}const G=_(`${m}fuzzy-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),M=_(`${m}tag-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}});export{q as H,E as _,A as a,G as f,M as t};
|
||||
import{R as y,C as v}from"./index-53d59921.js";import{cw as f,c as d,A as w,d as P,U as o,V as c,W as r,Z as S,a8 as V,aG as O,a3 as R,X as u,Y as p,a4 as b,ak as $,a0 as x,R as H,J as _,K as m}from"./index-8b1d4076.js";const A=f(y),E=f(v);var L={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3-15.4 12.3-16.6 35.4-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8z"}}]},name:"pushpin",theme:"filled"};const C=L;function h(t){for(var e=1;e<arguments.length;e++){var s=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(s);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(s).filter(function(i){return Object.getOwnPropertyDescriptor(s,i).enumerable}))),n.forEach(function(i){N(t,i,s[i])})}return t}function N(t,e,s){return e in t?Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[e]=s,t}var l=function(e,s){var n=h({},e,s.attrs);return d(w,h({},n,{icon:C}),null)};l.displayName="PushpinFilled";l.inheritAttrs=!1;const z=l,F={class:"record-container"},k={style:{flex:"1"}},I={class:"rec-actions"},B=["onClick"],J=P({__name:"HistoryRecord",props:{records:{}},emits:["reuseRecord"],setup(t){return(e,s)=>{const n=$;return o(),c("div",null,[r("ul",F,[(o(!0),c(S,null,V(e.records.getRecords(),i=>(o(),c("li",{key:i.id,class:"record"},[r("div",k,[O(e.$slots,"default",{record:i},void 0,!0)]),r("div",I,[d(n,{onClick:g=>e.$emit("reuseRecord",i),type:"primary"},{default:R(()=>[u(p(e.$t("restore")),1)]),_:2},1032,["onClick"]),r("div",{class:"pin",onClick:g=>e.records.switchPin(i)},[d(b(z)),u(" "+p(e.records.isPinned(i)?e.$t("unpin"):e.$t("pin")),1)],8,B)])]))),128))])])}}});const q=x(J,[["__scopeId","data-v-834a248f"]]);class a{constructor(e=128,s=[],n=[]){this.maxLength=e,this.records=s,this.pinnedValues=n}isPinned(e){return this.pinnedValues.some(s=>s.id===e.id)}add(e){this.records.length>=this.maxLength&&this.records.pop(),this.records.unshift({...e,id:H()+Date.now(),time:new Date().toLocaleString()})}pin(e){const s=this.records.findIndex(n=>n.id===e.id);s!==-1&&this.records.splice(s,1),this.pinnedValues.push(e)}unpin(e){const s=this.pinnedValues.findIndex(n=>n.id===e.id);s!==-1&&this.pinnedValues.splice(s,1),this.records.unshift(e)}switchPin(e){this.isPinned(e)?this.unpin(e):this.pin(e)}getRecords(){return[...this.pinnedValues,...this.records]}getPinnedValues(){return this.pinnedValues}}const G=_(`${m}fuzzy-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),M=_(`${m}tag-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}});export{q as H,E as _,A as a,G as f,M as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{u as G,g as d}from"./FileItem-5c27aa5d.js";import{r as b,t as j,ct as m,cu as y,cv as D}from"./index-8b1d4076.js";const r=new Map,A=()=>{const{useEventListen:k,sortedFiles:f,getViewableAreaFiles:w}=G().toRefs(),c=b(d.defaultChangeIndchecked),u=b(d.defaultSeedChangeChecked),g=async()=>{if(await j(100),!c.value)return;const o=w.value().filter(e=>m(e.fullpath)&&!e.gen_info_obj);if(!o.length)return;const t=await y(o.map(e=>e.fullpath).filter(e=>!r.has(e)));o.forEach(e=>{const i=t[e.fullpath]||r.get(e.fullpath)||"";r.set(e.fullpath,i),e.gen_info_obj=D(i),e.gen_info_raw=i})};k.value("viewableAreaFilesChange",g);const F=o=>{const t=f.value;return[o,u.value,t[o-1],t[o],t[o+1]]};function I(o,t,e,i){const a={diff:{},empty:!0,ownFile:"",otherFile:""};if(t+e<0||t+e>=f.value.length||f.value[t]==null||!("gen_info_obj"in f.value[t])||!("gen_info_obj"in f.value[t+e]))return a;const l=o,s=f.value[t+e].gen_info_obj;if(s==null)return a;const h=["hashes","resources"];a.diff={},a.ownFile=i.name,a.otherFile=f.value[t+e].name,a.empty=!1,u.value||h.push("seed");for(const n in l)if(!h.includes(n)){if(!(n in s)){a.diff[n]="+";continue}if(l[n]!=s[n])if(n.includes("rompt")&&l[n]!=""&&s[n]!=""){const p=l[n].split(","),C=s[n].split(",");let _=0;for(const v in p)p[v]!=C[v]&&_++;a.diff[n]=_}else a.diff[n]=[l[n],s[n]]}return a}return{getGenDiff:I,changeIndchecked:c,seedChangeChecked:u,getRawGenParams:()=>g(),getGenDiffWatchDep:F}};export{A as u};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as G,g as d}from"./FileItem-2ecfe4d5.js";import{r as b,t as j,cs as m,ct as y,cu as D}from"./index-5ed9cd5a.js";const r=new Map,A=()=>{const{useEventListen:k,sortedFiles:s,getViewableAreaFiles:w}=G().toRefs(),c=b(d.defaultChangeIndchecked),u=b(d.defaultSeedChangeChecked),g=async()=>{if(await j(100),!c.value)return;const o=w.value().filter(e=>m(e.fullpath)&&!e.gen_info_obj);if(!o.length)return;const t=await y(o.map(e=>e.fullpath).filter(e=>!r.has(e)));o.forEach(e=>{const i=t[e.fullpath]||r.get(e.fullpath)||"";r.set(e.fullpath,i),e.gen_info_obj=D(i),e.gen_info_raw=i})};k.value("viewableAreaFilesChange",g);const F=o=>{const t=s.value;return[o,u.value,t[o-1],t[o],t[o+1]]};function I(o,t,e,i){const a={diff:{},empty:!0,ownFile:"",otherFile:""};if(t+e<0||t+e>=s.value.length||s.value[t]==null||!("gen_info_obj"in s.value[t])||!("gen_info_obj"in s.value[t+e]))return a;const l=o,f=s.value[t+e].gen_info_obj;if(f==null)return a;const h=["hashes","resources"];a.diff={},a.ownFile=i.name,a.otherFile=s.value[t+e].name,a.empty=!1,u.value||h.push("seed");for(const n in l)if(!h.includes(n)){if(!(n in f)){a.diff[n]="+";continue}if(l[n]!=f[n])if(n.includes("rompt")&&l[n]!=""&&f[n]!=""){const p=l[n].split(","),C=f[n].split(",");let _=0;for(const v in p)p[v]!=C[v]&&_++;a.diff[n]=_}else a.diff[n]=[l[n],f[n]]}return a}return{getGenDiff:I,changeIndchecked:c,seedChangeChecked:u,getRawGenParams:()=>g(),getGenDiffWatchDep:F}};export{A as u};
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Infinite Image Browsing</title>
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-5ed9cd5a.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-c290c403.css">
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8b1d4076.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-d385cc4f.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -161,4 +161,151 @@ export interface RenameFileParams {
|
|||
export const renameFile = async (data: RenameFileParams) => {
|
||||
const resp = await axiosInst.value.post('/db/rename', data)
|
||||
return resp.data as Promise<{ new_path:string }>
|
||||
}
|
||||
|
||||
// ===== Natural language topic clustering =====
|
||||
export interface BuildIibOutputEmbeddingsReq {
|
||||
folder?: string
|
||||
model?: string
|
||||
force?: boolean
|
||||
batch_size?: number
|
||||
max_chars?: number
|
||||
}
|
||||
|
||||
export interface BuildIibOutputEmbeddingsResp {
|
||||
folder: string
|
||||
count: number
|
||||
updated: number
|
||||
skipped: number
|
||||
model: string
|
||||
}
|
||||
|
||||
export const buildIibOutputEmbeddings = async (req: BuildIibOutputEmbeddingsReq) => {
|
||||
const resp = await axiosInst.value.post('/db/build_iib_output_embeddings', req)
|
||||
return resp.data as BuildIibOutputEmbeddingsResp
|
||||
}
|
||||
|
||||
export interface ClusterIibOutputReq {
|
||||
folder?: string
|
||||
folder_paths?: string[]
|
||||
model?: string
|
||||
force_embed?: boolean
|
||||
threshold?: number
|
||||
batch_size?: number
|
||||
max_chars?: number
|
||||
min_cluster_size?: number
|
||||
lang?: string
|
||||
// advanced (backend-supported; optional)
|
||||
force_title?: boolean
|
||||
use_title_cache?: boolean
|
||||
assign_noise_threshold?: number
|
||||
}
|
||||
|
||||
export interface ClusterIibOutputResp {
|
||||
folder: string
|
||||
model: string
|
||||
threshold: number
|
||||
min_cluster_size: number
|
||||
count: number
|
||||
clusters: Array<{
|
||||
id: string
|
||||
title: string
|
||||
size: number
|
||||
paths: string[]
|
||||
sample_prompt: string
|
||||
}>
|
||||
noise: string[]
|
||||
}
|
||||
|
||||
// ===== Async clustering job (progress polling) =====
|
||||
export interface ClusterIibOutputJobStartResp {
|
||||
job_id: string
|
||||
}
|
||||
|
||||
export interface ClusterIibOutputJobStatusResp {
|
||||
job_id: string
|
||||
status: 'queued' | 'running' | 'done' | 'error'
|
||||
stage?: string
|
||||
folders?: string[]
|
||||
progress?: {
|
||||
// embedding totals
|
||||
scanned?: number
|
||||
to_embed?: number
|
||||
embedded_done?: number
|
||||
updated?: number
|
||||
skipped?: number
|
||||
folder?: string
|
||||
// clustering
|
||||
items_total?: number
|
||||
items_done?: number
|
||||
// titling
|
||||
clusters_total?: number
|
||||
clusters_done?: number
|
||||
}
|
||||
error?: string
|
||||
result?: ClusterIibOutputResp
|
||||
}
|
||||
|
||||
export const startClusterIibOutputJob = async (req: ClusterIibOutputReq) => {
|
||||
const resp = await axiosInst.value.post('/db/cluster_iib_output_job_start', req)
|
||||
return resp.data as ClusterIibOutputJobStartResp
|
||||
}
|
||||
|
||||
export const getClusterIibOutputJobStatus = async (job_id: string) => {
|
||||
const resp = await axiosInst.value.get('/db/cluster_iib_output_job_status', { params: { job_id } })
|
||||
return resp.data as ClusterIibOutputJobStatusResp
|
||||
}
|
||||
|
||||
export interface ClusterIibOutputCachedResp {
|
||||
cache_key: string
|
||||
cache_hit: boolean
|
||||
cached_at?: string
|
||||
stale: boolean
|
||||
stale_reason?: {
|
||||
folders_changed?: boolean
|
||||
reason?: string
|
||||
path?: string
|
||||
stored?: string
|
||||
current?: string
|
||||
embeddings_changed?: boolean
|
||||
embeddings_count?: number
|
||||
embeddings_max_updated_at?: string
|
||||
}
|
||||
result?: ClusterIibOutputResp | null
|
||||
}
|
||||
|
||||
export const getClusterIibOutputCached = async (req: ClusterIibOutputReq) => {
|
||||
const resp = await axiosInst.value.post('/db/cluster_iib_output_cached', req)
|
||||
return resp.data as ClusterIibOutputCachedResp
|
||||
}
|
||||
|
||||
// ===== Natural language prompt query (RAG-like retrieval) =====
|
||||
export interface PromptSearchReq {
|
||||
query: string
|
||||
folder?: string
|
||||
folder_paths?: string[]
|
||||
model?: string
|
||||
top_k?: number
|
||||
min_score?: number
|
||||
ensure_embed?: boolean
|
||||
max_chars?: number
|
||||
}
|
||||
|
||||
export interface PromptSearchResp {
|
||||
query: string
|
||||
folder: string
|
||||
model: string
|
||||
count: number
|
||||
top_k: number
|
||||
results: Array<{
|
||||
id: number
|
||||
path: string
|
||||
score: number
|
||||
sample_prompt: string
|
||||
}>
|
||||
}
|
||||
|
||||
export const searchIibOutputByPrompt = async (req: PromptSearchReq) => {
|
||||
const resp = await axiosInst.value.post('/db/search_iib_output_by_prompt', req, { timeout: Infinity })
|
||||
return resp.data as PromptSearchResp
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ export const getTargetFolderFiles = async (folder_path: string) => {
|
|||
|
||||
export const deleteFiles = async (file_paths: string[]) => {
|
||||
const resp = await axiosInst.value.post('/delete_files', { file_paths })
|
||||
return resp.data as { files: FileNodeInfo[] }
|
||||
return resp.data as { ok: true }
|
||||
}
|
||||
|
||||
export const moveFiles = async (
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const addInterceptor = (axiosInst: AxiosInstance) => {
|
|||
(resp) => resp,
|
||||
async (err) => {
|
||||
if (isAxiosError(err)) {
|
||||
if (err.response?.status === 401) {
|
||||
if (err.response?.status === 401 && err.response?.data?.detail?.type === 'secret_verification_failed') {
|
||||
const key = await promptServerKeyOnce()
|
||||
if (!key) {
|
||||
// user cancelled; leave the request rejected as-is
|
||||
|
|
@ -154,6 +154,15 @@ export const getGlobalSetting = async () => {
|
|||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后端原始 global_setting(包含 app_fe_setting),不受 isSync() 影响。
|
||||
* 仅在确实需要使用后端 KV(GlobalSetting 表)做持久化时使用。
|
||||
*/
|
||||
export const getGlobalSettingRaw = async () => {
|
||||
const resp = await axiosInst.value.get('/global_setting')
|
||||
return resp.data as GlobalConf
|
||||
}
|
||||
|
||||
export const getVersion = async () => {
|
||||
const resp = await axiosInst.value.get('/version')
|
||||
return resp.data as { hash?: string, tag?: string }
|
||||
|
|
@ -207,6 +216,14 @@ export const setAppFeSetting = async (name: keyof GlobalConf['app_fe_setting'],
|
|||
await axiosInst.value.post('/app_fe_setting', { name, value: JSON.stringify(setting) })
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制写入后端 app_fe_setting KV,不依赖 isSync()。
|
||||
* 用于需要“后端持久化”的少量功能开关/配置(例如 TopicSearch 的向量化范围)。
|
||||
*/
|
||||
export const setAppFeSettingForce = async (name: string, setting: Record<string, any>) => {
|
||||
await axiosInst.value.post('/app_fe_setting', { name, value: JSON.stringify(setting) })
|
||||
}
|
||||
|
||||
export const removeAppFeSetting = async (name: keyof GlobalConf['app_fe_setting']) => {
|
||||
if (!isSync()) return
|
||||
await axiosInst.value.delete('/app_fe_setting', { data: { name } })
|
||||
|
|
@ -214,4 +231,39 @@ export const removeAppFeSetting = async (name: keyof GlobalConf['app_fe_setting'
|
|||
|
||||
export const setTargetFrameAsCover = async (body: { path: string, base64_img: string, updated_time: string }) => {
|
||||
await axiosInst.value.post('/set_target_frame_as_video_cover', body)
|
||||
}
|
||||
|
||||
// AI 相关 API
|
||||
export interface AIChatMessage {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface AIChatRequest {
|
||||
messages: AIChatMessage[]
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
stream?: boolean
|
||||
}
|
||||
|
||||
export interface AIChatResponse {
|
||||
id: string
|
||||
object: string
|
||||
created: number
|
||||
model: string
|
||||
choices: Array<{
|
||||
index: number
|
||||
message: AIChatMessage
|
||||
finish_reason: string
|
||||
}>
|
||||
usage: {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
}
|
||||
}
|
||||
|
||||
export const aiChat = async (req: AIChatRequest) => {
|
||||
const resp = await axiosInst.value.post('/ai-chat', req)
|
||||
return resp.data as AIChatResponse
|
||||
}
|
||||
|
|
@ -20,6 +20,53 @@ export const de: Partial<IIBI18nMap> = {
|
|||
exclude: 'Ausschliessen',
|
||||
showTiktokNavigator: 'Navigationsschaltflächen anzeigen',
|
||||
showTiktokNavigatorDesc: 'Zeigen Sie die Navigationstasten (Pfeile nach oben/unten) in der TikTok-Ansicht an',
|
||||
|
||||
// ===== Topic Search (Experimental) =====
|
||||
topicSearchExperimental: 'Natürliche Sprach-Kategorisierung & Suche (Experimentell)',
|
||||
topicSearchTitleExperimental: 'Natürliche Sprach-Kategorisierung & Suche (Experimentell)',
|
||||
topicSearchScope: 'Bereich',
|
||||
topicSearchNeedScope: 'Bitte zuerst einen Bereich (Ordner) auswählen',
|
||||
topicSearchQueryPlaceholder: 'Geben Sie einen Satz ein, um ähnliche Bilder zu finden (RAG-ähnliche Suche)',
|
||||
topicSearchOpenResults: 'Ergebnisse öffnen',
|
||||
topicSearchThreshold: 'Schwelle',
|
||||
topicSearchMinClusterSize: 'Min. Cluster',
|
||||
topicSearchEmptyNoScope: 'Bitte zuerst einen Bereich auswählen, dann aktualisieren/kategorisieren',
|
||||
topicSearchEmptyNoTopics: 'Noch keine Themen (versuchen Sie „Min. Cluster“ zu verringern oder „Schwelle“ anzupassen)',
|
||||
topicSearchChooseScope: 'Bereich auswählen',
|
||||
topicSearchRefreshAndCluster: 'Aktualisieren/Kategorisieren',
|
||||
topicSearchScopeModalTitle: 'Vektorisierungsbereich auswählen (Ordner)',
|
||||
topicSearchScopeTip: 'Standardmäßig ist kein Bereich aktiviert. Sie müssen Ordner auswählen, um zu kategorisieren/suchen. Bereich stammt aus QuickMovePaths.',
|
||||
topicSearchSavingToBackend: 'Wird im Backend gespeichert...',
|
||||
topicSearchScopePlaceholder: 'Ordner auswählen (Mehrfachauswahl)',
|
||||
topicSearchRecallMsg: '{0} / {1} abgerufen (TopK={2})',
|
||||
topicSearchCacheStale: 'Zwischengespeichertes Ergebnis wird angezeigt (Update verfügbar)',
|
||||
topicSearchCacheStaleDesc: 'Ordner im ausgewählten Bereich könnten geändert worden sein. Der Cache könnte veraltet sein. Klicken Sie auf „Update“, um Themen neu zu erzeugen (Index wird zuerst aktualisiert).',
|
||||
topicSearchCacheUpdate: 'Cache aktualisieren',
|
||||
|
||||
topicSearchGuideTitle: 'Schnellstart (Experimentell)',
|
||||
topicSearchGuideStep1: 'Wählen Sie die Ordner (Bereich) zur Analyse aus (Mehrfachauswahl)',
|
||||
topicSearchGuideStep2: 'Klicken Sie auf „Aktualisieren", um Themenkarten zu erzeugen (inkrementelle Vektorisierung)',
|
||||
topicSearchGuideStep3: 'Geben Sie einen Satz ein, um zu suchen; ähnliche Bilder werden abgerufen und die Ergebnisse geöffnet',
|
||||
topicSearchGuideAdvantage1: 'Automatische Gruppierung nach semantischer Ähnlichkeit: KI entdeckt automatisch ähnliche Themen ohne manuelle Kategorisierung',
|
||||
topicSearchGuideAdvantage2: 'Natürliche Sprachsemantiksuche: Schnelles Finden verwandter Bilder mit einem Satz, ähnlich der RAG-Suche',
|
||||
topicSearchGuideEmptyReasonNoScope: 'Leer, weil: kein Bereich ausgewählt (standardmäßig deaktiviert). Klicken Sie auf „Bereich“, um Ordner zu wählen.',
|
||||
topicSearchGuideEmptyReasonNoTopics: 'Leer, weil: für diesen Bereich noch keine Themen erzeugt wurden (Aktualisieren oder Min. Cluster/Schwelle senken).',
|
||||
topicSearchRequirementsTitle: 'Voraussetzungen',
|
||||
topicSearchRequirementsOpenai: 'Erforderlich: OPENAI_BASE_URL und OPENAI_API_KEY (Backend-Umgebungsvariablen)',
|
||||
topicSearchRequirementsDepsPython: 'Erforderlich: Python-Abhängigkeiten numpy und hnswlib (ohne sie ist die Funktion deaktiviert)',
|
||||
topicSearchRequirementsDepsDesktop: 'Desktop-App: Abhängigkeiten sind enthalten (numpy/hnswlib müssen nicht installiert werden)',
|
||||
topicSearchRequirementsInstallCmd: 'Installation: pip install numpy hnswlib',
|
||||
topicSearchJobFailed: 'Job fehlgeschlagen',
|
||||
topicSearchJobStage: 'Phase',
|
||||
topicSearchJobQueued: 'Job in Warteschlange…',
|
||||
topicSearchJobStageEmbedding: 'Vektorisierung…',
|
||||
topicSearchJobStageClustering: 'Clustering…',
|
||||
topicSearchJobStageTitling: 'Titel werden erzeugt…',
|
||||
topicSearchJobStageDone: 'Fertig',
|
||||
topicSearchJobStageError: 'Fehler',
|
||||
topicSearchJobEmbeddingDesc: '{0}/{1} vektorisiert (gescannt {2}); aktuell: {3}',
|
||||
topicSearchJobClusteringDesc: 'Clustering {0}/{1}',
|
||||
topicSearchJobTitlingDesc: 'Titel {0}/{1}',
|
||||
'auto.refreshed': 'Automatische Aktualisierung erfolgreich durchgeführt!',
|
||||
copied: 'In die Zwischenablage kopiert!',
|
||||
'index.expired': 'Index abgelaufen, automatische Aktualisierung wird durchgeführt',
|
||||
|
|
@ -81,6 +128,7 @@ export const de: Partial<IIBI18nMap> = {
|
|||
'Fehler beim Senden des Bildes. Bitte kontaktieren Sie den Entwickler mit der Fehlermeldung aus der Konsole.',
|
||||
confirmDelete: 'Sind Sie sicher, dass Sie dies löschen möchten?',
|
||||
deleteSuccess: 'Erfolgreich gelöscht',
|
||||
moveToTrashSuccess: 'In den Papierkorb verschoben',
|
||||
doubleClickToCopy: 'Doppelklick zum Kopieren',
|
||||
root: 'Root',
|
||||
drive: ' Laufwerk',
|
||||
|
|
@ -148,5 +196,13 @@ export const de: Partial<IIBI18nMap> = {
|
|||
sortByDate: 'Nach Datum sortieren',
|
||||
fileTypeFilter: 'Dateityp-Filter',
|
||||
allFiles: 'Alle Dateien',
|
||||
audio: 'Audio'
|
||||
audio: 'Audio',
|
||||
aiAnalyzeTags: 'KI-Tags analysieren',
|
||||
aiAnalyzeTagsNoPrompt: 'Kein Prompt gefunden',
|
||||
aiAnalyzeTagsNoCustomTags: 'Keine benutzerdefinierten Tags verfügbar',
|
||||
aiAnalyzeTagsNoMatchedTags: 'KI hat keine passenden Tags gefunden',
|
||||
aiAnalyzeTagsNoValidTags: 'Keine gültigen passenden Tags gefunden',
|
||||
aiAnalyzeTagsAllTagsAlreadyAdded: 'Alle passenden Tags wurden bereits zum Bild hinzugefügt',
|
||||
aiAnalyzeTagsSuccess: '{0} Tags hinzugefügt: {1}',
|
||||
aiAnalyzeTagsFailed: 'KI-Tag-Analyse fehlgeschlagen, bitte Konfiguration überprüfen'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,53 @@ export const en: IIBI18nMap = {
|
|||
clearCacheIfNotTakeEffect: 'If the changes do not take effect, try clearing the page cache',
|
||||
showTiktokNavigator: 'Show Navigation Buttons',
|
||||
showTiktokNavigatorDesc: 'Show the navigation buttons (up/down arrows) in TikTok view',
|
||||
|
||||
// ===== Topic Search (Experimental) =====
|
||||
topicSearchExperimental: 'Natural Language Categorization & Search (Experimental)',
|
||||
topicSearchTitleExperimental: 'Natural Language Categorization & Search (Experimental)',
|
||||
topicSearchScope: 'Scope',
|
||||
topicSearchNeedScope: 'Please select a scope folder first',
|
||||
topicSearchQueryPlaceholder: 'Type a sentence to retrieve similar images (RAG-like retrieval)',
|
||||
topicSearchOpenResults: 'Open results',
|
||||
topicSearchThreshold: 'Threshold',
|
||||
topicSearchMinClusterSize: 'Min cluster',
|
||||
topicSearchEmptyNoScope: 'Please select a scope, then refresh/categorize',
|
||||
topicSearchEmptyNoTopics: 'No topics yet (try lowering “Min cluster” or adjusting “Threshold”)',
|
||||
topicSearchChooseScope: 'Select scope',
|
||||
topicSearchRefreshAndCluster: 'Refresh/Categorize',
|
||||
topicSearchScopeModalTitle: 'Select vectorization scope (folders)',
|
||||
topicSearchScopeTip: 'No scope is enabled by default. You must select folders to categorize/search. Scope comes from QuickMovePaths.',
|
||||
topicSearchSavingToBackend: 'Saving to backend...',
|
||||
topicSearchScopePlaceholder: 'Select folders (multi-select)',
|
||||
topicSearchRecallMsg: 'Retrieved {0} / {1} (TopK={2})',
|
||||
topicSearchCacheStale: 'Showing cached result (update available)',
|
||||
topicSearchCacheStaleDesc: 'Folders in the selected scope may have changed. The cache may be stale. Click Update to regenerate topics (will update index first).',
|
||||
topicSearchCacheUpdate: 'Update cache',
|
||||
|
||||
topicSearchGuideTitle: 'Quick Start (Experimental)',
|
||||
topicSearchGuideStep1: 'Select the scope folders to analyze (multi-select)',
|
||||
topicSearchGuideStep2: 'Click Refresh to generate topic cards (incremental vectorization)',
|
||||
topicSearchGuideStep3: 'Type a sentence to search; it will retrieve similar images and open the result page',
|
||||
topicSearchGuideAdvantage1: 'Auto-grouping by semantic similarity: AI automatically discovers similar themes without manual categorization',
|
||||
topicSearchGuideAdvantage2: 'Natural language semantic search: Quickly find related images with a sentence, similar to RAG retrieval',
|
||||
topicSearchGuideEmptyReasonNoScope: 'Empty because: no scope selected (disabled by default). Click “Scope” to choose folders.',
|
||||
topicSearchGuideEmptyReasonNoTopics: 'Empty because: no topics generated yet for this scope (try Refresh or lower Min cluster/Threshold).',
|
||||
topicSearchRequirementsTitle: 'Requirements',
|
||||
topicSearchRequirementsOpenai: 'Required: OPENAI_BASE_URL and OPENAI_API_KEY (backend environment variables)',
|
||||
topicSearchRequirementsDepsPython: 'Required: Python deps numpy and hnswlib (feature is disabled if missing)',
|
||||
topicSearchRequirementsDepsDesktop: 'Desktop app: deps are bundled (no need to install numpy/hnswlib)',
|
||||
topicSearchRequirementsInstallCmd: 'Install: pip install numpy hnswlib',
|
||||
topicSearchJobFailed: 'Job failed',
|
||||
topicSearchJobStage: 'Stage',
|
||||
topicSearchJobQueued: 'Job queued…',
|
||||
topicSearchJobStageEmbedding: 'Embedding…',
|
||||
topicSearchJobStageClustering: 'Clustering…',
|
||||
topicSearchJobStageTitling: 'Generating titles…',
|
||||
topicSearchJobStageDone: 'Done',
|
||||
topicSearchJobStageError: 'Error',
|
||||
topicSearchJobEmbeddingDesc: 'Embedded {0}/{1} (scanned {2}); current: {3}',
|
||||
topicSearchJobClusteringDesc: 'Clustering {0}/{1}',
|
||||
topicSearchJobTitlingDesc: 'Titling {0}/{1}',
|
||||
success: 'Success',
|
||||
setCurrFrameAsVideoPoster: 'Set Current Frame as Video Cover',
|
||||
sync: 'Sync',
|
||||
|
|
@ -269,6 +316,7 @@ You can specify which snapshot to restore to when starting IIB in the global set
|
|||
'Failed to send image. Please contact the developer with the error message from the console.',
|
||||
confirmDelete: 'Are you sure you want to delete?',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
moveToTrashSuccess: 'Moved to trash',
|
||||
doubleClickToCopy: 'Double-click to copy',
|
||||
root: 'Root',
|
||||
drive: ' drive',
|
||||
|
|
@ -384,5 +432,13 @@ You can specify which snapshot to restore to when starting IIB in the global set
|
|||
'autoTag.fields.seed': 'Seed',
|
||||
'autoTag.operators.contains': 'Contains',
|
||||
'autoTag.operators.equals': 'Equals',
|
||||
'autoTag.operators.regex': 'Regex'
|
||||
'autoTag.operators.regex': 'Regex',
|
||||
aiAnalyzeTags: 'AI Analyze Tags',
|
||||
aiAnalyzeTagsNoPrompt: 'No prompt found',
|
||||
aiAnalyzeTagsNoCustomTags: 'No custom tags available',
|
||||
aiAnalyzeTagsNoMatchedTags: 'AI found no matching tags',
|
||||
aiAnalyzeTagsNoValidTags: 'No valid matching tags found',
|
||||
aiAnalyzeTagsAllTagsAlreadyAdded: 'All matched tags have already been added to the image',
|
||||
aiAnalyzeTagsSuccess: 'Added {0} tags: {1}',
|
||||
aiAnalyzeTagsFailed: 'AI tag analysis failed, please check configuration'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,53 @@ export const zhHans = {
|
|||
clearCacheIfNotTakeEffect: '如果更改没有生效,请尝试清理页面缓存',
|
||||
showTiktokNavigator: '显示导航按钮',
|
||||
showTiktokNavigatorDesc: '在 TikTok 视图中显示导航按钮(上/下箭头)',
|
||||
|
||||
// ===== Topic Search (Experimental) =====
|
||||
topicSearchExperimental: '自然语言分类&搜索(实验性)',
|
||||
topicSearchTitleExperimental: '自然语言分类 & 搜索(实验性)',
|
||||
topicSearchScope: '范围',
|
||||
topicSearchNeedScope: '请先选择向量化范围(文件夹)',
|
||||
topicSearchQueryPlaceholder: '输入一句话,召回相似图片(RAG 召回)',
|
||||
topicSearchOpenResults: '打开结果',
|
||||
topicSearchThreshold: '阈值',
|
||||
topicSearchMinClusterSize: '最小组',
|
||||
topicSearchEmptyNoScope: '请先选择范围,然后刷新/归类',
|
||||
topicSearchEmptyNoTopics: '暂无主题结果(可尝试降低“最小组”或调整“阈值”)',
|
||||
topicSearchChooseScope: '选择范围',
|
||||
topicSearchRefreshAndCluster: '刷新/归类',
|
||||
topicSearchScopeModalTitle: '选择向量化范围(文件夹)',
|
||||
topicSearchScopeTip: '默认不启用任何范围;必须选择后才能归类/搜索。范围来源于 QuickMovePaths。',
|
||||
topicSearchSavingToBackend: '正在保存到后端...',
|
||||
topicSearchScopePlaceholder: '选择文件夹(可多选)',
|
||||
topicSearchRecallMsg: '召回 {0} / {1}(TopK={2})',
|
||||
topicSearchCacheStale: '已显示缓存结果(可更新)',
|
||||
topicSearchCacheStaleDesc: '检测到范围内文件夹可能有变更,缓存可能已过期。可点击更新重新生成主题(会先更新索引)。',
|
||||
topicSearchCacheUpdate: '更新缓存',
|
||||
|
||||
topicSearchGuideTitle: '快速上手(实验性)',
|
||||
topicSearchGuideStep1: '选择要分析的文件夹范围(可多选)',
|
||||
topicSearchGuideStep2: '点击刷新,生成主题卡片(会增量向量化)',
|
||||
topicSearchGuideStep3: '输入一句话搜索,会召回相似图片并打开结果页',
|
||||
topicSearchGuideAdvantage1: '基于语义相似度自动分组:AI自动发现相似主题,无需手动分类',
|
||||
topicSearchGuideAdvantage2: '自然语言语义检索:用一句话快速找到相关图片,类似RAG检索',
|
||||
topicSearchGuideEmptyReasonNoScope: '当前为空:未选择范围(已默认关闭),请先点“范围”选择文件夹',
|
||||
topicSearchGuideEmptyReasonNoTopics: '当前为空:该范围内还未生成主题(可点刷新,或调低最小组/阈值)',
|
||||
topicSearchRequirementsTitle: '使用前置条件',
|
||||
topicSearchRequirementsOpenai: '必须配置:OPENAI_BASE_URL、OPENAI_API_KEY(后端环境变量)',
|
||||
topicSearchRequirementsDepsPython: '必须安装:Python 依赖 numpy、hnswlib(缺少则功能不可用)',
|
||||
topicSearchRequirementsDepsDesktop: '桌面客户端:已内置依赖(无需手动安装 numpy/hnswlib)',
|
||||
topicSearchRequirementsInstallCmd: '安装命令:pip install numpy hnswlib',
|
||||
topicSearchJobFailed: '任务失败',
|
||||
topicSearchJobStage: '阶段',
|
||||
topicSearchJobQueued: '已提交任务,准备开始…',
|
||||
topicSearchJobStageEmbedding: '向量化中(Embedding)',
|
||||
topicSearchJobStageClustering: '归类中(Clustering)',
|
||||
topicSearchJobStageTitling: '生成标题中(LLM)',
|
||||
topicSearchJobStageDone: '完成',
|
||||
topicSearchJobStageError: '失败',
|
||||
topicSearchJobEmbeddingDesc: '已向量化 {0}/{1}(扫描 {2});当前:{3}',
|
||||
topicSearchJobClusteringDesc: '正在归类 {0}/{1}',
|
||||
topicSearchJobTitlingDesc: '正在生成标题 {0}/{1}',
|
||||
success: '成功',
|
||||
setCurrFrameAsVideoPoster: '设置当前帧为视频封面',
|
||||
sync: '同步',
|
||||
|
|
@ -128,6 +175,7 @@ export const zhHans = {
|
|||
sendImageFailed: '发送图像失败,请携带console的错误消息找开发者',
|
||||
confirmDelete: '确认删除?',
|
||||
deleteSuccess: '删除成功',
|
||||
moveToTrashSuccess: '已移动到回收站',
|
||||
doubleClickToCopy: '双击复制',
|
||||
root: '根',
|
||||
drive: '盘',
|
||||
|
|
@ -362,5 +410,13 @@ export const zhHans = {
|
|||
'autoTag.fields.seed': 'Seed',
|
||||
'autoTag.operators.contains': '包含 (Contains)',
|
||||
'autoTag.operators.equals': '等于 (Equals)',
|
||||
'autoTag.operators.regex': '正则 (Regex)'
|
||||
'autoTag.operators.regex': '正则 (Regex)',
|
||||
aiAnalyzeTags: 'AI分析标签',
|
||||
aiAnalyzeTagsNoPrompt: '没有找到提示词',
|
||||
aiAnalyzeTagsNoCustomTags: '没有自定义标签',
|
||||
aiAnalyzeTagsNoMatchedTags: 'AI没有找到匹配的标签',
|
||||
aiAnalyzeTagsNoValidTags: '没有找到有效的匹配标签',
|
||||
aiAnalyzeTagsAllTagsAlreadyAdded: '所有匹配的标签已经添加到图像上了',
|
||||
aiAnalyzeTagsSuccess: '已添加 {0} 个标签:{1}',
|
||||
aiAnalyzeTagsFailed: 'AI分析标签失败,请检查配置'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,53 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
addNewCustomTag: '添加新的自定義標籤',
|
||||
showTiktokNavigator: '顯示導航按鈕',
|
||||
showTiktokNavigatorDesc: '在 TikTok 視圖中顯示導航按鈕(上/下箭頭)',
|
||||
|
||||
// ===== Topic Search (Experimental) =====
|
||||
topicSearchExperimental: '自然語言分類&搜尋(實驗性)',
|
||||
topicSearchTitleExperimental: '自然語言分類 & 搜尋(實驗性)',
|
||||
topicSearchScope: '範圍',
|
||||
topicSearchNeedScope: '請先選擇向量化範圍(資料夾)',
|
||||
topicSearchQueryPlaceholder: '輸入一句話,召回相似圖片(RAG 召回)',
|
||||
topicSearchOpenResults: '打開結果',
|
||||
topicSearchThreshold: '閾值',
|
||||
topicSearchMinClusterSize: '最小組',
|
||||
topicSearchEmptyNoScope: '請先選擇範圍,然後刷新/歸類',
|
||||
topicSearchEmptyNoTopics: '暫無主題結果(可嘗試降低「最小組」或調整「閾值」)',
|
||||
topicSearchChooseScope: '選擇範圍',
|
||||
topicSearchRefreshAndCluster: '刷新/歸類',
|
||||
topicSearchScopeModalTitle: '選擇向量化範圍(資料夾)',
|
||||
topicSearchScopeTip: '預設不啟用任何範圍;必須選擇後才能歸類/搜尋。範圍來源於 QuickMovePaths。',
|
||||
topicSearchSavingToBackend: '正在保存到後端...',
|
||||
topicSearchScopePlaceholder: '選擇資料夾(可多選)',
|
||||
topicSearchRecallMsg: '召回 {0} / {1}(TopK={2})',
|
||||
topicSearchCacheStale: '已顯示快取結果(可更新)',
|
||||
topicSearchCacheStaleDesc: '偵測到範圍內資料夾可能有變更,快取可能已過期。可點擊更新重新生成主題(會先更新索引)。',
|
||||
topicSearchCacheUpdate: '更新快取',
|
||||
|
||||
topicSearchGuideTitle: '快速上手(實驗性)',
|
||||
topicSearchGuideStep1: '選擇要分析的資料夾範圍(可多選)',
|
||||
topicSearchGuideStep2: '點擊刷新,生成主題卡片(會增量向量化)',
|
||||
topicSearchGuideStep3: '輸入一句話搜尋,召回相似圖片並打開結果頁',
|
||||
topicSearchGuideAdvantage1: '基於語義相似度自動分組:AI自動發現相似主題,無需手動分類',
|
||||
topicSearchGuideAdvantage2: '自然語言語義檢索:用一句話快速找到相關圖片,類似RAG檢索',
|
||||
topicSearchGuideEmptyReasonNoScope: '目前為空:尚未選擇範圍(預設關閉),請先點「範圍」選擇資料夾',
|
||||
topicSearchGuideEmptyReasonNoTopics: '目前為空:此範圍尚未生成主題(可點刷新,或調低最小組/閾值)',
|
||||
topicSearchRequirementsTitle: '使用前置條件',
|
||||
topicSearchRequirementsOpenai: '必須配置:OPENAI_BASE_URL、OPENAI_API_KEY(後端環境變數)',
|
||||
topicSearchRequirementsDepsPython: '必須安裝:Python 依賴 numpy、hnswlib(缺少則功能不可用)',
|
||||
topicSearchRequirementsDepsDesktop: '桌面客戶端:已內建依賴(無需手動安裝 numpy/hnswlib)',
|
||||
topicSearchRequirementsInstallCmd: '安裝命令:pip install numpy hnswlib',
|
||||
topicSearchJobFailed: '任務失敗',
|
||||
topicSearchJobStage: '階段',
|
||||
topicSearchJobQueued: '已提交任務,準備開始…',
|
||||
topicSearchJobStageEmbedding: '向量化中(Embedding)',
|
||||
topicSearchJobStageClustering: '歸類中(Clustering)',
|
||||
topicSearchJobStageTitling: '生成標題中(LLM)',
|
||||
topicSearchJobStageDone: '完成',
|
||||
topicSearchJobStageError: '失敗',
|
||||
topicSearchJobEmbeddingDesc: '已向量化 {0}/{1}(掃描 {2});目前:{3}',
|
||||
topicSearchJobClusteringDesc: '正在歸類 {0}/{1}',
|
||||
topicSearchJobTitlingDesc: '正在生成標題 {0}/{1}',
|
||||
clearCacheIfNotTakeEffect: '如果更改沒有生效,請嘗試清理頁面緩存',
|
||||
success: '成功',
|
||||
setCurrFrameAsVideoPoster: '設置當前幀為視頻封面',
|
||||
|
|
@ -134,6 +181,7 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
sendImageFailed: '發送圖像失敗,請攜帶console的錯誤訊息找開發者',
|
||||
confirmDelete: '確認刪除?',
|
||||
deleteSuccess: '刪除成功',
|
||||
moveToTrashSuccess: '已移動到回收站',
|
||||
doubleClickToCopy: '雙擊複製',
|
||||
promptcompare: 'Compare Prompts',
|
||||
root: '根',
|
||||
|
|
@ -366,5 +414,13 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
'autoTag.fields.seed': 'Seed',
|
||||
'autoTag.operators.contains': '包含 (Contains)',
|
||||
'autoTag.operators.equals': '等於 (Equals)',
|
||||
'autoTag.operators.regex': '正則 (Regex)'
|
||||
'autoTag.operators.regex': '正則 (Regex)',
|
||||
aiAnalyzeTags: 'AI分析標籤',
|
||||
aiAnalyzeTagsNoPrompt: '沒有找到提示詞',
|
||||
aiAnalyzeTagsNoCustomTags: '沒有自定義標籤',
|
||||
aiAnalyzeTagsNoMatchedTags: 'AI沒有找到匹配的標籤',
|
||||
aiAnalyzeTagsNoValidTags: '沒有找到有效的匹配標籤',
|
||||
aiAnalyzeTagsAllTagsAlreadyAdded: '所有匹配的標籤已經添加到圖像上了',
|
||||
aiAnalyzeTagsSuccess: '已添加 {0} 個標籤:{1}',
|
||||
aiAnalyzeTagsFailed: 'AI分析標籤失敗,請檢查配置'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,12 @@ const compMap: Record<TabPane['type'], ReturnType<typeof defineAsyncComponent>>
|
|||
'tag-search-matched-image-grid': defineAsyncComponent(
|
||||
() => import('@/page/TagSearch/MatchedImageGrid.vue')
|
||||
),
|
||||
'topic-search-matched-image-grid': defineAsyncComponent(
|
||||
() => import('@/page/TopicSearch/MatchedImageGrid.vue')
|
||||
),
|
||||
'tag-search': defineAsyncComponent(() => import('@/page/TagSearch/TagSearch.vue')),
|
||||
'fuzzy-search': defineAsyncComponent(() => import('@/page/TagSearch/SubstrSearch.vue')),
|
||||
'topic-search': defineAsyncComponent(() => import('@/page/TopicSearch/TopicSearch.vue')),
|
||||
'img-sli': defineAsyncComponent(() => import('@/page/ImgSli/ImgSliPagePane.vue')),
|
||||
'batch-download': defineAsyncComponent(() => import('@/page/batchDownload/batchDownload.vue')),
|
||||
'grid-view': defineAsyncComponent(() => import('@/page/gridView/gridView.vue')),
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const compCnMap: Partial<Record<TabPane['type'], string>> = {
|
|||
local: t('local'),
|
||||
'tag-search': t('imgSearch'),
|
||||
'fuzzy-search': t('fuzzy-search'),
|
||||
'topic-search': t('topicSearchExperimental'),
|
||||
'batch-download': t('batchDownload') + ' / ' + t('archive'),
|
||||
'workspace-snapshot': t('WorkspaceSnapshot'),
|
||||
'random-image': t('randomImage'),
|
||||
|
|
@ -51,6 +52,7 @@ const createPane = (type: TabPane['type'], path?: string, mode?: FileTransModeIn
|
|||
switch (type) {
|
||||
case 'grid-view':
|
||||
case 'tag-search-matched-image-grid':
|
||||
case 'topic-search-matched-image-grid':
|
||||
case 'img-sli':
|
||||
return
|
||||
case 'global-setting':
|
||||
|
|
@ -58,6 +60,7 @@ const createPane = (type: TabPane['type'], path?: string, mode?: FileTransModeIn
|
|||
case 'batch-download':
|
||||
case 'workspace-snapshot':
|
||||
case 'fuzzy-search':
|
||||
case 'topic-search':
|
||||
case 'random-image':
|
||||
case 'empty':
|
||||
pane = { type, name: compCnMap[type]!, key: Date.now() + uniqueId() }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
<script lang="ts" setup>
|
||||
import fileItemCell from '@/components/FileItem.vue'
|
||||
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
// @ts-ignore
|
||||
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
||||
import { toRawFileUrl } from '@/util/file'
|
||||
import { nextTick, watch, reactive } from 'vue'
|
||||
import { copy2clipboardI18n } from '@/util'
|
||||
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
|
||||
import { LeftCircleOutlined, RightCircleOutlined } from '@/icon'
|
||||
import { useImageSearch } from '@/page/TagSearch/hook'
|
||||
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { useKeepMultiSelect } from '@/page/fileTransfer/hook'
|
||||
import { openTiktokViewWithFiles } from '@/util/tiktokHelper'
|
||||
import { batchGetFilesInfo } from '@/api/files'
|
||||
|
||||
const props = defineProps<{
|
||||
tabIdx: number
|
||||
paneIdx: number
|
||||
id: string
|
||||
title: string
|
||||
paths: string[]
|
||||
}>()
|
||||
|
||||
type Iter = {
|
||||
res: any[]
|
||||
load: boolean
|
||||
loading: boolean
|
||||
next: () => Promise<void>
|
||||
reset: (opts?: { refetch?: boolean }) => Promise<void>
|
||||
}
|
||||
|
||||
const iter = reactive<Iter>({
|
||||
res: [],
|
||||
load: false,
|
||||
loading: false,
|
||||
async next() {
|
||||
if (iter.loading || iter.load) return
|
||||
iter.loading = true
|
||||
try {
|
||||
const pageSize = 200
|
||||
const offset = iter.res.length
|
||||
const slice = (props.paths ?? []).slice(offset, offset + pageSize)
|
||||
if (!slice.length) {
|
||||
iter.load = true
|
||||
return
|
||||
}
|
||||
const infoMap = await batchGetFilesInfo(slice)
|
||||
const files = slice.map((p) => infoMap[p]).filter(Boolean)
|
||||
iter.res.push(...files)
|
||||
if (offset + pageSize >= (props.paths?.length ?? 0)) {
|
||||
iter.load = true
|
||||
}
|
||||
} finally {
|
||||
iter.loading = false
|
||||
}
|
||||
},
|
||||
async reset() {
|
||||
iter.res = []
|
||||
iter.load = false
|
||||
await iter.next()
|
||||
}
|
||||
})
|
||||
|
||||
const {
|
||||
queue,
|
||||
images,
|
||||
onContextMenuClickU,
|
||||
stackViewEl,
|
||||
previewIdx,
|
||||
previewing,
|
||||
onPreviewVisibleChange,
|
||||
previewImgMove,
|
||||
canPreview,
|
||||
itemSize,
|
||||
gridItems,
|
||||
showGenInfo,
|
||||
imageGenInfo,
|
||||
q: genInfoQueue,
|
||||
multiSelectedIdxs,
|
||||
onFileItemClick,
|
||||
scroller,
|
||||
showMenuIdx,
|
||||
onFileDragStart,
|
||||
onFileDragEnd,
|
||||
cellWidth,
|
||||
onScroll,
|
||||
saveAllFileAsJson,
|
||||
saveLoadedFileAsJson,
|
||||
changeIndchecked,
|
||||
seedChangeChecked,
|
||||
getGenDiff,
|
||||
getGenDiffWatchDep
|
||||
} = useImageSearch(iter as any)
|
||||
|
||||
watch(
|
||||
() => props.paths,
|
||||
async () => {
|
||||
await iter.reset({ refetch: true } as any)
|
||||
await nextTick()
|
||||
scroller.value?.scrollToItem(0)
|
||||
onScroll()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const g = useGlobalStore()
|
||||
const { onClearAllSelected, onSelectAll, onReverseSelect } = useKeepMultiSelect()
|
||||
|
||||
const onTiktokViewClick = () => {
|
||||
if (images.value.length === 0) return
|
||||
openTiktokViewWithFiles(images.value, 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container" ref="stackViewEl">
|
||||
<MultiSelectKeep
|
||||
:show="!!multiSelectedIdxs.length || g.keepMultiSelect"
|
||||
@clear-all-selected="onClearAllSelected"
|
||||
@select-all="onSelectAll"
|
||||
@reverse-select="onReverseSelect"
|
||||
/>
|
||||
<ASpin size="large" :spinning="!queue.isIdle || iter.loading">
|
||||
<AModal v-model:visible="showGenInfo" width="70vw" mask-closable @ok="showGenInfo = false">
|
||||
<template #cancelText />
|
||||
<ASkeleton active :loading="!genInfoQueue.isIdle">
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
white-space: pre-line;
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
"
|
||||
@dblclick="copy2clipboardI18n(imageGenInfo)"
|
||||
>
|
||||
<div class="hint">{{ $t('doubleClickToCopy') }}</div>
|
||||
{{ imageGenInfo }}
|
||||
</div>
|
||||
</ASkeleton>
|
||||
</AModal>
|
||||
|
||||
<div class="action-bar">
|
||||
<div class="title line-clamp-1">🧩 {{ props.title }}</div>
|
||||
<div flex-placeholder />
|
||||
<a-button @click="onTiktokViewClick" :disabled="!images?.length">{{ $t('tiktokView') }}</a-button>
|
||||
<a-button @click="saveLoadedFileAsJson" :disabled="!images?.length">{{ $t('saveLoadedImageAsJson') }}</a-button>
|
||||
<a-button @click="saveAllFileAsJson" :disabled="!images?.length">{{ $t('saveAllAsJson') }}</a-button>
|
||||
</div>
|
||||
|
||||
<RecycleScroller
|
||||
ref="scroller"
|
||||
class="file-list"
|
||||
v-if="images?.length"
|
||||
:items="images"
|
||||
:item-size="itemSize.first"
|
||||
key-field="fullpath"
|
||||
:item-secondary-size="itemSize.second"
|
||||
:gridItems="gridItems"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<template #after>
|
||||
<div style="padding: 16px 0 512px;" />
|
||||
</template>
|
||||
<template v-slot="{ item: file, index: idx }">
|
||||
<file-item-cell
|
||||
:idx="idx"
|
||||
:file="file"
|
||||
:cell-width="cellWidth"
|
||||
v-model:show-menu-idx="showMenuIdx"
|
||||
@dragstart="onFileDragStart"
|
||||
@dragend="onFileDragEnd"
|
||||
@file-item-click="onFileItemClick"
|
||||
@tiktok-view="(_file, idx) => openTiktokViewWithFiles(images, idx)"
|
||||
:full-screen-preview-image-url="images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''"
|
||||
:selected="multiSelectedIdxs.includes(idx)"
|
||||
@context-menu-click="onContextMenuClickU"
|
||||
@preview-visible-change="onPreviewVisibleChange"
|
||||
:is-selected-mutil-files="multiSelectedIdxs.length > 1"
|
||||
:enable-change-indicator="changeIndchecked"
|
||||
:seed-change-checked="seedChangeChecked"
|
||||
:get-gen-diff="getGenDiff"
|
||||
:get-gen-diff-watch-dep="getGenDiffWatchDep"
|
||||
/>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
|
||||
<div v-else class="no-res-hint">
|
||||
<p class="hint">暂无结果</p>
|
||||
</div>
|
||||
|
||||
<div v-if="previewing" class="preview-switch">
|
||||
<LeftCircleOutlined @click="previewImgMove('prev')" :class="{ disable: !canPreview('prev') }" />
|
||||
<RightCircleOutlined @click="previewImgMove('next')" :class="{ disable: !canPreview('next') }" />
|
||||
</div>
|
||||
</ASpin>
|
||||
|
||||
<fullScreenContextMenu
|
||||
v-if="previewing && images && images[previewIdx]"
|
||||
:file="images[previewIdx]"
|
||||
:idx="previewIdx"
|
||||
@context-menu-click="onContextMenuClickU"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
background: var(--zp-secondary-background);
|
||||
position: relative;
|
||||
height: var(--pane-max-height);
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
max-width: 40vw;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style: none;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
height: calc(var(--pane-max-height) - 44px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-res-hint {
|
||||
height: calc(var(--pane-max-height) - 44px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.hint {
|
||||
font-size: 1.2em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-switch {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 36px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.disable {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,668 @@
|
|||
<script setup lang="ts">
|
||||
import { getGlobalSettingRaw, setAppFeSettingForce } from '@/api'
|
||||
import {
|
||||
getClusterIibOutputJobStatus,
|
||||
getClusterIibOutputCached,
|
||||
searchIibOutputByPrompt,
|
||||
startClusterIibOutputJob,
|
||||
type ClusterIibOutputJobStatusResp,
|
||||
type ClusterIibOutputCachedResp,
|
||||
type ClusterIibOutputResp,
|
||||
type PromptSearchResp
|
||||
} from '@/api/db'
|
||||
import { updateImageData } from '@/api/db'
|
||||
import { t } from '@/i18n'
|
||||
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { uniqueId } from 'lodash-es'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { isTauri } from '@/util/env'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<{ tabIdx: number; paneIdx: number }>()
|
||||
const g = useGlobalStore()
|
||||
|
||||
const loading = ref(false)
|
||||
// Default stricter to avoid over-merged broad topics for natural-language prompts.
|
||||
const threshold = ref(0.9)
|
||||
const minClusterSize = ref(2)
|
||||
const result = ref<ClusterIibOutputResp | null>(null)
|
||||
const cacheInfo = ref<ClusterIibOutputCachedResp | null>(null)
|
||||
|
||||
const _REQS_LS_KEY = 'iib_topic_search_hide_requirements_v1'
|
||||
// true = show requirements; false = hidden
|
||||
const showRequirements = useLocalStorage<boolean>(_REQS_LS_KEY, true)
|
||||
const hideRequirements = () => {
|
||||
showRequirements.value = false
|
||||
}
|
||||
|
||||
const job = ref<ClusterIibOutputJobStatusResp | null>(null)
|
||||
const jobId = ref<string>('')
|
||||
let _jobTimer: any = null
|
||||
|
||||
const jobRunning = computed(() => {
|
||||
const st = job.value?.status
|
||||
return st === 'queued' || st === 'running'
|
||||
})
|
||||
const jobStageText = computed(() => {
|
||||
const st = String(job.value?.stage || '')
|
||||
if (!st || st === 'queued' || st === 'init') return t('topicSearchJobQueued')
|
||||
if (st === 'embedding') return t('topicSearchJobStageEmbedding')
|
||||
if (st === 'clustering') return t('topicSearchJobStageClustering')
|
||||
if (st === 'titling') return t('topicSearchJobStageTitling')
|
||||
if (st === 'done') return t('topicSearchJobStageDone')
|
||||
if (st === 'error') return t('topicSearchJobStageError')
|
||||
return `${t('topicSearchJobStage')}: ${st}`
|
||||
})
|
||||
const jobPercent = computed(() => {
|
||||
const p = job.value?.progress
|
||||
if (!p) return 0
|
||||
const total = Number(p.to_embed ?? 0)
|
||||
const done = Number(p.embedded_done ?? 0)
|
||||
if (total <= 0) return 0
|
||||
const v = Math.floor((done / total) * 100)
|
||||
return Math.max(0, Math.min(100, v))
|
||||
})
|
||||
const jobDesc = computed(() => {
|
||||
const p = job.value?.progress
|
||||
if (!p) return ''
|
||||
if (job.value?.stage === 'embedding') {
|
||||
const done = Number(p.embedded_done ?? 0)
|
||||
const total = Number(p.to_embed ?? 0)
|
||||
const scanned = Number(p.scanned ?? 0)
|
||||
const folder = String(p.folder ?? '')
|
||||
return t('topicSearchJobEmbeddingDesc', [done, total, scanned, folder])
|
||||
}
|
||||
if (job.value?.stage === 'clustering') {
|
||||
const done = Number(p.items_done ?? 0)
|
||||
const total = Number(p.items_total ?? 0)
|
||||
return t('topicSearchJobClusteringDesc', [done, total])
|
||||
}
|
||||
if (job.value?.stage === 'titling') {
|
||||
const done = Number(p.clusters_done ?? 0)
|
||||
const total = Number(p.clusters_total ?? 0)
|
||||
return t('topicSearchJobTitlingDesc', [done, total])
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const query = ref('')
|
||||
const qLoading = ref(false)
|
||||
const qResult = ref<PromptSearchResp | null>(null)
|
||||
|
||||
const scopeOpen = ref(false)
|
||||
const selectedFolders = ref<string[]>([])
|
||||
const _scopeInitDone = ref(false)
|
||||
const _SCOPE_SETTING_NAME = 'topic_search_scope'
|
||||
const _saving = ref(false)
|
||||
let _saveTimer: any = null
|
||||
let _lastSavedSig = ''
|
||||
const excludedDirs = computed(() => {
|
||||
const list = (g.quickMovePaths ?? []) as any[]
|
||||
return list
|
||||
.filter((v) => {
|
||||
const key = String(v?.key ?? '')
|
||||
const types = (v?.types ?? []) as string[]
|
||||
// 去掉默认注入的 3 项:工作文件夹 / home / 桌面
|
||||
return types.includes('preset') && ['cwd', 'home', 'desktop'].includes(key)
|
||||
})
|
||||
.map((v) => String(v?.dir ?? ''))
|
||||
.filter(Boolean)
|
||||
})
|
||||
watch(
|
||||
excludedDirs,
|
||||
(dirs) => {
|
||||
if (!dirs?.length) return
|
||||
selectedFolders.value = (selectedFolders.value ?? []).filter((p) => !dirs.includes(p))
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const folderOptions = computed(() => {
|
||||
const list = (g.quickMovePaths ?? []) as any[]
|
||||
return list
|
||||
.filter((v) => {
|
||||
const key = String(v?.key ?? '')
|
||||
const types = (v?.types ?? []) as string[]
|
||||
return !(types.includes('preset') && ['cwd', 'home', 'desktop'].includes(key))
|
||||
})
|
||||
.map((v) => ({ value: v.dir, label: v.zh || v.dir }))
|
||||
})
|
||||
const scopeCount = computed(() => (selectedFolders.value ?? []).filter(Boolean).length)
|
||||
const scopeFolders = computed(() => (selectedFolders.value ?? []).filter(Boolean))
|
||||
|
||||
const clusters = computed(() => result.value?.clusters ?? [])
|
||||
|
||||
const loadScopeFromBackend = async () => {
|
||||
if (_scopeInitDone.value) return
|
||||
try {
|
||||
const conf = await getGlobalSettingRaw()
|
||||
// 同步到全局 store,避免页面间不一致
|
||||
g.conf = conf as any
|
||||
} catch (e) {
|
||||
// ignore(axios interceptor 会提示错误)
|
||||
}
|
||||
|
||||
const app = (g.conf?.app_fe_setting as any) || {}
|
||||
const savedPaths = app?.[_SCOPE_SETTING_NAME]?.folder_paths
|
||||
if (Array.isArray(savedPaths) && savedPaths.length && (!selectedFolders.value?.length)) {
|
||||
selectedFolders.value = savedPaths.map((x: any) => String(x)).filter(Boolean)
|
||||
}
|
||||
_scopeInitDone.value = true
|
||||
}
|
||||
|
||||
const saveScopeToBackend = async () => {
|
||||
if (g.conf?.is_readonly) return
|
||||
if (!_scopeInitDone.value) return
|
||||
const payload = {
|
||||
folder_paths: scopeFolders.value,
|
||||
updated_at: Date.now()
|
||||
}
|
||||
await setAppFeSettingForce(_SCOPE_SETTING_NAME, payload)
|
||||
if (g.conf?.app_fe_setting) {
|
||||
(g.conf.app_fe_setting as any)[_SCOPE_SETTING_NAME] = payload
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleSaveScope = () => {
|
||||
if (g.conf?.is_readonly) return
|
||||
if (!_scopeInitDone.value) return
|
||||
const sig = JSON.stringify(scopeFolders.value)
|
||||
if (sig === _lastSavedSig) return
|
||||
if (_saveTimer) clearTimeout(_saveTimer)
|
||||
_saveTimer = setTimeout(async () => {
|
||||
_saving.value = true
|
||||
try {
|
||||
await saveScopeToBackend()
|
||||
_lastSavedSig = sig
|
||||
} finally {
|
||||
_saving.value = false
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const stopJobPoll = () => {
|
||||
if (_jobTimer) {
|
||||
clearInterval(_jobTimer)
|
||||
_jobTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const pollJob = async () => {
|
||||
const id = jobId.value
|
||||
if (!id) return
|
||||
const st = await getClusterIibOutputJobStatus(id)
|
||||
job.value = st
|
||||
if (st.status === 'done') {
|
||||
stopJobPoll()
|
||||
loading.value = false
|
||||
if (st.result) result.value = st.result
|
||||
cacheInfo.value = null
|
||||
} else if (st.status === 'error') {
|
||||
stopJobPoll()
|
||||
loading.value = false
|
||||
message.error(st.error || t('topicSearchJobFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const loadCached = async () => {
|
||||
if (!scopeCount.value) return
|
||||
// best-effort: do not block entering the page
|
||||
try {
|
||||
const cached = await getClusterIibOutputCached({
|
||||
threshold: threshold.value,
|
||||
min_cluster_size: minClusterSize.value,
|
||||
lang: g.lang,
|
||||
folder_paths: scopeFolders.value
|
||||
})
|
||||
cacheInfo.value = cached
|
||||
if (cached.cache_hit && cached.result) {
|
||||
result.value = cached.result
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
if (g.conf?.is_readonly) return
|
||||
if (!scopeCount.value) {
|
||||
message.warning(t('topicSearchNeedScope'))
|
||||
scopeOpen.value = true
|
||||
return
|
||||
}
|
||||
stopJobPoll()
|
||||
loading.value = true
|
||||
job.value = null
|
||||
jobId.value = ''
|
||||
try {
|
||||
// Ensure DB file index is up to date before clustering (so newly added/moved images are included).
|
||||
await updateImageData()
|
||||
const started = await startClusterIibOutputJob({
|
||||
threshold: threshold.value,
|
||||
min_cluster_size: minClusterSize.value,
|
||||
lang: g.lang,
|
||||
folder_paths: scopeFolders.value
|
||||
})
|
||||
jobId.value = started.job_id
|
||||
// poll immediately + interval
|
||||
await pollJob()
|
||||
_jobTimer = setInterval(() => {
|
||||
void pollJob()
|
||||
}, 800)
|
||||
} catch (e) {
|
||||
loading.value = false
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const runQuery = async () => {
|
||||
const q = (query.value || '').trim()
|
||||
if (!q) return
|
||||
if (!scopeCount.value) {
|
||||
message.warning(t('topicSearchNeedScope'))
|
||||
scopeOpen.value = true
|
||||
return
|
||||
}
|
||||
qLoading.value = true
|
||||
try {
|
||||
qResult.value = await searchIibOutputByPrompt({
|
||||
query: q,
|
||||
top_k: 80,
|
||||
ensure_embed: true,
|
||||
folder_paths: scopeFolders.value
|
||||
})
|
||||
// 搜索完成后自动打开结果页
|
||||
openQueryResult()
|
||||
} finally {
|
||||
qLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openQueryResult = () => {
|
||||
const paths = (qResult.value?.results ?? []).map((r) => r.path).filter(Boolean)
|
||||
if (!paths.length) return
|
||||
const title = `Query: ${query.value.trim()}(${paths.length})`
|
||||
const pane = {
|
||||
type: 'topic-search-matched-image-grid' as const,
|
||||
name: title,
|
||||
key: Date.now() + uniqueId(),
|
||||
id: uniqueId(),
|
||||
title,
|
||||
paths
|
||||
}
|
||||
const tab = g.tabList[props.tabIdx]
|
||||
tab.panes.push(pane as any)
|
||||
tab.key = (pane as any).key
|
||||
}
|
||||
|
||||
const openCluster = (item: ClusterIibOutputResp['clusters'][0]) => {
|
||||
const pane = {
|
||||
type: 'topic-search-matched-image-grid' as const,
|
||||
name: `${item.title}(${item.size})`,
|
||||
key: Date.now() + uniqueId(),
|
||||
id: uniqueId(),
|
||||
title: item.title,
|
||||
paths: item.paths
|
||||
}
|
||||
const tab = g.tabList[props.tabIdx]
|
||||
tab.panes.push(pane as any)
|
||||
tab.key = (pane as any).key
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认不启用任何范围:不自动刷新;但如果后端已持久化范围,则自动拉取一次结果
|
||||
void (async () => {
|
||||
await loadScopeFromBackend()
|
||||
if (scopeCount.value) {
|
||||
await loadCached()
|
||||
}
|
||||
})()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopJobPoll()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => scopeFolders.value,
|
||||
() => {
|
||||
// 选择即自动持久化(不依赖 OK,也不依赖同步开关)
|
||||
scheduleSaveScope()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="topic-search">
|
||||
<div class="toolbar">
|
||||
<div class="left">
|
||||
<div class="title">
|
||||
<span class="icon">🧠</span>
|
||||
<span>{{ $t('topicSearchTitleExperimental') }}</span>
|
||||
</div>
|
||||
<a-tag v-if="result" color="blue">共 {{ result.count }} 张</a-tag>
|
||||
<a-tag v-if="result" color="geekblue">主题 {{ result.clusters.length }}</a-tag>
|
||||
<a-tag v-if="result" color="default">噪声 {{ result.noise.length }}</a-tag>
|
||||
</div>
|
||||
<div class="right">
|
||||
<a-button @click="scopeOpen = true">
|
||||
{{ $t('topicSearchScope') }}
|
||||
<span v-if="scopeCount" style="opacity: 0.75;">({{ scopeCount }})</span>
|
||||
</a-button>
|
||||
<a-input
|
||||
v-model:value="query"
|
||||
style="width: min(420px, 72vw);"
|
||||
:placeholder="$t('topicSearchQueryPlaceholder')"
|
||||
:disabled="qLoading"
|
||||
@keydown.enter="runQuery"
|
||||
allow-clear
|
||||
/>
|
||||
<a-button :loading="qLoading" @click="runQuery">{{ $t('search') }}</a-button>
|
||||
<a-button v-if="qResult?.results?.length" @click="openQueryResult">{{ $t('topicSearchOpenResults') }}</a-button>
|
||||
<span class="label">{{ $t('topicSearchThreshold') }}</span>
|
||||
<a-input-number v-model:value="threshold" :min="0.5" :max="0.99" :step="0.01" />
|
||||
<span class="label">{{ $t('topicSearchMinClusterSize') }}</span>
|
||||
<a-input-number v-model:value="minClusterSize" :min="1" :max="50" :step="1" />
|
||||
<a-button type="primary" ghost :loading="loading" :disabled="g.conf?.is_readonly" @click="refresh">{{ $t('refresh') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
v-if="g.conf?.is_readonly"
|
||||
type="warning"
|
||||
:message="$t('readonlyModeSettingPageDesc')"
|
||||
style="margin: 12px 0;"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<a-alert
|
||||
v-if="showRequirements"
|
||||
type="info"
|
||||
show-icon
|
||||
closable
|
||||
style="margin: 10px 0 0 0;"
|
||||
:message="$t('topicSearchRequirementsTitle')"
|
||||
@close="hideRequirements"
|
||||
>
|
||||
<template #description>
|
||||
<div style="display: grid; gap: 6px;">
|
||||
<div>
|
||||
<span style="margin-right: 6px;">🔑</span>
|
||||
<span>{{ $t('topicSearchRequirementsOpenai') }}</span>
|
||||
</div>
|
||||
<template v-if="isTauri">
|
||||
<div>
|
||||
<span style="margin-right: 6px;">🧩</span>
|
||||
<span>{{ $t('topicSearchRequirementsDepsDesktop') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<span style="margin-right: 6px;">🐍</span>
|
||||
<span>{{ $t('topicSearchRequirementsDepsPython') }}</span>
|
||||
</div>
|
||||
<div style="opacity: 0.85;">
|
||||
<span style="margin-right: 6px;">💻</span>
|
||||
<span>{{ $t('topicSearchRequirementsInstallCmd') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<a-alert
|
||||
v-if="cacheInfo?.cache_hit && cacheInfo?.stale"
|
||||
type="warning"
|
||||
show-icon
|
||||
style="margin: 10px 0 0 0;"
|
||||
:message="$t('topicSearchCacheStale')"
|
||||
>
|
||||
<template #description>
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<span style="opacity: 0.85;">{{ $t('topicSearchCacheStaleDesc') }}</span>
|
||||
<a-button size="small" :loading="loading || jobRunning" :disabled="g.conf?.is_readonly" @click="refresh">
|
||||
{{ $t('topicSearchCacheUpdate') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<div v-if="jobRunning" style="margin: 10px 0 0 0;">
|
||||
<a-alert type="info" show-icon :message="jobStageText" :description="jobDesc" />
|
||||
<a-progress :percent="jobPercent" size="small" style="margin-top: 8px;" />
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="qResult" style="margin-top: 10px;">
|
||||
<a-alert
|
||||
type="info"
|
||||
:message="$t('topicSearchRecallMsg', [qResult.results.length, qResult.count, qResult.top_k])"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<div class="grid" v-if="clusters.length">
|
||||
<div class="card" v-for="c in clusters" :key="c.id" @click="openCluster(c)">
|
||||
<div class="card-top">
|
||||
<div class="card-title line-clamp-1">{{ c.title }}</div>
|
||||
<div class="card-count">{{ c.size }}</div>
|
||||
</div>
|
||||
<div class="card-desc line-clamp-2">{{ c.sample_prompt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty" v-else>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:message="$t('topicSearchGuideTitle')"
|
||||
style="margin-bottom: 10px;"
|
||||
/>
|
||||
|
||||
<div class="guide">
|
||||
<div class="guide-row">
|
||||
<span class="guide-icon">🗂️</span>
|
||||
<span class="guide-text">{{ $t('topicSearchGuideStep1') }}</span>
|
||||
<a-button size="small" @click="scopeOpen = true">{{ $t('topicSearchScope') }}</a-button>
|
||||
</div>
|
||||
<div class="guide-row">
|
||||
<span class="guide-icon">🧠</span>
|
||||
<span class="guide-text">{{ $t('topicSearchGuideStep2') }}</span>
|
||||
<a-button size="small" :loading="loading" :disabled="g.conf?.is_readonly" @click="refresh">{{ $t('refresh') }}</a-button>
|
||||
</div>
|
||||
<div class="guide-row">
|
||||
<span class="guide-icon">🔎</span>
|
||||
<span class="guide-text">{{ $t('topicSearchGuideStep3') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="guide-row">
|
||||
<span class="guide-icon">✨</span>
|
||||
<span class="guide-text">{{ $t('topicSearchGuideAdvantage1') }}</span>
|
||||
</div>
|
||||
<div class="guide-row">
|
||||
<span class="guide-icon">🚀</span>
|
||||
<span class="guide-text">{{ $t('topicSearchGuideAdvantage2') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="guide-hint">
|
||||
<span class="guide-icon">💡</span>
|
||||
<span class="guide-text" v-if="!scopeCount">{{ $t('topicSearchGuideEmptyReasonNoScope') }}</span>
|
||||
<span class="guide-text" v-else>{{ $t('topicSearchGuideEmptyReasonNoTopics') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="scopeOpen"
|
||||
:title="$t('topicSearchScopeModalTitle')"
|
||||
:mask-closable="true"
|
||||
@ok="
|
||||
() => {
|
||||
scopeOpen = false
|
||||
void saveScopeToBackend()
|
||||
}
|
||||
"
|
||||
>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:message="$t('topicSearchScopeTip')"
|
||||
style="margin-bottom: 10px;"
|
||||
/>
|
||||
<a-alert
|
||||
v-if="_saving"
|
||||
type="info"
|
||||
show-icon
|
||||
:message="$t('topicSearchSavingToBackend')"
|
||||
style="margin-bottom: 10px;"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="selectedFolders"
|
||||
mode="multiple"
|
||||
style="width: 100%;"
|
||||
:options="folderOptions"
|
||||
:placeholder="$t('topicSearchScopePlaceholder')"
|
||||
:max-tag-count="3"
|
||||
:getPopupContainer="(trigger: HTMLElement) => trigger.parentElement || trigger"
|
||||
allow-clear
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.topic-search {
|
||||
height: var(--pane-max-height);
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--zp-primary-background);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--zp-primary-background);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.guide-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.guide-hint {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.guide-icon {
|
||||
width: 22px;
|
||||
text-align: center;
|
||||
flex: 0 0 22px;
|
||||
}
|
||||
|
||||
.guide-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.grid {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--zp-primary-background);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: rgba(24, 144, 255, 0.6);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-count {
|
||||
min-width: 28px;
|
||||
text-align: right;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin-top: 6px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
height: calc(var(--pane-max-height) - 72px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 16px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -33,6 +33,8 @@ import { prefix } from '@/util/const'
|
|||
// @ts-ignore
|
||||
import * as Pinyin from 'jian-pinyin'
|
||||
import { Tag } from '@/api/db'
|
||||
import { aiChat } from '@/api'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const global = useGlobalStore()
|
||||
|
||||
|
|
@ -58,9 +60,15 @@ const geninfoStructNoPrompts = computed(() => {
|
|||
let p = parse(cleanImageGenInfo.value)
|
||||
delete p.prompt
|
||||
delete p.negativePrompt
|
||||
delete p.extraJsonMetaInfo
|
||||
return p
|
||||
})
|
||||
|
||||
// extraJsonMetaInfo 是需要额外显示的meta字段,使用原始 imageGenInfo 解析以避免 HTML 转义问题
|
||||
const extraJsonMetaInfo = computed(() => {
|
||||
const p = parse(imageGenInfo.value) // 使用原始的 imageGenInfo,而不是 cleanImageGenInfo
|
||||
return p.extraJsonMetaInfo as Record<string, any> | undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(type: 'contextMenuClick', e: MenuInfo, file: FileNodeInfo, idx: number): void
|
||||
|
|
@ -305,6 +313,92 @@ const onTiktokViewClick = () => {
|
|||
emit('contextMenuClick', { key: 'tiktokView' } as any, props.file, props.idx)
|
||||
}
|
||||
|
||||
// AI分析tag功能
|
||||
const analyzingTags = ref(false)
|
||||
const analyzeTagsWithAI = async () => {
|
||||
if (!geninfoStruct.value.prompt) {
|
||||
message.warning(t('aiAnalyzeTagsNoPrompt'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!global.conf?.all_custom_tags?.length) {
|
||||
message.warning(t('aiAnalyzeTagsNoCustomTags'))
|
||||
return
|
||||
}
|
||||
|
||||
analyzingTags.value = true
|
||||
try {
|
||||
const prompt = geninfoStruct.value.prompt
|
||||
const availableTags = global.conf.all_custom_tags.map(tag => tag.name).join(', ')
|
||||
|
||||
const systemMessage = `You are a professional AI assistant responsible for analyzing Stable Diffusion prompts and categorizing them into appropriate tags.
|
||||
|
||||
Your task is:
|
||||
1. Analyze the given prompt
|
||||
2. Find all relevant tags from the provided tag list
|
||||
3. Return only the matching tag names, separated by commas
|
||||
4. If no tags match, return an empty string
|
||||
5. Tag matching should be based on semantic similarity and thematic relevance
|
||||
|
||||
Available tags: ${availableTags}
|
||||
|
||||
Please return only tag names, do not include any other content.`
|
||||
|
||||
const response = await aiChat({
|
||||
messages: [
|
||||
{ role: 'system', content: systemMessage },
|
||||
{ role: 'user', content: `Please analyze this prompt and return matching tags: ${prompt}` }
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 200
|
||||
})
|
||||
|
||||
const matchedTagsText = response.choices[0].message.content.trim()
|
||||
if (!matchedTagsText) {
|
||||
message.info(t('aiAnalyzeTagsNoMatchedTags'))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析返回的标签
|
||||
const matchedTagNames = matchedTagsText.split(',').map((name: string) => name.trim()).filter((name: string) => name)
|
||||
|
||||
// 找到对应的tag对象
|
||||
const matchedTags = global.conf.all_custom_tags.filter((tag: Tag) =>
|
||||
matchedTagNames.some((matchedName: string) =>
|
||||
tag.name.toLowerCase() === matchedName.toLowerCase() ||
|
||||
tag.name.toLowerCase().includes(matchedName.toLowerCase()) ||
|
||||
matchedName.toLowerCase().includes(tag.name.toLowerCase())
|
||||
)
|
||||
)
|
||||
|
||||
// 过滤掉已经添加到图像上的标签
|
||||
const existingTagIds = new Set(selectedTag.value.map((t: Tag) => t.id))
|
||||
const tagsToAdd = matchedTags.filter((tag: Tag) => !existingTagIds.has(tag.id))
|
||||
|
||||
if (tagsToAdd.length === 0) {
|
||||
if (matchedTags.length > 0) {
|
||||
message.info(t('aiAnalyzeTagsAllTagsAlreadyAdded'))
|
||||
} else {
|
||||
message.info(t('aiAnalyzeTagsNoValidTags'))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 为每个匹配的tag发送添加请求(只添加新标签)
|
||||
for (const tag of tagsToAdd) {
|
||||
emit('contextMenuClick', { key: `toggle-tag-${tag.id}` } as any, props.file, props.idx)
|
||||
}
|
||||
|
||||
message.success(t('aiAnalyzeTagsSuccess', [tagsToAdd.length.toString(), tagsToAdd.map(t => t.name).join(', ')]))
|
||||
|
||||
} catch (error) {
|
||||
console.error('AI分析标签失败:', error)
|
||||
message.error(t('aiAnalyzeTagsFailed'))
|
||||
} finally {
|
||||
analyzingTags.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -384,6 +478,14 @@ const onTiktokViewClick = () => {
|
|||
<a-button @click="copyPositivePrompt" v-if="imageGenInfo">{{
|
||||
$t('copyPositivePrompt')
|
||||
}}</a-button>
|
||||
<a-button
|
||||
@click="analyzeTagsWithAI"
|
||||
type="primary"
|
||||
:loading="analyzingTags"
|
||||
v-if="imageGenInfo && global.conf?.all_custom_tags?.length"
|
||||
>
|
||||
{{ $t('aiAnalyzeTags') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
@click="onTiktokViewClick"
|
||||
@touchstart.prevent="onTiktokViewClick"
|
||||
|
|
@ -490,6 +592,17 @@ const onTiktokViewClick = () => {
|
|||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<template v-if="extraJsonMetaInfo && Object.keys(extraJsonMetaInfo).length"> <br />
|
||||
<h3>Extra Meta Info</h3>
|
||||
<table class="extra-meta-table">
|
||||
<tr v-for="(val, key) in extraJsonMetaInfo" :key="key" class="gen-info-frag">
|
||||
<td style="font-weight: 600;text-transform: capitalize;">{{ key }}</td>
|
||||
<td style="cursor: pointer;" @dblclick="copy(val)">
|
||||
<code class="extra-meta-value">{{ typeof val === 'string' ? val : JSON.stringify(val, null, 2) }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="sourceText" :tab="$t('sourceText')">
|
||||
<code>{{ imageGenInfo }}</code>
|
||||
|
|
@ -608,6 +721,21 @@ const onTiktokViewClick = () => {
|
|||
|
||||
tr td:first-child {
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
table.extra-meta-table {
|
||||
.extra-meta-value {
|
||||
display: block;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 0.85em;
|
||||
background: var(--zp-secondary-variant-background);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { throttle, debounce } from 'lodash-es'
|
|||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { prefix } from '@/util/const'
|
||||
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const wsStore = useWorkspeaceSnapshot()
|
||||
|
||||
|
|
@ -71,7 +70,7 @@ const defaultInitinalPageOptions = computed(() => {
|
|||
const shortCutsCountRec = computed(() => {
|
||||
const rec = globalStore.shortcut
|
||||
const res = {} as Dict<number>
|
||||
Object.entries(rec).forEach(([_k, v]) => {
|
||||
Object.values(rec).forEach((v) => {
|
||||
res[v + ''] ??= 0
|
||||
res[v + '']++
|
||||
})
|
||||
|
|
@ -96,6 +95,8 @@ const isShortcutConflict = (keyStr: string) => {
|
|||
return keyStr && keyStr in shortCutsCountRec.value && shortCutsCountRec.value[keyStr] > 1
|
||||
}
|
||||
const disableMaximize = useLocalStorage(prefix+'disable_maximize', false)
|
||||
|
||||
// 自然语言分类&搜索 已提升到首页启动入口(TopicSearch),全局设置不再保留旧入口
|
||||
</script>
|
||||
<template>
|
||||
<div class="panel">
|
||||
|
|
@ -172,6 +173,8 @@ const disableMaximize = useLocalStorage(prefix+'disable_maximize', false)
|
|||
<a-switch v-model:checked="disableMaximize" />
|
||||
<sub style="padding-left: 8px;color: #666;">{{ $t('takeEffectAfterReloadPage') }}</sub>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<h2>{{ t('shortcutKey') }}</h2>
|
||||
<a-form-item :label="item.label" v-for="item in shortcutsList" :key="item.key">
|
||||
<div class="col" :class="{ conflict: isShortcutConflict(globalStore.shortcut[item.key] + '') }"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ interface TabPaneBase {
|
|||
}
|
||||
|
||||
interface OtherTabPane extends TabPaneBase {
|
||||
type: 'global-setting' | 'tag-search' | 'batch-download' | 'workspace-snapshot' | 'random-image'
|
||||
type: 'global-setting' | 'tag-search' | 'batch-download' | 'workspace-snapshot' | 'random-image' | 'topic-search'
|
||||
}
|
||||
|
||||
export interface EmptyStartTabPane extends TabPaneBase {
|
||||
|
|
@ -90,6 +90,13 @@ interface TagSearchMatchedImageGridTabPane extends TabPaneBase {
|
|||
selectedTagIds: MatchImageByTagsReq
|
||||
id: string
|
||||
}
|
||||
|
||||
interface TopicSearchMatchedImageGridTabPane extends TabPaneBase {
|
||||
type: 'topic-search-matched-image-grid'
|
||||
id: string
|
||||
title: string
|
||||
paths: string[]
|
||||
}
|
||||
export interface ImgSliTabPane extends TabPaneBase {
|
||||
type: 'img-sli'
|
||||
left: FileNodeInfo
|
||||
|
|
@ -113,7 +120,16 @@ export interface FuzzySearchTabPane extends TabPaneBase {
|
|||
searchScope?: string
|
||||
}
|
||||
|
||||
export type TabPane = EmptyStartTabPane | FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane | ImgSliTabPane | TagSearchTabPane | FuzzySearchTabPane| GridViewTabPane
|
||||
export type TabPane =
|
||||
| EmptyStartTabPane
|
||||
| FileTransferTabPane
|
||||
| OtherTabPane
|
||||
| TagSearchMatchedImageGridTabPane
|
||||
| TopicSearchMatchedImageGridTabPane
|
||||
| ImgSliTabPane
|
||||
| TagSearchTabPane
|
||||
| FuzzySearchTabPane
|
||||
| GridViewTabPane
|
||||
|
||||
/**
|
||||
* This interface represents a tab, which contains an array of panes, an ID, and a key
|
||||
|
|
|
|||
|
|
@ -75,6 +75,19 @@ export function parse(parameters: string): ImageMeta {
|
|||
const metadata: ImageMeta = {};
|
||||
if (!parameters) return metadata;
|
||||
|
||||
// 提取 extraJsonMetaInfo 字段
|
||||
const extraJsonMetaInfoMatch = parameters.match(/\nextraJsonMetaInfo:\s*(\{[\s\S]*\})\s*$/);
|
||||
if (extraJsonMetaInfoMatch) {
|
||||
try {
|
||||
metadata.extraJsonMetaInfo = JSON.parse(extraJsonMetaInfoMatch[1]);
|
||||
// 从原始参数中移除 extraJsonMetaInfo 部分
|
||||
parameters = parameters.replace(/\nextraJsonMetaInfo:\s*\{[\s\S]*\}\s*$/, '');
|
||||
} catch (e) {
|
||||
// 解析失败,保留原始字符串
|
||||
metadata.extraJsonMetaInfo = extraJsonMetaInfoMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
const metaLines = parameters.split('\n').filter((line) => {
|
||||
return line.trim() !== '' && !stripKeys.some((key) => line.startsWith(key));
|
||||
});
|
||||
|
|
@ -117,10 +130,14 @@ export function parse(parameters: string): ImageMeta {
|
|||
});
|
||||
|
||||
// Extract prompts
|
||||
const [prompt, ...negativePrompt] = metaLines
|
||||
let [prompt, ...negativePrompt] = metaLines
|
||||
.join('\n')
|
||||
.split('Negative prompt:')
|
||||
.map((x) => x.trim());
|
||||
|
||||
// 确保 prompt 中不包含 extraJsonMetaInfo
|
||||
prompt = prompt.replace(/\nextraJsonMetaInfo:\s*\{[\s\S]*\}\s*$/, '').trim();
|
||||
|
||||
metadata.prompt = prompt;
|
||||
metadata.negativePrompt = negativePrompt.join(' ').trim();
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default defineConfig({
|
|||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
port: 3002,
|
||||
proxy: {
|
||||
'/infinite_image_browsing/': {
|
||||
target: 'http://127.0.0.1:7866/'
|
||||
|
|
|
|||
Loading…
Reference in New Issue