1. 增加了几种模板
This commit is contained in:
@@ -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.vue:canvas 绘制随机水墨明信片(宣纸底+纸纹、多层墨晕 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。
|
||||
|
||||
183
client/src/templates/guofeng/components/InkCard.vue
Normal file
183
client/src/templates/guofeng/components/InkCard.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user