Browse Source

feat(component): add texture identity and loading parts

main
Mohan 1 month ago
parent
commit
a89ba7b6c9
  1. 22
      src/components/SongDisclaimer.vue
  2. 105
      src/components/SongLoading.vue
  3. 30
      src/components/SongLogo.vue
  4. 105
      src/components/SongScrim.vue
  5. 82
      src/components/SongSeal.vue

22
src/components/SongDisclaimer.vue

@ -0,0 +1,22 @@
<script setup>
/* SongDisclaimer — 瘦金体版权免责声明 §3.5 */
defineProps({
text: { type: String, default: '' }
})
defineOptions({ name: 'SongDisclaimer' })
</script>
<template>
<p class="song-disclaimer caption">
{{ text || '本页面中的瘦金体风格标识为艺术字图形,参考宋徽宗瘦金体书风自行绘制的 SVG 作品;未被授权的瘦金体字库文件不得用于任何商业用途。如需商业使用宋徽宗瘦金体风格字体,请联系相应字库权利人获取授权。' }}
</p>
</template>
<style scoped>
.song-disclaimer {
margin: var(--song-space-4) 0 0;
color: var(--song-text-disabled);
line-height: 1.7;
opacity: 0.9;
}
</style>

105
src/components/SongLoading.vue

@ -0,0 +1,105 @@
<script setup>
/* SongLoading §7
* 墨点如滴入宣纸般扩散敛去合成层动效transform/opacity only
* mask: 全屏遮罩模式弹层加载 */
defineProps({
visible: { type: Boolean, default: true },
mask: { type: Boolean, default: false },
text: { type: String, default: '墨迹未干' }
})
defineOptions({ name: 'SongLoading' })
</script>
<template>
<div v-if="visible" class="song-loading" :class="{ 'song-loading--mask': mask }">
<div class="song-loading__inner">
<div class="song-loading__inks" aria-hidden="true">
<i class="song-loading__ink" />
<i class="song-loading__ink" />
<i class="song-loading__ink" />
</div>
<div class="song-loading__abs" aria-hidden="true" />
<span v-if="text" class="song-loading__text caption">{{ text }}</span>
</div>
</div>
</template>
<style scoped>
.song-loading {
display: grid;
place-items: center;
padding: 40px 0;
}
.song-loading--mask {
position: fixed;
inset: 0;
z-index: 950;
background: var(--song-bg-elevated);
opacity: 0.96;
}
.song-loading__inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
}
.song-loading__inks {
position: relative;
width: 64px;
height: 64px;
}
.song-loading__ink {
position: absolute;
left: 50%;
top: 50%;
width: 40px;
height: 40px;
border-radius: 50%;
background: radial-gradient(circle, var(--song-primary) 0%, rgba(79, 120, 184, 0) 68%);
opacity: 0;
transform: translate(-50%, -50%) scale(0.3);
animation: song-ink-soak 2.4s var(--song-ease) infinite;
}
.song-loading__ink:nth-child(2) {
animation-delay: 0.8s;
}
.song-loading__ink:nth-child(3) {
animation-delay: 1.6s;
}
.song-loading__abs {
position: absolute;
left: 50%;
top: 50%;
width: 52px;
height: 52px;
margin: -26px 0 0 -26px;
border: 1px solid var(--song-border-strong);
border-radius: 50%;
}
.song-loading__text {
letter-spacing: 0.4em;
color: var(--song-text-secondary);
}
@keyframes song-ink-soak {
0% {
transform: translate(-50%, -50%) scale(0.3);
opacity: 0;
}
35% {
opacity: 0.85;
}
80% {
opacity: 0;
}
100% {
transform: translate(-50%, -50%) scale(1.15);
opacity: 0;
}
}
/* 合成层约束 §7 */
.song-loading__ink {
will-change: transform, opacity;
}
</style>

30
src/components/SongLogo.vue

@ -0,0 +1,30 @@
<script setup>
/* SongLogo SVG
* 契约 §3.5仅作图形装饰禁止作为段落排版字体 */
defineProps({
size: { type: Number, default: 40 }
})
defineOptions({ name: 'SongLogo' })
</script>
<template>
<svg class="song-logo" :width="size" :height="size" viewBox="0 0 48 48" role="img" aria-label="檐下">
<!-- 印章外框细线方印 -->
<rect x="2" y="2" width="44" height="44" fill="none" stroke="currentColor" stroke-width="1.6" />
<!-- 屋檐起翘意象长短撇掠瘦劲屈铁 -->
<path d="M12 16 L36 10 L22 14 L12 16 M14 17 L16 8 M28 18 L31 9" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2.2" />
<!-- 石柱/直棂意象>_ 结构感 -->
<path d="M16 28 L18 40 M32 28 L30 40 M12 30 L36 30 M18 37 L30 37 M21 34 L27 34" fill="none" stroke="currentColor" stroke-width="2" />
<path d="M20 26 L24 22 L28 26" fill="none" stroke="currentColor" stroke-width="1.8" />
</svg>
</template>
<style scoped>
.song-logo {
color: var(--song-primary);
display: block;
}
:root[data-theme='dark'] .song-logo {
color: var(--song-primary);
}
</style>

105
src/components/SongScrim.vue

@ -0,0 +1,105 @@
<script setup>
/* SongScrim §5
* 纹样均以 SVG 几何化矢量低占比置于内容之下
* pattern: ice(冰裂纹) | blind(直棂窗) | tortoise(龟背纹) | vine(缠枝纹)
*/
defineProps({
pattern: { type: String, default: 'ice' },
opacity: { type: Number, default: 0.55 },
box: { type: Boolean, default: false }
})
defineOptions({ name: 'SongScrim' })
const PATTERNS = {
ice: `
<svg xmlns="http://www.w3.org/2000/svg" width="220" height="220" viewBox="0 0 220 220">
<g fill="none" stroke="#6B7A86" stroke-width="1">
<path d="M0 40 40 0 M60 0 0 60 M40 40 90 90 M90 90 110 70 M0 90 90 0"/>
<path d="M120 0 90 30 M0 120 30 90 M110 110 60 160 M160 60 110 110 M90 90 160 160"/>
<path d="M220 40 180 0 M180 180 220 220 M40 220 0 180 M180 180 160 200 M200 160 180 180"/>
</g>
</svg>`,
blind: `
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<g fill="none" stroke="#6B7A86" stroke-width="1">
<path d="M0 0v120 M20 0v120 M40 0v120 M60 0v120 M80 0v120 M100 0v120 M120 0v120"/>
</g>
</svg>`,
tortoise: `
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<g fill="none" stroke="#6B7A86" stroke-width="1">
<path d="M30 8 60 21 30 34Z M60 21 90 8 90 34Z"/>
<path d="M30 34 60 60 30 86Z M60 60 90 34 90 86Z"/>
<path d="M30 8 30 60 0 38Z M90 8 90 60 120 38Z"/>
<path d="M30 86 0 64 0 92Z M90 86 120 64 120 92Z"/>
</g>
</svg>`,
vine: `
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60" viewBox="0 0 120 60">
<g fill="none" stroke="#6B7A86" stroke-width="1">
<path d="M0 30 Q15 10 30 30 T60 30 T90 30 T120 30"/>
<path d="M10 45 q0 -12 0 -24 M30 45 30 15 M50 45 50 15 M70 45 70 15 M90 45 90 15 M110 45 110 15"/>
</g>
</svg>`,
water: `
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<g fill="none" stroke="#6B7A86" stroke-width="1">
<path d="M20 40 q10 -8 20 0 q10 8 20 0 q10 -8 20 0 q10 8 20 0"/>
<path d="M10 64 q10 -8 20 0 q10 8 20 0 q10 -8 20 0 q10 8 20 0 q10 -8 20 0"/>
<path d="M30 88 q10 -8 20 0 q10 8 20 0 q10 -8 20 0"/>
<path d="M5 16 q10 -8 20 0 q10 8 20 0 q10 -8 20 0 q10 8 20 0 q10 -8 20 0 q10 8 20 0"/>
</g>
</svg>`,
cloud: `
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<g fill="none" stroke="#6B7A86" stroke-width="1">
<path d="M10 30 C10 12 30 8 34 24 C38 6 62 10 58 28 C78 18 90 34 76 46 C96 46 96 66 72 62 C82 78 60 88 54 74 C52 92 30 92 28 74 C12 84 2 68 18 60 C0 54 2 38 22 44 C14 34 16 18 30 24 C22 26 14 26 10 30 Z"/>
</g>
</svg>`
}
</script>
<template>
<div class="song-scrim" :class="{ 'song-scrim--box': box }" aria-hidden="true">
<div
v-for="i in 4"
:key="i"
class="song-scrim__tile"
v-html="PATTERNS[pattern]"
:style="{ opacity }"
/>
</div>
</template>
<style scoped>
:global(.song-scrim) {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
opacity: 0.9;
}
.song-scrim--box {
position: absolute !important;
border-radius: var(--song-radius-l);
}
.song-scrim--box :deep(svg) {
filter: opacity(0.55);
}
.song-scrim__tile {
width: 100%;
height: 100%;
mix-blend-mode: multiply;
}
.song-scrim__tile :deep(svg) {
width: 100%;
height: 100%;
}
:global([data-theme='dark']) .song-scrim__tile {
mix-blend-mode: screen;
}
</style>

82
src/components/SongSeal.vue

@ -0,0 +1,82 @@
<script setup>
/* SongSeal SVG
* 方印 4 田字排布圆印单字连珠双圈
* color 默认 --song-seal印泥砂红rotate 模拟手钤微妙倾斜 */
import { computed } from 'vue'
const props = defineProps({
text: { type: String, default: '檐下' },
round: { type: Boolean, default: false },
size: { type: Number, default: 76 },
rotate: { type: Number, default: -2 }
})
defineOptions({ name: 'SongSeal' })
const chars = computed(() => props.text.slice(0, props.round ? 1 : 4).split(''))
const fd = computed(() => {
const n = chars.value.length
return n <= 1 ? 1 : n === 2 ? [0, 1] : [0, 1, 2, 3]
})
</script>
<template>
<svg
class="song-seal"
:width="size"
:height="size"
:viewBox="round ? '0 0 64 64' : '0 0 72 72'"
role="img"
:aria-label="`印章:${text}`"
:style="{ transform: `rotate(${rotate}deg)` }"
>
<!-- 方印田字界格 + 2×2 文字 -->
<g v-if="!round" fill="none" stroke="currentColor">
<rect x="2" y="2" width="68" height="68" rx="3" />
<line v-if="fd.length > 1" x1="36" y1="2" x2="36" y2="70" />
<line v-if="fd.length > 2" x1="2" y1="36" x2="70" y2="36" />
</g>
<!-- 圆印连珠双圈 + 单字 -->
<g v-else fill="none" stroke="currentColor">
<circle cx="32" cy="32" r="28" />
<circle cx="32" cy="32" r="21" />
</g>
<g
v-if="!round"
fill="currentColor"
fill-opacity="0.88"
font-family="var(--song-font-serif)"
font-weight="600"
text-anchor="middle"
dominant-baseline="central"
>
<text v-for="(c, i) in chars" :key="i"
:x="36 + (i % 2 === 0 ? -18 : 18)"
:y="36 + (i < 2 ? -18 : 18)"
opacity="0.72"
>{{ c }}</text>
</g>
<text
v-else
x="32" y="32"
fill="currentColor"
fill-opacity="0.88"
font-family="var(--song-font-serif)"
font-weight="600"
font-size="22"
text-anchor="middle"
dominant-baseline="central"
>{{ chars[0] }}</text>
</svg>
</template>
<style scoped>
.song-seal {
color: var(--song-seal, #a84635);
display: block;
opacity: 0.9;
transition: transform var(--song-dur-2) var(--song-ease);
}
.song-seal:hover {
opacity: 1;
transform: rotate(-1deg) !important;
}
</style>
Loading…
Cancel
Save