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
+222
View File
@@ -0,0 +1,222 @@
<template>
<view class="document-page" :class="`document-state--${complianceDocumentState}`">
<ModulePageBackground module="profile" />
<view class="page-layer">
<PageHeader :title="fallbackTitle" custom-back @back="requestBack" />
</view>
<view class="document-content page-layer">
<AppLoading
v-if="complianceDocumentState === 'loading'"
text="正在读取内容"
description="请稍候。"
/>
<view v-else-if="complianceDocumentState === 'error'" class="document-state-card">
<text>暂时无法读取内容</text>
<text>{{ errorMessage }}</text>
<AppButton block type="secondary" label="重新加载" @click="loadDocument" />
</view>
<view v-else class="document-sheet">
<view class="document-heading">
<text>{{ document.title }}</text>
<text>版本 {{ document.versionNo }}</text>
<text v-if="document.publishedAt">发布日期{{ displayDate(document.publishedAt) }}</text>
<text v-if="document.effectiveAt">生效日期{{ displayDate(document.effectiveAt) }}</text>
</view>
<view class="document-body">
<template v-for="(block, index) in contentBlocks" :key="`${block.type}-${index}`">
<text v-if="block.type === 'title'" class="content-title">{{ block.text }}</text>
<text v-else-if="block.type === 'heading'" class="content-heading">{{ block.text }}</text>
<view v-else-if="block.type === 'list-item'" class="content-list-item">
<text>{{ block.marker }}</text>
<text>{{ block.text }}</text>
</view>
<text v-else class="content-paragraph">{{ block.text }}</text>
</template>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, 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 {
COMPLIANCE_DOCUMENT_KEY
} from "@/services/api/site-content-contract.js";
import { siteContentApi } from "@/services/api/site-content-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { formatComplianceContent } from "@/utils/profile/compliance-content.js";
import { goBack, handleBackPress } from "@/utils/navigation/gateway.js";
const documentKey = ref("");
const complianceDocumentState = ref("loading");
const errorMessage = ref("");
const document = reactive({
title: "",
versionNo: "",
content: "",
publishedAt: "",
effectiveAt: "",
});
const complianceDocumentRequestController = createRequestController();
let pageActive = true;
const fallbackTitle = computed(() =>
documentKey.value === COMPLIANCE_DOCUMENT_KEY.PRIVACY_POLICY
? "隐私政策"
: "用户协议",
);
const contentBlocks = computed(() =>
formatComplianceContent(document.content, document.title),
);
const displayDate = (value) => String(value || "").slice(0, 10);
const loadDocument = async () => {
if (!Object.values(COMPLIANCE_DOCUMENT_KEY).includes(documentKey.value)) {
errorMessage.value = "文档类型不正确,请返回后重新进入。";
complianceDocumentState.value = "error";
return;
}
complianceDocumentRequestController.abort();
complianceDocumentState.value = "loading";
errorMessage.value = "";
try {
const complianceDocument = await siteContentApi.getComplianceDocument(documentKey.value, {
requestController: complianceDocumentRequestController,
});
if (!pageActive) return;
Object.assign(document, complianceDocument);
complianceDocumentState.value = "ready";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
errorMessage.value = getRequestErrorMessage(error, "请检查网络后重新加载。");
complianceDocumentState.value = "error";
}
};
const requestBack = () => goBack();
onLoad((options = {}) => {
documentKey.value = String(options.documentKey || "");
loadDocument();
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
pageActive = false;
complianceDocumentRequestController.abort();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.document-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.page-layer {
z-index: 1;
}
.document-content {
flex: 1;
padding: 24rpx 28rpx 72rpx;
}
.document-sheet,
.document-state-card {
@include adaptive-profile-content;
}
.document-sheet {
padding: 34rpx 36rpx 48rpx;
background-color: rgba(255, 252, 245, 0.9);
}
.document-heading {
padding-bottom: 24rpx;
border-bottom: 1rpx solid rgba(181, 137, 63, 0.42);
}
.document-heading text {
display: block;
}
.document-heading text:first-child {
color: $brand-red;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(21px, 38rpx, 27px);
font-weight: 700;
line-height: 1.35;
}
.document-heading text:not(:first-child) {
margin-top: 8rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
}
.document-body {
margin-top: 26rpx;
}
.content-title,
.content-heading,
.content-paragraph {
display: block;
overflow-wrap: anywhere;
}
.content-title {
color: $ink;
font-size: clamp(18px, 31rpx, 23px);
font-weight: 700;
line-height: 1.45;
}
.content-heading {
margin-top: 30rpx;
color: $brand-red;
font-size: clamp(17px, 28rpx, 21px);
font-weight: 700;
line-height: 1.5;
}
.content-paragraph,
.content-list-item {
margin-top: 18rpx;
color: $ink;
font-size: clamp(15px, 25rpx, 18px);
line-height: 1.8;
}
.content-list-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 12rpx;
}
.content-list-item text:last-child {
overflow-wrap: anywhere;
}
.document-state-card {
min-height: 320rpx;
padding: 58rpx 42rpx 42rpx;
text-align: center;
}
.document-state-card > text {
display: block;
}
.document-state-card > text:first-child {
color: $ink;
font-size: clamp(19px, 33rpx, 24px);
font-weight: 700;
}
.document-state-card > text:nth-child(2) {
margin-top: 16rpx;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.6;
}
.document-state-card .app-button {
margin-top: 28rpx;
}
</style>