|
|
@@ -0,0 +1,117 @@
|
|
|
+<template>
|
|
|
+ <div class="ai-summary">
|
|
|
+ <div class="ai-header">
|
|
|
+ <h3 class="ai-title">{{ lang.ssSpkAISummary }}</h3>
|
|
|
+ <button class="ai-refresh-btn" :disabled="loading" @click="$emit('refresh')">
|
|
|
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
|
+ :class="{ spinning: loading }">
|
|
|
+ <path d="M1 4v6h6M23 20v-6h-6" />
|
|
|
+ <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
|
|
|
+ </svg>
|
|
|
+ {{ lang.ssSpkRefresh }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <ul class="ai-bullets">
|
|
|
+ <li>• {{ bullets[0] }}</li>
|
|
|
+ <li v-if="loading && !bullets[1]" class="ai-skeleton"><span /></li>
|
|
|
+ <li v-else>• {{ bullets[1] }}</li>
|
|
|
+ <li v-if="loading && !bullets[2]" class="ai-skeleton"><span /></li>
|
|
|
+ <li v-else>• {{ bullets[2] }}</li>
|
|
|
+ </ul>
|
|
|
+ <div v-if="generatedAtLabel" class="ai-meta">{{ generatedAtLabel }}</div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
|
+import { lang } from '@/main'
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ bullets: [string, string, string]
|
|
|
+ loading: boolean
|
|
|
+ generatedAt: string | null
|
|
|
+}>()
|
|
|
+defineEmits<{ refresh: [] }>()
|
|
|
+
|
|
|
+// 「刚刚生成」/「N 秒前生成」/「N 分钟前生成」 — 每秒重算
|
|
|
+const now = ref(Date.now())
|
|
|
+let timer: number | null = null
|
|
|
+onMounted(() => { timer = window.setInterval(() => { now.value = Date.now() }, 1000) })
|
|
|
+onUnmounted(() => { if (timer) clearInterval(timer) })
|
|
|
+
|
|
|
+function formatTpl(tpl: string, vars: Record<string, string | number>): string {
|
|
|
+ return tpl.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ''))
|
|
|
+}
|
|
|
+
|
|
|
+const generatedAtLabel = computed(() => {
|
|
|
+ if (!props.generatedAt) return ''
|
|
|
+ const ts = new Date(props.generatedAt).getTime()
|
|
|
+ if (isNaN(ts)) return ''
|
|
|
+ const elapsed = Math.max(0, Math.floor((now.value - ts) / 1000))
|
|
|
+ if (elapsed < 5) return (lang as any).ssSpkJustNow
|
|
|
+ if (elapsed < 60) return formatTpl((lang as any).ssSpkSecondsAgoTpl, { n: elapsed })
|
|
|
+ return formatTpl((lang as any).ssSpkMinutesAgoTpl, { n: Math.floor(elapsed / 60) })
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.ai-summary {
|
|
|
+ background: #f9fafb;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.ai-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #111827;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+.ai-refresh-btn {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover:not(:disabled) { color: #374151; }
|
|
|
+ &:disabled { opacity: .6; cursor: default; }
|
|
|
+ svg.spinning { animation: spin 1s linear infinite; }
|
|
|
+}
|
|
|
+@keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
|
|
|
+
|
|
|
+.ai-bullets {
|
|
|
+ list-style: none; padding: 0; margin: 0;
|
|
|
+ display: flex; flex-direction: column; gap: 4px;
|
|
|
+ font-size: 12px; color: #374151;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-skeleton span {
|
|
|
+ display: inline-block;
|
|
|
+ width: 60%; height: 12px;
|
|
|
+ background: linear-gradient(90deg, #e5e7eb 0%, #f3f4f6 50%, #e5e7eb 100%);
|
|
|
+ background-size: 200% 100%;
|
|
|
+ border-radius: 4px;
|
|
|
+ animation: shimmer 1.4s ease-in-out infinite;
|
|
|
+}
|
|
|
+@keyframes shimmer {
|
|
|
+ 0% { background-position: 200% 0; }
|
|
|
+ 100% { background-position: -200% 0; }
|
|
|
+}
|
|
|
+
|
|
|
+.ai-meta {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+</style>
|