mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-09-24 23:11:59 +08:00
perf(component): ⚡ 优化虚拟列表加载更多功能 (#129)
* perf(component): ⚡ 优化虚拟列表加载更多功能 * fix(github): 🐛 修复PR copilot 工作流问题 * fix(github): 🐛 修复PR copilot工作流没安装pnpm问题 * fix(action): 🐛 修改pr分析的机器人为通义千问 * fix(action): 🐛 修复配置通义千问配置问题 * fix(action): 🐛 修复api调用方式
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
name: PR Review Bot
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get PR diff
|
||||
id: diff
|
||||
run: |
|
||||
git fetch origin ${{ github.event.pull_request.base.sha }}
|
||||
DIFF=$(git diff ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
|
||||
echo "DIFF<<EOF" >> $GITHUB_ENV
|
||||
echo "$DIFF" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Install OpenAI SDK
|
||||
run: |
|
||||
pnpm config set registry https://registry.npmmirror.com/
|
||||
pnpm add openai
|
||||
|
||||
- name: Analyze PR
|
||||
id: analyze
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
with:
|
||||
script: |
|
||||
const OpenAI = require('openai');
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.DASHSCOPE_API_KEY,
|
||||
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
});
|
||||
|
||||
const diff = process.env.DIFF;
|
||||
|
||||
try {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "qwen-plus",
|
||||
messages: [{
|
||||
role: "system",
|
||||
content: "你是一个代码审查助手,请用中文分析以下代码变更。"
|
||||
}, {
|
||||
role: "user",
|
||||
content: `请分析以下代码变更并总结主要改动:\n\n${diff}`
|
||||
}],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000
|
||||
});
|
||||
|
||||
const analysis = completion.choices[0].message.content;
|
||||
core.setOutput('analysis', analysis);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `## PR 代码分析
|
||||
|
||||
${analysis}
|
||||
|
||||
---
|
||||
*这是由通义千问 AI 自动生成的 PR 分析,仅供参考。*`
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(`分析失败: ${error.message}`);
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="virtual-list-container" @scroll="handleScroll">
|
||||
<n-flex v-if="isLoadingMore" justify="center" class="box-border absolute-x-center pt-10px">
|
||||
<img class="size-16px" src="@/assets/img/loading.svg" alt="" />
|
||||
<span class="text-(14px #909090)">加载中</span>
|
||||
</n-flex>
|
||||
<div class="virtual-list-phantom" :style="{ height: `${totalHeight}px` }"></div>
|
||||
<div class="virtual-list-content" :style="{ transform: `translateY(${offset}px)` }">
|
||||
<div v-for="item in visibleData" :key="item.message?.id" :id="`item-${item.message?.id}`">
|
||||
@@ -14,6 +18,7 @@ const props = defineProps<{
|
||||
items: any[]
|
||||
estimatedItemHeight?: number
|
||||
buffer?: number
|
||||
isLoadingMore?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -25,8 +30,9 @@ const emit = defineEmits<{
|
||||
const DEFAULT_ESTIMATED_HEIGHT = 80 // 默认预估的每项高度
|
||||
const BUFFER_SIZE = props.buffer || 5 // 上下缓冲区域的数量
|
||||
const OVERSCAN_SIZE = 1000 // 预渲染区域的像素高度,防止滚动时出现空白
|
||||
const estimatedItemHeight = props.estimatedItemHeight || DEFAULT_ESTIMATED_HEIGHT // 每项的预估高度
|
||||
const MAX_CACHE_SIZE = 1000 // 高度缓存的最大数量
|
||||
const LOADING_OFFSET = 26 // 加载中需要的偏移量(26px是加载动画的高度)
|
||||
const ESTIMATED_ITEM_HEIGHT = props.estimatedItemHeight || DEFAULT_ESTIMATED_HEIGHT // 每项的预估高度
|
||||
|
||||
// 响应式引用
|
||||
const containerRef = ref<HTMLElement | null>(null) // 容器元素引用
|
||||
@@ -78,7 +84,7 @@ const visibleData = computed(() => {
|
||||
const totalHeight = computed(() => {
|
||||
// 累加所有项目的高度,如果没有缓存则使用预估高度
|
||||
return props.items.reduce((total, item) => {
|
||||
return total + (heights.value.get(item.message?.id?.toString()) || estimatedItemHeight)
|
||||
return total + (heights.value.get(item.message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
@@ -127,18 +133,29 @@ const updateItemHeight = () => {
|
||||
|
||||
// 根据滚动位置计算起始索引
|
||||
const getStartIndex = (scrollTop: number) => {
|
||||
let total = 0
|
||||
let index = 0
|
||||
const accumulatedHeights: number[] = []
|
||||
let totalHeight = 0
|
||||
// 预计算累积高度 O(n),但只需要在列表数据变化时更新
|
||||
props.items.forEach((item, index) => {
|
||||
totalHeight += heights.value.get(item.message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT
|
||||
accumulatedHeights[index] = totalHeight
|
||||
})
|
||||
|
||||
// 累加高度直到超过当前滚动位置减去预渲染区域
|
||||
while (total < scrollTop - OVERSCAN_SIZE && index < props.items.length) {
|
||||
const itemHeight = heights.value.get(props.items[index].message?.id?.toString()) || estimatedItemHeight
|
||||
total += itemHeight
|
||||
index++
|
||||
// 二分查找 O(log n)
|
||||
let left = 0
|
||||
let right = accumulatedHeights.length - 1
|
||||
const target = scrollTop - OVERSCAN_SIZE
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
if (accumulatedHeights[mid] < target) {
|
||||
left = mid + 1
|
||||
} else {
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
|
||||
// 返回计算后的索引,确保留有缓冲区
|
||||
return Math.max(0, index - BUFFER_SIZE)
|
||||
return Math.max(0, left - BUFFER_SIZE)
|
||||
}
|
||||
|
||||
// 计算指定索引的偏移量
|
||||
@@ -146,7 +163,7 @@ const getOffsetForIndex = (index: number) => {
|
||||
let total = 0
|
||||
// 累加到目标索引前的所有项目高度
|
||||
for (let i = 0; i < index; i++) {
|
||||
const itemHeight = heights.value.get(props.items[i].message?.id?.toString()) || estimatedItemHeight
|
||||
const itemHeight = heights.value.get(props.items[i].message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT
|
||||
total += itemHeight
|
||||
}
|
||||
return total
|
||||
@@ -166,7 +183,7 @@ const updateVisibleRange = () => {
|
||||
|
||||
// 累加高度直到超过可视区域加上预渲染区域
|
||||
while (total < clientHeight + OVERSCAN_SIZE * 2 && end < props.items.length) {
|
||||
const itemHeight = heights.value.get(props.items[end].message?.id?.toString()) || estimatedItemHeight
|
||||
const itemHeight = heights.value.get(props.items[end].message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT
|
||||
total += itemHeight
|
||||
end++
|
||||
}
|
||||
@@ -176,7 +193,8 @@ const updateVisibleRange = () => {
|
||||
|
||||
// 更新可见范围和偏移量
|
||||
visibleRange.value = { start, end }
|
||||
offset.value = getOffsetForIndex(start)
|
||||
// 加上加载中需要的偏移量
|
||||
offset.value = getOffsetForIndex(start) + LOADING_OFFSET
|
||||
}
|
||||
|
||||
// 更新可见范围的帧动画处理
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
当前网络不可用,请检查你的网络设置
|
||||
</n-flex>
|
||||
|
||||
<!-- 加载中提示 -->
|
||||
<n-flex justify="center" class="box-border pt-10px" v-if="messageOptions?.isLoading && isLoadingMore">
|
||||
<img class="size-16px" src="@/assets/img/loading.svg" alt="" />
|
||||
<span class="text-(14px #909090)">加载中</span>
|
||||
</n-flex>
|
||||
|
||||
<Transition name="chat-init" appear mode="out-in" @after-leave="handleTransitionComplete">
|
||||
<!-- 初次加载的骨架屏 -->
|
||||
<n-flex
|
||||
@@ -50,11 +44,12 @@
|
||||
id="image-chat-main"
|
||||
ref="virtualListInst"
|
||||
:items="chatMessageList"
|
||||
:estimated-item-height="itemSize"
|
||||
:estimatedItemHeight="itemSize"
|
||||
:buffer="5"
|
||||
:isLoadingMore="messageOptions?.isLoading && isLoadingMore"
|
||||
@scroll="handleScroll"
|
||||
@scroll-direction-change="handleScrollDirectionChange"
|
||||
:style="{ maxHeight: `calc(100vh - ${messageOptions?.isLoading && isLoadingMore ? '286px' : '260px'})` }">
|
||||
style="max-height: calc(100vh - 260px)">
|
||||
<template #default="{ item, index }">
|
||||
<n-flex
|
||||
vertical
|
||||
@@ -491,7 +486,7 @@ watch(
|
||||
|
||||
// 如果正在加载历史消息,优先处理历史消息的滚动
|
||||
if (isLoadingMore.value) {
|
||||
if (scrollTop.value < 10) {
|
||||
if (scrollTop.value < 26) {
|
||||
requestAnimationFrame(() => {
|
||||
virtualListInst.value?.scrollTo({ index: value.length - oldValue.length })
|
||||
})
|
||||
@@ -541,13 +536,13 @@ const handleScroll = () => {
|
||||
const scrollHeight = container.scrollHeight
|
||||
// 获取容器的可视区域高度
|
||||
const clientHeight = container.clientHeight
|
||||
// 计算距离底部的距离
|
||||
// ��算距离底部的距离
|
||||
const distanceFromBottom = scrollHeight - scrollTop.value - clientHeight
|
||||
|
||||
// 存储 requestAnimationFrame 的返回值
|
||||
rafId.value = requestAnimationFrame(async () => {
|
||||
// 处理触顶加载更多
|
||||
if (scrollTop.value === 0) {
|
||||
if (scrollTop.value < 26) {
|
||||
// 如果正在加载或已经触发了加载,则不重复触发
|
||||
if (messageOptions.value?.isLoading || isLoadingMore.value) return
|
||||
|
||||
@@ -742,7 +737,7 @@ const canReEdit = computed(() => (msgId: number) => {
|
||||
const message = chatStore.getMessage(msgId)
|
||||
if (!recalledMsg || !message) return false
|
||||
|
||||
// 只需要判断是否是当前用户的消息,时间检查已经在 getRecalledMessage 中处理
|
||||
// 只需要判断是否是当前用户的消��,时间检查已经在 getRecalledMessage 中处理
|
||||
return message.fromUser.uid === userUid.value
|
||||
})
|
||||
|
||||
|
||||
Vendored
+2
@@ -43,6 +43,7 @@ declare module 'vue' {
|
||||
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||
NInfiniteScroll: typeof import('naive-ui')['NInfiniteScroll']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
@@ -58,6 +59,7 @@ declare module 'vue' {
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
|
||||
Reference in New Issue
Block a user