mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 14:20:51 +08:00
feat(wrapped-ui): 年度总结页支持懒加载与复古模式,新增概览/字数卡片
- wrapped 页面改为:先拉 meta/年份列表,再按页请求单张卡片,首屏更快 - 新增 Card#0 全局概览页(含图表) - 新增 Card#2 消息字数页(含键盘敲击统计与图表) - 新增复古模式:像素字体资源 + CRT Overlay,支持一键开关 - 调整 shared 组件、types/useApi,更新前端依赖与 lock
This commit is contained in:
@@ -2,6 +2,33 @@
|
||||
@tailwind components;
|
||||
@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 {
|
||||
:root {
|
||||
@@ -10,38 +37,43 @@
|
||||
--wechat-green-hover: #06ad56;
|
||||
--wechat-green-light: #e6f7f0;
|
||||
--wechat-green-dark: #059341;
|
||||
|
||||
|
||||
/* 主色调 */
|
||||
--primary-color: #07c160;
|
||||
--primary-hover: #06ad56;
|
||||
--secondary-color: #4c9e5f;
|
||||
|
||||
|
||||
/* 危险色 */
|
||||
--danger-color: #fa5151;
|
||||
--danger-hover: #e94848;
|
||||
|
||||
|
||||
/* 警告色 */
|
||||
--warning-color: #ffc300;
|
||||
--warning-hover: #e6ad00;
|
||||
|
||||
|
||||
/* 背景色 */
|
||||
--bg-primary: #f7f8fa;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-gray: #ededed;
|
||||
--bg-dark: #191919;
|
||||
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #191919;
|
||||
--text-secondary: #576b95;
|
||||
--text-light: #888888;
|
||||
--text-white: #ffffff;
|
||||
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-color: #e7e7e7;
|
||||
--border-light: #f4f4f4;
|
||||
|
||||
/* 统一消息圆角(聊天所有消息共用) */
|
||||
--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 {
|
||||
@@ -80,6 +112,16 @@
|
||||
/* 统一的消息圆角工具类 */
|
||||
.msg-radius { 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 {
|
||||
@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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
194
frontend/components/wrapped/cards/Card00GlobalOverview.vue
Normal file
194
frontend/components/wrapped/cards/Card00GlobalOverview.vue
Normal 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>
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<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
|
||||
:weekday-labels="card.data?.weekdayLabels"
|
||||
:hour-labels="card.data?.hourLabels"
|
||||
|
||||
42
frontend/components/wrapped/cards/Card02MessageChars.vue
Normal file
42
frontend/components/wrapped/cards/Card02MessageChars.vue
Normal 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>
|
||||
|
||||
24
frontend/components/wrapped/shared/WrappedCRTOverlay.vue
Normal file
24
frontend/components/wrapped/shared/WrappedCRTOverlay.vue
Normal 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>
|
||||
@@ -3,13 +3,12 @@
|
||||
<div class="px-6 py-5 border-b border-[#F3F3F3]">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000066]">
|
||||
CARD {{ String(cardId).padStart(2, '0') }}
|
||||
</div>
|
||||
<h2 class="mt-2 text-xl font-bold text-[#000000e6]">{{ title }}</h2>
|
||||
<p v-if="narrative" class="mt-2 text-sm text-[#7F7F7F]">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
<h2 class="wrapped-title text-xl text-[#000000e6]">{{ title }}</h2>
|
||||
<slot name="narrative">
|
||||
<p v-if="narrative" class="mt-2 wrapped-body text-sm text-[#7F7F7F] whitespace-pre-wrap">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="badge" />
|
||||
</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="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000066]">
|
||||
CARD {{ String(cardId).padStart(2, '0') }}
|
||||
</div>
|
||||
<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 }}
|
||||
</p>
|
||||
<h2 class="wrapped-title text-2xl sm:text-3xl text-[#000000e6]">{{ title }}</h2>
|
||||
<slot name="narrative">
|
||||
<p v-if="narrative" class="mt-3 wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl whitespace-pre-wrap">
|
||||
{{ narrative }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="badge" />
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
<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
|
||||
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"
|
||||
:value="modelAccount"
|
||||
@change="$emit('update:account', $event.target.value || '')"
|
||||
@@ -17,9 +17,9 @@
|
||||
</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
|
||||
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)"
|
||||
@change="$emit('update:year', Number($event.target.value))"
|
||||
>
|
||||
@@ -34,26 +34,26 @@
|
||||
:checked="modelRefresh"
|
||||
@change="$emit('update:refresh', !!$event.target.checked)"
|
||||
/>
|
||||
<span class="text-sm text-[#7F7F7F]">强制刷新(忽略缓存)</span>
|
||||
<span class="wrapped-body text-sm text-[#7F7F7F]">强制刷新(忽略缓存)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<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"
|
||||
@click="$emit('reload')"
|
||||
>
|
||||
<span v-if="!loading">生成报告</span>
|
||||
<span v-else>生成中...</span>
|
||||
<span v-if="!loading">Generate</span>
|
||||
<span v-else>Loading...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="accountsLoading" class="text-xs text-[#7F7F7F]">
|
||||
<div v-if="accountsLoading" class="wrapped-body text-xs text-[#7F7F7F]">
|
||||
{{ showAccount ? '正在加载账号列表...' : '正在检查数据...' }}
|
||||
</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 ? '未发现已解密账号(请先解密数据库)。' : '未发现可用数据(请先解密数据库)。' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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]"
|
||||
></div>
|
||||
|
||||
<!-- Grain/noise: adds texture without changing the base color -->
|
||||
<div class="absolute inset-0 wrapped-noise opacity-[0.06]"></div>
|
||||
<!-- Grain/noise: enhanced with dynamic jitter for CRT feel -->
|
||||
<div class="absolute inset-0 wrapped-noise-enhanced opacity-[0.08]"></div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -11,26 +11,25 @@
|
||||
<template v-if="variant === 'slide'">
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="text-xs font-semibold tracking-[0.28em] text-[#00000080]">
|
||||
WECHAT · WRAPPED
|
||||
<div class="wrapped-label text-xs text-[#00000080]">
|
||||
WECHAT WRAPPED
|
||||
</div>
|
||||
<div class="text-xs font-semibold tracking-[0.22em] text-[#00000055]">
|
||||
<div class="wrapped-body text-xs text-[#00000055]">
|
||||
年度回望
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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]">
|
||||
轻轻翻一翻
|
||||
{{ randomTitle.highlight }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,23 +44,23 @@
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="text-xs font-semibold tracking-[0.28em] text-[#00000080]">
|
||||
WECHAT · WRAPPED
|
||||
<div class="wrapped-label text-xs text-[#00000080]">
|
||||
WECHAT WRAPPED
|
||||
</div>
|
||||
<!-- 年份放到右上角(分享视图不包含账号信息) -->
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p class="text-sm sm:text-base text-[#7F7F7F] max-w-2xl">
|
||||
从时间里回看你的聊天节奏。第一张卡:年度赛博作息表(24H × 7Days)。
|
||||
<p class="wrapped-body text-sm sm:text-base text-[#7F7F7F] max-w-2xl">
|
||||
从时间里回看你的聊天节奏。第一张卡:年度赛博作息表(24H x 7Days)。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -72,6 +71,120 @@
|
||||
</template>
|
||||
|
||||
<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({
|
||||
year: { type: Number, required: true },
|
||||
variant: { type: String, default: 'panel' } // 'panel' | 'slide'
|
||||
|
||||
@@ -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>
|
||||
416
frontend/components/wrapped/visualizations/MessageCharsChart.vue
Normal file
416
frontend/components/wrapped/visualizations/MessageCharsChart.vue
Normal 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>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="text-sm text-[#7F7F7F]">
|
||||
共 <span class="text-[#07C160] font-semibold">{{ totalMessages }}</span> 条消息
|
||||
<div class="wrapped-body text-sm text-[#7F7F7F]">
|
||||
共 <span class="wrapped-number text-[#07C160] font-semibold">{{ totalMessages }}</span> 条消息
|
||||
</div>
|
||||
<div class="text-xs text-[#00000066]">24H × 7Days</div>
|
||||
<div class="wrapped-label text-xs text-[#00000066]">24H x 7Days</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-x-auto" data-wrapped-scroll-x>
|
||||
@@ -15,7 +15,7 @@
|
||||
<span
|
||||
v-for="(s, idx) in timeLabels"
|
||||
:key="idx"
|
||||
class="col-span-4 font-mono"
|
||||
class="col-span-4 wrapped-number"
|
||||
>
|
||||
{{ s }}
|
||||
</span>
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<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 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 }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
v-for="(v, hi) in row"
|
||||
:key="`${wi}-${hi}`"
|
||||
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)"
|
||||
/>
|
||||
</template>
|
||||
@@ -46,13 +46,13 @@
|
||||
|
||||
<div class="mt-4 flex items-center justify-between text-xs text-[#00000066]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>低</span>
|
||||
<span class="wrapped-body">低</span>
|
||||
<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>
|
||||
</div>
|
||||
<span>高</span>
|
||||
<span class="wrapped-body">高</span>
|
||||
</div>
|
||||
<div v-if="maxValue > 0">最大 {{ maxValue }}</div>
|
||||
<div v-if="maxValue > 0" class="wrapped-number">最大 {{ maxValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -102,4 +102,12 @@ const legendColor = (i) => {
|
||||
const t = i / 6
|
||||
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>
|
||||
|
||||
@@ -321,6 +321,28 @@ export const useApi = () => {
|
||||
const url = '/wrapped/annual' + (query.toString() ? `?${query.toString()}` : '')
|
||||
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 {
|
||||
detectWechat,
|
||||
@@ -350,6 +372,8 @@ export const useApi = () => {
|
||||
getChatExport,
|
||||
listChatExports,
|
||||
cancelChatExport,
|
||||
getWrappedAnnual
|
||||
getWrappedAnnual,
|
||||
getWrappedAnnualMeta,
|
||||
getWrappedAnnualCard
|
||||
}
|
||||
}
|
||||
|
||||
342
frontend/package-lock.json
generated
342
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"axios": "^1.11.0",
|
||||
"nuxt": "^4.0.1",
|
||||
"vue": "^3.5.17",
|
||||
@@ -1036,6 +1037,16 @@
|
||||
"@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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -1056,9 +1067,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
@@ -3898,6 +3909,12 @@
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"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": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
@@ -4328,6 +4345,114 @@
|
||||
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
|
||||
"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": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
|
||||
@@ -4916,26 +5041,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/c12": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/c12/-/c12-3.1.0.tgz",
|
||||
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/c12/-/c12-3.3.3.tgz",
|
||||
"integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"chokidar": "^5.0.0",
|
||||
"confbox": "^0.2.2",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.6.1",
|
||||
"exsolve": "^1.0.7",
|
||||
"dotenv": "^17.2.3",
|
||||
"exsolve": "^1.0.8",
|
||||
"giget": "^2.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"jiti": "^2.6.1",
|
||||
"ohash": "^2.0.11",
|
||||
"pathe": "^2.0.3",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^2.2.0",
|
||||
"perfect-debounce": "^2.0.0",
|
||||
"pkg-types": "^2.3.0",
|
||||
"rc9": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"magicast": "^0.3.5"
|
||||
"magicast": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"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": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz",
|
||||
@@ -6583,9 +6754,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz",
|
||||
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
@@ -6673,10 +6844,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -6878,6 +7052,21 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
|
||||
@@ -7260,6 +7449,12 @@
|
||||
"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": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||
@@ -7842,9 +8037,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -7944,9 +8139,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/knitwork": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.2.0.tgz",
|
||||
"integrity": "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.3.0.tgz",
|
||||
"integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/koa": {
|
||||
@@ -8378,12 +8573,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string-ast": {
|
||||
@@ -8618,15 +8813,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.7.4.tgz",
|
||||
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz",
|
||||
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"pathe": "^2.0.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"ufo": "^1.5.4"
|
||||
"acorn": "^8.15.0",
|
||||
"pathe": "^2.0.3",
|
||||
"pkg-types": "^1.3.1",
|
||||
"ufo": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly/node_modules/confbox": {
|
||||
@@ -9739,9 +9934,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
@@ -9749,6 +9944,24 @@
|
||||
"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": {
|
||||
"version": "1.0.37",
|
||||
"resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.37.tgz",
|
||||
@@ -11071,9 +11284,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -11584,6 +11797,22 @@
|
||||
"integrity": "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==",
|
||||
"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": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/stylehacks/-/stylehacks-7.0.6.tgz",
|
||||
@@ -12235,9 +12464,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz",
|
||||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz",
|
||||
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ultrahtml": {
|
||||
@@ -12253,15 +12482,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unctx": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/unctx/-/unctx-2.4.1.tgz",
|
||||
"integrity": "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/unctx/-/unctx-2.5.0.tgz",
|
||||
"integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.15.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17",
|
||||
"unplugin": "^2.1.0"
|
||||
"magic-string": "^0.30.21",
|
||||
"unplugin": "^2.3.11"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
@@ -12367,13 +12596,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.5.tgz",
|
||||
"integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==",
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.11.tgz",
|
||||
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"acorn": "^8.15.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"axios": "^1.11.0",
|
||||
"nuxt": "^4.0.1",
|
||||
"vue": "^3.5.17",
|
||||
|
||||
@@ -1,18 +1,96 @@
|
||||
<template>
|
||||
<!-- PPT 风格:单张卡片占据全页面,鼠标滚轮切换 -->
|
||||
<div
|
||||
ref="deckEl"
|
||||
class="relative h-screen w-full overflow-hidden transition-colors duration-500"
|
||||
:class="{ 'wrapped-retro': retro }"
|
||||
:style="{ backgroundColor: currentBg }"
|
||||
>
|
||||
<!-- PPT 风格:单张卡片占据全页面,鼠标滚轮切换 -->
|
||||
<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="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">
|
||||
{{ year }}年
|
||||
<div class="relative flex justify-end">
|
||||
<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 class="relative mt-1 h-[1px] w-16 ml-auto bg-gradient-to-l from-[#07C160]/40 to-transparent"></div>
|
||||
</div>
|
||||
@@ -26,33 +104,6 @@
|
||||
<section class="w-full" :style="slideStyle">
|
||||
<div class="h-full w-full relative">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
@@ -63,8 +114,55 @@
|
||||
class="w-full"
|
||||
: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
|
||||
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"
|
||||
variant="slide"
|
||||
class="h-full w-full"
|
||||
@@ -96,21 +194,36 @@ useHead({
|
||||
|
||||
const api = useApi()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const year = ref(Number(route.query?.year) || new Date().getFullYear())
|
||||
// 分享视图不展示账号信息:默认让后端自动选择;需要指定时可用 query ?account=wxid_xxx
|
||||
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 accountsLoading = ref(true)
|
||||
|
||||
// Avoid flashing the "year card" controls before the initial auto-load finishes.
|
||||
const bootstrapped = ref(false)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const report = ref(null)
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const report = ref(null)
|
||||
// If user clicks "强制刷新", pass refresh=true for subsequent per-card requests in this session.
|
||||
const refreshCards = ref(false)
|
||||
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 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
|
||||
error.value = ''
|
||||
loading.value = true
|
||||
refreshCards.value = !!forceRefresh
|
||||
try {
|
||||
const resp = await api.getWrappedAnnual({
|
||||
const resp = await api.getWrappedAnnualMeta({
|
||||
year: year.value,
|
||||
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) {
|
||||
if (token !== reportToken) return
|
||||
report.value = null
|
||||
error.value = e?.message || String(e)
|
||||
} finally {
|
||||
if (token !== reportToken) return
|
||||
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 () => {
|
||||
updateViewport()
|
||||
window.addEventListener('resize', updateViewport)
|
||||
@@ -306,14 +555,10 @@ onMounted(async () => {
|
||||
deckEl.value?.addEventListener('touchstart', onTouchStart, { passive: true })
|
||||
deckEl.value?.addEventListener('touchend', onTouchEnd, { passive: true })
|
||||
|
||||
try {
|
||||
await loadAccounts()
|
||||
// Auto-generate once if we already have decrypted accounts, to match "one click" expectations.
|
||||
if (accounts.value.length > 0) {
|
||||
await reload()
|
||||
}
|
||||
} finally {
|
||||
bootstrapped.value = true
|
||||
await loadAccounts()
|
||||
// Auto-generate once if we already have decrypted accounts, to match "one click" expectations.
|
||||
if (accounts.value.length > 0) {
|
||||
await reload()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
BIN
frontend/public/assets/images/wechat-audio-dark.png
Normal file
BIN
frontend/public/assets/images/wechat-audio-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 484 B |
0
frontend/public/fonts/.gitkeep
Normal file
0
frontend/public/fonts/.gitkeep
Normal file
94
frontend/public/fonts/LICENSE/ark-pixel.txt
Normal file
94
frontend/public/fonts/LICENSE/ark-pixel.txt
Normal 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.
|
||||
110
frontend/public/fonts/LICENSE/boutique-bitmap-7x7.txt
Normal file
110
frontend/public/fonts/LICENSE/boutique-bitmap-7x7.txt
Normal 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.
|
||||
122
frontend/public/fonts/LICENSE/boutique-bitmap-9x9.txt
Normal file
122
frontend/public/fonts/LICENSE/boutique-bitmap-9x9.txt
Normal 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.
|
||||
108
frontend/public/fonts/LICENSE/cubic-11.txt
Normal file
108
frontend/public/fonts/LICENSE/cubic-11.txt
Normal 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.
|
||||
93
frontend/public/fonts/LICENSE/galmuri.txt
Normal file
93
frontend/public/fonts/LICENSE/galmuri.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright (c) 2019–2025 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.
|
||||
258
frontend/public/fonts/LICENSE/misaki.txt
Normal file
258
frontend/public/fonts/LICENSE/misaki.txt
Normal 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 の 00h~1Fh に 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/
|
||||
92
frontend/public/fonts/LICENSE/miseki-bitmap.txt
Normal file
92
frontend/public/fonts/LICENSE/miseki-bitmap.txt
Normal 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.
|
||||
94
frontend/public/fonts/OFL.txt
Normal file
94
frontend/public/fonts/OFL.txt
Normal 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.
|
||||
BIN
frontend/public/fonts/fusion-pixel-10px-monospaced-ja.otf.woff2
Normal file
BIN
frontend/public/fonts/fusion-pixel-10px-monospaced-ja.otf.woff2
Normal file
Binary file not shown.
BIN
frontend/public/fonts/fusion-pixel-10px-monospaced-ko.otf.woff2
Normal file
BIN
frontend/public/fonts/fusion-pixel-10px-monospaced-ko.otf.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/public/fonts/fusion-pixel-12px-monospaced-ja.otf.woff2
Normal file
BIN
frontend/public/fonts/fusion-pixel-12px-monospaced-ja.otf.woff2
Normal file
Binary file not shown.
BIN
frontend/public/fonts/fusion-pixel-12px-monospaced-ko.otf.woff2
Normal file
BIN
frontend/public/fonts/fusion-pixel-12px-monospaced-ko.otf.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/public/fonts/fusion-pixel-8px-monospaced-ja.otf.woff2
Normal file
BIN
frontend/public/fonts/fusion-pixel-8px-monospaced-ja.otf.woff2
Normal file
Binary file not shown.
BIN
frontend/public/fonts/fusion-pixel-8px-monospaced-ko.otf.woff2
Normal file
BIN
frontend/public/fonts/fusion-pixel-8px-monospaced-ko.otf.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,12 +6,30 @@
|
||||
* @property {string} title
|
||||
* @property {'global'} scope
|
||||
* @property {'A'|'B'|'C'|'D'|'E'} category
|
||||
* @property {'ok'|'error'} status
|
||||
* @property {'ok'|'error'|'idle'|'loading'} status
|
||||
* @property {string} kind
|
||||
* @property {string} narrative
|
||||
* @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
|
||||
* @property {string} account
|
||||
@@ -20,8 +38,8 @@
|
||||
* @property {string|null} username
|
||||
* @property {number} generated_at
|
||||
* @property {boolean} cached
|
||||
* @property {number[]|undefined} availableYears
|
||||
* @property {WrappedCardBase[]} cards
|
||||
*/
|
||||
|
||||
export {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user