diff --git a/.cursor/skills/figma-ui-implementation/SKILL.md b/.cursor/skills/figma-ui-implementation/SKILL.md index 8de4906..84678da 100644 --- a/.cursor/skills/figma-ui-implementation/SKILL.md +++ b/.cursor/skills/figma-ui-implementation/SKILL.md @@ -40,8 +40,6 @@ Hard Constraints 图片资源必须使用设计稿中导出的真实图片文件(如 PNG、JPG、WebP 等),禁止使用 SVG、CSS 绘制(如 ::before、::after、background 渐变模拟)、或任何其他技术手段来替代或模拟图片内容。图片的视觉内容(产品图、人物头像、图标等)必须与设计稿保持一致。 -当当前 Codex 环境无法从 Figma 临时资源链接下载图片到本地时,不要阻塞实现流程;所有图片资源先直接使用 Figma MCP 返回的临时资源地址,并在代码或交付说明中标明后续可替换为本地导出文件。 - Implementation Rules Prefer project design tokens and existing components when they match the Figma values exactly. @@ -58,6 +56,25 @@ Keep spacing scale, typography, and visual hierarchy consistent with the Figma f SCSS 结构必须保持和 HTML 一致的嵌套结构,禁止使用 &_ 这种省略父级类名的简写方式,必须显式写出完整的类名以保持嵌套结构的清晰和可维护性。 +Codex 环境限制规则 + +以下规则用于规避当前 Codex 环境中已知的不可行路径,后续如环境修复,可集中修改本小节: + +Figma 图片资源: + +- 不要尝试下载 Figma 临时资源到本地。 +- 不要尝试通过其他网络、浏览器、代理、脚本、原始 socket 或任何绕路方案获取本地文件。 +- 直接使用 Figma MCP 返回的临时资源地址实现 UI。 +- 在代码或交付说明中标明临时资源地址后续可替换为本地导出文件。 + +Vite 本地服务: + +- 不要默认启动 Vite dev server。 +- 只有在 UI 任务确实需要浏览器截图、交互检查或视觉还原验证时,才启动 Vite。 +- 启动前先检查是否已有可复用的 dev server,避免重复启动。 +- 启动后复用已有进程,不要反复重启。 +- 普通代码验证优先使用 build、lint、typecheck 等一次性命令,不用 Vite dev server 作为默认验证方式。 + Delivery Checklist Before finishing, confirm: @@ -73,6 +90,6 @@ Basic responsive behavior works on common screen widths. All images are using real exported assets (PNG/JPG/WebP), not SVG substitutions or CSS-drawn alternatives. -When local Figma asset download is unavailable in Codex, images temporarily use the Figma MCP asset URLs and are clearly noted for later local replacement. +In Codex, Figma assets use the Figma MCP temporary asset URLs directly without local download attempts or workaround attempts. SCSS nested structure strictly mirrors the HTML structure, with no use of abbreviated class name selectors like &_. diff --git a/src/api/api.ts b/src/api/api.ts index 325ca50..b3e9062 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -404,6 +404,78 @@ export const getContent = (params: { id: string }): Promise => { + return request.get("content/notification/list", { params }); +}; + +export type NotificationCountResponse = { + data?: { + num?: number; + }; + message?: string; + status?: string; +}; + +export const getNotificationCount = (params?: { messageType?: NotificationMessageType }): Promise => { + return request.post("content/notification/count", params ?? {}); +}; + +export const readNotification = (params: { id: EntityId }): Promise => { + return request.post("content/notification/read", params, { skipGlobalErrorHandler: true }); +}; + // 上传媒体 export type UploadMediaFileResponse = { data?: { @@ -495,6 +567,7 @@ export type ParamConfigResponse = { sdVideoGenerationCoinPrice?: number; hdVideoGenerationCoinPrice?: number; audioGenerationCoinPrice?: number; + ossVideoCoverSuffix?: string; watermarkText?: string; waterMarkFontSize?: number; videoPowerPerSecond?: number; diff --git a/src/assets/images/addImage.png b/src/assets/images/addImage.png deleted file mode 100644 index b4b493c..0000000 Binary files a/src/assets/images/addImage.png and /dev/null differ diff --git a/src/assets/images/ic_detail_repair_brush.png b/src/assets/images/asset-details/repair-brush.png similarity index 100% rename from src/assets/images/ic_detail_repair_brush.png rename to src/assets/images/asset-details/repair-brush.png diff --git a/src/assets/images/ic_detail_repair_eraser.png b/src/assets/images/asset-details/repair-eraser.png similarity index 100% rename from src/assets/images/ic_detail_repair_eraser.png rename to src/assets/images/asset-details/repair-eraser.png diff --git a/src/assets/images/ic_detail_repair_hand.png b/src/assets/images/asset-details/repair-hand.png similarity index 100% rename from src/assets/images/ic_detail_repair_hand.png rename to src/assets/images/asset-details/repair-hand.png diff --git a/src/assets/images/ic_detail_repair_redo.png b/src/assets/images/asset-details/repair-redo.png similarity index 100% rename from src/assets/images/ic_detail_repair_redo.png rename to src/assets/images/asset-details/repair-redo.png diff --git a/src/assets/images/ic_detail_repair_undo.png b/src/assets/images/asset-details/repair-undo.png similarity index 100% rename from src/assets/images/ic_detail_repair_undo.png rename to src/assets/images/asset-details/repair-undo.png diff --git a/src/assets/images/ic_detail_repair.png b/src/assets/images/asset-details/repair.png similarity index 100% rename from src/assets/images/ic_detail_repair.png rename to src/assets/images/asset-details/repair.png diff --git a/src/assets/images/characters-create-right/cta-create-bg.png b/src/assets/images/characters-create-right/cta-create-bg.png deleted file mode 100644 index 90007a6..0000000 Binary files a/src/assets/images/characters-create-right/cta-create-bg.png and /dev/null differ diff --git a/src/assets/images/characters-create-right/cta-upload-bg.png b/src/assets/images/characters-create-right/cta-upload-bg.png deleted file mode 100644 index 2663669..0000000 Binary files a/src/assets/images/characters-create-right/cta-upload-bg.png and /dev/null differ diff --git a/src/assets/images/characters-copyright-deposit/step-1.png b/src/assets/images/characters/copyright-deposit-step-1.png similarity index 100% rename from src/assets/images/characters-copyright-deposit/step-1.png rename to src/assets/images/characters/copyright-deposit-step-1.png diff --git a/src/assets/images/characters-copyright-deposit/step-2.png b/src/assets/images/characters/copyright-deposit-step-2.png similarity index 100% rename from src/assets/images/characters-copyright-deposit/step-2.png rename to src/assets/images/characters/copyright-deposit-step-2.png diff --git a/src/assets/images/characters-copyright-deposit/step-arrow.png b/src/assets/images/characters/copyright-deposit-step-arrow.png similarity index 100% rename from src/assets/images/characters-copyright-deposit/step-arrow.png rename to src/assets/images/characters/copyright-deposit-step-arrow.png diff --git a/src/assets/images/characters-create-right/hero-chevron.png b/src/assets/images/characters/create-hero-chevron.png similarity index 100% rename from src/assets/images/characters-create-right/hero-chevron.png rename to src/assets/images/characters/create-hero-chevron.png diff --git a/src/assets/images/characters-create-right/info-icon.png b/src/assets/images/characters/create-info-icon.png similarity index 100% rename from src/assets/images/characters-create-right/info-icon.png rename to src/assets/images/characters/create-info-icon.png diff --git a/src/assets/images/ic_deposit_C_active.png b/src/assets/images/characters/deposit-active-icon.png similarity index 100% rename from src/assets/images/ic_deposit_C_active.png rename to src/assets/images/characters/deposit-active-icon.png diff --git a/src/assets/images/ic_deposit_active.png b/src/assets/images/characters/deposit-active-large-icon.png similarity index 100% rename from src/assets/images/ic_deposit_active.png rename to src/assets/images/characters/deposit-active-large-icon.png diff --git a/src/assets/images/character-deposit-guide-modal/certificate_reference.png b/src/assets/images/characters/deposit-guide-certificate-reference.png similarity index 100% rename from src/assets/images/character-deposit-guide-modal/certificate_reference.png rename to src/assets/images/characters/deposit-guide-certificate-reference.png diff --git a/src/assets/images/character-deposit-guide-modal/hero-cert.png b/src/assets/images/characters/deposit-guide-hero-cert.png similarity index 100% rename from src/assets/images/character-deposit-guide-modal/hero-cert.png rename to src/assets/images/characters/deposit-guide-hero-cert.png diff --git a/src/assets/images/character-deposit-guide-modal/seal.png b/src/assets/images/characters/deposit-guide-seal.png similarity index 100% rename from src/assets/images/character-deposit-guide-modal/seal.png rename to src/assets/images/characters/deposit-guide-seal.png diff --git a/src/assets/images/character-deposit-guide-modal/sparkle.png b/src/assets/images/characters/deposit-guide-sparkle.png similarity index 100% rename from src/assets/images/character-deposit-guide-modal/sparkle.png rename to src/assets/images/characters/deposit-guide-sparkle.png diff --git a/src/assets/images/ic_deposit_C.png b/src/assets/images/characters/deposit-icon.png similarity index 100% rename from src/assets/images/ic_deposit_C.png rename to src/assets/images/characters/deposit-icon.png diff --git a/src/assets/images/ic_deposit.png b/src/assets/images/characters/deposit-outline-icon.png similarity index 100% rename from src/assets/images/ic_deposit.png rename to src/assets/images/characters/deposit-outline-icon.png diff --git a/src/assets/images/ic_use_character.png b/src/assets/images/characters/use-character-icon.png similarity index 100% rename from src/assets/images/ic_use_character.png rename to src/assets/images/characters/use-character-icon.png diff --git a/src/assets/images/avatar.png b/src/assets/images/common/avatars/default-user-avatar.png similarity index 100% rename from src/assets/images/avatar.png rename to src/assets/images/common/avatars/default-user-avatar.png diff --git a/src/assets/images/popi_avatar.png b/src/assets/images/common/avatars/popi-avatar.png similarity index 100% rename from src/assets/images/popi_avatar.png rename to src/assets/images/common/avatars/popi-avatar.png diff --git a/src/assets/images/ic_arrow_right_gray.png b/src/assets/images/common/icons/arrow-right-gray.png similarity index 100% rename from src/assets/images/ic_arrow_right_gray.png rename to src/assets/images/common/icons/arrow-right-gray.png diff --git a/src/assets/images/ic_arrow_right_light_gray.png b/src/assets/images/common/icons/arrow-right-light-gray.png similarity index 100% rename from src/assets/images/ic_arrow_right_light_gray.png rename to src/assets/images/common/icons/arrow-right-light-gray.png diff --git a/src/assets/images/ic_arrow_right_purple.png b/src/assets/images/common/icons/arrow-right-purple.png similarity index 100% rename from src/assets/images/ic_arrow_right_purple.png rename to src/assets/images/common/icons/arrow-right-purple.png diff --git a/src/assets/images/ic_arrow_top.png b/src/assets/images/common/icons/arrow-up.png similarity index 100% rename from src/assets/images/ic_arrow_top.png rename to src/assets/images/common/icons/arrow-up.png diff --git a/src/assets/images/ic_imageExpansion.png b/src/assets/images/common/icons/asset-detail-expand-image.png similarity index 100% rename from src/assets/images/ic_imageExpansion.png rename to src/assets/images/common/icons/asset-detail-expand-image.png diff --git a/src/assets/images/ic_audio_forward.png b/src/assets/images/common/icons/audio-forward.png similarity index 100% rename from src/assets/images/ic_audio_forward.png rename to src/assets/images/common/icons/audio-forward.png diff --git a/src/assets/images/ic_avatar_add.png b/src/assets/images/common/icons/avatar-add.png similarity index 100% rename from src/assets/images/ic_avatar_add.png rename to src/assets/images/common/icons/avatar-add.png diff --git a/src/assets/images/ic_checkbox_agreement_off.png b/src/assets/images/common/icons/checkbox-agreement-off.png similarity index 100% rename from src/assets/images/ic_checkbox_agreement_off.png rename to src/assets/images/common/icons/checkbox-agreement-off.png diff --git a/src/assets/images/ic_checkbox_agreement_on.png b/src/assets/images/common/icons/checkbox-agreement-on.png similarity index 100% rename from src/assets/images/ic_checkbox_agreement_on.png rename to src/assets/images/common/icons/checkbox-agreement-on.png diff --git a/src/assets/images/ic_close.png b/src/assets/images/common/icons/close.png similarity index 100% rename from src/assets/images/ic_close.png rename to src/assets/images/common/icons/close.png diff --git a/src/assets/images/ic_copy.png b/src/assets/images/common/icons/copy.png similarity index 100% rename from src/assets/images/ic_copy.png rename to src/assets/images/common/icons/copy.png diff --git a/src/assets/images/ic_crop_gray.png b/src/assets/images/common/icons/crop-gray.png similarity index 100% rename from src/assets/images/ic_crop_gray.png rename to src/assets/images/common/icons/crop-gray.png diff --git a/src/assets/images/ic_crop.png b/src/assets/images/common/icons/crop.png similarity index 100% rename from src/assets/images/ic_crop.png rename to src/assets/images/common/icons/crop.png diff --git a/src/assets/images/ic_delete.png b/src/assets/images/common/icons/delete.png similarity index 100% rename from src/assets/images/ic_delete.png rename to src/assets/images/common/icons/delete.png diff --git a/src/assets/images/ic_download.png b/src/assets/images/common/icons/download.png similarity index 100% rename from src/assets/images/ic_download.png rename to src/assets/images/common/icons/download.png diff --git a/src/assets/images/ic_character_edit.png b/src/assets/images/common/icons/edit-character.png similarity index 100% rename from src/assets/images/ic_character_edit.png rename to src/assets/images/common/icons/edit-character.png diff --git a/src/assets/images/ic_eye_gray.png b/src/assets/images/common/icons/eye-gray.png similarity index 100% rename from src/assets/images/ic_eye_gray.png rename to src/assets/images/common/icons/eye-gray.png diff --git a/src/assets/images/ic_eye.png b/src/assets/images/common/icons/eye.png similarity index 100% rename from src/assets/images/ic_eye.png rename to src/assets/images/common/icons/eye.png diff --git a/src/assets/images/ic_file.png b/src/assets/images/common/icons/file.png similarity index 100% rename from src/assets/images/ic_file.png rename to src/assets/images/common/icons/file.png diff --git a/src/assets/images/ic_help_light_gray.png b/src/assets/images/common/icons/help-light-gray.png similarity index 100% rename from src/assets/images/ic_help_light_gray.png rename to src/assets/images/common/icons/help-light-gray.png diff --git a/src/assets/images/ic_image_close.png b/src/assets/images/common/icons/image-close.png similarity index 100% rename from src/assets/images/ic_image_close.png rename to src/assets/images/common/icons/image-close.png diff --git a/src/assets/images/ic_like_active.png b/src/assets/images/common/icons/like-active.png similarity index 100% rename from src/assets/images/ic_like_active.png rename to src/assets/images/common/icons/like-active.png diff --git a/src/assets/images/ic_like_white.png b/src/assets/images/common/icons/like-white.png similarity index 100% rename from src/assets/images/ic_like_white.png rename to src/assets/images/common/icons/like-white.png diff --git a/src/assets/images/ic_like.png b/src/assets/images/common/icons/like.png similarity index 100% rename from src/assets/images/ic_like.png rename to src/assets/images/common/icons/like.png diff --git a/src/assets/images/ic_link.png b/src/assets/images/common/icons/link.png similarity index 100% rename from src/assets/images/ic_link.png rename to src/assets/images/common/icons/link.png diff --git a/src/assets/images/ic_make_same.png b/src/assets/images/common/icons/make-same.png similarity index 100% rename from src/assets/images/ic_make_same.png rename to src/assets/images/common/icons/make-same.png diff --git a/src/assets/images/ic_message.png b/src/assets/images/common/icons/message.png similarity index 100% rename from src/assets/images/ic_message.png rename to src/assets/images/common/icons/message.png diff --git a/src/assets/images/ic_model_gray.png b/src/assets/images/common/icons/model-gray.png similarity index 100% rename from src/assets/images/ic_model_gray.png rename to src/assets/images/common/icons/model-gray.png diff --git a/src/assets/images/ic_model.png b/src/assets/images/common/icons/model.png similarity index 100% rename from src/assets/images/ic_model.png rename to src/assets/images/common/icons/model.png diff --git a/src/assets/images/ic_more.png b/src/assets/images/common/icons/more.png similarity index 100% rename from src/assets/images/ic_more.png rename to src/assets/images/common/icons/more.png diff --git a/src/assets/images/ic_collapsed.png b/src/assets/images/common/icons/panel-collapsed.png similarity index 100% rename from src/assets/images/ic_collapsed.png rename to src/assets/images/common/icons/panel-collapsed.png diff --git a/src/assets/images/ic_pause_gray.png b/src/assets/images/common/icons/pause-gray.png similarity index 100% rename from src/assets/images/ic_pause_gray.png rename to src/assets/images/common/icons/pause-gray.png diff --git a/src/assets/images/ic_pause.png b/src/assets/images/common/icons/pause.png similarity index 100% rename from src/assets/images/ic_pause.png rename to src/assets/images/common/icons/pause.png diff --git a/src/assets/images/ic_play_gray.png b/src/assets/images/common/icons/play-gray.png similarity index 100% rename from src/assets/images/ic_play_gray.png rename to src/assets/images/common/icons/play-gray.png diff --git a/src/assets/images/ic_play.png b/src/assets/images/common/icons/play.png similarity index 100% rename from src/assets/images/ic_play.png rename to src/assets/images/common/icons/play.png diff --git a/src/assets/images/ic_plus_active_fine.png b/src/assets/images/common/icons/plus-active-fine.png similarity index 100% rename from src/assets/images/ic_plus_active_fine.png rename to src/assets/images/common/icons/plus-active-fine.png diff --git a/src/assets/images/ic_plus_active.png b/src/assets/images/common/icons/plus-active.png similarity index 100% rename from src/assets/images/ic_plus_active.png rename to src/assets/images/common/icons/plus-active.png diff --git a/src/assets/images/ic_plus.png b/src/assets/images/common/icons/plus.png similarity index 100% rename from src/assets/images/ic_plus.png rename to src/assets/images/common/icons/plus.png diff --git a/src/assets/images/ic_push_white.png b/src/assets/images/common/icons/push-white.png similarity index 100% rename from src/assets/images/ic_push_white.png rename to src/assets/images/common/icons/push-white.png diff --git a/src/assets/images/ic_refresh_active.png b/src/assets/images/common/icons/refresh-active.png similarity index 100% rename from src/assets/images/ic_refresh_active.png rename to src/assets/images/common/icons/refresh-active.png diff --git a/src/assets/images/ic_refresh.png b/src/assets/images/common/icons/refresh.png similarity index 100% rename from src/assets/images/ic_refresh.png rename to src/assets/images/common/icons/refresh.png diff --git a/src/assets/images/ic_search.png b/src/assets/images/common/icons/search.png similarity index 100% rename from src/assets/images/ic_search.png rename to src/assets/images/common/icons/search.png diff --git a/src/assets/images/ic_share_gray.png b/src/assets/images/common/icons/share-gray.png similarity index 100% rename from src/assets/images/ic_share_gray.png rename to src/assets/images/common/icons/share-gray.png diff --git a/src/assets/images/ic_stars_white.png b/src/assets/images/common/icons/stars-white.png similarity index 100% rename from src/assets/images/ic_stars_white.png rename to src/assets/images/common/icons/stars-white.png diff --git a/src/assets/images/ic_publish.png b/src/assets/images/common/icons/upload.png similarity index 100% rename from src/assets/images/ic_publish.png rename to src/assets/images/common/icons/upload.png diff --git a/src/assets/images/ic_video.png b/src/assets/images/common/icons/video.png similarity index 100% rename from src/assets/images/ic_video.png rename to src/assets/images/common/icons/video.png diff --git a/src/assets/images/popiart.png b/src/assets/images/common/logos/popiart-logo.png similarity index 100% rename from src/assets/images/popiart.png rename to src/assets/images/common/logos/popiart-logo.png diff --git a/src/assets/images/logo_alipay.png b/src/assets/images/common/payments/alipay-logo.png similarity index 100% rename from src/assets/images/logo_alipay.png rename to src/assets/images/common/payments/alipay-logo.png diff --git a/src/assets/images/wxgh_loading.png b/src/assets/images/common/payments/wechat-loading.png similarity index 100% rename from src/assets/images/wxgh_loading.png rename to src/assets/images/common/payments/wechat-loading.png diff --git a/src/assets/images/logo_wxpay.png b/src/assets/images/common/payments/wxpay-logo.png similarity index 100% rename from src/assets/images/logo_wxpay.png rename to src/assets/images/common/payments/wxpay-logo.png diff --git a/src/assets/images/audio.png b/src/assets/images/common/placeholders/audio.png similarity index 100% rename from src/assets/images/audio.png rename to src/assets/images/common/placeholders/audio.png diff --git a/src/assets/images/ic_empty.png b/src/assets/images/common/placeholders/empty.png similarity index 100% rename from src/assets/images/ic_empty.png rename to src/assets/images/common/placeholders/empty.png diff --git a/src/assets/images/upload_bg.png b/src/assets/images/common/placeholders/upload-bg.png similarity index 100% rename from src/assets/images/upload_bg.png rename to src/assets/images/common/placeholders/upload-bg.png diff --git a/src/assets/images/upload_loading.png b/src/assets/images/common/placeholders/upload-loading.png similarity index 100% rename from src/assets/images/upload_loading.png rename to src/assets/images/common/placeholders/upload-loading.png diff --git a/src/assets/images/action-mimic-card-icon.png b/src/assets/images/create/action-mimic-card-icon.png similarity index 100% rename from src/assets/images/action-mimic-card-icon.png rename to src/assets/images/create/action-mimic-card-icon.png diff --git a/src/assets/images/AiCategories_template_active.png b/src/assets/images/create/composer-category-template-active.png similarity index 100% rename from src/assets/images/AiCategories_template_active.png rename to src/assets/images/create/composer-category-template-active.png diff --git a/src/assets/images/AiCategories_template.png b/src/assets/images/create/composer-category-template.png similarity index 100% rename from src/assets/images/AiCategories_template.png rename to src/assets/images/create/composer-category-template.png diff --git a/src/assets/images/ic_extension.png b/src/assets/images/create/composer-extension.png similarity index 100% rename from src/assets/images/ic_extension.png rename to src/assets/images/create/composer-extension.png diff --git a/src/assets/images/create/folders/ic_more.png b/src/assets/images/create/folders/ic_more.png deleted file mode 100644 index 8139c38..0000000 Binary files a/src/assets/images/create/folders/ic_more.png and /dev/null differ diff --git a/src/assets/images/ic_template_chip.png b/src/assets/images/create/template-chip.png similarity index 100% rename from src/assets/images/ic_template_chip.png rename to src/assets/images/create/template-chip.png diff --git a/src/assets/images/ic_health.png b/src/assets/images/create/voice-health.png similarity index 100% rename from src/assets/images/ic_health.png rename to src/assets/images/create/voice-health.png diff --git a/src/assets/images/dot.png b/src/assets/images/dot.png deleted file mode 100644 index ce64e2d..0000000 Binary files a/src/assets/images/dot.png and /dev/null differ diff --git a/src/assets/images/explore/hero/alice.png b/src/assets/images/explore/hero/alice.png deleted file mode 100644 index 1cbd8af..0000000 Binary files a/src/assets/images/explore/hero/alice.png and /dev/null differ diff --git a/src/assets/images/explore/hero/diy-character.png b/src/assets/images/explore/hero/diy-character.png deleted file mode 100644 index 0a846bd..0000000 Binary files a/src/assets/images/explore/hero/diy-character.png and /dev/null differ diff --git a/src/assets/images/explore/ip-avatars/ip-avatar3.png b/src/assets/images/explore/ip-avatars/ip-avatar3.png deleted file mode 100644 index c01501a..0000000 Binary files a/src/assets/images/explore/ip-avatars/ip-avatar3.png and /dev/null differ diff --git a/src/assets/images/Vector.png b/src/assets/images/explore/post-feed-play-icon.png similarity index 100% rename from src/assets/images/Vector.png rename to src/assets/images/explore/post-feed-play-icon.png diff --git a/src/assets/images/footer-logo.png b/src/assets/images/footer-logo.png deleted file mode 100644 index 05ae621..0000000 Binary files a/src/assets/images/footer-logo.png and /dev/null differ diff --git a/src/assets/images/index/brand_logo2.png b/src/assets/images/home/brand-logo-secondary.png similarity index 100% rename from src/assets/images/index/brand_logo2.png rename to src/assets/images/home/brand-logo-secondary.png diff --git a/src/assets/images/index/brand_logo.png b/src/assets/images/home/brand-logo.png similarity index 100% rename from src/assets/images/index/brand_logo.png rename to src/assets/images/home/brand-logo.png diff --git a/src/assets/images/index/cloud_auto_match_icon.png b/src/assets/images/home/cloud-auto-match-icon.png similarity index 100% rename from src/assets/images/index/cloud_auto_match_icon.png rename to src/assets/images/home/cloud-auto-match-icon.png diff --git a/src/assets/images/index/cloud_ip_asset_reuse_icon.png b/src/assets/images/home/cloud-ip-asset-reuse-icon.png similarity index 100% rename from src/assets/images/index/cloud_ip_asset_reuse_icon.png rename to src/assets/images/home/cloud-ip-asset-reuse-icon.png diff --git a/src/assets/images/index/cloud_one_prompt_icon.png b/src/assets/images/home/cloud-one-prompt-icon.png similarity index 100% rename from src/assets/images/index/cloud_one_prompt_icon.png rename to src/assets/images/home/cloud-one-prompt-icon.png diff --git a/src/assets/images/index/cloud_team_collaboration_icon.png b/src/assets/images/home/cloud-team-collaboration-icon.png similarity index 100% rename from src/assets/images/index/cloud_team_collaboration_icon.png rename to src/assets/images/home/cloud-team-collaboration-icon.png diff --git a/src/assets/images/index/feature_check_icon.png b/src/assets/images/home/feature-check-icon.png similarity index 100% rename from src/assets/images/index/feature_check_icon.png rename to src/assets/images/home/feature-check-icon.png diff --git a/src/assets/images/index/header_login_arrow_icon.png b/src/assets/images/home/header-login-arrow-icon.png similarity index 100% rename from src/assets/images/index/header_login_arrow_icon.png rename to src/assets/images/home/header-login-arrow-icon.png diff --git a/src/assets/images/index/hero_creator_lifestyle_bg.png b/src/assets/images/home/hero-creator-lifestyle-bg.png similarity index 100% rename from src/assets/images/index/hero_creator_lifestyle_bg.png rename to src/assets/images/home/hero-creator-lifestyle-bg.png diff --git a/src/assets/images/index/hero_download_arrow_icon.png b/src/assets/images/home/hero-download-arrow-icon.png similarity index 100% rename from src/assets/images/index/hero_download_arrow_icon.png rename to src/assets/images/home/hero-download-arrow-icon.png diff --git a/src/assets/images/index/popi_art_assets_preview.png b/src/assets/images/home/popi-art-assets-preview.png similarity index 100% rename from src/assets/images/index/popi_art_assets_preview.png rename to src/assets/images/home/popi-art-assets-preview.png diff --git a/src/assets/images/index/popi_art_workspace_preview.png b/src/assets/images/home/popi-art-workspace-preview.png similarity index 100% rename from src/assets/images/index/popi_art_workspace_preview.png rename to src/assets/images/home/popi-art-workspace-preview.png diff --git a/src/assets/images/index/popi_mcn_agent_preview.png b/src/assets/images/home/popi-mcn-agent-preview.png similarity index 100% rename from src/assets/images/index/popi_mcn_agent_preview.png rename to src/assets/images/home/popi-mcn-agent-preview.png diff --git a/src/assets/images/index/popi_tv_canvas_preview.png b/src/assets/images/home/popi-tv-canvas-preview.png similarity index 100% rename from src/assets/images/index/popi_tv_canvas_preview.png rename to src/assets/images/home/popi-tv-canvas-preview.png diff --git a/src/assets/images/index/product_download_arrow_icon.png b/src/assets/images/home/product-download-arrow-icon.png similarity index 100% rename from src/assets/images/index/product_download_arrow_icon.png rename to src/assets/images/home/product-download-arrow-icon.png diff --git a/src/assets/images/index/product_open_arrow_icon.png b/src/assets/images/home/product-open-arrow-icon.png similarity index 100% rename from src/assets/images/index/product_open_arrow_icon.png rename to src/assets/images/home/product-open-arrow-icon.png diff --git a/src/assets/images/index/product_popi_art_icon.png b/src/assets/images/home/product-popi-art-icon.png similarity index 100% rename from src/assets/images/index/product_popi_art_icon.png rename to src/assets/images/home/product-popi-art-icon.png diff --git a/src/assets/images/index/product_popi_cloud_icon.png b/src/assets/images/home/product-popi-cloud-icon.png similarity index 100% rename from src/assets/images/index/product_popi_cloud_icon.png rename to src/assets/images/home/product-popi-cloud-icon.png diff --git a/src/assets/images/index/product_popi_mcn_icon.png b/src/assets/images/home/product-popi-mcn-icon.png similarity index 100% rename from src/assets/images/index/product_popi_mcn_icon.png rename to src/assets/images/home/product-popi-mcn-icon.png diff --git a/src/assets/images/index/product_popi_tv_icon.png b/src/assets/images/home/product-popi-tv-icon.png similarity index 100% rename from src/assets/images/index/product_popi_tv_icon.png rename to src/assets/images/home/product-popi-tv-icon.png diff --git a/src/assets/images/ic_artist.png b/src/assets/images/ic_artist.png deleted file mode 100644 index 6deb90b..0000000 Binary files a/src/assets/images/ic_artist.png and /dev/null differ diff --git a/src/assets/images/ic_audio_forward_10s.png b/src/assets/images/ic_audio_forward_10s.png deleted file mode 100644 index c54d329..0000000 Binary files a/src/assets/images/ic_audio_forward_10s.png and /dev/null differ diff --git a/src/assets/images/ic_audio_rewind_10s.png b/src/assets/images/ic_audio_rewind_10s.png deleted file mode 100644 index ad9970a..0000000 Binary files a/src/assets/images/ic_audio_rewind_10s.png and /dev/null differ diff --git a/src/assets/images/ic_checkbox.png b/src/assets/images/ic_checkbox.png deleted file mode 100644 index 58b2640..0000000 Binary files a/src/assets/images/ic_checkbox.png and /dev/null differ diff --git a/src/assets/images/ic_checkbox_checked.png b/src/assets/images/ic_checkbox_checked.png deleted file mode 100644 index ceabef6..0000000 Binary files a/src/assets/images/ic_checkbox_checked.png and /dev/null differ diff --git a/src/assets/images/ic_collapse.png b/src/assets/images/ic_collapse.png deleted file mode 100644 index b0647b1..0000000 Binary files a/src/assets/images/ic_collapse.png and /dev/null differ diff --git a/src/assets/images/ic_copyright.png b/src/assets/images/ic_copyright.png deleted file mode 100644 index a2883d0..0000000 Binary files a/src/assets/images/ic_copyright.png and /dev/null differ diff --git a/src/assets/images/ic_dui.png b/src/assets/images/ic_dui.png deleted file mode 100644 index 90061c8..0000000 Binary files a/src/assets/images/ic_dui.png and /dev/null differ diff --git a/src/assets/images/ic_gout.png b/src/assets/images/ic_gout.png deleted file mode 100644 index 73aadee..0000000 Binary files a/src/assets/images/ic_gout.png and /dev/null differ diff --git a/src/assets/images/ic_help.png b/src/assets/images/ic_help.png deleted file mode 100644 index d615dbb..0000000 Binary files a/src/assets/images/ic_help.png and /dev/null differ diff --git a/src/assets/images/ic_invite.png b/src/assets/images/ic_invite.png deleted file mode 100644 index 4703dbf..0000000 Binary files a/src/assets/images/ic_invite.png and /dev/null differ diff --git a/src/assets/images/ic_key.png b/src/assets/images/ic_key.png deleted file mode 100644 index 14253e5..0000000 Binary files a/src/assets/images/ic_key.png and /dev/null differ diff --git a/src/assets/images/ic_lamp.png b/src/assets/images/ic_lamp.png deleted file mode 100644 index b5cfdaf..0000000 Binary files a/src/assets/images/ic_lamp.png and /dev/null differ diff --git a/src/assets/images/ic_language.png b/src/assets/images/ic_language.png deleted file mode 100644 index 3aa3c01..0000000 Binary files a/src/assets/images/ic_language.png and /dev/null differ diff --git a/src/assets/images/ic_look.png b/src/assets/images/ic_look.png deleted file mode 100644 index c16c666..0000000 Binary files a/src/assets/images/ic_look.png and /dev/null differ diff --git a/src/assets/images/ic_menu.png b/src/assets/images/ic_menu.png deleted file mode 100644 index 269ed04..0000000 Binary files a/src/assets/images/ic_menu.png and /dev/null differ diff --git a/src/assets/images/ic_regenerate.png b/src/assets/images/ic_regenerate.png deleted file mode 100644 index 3d9264a..0000000 Binary files a/src/assets/images/ic_regenerate.png and /dev/null differ diff --git a/src/assets/images/ic_service_wx.png b/src/assets/images/ic_service_wx.png deleted file mode 100644 index 7bf9771..0000000 Binary files a/src/assets/images/ic_service_wx.png and /dev/null differ diff --git a/src/assets/images/ic_speaker.png b/src/assets/images/ic_speaker.png deleted file mode 100644 index d9f7418..0000000 Binary files a/src/assets/images/ic_speaker.png and /dev/null differ diff --git a/src/assets/images/ic_user.png b/src/assets/images/ic_user.png deleted file mode 100644 index 4608bcd..0000000 Binary files a/src/assets/images/ic_user.png and /dev/null differ diff --git a/src/assets/images/layout/alice-live-class/meetup-arrow.png b/src/assets/images/layout/alice-live-class/meetup-arrow.png deleted file mode 100644 index 99ff7f6..0000000 Binary files a/src/assets/images/layout/alice-live-class/meetup-arrow.png and /dev/null differ diff --git a/src/assets/images/layout/alice-live-class/meetup-avatar.png b/src/assets/images/layout/alice-live-class/meetup-avatar.png deleted file mode 100644 index b0d95af..0000000 Binary files a/src/assets/images/layout/alice-live-class/meetup-avatar.png and /dev/null differ diff --git a/src/assets/images/alice_live_streaming.png b/src/assets/images/layout/alice-live-class/service-qr.png similarity index 100% rename from src/assets/images/alice_live_streaming.png rename to src/assets/images/layout/alice-live-class/service-qr.png diff --git a/src/assets/images/layout/app-download/ic-popi-logo.png b/src/assets/images/layout/app-download/ic-popi-logo.png deleted file mode 100644 index c3dfcce..0000000 Binary files a/src/assets/images/layout/app-download/ic-popi-logo.png and /dev/null differ diff --git a/src/assets/images/layout/nav/collapse.png b/src/assets/images/layout/nav/collapse.png deleted file mode 100644 index 9c62c7a..0000000 Binary files a/src/assets/images/layout/nav/collapse.png and /dev/null differ diff --git a/src/assets/images/layout/nav/top-help.png b/src/assets/images/layout/nav/top-help.png deleted file mode 100644 index d6ece4e..0000000 Binary files a/src/assets/images/layout/nav/top-help.png and /dev/null differ diff --git a/src/assets/images/layout/nav/top-skill.png b/src/assets/images/layout/nav/top-skill.png deleted file mode 100644 index 2e39a74..0000000 Binary files a/src/assets/images/layout/nav/top-skill.png and /dev/null differ diff --git a/src/assets/images/logo-full-white.png b/src/assets/images/logo-full-white.png deleted file mode 100644 index 7850d09..0000000 Binary files a/src/assets/images/logo-full-white.png and /dev/null differ diff --git a/src/assets/images/menu/canvas.png b/src/assets/images/menu/canvas.png deleted file mode 100644 index f880a11..0000000 Binary files a/src/assets/images/menu/canvas.png and /dev/null differ diff --git a/src/assets/images/menu/canvas.svg b/src/assets/images/menu/canvas.svg deleted file mode 100644 index 3e73e65..0000000 --- a/src/assets/images/menu/canvas.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/images/menu/characters.svg b/src/assets/images/menu/characters.svg deleted file mode 100644 index 468f69c..0000000 --- a/src/assets/images/menu/characters.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/assets/images/menu/create.svg b/src/assets/images/menu/create.svg deleted file mode 100644 index 199c07a..0000000 --- a/src/assets/images/menu/create.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/assets/images/menu/explore-group.svg b/src/assets/images/menu/explore-group.svg deleted file mode 100644 index fc17e66..0000000 --- a/src/assets/images/menu/explore-group.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/images/menu/explore-vector.svg b/src/assets/images/menu/explore-vector.svg deleted file mode 100644 index 3381de9..0000000 --- a/src/assets/images/menu/explore-vector.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/images/menu/more.svg b/src/assets/images/menu/more.svg deleted file mode 100644 index 34f3b35..0000000 --- a/src/assets/images/menu/more.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/images/nav-arrow.png b/src/assets/images/nav-arrow.png deleted file mode 100644 index a6c2961..0000000 Binary files a/src/assets/images/nav-arrow.png and /dev/null differ diff --git a/src/assets/images/pending.png b/src/assets/images/pending.png deleted file mode 100644 index d9a21fb..0000000 Binary files a/src/assets/images/pending.png and /dev/null differ diff --git a/src/assets/images/personal-role-card.png b/src/assets/images/personal-role-card.png deleted file mode 100644 index 32ebc89..0000000 Binary files a/src/assets/images/personal-role-card.png and /dev/null differ diff --git a/src/assets/images/points-invite-reward-character.png b/src/assets/images/points-invite-reward-character.png deleted file mode 100644 index 55ffe4b..0000000 Binary files a/src/assets/images/points-invite-reward-character.png and /dev/null differ diff --git a/src/assets/images/points-invite-reward-bubble.png b/src/assets/images/points/invite-reward-bubble.png similarity index 100% rename from src/assets/images/points-invite-reward-bubble.png rename to src/assets/images/points/invite-reward-bubble.png diff --git a/src/assets/images/points-invite-reward-character-display.png b/src/assets/images/points/invite-reward-character-display.png similarity index 100% rename from src/assets/images/points-invite-reward-character-display.png rename to src/assets/images/points/invite-reward-character-display.png diff --git a/src/assets/images/ic_points_active.png b/src/assets/images/points/points-active.png similarity index 100% rename from src/assets/images/ic_points_active.png rename to src/assets/images/points/points-active.png diff --git a/src/assets/images/ic_points.png b/src/assets/images/points/points.png similarity index 100% rename from src/assets/images/ic_points.png rename to src/assets/images/points/points.png diff --git a/src/assets/images/ic_stars_active.png b/src/assets/images/points/stars-active.png similarity index 100% rename from src/assets/images/ic_stars_active.png rename to src/assets/images/points/stars-active.png diff --git a/src/assets/images/scene1.png b/src/assets/images/scene1.png deleted file mode 100644 index 599a910..0000000 Binary files a/src/assets/images/scene1.png and /dev/null differ diff --git a/src/assets/images/scene2.png b/src/assets/images/scene2.png deleted file mode 100644 index 3e6e0ef..0000000 Binary files a/src/assets/images/scene2.png and /dev/null differ diff --git a/src/assets/images/scene3.png b/src/assets/images/scene3.png deleted file mode 100644 index fd5d796..0000000 Binary files a/src/assets/images/scene3.png and /dev/null differ diff --git a/src/assets/images/scene4.png b/src/assets/images/scene4.png deleted file mode 100644 index 01f1ff6..0000000 Binary files a/src/assets/images/scene4.png and /dev/null differ diff --git a/src/assets/images/sceneCharacter1.png b/src/assets/images/sceneCharacter1.png deleted file mode 100644 index 2129ee9..0000000 Binary files a/src/assets/images/sceneCharacter1.png and /dev/null differ diff --git a/src/assets/images/sceneCharacter2.png b/src/assets/images/sceneCharacter2.png deleted file mode 100644 index 80ef185..0000000 Binary files a/src/assets/images/sceneCharacter2.png and /dev/null differ diff --git a/src/assets/images/sceneCharacter3.png b/src/assets/images/sceneCharacter3.png deleted file mode 100644 index 8284d84..0000000 Binary files a/src/assets/images/sceneCharacter3.png and /dev/null differ diff --git a/src/assets/images/sceneCharacter4.png b/src/assets/images/sceneCharacter4.png deleted file mode 100644 index 1503955..0000000 Binary files a/src/assets/images/sceneCharacter4.png and /dev/null differ diff --git a/src/assets/images/subscribe_banner1.png b/src/assets/images/subscription/banner-1.png similarity index 100% rename from src/assets/images/subscribe_banner1.png rename to src/assets/images/subscription/banner-1.png diff --git a/src/assets/images/subscribe_banner2.png b/src/assets/images/subscription/banner-2.png similarity index 100% rename from src/assets/images/subscribe_banner2.png rename to src/assets/images/subscription/banner-2.png diff --git a/src/assets/images/subscribe_banner3.png b/src/assets/images/subscription/banner-3.png similarity index 100% rename from src/assets/images/subscribe_banner3.png rename to src/assets/images/subscription/banner-3.png diff --git a/src/assets/images/ic_crown_book.png b/src/assets/images/subscription/crown-book.png similarity index 100% rename from src/assets/images/ic_crown_book.png rename to src/assets/images/subscription/crown-book.png diff --git a/src/assets/images/ic_crown.png b/src/assets/images/subscription/crown.png similarity index 100% rename from src/assets/images/ic_crown.png rename to src/assets/images/subscription/crown.png diff --git a/src/assets/images/ic_stars_black.png b/src/assets/images/subscription/stars-black.png similarity index 100% rename from src/assets/images/ic_stars_black.png rename to src/assets/images/subscription/stars-black.png diff --git a/src/assets/images/voice-library/ic_cover_default.png b/src/assets/images/voice-library/ic_cover_default.png deleted file mode 100644 index f21ffec..0000000 Binary files a/src/assets/images/voice-library/ic_cover_default.png and /dev/null differ diff --git a/src/assets/images/voice/ic_clear.png b/src/assets/images/voice/ic_clear.png deleted file mode 100644 index 745c7f3..0000000 Binary files a/src/assets/images/voice/ic_clear.png and /dev/null differ diff --git a/src/assets/images/voice/ic_upload_music.png b/src/assets/images/voice/ic_upload_music.png deleted file mode 100644 index 5c51a7f..0000000 Binary files a/src/assets/images/voice/ic_upload_music.png and /dev/null differ diff --git a/src/assets/images/voice-library/ic_cover_default-display.jpg b/src/assets/images/voice/library/cover-default-display.jpg similarity index 100% rename from src/assets/images/voice-library/ic_cover_default-display.jpg rename to src/assets/images/voice/library/cover-default-display.jpg diff --git a/src/assets/images/voice-library/ic_pause.png b/src/assets/images/voice/library/pause.png similarity index 100% rename from src/assets/images/voice-library/ic_pause.png rename to src/assets/images/voice/library/pause.png diff --git a/src/assets/images/voice-library/ic_play.png b/src/assets/images/voice/library/play.png similarity index 100% rename from src/assets/images/voice-library/ic_play.png rename to src/assets/images/voice/library/play.png diff --git a/src/assets/images/voice-library/ic_search.png b/src/assets/images/voice/library/search.png similarity index 100% rename from src/assets/images/voice-library/ic_search.png rename to src/assets/images/voice/library/search.png diff --git a/src/assets/images/voice-library/ic_star_liked.png b/src/assets/images/voice/library/star-liked.png similarity index 100% rename from src/assets/images/voice-library/ic_star_liked.png rename to src/assets/images/voice/library/star-liked.png diff --git a/src/assets/images/voice-library/ic_star.png b/src/assets/images/voice/library/star.png similarity index 100% rename from src/assets/images/voice-library/ic_star.png rename to src/assets/images/voice/library/star.png diff --git a/src/assets/images/no_music.png b/src/assets/images/voice/no-music.png similarity index 100% rename from src/assets/images/no_music.png rename to src/assets/images/voice/no-music.png diff --git a/src/assets/images/upload_music.png b/src/assets/images/voice/upload-music.png similarity index 100% rename from src/assets/images/upload_music.png rename to src/assets/images/voice/upload-music.png diff --git a/src/assets/images/测试文案.m4a b/src/assets/images/测试文案.m4a deleted file mode 100644 index 8700e97..0000000 Binary files a/src/assets/images/测试文案.m4a and /dev/null differ diff --git a/src/assets/images/测试文案.mp3 b/src/assets/images/测试文案.mp3 deleted file mode 100644 index 8546042..0000000 Binary files a/src/assets/images/测试文案.mp3 and /dev/null differ diff --git a/src/components/AssetAudioPlayer/index.tsx b/src/components/AssetAudioPlayer/index.tsx index 258f0a9..cc310bf 100644 --- a/src/components/AssetAudioPlayer/index.tsx +++ b/src/components/AssetAudioPlayer/index.tsx @@ -155,10 +155,10 @@ export const AssetAudioPlayer = forwardRef - +
diff --git a/src/components/AssetGalleryTile/index.tsx b/src/components/AssetGalleryTile/index.tsx index 97bd44c..e18b721 100644 --- a/src/components/AssetGalleryTile/index.tsx +++ b/src/components/AssetGalleryTile/index.tsx @@ -115,8 +115,8 @@ export function AssetGalleryTile(props: AssetGalleryTileProps) { } if (kind === "video") { - const poster = resolveApiMediaUrl(videoPoster); const resolvedVideoSrc = resolveApiMediaUrl(videoSrc); + const poster = resolveOssVideoCoverUrl(resolvedVideoSrc); return ( {poster ? : null} @@ -129,7 +129,7 @@ export function AssetGalleryTile(props: AssetGalleryTileProps) { } if (kind === "voice") { - const resolvedVoiceCover = resolveApiMediaUrl(voiceCover) || ASSET_GALLERY_AUDIO_PLACEHOLDER_SRC; + const resolvedVoiceCover = resolveOssImageThumbUrl(resolveApiMediaUrl(voiceCover)) || ASSET_GALLERY_AUDIO_PLACEHOLDER_SRC; const resolvedVoicePrompt = (voicePrompt ?? "").trim(); const resolvedIsVoicePlaying = Boolean(isVoicePlaying); const resolvedVoiceSrc = resolveApiMediaUrl(voiceSrc); @@ -196,7 +196,7 @@ export function AssetGalleryTile(props: AssetGalleryTileProps) { return ( - + ); } diff --git a/src/components/AssetGalleryTile/utils.ts b/src/components/AssetGalleryTile/utils.ts index 085e789..875d421 100644 --- a/src/components/AssetGalleryTile/utils.ts +++ b/src/components/AssetGalleryTile/utils.ts @@ -1,12 +1,12 @@ import { resolveApiMediaUrl } from "@/utils/apiMediaUrl"; export const ASSET_GALLERY_AUDIO_PLACEHOLDER_SRC = new URL("/src/assets/images/create/asset_gallery_audio_bg.png", import.meta.url).href; -export const ASSET_GALLERY_VOICE_PLAY_ICON_SRC = new URL("/src/assets/images/ic_play.png", import.meta.url).href; -export const ASSET_GALLERY_VOICE_PAUSE_ICON_SRC = new URL("/src/assets/images/ic_pause.png", import.meta.url).href; +export const ASSET_GALLERY_VOICE_PLAY_ICON_SRC = new URL("/src/assets/images/common/icons/play.png", import.meta.url).href; +export const ASSET_GALLERY_VOICE_PAUSE_ICON_SRC = new URL("/src/assets/images/common/icons/pause.png", import.meta.url).href; export const ASSET_GALLERY_VOICE_CAPTION_ICON_SRC = new URL("/src/assets/images/create/ic_asset_gallery_audio_caption.png", import.meta.url).href; -export const ASSET_GALLERY_VIDEO_PLAY_ICON_SRC = new URL("/src/assets/images/ic_play.png", import.meta.url).href; -export const ASSET_GALLERY_PENDING_IMAGE_SRC = new URL("/src/assets/images/upload_bg.png", import.meta.url).href; -export const ASSET_GALLERY_PENDING_ICON_SRC = new URL("/src/assets/images/upload_loading.png", import.meta.url).href; +export const ASSET_GALLERY_VIDEO_PLAY_ICON_SRC = new URL("/src/assets/images/common/icons/play.png", import.meta.url).href; +export const ASSET_GALLERY_PENDING_IMAGE_SRC = new URL("/src/assets/images/common/placeholders/upload-bg.png", import.meta.url).href; +export const ASSET_GALLERY_PENDING_ICON_SRC = new URL("/src/assets/images/common/placeholders/upload-loading.png", import.meta.url).href; export type AssetGalleryTileKind = "image" | "video" | "voice" | "pending"; diff --git a/src/components/AvatarCropModal/index.tsx b/src/components/AvatarCropModal/index.tsx index 77860c5..1590605 100644 --- a/src/components/AvatarCropModal/index.tsx +++ b/src/components/AvatarCropModal/index.tsx @@ -174,7 +174,7 @@ export default function AvatarCropModal({ image, onUploaded, onClose }: AvatarCr

{t("avatarCrop.title")}

diff --git a/src/components/CategoryDropdown/index.tsx b/src/components/CategoryDropdown/index.tsx index 679e875..e8a7886 100644 --- a/src/components/CategoryDropdown/index.tsx +++ b/src/components/CategoryDropdown/index.tsx @@ -19,7 +19,7 @@ export interface CategoryDropdownProps { arrowSrc?: string; } -const DEFAULT_ARROW_SRC = new URL("/src/assets/images/ic_arrow_right_gray.png", import.meta.url).href; +const DEFAULT_ARROW_SRC = new URL("/src/assets/images/common/icons/arrow-right-gray.png", import.meta.url).href; export function CategoryDropdown({ label, ariaLabel, allLabel, items, selectedId, onSelect, className, menuId, arrowSrc = DEFAULT_ARROW_SRC }: CategoryDropdownProps) { const generatedMenuId = useId(); diff --git a/src/components/DeleteConfirmModal/index.tsx b/src/components/DeleteConfirmModal/index.tsx index 91874bf..efd2283 100644 --- a/src/components/DeleteConfirmModal/index.tsx +++ b/src/components/DeleteConfirmModal/index.tsx @@ -22,7 +22,7 @@ export function DeleteConfirmModal({ visible, title, description, cancelText, ca

{title}

{description}

diff --git a/src/components/PreviewImageThumb/index.tsx b/src/components/PreviewImageThumb/index.tsx index 3422665..f727656 100644 --- a/src/components/PreviewImageThumb/index.tsx +++ b/src/components/PreviewImageThumb/index.tsx @@ -7,9 +7,9 @@ export type PreviewImageThumbProps = { export function PreviewImageThumb({ src }: PreviewImageThumbProps) { return (
- +
- +
); diff --git a/src/components/TabPill/index.scss b/src/components/TabPill/index.scss index 836ded7..e86e353 100644 --- a/src/components/TabPill/index.scss +++ b/src/components/TabPill/index.scss @@ -30,6 +30,9 @@ height: 20px; display: block; object-fit: contain; +} + +.tabPill__icon:not(.commonImageIcon_purple) { filter: brightness(0) saturate(100%) invert(17%) sepia(0%) saturate(0%) hue-rotate(187deg) brightness(95%) contrast(91%); } diff --git a/src/components/UploadPendingIndicator/index.tsx b/src/components/UploadPendingIndicator/index.tsx index 65eebf8..3858cfd 100644 --- a/src/components/UploadPendingIndicator/index.tsx +++ b/src/components/UploadPendingIndicator/index.tsx @@ -1,7 +1,7 @@ import "./index.scss"; -const UPLOAD_PENDING_BG_SRC = new URL("/src/assets/images/upload_bg.png", import.meta.url).href; -const UPLOAD_PENDING_LOADING_SRC = new URL("/src/assets/images/upload_loading.png", import.meta.url).href; +const UPLOAD_PENDING_BG_SRC = new URL("/src/assets/images/common/placeholders/upload-bg.png", import.meta.url).href; +const UPLOAD_PENDING_LOADING_SRC = new URL("/src/assets/images/common/placeholders/upload-loading.png", import.meta.url).href; type UploadPendingIndicatorProps = { className?: string; diff --git a/src/components/UserAvatarMenu/index.tsx b/src/components/UserAvatarMenu/index.tsx index 858e68e..e1233b8 100644 --- a/src/components/UserAvatarMenu/index.tsx +++ b/src/components/UserAvatarMenu/index.tsx @@ -30,9 +30,9 @@ interface UserAvatarMenuProps { } const USER_AVATAR_MENU_ASSETS = { - defaultAvatar: new URL("@/assets/images/avatar.png", import.meta.url).href, - copy: new URL("@/assets/images/ic_copy.png", import.meta.url).href, - stars: new URL("@/assets/images/ic_points_active.png", import.meta.url).href, + defaultAvatar: new URL("@/assets/images/common/avatars/default-user-avatar.png", import.meta.url).href, + copy: new URL("@/assets/images/common/icons/copy.png", import.meta.url).href, + stars: new URL("@/assets/images/points/points-active.png", import.meta.url).href, profile: new URL("@/assets/images/layout/nav/user-menu-profile.png", import.meta.url).href, apiKey: new URL("@/assets/images/layout/nav/user-menu-api-key.png", import.meta.url).href, invite: new URL("@/assets/images/layout/nav/user-menu-invite.png", import.meta.url).href, @@ -250,13 +250,13 @@ export function UserAvatarMenu({ user, variant = "layout", afterLoginRedirectPat aria-label={t("layout.userMenu.aria")} aria-expanded={open} > - {userName} + {userName} {open ? (
- +
{userName}
diff --git a/src/components/VoiceLibraryPicker/VoiceLibraryPickerPanel.tsx b/src/components/VoiceLibraryPicker/VoiceLibraryPickerPanel.tsx index 72447bd..90b84ba 100644 --- a/src/components/VoiceLibraryPicker/VoiceLibraryPickerPanel.tsx +++ b/src/components/VoiceLibraryPicker/VoiceLibraryPickerPanel.tsx @@ -38,8 +38,8 @@ export type VoiceLibraryPickerPanelProps = { }; const VOICE_LIBRARY_PICKER_MENU_ASSETS = { - delete: new URL("@/assets/images/ic_delete.png", import.meta.url).href, - more: new URL("@/assets/images/ic_more.png", import.meta.url).href, + delete: new URL("@/assets/images/common/icons/delete.png", import.meta.url).href, + more: new URL("@/assets/images/common/icons/more.png", import.meta.url).href, } as const; type VoiceLibraryPickerActionsProps = { @@ -230,7 +230,7 @@ export function VoiceLibraryPickerPanel({ >
- + {isPreviewPlaying ? : null}

{t("pages.assetDetails.detailRepair.title")}

@@ -742,7 +742,7 @@ export function AssetDetailRepairModal({ visible, imageUrl, originalRatio = "", aria-label={t("pages.assetDetails.detailRepair.createAria", { credits: createCost })} onClick={() => { void handleCreate(); }} > - + {createCost}
diff --git a/src/dialog/asset-details/components/AssetExpandImageModal/index.tsx b/src/dialog/asset-details/components/AssetExpandImageModal/index.tsx index 142d2db..175b017 100644 --- a/src/dialog/asset-details/components/AssetExpandImageModal/index.tsx +++ b/src/dialog/asset-details/components/AssetExpandImageModal/index.tsx @@ -498,7 +498,7 @@ export function AssetExpandImageModal({ visible, imageUrl, originalRatio = "", o

{t("pages.assetDetails.expandImage.title")}

@@ -609,7 +609,7 @@ export function AssetExpandImageModal({ visible, imageUrl, originalRatio = "", o aria-label={t("pages.assetDetails.expandImage.createAria", { credits: createCost })} onClick={() => { void handleCreate(); }} > - + {createCost}
diff --git a/src/dialog/asset-details/index.tsx b/src/dialog/asset-details/index.tsx index cab8ff8..285d8f6 100644 --- a/src/dialog/asset-details/index.tsx +++ b/src/dialog/asset-details/index.tsx @@ -40,13 +40,13 @@ import { AssetExpandImageModal } from "@/dialog/asset-details/components/AssetEx import { AssetDetailRepairModal } from "@/dialog/asset-details/components/AssetDetailRepairModal"; import "./index.scss"; -const VOICE_TEMPLATE_COVER_DEFAULT_SRC = new URL("/src/assets/images/voice-library/ic_cover_default-display.jpg", import.meta.url).href; +const VOICE_TEMPLATE_COVER_DEFAULT_SRC = new URL("/src/assets/images/voice/library/cover-default-display.jpg", import.meta.url).href; const VOICE_TEMPLATE_IMG = { - play: new URL("/src/assets/images/voice-library/ic_play.png", import.meta.url).href, - pause: new URL("/src/assets/images/voice-library/ic_pause.png", import.meta.url).href, + play: new URL("/src/assets/images/voice/library/play.png", import.meta.url).href, + pause: new URL("/src/assets/images/voice/library/pause.png", import.meta.url).href, }; -const ASSET_DETAIL_LIKE_ICON_SRC = new URL("/src/assets/images/ic_like.png", import.meta.url).href; -const ASSET_DETAIL_LIKE_ACTIVE_ICON_SRC = new URL("/src/assets/images/ic_like_active.png", import.meta.url).href; +const ASSET_DETAIL_LIKE_ICON_SRC = new URL("/src/assets/images/common/icons/like.png", import.meta.url).href; +const ASSET_DETAIL_LIKE_ACTIVE_ICON_SRC = new URL("/src/assets/images/common/icons/like-active.png", import.meta.url).href; const ASSET_DETAIL_HIDE_REUSE_ACTION_SUB_TYPES = new Set([110, 112]); function getVoiceTemplateTitle(item: VoiceLibraryItem, fallback: string) { @@ -709,14 +709,14 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation if (asset.type === 1 || (asset.type === 4 && asset.taskInfo?.type === 1)) { return (
- +
); } if (asset.type === 2 || (asset.type === 4 && asset.taskInfo?.type === 2)) { return (
-
); } @@ -731,7 +731,7 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation })()}
- onClose?.()} /> + onClose?.()} />
{showEmbeddedComposer ? ( @@ -744,10 +744,10 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation {navigation ? (
) : null} @@ -760,7 +760,7 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation
- +

{asset.userName}

{!asset.userFollowed ? ( {saveFolderSubmenuOpen ? ( @@ -821,17 +821,17 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation
{isOwnAsset && !isAudioAsset ? ( ) : null} {isOwnAsset ? ( ) : null} @@ -839,7 +839,7 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation } >
@@ -858,7 +858,7 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation

{t("pages.assetDetails.promptLabel")}

{asset?.taskInfo?.chatPrompt}

{/*
onCopyPrompt(asset?.taskInfo?.chatPrompt)}> - + @@ -871,7 +871,7 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation {asset.taskInfo?.aiModelName ? ( <> - + {asset.taskInfo.aiModelName} @@ -994,7 +994,7 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation

{t("pages.assetDetails.voiceTemplateLabel")}

- + {voiceTemplatePreviewPlaying ?
@@ -1126,7 +1126,7 @@ export default function AssetDetailsDialog({ assetId: initialAssetId, navigation

{asset.published ? t("pages.assetDetails.unpublish") : t("pages.assetDetails.publish")}

{asset.published ? t("pages.assetDetails.unpublishConfirmDesc") : t("pages.assetDetails.publishConfirmDesc")}

diff --git a/src/dialog/character-detail/index.tsx b/src/dialog/character-detail/index.tsx index 2419233..2328325 100644 --- a/src/dialog/character-detail/index.tsx +++ b/src/dialog/character-detail/index.tsx @@ -43,8 +43,8 @@ export type { CharacterDetailListSource, CharacterDetailNavigation } from "@/dia /** 角色详情侧栏拉取资产工具箱时使用的 taskType(与 /anime/assetDetailToolbox/list 约定一致) */ const CHARACTER_DETAIL_MEDIA_TOOLBOX_TASK_TYPE = 5; -const CHARACTER_DETAIL_LIKE_ICON_SRC = new URL("/src/assets/images/ic_like.png", import.meta.url).href; -const CHARACTER_DETAIL_LIKE_ACTIVE_ICON_SRC = new URL("/src/assets/images/ic_like_active.png", import.meta.url).href; +const CHARACTER_DETAIL_LIKE_ICON_SRC = new URL("/src/assets/images/common/icons/like.png", import.meta.url).href; +const CHARACTER_DETAIL_LIKE_ACTIVE_ICON_SRC = new URL("/src/assets/images/common/icons/like-active.png", import.meta.url).href; interface UserFileTreeNode { id: number; @@ -871,10 +871,10 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati
{isDetailLoading ?
: null} - +
- +
{showEmbeddedComposer ? ( @@ -887,10 +887,10 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati {navigation ? (
) : ( @@ -903,7 +903,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati
{characterDetail.userInfo && isUserUploadCharacter ? (
- +

{characterDetail.userInfo.name}

{!characterDetail.userInfo.userFollowed ? (
) : !isUserUploadCharacter && characterDetail?.id != null && characterDetail.id !== "" ? (
- +

Popi

) : ( @@ -971,12 +971,12 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati ) : null}
{isUserUploadCharacter && isOwnCharacter ? ( ) : null} @@ -984,7 +984,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati } >
@@ -999,10 +999,10 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati
- + {isOwnCharacter && isUserUploadCharacter ? ( ) : null}
@@ -1012,7 +1012,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati {!isEditingCharacterName ? ( <>
{characterDetail.title}
- {isOwnCharacter && } + {isOwnCharacter && } ) : (
@@ -1057,13 +1057,13 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati aria-expanded={certificateProofModalOpen} aria-controls="character-detail-certificate-proof-dialog" > - + {t("character_detail.deposit_status_yes")} - + ) : ( )} @@ -1086,7 +1086,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati {!isEditingDescription ? ( <>

{characterDetail.description}

- {isOwnCharacter && } + {isOwnCharacter && } ) : ( - + {characterDetail.taskInfo?.aiModelName ?? ""} @@ -1206,7 +1206,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati void requestApplyMedia({ subType: 202, mediaItems: [{ type: "image", url: String(characterDetail.threeViewImage ?? "") }], mediaMode: "replace" }); }} > - + 生成视频
@@ -1214,11 +1214,11 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati {/* 使用角色 / 做个角色 */}
- + {t("character_detail.use_character")}
@@ -1240,10 +1240,10 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati
- +
- +
@@ -1254,7 +1254,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati

@@ -1264,7 +1264,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati
{t("character_detail.deposit_guide_agree_prefix")} @@ -1288,7 +1288,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati @@ -1303,7 +1303,7 @@ export default function CharacterDetailDialog({ id: initialCharacterId, navigati ? createPortal(
setCertificateProofModalOpen(false)}>
, document.body, diff --git a/src/dialog/character-picker/index.tsx b/src/dialog/character-picker/index.tsx index 27c78c8..bef171c 100644 --- a/src/dialog/character-picker/index.tsx +++ b/src/dialog/character-picker/index.tsx @@ -19,13 +19,10 @@ export interface CharacterPickerDialogProps { } function parseCharacterListPayload(res: unknown): any[] { - const result = res as { data?: { list?: unknown[] }; list?: unknown[] }; + const result = res as { data?: { list?: unknown[] } }; if (Array.isArray(result?.data?.list)) { return result.data.list as any[]; } - if (Array.isArray(result?.list)) { - return result.list as any[]; - } return []; } @@ -384,13 +381,13 @@ export default function CharacterPickerDialog({ onClose, onSelect }: CharacterPi return (

{t("pages.characters.my_section")}

@@ -503,7 +500,7 @@ export default function CharacterPickerDialog({ onClose, onSelect }: CharacterPi handlePick(item); }} > - + {item.title} ))} diff --git a/src/dialog/contact-service/index.tsx b/src/dialog/contact-service/index.tsx index 48660a7..d8dab9a 100644 --- a/src/dialog/contact-service/index.tsx +++ b/src/dialog/contact-service/index.tsx @@ -13,14 +13,14 @@ export default function ContactServiceDialog({ onClose }: ContactServiceDialogPr return (

{t("contactService.title")}

{t("contactService.description")}

{serviceQrCode ? (
- +
) : (
diff --git a/src/dialog/copyright-deposit/index.tsx b/src/dialog/copyright-deposit/index.tsx index 0566341..e62481e 100644 --- a/src/dialog/copyright-deposit/index.tsx +++ b/src/dialog/copyright-deposit/index.tsx @@ -17,14 +17,14 @@ export default function CopyrightDepositDialog({ onClose }: CopyrightDepositDial
- {t("pages.characters.copyright_deposit_modal_step_1_alt")} + {t("pages.characters.copyright_deposit_modal_step_1_alt")} 1
- +
- {t("pages.characters.copyright_deposit_modal_step_2_alt")} + {t("pages.characters.copyright_deposit_modal_step_2_alt")} 2 diff --git a/src/dialog/folders-asset-details/index.tsx b/src/dialog/folders-asset-details/index.tsx index 8f6b66c..656317d 100644 --- a/src/dialog/folders-asset-details/index.tsx +++ b/src/dialog/folders-asset-details/index.tsx @@ -271,12 +271,12 @@ export default function FoldersAssetDetailsDialog({ assetId: initialAssetId, par
{detailFileCategory === "video" && detailUrl ? (
-
) : null} {detailFileCategory === "image" && detailUrl ? (
- +
) : null} {detailFileCategory === "audio" && detailUrl ? ( @@ -286,7 +286,7 @@ export default function FoldersAssetDetailsDialog({ assetId: initialAssetId, par ) : null}
- onClose?.()} /> + onClose?.()} />
@@ -294,10 +294,10 @@ export default function FoldersAssetDetailsDialog({ assetId: initialAssetId, par {navigation ? (
) : null} @@ -310,7 +310,7 @@ export default function FoldersAssetDetailsDialog({ assetId: initialAssetId, par
- +

{detailUserName || detailName || "-"}

@@ -357,18 +357,18 @@ export default function FoldersAssetDetailsDialog({ assetId: initialAssetId, par ) : null}
} >
@@ -436,7 +436,7 @@ export default function FoldersAssetDetailsDialog({ assetId: initialAssetId, par )}
diff --git a/src/dialog/gateway-apikey/index.tsx b/src/dialog/gateway-apikey/index.tsx index 18d25cc..3d7e24c 100644 --- a/src/dialog/gateway-apikey/index.tsx +++ b/src/dialog/gateway-apikey/index.tsx @@ -112,7 +112,7 @@ export default function GatewayApikeyDialog() { }} aria-label={t("gatewayApiKey.copyKey")} > - +
diff --git a/src/dialog/material-picker/index.tsx b/src/dialog/material-picker/index.tsx index e901db0..4145d29 100644 --- a/src/dialog/material-picker/index.tsx +++ b/src/dialog/material-picker/index.tsx @@ -171,14 +171,14 @@ export default function MaterialPickerDialog({ onClose, onSelect, initialCategor return (

{t("pages.create.inspiration.filters.characterAssets")}

@@ -250,7 +250,7 @@ export default function MaterialPickerDialog({ onClose, onSelect, initialCategor handlePick(item); }} > - + {t("pages.create.inspiration.materialLibrary.useMaterial")} ))} diff --git a/src/dialog/pay-subscribe/index.tsx b/src/dialog/pay-subscribe/index.tsx index 9519600..7e4f0da 100644 --- a/src/dialog/pay-subscribe/index.tsx +++ b/src/dialog/pay-subscribe/index.tsx @@ -481,9 +481,9 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P } }, [fetchAlipayAndRedirect, fetchWxOrder, paymentMethod, resumeWxPolling]); - const wxLogo = new URL("@/assets/images/logo_wxpay.png", import.meta.url).href; - const alipayLogo = new URL("@/assets/images/logo_alipay.png", import.meta.url).href; - const loadingImg = new URL("@/assets/images/wxgh_loading.png", import.meta.url).href; + const wxLogo = new URL("@/assets/images/common/payments/wxpay-logo.png", import.meta.url).href; + const alipayLogo = new URL("@/assets/images/common/payments/alipay-logo.png", import.meta.url).href; + const loadingImg = new URL("@/assets/images/common/payments/wechat-loading.png", import.meta.url).href; return (
diff --git a/src/dialog/points-detail-modal/index.tsx b/src/dialog/points-detail-modal/index.tsx index 45d8f25..89a3120 100644 --- a/src/dialog/points-detail-modal/index.tsx +++ b/src/dialog/points-detail-modal/index.tsx @@ -6,9 +6,9 @@ import { getInviteCodeIsUsed, getInviteCodeList, getInviteCodeLogList, getUserPo import { memberLevelMap } from "@/constants"; import { useCopyText } from "@/hooks/useCopyText"; import { useUserStore } from "@/store/useUserStore"; -import inviteRewardBubbleImg from "@/assets/images/points-invite-reward-bubble.png"; -import inviteRewardCharacterImg from "@/assets/images/points-invite-reward-character-display.png"; -import inviteRewardSparkleImg from "@/assets/images/character-deposit-guide-modal/sparkle.png"; +import inviteRewardBubbleImg from "@/assets/images/points/invite-reward-bubble.png"; +import inviteRewardCharacterImg from "@/assets/images/points/invite-reward-character-display.png"; +import inviteRewardSparkleImg from "@/assets/images/characters/deposit-guide-sparkle.png"; import "./index.scss"; /** 侧边栏页签:积分详情 | 我的邀请码 | 邀请码奖励 */ @@ -27,9 +27,9 @@ const SCROLL_LOAD_MORE_THRESHOLD_PX = 24; const INVITE_FRIEND_REGISTER_REWARD_POINTS = 200; const POINTS_DETAIL_ASSETS = { - defaultAvatar: new URL("@/assets/images/avatar.png", import.meta.url).href, - copy: new URL("@/assets/images/ic_copy.png", import.meta.url).href, - check: new URL("@/assets/images/index/feature_check_icon.png", import.meta.url).href, + defaultAvatar: new URL("@/assets/images/common/avatars/default-user-avatar.png", import.meta.url).href, + copy: new URL("@/assets/images/common/icons/copy.png", import.meta.url).href, + check: new URL("@/assets/images/home/feature-check-icon.png", import.meta.url).href, } as const; /** 积分流水单条记录(/users/userPointsLog/list) */ @@ -616,7 +616,7 @@ export default function PointsDetailModal({ initialTab = "points", onRecharge }:
- +
{userName} diff --git a/src/dialog/points-recharge/index.tsx b/src/dialog/points-recharge/index.tsx index a6df0b6..c5076a4 100644 --- a/src/dialog/points-recharge/index.tsx +++ b/src/dialog/points-recharge/index.tsx @@ -73,7 +73,7 @@ export default function PointsRecharge({ title }: PointsRechargeProps) {
{title ?? t("points_recharge.title")}
- +
{userName}
@@ -81,7 +81,7 @@ export default function PointsRecharge({ title }: PointsRechargeProps) {
ID:{userId}
@@ -102,7 +102,7 @@ export default function PointsRecharge({ title }: PointsRechargeProps) { }} >
- +
{opt.points_amount}
¥{opt.price_amount}
diff --git a/src/dialog/user-edit/index.tsx b/src/dialog/user-edit/index.tsx index a812ae4..ada9c67 100644 --- a/src/dialog/user-edit/index.tsx +++ b/src/dialog/user-edit/index.tsx @@ -19,9 +19,9 @@ interface UserEditProps { const NAME_LIMIT = 15; const SIGNATURE_LIMIT = 50; const TextArea = Input.TextArea; -const USER_EDIT_CLOSE_ICON_SRC = new URL("@/assets/images/ic_close.png", import.meta.url).href; -const USER_EDIT_DEFAULT_AVATAR_SRC = new URL("@/assets/images/avatar.png", import.meta.url).href; -const USER_EDIT_AVATAR_LOADING_SRC = new URL("@/assets/images/upload_loading.png", import.meta.url).href; +const USER_EDIT_CLOSE_ICON_SRC = new URL("@/assets/images/common/icons/close.png", import.meta.url).href; +const USER_EDIT_DEFAULT_AVATAR_SRC = new URL("@/assets/images/common/avatars/default-user-avatar.png", import.meta.url).href; +const USER_EDIT_AVATAR_LOADING_SRC = new URL("@/assets/images/common/placeholders/upload-loading.png", import.meta.url).href; export default function UserEdit({ initialData, onClose, onSaved }: UserEditProps) { const { t } = useTranslation(); @@ -121,7 +121,7 @@ export default function UserEdit({ initialData, onClose, onSaved }: UserEditProp
- {tt("pages.profile.avatarAlt", + {tt("pages.profile.avatarAlt", {showAvatarStatus ? ( diff --git a/src/dialog/voice-audio-upload/index.tsx b/src/dialog/voice-audio-upload/index.tsx index a2acc6c..b642105 100644 --- a/src/dialog/voice-audio-upload/index.tsx +++ b/src/dialog/voice-audio-upload/index.tsx @@ -8,10 +8,10 @@ import { openLatestPrivacyPolicyLink, PRIVACY_POLICY_KEY_PRIVACY_POLICY, PRIVACY import "./index.scss"; const VOICE_AUDIO_UPLOAD_IMG = { - close: new URL("/src/assets/images/ic_close.png", import.meta.url).href, - upload: new URL("/src/assets/images/ic_publish.png", import.meta.url).href, - checkboxOn: new URL("/src/assets/images/ic_checkbox_agreement_on.png", import.meta.url).href, - checkboxOff: new URL("/src/assets/images/ic_checkbox_agreement_off.png", import.meta.url).href, + close: new URL("/src/assets/images/common/icons/close.png", import.meta.url).href, + upload: new URL("/src/assets/images/common/icons/upload.png", import.meta.url).href, + checkboxOn: new URL("/src/assets/images/common/icons/checkbox-agreement-on.png", import.meta.url).href, + checkboxOff: new URL("/src/assets/images/common/icons/checkbox-agreement-off.png", import.meta.url).href, }; const MATERIAL_UPLOAD_ACCEPT = ".pdf,.txt,.html,.docx,application/pdf,text/plain,text/html,application/vnd.openxmlformats-officedocument.wordprocessingml.document"; diff --git a/src/layouts/Layout/Layout.scss b/src/layouts/Layout/Layout.scss index c54e94f..f07f033 100644 --- a/src/layouts/Layout/Layout.scss +++ b/src/layouts/Layout/Layout.scss @@ -179,6 +179,7 @@ max-width: 100%; } + .layoutShell__topbarNotificationTool, .layoutShell__topbarServiceTool { position: relative; flex-shrink: 0; @@ -237,6 +238,282 @@ object-fit: contain; } + .layoutShell__topbarNotificationBtn { + position: relative; + } + + .layoutShell__topbarNotificationDot { + position: absolute; + top: 2px; + right: 2px; + width: 6px; + height: 6px; + border-radius: 50%; + background: #cc4646; + pointer-events: none; + } + + .layoutShell__notificationPopover { + position: absolute; + top: calc(100% + 18px); + left: 0; + z-index: 1300; + width: 327px; + height: 581px; + padding: 15px; + border-radius: 26px; + background: #ffffff; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: visible; + } + + .layoutShell__notificationTabs { + width: 228px; + height: 28px; + margin: 0 auto; + display: flex; + align-items: center; + gap: 10px; + } + + .layoutShell__notificationTabBtn { + width: 109px; + height: 28px; + padding: 4px 20px; + border: 0; + border-radius: 18px; + background: #ffffff; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + gap: 3px; + color: #333333; + cursor: pointer; + } + + .layoutShell__notificationTabBtn:hover, + .layoutShell__notificationTabBtn:focus-visible { + background: #ffffff; + outline: none; + } + + .layoutShell__notificationTabBtn_active, + .layoutShell__notificationTabBtn_active:hover, + .layoutShell__notificationTabBtn_active:focus-visible { + background: #f0f4f9; + color: #6f47f5; + } + + .layoutShell__notificationTabText { + color: currentcolor; + font-size: 14px; + font-weight: 400; + line-height: 20px; + white-space: nowrap; + } + + .layoutShell__notificationTabBadge { + width: 10px; + height: 10px; + padding: 2px; + border-radius: 5px; + background: #cc4646; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + font-size: 8px; + font-weight: 400; + line-height: 11px; + white-space: nowrap; + } + + .layoutShell__notificationList { + margin-top: 10px; + min-height: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + scrollbar-width: none; + } + + .layoutShell__notificationList::-webkit-scrollbar { + display: none; + } + + .layoutShell__notificationCard { + width: 297px; + padding: 20px; + border: 0; + border-radius: 20px; + background: #f8f8f8; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + cursor: pointer; + text-align: left; + flex-shrink: 0; + } + + .layoutShell__notificationCard:hover, + .layoutShell__notificationCard:focus-visible, + .layoutShell__notificationCard_selected { + background: #f8f8f8; + outline: none; + } + + .layoutShell__notificationCardHeader { + height: 22px; + display: flex; + align-items: center; + gap: 5px; + } + + .layoutShell__notificationCardTitle { + min-width: 0; + color: #333333; + font-size: 14px; + font-weight: 400; + line-height: 22px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .layoutShell__notificationUnreadDot { + width: 6px; + height: 6px; + flex-shrink: 0; + display: block; + object-fit: contain; + } + + .layoutShell__notificationCardSummary { + margin-top: 10px; + overflow: hidden; + color: #4f4f4f; + font-size: 12px; + font-weight: 400; + line-height: 16.3px; + word-break: break-word; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + } + + .layoutShell__notificationCardTime { + height: 13px; + margin-top: 10px; + color: #999999; + font-size: 12px; + font-weight: 400; + line-height: 13px; + white-space: nowrap; + } + + .layoutShell__notificationState { + width: 297px; + height: 144px; + margin: 0; + border-radius: 20px; + background: #f8f8f8; + color: #999999; + font-size: 12px; + font-weight: 400; + line-height: 144px; + text-align: center; + flex-shrink: 0; + } + + .layoutShell__notificationDetailFloat { + position: absolute; + left: -307px; + z-index: 1310; + width: 297px; + padding: 20px; + border-radius: 20px; + background: #ffffff; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + box-sizing: border-box; + display: flex; + flex-direction: column; + top: calc(100% + 33px); + } + + .layoutShell__notificationDetailHeader { + height: 22px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .layoutShell__notificationDetailTitle { + height: 22px; + margin: 0; + display: flex; + align-items: center; + min-width: 0; + color: #333333; + font-size: 14px; + font-weight: 400; + line-height: 22px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .layoutShell__notificationDetailAction { + width: 68px; + height: 22px; + padding: 0; + border: 0; + border-radius: 100px; + background: #6f47f5; + color: #ffffff; + font-size: 12px; + font-weight: 400; + line-height: 22px; + text-align: center; + white-space: nowrap; + cursor: pointer; + flex-shrink: 0; + } + + .layoutShell__notificationDetailAction:hover, + .layoutShell__notificationDetailAction:focus-visible { + background: #6f47f5; + outline: none; + } + + .layoutShell__notificationDetailContent { + margin: 10px 0 0; + color: #4f4f4f; + font-size: 12px; + font-weight: 400; + line-height: 16.3px; + word-break: break-word; + } + + .layoutShell__notificationDetailTime { + height: 13px; + margin: 10px 0 0; + color: #999999; + font-size: 12px; + font-weight: 400; + line-height: 13px; + white-space: nowrap; + } + .layoutShell__topbarIconBtn { width: 31px; height: 31px; @@ -796,7 +1073,7 @@ grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); - > .layoutShellScaleSpacer { + >.layoutShellScaleSpacer { grid-area: content; overflow: hidden; } @@ -814,7 +1091,7 @@ scrollbar-width: none; -ms-overflow-style: none; - > .createPage { + >.createPage { height: 100%; max-height: 100%; min-height: 0; @@ -889,6 +1166,7 @@ .character-detail-arco-modal, .asset-details-arco-modal { + &.zoomModal-appear, &.zoomModal-enter, &.zoomModal-appear-active, @@ -902,7 +1180,7 @@ } } - .arco-modal-wrapper > * { + .arco-modal-wrapper>* { flex: 1 1 0; min-width: 0; min-height: 0; @@ -923,8 +1201,8 @@ flex-direction: column; } - .arco-modal-wrapper .asset-details-arco-modal.arco-modal > *, - .arco-modal-wrapper .character-detail-arco-modal.arco-modal > * { + .arco-modal-wrapper .asset-details-arco-modal.arco-modal>*, + .arco-modal-wrapper .character-detail-arco-modal.arco-modal>* { flex: 1 1 0; min-width: 0; min-height: 0; @@ -1060,4 +1338,4 @@ display: none; } } -} +} \ No newline at end of file diff --git a/src/layouts/Layout/Layout.tsx b/src/layouts/Layout/Layout.tsx index 00c4f62..5f1848a 100644 --- a/src/layouts/Layout/Layout.tsx +++ b/src/layouts/Layout/Layout.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { useShallow } from "zustand/react/shallow"; import "./Layout.scss"; +import { getNotificationCount, getNotificationList, readNotification, type NotificationListItem } from "@/api/api"; import { memberLevelMap, ROUTES } from "@/constants"; import { hasPendingExploreTemplateTabIntent } from "@/store/useExploreFeedTabStore"; import { isLayoutContentScrollLocked, isScrollEventFromLayoutModalMount, isScrollEventInLayoutContentScroll, setLayoutContentModalMount, setLayoutContentScrollRoot } from "@/layouts/layoutContentModalMount"; @@ -16,6 +17,7 @@ import { useParamConfigStore } from "@/store/useParamConfigStore"; import { useUserStore } from "@/store/useUserStore"; import { getToken } from "@/utils/auth"; import { buildCanvasUrl, CANVAS_BASE_URL } from "@/utils/canvasUrl"; +import { formatLocalDate } from "@/utils"; import { openLatestPrivacyPolicyLink, PRIVACY_POLICY_KEY_AI_USE_NOTICE, PRIVACY_POLICY_KEY_PRIVACY_POLICY, PRIVACY_POLICY_KEY_USAGE_TUTORIAL, PRIVACY_POLICY_KEY_USER_TERMS } from "@/utils/openLatestPrivacyPolicyLink"; import { LAYOUT_CONTENT_DESIGN_WIDTH, useLayoutViewportScale } from "@/hooks/useViewportScale"; import { GlobalPromptPanel } from "@/layouts/globalPromptPanel/globalPromptPanel"; @@ -39,6 +41,26 @@ interface LayoutSideNavIconProps { item: LayoutSideNavItem; } +type LayoutNotificationTabId = "system" | "personal"; + +interface LayoutNotificationTab { + id: LayoutNotificationTabId; + labelKey: string; +} + +type LayoutNotificationItemsByTab = Record; +type LayoutNotificationLoadingByTab = Record; +type LayoutNotificationUnreadCountByTab = Record; +type LayoutNotificationJump = { + type: "page" | "url"; + value: string; +}; +type LayoutNotificationBroadcastMessage = { + type: "read"; + id: string; + tabId: LayoutNotificationTabId; +}; + const LAYOUT_NAV_ASSETS = { logo: new URL("@/assets/images/layout/nav/popi-logo.png", import.meta.url).href, explore: new URL("@/assets/images/layout/nav/explore.png", import.meta.url).href, @@ -59,11 +81,13 @@ const LAYOUT_NAV_ASSETS = { topNotification: new URL("@/assets/images/layout/nav/top-notification.png", import.meta.url).href, topHelp: new URL("@/assets/images/layout/nav/top-language.png", import.meta.url).href, topService: new URL("@/assets/images/layout/nav/top-weixin.png", import.meta.url).href, - topLoginArrow: new URL("@/assets/images/index/header_login_arrow_icon.png", import.meta.url).href, - points: new URL("@/assets/images/ic_points_active.png", import.meta.url).href, - arrowRightGray: new URL("@/assets/images/ic_arrow_right_gray.png", import.meta.url).href, + topLoginArrow: new URL("@/assets/images/home/header-login-arrow-icon.png", import.meta.url).href, + points: new URL("@/assets/images/points/points-active.png", import.meta.url).href, + arrowRightGray: new URL("@/assets/images/common/icons/arrow-right-gray.png", import.meta.url).href, } as const; +const LAYOUT_NOTIFICATION_UNREAD_DOT_SRC = "https://www.figma.com/api/mcp/asset/d7fad30a-c786-4175-84a2-d921459889a4"; + // 侧栏图标顺序与 2026 7月官网 Figma 设计保持一致。 const LAYOUT_SIDE_NAV_ITEMS: readonly LayoutSideNavItem[] = [ { @@ -105,6 +129,15 @@ const LAYOUT_SIDE_NAV_ITEMS: readonly LayoutSideNavItem[] = [ ]; const LAYOUT_PROMO_BAR_DISMISSED_KEY = "popiart.layout.promoBar.dismissed"; +const LAYOUT_NOTIFICATION_BROADCAST_CHANNEL = "popiart.layout.notification"; +const LAYOUT_NOTIFICATION_IDLE_POLL_INTERVAL_MS = 60 * 1000; +const LAYOUT_NOTIFICATION_OPEN_POLL_INTERVAL_MS = 30 * 1000; +const LAYOUT_NOTIFICATION_OPEN_LIST_REFRESH_INTERVAL_MS = 2 * 60 * 1000; + +const LAYOUT_NOTIFICATION_TABS: readonly LayoutNotificationTab[] = [ + { id: "system", labelKey: "layout.notifications.tabs.system" }, + { id: "personal", labelKey: "layout.notifications.tabs.personal" }, +]; function isLayoutPromoBarDismissed(): boolean { try { @@ -120,6 +153,121 @@ function readUserNumericValue(user: Record | null, key: string, return Number.isFinite(numericValue) ? numericValue : fallback; } +function createEmptyNotificationItemsByTab(): LayoutNotificationItemsByTab { + return { + system: [], + personal: [], + }; +} + +function createNotificationLoadingByTab(loading: boolean): LayoutNotificationLoadingByTab { + return { + system: loading, + personal: loading, + }; +} + +function createNotificationUnreadCountByTab(): LayoutNotificationUnreadCountByTab { + return { + system: 0, + personal: 0, + }; +} + +function getLayoutNotificationItemId(item: NotificationListItem, index: number): string { + return item.id == null ? `notification-${index}` : String(item.id); +} + +function getLayoutNotificationStableId(item: NotificationListItem): string | null { + return item.id == null ? null : String(item.id); +} + +function getLayoutNotificationText(value: string | undefined): string { + return value?.trim() ?? ""; +} + +function isLayoutNotificationUnread(item: NotificationListItem): boolean { + return Number(item.readStatus) === 0; +} + +function applyLayoutNotificationLocalReadState(items: NotificationListItem[], locallyReadIds: ReadonlySet): NotificationListItem[] { + let hasChange = false; + const nextItems = items.map((item) => { + const itemId = getLayoutNotificationStableId(item); + if (itemId == null || !locallyReadIds.has(itemId) || !isLayoutNotificationUnread(item)) return item; + hasChange = true; + return { ...item, readStatus: 1 }; + }); + return hasChange ? nextItems : items; +} + +function markLayoutNotificationItemRead(items: NotificationListItem[], itemId: string): NotificationListItem[] { + return items.map((item) => (getLayoutNotificationStableId(item) === itemId ? { ...item, readStatus: 1 } : item)); +} + +function markLayoutNotificationItemsByTabRead(itemsByTab: LayoutNotificationItemsByTab, itemId: string): LayoutNotificationItemsByTab { + return { + system: markLayoutNotificationItemRead(itemsByTab.system, itemId), + personal: markLayoutNotificationItemRead(itemsByTab.personal, itemId), + }; +} + +function isLayoutNotificationBroadcastMessage(value: unknown): value is LayoutNotificationBroadcastMessage { + if (typeof value !== "object" || value == null) return false; + const message = value as Partial; + return message.type === "read" && typeof message.id === "string" && (message.tabId === "system" || message.tabId === "personal"); +} + +function getLayoutNotificationJump(item: NotificationListItem): LayoutNotificationJump | null { + const linkType = item.linkType?.trim().toLowerCase(); + const linkValue = item.linkValue?.trim(); + if (!linkValue) return null; + if (linkType === "page") return { type: "page", value: linkValue }; + if (linkType === "url") return { type: "url", value: linkValue }; + return null; +} + +function normalizeNotificationPagePath(value: string): string { + if (value.startsWith("/") || value.startsWith("#") || value.startsWith("?")) return value; + return `/${value}`; +} + +function normalizeNotificationExternalUrl(value: string): string { + if (/^https?:\/\//i.test(value)) return value; + if (value.startsWith("//")) return `${window.location.protocol}${value}`; + return `https://${value}`; +} + +function formatLayoutNotificationBadgeCount(count: number): string { + return count > 9 ? "9+" : String(count); +} + +function normalizeLayoutNotificationCount(value: number | undefined): number { + if (typeof value !== "number" || !Number.isFinite(value)) return 0; + return Math.max(0, Math.floor(value)); +} + +function formatLayoutNotificationTime(value: string | undefined, locale: string): string { + const rawValue = value?.trim(); + if (!rawValue) return ""; + const date = new Date(rawValue); + const time = date.getTime(); + if (!Number.isFinite(time)) return rawValue; + + const diffMs = time - Date.now(); + const absDiffMs = Math.abs(diffMs); + const minuteMs = 60 * 1000; + const hourMs = 60 * minuteMs; + const dayMs = 24 * hourMs; + + const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); + if (absDiffMs < minuteMs) return formatter.format(0, "second"); + if (absDiffMs < hourMs) return formatter.format(Math.round(diffMs / minuteMs), "minute"); + if (absDiffMs < dayMs) return formatter.format(Math.round(diffMs / hourMs), "hour"); + if (absDiffMs < 30 * dayMs) return formatter.format(Math.round(diffMs / dayMs), "day"); + return formatLocalDate(time, "YYYY-MM-DD"); +} + const LAYOUT_FOOTER_NAV_ITEMS: readonly LayoutSideNavItem[] = [ { id: "subscribe", @@ -158,7 +306,7 @@ function LayoutSideNavIcon({ item }: LayoutSideNavIconProps) { export function Layout({ children }: LayoutProps) { const { pathname, search } = useLocation(); const navigate = useNavigate(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { user, setUser, clearUser } = useUserStore( useShallow((s) => ({ user: s.user, @@ -170,6 +318,14 @@ export function Layout({ children }: LayoutProps) { const [footerPopoverOpen, setFooterPopoverOpen] = useState(false); const [topbarServiceQrOpen, setTopbarServiceQrOpen] = useState(false); const [userPointsPopoverOpen, setUserPointsPopoverOpen] = useState(false); + const [notificationPopoverOpen, setNotificationPopoverOpen] = useState(false); + const [activeNotificationTab, setActiveNotificationTab] = useState("system"); + const [notificationItemsByTab, setNotificationItemsByTab] = useState(() => createEmptyNotificationItemsByTab()); + const [notificationLoadingByTab, setNotificationLoadingByTab] = useState(() => createNotificationLoadingByTab(false)); + const [notificationUnreadCountByTab, setNotificationUnreadCountByTab] = useState(() => createNotificationUnreadCountByTab()); + const [notificationTotalUnreadCount, setNotificationTotalUnreadCount] = useState(0); + const [selectedNotificationId, setSelectedNotificationId] = useState(null); + const [locallyReadNotificationIds, setLocallyReadNotificationIds] = useState>(() => new Set()); const [avatarMenuCloseSignal, setAvatarMenuCloseSignal] = useState(0); const [promoBarVisible, setPromoBarVisible] = useState(() => !isLayoutPromoBarDismissed()); const activePromptPanelHost = useComposerStore((s) => s.activePromptPanelHost); @@ -183,6 +339,16 @@ export function Layout({ children }: LayoutProps) { const pointPackageCoins = readUserNumericValue(user, "pointPackageCoins"); const footerPopoverRef = useRef(null); + const notificationToolRef = useRef(null); + const notificationPopoverRef = useRef(null); + const notificationReadingIdsRef = useRef>(new Set()); + const notificationLastOpenListRefreshAtRef = useRef(0); + const notificationTotalUnreadCountRef = useRef(0); + const notificationUnreadCountByTabRef = useRef(createNotificationUnreadCountByTab()); + const notificationActiveTabRef = useRef("system"); + const locallyReadNotificationIdsRef = useRef>(new Set()); + const notificationLocalVersionRef = useRef(0); + const notificationBroadcastChannelRef = useRef(null); const serviceToolRef = useRef(null); const userPointsToolRef = useRef(null); const contentFrameShellRef = useRef(null); @@ -202,6 +368,8 @@ export function Layout({ children }: LayoutProps) { setFooterPopoverOpen(false); setTopbarServiceQrOpen(false); setUserPointsPopoverOpen(false); + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); }, []); useEffect(() => { @@ -214,11 +382,273 @@ export function Layout({ children }: LayoutProps) { void setUser(); }, [clearUser, setUser]); + const resetNotificationState = useCallback((): void => { + notificationLocalVersionRef.current += 1; + notificationReadingIdsRef.current.clear(); + notificationLastOpenListRefreshAtRef.current = 0; + notificationTotalUnreadCountRef.current = 0; + notificationUnreadCountByTabRef.current = createNotificationUnreadCountByTab(); + locallyReadNotificationIdsRef.current = new Set(); + setNotificationItemsByTab(createEmptyNotificationItemsByTab()); + setNotificationLoadingByTab(createNotificationLoadingByTab(false)); + setNotificationUnreadCountByTab(createNotificationUnreadCountByTab()); + setNotificationTotalUnreadCount(0); + setLocallyReadNotificationIds(new Set()); + setSelectedNotificationId(null); + }, []); + + const refreshNotificationTotalCount = useCallback(async (): Promise => { + if (!getToken()) { + resetNotificationState(); + return 0; + } + + try { + const requestVersion = notificationLocalVersionRef.current; + const response = await getNotificationCount(); + if (requestVersion !== notificationLocalVersionRef.current) return null; + const nextCount = normalizeLayoutNotificationCount(response.data?.num); + notificationTotalUnreadCountRef.current = nextCount; + setNotificationTotalUnreadCount(nextCount); + return nextCount; + } catch { + return null; + } + }, [resetNotificationState]); + + const refreshNotificationTabCount = useCallback(async (tabId: LayoutNotificationTabId): Promise => { + if (!getToken()) { + resetNotificationState(); + return 0; + } + + try { + const requestVersion = notificationLocalVersionRef.current; + const response = await getNotificationCount({ messageType: tabId }); + if (requestVersion !== notificationLocalVersionRef.current) return null; + const nextCount = normalizeLayoutNotificationCount(response.data?.num); + setNotificationUnreadCountByTab((prev) => { + const nextCounts = { + ...prev, + [tabId]: nextCount, + }; + notificationUnreadCountByTabRef.current = nextCounts; + return nextCounts; + }); + return nextCount; + } catch { + return null; + } + }, [resetNotificationState]); + + const refreshNotificationList = useCallback( + async (tabId: LayoutNotificationTabId, showLoading: boolean): Promise => { + if (!getToken()) { + resetNotificationState(); + return []; + } + + if (showLoading) { + setNotificationLoadingByTab((prev) => ({ + ...prev, + [tabId]: true, + })); + } + + try { + const requestVersion = notificationLocalVersionRef.current; + const response = await getNotificationList({ messageType: tabId }); + if (requestVersion !== notificationLocalVersionRef.current) return null; + if (!getToken()) return []; + const nextItems = applyLayoutNotificationLocalReadState(response.data?.list ?? [], locallyReadNotificationIdsRef.current); + setNotificationItemsByTab((prev) => ({ + ...prev, + [tabId]: nextItems, + })); + notificationLastOpenListRefreshAtRef.current = Date.now(); + return nextItems; + } catch { + return null; + } finally { + if (showLoading) { + setNotificationLoadingByTab((prev) => ({ + ...prev, + [tabId]: false, + })); + } + } + }, + [resetNotificationState], + ); + + const refreshAllNotificationPanelData = useCallback(async (): Promise => { + if (!getToken()) { + resetNotificationState(); + return; + } + + try { + const requestVersion = notificationLocalVersionRef.current; + setNotificationLoadingByTab(createNotificationLoadingByTab(true)); + const listResults = await Promise.allSettled( + LAYOUT_NOTIFICATION_TABS.map((tab) => getNotificationList({ messageType: tab.id }).then((response) => response.data?.list ?? [])), + ); + const countResults = await Promise.allSettled( + LAYOUT_NOTIFICATION_TABS.map((tab) => getNotificationCount({ messageType: tab.id }).then((response) => normalizeLayoutNotificationCount(response.data?.num))), + ); + const totalCountResult = await getNotificationCount() + .then((response) => normalizeLayoutNotificationCount(response.data?.num)) + .catch(() => null); + + if (requestVersion !== notificationLocalVersionRef.current) return; + if (!getToken()) return; + + const nextItemsByTab = createEmptyNotificationItemsByTab(); + listResults.forEach((result, index) => { + const tab = LAYOUT_NOTIFICATION_TABS[index]; + if (tab != null && result.status === "fulfilled") { + nextItemsByTab[tab.id] = applyLayoutNotificationLocalReadState(result.value, locallyReadNotificationIdsRef.current); + } + }); + + const nextUnreadCountByTab = createNotificationUnreadCountByTab(); + countResults.forEach((result, index) => { + const tab = LAYOUT_NOTIFICATION_TABS[index]; + if (tab != null && result.status === "fulfilled") { + nextUnreadCountByTab[tab.id] = result.value; + } + }); + + setNotificationItemsByTab(nextItemsByTab); + setNotificationUnreadCountByTab(nextUnreadCountByTab); + notificationUnreadCountByTabRef.current = nextUnreadCountByTab; + if (totalCountResult != null) { + notificationTotalUnreadCountRef.current = totalCountResult; + setNotificationTotalUnreadCount(totalCountResult); + } + notificationLastOpenListRefreshAtRef.current = Date.now(); + } finally { + setNotificationLoadingByTab(createNotificationLoadingByTab(false)); + } + }, [resetNotificationState]); + + useEffect(() => { + notificationTotalUnreadCountRef.current = notificationTotalUnreadCount; + }, [notificationTotalUnreadCount]); + + useEffect(() => { + notificationUnreadCountByTabRef.current = notificationUnreadCountByTab; + }, [notificationUnreadCountByTab]); + + useEffect(() => { + notificationActiveTabRef.current = activeNotificationTab; + }, [activeNotificationTab]); + + useEffect(() => { + locallyReadNotificationIdsRef.current = locallyReadNotificationIds; + }, [locallyReadNotificationIds]); + + useEffect(() => { + if (!isLogin || !getToken()) { + resetNotificationState(); + return; + } + + void refreshNotificationTotalCount(); + }, [isLogin, refreshNotificationTotalCount, resetNotificationState]); + + useEffect(() => { + if (!isLogin || typeof BroadcastChannel === "undefined") return; + + const channel = new BroadcastChannel(LAYOUT_NOTIFICATION_BROADCAST_CHANNEL); + notificationBroadcastChannelRef.current = channel; + channel.onmessage = (evt: MessageEvent) => { + if (!isLayoutNotificationBroadcastMessage(evt.data)) return; + + const { id, tabId } = evt.data; + setLocallyReadNotificationIds((prev) => { + if (prev.has(id)) return prev; + const nextIds = new Set(prev); + nextIds.add(id); + locallyReadNotificationIdsRef.current = nextIds; + return nextIds; + }); + setNotificationItemsByTab((prev) => markLayoutNotificationItemsByTabRead(prev, id)); + void refreshNotificationTotalCount(); + void refreshNotificationTabCount(tabId); + }; + + return () => { + if (notificationBroadcastChannelRef.current === channel) { + notificationBroadcastChannelRef.current = null; + } + channel.close(); + }; + }, [isLogin, refreshNotificationTabCount, refreshNotificationTotalCount]); + + useEffect(() => { + if (!isLogin || notificationPopoverOpen) return; + + const tick = (): void => { + if (document.visibilityState !== "visible") return; + void refreshNotificationTotalCount(); + }; + const onVisibilityChange = (): void => { + if (document.visibilityState === "visible") { + void refreshNotificationTotalCount(); + } + }; + + const timer = window.setInterval(tick, LAYOUT_NOTIFICATION_IDLE_POLL_INTERVAL_MS); + document.addEventListener("visibilitychange", onVisibilityChange); + return () => { + window.clearInterval(timer); + document.removeEventListener("visibilitychange", onVisibilityChange); + }; + }, [isLogin, notificationPopoverOpen, refreshNotificationTotalCount]); + + useEffect(() => { + if (!isLogin || !notificationPopoverOpen) return; + + void refreshAllNotificationPanelData(); + + const tick = async (): Promise => { + if (document.visibilityState !== "visible") return; + + const activeTab = notificationActiveTabRef.current; + const prevTotalCount = notificationTotalUnreadCountRef.current; + const prevTabCount = notificationUnreadCountByTabRef.current[activeTab]; + const totalCountResult = await refreshNotificationTotalCount(); + const tabCountResults = await Promise.all(LAYOUT_NOTIFICATION_TABS.map((tab) => refreshNotificationTabCount(tab.id))); + const activeTabIndex = LAYOUT_NOTIFICATION_TABS.findIndex((tab) => tab.id === activeTab); + const activeTabCountResult = activeTabIndex >= 0 ? tabCountResults[activeTabIndex] : null; + const hasCountChange = + (totalCountResult != null && totalCountResult !== prevTotalCount) || (activeTabCountResult != null && activeTabCountResult !== prevTabCount); + const shouldRefreshList = hasCountChange || Date.now() - notificationLastOpenListRefreshAtRef.current >= LAYOUT_NOTIFICATION_OPEN_LIST_REFRESH_INTERVAL_MS; + + if (shouldRefreshList) { + void refreshNotificationList(activeTab, false); + } + }; + const onVisibilityChange = (): void => { + if (document.visibilityState === "visible") { + void refreshAllNotificationPanelData(); + } + }; + + const timer = window.setInterval(() => void tick(), LAYOUT_NOTIFICATION_OPEN_POLL_INTERVAL_MS); + document.addEventListener("visibilitychange", onVisibilityChange); + return () => { + window.clearInterval(timer); + document.removeEventListener("visibilitychange", onVisibilityChange); + }; + }, [isLogin, notificationPopoverOpen, refreshAllNotificationPanelData, refreshNotificationList, refreshNotificationTabCount, refreshNotificationTotalCount]); + const handleLayoutNavClick = (evt: ReactMouseEvent, to: string): void => { if (CANVAS_BASE_URL && to === CANVAS_BASE_URL) { evt.preventDefault(); const url = buildCanvasUrl(); - if (url) window.open(url, "_blank"); + if (url) window.open(url, "_blank", "noopener,noreferrer"); return; } if (!canAccessRoute(to)) { @@ -231,9 +661,86 @@ export function Layout({ children }: LayoutProps) { } }; + const markNotificationAsRead = useCallback((tabId: LayoutNotificationTabId, item: NotificationListItem): void => { + const rawItemId = item.id; + if (rawItemId == null || !isLayoutNotificationUnread(item)) return; + const itemId = String(rawItemId); + if (locallyReadNotificationIdsRef.current.has(itemId) || notificationReadingIdsRef.current.has(itemId)) return; + + notificationReadingIdsRef.current.add(itemId); + void readNotification({ id: rawItemId }) + .then(() => { + notificationLocalVersionRef.current += 1; + setLocallyReadNotificationIds((prev) => { + if (prev.has(itemId)) return prev; + const nextIds = new Set(prev); + nextIds.add(itemId); + locallyReadNotificationIdsRef.current = nextIds; + return nextIds; + }); + setNotificationItemsByTab((prev) => ({ + ...prev, + [tabId]: markLayoutNotificationItemRead(prev[tabId], itemId), + })); + setNotificationUnreadCountByTab((prev) => { + const nextCounts = { + ...prev, + [tabId]: Math.max(0, prev[tabId] - 1), + }; + notificationUnreadCountByTabRef.current = nextCounts; + return nextCounts; + }); + setNotificationTotalUnreadCount((prev) => { + const nextCount = Math.max(0, prev - 1); + notificationTotalUnreadCountRef.current = nextCount; + return nextCount; + }); + notificationBroadcastChannelRef.current?.postMessage({ + type: "read", + id: itemId, + tabId, + } satisfies LayoutNotificationBroadcastMessage); + }) + .catch(() => { + // 详情已打开,已读失败保持原未读状态,下一次打开通知时由 count 接口校准。 + }) + .finally(() => { + notificationReadingIdsRef.current.delete(itemId); + }); + }, []); + + const handleNotificationCardClick = useCallback( + (itemId: string, item: NotificationListItem): void => { + setSelectedNotificationId(itemId); + markNotificationAsRead(activeNotificationTab, item); + }, + [activeNotificationTab, markNotificationAsRead], + ); + + const handleNotificationDetailAction = useCallback( + (item: NotificationListItem): void => { + const jump = getLayoutNotificationJump(item); + if (jump == null) return; + + if (jump.type === "page") { + navigate(normalizeNotificationPagePath(jump.value)); + } else { + window.open(normalizeNotificationExternalUrl(jump.value), "_blank", "noopener,noreferrer"); + } + + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); + }, + [navigate], + ); + const isVoiceArea = pathname === ROUTES.VOICE || pathname.startsWith(`${ROUTES.VOICE_MUSIC_SHARE}/`); const shouldShowFloatingComposer = pathname !== ROUTES.SUBSCRIBE && pathname !== ROUTES.CHARACTERS_CREATE && !isVoiceArea; - + const notificationItems = notificationItemsByTab[activeNotificationTab]; + const selectedNotificationIndex = notificationItems.findIndex((item, index) => getLayoutNotificationItemId(item, index) === selectedNotificationId); + const selectedNotification = selectedNotificationIndex >= 0 ? notificationItems[selectedNotificationIndex] : null; + const selectedNotificationJump = selectedNotification == null ? null : getLayoutNotificationJump(selectedNotification); + const selectedNotificationTime = selectedNotification == null ? "" : formatLayoutNotificationTime(selectedNotification.sendTime ?? selectedNotification.createTime, i18n.language); const handlePromoBarClose = useCallback((evt: ReactMouseEvent) => { evt.stopPropagation(); evt.preventDefault(); @@ -372,6 +879,22 @@ export function Layout({ children }: LayoutProps) { }; }, [userPointsPopoverOpen]); + useEffect(() => { + if (!notificationPopoverOpen) return; + const onDocMouseDown = (evt: MouseEvent) => { + const el = notificationToolRef.current; + const target = evt.target as Node | null; + if (el && target && !el.contains(target)) { + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); + } + }; + document.addEventListener("mousedown", onDocMouseDown); + return () => { + document.removeEventListener("mousedown", onDocMouseDown); + }; + }, [notificationPopoverOpen]); + const dismissMainDetailModals = useCallback(() => { closeCharacterDetailModal(); closeAssetDetailsModal(); @@ -424,19 +947,132 @@ export function Layout({ children }: LayoutProps) {

) : null}
- {/* 通知:当前只按 2026 7月官网 UI 占位,后续接入通知功能。 */} - - - +
+ + + + {notificationPopoverOpen ? ( + <> +
{ + e.stopPropagation(); + }} + > +
+ {LAYOUT_NOTIFICATION_TABS.map((tab) => { + const tabUnreadCount = notificationUnreadCountByTab[tab.id]; + const isActiveTab = activeNotificationTab === tab.id; + return ( + + ); + })} +
+
+ {notificationLoadingByTab[activeNotificationTab] ?

{t("common.loading")}

: null} + {!notificationLoadingByTab[activeNotificationTab] && notificationItems.length === 0 ?

{t("layout.notifications.empty")}

: null} + {!notificationLoadingByTab[activeNotificationTab] + ? notificationItems.map((item, index) => { + const itemId = getLayoutNotificationItemId(item, index); + const title = getLayoutNotificationText(item.title) || t("layout.notifications.untitled"); + const content = getLayoutNotificationText(item.content); + const time = formatLayoutNotificationTime(item.sendTime ?? item.createTime, i18n.language); + const stableItemId = getLayoutNotificationStableId(item); + const isUnread = isLayoutNotificationUnread(item) && (stableItemId == null || !locallyReadNotificationIds.has(stableItemId)); + return ( + + ); + }) + : null} +
+
+ {selectedNotification ? ( +
{ + e.stopPropagation(); + }} + > +
+

{getLayoutNotificationText(selectedNotification.title) || t("layout.notifications.untitled")}

+ {selectedNotificationJump ? ( + + ) : null} +
+

{getLayoutNotificationText(selectedNotification.content)}

+ {selectedNotificationTime ?

{selectedNotificationTime}

: null} +
+ ) : null} + + ) : null} +
) : null} @@ -495,6 +1133,8 @@ export function Layout({ children }: LayoutProps) { e.stopPropagation(); setUserPointsPopoverOpen((v) => !v); setTopbarServiceQrOpen(false); + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); setFooterPopoverOpen(false); closeAvatarMenu(); }} @@ -683,6 +1323,8 @@ export function Layout({ children }: LayoutProps) { setFooterPopoverOpen((v) => !v); setTopbarServiceQrOpen(false); setUserPointsPopoverOpen(false); + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); closeAvatarMenu(); }} aria-label={t("layout.footerPopover.menuAria")} @@ -707,6 +1349,8 @@ export function Layout({ children }: LayoutProps) { setFooterPopoverOpen(false); setTopbarServiceQrOpen(false); setUserPointsPopoverOpen(false); + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); void openLatestPrivacyPolicyLink(PRIVACY_POLICY_KEY_USER_TERMS); }} > @@ -719,6 +1363,8 @@ export function Layout({ children }: LayoutProps) { setFooterPopoverOpen(false); setTopbarServiceQrOpen(false); setUserPointsPopoverOpen(false); + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); void openLatestPrivacyPolicyLink(PRIVACY_POLICY_KEY_PRIVACY_POLICY); }} > @@ -731,6 +1377,8 @@ export function Layout({ children }: LayoutProps) { setFooterPopoverOpen(false); setTopbarServiceQrOpen(false); setUserPointsPopoverOpen(false); + setNotificationPopoverOpen(false); + setSelectedNotificationId(null); void openLatestPrivacyPolicyLink(PRIVACY_POLICY_KEY_AI_USE_NOTICE); }} > diff --git a/src/layouts/globalPromptPanel/GlobalTemplateComposerShell/GlobalTemplateComposerShell.tsx b/src/layouts/globalPromptPanel/GlobalTemplateComposerShell/GlobalTemplateComposerShell.tsx index 68c20d2..04c2c74 100644 --- a/src/layouts/globalPromptPanel/GlobalTemplateComposerShell/GlobalTemplateComposerShell.tsx +++ b/src/layouts/globalPromptPanel/GlobalTemplateComposerShell/GlobalTemplateComposerShell.tsx @@ -775,7 +775,7 @@ export const GlobalTemplateComposerShell = forwardRef

{t("composer.templateLibrary")}

@@ -805,7 +805,7 @@ export const GlobalTemplateComposerShell = forwardRef - {thumb ? : } + {thumb ? : } {item.name ?? item.title ?? t("composer.unnamedTemplate")} @@ -821,11 +821,11 @@ export const GlobalTemplateComposerShell = forwardRef
); @@ -874,7 +874,7 @@ export const GlobalTemplateComposerShell = forwardRef - + ) : null} @@ -956,7 +956,7 @@ export const GlobalTemplateComposerShell = forwardRef{t("composer.fields.size")}

W {ratioSizePreview.width} - + H {ratioSizePreview.height}
@@ -1002,7 +1002,7 @@ export const GlobalTemplateComposerShell = forwardRef - + {item.name ?? t("composer.unnamedStyle")} ); @@ -1013,7 +1013,7 @@ export const GlobalTemplateComposerShell = forwardRef @@ -1021,11 +1021,11 @@ export const GlobalTemplateComposerShell = forwardRef
- + {templateTaskCost}
diff --git a/src/layouts/globalPromptPanel/composerAssets.ts b/src/layouts/globalPromptPanel/composerAssets.ts index 361ab4a..9d01f4c 100644 --- a/src/layouts/globalPromptPanel/composerAssets.ts +++ b/src/layouts/globalPromptPanel/composerAssets.ts @@ -1,14 +1,14 @@ /** 创作区静态图标,集中管理避免组件内重复 `new URL`。 */ export const COMPOSER_ASSET = { - icHelp: new URL("@/assets/images/ic_help_light_gray.png", import.meta.url).href, - icArrowRightGray: new URL("@/assets/images/ic_arrow_right_light_gray.png", import.meta.url).href, - icLink: new URL("@/assets/images/ic_link.png", import.meta.url).href, - icModel: new URL("@/assets/images/ic_model.png", import.meta.url).href, - icRefresh: new URL("@/assets/images/ic_refresh_active.png", import.meta.url).href, - icPublish: new URL("@/assets/images/ic_arrow_top.png", import.meta.url).href, - icStarsBlack: new URL("@/assets/images/ic_points.png", import.meta.url).href, - icExtension: new URL("@/assets/images/ic_extension.png", import.meta.url).href, - icCollapsed: new URL("@/assets/images/ic_collapsed.png", import.meta.url).href, + icHelp: new URL("@/assets/images/common/icons/help-light-gray.png", import.meta.url).href, + icArrowRightGray: new URL("@/assets/images/common/icons/arrow-right-light-gray.png", import.meta.url).href, + icLink: new URL("@/assets/images/common/icons/link.png", import.meta.url).href, + icModel: new URL("@/assets/images/common/icons/model.png", import.meta.url).href, + icRefresh: new URL("@/assets/images/common/icons/refresh-active.png", import.meta.url).href, + icPublish: new URL("@/assets/images/common/icons/arrow-up.png", import.meta.url).href, + icStarsBlack: new URL("@/assets/images/points/points.png", import.meta.url).href, + icExtension: new URL("@/assets/images/create/composer-extension.png", import.meta.url).href, + icCollapsed: new URL("@/assets/images/common/icons/panel-collapsed.png", import.meta.url).href, } as const; /** 模型图标可能来自 CDN、data URL 或本地相对路径;统一在这里兜底为默认模型图标。 */ diff --git a/src/layouts/globalPromptPanel/composerSections/ActionMimicUploadPromptSection.tsx b/src/layouts/globalPromptPanel/composerSections/ActionMimicUploadPromptSection.tsx index 6e6673e..3fdb059 100644 --- a/src/layouts/globalPromptPanel/composerSections/ActionMimicUploadPromptSection.tsx +++ b/src/layouts/globalPromptPanel/composerSections/ActionMimicUploadPromptSection.tsx @@ -66,7 +66,7 @@ const ActionMimicExampleCard = memo(function ActionMimicExampleCard({ item, acti videoEl.currentTime = 0; }} > -
); @@ -338,12 +338,12 @@ export function ActionMimicUploadPromptSection({ - {selectedExampleItem ? {!selectedExampleItem ? ( <> - + {t("composer.taskName.actionMimic")} @@ -353,7 +353,7 @@ export function ActionMimicUploadPromptSection({ {composerMediaUploadEnabled && !hasUploadItems && canAddMoreImages ? ( ) : composerMediaUploadEnabled ? (
); @@ -391,7 +391,7 @@ export function ActionMimicUploadPromptSection({ ))} {canAddMoreMediaIncludingPending ? ( ) : null} diff --git a/src/layouts/globalPromptPanel/composerSections/CommonUploadPromptSection.tsx b/src/layouts/globalPromptPanel/composerSections/CommonUploadPromptSection.tsx index 4f10df1..afd8156 100644 --- a/src/layouts/globalPromptPanel/composerSections/CommonUploadPromptSection.tsx +++ b/src/layouts/globalPromptPanel/composerSections/CommonUploadPromptSection.tsx @@ -132,7 +132,7 @@ export function CommonUploadPromptSection({ {composerMediaUploadEnabled ? ( !hasUploadItems && canAddMoreImages ? ( ) : (
); @@ -166,7 +166,7 @@ export function CommonUploadPromptSection({ {renderPendingCards()} {canAddMoreMediaIncludingPending ? ( ) : null} diff --git a/src/layouts/globalPromptPanel/composerSections/ComposerUploadMediaPreview.tsx b/src/layouts/globalPromptPanel/composerSections/ComposerUploadMediaPreview.tsx index 25a21d8..f853294 100644 --- a/src/layouts/globalPromptPanel/composerSections/ComposerUploadMediaPreview.tsx +++ b/src/layouts/globalPromptPanel/composerSections/ComposerUploadMediaPreview.tsx @@ -18,7 +18,7 @@ type ComposerUploadMediaPreviewProps = { disableZoomPreview?: boolean; }; -const AUDIO_ICON_SRC = new URL("/src/assets/images/audio.png", import.meta.url).href; +const AUDIO_ICON_SRC = new URL("/src/assets/images/common/placeholders/audio.png", import.meta.url).href; export function ComposerUploadMediaPreview({ mediaItem, classNames, disableZoomPreview = false }: ComposerUploadMediaPreviewProps) { const [previewPosition, setPreviewPosition] = useState<{ left: number; top: number } | null>(null); @@ -45,7 +45,7 @@ export function ComposerUploadMediaPreview({ mediaItem, classNames, disableZoomP }, [closeImagePreview, disableZoomPreview]); if (mediaItem.type === "video") { - return
); @@ -102,7 +102,7 @@ export function HeadTailUploadPromptSection({ return ( ); }; diff --git a/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx b/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx index b4dfd62..8a3f5f4 100644 --- a/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx +++ b/src/layouts/globalPromptPanel/composerSections/ReferenceUploadPromptSection.tsx @@ -1250,15 +1250,15 @@ export function ReferenceUploadPromptSection(props: CommonUploadPromptSectionPro }; const renderPickerThumb = (subject: SubjectRecord) => { - if (subject.type === "video") return
{composerMediaUploadEnabled && !hasUploadItems && canAddMoreImages ? ( ) : composerMediaUploadEnabled ? (
{mediaSubjectLabel}
); @@ -1309,7 +1309,7 @@ export function ReferenceUploadPromptSection(props: CommonUploadPromptSectionPro ))} {canAddMoreMediaIncludingPending ? ( ) : null} diff --git a/src/layouts/globalPromptPanel/composerSections/VoiceUploadPromptSection.tsx b/src/layouts/globalPromptPanel/composerSections/VoiceUploadPromptSection.tsx index c6aa09d..df9c004 100644 --- a/src/layouts/globalPromptPanel/composerSections/VoiceUploadPromptSection.tsx +++ b/src/layouts/globalPromptPanel/composerSections/VoiceUploadPromptSection.tsx @@ -80,10 +80,10 @@ export function VoiceUploadPromptSection({ chatPrompt, placeholder, onChatPrompt
diff --git a/src/pages/CharactersCreate/components/CreateRoleModal/index.tsx b/src/pages/CharactersCreate/components/CreateRoleModal/index.tsx index e7d38c3..0be79e1 100644 --- a/src/pages/CharactersCreate/components/CreateRoleModal/index.tsx +++ b/src/pages/CharactersCreate/components/CreateRoleModal/index.tsx @@ -157,7 +157,7 @@ export function CreateRoleForm({ activeTaskItem, className = "", compact = false {!hasCreatedCharacter ? ( ) : null}
@@ -169,7 +169,7 @@ export function CreateRoleForm({ activeTaskItem, className = "", compact = false disabled={hasCreatedCharacter || !avatarOriginImage} aria-label={t("pages.charactersCreate.createRoleModal.uploadAvatarFirst")} > - + )}
@@ -222,7 +222,7 @@ export function CreateRoleForm({ activeTaskItem, className = "", compact = false {activeTaskItem.taskInfo?.aiModelName ? ( <> - + {activeTaskItem.taskInfo.aiModelName} @@ -267,13 +267,13 @@ export function CreateRoleForm({ activeTaskItem, className = "", compact = false <>

{t("pages.charactersCreate.createRoleModal.createCharacterFolder")}

{t("pages.charactersCreate.createRoleModal.uploadMeansAgree")} diff --git a/src/pages/CharactersCreate/index.tsx b/src/pages/CharactersCreate/index.tsx index b5dfb50..ebf5380 100644 --- a/src/pages/CharactersCreate/index.tsx +++ b/src/pages/CharactersCreate/index.tsx @@ -605,7 +605,7 @@ export default function CharactersCreate() { // 缩略图拖拽预热:用 cancelled + cancelIdleCallback/clearTimeout,避免卸载后仍调用 manager。 useEffect(() => { let cancelled = false; - const urls = characterList.flatMap((item) => [item?.avatar, item?.threeViewImage ?? item?.threeView]).filter((item): item is string => typeof item === "string" && item.trim() !== ""); + const urls = characterList.flatMap((item) => [item?.avatar, item?.threeViewImage]).filter((item): item is string => typeof item === "string" && item.trim() !== ""); if (urls.length === 0) return; const run = () => { if (!cancelled) warmupPreviewUrls(urls); @@ -1277,7 +1277,7 @@ export default function CharactersCreate() { if (!activeTask) { return (

- +

{t("pages.charactersCreate.emptyLine1")}

{t("pages.charactersCreate.emptyLine2")}

@@ -1322,7 +1322,7 @@ export default function CharactersCreate() { {resultImage ? ( - + ) : null} {isPending ? ( @@ -1356,7 +1356,7 @@ export default function CharactersCreate() { {promptText ? ( ) : null} @@ -1368,7 +1368,7 @@ export default function CharactersCreate() {
@@ -1455,7 +1455,7 @@ export default function CharactersCreate() { {t("pages.charactersCreate.rightPanel.copyrightSub")}
- +
@@ -1476,7 +1476,7 @@ export default function CharactersCreate() { {t("pages.charactersCreate.rightPanel.commissionSub")}
- +
@@ -1543,7 +1543,7 @@ export default function CharactersCreate() { }} > - + )) @@ -1568,7 +1568,7 @@ export default function CharactersCreate() { }} > - + ); @@ -1611,13 +1611,13 @@ export default function CharactersCreate() { {showPromptLibrarySkeleton ? ( ) : selectedPromptLibraryCoverUrl ? ( - + ) : null} {selectedPromptLibraryCoverUrl || showPromptLibrarySkeleton ? null : ( <> - + {t("pages.charactersCreate.promptLibrary")} @@ -1637,7 +1637,7 @@ export default function CharactersCreate() { const isSelected = selectedPromptLibraryKey ? rowKey === selectedPromptLibraryKey : idx === 0; return ( ); @@ -1648,7 +1648,7 @@ export default function CharactersCreate() {
{images.length === 0 && uploadPendingCount === 0 ? ( ) : ( - +
); @@ -1680,7 +1680,7 @@ export default function CharactersCreate() { ))} {images.length + uploadPendingCount < MAX_UPLOAD_IMAGES ? ( ) : null} @@ -1812,7 +1812,7 @@ export default function CharactersCreate() {

{t("composer.fields.size")}

W {ratioSizePreview.width} - + H {ratioSizePreview.height}
@@ -1829,7 +1829,7 @@ export default function CharactersCreate() { @@ -1844,7 +1844,7 @@ export default function CharactersCreate() { > {showComposerActionSkeleton ? null : ( <> - + {generateCost} {showGenerateDiscountBadge ? ( @@ -265,7 +265,7 @@ export default function Profile() { - {t("pages.profile.avatarAlt")} + {t("pages.profile.avatarAlt")}
diff --git a/src/pages/Voice/VoiceMusicSharePage.tsx b/src/pages/Voice/VoiceMusicSharePage.tsx index 155dffd..bb654cb 100644 --- a/src/pages/Voice/VoiceMusicSharePage.tsx +++ b/src/pages/Voice/VoiceMusicSharePage.tsx @@ -14,7 +14,7 @@ import { parseVoiceMusicAssetDetail } from "./utils/voiceGalleryTaskUtils"; import "./index.scss"; const SHARE_PAGE_ASSETS = { - close: new URL("@/assets/images/ic_close.png", import.meta.url).href, + close: new URL("@/assets/images/common/icons/close.png", import.meta.url).href, } as const; export default function VoiceMusicSharePage() { diff --git a/src/pages/Voice/components/music/VoiceGalleryPlayerBar.tsx b/src/pages/Voice/components/music/VoiceGalleryPlayerBar.tsx index a394eb9..179ec90 100644 --- a/src/pages/Voice/components/music/VoiceGalleryPlayerBar.tsx +++ b/src/pages/Voice/components/music/VoiceGalleryPlayerBar.tsx @@ -7,11 +7,11 @@ import type { VoiceGalleryCompletedRow } from "../../utils/voiceGalleryTaskUtils import { downloadVoiceWork } from "./VoiceMusicPanel"; const PLAYER_ASSETS = { - coverDefault: new URL("@/assets/images/voice-library/ic_cover_default-display.jpg", import.meta.url).href, - favorite: new URL("@/assets/images/voice-library/ic_star.png", import.meta.url).href, - favoriteActive: new URL("@/assets/images/voice-library/ic_star_liked.png", import.meta.url).href, - download: new URL("@/assets/images/ic_download.png", import.meta.url).href, - close: new URL("@/assets/images/ic_close.png", import.meta.url).href, + coverDefault: new URL("@/assets/images/voice/library/cover-default-display.jpg", import.meta.url).href, + favorite: new URL("@/assets/images/voice/library/star.png", import.meta.url).href, + favoriteActive: new URL("@/assets/images/voice/library/star-liked.png", import.meta.url).href, + download: new URL("@/assets/images/common/icons/download.png", import.meta.url).href, + close: new URL("@/assets/images/common/icons/close.png", import.meta.url).href, } as const; export type VoiceGalleryPlayerBarProps = { @@ -59,7 +59,7 @@ export function VoiceGalleryPlayerBar({
- +

{work.title}

diff --git a/src/pages/Voice/components/music/VoiceMusicDetailModal.tsx b/src/pages/Voice/components/music/VoiceMusicDetailModal.tsx index ce6b442..ef34886 100644 --- a/src/pages/Voice/components/music/VoiceMusicDetailModal.tsx +++ b/src/pages/Voice/components/music/VoiceMusicDetailModal.tsx @@ -11,8 +11,8 @@ import { downloadVoiceWork } from "./VoiceMusicPanel"; import "./VoiceMusicDetailModal.scss"; const DETAIL_ASSETS = { - close: new URL("@/assets/images/ic_close.png", import.meta.url).href, - navArrow: new URL("@/assets/images/ic_arrow_right_gray.png", import.meta.url).href, + close: new URL("@/assets/images/common/icons/close.png", import.meta.url).href, + navArrow: new URL("@/assets/images/common/icons/arrow-right-gray.png", import.meta.url).href, } as const; const FADE_MS = 280; diff --git a/src/pages/Voice/components/music/VoiceMusicDetailView.tsx b/src/pages/Voice/components/music/VoiceMusicDetailView.tsx index 3301d9c..0993535 100644 --- a/src/pages/Voice/components/music/VoiceMusicDetailView.tsx +++ b/src/pages/Voice/components/music/VoiceMusicDetailView.tsx @@ -7,12 +7,12 @@ import { formatVoiceGalleryCreateDate } from "../../utils/voiceGalleryTaskUtils" import { isLyricsSectionTag } from "./lyricsSectionTags"; const DETAIL_ASSETS = { - coverDefault: new URL("@/assets/images/voice-library/ic_cover_default-display.jpg", import.meta.url).href, - avatarDefault: new URL("@/assets/images/popi_avatar.png", import.meta.url).href, - favorite: new URL("@/assets/images/voice-library/ic_star.png", import.meta.url).href, - favoriteActive: new URL("@/assets/images/voice-library/ic_star_liked.png", import.meta.url).href, - download: new URL("@/assets/images/ic_download.png", import.meta.url).href, - share: new URL("@/assets/images/ic_share_gray.png", import.meta.url).href, + coverDefault: new URL("@/assets/images/voice/library/cover-default-display.jpg", import.meta.url).href, + avatarDefault: new URL("@/assets/images/common/avatars/popi-avatar.png", import.meta.url).href, + favorite: new URL("@/assets/images/voice/library/star.png", import.meta.url).href, + favoriteActive: new URL("@/assets/images/voice/library/star-liked.png", import.meta.url).href, + download: new URL("@/assets/images/common/icons/download.png", import.meta.url).href, + share: new URL("@/assets/images/common/icons/share-gray.png", import.meta.url).href, } as const; export type VoiceMusicDetailViewProps = { @@ -84,12 +84,12 @@ export function VoiceMusicDetailView({
- +
{userName} @@ -76,7 +76,7 @@ export default function Subscribe() {
ID:{userId}
{memberEndTimeText ? ( @@ -111,13 +111,13 @@ export default function Subscribe() {
{/* - + */} - + - +
diff --git a/src/types/global.d.ts b/src/types/global.d.ts index eb0ce70..814f1d7 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -11,6 +11,8 @@ declare global { } var classNames: (...parts: Array) => string; + var resolveOssImageThumbUrl: (rawUrl: unknown) => string; + var resolveOssVideoCoverUrl: (rawUrl: unknown) => string; } export {}; diff --git a/src/utils/globalOssImageThumb.ts b/src/utils/globalOssImageThumb.ts new file mode 100644 index 0000000..713fed9 --- /dev/null +++ b/src/utils/globalOssImageThumb.ts @@ -0,0 +1,74 @@ +import { useParamConfigStore } from "@/store/useParamConfigStore"; +import { resolveApiMediaUrl } from "@/utils/apiMediaUrl"; + +function normalizeOssMediaSuffix(suffix: unknown): string { + const value = typeof suffix === "string" ? suffix.trim() : ""; + if (!value) return ""; + return value.startsWith("?") || value.startsWith("&") ? value.slice(1) : value; +} + +function isLocalFrontendAssetUrl(url: string): boolean { + if (/^(data|blob):/i.test(url)) return true; + if (url.startsWith("./") || url.startsWith("../")) return true; + + try { + const parsed = new URL(url, typeof window === "undefined" ? "http://localhost" : window.location.origin); + return parsed.pathname.startsWith("/assets/") || parsed.pathname.startsWith("/src/assets/"); + } catch { + return false; + } +} + +function isPopiMediaUrl(url: string): boolean { + return url.toLowerCase().includes("popi"); +} + +function hasQueryParam(url: string, paramName: string): boolean { + if (!paramName) return false; + const escapedName = paramName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`[?&]${escapedName}(=|&|$)`).test(url); +} + +function appendMediaSuffix(url: string, suffix: string): string { + const paramName = suffix.split("=")[0] ?? ""; + if (hasQueryParam(url, paramName)) return url; + + const hashIndex = url.indexOf("#"); + const urlWithoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url; + const hash = hashIndex >= 0 ? url.slice(hashIndex) : ""; + const separator = urlWithoutHash.includes("?") ? "&" : "?"; + + return `${urlWithoutHash}${separator}${suffix}${hash}`; +} + +function resolveOssImageThumbUrlImpl(rawUrl: unknown): string { + const url = resolveApiMediaUrl(rawUrl); + if (!url || isLocalFrontendAssetUrl(url)) return url; + if (!isPopiMediaUrl(url)) return url; + + const suffix = normalizeOssMediaSuffix(useParamConfigStore.getState().config?.systemConfig?.ossImageThumbSuffix); + if (!suffix) return url; + + return appendMediaSuffix(url, suffix); +} + +function resolveOssVideoCoverUrlImpl(rawUrl: unknown): string { + const url = resolveApiMediaUrl(rawUrl); + if (!url || isLocalFrontendAssetUrl(url)) return ""; + if (!isPopiMediaUrl(url)) return ""; + + const suffix = normalizeOssMediaSuffix(useParamConfigStore.getState().config?.systemConfig?.ossVideoCoverSuffix); + if (!suffix) return ""; + + return appendMediaSuffix(url, suffix); +} + +if (typeof globalThis.resolveOssImageThumbUrl !== "function") { + globalThis.resolveOssImageThumbUrl = resolveOssImageThumbUrlImpl; +} + +if (typeof globalThis.resolveOssVideoCoverUrl !== "function") { + globalThis.resolveOssVideoCoverUrl = resolveOssVideoCoverUrlImpl; +} + +export {};