1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-07 16:22:34 +08:00

add: MaterialCard component

This commit is contained in:
2025-12-05 22:56:33 +08:00
parent 01e2e5545c
commit 6e5dd124b4
3 changed files with 280 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { ref } from "vue";
interface Props {
category?: string[];
date?: string;
description?: string;
href?: string;
impression?: string[];
tags?: string[];
title?: string;
color?: "elevated" | "filled" | "outlined";
size?: "s" | "m" | "l";
variant?: "feed";
}
const props = withDefaults(defineProps<Props>(), {
color: "filled",
size: "m",
variant: "feed",
impression: () => [],
});
const isSwapped = ref(false);
let touchStartX = 0;
// 是否有多张图片
const hasMultipleImages = () => props.impression && props.impression.length >= 2;
// 处理开始触摸
const handleTouchStart = (e: TouchEvent) => {
if (!hasMultipleImages()) return;
touchStartX = e.touches[0].clientX;
};
// 处理结束触摸
const handleTouchEnd = (e: TouchEvent) => {
if (!hasMultipleImages()) return;
const touchEndX = e.changedTouches[0].clientX;
const diff = touchStartX - touchEndX;
// 滑动距离超过 50px 视为有效
if (Math.abs(diff) > 50) {
if (diff > 0) {
isSwapped.value = true;
} else {
isSwapped.value = false;
}
}
};
</script>
<template>
<component :is="href ? 'a' : 'div'" :href="href" class="MaterialCard" :class="[props.variant, props.size, props.color]">
<div class="content">
<div v-if="(props.impression && props.impression.length > 0) || props.title" class="impression-area">
<div class="title-container">
<div v-if="props.title" class="title">
<h4>{{ props.title }}</h4>
<h6>{{ props.date }}发布</h6>
</div>
</div>
<div
v-if="props.impression && props.impression.length > 0"
class="image-container"
:class="{ swapped: isSwapped }"
@touchstart.passive="handleTouchStart"
@touchend.passive="handleTouchEnd"
>
<img
v-for="(imgUrl, index) in props.impression.slice(0, 2)"
:key="index"
:src="imgUrl"
:alt="props.title + ' cover ' + (index + 1)"
/>
</div>
</div>
<div v-if="props.description || props.title || props.date" class="supporting-area">
<p v-if="props.description">{{ props.description }}</p>
</div>
</div>
</component>
</template>
<style lang="scss" scoped>
@use "sass:meta";
@include meta.load-css("../styles/components/Card");
</style>

View File

@@ -4,6 +4,7 @@ import Layout from "./layouts/Default.vue";
import AppBar from "./components/AppBar.vue";
import Button from "./components/Button.vue";
import Card from "./components/Card.vue";
import Footer from "./components/Footer.vue";
import Header from "./components/Header.vue";
import ImageViewer from "./components/ImageViewer.vue";
@@ -13,6 +14,7 @@ import ScrollToTop from "./components/ScrollToTop.vue";
import NavBar from "./components/NavBar.vue";
import "./styles/main.scss";
const pinia = createPinia();
export default {
@@ -26,6 +28,7 @@ export default {
app.component("ImageViewer", ImageViewer);
app.component("MainLayout", Layout);
app.component("MaterialButton", Button);
app.component("MaterialCard", Card);
app.component("NavBar", NavBar);
app.component("PageIndicator", PageIndicator);
app.component("PrevNext", PrevNext);

View File

@@ -0,0 +1,187 @@
@use "../mixin";
.MaterialCard {
display: flex;
flex-direction: column;
position: relative;
text-decoration: none;
overflow: hidden;
.content {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
width: 100%;
.impression-area {
z-index: 1;
}
.supporting-area {
color: var(--md-sys-color-on-surface);
pointer-events: none;
h3 {
transition: var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}
}
}
&.feed {
.content {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
.impression-area {
display: flex;
gap: 12px;
flex-direction: column;
.title-container {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 12px;
.title {
h4 {
font-variation-settings: "wdth" 500;
text-transform: uppercase;
}
}
}
.image-container {
display: grid;
grid-template-columns: 100%;
gap: 6px;
transition: grid-template-columns var(--md-sys-motion-spring-fast-spatial-duration)
var(--md-sys-motion-spring-fast-spatial);
&:has(img:nth-child(2)) {
grid-template-columns: calc(90% - 6px) 10%;
}
&.swapped,
&:hover {
&:has(img:nth-child(2)) {
grid-template-columns: 10% calc(90% - 6px);
}
}
img {
height: 200px;
width: 100%;
object-fit: cover;
transition: border-radius var(--md-sys-motion-spring-fast-spatial-duration)
var(--md-sys-motion-spring-fast-spatial);
&:nth-child(1) {
border-radius: var(--md-sys-shape-corner-extra-large);
}
&:nth-child(2) {
border-radius: var(--md-sys-shape-corner-medium);
}
}
&.swapped:hover {
img:nth-child(1) {
border-radius: var(--md-sys-shape-corner-medium);
}
img:nth-child(2) {
border-radius: var(--md-sys-shape-corner-extra-large);
}
}
}
}
.supporting-area {
p {
letter-spacing: 0.6rem;
}
}
.actions-area {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
p {
padding: 8px 12px;
border-radius: var(--md-sys-shape-corner-medium);
}
div {
display: flex;
flex-direction: row;
gap: 12px;
}
a {
text-decoration: none;
}
}
}
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 0;
}
&:hover {
&::after {
background-color: var(--md-sys-state-hover-state-layer);
}
}
&:focus-visible {
@include mixin.focus-ring($thickness: 3, $offset: 2);
}
&.elevated {
color: var(--md-sys-color-primary);
background-color: var(--md-sys-color-surface-container-low);
box-shadow: 0px 1px 3px var(--md-sys-color-shadow);
}
&.filled {
color: var(--md-sys-color-on-surface);
border-radius: var(--md-sys-shape-corner-medium);
background-color: var(--md-sys-color-surface-container-highest);
}
&.outlined {
color: var(--md-sys-color-on-surface-variant);
border-color: var(--md-sys-color-outline-variant);
}
}