feat: migrate app routes and business modules

This commit is contained in:
2026-08-12 18:22:59 +08:00
parent 555aa00043
commit cc706378c2
247 changed files with 28623 additions and 14988 deletions
+240
View File
@@ -0,0 +1,240 @@
<template>
<view
v-if="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 === 'error'" class="promotion-strip__error">
<text>推荐内容暂时没有显示</text>
<button @click="loadPromotions">重新加载</button>
</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: "推荐内容" },
});
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__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>