1. 增加了几种模板

This commit is contained in:
李琦
2026-08-24 13:11:21 +08:00
parent 287e428e87
commit 0a466a40d3
4 changed files with 264 additions and 44 deletions

View File

@@ -149,3 +149,10 @@
6. glass 升级:背景加第四层光斑;卡片渐变玻璃(linear-gradient 150deg)、顶部弧光(::before radial)、内高光 inset、hover 蓝辉光 0 0 40px。
每套重写后补回 --art-* 覆盖与 .xxx-main.full 规则。magazine Blog 文章卡补 data-index 编号。
验证vue-tsc OK、vite build OK、6 套预览 200。
## 水墨明信片异形卡canvas 随机水墨画)
用户:卡片不要全是方块,以水墨为例做异形 + 随机水墨画边框。
- 新组件 templates/guofeng/components/InkCard.vuecanvas 绘制随机水墨明信片(宣纸底+纸纹、多层墨晕 radial 模糊圆、2-3 条淡墨远山曲线、底部淡墨影toDataURL 作卡片背景明信片异形圆角border-radius 10px 34px 14px 40px、内白边框(.ink-card-frame)、旋转印章年糕、hover 上浮;支持 seed 伪随机(同 seed 稳定,如 Blog 按 p.id
- guofeng Home/Blog 改用 InkCard精选文章/最新作品/栏目卡 + 文章列表 2 列网格hero 保留 + 竖排点缀。
- 其他套上一轮已差异化非方块杂志编号、报纸条目、retro 贴纸、bento 圆角宫格、glass 光弧卡)。
验证vue-tsc OK、vite build OK、guofeng 预览 200。

View File

@@ -0,0 +1,183 @@
<template>
<router-link v-if="to" :to="to" class="ink-card" :style="{ backgroundImage: `url(${bgUrl})` }">
<div class="ink-card-frame"></div>
<div class="ink-card-content">
<slot />
</div>
<span class="ink-card-seal" aria-hidden="true">年糕</span>
</router-link>
<div v-else class="ink-card" :style="{ backgroundImage: `url(${bgUrl})` }">
<div class="ink-card-frame"></div>
<div class="ink-card-content">
<slot />
</div>
<span class="ink-card-seal" aria-hidden="true">年糕</span>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const props = withDefaults(defineProps<{ to?: string; seed?: number }>(), { to: undefined, seed: undefined })
const bgUrl = ref('')
// 伪随机seeded保证同一 seed 每次一致;无 seed 则完全随机
function rnd(seed?: number) {
let s = seed ?? Math.random() * 1e9
return () => {
s = (s * 9301 + 49297) % 233280
return s / 233280
}
}
function drawInkCard(): string {
const w = 440
const h = 320
const dpr = Math.min(window.devicePixelRatio || 1, 2)
const canvas = document.createElement('canvas')
canvas.width = w * dpr
canvas.height = h * dpr
const ctx = canvas.getContext('2d')!
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
const r = rnd(props.seed)
// 宣纸底(带轻微不均匀色)
ctx.fillStyle = '#f7f2e7'
ctx.fillRect(0, 0, w, h)
for (let i = 0; i < 40; i++) {
ctx.fillStyle = `rgba(200,190,160,${0.02 + r() * 0.04})`
ctx.fillRect(r() * w, r() * h, 3 + r() * 10, 1 + r() * 3)
}
// 随机墨晕(多层模糊圆,模拟水墨晕染)
const blobs = 7 + Math.floor(r() * 6)
for (let i = 0; i < blobs; i++) {
const x = r() * w
const y = 30 + r() * h * 0.75
const rad = 24 + r() * 110
const alpha = 0.03 + r() * 0.07
const g = ctx.createRadialGradient(x, y, 0, x, y, rad)
g.addColorStop(0, `rgba(42,46,51,${alpha})`)
g.addColorStop(0.6, `rgba(42,46,51,${alpha * 0.5})`)
g.addColorStop(1, 'rgba(42,46,51,0)')
ctx.fillStyle = g
ctx.fillRect(x - rad, y - rad, rad * 2, rad * 2)
}
// 淡墨远山2-3 条曲线,浓淡不一)
const mounts = 2 + Math.floor(r() * 2)
for (let m = 0; m < mounts; m++) {
ctx.strokeStyle = `rgba(42,46,51,${0.25 + r() * 0.3})`
ctx.lineWidth = 1 + r() * 1.5
ctx.beginPath()
let x = -10
let y = h * (0.55 + r() * 0.2)
ctx.moveTo(x, y)
while (x < w + 10) {
x += 34 + r() * 36
y += (r() - 0.5) * 34
ctx.lineTo(x, y)
}
ctx.stroke()
}
// 底部淡墨影
const shadow = ctx.createLinearGradient(0, h * 0.72, 0, h)
shadow.addColorStop(0, 'rgba(42,46,51,0)')
shadow.addColorStop(1, 'rgba(42,46,51,0.08)')
ctx.fillStyle = shadow
ctx.fillRect(0, h * 0.72, w, h * 0.28)
return canvas.toDataURL('image/png')
}
onMounted(() => {
bgUrl.value = drawInkCard()
})
</script>
<style scoped>
.ink-card {
position: relative;
display: block;
text-decoration: none;
color: inherit;
border-radius: 10px 34px 14px 40px; /* 明信片异形圆角 */
background-size: cover;
background-position: center;
padding: 24px 26px;
min-height: 190px;
box-shadow: 0 10px 34px rgba(42, 46, 51, 0.1);
overflow: hidden;
transition: transform 0.25s, box-shadow 0.25s;
}
.ink-card:hover {
transform: translateY(-4px);
box-shadow: 0 18px 44px rgba(42, 46, 51, 0.16);
}
/* 明信片白边 */
.ink-card-frame {
position: absolute;
inset: 10px;
border: 1px solid rgba(255, 255, 255, 0.55);
border-radius: 4px 26px 8px 32px;
pointer-events: none;
}
.ink-card-content {
position: relative;
z-index: 2;
}
/* 印章 */
.ink-card-seal {
position: absolute;
top: 14px;
right: 16px;
background: rgba(168, 55, 44, 0.78);
color: #f7f2e7;
font-size: 12px;
padding: 4px 7px;
border-radius: 2px;
letter-spacing: 0.1em;
transform: rotate(4deg);
z-index: 3;
}
.ink-cat {
display: block;
font-size: 12px;
letter-spacing: 0.35em;
color: #a8372c;
margin-bottom: 10px;
}
.ink-title {
display: block;
font-size: 19px;
font-weight: 700;
line-height: 1.8;
letter-spacing: 0.08em;
color: #2a2e33;
margin-bottom: 6px;
}
.ink-desc {
display: block;
font-size: 13px;
color: #8d8879;
line-height: 1.9;
letter-spacing: 0.05em;
}
.ink-meta {
display: block;
font-size: 12px;
color: #8d8879;
margin-top: 10px;
letter-spacing: 0.15em;
}
</style>

View File

@@ -10,17 +10,18 @@
</div>
<div v-if="loading" style="padding:40px 0;color:var(--g-muted);">研墨中</div>
<template v-else>
<div class="gf-list">
<router-link v-for="p in posts" :key="p.id" :to="`/blog/${p.id}`" class="gf-card gf-card-link gf-row">
<div class="gf-card-cat">{{ p.categoryName || '未分类' }} · {{ p.date }}</div>
<div class="gf-card-title" style="font-size:20px;">{{ p.title }}</div>
<div class="gf-card-desc">{{ p.excerpt || '(暂无摘要)' }}</div>
</router-link>
<div class="ink-grid">
<InkCard v-for="p in posts" :key="p.id" :to="`/blog/${p.id}`" :seed="p.id">
<span class="ink-cat">{{ p.categoryName || '未分类' }} · {{ p.date }}</span>
<span class="ink-title">{{ p.title }}</span>
<span class="ink-desc">{{ p.excerpt || '(暂无摘要)' }}</span>
</InkCard>
</div>
<div v-if="!posts.length" style="padding:40px 0;color:var(--g-muted);">暂无文章</div>
<div v-if="totalPages > 1" style="display:flex;gap:14px;justify-content:center;margin-top:28px;">
<div v-if="totalPages > 1" class="ink-pager">
<button class="gf-btn" :disabled="page <= 1" @click="load(page - 1)"> 上一页</button>
<span style="align-self:center;color:var(--g-muted);font-size:13px;letter-spacing:0.2em;"> {{ page }} / {{ totalPages }} </span>
<button class="gf-btn" :disabled="page >= totalPages" @click="load(page + 1)">下一页 </button>
@@ -32,6 +33,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { fetchPosts, fetchCategories, type Post } from '../../../services/api'
import InkCard from '../components/InkCard.vue'
const posts = ref<Post[]>([])
const categories = ref<Awaited<ReturnType<typeof fetchCategories>>>([])
@@ -64,10 +66,30 @@ onMounted(async () => {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
margin-bottom: 22px;
}
.gf-row {
margin-bottom: 16px;
.ink-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 22px;
}
.ink-pager {
display: flex;
gap: 14px;
justify-content: center;
margin-top: 30px;
}
.gf-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: 760px) {
.ink-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,41 +1,42 @@
<template>
<div>
<!-- hero留白 + 竖排标题 -->
<section style="display:flex;gap:36px;align-items:stretch;margin-bottom:36px;">
<div style="flex:1;padding:30px 0;">
<!-- hero水墨意境 -->
<section style="display:flex;gap:36px;align-items:stretch;margin-bottom:44px;">
<div style="flex:1;padding:24px 0;">
<div class="gf-card-cat">墨韵 · 首页</div>
<h1 style="font-size:clamp(32px,6vw,52px);font-weight:700;letter-spacing:0.2em;margin:12px 0 16px;">{{ title }}</h1>
<p style="color:var(--g-muted);line-height:2.2;font-size:15px;">{{ subtitle || '以水墨之心,记录设计与技术的思考。' }}</p>
<div style="display:flex;gap:14px;margin-top:24px;">
<h1 class="gf-huge" style="margin:14px 0 18px;">{{ title }}</h1>
<p style="color:var(--g-muted);line-height:2.3;font-size:15px;letter-spacing:0.08em;">{{ subtitle || '以水墨之心,记录设计与技术的思考。' }}</p>
<div style="display:flex;gap:14px;margin-top:28px;">
<router-link to="/blog" class="gf-btn">品读文章</router-link>
<router-link to="/works" class="gf-btn" style="background:var(--g-red);border-color:var(--g-red);">观览作品</router-link>
</div>
</div>
<div class="gf-vertical" style="border-left:1px solid var(--g-line);padding-left:20px;color:var(--g-muted);font-size:14px;display:flex;align-items:center;">
<span>年糕集 · 甲辰年编</span>
<div class="gf-vertical" style="border-left:1px solid var(--g-line);padding-left:22px;color:var(--g-muted);font-size:14px;display:flex;align-items:center;">
<span>年糕集 · 水墨明信片</span>
</div>
</section>
<div class="gf-grid">
<router-link v-if="featuredPost.id" :to="`/blog/${featuredPost.id}`" class="gf-card gf-card-link">
<div class="gf-card-cat">精选文章</div>
<div class="gf-card-title">{{ featuredPost.title }}</div>
<div class="gf-card-desc">{{ featuredPost.excerpt || '(暂无摘要)' }}</div>
<div class="gf-card-meta">{{ featuredPost.categoryName || '未分类' }} · {{ featuredPost.date }}</div>
</router-link>
<!-- 水墨明信片卡片 -->
<div class="ink-grid">
<InkCard v-if="featuredPost.id" :to="`/blog/${featuredPost.id}`">
<span class="ink-cat">精选文章</span>
<span class="ink-title">{{ featuredPost.title }}</span>
<span class="ink-desc">{{ featuredPost.excerpt || '(暂无摘要)' }}</span>
<span class="ink-meta">{{ featuredPost.categoryName || '未分类' }} · {{ featuredPost.date }}</span>
</InkCard>
<router-link v-if="latestWork.id" :to="`/works/${latestWork.id}`" class="gf-card gf-card-link">
<div class="gf-card-cat">最新作品</div>
<div class="gf-card-title">{{ latestWork.title }}</div>
<div class="gf-card-meta">{{ latestWork.category }} · {{ latestWork.year }}</div>
</router-link>
<InkCard v-if="latestWork.id" :to="`/works/${latestWork.id}`">
<span class="ink-cat">最新作品</span>
<span class="ink-title">{{ latestWork.title }}</span>
<span class="ink-desc">{{ latestWork.category }} · {{ latestWork.year }}</span>
</InkCard>
<div class="gf-card">
<div class="gf-card-cat">栏目</div>
<div class="gf-quicks">
<router-link v-for="q in quicks" :key="q.path" :to="q.path" class="gf-quick">{{ q.name }}</router-link>
<InkCard class="ink-wide">
<span class="ink-cat">栏目</span>
<div class="ink-quicks">
<router-link v-for="q in quicks" :key="q.path" :to="q.path" class="ink-quick">{{ q.name }}</router-link>
</div>
</div>
</InkCard>
</div>
</div>
</template>
@@ -44,6 +45,7 @@
import { onMounted, ref } from 'vue'
import { fetchWorks, fetchPosts, type Work, type Post } from '../../../services/api'
import { loadPublicSettings } from '../../../composables/usePublicSettings'
import InkCard from '../components/InkCard.vue'
const title = ref('年糕崽崽.Dev')
const subtitle = ref('')
@@ -72,35 +74,41 @@ onMounted(async () => {
</script>
<style scoped>
.gf-grid {
.ink-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 22px;
}
.gf-quicks {
.ink-wide {
grid-column: 1 / -1;
}
.ink-quicks {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.gf-quick {
.ink-quick {
font-size: 14px;
letter-spacing: 0.2em;
color: var(--g-ink);
color: #2a2e33;
text-decoration: none;
border: 1px solid var(--g-line);
border: 1px solid rgba(42, 46, 51, 0.35);
padding: 8px 18px;
transition: all 0.15s;
background: rgba(255, 255, 255, 0.3);
}
.gf-quick:hover {
border-color: var(--g-red);
color: var(--g-red);
.ink-quick:hover {
border-color: #a8372c;
color: #a8372c;
}
@media (max-width: 760px) {
.gf-grid {
.ink-grid {
grid-template-columns: 1fr;
}
}