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
+242
View File
@@ -0,0 +1,242 @@
<template>
<view class="promotion-page">
<ModulePageBackground module="profile" />
<view class="page-layer"><PageHeader title="推广中心" custom-back @back="requestBack" /></view>
<view class="page-content page-layer">
<view class="promotion-hero">
<image
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<text>家谱服务推荐</text>
<text>这里会展示家谱相关服务和活动</text>
</view>
<view v-if="promotionListState === 'loading'" class="promotion-state-card">
<AppLoading text="正在读取推广内容" />
</view>
<view v-else-if="promotionListState === 'error'" class="promotion-state-card">
<text>暂时无法读取推广内容</text>
<text>{{ promotionListError || '请检查网络后重新加载。' }}</text>
<AppButton block type="secondary" label="重新加载" @click="loadPromotions" />
</view>
<view v-else-if="!promotions.length" class="promotion-state-card">
<text>当前没有推广内容</text>
<text>后续正式发布的家谱服务推荐会在这里展示</text>
</view>
<view v-else class="promotion-list">
<text v-if="operationFeedback" class="promotion-feedback" role="status">{{ operationFeedback }}</text>
<view
v-for="item in promotions"
:key="item.id"
class="promotion-card"
:class="{ 'promotion-card--linked': item.targetUrl }"
:role="item.targetUrl ? 'button' : undefined"
:aria-label="item.targetUrl ? `${item.title}查看详情` : undefined"
@click="openPromotion(item)"
>
<image
v-if="item.coverFile?.accessUrl"
class="promotion-card__cover"
:src="item.coverFile.accessUrl"
mode="aspectFill"
/>
<text class="promotion-card__title">{{ item.title }}</text>
<text v-if="item.description" class="promotion-card__description">{{ item.description }}</text>
<view v-if="item.targetUrl" class="promotion-card__actions">
<AppButton compact type="secondary" label="复制链接" @click.stop="copyPromotionLink(item)" />
<AppButton compact label="分享给家人" @click.stop="sharePromotion(item)" />
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onBackPress, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { siteContentApi } from "@/services/api/site-content-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { goBack, handleBackPress, openSiteContentTarget } from "@/utils/navigation/gateway.js";
const promotions = ref([]);
const promotionListState = ref("loading");
const promotionListError = ref("");
const operationFeedback = ref("");
const promotionListRequestController = createRequestController();
let pageActive = true;
const loadPromotions = async () => {
promotionListRequestController.abort();
promotionListState.value = "loading";
promotionListError.value = "";
try {
const promotionRows = await siteContentApi.getPromotions({
requestController: promotionListRequestController,
});
if (!pageActive) return;
promotions.value = promotionRows;
promotionListState.value = "ready";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
promotionListError.value = getRequestErrorMessage(error, "推广内容暂时无法加载,请稍后再试。");
promotionListState.value = "error";
}
};
const openPromotion = async (item) => {
const targetUrl = item?.targetUrl || "";
if (!targetUrl) return;
const showExternalLinkError = () => {
operationFeedback.value = "链接暂时打不开,请稍后再试。";
};
try {
await openSiteContentTarget(targetUrl, showExternalLinkError);
} catch {
operationFeedback.value = targetUrl.startsWith("/")
? "这个页面暂时打不开,请稍后再试。"
: "链接暂时打不开,请稍后再试。";
}
};
const copyPromotionLink = (item) => {
if (!item?.targetUrl || typeof uni?.setClipboardData !== "function") {
operationFeedback.value = "当前设备暂时不能复制链接。";
return;
}
uni.setClipboardData({
data: item.targetUrl,
success: () => { operationFeedback.value = "推广链接已复制,可以发给家人。"; },
fail: () => { operationFeedback.value = "复制失败,请稍后再试。"; },
});
};
const sharePromotion = (item) => {
if (!item?.targetUrl) return;
const content = [item.title, item.description, item.targetUrl].filter(Boolean).join("\n");
// #ifdef APP-PLUS
if (typeof plus?.share?.sendWithSystem === "function") {
plus.share.sendWithSystem(
{ type: "text", content },
() => { operationFeedback.value = "已打开系统分享。"; },
() => { operationFeedback.value = "已取消分享。"; },
);
return;
}
// #endif
// #ifdef H5
if (typeof navigator?.share === "function") {
navigator.share({ title: item.title, text: item.description, url: item.targetUrl })
.then(() => { operationFeedback.value = "分享已完成。"; })
.catch(() => { operationFeedback.value = "已取消分享。"; });
return;
}
// #endif
copyPromotionLink(item);
};
const requestBack = () => goBack();
onLoad(loadPromotions);
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
pageActive = false;
promotionListRequestController.abort();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.promotion-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.page-layer {
z-index: 1;
}
.page-content {
flex: 1;
padding: 28rpx 30rpx 72rpx;
}
.promotion-hero,
.promotion-state-card,
.promotion-list {
@include adaptive-profile-content;
}
.promotion-hero {
display: flex;
min-height: 250rpx;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 38rpx 48rpx;
text-align: center;
}
.promotion-hero image {
width: 90rpx;
height: 102rpx;
}
.promotion-hero text,
.promotion-state-card text {
display: block;
}
.promotion-hero text:nth-child(2),
.promotion-state-card text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(19px, 34rpx, 24px);
font-weight: 700;
}
.promotion-hero text:last-child,
.promotion-state-card text:last-child {
margin-top: 12rpx;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.55;
}
.promotion-state-card {
display: flex;
min-height: 210rpx;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 20rpx;
padding: 38rpx;
text-align: center;
}
.promotion-state-card .app-button { margin-top: 28rpx; }
.promotion-list { display: grid; gap: 16rpx; }
.promotion-card {
overflow: hidden;
padding: 26rpx 28rpx;
border: 1rpx solid rgba(128, 89, 49, 0.26);
border-radius: 12rpx;
background: rgba(255, 252, 245, 0.78);
}
.promotion-card--linked { cursor: pointer; }
.promotion-card__cover {
display: block;
width: calc(100% + 56rpx);
height: 260rpx;
margin: -26rpx -28rpx 22rpx;
}
.promotion-card text { display: block; }
.promotion-card__title { color: $ink; font-size: clamp(16px, 28rpx, 20px); font-weight: 700; }
.promotion-card__description { margin-top: 10rpx; color: $ink-muted; font-size: clamp(14px, 23rpx, 17px); line-height: 1.6; }
.promotion-feedback { display: block; padding: 16rpx 20rpx; border: 1rpx solid rgba(66, 107, 88, .32); border-radius: 10rpx; background: rgba(66, 107, 88, .08); color: #426b58; font-size: clamp(14px, 22rpx, 17px); }
.promotion-card__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; margin-top: 16rpx; gap: 12rpx; }
.promotion-card__actions .app-button { width: auto; min-width: 142rpx; }
@media (max-width: 340px) {
.page-content {
padding-right: 22rpx;
padding-left: 22rpx;
}
}
</style>