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
+353
View File
@@ -0,0 +1,353 @@
<template>
<view class="help-page">
<ModulePageBackground module="profile" />
<view class="page-layer"
><PageHeader title="帮助中心" custom-back @back="requestBack"
/></view>
<view class="page-content page-layer">
<view class="help-hero">
<text>常见问题</text>
<text>帮助内容会随页面更新</text>
</view>
<view v-if="helpCenterState === 'loading'" class="help-state-card">
<AppLoading text="正在读取帮助内容" />
</view>
<view v-else-if="helpCenterState === 'error'" class="help-state-card">
<text>暂时无法读取帮助内容</text>
<text>{{ helpCenterError || "请检查网络后重新加载。" }}</text>
<AppButton block type="secondary" label="重新加载" @click="loadHelpArticles" />
</view>
<template v-else>
<view v-if="articles.length" class="help-list-panel">
<input
v-model.trim="searchText"
class="help-search"
type="text"
maxlength="80"
placeholder="搜索问题"
placeholder-class="help-search__placeholder"
/>
<scroll-view class="help-category-list" scroll-x>
<button
v-for="category in categories"
:key="category"
class="help-category"
:class="{ 'help-category--active': activeCategory === category }"
:aria-pressed="activeCategory === category"
@click="selectCategory(category)"
>{{ categoryLabel(category) }}</button>
</scroll-view>
<view v-if="visibleArticles.length" class="help-article-list">
<view v-for="item in visibleArticles" :key="item.key" class="help-article">
<button
class="help-article__question"
:aria-expanded="expandedKey === item.key"
:aria-controls="`help-answer-${item.key}`"
@click="toggleArticle(item.key)"
>
<view>
<text class="help-article__category">{{ item.categoryLabel }}</text>
<text class="help-article__title">{{ item.title }}</text>
</view>
<text class="help-article__indicator">{{ expandedKey === item.key ? "收起" : "查看" }}</text>
</button>
<text
v-if="expandedKey === item.key"
:id="`help-answer-${item.key}`"
class="help-article__answer"
>{{ item.content }}</text>
</view>
</view>
<view v-else class="help-empty-state">
<text>没有符合条件的帮助内容</text>
<button v-if="searchText || activeCategory !== '全部'" @click="clearFilters">清除筛选</button>
</view>
</view>
<view v-else class="help-state-card">
<text>当前没有帮助内容</text>
<text>如有使用问题可以直接提交反馈</text>
</view>
</template>
<view class="help-feedback-card">
<text>没有找到答案</text>
<text>把问题提交给我们我们会尽快处理</text>
<AppButton block label="提交问题反馈" @click="contactSupport" />
</view>
</view>
</view>
</template>
<script setup>
import { computed, 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 { siteContentApi } from "@/services/api/site-content-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { goBack, handleBackPress, openPage } from "@/utils/navigation/gateway.js";
const articles = ref([]);
const helpCenterState = ref("loading");
const helpCenterError = ref("");
const searchText = ref("");
const activeCategory = ref("全部");
const expandedKey = ref("");
const helpArticleRequestController = createRequestController();
let pageActive = true;
const categories = computed(() => ["全部", ...new Set(articles.value.map((item) => item.category))]);
const categoryLabel = (category) => (
articles.value.find((item) => item.category === category)?.categoryLabel || category
);
const visibleArticles = computed(() => {
const keyword = searchText.value.trim().toLocaleLowerCase();
return articles.value.filter((item) => (
(activeCategory.value === "全部" || item.category === activeCategory.value)
&& (!keyword || `${item.title}\n${item.content}`.toLocaleLowerCase().includes(keyword))
));
});
const loadHelpArticles = async () => {
helpArticleRequestController.abort();
helpCenterState.value = "loading";
helpCenterError.value = "";
expandedKey.value = "";
try {
const helpArticles = await siteContentApi.getHelpArticles({
requestController: helpArticleRequestController,
});
if (!pageActive) return;
articles.value = helpArticles;
if (!categories.value.includes(activeCategory.value)) activeCategory.value = "全部";
helpCenterState.value = "ready";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
helpCenterError.value = getRequestErrorMessage(error, "请稍后重试。");
helpCenterState.value = "error";
}
};
const selectCategory = (category) => {
activeCategory.value = category;
expandedKey.value = "";
};
const toggleArticle = (key) => {
expandedKey.value = expandedKey.value === key ? "" : key;
};
const clearFilters = () => {
searchText.value = "";
activeCategory.value = "全部";
expandedKey.value = "";
};
const contactSupport = () => openPage("M07", {}, "M06");
const requestBack = () => goBack();
onLoad(loadHelpArticles);
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
pageActive = false;
helpArticleRequestController.abort();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.help-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.page-layer {
z-index: 1;
}
.page-content {
flex: 1;
padding: 24rpx 28rpx 72rpx;
}
.help-hero,
.help-state-card,
.help-list-panel,
.help-feedback-card {
@include adaptive-profile-content;
}
.help-hero {
padding: 34rpx 36rpx;
text-align: center;
}
.help-hero text {
display: block;
}
.help-hero text:first-child,
.help-state-card text:first-child,
.help-feedback-card text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(18px, 32rpx, 22px);
font-weight: 700;
}
.help-hero text:last-child,
.help-state-card text:last-child,
.help-feedback-card text:nth-child(2) {
margin-top: 12rpx;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.6;
}
.help-state-card {
display: flex;
min-height: 210rpx;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 20rpx;
padding: 38rpx;
text-align: center;
}
.help-state-card text {
display: block;
}
.help-state-card .app-button,
.help-feedback-card .app-button {
width: 100%;
margin-top: 24rpx;
}
.help-list-panel {
margin-top: 20rpx;
padding: 24rpx;
}
.help-search {
width: 100%;
min-height: 72rpx;
box-sizing: border-box;
padding: 0 22rpx;
border: 1rpx solid rgba(128, 89, 49, 0.28);
border-radius: 12rpx;
background: rgba(255, 252, 245, 0.78);
color: $ink;
font-size: clamp(14px, 24rpx, 18px);
}
.help-search__placeholder {
color: #a49382;
}
.help-category-list {
width: 100%;
margin-top: 18rpx;
white-space: nowrap;
}
.help-category {
display: inline-flex;
min-width: 112rpx;
min-height: 58rpx;
align-items: center;
justify-content: center;
margin-right: 12rpx;
padding: 0 20rpx;
border: 1rpx solid rgba(128, 89, 49, 0.24);
border-radius: 29rpx;
background: rgba(255, 252, 245, 0.54);
color: $ink-muted;
font-size: clamp(13px, 22rpx, 16px);
line-height: 1.4;
}
.help-category::after,
.help-article__question::after,
.help-empty-state button::after {
border: 0;
}
.help-category--active {
border-color: #8d2722;
background: #8d2722;
color: #fff7e8;
}
.help-article-list {
display: grid;
gap: 14rpx;
width: 100%;
}
.help-article {
overflow: hidden;
border: 1rpx solid rgba(128, 89, 49, 0.22);
border-radius: 12rpx;
background: rgba(255, 252, 245, 0.66);
}
.help-article__question {
display: flex;
width: 100%;
min-height: 100rpx;
align-items: center;
justify-content: space-between;
gap: 18rpx;
padding: 20rpx 22rpx;
border: 0;
border-radius: 0;
background: transparent;
text-align: left;
}
.help-article__question view {
min-width: 0;
flex: 1;
}
.help-article__question text {
display: block;
}
.help-article__category {
color: #9a6555;
font-size: clamp(12px, 20rpx, 15px);
}
.help-article__title {
margin-top: 5rpx;
color: $ink;
font-size: clamp(15px, 25rpx, 19px);
font-weight: 700;
line-height: 1.45;
}
.help-article__indicator {
flex: 0 0 auto;
color: #9f170f;
font-size: clamp(12px, 20rpx, 15px);
}
.help-article__answer {
display: block;
padding: 0 22rpx 24rpx;
border-top: 1rpx solid rgba(128, 89, 49, 0.16);
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.7;
white-space: pre-wrap;
}
.help-empty-state {
padding: 50rpx 22rpx 28rpx;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
text-align: center;
}
.help-empty-state text {
display: block;
}
.help-empty-state button {
margin-top: 16rpx;
border: 0;
background: transparent;
color: #9f170f;
font-size: clamp(14px, 23rpx, 17px);
}
.help-feedback-card {
margin-top: 20rpx;
padding: 32rpx 34rpx;
text-align: center;
}
@media (max-width: 340px) {
.page-content {
padding-right: 20rpx;
padding-left: 20rpx;
}
}
</style>