Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bbfd30627 | ||
|
|
a691b904eb | ||
|
|
59b2c2aaa3 | ||
|
|
c135d7e599 | ||
|
|
95fff56513 | ||
|
|
ed0715f1c8 | ||
|
|
20360c8318 | ||
|
|
cc09696394 | ||
|
|
d8842c329a | ||
|
|
a2f09faca8 |
@@ -145,7 +145,7 @@ function exportScript() {
|
||||
</template>
|
||||
</NTabs>
|
||||
</div>
|
||||
<div class="content-with-history panel mt-4" :class="{ 'history-hidden': !showHistory }">
|
||||
<div class="content-with-history panel mt-2" :class="{ 'history-hidden': !showHistory }">
|
||||
<main class="min-w-0">
|
||||
<div v-show="tab === 'episodes'" class="h-full">
|
||||
<EpisodeReader v-if="episodes.length" :episodes="episodes" />
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import { computed, h, nextTick, ref, watch } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { NAlert, NButton, NDataTable, NInput, NRadioButton, NRadioGroup, type DataTableColumns } from 'naive-ui'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NDataTable,
|
||||
NInput,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
type DataTableColumns,
|
||||
type DataTableInst
|
||||
} from 'naive-ui'
|
||||
import { RefreshCw, Search } from '@lucide/vue'
|
||||
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
||||
import { usePolling } from '../../composables/usePolling'
|
||||
@@ -13,11 +22,14 @@ import CreateProjectDialog from './components/CreateProjectDialog.vue'
|
||||
|
||||
/** 项目表格使用固定表头与独立滚动;搜索筛选仍作用于真实接口数据。 */
|
||||
const router = useRouter()
|
||||
/** 列表已有刷新按钮,只在进入、筛选切换和手动刷新时读取。 */
|
||||
/** 列表已有刷新按钮,只在进入和手动刷新时读取;筛选在完整结果中进行。 */
|
||||
const query = usePolling(ref('projects'), (_, signal) => projectsApi.list(signal), false)
|
||||
const search = ref('')
|
||||
const filter = ref('all')
|
||||
const page = ref(1)
|
||||
/** 接口暂未分页:对完整查询结果分批展示,滚动时追加而非切换页码。 */
|
||||
const PAGE_SIZE = 20
|
||||
const visibleCount = ref(PAGE_SIZE)
|
||||
const table = ref<DataTableInst | null>(null)
|
||||
const projects = computed(() => query.data.value ?? [])
|
||||
const filtered = computed(() =>
|
||||
projects.value.filter(item => {
|
||||
@@ -34,9 +46,20 @@ const filters = [
|
||||
{ value: 'need_review', label: '待审核' },
|
||||
{ value: 'failed', label: '失败' }
|
||||
]
|
||||
watch([search, filter], () => {
|
||||
page.value = 1
|
||||
const visibleProjects = computed(() => filtered.value.slice(0, visibleCount.value))
|
||||
/** 条件或接口数据更新后从首批重新展示,避免保留旧列表的滚动位置。 */
|
||||
watch([search, filter, query.data], async () => {
|
||||
visibleCount.value = PAGE_SIZE
|
||||
await nextTick()
|
||||
table.value?.scrollTo({ top: 0 })
|
||||
})
|
||||
/** 靠近表格底部时追加下一批,保留已有行与当前位置。 */
|
||||
function loadMore(event: Event) {
|
||||
const scroller = event.target as HTMLElement | null
|
||||
if (!scroller || query.loading.value || visibleCount.value >= filtered.value.length) return
|
||||
if (scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight > 120) return
|
||||
visibleCount.value = Math.min(visibleCount.value + PAGE_SIZE, filtered.value.length)
|
||||
}
|
||||
/** 表格列通过正式项目 ID 跳转,不依赖列表索引。 */
|
||||
const columns: DataTableColumns<Project> = [
|
||||
{
|
||||
@@ -58,7 +81,7 @@ const columns: DataTableColumns<Project> = [
|
||||
function openProject(id: string) {
|
||||
void router.push('/projects/' + encodeURIComponent(id) + '/create-drama')
|
||||
}
|
||||
/** 清空条件后返回第一页,不改变后端数据。 */
|
||||
/** 清空条件后返回首批,不改变后端数据。 */
|
||||
function clearFilters() {
|
||||
search.value = ''
|
||||
filter.value = 'all'
|
||||
@@ -115,18 +138,19 @@ function clearFilters() {
|
||||
>
|
||||
<div class="project-table-area">
|
||||
<NDataTable
|
||||
ref="table"
|
||||
class="project-data-table"
|
||||
:bordered="false"
|
||||
:bottom-bordered="false"
|
||||
striped
|
||||
:columns="columns"
|
||||
:data="filtered"
|
||||
:data="visibleProjects"
|
||||
:row-key="row => row.id"
|
||||
:loading="query.loading.value"
|
||||
flex-height
|
||||
table-layout="fixed"
|
||||
:scroll-x="785"
|
||||
:pagination="{ page, pageSize: 20, onUpdatePage: (value: number) => (page = value) }"
|
||||
@scroll="loadMore"
|
||||
>
|
||||
<template #empty
|
||||
><div>
|
||||
@@ -144,11 +168,6 @@ function clearFilters() {
|
||||
>
|
||||
</NDataTable>
|
||||
</div>
|
||||
<p class="text-xs text-muted">
|
||||
共 {{ filtered.length }} 个项目<span v-if="query.updatedAt.value">
|
||||
· 更新于 {{ formatDate(query.updatedAt.value) }}</span
|
||||
>
|
||||
</p>
|
||||
</WorkspacePage>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const emit = defineEmits<{ created: [projectId: string] }>()
|
||||
const open = ref(false)
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
const form = reactive({ topic: '', style: '', episodeCount: 3 })
|
||||
const form = reactive({ topic: '', style: '', episodeCount: 1 })
|
||||
|
||||
/** 校验正整数集数和主题,禁止双击产生重复项目。 */
|
||||
async function submit() {
|
||||
@@ -92,7 +92,7 @@ const rules = { topic: requiredTextRule('故事主题'), episodeCount: integerRu
|
||||
></NFormItem>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-muted">
|
||||
建议先用 3 集验证生成效果。生成会实际调用后端模型,产生相应费用。
|
||||
建议先用 1 集验证生成效果。生成会实际调用后端模型,产生相应费用。
|
||||
</p>
|
||||
<NAlert v-if="error" role="alert" type="error" :show-icon="false" class=" ">{{ error }}</NAlert>
|
||||
<div class="dialog-footer">
|
||||
|
||||
+24
-5
@@ -215,6 +215,24 @@ body {
|
||||
min-height: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* 每项保留一致的左右留白。 */
|
||||
.workspace-tabs.n-tabs .n-tabs-nav .n-tabs-tab {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
/* 相邻 Tab 紧贴排列,间隔由各项内部留白提供。 */
|
||||
.workspace-tabs.n-tabs .n-tabs-nav .n-tabs-tab-pad {
|
||||
width: 0;
|
||||
}
|
||||
/* 选中状态仅通过文字颜色表达,不显示底部指示线。 */
|
||||
.workspace-tabs.n-tabs .n-tabs-nav .n-tabs-bar {
|
||||
display: none;
|
||||
}
|
||||
/* 标签与两侧附加操作共用无底边框的导航行。 */
|
||||
.workspace-tabs.n-tabs .n-tabs-nav.n-tabs-nav--line-type.n-tabs-nav--top .n-tabs-nav-scroll-content,
|
||||
.workspace-tabs.n-tabs .n-tabs-nav.n-tabs-nav--line-type.n-tabs-nav--top .n-tabs-nav__prefix,
|
||||
.workspace-tabs.n-tabs .n-tabs-nav.n-tabs-nav--line-type.n-tabs-nav--top .n-tabs-nav__suffix {
|
||||
border-bottom: none;
|
||||
}
|
||||
.workspace-tabs .n-tabs-nav-scroll-wrapper {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -439,10 +457,11 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航保留明确选中态与轻量悬停反馈,明暗模式共用主题变量。 */
|
||||
.workspace-tabs .n-tabs-tab--active {
|
||||
font-weight: 600;
|
||||
/* 明暗模式沿用组件的 primary 文字色,选中和悬停均不绘制背景。 */
|
||||
.workspace-tabs.n-tabs .n-tabs-nav .n-tabs-tab--active {
|
||||
color: var(--n-tab-text-color-active);
|
||||
}
|
||||
.workspace-tabs .n-tabs-tab:hover {
|
||||
background: var(--app-subtle);
|
||||
.workspace-tabs.n-tabs .n-tabs-nav .n-tabs-tab--active,
|
||||
.workspace-tabs.n-tabs .n-tabs-nav .n-tabs-tab:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { NImage, NRadioGroup, NScrollbar } from 'naive-ui'
|
||||
import { NDataTable, 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'
|
||||
@@ -1444,6 +1444,51 @@ describe('工作台页面交互', () => {
|
||||
expect(wrapper.findAll('tbody tr')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('项目列表滚动追加,搜索覆盖未展示项目且清除后回到首批', async () => {
|
||||
const projects = Array.from({ length: 45 }, (_, index) => ({
|
||||
...fixture,
|
||||
id: 'project-' + index,
|
||||
title: '剧本' + index
|
||||
}))
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () =>
|
||||
new Response(JSON.stringify({ data: projects }))
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [{ path: '/', component: ProjectsPage }]
|
||||
})
|
||||
await router.push('/')
|
||||
wrapper = mount(ProjectsPage, { attachTo: document.body, global: { plugins: [router] } })
|
||||
await flushPromises()
|
||||
const table = wrapper.getComponent(NDataTable)
|
||||
expect(table.props('data')).toHaveLength(20)
|
||||
expect(wrapper.find('.n-pagination').exists()).toBe(false)
|
||||
expect(wrapper.text()).not.toContain('个项目')
|
||||
expect(wrapper.text()).not.toContain('更新于')
|
||||
// 远离底部不追加;抵达底部后逐批追加,末批不重复。
|
||||
const scroll = async (scrollTop: number) => {
|
||||
table.vm.$emit('scroll', { target: { scrollHeight: 1000, clientHeight: 200, scrollTop } })
|
||||
await flushPromises()
|
||||
}
|
||||
await scroll(100)
|
||||
expect(table.props('data')).toHaveLength(20)
|
||||
await scroll(800)
|
||||
expect(table.props('data')).toHaveLength(40)
|
||||
await scroll(800)
|
||||
expect(table.props('data')).toHaveLength(45)
|
||||
await scroll(800)
|
||||
expect(table.props('data')).toHaveLength(45)
|
||||
await wrapper.get('input[aria-label="搜索项目"]').setValue('剧本44')
|
||||
await flushPromises()
|
||||
expect(table.props('data')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('剧本44')
|
||||
await wrapper.get('input[aria-label="搜索项目"]').setValue('')
|
||||
await flushPromises()
|
||||
expect(table.props('data')).toHaveLength(20)
|
||||
expect(fetcher).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('新建弹窗提交真实参数并返回 202 的项目 ID', async () => {
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
|
||||
Reference in New Issue
Block a user