Files
Starlight_Lancher/apps/app-frontend/src/components/lab/mod-translation/ModTranslationTaskTimeline.vue

197 lines
4.7 KiB
Vue

<script setup lang="ts">
import { ChevronDownIcon, ChevronUpIcon, HistoryIcon } from '@modrinth/assets'
import { computed, nextTick, ref, watch } from 'vue'
import { groupTimelineByRepairPass } from '@/lab/mod-translation/timeline'
import type { ModTranslationTimelineEntry } from '@/lab/mod-translation/types.ts'
const props = defineProps<{ entries: ModTranslationTimelineEntry[] }>()
const open = ref(false)
const scrollEl = ref<HTMLElement | null>(null)
const stayAtBottom = ref(true)
const debugOpen = ref(new Set<string>())
const groups = computed(() => groupTimelineByRepairPass(props.entries))
function onScroll() {
const element = scrollEl.value
if (!element) return
stayAtBottom.value = element.scrollHeight - element.scrollTop - element.clientHeight < 24
}
watch(
() => props.entries.length,
async () => {
if (!open.value || !stayAtBottom.value) return
await nextTick()
if (scrollEl.value) scrollEl.value.scrollTop = scrollEl.value.scrollHeight
},
)
function toggleDebug(id: string) {
const next = new Set(debugOpen.value)
if (next.has(id)) next.delete(id)
else next.add(id)
debugOpen.value = next
}
function copyDebug(value: unknown) {
void navigator.clipboard.writeText(JSON.stringify(value, null, 2))
}
</script>
<template>
<section class="flex flex-col gap-[0.35rem] border-t border-divider pt-[0.55rem]">
<button type="button" class="toggle" :aria-expanded="open" @click="open = !open">
<HistoryIcon />处理时间线 ({{ entries.length }}) <ChevronUpIcon v-if="open" /><ChevronDownIcon
v-else
/>
</button>
<div v-show="open" ref="scrollEl" class="timeline" @scroll="onScroll">
<div v-if="!groups.length" class="empty">等待处理事件</div>
<section v-for="group in groups" :key="group.id" class="group">
<h4 v-if="group.pass">Repair Pass {{ group.pass }}</h4>
<div
v-for="entry in group.entries"
:key="entry.id"
class="entry grid grid-cols-[auto_minmax(0,1fr)] gap-2"
:class="`entry--${entry.status}`"
>
<span class="dot" />
<div class="entry-body">
<strong>{{ entry.title }}</strong>
<span v-if="entry.summary">{{ entry.summary }}</span>
<span v-if="entry.issueIds.length" class="issues"
>{{ entry.issueIds.length }} issue IDs</span
>
<div v-if="entry.debug" class="debug">
<button
type="button"
:aria-expanded="debugOpen.has(entry.id)"
@click="toggleDebug(entry.id)"
>
技术详情
</button>
<button type="button" @click="copyDebug(entry.debug)">复制诊断</button>
<pre v-if="debugOpen.has(entry.id)">{{ JSON.stringify(entry.debug, null, 2) }}</pre>
</div>
</div>
</div>
</section>
</div>
</section>
</template>
<style scoped>
.toggle {
display: flex;
width: 100%;
align-items: center;
gap: 0.4rem;
border: 0;
background: transparent;
padding: 0;
color: var(--color-text-secondary);
font-size: 0.68rem;
font-weight: 700;
text-align: left;
cursor: pointer;
}
.toggle:hover {
color: var(--color-contrast);
}
.toggle:focus-visible,
.debug button:focus-visible {
outline: 2px solid var(--color-brand);
outline-offset: -2px;
}
.toggle :deep(svg) {
width: 0.9rem;
height: 0.9rem;
}
.toggle :deep(svg:last-child) {
margin-left: auto;
}
.timeline {
display: flex;
max-height: 18rem;
flex-direction: column;
gap: 0.7rem;
overflow-y: auto;
border: 1px solid var(--color-divider);
border-radius: var(--radius-md);
background: var(--color-button-bg);
padding: 0.65rem 0.7rem;
}
.empty {
color: var(--color-text-secondary);
font-size: 0.72rem;
}
.group {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.group h4 {
margin: 0;
color: var(--color-brand);
font-size: 0.68rem;
text-transform: uppercase;
}
.dot {
width: 0.5rem;
height: 0.5rem;
margin-top: 0.3rem;
border-radius: 999px;
background: var(--color-brand);
}
.entry--success .dot {
background: var(--color-green);
}
.entry--warning .dot,
.entry--warn .dot {
background: var(--color-orange);
}
.entry--error .dot {
background: var(--color-red);
}
.entry-body {
display: flex;
min-width: 0;
flex-direction: column;
gap: 0.12rem;
font-size: 0.72rem;
}
.entry-body span {
color: var(--color-text-secondary);
}
.issues {
font-family: monospace;
font-size: 0.64rem;
}
.debug {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-top: 0.25rem;
}
.debug button {
border: 1px solid var(--surface-5);
border-radius: var(--radius-sm);
background: var(--surface-2);
padding: 0.2rem 0.4rem;
color: var(--color-text-secondary);
font-size: 0.65rem;
}
.debug pre {
width: 100%;
max-height: 14rem;
overflow: auto;
margin: 0;
border-radius: var(--radius-sm);
background: var(--surface-1);
padding: 0.55rem;
font-size: 0.62rem;
white-space: pre-wrap;
}
</style>