diff --git a/core/backend/apps/accounts/serializers.py b/core/backend/apps/accounts/serializers.py
index 14ec43a..753b473 100644
--- a/core/backend/apps/accounts/serializers.py
+++ b/core/backend/apps/accounts/serializers.py
@@ -21,6 +21,15 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
fields = ["notify", "two_factor_enabled", "creation_defaults", "display", "updated_at"]
read_only_fields = ["updated_at"]
+ def update(self, instance, validated_data):
+ # JSON 字段按 key 合并,避免消息中心写入的 mute-* 被设置页整包覆盖丢掉
+ for field in ("notify", "creation_defaults", "display"):
+ incoming = validated_data.get(field)
+ current = getattr(instance, field)
+ if isinstance(incoming, dict) and isinstance(current, dict):
+ validated_data[field] = {**current, **incoming}
+ return super().update(instance, validated_data)
+
class LoginSessionSerializer(serializers.ModelSerializer):
is_current = serializers.SerializerMethodField()
diff --git a/core/backend/apps/ops/tests.py b/core/backend/apps/ops/tests.py
index 92bf300..356ac6a 100644
--- a/core/backend/apps/ops/tests.py
+++ b/core/backend/apps/ops/tests.py
@@ -1,13 +1,15 @@
from decimal import Decimal
+from unittest.mock import patch
from django.test import TestCase
+from rest_framework.test import APIClient
from apps.accounts.models import Team, TeamMember, User
from apps.ai.models import AITask, ModelConfig, ModelProvider
from apps.assets.models import Asset
from apps.billing.models import CreditAccount, CreditLedger
from apps.ops.models import Notification
-from apps.ops.views import ensure_team_notifications
+from apps.ops.views import NotificationViewSet, ensure_team_notifications
from apps.products.models import Product
from apps.projects.models import Project
@@ -84,3 +86,38 @@ class BillingNotificationTests(TestCase):
ensure_team_notifications(self.team, self.user)
note.refresh_from_db()
self.assertEqual(note.cost_label, "¥0.32")
+
+
+class NotificationMuteFilterTests(TestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(username="owner", password="pass")
+ self.team = Team.objects.create(name="T", owner=self.user)
+ TeamMember.objects.create(team=self.team, user=self.user, role=TeamMember.Role.OWNER)
+ self.client = APIClient()
+ self.client.force_authenticate(self.user)
+ for kind, title in (
+ (Notification.Type.BILLING, "计费 1"),
+ (Notification.Type.BILLING, "计费 2"),
+ (Notification.Type.TASK, "任务 1"),
+ (Notification.Type.SYSTEM, "系统 1"),
+ ):
+ Notification.objects.create(
+ team=self.team,
+ recipient=self.user,
+ notification_type=kind,
+ title=title,
+ dedupe_key=f"mute-test:{title}",
+ )
+
+ @patch.object(NotificationViewSet, "_refresh_notifications")
+ def test_exclude_type_hides_muted_category(self, _refresh):
+ res = self.client.get("/api/ops/notifications/", {"exclude_type": "billing", "page_size": 50})
+ self.assertEqual(res.status_code, 200)
+ titles = [row["title"] for row in res.data["results"]]
+ self.assertNotIn("计费 1", titles)
+ self.assertNotIn("计费 2", titles)
+ self.assertIn("任务 1", titles)
+ self.assertIn("系统 1", titles)
+ # chip 计数仍是全量,不被静音筛掉
+ self.assertEqual(res.data["type_counts"]["billing"], 2)
+ self.assertEqual(res.data["type_counts"]["task"], 1)
diff --git a/core/backend/apps/ops/views.py b/core/backend/apps/ops/views.py
index 57fe184..e3193a5 100644
--- a/core/backend/apps/ops/views.py
+++ b/core/backend/apps/ops/views.py
@@ -257,6 +257,15 @@ class NotificationViewSet(TeamScopedViewSetMixin, ModelViewSet):
user = self.request.user
return queryset.filter(Q(recipient=user) | Q(recipient__isnull=True))
+ def _excluded_types(self):
+ """静音同类: ?exclude_type=billing&exclude_type=system 或逗号分隔。非法值丢弃。"""
+ allowed = {choice[0] for choice in Notification.Type.choices}
+ raw = self.request.query_params.getlist("exclude_type")
+ types: list[str] = []
+ for item in raw:
+ types.extend(part.strip() for part in item.split(",") if part.strip())
+ return [item for item in types if item in allowed]
+
def get_queryset(self):
queryset = self._recipient_scope()
notification_type = self.request.query_params.get("type")
@@ -264,6 +273,9 @@ class NotificationViewSet(TeamScopedViewSetMixin, ModelViewSet):
queryset = queryset.filter(notification_type=notification_type)
if self.request.query_params.get("unread") in {"1", "true", "yes"}:
queryset = queryset.filter(is_read=False)
+ excluded = self._excluded_types()
+ if excluded:
+ queryset = queryset.exclude(notification_type__in=excluded)
return queryset
def _refresh_notifications(self, request):
diff --git a/core/frontend/index.html b/core/frontend/index.html
index 7ed3500..660a678 100644
--- a/core/frontend/index.html
+++ b/core/frontend/index.html
@@ -4,8 +4,8 @@
-
-
AirShelf
+
+ 影擎
diff --git a/core/frontend/public/assets/logo-dark.png b/core/frontend/public/assets/logo-dark.png
index 42f1fdb..e17fe80 100644
Binary files a/core/frontend/public/assets/logo-dark.png and b/core/frontend/public/assets/logo-dark.png differ
diff --git a/core/frontend/public/assets/logo.png b/core/frontend/public/assets/logo.png
index 94121b1..1d9443a 100644
Binary files a/core/frontend/public/assets/logo.png and b/core/frontend/public/assets/logo.png differ
diff --git a/core/frontend/public/assets/yz/login-bg.jpg b/core/frontend/public/assets/yz/login-bg.jpg
new file mode 100644
index 0000000..ce216f9
Binary files /dev/null and b/core/frontend/public/assets/yz/login-bg.jpg differ
diff --git a/core/frontend/public/assets/yz/logo-horizontal.png b/core/frontend/public/assets/yz/logo-horizontal.png
new file mode 100644
index 0000000..1d9443a
Binary files /dev/null and b/core/frontend/public/assets/yz/logo-horizontal.png differ
diff --git a/core/frontend/public/assets/yz/logo-on-dark.png b/core/frontend/public/assets/yz/logo-on-dark.png
new file mode 100644
index 0000000..e17fe80
Binary files /dev/null and b/core/frontend/public/assets/yz/logo-on-dark.png differ
diff --git a/core/frontend/public/assets/yz/logo.png b/core/frontend/public/assets/yz/logo.png
new file mode 100644
index 0000000..fb86b60
Binary files /dev/null and b/core/frontend/public/assets/yz/logo.png differ
diff --git a/core/frontend/public/assets/yz/tool-cover.jpg b/core/frontend/public/assets/yz/tool-cover.jpg
new file mode 100644
index 0000000..04077f0
Binary files /dev/null and b/core/frontend/public/assets/yz/tool-cover.jpg differ
diff --git a/core/frontend/public/assets/yz/tool-model.jpg b/core/frontend/public/assets/yz/tool-model.jpg
new file mode 100644
index 0000000..c691a75
Binary files /dev/null and b/core/frontend/public/assets/yz/tool-model.jpg differ
diff --git a/core/frontend/public/assets/yz/tool-studio.jpg b/core/frontend/public/assets/yz/tool-studio.jpg
new file mode 100644
index 0000000..ce216f9
Binary files /dev/null and b/core/frontend/public/assets/yz/tool-studio.jpg differ
diff --git a/core/frontend/public/assets/yz/video-free.jpg b/core/frontend/public/assets/yz/video-free.jpg
new file mode 100644
index 0000000..f0437a6
Binary files /dev/null and b/core/frontend/public/assets/yz/video-free.jpg differ
diff --git a/core/frontend/public/assets/yz/video-pro.jpg b/core/frontend/public/assets/yz/video-pro.jpg
new file mode 100644
index 0000000..dfcfac1
Binary files /dev/null and b/core/frontend/public/assets/yz/video-pro.jpg differ
diff --git a/core/frontend/public/assets/yz/video-quick.jpg b/core/frontend/public/assets/yz/video-quick.jpg
new file mode 100644
index 0000000..59fdaac
Binary files /dev/null and b/core/frontend/public/assets/yz/video-quick.jpg differ
diff --git a/core/frontend/public/assets/yz/video-remix.jpg b/core/frontend/public/assets/yz/video-remix.jpg
new file mode 100644
index 0000000..aa0bc95
Binary files /dev/null and b/core/frontend/public/assets/yz/video-remix.jpg differ
diff --git a/core/frontend/public/assets/yz/video-replace.jpg b/core/frontend/public/assets/yz/video-replace.jpg
new file mode 100644
index 0000000..04077f0
Binary files /dev/null and b/core/frontend/public/assets/yz/video-replace.jpg differ
diff --git a/core/frontend/public/assets/yz/welcome-background.png b/core/frontend/public/assets/yz/welcome-background.png
new file mode 100644
index 0000000..3fa90c3
Binary files /dev/null and b/core/frontend/public/assets/yz/welcome-background.png differ
diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx
index 3c2ea4a..e02c5ce 100644
--- a/core/frontend/src/App.tsx
+++ b/core/frontend/src/App.tsx
@@ -21,7 +21,7 @@ import type {
} from "./types";
import { publicModelDisplayName } from "./model-display";
import { generationErrorText } from "./generation-error";
-import { CornerMarks, Decorations, openCommandPalette, Sidebar, ToastLike, topModuleForPage } from "./components/app-shell";
+import { AccountMenu, CornerMarks, Decorations, openCommandPalette, Sidebar, ToastLike, topModuleForPage } from "./components/app-shell";
import { SystemLoading } from "./components/loading";
import {
AccountPage,
@@ -115,6 +115,7 @@ export function App() {
const [activeProjectId, setActiveProjectId] = useState(route.projectId || "");
const [notice, setNotice] = useState(null);
const [loading, setLoading] = useState(false);
+ const [accountAnchor, setAccountAnchor] = useState(null);
// 右上角 toast 自动消失:成功/信息 3s,错误 5s(此前 notice 不会自己清,导致「提示一直挂着」)
useEffect(() => {
@@ -899,7 +900,7 @@ export function App() {
暂无项目
- // 先创建一个视频项目
+ 先创建一个视频项目
@@ -935,6 +936,7 @@ export function App() {
billing={billing}
projects={projects}
team={currentTeam}
+ navigate={navigate}
onRecharge={(amount, bonusPoints) => action(() => api.recharge({ amount, bonus_points: bonusPoints }), "充值成功")}
onNotify={(type, text) => setNotice({ type, text })}
/>
@@ -966,7 +968,7 @@ export function App() {
case "assetFactory":
return
;
case "freeCreate":
- return
setNotice({ type, text })} onTaskSettled={refreshFreeCreateShell} />;
+ return setNotice({ type, text })} onTaskSettled={refreshFreeCreateShell} onBack={() => navigate("projects")} />;
case "imageOptimize":
return navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />;
case "modelPhoto":
@@ -1016,13 +1018,12 @@ export function App() {
/>
);
}
- if (page === "pipeline" && pipelineProject) {
- const textModel = modelConfigs.find((m) => m.capability === "text" && m.status === "active") || modelConfigs.find((m) => m.capability === "text");
- return (
+ const pipelineTextModel = modelConfigs.find((m) => m.capability === "text" && m.status === "active") || modelConfigs.find((m) => m.capability === "text");
+ const pipelinePage = page === "pipeline" && pipelineProject ? (
m.capability === "text" && m.status === "active")}
loading={loading}
navigate={navigate}
@@ -1123,12 +1124,11 @@ export function App() {
onSaveTimeline={(payload) => action(() => api.saveTimeline(pipelineProject.id, payload), "草稿已保存")}
onSubmitExport={submitExport}
/>
- );
- }
+ ) : null;
return (
-
navigateAdmin("")} />
+ navigateAdmin("")} />
@@ -1163,17 +1163,38 @@ export function App() {
{unreadCount > 0 && {unreadCount}}
-
+
+
{notice && }
- {renderPage()}
+ {pipelinePage || renderPage()}
+ {accountAnchor && (
+ setAccountAnchor(null)}
+ navigate={navigate}
+ logout={logout}
+ user={currentUser}
+ team={currentTeam}
+ canManageBilling={isOwner}
+ />
+ )}
);
}
diff --git a/core/frontend/src/account-page.css b/core/frontend/src/account-page.css
index eb43601..a12e8a6 100644
--- a/core/frontend/src/account-page.css
+++ b/core/frontend/src/account-page.css
@@ -1,20 +1,60 @@
-/* 账户页 · 从 public/exact/account.html 内联