Files
jiapuapp/components/AppPromotionStrip.vue
2026-09-13 17:45:52 +08:00

255 lines
6.5 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view
v-if="showEmpty || promotionState === 'error' || promotions.length"
class="promotion-strip"
:aria-label="title"
>
<view class="promotion-strip__heading">
<view class="promotion-strip__mark" aria-hidden="true"></view>
<text>{{ title }}</text>
</view>
<view v-if="promotionState === 'loading'" class="promotion-strip__state">
<text>正在加载广告内容</text>
</view>
<view v-else-if="promotionState === 'error'" class="promotion-strip__error">
<text>推荐内容暂时没有显示</text>
<button @click="loadPromotions">重新加载</button>
</view>
<view v-else-if="!promotions.length" class="promotion-strip__state">
<text>暂无广告内容</text>
</view>
<scroll-view v-else class="promotion-strip__scroll" scroll-x :show-scrollbar="false">
<view class="promotion-strip__list">
<view
v-for="promotion in promotions"
:key="promotion.id"
class="promotion-strip__card"
:class="{ 'promotion-strip__card--linked': promotion.targetUrl }"
:role="promotion.targetUrl ? 'button' : undefined"
:aria-label="promotion.targetUrl ? `${promotion.title}查看详情` : promotion.title"
@click="openPromotion(promotion)"
>
<image
v-if="promotion.coverFile?.accessUrl"
class="promotion-strip__cover"
:src="promotion.coverFile.accessUrl"
mode="aspectFill"
/>
<view class="promotion-strip__copy">
<text class="promotion-strip__title">{{ promotion.title }}</text>
<text v-if="promotion.description" class="promotion-strip__description">
{{ promotion.description }}
</text>
<text v-if="promotion.targetUrl" class="promotion-strip__action">
查看详情
</text>
</view>
</view>
</view>
</scroll-view>
<text
v-if="operationFeedback"
class="promotion-strip__feedback"
role="status"
>
{{ operationFeedback }}
</text>
</view>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref } from "vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { siteContentApi } from "@/services/api/site-content-service.js";
import { openSiteContentTarget } from "@/utils/navigation/gateway.js";
const props = defineProps({
placement: { type: String, required: true },
title: { type: String, default: "推荐内容" },
showEmpty: { type: Boolean, default: false },
});
const promotions = ref([]);
const promotionState = ref("loading");
const operationFeedback = ref("");
const promotionListController = createRequestController();
let isMounted = true;
const loadPromotions = async () => {
promotionListController.abort();
promotionState.value = "loading";
operationFeedback.value = "";
try {
const promotionRows = await siteContentApi.getPromotions({
placement: props.placement,
requestController: promotionListController,
});
if (!isMounted) return;
promotions.value = promotionRows;
promotionState.value = "ready";
} catch (error) {
if (!isMounted || isRequestCancelled(error)) return;
promotionState.value = "error";
}
};
const openPromotion = async (promotion) => {
const targetUrl = promotion?.targetUrl || "";
if (!targetUrl) return;
const showExternalLinkError = () => {
if (!isMounted) return;
operationFeedback.value = "链接暂时打不开,请稍后再试。";
};
try {
await openSiteContentTarget(targetUrl, showExternalLinkError);
} catch {
if (!isMounted) return;
operationFeedback.value = targetUrl.startsWith("/")
? "这个页面暂时打不开,请稍后再试。"
: "链接暂时打不开,请稍后再试。";
}
};
onMounted(loadPromotions);
onBeforeUnmount(() => {
isMounted = false;
promotionListController.abort();
});
</script>
<style scoped lang="scss">
.promotion-strip {
position: relative;
z-index: 1;
margin: 20rpx 24rpx 28rpx;
padding: 22rpx 0 20rpx;
border: 1rpx solid rgba(145, 89, 36, 0.34);
border-radius: 14rpx;
background: rgba(255, 250, 239, 0.9);
box-shadow: 0 7rpx 20rpx rgba(74, 37, 18, 0.08);
}
.promotion-strip__heading {
display: flex;
align-items: center;
gap: 12rpx;
padding: 0 22rpx 18rpx;
color: #6f1a14;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(16px, 27rpx, 20px);
font-weight: 700;
}
.promotion-strip__mark {
width: 7rpx;
height: 30rpx;
border-radius: 8rpx;
background: linear-gradient(#c89b4b, #8f1b14);
}
.promotion-strip__scroll {
width: 100%;
white-space: nowrap;
}
.promotion-strip__list {
display: inline-flex;
gap: 16rpx;
padding: 0 22rpx;
}
.promotion-strip__card {
display: flex;
width: 500rpx;
min-height: 142rpx;
overflow: hidden;
border: 1rpx solid rgba(125, 83, 45, 0.23);
border-radius: 12rpx;
background: #fffdf8;
white-space: normal;
}
.promotion-strip__card--linked:active {
opacity: 0.78;
}
.promotion-strip__cover {
width: 176rpx;
min-height: 142rpx;
flex: 0 0 auto;
background: #eee4d4;
}
.promotion-strip__copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
padding: 18rpx 20rpx;
}
.promotion-strip__title {
overflow: hidden;
color: #402c20;
font-size: clamp(15px, 25rpx, 18px);
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.promotion-strip__description {
display: -webkit-box;
overflow: hidden;
margin-top: 8rpx;
color: #766252;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.promotion-strip__action {
margin-top: auto;
padding-top: 8rpx;
color: #9f170f;
font-size: clamp(13px, 21rpx, 16px);
}
.promotion-strip__error {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
padding: 0 22rpx;
color: #766252;
font-size: clamp(13px, 21rpx, 16px);
}
.promotion-strip__state {
padding: 12rpx 22rpx 18rpx;
color: #766252;
font-size: clamp(13px, 21rpx, 16px);
text-align: center;
}
.promotion-strip__error button {
margin: 0;
padding: 8rpx 18rpx;
border: 1rpx solid rgba(159, 23, 15, 0.36);
border-radius: 999rpx;
background: transparent;
color: #9f170f;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.5;
}
.promotion-strip__error button::after {
border: 0;
}
.promotion-strip__feedback {
display: block;
padding: 14rpx 22rpx 0;
color: #9f170f;
font-size: clamp(13px, 21rpx, 16px);
}
</style>