style: 恢复标签页分隔线并统一工具栏与图标按钮
This commit is contained in:
@@ -75,7 +75,7 @@ function clearFilters() {
|
||||
</div>
|
||||
</template>
|
||||
<div class="project-index-toolbar">
|
||||
<NRadioGroup v-model:value="filter" size="small" aria-label="项目状态筛选"
|
||||
<NRadioGroup v-model:value="filter" size="medium" class="project-status-filters" aria-label="项目状态筛选"
|
||||
><NRadioButton v-for="item in filters" :key="item.value" :value="item.value">{{
|
||||
item.label
|
||||
}}</NRadioButton></NRadioGroup
|
||||
@@ -89,9 +89,14 @@ function clearFilters() {
|
||||
:input-props="{ 'aria-label': '搜索项目' }"
|
||||
><template #prefix><Search :size="15" /></template
|
||||
></NInput>
|
||||
<NButton :loading="query.loading.value" aria-label="刷新项目" @click="query.refresh"
|
||||
><RefreshCw :size="16"
|
||||
/></NButton>
|
||||
<NButton
|
||||
class="icon-button"
|
||||
:loading="query.loading.value"
|
||||
aria-label="刷新项目"
|
||||
title="刷新项目"
|
||||
@click="query.refresh"
|
||||
><template #icon><RefreshCw :size="16" /></template
|
||||
></NButton>
|
||||
</div>
|
||||
</div>
|
||||
<NAlert v-if="query.error.value" type="error" :show-icon="false"
|
||||
|
||||
@@ -423,12 +423,12 @@ watch(
|
||||
:disabled="catalog.loading.value || castingQuery.loading.value"
|
||||
@click="refreshDirectory"
|
||||
quaternary
|
||||
circle
|
||||
class="icon-button"
|
||||
size="small"
|
||||
aria-label="刷新主体目录"
|
||||
title="刷新主体目录"
|
||||
><RefreshCw :size="14"
|
||||
/></NButton>
|
||||
><template #icon><RefreshCw :size="14" /></template
|
||||
></NButton>
|
||||
</div>
|
||||
<div class="directory-filters">
|
||||
<NInput
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
||||
import { NAlert, NButton, NTag } from 'naive-ui'
|
||||
import { NAlert, NButton } from 'naive-ui'
|
||||
import { LoaderCircle, LockKeyhole, LockKeyholeOpen, Palette, RefreshCw, TriangleAlert } from '@lucide/vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { usePolling } from '../../composables/usePolling'
|
||||
import { runOperation } from '../workflows/operations'
|
||||
@@ -24,6 +25,16 @@ const editorRevision = ref(0)
|
||||
const imageRevision = ref(0)
|
||||
const dirty = ref(false)
|
||||
|
||||
/** 状态是只读说明,与操作按钮分组;读取失败不能被误报成尚未创建。 */
|
||||
const statusInfo = computed(() => {
|
||||
if (query.error.value) return { label: '读取失败', hint: '请刷新重试,现有内容不会被覆盖', icon: TriangleAlert }
|
||||
if (!query.data.value) return { label: '正在读取', hint: '正在获取项目视觉风格', icon: LoaderCircle }
|
||||
if (!style.value) return { label: '尚未创建', hint: '可使用 AI 生成,或在下方手动填写', icon: Palette }
|
||||
return style.value.isLocked
|
||||
? { label: '已锁定', hint: 'AI 不会覆盖,仍可在下方手动编辑', icon: LockKeyhole }
|
||||
: { label: '未锁定', hint: '定稿后可在下方锁定,避免 AI 覆盖', icon: LockKeyholeOpen }
|
||||
})
|
||||
|
||||
/** 防止错误项目的数据进入编辑器;空风格是正常的首次使用状态。 */
|
||||
function assertProject(value: VisualStyle | null, id: string) {
|
||||
if (value && (value.projectId !== id || value.images.some(image => image.visualStyleId !== value.id)))
|
||||
@@ -101,18 +112,26 @@ function removeImage(image: VisualStyleImage) {
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
<div class="my-5 flex flex-wrap items-center gap-3">
|
||||
<NTag size="small" :bordered="false">{{
|
||||
style ? (style.isLocked ? '已锁定' : '未锁定') : '尚未创建'
|
||||
}}</NTag>
|
||||
<ConfirmAction
|
||||
:label="style ? 'AI 重新生成风格' : 'AI 生成风格'"
|
||||
:disabled="disabled || !!style?.isLocked || dirty"
|
||||
acknowledgement
|
||||
description="调用文本模型生成整体及分类风格。重新生成会覆盖已保存的文本与硬约束;旧身份、旧图片和已有提示词不会自动更新。"
|
||||
@confirm="generate"
|
||||
/>
|
||||
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small">刷新风格</NButton>
|
||||
<div class="visual-style-toolbar">
|
||||
<div class="visual-style-status" role="status">
|
||||
<component :is="statusInfo.icon" :size="18" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{{ statusInfo.label }}</strong>
|
||||
<p>{{ statusInfo.hint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="visual-style-actions">
|
||||
<ConfirmAction
|
||||
:label="style ? 'AI 重新生成风格' : 'AI 生成风格'"
|
||||
:disabled="disabled || !!style?.isLocked || dirty"
|
||||
acknowledgement
|
||||
description="调用文本模型生成整体及分类风格。重新生成会覆盖已保存的文本与硬约束;旧身份、旧图片和已有提示词不会自动更新。"
|
||||
@confirm="generate"
|
||||
/>
|
||||
<NButton :loading="query.loading.value" @click="query.refresh"
|
||||
><template #icon><RefreshCw :size="16" /></template>刷新风格</NButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mb-4"
|
||||
>{{ query.error.value }} 请确认后端 dev 已更新并重启;查询失败不等同于尚未创建。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { NImage, NScrollbar } from 'naive-ui'
|
||||
import { NImage, NRadioGroup, NScrollbar } from 'naive-ui'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -223,6 +223,28 @@ describe('内容级滚动下的生产反馈', () => {
|
||||
})
|
||||
|
||||
describe('视觉风格与主体身份工作区', () => {
|
||||
it.each([
|
||||
{ locked: null, label: '尚未创建', action: 'AI 生成风格', disabled: false },
|
||||
{ locked: false, label: '未锁定', action: 'AI 重新生成风格', disabled: false },
|
||||
{ locked: true, label: '已锁定', action: 'AI 重新生成风格', disabled: true }
|
||||
])('视觉风格 $label 使用独立状态说明与等高操作按钮,保留锁定限制', async ({ locked, label, action, disabled }) => {
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockImplementation(async () => jsonResponse(locked === null ? null : styleFixture({ isLocked: locked })))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
await mountAssets('style')
|
||||
const status = wrapper!.get('.visual-style-status')
|
||||
expect(status.attributes('role')).toBe('status')
|
||||
expect(status.get('strong').text()).toBe(label)
|
||||
expect(status.find('button').exists()).toBe(false)
|
||||
expect(wrapper!.find('.visual-style-toolbar .n-tag').exists()).toBe(false)
|
||||
expect(wrapper!.get('.visual-style-actions').findAll('button')).toHaveLength(2)
|
||||
expect(button(action).disabled).toBe(disabled)
|
||||
expect(button(action).style.getPropertyValue('--n-height')).toBe('34px')
|
||||
expect(button('刷新风格').style.getPropertyValue('--n-height')).toBe('34px')
|
||||
expect(fetcher.mock.calls.every(([, options]) => !options?.method || options.method === 'GET')).toBe(true)
|
||||
})
|
||||
|
||||
it('主体目录显示已确认母版和默认占位,点缩略图只切换主体,不重复读取所有角色图库', async () => {
|
||||
const first = formFixture()
|
||||
const forms = [
|
||||
@@ -1326,6 +1348,11 @@ describe('工作台页面交互', () => {
|
||||
await router.push('/')
|
||||
wrapper = mount(ProjectsPage, { attachTo: document.body, global: { plugins: [router] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.getComponent(NRadioGroup).props('size')).toBe('medium')
|
||||
const refresh = wrapper.get('[aria-label="刷新项目"]')
|
||||
expect(refresh.classes()).toContain('icon-button')
|
||||
expect(refresh.find('.n-button__icon svg').exists()).toBe(true)
|
||||
expect(refresh.text()).toBe('')
|
||||
expect(wrapper.text()).toContain('雨夜来信')
|
||||
await wrapper.get('input[aria-label="搜索项目"]').setValue('不存在的关键词')
|
||||
expect(wrapper.text()).toContain('没有匹配的剧本')
|
||||
|
||||
Reference in New Issue
Block a user