feat: update AigcPhotoCard, FaqHelpCard, MapNavigationCard, NoticeCard, ScenicImageCard with new layouts, styles, and mock data; enhance user interaction and visual appeal

This commit is contained in:
DEV_DSW
2026-05-13 16:28:57 +08:00
parent 9da046c314
commit 0afb690262
15 changed files with 514 additions and 266 deletions

View File

@@ -1,31 +1,27 @@
<template>
<CardShell class="aigc-photo-card p-16" variant="soft">
<view class="aigc-photo-card__body flex flex-items-center gap-12">
<view class="aigc-photo-card__icon flex flex-items-center flex-justify-center w-44 h-44 rounded-16 bg-CCFBF1 font-size-22 font-900">{{ data.icon }}</view>
<view class="aigc-photo-card__content flex-full">
<view class="aigc-photo-card__title font-size-16 font-900">{{ data.title }}</view>
<view class="aigc-photo-card__subtitle color-0F766E font-size-11 font-700">{{ data.subtitle }}</view>
</view>
</view>
<view class="aigc-photo-card__preview grid grid-cols-3 gap-8 mt-14">
<MediaFrame
v-for="(image, index) in images"
:key="image || index"
class="aigc-photo-card__image h-82 rounded-14"
:src="image"
<view
class="aigc-photo-card relative rounded-24 overflow-hidden w-full"
:class="{ 'is-disabled': disabled }"
@click="handleAction"
>
<image
class="aigc-photo-card__image block w-full"
:src="data.cover"
mode="aspectFill"
/>
<view class="aigc-photo-card__shade"></view>
<view class="aigc-photo-card__play flex flex-items-center flex-justify-center rounded-full">
<view class="aigc-photo-card__triangle"></view>
</view>
<view class="aigc-photo-card__title color-white font-size-16 font-900">
{{ data.title }}
</view>
<view class="aigc-photo-card__button mt-14 p-12 rounded-16 color-white bg-0F766E font-size-13 font-900 text-center" :class="{ 'is-disabled': disabled }" @click="handleAction">
{{ data.buttonText }}
</view>
</CardShell>
</template>
<script setup>
import { computed } from "vue";
import CardShell from "../SharedVisual/CardShell.vue";
import MediaFrame from "../SharedVisual/MediaFrame.vue";
const props = defineProps({
data: {
type: Object,
@@ -39,8 +35,6 @@ const props = defineProps({
const emit = defineEmits(["select", "action"]);
const images = computed(() => props.data.images || []);
const handleAction = () => {
if (props.disabled) return;
emit("select", props.data);

View File

@@ -1,11 +1,8 @@
export default {
icon: "AI",
title: "生成一张景区合照",
subtitle: "上传照片后生成同框纪念照",
buttonText: "开始生成",
images: [
"https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=400&q=80",
"https://images.unsplash.com/photo-1529156069898-49953e39b3ac?auto=format&fit=crop&w=400&q=80",
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=400&q=80",
],
id: "xiaoqikong-flying",
title: "沉浸式飞越 · 小七孔",
cover: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1000&q=80",
action: {
type: "play",
},
};

View File

@@ -1,35 +1,54 @@
.aigc-photo-card {
background: linear-gradient(145deg, #ffffff 0%, #f0fdfa 100%);
height: 272px;
background: #0f172a;
}
.aigc-photo-card__body {
.aigc-photo-card:active {
opacity: 0.86;
}
.aigc-photo-card__icon {
color: #0f766e;
}
.aigc-photo-card__content {
min-width: 0;
}
.aigc-photo-card__title {
color: #134e4a;
}
.aigc-photo-card__subtitle {
margin-top: 5px;
}
.aigc-photo-card__preview {
.aigc-photo-card.is-disabled {
opacity: 0.55;
}
.aigc-photo-card__image {
height: 100%;
}
.aigc-photo-card__button {
.aigc-photo-card__shade {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.38);
}
.aigc-photo-card__button.is-disabled {
opacity: 0.55;
.aigc-photo-card__play {
position: absolute;
left: 50%;
top: 50%;
width: 76px;
height: 76px;
border: 1px solid rgba(255, 255, 255, 0.48);
background: rgba(255, 255, 255, 0.28);
transform: translate(-50%, -50%);
box-sizing: border-box;
}
.aigc-photo-card__triangle {
width: 0;
height: 0;
margin-left: 6px;
border-top: 17px solid transparent;
border-bottom: 17px solid transparent;
border-left: 23px solid #fff;
}
.aigc-photo-card__title {
position: absolute;
left: 0;
right: 0;
bottom: 72px;
text-align: center;
line-height: 22px;
letter-spacing: 0;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.32);
}

View File

@@ -1,36 +1,35 @@
<template>
<CardShell class="faq-help-card p-16" variant="soft">
<view class="faq-help-card__header flex flex-justify-between gap-12">
<view>
<view class="faq-help-card__title color-1E293B font-size-16 font-900">{{ data.title }}</view>
<view class="faq-help-card__subtitle color-94A3B8 font-size-11 font-700">{{ data.subtitle }}</view>
<view class="faq-help-card bg-white rounded-24 w-full">
<view class="faq-help-card__header flex flex-items-center">
<view class="faq-help-card__icon flex flex-items-center flex-justify-center rounded-full font-size-16 font-900">
{{ data.icon }}
</view>
<BadgePill v-if="data.badge" :label="data.badge" tone="blue" />
<view class="faq-help-card__title color-1E293B font-size-20 font-900">
{{ data.title }}
</view>
<view class="faq-help-card__list flex flex-col gap-8">
</view>
<view class="faq-help-card__divider"></view>
<view class="faq-help-card__list">
<view
v-for="item in questions"
:key="item.id || item.question"
class="faq-help-card__item p-12 rounded-16 bg-F8FAFC"
:class="{ 'is-open': openedId === item.id }"
@click="toggle(item)"
:key="item.id || item.text"
class="faq-help-card__item flex flex-items-center"
:class="{ 'is-disabled': disabled }"
@click="handleSelect(item)"
>
<view class="faq-help-card__question flex flex-justify-between gap-12 color-1E293B font-size-13 font-900">
<text>{{ item.question }}</text>
<text class="faq-help-card__arrow color-2563EB">{{ openedId === item.id ? "" : "+" }}</text>
<view class="faq-help-card__question color-475569 font-size-18 font-900 ellipsis-1 flex-full">
{{ item.text }}
</view>
<view v-if="openedId === item.id" class="faq-help-card__answer color-64748B font-size-12 font-700">
{{ item.answer }}
<view class="faq-help-card__arrow color-CBD5E1 font-size-22 font-900 flex-shrink-0"></view>
</view>
</view>
</view>
</CardShell>
</template>
<script setup>
import { computed, ref } from "vue";
import CardShell from "../SharedVisual/CardShell.vue";
import BadgePill from "../SharedVisual/BadgePill.vue";
import { computed } from "vue";
const props = defineProps({
data: {
@@ -45,12 +44,10 @@ const props = defineProps({
const emit = defineEmits(["select"]);
const openedId = ref("");
const questions = computed(() => props.data.questions || []);
const toggle = (item) => {
const handleSelect = (item) => {
if (props.disabled) return;
openedId.value = openedId.value === item.id ? "" : item.id;
emit("select", item);
};
</script>

View File

@@ -1,10 +1,19 @@
export default {
title: "常见问题",
subtitle: "快速了解游览和服务信息",
badge: "FAQ",
id: "faq-help",
icon: "?",
title: "您可能还想了解",
questions: [
{ id: "q1", question: "景区几点停止入园?", answer: "通常闭园前 1 小时停止入园,节假日以现场公告为准。" },
{ id: "q2", question: "可以携带宠物吗?", answer: "多数室内区域不支持宠物进入,导盲犬等服务犬除外。" },
{ id: "q3", question: "下雨还能游玩吗?", answer: "小雨可游览,雨后水景更好,但请注意栈道湿滑。" },
{
id: "shuttle-time",
text: "观光车末班车发车时间",
},
{
id: "power-bank",
text: "景区内哪里有共享充电宝",
},
{
id: "pet-policy",
text: "可以带宠物入园吗",
},
],
};

View File

@@ -1,36 +1,54 @@
.faq-help-card {
padding: 34px 30px 26px;
border: 1px solid rgba(226, 232, 240, 0.72);
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.faq-help-card__header {
margin-bottom: 12px;
min-width: 0;
}
.faq-help-card__icon {
width: 22px;
height: 22px;
margin-right: 10px;
border: 2px solid #10b981;
color: #10b981;
line-height: 18px;
box-sizing: border-box;
}
.faq-help-card__title {
line-height: 26px;
}
.faq-help-card__subtitle {
margin-top: 4px;
}
.faq-help-card__list {
.faq-help-card__divider {
height: 1px;
margin: 24px 0 18px;
background: #f1f5f9;
}
.faq-help-card__item {
min-height: 42px;
}
.faq-help-card__item.is-open {
background: #eff6ff;
.faq-help-card__item:active {
opacity: 0.86;
}
.faq-help-card__item.is-disabled {
opacity: 0.55;
}
.faq-help-card__question {
min-width: 0;
line-height: 24px;
}
.faq-help-card__arrow {
font-size: 18px;
line-height: 14px;
}
.faq-help-card__answer {
margin-top: 8px;
line-height: 20px;
width: 22px;
margin-left: 16px;
text-align: right;
line-height: 24px;
}

View File

@@ -1,24 +1,40 @@
<template>
<CardShell class="map-navigation-card" variant="soft">
<view class="map-navigation-card__map">
<MediaFrame class="map-navigation-card__image h-178" :src="data.mapImage" />
<view class="map-navigation-card__pin color-334155 font-size-10 font-900">{{ data.pinText }}</view>
<view class="map-navigation-card bg-white rounded-24 overflow-hidden w-full">
<view class="map-navigation-card__media">
<image
class="map-navigation-card__image block w-full"
:src="data.image"
mode="aspectFill"
/>
<view class="map-navigation-card__badge flex flex-items-center color-white font-size-12 font-900">
<text class="map-navigation-card__badge-icon">{{ badge.icon }}</text>
<text>{{ badge.text }}</text>
</view>
<view class="map-navigation-card__bar flex flex-items-center gap-12">
</view>
<view class="map-navigation-card__content flex flex-items-center">
<view class="map-navigation-card__info flex-full">
<view class="map-navigation-card__title color-1E293B font-size-14 font-900 ellipsis-1">{{ data.title }}</view>
<view class="map-navigation-card__distance color-94A3B8 font-size-11 font-800 ellipsis-1">{{ data.distance }}</view>
<view class="map-navigation-card__title color-1E293B font-size-18 font-900 ellipsis-1">
{{ data.title }}
</view>
<view class="map-navigation-card__distance color-94A3B8 font-size-14 font-900 ellipsis-1">
{{ data.distance }}
</view>
</view>
<view
class="map-navigation-card__button flex flex-items-center flex-justify-center bg-0F172A color-white font-size-18 font-900"
:class="{ 'is-disabled': disabled }"
@click="handleAction"
>
<text class="map-navigation-card__button-icon">{{ action.icon }}</text>
<text>{{ action.text }}</text>
</view>
<view class="map-navigation-card__button rounded-50 color-white bg-0F172A font-size-12 font-900" :class="{ 'is-disabled': disabled }" @click="handleAction">
{{ data.buttonText }}
</view>
</view>
</CardShell>
</template>
<script setup>
import CardShell from "../SharedVisual/CardShell.vue";
import MediaFrame from "../SharedVisual/MediaFrame.vue";
import { computed } from "vue";
const props = defineProps({
data: {
@@ -33,6 +49,9 @@ const props = defineProps({
const emit = defineEmits(["select", "action"]);
const badge = computed(() => props.data.badge || {});
const action = computed(() => props.data.action || {});
const handleAction = () => {
if (props.disabled) return;
emit("select", props.data);

View File

@@ -1,7 +1,14 @@
export default {
mapImage: "https://images.unsplash.com/photo-1524661135-423995f22d0b?auto=format&fit=crop&w=900&q=80",
pinText: "目的地",
title: "前往翠谷瀑布",
distance: "距你 500m · 步行约 8 分钟",
buttonText: "导航",
id: "xiaoqikong-bridge-nav",
title: "前往小七孔桥",
image: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=1000&q=80",
distance: "步行约 450 米",
badge: {
icon: "⌖",
text: "目的地",
},
action: {
icon: "↗",
text: "导航",
},
};

View File

@@ -1,22 +1,38 @@
.map-navigation-card__map {
.map-navigation-card {
border: 1px solid rgba(226, 232, 240, 0.72);
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.map-navigation-card__media {
position: relative;
padding: 8px;
}
.map-navigation-card__image {
height: 248px;
}
.map-navigation-card__pin {
.map-navigation-card__badge {
position: absolute;
top: 18px;
left: 18px;
padding: 5px 9px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
left: 16px;
bottom: 16px;
height: 34px;
padding: 0 12px;
border-radius: 8px;
background: rgba(15, 23, 42, 0.72);
line-height: 34px;
}
.map-navigation-card__bar {
padding: 0 14px 14px;
.map-navigation-card__badge-icon {
margin-right: 6px;
font-size: 15px;
line-height: 15px;
}
.map-navigation-card__content {
min-height: 96px;
padding: 20px 26px 22px;
box-sizing: border-box;
}
.map-navigation-card__info {
@@ -24,16 +40,31 @@
}
.map-navigation-card__title {
line-height: 24px;
}
.map-navigation-card__distance {
margin-top: 4px;
margin-top: 6px;
line-height: 18px;
}
.map-navigation-card__button {
padding: 9px 14px;
width: 108px;
height: 58px;
margin-left: 18px;
border-radius: 16px;
}
.map-navigation-card__button:active {
opacity: 0.86;
}
.map-navigation-card__button.is-disabled {
opacity: 0.55;
}
.map-navigation-card__button-icon {
margin-right: 8px;
font-size: 19px;
line-height: 19px;
}

View File

@@ -1,56 +1,74 @@
<template>
<view class="notice-card">
<CardShell v-if="!activeNotice" class="notice-card__list p-16" variant="soft">
<view class="notice-card__header flex flex-justify-between gap-12">
<view>
<view class="notice-card__title color-1E293B font-size-16 font-900">{{ data.title }}</view>
<view class="notice-card__subtitle color-94A3B8 font-size-11 font-700">{{ data.subtitle }}</view>
<view class="notice-card w-full">
<view v-if="!detailOpen" class="notice-card__summary bg-FFFBEB rounded-24 w-full">
<view class="notice-card__summary-title color-B45309 font-size-16 font-900">
{{ summary.title }}
</view>
<BadgePill v-if="data.badge" :label="data.badge" tone="rose" />
<view class="notice-card__summary-content color-D97706 font-size-14 font-900">
{{ summary.content }}
</view>
<view class="notice-card__summary-footer flex flex-items-center flex-justify-between">
<view class="notice-card__summary-time color-D97706 font-size-12 font-900">
{{ summary.publishTime }}
</view>
<view
v-for="item in notices"
:key="item.id || item.title"
class="notice-card__item flex gap-12 border-bottom-F1F5F9"
class="notice-card__summary-link color-B45309 font-size-14 font-900"
:class="{ 'is-disabled': disabled }"
@click="openNotice(item)"
@click="openDetail"
>
<view class="notice-card__item-body flex-full">
<view class="notice-card__item-title color-1E293B font-size-13 font-900 ellipsis-1">{{ item.title }}</view>
<view class="notice-card__item-desc color-94A3B8 font-size-11 font-700 ellipsis-2">{{ item.summary }}</view>
{{ summary.actionText }}
</view>
</view>
<view class="notice-card__item-date color-E11D48 font-size-10 font-900">{{ item.date }}</view>
</view>
</CardShell>
<DetailShell
v-else
:title="activeNotice.title"
:tag="activeNotice.type"
tone="rose"
@back="closeNotice"
>
<view class="notice-card__detail p-16">
<MediaFrame v-if="activeNotice.cover" class="notice-card__cover h-154" :src="activeNotice.cover" />
<view class="notice-card__meta color-94A3B8 font-size-11 font-800">{{ activeNotice.date }}</view>
<view v-else class="notice-card__detail bg-white rounded-24 overflow-hidden w-full">
<view class="notice-card__detail-head flex flex-items-center">
<view
v-for="(paragraph, index) in paragraphs"
:key="index"
class="notice-card__paragraph color-475569 font-size-13 font-700"
class="notice-card__back flex flex-items-center flex-justify-center rounded-full flex-shrink-0"
@click="closeDetail"
>
{{ paragraph }}
</view>
<view class="notice-card__head-title color-1E293B font-size-18 font-900">
{{ detail.navTitle }}
</view>
</view>
<image
class="notice-card__image block w-full"
:src="detail.image"
mode="aspectFill"
/>
<view class="notice-card__detail-body">
<view class="notice-card__detail-title color-1E293B font-size-20 font-900">
{{ detail.title }}
</view>
<view class="notice-card__time-pill flex flex-items-center bg-F8FAFC color-475569 font-size-13 font-900">
<text class="notice-card__time-icon">{{ detail.timeIcon }}</text>
<text>{{ detail.publishTime }}</text>
</view>
<view class="notice-card__divider"></view>
<view
v-for="paragraph in paragraphs"
:key="paragraph.id"
class="notice-card__paragraph color-475569 font-size-14 font-800"
>
<text
v-for="segment in paragraph.segments"
:key="segment.text"
:class="{ 'notice-card__strong': segment.strong }"
>
{{ segment.text }}
</text>
</view>
</view>
</view>
</DetailShell>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import CardShell from "../SharedVisual/CardShell.vue";
import DetailShell from "../SharedVisual/DetailShell.vue";
import BadgePill from "../SharedVisual/BadgePill.vue";
import MediaFrame from "../SharedVisual/MediaFrame.vue";
const props = defineProps({
data: {
@@ -63,20 +81,22 @@ const props = defineProps({
},
});
const emit = defineEmits(["select", "back"]);
const emit = defineEmits(["select", "back", "action"]);
const activeNotice = ref(null);
const notices = computed(() => props.data.notices || []);
const paragraphs = computed(() => activeNotice.value?.paragraphs || []);
const detailOpen = ref(false);
const summary = computed(() => props.data.summary || {});
const detail = computed(() => props.data.detail || {});
const paragraphs = computed(() => detail.value.paragraphs || []);
const openNotice = (item) => {
const openDetail = () => {
if (props.disabled) return;
activeNotice.value = item;
emit("select", item);
detailOpen.value = true;
emit("select", props.data);
emit("action", props.data);
};
const closeNotice = () => {
activeNotice.value = null;
const closeDetail = () => {
detailOpen.value = false;
emit("back");
};
</script>

View File

@@ -1,24 +1,49 @@
export default {
title: "景区公告",
subtitle: "重要通知与运营提醒",
badge: "公告",
notices: [
id: "wolongtan-trail-notice",
summary: {
title: "卧龙潭",
content: "卧龙潭排队严重。",
publishTime: "发布时间2025年7月12日 09:30",
actionText: "详情",
},
detail: {
navTitle: "交通公告",
image: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=1000&q=80",
title: "卧龙潭至鸳鸯湖步道临时封闭通知",
timeIcon: "◷",
publishTime: "发布时间2025年7月12日 09:30",
paragraphs: [
{
id: "notice-1",
title: "观光车运营时间调整",
summary: "因道路维护,今日 16:30 后部分站点临时调整。",
date: "今日",
type: "运营",
cover: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=900&q=80",
paragraphs: ["今日 16:30 后,水上森林站点将临时调整至游客中心东侧。", "已购票游客可凭原票乘坐接驳车,请根据现场指引有序排队。"],
id: "rain",
segments: [
{
text: "因近日持续降雨导致景区内水位明显上升,为保障游客人身安全,景区管理部门决定自即日起临时封闭",
},
{
id: "notice-2",
title: "雨后栈道防滑提醒",
summary: "部分亲水路段地面湿滑,请穿防滑鞋并照看儿童。",
date: "昨天",
type: "安全",
paragraphs: ["雨后栈道区域湿滑,请勿奔跑或翻越护栏。", "如遇临时封闭,请服从现场工作人员安排。"],
text: "卧龙潭至鸳鸯湖沉溪步道",
strong: true,
},
{
text: ",封闭时间视水位情况另行通知。",
},
],
},
{
id: "route",
segments: [
{
text: "受影响游客可改走景区主干道绕行全程增加约15分钟。观光车服务正常运营建议优先乘车前往鸳鸯湖区域。",
},
],
},
{
id: "reopen",
segments: [
{
text: "如水位下降恢复安全标准,景区将第一时间通过广播及公告重新开放步道,请游客留意景区实时公告。",
},
],
},
],
},
};

View File

@@ -2,59 +2,122 @@
width: 100%;
}
.notice-card__list {
.notice-card__summary,
.notice-card__detail {
box-sizing: border-box;
}
.notice-card__header {
margin-bottom: 12px;
.notice-card__summary {
padding: 16px 20px 14px;
border: 1px solid #fde68a;
box-shadow: 0 8px 22px rgba(245, 158, 11, 0.08);
}
.notice-card__title {
.notice-card__summary-title {
line-height: 22px;
}
.notice-card__subtitle {
margin-top: 4px;
.notice-card__summary-content {
margin-top: 6px;
line-height: 20px;
}
.notice-card__item {
padding: 13px 0;
.notice-card__summary-footer {
min-width: 0;
margin-top: 8px;
}
.notice-card__item:last-child {
border-bottom: none;
.notice-card__summary-time {
min-width: 0;
line-height: 18px;
}
.notice-card__item.is-disabled {
.notice-card__summary-link {
margin-left: 16px;
line-height: 18px;
text-decoration: underline;
white-space: nowrap;
}
.notice-card__summary-link:active {
opacity: 0.82;
}
.notice-card__summary-link.is-disabled {
opacity: 0.55;
}
.notice-card__item-body {
min-width: 0;
}
.notice-card__item-title {
}
.notice-card__item-desc {
margin-top: 5px;
line-height: 17px;
}
.notice-card__item-date {
}
.notice-card__detail {
border: 1px solid rgba(226, 232, 240, 0.72);
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05);
}
.notice-card__cover {
margin-bottom: 14px;
.notice-card__detail-head {
height: 68px;
padding: 0 20px;
box-sizing: border-box;
}
.notice-card__meta {
margin-bottom: 12px;
.notice-card__back {
width: 34px;
height: 34px;
margin-right: 14px;
color: #64748b;
font-size: 28px;
line-height: 1;
background: #f8fafc;
}
.notice-card__head-title {
line-height: 24px;
}
.notice-card__image {
height: 250px;
}
.notice-card__detail-body {
padding: 22px 20px 28px;
box-sizing: border-box;
}
.notice-card__detail-title {
line-height: 28px;
}
.notice-card__time-pill {
display: inline-flex;
height: 34px;
margin-top: 16px;
padding: 0 12px;
border: 1px solid #e2e8f0;
border-radius: 999px;
line-height: 34px;
box-sizing: border-box;
}
.notice-card__time-icon {
margin-right: 6px;
color: #64748b;
font-size: 15px;
line-height: 15px;
}
.notice-card__divider {
height: 1px;
margin: 26px 0 20px;
background: #e5e7eb;
}
.notice-card__paragraph {
margin-bottom: 12px;
line-height: 22px;
line-height: 30px;
}
.notice-card__paragraph + .notice-card__paragraph {
margin-top: 18px;
}
.notice-card__strong {
color: #1e293b;
font-weight: 900;
}

View File

@@ -1,22 +1,35 @@
<template>
<CardShell class="scenic-image-card" variant="soft" pressable :disabled="disabled" @select="handleAction">
<view class="scenic-image-card__image-wrap relative">
<MediaFrame class="scenic-image-card__image h-260" :src="data.image" />
<view
class="scenic-image-card relative rounded-24 overflow-hidden w-full"
:class="{ 'is-disabled': disabled }"
@click="handleSelect"
>
<image
class="scenic-image-card__image block w-full"
:src="data.image"
mode="aspectFill"
/>
<view
class="scenic-image-card__expand flex flex-items-center flex-justify-center rounded-full color-white font-size-18 font-900"
@click.stop="handleAction"
>
{{ action.icon }}
</view>
<view v-if="hasCaption" class="scenic-image-card__caption">
<view class="scenic-image-card__caption-title color-white font-size-14 font-900">{{ data.caption.title }}</view>
<view v-if="data.caption.subtitle" class="scenic-image-card__caption-subtitle font-size-11 font-700">
{{ data.caption.subtitle }}
<view v-if="caption.title" class="scenic-image-card__title color-white font-size-18 font-900 ellipsis-1">
{{ caption.title }}
</view>
<view v-if="caption.subtitle" class="scenic-image-card__subtitle color-white font-size-14 font-900 ellipsis-1">
{{ caption.subtitle }}
</view>
</view>
<view class="scenic-image-card__expand absolute flex flex-items-center flex-justify-center w-32 h-32 rounded-full color-white font-size-16 font-900"></view>
</view>
</CardShell>
</template>
<script setup>
import { computed } from "vue";
import CardShell from "../SharedVisual/CardShell.vue";
import MediaFrame from "../SharedVisual/MediaFrame.vue";
const props = defineProps({
data: {
@@ -31,11 +44,17 @@ const props = defineProps({
const emit = defineEmits(["select", "action"]);
const hasCaption = computed(() => Boolean(props.data.caption?.title || props.data.caption?.subtitle));
const caption = computed(() => props.data.caption || {});
const action = computed(() => props.data.action || {});
const hasCaption = computed(() => Boolean(caption.value.title || caption.value.subtitle));
const handleSelect = () => {
if (props.disabled) return;
emit("select", props.data);
};
const handleAction = () => {
if (props.disabled) return;
emit("select", props.data);
emit("action", props.data);
};
</script>

View File

@@ -1,7 +1,19 @@
export default {
export const scenicImageWithoutCaption = {
id: "bridge-mist-plain",
image: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=1000&q=80",
caption: {
title: "水上森林二号机位",
subtitle: "下午逆光较弱,适合拍摄树影和水面反射",
action: {
icon: "",
},
};
export default {
id: "bridge-mist",
image: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=1000&q=80",
caption: {
title: "古桥晨雾机位",
subtitle: "清晨 6:30 雾气最浓,长焦压缩效果极佳",
},
action: {
icon: "↗",
},
};

View File

@@ -1,8 +1,30 @@
.scenic-image-card__image-wrap {
.scenic-image-card {
height: 324px;
background: #e2e8f0;
}
.scenic-image-card:active,
.scenic-image-card__expand:active {
opacity: 0.86;
}
.scenic-image-card.is-disabled {
opacity: 0.55;
}
.scenic-image-card__image {
border-radius: 0;
height: 100%;
}
.scenic-image-card__expand {
position: absolute;
top: 16px;
right: 16px;
z-index: 2;
width: 40px;
height: 40px;
background: rgba(15, 23, 42, 0.36);
line-height: 40px;
}
.scenic-image-card__caption {
@@ -10,21 +32,17 @@
left: 0;
right: 0;
bottom: 0;
padding: 42px 16px 16px;
padding: 72px 22px 20px;
background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.72) 100%);
}
.scenic-image-card__caption-title {
line-height: 20px;
.scenic-image-card__title {
line-height: 24px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
}
.scenic-image-card__caption-subtitle {
margin-top: 3px;
color: rgba(255, 255, 255, 0.72);
}
.scenic-image-card__expand {
top: 12px;
right: 12px;
background: rgba(0, 0, 0, 0.32);
.scenic-image-card__subtitle {
margin-top: 4px;
line-height: 18px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
}