Update loading more experience (#292)

This commit is contained in:
Germey
2025-12-06 23:49:38 +08:00
committed by GitHub
parent ea1603cf0f
commit aa2c2cd2f7
25 changed files with 291 additions and 41 deletions
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "update loading more",
"packageName": "@acedatacloud/nexior",
"email": "cqc@cuiqingcai.com",
"dependentChangeType": "patch"
}
+51
View File
@@ -0,0 +1,51 @@
<template>
<div ref="panel" class="scroll-list relative" @scroll="onHandleScroll">
<top-loading v-if="loading" :text="loadingText" :floating="floatingLoader" />
<slot />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TopLoading from './TopLoading.vue';
export default defineComponent({
name: 'ScrollList',
components: {
TopLoading
},
props: {
loading: {
type: Boolean,
default: false
},
loadingText: {
type: String,
default: ''
},
floatingLoader: {
type: Boolean,
default: true
}
},
emits: ['reach-top', 'scroll'],
methods: {
onHandleScroll() {
const el = this.$refs.panel as HTMLElement;
this.$emit('scroll', el);
if (el.scrollTop === 0) {
this.$emit('reach-top');
}
},
getScrollElement(): HTMLElement | undefined {
return this.$refs.panel as HTMLElement | undefined;
}
}
});
</script>
<style lang="scss" scoped>
.scroll-list {
position: relative;
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div :class="['top-loading', { floating }]" role="status" aria-live="polite">
<el-icon class="is-loading" :size="size">
<loading />
</el-icon>
<span>{{ label }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { ElIcon } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
export default defineComponent({
name: 'TopLoading',
components: {
ElIcon,
Loading
},
props: {
text: {
type: String,
default: ''
},
size: {
type: Number,
default: 16
},
floating: {
type: Boolean,
default: true
}
},
computed: {
label(): string {
return this.text || (this as any).$t('common.status.loading');
}
}
});
</script>
<style lang="scss" scoped>
.top-loading {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
background: var(--el-bg-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
color: var(--el-text-color-regular);
pointer-events: none;
&.floating {
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
}
</style>
+20 -8
View File
@@ -2,9 +2,15 @@
<div v-if="tasks?.items === undefined">
<bot-placeholder />
</div>
<div v-else-if="tasks?.items?.length && tasks?.items?.length > 0" class="tasks h-full w-full overflow-y-auto">
<scroll-list
v-else-if="tasks?.items?.length && tasks?.items?.length > 0"
ref="scrollList"
class="tasks h-full w-full overflow-y-auto"
:loading="loading"
@reach-top="$emit('reach-top')"
>
<task-preview v-for="task in tasks?.items" :key="task.id" :model-value="task" />
</div>
</scroll-list>
<div v-if="tasks?.items?.length === 0" class="w-full h-full flex items-center justify-center">
<no-tasks />
</div>
@@ -15,13 +21,21 @@ import { defineComponent } from 'vue';
import TaskPreview from './task/Preview.vue';
import BotPlaceholder from '../common/BotPlaceholder.vue';
import NoTasks from '@/components/common/NoTasks.vue';
import ScrollList from '@/components/common/ScrollList.vue';
export default defineComponent({
name: 'RecentPanel',
components: {
TaskPreview,
BotPlaceholder,
NoTasks
NoTasks,
ScrollList
},
props: {
loading: {
type: Boolean,
default: false
}
},
emits: ['reach-top'],
data() {
@@ -38,11 +52,9 @@ export default defineComponent({
}
},
methods: {
onHandleScroll() {
const el = this.$refs.panel as HTMLElement;
if (el.scrollTop === 0) {
this.$emit('reach-top');
}
getScrollElement(): HTMLElement | undefined {
const list = this.$refs.scrollList as any;
return list?.getScrollElement?.();
}
}
});
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "الحالة",
"description": "حالة الكيان"
},
"status.loading": {
"message": "جارٍ التحميل...",
"description": "نص الحالة المعروض أثناء تحميل المحتوى"
},
"entity.operation": {
"message": "العملية",
"description": "عملية الكيان"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "نص في شريط التنقل في الموقع لعرض صفحة Claude، يجب أن يبقى كما هو 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Status",
"description": "Der Status der Entität"
},
"status.loading": {
"message": "Wird geladen...",
"description": "Statustext, der während des Ladens angezeigt wird"
},
"entity.operation": {
"message": "Operation",
"description": "Die Operation der Entität"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Text in der Navigationsleiste der Website, um die Claude-Seite anzuzeigen, muss als 'Claude' bleiben"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Κατάσταση",
"description": "Η κατάσταση της οντότητας"
},
"status.loading": {
"message": "Φόρτωση...",
"description": "Κείμενο κατάστασης που εμφανίζεται κατά τη φόρτωση περιεχομένου"
},
"entity.operation": {
"message": "Λειτουργία",
"description": "Η λειτουργία της οντότητας"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Κείμενο στη γραμμή πλοήγησης του ιστότοπου για να εμφανιστεί η σελίδα Claude, πρέπει να παραμείνει ως 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Status",
"description": "The status of the entity"
},
"status.loading": {
"message": "Loading...",
"description": "Status text shown while content is loading"
},
"entity.operation": {
"message": "Operation",
"description": "The operation of the entity"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Text in the website navigation bar to display the Claude page, must remain as 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Estado",
"description": "El estado de la entidad"
},
"status.loading": {
"message": "Cargando...",
"description": "Texto de estado que se muestra mientras se carga el contenido"
},
"entity.operation": {
"message": "Operación",
"description": "La operación de la entidad"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Texto en la barra de navegación del sitio web para mostrar la página de Claude, debe permanecer como 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Tila",
"description": "Entiteetin tila"
},
"status.loading": {
"message": "Ladataan...",
"description": "Tilateksti, joka näytetään sisällön latautuessa"
},
"entity.operation": {
"message": "Toiminto",
"description": "Entiteetin toiminto"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Teksti verkkosivuston navigointipalkissa, joka näyttää Claude-sivun, on säilytettävä 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Statut",
"description": "Le statut de l'entité"
},
"status.loading": {
"message": "Chargement...",
"description": "Texte de statut affiché pendant le chargement du contenu"
},
"entity.operation": {
"message": "Opération",
"description": "L'opération de l'entité"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Texte dans la barre de navigation du site pour afficher la page Claude, doit rester 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Stato",
"description": "Lo stato dell'entità"
},
"status.loading": {
"message": "Caricamento...",
"description": "Testo di stato mostrato durante il caricamento dei contenuti"
},
"entity.operation": {
"message": "Operazione",
"description": "L'operazione dell'entità"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Testo nella barra di navigazione del sito web per visualizzare la pagina Claude, deve rimanere come 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "ステータス",
"description": "エンティティのステータス"
},
"status.loading": {
"message": "読み込み中...",
"description": "コンテンツ読込中に表示されるステータス文言"
},
"entity.operation": {
"message": "操作",
"description": "エンティティの操作"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "ウェブサイトのナビゲーションバーに表示されるClaudeページのテキスト、'Claude'のままにする必要があります"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "상태",
"description": "엔티티의 상태"
},
"status.loading": {
"message": "로딩 중...",
"description": "콘텐츠가 로딩될 때 표시되는 상태 문구"
},
"entity.operation": {
"message": "작업",
"description": "엔티티의 작업"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "웹사이트 내비게이션 바에서 Claude 페이지를 표시하는 텍스트, 'Claude'로 유지해야 함"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Status",
"description": "Status encji"
},
"status.loading": {
"message": "Ładowanie...",
"description": "Tekst statusu wyświetlany podczas wczytywania treści"
},
"entity.operation": {
"message": "Operacja",
"description": "Operacja encji"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Tekst w pasku nawigacyjnym strony internetowej, aby wyświetlić stronę Claude, musi pozostać jako 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Status",
"description": "Status da entidade"
},
"status.loading": {
"message": "Carregando...",
"description": "Texto de status exibido enquanto o conteúdo é carregado"
},
"entity.operation": {
"message": "Operação",
"description": "Operação da entidade"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Texto na barra de navegação do site para exibir a página Claude, deve permanecer como 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Статус",
"description": "Статус сущности"
},
"status.loading": {
"message": "Загрузка...",
"description": "Текст статуса, отображаемый во время загрузки контента"
},
"entity.operation": {
"message": "Операция",
"description": "Операция сущности"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Текст в навигационной панели сайта для отображения страницы Claude, должен оставаться как 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Status",
"description": "Status entiteta"
},
"status.loading": {
"message": "Учитавање...",
"description": "Текст статуса који се приказује док се садржај учитава"
},
"entity.operation": {
"message": "Operacija",
"description": "Operacija entiteta"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Tekst u navigacionoj traci sajta za prikaz Claude stranice, mora ostati kao 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Status",
"description": "Status för enheten"
},
"status.loading": {
"message": "Laddar...",
"description": "Statustext som visas medan innehållet laddas"
},
"entity.operation": {
"message": "Operation",
"description": "Operationen för enheten"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Text i webbplatsens navigeringsfält för att visa Claude-sidan, måste förbli som 'Claude'"
}
}
}
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "Статус",
"description": "Статус сутності"
},
"status.loading": {
"message": "Завантаження...",
"description": "Текст статусу, що показується під час завантаження контенту"
},
"entity.operation": {
"message": "Операція",
"description": "Операція сутності"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "Текст у навігаційній панелі сайту для відображення сторінки Claude, має залишатися 'Claude'"
}
}
}
+4
View File
@@ -187,6 +187,10 @@
"message": "状态",
"description": "实体的状态"
},
"status.loading": {
"message": "加载中...",
"description": "内容加载时显示的状态文案"
},
"entity.operation": {
"message": "操作",
"description": "实体的操作"
+5 -1
View File
@@ -187,6 +187,10 @@
"message": "狀態",
"description": "實體的狀態"
},
"status.loading": {
"message": "載入中...",
"description": "內容載入時顯示的狀態文案"
},
"entity.operation": {
"message": "操作",
"description": "實體的操作"
@@ -575,4 +579,4 @@
"message": "Claude",
"description": "網站導航欄中的文本,用於顯示Claude頁面,必須保持為'Claude'"
}
}
}
+47 -13
View File
@@ -4,7 +4,7 @@
<config-panel @generate="onGenerate" />
</template>
<template #result>
<recent-panel @reach-top="onReachTop" />
<recent-panel ref="recentPanel" :loading="loading" @reach-top="onReachTop" />
</template>
</layout>
</template>
@@ -25,6 +25,7 @@ const CALLBACK_URL = 'https://webhook.acedata.cloud/nanobanana';
interface IData {
task: INanobananaTask | undefined;
job: number;
loading: boolean;
}
export default defineComponent({
@@ -38,13 +39,17 @@ export default defineComponent({
data(): IData {
return {
task: undefined,
job: 0
job: 0,
loading: false
};
},
computed: {
loading() {
applicationsLoading() {
return this.$store.state.nanobanana?.status?.getApplications === Status.Request;
},
tasksLoading() {
return this.$store.state.nanobanana?.status?.getTasks === Status.Request;
},
credential() {
return this.$store.state.nanobanana?.credential;
},
@@ -90,9 +95,35 @@ export default defineComponent({
methods: {
async onReachTop() {
console.debug('reached top');
await this.onGetTasks({
createdAtMax: this.tasks?.items?.[0]?.created_at
});
if (this.loading || this.tasksLoading) {
return;
}
const total = this.tasks?.total;
const currentLength = this.tasks?.items?.length || 0;
if (total !== undefined && total <= currentLength) {
return;
}
const oldest = this.tasks?.items?.[0];
if (!oldest?.created_at) {
return;
}
const panel = this.$refs.recentPanel as any;
const el = panel?.getScrollElement?.() as HTMLElement | undefined;
const previousHeight = el?.scrollHeight || 0;
const previousScrollTop = el?.scrollTop || 0;
this.loading = true;
try {
await this.onGetTasks({
createdAtMax: oldest.created_at
});
await this.$nextTick();
if (el) {
const newHeight = el.scrollHeight;
el.scrollTop = newHeight - previousHeight + previousScrollTop;
}
} finally {
this.loading = false;
}
},
async onGetService() {
console.debug('start onGetService');
@@ -106,15 +137,14 @@ export default defineComponent({
await this.onGetTasks();
},
async onScrollDown() {
setTimeout(() => {
const el = document.querySelector('.tasks');
if (el) {
el.scrollTop = el.scrollHeight;
}
}, 1000);
await this.$nextTick();
const el = this.getTasksScrollElement();
if (el) {
el.scrollTop = el.scrollHeight;
}
},
async onGetTasks(payload?: { limit?: number; createdAtMin?: number; createdAtMax?: number }) {
if (this.loading) {
if (this.applicationsLoading || this.tasksLoading) {
console.debug('loading');
return;
}
@@ -176,6 +206,10 @@ export default defineComponent({
await this.onScrollDown();
}, 1000);
});
},
getTasksScrollElement(): HTMLElement | undefined {
const panel = this.$refs.recentPanel as any;
return panel?.getScrollElement?.();
}
}
});
+6
View File
@@ -129,18 +129,22 @@ export const getTasks = async (
createdAtMax
}: { offset?: number; limit?: number; createdAtMin?: number; createdAtMax?: number }
): Promise<INanobananaTask[]> => {
state.status.getTasks = Status.Request;
return new Promise((resolve, reject) => {
console.debug('start to get tasks', offset, limit);
const credential = state.credential;
console.debug('current credential', credential);
const token = credential?.token;
if (!token) {
state.status.getTasks = Status.Error;
return reject('no token');
}
nanobananaOperator
.tasks(
{
userId: rootState?.user?.id,
offset,
limit,
createdAtMin,
createdAtMax,
type: 'images'
@@ -158,9 +162,11 @@ export const getTasks = async (
const mergedItems = mergeAndSortLists(existingItems, newItems);
commit('setTasksItems', mergedItems);
commit('setTasksTotal', response.data.count);
state.status.getTasks = Status.Success;
resolve(response.data.items);
})
.catch((error) => {
state.status.getTasks = Status.Error;
return reject(error);
});
});
+9 -3
View File
@@ -2,6 +2,12 @@ import { IApplication, ICredential, INanobananaConfig, INanobananaTask, IService
import initialState from './state';
import { INanobananaState } from './models';
const defaultTasks = {
items: undefined,
total: undefined,
active: undefined
};
export const resetAll = (state: INanobananaState): void => {
Object.assign(state, initialState());
};
@@ -28,7 +34,7 @@ export const setConfig = (state: INanobananaState, payload: INanobananaConfig):
export const setTasksItems = (state: INanobananaState, payload: INanobananaTask[]): void => {
const newPayload = {
...state.tasks,
...(state.tasks || defaultTasks),
items: payload
} as typeof state.tasks;
state.tasks = newPayload;
@@ -36,7 +42,7 @@ export const setTasksItems = (state: INanobananaState, payload: INanobananaTask[
export const setTasksTotal = (state: INanobananaState, payload: number): void => {
const newPayload = {
...state.tasks,
...(state.tasks || defaultTasks),
total: payload
} as typeof state.tasks;
state.tasks = newPayload;
@@ -44,7 +50,7 @@ export const setTasksTotal = (state: INanobananaState, payload: number): void =>
export const setTasksActive = (state: INanobananaState, payload: INanobananaTask): void => {
const newPayload = {
...state.tasks,
...(state.tasks || defaultTasks),
active: payload
} as typeof state.tasks;
state.tasks = newPayload;