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:
90
.vitepress/theme/components/Card.vue
Normal file
90
.vitepress/theme/components/Card.vue
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
187
.vitepress/theme/styles/components/Card.scss
Normal file
187
.vitepress/theme/styles/components/Card.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user