Spaces:
Running
Running
优化消息提醒
Browse files- app.py +1 -1
- router_items.py +14 -3
- router_messages.py +5 -3
- router_proxy.py +15 -0
- 云端_定时版本检测引擎.py +2 -106
- 前端页面/components/关于插件组件.js +59 -3
- 前端页面/components/动画音效引擎.js +1 -0
- 前端页面/components/用户体验_国际化.js +41 -9
- 前端页面/components/顶部导航组件.js +15 -7
- 前端页面/core/侧边栏主程序.js +14 -11
- 前端页面/core/侧边栏数据引擎.js +9 -9
- 前端页面/core/类型定义.js +1 -1
- 前端页面/market/列表卡片组件.js +2 -2
- 前端页面/market/创作者卡片组件.js +3 -2
- 前端页面/market/发布内容_UI模板.js +2 -0
- 前端页面/market/发布内容_提交引擎.js +8 -6
- 前端页面/market/发布内容组件.js +10 -4
- 前端页面/market/资源安装引擎.js +10 -6
- 前端页面/market/资源详情页面组件.js +2 -2
- 前端页面/profile/个人列表组件.js +8 -6
- 前端页面/social/私信聊天组件.js +33 -2
- 前端页面/social/通知中心组件.js +8 -29
app.py
CHANGED
|
@@ -48,7 +48,7 @@ from router_users_social import router as users_social_router
|
|
| 48 |
# ==========================================
|
| 49 |
# 其他业务模块
|
| 50 |
# ==========================================
|
| 51 |
-
from router_items import router as items_router # 📦 内容管理(工具/应用/推荐)
|
| 52 |
from router_comments import router as comments_router # 💬 评论系统
|
| 53 |
from router_messages import router as messages_router # ✉️ 私信系统
|
| 54 |
from router_wallet import router as wallet_router # 💰 钱包/提现
|
|
|
|
| 48 |
# ==========================================
|
| 49 |
# 其他业务模块
|
| 50 |
# ==========================================
|
| 51 |
+
from router_items import router as items_router # 📦 内容管理(工具/Skill/应用/推荐)
|
| 52 |
from router_comments import router as comments_router # 💬 评论系统
|
| 53 |
from router_messages import router as messages_router # ✉️ 私信系统
|
| 54 |
from router_wallet import router as wallet_router # 💰 钱包/提现
|
router_items.py
CHANGED
|
@@ -129,6 +129,7 @@ def _build_creator_trend_data(account: str, u_items: list, months: list) -> dict
|
|
| 129 |
提取为独立函数供列表接口和详情接口复用
|
| 130 |
"""
|
| 131 |
trend_tools = {m: 0 for m in months}
|
|
|
|
| 132 |
trend_apps = {m: 0 for m in months}
|
| 133 |
trend_recommends = {m: 0 for m in months}
|
| 134 |
|
|
@@ -138,6 +139,9 @@ def _build_creator_trend_data(account: str, u_items: list, months: list) -> dict
|
|
| 138 |
if itype == "tool" or itype == "recommend_tool":
|
| 139 |
for m, v in history.items():
|
| 140 |
if m in trend_tools: trend_tools[m] += v
|
|
|
|
|
|
|
|
|
|
| 141 |
elif itype == "app" or itype == "recommend_app":
|
| 142 |
for m, v in history.items():
|
| 143 |
if m in trend_apps: trend_apps[m] += v
|
|
@@ -147,7 +151,8 @@ def _build_creator_trend_data(account: str, u_items: list, months: list) -> dict
|
|
| 147 |
|
| 148 |
return {
|
| 149 |
"months": months,
|
| 150 |
-
"tools": [trend_tools[m] for m in months],
|
|
|
|
| 151 |
"apps": [trend_apps[m] for m in months],
|
| 152 |
"recommends": [trend_recommends[m] for m in months]
|
| 153 |
}
|
|
@@ -189,12 +194,15 @@ async def search_creators(keyword: str, sort: str = "downloads", limit: int = 50
|
|
| 189 |
continue
|
| 190 |
|
| 191 |
tools_count = 0
|
|
|
|
| 192 |
apps_count = 0
|
| 193 |
|
| 194 |
for i in u_items:
|
| 195 |
itype = i.get("type", "")
|
| 196 |
if itype == "tool":
|
| 197 |
tools_count += 1
|
|
|
|
|
|
|
| 198 |
elif itype == "app":
|
| 199 |
apps_count += 1
|
| 200 |
|
|
@@ -206,7 +214,7 @@ async def search_creators(keyword: str, sort: str = "downloads", limit: int = 50
|
|
| 206 |
"downloads": sum(i.get("uses", 0) for i in u_items),
|
| 207 |
"views": sum(i.get("views", 0) for i in u_items),
|
| 208 |
"daily_views": sum(i.get("daily_views", 0) for i in u_items),
|
| 209 |
-
"toolsCount": tools_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
|
| 210 |
"recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0),
|
| 211 |
})
|
| 212 |
|
|
@@ -249,12 +257,15 @@ async def get_creators(sort: str = "downloads", limit: int = 100):
|
|
| 249 |
u_items = author_items_index.get(account, [])
|
| 250 |
|
| 251 |
tools_count = 0
|
|
|
|
| 252 |
apps_count = 0
|
| 253 |
|
| 254 |
for i in u_items:
|
| 255 |
itype = i.get("type", "")
|
| 256 |
if itype == "tool":
|
| 257 |
tools_count += 1
|
|
|
|
|
|
|
| 258 |
elif itype == "app":
|
| 259 |
apps_count += 1
|
| 260 |
|
|
@@ -266,7 +277,7 @@ async def get_creators(sort: str = "downloads", limit: int = 100):
|
|
| 266 |
"downloads": sum(i.get("uses", 0) for i in u_items),
|
| 267 |
"views": sum(i.get("views", 0) for i in u_items),
|
| 268 |
"daily_views": sum(i.get("daily_views", 0) for i in u_items),
|
| 269 |
-
"toolsCount": tools_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
|
| 270 |
"recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0), # 🚀 新增:本月收益统计
|
| 271 |
})
|
| 272 |
|
|
|
|
| 129 |
提取为独立函数供列表接口和详情接口复用
|
| 130 |
"""
|
| 131 |
trend_tools = {m: 0 for m in months}
|
| 132 |
+
trend_skills = {m: 0 for m in months}
|
| 133 |
trend_apps = {m: 0 for m in months}
|
| 134 |
trend_recommends = {m: 0 for m in months}
|
| 135 |
|
|
|
|
| 139 |
if itype == "tool" or itype == "recommend_tool":
|
| 140 |
for m, v in history.items():
|
| 141 |
if m in trend_tools: trend_tools[m] += v
|
| 142 |
+
elif itype == "skill" or itype == "recommend_skill":
|
| 143 |
+
for m, v in history.items():
|
| 144 |
+
if m in trend_skills: trend_skills[m] += v
|
| 145 |
elif itype == "app" or itype == "recommend_app":
|
| 146 |
for m, v in history.items():
|
| 147 |
if m in trend_apps: trend_apps[m] += v
|
|
|
|
| 151 |
|
| 152 |
return {
|
| 153 |
"months": months,
|
| 154 |
+
"tools": [trend_tools[m] for m in months],
|
| 155 |
+
"skills": [trend_skills[m] for m in months],
|
| 156 |
"apps": [trend_apps[m] for m in months],
|
| 157 |
"recommends": [trend_recommends[m] for m in months]
|
| 158 |
}
|
|
|
|
| 194 |
continue
|
| 195 |
|
| 196 |
tools_count = 0
|
| 197 |
+
skills_count = 0
|
| 198 |
apps_count = 0
|
| 199 |
|
| 200 |
for i in u_items:
|
| 201 |
itype = i.get("type", "")
|
| 202 |
if itype == "tool":
|
| 203 |
tools_count += 1
|
| 204 |
+
elif itype == "skill":
|
| 205 |
+
skills_count += 1
|
| 206 |
elif itype == "app":
|
| 207 |
apps_count += 1
|
| 208 |
|
|
|
|
| 214 |
"downloads": sum(i.get("uses", 0) for i in u_items),
|
| 215 |
"views": sum(i.get("views", 0) for i in u_items),
|
| 216 |
"daily_views": sum(i.get("daily_views", 0) for i in u_items),
|
| 217 |
+
"toolsCount": tools_count, "skillsCount": skills_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
|
| 218 |
"recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0),
|
| 219 |
})
|
| 220 |
|
|
|
|
| 257 |
u_items = author_items_index.get(account, [])
|
| 258 |
|
| 259 |
tools_count = 0
|
| 260 |
+
skills_count = 0
|
| 261 |
apps_count = 0
|
| 262 |
|
| 263 |
for i in u_items:
|
| 264 |
itype = i.get("type", "")
|
| 265 |
if itype == "tool":
|
| 266 |
tools_count += 1
|
| 267 |
+
elif itype == "skill":
|
| 268 |
+
skills_count += 1
|
| 269 |
elif itype == "app":
|
| 270 |
apps_count += 1
|
| 271 |
|
|
|
|
| 277 |
"downloads": sum(i.get("uses", 0) for i in u_items),
|
| 278 |
"views": sum(i.get("views", 0) for i in u_items),
|
| 279 |
"daily_views": sum(i.get("daily_views", 0) for i in u_items),
|
| 280 |
+
"toolsCount": tools_count, "skillsCount": skills_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
|
| 281 |
"recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0), # 🚀 新增:本月收益统计
|
| 282 |
})
|
| 283 |
|
router_messages.py
CHANGED
|
@@ -7,7 +7,6 @@ import uuid
|
|
| 7 |
import subprocess
|
| 8 |
import os
|
| 9 |
import 数据库连接 as db
|
| 10 |
-
from notifications import add_notification
|
| 11 |
from models import PrivateMessage
|
| 12 |
from 安全认证 import require_auth, is_admin
|
| 13 |
|
|
@@ -177,7 +176,8 @@ async def send_private_message(request: Request, msg: PrivateMessage, current_us
|
|
| 177 |
chats_db[conv_id].append(chat_msg)
|
| 178 |
db.save_data("chats.json", chats_db)
|
| 179 |
|
| 180 |
-
|
|
|
|
| 181 |
return {"status": "success"}
|
| 182 |
|
| 183 |
@router.get("/api/chats/{account}")
|
|
@@ -265,9 +265,11 @@ async def get_messages(account: str, count_only: bool = False, current_user: str
|
|
| 265 |
now = int(time.time())
|
| 266 |
seven_days = 7 * 24 * 3600
|
| 267 |
|
| 268 |
-
# 只统计未过期的未读消息
|
|
|
|
| 269 |
unread = sum(1 for m in user_msgs
|
| 270 |
if not m.get("is_read")
|
|
|
|
| 271 |
and (now - m.get("created_at", 0) < seven_days))
|
| 272 |
return {"status": "success", "unread_count": unread}
|
| 273 |
|
|
|
|
| 7 |
import subprocess
|
| 8 |
import os
|
| 9 |
import 数据库连接 as db
|
|
|
|
| 10 |
from models import PrivateMessage
|
| 11 |
from 安全认证 import require_auth, is_admin
|
| 12 |
|
|
|
|
| 176 |
chats_db[conv_id].append(chat_msg)
|
| 177 |
db.save_data("chats.json", chats_db)
|
| 178 |
|
| 179 |
+
# 🔔 私信未读提醒改由聊天入口(✉️红点)承载(读取 chats.json 每会话未读数),
|
| 180 |
+
# 不再向 messages.json 写入 private 铃铛通知,避免与聊天红点重复计数
|
| 181 |
return {"status": "success"}
|
| 182 |
|
| 183 |
@router.get("/api/chats/{account}")
|
|
|
|
| 265 |
now = int(time.time())
|
| 266 |
seven_days = 7 * 24 * 3600
|
| 267 |
|
| 268 |
+
# 🔔 只统计未过期的未读消息;私信提醒已迁移至聊天入口(✉️红点)、
|
| 269 |
+
# 插件更新提醒已迁移至卡片徽章,铃铛未读数不再计入这两类,避免同一提醒重复出现
|
| 270 |
unread = sum(1 for m in user_msgs
|
| 271 |
if not m.get("is_read")
|
| 272 |
+
and m.get("type") not in ("private", "plugin_update")
|
| 273 |
and (now - m.get("created_at", 0) < seven_days))
|
| 274 |
return {"status": "success", "unread_count": unread}
|
| 275 |
|
router_proxy.py
CHANGED
|
@@ -88,6 +88,10 @@ async def proxy_github_zip(req_data: ProxyGithubZipRequest, account: str = Depen
|
|
| 88 |
cache_headers = {}
|
| 89 |
if content_length:
|
| 90 |
cache_headers["Content-Length"] = content_length
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
async def cache_stream_generator():
|
| 93 |
try:
|
|
@@ -168,6 +172,17 @@ async def proxy_github_zip(req_data: ProxyGithubZipRequest, account: str = Depen
|
|
| 168 |
if content_length:
|
| 169 |
resp_headers["Content-Length"] = content_length
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
async def stream_generator():
|
| 172 |
try:
|
| 173 |
async for chunk in response.aiter_bytes():
|
|
|
|
| 88 |
cache_headers = {}
|
| 89 |
if content_length:
|
| 90 |
cache_headers["Content-Length"] = content_length
|
| 91 |
+
# 🔖 回传本次实际下发的版本戳(缓存命中即 item_version['hash']),
|
| 92 |
+
# 供本地私有安装写入 LocalVer 驱动卡片更新徽章(前端 installed_version 优先)
|
| 93 |
+
if item_version.get('hash'):
|
| 94 |
+
cache_headers["X-Installed-Version"] = item_version['hash']
|
| 95 |
|
| 96 |
async def cache_stream_generator():
|
| 97 |
try:
|
|
|
|
| 172 |
if content_length:
|
| 173 |
resp_headers["Content-Length"] = content_length
|
| 174 |
|
| 175 |
+
# 🔖 缓存未命中(含 versions.json 尚未填充)时,现取仓库最新 commit 短 hash 回传,
|
| 176 |
+
# 补齐私有工具"安装瞬间无版本戳"的边界,使卡片更新徽章可正常触发。
|
| 177 |
+
# 解析失败不影响 ZIP 下发(前端会兜底 latest_version)
|
| 178 |
+
try:
|
| 179 |
+
from 云端_定时版本检测引擎 import fetch_latest_github_hash
|
| 180 |
+
live_hash = await fetch_latest_github_hash(repo_url, active_token)
|
| 181 |
+
if live_hash:
|
| 182 |
+
resp_headers["X-Installed-Version"] = live_hash
|
| 183 |
+
except Exception as ver_err:
|
| 184 |
+
print(f"[proxy_github_zip] ⚠️ 版本戳解析失败(不影响下发): {ver_err}")
|
| 185 |
+
|
| 186 |
async def stream_generator():
|
| 187 |
try:
|
| 188 |
async for chunk in response.aiter_bytes():
|
云端_定时版本检测引擎.py
CHANGED
|
@@ -8,9 +8,6 @@ import os
|
|
| 8 |
import tempfile
|
| 9 |
import zipfile
|
| 10 |
import 数据库连接 as db
|
| 11 |
-
from notifications import add_notification
|
| 12 |
-
from database_sql import SessionLocal
|
| 13 |
-
from models_sql import Ownership
|
| 14 |
from huggingface_hub import upload_file, HfApi
|
| 15 |
|
| 16 |
logger = logging.getLogger("ComfyUI-Ranking.VersionCheck")
|
|
@@ -83,107 +80,6 @@ async def fetch_latest_github_hash(repo_url, token):
|
|
| 83 |
logger.error(f"检测版本失败 {repo_url}: {e}")
|
| 84 |
return None
|
| 85 |
|
| 86 |
-
async def trigger_update_notifications(updated_items: list):
|
| 87 |
-
"""
|
| 88 |
-
向已购买/已安装插件的用户发送更新通知
|
| 89 |
-
|
| 90 |
-
参数:
|
| 91 |
-
updated_items: 有更新的插件列表,每项包含 id, title, version_hash
|
| 92 |
-
"""
|
| 93 |
-
if not updated_items:
|
| 94 |
-
return
|
| 95 |
-
|
| 96 |
-
try:
|
| 97 |
-
# 加载已通知记录,防止重复通知
|
| 98 |
-
update_notifications_db = db.load_data("update_notifications.json", default_data={})
|
| 99 |
-
|
| 100 |
-
# 构建插件ID到最新版本号的映射
|
| 101 |
-
updated_versions = {item["id"]: item["version_hash"] for item in updated_items}
|
| 102 |
-
|
| 103 |
-
# 筛选出需要通知的插件(版本号变化才通知)
|
| 104 |
-
items_to_notify = []
|
| 105 |
-
for item in updated_items:
|
| 106 |
-
item_id = item["id"]
|
| 107 |
-
current_version = item["version_hash"]
|
| 108 |
-
last_notified_version = update_notifications_db.get(item_id)
|
| 109 |
-
|
| 110 |
-
if last_notified_version != current_version:
|
| 111 |
-
items_to_notify.append(item)
|
| 112 |
-
|
| 113 |
-
if not items_to_notify:
|
| 114 |
-
print("📢 所有插件更新已通知过,无需重复发送")
|
| 115 |
-
return
|
| 116 |
-
|
| 117 |
-
# 从 SQL 数据库查询购买记录
|
| 118 |
-
with SessionLocal() as session:
|
| 119 |
-
# 为每个需要通知的插件,查找已购买用户并发送通知
|
| 120 |
-
total_notified = 0
|
| 121 |
-
batch_size = 50 # 分批发送,每扐50个用户
|
| 122 |
-
|
| 123 |
-
for item in items_to_notify:
|
| 124 |
-
item_id = item["id"]
|
| 125 |
-
title = item.get("title", "未知插件")
|
| 126 |
-
version_hash = item["version_hash"]
|
| 127 |
-
|
| 128 |
-
# 从 SQL ownerships 表查询已购买该插件的用户(排除已退款记录)
|
| 129 |
-
ownerships = session.query(Ownership).filter(
|
| 130 |
-
Ownership.item_id == item_id,
|
| 131 |
-
Ownership.is_refunded == False
|
| 132 |
-
).all()
|
| 133 |
-
|
| 134 |
-
# 使用 set 去重用户账号
|
| 135 |
-
target_users = list(set(record.account for record in ownerships if record.account))
|
| 136 |
-
|
| 137 |
-
if not target_users:
|
| 138 |
-
# 无人购买,但仍记录已通知版本
|
| 139 |
-
update_notifications_db[item_id] = version_hash
|
| 140 |
-
continue
|
| 141 |
-
|
| 142 |
-
# 准备通知数据
|
| 143 |
-
notif_data = {
|
| 144 |
-
"type": "plugin_update",
|
| 145 |
-
"from_user": "system",
|
| 146 |
-
"target_item_id": item_id,
|
| 147 |
-
"target_item_title": title,
|
| 148 |
-
"content": f"您已安装的插件 [{title}] 有新版本可用"
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
# 分批发送通知
|
| 152 |
-
notified_count = 0
|
| 153 |
-
failed_users = []
|
| 154 |
-
for i in range(0, len(target_users), batch_size):
|
| 155 |
-
batch = target_users[i:i + batch_size]
|
| 156 |
-
|
| 157 |
-
for account in batch:
|
| 158 |
-
try:
|
| 159 |
-
add_notification(account, notif_data)
|
| 160 |
-
notified_count += 1
|
| 161 |
-
except Exception as e:
|
| 162 |
-
failed_users.append(account)
|
| 163 |
-
logger.error(f"发送更新通知失败 (user={account}, item={item_id}): {e}")
|
| 164 |
-
# 单个失败不影响其他用户
|
| 165 |
-
|
| 166 |
-
if failed_users:
|
| 167 |
-
logger.warning(f"NOTIFY_FAILED | count={len(failed_users)} | users={failed_users[:10]}")
|
| 168 |
-
|
| 169 |
-
# 每批之间短暂休眠,避免通知风暴
|
| 170 |
-
if i + batch_size < len(target_users):
|
| 171 |
-
await asyncio.sleep(0.5)
|
| 172 |
-
|
| 173 |
-
# 记录该插件已通知的版本
|
| 174 |
-
update_notifications_db[item_id] = version_hash
|
| 175 |
-
total_notified += notified_count
|
| 176 |
-
print(f"📢 插件 [{title}] 已向 {notified_count} 位用户发送更新通知")
|
| 177 |
-
|
| 178 |
-
# 保存已通知记录
|
| 179 |
-
db.save_data("update_notifications.json", update_notifications_db)
|
| 180 |
-
print(f"✅ 更新通知发送完成,共通知 {total_notified} 位用户,涉及 {len(items_to_notify)} 个插件")
|
| 181 |
-
|
| 182 |
-
except Exception as e:
|
| 183 |
-
logger.error(f"触发更新通知时发生错误: {e}")
|
| 184 |
-
# 不抛出异常,让主流程继续
|
| 185 |
-
|
| 186 |
-
|
| 187 |
async def precache_github_zip(repo_url: str, token: Optional[str], item_id: str, version_hash: str):
|
| 188 |
"""预下载 GitHub ZIP 并上传到 HF Dataset 缓存"""
|
| 189 |
try:
|
|
@@ -463,8 +359,8 @@ async def daily_version_check_task():
|
|
| 463 |
if updated_count > 0:
|
| 464 |
db.save_data("versions.json", versions_db)
|
| 465 |
print(f"✅ 版本检测任务完成,共发现并更新了 {updated_count} 个资源的新版本。")
|
| 466 |
-
#
|
| 467 |
-
|
| 468 |
else:
|
| 469 |
print("✅ 版本检测任务完成,暂无任何新版本发现。")
|
| 470 |
|
|
|
|
| 8 |
import tempfile
|
| 9 |
import zipfile
|
| 10 |
import 数据库连接 as db
|
|
|
|
|
|
|
|
|
|
| 11 |
from huggingface_hub import upload_file, HfApi
|
| 12 |
|
| 13 |
logger = logging.getLogger("ComfyUI-Ranking.VersionCheck")
|
|
|
|
| 80 |
logger.error(f"检测版本失败 {repo_url}: {e}")
|
| 81 |
return None
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
async def precache_github_zip(repo_url: str, token: Optional[str], item_id: str, version_hash: str):
|
| 84 |
"""预下载 GitHub ZIP 并上传到 HF Dataset 缓存"""
|
| 85 |
try:
|
|
|
|
| 359 |
if updated_count > 0:
|
| 360 |
db.save_data("versions.json", versions_db)
|
| 361 |
print(f"✅ 版本检测任务完成,共发现并更新了 {updated_count} 个资源的新版本。")
|
| 362 |
+
# 📦 更新提醒改由卡片徽章承载:前端比对本地版本戳与云端 versions.json 的 latest_version,
|
| 363 |
+
# 检测到差异即在卡片上显示「🔄 更新可用」,不再向用户推送 plugin_update 铃铛通知
|
| 364 |
else:
|
| 365 |
print("✅ 版本检测任务完成,暂无任何新版本发现。")
|
| 366 |
|
前端页面/components/关于插件组件.js
CHANGED
|
@@ -158,7 +158,62 @@ export function createAboutView(versionString, stageLabel) {
|
|
| 158 |
</div>
|
| 159 |
`);
|
| 160 |
|
| 161 |
-
// ===== 4. 用
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
const userValueSection = createCardSection("👤 " + t('about.benefits_title'), `
|
| 163 |
<div style="display: flex; flex-direction: column; gap: 10px; padding-left: 4px;">
|
| 164 |
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
|
@@ -184,7 +239,7 @@ export function createAboutView(versionString, stageLabel) {
|
|
| 184 |
</div>
|
| 185 |
`);
|
| 186 |
|
| 187 |
-
// =====
|
| 188 |
const warningSection = document.createElement("div");
|
| 189 |
Object.assign(warningSection.style, {
|
| 190 |
background: "rgba(240, 160, 48, 0.05)",
|
|
@@ -209,7 +264,7 @@ export function createAboutView(versionString, stageLabel) {
|
|
| 209 |
</div>
|
| 210 |
`;
|
| 211 |
|
| 212 |
-
// =====
|
| 213 |
const authorSection = document.createElement("div");
|
| 214 |
Object.assign(authorSection.style, { ...CARD_BASE_STYLE, textAlign: "center" });
|
| 215 |
authorSection.innerHTML = `
|
|
@@ -261,6 +316,7 @@ export function createAboutView(versionString, stageLabel) {
|
|
| 261 |
container.appendChild(heroSection);
|
| 262 |
container.appendChild(advantageSection);
|
| 263 |
container.appendChild(featureSection);
|
|
|
|
| 264 |
container.appendChild(userValueSection);
|
| 265 |
container.appendChild(warningSection);
|
| 266 |
container.appendChild(authorSection);
|
|
|
|
| 158 |
</div>
|
| 159 |
`);
|
| 160 |
|
| 161 |
+
// ===== 4. 使用指南区 =====
|
| 162 |
+
const guideSection = createCardSection("📖 " + t('about.guide_title'), `
|
| 163 |
+
<div style="display: flex; flex-direction: column; gap: 12px; padding-left: 4px;">
|
| 164 |
+
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
| 165 |
+
<span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">1.</span>
|
| 166 |
+
<div>
|
| 167 |
+
<div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_install_title')}</div>
|
| 168 |
+
<div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_install_desc')}</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
| 172 |
+
<span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">2.</span>
|
| 173 |
+
<div>
|
| 174 |
+
<div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_path_title')}</div>
|
| 175 |
+
<div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_path_desc')}</div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
| 179 |
+
<span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">3.</span>
|
| 180 |
+
<div>
|
| 181 |
+
<div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_update_title')}</div>
|
| 182 |
+
<div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_update_desc')}</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
| 186 |
+
<span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">4.</span>
|
| 187 |
+
<div>
|
| 188 |
+
<div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_publish_title')}</div>
|
| 189 |
+
<div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_publish_desc')}</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
| 193 |
+
<span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">5.</span>
|
| 194 |
+
<div>
|
| 195 |
+
<div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_buy_title')}</div>
|
| 196 |
+
<div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_buy_desc')}</div>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
| 200 |
+
<span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">6.</span>
|
| 201 |
+
<div>
|
| 202 |
+
<div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_refund_title')}</div>
|
| 203 |
+
<div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_refund_desc')}</div>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
| 207 |
+
<span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">7.</span>
|
| 208 |
+
<div>
|
| 209 |
+
<div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_withdraw_title')}</div>
|
| 210 |
+
<div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_withdraw_desc')}</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
`);
|
| 215 |
+
|
| 216 |
+
// ===== 5. 用户价值区 =====
|
| 217 |
const userValueSection = createCardSection("👤 " + t('about.benefits_title'), `
|
| 218 |
<div style="display: flex; flex-direction: column; gap: 10px; padding-left: 4px;">
|
| 219 |
<div style="display: flex; align-items: flex-start; gap: 8px;">
|
|
|
|
| 239 |
</div>
|
| 240 |
`);
|
| 241 |
|
| 242 |
+
// ===== 6. 注意事项区 =====
|
| 243 |
const warningSection = document.createElement("div");
|
| 244 |
Object.assign(warningSection.style, {
|
| 245 |
background: "rgba(240, 160, 48, 0.05)",
|
|
|
|
| 264 |
</div>
|
| 265 |
`;
|
| 266 |
|
| 267 |
+
// ===== 7. 关于作者区 =====
|
| 268 |
const authorSection = document.createElement("div");
|
| 269 |
Object.assign(authorSection.style, { ...CARD_BASE_STYLE, textAlign: "center" });
|
| 270 |
authorSection.innerHTML = `
|
|
|
|
| 316 |
container.appendChild(heroSection);
|
| 317 |
container.appendChild(advantageSection);
|
| 318 |
container.appendChild(featureSection);
|
| 319 |
+
container.appendChild(guideSection);
|
| 320 |
container.appendChild(userValueSection);
|
| 321 |
container.appendChild(warningSection);
|
| 322 |
container.appendChild(authorSection);
|
前端页面/components/动画音效引擎.js
CHANGED
|
@@ -494,6 +494,7 @@ export function applyViewportAnimations(container, cards, animationType, isFirst
|
|
| 494 |
export function getAnimationTypeForTab(tabId) {
|
| 495 |
const animationMap = {
|
| 496 |
'tools': 'cascade', // 工具榜
|
|
|
|
| 497 |
'apps': 'cascade', // 应用榜
|
| 498 |
'recommends': 'cascade', // 推荐榜
|
| 499 |
'creators': 'fan', // 创作者榜
|
|
|
|
| 494 |
export function getAnimationTypeForTab(tabId) {
|
| 495 |
const animationMap = {
|
| 496 |
'tools': 'cascade', // 工具榜
|
| 497 |
+
'skills': 'cascade', // Skill榜
|
| 498 |
'apps': 'cascade', // 应用榜
|
| 499 |
'recommends': 'cascade', // 推荐榜
|
| 500 |
'creators': 'fan', // 创作者榜
|
前端页面/components/用户体验_国际化.js
CHANGED
|
@@ -59,6 +59,7 @@ const translations = {
|
|
| 59 |
|
| 60 |
// 导航
|
| 61 |
'nav.tools': '插件榜',
|
|
|
|
| 62 |
'nav.apps': '工作流',
|
| 63 |
'nav.creators': '创作者',
|
| 64 |
'nav.recommends': '推荐榜',
|
|
@@ -448,7 +449,6 @@ const translations = {
|
|
| 448 |
'notif.title': '消息与通知',
|
| 449 |
'notif.clear': '清空记录',
|
| 450 |
'notif.no_messages': '暂无任何消息通知',
|
| 451 |
-
'notif.private_msg': '给您发送了私信',
|
| 452 |
'notif.followed_you': '关注了您',
|
| 453 |
'notif.liked': '点赞了您的作品',
|
| 454 |
'notif.favorited': '收藏了您的作品',
|
|
@@ -469,9 +469,6 @@ const translations = {
|
|
| 469 |
'notif.system_announcement': '系统公告',
|
| 470 |
'notif.clear_confirm': '确定要清空本地的所有通知记录吗?云端7天前的记录不会再同步下来。',
|
| 471 |
'notif.cleared': '通知已清空',
|
| 472 |
-
'notif.plugin_update_title': '插件更新',
|
| 473 |
-
'notif.plugin_update': '插件',
|
| 474 |
-
'notif.click_to_view': '点击查看',
|
| 475 |
'notif.item_not_found': '资源不存在或已被删除',
|
| 476 |
'notif.load_item_failed': '加载资源失败,请稍后重试',
|
| 477 |
|
|
@@ -511,12 +508,14 @@ const translations = {
|
|
| 511 |
'creator.stats.favorites': '收藏',
|
| 512 |
'creator.stats.followers': '粉丝',
|
| 513 |
'creator.output.tools': '产出插件',
|
|
|
|
| 514 |
'creator.output.apps': '产出工作流',
|
| 515 |
'creator.output.unit': '个',
|
| 516 |
'creator.chart.download_trend': '📈 下载量增长趋势',
|
| 517 |
'creator.chart.loading': '正在加载图表...',
|
| 518 |
'creator.chart.load_failed': '图表库加载失败',
|
| 519 |
'creator.chart.series.tools': '插件下载',
|
|
|
|
| 520 |
'creator.chart.series.apps': '工作流下载',
|
| 521 |
'creator.chart.series.recommends': '推荐榜获取',
|
| 522 |
'creator.tip_board.title': '🎁 赞赏贡献总榜',
|
|
@@ -1208,10 +1207,12 @@ const translations = {
|
|
| 1208 |
'publish.confirm_publish': '确 认 发 布',
|
| 1209 |
'publish.main_category': '主类别',
|
| 1210 |
'publish.type_tool': '原创插件',
|
|
|
|
| 1211 |
'publish.type_app': '原创工作流',
|
| 1212 |
'publish.type_recommend': '推荐他人作品',
|
| 1213 |
-
'publish.recommend_form': '推荐形式 (
|
| 1214 |
'publish.recommend_as_tool': '作为插件 (支持Git安装)',
|
|
|
|
| 1215 |
'publish.recommend_as_app': '作为工作流 (加载JSON)',
|
| 1216 |
'publish.recommend_as_link': '纯链接 (新窗口打开)',
|
| 1217 |
'publish.name': '名称',
|
|
@@ -1436,6 +1437,21 @@ const translations = {
|
|
| 1436 |
'about.warning_login_desc': '之前注册的账号无法登录的,请修改密码后再登录。',
|
| 1437 |
'about.warning_account_title': '登录账号错误:',
|
| 1438 |
'about.warning_account_desc': '可能是你的浏览器自动帮你填入别的网站账号密码,账号是字符(字母、数字、符号)没有中文。',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1439 |
'about.author_title': '关于作者',
|
| 1440 |
'about.author_thanks': '感谢使用 ComfyUI 社区精选,愿 ComfyUI 生态繁荣昌盛 🌱'
|
| 1441 |
},
|
|
@@ -1488,6 +1504,7 @@ const translations = {
|
|
| 1488 |
|
| 1489 |
// Navigation
|
| 1490 |
'nav.tools': 'Plugins',
|
|
|
|
| 1491 |
'nav.apps': 'Workflows',
|
| 1492 |
'nav.creators': 'Creators',
|
| 1493 |
'nav.recommends': 'Top Picks',
|
|
@@ -1869,7 +1886,6 @@ const translations = {
|
|
| 1869 |
'notif.title': 'Messages & Notifications',
|
| 1870 |
'notif.clear': 'Clear All',
|
| 1871 |
'notif.no_messages': 'No messages yet',
|
| 1872 |
-
'notif.private_msg': 'sent you a private message',
|
| 1873 |
'notif.followed_you': 'followed you',
|
| 1874 |
'notif.liked': 'liked your work',
|
| 1875 |
'notif.favorited': 'favorited your work',
|
|
@@ -1890,9 +1906,6 @@ const translations = {
|
|
| 1890 |
'notif.system_announcement': 'System Announcement',
|
| 1891 |
'notif.clear_confirm': 'Clear all local notifications? Records older than 7 days will not be synced again.',
|
| 1892 |
'notif.cleared': 'Notifications cleared',
|
| 1893 |
-
'notif.plugin_update_title': 'Plugin Update',
|
| 1894 |
-
'notif.plugin_update': 'Plugin',
|
| 1895 |
-
'notif.click_to_view': 'click to view',
|
| 1896 |
'notif.item_not_found': 'Item not found or deleted',
|
| 1897 |
'notif.load_item_failed': 'Failed to load item, please try again later',
|
| 1898 |
|
|
@@ -1932,12 +1945,14 @@ const translations = {
|
|
| 1932 |
'creator.stats.favorites': 'Favorites',
|
| 1933 |
'creator.stats.followers': 'Followers',
|
| 1934 |
'creator.output.tools': 'Plugins',
|
|
|
|
| 1935 |
'creator.output.apps': 'Workflows',
|
| 1936 |
'creator.output.unit': '',
|
| 1937 |
'creator.chart.download_trend': '📈 Download Growth Trend',
|
| 1938 |
'creator.chart.loading': 'Loading chart...',
|
| 1939 |
'creator.chart.load_failed': 'Chart failed to load',
|
| 1940 |
'creator.chart.series.tools': 'Plugin Downloads',
|
|
|
|
| 1941 |
'creator.chart.series.apps': 'Workflow Downloads',
|
| 1942 |
'creator.chart.series.recommends': 'Top Picks',
|
| 1943 |
'creator.tip_board.title': '🎁 Top Contributors',
|
|
@@ -2569,10 +2584,12 @@ const translations = {
|
|
| 2569 |
'publish.confirm_publish': 'Confirm Publish',
|
| 2570 |
'publish.main_category': 'Category',
|
| 2571 |
'publish.type_tool': 'Original Plugin',
|
|
|
|
| 2572 |
'publish.type_app': 'Original Workflow',
|
| 2573 |
'publish.type_recommend': 'Recommend Others\' Work',
|
| 2574 |
'publish.recommend_form': 'Recommendation Type',
|
| 2575 |
'publish.recommend_as_tool': 'As Plugin (Git Install)',
|
|
|
|
| 2576 |
'publish.recommend_as_app': 'As Workflow (Load JSON)',
|
| 2577 |
'publish.recommend_as_link': 'Link Only (Opens in New Tab)',
|
| 2578 |
'publish.name': 'Name',
|
|
@@ -2795,6 +2812,21 @@ const translations = {
|
|
| 2795 |
'about.warning_login_desc': 'If your previously registered account cannot log in, please reset your password and try again.',
|
| 2796 |
'about.warning_account_title': 'Wrong Account Auto-filled:',
|
| 2797 |
'about.warning_account_desc': 'Your browser may have auto-filled credentials from another site. Account names only contain letters, numbers, and symbols — no Chinese characters.',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2798 |
'about.author_title': 'About the Author',
|
| 2799 |
'about.author_thanks': 'Thank you for using ComfyUI Community Hub. May the ComfyUI ecosystem thrive and flourish 🌱'
|
| 2800 |
}
|
|
|
|
| 59 |
|
| 60 |
// 导航
|
| 61 |
'nav.tools': '插件榜',
|
| 62 |
+
'nav.skills': 'Skill',
|
| 63 |
'nav.apps': '工作流',
|
| 64 |
'nav.creators': '创作者',
|
| 65 |
'nav.recommends': '推荐榜',
|
|
|
|
| 449 |
'notif.title': '消息与通知',
|
| 450 |
'notif.clear': '清空记录',
|
| 451 |
'notif.no_messages': '暂无任何消息通知',
|
|
|
|
| 452 |
'notif.followed_you': '关注了您',
|
| 453 |
'notif.liked': '点赞了您的作品',
|
| 454 |
'notif.favorited': '收藏了您的作品',
|
|
|
|
| 469 |
'notif.system_announcement': '系统公告',
|
| 470 |
'notif.clear_confirm': '确定要清空本地的所有通知记录吗?云端7天前的记录不会再同步下来。',
|
| 471 |
'notif.cleared': '通知已清空',
|
|
|
|
|
|
|
|
|
|
| 472 |
'notif.item_not_found': '资源不存在或已被删除',
|
| 473 |
'notif.load_item_failed': '加载资源失败,请稍后重试',
|
| 474 |
|
|
|
|
| 508 |
'creator.stats.favorites': '收藏',
|
| 509 |
'creator.stats.followers': '粉丝',
|
| 510 |
'creator.output.tools': '产出插件',
|
| 511 |
+
'creator.output.skills': '产出Skill',
|
| 512 |
'creator.output.apps': '产出工作流',
|
| 513 |
'creator.output.unit': '个',
|
| 514 |
'creator.chart.download_trend': '📈 下载量增长趋势',
|
| 515 |
'creator.chart.loading': '正在加载图表...',
|
| 516 |
'creator.chart.load_failed': '图表库加载失败',
|
| 517 |
'creator.chart.series.tools': '插件下载',
|
| 518 |
+
'creator.chart.series.skills': 'Skill下载',
|
| 519 |
'creator.chart.series.apps': '工作流下载',
|
| 520 |
'creator.chart.series.recommends': '推荐榜获取',
|
| 521 |
'creator.tip_board.title': '🎁 赞赏贡献总榜',
|
|
|
|
| 1207 |
'publish.confirm_publish': '确 认 发 布',
|
| 1208 |
'publish.main_category': '主类别',
|
| 1209 |
'publish.type_tool': '原创插件',
|
| 1210 |
+
'publish.type_skill': '原创Skill',
|
| 1211 |
'publish.type_app': '原创工作流',
|
| 1212 |
'publish.type_recommend': '推荐他人作品',
|
| 1213 |
+
'publish.recommend_form': '推荐形式 (四选一)',
|
| 1214 |
'publish.recommend_as_tool': '作为插件 (支持Git安装)',
|
| 1215 |
+
'publish.recommend_as_skill': '作为Skill (支持Git安装)',
|
| 1216 |
'publish.recommend_as_app': '作为工作流 (加载JSON)',
|
| 1217 |
'publish.recommend_as_link': '纯链接 (新窗口打开)',
|
| 1218 |
'publish.name': '名称',
|
|
|
|
| 1437 |
'about.warning_login_desc': '之前注册的账号无法登录的,请修改密码后再登录。',
|
| 1438 |
'about.warning_account_title': '登录账号错误:',
|
| 1439 |
'about.warning_account_desc': '可能是你的浏览器自动帮你填入别的网站账号密码,账号是字符(字母、数字、符号)没有中文。',
|
| 1440 |
+
'about.guide_title': '使用指南',
|
| 1441 |
+
'about.guide_install_title': '安装插件和Skill',
|
| 1442 |
+
'about.guide_install_desc': '打开喜欢的作品详情页,点「立即获取使用」按钮:免费作品一键装好,付费作品买完后就能装。装完后重启 ComfyUI,新插件就会出现在节点列表里。',
|
| 1443 |
+
'about.guide_path_title': '文件下载到哪里',
|
| 1444 |
+
'about.guide_path_desc': '插件和Skill统一装进 ComfyUI 安装目录里的 custom_nodes 文件夹,不用自己搬文件;工作流下载后自动加载到画布上,打开就能用。',
|
| 1445 |
+
'about.guide_update_title': '更新到新版本',
|
| 1446 |
+
'about.guide_update_desc': '作者发布新版本后,作品详情页会出现「发现新版本」按钮,点一下自动完成更新,不用先卸载旧版本。',
|
| 1447 |
+
'about.guide_publish_title': '发布自己的作品',
|
| 1448 |
+
'about.guide_publish_desc': '点榜单顶部的「发布」按钮,填好名称、介绍和效果图就能提交。插件和Skill填代码仓库的网址,工作流直接上传工作流文件。',
|
| 1449 |
+
'about.guide_buy_title': '购买付费作品',
|
| 1450 |
+
'about.guide_buy_desc': '买付费作品要先让账户里有足够余额:进「个人中心-钱包」用支付宝扫码充值。充好后回详情页点「立即获取使用」即完成购买,一次购买终身有效。',
|
| 1451 |
+
'about.guide_refund_title': '申请退款',
|
| 1452 |
+
'about.guide_refund_desc': '带「支持退款」标识的作品,购买后24小时内可退:打开作品详情页点退款按钮,积分原路退回余额。注意:退款后30天内不能再买同一作品。',
|
| 1453 |
+
'about.guide_withdraw_title': '提现赚到的积分',
|
| 1454 |
+
'about.guide_withdraw_desc': '卖作品或收到打赏赚的积分可以提现:进「个��中心-钱包-提现」,填好金额提交申请,管理员审核后打款给你。终身前100积分免手续费,超出部分收10%手续费。',
|
| 1455 |
'about.author_title': '关于作者',
|
| 1456 |
'about.author_thanks': '感谢使用 ComfyUI 社区精选,愿 ComfyUI 生态繁荣昌盛 🌱'
|
| 1457 |
},
|
|
|
|
| 1504 |
|
| 1505 |
// Navigation
|
| 1506 |
'nav.tools': 'Plugins',
|
| 1507 |
+
'nav.skills': 'Skills',
|
| 1508 |
'nav.apps': 'Workflows',
|
| 1509 |
'nav.creators': 'Creators',
|
| 1510 |
'nav.recommends': 'Top Picks',
|
|
|
|
| 1886 |
'notif.title': 'Messages & Notifications',
|
| 1887 |
'notif.clear': 'Clear All',
|
| 1888 |
'notif.no_messages': 'No messages yet',
|
|
|
|
| 1889 |
'notif.followed_you': 'followed you',
|
| 1890 |
'notif.liked': 'liked your work',
|
| 1891 |
'notif.favorited': 'favorited your work',
|
|
|
|
| 1906 |
'notif.system_announcement': 'System Announcement',
|
| 1907 |
'notif.clear_confirm': 'Clear all local notifications? Records older than 7 days will not be synced again.',
|
| 1908 |
'notif.cleared': 'Notifications cleared',
|
|
|
|
|
|
|
|
|
|
| 1909 |
'notif.item_not_found': 'Item not found or deleted',
|
| 1910 |
'notif.load_item_failed': 'Failed to load item, please try again later',
|
| 1911 |
|
|
|
|
| 1945 |
'creator.stats.favorites': 'Favorites',
|
| 1946 |
'creator.stats.followers': 'Followers',
|
| 1947 |
'creator.output.tools': 'Plugins',
|
| 1948 |
+
'creator.output.skills': 'Skills',
|
| 1949 |
'creator.output.apps': 'Workflows',
|
| 1950 |
'creator.output.unit': '',
|
| 1951 |
'creator.chart.download_trend': '📈 Download Growth Trend',
|
| 1952 |
'creator.chart.loading': 'Loading chart...',
|
| 1953 |
'creator.chart.load_failed': 'Chart failed to load',
|
| 1954 |
'creator.chart.series.tools': 'Plugin Downloads',
|
| 1955 |
+
'creator.chart.series.skills': 'Skill Downloads',
|
| 1956 |
'creator.chart.series.apps': 'Workflow Downloads',
|
| 1957 |
'creator.chart.series.recommends': 'Top Picks',
|
| 1958 |
'creator.tip_board.title': '🎁 Top Contributors',
|
|
|
|
| 2584 |
'publish.confirm_publish': 'Confirm Publish',
|
| 2585 |
'publish.main_category': 'Category',
|
| 2586 |
'publish.type_tool': 'Original Plugin',
|
| 2587 |
+
'publish.type_skill': 'Original Skill',
|
| 2588 |
'publish.type_app': 'Original Workflow',
|
| 2589 |
'publish.type_recommend': 'Recommend Others\' Work',
|
| 2590 |
'publish.recommend_form': 'Recommendation Type',
|
| 2591 |
'publish.recommend_as_tool': 'As Plugin (Git Install)',
|
| 2592 |
+
'publish.recommend_as_skill': 'As Skill (Git Install)',
|
| 2593 |
'publish.recommend_as_app': 'As Workflow (Load JSON)',
|
| 2594 |
'publish.recommend_as_link': 'Link Only (Opens in New Tab)',
|
| 2595 |
'publish.name': 'Name',
|
|
|
|
| 2812 |
'about.warning_login_desc': 'If your previously registered account cannot log in, please reset your password and try again.',
|
| 2813 |
'about.warning_account_title': 'Wrong Account Auto-filled:',
|
| 2814 |
'about.warning_account_desc': 'Your browser may have auto-filled credentials from another site. Account names only contain letters, numbers, and symbols — no Chinese characters.',
|
| 2815 |
+
'about.guide_title': 'Usage Guide',
|
| 2816 |
+
'about.guide_install_title': 'Installing Plugins & Skills',
|
| 2817 |
+
'about.guide_install_desc': 'Open the detail page of an item you like and click the "Get Now" button: free items install with one click, and paid items install right after purchase. When the installation finishes, restart ComfyUI and the new plugin will appear in your node list.',
|
| 2818 |
+
'about.guide_path_title': 'Where Files Are Downloaded',
|
| 2819 |
+
'about.guide_path_desc': 'Plugins and Skills are installed automatically into the custom_nodes folder inside your ComfyUI directory, so you never need to move files yourself. Workflows are loaded straight onto the canvas after download, ready to use.',
|
| 2820 |
+
'about.guide_update_title': 'Updating to New Versions',
|
| 2821 |
+
'about.guide_update_desc': 'When an author releases a new version, an "Update available" button appears on the item\'s detail page. Click it to update automatically — no need to remove the old version first.',
|
| 2822 |
+
'about.guide_publish_title': 'Publishing Your Own Work',
|
| 2823 |
+
'about.guide_publish_desc': 'Click the "Publish" button at the top of the ranking, fill in the name, description and preview images, then submit. For plugins and Skills, paste the web address of your code repository; for workflows, upload the workflow file directly.',
|
| 2824 |
+
'about.guide_buy_title': 'Buying Paid Items',
|
| 2825 |
+
'about.guide_buy_desc': 'Paid items need enough balance in your account first: open "Profile - Wallet" and top up by scanning the Alipay QR code. Then return to the detail page and click "Get Now" to finish the purchase — one purchase, yours forever.',
|
| 2826 |
+
'about.guide_refund_title': 'Requesting a Refund',
|
| 2827 |
+
'about.guide_refund_desc': 'Items marked "Refundable" can be refunded within 24 hours of purchase: open the item\'s detail page, click the refund button, and the credits return to your balance. Note: after a refund, the same item cannot be repurchased for 30 days.',
|
| 2828 |
+
'about.guide_withdraw_title': 'Withdrawing Your Earnings',
|
| 2829 |
+
'about.guide_withdraw_desc': 'Credits earned from sales or tips can be withdrawn: open "Profile - Wallet - Withdraw", enter the amount and submit. An admin will review and transfer the money to you. The first 100 credits in a lifetime are fee-free; a 10% fee applies to the amount above that.',
|
| 2830 |
'about.author_title': 'About the Author',
|
| 2831 |
'about.author_thanks': 'Thank you for using ComfyUI Community Hub. May the ComfyUI ecosystem thrive and flourish 🌱'
|
| 2832 |
}
|
前端页面/components/顶部导航组件.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
// 前端页面/components/顶部导航组件.js
|
| 2 |
import { createAuthView } from "../auth/用户注册登录组件.js";
|
| 3 |
import { openUserProfileModal } from "../profile/个人中心视图.js";
|
| 4 |
-
import { openChatModal } from "../social/私信聊天组件.js";
|
| 5 |
import { api } from "../core/网络请求API.js";
|
| 6 |
import { showToast } from "./UI交互提示组件.js";
|
| 7 |
import { showAboutInfo } from "./关于插件组件.js";
|
|
@@ -29,7 +29,9 @@ function _registerNavWindowListeners() {
|
|
| 29 |
{ event: "comfy-user-logout", handler: () => { if (_navInstance) _navInstance.onLogout(); } },
|
| 30 |
// 🔧 修复:凭证失效(401)时除停止轮询外,同步重置导航登录态(原实现未重置,头像按钮仍显示已登录)
|
| 31 |
{ event: "comfy-ranking-auth-expired", handler: () => { stopMessagePolling(); if (_navInstance) _navInstance.onLogout(); } },
|
| 32 |
-
{ event: "storage", handler: (event) => { if (_navInstance) _navInstance.onStorage(event); } }
|
|
|
|
|
|
|
| 33 |
];
|
| 34 |
for (const { event, handler } of _navWindowHandlers) window.addEventListener(event, handler);
|
| 35 |
}
|
|
@@ -80,8 +82,9 @@ export function createTopNav() {
|
|
| 80 |
Object.assign(userActionBtn.style, { padding: "6px 12px", backgroundColor: "#333", color: "#fff", border: "1px solid #555", borderRadius: "4px", cursor: "pointer", fontSize: "12px", transition: "background 0.2s" });
|
| 81 |
|
| 82 |
const chatEntryBtn = document.createElement("button");
|
| 83 |
-
Object.assign(chatEntryBtn.style, { background: "transparent", border: "none", color: "#aaa", cursor: "pointer", fontSize: "18px", marginRight: "10px", transition: "0.2s" });
|
| 84 |
-
|
|
|
|
| 85 |
chatEntryBtn.title = t('social.chat');
|
| 86 |
|
| 87 |
const bellBtn = document.createElement("button");
|
|
@@ -119,9 +122,10 @@ export function createTopNav() {
|
|
| 119 |
userActionBtn.style.borderColor = "#2196F3";
|
| 120 |
userActionBtn.onclick = () => openUserProfileModal(currentUser);
|
| 121 |
loadUnreadCount(currentUser, bellBtn);
|
|
|
|
| 122 |
|
| 123 |
// 🚀 新增:启动消息定时轮询
|
| 124 |
-
startMessagePolling(currentUser, bellBtn);
|
| 125 |
} else {
|
| 126 |
// 🚀 新增:停止轮询
|
| 127 |
stopMessagePolling();
|
|
@@ -130,6 +134,7 @@ export function createTopNav() {
|
|
| 130 |
userActionBtn.style.backgroundColor = "#333";
|
| 131 |
userActionBtn.style.borderColor = "#555";
|
| 132 |
bellBtn.querySelector("#unread-badge").style.display = "none";
|
|
|
|
| 133 |
|
| 134 |
userActionBtn.onclick = () => {
|
| 135 |
const view = createAuthView(async (formData) => {
|
|
@@ -240,7 +245,8 @@ export function createTopNav() {
|
|
| 240 |
// 🔧 P1修复:注册为当前活跃实例,模块级监听将路由到本实例的回调
|
| 241 |
_navInstance = {
|
| 242 |
onLogout: () => { currentUser = null; updateUserButtonState(); },
|
| 243 |
-
onStorage: handleCrossTabStorage
|
|
|
|
| 244 |
};
|
| 245 |
_registerNavWindowListeners();
|
| 246 |
|
|
@@ -263,7 +269,7 @@ export function createTopNav() {
|
|
| 263 |
}
|
| 264 |
|
| 265 |
// 🚀 新增:启动消息定时轮询
|
| 266 |
-
function startMessagePolling(currentUser, bellBtn) {
|
| 267 |
if (!CACHE.MESSAGE_POLL.ENABLED) return;
|
| 268 |
if (isPollingActive) return; // 🔧 P3优化:防止重复启动
|
| 269 |
|
|
@@ -279,7 +285,9 @@ function startMessagePolling(currentUser, bellBtn) {
|
|
| 279 |
return;
|
| 280 |
}
|
| 281 |
try {
|
|
|
|
| 282 |
await loadUnreadCount(currentUser, bellBtn);
|
|
|
|
| 283 |
} catch (e) {
|
| 284 |
// 静默失败,不影响用户体验
|
| 285 |
}
|
|
|
|
| 1 |
// 前端页面/components/顶部导航组件.js
|
| 2 |
import { createAuthView } from "../auth/用户注册登录组件.js";
|
| 3 |
import { openUserProfileModal } from "../profile/个人中心视图.js";
|
| 4 |
+
import { openChatModal, loadChatUnreadCount } from "../social/私信聊天组件.js";
|
| 5 |
import { api } from "../core/网络请求API.js";
|
| 6 |
import { showToast } from "./UI交互提示组件.js";
|
| 7 |
import { showAboutInfo } from "./关于插件组件.js";
|
|
|
|
| 29 |
{ event: "comfy-user-logout", handler: () => { if (_navInstance) _navInstance.onLogout(); } },
|
| 30 |
// 🔧 修复:凭证失效(401)时除停止轮询外,同步重置导航登录态(原实现未重置,头像按钮仍显示已登录)
|
| 31 |
{ event: "comfy-ranking-auth-expired", handler: () => { stopMessagePolling(); if (_navInstance) _navInstance.onLogout(); } },
|
| 32 |
+
{ event: "storage", handler: (event) => { if (_navInstance) _navInstance.onStorage(event); } },
|
| 33 |
+
// 🔔 聊天关闭后刷新 ✉️ 未读红点(打开会话已标记已读,未读数下降,需即时回写)
|
| 34 |
+
{ event: "comfy-chat-unread-changed", handler: () => { if (_navInstance) _navInstance.onChatUnreadChanged(); } }
|
| 35 |
];
|
| 36 |
for (const { event, handler } of _navWindowHandlers) window.addEventListener(event, handler);
|
| 37 |
}
|
|
|
|
| 82 |
Object.assign(userActionBtn.style, { padding: "6px 12px", backgroundColor: "#333", color: "#fff", border: "1px solid #555", borderRadius: "4px", cursor: "pointer", fontSize: "12px", transition: "background 0.2s" });
|
| 83 |
|
| 84 |
const chatEntryBtn = document.createElement("button");
|
| 85 |
+
Object.assign(chatEntryBtn.style, { background: "transparent", border: "none", color: "#aaa", cursor: "pointer", fontSize: "18px", marginRight: "10px", transition: "0.2s", position: "relative" });
|
| 86 |
+
// 🔔 私信未读红点:与铃铛 #unread-badge 同款样式,数据源为 chats.json 各会话未读汇总
|
| 87 |
+
chatEntryBtn.innerHTML = `✉️<span id="chat-unread-badge" style="display:none; position:absolute; top:-4px; right:-8px; background:#F44336; color:white; font-size:10px; font-weight:bold; padding:2px 5px; border-radius:10px; line-height:1;">0</span>`;
|
| 88 |
chatEntryBtn.title = t('social.chat');
|
| 89 |
|
| 90 |
const bellBtn = document.createElement("button");
|
|
|
|
| 122 |
userActionBtn.style.borderColor = "#2196F3";
|
| 123 |
userActionBtn.onclick = () => openUserProfileModal(currentUser);
|
| 124 |
loadUnreadCount(currentUser, bellBtn);
|
| 125 |
+
loadChatUnreadCount(currentUser, chatEntryBtn);
|
| 126 |
|
| 127 |
// 🚀 新增:启动消息定时轮询
|
| 128 |
+
startMessagePolling(currentUser, bellBtn, chatEntryBtn);
|
| 129 |
} else {
|
| 130 |
// 🚀 新增:停止轮询
|
| 131 |
stopMessagePolling();
|
|
|
|
| 134 |
userActionBtn.style.backgroundColor = "#333";
|
| 135 |
userActionBtn.style.borderColor = "#555";
|
| 136 |
bellBtn.querySelector("#unread-badge").style.display = "none";
|
| 137 |
+
chatEntryBtn.querySelector("#chat-unread-badge").style.display = "none";
|
| 138 |
|
| 139 |
userActionBtn.onclick = () => {
|
| 140 |
const view = createAuthView(async (formData) => {
|
|
|
|
| 245 |
// 🔧 P1修复:注册为当前活跃实例,模块级监听将路由到本实例的回调
|
| 246 |
_navInstance = {
|
| 247 |
onLogout: () => { currentUser = null; updateUserButtonState(); },
|
| 248 |
+
onStorage: handleCrossTabStorage,
|
| 249 |
+
onChatUnreadChanged: () => loadChatUnreadCount(currentUser, chatEntryBtn)
|
| 250 |
};
|
| 251 |
_registerNavWindowListeners();
|
| 252 |
|
|
|
|
| 269 |
}
|
| 270 |
|
| 271 |
// 🚀 新增:启动消息定时轮询
|
| 272 |
+
function startMessagePolling(currentUser, bellBtn, chatEntryBtn) {
|
| 273 |
if (!CACHE.MESSAGE_POLL.ENABLED) return;
|
| 274 |
if (isPollingActive) return; // 🔧 P3优化:防止重复启动
|
| 275 |
|
|
|
|
| 285 |
return;
|
| 286 |
}
|
| 287 |
try {
|
| 288 |
+
// 🔔 铃铛未读(点赞/评论/购买/任务/系统公告等)与 ✉️ 私信未读分别刷新
|
| 289 |
await loadUnreadCount(currentUser, bellBtn);
|
| 290 |
+
await loadChatUnreadCount(currentUser, chatEntryBtn);
|
| 291 |
} catch (e) {
|
| 292 |
// 静默失败,不影响用户体验
|
| 293 |
}
|
前端页面/core/侧边栏主程序.js
CHANGED
|
@@ -108,6 +108,7 @@ export function buildSidebarDOM() {
|
|
| 108 |
// 🌐 多语言支持:Tab 名称使用翻译函数
|
| 109 |
const tabs = [
|
| 110 |
{ id: "tools", label: t('nav.tools') },
|
|
|
|
| 111 |
{ id: "apps", label: t('nav.apps') },
|
| 112 |
{ id: "recommends", label: t('nav.recommends') },
|
| 113 |
{ id: "creators", label: t('nav.creators') },
|
|
@@ -365,8 +366,9 @@ export function buildSidebarDOM() {
|
|
| 365 |
// 1. 根据 itemType 确定 Tab
|
| 366 |
let targetTab;
|
| 367 |
if (itemType === "tool") targetTab = "tools";
|
|
|
|
| 368 |
else if (itemType === "app") targetTab = "apps";
|
| 369 |
-
else if (itemType
|
| 370 |
else targetTab = "tools"; // 兜底
|
| 371 |
|
| 372 |
// 【问题2修复】如果已经在目标 Tab,先尝试在当前 DOM 中查找卡片
|
|
@@ -475,26 +477,27 @@ export function buildSidebarDOM() {
|
|
| 475 |
const view = createPublishPromptView(currentUser);
|
| 476 |
showInlineView(view);
|
| 477 |
} else {
|
| 478 |
-
// 工具/应用/推荐 -> 打开发布内容界面,并自动设置对应类型
|
| 479 |
const publishView = createPublishView(currentUser,
|
| 480 |
() => hideInlineView(),
|
| 481 |
() => { hideInlineView(); triggerLoad(true); },
|
| 482 |
null, // editItemData
|
| 483 |
-
currentTab // initialType: tools/apps/recommends
|
| 484 |
);
|
| 485 |
showInlineView(publishView);
|
| 486 |
}
|
| 487 |
};
|
| 488 |
|
| 489 |
-
// Tab 颜色配置:
|
| 490 |
const tabColors = {
|
| 491 |
-
tools: { active: "#4CAF50", inactive: "#
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
|
|
|
| 498 |
};
|
| 499 |
|
| 500 |
// 文字阴影效果:多层阴影确保任何背景下都清晰可见
|
|
|
|
| 108 |
// 🌐 多语言支持:Tab 名称使用翻译函数
|
| 109 |
const tabs = [
|
| 110 |
{ id: "tools", label: t('nav.tools') },
|
| 111 |
+
{ id: "skills", label: t('nav.skills') },
|
| 112 |
{ id: "apps", label: t('nav.apps') },
|
| 113 |
{ id: "recommends", label: t('nav.recommends') },
|
| 114 |
{ id: "creators", label: t('nav.creators') },
|
|
|
|
| 366 |
// 1. 根据 itemType 确定 Tab
|
| 367 |
let targetTab;
|
| 368 |
if (itemType === "tool") targetTab = "tools";
|
| 369 |
+
else if (itemType === "skill") targetTab = "skills";
|
| 370 |
else if (itemType === "app") targetTab = "apps";
|
| 371 |
+
else if (itemType.startsWith("recommend")) targetTab = "recommends";
|
| 372 |
else targetTab = "tools"; // 兜底
|
| 373 |
|
| 374 |
// 【问题2修复】如果已经在目标 Tab,先尝试在当前 DOM 中查找卡片
|
|
|
|
| 477 |
const view = createPublishPromptView(currentUser);
|
| 478 |
showInlineView(view);
|
| 479 |
} else {
|
| 480 |
+
// 工具/Skill/应用/推荐 -> 打开发布内容界面,并自动设置对应类型
|
| 481 |
const publishView = createPublishView(currentUser,
|
| 482 |
() => hideInlineView(),
|
| 483 |
() => { hideInlineView(); triggerLoad(true); },
|
| 484 |
null, // editItemData
|
| 485 |
+
currentTab // initialType: tools/skills/apps/recommends
|
| 486 |
);
|
| 487 |
showInlineView(publishView);
|
| 488 |
}
|
| 489 |
};
|
| 490 |
|
| 491 |
+
// Tab 颜色配置:按色环渐变顺序排列(绿→青→蓝→紫→粉→红→橙→黄),相邻 Tab 色相平滑过渡
|
| 492 |
const tabColors = {
|
| 493 |
+
tools: { active: "#4CAF50", inactive: "#81C784" }, // 插件榜 - 绿(渐变起点)
|
| 494 |
+
skills: { active: "#00BCD4", inactive: "#4DD0E1" }, // Skill - 青
|
| 495 |
+
apps: { active: "#2196F3", inactive: "#64B5F6" }, // 工作流 - 蓝
|
| 496 |
+
recommends: { active: "#7C4DFF", inactive: "#B388FF" }, // 推荐榜 - 紫
|
| 497 |
+
creators: { active: "#E91E63", inactive: "#F06292" }, // 创作者 - 粉
|
| 498 |
+
tasks: { active: "#F44336", inactive: "#E57373" }, // 任务榜 - 红
|
| 499 |
+
posts: { active: "#FF9800", inactive: "#FFB74D" }, // 讨论区 - 橙
|
| 500 |
+
prompts: { active: "#FFC107", inactive: "#FFD54F" } // 提示词 - 黄(渐变终点)
|
| 501 |
};
|
| 502 |
|
| 503 |
// 文字阴影效果:多层阴影确保任何背景下都清晰可见
|
前端页面/core/侧边栏数据引擎.js
CHANGED
|
@@ -115,7 +115,7 @@ function sortDataLocally(data, tab, sort) {
|
|
| 115 |
default: sorted.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); break; // time
|
| 116 |
}
|
| 117 |
} else {
|
| 118 |
-
// 工具/应用/推荐排序(与后端 get_items 一致)
|
| 119 |
switch (sort) {
|
| 120 |
case "downloads": sorted.sort((a, b) => (b.uses || 0) - (a.uses || 0)); break;
|
| 121 |
case "likes": sorted.sort((a, b) => (b.likes || 0) - (a.likes || 0)); break;
|
|
@@ -400,7 +400,7 @@ export async function loadSidebarContent({
|
|
| 400 |
const cards = [];
|
| 401 |
let targetCard = null; // 用于保存需要展开的目标卡片
|
| 402 |
|
| 403 |
-
if (tab === "tools" || tab === "apps" || tab === "recommends") {
|
| 404 |
displayData.forEach(data => {
|
| 405 |
const card = createItemCard(data, currentUser);
|
| 406 |
cards.push(card);
|
|
@@ -523,8 +523,8 @@ export async function loadSidebarContent({
|
|
| 523 |
|
| 524 |
try {
|
| 525 |
let newData;
|
| 526 |
-
if (savedTab === "tools" || savedTab === "apps" || savedTab === "recommends") {
|
| 527 |
-
const itemType = savedTab === "tools" ? "tool" : (savedTab === "apps" ? "app" : "recommend");
|
| 528 |
const response = await api.getItems(itemType, savedSort, 200);
|
| 529 |
newData = proxyImages(response.data || []);
|
| 530 |
} else if (savedTab === "creators") {
|
|
@@ -640,7 +640,7 @@ export async function loadSidebarContent({
|
|
| 640 |
state.allData = sortedData;
|
| 641 |
state.isFullyLoaded = true;
|
| 642 |
state.isSearchResult = false;
|
| 643 |
-
// 🔧 先过滤再切片(tools/apps/recommends 带 keyword 时同样会进入本分支)
|
| 644 |
const scopedInstant = _applySearchScope(sortedData);
|
| 645 |
renderBatch(scopedInstant.slice(0, pageSize), false);
|
| 646 |
if (scopedInstant.length > pageSize) {
|
|
@@ -661,8 +661,8 @@ export async function loadSidebarContent({
|
|
| 661 |
try {
|
| 662 |
let response, realData;
|
| 663 |
|
| 664 |
-
if (tab === "tools" || tab === "apps" || tab === "recommends") {
|
| 665 |
-
const itemType = tab === "tools" ? "tool" : (tab === "apps" ? "app" : "recommend");
|
| 666 |
response = await api.getItems(itemType, sort, 200); // 获取较多数据
|
| 667 |
realData = response.data || [];
|
| 668 |
realData = proxyImages(realData); // 确保图片走本地缓存代理
|
|
@@ -721,7 +721,7 @@ export async function loadSidebarContent({
|
|
| 721 |
// 更新状态
|
| 722 |
state.allData = realData;
|
| 723 |
|
| 724 |
-
// 渲染首屏(先过滤再切片:tools/apps/recommends 的 keyword 是本地搜索,
|
| 725 |
// realData 是未过滤的全量数据,原实现在前 20 条里过滤会漏掉靠后的匹配项)
|
| 726 |
const scopedReal = _applySearchScope(realData);
|
| 727 |
renderBatch(scopedReal.slice(0, pageSize), false);
|
|
@@ -761,7 +761,7 @@ export async function loadSidebarContent({
|
|
| 761 |
if (hasCacheData && cachedData && !(tab === "creators" && keyword)) {
|
| 762 |
console.warn(`📴 网络失败,降级显示${isCacheExpired ? '过期' : ''}缓存`);
|
| 763 |
state.allData = cachedData;
|
| 764 |
-
// 🔧 先过滤再切片:本分支已排除 creators+keyword,但 tools/apps/recommends
|
| 765 |
// 带 keyword 时会进来;renderBatch 不再自行过滤,此处必须显式过滤
|
| 766 |
const scopedCached = _applySearchScope(cachedData);
|
| 767 |
renderBatch(scopedCached.slice(0, pageSize), false);
|
|
|
|
| 115 |
default: sorted.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); break; // time
|
| 116 |
}
|
| 117 |
} else {
|
| 118 |
+
// 工具/Skill/应用/推荐排序(与后端 get_items 一致)
|
| 119 |
switch (sort) {
|
| 120 |
case "downloads": sorted.sort((a, b) => (b.uses || 0) - (a.uses || 0)); break;
|
| 121 |
case "likes": sorted.sort((a, b) => (b.likes || 0) - (a.likes || 0)); break;
|
|
|
|
| 400 |
const cards = [];
|
| 401 |
let targetCard = null; // 用于保存需要展开的目标卡片
|
| 402 |
|
| 403 |
+
if (tab === "tools" || tab === "skills" || tab === "apps" || tab === "recommends") {
|
| 404 |
displayData.forEach(data => {
|
| 405 |
const card = createItemCard(data, currentUser);
|
| 406 |
cards.push(card);
|
|
|
|
| 523 |
|
| 524 |
try {
|
| 525 |
let newData;
|
| 526 |
+
if (savedTab === "tools" || savedTab === "skills" || savedTab === "apps" || savedTab === "recommends") {
|
| 527 |
+
const itemType = savedTab === "tools" ? "tool" : (savedTab === "skills" ? "skill" : (savedTab === "apps" ? "app" : "recommend"));
|
| 528 |
const response = await api.getItems(itemType, savedSort, 200);
|
| 529 |
newData = proxyImages(response.data || []);
|
| 530 |
} else if (savedTab === "creators") {
|
|
|
|
| 640 |
state.allData = sortedData;
|
| 641 |
state.isFullyLoaded = true;
|
| 642 |
state.isSearchResult = false;
|
| 643 |
+
// 🔧 先过滤再切片(tools/skills/apps/recommends 带 keyword 时同样会进入本分支)
|
| 644 |
const scopedInstant = _applySearchScope(sortedData);
|
| 645 |
renderBatch(scopedInstant.slice(0, pageSize), false);
|
| 646 |
if (scopedInstant.length > pageSize) {
|
|
|
|
| 661 |
try {
|
| 662 |
let response, realData;
|
| 663 |
|
| 664 |
+
if (tab === "tools" || tab === "skills" || tab === "apps" || tab === "recommends") {
|
| 665 |
+
const itemType = tab === "tools" ? "tool" : (tab === "skills" ? "skill" : (tab === "apps" ? "app" : "recommend"));
|
| 666 |
response = await api.getItems(itemType, sort, 200); // 获取较多数据
|
| 667 |
realData = response.data || [];
|
| 668 |
realData = proxyImages(realData); // 确保图片走本地缓存代理
|
|
|
|
| 721 |
// 更新状态
|
| 722 |
state.allData = realData;
|
| 723 |
|
| 724 |
+
// 渲染首屏(先过滤再切片:tools/skills/apps/recommends 的 keyword 是本地搜索,
|
| 725 |
// realData 是未过滤的全量数据,原实现在前 20 条里过滤会漏掉靠后的匹配项)
|
| 726 |
const scopedReal = _applySearchScope(realData);
|
| 727 |
renderBatch(scopedReal.slice(0, pageSize), false);
|
|
|
|
| 761 |
if (hasCacheData && cachedData && !(tab === "creators" && keyword)) {
|
| 762 |
console.warn(`📴 网络失败,降级显示${isCacheExpired ? '过期' : ''}缓存`);
|
| 763 |
state.allData = cachedData;
|
| 764 |
+
// 🔧 先过滤再切片:本分支已排除 creators+keyword,但 tools/skills/apps/recommends
|
| 765 |
// 带 keyword 时会进来;renderBatch 不再自行过滤,此处必须显式过滤
|
| 766 |
const scopedCached = _applySearchScope(cachedData);
|
| 767 |
renderBatch(scopedCached.slice(0, pageSize), false);
|
前端页面/core/类型定义.js
CHANGED
|
@@ -80,7 +80,7 @@
|
|
| 80 |
/**
|
| 81 |
* @typedef {Object} ItemData
|
| 82 |
* @property {string} id - 内容ID
|
| 83 |
-
* @property {string} type - 类型:tool/app/recommend
|
| 84 |
* @property {string} title - 标题
|
| 85 |
* @property {string} shortDesc - 简短描述
|
| 86 |
* @property {string} fullDesc - 详细描述
|
|
|
|
| 80 |
/**
|
| 81 |
* @typedef {Object} ItemData
|
| 82 |
* @property {string} id - 内容ID
|
| 83 |
+
* @property {string} type - 类型:tool/skill/app/recommend
|
| 84 |
* @property {string} title - 标题
|
| 85 |
* @property {string} shortDesc - 简短描述
|
| 86 |
* @property {string} fullDesc - 详细描述
|
前端页面/market/列表卡片组件.js
CHANGED
|
@@ -193,8 +193,8 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
|
|
| 193 |
detailView.appendChild(originalBadge);
|
| 194 |
}
|
| 195 |
|
| 196 |
-
// 支持退款标识(仅工具和应用类型显示,且允许退款时)
|
| 197 |
-
if (itemData.allow_refund !== false && (itemData.type === 'tool' || itemData.type === 'app')) {
|
| 198 |
const refundBadge = document.createElement("div");
|
| 199 |
Object.assign(refundBadge.style, {
|
| 200 |
display: "inline-flex", alignItems: "center", gap: "6px",
|
|
|
|
| 193 |
detailView.appendChild(originalBadge);
|
| 194 |
}
|
| 195 |
|
| 196 |
+
// 支持退款标识(仅工具、Skill和应用类型显示,且允许退款时)
|
| 197 |
+
if (itemData.allow_refund !== false && (itemData.type === 'tool' || itemData.type === 'skill' || itemData.type === 'app')) {
|
| 198 |
const refundBadge = document.createElement("div");
|
| 199 |
Object.assign(refundBadge.style, {
|
| 200 |
display: "inline-flex", alignItems: "center", gap: "6px",
|
前端页面/market/创作者卡片组件.js
CHANGED
|
@@ -279,6 +279,7 @@ export function createCreatorCard(creatorData, currentUser = null) {
|
|
| 279 |
</div>
|
| 280 |
<div style="display: flex; gap: 15px; font-size: 12px; color: #ccc; justify-content: center; border-top: 1px solid var(--border-color, #333); padding-top: 8px;">
|
| 281 |
<span data-stat="toolsCount" style="color: #2196F3;">🛠️ ${t('creator.output.tools')}: <strong>${creatorData.toolsCount}</strong> ${t('creator.output.unit')}</span>
|
|
|
|
| 282 |
<span data-stat="appsCount" style="color: #9C27B0;">📦 ${t('creator.output.apps')}: <strong>${creatorData.appsCount}</strong> ${t('creator.output.unit')}</span>
|
| 283 |
</div>
|
| 284 |
</div>
|
|
@@ -413,7 +414,7 @@ export function createCreatorCard(creatorData, currentUser = null) {
|
|
| 413 |
chartDom.innerHTML = "";
|
| 414 |
chartInstance = echarts.init(chartDom, 'dark', { backgroundColor: 'transparent' });
|
| 415 |
|
| 416 |
-
const data = trendData || { months: [], tools: [], apps: [], recommends: [] };
|
| 417 |
|
| 418 |
chartInstance.setOption({
|
| 419 |
tooltip: { trigger: 'axis', textStyle: { fontSize: 11 } },
|
|
@@ -422,8 +423,8 @@ export function createCreatorCard(creatorData, currentUser = null) {
|
|
| 422 |
yAxis: { type: 'value', splitLine: { lineStyle: { color: '#333', type: 'dashed' } }, axisLabel: { color: '#888', fontSize: 10 }, minInterval: 1 },
|
| 423 |
series: [
|
| 424 |
{ name: t('creator.chart.series.tools'), type: 'line', data: data.tools, smooth: true, itemStyle: { color: '#4CAF50' }, lineStyle: { width: 2, type: 'dashed' } },
|
|
|
|
| 425 |
{ name: t('creator.chart.series.apps'), type: 'line', data: data.apps, smooth: true, itemStyle: { color: '#2196F3' }, lineStyle: { width: 2, type: 'dashed' } },
|
| 426 |
-
// 【核心修改】:添加第 3 根趋势折线 —— 推荐资源的点击与获取量
|
| 427 |
{ name: t('creator.chart.series.recommends'), type: 'line', data: data.recommends, smooth: true, itemStyle: { color: '#FF9800' }, lineStyle: { width: 2, type: 'dashed' } }
|
| 428 |
]
|
| 429 |
});
|
|
|
|
| 279 |
</div>
|
| 280 |
<div style="display: flex; gap: 15px; font-size: 12px; color: #ccc; justify-content: center; border-top: 1px solid var(--border-color, #333); padding-top: 8px;">
|
| 281 |
<span data-stat="toolsCount" style="color: #2196F3;">🛠️ ${t('creator.output.tools')}: <strong>${creatorData.toolsCount}</strong> ${t('creator.output.unit')}</span>
|
| 282 |
+
<span data-stat="skillsCount" style="color: #7C4DFF;">⚡ ${t('creator.output.skills')}: <strong>${creatorData.skillsCount || 0}</strong> ${t('creator.output.unit')}</span>
|
| 283 |
<span data-stat="appsCount" style="color: #9C27B0;">📦 ${t('creator.output.apps')}: <strong>${creatorData.appsCount}</strong> ${t('creator.output.unit')}</span>
|
| 284 |
</div>
|
| 285 |
</div>
|
|
|
|
| 414 |
chartDom.innerHTML = "";
|
| 415 |
chartInstance = echarts.init(chartDom, 'dark', { backgroundColor: 'transparent' });
|
| 416 |
|
| 417 |
+
const data = trendData || { months: [], tools: [], skills: [], apps: [], recommends: [] };
|
| 418 |
|
| 419 |
chartInstance.setOption({
|
| 420 |
tooltip: { trigger: 'axis', textStyle: { fontSize: 11 } },
|
|
|
|
| 423 |
yAxis: { type: 'value', splitLine: { lineStyle: { color: '#333', type: 'dashed' } }, axisLabel: { color: '#888', fontSize: 10 }, minInterval: 1 },
|
| 424 |
series: [
|
| 425 |
{ name: t('creator.chart.series.tools'), type: 'line', data: data.tools, smooth: true, itemStyle: { color: '#4CAF50' }, lineStyle: { width: 2, type: 'dashed' } },
|
| 426 |
+
{ name: t('creator.chart.series.skills'), type: 'line', data: data.skills || [], smooth: true, itemStyle: { color: '#7C4DFF' }, lineStyle: { width: 2, type: 'dashed' } },
|
| 427 |
{ name: t('creator.chart.series.apps'), type: 'line', data: data.apps, smooth: true, itemStyle: { color: '#2196F3' }, lineStyle: { width: 2, type: 'dashed' } },
|
|
|
|
| 428 |
{ name: t('creator.chart.series.recommends'), type: 'line', data: data.recommends, smooth: true, itemStyle: { color: '#FF9800' }, lineStyle: { width: 2, type: 'dashed' } }
|
| 429 |
]
|
| 430 |
});
|
前端页面/market/发布内容_UI模板.js
CHANGED
|
@@ -19,6 +19,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
|
|
| 19 |
<label style="display: block; margin-bottom: 5px;">${t('publish.main_category')} <span style="color: #F44336;">*</span></label>
|
| 20 |
<select id="pub-type" class="form-input">
|
| 21 |
<option value="tool">🧰 ${t('publish.type_tool')}</option>
|
|
|
|
| 22 |
<option value="app">📦 ${t('publish.type_app')}</option>
|
| 23 |
<option value="recommend">🌟 ${t('publish.type_recommend')}</option>
|
| 24 |
</select>
|
|
@@ -27,6 +28,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
|
|
| 27 |
<label style="display: block; margin-bottom: 5px;">${t('publish.recommend_form')} <span style="color: #F44336;">*</span></label>
|
| 28 |
<select id="pub-recommend-type" class="form-input">
|
| 29 |
<option value="recommend_tool">🧰 ${t('publish.recommend_as_tool')}</option>
|
|
|
|
| 30 |
<option value="recommend_app">📦 ${t('publish.recommend_as_app')}</option>
|
| 31 |
<option value="recommend_link">🔗 ${t('publish.recommend_as_link')}</option>
|
| 32 |
</select>
|
|
|
|
| 19 |
<label style="display: block; margin-bottom: 5px;">${t('publish.main_category')} <span style="color: #F44336;">*</span></label>
|
| 20 |
<select id="pub-type" class="form-input">
|
| 21 |
<option value="tool">🧰 ${t('publish.type_tool')}</option>
|
| 22 |
+
<option value="skill">⚡ ${t('publish.type_skill')}</option>
|
| 23 |
<option value="app">📦 ${t('publish.type_app')}</option>
|
| 24 |
<option value="recommend">🌟 ${t('publish.type_recommend')}</option>
|
| 25 |
</select>
|
|
|
|
| 28 |
<label style="display: block; margin-bottom: 5px;">${t('publish.recommend_form')} <span style="color: #F44336;">*</span></label>
|
| 29 |
<select id="pub-recommend-type" class="form-input">
|
| 30 |
<option value="recommend_tool">🧰 ${t('publish.recommend_as_tool')}</option>
|
| 31 |
+
<option value="recommend_skill">⚡ ${t('publish.recommend_as_skill')}</option>
|
| 32 |
<option value="recommend_app">📦 ${t('publish.recommend_as_app')}</option>
|
| 33 |
<option value="recommend_link">🔗 ${t('publish.recommend_as_link')}</option>
|
| 34 |
</select>
|
前端页面/market/发布内容_提交引擎.js
CHANGED
|
@@ -3,11 +3,11 @@ import { api } from "../core/网络请求API.js";
|
|
| 3 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 4 |
import { t } from "../components/用户体验增强.js";
|
| 5 |
import { removeCache, removeCacheByPrefix } from "../components/性能优化工具.js";
|
| 6 |
-
import { TYPE_RULES } from "./发布内容组件.js"; // 📋
|
| 7 |
|
| 8 |
/**
|
| 9 |
* 📋 获取指定主类型的提交规则(校验与字段组装统一读取,与 UI 显隐同源)
|
| 10 |
-
* @param {string} mainType - 主类型: tool/app/recommend
|
| 11 |
*/
|
| 12 |
function getSubmitRules(mainType) {
|
| 13 |
return TYPE_RULES[mainType] || TYPE_RULES.tool;
|
|
@@ -15,13 +15,15 @@ function getSubmitRules(mainType) {
|
|
| 15 |
|
| 16 |
/**
|
| 17 |
* 🟢 根据发布类型清除对应缓存(精确清除,不影响其他页面)
|
| 18 |
-
* @param {string} type - 发布类型: tool/app/recommend/recommend_tool/recommend_app/recommend_link
|
| 19 |
*/
|
| 20 |
function clearItemCacheByType(type) {
|
| 21 |
// 确定当前类型对应的 tab
|
| 22 |
let tab = 'recommends';
|
| 23 |
if (type === 'tool' || type === 'recommend_tool') {
|
| 24 |
tab = 'tools';
|
|
|
|
|
|
|
| 25 |
} else if (type === 'app' || type === 'recommend_app') {
|
| 26 |
tab = 'apps';
|
| 27 |
}
|
|
@@ -298,7 +300,7 @@ export async function handlePublishSubmit(params) {
|
|
| 298 |
|
| 299 |
if (!title || !shortDesc) return showToast(t('publish.name_desc_required'), "warning");
|
| 300 |
if (type === "recommend_link" && !finalLink) return showToast(t('publish.link_required'), "warning");
|
| 301 |
-
if ((type === "tool" || type === "recommend_tool") && !isJsonUpload && !isNetdisk && !finalLink) return showToast(t('publish.git_required'), "warning");
|
| 302 |
if (isJsonUpload && !jsonFile && !finalLink) return showToast(t('publish.json_required'), "warning");
|
| 303 |
if (isNetdisk && !finalLink) return showToast(t('publish.netdisk_required'), "warning"); // ☁️
|
| 304 |
|
|
@@ -320,7 +322,7 @@ export async function handlePublishSubmit(params) {
|
|
| 320 |
try {
|
| 321 |
if (isJsonUpload && jsonFile) {
|
| 322 |
submitBtn.innerHTML = `⏳ ${t('publish.uploading_secure')}`;
|
| 323 |
-
const uploadType = type.includes("app") ? "app" : (type.includes("tool") ? "tool" : "recommend");
|
| 324 |
const jsonUploadRes = await api.uploadFile(jsonFile, uploadType);
|
| 325 |
finalLink = jsonUploadRes.url;
|
| 326 |
}
|
|
@@ -362,7 +364,7 @@ export async function handlePublishSubmit(params) {
|
|
| 362 |
is_original: isOriginal // 🎨 标记为原创作品
|
| 363 |
};
|
| 364 |
|
| 365 |
-
// 💸 仅 tool/app 类型提交 allow_refund 字段
|
| 366 |
if (allowRefund !== null) {
|
| 367 |
submitData.allow_refund = allowRefund;
|
| 368 |
}
|
|
|
|
| 3 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 4 |
import { t } from "../components/用户体验增强.js";
|
| 5 |
import { removeCache, removeCacheByPrefix } from "../components/性能优化工具.js";
|
| 6 |
+
import { TYPE_RULES } from "./发布内容组件.js"; // 📋 四榜发布表单声明式规则(与 UI 联动同源)
|
| 7 |
|
| 8 |
/**
|
| 9 |
* 📋 获取指定主类型的提交规则(校验与字段组装统一读取,与 UI 显隐同源)
|
| 10 |
+
* @param {string} mainType - 主类型: tool/skill/app/recommend
|
| 11 |
*/
|
| 12 |
function getSubmitRules(mainType) {
|
| 13 |
return TYPE_RULES[mainType] || TYPE_RULES.tool;
|
|
|
|
| 15 |
|
| 16 |
/**
|
| 17 |
* 🟢 根据发布类型清除对应缓存(精确清除,不影响其他页面)
|
| 18 |
+
* @param {string} type - 发布类型: tool/skill/app/recommend/recommend_tool/recommend_skill/recommend_app/recommend_link
|
| 19 |
*/
|
| 20 |
function clearItemCacheByType(type) {
|
| 21 |
// 确定当前类型对应的 tab
|
| 22 |
let tab = 'recommends';
|
| 23 |
if (type === 'tool' || type === 'recommend_tool') {
|
| 24 |
tab = 'tools';
|
| 25 |
+
} else if (type === 'skill' || type === 'recommend_skill') {
|
| 26 |
+
tab = 'skills';
|
| 27 |
} else if (type === 'app' || type === 'recommend_app') {
|
| 28 |
tab = 'apps';
|
| 29 |
}
|
|
|
|
| 300 |
|
| 301 |
if (!title || !shortDesc) return showToast(t('publish.name_desc_required'), "warning");
|
| 302 |
if (type === "recommend_link" && !finalLink) return showToast(t('publish.link_required'), "warning");
|
| 303 |
+
if ((type === "tool" || type === "skill" || type === "recommend_tool" || type === "recommend_skill") && !isJsonUpload && !isNetdisk && !finalLink) return showToast(t('publish.git_required'), "warning");
|
| 304 |
if (isJsonUpload && !jsonFile && !finalLink) return showToast(t('publish.json_required'), "warning");
|
| 305 |
if (isNetdisk && !finalLink) return showToast(t('publish.netdisk_required'), "warning"); // ☁️
|
| 306 |
|
|
|
|
| 322 |
try {
|
| 323 |
if (isJsonUpload && jsonFile) {
|
| 324 |
submitBtn.innerHTML = `⏳ ${t('publish.uploading_secure')}`;
|
| 325 |
+
const uploadType = type.includes("app") ? "app" : (type.includes("tool") || type === "skill" ? "tool" : "recommend");
|
| 326 |
const jsonUploadRes = await api.uploadFile(jsonFile, uploadType);
|
| 327 |
finalLink = jsonUploadRes.url;
|
| 328 |
}
|
|
|
|
| 364 |
is_original: isOriginal // 🎨 标记为原创作品
|
| 365 |
};
|
| 366 |
|
| 367 |
+
// 💸 仅 tool/skill/app 类型提交 allow_refund 字段
|
| 368 |
if (allowRefund !== null) {
|
| 369 |
submitData.allow_refund = allowRefund;
|
| 370 |
}
|
前端页面/market/发布内容组件.js
CHANGED
|
@@ -10,13 +10,17 @@ import { escapeHtml } from "../core/全局配置.js";
|
|
| 10 |
// 📌 模块级常量
|
| 11 |
// ==========================================
|
| 12 |
|
| 13 |
-
// 📋
|
| 14 |
// UI 显隐、联动、校验统一读取此配置,避免类型分支散落多处
|
| 15 |
export const TYPE_RULES = {
|
| 16 |
tool: {
|
| 17 |
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 18 |
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 19 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
app: {
|
| 21 |
recommendForm: false, resType: { fixed: 'json' },
|
| 22 |
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
|
@@ -24,7 +28,7 @@ export const TYPE_RULES = {
|
|
| 24 |
recommend: {
|
| 25 |
recommendForm: true, resType: { lockedByForm: true },
|
| 26 |
privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
|
| 27 |
-
formToResType: { recommend_tool: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
|
| 28 |
}
|
| 29 |
};
|
| 30 |
|
|
@@ -305,8 +309,8 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
|
|
| 305 |
inputNetdiskPassword.value = editItemData.netdisk_password;
|
| 306 |
}
|
| 307 |
} else if (editItemData.link && editItemData.link.includes("huggingface.co")) {
|
| 308 |
-
if (editItemData.type === "tool") {
|
| 309 |
-
// 🔧 缺陷修复:tool 类型禁用 json 资源,HF 云端文件链接直接按外部链接回显,
|
| 310 |
// 否则联动回退 link 后链接输入框为空,保存必然校验失败
|
| 311 |
resTypeSelect.value = "link";
|
| 312 |
inputLink.value = editItemData.link;
|
|
@@ -432,6 +436,8 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
|
|
| 432 |
if (!isEditMode && initialType) {
|
| 433 |
if (initialType === "tool" || initialType === "tools") {
|
| 434 |
typeSelect.value = "tool";
|
|
|
|
|
|
|
| 435 |
} else if (initialType === "app" || initialType === "apps") {
|
| 436 |
typeSelect.value = "app";
|
| 437 |
} else if (initialType === "recommend" || initialType === "recommends") {
|
|
|
|
| 10 |
// 📌 模块级常量
|
| 11 |
// ==========================================
|
| 12 |
|
| 13 |
+
// 📋 四榜发布表单声明式规则:tool 插件榜 / skill Skill榜 / app 工作流 / recommend 推荐榜
|
| 14 |
// UI 显隐、联动、校验统一读取此配置,避免类型分支散落多处
|
| 15 |
export const TYPE_RULES = {
|
| 16 |
tool: {
|
| 17 |
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 18 |
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 19 |
},
|
| 20 |
+
skill: {
|
| 21 |
+
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 22 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 23 |
+
},
|
| 24 |
app: {
|
| 25 |
recommendForm: false, resType: { fixed: 'json' },
|
| 26 |
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
|
|
|
| 28 |
recommend: {
|
| 29 |
recommendForm: true, resType: { lockedByForm: true },
|
| 30 |
privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
|
| 31 |
+
formToResType: { recommend_tool: 'link', recommend_skill: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
|
| 32 |
}
|
| 33 |
};
|
| 34 |
|
|
|
|
| 309 |
inputNetdiskPassword.value = editItemData.netdisk_password;
|
| 310 |
}
|
| 311 |
} else if (editItemData.link && editItemData.link.includes("huggingface.co")) {
|
| 312 |
+
if (editItemData.type === "tool" || editItemData.type === "skill") {
|
| 313 |
+
// 🔧 缺陷修复:tool/skill 类型禁用 json 资源,HF 云端文件链接直接按外部链接回显,
|
| 314 |
// 否则联动回退 link 后链接输入框为空,保存必然校验失败
|
| 315 |
resTypeSelect.value = "link";
|
| 316 |
inputLink.value = editItemData.link;
|
|
|
|
| 436 |
if (!isEditMode && initialType) {
|
| 437 |
if (initialType === "tool" || initialType === "tools") {
|
| 438 |
typeSelect.value = "tool";
|
| 439 |
+
} else if (initialType === "skill" || initialType === "skills") {
|
| 440 |
+
typeSelect.value = "skill";
|
| 441 |
} else if (initialType === "app" || initialType === "apps") {
|
| 442 |
typeSelect.value = "app";
|
| 443 |
} else if (initialType === "recommend" || initialType === "recommends") {
|
前端页面/market/资源安装引擎.js
CHANGED
|
@@ -32,7 +32,7 @@ function clearUsesCache() {
|
|
| 32 |
// 安装后使用量/下载数仍会从旧缓存返回,改用前缀删除
|
| 33 |
removeCacheByPrefix('api_/api/items');
|
| 34 |
removeCacheByPrefix('api_/api/creators');
|
| 35 |
-
const tabs = ['tools', 'apps', 'recommends', 'creators'];
|
| 36 |
const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
|
| 37 |
for (const tab of tabs) {
|
| 38 |
for (const sort of sorts) {
|
|
@@ -197,7 +197,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 197 |
if (!currentUser) return showToast("⚠️ 请先登录您的社区账号后再获取!", "warning");
|
| 198 |
|
| 199 |
const isFree = !itemData.price || itemData.price <= 0 || currentUser.account === itemData.author;
|
| 200 |
-
const isTool = itemData.type === 'tool' || itemData.type === 'recommend_tool';
|
| 201 |
const isApp = itemData.type === 'app' || itemData.type === 'recommend_app';
|
| 202 |
|
| 203 |
// 📱 Web 模式:不支持下载/安装。免费(含作者本人)或已购买 → 仅提醒电脑端安装;未购买 → 放行到下方纯购买流程
|
|
@@ -417,8 +417,11 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 417 |
}
|
| 418 |
|
| 419 |
// 🚀 安装成功后,盖上本地版本戳
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
| 422 |
}
|
| 423 |
|
| 424 |
// 记录使用量(后端自动去重)
|
|
@@ -474,8 +477,9 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 474 |
showToast(`🎉 插件 [${itemData.title}] 安装成功!请重启 ComfyUI。`, "success");
|
| 475 |
}
|
| 476 |
|
| 477 |
-
|
| 478 |
-
|
|
|
|
| 479 |
}
|
| 480 |
|
| 481 |
try {
|
|
|
|
| 32 |
// 安装后使用量/下载数仍会从旧缓存返回,改用前缀删除
|
| 33 |
removeCacheByPrefix('api_/api/items');
|
| 34 |
removeCacheByPrefix('api_/api/creators');
|
| 35 |
+
const tabs = ['tools', 'skills', 'apps', 'recommends', 'creators'];
|
| 36 |
const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
|
| 37 |
for (const tab of tabs) {
|
| 38 |
for (const sort of sorts) {
|
|
|
|
| 197 |
if (!currentUser) return showToast("⚠️ 请先登录您的社区账号后再获取!", "warning");
|
| 198 |
|
| 199 |
const isFree = !itemData.price || itemData.price <= 0 || currentUser.account === itemData.author;
|
| 200 |
+
const isTool = itemData.type === 'tool' || itemData.type === 'recommend_tool' || itemData.type === 'skill' || itemData.type === 'recommend_skill';
|
| 201 |
const isApp = itemData.type === 'app' || itemData.type === 'recommend_app';
|
| 202 |
|
| 203 |
// 📱 Web 模式:不支持下载/安装。免费(含作者本人)或已购买 → 仅提醒电脑端安装;未购买 → 放行到下方纯购买流程
|
|
|
|
| 417 |
}
|
| 418 |
|
| 419 |
// 🚀 安装成功后,盖上本地版本戳
|
| 420 |
+
// 优先用本地引擎回传的真实安装 commit(installed_version),兜底云端 latest_version;
|
| 421 |
+
// 二者皆空才跳过——避免版本戳缺失导致卡片更新徽章永不触发
|
| 422 |
+
const _installedVer = result.installed_version || itemData.latest_version;
|
| 423 |
+
if (_installedVer) {
|
| 424 |
+
localStorage.setItem(`ComfyCommunity_LocalVer_${itemData.id}`, _installedVer);
|
| 425 |
}
|
| 426 |
|
| 427 |
// 记录使用量(后端自动去重)
|
|
|
|
| 477 |
showToast(`🎉 插件 [${itemData.title}] 安装成功!请重启 ComfyUI。`, "success");
|
| 478 |
}
|
| 479 |
|
| 480 |
+
const _installedVer = data.installed_version || itemData.latest_version;
|
| 481 |
+
if (_installedVer) {
|
| 482 |
+
localStorage.setItem(`ComfyCommunity_LocalVer_${itemData.id}`, _installedVer);
|
| 483 |
}
|
| 484 |
|
| 485 |
try {
|
前端页面/market/资源详情页面组件.js
CHANGED
|
@@ -217,8 +217,8 @@ function burnLocalFiles(itemId) {
|
|
| 217 |
removeCacheByPrefix('api_/api/items');
|
| 218 |
removeCacheByPrefix('api_/api/creators');
|
| 219 |
|
| 220 |
-
// 清除工具/应用/推荐列表缓存(使用标准 ListCache 格式)
|
| 221 |
-
const tabs = ['tools', 'apps', 'recommends'];
|
| 222 |
const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
|
| 223 |
for (const tab of tabs) {
|
| 224 |
for (const sort of sorts) {
|
|
|
|
| 217 |
removeCacheByPrefix('api_/api/items');
|
| 218 |
removeCacheByPrefix('api_/api/creators');
|
| 219 |
|
| 220 |
+
// 清除工具/Skill/应用/推荐列表缓存(使用标准 ListCache 格式)
|
| 221 |
+
const tabs = ['tools', 'skills', 'apps', 'recommends'];
|
| 222 |
const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
|
| 223 |
for (const tab of tabs) {
|
| 224 |
for (const sort of sorts) {
|
前端页面/profile/个人列表组件.js
CHANGED
|
@@ -191,14 +191,15 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
|
|
| 191 |
const timeoutMs = 5000;
|
| 192 |
const fetchPromise = Promise.all([
|
| 193 |
api.getItems("tool", "time", 200),
|
|
|
|
| 194 |
api.getItems("app", "time", 200)
|
| 195 |
]);
|
| 196 |
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs));
|
| 197 |
const result = await Promise.race([fetchPromise, timeoutPromise]);
|
| 198 |
|
| 199 |
if (result) {
|
| 200 |
-
const [toolsRes, appsRes] = result;
|
| 201 |
-
cloudItems = [...(toolsRes?.data || []), ...(appsRes?.data || [])];
|
| 202 |
}
|
| 203 |
|
| 204 |
const cloudMap = {};
|
|
@@ -511,13 +512,14 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
|
|
| 511 |
}
|
| 512 |
|
| 513 |
try {
|
| 514 |
-
const [toolsRes, appsRes, postsRes, promptsRes] = await Promise.all([
|
| 515 |
api.getItems("tool", "time", 100),
|
|
|
|
| 516 |
api.getItems("app", "time", 100),
|
| 517 |
api.getPosts(1, 200, "latest"),
|
| 518 |
api.getPrompts(null, null, 1, 200, "latest")
|
| 519 |
]);
|
| 520 |
-
const allItems = [...(toolsRes.data || []), ...(appsRes.data || [])];
|
| 521 |
const allPosts = postsRes.data || [];
|
| 522 |
const allPrompts = promptsRes.data || [];
|
| 523 |
const collectedItems = allItems.filter(item => item.favorited_by && item.favorited_by.includes(userData.account));
|
|
@@ -553,8 +555,8 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
|
|
| 553 |
}
|
| 554 |
|
| 555 |
try {
|
| 556 |
-
const [toolsRes, appsRes] = await Promise.all([ api.getItems("tool", "time", 100), api.getItems("app", "time", 100) ]);
|
| 557 |
-
const allItems = [...(toolsRes.data || []), ...(appsRes.data || [])];
|
| 558 |
let filteredItems = [];
|
| 559 |
|
| 560 |
if (tabId === "published") filteredItems = allItems.filter(item => item.author === userData.account);
|
|
|
|
| 191 |
const timeoutMs = 5000;
|
| 192 |
const fetchPromise = Promise.all([
|
| 193 |
api.getItems("tool", "time", 200),
|
| 194 |
+
api.getItems("skill", "time", 200),
|
| 195 |
api.getItems("app", "time", 200)
|
| 196 |
]);
|
| 197 |
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs));
|
| 198 |
const result = await Promise.race([fetchPromise, timeoutPromise]);
|
| 199 |
|
| 200 |
if (result) {
|
| 201 |
+
const [toolsRes, skillsRes, appsRes] = result;
|
| 202 |
+
cloudItems = [...(toolsRes?.data || []), ...(skillsRes?.data || []), ...(appsRes?.data || [])];
|
| 203 |
}
|
| 204 |
|
| 205 |
const cloudMap = {};
|
|
|
|
| 512 |
}
|
| 513 |
|
| 514 |
try {
|
| 515 |
+
const [toolsRes, skillsRes, appsRes, postsRes, promptsRes] = await Promise.all([
|
| 516 |
api.getItems("tool", "time", 100),
|
| 517 |
+
api.getItems("skill", "time", 100),
|
| 518 |
api.getItems("app", "time", 100),
|
| 519 |
api.getPosts(1, 200, "latest"),
|
| 520 |
api.getPrompts(null, null, 1, 200, "latest")
|
| 521 |
]);
|
| 522 |
+
const allItems = [...(toolsRes.data || []), ...(skillsRes.data || []), ...(appsRes.data || [])];
|
| 523 |
const allPosts = postsRes.data || [];
|
| 524 |
const allPrompts = promptsRes.data || [];
|
| 525 |
const collectedItems = allItems.filter(item => item.favorited_by && item.favorited_by.includes(userData.account));
|
|
|
|
| 555 |
}
|
| 556 |
|
| 557 |
try {
|
| 558 |
+
const [toolsRes, skillsRes, appsRes] = await Promise.all([ api.getItems("tool", "time", 100), api.getItems("skill", "time", 100), api.getItems("app", "time", 100) ]);
|
| 559 |
+
const allItems = [...(toolsRes.data || []), ...(skillsRes.data || []), ...(appsRes.data || [])];
|
| 560 |
let filteredItems = [];
|
| 561 |
|
| 562 |
if (tabId === "published") filteredItems = allItems.filter(item => item.author === userData.account);
|
前端页面/social/私信聊天组件.js
CHANGED
|
@@ -70,6 +70,9 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 70 |
const cleanup = () => {
|
| 71 |
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
| 72 |
saveDraft();
|
|
|
|
|
|
|
|
|
|
| 73 |
};
|
| 74 |
|
| 75 |
topHeader.querySelector("#btn-back-chat").onclick = () => {
|
|
@@ -500,8 +503,11 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 500 |
if (!CACHE.MESSAGE_POLL || !CACHE.MESSAGE_POLL.ENABLED) return;
|
| 501 |
pollTimer = setInterval(async () => {
|
| 502 |
try {
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
| 505 |
loadChatList();
|
| 506 |
// 如果当前有打开的对话,也刷新消息
|
| 507 |
if (currentChatTarget) {
|
|
@@ -550,4 +556,29 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 550 |
}, 0);
|
| 551 |
|
| 552 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view: container } }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
}
|
|
|
|
| 70 |
const cleanup = () => {
|
| 71 |
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
| 72 |
saveDraft();
|
| 73 |
+
// 🔔 关闭聊天时通知顶部导航刷新 ✉️ 未读红点:
|
| 74 |
+
// 打开会话已把收到的消息标记为已读,未读数随之下降,需即时回写红点
|
| 75 |
+
window.dispatchEvent(new CustomEvent("comfy-chat-unread-changed"));
|
| 76 |
};
|
| 77 |
|
| 78 |
topHeader.querySelector("#btn-back-chat").onclick = () => {
|
|
|
|
| 503 |
if (!CACHE.MESSAGE_POLL || !CACHE.MESSAGE_POLL.ENABLED) return;
|
| 504 |
pollTimer = setInterval(async () => {
|
| 505 |
try {
|
| 506 |
+
// 🔔 私信未读以会话列表为准:铃铛的 getUnreadCount(count_only) 已不再计入 private,
|
| 507 |
+
// 聊天内轮询改用 getChatList 汇总各会话未读,语义正确且不受铃铛过滤影响
|
| 508 |
+
const res = await api.getChatList(currentUser.account);
|
| 509 |
+
const list = res.data || [];
|
| 510 |
+
if (list.some(c => (c.unread_count || 0) > 0)) {
|
| 511 |
loadChatList();
|
| 512 |
// 如果当前有打开的对话,也刷新消息
|
| 513 |
if (currentChatTarget) {
|
|
|
|
| 556 |
}, 0);
|
| 557 |
|
| 558 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view: container } }));
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
// 🔔 聊天未读红点:汇总 chats.json 各会话未读数,驱动顶部导航 ✉️ 按钮的红点。
|
| 562 |
+
// 私信提醒已从铃铛(🔔)迁移至此——铃铛未读数不再计入 private,避免同一提醒重复出现。
|
| 563 |
+
// 与 loadUnreadCount(通知中心) 保持一致的写法:从传入按钮内查询徽标元素并回写。
|
| 564 |
+
export async function loadChatUnreadCount(currentUser, chatBtn) {
|
| 565 |
+
const badge = chatBtn ? chatBtn.querySelector("#chat-unread-badge") : null;
|
| 566 |
+
if (!currentUser) { if (badge) badge.style.display = "none"; return 0; }
|
| 567 |
+
try {
|
| 568 |
+
const res = await api.getChatList(currentUser.account);
|
| 569 |
+
const list = res.data || [];
|
| 570 |
+
const total = list.reduce((sum, c) => sum + (c.unread_count || 0), 0);
|
| 571 |
+
if (badge) {
|
| 572 |
+
if (total > 0) {
|
| 573 |
+
badge.innerText = total > 99 ? "99+" : total;
|
| 574 |
+
badge.style.display = "flex";
|
| 575 |
+
} else {
|
| 576 |
+
badge.style.display = "none";
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
return total;
|
| 580 |
+
} catch(e) {
|
| 581 |
+
if (badge) badge.style.display = "none";
|
| 582 |
+
return 0;
|
| 583 |
+
}
|
| 584 |
}
|
前端页面/social/通知中心组件.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
// 前端页面/social/通知中心组件.js
|
| 2 |
import { api } from "../core/网络请求API.js";
|
| 3 |
-
import { request } from "../core/网络请求_基础设施.js";
|
| 4 |
-
import { openChatModal } from "./私信聊天组件.js";
|
| 5 |
import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
|
| 6 |
import { showToast, showConfirm } from "../components/UI交互提示组件.js";
|
| 7 |
import { CACHE, PLACEHOLDERS } from "../core/全局配置.js";
|
|
@@ -89,37 +87,35 @@ export async function openNotificationCenter(currentUser, bellBtn) {
|
|
| 89 |
|
| 90 |
const renderMsgList = (msgs) => {
|
| 91 |
listArea.innerHTML = "";
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
| 93 |
listArea.innerHTML = `<div style='text-align:center; padding: 30px; color:#666;'>${t('notif.no_messages')}</div>`;
|
| 94 |
return;
|
| 95 |
}
|
| 96 |
|
| 97 |
let html = "";
|
| 98 |
-
|
| 99 |
const isUnread = !msg.is_read;
|
| 100 |
// 【核心新增】:判断是否为系统公告
|
| 101 |
const isSystem = msg.type === "system";
|
| 102 |
const isAnonymous = msg.from_user === "anonymous";
|
| 103 |
|
| 104 |
// 系统公告使用尊贵的橙色/金色 UI 边框,普通消息使用默认颜色
|
| 105 |
-
// 插件更新通知使用蓝色主题
|
| 106 |
let bg = isUnread ? "rgba(76, 175, 80, 0.1)" : "var(--comfy-input-bg)";
|
| 107 |
let border = isUnread ? "1px solid #4CAF50" : "1px solid #444";
|
| 108 |
|
| 109 |
if (isSystem) {
|
| 110 |
bg = isUnread ? "rgba(255, 152, 0, 0.15)" : "rgba(255, 152, 0, 0.05)";
|
| 111 |
border = isUnread ? "1px solid #FF9800" : "1px solid #886020";
|
| 112 |
-
} else if (msg.type === "plugin_update") {
|
| 113 |
-
bg = isUnread ? "rgba(33, 150, 243, 0.15)" : "rgba(33, 150, 243, 0.05)";
|
| 114 |
-
border = isUnread ? "1px solid #2196F3" : "1px solid #1a5a8a";
|
| 115 |
}
|
| 116 |
|
| 117 |
// 生成标题链接
|
| 118 |
const titleLink = makeTitleLink(msg.target_item_id, msg.target_item_title);
|
| 119 |
|
| 120 |
let actionText = "";
|
| 121 |
-
if (msg.type === "
|
| 122 |
-
else if (msg.type === "follow") actionText = t('notif.followed_you');
|
| 123 |
else if (msg.type === "like") actionText = `${t('notif.liked')} ${titleLink}`;
|
| 124 |
else if (msg.type === "favorite") actionText = `${t('notif.favorited')} ${titleLink}`;
|
| 125 |
else if (msg.type === "comment") actionText = `${t('notif.commented_on')} ${titleLink}:<br><span style="color:#ccc;">${escapeHtml(msg.content)}</span>`;
|
|
@@ -139,19 +135,14 @@ export async function openNotificationCenter(currentUser, bellBtn) {
|
|
| 139 |
else if (msg.type === "dispute_resolved") actionText = `<span style="color:#9C27B0;">🔨</span> ${escapeHtml(msg.content) || t('notif.dispute_resolved')}`;
|
| 140 |
// 【核心新增】:系统公告的正文排版,保留原格式的换行
|
| 141 |
else if (isSystem) actionText = `<div style="margin-top: 6px; color: #eee; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">${escapeHtml(msg.content)}</div>`;
|
| 142 |
-
// 【核心新增】:插件更新通知 - 保持现有模板不变
|
| 143 |
-
else if (msg.type === "plugin_update") actionText = `${t('notif.plugin_update')} <span style="color:#2196F3;">[${escapeHtml(msg.target_item_title)}]</span>,${t('notif.click_to_view')}`;
|
| 144 |
|
| 145 |
const timeStr = new Date(msg.created_at * 1000).toLocaleString();
|
| 146 |
|
| 147 |
// 【核心新增】:如果是系统消息,增加 📢 标签并改变标题颜色
|
| 148 |
-
// 插件更新通知使用 📦 图标
|
| 149 |
// 匿名打赏和系统通知的用户名不显示为链接
|
| 150 |
let nameLabel;
|
| 151 |
if (isSystem) {
|
| 152 |
nameLabel = `<strong style="color: #FF9800; font-size: 15px;">📢 [${t('notif.system_announcement')}] ${escapeHtml(msg.from_name)}</strong>`;
|
| 153 |
-
} else if (msg.type === "plugin_update") {
|
| 154 |
-
nameLabel = `<strong style="color: #2196F3; font-size: 15px;">📦 [${t('notif.plugin_update_title')}] ${escapeHtml(msg.from_name)}</strong>`;
|
| 155 |
} else if (isAnonymous) {
|
| 156 |
nameLabel = `<strong style="color: #fff;">${escapeHtml(msg.from_name || msg.from_user)}</strong>`;
|
| 157 |
} else {
|
|
@@ -215,12 +206,6 @@ export async function openNotificationCenter(currentUser, bellBtn) {
|
|
| 215 |
// 【核心新增】:系统公告点击不跳转,只触发消除红点
|
| 216 |
if (type === "system") return;
|
| 217 |
|
| 218 |
-
// 📦 插件更新通知:切换到对应Tab并展开卡片
|
| 219 |
-
if (type === "plugin_update" && itemId) {
|
| 220 |
-
_navigateToItemView(itemId);
|
| 221 |
-
return;
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
// 📝 任务榜通知:跳转到任务详情
|
| 225 |
if (type.startsWith("task_") && itemId) {
|
| 226 |
_navigateToTaskDetail(itemId, currentUser);
|
|
@@ -246,12 +231,6 @@ export async function openNotificationCenter(currentUser, bellBtn) {
|
|
| 246 |
return;
|
| 247 |
}
|
| 248 |
|
| 249 |
-
// 私信:打开对话
|
| 250 |
-
if (type === "private") {
|
| 251 |
-
openChatModal(currentUser, acc);
|
| 252 |
-
return;
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
// 其他:跳转发送者资料页(兜底)
|
| 256 |
if (acc && acc !== 'anonymous') openOtherUserProfileModal(acc, currentUser);
|
| 257 |
};
|
|
@@ -304,8 +283,8 @@ export async function openNotificationCenter(currentUser, bellBtn) {
|
|
| 304 |
export async function loadUnreadCount(currentUser, bellBtn) {
|
| 305 |
if (!currentUser) { bellBtn.querySelector("#unread-badge").style.display = "none"; return; }
|
| 306 |
try {
|
| 307 |
-
// 🔥 修复:
|
| 308 |
-
const res = await
|
| 309 |
const unreadCount = res.unread_count || 0;
|
| 310 |
const badge = bellBtn.querySelector("#unread-badge");
|
| 311 |
if (unreadCount > 0) {
|
|
|
|
| 1 |
// 前端页面/social/通知中心组件.js
|
| 2 |
import { api } from "../core/网络请求API.js";
|
|
|
|
|
|
|
| 3 |
import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
|
| 4 |
import { showToast, showConfirm } from "../components/UI交互提示组件.js";
|
| 5 |
import { CACHE, PLACEHOLDERS } from "../core/全局配置.js";
|
|
|
|
| 87 |
|
| 88 |
const renderMsgList = (msgs) => {
|
| 89 |
listArea.innerHTML = "";
|
| 90 |
+
// 🔔 私信提醒已迁移至聊天入口(✉️红点)、插件更新提醒已迁移至卡片徽章(🔄),
|
| 91 |
+
// 通知中心不再展示这两类(含存量历史数据),避免同一提醒重复出现
|
| 92 |
+
const visibleMsgs = msgs.filter(m => m.type !== "private" && m.type !== "plugin_update");
|
| 93 |
+
if (visibleMsgs.length === 0) {
|
| 94 |
listArea.innerHTML = `<div style='text-align:center; padding: 30px; color:#666;'>${t('notif.no_messages')}</div>`;
|
| 95 |
return;
|
| 96 |
}
|
| 97 |
|
| 98 |
let html = "";
|
| 99 |
+
visibleMsgs.forEach(msg => {
|
| 100 |
const isUnread = !msg.is_read;
|
| 101 |
// 【核心新增】:判断是否为系统公告
|
| 102 |
const isSystem = msg.type === "system";
|
| 103 |
const isAnonymous = msg.from_user === "anonymous";
|
| 104 |
|
| 105 |
// 系统公告使用尊贵的橙色/金色 UI 边框,普通消息使用默认颜色
|
|
|
|
| 106 |
let bg = isUnread ? "rgba(76, 175, 80, 0.1)" : "var(--comfy-input-bg)";
|
| 107 |
let border = isUnread ? "1px solid #4CAF50" : "1px solid #444";
|
| 108 |
|
| 109 |
if (isSystem) {
|
| 110 |
bg = isUnread ? "rgba(255, 152, 0, 0.15)" : "rgba(255, 152, 0, 0.05)";
|
| 111 |
border = isUnread ? "1px solid #FF9800" : "1px solid #886020";
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
// 生成标题链接
|
| 115 |
const titleLink = makeTitleLink(msg.target_item_id, msg.target_item_title);
|
| 116 |
|
| 117 |
let actionText = "";
|
| 118 |
+
if (msg.type === "follow") actionText = t('notif.followed_you');
|
|
|
|
| 119 |
else if (msg.type === "like") actionText = `${t('notif.liked')} ${titleLink}`;
|
| 120 |
else if (msg.type === "favorite") actionText = `${t('notif.favorited')} ${titleLink}`;
|
| 121 |
else if (msg.type === "comment") actionText = `${t('notif.commented_on')} ${titleLink}:<br><span style="color:#ccc;">${escapeHtml(msg.content)}</span>`;
|
|
|
|
| 135 |
else if (msg.type === "dispute_resolved") actionText = `<span style="color:#9C27B0;">🔨</span> ${escapeHtml(msg.content) || t('notif.dispute_resolved')}`;
|
| 136 |
// 【核心新增】:系统公告的正文排版,保留原格式的换行
|
| 137 |
else if (isSystem) actionText = `<div style="margin-top: 6px; color: #eee; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">${escapeHtml(msg.content)}</div>`;
|
|
|
|
|
|
|
| 138 |
|
| 139 |
const timeStr = new Date(msg.created_at * 1000).toLocaleString();
|
| 140 |
|
| 141 |
// 【核心新增】:如果是系统消息,增加 📢 标签并改变标题颜色
|
|
|
|
| 142 |
// 匿名打赏和系统通知的用户名不显示为链接
|
| 143 |
let nameLabel;
|
| 144 |
if (isSystem) {
|
| 145 |
nameLabel = `<strong style="color: #FF9800; font-size: 15px;">📢 [${t('notif.system_announcement')}] ${escapeHtml(msg.from_name)}</strong>`;
|
|
|
|
|
|
|
| 146 |
} else if (isAnonymous) {
|
| 147 |
nameLabel = `<strong style="color: #fff;">${escapeHtml(msg.from_name || msg.from_user)}</strong>`;
|
| 148 |
} else {
|
|
|
|
| 206 |
// 【核心新增】:系统公告点击不跳转,只触发消除红点
|
| 207 |
if (type === "system") return;
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
// 📝 任务榜通知:跳转到任务详情
|
| 210 |
if (type.startsWith("task_") && itemId) {
|
| 211 |
_navigateToTaskDetail(itemId, currentUser);
|
|
|
|
| 231 |
return;
|
| 232 |
}
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
// 其他:跳转发送者资料页(兜底)
|
| 235 |
if (acc && acc !== 'anonymous') openOtherUserProfileModal(acc, currentUser);
|
| 236 |
};
|
|
|
|
| 283 |
export async function loadUnreadCount(currentUser, bellBtn) {
|
| 284 |
if (!currentUser) { bellBtn.querySelector("#unread-badge").style.display = "none"; return; }
|
| 285 |
try {
|
| 286 |
+
// 🔥 修复:复用 api.getUnreadCount(内部走 count_only=true),只获取未读数、不标记已读
|
| 287 |
+
const res = await api.getUnreadCount(currentUser.account);
|
| 288 |
const unreadCount = res.unread_count || 0;
|
| 289 |
const badge = bellBtn.querySelector("#unread-badge");
|
| 290 |
if (unreadCount > 0) {
|