From 7773721399a629f304b899005e8d62e6f8127df5 Mon Sep 17 00:00:00 2001 From: GouJ Date: Tue, 1 Sep 2026 18:12:54 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E4=BC=98=E5=8C=96=E9=80=89=E8=A7=92?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E9=97=B4=E8=B7=9D=E4=B8=8E=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/admin.css | 57 ++++++++++++++++- src/components/ui/layout.test.ts | 14 +++++ .../subject-identity/SubjectIdentityPage.vue | 14 ++--- src/features/workflows/pages.test.ts | 63 +++++++++++++++++++ src/styles.css | 25 -------- 5 files changed, 139 insertions(+), 34 deletions(-) diff --git a/src/admin.css b/src/admin.css index 52ac800..29b25c3 100644 --- a/src/admin.css +++ b/src/admin.css @@ -221,6 +221,10 @@ body { .workspace-tools-body { padding: 20px; } +/* 身份说明与折叠入口分开,避免正文紧贴下一组操作。 */ +.identity-tools-intro { + margin-bottom: 24px; +} .workspace-tools-body .storyboard-controls, .workspace-tools-body .production-controls { display: grid; @@ -886,7 +890,6 @@ body { .n-button:disabled { opacity: 1; } -.n-button.casting-item, .n-button.image-history-item { height: auto; white-space: normal; @@ -897,9 +900,59 @@ body { text-align: left; overflow: hidden; } -.n-button.casting-item .n-button__content { +/* 选角进度按抽屉实际宽度排列;按钮尺寸显式覆盖组件默认值。 */ +.casting-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); + gap: 12px; + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid var(--app-border); +} +.n-button.casting-item { width: 100%; + min-width: 0; + height: auto; + min-height: 76px; + padding: 12px 14px; + white-space: normal; + text-align: left; + background: var(--app-surface); +} +.n-button.casting-item.selected { + background: var(--app-subtle); + box-shadow: inset 3px 0 0 var(--app-ink); +} +.n-button.casting-item .n-button__content { + display: flex; + width: 100%; + min-width: 0; + align-items: center; justify-content: space-between; + gap: 16px; + text-align: left; +} +.casting-item-identity { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} +.casting-item-name { + font-size: 14px; + font-weight: 500; + line-height: 1.5; + overflow-wrap: anywhere; +} +.casting-item-ref { + color: var(--app-muted); + font-family: ui-monospace, monospace; + font-size: 11px; + line-height: 1.5; + overflow-wrap: anywhere; +} +.casting-item-status { + flex-shrink: 0; } .search-field { padding: 0; diff --git a/src/components/ui/layout.test.ts b/src/components/ui/layout.test.ts index 429f7a6..f830d13 100644 --- a/src/components/ui/layout.test.ts +++ b/src/components/ui/layout.test.ts @@ -32,6 +32,20 @@ describe('管理后台组件边界', () => { const css = readFileSync('src/admin.css', 'utf8') + readFileSync('src/styles.css', 'utf8') expect(css).not.toMatch(/overflow(?:-[xy])?\s*:\s*(?:auto|scroll)\b/) }) + it('选角面板保留说明间距,卡片按可用宽度排列并覆盖按钮默认高度', () => { + // 保护间距与换行契约,真实宽高和暗色效果仍需浏览器验收。 + const css = readFileSync('src/admin.css', 'utf8') + expect(css).toMatch(/\.identity-tools-intro\s*\{[^}]*margin-bottom:\s*24px/) + expect(css).toMatch( + /\.casting-list\s*\{[^}]*repeat\(auto-fit, minmax\(min\(100%, 260px\), 1fr\)\);[^}]*gap:\s*12px;[^}]*margin-top:\s*24px/ + ) + expect(css).toMatch( + /\.n-button\.casting-item\s*\{[^}]*height:\s*auto;[^}]*min-height:\s*76px;[^}]*padding:\s*12px 14px/ + ) + expect(css).toMatch(/\.casting-item-name\s*\{[^}]*overflow-wrap:\s*anywhere/) + expect(css).toMatch(/\.casting-item-status\s*\{[^}]*flex-shrink:\s*0/) + expect(css).toMatch(/\.n-button\.casting-item\.selected\s*\{[^}]*var\(--app-subtle\)[^}]*var\(--app-ink\)/) + }) it('拆解分栏跟随剩余高度收缩,移动端历史区不挤出结果区', () => { // DOM 环境不计算几何,保护完整的 flex → grid → NScrollbar 高度链。 const css = readFileSync('src/admin.css', 'utf8') diff --git a/src/features/subject-identity/SubjectIdentityPage.vue b/src/features/subject-identity/SubjectIdentityPage.vue index ab7d6ae..969973f 100644 --- a/src/features/subject-identity/SubjectIdentityPage.vue +++ b/src/features/subject-identity/SubjectIdentityPage.vue @@ -166,7 +166,7 @@ watch( title="角色选角总览与批量操作" :has-receipt="!!session.receipt" > -
+

主体身份

@@ -294,21 +294,21 @@ watch( > 并发数与本批上限都必须是正整数。

-
+
- {{ - item.subjectName - }} - {{ + :aria-pressed="selectedId === item.subjectId" + > + {{ item.subjectName }} + {{ item.subjectRef }} diff --git a/src/features/workflows/pages.test.ts b/src/features/workflows/pages.test.ts index f44b6c1..6dda84f 100644 --- a/src/features/workflows/pages.test.ts +++ b/src/features/workflows/pages.test.ts @@ -527,6 +527,69 @@ describe('视觉风格与主体身份工作区', () => { expect(wrapper!.text()).toContain('当前身份母版') }) + it('选角进度卡片分层显示长姓名、编号和状态,点击仍按正式 ID 切换主体', async () => { + const longName = '来自记忆当铺的另一条时间线中尚未丢失记忆的林默' + const items = [ + { + subjectId: 'subject-db-1', + subjectRef: '@CH0001', + subjectName: longName, + status: 'ready', + isLocked: true + }, + { + subjectId: 'subject-db-2', + subjectRef: '@CH0002', + subjectName: '老周', + status: 'candidate_pending', + isLocked: false + } + ] + const fetcher = vi.fn().mockImplementation(async url => { + const path = String(url) + if (path.endsWith('/subject-forms')) return jsonResponse([formFixture()]) + if (path.endsWith('/visual-style')) return jsonResponse(styleFixture()) + if (path.endsWith('/character-casting/readiness')) + return jsonResponse({ + total: 2, + ready: 1, + missingIdentity: 0, + missingAnchor: 0, + candidatePending: 1, + unlocked: 0, + locked: 1, + items + }) + if (path.endsWith('/identity/images')) return jsonResponse([]) + const second = path.includes('/subject-db-2/') + return jsonResponse( + identityFixture({ + subjectId: second ? 'subject-db-2' : 'subject-db-1', + description: second ? '老周的稳定身份' : '林默的稳定身份' + }) + ) + }) + vi.stubGlobal('fetch', fetcher) + await mountAssets('identity') + expect(wrapper!.get('.identity-tools-intro').text()).toContain('为同一个人物、场景或道具固定稳定特征') + const cards = wrapper!.get('[aria-label="角色选角进度"]').findAll('.casting-item') + expect(cards).toHaveLength(2) + expect(cards[0]!.get('.casting-item-name').text()).toBe(longName) + expect(cards[0]!.get('.casting-item-ref').text()).toBe('@CH0001') + expect(cards[0]!.get('.casting-item-status').text()).toBe('选角已完成') + expect(cards[0]!.find('.truncate').exists()).toBe(false) + expect(cards[0]!.attributes('aria-pressed')).toBe('true') + expect(cards[1]!.get('.casting-item-status').text()).toBe('等待确认演员') + await cards[1]!.trigger('click') + await flushPromises() + expect(cards[0]!.attributes('aria-pressed')).toBe('false') + expect(cards[1]!.attributes('aria-pressed')).toBe('true') + expect(cards[1]!.classes()).toContain('selected') + expect(wrapper!.get('#identity-description').element.value).toBe('老周的稳定身份') + expect(fetcher.mock.calls.some(([url]) => String(url).endsWith('/subjects/subject-db-2/identity'))).toBe(true) + expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true) + }) + it('Character 确认选角原子切换 Anchor 并锁定 Identity,不调用普通母版接口', async () => { let locked = false let selected = false diff --git a/src/styles.css b/src/styles.css index 9198a9d..67d804e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -91,28 +91,6 @@ font-family: ui-monospace, monospace; font-size: 13px; } - .casting-list { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; - } - .casting-item { - display: flex; - min-width: 0; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 11px 12px; - border: 1px solid var(--color-line); - border-radius: 5px; - background: var(--app-surface); - text-align: left; - } - .casting-item:hover, - .casting-item.selected { - border-color: var(--app-border); - background: var(--app-subtle); - } @media (max-width: 900px) { .identity-workspace { grid-template-columns: minmax(0, 1fr); @@ -123,9 +101,6 @@ .casting-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .casting-list { - grid-template-columns: 1fr; - } } .page-container {