feat(wrapped-ui): 年度总结页支持懒加载与复古模式,新增概览/字数卡片

- wrapped 页面改为:先拉 meta/年份列表,再按页请求单张卡片,首屏更快
- 新增 Card#0 全局概览页(含图表)
- 新增 Card#2 消息字数页(含键盘敲击统计与图表)
- 新增复古模式:像素字体资源 + CRT Overlay,支持一键开关
- 调整 shared 组件、types/useApi,更新前端依赖与 lock
This commit is contained in:
2977094657
2026-01-31 14:54:43 +08:00
parent 77a60bde70
commit 645dc1cff1
42 changed files with 2901 additions and 172 deletions

View File

@@ -2,6 +2,33 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Fusion Pixel Font - 像素字体 @font-face 声明 */
/* 下载地址: https://github.com/TakWolf/fusion-pixel-font/releases */
@font-face {
font-family: 'Fusion Pixel 12';
src: url('/fonts/fusion-pixel-12px-monospaced-zh_hans.otf.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Fusion Pixel 10';
src: url('/fonts/fusion-pixel-10px-monospaced-zh_hans.otf.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Fusion Pixel 8';
src: url('/fonts/fusion-pixel-8px-monospaced-zh_hans.otf.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* 自定义全局样式 - 微信配色主题 */ /* 自定义全局样式 - 微信配色主题 */
@layer base { @layer base {
:root { :root {
@@ -42,6 +69,11 @@
/* 统一消息圆角(聊天所有消息共用) */ /* 统一消息圆角(聊天所有消息共用) */
--message-radius: 4px; --message-radius: 4px;
/* Wrapped 年度总结 - 像素字体 */
--font-pixel-12: 'Fusion Pixel 12', 'Microsoft YaHei', sans-serif;
--font-pixel-10: 'Fusion Pixel 10', 'Microsoft YaHei', sans-serif;
--font-pixel-8: 'Fusion Pixel 8', 'Microsoft YaHei', sans-serif;
} }
body { body {
@@ -80,6 +112,16 @@
/* 统一的消息圆角工具类 */ /* 统一的消息圆角工具类 */
.msg-radius { border-radius: var(--message-radius); } .msg-radius { border-radius: var(--message-radius); }
.msg-bubble { @apply leading-normal break-words text-pretty; border-radius: var(--message-radius); } .msg-bubble { @apply leading-normal break-words text-pretty; border-radius: var(--message-radius); }
/* 隐私模式(通用):默认模糊,悬停显示 */
.privacy-blur {
filter: blur(9px);
transition: filter 0.2s ease;
}
.privacy-blur:hover {
filter: none;
}
/* 按钮样式 */ /* 按钮样式 */
.btn { .btn {
@apply px-6 py-3 rounded-full font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 transform active:scale-95; @apply px-6 py-3 rounded-full font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 transform active:scale-95;
@@ -118,6 +160,129 @@
mix-blend-mode: multiply; mix-blend-mode: multiply;
} }
/* Wrapped 增强噪点纹理(动态抖动) */
.wrapped-noise-enhanced {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj4KICA8ZmlsdGVyIGlkPSJuIj4KICAgIDxmZVR1cmJ1bGVuY2UgdHlwZT0iZnJhY3RhbE5vaXNlIiBiYXNlRnJlcXVlbmN5PSIwLjkiIG51bU9jdGF2ZXM9IjUiIHN0aXRjaFRpbGVzPSJzdGl0Y2giLz4KICAgIDxmZUNvbG9yTWF0cml4IHR5cGU9InNhdHVyYXRlIiB2YWx1ZXM9IjAiLz4KICA8L2ZpbHRlcj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsdGVyPSJ1cmwoI24pIiBvcGFjaXR5PSIwLjUiLz4KPC9zdmc+");
background-repeat: repeat;
background-size: 200px 200px;
mix-blend-mode: multiply;
animation: noise-jitter 0.5s steps(3) infinite;
}
/* Wrapped 像素字体类 */
/* Wrapped typography: default is modern; `.wrapped-retro` enables pixel font + CRT vibe. */
.wrapped-title {
font-weight: 700;
letter-spacing: 0.02em;
}
.wrapped-title-en {
font-weight: 700;
letter-spacing: 0.04em;
}
.wrapped-body {
line-height: 1.8;
}
.wrapped-label {
font-weight: 600;
letter-spacing: 0.15em;
text-transform: uppercase;
}
.wrapped-number {
font-variant-numeric: tabular-nums;
}
.wrapped-retro .wrapped-title {
font-family: var(--font-pixel-12);
font-weight: normal;
letter-spacing: 0.05em;
image-rendering: pixelated;
-webkit-font-smoothing: none;
}
.wrapped-retro .wrapped-title-en {
font-family: var(--font-pixel-12);
font-weight: normal;
letter-spacing: 0.08em;
image-rendering: pixelated;
-webkit-font-smoothing: none;
}
.wrapped-retro .wrapped-body {
font-family: var(--font-pixel-10);
font-weight: normal;
line-height: 1.8;
image-rendering: pixelated;
-webkit-font-smoothing: none;
}
.wrapped-retro .wrapped-label {
font-family: var(--font-pixel-8);
font-weight: normal;
letter-spacing: 0.15em;
text-transform: uppercase;
image-rendering: pixelated;
-webkit-font-smoothing: none;
}
.wrapped-retro .wrapped-number {
font-family: var(--font-pixel-12);
font-weight: normal;
font-variant-numeric: tabular-nums;
image-rendering: pixelated;
-webkit-font-smoothing: none;
}
/* CRT 扫描线 - 水平线条(明显可见) */
.crt-scanlines {
background: repeating-linear-gradient(
to bottom,
transparent 0px,
transparent 3px,
rgba(0, 0, 0, 0.15) 3px,
rgba(0, 0, 0, 0.15) 4px
);
background-size: 100% 4px;
animation: scanline-scroll 12s linear infinite;
}
/* CRT RGB 子像素 - 垂直彩色条纹 */
.crt-rgb-pixels {
background-image: repeating-linear-gradient(
to right,
rgba(255, 0, 0, 0.06) 0px,
rgba(255, 0, 0, 0.06) 1px,
rgba(0, 255, 0, 0.06) 1px,
rgba(0, 255, 0, 0.06) 2px,
rgba(0, 0, 255, 0.06) 2px,
rgba(0, 0, 255, 0.06) 3px
);
}
/* CRT 闪烁 - 亮度波动 */
.crt-flicker {
background-color: rgba(255, 255, 255, 0.015);
animation: crt-flicker 0.08s infinite alternate;
}
/* CRT 暗角 - 边缘渐暗(更强) */
.crt-vignette {
box-shadow: inset 0 0 250px 80px rgba(0, 0, 0, 0.15);
}
/* CRT 屏幕曲率效果 */
.crt-curvature {
background: radial-gradient(
ellipse at center,
transparent 0%,
transparent 40%,
rgba(0, 0, 0, 0.1) 100%
);
}
/* 输入框样式 */ /* 输入框样式 */
.input { .input {
@apply w-full px-4 py-3 bg-[#f7f8fa] border border-transparent rounded-xl focus:outline-none focus:ring-2 focus:ring-[#07c160] focus:bg-white focus:border-[#07c160] transition-all duration-200; @apply w-full px-4 py-3 bg-[#f7f8fa] border border-transparent rounded-xl focus:outline-none focus:ring-2 focus:ring-[#07c160] focus:bg-white focus:border-[#07c160] transition-all duration-200;
@@ -932,3 +1097,53 @@
opacity: 1; opacity: 1;
} }
} }
/* CRT 和 Wrapped 动画关键帧 */
@keyframes scanline-scroll {
0% {
background-position: 0 0;
}
100% {
background-position: 0 100vh;
}
}
@keyframes crt-flicker {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.98;
}
}
@keyframes noise-jitter {
0% {
transform: translate(0, 0);
}
33% {
transform: translate(-1px, 1px);
}
66% {
transform: translate(1px, -1px);
}
100% {
transform: translate(0, 0);
}
}
/* Wrapped 入场动画 */
@keyframes wrapped-fade-in {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.wrapped-animate-in {
animation: wrapped-fade-in 0.6s ease-out forwards;
}

View File

@@ -0,0 +1,194 @@
<template>
<WrappedCardShell :card-id="card.id" :title="card.title" :narrative="''" :variant="variant">
<template #narrative>
<div class="mt-2 wrapped-body text-sm text-[#7F7F7F] leading-relaxed">
<p>
<template v-if="totalMessages > 0">
这一年你在微信里发送了
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(totalMessages) }}</span>
条消息平均每天
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatFloat(messagesPerDay, 1) }}</span>
</template>
<template v-else>
这一年你在微信里还没有发出聊天消息也许你把时间留给了更重要的人和事
</template>
<template v-if="activeDays > 0">
在与你相伴的
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(activeDays) }}</span>
天里
<template v-if="mostActiveHour !== null && mostActiveWeekdayName">
你最常在 {{ mostActiveWeekdayName }}
<span class="wrapped-number text-[#07C160] font-semibold">{{ mostActiveHour }}</span>
点出现
</template>
<template v-else>
你留下了不少对话的痕迹
</template>
</template>
<template v-if="topContact || topGroup">
<template v-if="topContact">
你发消息最多的人是
<span
class="privacy-blur inline-flex items-center gap-2 align-bottom max-w-[12rem]"
:title="topContact.displayName"
>
<span class="w-6 h-6 rounded-md overflow-hidden bg-[#0000000d] flex items-center justify-center flex-shrink-0">
<img
v-if="topContactAvatarUrl && avatarOk.topContact"
:src="topContactAvatarUrl"
class="w-full h-full object-cover"
alt="avatar"
@error="avatarOk.topContact = false"
/>
<span v-else class="wrapped-number text-[11px] text-[#00000066]">
{{ avatarFallback(topContact.displayName) }}
</span>
</span>
<span class="inline-block max-w-[10rem] truncate align-bottom">{{ topContact.displayName }}</span>
</span>
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(topContact.messages) }}</span>
</template>
<template v-if="topContact && topGroup"></template>
<template v-if="topGroup">
你最常发言的群是
<span
class="privacy-blur inline-flex items-center gap-2 align-bottom max-w-[12rem]"
:title="topGroup.displayName"
>
<span class="w-6 h-6 rounded-md overflow-hidden bg-[#0000000d] flex items-center justify-center flex-shrink-0">
<img
v-if="topGroupAvatarUrl && avatarOk.topGroup"
:src="topGroupAvatarUrl"
class="w-full h-full object-cover"
alt="avatar"
@error="avatarOk.topGroup = false"
/>
<span v-else class="wrapped-number text-[11px] text-[#00000066]">
{{ avatarFallback(topGroup.displayName) }}
</span>
</span>
<span class="inline-block max-w-[10rem] truncate align-bottom">{{ topGroup.displayName }}</span>
</span>
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(topGroup.messages) }}</span>
</template>
</template>
<template v-if="topKind && topKindPct > 0">
你更常用 {{ topKind.label }} 来表达<span class="wrapped-number text-[#07C160] font-semibold">{{ topKindPct }}</span>%
</template>
<template v-if="topPhrase && topPhrase.phrase && topPhrase.count > 0">
你说得最多的一句话是<span
class="privacy-blur inline-block max-w-[12rem] truncate align-bottom"
:title="topPhrase.phrase"
>{{ topPhrase.phrase }}</span><span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(topPhrase.count) }}</span>
</template>
<span class="hidden sm:inline text-[#00000055]">愿你的每一句分享都有人回应</span>
</p>
</div>
</template>
<GlobalOverviewChart :data="card.data || {}" />
</WrappedCardShell>
</template>
<script setup>
import GlobalOverviewChart from '~/components/wrapped/visualizations/GlobalOverviewChart.vue'
const props = defineProps({
card: { type: Object, required: true },
variant: { type: String, default: 'panel' } // 'panel' | 'slide'
})
const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 })
const formatInt = (n) => nfInt.format(Math.round(Number(n) || 0))
const formatFloat = (n, digits = 1) => {
const v = Number(n)
if (!Number.isFinite(v)) return '0'
return v.toFixed(digits)
}
const totalMessages = computed(() => Number(props.card?.data?.totalMessages || 0))
const activeDays = computed(() => Number(props.card?.data?.activeDays || 0))
const messagesPerDay = computed(() => Number(props.card?.data?.messagesPerDay || 0))
const mostActiveHour = computed(() => {
const h = props.card?.data?.mostActiveHour
return Number.isFinite(Number(h)) ? Number(h) : null
})
const mostActiveWeekdayName = computed(() => {
const s = props.card?.data?.mostActiveWeekdayName
return typeof s === 'string' && s.trim() ? s.trim() : ''
})
const topContact = computed(() => {
const o = props.card?.data?.topContact
return o && typeof o === 'object' && typeof o.displayName === 'string' ? o : null
})
const topGroup = computed(() => {
const o = props.card?.data?.topGroup
return o && typeof o === 'object' && typeof o.displayName === 'string' ? o : null
})
// Keep the same behavior as the chat page: media (including avatars) comes from backend :8000 in dev.
const mediaBase = process.client ? 'http://localhost:8000' : ''
const resolveMediaUrl = (value) => {
const raw = String(value || '').trim()
if (!raw) return ''
if (/^https?:\/\//i.test(raw)) {
// qpic/qlogo are often hotlink-protected; proxy via backend (same as chat page).
try {
const host = new URL(raw).hostname.toLowerCase()
if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) {
return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}`
}
} catch {}
return raw
}
// Most backend fields are like "/api/...", so just prefix.
return `${mediaBase}${raw.startsWith('/') ? '' : '/'}${raw}`
}
const topContactAvatarUrl = computed(() => {
return resolveMediaUrl(topContact.value?.avatarUrl)
})
const topGroupAvatarUrl = computed(() => {
return resolveMediaUrl(topGroup.value?.avatarUrl)
})
const avatarOk = reactive({ topContact: true, topGroup: true })
const avatarFallback = (name) => {
const s = String(name || '').trim()
if (!s) return '?'
return s[0]
}
watch(topContactAvatarUrl, () => { avatarOk.topContact = true })
watch(topGroupAvatarUrl, () => { avatarOk.topGroup = true })
const topKind = computed(() => {
const o = props.card?.data?.topKind
return o && typeof o === 'object' && typeof o.label === 'string' ? o : null
})
const topKindPct = computed(() => {
const r = Number(topKind.value?.ratio || 0)
if (!Number.isFinite(r) || r <= 0) return 0
return Math.max(0, Math.min(100, Math.round(r * 100)))
})
const topPhrase = computed(() => {
const o = props.card?.data?.topPhrase
return o && typeof o === 'object' ? o : null
})
</script>

View File

@@ -1,11 +1,5 @@
<template> <template>
<WrappedCardShell :card-id="card.id" :title="card.title" :narrative="card.narrative" :variant="variant"> <WrappedCardShell :card-id="card.id" :title="card.title" :narrative="card.narrative" :variant="variant">
<template #badge>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs bg-[#07C160]/10 text-[#07C160] border border-[#07C160]/20">
作息规律
</span>
</template>
<WeekdayHourHeatmap <WeekdayHourHeatmap
:weekday-labels="card.data?.weekdayLabels" :weekday-labels="card.data?.weekdayLabels"
:hour-labels="card.data?.hourLabels" :hour-labels="card.data?.hourLabels"

View File

@@ -0,0 +1,42 @@
<template>
<WrappedCardShell :card-id="card.id" :title="card.title" :narrative="''" :variant="variant">
<template #narrative>
<div class="mt-2 wrapped-body text-sm text-[#7F7F7F] leading-relaxed">
<p>
<template v-if="sentChars > 0">
这一年你在微信里敲下了
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(sentChars) }}</span>
个字
</template>
<template v-else>
这一年你还没有发出文字消息
</template>
<template v-if="receivedChars > 0">
你也收到了
<span class="wrapped-number text-[#07C160] font-semibold">{{ formatInt(receivedChars) }}</span>
个字
</template>
</p>
</div>
</template>
<MessageCharsChart :data="card.data || {}" />
</WrappedCardShell>
</template>
<script setup>
import MessageCharsChart from '~/components/wrapped/visualizations/MessageCharsChart.vue'
const props = defineProps({
card: { type: Object, required: true },
variant: { type: String, default: 'panel' } // 'panel' | 'slide'
})
const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 })
const formatInt = (n) => nfInt.format(Math.round(Number(n) || 0))
const sentChars = computed(() => Number(props.card?.data?.sentChars || 0))
const receivedChars = computed(() => Number(props.card?.data?.receivedChars || 0))
</script>

View File

@@ -0,0 +1,24 @@
<template>
<!-- CRT 滤镜叠加层 - 模拟老电视机效果 -->
<div class="absolute inset-0 pointer-events-none select-none z-30" aria-hidden="true">
<!-- 扫描线层 - 水平条纹带滚动动画 -->
<div class="absolute inset-0 crt-scanlines"></div>
<!-- RGB 子像素层 - 模拟 CRT 像素结构 -->
<div class="absolute inset-0 crt-rgb-pixels"></div>
<!-- 闪烁层 - 轻微亮度波动 -->
<div class="absolute inset-0 crt-flicker"></div>
<!-- 暗角层 - 边缘渐暗效果 -->
<div class="absolute inset-0 crt-vignette"></div>
<!-- 屏幕曲率层 - 边缘微暗模拟曲面 -->
<div class="absolute inset-0 crt-curvature"></div>
</div>
</template>
<script setup>
// CRT 滤镜叠加层组件
// 通过多层叠加实现复古显像管效果,不修改原始背景
</script>

View File

@@ -3,13 +3,12 @@
<div class="px-6 py-5 border-b border-[#F3F3F3]"> <div class="px-6 py-5 border-b border-[#F3F3F3]">
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div> <div>
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000066]"> <h2 class="wrapped-title text-xl text-[#000000e6]">{{ title }}</h2>
CARD {{ String(cardId).padStart(2, '0') }} <slot name="narrative">
</div> <p v-if="narrative" class="mt-2 wrapped-body text-sm text-[#7F7F7F] whitespace-pre-wrap">
<h2 class="mt-2 text-xl font-bold text-[#000000e6]">{{ title }}</h2>
<p v-if="narrative" class="mt-2 text-sm text-[#7F7F7F]">
{{ narrative }} {{ narrative }}
</p> </p>
</slot>
</div> </div>
<slot name="badge" /> <slot name="badge" />
</div> </div>
@@ -24,13 +23,12 @@
<div class="relative h-full max-w-5xl mx-auto px-6 py-10 sm:px-8 sm:py-12 flex flex-col"> <div class="relative h-full max-w-5xl mx-auto px-6 py-10 sm:px-8 sm:py-12 flex flex-col">
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div> <div>
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000066]"> <h2 class="wrapped-title text-2xl sm:text-3xl text-[#000000e6]">{{ title }}</h2>
CARD {{ String(cardId).padStart(2, '0') }} <slot name="narrative">
</div> <p v-if="narrative" class="mt-3 wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl whitespace-pre-wrap">
<h2 class="mt-2 text-2xl sm:text-3xl font-bold text-[#000000e6]">{{ title }}</h2>
<p v-if="narrative" class="mt-3 text-sm sm:text-base text-[#7F7F7F] max-w-2xl">
{{ narrative }} {{ narrative }}
</p> </p>
</slot>
</div> </div>
<slot name="badge" /> <slot name="badge" />
</div> </div>

View File

@@ -4,9 +4,9 @@
<div class="flex flex-col sm:flex-row gap-3 sm:items-end sm:justify-between"> <div class="flex flex-col sm:flex-row gap-3 sm:items-end sm:justify-between">
<div class="flex flex-col sm:flex-row gap-3 sm:items-end"> <div class="flex flex-col sm:flex-row gap-3 sm:items-end">
<div v-if="showAccount"> <div v-if="showAccount">
<div class="text-xs font-medium text-[#00000099] mb-1">账号</div> <div class="wrapped-label text-xs text-[#00000099] mb-1">Account</div>
<select <select
class="w-full sm:w-56 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160]" class="w-full sm:w-56 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm wrapped-body focus:outline-none focus:ring-2 focus:ring-[#07C160]"
:disabled="accountsLoading || accounts.length === 0" :disabled="accountsLoading || accounts.length === 0"
:value="modelAccount" :value="modelAccount"
@change="$emit('update:account', $event.target.value || '')" @change="$emit('update:account', $event.target.value || '')"
@@ -17,9 +17,9 @@
</div> </div>
<div> <div>
<div class="text-xs font-medium text-[#00000099] mb-1">年份</div> <div class="wrapped-label text-xs text-[#00000099] mb-1">Year</div>
<select <select
class="w-full sm:w-40 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160]" class="w-full sm:w-40 px-3 py-2 rounded-lg border border-[#EDEDED] bg-white text-sm wrapped-body focus:outline-none focus:ring-2 focus:ring-[#07C160]"
:value="String(modelYear)" :value="String(modelYear)"
@change="$emit('update:year', Number($event.target.value))" @change="$emit('update:year', Number($event.target.value))"
> >
@@ -34,26 +34,26 @@
:checked="modelRefresh" :checked="modelRefresh"
@change="$emit('update:refresh', !!$event.target.checked)" @change="$emit('update:refresh', !!$event.target.checked)"
/> />
<span class="text-sm text-[#7F7F7F]">强制刷新忽略缓存</span> <span class="wrapped-body text-sm text-[#7F7F7F]">强制刷新忽略缓存</span>
</label> </label>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
class="inline-flex items-center justify-center px-4 py-2 rounded-lg bg-[#07C160] text-white text-sm font-medium hover:bg-[#06AD56] disabled:opacity-60 disabled:cursor-not-allowed transition" class="inline-flex items-center justify-center px-4 py-2 rounded-lg bg-[#07C160] text-white text-sm wrapped-label hover:bg-[#06AD56] disabled:opacity-60 disabled:cursor-not-allowed transition"
:disabled="loading" :disabled="loading"
@click="$emit('reload')" @click="$emit('reload')"
> >
<span v-if="!loading">生成报告</span> <span v-if="!loading">Generate</span>
<span v-else>生成中...</span> <span v-else>Loading...</span>
</button> </button>
</div> </div>
</div> </div>
<div v-if="accountsLoading" class="text-xs text-[#7F7F7F]"> <div v-if="accountsLoading" class="wrapped-body text-xs text-[#7F7F7F]">
{{ showAccount ? '正在加载账号列表...' : '正在检查数据...' }} {{ showAccount ? '正在加载账号列表...' : '正在检查数据...' }}
</div> </div>
<div v-else-if="accounts.length === 0" class="text-xs text-[#B37800]"> <div v-else-if="accounts.length === 0" class="wrapped-body text-xs text-[#B37800]">
{{ showAccount ? '未发现已解密账号请先解密数据库' : '未发现可用数据请先解密数据库' }} {{ showAccount ? '未发现已解密账号请先解密数据库' : '未发现可用数据请先解密数据库' }}
</div> </div>
</div> </div>

View File

@@ -11,8 +11,8 @@
class="absolute inset-0 bg-[linear-gradient(rgba(7,193,96,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(7,193,96,0.05)_1px,transparent_1px)] bg-[size:52px_52px] opacity-[0.28]" class="absolute inset-0 bg-[linear-gradient(rgba(7,193,96,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(7,193,96,0.05)_1px,transparent_1px)] bg-[size:52px_52px] opacity-[0.28]"
></div> ></div>
<!-- Grain/noise: adds texture without changing the base color --> <!-- Grain/noise: enhanced with dynamic jitter for CRT feel -->
<div class="absolute inset-0 wrapped-noise opacity-[0.06]"></div> <div class="absolute inset-0 wrapped-noise-enhanced opacity-[0.08]"></div>
<!-- Gentle vignette so typography stays readable on textured bg --> <!-- Gentle vignette so typography stays readable on textured bg -->
<div class="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-white/50 to-transparent"></div> <div class="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-white/50 to-transparent"></div>

View File

@@ -11,26 +11,25 @@
<template v-if="variant === 'slide'"> <template v-if="variant === 'slide'">
<div class="h-full flex flex-col justify-between"> <div class="h-full flex flex-col justify-between">
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div class="text-xs font-semibold tracking-[0.28em] text-[#00000080]"> <div class="wrapped-label text-xs text-[#00000080]">
WECHAT · WRAPPED WECHAT WRAPPED
</div> </div>
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000055]"> <div class="wrapped-body text-xs text-[#00000055]">
年度回望 年度回望
</div> </div>
</div> </div>
<div class="mt-10 sm:mt-14"> <div class="mt-10 sm:mt-14">
<h1 class="text-4xl sm:text-6xl font-black tracking-tight text-[#000000e6] leading-[1.05]"> <h1 class="wrapped-title text-4xl sm:text-6xl text-[#000000e6] leading-[1.05]">
把这一年的聊天 {{ randomTitle.main }}
<span class="block mt-3 text-[#07C160]"> <span class="block mt-3 text-[#07C160]">
轻轻翻一翻 {{ randomTitle.highlight }}
</span> </span>
</h1> </h1>
<div class="mt-7 sm:mt-9 max-w-2xl"> <div class="mt-7 sm:mt-9 max-w-2xl">
<p class="text-base sm:text-lg text-[#00000080] leading-relaxed"> <p class="wrapped-body text-base sm:text-lg text-[#00000080]">
有些问候写在对话框里有些陪伴藏在深夜里 {{ randomSubtitle }}
我们不读取内容只把时间整理成几张卡片让你温柔地回望这一年
</p> </p>
</div> </div>
</div> </div>
@@ -45,23 +44,23 @@
<template v-else> <template v-else>
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div class="text-xs font-semibold tracking-[0.28em] text-[#00000080]"> <div class="wrapped-label text-xs text-[#00000080]">
WECHAT · WRAPPED WECHAT WRAPPED
</div> </div>
<!-- 年份放到右上角分享视图不包含账号信息 --> <!-- 年份放到右上角分享视图不包含账号信息 -->
<span <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs bg-[#00000008] text-[#00000099] border border-[#00000010]" class="wrapped-label inline-flex items-center px-3 py-1 rounded-full text-xs bg-[#00000008] text-[#00000099] border border-[#00000010]"
> >
{{ yearText }} {{ yearText }}
</span> </span>
</div> </div>
<div class="mt-5 sm:mt-7 flex flex-col gap-2"> <div class="mt-5 sm:mt-7 flex flex-col gap-2">
<h1 class="text-3xl sm:text-4xl font-bold text-[#000000e6] leading-tight"> <h1 class="wrapped-title text-3xl sm:text-4xl text-[#000000e6] leading-tight">
聊天年度总结 聊天年度总结
</h1> </h1>
<p class="text-sm sm:text-base text-[#7F7F7F] max-w-2xl"> <p class="wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl">
从时间里回看你的聊天节奏第一张卡年度赛博作息表24H × 7Days 从时间里回看你的聊天节奏第一张卡年度赛博作息表24H x 7Days
</p> </p>
</div> </div>
@@ -72,6 +71,120 @@
</template> </template>
<script setup> <script setup>
// 50 个主标题(主句 + 高亮句)
const TITLES = [
{ main: '把这一年的聊天', highlight: '轻轻翻一翻' },
{ main: '这一年', highlight: '谁陪你说了最多的话' },
{ main: '那些深夜的消息', highlight: '都去哪儿了' },
{ main: '一年的对话', highlight: '值得被温柔记住' },
{ main: '你的聊天记录里', highlight: '藏着这一年' },
{ main: '有些人', highlight: '一直在消息列表里陪着你' },
{ main: '翻开这一年的', highlight: '对话框' },
{ main: '这一年的问候', highlight: '都在这里了' },
{ main: '一年又一年', highlight: '聊天框里的人还在吗' },
{ main: '回头看看', highlight: '这一年你和谁聊得最多' },
{ main: '你今年说得最多的', highlight: '那个人是谁' },
{ main: '这一年', highlight: '你在深夜回复过谁' },
{ main: '你的消息', highlight: '都发给了谁' },
{ main: '谁在等你的消息', highlight: '你又在等谁的' },
{ main: '你有多久', highlight: '没和 TA 聊天了' },
{ main: '那个秒回你的人', highlight: '还在吗' },
{ main: '你置顶的人', highlight: '这一年变过吗' },
{ main: '最后一条消息', highlight: '是你发的还是 TA 发的' },
{ main: '你的「在吗」', highlight: '都发给了谁' },
{ main: '有没有一个人', highlight: '你想聊却没聊' },
{ main: '对话框里的', highlight: '四季' },
{ main: '字里行间', highlight: '这一年' },
{ main: '消息如潮水', highlight: '来了又退' },
{ main: '屏幕那头', highlight: '有人亮着灯' },
{ main: '打字的手指', highlight: '记得这一年' },
{ main: '时间会走', highlight: '对话会留下来' },
{ main: '每一条消息', highlight: '都是一次想起' },
{ main: '文字落下的地方', highlight: '有人在等' },
{ main: '那些发出去的字', highlight: '都有回响吗' },
{ main: '对话框亮起的', highlight: '瞬间' },
{ main: '这一年的', highlight: '「在吗」和「晚安」' },
{ main: '聊着聊着', highlight: '一年就过去了' },
{ main: '发出去的消息', highlight: '收到的回复' },
{ main: '那些秒回你的人', highlight: '和你秒回的人' },
{ main: '置顶的人', highlight: '还是那几个吗' },
{ main: '深夜的消息', highlight: '清晨的问候' },
{ main: '群聊里的热闹', highlight: '私聊里的安静' },
{ main: '表情包发了多少', highlight: '真心话说了几句' },
{ main: '已读不回的', highlight: '和秒回的' },
{ main: '消息免打扰的', highlight: '和置顶的' },
{ main: '总有人', highlight: '在消息那头' },
{ main: '每条消息背后', highlight: '都有一个想你的人' },
{ main: '感谢这一年', highlight: '愿意听你说话的人' },
{ main: '有人找你聊天', highlight: '是件幸运的事' },
{ main: '被回复的感觉', highlight: '叫做被在乎' },
{ main: '有些陪伴', highlight: '藏在对话框里' },
{ main: '谢谢那些', highlight: '愿意等你回复的人' },
{ main: '聊天这件小事', highlight: '其实是大事' },
{ main: '能说话的人', highlight: '都是重要的人' },
{ main: '这一年', highlight: '谢谢你们陪我聊天' },
]
// 50 个副标题
const SUBTITLES = [
'有些问候写在对话框里,有些陪伴藏在深夜里。',
'有些陪伴不说出口,但聊天记录都记得。',
'凌晨三点的消息、周末的闲聊、节日的祝福——都在这里。',
'一年的对话,浓缩成几张卡片,轻轻回看。',
'有些人聊着聊着就淡了,有些人聊着聊着就近了。',
'消息可以删除,但陪伴的时间删不掉。',
'那些打出来又删掉的字,也算说过了。',
'每一次「在吗」,都是一次想念。',
'深夜的对话,往往最真。',
'感谢每一个愿意听你说话的人。',
'一年的时间,几张卡片,一些数字,一点回忆。',
'深夜、清晨、周末、假期——你的聊天节奏,藏着生活的样子。',
'数字不会说谎,时间不会忘记。',
'365 天的对话,整理成几个瞬间。',
'时间知道你和谁聊得最多。',
'从时间维度,回看你的聊天节奏。',
'把一年的对话,整理成可以回望的样子。',
'时间会告诉你,谁一直都在。',
'这一年的时间,都花在了谁身上。',
'日历翻过去了,对话还留着。',
'不读内容,只看时间。让数字告诉你,谁一直都在。',
'我们只整理时间,不窥探秘密。这是属于你的一年。',
'不翻聊天记录,只看时间留下的痕迹。',
'这不是监控,是回望。这不是窥探,是整理。',
'我们只看时间,不看内容。你的秘密,依然是秘密。',
'不读取内容,只呈现时间的痕迹。',
'你的对话内容我们不碰,只帮你数数时间。',
'隐私是你的,回忆也是你的。',
'内容属于你,我们只借用时间。',
'安全地回望,温柔地整理。',
'从时间里回看你的聊天节奏。',
'一些数字,一点回忆。',
'简单整理,安静回看。',
'不多说,你自己看。',
'数字背后,是你的生活。',
'几张卡片,一年时光。',
'安静地看看这一年。',
'让数据说话。',
'你的一年,你的节奏。',
'回望,不打扰。',
'早安、晚安、在吗、好的——这些小词,撑起了一整年。',
'工作日的忙碌,周末的闲聊,都在这里了。',
'有些群天天响,有些人很少聊,但都是生活的一部分。',
'秒回的、已读不回的、消息免打扰的——都是你的选择。',
'置顶的那几个人,大概就是最重要的人吧。',
'表情包、语音、文字——你更喜欢哪种聊天方式?',
'深夜还在聊的,大概都是真朋友。',
'节日的群发祝福,和单独发的那条,不一样。',
'有些对话很长,有些只有一个表情包,但都算聊过。',
'聊天记录里,藏着你这一年的喜怒哀乐。',
]
// 随机选择 - 使用 useState 确保 SSR/CSR 一致,避免 hydration mismatch
const titleIndex = useState('wrapped-title-index', () => Math.floor(Math.random() * TITLES.length))
const subtitleIndex = useState('wrapped-subtitle-index', () => Math.floor(Math.random() * SUBTITLES.length))
const randomTitle = computed(() => TITLES[titleIndex.value])
const randomSubtitle = computed(() => SUBTITLES[subtitleIndex.value])
const props = defineProps({ const props = defineProps({
year: { type: Number, required: true }, year: { type: Number, required: true },
variant: { type: String, default: 'panel' } // 'panel' | 'slide' variant: { type: String, default: 'panel' } // 'panel' | 'slide'

View File

@@ -0,0 +1,236 @@
<template>
<div class="w-full">
<div class="rounded-2xl border border-[#00000010] bg-white/60 backdrop-blur p-4 sm:p-6">
<div class="flex items-center justify-between gap-4">
<div class="wrapped-label text-xs text-[#00000066]">年度聊天画像</div>
<div class="wrapped-body text-xs text-[#00000066]">Radar</div>
</div>
<div class="mt-4 grid gap-6 sm:grid-cols-[280px_1fr] items-center">
<div class="w-full max-w-[320px] mx-auto">
<svg viewBox="0 0 220 220" class="w-full h-auto select-none">
<!-- Grid -->
<g>
<polygon
v-for="i in rings"
:key="i"
:points="gridPolygonPoints(i / rings)"
fill="none"
stroke="rgba(0,0,0,0.08)"
stroke-width="1"
/>
<line
v-for="(p, idx) in axisPoints"
:key="idx"
:x1="cx"
:y1="cy"
:x2="p.x"
:y2="p.y"
stroke="rgba(0,0,0,0.10)"
stroke-width="1"
/>
</g>
<!-- Data polygon -->
<polygon
:points="dataPolygonPoints"
fill="rgba(7,193,96,0.18)"
stroke="rgba(7,193,96,0.85)"
stroke-width="2"
/>
<!-- Data nodes + tooltips -->
<g>
<circle
v-for="(p, idx) in dataPoints"
:key="idx"
:cx="p.x"
:cy="p.y"
r="4"
fill="#07C160"
stroke="white"
stroke-width="1.5"
>
<title>{{ p.title }}</title>
</circle>
</g>
<!-- Labels -->
<g>
<text
v-for="(l, idx) in labels"
:key="idx"
:x="l.x"
:y="l.y"
:text-anchor="l.anchor"
dominant-baseline="middle"
font-size="11"
fill="rgba(0,0,0,0.70)"
>
{{ l.label }}
</text>
</g>
</svg>
</div>
<div class="grid gap-3">
<div
v-for="m in metrics"
:key="m.key"
class="flex items-center justify-between gap-4"
>
<div class="wrapped-body text-sm text-[#00000099]">{{ m.name }}</div>
<div class="flex items-center gap-3 min-w-[160px]">
<div class="h-2 flex-1 rounded-full bg-[#0000000d] overflow-hidden">
<div class="h-full rounded-full bg-[#07C160]" :style="{ width: Math.round(m.norm * 100) + '%' }" />
</div>
<div
:class="[
'wrapped-number text-sm w-[74px] text-right',
m.display === '—' ? 'text-[#00000055]' : 'text-[#07C160] font-semibold'
]"
>
{{ m.display }}
</div>
</div>
</div>
<!-- Note removed per UI requirement (keep layout compact). -->
</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: () => ({}) }
})
const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 })
const formatInt = (n) => nfInt.format(Math.round(Number(n) || 0))
const formatFloat = (n, digits = 1) => {
const v = Number(n)
if (!Number.isFinite(v)) return '0'
return v.toFixed(digits)
}
const clamp01 = (v) => Math.max(0, Math.min(1, Number(v) || 0))
const logNorm = (v, maxLog) => {
const n = Number(v) || 0
const ml = Number(maxLog) || 1
if (n <= 0) return 0
return clamp01(Math.log10(1 + n) / ml)
}
const totalMessages = computed(() => Number(props.data?.totalMessages || 0))
const activeDays = computed(() => Number(props.data?.activeDays || 0))
const messagesPerDay = computed(() => Number(props.data?.messagesPerDay || 0))
const topContactMessages = computed(() => Number(props.data?.topContact?.messages || 0))
const topGroupMessages = computed(() => Number(props.data?.topGroup?.messages || 0))
const topKindPct = computed(() => {
const ratio = Number(props.data?.topKind?.ratio || 0)
if (!Number.isFinite(ratio) || ratio <= 0) return 0
return Math.max(0, Math.min(100, Math.round(ratio * 100)))
})
const metrics = computed(() => [
{
key: 'totalMessages',
name: '发送消息',
label: '发送',
display: `${formatInt(totalMessages.value)}`,
norm: logNorm(totalMessages.value, 6)
},
{
key: 'activeDays',
name: '发消息天数',
label: '天数',
display: `${formatInt(activeDays.value)}/365`,
norm: clamp01(activeDays.value / 365)
},
{
key: 'messagesPerDay',
name: '日均发送',
label: '日均',
display: `${formatFloat(messagesPerDay.value, 1)}`,
norm: logNorm(messagesPerDay.value, 3)
},
{
key: 'topContactMessages',
name: '发得最多的人',
label: '常发',
display: topContactMessages.value > 0 ? `${formatInt(topContactMessages.value)}` : '—',
norm: logNorm(topContactMessages.value, 5)
},
{
key: 'topGroupMessages',
name: '发言最多的群',
label: '发言',
display: topGroupMessages.value > 0 ? `${formatInt(topGroupMessages.value)}` : '—',
norm: logNorm(topGroupMessages.value, 5)
},
{
key: 'topKindPct',
name: '最常用表达',
label: '表达',
display: topKindPct.value > 0 ? `${topKindPct.value}%` : '—',
norm: clamp01(topKindPct.value / 100)
}
])
const rings = 5
const cx = 110
const cy = 110
const radius = 74
const axisPoints = computed(() => {
const n = metrics.value.length
return metrics.value.map((_, idx) => {
const a = (Math.PI * 2 * idx) / n - Math.PI / 2
return { x: cx + radius * Math.cos(a), y: cy + radius * Math.sin(a), a }
})
})
const gridPolygonPoints = (t) => {
const pts = axisPoints.value.map((p) => `${cx + (p.x - cx) * t},${cy + (p.y - cy) * t}`)
return pts.join(' ')
}
const dataPoints = computed(() => {
const pts = []
const n = metrics.value.length
for (let i = 0; i < n; i++) {
const m = metrics.value[i]
const a = (Math.PI * 2 * i) / n - Math.PI / 2
const r = radius * clamp01(m.norm)
const x = cx + r * Math.cos(a)
const y = cy + r * Math.sin(a)
pts.push({ x, y, title: `${m.name}${m.display}` })
}
return pts
})
const dataPolygonPoints = computed(() => dataPoints.value.map((p) => `${p.x},${p.y}`).join(' '))
const labels = computed(() => {
const out = []
const n = metrics.value.length
for (let i = 0; i < n; i++) {
const m = metrics.value[i]
const a = (Math.PI * 2 * i) / n - Math.PI / 2
const r = radius + 18
const x = cx + r * Math.cos(a)
const y = cy + r * Math.sin(a)
const cos = Math.cos(a)
let anchor = 'middle'
if (cos > 0.35) anchor = 'start'
else if (cos < -0.35) anchor = 'end'
out.push({ x, y, label: m.label, anchor })
}
return out
})
</script>

View File

@@ -0,0 +1,416 @@
<template>
<div class="w-full">
<!-- 聊天气泡区域 -->
<div class="rounded-2xl border border-[#00000010] bg-[#F5F5F5] p-3 sm:p-4">
<div class="flex flex-col gap-3">
<!-- Received (left) -->
<div class="flex items-end gap-2">
<div class="avatar-box bg-white">
<svg viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="#07C160" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 3h10a2 2 0 0 1 2 2v14H6V5a2 2 0 0 1 2-2z" />
<path d="M6 7H4a2 2 0 0 0-2 2v10h4" />
</svg>
</div>
<div class="bubble-left">
<div class="wrapped-label text-xs text-[#00000066]">你收到的字</div>
<div class="mt-0.5 wrapped-number text-xl sm:text-2xl text-[#000000e6]">
{{ formatInt(receivedChars) }}
</div>
<div class="mt-1 wrapped-body text-xs text-[#7F7F7F]">
<template v-if="receivedA4Text">{{ receivedA4Text }}</template>
<template v-else-if="receivedChars > 0">这么多字都是别人认真对你的回应</template>
<template v-else>今年还没有收到文字消息</template>
</div>
<div v-if="receivedA4 && receivedA4.a4 && receivedA4.a4.sheets > 0" class="mt-1 text-[10px] text-[#00000055] wrapped-label">
{{ formatInt(receivedA4.a4.sheets) }} A4 · 堆叠高度约 {{ receivedA4.a4.heightText }}
</div>
</div>
</div>
<!-- Sent (right) -->
<div class="flex items-end gap-2 justify-end">
<div class="bubble-right">
<div class="wrapped-label text-xs text-[#00000080] text-right">你发送的字</div>
<div class="mt-0.5 wrapped-number text-xl sm:text-2xl text-[#000000e6] text-right">
{{ formatInt(sentChars) }}
</div>
<div class="mt-1 wrapped-body text-xs text-[#00000099] text-right">
<template v-if="sentBookText">{{ sentBookText }}</template>
<template v-else-if="sentChars > 0">这么多字是你打出的每一次认真</template>
<template v-else>今年还没有文字消息</template>
</div>
</div>
<div class="avatar-box bg-[#95EC69]">
<svg viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="#1f2d1f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9" />
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
</svg>
</div>
</div>
</div>
</div>
<!-- 键盘磨损可视化 -->
<div class="keyboard-outer">
<div class="keyboard-inner">
<!-- 顶部信息 -->
<div class="keyboard-header">
<div class="keyboard-dots">
<span class="dot dot-red"></span>
<span class="dot dot-yellow"></span>
<span class="dot dot-green"></span>
</div>
<div class="keyboard-stats">{{ formatInt(totalKeyHits) }} KEYSTROKES</div>
</div>
<!-- 键盘主体 -->
<div class="keyboard-body">
<div v-for="(row, ri) in keyboardRows" :key="ri" class="kb-row">
<div
v-for="key in row"
:key="key.code + key.label"
class="kb-key"
:class="[`kb-w-${key.w || 1}`, { 'kb-space': key.isSpace }]"
:style="getKeyStyle(key.code)"
>
<div class="kb-key-top" :style="getKeyTopStyle(key.code)">
<span v-if="key.sub" class="kb-sub">{{ key.sub }}</span>
<span v-if="key.label" class="kb-label" :class="{ 'kb-label-sm': key.isFunc }" :style="getLabelStyle(key.code)">{{ key.label }}</span>
<div v-if="key.isSpace" class="kb-space-bar"></div>
</div>
</div>
</div>
</div>
<!-- 底部品牌 -->
<div class="keyboard-brand">WeChat Mechanical KB</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: () => ({}) }
})
const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 })
const formatInt = (n) => nfInt.format(Math.round(Number(n) || 0))
const sentChars = computed(() => Number(props.data?.sentChars || 0))
const receivedChars = computed(() => Number(props.data?.receivedChars || 0))
const sentBookText = computed(() => props.data?.sentBook?.text || '')
const receivedA4 = computed(() => props.data?.receivedA4 || null)
const receivedA4Text = computed(() => receivedA4.value?.text || '')
// 从后端获取键盘统计数据
const keyboardData = computed(() => props.data?.keyboard || null)
// 总敲击次数(优先使用后端数据)
const totalKeyHits = computed(() => {
// 注意totalKeyHits 可能为 0比如今年没发出文字消息不能用 truthy 判断。
const backend = Number(keyboardData.value?.totalKeyHits)
if (Number.isFinite(backend)) return backend
// 回退:粗略估算(仅基于"你发送的字",假设拼音输入 + 一定比例空格)
const letterHits = Math.round(sentChars.value * 2.8)
return letterHits + Math.round(letterHits * 0.15)
})
// 获取各键的敲击次数(优先使用后端精确数据)
const keyHitsMap = computed(() => {
const backendHits = keyboardData.value?.keyHits
const backendSpace = Number(keyboardData.value?.spaceHits || 0)
if (backendHits && typeof backendHits === 'object') {
// 后端把空格次数单独放在 spaceHits这里合并进 keyHitsMap 以便空格键也能显示磨损。
return backendSpace > 0 ? { ...backendHits, space: backendSpace } : backendHits
}
// 回退:使用默认频率估算(仅基于"你发送的字"
const defaultFreq = {
a: 0.121, i: 0.118, n: 0.098, e: 0.089, u: 0.082, g: 0.072, h: 0.065,
o: 0.052, z: 0.048, s: 0.042, x: 0.038, y: 0.036, d: 0.032, l: 0.028,
j: 0.026, b: 0.022, c: 0.020, w: 0.018, m: 0.016, f: 0.014, t: 0.012,
r: 0.010, p: 0.009, k: 0.007, q: 0.005, v: 0.001,
}
const letterHits = Math.round(sentChars.value * 2.8)
const spaceHits = Math.round(letterHits * 0.15)
const result = {}
for (const [k, freq] of Object.entries(defaultFreq)) {
result[k] = Math.round(freq * letterHits)
}
if (spaceHits > 0) result.space = spaceHits
return result
})
// 计算磨损程度0-1基于实际敲击次数
const getWear = (code) => {
const k = code.toLowerCase()
const hits = Number(keyHitsMap.value[k] || 0)
if (!Number.isFinite(hits) || hits <= 0) return 0
const values = Object.values(keyHitsMap.value).map((v) => Number(v) || 0)
const maxHits = Math.max(...values, 1)
// 小数量级键(如数字/标点)容易"看起来没变化",用对数缩放增强可视化差异。
const ratio = Math.log1p(hits) / Math.log1p(maxHits)
return Math.min(1, Math.pow(ratio, 1.6))
}
// 键盘布局
const keyboardRows = [
[
{ code: '`', label: '`', sub: '~' }, { code: '1', label: '1', sub: '!' },
{ code: '2', label: '2', sub: '@' }, { code: '3', label: '3', sub: '#' },
{ code: '4', label: '4', sub: '$' }, { code: '5', label: '5', sub: '%' },
{ code: '6', label: '6', sub: '^' }, { code: '7', label: '7', sub: '&' },
{ code: '8', label: '8', sub: '*' }, { code: '9', label: '9', sub: '(' },
{ code: '0', label: '0', sub: ')' }, { code: '-', label: '-', sub: '_' },
{ code: '=', label: '=', sub: '+' }, { code: 'backspace', label: '⌫', w: 2, isFunc: true },
],
[
{ code: 'tab', label: 'Tab', w: 1.5, isFunc: true },
{ code: 'q', label: 'Q' }, { code: 'w', label: 'W' }, { code: 'e', label: 'E' },
{ code: 'r', label: 'R' }, { code: 't', label: 'T' }, { code: 'y', label: 'Y' },
{ code: 'u', label: 'U' }, { code: 'i', label: 'I' }, { code: 'o', label: 'O' },
{ code: 'p', label: 'P' }, { code: '[', label: '[', sub: '{' },
{ code: ']', label: ']', sub: '}' }, { code: '\\', label: '\\', sub: '|', w: 1.5 },
],
[
{ code: 'caps', label: 'Caps', w: 1.75, isFunc: true },
{ code: 'a', label: 'A' }, { code: 's', label: 'S' }, { code: 'd', label: 'D' },
{ code: 'f', label: 'F' }, { code: 'g', label: 'G' }, { code: 'h', label: 'H' },
{ code: 'j', label: 'J' }, { code: 'k', label: 'K' }, { code: 'l', label: 'L' },
{ code: ';', label: ';', sub: ':' }, { code: "'", label: "'", sub: '"' },
{ code: 'enter', label: 'Enter', w: 2.25, isFunc: true },
],
[
{ code: 'shift', label: 'Shift', w: 2.25, isFunc: true },
{ code: 'z', label: 'Z' }, { code: 'x', label: 'X' }, { code: 'c', label: 'C' },
{ code: 'v', label: 'V' }, { code: 'b', label: 'B' }, { code: 'n', label: 'N' },
{ code: 'm', label: 'M' }, { code: ',', label: ',', sub: '<' },
{ code: '.', label: '.', sub: '>' }, { code: '/', label: '/', sub: '?' },
{ code: 'shift', label: 'Shift', w: 2.75, isFunc: true },
],
[
{ code: 'ctrl', label: 'Ctrl', w: 1.25, isFunc: true },
{ code: 'alt', label: 'Alt', w: 1.25, isFunc: true },
{ code: 'space', label: '', w: 6.25, isSpace: true },
{ code: 'alt', label: 'Alt', w: 1.25, isFunc: true },
{ code: 'ctrl', label: 'Ctrl', w: 1.25, isFunc: true },
],
]
// 键帽样式
const getKeyStyle = (code) => {
const w = getWear(code)
// Light theme: clean keys are bright; wear makes keys warmer and slightly darker.
const baseL = 94 - w * 18
const sat = 8 + w * 20
return {
'--key-bg': `hsl(40, ${sat}%, ${baseL}%)`,
'--key-bg-dark': `hsl(40, ${sat}%, ${baseL - 6}%)`,
'--key-border': `hsl(40, ${Math.max(0, sat - 2)}%, ${baseL - 18}%)`,
}
}
const getKeyTopStyle = (code) => {
const w = getWear(code)
const highlight = 0.55 - w * 0.35
const depth = 0.12 + w * 0.06
return {
background: `linear-gradient(180deg, var(--key-bg) 0%, var(--key-bg-dark) 100%)`,
// 用连续函数替代"阈值切换",避免出现"没到阈值就看不出变化"的感觉。
boxShadow: `inset 0 1px 0 rgba(255,255,255,${highlight}), inset 0 -1px 2px rgba(0,0,0,${depth})`,
}
}
const getLabelStyle = (code) => {
const w = getWear(code)
return {
opacity: 1 - w * 0.85,
filter: `blur(${w * 1.8}px)`,
}
}
</script>
<style scoped>
/* 头像 */
.avatar-box {
@apply w-8 h-8 rounded-lg border border-[#00000010] flex items-center justify-center flex-shrink-0;
}
/* 气泡 - 左侧 */
.bubble-left {
@apply relative max-w-[85%] bg-white shadow-sm rounded-xl px-3 py-2;
}
.bubble-left::before {
content: '';
position: absolute;
left: -6px;
bottom: 8px;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid #fff;
filter: drop-shadow(-1px 0 0 rgba(0,0,0,0.05));
}
/* 气泡 - 右侧 */
.bubble-right {
@apply relative max-w-[85%] bg-[#95EC69] shadow-sm rounded-xl px-3 py-2;
}
.bubble-right::after {
content: '';
position: absolute;
right: -6px;
bottom: 8px;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid #95EC69;
filter: drop-shadow(1px 0 0 rgba(0,0,0,0.05));
}
/* 键盘外框 */
.keyboard-outer {
@apply mt-3 rounded-2xl p-1;
background: linear-gradient(145deg, #ffffff, #e8e8e8);
border: 1px solid rgba(0,0,0,0.06);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.keyboard-inner {
@apply rounded-xl p-3;
background: linear-gradient(180deg, #fbfbfb, #f0f0f0);
border: 1px solid rgba(0,0,0,0.06);
}
.keyboard-header {
@apply flex items-center justify-between mb-2 px-1;
}
.keyboard-dots {
@apply flex items-center gap-1.5;
}
.dot {
@apply w-2 h-2 rounded-full;
}
.dot-red { background: #ff5f57; }
.dot-yellow { background: #febc2e; }
.dot-green { background: #28c840; }
.keyboard-stats {
@apply text-[10px] text-[#00000066] tracking-wider;
font-family: ui-monospace, monospace;
}
.keyboard-body {
@apply rounded-lg p-2;
background: #f4f4f5;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.12);
}
.kb-row {
@apply flex justify-center gap-[3px] mb-[3px];
}
.kb-row:last-child {
@apply mb-0;
}
/* 键帽 */
.kb-key {
--unit: 22px;
height: 26px;
width: var(--unit);
position: relative;
}
@media (min-width: 640px) {
.kb-key {
--unit: 28px;
height: 32px;
}
}
/* 宽度变体 */
.kb-w-1 { width: var(--unit); }
.kb-w-1\.25 { width: calc(var(--unit) * 1.25 + 3px * 0.25); }
.kb-w-1\.5 { width: calc(var(--unit) * 1.5 + 3px * 0.5); }
.kb-w-1\.75 { width: calc(var(--unit) * 1.75 + 3px * 0.75); }
.kb-w-2 { width: calc(var(--unit) * 2 + 3px); }
.kb-w-2\.25 { width: calc(var(--unit) * 2.25 + 3px * 1.25); }
.kb-w-2\.75 { width: calc(var(--unit) * 2.75 + 3px * 1.75); }
.kb-w-6\.25 { width: calc(var(--unit) * 6.25 + 3px * 5.25); }
.kb-key::before {
content: '';
position: absolute;
inset: 0;
top: 2px;
background: #d4d4d8;
border-radius: 4px;
}
.kb-key-top {
position: absolute;
inset: 0;
bottom: 2px;
border-radius: 4px;
border: 1px solid var(--key-border);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.kb-sub {
font-size: 7px;
line-height: 1;
color: #666;
margin-bottom: 1px;
}
@media (min-width: 640px) {
.kb-sub {
font-size: 8px;
}
}
.kb-label {
font-size: 10px;
font-weight: 500;
color: #262626;
line-height: 1;
text-shadow: 0 1px 0 rgba(255,255,255,0.6);
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
}
@media (min-width: 640px) {
.kb-label {
font-size: 11px;
}
}
.kb-label-sm {
font-size: 7px !important;
font-weight: 400;
}
@media (min-width: 640px) {
.kb-label-sm {
font-size: 8px !important;
}
}
.kb-space-bar {
width: 50%;
height: 3px;
background: rgba(0,0,0,0.12);
border-radius: 2px;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.18);
}
.keyboard-brand {
@apply mt-2 text-center text-[8px] text-[#00000025] tracking-[0.15em] uppercase;
}
</style>

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div class="text-sm text-[#7F7F7F]"> <div class="wrapped-body text-sm text-[#7F7F7F]">
<span class="text-[#07C160] font-semibold">{{ totalMessages }}</span> 条消息 <span class="wrapped-number text-[#07C160] font-semibold">{{ totalMessages }}</span> 条消息
</div> </div>
<div class="text-xs text-[#00000066]">24H × 7Days</div> <div class="wrapped-label text-xs text-[#00000066]">24H x 7Days</div>
</div> </div>
<div class="mt-4 overflow-x-auto" data-wrapped-scroll-x> <div class="mt-4 overflow-x-auto" data-wrapped-scroll-x>
@@ -15,7 +15,7 @@
<span <span
v-for="(s, idx) in timeLabels" v-for="(s, idx) in timeLabels"
:key="idx" :key="idx"
class="col-span-4 font-mono" class="col-span-4 wrapped-number"
> >
{{ s }} {{ s }}
</span> </span>
@@ -24,7 +24,7 @@
<div class="grid gap-[3px] [grid-template-columns:24px_1fr] items-stretch"> <div class="grid gap-[3px] [grid-template-columns:24px_1fr] items-stretch">
<div class="grid gap-[3px] [grid-template-rows:repeat(7,minmax(0,1fr))] text-[11px] text-[#00000066]"> <div class="grid gap-[3px] [grid-template-rows:repeat(7,minmax(0,1fr))] text-[11px] text-[#00000066]">
<div v-for="(w, wi) in weekdayLabels" :key="wi" class="flex items-center"> <div v-for="(w, wi) in weekdayLabels" :key="wi" class="flex items-center wrapped-body">
{{ w }} {{ w }}
</div> </div>
</div> </div>
@@ -35,7 +35,7 @@
v-for="(v, hi) in row" v-for="(v, hi) in row"
:key="`${wi}-${hi}`" :key="`${wi}-${hi}`"
class="aspect-square min-h-[10px] rounded-[2px] transition-transform duration-150 hover:scale-125 hover:z-10 relative" class="aspect-square min-h-[10px] rounded-[2px] transition-transform duration-150 hover:scale-125 hover:z-10 relative"
:style="{ backgroundColor: colorFor(v) }" :style="{ backgroundColor: colorFor(v), transformOrigin: originFor(wi, hi) }"
:title="tooltipFor(wi, hi, v)" :title="tooltipFor(wi, hi, v)"
/> />
</template> </template>
@@ -46,13 +46,13 @@
<div class="mt-4 flex items-center justify-between text-xs text-[#00000066]"> <div class="mt-4 flex items-center justify-between text-xs text-[#00000066]">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span></span> <span class="wrapped-body"></span>
<div class="flex items-center gap-[2px]"> <div class="flex items-center gap-[2px]">
<span v-for="i in 6" :key="i" class="w-4 h-2 rounded-[2px]" :style="{ backgroundColor: legendColor(i) }"></span> <span v-for="i in 6" :key="i" class="w-4 h-2 rounded-[2px]" :style="{ backgroundColor: legendColor(i) }"></span>
</div> </div>
<span></span> <span class="wrapped-body"></span>
</div> </div>
<div v-if="maxValue > 0">最大 {{ maxValue }}</div> <div v-if="maxValue > 0" class="wrapped-number">最大 {{ maxValue }}</div>
</div> </div>
</div> </div>
</template> </template>
@@ -102,4 +102,12 @@ const legendColor = (i) => {
const t = i / 6 const t = i / 6
return heatColor(Math.max(1, t * (maxValue.value || 1)), maxValue.value || 1) return heatColor(Math.max(1, t * (maxValue.value || 1)), maxValue.value || 1)
} }
const originFor = (weekdayIndex, hour) => {
// Avoid hover scaling pushing scrollWidth/scrollHeight and showing scrollbars:
// keep the "outer" edges anchored on the first/last row/col.
const x = hour === 0 ? 'left' : (hour === 23 ? 'right' : 'center')
const y = weekdayIndex === 0 ? 'top' : (weekdayIndex === 6 ? 'bottom' : 'center')
return `${x} ${y}`
}
</script> </script>

View File

@@ -322,6 +322,28 @@ export const useApi = () => {
return await request(url) return await request(url)
} }
// WeChat Wrapped年度总结- 目录/元信息(轻量,用于按页懒加载)
const getWrappedAnnualMeta = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.year != null) query.set('year', String(params.year))
if (params && params.account) query.set('account', String(params.account))
if (params && params.refresh != null) query.set('refresh', String(!!params.refresh))
const url = '/wrapped/annual/meta' + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
// WeChat Wrapped年度总结- 单张卡片(按页加载)
const getWrappedAnnualCard = async (cardId, params = {}) => {
if (cardId == null) throw new Error('Missing cardId')
const query = new URLSearchParams()
if (params && params.year != null) query.set('year', String(params.year))
if (params && params.account) query.set('account', String(params.account))
if (params && params.refresh != null) query.set('refresh', String(!!params.refresh))
const safeId = encodeURIComponent(String(cardId))
const url = `/wrapped/annual/cards/${safeId}` + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
return { return {
detectWechat, detectWechat,
detectCurrentAccount, detectCurrentAccount,
@@ -350,6 +372,8 @@ export const useApi = () => {
getChatExport, getChatExport,
listChatExports, listChatExports,
cancelChatExport, cancelChatExport,
getWrappedAnnual getWrappedAnnual,
getWrappedAnnualMeta,
getWrappedAnnualCard
} }
} }

View File

@@ -9,6 +9,7 @@
"dependencies": { "dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
"@pinia/nuxt": "^0.11.2", "@pinia/nuxt": "^0.11.2",
"@vueuse/motion": "^3.0.3",
"axios": "^1.11.0", "axios": "^1.11.0",
"nuxt": "^4.0.1", "nuxt": "^4.0.1",
"vue": "^3.5.17", "vue": "^3.5.17",
@@ -1036,6 +1037,16 @@
"@jridgewell/trace-mapping": "^0.3.24" "@jridgewell/trace-mapping": "^0.3.24"
} }
}, },
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -1056,9 +1067,9 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4", "version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
@@ -3898,6 +3909,12 @@
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@types/yauzl": { "node_modules/@types/yauzl": {
"version": "2.10.3", "version": "2.10.3",
"resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -4328,6 +4345,114 @@
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vueuse/core": {
"version": "13.9.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-13.9.0.tgz",
"integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.9.0",
"@vueuse/shared": "13.9.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "13.9.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.9.0.tgz",
"integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/motion": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@vueuse/motion/-/motion-3.0.3.tgz",
"integrity": "sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==",
"license": "MIT",
"dependencies": {
"@vueuse/core": "^13.0.0",
"@vueuse/shared": "^13.0.0",
"defu": "^6.1.4",
"framesync": "^6.1.2",
"popmotion": "^11.0.5",
"style-value-types": "^5.1.2"
},
"optionalDependencies": {
"@nuxt/kit": "^3.13.0"
},
"peerDependencies": {
"vue": ">=3.0.0"
}
},
"node_modules/@vueuse/motion/node_modules/@nuxt/kit": {
"version": "3.21.0",
"resolved": "https://registry.npmmirror.com/@nuxt/kit/-/kit-3.21.0.tgz",
"integrity": "sha512-KMTLK/dsGaQioZzkYUvgfN9le4grNW54aNcA1jqzgVZLcFVy4jJfrJr5WZio9NT2EMfajdoZ+V28aD7BRr4Zfw==",
"license": "MIT",
"optional": true,
"dependencies": {
"c12": "^3.3.3",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.8",
"ignore": "^7.0.5",
"jiti": "^2.6.1",
"klona": "^2.0.6",
"knitwork": "^1.3.0",
"mlly": "^1.8.0",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.3.0",
"rc9": "^2.1.2",
"scule": "^1.3.0",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ufo": "^1.6.3",
"unctx": "^2.5.0",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/@vueuse/motion/node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/@vueuse/shared": {
"version": "13.9.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.9.0.tgz",
"integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@whatwg-node/disposablestack": { "node_modules/@whatwg-node/disposablestack": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmmirror.com/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", "resolved": "https://registry.npmmirror.com/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
@@ -4916,26 +5041,26 @@
} }
}, },
"node_modules/c12": { "node_modules/c12": {
"version": "3.1.0", "version": "3.3.3",
"resolved": "https://registry.npmmirror.com/c12/-/c12-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/c12/-/c12-3.3.3.tgz",
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^4.0.3", "chokidar": "^5.0.0",
"confbox": "^0.2.2", "confbox": "^0.2.2",
"defu": "^6.1.4", "defu": "^6.1.4",
"dotenv": "^16.6.1", "dotenv": "^17.2.3",
"exsolve": "^1.0.7", "exsolve": "^1.0.8",
"giget": "^2.0.0", "giget": "^2.0.0",
"jiti": "^2.4.2", "jiti": "^2.6.1",
"ohash": "^2.0.11", "ohash": "^2.0.11",
"pathe": "^2.0.3", "pathe": "^2.0.3",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^2.0.0",
"pkg-types": "^2.2.0", "pkg-types": "^2.3.0",
"rc9": "^2.1.2" "rc9": "^2.1.2"
}, },
"peerDependencies": { "peerDependencies": {
"magicast": "^0.3.5" "magicast": "*"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"magicast": { "magicast": {
@@ -4943,6 +5068,52 @@
} }
} }
}, },
"node_modules/c12/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/c12/node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/c12/node_modules/perfect-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
"license": "MIT"
},
"node_modules/c12/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/cac": { "node_modules/cac": {
"version": "6.7.14", "version": "6.7.14",
"resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz",
@@ -6583,9 +6754,9 @@
} }
}, },
"node_modules/exsolve": { "node_modules/exsolve": {
"version": "1.0.7", "version": "1.0.8",
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/extract-zip": { "node_modules/extract-zip": {
@@ -6673,10 +6844,13 @@
} }
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.6", "version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },
@@ -6878,6 +7052,21 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framesync": {
"version": "6.1.2",
"resolved": "https://registry.npmmirror.com/framesync/-/framesync-6.1.2.tgz",
"integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
"license": "MIT",
"dependencies": {
"tslib": "2.4.0"
}
},
"node_modules/framesync/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"license": "0BSD"
},
"node_modules/fresh": { "node_modules/fresh": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
@@ -7260,6 +7449,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
"license": "MIT"
},
"node_modules/hookable": { "node_modules/hookable": {
"version": "5.5.3", "version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
@@ -7842,9 +8037,9 @@
} }
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.5.1", "version": "2.6.1",
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.5.1.tgz", "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
@@ -7944,9 +8139,9 @@
} }
}, },
"node_modules/knitwork": { "node_modules/knitwork": {
"version": "1.2.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.3.0.tgz",
"integrity": "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==", "integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/koa": { "node_modules/koa": {
@@ -8378,12 +8573,12 @@
} }
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/magic-string-ast": { "node_modules/magic-string-ast": {
@@ -8618,15 +8813,15 @@
} }
}, },
"node_modules/mlly": { "node_modules/mlly": {
"version": "1.7.4", "version": "1.8.0",
"resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.7.4.tgz", "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz",
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"acorn": "^8.14.0", "acorn": "^8.15.0",
"pathe": "^2.0.1", "pathe": "^2.0.3",
"pkg-types": "^1.3.0", "pkg-types": "^1.3.1",
"ufo": "^1.5.4" "ufo": "^1.6.1"
} }
}, },
"node_modules/mlly/node_modules/confbox": { "node_modules/mlly/node_modules/confbox": {
@@ -9739,9 +9934,9 @@
} }
}, },
"node_modules/pkg-types": { "node_modules/pkg-types": {
"version": "2.2.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.2.0.tgz", "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"confbox": "^0.2.2", "confbox": "^0.2.2",
@@ -9749,6 +9944,24 @@
"pathe": "^2.0.3" "pathe": "^2.0.3"
} }
}, },
"node_modules/popmotion": {
"version": "11.0.5",
"resolved": "https://registry.npmmirror.com/popmotion/-/popmotion-11.0.5.tgz",
"integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
"license": "MIT",
"dependencies": {
"framesync": "6.1.2",
"hey-listen": "^1.0.8",
"style-value-types": "5.1.2",
"tslib": "2.4.0"
}
},
"node_modules/popmotion/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"license": "0BSD"
},
"node_modules/portfinder": { "node_modules/portfinder": {
"version": "1.0.37", "version": "1.0.37",
"resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.37.tgz", "resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.37.tgz",
@@ -11071,9 +11284,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -11584,6 +11797,22 @@
"integrity": "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==", "integrity": "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/style-value-types": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/style-value-types/-/style-value-types-5.1.2.tgz",
"integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
"license": "MIT",
"dependencies": {
"hey-listen": "^1.0.8",
"tslib": "2.4.0"
}
},
"node_modules/style-value-types/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"license": "0BSD"
},
"node_modules/stylehacks": { "node_modules/stylehacks": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmmirror.com/stylehacks/-/stylehacks-7.0.6.tgz", "resolved": "https://registry.npmmirror.com/stylehacks/-/stylehacks-7.0.6.tgz",
@@ -12235,9 +12464,9 @@
} }
}, },
"node_modules/ufo": { "node_modules/ufo": {
"version": "1.6.1", "version": "1.6.3",
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz",
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ultrahtml": { "node_modules/ultrahtml": {
@@ -12253,15 +12482,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/unctx": { "node_modules/unctx": {
"version": "2.4.1", "version": "2.5.0",
"resolved": "https://registry.npmmirror.com/unctx/-/unctx-2.4.1.tgz", "resolved": "https://registry.npmmirror.com/unctx/-/unctx-2.5.0.tgz",
"integrity": "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==", "integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"acorn": "^8.14.0", "acorn": "^8.15.0",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"magic-string": "^0.30.17", "magic-string": "^0.30.21",
"unplugin": "^2.1.0" "unplugin": "^2.3.11"
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
@@ -12367,13 +12596,14 @@
} }
}, },
"node_modules/unplugin": { "node_modules/unplugin": {
"version": "2.3.5", "version": "2.3.11",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.5.tgz", "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.11.tgz",
"integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"acorn": "^8.14.1", "@jridgewell/remapping": "^2.3.5",
"picomatch": "^4.0.2", "acorn": "^8.15.0",
"picomatch": "^4.0.3",
"webpack-virtual-modules": "^0.6.2" "webpack-virtual-modules": "^0.6.2"
}, },
"engines": { "engines": {

View File

@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
"@pinia/nuxt": "^0.11.2", "@pinia/nuxt": "^0.11.2",
"@vueuse/motion": "^3.0.3",
"axios": "^1.11.0", "axios": "^1.11.0",
"nuxt": "^4.0.1", "nuxt": "^4.0.1",
"vue": "^3.5.17", "vue": "^3.5.17",

View File

@@ -1,18 +1,96 @@
<template> <template>
<!-- PPT 风格单张卡片占据全页面鼠标滚轮切换 -->
<div <div
ref="deckEl" ref="deckEl"
class="relative h-screen w-full overflow-hidden transition-colors duration-500" class="relative h-screen w-full overflow-hidden transition-colors duration-500"
:class="{ 'wrapped-retro': retro }"
:style="{ backgroundColor: currentBg }" :style="{ backgroundColor: currentBg }"
> >
<!-- PPT 风格单张卡片占据全页面鼠标滚轮切换 -->
<WrappedDeckBackground /> <WrappedDeckBackground />
<WrappedCRTOverlay v-if="retro" />
<!-- 年份固定在右上角类似 PPT 页眉避免把年份写进标题里 --> <!-- 上角刷新 + 复古模式开关 -->
<div class="absolute top-6 right-6 z-20 pointer-events-none select-none"> <div class="absolute top-6 left-6 z-20 select-none">
<div class="flex items-center gap-3">
<button
type="button"
class="pointer-events-auto inline-flex items-center justify-center w-9 h-9 rounded-full bg-transparent text-[#07C160] hover:bg-[#07C160]/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#07C160]/30 disabled:opacity-60 disabled:cursor-not-allowed transition"
:disabled="loading || accountsLoading || accounts.length === 0"
aria-label="强制刷新忽略缓存"
title="强制刷新(忽略缓存)"
@click="reload(true)"
>
<!-- Refresh icon (spins while loading) -->
<svg
class="w-4 h-4"
:class="loading ? 'animate-spin' : ''"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 12a9 9 0 1 1-3-6.7" />
<path d="M21 3v7h-7" />
</svg>
</button>
<button
type="button"
class="pointer-events-auto inline-flex items-center justify-center w-9 h-9 rounded-full bg-transparent transition disabled:opacity-60 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-[#07C160]/30"
:class="retro ? 'text-[#07C160] hover:bg-[#07C160]/10' : 'text-[#00000055] hover:bg-[#000000]/5'"
:aria-pressed="retro ? 'true' : 'false'"
aria-label="复古模式像素字体 + CRT 滤镜"
title="复古模式:像素字体 + CRT 滤镜"
@click="retro = !retro"
>
<img
src="/assets/images/wechat-audio-dark.png"
class="w-4 h-4 transition"
:style="{ filter: retro ? 'none' : 'grayscale(1)', opacity: retro ? '1' : '0.55' }"
alt=""
aria-hidden="true"
draggable="false"
/>
</button>
</div>
<div v-if="error" class="mt-2 pointer-events-auto bg-white/90 backdrop-blur rounded-xl border border-red-200 px-3 py-2">
<div class="wrapped-label text-xs text-red-700">生成失败</div>
<div class="mt-1 wrapped-body text-xs text-red-600 whitespace-pre-wrap">{{ error }}</div>
</div>
</div>
<!-- 右上角年份仅可切换有数据的年份 -->
<div class="absolute top-6 right-6 z-20 pointer-events-auto select-none">
<div class="relative"> <div class="relative">
<div class="absolute -inset-6 rounded-full bg-[#07C160]/10 blur-2xl"></div> <div class="absolute -inset-6 rounded-full bg-[#07C160]/10 blur-2xl"></div>
<div class="relative text-xs font-semibold tracking-[0.28em] text-[#00000066] text-right"> <div class="relative flex justify-end">
{{ year }} <div class="relative inline-flex items-center">
<select
class="pointer-events-auto appearance-none bg-transparent pr-5 pl-0 py-0.5 rounded-md wrapped-label text-xs text-[#00000066] text-right focus:outline-none focus-visible:ring-2 focus-visible:ring-[#07C160]/30 hover:bg-[#000000]/5 transition disabled:opacity-70 disabled:cursor-default"
:disabled="loading || accountsLoading || yearOptions.length <= 1"
:value="String(year)"
@change="setYear($event.target.value)"
>
<option v-for="y in yearOptions" :key="y" :value="String(y)">{{ y }}</option>
</select>
<svg
v-if="yearOptions.length > 1"
class="pointer-events-none absolute right-1 w-3 h-3 text-[#00000066]"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 10.94l3.71-3.71a.75.75 0 1 1 1.06 1.06l-4.24 4.24a.75.75 0 0 1-1.06 0L5.21 8.29a.75.75 0 0 1 .02-1.08z"
clip-rule="evenodd"
/>
</svg>
</div>
</div> </div>
<div class="relative mt-1 h-[1px] w-16 ml-auto bg-gradient-to-l from-[#07C160]/40 to-transparent"></div> <div class="relative mt-1 h-[1px] w-16 ml-auto bg-gradient-to-l from-[#07C160]/40 to-transparent"></div>
</div> </div>
@@ -26,33 +104,6 @@
<section class="w-full" :style="slideStyle"> <section class="w-full" :style="slideStyle">
<div class="h-full w-full relative"> <div class="h-full w-full relative">
<WrappedHero :year="year" variant="slide" class="h-full w-full" /> <WrappedHero :year="year" variant="slide" class="h-full w-full" />
<!-- 生成面板仅在尚未生成报告时显示分享视图隐藏账号相关内容 -->
<div v-if="bootstrapped && !report" class="absolute left-0 right-0 bottom-0 pb-8">
<div class="max-w-5xl mx-auto px-6 sm:px-8 space-y-3">
<div v-if="error" class="bg-white/90 backdrop-blur rounded-2xl border border-red-200 p-5">
<div class="text-red-700 font-semibold">生成失败</div>
<div class="mt-2 text-sm text-red-600 whitespace-pre-wrap">{{ error }}</div>
<div class="mt-4 text-xs text-[#7F7F7F]">
提示请确认已完成解密并且后端服务正在运行默认 http://127.0.0.1:8000
</div>
</div>
<WrappedControls
:accounts="accounts"
:accounts-loading="accountsLoading"
:loading="loading"
:model-year="year"
:model-account="account"
:model-refresh="refresh"
:show-account="false"
@update:year="(v) => { year.value = v }"
@update:account="(v) => { account.value = v }"
@update:refresh="(v) => { refresh.value = v }"
@reload="reload"
/>
</div>
</div>
</div> </div>
</section> </section>
@@ -63,8 +114,55 @@
class="w-full" class="w-full"
:style="slideStyle" :style="slideStyle"
> >
<WrappedCardShell
v-if="!c || c.status !== 'ok'"
:card-id="Number(c?.id || (idx + 1))"
:title="c?.title || '正在生成…'"
:narrative="c?.status === 'error' ? '生成失败' : (c?.status === 'loading' ? '正在生成本页数据…' : '进入该页后将开始生成')"
variant="slide"
class="h-full w-full"
>
<div v-if="c?.status === 'error'" class="text-sm text-[#7F7F7F]">
<div class="wrapped-body text-sm text-red-600 whitespace-pre-wrap">{{ c?.error || '未知错误' }}</div>
<button
type="button"
class="mt-4 inline-flex items-center justify-center px-4 py-2 rounded-lg bg-[#07C160] text-white text-sm wrapped-label hover:bg-[#06AD56] transition"
@click="retryCard(Number(c?.id))"
>
重试
</button>
</div>
<div v-else class="flex items-center gap-3 text-sm text-[#7F7F7F]">
<svg class="w-4 h-4 animate-spin text-[#07C160]" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z"
/>
</svg>
<div class="wrapped-body text-sm text-[#7F7F7F]">
<span v-if="c?.status === 'idle'">翻到此页后开始生成</span>
<span v-else>正在生成本页数据</span>
</div>
</div>
</WrappedCardShell>
<Card00GlobalOverview
v-else-if="c && (c.kind === 'global/overview' || c.id === 0)"
:card="c"
variant="slide"
class="h-full w-full"
/>
<Card01CyberSchedule <Card01CyberSchedule
v-if="c && (c.kind === 'time/weekday_hour_heatmap' || c.id === 1)" v-else-if="c && (c.kind === 'time/weekday_hour_heatmap' || c.id === 1)"
:card="c"
variant="slide"
class="h-full w-full"
/>
<Card02MessageChars
v-else-if="c && (c.kind === 'text/message_chars' || c.id === 2)"
:card="c" :card="c"
variant="slide" variant="slide"
class="h-full w-full" class="h-full w-full"
@@ -96,21 +194,36 @@ useHead({
const api = useApi() const api = useApi()
const route = useRoute() const route = useRoute()
const router = useRouter()
const year = ref(Number(route.query?.year) || new Date().getFullYear()) const year = ref(Number(route.query?.year) || new Date().getFullYear())
// 分享视图不展示账号信息:默认让后端自动选择;需要指定时可用 query ?account=wxid_xxx // 分享视图不展示账号信息:默认让后端自动选择;需要指定时可用 query ?account=wxid_xxx
const account = ref(typeof route.query?.account === 'string' ? route.query.account : '') const account = ref(typeof route.query?.account === 'string' ? route.query.account : '')
const refresh = ref(false)
// Retro mode: pixel font + CRT overlay.
const retro = ref(true)
const accounts = ref([]) const accounts = ref([])
const accountsLoading = ref(true) const accountsLoading = ref(true)
// Avoid flashing the "year card" controls before the initial auto-load finishes. const loading = ref(false)
const bootstrapped = ref(false) const error = ref('')
const report = ref(null)
const loading = ref(false) // If user clicks "强制刷新", pass refresh=true for subsequent per-card requests in this session.
const error = ref('') const refreshCards = ref(false)
const report = ref(null) let reportToken = 0
const availableYears = ref([])
const yearOptions = computed(() => {
const ys = Array.isArray(availableYears.value) ? availableYears.value : []
const out = ys
.map((x) => Number(x))
.filter((x) => Number.isFinite(x))
.sort((a, b) => b - a)
// Fallback to current year if backend couldn't provide a list yet.
return out.length > 0 ? out : [year.value]
})
const deckEl = ref(null) const deckEl = ref(null)
const viewportHeight = ref(0) const viewportHeight = ref(0)
@@ -278,25 +391,161 @@ const loadAccounts = async () => {
} }
} }
const reload = async () => { const ensureCardLoaded = async (cardId) => {
const id = Number(cardId)
if (!Number.isFinite(id)) return
const token = reportToken
const cards = report.value?.cards
if (!Array.isArray(cards)) return
const idx = cards.findIndex((x) => Number(x?.id) === id)
if (idx < 0) return
const cur = cards[idx]
if (cur?.status === 'ok' || cur?.status === 'loading') return
// Mark as loading immediately so the UI can show a spinner on this slide.
cards[idx] = {
...(cur || {}),
id,
title: cur?.title || `Card ${id}`,
scope: cur?.scope || 'global',
category: cur?.category || 'A',
kind: cur?.kind || '',
status: 'loading',
error: ''
}
try {
const resp = await api.getWrappedAnnualCard(id, {
year: year.value,
account: account.value || null,
refresh: !!refreshCards.value
})
// Ignore stale responses after year/account reload.
if (token !== reportToken) return
if (resp && Number(resp?.id) === id) {
cards[idx] = resp
} else {
// Best-effort fallback (shouldn't happen unless backend shape changes).
cards[idx] = resp || cards[idx]
}
} catch (e) {
if (token !== reportToken) return
const msg = e?.message || String(e)
cards[idx] = {
...(cur || {}),
id,
title: cur?.title || `Card ${id}`,
scope: cur?.scope || 'global',
category: cur?.category || 'A',
kind: cur?.kind || '',
status: 'error',
narrative: '',
data: null,
error: msg
}
}
}
const retryCard = async (cardId) => {
await ensureCardLoaded(cardId)
}
const reload = async (forceRefresh = false) => {
const token = ++reportToken
activeIndex.value = 0 activeIndex.value = 0
error.value = '' error.value = ''
loading.value = true loading.value = true
refreshCards.value = !!forceRefresh
try { try {
const resp = await api.getWrappedAnnual({ const resp = await api.getWrappedAnnualMeta({
year: year.value, year: year.value,
account: account.value || null, account: account.value || null,
refresh: !!refresh.value refresh: !!forceRefresh
}) })
report.value = resp || null
if (token !== reportToken) return
const manifest = Array.isArray(resp?.cards) ? resp.cards : []
report.value = {
...(resp || {}),
cards: manifest.map((m, i) => ({
id: Number(m?.id ?? i),
title: String(m?.title || `Card ${m?.id ?? i}`),
scope: m?.scope || 'global',
category: m?.category || 'A',
kind: String(m?.kind || ''),
status: 'idle',
narrative: '',
data: null,
error: ''
}))
}
// Backend may snap the year to the latest available year (only years with data are selectable).
const respYear = Number(resp?.year)
if (Number.isFinite(respYear)) {
year.value = respYear
try {
await router.replace({ query: { ...route.query, year: String(respYear) } })
} catch {
// ignore
}
}
availableYears.value = Array.isArray(resp?.availableYears) ? resp.availableYears : []
} catch (e) { } catch (e) {
if (token !== reportToken) return
report.value = null report.value = null
error.value = e?.message || String(e) error.value = e?.message || String(e)
} finally { } finally {
if (token !== reportToken) return
loading.value = false loading.value = false
} }
} }
// Lazy-load the active slide's card data.
watch(activeIndex, (i) => {
const cardIdx = Number(i) - 1
if (!Number.isFinite(cardIdx) || cardIdx < 0) return
const c = report.value?.cards?.[cardIdx]
const id = Number(c?.id)
if (!Number.isFinite(id)) return
void ensureCardLoaded(id)
})
const setYear = async (y) => {
const ny = Number(y)
if (!Number.isFinite(ny)) return
if (ny === year.value) return
// Only allow switching to years that the backend reported as having data.
if (Array.isArray(availableYears.value) && availableYears.value.length > 0 && !availableYears.value.includes(ny)) return
year.value = ny
await reload()
}
onMounted(() => {
try {
const saved = localStorage.getItem('wrapped_retro')
if (saved === '0') retro.value = false
if (saved === '1') retro.value = true
} catch {
// ignore
}
})
watch(retro, (v) => {
try {
localStorage.setItem('wrapped_retro', v ? '1' : '0')
} catch {
// ignore
}
})
onMounted(async () => { onMounted(async () => {
updateViewport() updateViewport()
window.addEventListener('resize', updateViewport) window.addEventListener('resize', updateViewport)
@@ -306,15 +555,11 @@ onMounted(async () => {
deckEl.value?.addEventListener('touchstart', onTouchStart, { passive: true }) deckEl.value?.addEventListener('touchstart', onTouchStart, { passive: true })
deckEl.value?.addEventListener('touchend', onTouchEnd, { passive: true }) deckEl.value?.addEventListener('touchend', onTouchEnd, { passive: true })
try {
await loadAccounts() await loadAccounts()
// Auto-generate once if we already have decrypted accounts, to match "one click" expectations. // Auto-generate once if we already have decrypted accounts, to match "one click" expectations.
if (accounts.value.length > 0) { if (accounts.value.length > 0) {
await reload() await reload()
} }
} finally {
bootstrapped.value = true
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

View File

View File

@@ -0,0 +1,94 @@
Copyright (c) 2021, TakWolf (https://takwolf.com),
with Reserved Font Name "Ark Pixel".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,110 @@
[BoutiqueBitmap7x7]
2020-2022《字言字語》Cen-cyun, Liu. Luke Liu.
https://fontspeech.blogspot.com/
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
此字型是免費的。
無論您是否進行對本字型進行商業或非商業性修改,均可無限制地使用,複製和分發它們。
本字型的衍生品之授權必須與此字型相同,且不作任何擔保。
[MisakiGothic]
Copyright (C) 2002-2019 Num Kadoma
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
これらのフォントはフリー(自由な)ソフトウエアです。
あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
[观致]
这是日文字体Misaki基础上补充的
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,122 @@
[BoutiqueBitmap9x9]
Copyright © 2025 字言字型 版權所有。
Copyright © 2025 fancy type foundry. All rights reserved.
2020-2025《字言字語》Cen-cyun, Liu. Luke Liu.
https://fontspeech.blogspot.com/
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
此字型是免費的。
無論您是否進行對本字型進行商業或非商業性修改,均可無限制地使用,複製和分發它們。
本字型的衍生品之授權必須與此字型相同,且不作任何擔保。
此字体是免费的。
无论您是否进行对本字型进行商业或非商业性修改,均可无限制地使用,复制和分发它们。
本字型的衍生品之授权必须与此字型相同,且不作任何担保。
[M+ BITMAP FONTS]
Copyright (C) 2002-2004 COZ
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute it, with or without modification, either commercially and noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
これらのフォントはフリー(自由な)ソフトウエアです。
あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
[ベストテンBestTen-DOT]
フリーフォントのベストテンは無料でダウンロードできるドットフォント。商用・非商用問わず使用可能なので、安心してダウンロードしてください。
作成した印刷物およびデジタル・コンテンツにつき、その商用・非商用にかかわらず印刷、放送、通信、各種記録メディアなどの媒体の形式も問わず、使用をすることができます。プログラムへの埋め込みが可能です。
このフォントのライセンスは、
M+のライセンスに準じます。
M+ FONT LICENSEについては、配布物に含まれる
mplus_bitmap_fonts をご覧ください。
[Fusion Pixel]
Copyright (c) 2022, TakWolf (https://takwolf.com), with Reserved Font Name 'Fusion Pixel'.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,108 @@
[Cubic 11]
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
此字型是免費的。
無論您是否進行對本字型進行商業或非商業性修改,均可無限制地使用,複製和分發它們。
本字型的衍生品之授權必須與此字型相同,且不作任何擔保。
[JF Dot M+H 12]
Copyright(c) 2005 M+ FONTS PROJECT
[M+ BITMAP FONTS]
Copyright (C) 2002-2004 COZ
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute it, with or without modification, either commercially and noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
これらのフォントはフリー(自由な)ソフトウエアです。
あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,93 @@
Copyright (c) 20192025 Lee Minseo (quiple@quiple.dev)
This font software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,258 @@
===============================================================================
8×8 ドット日本語フォント「美咲フォント」
(2021-05-05 版)
Copyright(C) 2002-2021 Num Kadoma
===============================================================================
-------------------------------------------------------------------------------
概要
-------------------------------------------------------------------------------
 美咲フォントは 8×8 ドットの日本語ビットマップフォントです。
 JIS第一・第二水準をサポートしています。
 一部の記号を除いた全ての文字は 7×7 ドットの範囲に収まっているため、
 文字同士を隣接させても行間・字間が確保できます。
 フォント名は制作のきっかけとなった美咲 礼威氏からいただきました。
-------------------------------------------------------------------------------
バリエーション
-------------------------------------------------------------------------------
・「美咲明朝」は、美咲ゴシックの非漢字部を明朝風の字形に差し替えたものです。
・「美咲ゴシック第2」は美咲ゴシックをベースに、半角文字は縦 7 px、
 全角仮名は横 7 px をいっぱいに使う字形に差し替えたものです。
 各機種用のアーカイブに、差替え済みのフォントファイルを同梱してあります。
-------------------------------------------------------------------------------
ライセンス
-------------------------------------------------------------------------------
 These fonts are free softwares.
 Unlimited permission is granted to use, copy, and distribute it, with or without modification, either commercially and noncommercially.
 THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
 これらのフォントはフリー(自由な)ソフトウエアです。
 あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
-------------------------------------------------------------------------------
制作履歴
-------------------------------------------------------------------------------
■2021-05-05
・文字を追加
 共通: ␣
・字形を修正・変更
 共通: №
 ゴシック第2: プヴ
・(TTF) ビットマップを埋込み
■2019-10-19
・ドキュメントの文字コードを UTF-8N、改行コードを LF に統一
・(TTF, BDF) 記号を追加:
  ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉▔▕╭╮╰╯♠♥♦♣╱╲╳♤♡♢♧
■2019-08-18
・各ファイルのパーミッションから、不要な実行ビットを除去 (フォントの変更はなし)
■2019-06-03
・新フォント「美咲ゴシック第2」を追加。英語名「MisakiGothic2nd」
・「令和」の合字を追加
・各種字形の修正
・(E500) ファイル名のつけ方を変更して配布を再開
・(BDF) 一部仕様を変更して配布を再開
  エンコーディングは ISO 10646-1 のみとし、全ての文字を 1 ファイルに収録
  XLFD 変更
  BITMAP を文字の上下左右端の空白を削った形とし、BBX で位置を指定する形に修正
■2019-02-03a
・制作履歴における、変更した文字の抜け (ゴシックの「と」「ね」「ゐ」) を修正
■2019-02-03
・ゴシック・明朝共通の変更: &,.‘’()〔〕[]{}〈〉【】°′″№0索二
  半角 (「&」字形変更)
  記号類 (主に括弧類の位置調整)
  数字 (ゼロにスラッシュを追加)
  漢字 (「索」「二」の字形変更)
・ゴシックのみの変更: 69CGOQΟОСЭぐぜとどねばぶゐガグゾダバヴ
  数字 (「6」「9」の先端を伸ばす形に変更)
  アルファベット (オーなどの丸み変更)
  仮名 (主に濁点/半濁点つき文字の調整)
・明朝のみの変更: ぐでとどなぶウグケサザソゾダドワヱヴЁ
  仮名 (各種字形変更)
  キリル (「Ё」の字形変更)
■2015-04-10
・誤字になっていた「喪」を修正
・(TTF) ウェイトを「Regular」に変更
  一部の記号を追加、変更
  バージョン番号を「yyyy.mmdd」の形に変更
  その他、パラメータを見直し
  埋込みビットマップ版の同梱を取り止め
■2012-06-03
・正式公開初版
■2012-03-31
・各種ファイル名、ドキュメントの見直し
・BDF 版以外のアーカイブ形式を zip に変更
・(TTF) アウトライン版の正式採用、Mac におそらく対応
・(FONTX) 4X8.FNT の 00h1Fh に DOS/V 罫線を追加
■2011-03-08
・(TTF-Outline) 生成手順の改善に伴い periodβ11 作成・公開
■2010-03-07
・(BDF) FOUNDRY, POINT_SIZE, AVERAGE_WIDTH, SWIDTH, XLFD, 各ファイル名の変更
■2006-10-01
・(TTF-Outline) 試作版配布開始
■2006-04-01
・(PNG) 配布開始
■2006-03-26
・(E500) 13 区追加版・軽量版 (第一水準のみ) 同梱開始
  フォントファイルの命名規則変更
・(BDF) 美咲明朝の配布形態をゴシックとの差分から実ファイル同梱へ
  BITMAP の記述方法を修正 (16 進 4 桁から 2 桁へ)
・(FONTX) 配布再開
■2005-07-20
・アーカイブの命名規則を変更
・(TTF, BDF) 著作権表記を変更
・(BDF) 1 バイト文字フォントのファイル名をそれぞれ 4x8.bdf, 4x8_8859.bdf に
■2004-09-12
・(TTF, BDF) 13 区の文字追加
■2004-06-24
・フォントの名称変更
・(美咲フォントを美咲ゴシック、ファミリーの総称を美咲フォントに)
■2004-05-09
・美咲明朝の同梱開始
■2004-04-19
・半濁点の処理を変更
■2004-04-11
・3 フォント同時編集ツール「"SCRNJPN.FNT" Editor」完成
■2004-02-09
・濁点の処理を変更
■2003-09-21
・制作履歴の日付ミスを修正 (美咲氏 Thanx!)
■2003-06-04
・ライセンスに関する記述を変更
■2003-05-04
・(BDF) 3x7_8859.bdf の CHARS の値を修正
・(BDF) 3x7_8859.bdf のファイル名が 3X7_8859.bdf になっていたのを修正
■2003-03-26
・マニュアルをフォントの説明と各形式の説明に分離
■2003-01-02
・各部のフォント間での統一化作業を本格的に開始
■2002-06-10
・JIS X 0208-1983 での文字の変更、JIS X 0208-1990 での文字の追加に対応
■2002-06-06
・未定義文字のフォントを 3×3 の ■ に
■2002-06-01
・第二水準完成
■2002-05-22
・制作再開
■2002-05-21
・フォント作成専用ツール「'SCRNJPN.FNT' Editor」完成
■2002-05-11
・E650 暴走により第二水準中の 600 文字近くが消滅→制作中断
■2002-04-初
・第一水準完成
■2002-03-中
・制作開始
-------------------------------------------------------------------------------
バージョン情報
-------------------------------------------------------------------------------
■2021-05-05
・美咲フォント 2021-05-05 版 (E500/BDF/TTF/PNG)
■2019-10-19
・美咲フォント 2019-10-19 版 (E500/BDF/TTF/PNG)
■2019-08-18
・美咲フォント 2019-06-03a 版 (E500/BDF/TTF/PNG)
■2019-06-03
・美咲フォント 2019-06-03 版 (E500/BDF/TTF/PNG)
■2019-02-03
・美咲フォント 2019-02-03 版 (TTF/PNG)
■2015-04-10
・美咲フォント 2015-04-10 版 (TTF/PNG)
■2012-06-03
・美咲フォント 2012-06-03 版 (E500/BDF/TTF/Ruputer/FONTX/PNG)
■2012-03-31
・美咲フォント periodβ12 (E500/BDF/TTF/Ruputer/FONTX/PNG)
■2010-03-07
・美咲フォント periodβ11a (BDF)
■2008-06-03
・美咲フォント periodβ11 (E500/BDF/TTF/Ruputer/FONTX/PNG/TTF-Outline)
■2006-03-26
・美咲フォント periodβ10 (E500/BDF/TTF/Ruputer/FONTX/PNG/TTF-Outline)
■2005-07-20
・美咲フォント periodβ9 (E500/BDF/TTF/Ruputer)
■2004-05-09
・美咲フォント periodβ8 (E500/BDF/TTF/Ruputer)
■2003-09-21
・美咲フォント periodβ7 (E500/BDF/TTF/Ruputer)
■2003-06-04
・美咲フォント periodβ6 (E500/BDF/TTF/Ruputer)
■2003-05-04
・美咲フォント periodβ5 (E500/BDF/TTF/Ruputer)
■2003-03-26
・美咲フォント periodβ4 (E500/BDF/TTF/Ruputer/FONTX)
■2002-12-14
・美咲フォント periodβ3 (E500)
■2002-06-18
・美咲フォント periodβ2 (E500)
■2002-06-03
・美咲フォント periodβ1 (E500)
-------------------------------------------------------------------------------
連絡先
-------------------------------------------------------------------------------
 門真 なむ (Num Kadoma)
 ・Twitter: @num_kadoma
 ・Website: http://littlelimit.net/

View File

@@ -0,0 +1,92 @@
Copyright (c) 2023-2024 Mark Li (itmarkibfb@gmail.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,94 @@
Copyright (c) 2022, TakWolf (https://takwolf.com),
with Reserved Font Name "Fusion Pixel".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -6,12 +6,30 @@
* @property {string} title * @property {string} title
* @property {'global'} scope * @property {'global'} scope
* @property {'A'|'B'|'C'|'D'|'E'} category * @property {'A'|'B'|'C'|'D'|'E'} category
* @property {'ok'|'error'} status * @property {'ok'|'error'|'idle'|'loading'} status
* @property {string} kind * @property {string} kind
* @property {string} narrative * @property {string} narrative
* @property {Record<string, any>} data * @property {Record<string, any>} data
*/ */
/**
* @typedef {Object} WrappedCardManifest
* @property {number} id
* @property {string} title
* @property {'global'} scope
* @property {'A'|'B'|'C'|'D'|'E'} category
* @property {string} kind
*/
/**
* @typedef {Object} WrappedAnnualMetaResponse
* @property {string} account
* @property {number} year
* @property {'global'} scope
* @property {number[]|undefined} availableYears
* @property {WrappedCardManifest[]} cards
*/
/** /**
* @typedef {Object} WrappedAnnualResponse * @typedef {Object} WrappedAnnualResponse
* @property {string} account * @property {string} account
@@ -20,8 +38,8 @@
* @property {string|null} username * @property {string|null} username
* @property {number} generated_at * @property {number} generated_at
* @property {boolean} cached * @property {boolean} cached
* @property {number[]|undefined} availableYears
* @property {WrappedCardBase[]} cards * @property {WrappedCardBase[]} cards
*/ */
export {} export {}