diff --git a/src/assets/images/create/tabs/create_tab_all.png b/src/assets/images/create/tabs/create_tab_all.png index 3fafa67..1dcff02 100644 Binary files a/src/assets/images/create/tabs/create_tab_all.png and b/src/assets/images/create/tabs/create_tab_all.png differ diff --git a/src/assets/images/ic_dui.png b/src/assets/images/ic_dui.png new file mode 100644 index 0000000..90061c8 Binary files /dev/null and b/src/assets/images/ic_dui.png differ diff --git a/src/components/TabPill/index.scss b/src/components/TabPill/index.scss new file mode 100644 index 0000000..7c24577 --- /dev/null +++ b/src/components/TabPill/index.scss @@ -0,0 +1,52 @@ +.tabPill { + width: 91px; + height: 28px; + margin: 0; + padding: 4px 20px; + border: none; + border-radius: 18px; + background: #ffffff; + color: #333333; + font-size: 14px; + font-weight: 400; + line-height: normal; + cursor: pointer; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 3px; + white-space: nowrap; + flex-shrink: 0; +} + +.tabPill_wide { + width: 97px; +} + +.tabPill__icon { + flex-shrink: 0; + width: 20px; + height: 20px; + display: block; + object-fit: contain; + filter: brightness(0) saturate(100%) invert(17%) sepia(0%) saturate(0%) hue-rotate(187deg) brightness(95%) contrast(91%); +} + +.tabPill__label { + flex-shrink: 0; +} + +.tabPill_active { + background: #fbf0ff; + color: #6f47f5; +} + +.tabPill_active .tabPill__icon { + filter: brightness(0) saturate(100%) invert(41%) sepia(56%) saturate(6861%) hue-rotate(241deg) brightness(93%) contrast(106%); +} + +.tabPill:focus-visible { + outline: 2px solid rgba(111, 71, 245, 0.45); + outline-offset: 2px; +} diff --git a/src/components/TabPill/index.tsx b/src/components/TabPill/index.tsx new file mode 100644 index 0000000..f9f1408 --- /dev/null +++ b/src/components/TabPill/index.tsx @@ -0,0 +1,21 @@ +import type { ButtonHTMLAttributes, ReactNode } from "react"; + +import "./index.scss"; + +export type TabPillProps = ButtonHTMLAttributes & { + active?: boolean; + children: ReactNode; + iconSrc?: string; + wide?: boolean; +}; + +export function TabPill({ active, children, className, iconSrc, type = "button", wide, ...props }: TabPillProps) { + const tabPillClassName = ["tabPill", active ? "tabPill_active" : null, wide ? "tabPill_wide" : null, className].filter(Boolean).join(" "); + + return ( + + ); +} diff --git a/src/pages/Create/createTabIcons.ts b/src/pages/Create/createTabIcons.ts new file mode 100644 index 0000000..8f2f72b --- /dev/null +++ b/src/pages/Create/createTabIcons.ts @@ -0,0 +1,12 @@ +export type CreateTabIconName = "all" | "image" | "video" | "voice" | "template" | "character" | "background" | "characterAssets"; + +export const CREATE_TAB_ICON_SRC: Record = { + all: new URL("/src/assets/images/create/tabs/create_tab_all.png", import.meta.url).href, + image: new URL("/src/assets/images/create/tabs/create_tab_image.png", import.meta.url).href, + video: new URL("/src/assets/images/create/tabs/create_tab_video.png", import.meta.url).href, + voice: new URL("/src/assets/images/create/tabs/create_tab_voice.png", import.meta.url).href, + template: new URL("/src/assets/images/create/tabs/create_tab_template.png", import.meta.url).href, + character: new URL("/src/assets/images/create/tabs/create_tab_character.png", import.meta.url).href, + background: new URL("/src/assets/images/create/tabs/create_tab_background.png", import.meta.url).href, + characterAssets: new URL("/src/assets/images/create/tabs/create_tab_character_assets.png", import.meta.url).href, +}; diff --git a/src/pages/Create/index.scss b/src/pages/Create/index.scss index ff3a6e3..bd6df5c 100644 --- a/src/pages/Create/index.scss +++ b/src/pages/Create/index.scss @@ -131,49 +131,6 @@ box-sizing: border-box; } -.createPage__filterPill { - width: 91px; - height: 28px; - margin: 0; - padding: 4px 20px; - border: none; - border-radius: 18px; - background: #ffffff; - color: #333333; - font-size: 14px; - font-weight: 400; - line-height: normal; - cursor: pointer; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 3px; - white-space: nowrap; -} - -.createPage__filterPillIcon { - flex-shrink: 0; - width: 20px; - height: 20px; - display: block; - object-fit: contain; -} - -.createPage__filterPillIcon_placeholder { - visibility: hidden; -} - -.createPage__filterPill_active { - background: #fbf0ff; - color: #6f47f5; -} - -.createPage__filterPill:focus-visible { - outline: 2px solid rgba(111, 71, 245, 0.45); - outline-offset: 2px; -} - .createPage__batchBtn { width: 91px; height: 36px; diff --git a/src/pages/Create/panels/InspirationLibraryPanel/index.scss b/src/pages/Create/panels/InspirationLibraryPanel/index.scss index ba5bdfe..121dd73 100644 --- a/src/pages/Create/panels/InspirationLibraryPanel/index.scss +++ b/src/pages/Create/panels/InspirationLibraryPanel/index.scss @@ -25,59 +25,6 @@ gap: 10px; } -.inspirationPanel__filterPill { - flex-shrink: 0; - width: 91px; - height: 28px; - margin: 0; - padding: 4px 20px; - border: none; - border-radius: 18px; - background: #ffffff; - color: #333333; - font-size: 14px; - font-weight: 400; - line-height: normal; - cursor: pointer; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 3px; - white-space: nowrap; -} - -.inspirationPanel__filterPill_wide { - width: 97px; -} - -.inspirationPanel__filterPillIcon { - width: 20px; - height: 20px; - flex-shrink: 0; - display: block; - object-fit: contain; - filter: brightness(0) saturate(100%) invert(17%) sepia(0%) saturate(0%) hue-rotate(187deg) brightness(95%) contrast(91%); -} - -.inspirationPanel__filterPill_active .inspirationPanel__filterPillIcon { - filter: brightness(0) saturate(100%) invert(39%) sepia(93%) saturate(2844%) hue-rotate(240deg) brightness(98%) contrast(96%); -} - -.inspirationPanel__filterPillLabel { - flex-shrink: 0; -} - -.inspirationPanel__filterPill_active { - background: #fbf0ff; - color: #6f47f5; -} - -.inspirationPanel__filterPill:focus-visible { - outline: 2px solid rgba(111, 71, 245, 0.45); - outline-offset: 2px; -} - /* 设计稿:分类行底到「我的角色」标题区约 13px */ .inspirationPanel__content { margin-top: 20px; diff --git a/src/pages/Create/panels/InspirationLibraryPanel/index.tsx b/src/pages/Create/panels/InspirationLibraryPanel/index.tsx index 7ad521b..b5084f3 100644 --- a/src/pages/Create/panels/InspirationLibraryPanel/index.tsx +++ b/src/pages/Create/panels/InspirationLibraryPanel/index.tsx @@ -3,8 +3,10 @@ import { useTranslation } from "react-i18next"; import { AssetLibraryToolbar } from "./AssetLibraryToolbar"; import { BackgroundAssetsPanel, useBackgroundAssetsModel } from "./BackgroundAssets/BackgroundAssets"; import { TemplateAssetsPanel, useTemplateAssetsModel } from "./TemplateAssets/TemplateAssets"; +import { TabPill } from "@/components/TabPill"; import { CharacterLibrarySections } from "@/pages/Characters"; import { CharacterAssetsPanel, useCharacterAssetsModel } from "./CharacterAssetsPanel/CharacterAssetsPanel"; +import { CREATE_TAB_ICON_SRC, type CreateTabIconName } from "../../createTabIcons"; import "./index.scss"; export interface InspirationLibraryPanelProps { @@ -24,20 +26,13 @@ function useInspirationLibraryToolbar(initialFilter: InspirationFilterId = "back return { filter, selectFilter }; } -const FILTER_CONFIG: readonly InspirationFilterId[] = [ - "background", // 背景素材 - "character", // 角色 - "characterAssets", // 角色素材 - "templates", // 模板 +const FILTER_CONFIG: readonly { iconName: CreateTabIconName; id: InspirationFilterId; wide?: boolean }[] = [ + { iconName: "background", id: "background" }, // 背景素材 + { iconName: "character", id: "character" }, // 角色 + { iconName: "characterAssets", id: "characterAssets", wide: true }, // 角色素材 + { iconName: "template", id: "templates" }, // 模板 ]; -const INSPIRATION_FILTER_ICON_SRC: Record = { - background: new URL("/src/assets/images/create/tabs/create_tab_background.png", import.meta.url).href, - character: new URL("/src/assets/images/create/tabs/create_tab_character.png", import.meta.url).href, - characterAssets: new URL("/src/assets/images/create/tabs/create_tab_character_assets.png", import.meta.url).href, - templates: new URL("/src/assets/images/create/tabs/create_tab_template.png", import.meta.url).href, -}; - export default function InspirationLibraryPanel({ tabIdPrefix, setToolbarExtra }: InspirationLibraryPanelProps) { const { t } = useTranslation(); const { filter, selectFilter } = useInspirationLibraryToolbar("background"); @@ -52,11 +47,10 @@ export default function InspirationLibraryPanel({ tabIdPrefix, setToolbarExtra } () => (
- {FILTER_CONFIG.map((id) => ( - + {FILTER_CONFIG.map(({ iconName, id, wide }) => ( + selectFilter(id)}> + {t(`pages.create.inspiration.filters.${id}`)} + ))}
{showAssetToolbar ? ( diff --git a/src/pages/Create/panels/MyCreationsPanel/index.scss b/src/pages/Create/panels/MyCreationsPanel/index.scss index b382920..5279ede 100644 --- a/src/pages/Create/panels/MyCreationsPanel/index.scss +++ b/src/pages/Create/panels/MyCreationsPanel/index.scss @@ -35,19 +35,19 @@ list-style: none; } -.createPage__myCreationsList > .assetGalleryTile { +.createPage__myCreationsList>.assetGalleryTile { width: 219px; height: 219px; margin-bottom: 0; border-radius: 26px; } -.createPage__myCreationsList > .assetGalleryTile .assetGalleryTile__thumb, -.createPage__myCreationsList > .assetGalleryTile .assetGalleryTile__poster { +.createPage__myCreationsList>.assetGalleryTile .assetGalleryTile__thumb, +.createPage__myCreationsList>.assetGalleryTile .assetGalleryTile__poster { border-radius: 26px; } -.createPage__myCreationsList > .assetGalleryTile_pending { +.createPage__myCreationsList>.assetGalleryTile_pending { border-radius: 26px; } @@ -108,10 +108,8 @@ } .createPage__myCreationsCheckboxIcon { - position: absolute; - inset: -2px; - width: 25px; - height: 25px; + width: 21px; + height: 21px; display: block; } @@ -122,7 +120,7 @@ justify-content: center; } - .createPage__myCreationsList > .assetGalleryTile, + .createPage__myCreationsList>.assetGalleryTile, .createPage__myCreationsItem { width: 182px; height: auto; @@ -150,4 +148,4 @@ .createPage__batchBtn { align-self: flex-end; } -} +} \ No newline at end of file diff --git a/src/pages/Create/panels/MyCreationsPanel/index.tsx b/src/pages/Create/panels/MyCreationsPanel/index.tsx index dd768b2..be91dc3 100644 --- a/src/pages/Create/panels/MyCreationsPanel/index.tsx +++ b/src/pages/Create/panels/MyCreationsPanel/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { getTaskAssetForAsset, getTaskList, getTemplateTaskList, getAssetDetail, addPostLike, delPostLike, deleteAsset, deleteTemplateAsset, getPostDownloadUrls } from "@/api/api"; import { AssetGalleryTile, getAssetDragUrls, resolveAssetGalleryTile, useAssetGalleryVoicePlayback } from "@/components/AssetGalleryTile"; +import { TabPill } from "@/components/TabPill"; import { MY_CREATIONS_REFRESH_QUERY } from "@/constants"; import { formatLocalDate } from "@/utils"; import { openAssetDetailsDialog } from "@/dialog"; @@ -13,6 +14,7 @@ import { useImageDragTransfer, useWarmupImageDragUrls } from "@/hooks/useImageDr import { VoiceMusicDetailModal } from "@/pages/Voice/components/music/VoiceMusicDetailModal"; import { VOICE_MUSIC_SUB_TYPE } from "@/pages/Voice/mockData"; import { getVoiceMusicDetailNeighborId, parseVoiceMusicAssetDetail, type VoiceMusicDetailNavigation } from "@/pages/Voice/utils/voiceGalleryTaskUtils"; +import { CREATE_TAB_ICON_SRC, type CreateTabIconName } from "../../createTabIcons"; import "./index.scss"; const PAGE_SIZE = 20; @@ -27,24 +29,16 @@ export interface MyCreationsPanelProps { } /** 筛选项:title 对应文案 key,type 对应接口参数 */ -const MY_CREATIONS_FILTER_OPTIONS: { title: string; type: number }[] = [ - { title: "all", type: 0 }, - { title: "image", type: 1 }, - { title: "video", type: 2 }, - { title: "voice", type: 3 }, - { title: "template", type: 4 }, +const MY_CREATIONS_FILTER_OPTIONS: { iconName: CreateTabIconName; title: string; type: number }[] = [ + { iconName: "all", title: "all", type: 0 }, + { iconName: "image", title: "image", type: 1 }, + { iconName: "video", title: "video", type: 2 }, + { iconName: "voice", title: "voice", type: 3 }, + { iconName: "template", title: "template", type: 4 }, // { title: "character", type: 5 }, ]; -const MY_CREATIONS_FILTER_ICON_SRC: Record = { - all: new URL("/src/assets/images/create/tabs/create_tab_all.png", import.meta.url).href, - image: new URL("/src/assets/images/create/tabs/create_tab_image.png", import.meta.url).href, - video: new URL("/src/assets/images/create/tabs/create_tab_video.png", import.meta.url).href, - voice: new URL("/src/assets/images/create/tabs/create_tab_voice.png", import.meta.url).href, - template: new URL("/src/assets/images/create/tabs/create_tab_template.png", import.meta.url).href, -}; - -const MY_CREATIONS_BATCH_CHECKED_SRC = "https://www.figma.com/api/mcp/asset/73956270-6032-4730-94ec-3115dfde0970"; +const MY_CREATIONS_BATCH_CHECKED_SRC = new URL("@/assets/images/ic_dui.png", import.meta.url).href; function toMs(raw: any): number { if (raw == null || raw === "") return 0; @@ -328,7 +322,7 @@ export default function MyCreationsPanel({ tabIdPrefix, setToolbarExtra }: MyCre tRef.current = t; /** 始终指向最新 loadNextPage,供 IntersectionObserver 回调使用,减少 observer 反复绑定。 */ - const loadNextPageRef = useRef<() => Promise>(async () => {}); + const loadNextPageRef = useRef<() => Promise>(async () => { }); const todayKey = formatLocalDate(Date.now(), "YYYY-MM-DD"); const selectedCount = selectedAssetIds.size; @@ -883,12 +877,10 @@ export default function MyCreationsPanel({ tabIdPrefix, setToolbarExtra }: MyCre
{MY_CREATIONS_FILTER_OPTIONS.map((option) => { const selected = selectedType === option.type; - const filterIconSrc = MY_CREATIONS_FILTER_ICON_SRC[option.title]; return ( - + ); })}
@@ -996,11 +988,11 @@ export default function MyCreationsPanel({ tabIdPrefix, setToolbarExtra }: MyCre batchMode ? undefined : (voiceRowKey, evt) => { - if (evt.key !== "Enter" && evt.key !== " ") return; - evt.preventDefault(); - evt.stopPropagation(); - toggleVoicePlayback(voiceRowKey, evt.currentTarget.closest("[data-asset-gallery-tile]")); - } + if (evt.key !== "Enter" && evt.key !== " ") return; + evt.preventDefault(); + evt.stopPropagation(); + toggleVoicePlayback(voiceRowKey, evt.currentTarget.closest("[data-asset-gallery-tile]")); + } } onVoiceAudioEnded={onVoiceAudioEnded} onTileClick={openDetails} diff --git a/src/pages/Create/panels/MyFavoritePanel/index.tsx b/src/pages/Create/panels/MyFavoritePanel/index.tsx index dd3c1a7..99556bc 100644 --- a/src/pages/Create/panels/MyFavoritePanel/index.tsx +++ b/src/pages/Create/panels/MyFavoritePanel/index.tsx @@ -3,8 +3,10 @@ import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { getPostLikeList, getUserLikeCharacterList } from "@/api/api"; import { AssetGalleryTile, getAssetDragUrls, resolveAssetGalleryTile, useAssetGalleryVoicePlayback } from "@/components/AssetGalleryTile"; +import { TabPill } from "@/components/TabPill"; import { openAssetDetailsDialog, openCharacterDetailDialog } from "@/dialog"; import { useImageDragTransfer } from "@/hooks/useImageDragTransfer"; +import { CREATE_TAB_ICON_SRC, type CreateTabIconName } from "../../createTabIcons"; import "./index.scss"; const PAGE_SIZE = 20; @@ -14,34 +16,21 @@ export interface MyFavoritePanelProps { } /** 筛选项:title 对应文案 key,assetType 对应接口参数(与「我的创造」一致) */ -const MY_FAVORITE_FILTER_OPTIONS: { title: string; assetType: string }[] = [ - { title: "all", assetType: "0" }, - { title: "image", assetType: "1" }, - { title: "video", assetType: "2" }, - { title: "voice", assetType: "3" }, - { title: "template", assetType: "4" }, - { title: "character", assetType: "5" }, +const MY_FAVORITE_FILTER_OPTIONS: { assetType: string; iconName: CreateTabIconName; title: string }[] = [ + { assetType: "0", iconName: "all", title: "all" }, + { assetType: "1", iconName: "image", title: "image" }, + { assetType: "2", iconName: "video", title: "video" }, + { assetType: "3", iconName: "voice", title: "voice" }, + { assetType: "4", iconName: "template", title: "template" }, + { assetType: "5", iconName: "character", title: "character" }, ]; -const MY_FAVORITE_FILTER_ICON_SRC: Record = { - all: new URL("/src/assets/images/create/tabs/create_tab_all.png", import.meta.url).href, - image: new URL("/src/assets/images/create/tabs/create_tab_image.png", import.meta.url).href, - video: new URL("/src/assets/images/create/tabs/create_tab_video.png", import.meta.url).href, - voice: new URL("/src/assets/images/create/tabs/create_tab_voice.png", import.meta.url).href, - template: new URL("/src/assets/images/create/tabs/create_tab_template.png", import.meta.url).href, - character: new URL("/src/assets/images/create/tabs/create_tab_character.png", import.meta.url).href, -}; - function buildFavoriteListParams(assetType: string, pageNum: number): { page: number; pageSize: number; assetType?: string } { const params: { page: number; pageSize: number; assetType?: string } = { page: pageNum, pageSize: PAGE_SIZE }; if (assetType !== "0") params.assetType = assetType; return params; } -function joinClassNames(...parts: Array) { - return parts.filter(Boolean).join(" "); -} - function findScrollContainer(node: HTMLElement | null): HTMLElement | null { let current = node?.parentElement ?? null; while (current != null) { @@ -210,9 +199,9 @@ export default function MyFavoritePanel({ tabIdPrefix, setToolbarExtra }: MyFavo const translatedFilters = useMemo( () => - MY_FAVORITE_FILTER_OPTIONS.map(({ title, assetType }) => ({ + MY_FAVORITE_FILTER_OPTIONS.map(({ assetType, iconName, title }) => ({ assetType, - iconSrc: MY_FAVORITE_FILTER_ICON_SRC[title], + iconName, label: t(`pages.create.filters.${title}`), })), [t], @@ -221,19 +210,10 @@ export default function MyFavoritePanel({ tabIdPrefix, setToolbarExtra }: MyFavo const toolbarExtra = useMemo( () => (
- {translatedFilters.map(({ assetType, iconSrc, label }) => ( - + ))}
),