Skip to content

Commit d9bb979

Browse files
committed
新增:实现模型的自定义NSFW状态切换功能,更新相关API和前端组件,优化模型列表和详情展示
1 parent c77a0a3 commit d9bb979

File tree

6 files changed

+135
-7
lines changed

6 files changed

+135
-7
lines changed

frontend/src/App.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
:blur-nsfw="blurNsfw"
4545
@model-click="openModelDetails"
4646
@open-settings="openSettings"
47+
@model-updated="handleModelUpdated"
4748
/>
4849
</div>
4950
</div>
@@ -309,6 +310,15 @@ function closeModelDetail() {
309310
selectedModel.value = null;
310311
}
311312
313+
// 处理模型更新
314+
function handleModelUpdated(updatedModel: Model) {
315+
// 查找并更新模型列表中的对应模型
316+
const index = models.value.findIndex(model => model.id === updatedModel.id);
317+
if (index !== -1) {
318+
models.value[index] = { ...models.value[index], ...updatedModel };
319+
}
320+
}
321+
312322
// 监听模型列表变化
313323
watch(models, () => {
314324
updateFilterOptions();

frontend/src/api/models.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface Model {
1818
size?: number;
1919
preview?: string;
2020
nsfw?: boolean;
21+
custom_nsfw?: boolean;
22+
original_nsfw?: boolean;
2123
created_at?: string;
2224
hash?: string;
2325
tags?: string[];
@@ -32,6 +34,8 @@ interface BackendModel {
3234
type: string;
3335
preview_url?: string;
3436
nsfw?: boolean;
37+
custom_nsfw?: boolean;
38+
original_nsfw?: boolean;
3539
baseModel?: string;
3640
url?: string;
3741
nsfwLevel?: number;
@@ -49,6 +53,8 @@ function convertModel(backendModel: BackendModel): Model {
4953
type: backendModel.type,
5054
preview: backendModel.preview_url,
5155
nsfw: backendModel.nsfw || false,
56+
custom_nsfw: backendModel.custom_nsfw || false,
57+
original_nsfw: backendModel.original_nsfw || false,
5258
base_model: backendModel.baseModel,
5359
url: backendModel.url
5460
};
@@ -184,5 +190,11 @@ export const ModelsAPI = {
184190
getVersion: async (): Promise<{version: string; company: string; copyright: string}> => {
185191
const response = await apiClient.get('/version');
186192
return response.data;
193+
},
194+
195+
// 切换模型的自定义NSFW状态
196+
toggleModelNsfw: async (modelId: string): Promise<{nsfw: boolean}> => {
197+
const response = await apiClient.post('/toggle-nsfw', { model_id: modelId });
198+
return { nsfw: response.data.nsfw };
187199
}
188200
};

frontend/src/components/ModelDetailModal.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@
1919
<div class="modal-dialog overlay-open:opacity-100 overlay-open:duration-300 max-w-4xl">
2020
<div class="modal-content bg-base-100">
2121
<div class="modal-header border-b border-base-200">
22-
<h3 class="modal-title text-base-content">{{ model?.name || '' }}</h3>
22+
<h3 class="modal-title text-base-content">
23+
{{ model?.name || '' }}
24+
<span
25+
v-if="model?.nsfw"
26+
class="badge ms-2 align-middle"
27+
:class="model.custom_nsfw ? 'badge-warning' : 'badge-error'"
28+
>NSFW</span>
29+
</h3>
2330
<button
2431
type="button"
2532
class="btn btn-text btn-circle btn-sm absolute end-3 top-3 text-base-content/70 hover:text-base-content"

frontend/src/components/ModelList.vue

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,20 @@
4444
</div>
4545
<div
4646
v-if="model.nsfw"
47-
class="badge badge-error absolute top-2 right-2 z-10"
47+
class="badge absolute top-2 right-2 z-10"
48+
:class="model.custom_nsfw ? 'badge-warning' : 'badge-error'"
4849
>NSFW</div>
50+
51+
<!-- NSFW标记按钮 - 只在非原始NSFW模型上显示 -->
52+
<button
53+
v-if="!model.original_nsfw"
54+
class="absolute top-2 left-2 z-10 btn btn-circle btn-xs"
55+
:class="[model.custom_nsfw ? 'btn-warning' : 'btn-outline btn-neutral']"
56+
title="标记/取消标记为NSFW内容"
57+
@click.stop="toggleNsfw(model)"
58+
>
59+
<span class="icon-[tabler--eye-off] size-3.5"></span>
60+
</button>
4961
</div>
5062
<div
5163
class="p-4 flex-1 cursor-pointer"
@@ -102,6 +114,7 @@
102114

103115
<script setup lang="ts">
104116
import type { Model } from '../api/models';
117+
import { ModelsAPI } from '../api/models';
105118
import toast from '../utils/toast';
106119
107120
defineProps<{
@@ -118,6 +131,7 @@ defineProps<{
118131
const emit = defineEmits<{
119132
'open-settings': [];
120133
'model-click': [model: Model];
134+
'model-updated': [model: Model];
121135
}>();
122136
123137
function onOpenSettings() {
@@ -128,6 +142,32 @@ function onModelClick(model: Model) {
128142
emit('model-click', model);
129143
}
130144
145+
async function toggleNsfw(model: Model) {
146+
// 如果是原始NSFW模型,不允许更改
147+
if (model.original_nsfw) {
148+
toast.error('无法修改原始NSFW模型的状态');
149+
return;
150+
}
151+
152+
try {
153+
// 调用API切换NSFW状态
154+
const result = await ModelsAPI.toggleModelNsfw(model.id);
155+
156+
// 更新模型状态
157+
model.custom_nsfw = result.nsfw;
158+
model.nsfw = result.nsfw || model.original_nsfw; // 保持与原始NSFW状态一致
159+
160+
// 通知父组件模型已更新
161+
emit('model-updated', model);
162+
163+
// 显示成功提示
164+
toast.success(`已${result.nsfw ? '标记' : '取消标记'}为NSFW内容`);
165+
} catch (error) {
166+
console.error('切换NSFW状态失败:', error);
167+
toast.error('切换NSFW状态失败');
168+
}
169+
}
170+
131171
function copyFileName(filename: string) {
132172
navigator.clipboard.writeText(filename)
133173
.then(() => {

src/api/model_api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
class PathUpdate(BaseModel):
1212
path: str
1313

14+
class ModelIdParam(BaseModel):
15+
model_id: str
16+
1417
def create_api(manager, frontend_url=None):
1518
"""创建并配置FastAPI应用
1619
@@ -140,6 +143,15 @@ async def get_config():
140143
"is_path_valid": os.path.exists(manager.models_path) if manager.models_path else False
141144
}
142145

146+
@app.post("/api/toggle-nsfw")
147+
async def toggle_model_nsfw(model_param: ModelIdParam):
148+
"""切换模型的NSFW状态"""
149+
try:
150+
new_state = manager.toggle_custom_nsfw(model_param.model_id)
151+
return {"success": True, "model_id": model_param.model_id, "nsfw": new_state}
152+
except Exception as e:
153+
raise HTTPException(status_code=500, detail=f"设置NSFW状态失败: {str(e)}")
154+
143155
@app.get("/api/select-path")
144156
async def select_path_endpoint():
145157
"""选择目录"""

src/core/model_manager.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ def __init__(self, config_file="config.json"):
2525
# 添加并发限制和超时设置
2626
self.semaphore = asyncio.Semaphore(5) # 限制并发请求数
2727
self.timeout = ClientTimeout(total=10) # 10秒超时
28+
# 添加自定义NSFW模型ID列表
29+
self.custom_nsfw_models = self.config.get("custom_nsfw_models", [])
2830

2931
def load_config(self) -> dict:
3032
"""加载配置文件"""
3133
if os.path.exists(self.config_file):
3234
with open(self.config_file, "r", encoding="utf-8") as f:
3335
return json.load(f)
34-
return {"models_path": "", "output_file": "models_info.json"}
36+
return {"models_path": "", "output_file": "models_info.json", "custom_nsfw_models": []}
3537

3638
def save_config(self):
3739
"""保存配置文件"""
@@ -212,7 +214,9 @@ def get_model_display_info(self, model_path: str) -> dict:
212214
"preview_url": None,
213215
"description": "未找到模型信息",
214216
"baseModel": "未知",
215-
"nsfw": False,
217+
"nsfw": str(model_path) in self.custom_nsfw_models, # 检查是否在自定义NSFW列表中
218+
"custom_nsfw": str(model_path) in self.custom_nsfw_models, # 新增自定义NSFW标记
219+
"original_nsfw": False, # 新增原始NSFW标记
216220
"nsfwLevel": 0,
217221
}
218222

@@ -224,13 +228,19 @@ def get_model_display_info(self, model_path: str) -> dict:
224228
# 添加本地图片路径
225229
local_preview = info.get("local_preview")
226230

231+
# 如果在自定义NSFW列表中,覆盖API返回的nsfw值
232+
is_custom_nsfw = str(model_path) in self.custom_nsfw_models
233+
is_original_nsfw = model_data.get("nsfw", False)
234+
227235
return {
228236
"name": model_data.get("name", Path(model_path).name),
229237
"type": model_data.get("type", "未知"),
230238
"preview_url": local_preview or preview_url,
231239
"baseModel": info.get("baseModel", "未知"),
232-
"url": f"https://civitai.com/models/{info['modelId']}?modelVersionId={info['id']}",
233-
"nsfw": model_data.get("nsfw", False),
240+
"url": f"https://civitai.com/models/{info['modelId']}?modelVersionId={info['id']}" if 'modelId' in info and 'id' in info else None,
241+
"nsfw": is_custom_nsfw or is_original_nsfw, # 自定义NSFW或API返回的NSFW
242+
"custom_nsfw": is_custom_nsfw, # 新增自定义NSFW标记
243+
"original_nsfw": is_original_nsfw, # 新增原始NSFW标记
234244
"nsfwLevel": preview_image.get("nsfwLevel", 0),
235245
}
236246

@@ -244,4 +254,41 @@ def get_all_models_info(self) -> list:
244254
}
245255
for model_path in self.models_info.keys()
246256
if str(model_path).startswith(current_path)
247-
]
257+
]
258+
259+
def toggle_custom_nsfw(self, model_path: str) -> bool:
260+
"""切换模型的自定义NSFW状态
261+
262+
Args:
263+
model_path: 模型路径
264+
265+
Returns:
266+
bool: 更新后的NSFW状态
267+
"""
268+
model_path = str(model_path) # 确保为字符串
269+
270+
# 检查是否为原始NSFW模型
271+
model_info = self.models_info.get(model_path, {})
272+
if model_info:
273+
info = model_info.get("info", {})
274+
model_data = info.get("model", {})
275+
is_original_nsfw = model_data.get("nsfw", False)
276+
277+
# 如果是原始NSFW模型,不允许更改
278+
if is_original_nsfw:
279+
return True # 保持NSFW状态
280+
281+
if model_path in self.custom_nsfw_models:
282+
# 如果模型已在自定义NSFW列表中,则移除
283+
self.custom_nsfw_models.remove(model_path)
284+
current_state = False
285+
else:
286+
# 否则添加到列表中
287+
self.custom_nsfw_models.append(model_path)
288+
current_state = True
289+
290+
# 更新配置并保存
291+
self.config["custom_nsfw_models"] = self.custom_nsfw_models
292+
self.save_config()
293+
294+
return current_state

0 commit comments

Comments
 (0)