diff --git a/stash-web/src/App.css b/stash-web/src/App.css
index d897780..6a7d9a9 100644
--- a/stash-web/src/App.css
+++ b/stash-web/src/App.css
@@ -1053,6 +1053,46 @@
cursor: default;
}
+/* 分组图标选择:搜索 + 图标网格 */
+.icon-picker {
+ display: flex;
+ flex-direction: column;
+ gap: var(--su-space-3);
+ margin-top: var(--su-space-3);
+ padding: var(--su-space-3);
+ border: 1px solid var(--su-border);
+ border-radius: var(--su-radius-md);
+ background-color: var(--su-bg-subtle);
+}
+
+.icon-picker-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
+ gap: var(--su-space-2);
+ max-height: 220px;
+ overflow-y: auto;
+}
+
+.icon-picker-item {
+ display: grid;
+ place-items: center;
+ height: 36px;
+ border: 1px solid transparent;
+ border-radius: var(--su-radius-sm);
+ background-color: var(--su-bg-card);
+ color: var(--su-text-sub);
+ font-size: var(--su-font-size-lg);
+ cursor: pointer;
+ transition:
+ border-color var(--su-duration-fast) var(--su-ease-in-out),
+ color var(--su-duration-fast) var(--su-ease-in-out);
+}
+
+.icon-picker-item:hover {
+ border-color: var(--su-color-primary);
+ color: var(--su-color-primary);
+}
+
.group-mark {
display: inline-flex;
flex: none;
diff --git a/stash-web/src/AppNext.tsx b/stash-web/src/AppNext.tsx
index 629b2e4..0d11853 100644
--- a/stash-web/src/AppNext.tsx
+++ b/stash-web/src/AppNext.tsx
@@ -18,6 +18,7 @@ import { ItemEditor } from './components/content/ItemEditor'
import { PosterGrid } from './components/content/PosterGrid'
import { SectionCard } from './components/content/SectionCard'
import { SiteHeader } from './components/header/SiteHeader'
+import { GroupMark } from './components/common/GroupMark'
import { LoginDialog } from './components/auth/LoginDialog'
import { ProfileCard } from './components/sidebar/ProfileCard'
import { ServicePanel } from './components/sidebar/ServicePanel'
@@ -486,7 +487,7 @@ export default function AppNext() {
- {group.icon || '•'}
+
{group.title}
[{groupEntryCount(group)}]
diff --git a/stash-web/src/components/admin/AdminNext.tsx b/stash-web/src/components/admin/AdminNext.tsx
index 6b43aa4..be85e2f 100644
--- a/stash-web/src/components/admin/AdminNext.tsx
+++ b/stash-web/src/components/admin/AdminNext.tsx
@@ -25,6 +25,7 @@ import {
import { useEffect, useMemo, useState } from 'react'
import { changePassword, fetchSession, logout, type SessionState } from '../../auth'
import { LoginDialog } from '../auth/LoginDialog'
+import { GroupMark as GroupGlyph } from '../common/GroupMark'
import {
fetchSettings,
saveSettings,
@@ -430,19 +431,18 @@ const FORMAT_LABEL: Record
= {
}
function GroupMark({ icon }: { icon: string }) {
- if (icon.trim() === '') {
- return
- }
- if (ICON_NAMES.includes(icon as IconName)) {
- return (
-
-
-
- )
- }
+ const value = icon.trim()
+ const isIcon = ICON_NAMES.includes(value as IconName)
+ const classes = [
+ 'group-mark',
+ value === '' ? 'group-mark-empty' : isIcon ? '' : 'group-mark-text',
+ ]
+ .filter(Boolean)
+ .join(' ')
+
return (
-
- {icon}
+
+
)
}
@@ -632,6 +632,16 @@ function GroupDialog({
const [enabled, setEnabled] = useState(true)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
+ const [iconPickerOpen, setIconPickerOpen] = useState(false)
+ const [iconQuery, setIconQuery] = useState('')
+
+ // 图标库里 446 个名字,先按关键字过滤,没输入时只给前 120 个,避免一次渲染太多
+ const iconList = useMemo(() => {
+ const query = iconQuery.trim().toLowerCase()
+ const names =
+ query === '' ? ICON_NAMES : ICON_NAMES.filter((name) => name.includes(query))
+ return names.slice(0, 120)
+ }, [iconQuery])
useEffect(() => {
if (!open) return
@@ -787,7 +797,11 @@ function GroupDialog({
)}
-
+
@@ -795,10 +809,47 @@ function GroupDialog({
setIcon(event.target.value)}
/>
+
+ {iconPickerOpen && (
+
+
setIconQuery('')}
+ onChange={(event) => setIconQuery(event.target.value)}
+ />
+
+ {iconList.map((name) => (
+
+ ))}
+
+ {iconList.length === 0 && (
+
+ 没有匹配的图标,换个关键字试试。
+
+ )}
+
+ )}
diff --git a/stash-web/src/components/common/GroupMark.tsx b/stash-web/src/components/common/GroupMark.tsx
new file mode 100644
index 0000000..7cb09b1
--- /dev/null
+++ b/stash-web/src/components/common/GroupMark.tsx
@@ -0,0 +1,14 @@
+import { Icon, ICON_NAMES, type IconName } from '@stash/ui'
+
+/**
+ * 分组图标:能对上 iconfont 名字就渲染图标,否则按字符渲染。
+ *
+ * 旧数据里存的是 emoji / 任意字符,新数据存图标名(如 `folder_2_line`);
+ * 两种都要能显示,所以这里做一次判定,前后台共用同一套规则。
+ */
+export function GroupMark({ icon, fallback = '' }: { icon: string; fallback?: string }) {
+ const value = icon.trim()
+ if (value === '') return fallback === '' ? null : <>{fallback}>
+ if (ICON_NAMES.includes(value as IconName)) return
+ return <>{value}>
+}