fix: VirtualScroller 滚动锚定防抖 + platform 方法引用修复 + CLAUDE.md 更新

- VirtualScroller: measureItem 高度变化时,对可视区上方项的累积 delta 通过微任务延迟补偿 scrollTop,避免同步调整导致的画面抖动
- VirtualScroller: 新增独立测试页 test.html + test-data.js,用于验证虚拟滚动行为
- platform: 修复 painting/video 中 imageUploadLimit() 调用方式为 this.imageUploadLimit()
- display: 修复 Sender_variant 在非 pageTop/pageBottom 中间状态时未设置的问题,补充 isInitializing 异常状态重置
- CLAUDE.md: 补充 VirtualScroller 180deg 旋转机制说明、模型切换完整链路、反旋转注意事项
This commit is contained in:
王佑琳 2026-06-09 15:52:31 +08:00
parent 72e4acf956
commit 481afadd2b
7 changed files with 930 additions and 5 deletions

View File

@ -61,7 +61,7 @@ src/
│ ├── Popover/ # 自定义弹出层Teleport to bodyposition:fixed + fit-content 宽度)
│ ├── Select/ # 自定义下拉选择器(分组选项,与 Popover 协调互斥开关)
│ ├── Img/ # 图片包装组件点击全屏查看Teleport 实现)
│ ├── virtual-scroller/# 虚拟滚动列表组件自定义实现reverse 模式)
│ ├── virtual-scroller/ # 虚拟滚动列表自定义实现。reverse 模式用 180deg 旋转实现底部锚定slot 内容须反旋转
│ └── canvas/ # 图片画布编辑(圆/矩形选区局部重绘undo/redo
├── views/ # 页面home、login
└── utils/
@ -129,7 +129,7 @@ props: (config) => ({
- **平台切换**`const platform = computed(() => createPlatform(props.type))`,切换时重置默认模型并加载模型列表
- **控件渲染**`visibleControls = platform.controls.filter(c => c.show(getCurrentConfig()))`,用 `<component :is>` + `v-bind="ctrl.props(...)"` 渲染
- **配置获取**`getCurrentConfig()` 返回 `platform.modelConfig?.value ?? platform.modelDisplayConfig?.value`,兼容两种配置来源
- **模型切换**监听 `model + modelType`,调用 `await platform.loadConfig(newModel, newModelType)`
- **模型切换**`watch([model, modelType])` → `platform.loadConfig()``syncDefaults()` 将 schema 写入响应式 state → controls 的 `show()`/`props()` 读取 state → `visibleControls` 自动更新 → UI 重渲染
- **任务发起**`handleStart()` 调用 `platform.validateBeforeSubmit()``platform.buildTaskBody()``generate()`
- **参数回填**`fillParamsFromResult()` 委托给 `platform.fillFromResult()`
@ -185,6 +185,7 @@ Painting 模型参数 schema 在 `src/platforms/painting/models/*.js` 中,参
- **`X-Session-Id`** 自定义 header 需要 nginx 在 `/suanli/` location 的 `Access-Control-Allow-Headers` 中加入,否则 POST 请求会触发 CORS 预检失败。
- **模型列表缓存**`modelApi.js` 中 `fetchPlatformModels` 使用 localStorage 30 秒 TTL + `pendingRequests` Map 并发去重。
- **平台包预加载**dialogBox 顶层 `import` Painting 和 Video 平台包,触发自注册,确保首次使用时 registry 已就绪。
- **VirtualScroller 反旋转**:组件用外层 `transform: rotate(180deg)` 实现 reverse 底部锚定。所有作为 slot 插入的内容必须在根元素加 `transform: rotate(180deg)` 反旋转,否则文字/图片会颠倒显示。参考 `src/views/home/display/components/set.vue:2`
### 接口速查

View File

@ -296,6 +296,10 @@ const getItemStyle = (renderItem) => {
}
}
//
let pendingScrollDelta = 0
let scrollAdjustPending = false
const measureItem = (index, element) => {
if (!element) return
@ -307,8 +311,31 @@ const measureItem = (index, element) => {
if (height > 0) {
const cachedHeight = itemHeights.value.get(index)
if (cachedHeight !== height) {
const oldHeight = cachedHeight ?? props.estimatedHeight
const delta = height - oldHeight
//
const prevStart = visibleRange.value.start
itemHeights.value.set(index, height)
heightVersion.value++
// delta
if (delta !== 0 && index < prevStart) {
pendingScrollDelta += delta
if (!scrollAdjustPending) {
scrollAdjustPending = true
// Vue
Promise.resolve().then(() => {
scrollAdjustPending = false
const container = renderContainerRef.value
if (container && pendingScrollDelta !== 0) {
container.scrollTop += pendingScrollDelta
pendingScrollDelta = 0
}
})
}
}
}
}
}

View File

@ -0,0 +1,194 @@
window.TEST_DATA = [
{
"id": "839217090555557410",
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "839211834861958673",
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "BananaPro"
},
{
"id": "839209605929121287",
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "837370053866304564",
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "837360015437214709",
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "836534979084169461",
"fileUrl": "http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png",
"prompt": "<div data-v-43afc57f=\"\" class=\"prompt-container\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255); height: 39px;\"><div data-v-43afc57f=\"\" class=\"prompt-wrapper\" style=\"width: 1118px;\"><div data-v-43afc57f=\"\" class=\"prompt expanded\" style=\"max-height: none; overflow: visible;\"><span data-v-43afc57f=\"\" class=\"prompt-text\">将图1红色框内的【苹果】替换为【火龙果】</span></div><div><span data-v-43afc57f=\"\" class=\"prompt-text\"><br></span></div></div></div><div data-v-43afc57f=\"\" class=\"box success-box\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255);\"><div data-v-43afc57f=\"\" class=\"one-box\"></div></div>",
"model": "banana"
},
{
"id": "835464458670191734",
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "835463648116749398",
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "835463392293565520",
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "832562717234575283",
"fileUrl": "http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf66.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf67.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf68.png,http://test.xueai.art/file/2026/4/9/69d70e743888d39e4ed1cf69.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "839217090555557410",
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "839211834861958673",
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "BananaPro"
},
{
"id": "839209605929121287",
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "837370053866304564",
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "837360015437214709",
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "836534979084169461",
"fileUrl": "http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png",
"prompt": "<div data-v-43afc57f=\"\" class=\"prompt-container\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255); height: 39px;\"><div data-v-43afc57f=\"\" class=\"prompt-wrapper\" style=\"width: 1118px;\"><div data-v-43afc57f=\"\" class=\"prompt expanded\" style=\"max-height: none; overflow: visible;\"><span data-v-43afc57f=\"\" class=\"prompt-text\">将图1红色框内的【苹果】替换为【火龙果】</span></div><div><span data-v-43afc57f=\"\" class=\"prompt-text\"><br></span></div></div></div><div data-v-43afc57f=\"\" class=\"box success-box\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255);\"><div data-v-43afc57f=\"\" class=\"one-box\"></div></div>",
"model": "banana"
},
{
"id": "835464458670191734",
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "835463648116749398",
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "835463392293565520",
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "832562717234575283",
"fileUrl": "http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf66.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf67.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf68.png,http://test.xueai.art/file/2026/4/9/69d70e743888d39e4ed1cf69.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "830138209152283808",
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce3c743888d39e4ed1cf4f.png",
"prompt": "将图1红色框内的【苹果】替换为【火龙果】",
"model": "banana"
},
{
"id": "830136945106498711",
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce3b463888d39e4ed1cf4e.png",
"prompt": "一个女孩在树下吃苹果",
"model": "flux"
},
{
"id": "830083758811001839",
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce09be3888d39e4ed1cf48.png",
"prompt": "一个女孩在树下吃苹果",
"model": "flux"
},
{
"id": "829393290267734386",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb86b13888d39e4ed1cf32.png",
"prompt": "将图1红色框内的【女孩】替换为【图2中的女孩】",
"model": "banana"
},
{
"id": "829389466203337022",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb83223888d39e4ed1cf31.png",
"prompt": "将图1红色框内的【<span style=\"font-size: 14px;\">女孩</span>】替换为【图2中的女孩】",
"model": "banana"
},
{
"id": "829388114303660338",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb81df3888d39e4ed1cf30.png",
"prompt": "将图1红色框内的【女孩】替换为【男孩】",
"model": "banana"
},
{
"id": "829381253919682782",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb7b7c3888d39e4ed1cf2d.png",
"prompt": "将图1红色框内的【女孩】替换为【图2中的男孩】",
"model": "banana"
},
{
"id": "829324561060212843",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb46af3888d39e4ed1cf29.png",
"prompt": "一个女孩在树下吃苹果",
"model": "flux"
},
{
"id": "829319226454978647",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf25.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf26.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf27.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf28.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "829317957644464188",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb40893888d39e4ed1cf20.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "829305227994738709",
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb34ae3888d39e4ed1cf1e.png",
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
"model": "flux"
},
{
"id": "829068575099597628",
"fileUrl": "http://test.xueai.art/file/2026/3/30/69ca58473888d39e0bb8728b.png",
"prompt": "",
"model": ""
}
];

View File

@ -0,0 +1,700 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VirtualScroller 测试页</title>
<style>
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #eee;
overflow: hidden;
height: 100vh;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
/* 控制面板 */
.control-panel {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: #16213e;
border-bottom: 1px solid #0f3460;
flex-shrink: 0;
flex-wrap: wrap;
}
.control-panel button {
padding: 6px 16px;
border: 1px solid #0f3460;
border-radius: 6px;
background: #0f3460;
color: #e94560;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.control-panel button:hover {
background: #e94560;
color: #fff;
border-color: #e94560;
}
.control-panel .stat {
font-size: 12px;
color: #a0a0b0;
margin-left: 8px;
}
.control-panel .stat span {
color: #e94560;
font-weight: 600;
}
.control-panel .divider {
width: 1px;
height: 20px;
background: #0f3460;
margin: 0 4px;
}
/* 虚拟滚动容器 */
.test-scroller-wrap {
flex: 1;
min-height: 0;
}
/* 消息卡片 */
.message-card {
padding: 12px 16px;
border-bottom: 1px solid rgba(255,255,255,0.06);
display: flex;
gap: 12px;
align-items: flex-start;
transform: rotate(180deg);
}
.message-card .card-img-wrap {
flex-shrink: 0;
width: 274px;
border-radius: 8px;
overflow: hidden;
background: #0f3460;
position: relative;
}
.message-card .card-img-wrap img {
width: 100%;
height: auto;
display: block;
}
.message-card .card-img-wrap .img-placeholder {
width: 100%;
min-height: 150px;
background: linear-gradient(135deg, #0f3460, #16213e);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #a0a0b0;
}
.message-card .card-body {
flex: 1;
min-width: 0;
}
.message-card .card-model {
font-size: 11px;
color: #e94560;
margin-bottom: 4px;
font-weight: 600;
text-transform: uppercase;
}
.message-card .card-prompt {
font-size: 13px;
line-height: 1.6;
color: #ccc;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
.message-card .card-index {
font-size: 10px;
color: #666;
margin-top: 4px;
}
/* 底部加载提示 */
.bottom-loader {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
font-size: 13px;
gap: 8px;
transform: rotate(180deg);
}
.bottom-loader .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #e94560;
animation: pulse 1.4s infinite ease-in-out both;
}
.bottom-loader .dot:nth-child(1) { animation-delay: -0.32s; }
.bottom-loader .dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes pulse {
0%, 80%, 100% { transform: scale(0); opacity: 0.3; }
40% { transform: scale(1); opacity: 1; }
}
/* 多图网格 */
.multi-img-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px;
}
.multi-img-grid img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
}
/* 自定义滚动条 */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
</style>
</head>
<body>
<div id="app">
<div class="control-panel">
<button @click="addSingleItem">+ 添加 1 条</button>
<button @click="addBatchItems">+ 批量添加 10 条</button>
<button @click="scrollToBottom">↓ 滚到底部</button>
<button @click="scrollToTop">↑ 滚到顶部</button>
<span class="divider"></span>
<span class="stat">总数: <span>{{ stats.total }}</span></span>
<span class="stat">可见范围: <span>{{ stats.firstVisible }} - {{ stats.lastVisible }}</span></span>
<span class="stat">可见数: <span>{{ stats.visibleCount }}</span></span>
<span class="stat">测量项目: <span>{{ stats.measuredCount }}</span></span>
<span class="stat">高度版本: <span>{{ stats.heightVersion }}</span></span>
</div>
<div class="test-scroller-wrap">
<virtual-scroller
ref="scrollerRef"
:items="displayItems"
:estimated-height="180"
:buffer-size="2"
direction="reverse"
:bottom-placeholder-height="60"
@visible-change="onVisibleChange"
>
<!-- 消息卡片 -->
<template #default="{ item }">
<div class="message-card">
<div class="card-img-wrap">
<img
v-if="item.firstImageUrl"
:src="item.firstImageUrl"
@load="onImgLoad"
@error="onImgError"
referrerpolicy="no-referrer"
style="width:100%;height:auto;display:block;"
>
<div v-else class="img-placeholder">无图片</div>
</div>
<div class="card-body">
<div class="card-model">{{ item.model || 'Unknown' }}</div>
<div class="card-prompt">{{ item.prompt }}</div>
<div class="card-index">#{{ item._idx }}</div>
</div>
</div>
</template>
<!-- 底部占位 -->
<template #bottom-placeholder>
<div class="bottom-loader">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<span>下拉加载更多</span>
</div>
</template>
</virtual-scroller>
</div>
</div>
<script src="./test-data.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { ref, reactive, computed, watch, nextTick, onMounted, onBeforeUnmount, defineComponent, createApp } = Vue
// ===================== VirtualScroller 组件 =====================
const VirtualScroller = defineComponent({
name: 'VirtualScroller',
props: {
items: { type: Array, default: () => [] },
itemKey: { type: [String, Function], default: 'id' },
keyField: { type: String, default: 'id' },
estimatedHeight: { type: Number, default: 100 },
buffer: { type: Number, default: 3 },
bufferSize: { type: Number, default: 3 },
direction: { type: String, default: 'reverse' },
bottomPlaceholderHeight: { type: Number, default: 350 },
},
emits: ['scroll', 'scroll-start', 'scroll-end', 'visible-change', 'height-version-change'],
setup(props, { emit, expose }) {
const containerRef = ref(null)
const renderContainerRef = ref(null)
const itemHeights = ref(new Map())
const heightVersion = ref(0)
const resizeObserver = ref(null)
const scrollTop = ref(0)
const pendingScrollToBottom = ref(false)
// 推算 buffer
const computedBuffer = computed(() =>
props.buffer !== 3 ? props.buffer : props.bufferSize
)
// 容器高度
const containerHeight = computed(() =>
renderContainerRef.value ? renderContainerRef.value.clientHeight : 0
)
// 总高度
const totalHeight = computed(() => {
heightVersion.value // 依赖追踪
let h = 0
for (let i = 0; i < props.items.length; i++) {
h += itemHeights.value.get(i) ?? props.estimatedHeight
}
return h + props.bottomPlaceholderHeight
})
// key 提取
const getItemKey = (item, index) => {
const kf = typeof props.itemKey === 'function' ? props.itemKey : (props.itemKey !== 'id' ? props.itemKey : props.keyField)
if (typeof kf === 'function') return kf(item, index)
if (typeof kf === 'string' && item && typeof item === 'object') return item[kf] ?? index
return index
}
// 可见范围(两趟扫描)
const visibleRange = computed(() => {
if (!renderContainerRef.value || props.items.length === 0) {
return { start: 0, end: 0, offset: 0 }
}
heightVersion.value
const viewH = containerHeight.value
const st = scrollTop.value
const buf = computedBuffer.value
const len = props.items.length
// 第一趟:定位首个可见项
let offset = 0
let firstVisible = 0
for (let i = 0; i < len; i++) {
const h = itemHeights.value.get(i) ?? props.estimatedHeight
if (offset + h > st) { firstVisible = i; break }
offset += h
}
const startIdx = Math.max(0, firstVisible - buf)
// 第二趟:计算 startOffset + 定位 endIndex
let startOffset = 0
let endIdx = len - 1
offset = 0
for (let i = 0; i < len; i++) {
const h = itemHeights.value.get(i) ?? props.estimatedHeight
if (i < startIdx) { startOffset += h }
if (i >= startIdx) {
if (offset + h > st + viewH) {
endIdx = Math.min(len - 1, i + buf)
break
}
endIdx = i
}
offset += h
}
return { start: startIdx, end: endIdx, offset: startOffset }
})
// 可见项列表
const visibleItems = computed(() => {
const { start, end, offset } = visibleRange.value
const items = []
let curOffset = offset
for (let i = start; i <= end && i < props.items.length; i++) {
const h = itemHeights.value.get(i) ?? props.estimatedHeight
items.push({
item: props.items[i],
index: i,
offset: curOffset + props.bottomPlaceholderHeight,
height: h,
})
curOffset += h
}
return items
})
// 测量单项高度(直接 mutate Map + 版本号)
// 滚动锚定:累积可视区上方项的高度变化量,在微任务中统一补偿
let pendingScrollDelta = 0
let scrollAdjustPending = false
const measureItem = (index, element) => {
if (!element) return
const firstChild = element.firstElementChild
const target = firstChild || element
const h = Math.ceil(target.offsetHeight)
if (h > 0) {
const cached = itemHeights.value.get(index)
if (cached !== h) {
const oldH = cached ?? props.estimatedHeight
const delta = h - oldH
const prevStart = visibleRange.value.start
itemHeights.value.set(index, h)
heightVersion.value++
if (delta !== 0 && index < prevStart) {
pendingScrollDelta += delta
if (!scrollAdjustPending) {
scrollAdjustPending = true
Promise.resolve().then(() => {
scrollAdjustPending = false
const container = renderContainerRef.value
if (container && pendingScrollDelta !== 0) {
container.scrollTop += pendingScrollDelta
pendingScrollDelta = 0
}
})
}
}
}
}
}
// ResizeObserver
const setupResizeObserver = () => {
if (resizeObserver.value) resizeObserver.value.disconnect()
resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of entries) {
const idx = parseInt(entry.target.dataset.index, 10)
if (!isNaN(idx)) measureItem(idx, entry.target)
}
})
}
// 观察可见项(基于 visibleItems + querySelector不依赖外部 Map
const observeVisibleItems = () => {
if (!resizeObserver.value || !renderContainerRef.value) return
resizeObserver.value.disconnect()
for (const vItem of visibleItems.value) {
const el = renderContainerRef.value.querySelector(`[data-index="${vItem.index}"]`)
if (el) resizeObserver.value.observe(el)
}
}
// 滚轮处理(反向容器中取反 deltaY
const handleWheel = (event) => {
if (!renderContainerRef.value) return
renderContainerRef.value.scrollBy({ top: -event.deltaY, behavior: 'instant' })
event.preventDefault()
}
// 滚动事件
const handleScroll = (event) => {
const t = event.target
scrollTop.value = t.scrollTop
const st = t.scrollTop
const sh = t.scrollHeight
const ch = t.clientHeight
emit('scroll', {
target: t,
scrollTop: st,
scrollHeight: sh,
clientHeight: ch,
distanceToPageTop: sh - st - ch,
distanceToPageBottom: st,
isAtPageTop: sh - st - ch <= 0,
isAtPageBottom: st <= 0,
})
if (sh - st - ch <= 0) emit('scroll-start')
if (st <= 0) emit('scroll-end')
}
// ===== 暴露方法 =====
const scrollToBottom = (behavior = 'smooth') => {
if (!renderContainerRef.value) { pendingScrollToBottom.value = true; return }
requestAnimationFrame(() => {
renderContainerRef.value?.scrollTo({ top: 0, behavior })
})
}
const scrollToTop = (behavior = 'smooth') => {
if (!renderContainerRef.value) return
requestAnimationFrame(() => {
if (!renderContainerRef.value) return
renderContainerRef.value.scrollTo({ top: renderContainerRef.value.scrollHeight, behavior })
})
}
const resetMeasurements = () => {
itemHeights.value = new Map()
heightVersion.value++
}
// 监听数据变化
watch(() => props.items, (newData, oldData) => {
const oldLen = oldData?.length || 0
const newLen = newData.length
if (newLen !== oldLen) {
const newHeights = new Map()
const minLen = Math.min(oldLen, newLen)
for (let i = 0; i < minLen; i++) {
if (itemHeights.value.has(i)) newHeights.set(i, itemHeights.value.get(i))
}
itemHeights.value = newHeights
heightVersion.value++
nextTick(() => observeVisibleItems())
}
}, { deep: false })
// 监听可见项变化
watch(visibleItems, (newItems) => {
nextTick(() => observeVisibleItems())
if (newItems.length > 0) {
emit('visible-change', newItems[0].index, newItems[newItems.length - 1].index)
}
}, { deep: true })
onMounted(() => {
setupResizeObserver()
nextTick(() => {
if (pendingScrollToBottom.value) {
pendingScrollToBottom.value = false
scrollToBottom()
}
observeVisibleItems()
})
})
onBeforeUnmount(() => {
if (resizeObserver.value) resizeObserver.value.disconnect()
})
expose({
scrollToBottom,
scrollToTop,
resetMeasurements,
getScrollElement: () => renderContainerRef.value,
heightVersion,
itemHeights,
})
// 模板渲染函数
return {
containerRef, renderContainerRef,
totalHeight, visibleItems, getItemKey,
handleScroll, handleWheel,
bottomPlaceholderHeight: computed(() => props.bottomPlaceholderHeight),
}
},
template: `
<div ref="containerRef" style="height:100%;width:100%;position:relative;">
<div style="direction:rtl;height:100%;position:relative;overflow:hidden;transform:rotate(180deg);width:100%;">
<div
ref="renderContainerRef"
@scroll.passive="handleScroll"
@wheel="handleWheel"
style="direction:ltr;display:flex;flex-direction:column;justify-content:flex-end;bottom:0;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;width:100%;contain:layout style;"
>
<div style="flex-shrink:0;width:100%;" :style="{ height: totalHeight + 'px' }"></div>
<div :style="{ position:'absolute',left:0,right:0,top:0,width:'100%',height:bottomPlaceholderHeight+'px',zIndex:1 }">
<slot name="bottom-placeholder" />
</div>
<div
v-for="vItem in visibleItems"
:key="getItemKey(vItem.item, vItem.index)"
:data-index="vItem.index"
:style="{ position:'absolute',left:0,right:0,top:0,width:'100%',transform:'translateY('+vItem.offset+'px)',willChange:'transform',contain:'layout style' }"
>
<slot name="default" :item="vItem.item" :index="vItem.index" />
</div>
</div>
</div>
</div>
`,
})
// ===================== 测试应用 =====================
const app = createApp({
components: { VirtualScroller },
setup() {
// 初始化测试数据
const rawData = window.TEST_DATA || []
const baseItems = rawData.map((d, i) => ({
...d,
id: d.id || ('item-' + i),
_idx: i,
firstImageUrl: (d.fileUrl || '').split(',')[0].trim(),
}))
const displayItems = ref([...baseItems])
const scrollerRef = ref(null)
let counter = baseItems.length
const stats = reactive({
total: displayItems.value.length,
firstVisible: '-',
lastVisible: '-',
visibleCount: 0,
measuredCount: 0,
heightVersion: 0,
})
// 定时同步统计
setInterval(() => {
stats.total = displayItems.value.length
if (scrollerRef.value) {
stats.heightVersion = scrollerRef.value.heightVersion?.value ?? 0
stats.measuredCount = scrollerRef.value.itemHeights?.value?.size ?? 0
}
}, 500)
// 可见范围变化
const onVisibleChange = (first, last) => {
stats.firstVisible = first
stats.lastVisible = last
stats.visibleCount = last - first + 1
}
// 生成一条新消息
const makeItem = () => {
const tpl = baseItems[counter % baseItems.length]
return {
...tpl,
id: 'new-' + (counter + 1) + '-' + Date.now(),
_idx: counter,
firstImageUrl: tpl.firstImageUrl,
}
}
// 添加单条
const addSingleItem = () => {
counter++
displayItems.value = [...displayItems.value, makeItem()]
nextTick(() => scrollerRef.value?.scrollToBottom('smooth'))
}
// 批量添加
const addBatchItems = () => {
const newItems = []
for (let i = 0; i < 10; i++) {
counter++
newItems.push(makeItem())
}
displayItems.value = [...displayItems.value, ...newItems]
nextTick(() => scrollerRef.value?.scrollToBottom('smooth'))
}
const scrollToBottom = () => scrollerRef.value?.scrollToBottom('smooth')
const scrollToTop = () => scrollerRef.value?.scrollToTop('smooth')
const onImgLoad = () => {
// 图片加载后 ResizeObserver 会自动重新测量,无需额外处理
}
const onImgError = (e) => {
e.target.style.display = 'none'
}
onMounted(() => {
nextTick(() => scrollerRef.value?.scrollToBottom('auto'))
})
return {
displayItems, scrollerRef, stats,
onVisibleChange, addSingleItem, addBatchItems,
scrollToBottom, scrollToTop, onImgLoad, onImgError,
}
},
template: `
<div style="height:100vh;display:flex;flex-direction:column;">
<div class="control-panel">
<button @click="addSingleItem">+ 添加 1 条</button>
<button @click="addBatchItems">+ 批量添加 10 条</button>
<button @click="scrollToBottom">↓ 滚到底部</button>
<button @click="scrollToTop">↑ 滚到顶部</button>
<span class="divider"></span>
<span class="stat">总数: <span>{{ stats.total }}</span></span>
<span class="stat">可见范围: <span>{{ stats.firstVisible }} - {{ stats.lastVisible }}</span></span>
<span class="stat">可见数: <span>{{ stats.visibleCount }}</span></span>
<span class="stat">测量项目: <span>{{ stats.measuredCount }}</span></span>
<span class="stat">高度版本: <span>{{ stats.heightVersion }}</span></span>
</div>
<div style="flex:1;min-height:0;">
<virtual-scroller
ref="scrollerRef"
:items="displayItems"
:estimated-height="180"
:buffer-size="2"
direction="reverse"
:bottom-placeholder-height="60"
@visible-change="onVisibleChange"
>
<template #default="{ item }">
<div class="message-card">
<div class="card-img-wrap">
<img
v-if="item.firstImageUrl"
:src="item.firstImageUrl"
@load="onImgLoad"
@error="onImgError"
referrerpolicy="no-referrer"
style="width:100%;height:auto;display:block;"
>
<div v-else class="img-placeholder">无图片</div>
</div>
<div class="card-body">
<div class="card-model">{{ item.model || 'Unknown' }}</div>
<div class="card-prompt">{{ item.prompt }}</div>
<div class="card-index">#{{ item._idx }}</div>
</div>
</div>
</template>
<template #bottom-placeholder>
<div class="bottom-loader">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<span>下拉加载更多</span>
</div>
</template>
</virtual-scroller>
</div>
</div>
`,
})
app.mount('#app')
</script>
</body>
</html>

View File

@ -206,7 +206,7 @@ export function definePaintingPlatform() {
},
getUploaderBindings() {
return { limit: imageUploadLimit() }
return { limit: this.imageUploadLimit() }
},
showImageUploader() {

View File

@ -135,7 +135,7 @@ export function defineVideoPlatform() {
},
getUploaderBindings() {
return { modelType: modelType.value, imagesCount: imageUploadLimit() }
return { modelType: modelType.value, imagesCount: this.imageUploadLimit() }
},
showImageUploader() {

View File

@ -242,6 +242,7 @@ const fetchHistory = async (isLoadMore = false) => {
message: '获取历史失败',
type: 'warning'
})
isInitializing.value = false
} finally {
isLoading.value = false
}
@ -263,8 +264,10 @@ const handleScroll = (scrollInfo) => {
if (isAtPageBottom) {
useDisplay.Sender_variant = 'updown'
} else if (distanceToPageTop >= 350) {
} else if (distanceToPageBottom >= 350) {
useDisplay.Sender_variant = 'default'
} else {
useDisplay.Sender_variant = 'updown'
}
}