feat(chat): add randomized summary loaders in minimal mode

This commit is contained in:
JOJO 2026-04-04 19:47:17 +08:00
parent 8d5a034c70
commit 3b710020fe
9 changed files with 859 additions and 63 deletions

View File

@ -1,12 +1,5 @@
<template> <template>
<div class="minimal-blocks-container"> <div class="minimal-blocks-container">
<!-- 等待动画 -->
<div v-if="showGenerating" class="generating-placeholder">
<span v-for="(letter, letterIdx) in '思考中...'.split('')" :key="letterIdx" class="letter" :style="{ animationDelay: `${letterIdx * 0.1}s` }">
{{ letter }}
</span>
</div>
<!-- 按真实顺序显示分组 --> <!-- 按真实顺序显示分组 -->
<template v-for="group in blockGroups" :key="group.id"> <template v-for="group in blockGroups" :key="group.id">
<!-- 摘要组thinking/tool --> <!-- 摘要组thinking/tool -->
@ -20,9 +13,14 @@
<div class="summary-content-wrapper"> <div class="summary-content-wrapper">
<span class="summary-preview">{{ getSummaryPreview(group.actions) }}</span> <span class="summary-preview">{{ getSummaryPreview(group.actions) }}</span>
</div> </div>
<svg class="expand-icon" :class="{ expanded: expandedGroups.has(group.id) }" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <!-- 加载动画或完成图标 -->
<polyline points="6 9 12 15 18 9"></polyline> <div class="summary-status-icon">
</svg> <component v-if="isSummaryRunning(group.actions, group.id)" :is="getSummaryLoader(group.id)" />
<svg v-else class="check-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<polyline points="7.5 12 10.5 15 16.5 9" />
</svg>
</div>
</div> </div>
<!-- 步骤容器展开时显示所有步骤 --> <!-- 步骤容器展开时显示所有步骤 -->
@ -78,9 +76,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'; import { ref, computed, watch, nextTick, Component } from 'vue';
import { usePersonalizationStore } from '@/stores/personalization'; import { usePersonalizationStore } from '@/stores/personalization';
import { renderEnhancedToolResult } from './actions/toolRenderers'; import { renderEnhancedToolResult } from './actions/toolRenderers';
import { getRandomLoader } from './loaders/index';
interface Action { interface Action {
id?: string; id?: string;
@ -128,6 +127,46 @@ const props = defineProps<{
const personalizationStore = usePersonalizationStore(); const personalizationStore = usePersonalizationStore();
const expandedGroups = ref(new Set<string>()); const expandedGroups = ref(new Set<string>());
const thinkingRefs = new Map<string, HTMLElement>();
const summaryLoaders = new Map<string, Component>();
// action
const getSummaryLoader = (groupId: string) => {
const group = blockGroups.value.find(g => g.id === groupId);
if (!group || !group.actions) return getRandomLoader();
// streamingactionaction
const streamingAction = group.actions.find(a => a.streaming);
const currentAction = streamingAction || group.actions[group.actions.length - 1];
if (!currentAction) return getRandomLoader();
// actionaction
// thinking <-> tool
const currentIndex = group.actions.indexOf(currentAction);
let lastDifferentTypeIndex = -1;
for (let i = currentIndex - 1; i >= 0; i--) {
if (group.actions[i].type !== currentAction.type) {
lastDifferentTypeIndex = i;
break;
}
}
// 使""key
// actionkey
// action使key
const typeSegmentKey = lastDifferentTypeIndex >= 0
? `${group.actions[lastDifferentTypeIndex].id}-${currentAction.type}`
: `start-${currentAction.type}`;
const cacheKey = `${groupId}-${typeSegmentKey}`;
if (!summaryLoaders.has(cacheKey)) {
summaryLoaders.set(cacheKey, getRandomLoader());
}
return summaryLoaders.get(cacheKey);
};
// actions thinking/tool text // actions thinking/tool text
const blockGroups = computed(() => { const blockGroups = computed(() => {
@ -141,11 +180,14 @@ const blockGroups = computed(() => {
} else if (action.type === 'text') { } else if (action.type === 'text') {
// thinking/tool // thinking/tool
if (currentGroup.length > 0) { if (currentGroup.length > 0) {
// 使 action id group id
const firstActionId = currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
groups.push({ groups.push({
type: 'summary', type: 'summary',
id: `summary-${groupIndex++}`, id: `summary-${firstActionId}`,
actions: currentGroup actions: currentGroup
}); });
groupIndex++;
currentGroup = []; currentGroup = [];
} }
// text // text
@ -160,9 +202,11 @@ const blockGroups = computed(() => {
// thinking/tool // thinking/tool
if (currentGroup.length > 0) { if (currentGroup.length > 0) {
// 使 action id group id
const firstActionId = currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
groups.push({ groups.push({
type: 'summary', type: 'summary',
id: `summary-${groupIndex}`, id: `summary-${firstActionId}`,
actions: currentGroup actions: currentGroup
}); });
} }
@ -170,20 +214,25 @@ const blockGroups = computed(() => {
return groups; return groups;
}); });
//
const showGenerating = computed(() => {
const hasAnyThinkingOrTool = props.actions.some(a => a.type === 'thinking' || a.type === 'tool');
const isStreaming = props.actions.some(a => a.streaming && (a.type === 'thinking' || a.type === 'tool'));
return isStreaming && !hasAnyThinkingOrTool;
});
// / // /
const getSummaryPreview = (actions: Action[]) => { const getSummaryPreview = (actions: Action[]) => {
// streaming // streaming
const streamingActions = actions.filter(a => a.streaming); const streamingActions = actions.filter(a => a.streaming);
const currentStep = streamingActions.length > 0
? streamingActions[streamingActions.length - 1] let currentStep;
: actions[actions.length - 1]; // streaming if (streamingActions.length > 0) {
// streaming streaming
currentStep = streamingActions[streamingActions.length - 1];
} else {
// streaming
const toolActions = actions.filter(a => a.type === 'tool');
if (toolActions.length > 0) {
currentStep = toolActions[toolActions.length - 1];
} else {
//
currentStep = actions[actions.length - 1];
}
}
if (!currentStep) return ''; if (!currentStep) return '';
@ -223,7 +272,7 @@ const getSummaryPreview = (actions: Action[]) => {
return ''; return '';
}; };
// //
const isSummaryRunning = (actions: Action[], groupId: string) => { const isSummaryRunning = (actions: Action[], groupId: string) => {
// group blockGroups // group blockGroups
const currentIndex = blockGroups.value.findIndex(g => g.id === groupId); const currentIndex = blockGroups.value.findIndex(g => g.id === groupId);
@ -232,11 +281,9 @@ const isSummaryRunning = (actions: Action[], groupId: string) => {
// //
const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some(g => g.type === 'text'); const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some(g => g.type === 'text');
// summary group //
if (hasTextAfter) return false; // 使
return !hasTextAfter;
// summary group
return true;
}; };
// //
@ -253,8 +300,45 @@ const getSummarySteps = (actions: Action[]) => {
const toggleExpand = (groupId: string) => { const toggleExpand = (groupId: string) => {
if (expandedGroups.value.has(groupId)) { if (expandedGroups.value.has(groupId)) {
expandedGroups.value.delete(groupId); expandedGroups.value.delete(groupId);
// ref
nextTick(() => {
const group = blockGroups.value.find(g => g.id === groupId);
if (group && group.actions) {
group.actions.forEach(action => {
if (action.type === 'thinking') {
const blockId = action.id || action.blockId;
if (blockId && typeof props.registerThinkingRef === 'function') {
props.registerThinkingRef(blockId, null);
}
}
});
}
});
} else { } else {
expandedGroups.value.add(groupId); expandedGroups.value.add(groupId);
// ref
nextTick(() => {
const group = blockGroups.value.find(g => g.id === groupId);
if (group && group.actions) {
group.actions.forEach(action => {
if (action.type === 'thinking') {
const blockId = action.id || action.blockId;
if (blockId) {
const el = thinkingRefs.get(blockId);
if (el && typeof props.registerThinkingRef === 'function') {
props.registerThinkingRef(blockId, el);
// streaming
if (action.streaming) {
el.scrollTop = el.scrollHeight;
}
}
}
}
});
}
});
} }
}; };
@ -338,8 +422,23 @@ const escapeHtml = (text: string) => {
}; };
const registerThinking = (key: string, el: Element | null) => { const registerThinking = (key: string, el: Element | null) => {
if (typeof props.registerThinkingRef === 'function') { // /
props.registerThinkingRef(key, el); if (el instanceof HTMLElement) {
thinkingRefs.set(key, el);
} else {
thinkingRefs.delete(key);
}
//
const group = blockGroups.value.find(g =>
g.type === 'summary' && g.actions && g.actions.some(a => (a.id === key || a.blockId === key))
);
//
if (group && expandedGroups.value.has(group.id)) {
if (typeof props.registerThinkingRef === 'function') {
props.registerThinkingRef(key, el);
}
} }
}; };
@ -359,33 +458,6 @@ watch(() => props.actions, () => {
margin: 16px 0; margin: 16px 0;
} }
/* 等待动画 */
.generating-placeholder {
display: flex;
gap: 4px;
padding: 12px 0;
font-size: 15px;
font-weight: 500;
color: var(--claude-text-tertiary);
}
.generating-placeholder .letter {
display: inline-block;
animation: wave 1.6s ease-in-out infinite;
opacity: 0.4;
}
@keyframes wave {
0%, 100% {
opacity: 0.4;
transform: translateY(0);
}
50% {
opacity: 1;
transform: translateY(-3px);
}
}
/* 摘要组 */ /* 摘要组 */
.summary-group { .summary-group {
margin: 16px 0; margin: 16px 0;
@ -421,15 +493,20 @@ watch(() => props.actions, () => {
color: inherit; color: inherit;
} }
.expand-icon { .summary-status-icon {
flex-shrink: 0; flex-shrink: 0;
margin-left: 8px; margin-left: 8px;
color: var(--claude-text-tertiary); display: flex;
transition: transform 0.2s; align-items: center;
justify-content: center;
width: 18px;
height: 18px;
} }
.expand-icon.expanded { .check-icon {
transform: rotate(180deg); color: var(--claude-text-tertiary);
width: 18px;
height: 18px;
} }
/* 运行中的发光效果 */ /* 运行中的发光效果 */

View File

@ -0,0 +1,57 @@
<template>
<div class="bouncing-squares-loader">
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
</div>
</template>
<script setup lang="ts">
// From Uiverse.io by mobinkakei (modified)
</script>
<style scoped>
.bouncing-squares-loader {
width: 40px;
height: 20px;
position: relative;
}
.square {
width: 3px;
height: 3px;
position: absolute;
background-color: var(--claude-text-tertiary);
left: 15%;
transform-origin: 50%;
animation: bounce-square .5s alternate infinite ease;
}
@keyframes bounce-square {
0% {
top: 20px;
height: 2px;
transform: scaleX(1.7);
}
40% {
height: 3px;
transform: scaleX(1);
}
100% {
top: 0%;
}
}
.square:nth-child(2) {
left: 45%;
animation-delay: .2s;
}
.square:nth-child(3) {
left: auto;
right: 15%;
animation-delay: .3s;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="dual-chase-loader"></div>
</template>
<script setup lang="ts">
// From Uiverse.io by satyamchaudharydev (modified)
</script>
<style scoped>
.dual-chase-loader {
--size: 6px;
width: 16px;
height: 16px;
position: relative;
}
.dual-chase-loader::after,
.dual-chase-loader::before {
box-sizing: border-box;
position: absolute;
content: "";
width: var(--size);
height: var(--size);
top: 50%;
animation: chase-up 2.4s cubic-bezier(0, 0, 0.24, 1.21) infinite;
left: 50%;
background: var(--claude-text-tertiary);
}
.dual-chase-loader::after {
top: calc(50% - var(--size));
left: calc(50% - var(--size));
animation: chase-down 2.4s cubic-bezier(0, 0, 0.24, 1.21) infinite;
}
@keyframes chase-down {
0%, 100% {
transform: none;
}
25% {
transform: translateX(100%);
}
50% {
transform: translateX(100%) translateY(100%);
}
75% {
transform: translateY(100%);
}
}
@keyframes chase-up {
0%, 100% {
transform: none;
}
25% {
transform: translateX(-100%);
}
50% {
transform: translateX(-100%) translateY(-100%);
}
75% {
transform: translateY(-100%);
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<div class="five-squares-loader">
<div id="sq1"></div>
<div id="sq2"></div>
<div id="sq3"></div>
<div id="sq4"></div>
<div id="sq5"></div>
</div>
</template>
<script setup lang="ts">
// From Uiverse.io by Nawsome (modified)
</script>
<style scoped>
.five-squares-loader {
--square: 3px;
--offset: 7.5px;
width: calc(3 * var(--offset) + var(--square));
height: calc(2 * var(--offset) + var(--square));
position: relative;
}
.five-squares-loader div {
display: inline-block;
background: var(--claude-text-tertiary);
width: var(--square);
height: var(--square);
position: absolute;
}
.five-squares-loader #sq1 {
left: 0;
top: 0;
animation: square1 2.4s 0.2s ease-in-out infinite;
}
.five-squares-loader #sq2 {
left: 0;
top: var(--offset);
animation: square2 2.4s 0.2s ease-in-out infinite;
}
.five-squares-loader #sq3 {
left: var(--offset);
top: var(--offset);
animation: square3 2.4s 0.2s ease-in-out infinite;
}
.five-squares-loader #sq4 {
left: calc(2 * var(--offset));
top: var(--offset);
animation: square4 2.4s 0.2s ease-in-out infinite;
}
.five-squares-loader #sq5 {
left: calc(3 * var(--offset));
top: var(--offset);
animation: square5 2.4s 0.2s ease-in-out infinite;
}
@keyframes square1 {
0% { left: 0; top: 0; }
8.33% { left: 0; top: var(--offset); }
100% { left: 0; top: var(--offset); }
}
@keyframes square2 {
0% { left: 0; top: var(--offset); }
8.33% { left: 0; top: calc(2 * var(--offset)); }
16.67% { left: var(--offset); top: calc(2 * var(--offset)); }
25% { left: var(--offset); top: var(--offset); }
83.33% { left: var(--offset); top: var(--offset); }
91.67% { left: var(--offset); top: 0; }
100% { left: 0; top: 0; }
}
@keyframes square3 {
0%, 100% { left: var(--offset); top: var(--offset); }
16.67% { left: var(--offset); top: var(--offset); }
25% { left: var(--offset); top: 0; }
33.33% { left: calc(2 * var(--offset)); top: 0; }
41.67% { left: calc(2 * var(--offset)); top: var(--offset); }
66.67% { left: calc(2 * var(--offset)); top: var(--offset); }
75% { left: calc(2 * var(--offset)); top: calc(2 * var(--offset)); }
83.33% { left: var(--offset); top: calc(2 * var(--offset)); }
91.67% { left: var(--offset); top: var(--offset); }
}
@keyframes square4 {
0% { left: calc(2 * var(--offset)); top: var(--offset); }
33.33% { left: calc(2 * var(--offset)); top: var(--offset); }
41.67% { left: calc(2 * var(--offset)); top: calc(2 * var(--offset)); }
50% { left: calc(3 * var(--offset)); top: calc(2 * var(--offset)); }
58.33% { left: calc(3 * var(--offset)); top: var(--offset); }
100% { left: calc(3 * var(--offset)); top: var(--offset); }
}
@keyframes square5 {
0% { left: calc(3 * var(--offset)); top: var(--offset); }
50% { left: calc(3 * var(--offset)); top: var(--offset); }
58.33% { left: calc(3 * var(--offset)); top: 0; }
66.67% { left: calc(2 * var(--offset)); top: 0; }
75% { left: calc(2 * var(--offset)); top: var(--offset); }
100% { left: calc(2 * var(--offset)); top: var(--offset); }
}
</style>

View File

@ -0,0 +1,197 @@
<template>
<div class="nine-grid-loader">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
</template>
<script setup lang="ts">
// From Uiverse.io by Nawsome (modified)
</script>
<style scoped>
.nine-grid-loader {
position: relative;
width: 20px;
height: 20px;
}
.box {
float: left;
position: relative;
width: 3px;
height: 3px;
margin-right: 4px;
}
.box:before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: var(--claude-text-tertiary);
}
.box:nth-child(3n) {
margin-right: 0;
margin-bottom: 4px;
}
.box:nth-child(1):before,
.box:nth-child(4):before {
margin-left: 7px;
}
.box:nth-child(3):before {
margin-top: 14px;
}
.box:last-child {
margin-bottom: 0;
}
.box:nth-child(1) { animation: moveBox-1 4s infinite; }
.box:nth-child(2) { animation: moveBox-2 4s infinite; }
.box:nth-child(3) { animation: moveBox-3 4s infinite; }
.box:nth-child(4) { animation: moveBox-4 4s infinite; }
.box:nth-child(5) { animation: moveBox-5 4s infinite; }
.box:nth-child(6) { animation: moveBox-6 4s infinite; }
.box:nth-child(7) { animation: moveBox-7 4s infinite; }
.box:nth-child(8) { animation: moveBox-8 4s infinite; }
.box:nth-child(9) { animation: moveBox-9 4s infinite; }
@keyframes moveBox-1 {
9% { transform: translate(-7px, 0); }
18% { transform: translate(0, 0); }
27% { transform: translate(0, 0); }
36% { transform: translate(7px, 0); }
45% { transform: translate(7px, 7px); }
54% { transform: translate(7px, 7px); }
63% { transform: translate(7px, 7px); }
72% { transform: translate(7px, 0); }
81% { transform: translate(0, 0); }
90% { transform: translate(-7px, 0); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-2 {
9% { transform: translate(0, 0); }
18% { transform: translate(7px, 0); }
27% { transform: translate(0, 0); }
36% { transform: translate(7px, 0); }
45% { transform: translate(7px, 7px); }
54% { transform: translate(7px, 7px); }
63% { transform: translate(7px, 7px); }
72% { transform: translate(7px, 7px); }
81% { transform: translate(0, 7px); }
90% { transform: translate(0, 7px); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-3 {
9% { transform: translate(-7px, 0); }
18% { transform: translate(-7px, 0); }
27% { transform: translate(0, 0); }
36% { transform: translate(-7px, 0); }
45% { transform: translate(-7px, 0); }
54% { transform: translate(-7px, 0); }
63% { transform: translate(-7px, 0); }
72% { transform: translate(-7px, 0); }
81% { transform: translate(-7px, -7px); }
90% { transform: translate(0, -7px); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-4 {
9% { transform: translate(-7px, 0); }
18% { transform: translate(-7px, 0); }
27% { transform: translate(-7px, -7px); }
36% { transform: translate(0, -7px); }
45% { transform: translate(0, 0); }
54% { transform: translate(0, -7px); }
63% { transform: translate(0, -7px); }
72% { transform: translate(0, -7px); }
81% { transform: translate(-7px, -7px); }
90% { transform: translate(-7px, 0); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-5 {
9% { transform: translate(0, 0); }
18% { transform: translate(0, 0); }
27% { transform: translate(0, 0); }
36% { transform: translate(7px, 0); }
45% { transform: translate(7px, 0); }
54% { transform: translate(7px, 0); }
63% { transform: translate(7px, 0); }
72% { transform: translate(7px, 0); }
81% { transform: translate(7px, -7px); }
90% { transform: translate(0, -7px); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-6 {
9% { transform: translate(0, 0); }
18% { transform: translate(-7px, 0); }
27% { transform: translate(-7px, 0); }
36% { transform: translate(0, 0); }
45% { transform: translate(0, 0); }
54% { transform: translate(0, 0); }
63% { transform: translate(0, 0); }
72% { transform: translate(0, 7px); }
81% { transform: translate(-7px, 7px); }
90% { transform: translate(-7px, 0); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-7 {
9% { transform: translate(7px, 0); }
18% { transform: translate(7px, 0); }
27% { transform: translate(7px, 0); }
36% { transform: translate(0, 0); }
45% { transform: translate(0, -7px); }
54% { transform: translate(7px, -7px); }
63% { transform: translate(0, -7px); }
72% { transform: translate(0, -7px); }
81% { transform: translate(0, 0); }
90% { transform: translate(7px, 0); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-8 {
9% { transform: translate(0, 0); }
18% { transform: translate(-7px, 0); }
27% { transform: translate(-7px, -7px); }
36% { transform: translate(0, -7px); }
45% { transform: translate(0, -7px); }
54% { transform: translate(0, -7px); }
63% { transform: translate(0, -7px); }
72% { transform: translate(0, -7px); }
81% { transform: translate(7px, -7px); }
90% { transform: translate(7px, 0); }
100% { transform: translate(0, 0); }
}
@keyframes moveBox-9 {
9% { transform: translate(-7px, 0); }
18% { transform: translate(-7px, 0); }
27% { transform: translate(0, 0); }
36% { transform: translate(-7px, 0); }
45% { transform: translate(0, 0); }
54% { transform: translate(0, 0); }
63% { transform: translate(-7px, 0); }
72% { transform: translate(-7px, 0); }
81% { transform: translate(-14px, 0); }
90% { transform: translate(-7px, 0); }
100% { transform: translate(0, 0); }
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="ripple-loader">
<div class="cell d-0"></div>
<div class="cell d-1"></div>
<div class="cell d-2"></div>
<div class="cell d-1"></div>
<div class="cell d-2"></div>
<div class="cell d-2"></div>
<div class="cell d-3"></div>
<div class="cell d-3"></div>
<div class="cell d-4"></div>
</div>
</template>
<script setup lang="ts">
// From Uiverse.io by alexruix (modified)
</script>
<style scoped>
.ripple-loader {
--cell-size: 3px;
--cell-spacing: 2px;
--cells: 3;
--total-size: calc(var(--cells) * (var(--cell-size) + 2 * var(--cell-spacing)));
display: flex;
flex-wrap: wrap;
width: var(--total-size);
height: var(--total-size);
}
.cell {
flex: 0 0 var(--cell-size);
margin: var(--cell-spacing);
background-color: transparent;
box-sizing: border-box;
animation: 1.5s ripple ease infinite;
}
.cell.d-1 {
animation-delay: 100ms;
}
.cell.d-2 {
animation-delay: 200ms;
}
.cell.d-3 {
animation-delay: 300ms;
}
.cell.d-4 {
animation-delay: 400ms;
}
.cell:nth-child(1),
.cell:nth-child(2),
.cell:nth-child(3),
.cell:nth-child(4),
.cell:nth-child(5),
.cell:nth-child(6),
.cell:nth-child(7),
.cell:nth-child(8),
.cell:nth-child(9) {
--cell-color: var(--claude-text-tertiary);
}
@keyframes ripple {
0% {
background-color: transparent;
}
30% {
background-color: var(--cell-color);
}
60% {
background-color: transparent;
}
100% {
background-color: transparent;
}
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<div class="rotating-squares-loader">
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
</div>
</template>
<script setup lang="ts">
// From Uiverse.io by ZacharyCrespin (modified)
</script>
<style scoped>
@keyframes square-animation {
0% { left: 0; top: 0; }
10.5% { left: 0; top: 0; }
12.5% { left: 8px; top: 0; }
23% { left: 8px; top: 0; }
25% { left: 16px; top: 0; }
35.5% { left: 16px; top: 0; }
37.5% { left: 16px; top: 8px; }
48% { left: 16px; top: 8px; }
50% { left: 8px; top: 8px; }
60.5% { left: 8px; top: 8px; }
62.5% { left: 8px; top: 16px; }
73% { left: 8px; top: 16px; }
75% { left: 0; top: 16px; }
85.5% { left: 0; top: 16px; }
87.5% { left: 0; top: 8px; }
98% { left: 0; top: 8px; }
100% { left: 0; top: 0; }
}
.rotating-squares-loader {
position: relative;
width: 24px;
height: 24px;
transform: rotate(45deg);
}
.square {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 3px;
margin: 0.5px;
background: var(--claude-text-tertiary);
animation: square-animation 10s ease-in-out infinite both;
}
.square:nth-of-type(1) {
animation-delay: -1.43s;
}
.square:nth-of-type(2) {
animation-delay: -2.86s;
}
.square:nth-of-type(3) {
animation-delay: -4.29s;
}
.square:nth-of-type(4) {
animation-delay: -5.71s;
}
.square:nth-of-type(5) {
animation-delay: -7.14s;
}
.square:nth-of-type(6) {
animation-delay: -8.57s;
}
.square:nth-of-type(7) {
animation-delay: -10s;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div class="square-grid-loader">
<div class="square" id="sq1"></div>
<div class="square" id="sq2"></div>
<div class="square" id="sq3"></div>
<div class="square" id="sq4"></div>
<div class="square" id="sq5"></div>
<div class="square" id="sq6"></div>
<div class="square" id="sq7"></div>
<div class="square" id="sq8"></div>
<div class="square" id="sq9"></div>
</div>
</template>
<script setup lang="ts">
// From Uiverse.io by JkHuger
</script>
<style scoped>
@keyframes loader_5191 {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.square-grid-loader {
position: relative;
width: 16px;
height: 16px;
}
.square {
background: var(--claude-text-tertiary);
width: 3px;
height: 3px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -1.5px;
margin-left: -1.5px;
border-radius: 0.5px;
}
#sq1 {
margin-top: -7.5px;
margin-left: -7.5px;
animation: loader_5191 675ms ease-in-out 0s infinite alternate;
}
#sq2 {
margin-top: -7.5px;
animation: loader_5191 675ms ease-in-out 75ms infinite alternate;
}
#sq3 {
margin-top: -7.5px;
margin-left: 4.5px;
animation: loader_5191 675ms ease-in-out 150ms infinite;
}
#sq4 {
margin-left: -7.5px;
animation: loader_5191 675ms ease-in-out 225ms infinite;
}
#sq5 {
animation: loader_5191 675ms ease-in-out 300ms infinite;
}
#sq6 {
margin-left: 4.5px;
animation: loader_5191 675ms ease-in-out 375ms infinite;
}
#sq7 {
margin-top: 4.5px;
margin-left: -7.5px;
animation: loader_5191 675ms ease-in-out 450ms infinite;
}
#sq8 {
margin-top: 4.5px;
animation: loader_5191 675ms ease-in-out 525ms infinite;
}
#sq9 {
margin-top: 4.5px;
margin-left: 4.5px;
animation: loader_5191 675ms ease-in-out 600ms infinite;
}
</style>

View File

@ -0,0 +1,25 @@
import { Component } from 'vue';
import SquareGridLoader from './SquareGridLoader.vue';
import RotatingSquaresLoader from './RotatingSquaresLoader.vue';
import DualChaseLoader from './DualChaseLoader.vue';
import BouncingSquaresLoader from './BouncingSquaresLoader.vue';
import FiveSquaresLoader from './FiveSquaresLoader.vue';
import NineGridLoader from './NineGridLoader.vue';
import RippleLoader from './RippleLoader.vue';
// 加载动画池
export const loaderPool: Component[] = [
SquareGridLoader,
RotatingSquaresLoader,
DualChaseLoader,
BouncingSquaresLoader,
FiveSquaresLoader,
NineGridLoader,
RippleLoader
];
// 随机获取一个加载动画
export function getRandomLoader(): Component {
const randomIndex = Math.floor(Math.random() * loaderPool.length);
return loaderPool[randomIndex];
}