hejianhao
2026-08-18 e782b94370da4b44e0f308f723b7d94148575a37
feat0806: 提交遗漏的APP文件
24个文件已添加
1346 ■■■■■ 已修改文件
traditionalApp/pages/bankCard/bankCard.vue 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/bankCard/service.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/projectDetail.vue 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/projectList.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/static-content.js 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/healthPopularization/healthPopularization.vue 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/healthPopularization/service.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/passwordInput/passwordInput.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/healthCoursesHub/gongfa.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/healthCoursesHub/kepu.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/healthCoursesHub/zhishi.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/home/weizan-live.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/mine/dingdan.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/mine/tongji.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/mine/yinghangka.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/mine/yinghanka.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/mine/zhibodingdan.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/mine/zhibotongji.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/project/dongzuo.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/project/jieshao.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/project/moshi.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/tabBar/Yproject.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/static/image/tabBar/project.png 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/bankCard/bankCard.vue
New file
@@ -0,0 +1,289 @@
<template>
    <view class="container">
        <view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
            <view class="nav-content">
                <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
                <text class="nav-title">银行卡设置</text>
                <view class="nav-right-placeholder"></view>
            </view>
        </view>
        <view class="form">
            <view class="form-item" v-for="item in fieldList" :key="item.key">
                <view class="label">{{ item.label }}</view>
                <view class="input-wrapper">
                    <input
                        class="input"
                        type="text"
                        :placeholder="item.placeholder"
                        placeholder-class="placeholder-style"
                        v-model="form[item.key]"
                    />
                </view>
                <view class="line"></view>
            </view>
        </view>
        <view class="submit-btn" :class="{ disabled: submitting }" @tap="submit">保存</view>
    </view>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { getBankCardDetail, updateBankCard } from './service.js'
type BankCardFieldKey = 'bankName' | 'accountName' | 'accountNumber' | 'branchName'
interface BankCardForm {
    bankName: string
    accountName: string
    accountNumber: string
    branchName: string
}
const statusBarHeight = ref(20)
const submitting = ref(false)
const form = reactive<BankCardForm>({
    bankName: '',
    accountName: '',
    accountNumber: '',
    branchName: '',
})
const fieldList: Array<{ key: BankCardFieldKey; label: string; placeholder: string }> = [
    { key: 'bankName', label: '银行名称', placeholder: '请输入银行名称' },
    { key: 'accountName', label: '账户名称', placeholder: '请输入账户名称' },
    { key: 'accountNumber', label: '账号', placeholder: '请输入账号' },
    { key: 'branchName', label: '支行名称', placeholder: '请输入支行名称' },
]
onMounted(() => {
    const systemInfo = uni.getSystemInfoSync()
    statusBarHeight.value = systemInfo.statusBarHeight || 20
    loadBankCard()
})
/**
 * 拉取已保存的银行卡信息并回显。
 */
async function loadBankCard() {
    try {
        const response = await getBankCardDetail()
        if (!isSuccessResponse(response) || !response.data) {
            return
        }
        fillForm(response.data)
    } catch {
        // 接口暂未就绪时保留空白表单,不影响页面展示。
    }
}
/**
 * 校验并保存银行卡信息。
 */
async function submit() {
    if (submitting.value) {
        return
    }
    if (!validateForm()) {
        return
    }
    submitting.value = true
    try {
        const payload = {
            bankName: form.bankName.trim(),
            accountName: form.accountName.trim(),
            accountNumber: form.accountNumber.trim(),
            branchName: form.branchName.trim(),
        }
        const response = await updateBankCard(payload)
        if (!isSuccessResponse(response)) {
            return
        }
        if (response.data) {
            fillForm(response.data)
        }
        uni.showToast({
            title: '保存成功',
            icon: 'success',
        })
        setTimeout(() => {
            goBack()
        }, 800)
    } catch (error: any) {
        uni.showToast({
            title: error?.message || '银行卡信息保存失败,请稍后重试',
            icon: 'none',
        })
    } finally {
        submitting.value = false
    }
}
/**
 * 保存前校验四个必填字段。
 */
function validateForm() {
    if (!form.bankName.trim()) {
        showToast('请输入银行名称')
        return false
    }
    if (!form.accountName.trim()) {
        showToast('请输入账户名称')
        return false
    }
    if (!form.accountNumber.trim()) {
        showToast('请输入账号')
        return false
    }
    if (!form.branchName.trim()) {
        showToast('请输入支行名称')
        return false
    }
    return true
}
/**
 * 将接口返回的银行卡信息回填到表单。
 */
function fillForm(value: unknown) {
    const source = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>
    form.bankName = normalizeOptionalText(source.bankName)
    form.accountName = normalizeOptionalText(source.accountName)
    form.accountNumber = normalizeOptionalText(source.accountNumber || source.accountNo)
    form.branchName = normalizeOptionalText(source.branchName)
}
function isSuccessResponse(response: any) {
    return response && (response.code === 0 || response.code === 200)
}
function showToast(title: string) {
    uni.showToast({
        title,
        icon: 'none',
    })
}
function normalizeOptionalText(value: unknown) {
    if (typeof value !== 'string') {
        return ''
    }
    return value.trim()
}
function goBack() {
    const currentPages = getCurrentPages()
    if (currentPages.length > 1) {
        uni.navigateBack()
        return
    }
    uni.switchTab({
        url: '/pages/mine/mine',
    })
}
</script>
<style lang="scss" scoped>
.container {
    min-height: 100vh;
    background-color: #ffffff;
    display: flex;
    flex-direction: column;
}
.custom-navbar {
    width: 100%;
    background-color: #ffffff;
    position: relative;
    z-index: 100;
}
.nav-content {
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 32rpx;
}
.back-icon {
    width: 40rpx;
    height: 40rpx;
}
.nav-title {
    font-size: 36rpx;
    color: #1A1A1A;
    font-weight: bold;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
}
.nav-right-placeholder {
    width: 40rpx;
}
.form {
    width: 335px;
    margin: 40rpx auto 0;
}
.form-item {
    margin-bottom: 40rpx;
}
.label {
    font-family: 'PingFang SC', 'PingFang-SC', sans-serif;
    font-size: 12px;
    line-height: 20px;
    color: #333333;
    margin-bottom: 16rpx;
}
.input-wrapper {
    width: 335px;
    height: 48px;
    display: flex;
    align-items: center;
}
.input {
    width: 100%;
    height: 24px;
    font-family: 'PingFang SC', 'PingFang-SC', sans-serif;
    font-size: 16px;
    line-height: 24px;
    color: #333333;
}
.placeholder-style {
    font-family: 'PingFang SC', 'PingFang-SC', sans-serif;
    font-size: 16px;
    line-height: 24px;
    color: #999999;
}
.line {
    width: 335px;
    height: 1px;
    background: #F2F3F5;
}
.submit-btn {
    width: 335px;
    height: 48px;
    line-height: 48px;
    text-align: center;
    background-color: #C63520;
    color: #ffffff;
    font-size: 16px;
    font-family: 'PingFang SC', 'PingFang-SC', sans-serif;
    border-radius: 8px;
    margin: 80rpx auto 0;
}
.submit-btn.disabled {
    opacity: 0.6;
}
</style>
traditionalApp/pages/bankCard/service.js
New file
@@ -0,0 +1,15 @@
import request from '@/utils/request.js'
/**
 * 获取 APP 银行卡设置详情。
 */
export const getBankCardDetail = () => {
    return request.get('/api/app/bank-card', {}, false, false)
}
/**
 * 保存 APP 银行卡设置。
 */
export const updateBankCard = (data) => {
    return request.put('/api/app/bank-card', data)
}
traditionalApp/pages/good/projectDetail.vue
New file
@@ -0,0 +1,204 @@
<template>
    <view class="container">
        <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
        <view class="nav-bar">
            <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
            <text class="nav-title">详情</text>
        </view>
        <view v-if="isVideoType" class="video-container">
            <video class="video-player" :src="detail.videoUrl" controls :autoplay="false"></video>
        </view>
        <view v-else-if="detail.coverImage" class="cover-container">
            <image class="cover-image" :src="detail.coverImage" mode="aspectFill"></image>
        </view>
        <view class="content-area">
            <text class="content-title">{{ detail.title || '详情' }}</text>
            <text v-if="isVideoType && videoSubtitle" class="content-subtitle">{{ videoSubtitle }}</text>
            <rich-text v-if="!isVideoType" :nodes="contentNodes" class="rich-content"></rich-text>
            <view v-if="!detail.id" class="status-text">暂无详情内容</view>
        </view>
    </view>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getProjectContentDetail } from './static-content.js'
interface ProjectDetail {
    id?: number | null
    title: string
    subtitle: string
    coverImage: string
    contentType?: number | null
    content: string
    videoUrl: string
}
const statusBarHeight = ref(0)
const detail = ref<ProjectDetail>(createDefaultDetail())
function createDefaultDetail(): ProjectDetail {
    return {
        id: null,
        title: '',
        subtitle: '',
        coverImage: '',
        contentType: null,
        content: '',
        videoUrl: '',
    }
}
const isVideoType = computed(() => Number(detail.value.contentType) === 2)
const videoSubtitle = computed(() => String(detail.value.subtitle || '').trim())
const contentNodes = computed(() => buildRichTextNodes(detail.value.content, '暂无详情内容'))
onMounted(() => {
    uni.getSystemInfo({
        success: (res) => {
            statusBarHeight.value = res.statusBarHeight || 0
        }
    })
})
onLoad((options) => {
    const type = options?.type ? decodeURIComponent(options.type) : ''
    const id = options?.id ? decodeURIComponent(options.id) : ''
    const matched = getProjectContentDetail(type, id)
    detail.value = matched ? { ...createDefaultDetail(), ...matched } : createDefaultDetail()
})
/**
 * 将富文本内容转换为 `rich-text` 可直接渲染的节点。
 */
function buildRichTextNodes(content?: string, fallbackText?: string) {
    if (typeof content === 'string' && content.trim()) {
        return content.replace(/<img/gi,
            '<img style="max-width:100%;height:auto;margin:0 auto;display:block"')
    }
    if (typeof fallbackText === 'string' && fallbackText.trim()) {
        return `<p>${fallbackText.trim()}</p>`
    }
    return '<p></p>'
}
/**
 * 返回上一页;没有历史栈时回到项目中心。
 */
function goBack() {
    const currentPages = getCurrentPages()
    if (currentPages.length > 1) {
        uni.navigateBack()
        return
    }
    uni.switchTab({
        url: '/pages/good/good'
    })
}
</script>
<style>
page,
html {
    background-color: #F5F6F8;
}
</style>
<style lang="scss" scoped>
.container {
    min-height: 100vh;
    background-color: #F5F6F8;
}
.status-bar-placeholder {
    width: 100%;
}
.nav-bar {
    height: 88rpx;
    display: flex;
    align-items: center;
    padding: 0 24rpx;
    background-color: #fff;
    position: relative;
}
.back-icon {
    width: 40rpx;
    height: 40rpx;
    position: absolute;
    left: 24rpx;
    z-index: 10;
}
.nav-title {
    flex: 1;
    text-align: center;
    font-size: 36rpx;
    font-weight: 500;
    color: #333;
}
.video-container {
    width: 100%;
    height: 402rpx;
}
.video-player {
    width: 100%;
    height: 100%;
}
.cover-container {
    width: 100%;
    height: 402rpx;
}
.cover-image {
    width: 100%;
    height: 100%;
}
.content-area {
    padding: 32rpx;
    background-color: #F5F6F8;
}
.content-title {
    font-size: 32rpx;
    font-weight: 500;
    color: #333;
    line-height: 48rpx;
}
.content-subtitle {
    display: block;
    margin-top: 12rpx;
    font-size: 12px;
    font-family: 'PingFang SC', 'PingFang-SC', sans-serif;
    color: #5B6475;
    line-height: 1.5;
}
.rich-content {
    margin-top: 24rpx;
    width: 100%;
    font-size: 28rpx;
    line-height: 1.8;
    color: #333;
}
.status-text {
    margin-top: 24rpx;
    font-size: 26rpx;
    line-height: 40rpx;
    color: #86909C;
}
</style>
traditionalApp/pages/good/projectList.vue
New file
@@ -0,0 +1,210 @@
<template>
    <view class="container">
        <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
        <view class="nav-bar">
            <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
            <text class="nav-title">{{ pageTitle }}</text>
            <view class="nav-right-placeholder"></view>
        </view>
        <view class="content">
            <view v-if="articleList.length" class="article-list">
                <view class="article-item" v-for="item in articleList" :key="item.id" @tap="goDetail(item)">
                    <image class="article-img" :src="item.coverImage" mode="aspectFill"></image>
                    <view v-if="isVideoType(item)" class="play-btn">
                        <text class="play-icon">▶</text>
                    </view>
                    <view class="article-shadow">
                        <text class="article-text">{{ item.title }}</text>
                    </view>
                </view>
            </view>
            <view v-else class="status-text">暂无内容</view>
        </view>
    </view>
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { PROJECT_CONTENT_MAP } from './static-content.js'
interface ProjectListItem {
    id: number
    title: string
    coverImage: string
    contentType: number
}
const statusBarHeight = ref(0)
const pageTitle = ref('项目中心')
const listType = ref('')
const articleList = ref<ProjectListItem[]>([])
onLoad((options) => {
    uni.getSystemInfo({
        success: (res) => {
            statusBarHeight.value = res.statusBarHeight || 0
        }
    })
    const title = options?.title ? decodeURIComponent(options.title) : ''
    const type = options?.type ? decodeURIComponent(options.type) : ''
    pageTitle.value = title || '项目中心'
    listType.value = type
    articleList.value = PROJECT_CONTENT_MAP[type] || []
})
/**
 * 视频类型在封面上展示播放按钮。
 */
function isVideoType(item: ProjectListItem) {
    return Number(item.contentType) === 2
}
/**
 * 进入项目中心详情页,图文和视频共用同一详情结构。
 */
function goDetail(item: ProjectListItem) {
    uni.navigateTo({
        url: `/pages/good/projectDetail?type=${encodeURIComponent(listType.value)}&id=${item.id}`
    })
}
/**
 * 返回项目中心主页面。
 */
function goBack() {
    const currentPages = getCurrentPages()
    if (currentPages.length > 1) {
        uni.navigateBack()
        return
    }
    uni.switchTab({
        url: '/pages/good/good'
    })
}
</script>
<style>
page, html {
    background-color: #fff;
}
</style>
<style lang="scss" scoped>
.container {
    min-height: 100vh;
    background-color: #fff;
}
.status-bar-placeholder {
    width: 100%;
}
.nav-bar {
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 32rpx;
}
.back-icon {
    width: 40rpx;
    height: 40rpx;
    flex-shrink: 0;
}
.nav-title {
    flex: 1;
    text-align: center;
    font-size: 36rpx;
    font-weight: 600;
    color: #333;
}
.nav-right-placeholder {
    width: 40rpx;
    flex-shrink: 0;
}
.content {
    margin-top: 32rpx;
    padding: 0 32rpx;
    padding-bottom: 40rpx;
}
.status-text {
    text-align: center;
    font-size: 24rpx;
    color: #C0C4CC;
    padding: 80rpx 0;
}
.article-list {
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
}
.article-item {
    width: 328rpx;
    height: 328rpx;
    border-radius: 16rpx;
    position: relative;
    overflow: hidden;
    margin-bottom: 24rpx;
}
.article-img {
    width: 100%;
    height: 100%;
}
.play-btn {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 80rpx;
    height: 80rpx;
    margin-left: -40rpx;
    margin-top: -40rpx;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.5);
    border: 2rpx solid rgba(255, 255, 255, 0.9);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 2;
}
.play-icon {
    color: #fff;
    font-size: 32rpx;
    line-height: 1;
    margin-left: 4rpx;
}
.article-shadow {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 64rpx;
    background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 100%);
    display: flex;
    align-items: center;
    padding: 0 16rpx;
    box-sizing: border-box;
    z-index: 1;
}
.article-text {
    color: #FFFFFF;
    font-size: 24rpx;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    width: 100%;
}
</style>
traditionalApp/pages/good/static-content.js
New file
@@ -0,0 +1,125 @@
const DEFAULT_COVER = '/static/image/home/zixun.png'
const DEMO_VIDEO_URL = 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/2minute-demo.mp4'
/**
 * 项目中心静态内容。contentType:1 图文,2 视频。后续改为接口数据。
 */
export const PROJECT_CONTENT_MAP = {
    platform: [
        {
            id: 1,
            title: '平台介绍图文一',
            coverImage: DEFAULT_COVER,
            contentType: 1,
            content: '<p>这里是平台介绍图文内容,后续将替换为后台配置的正文。</p><p>当前为静态占位数据。</p>',
            videoUrl: '',
        },
        {
            id: 2,
            title: '平台介绍视频一',
            subtitle: '了解平台定位与核心能力',
            coverImage: DEFAULT_COVER,
            contentType: 2,
            content: '',
            videoUrl: DEMO_VIDEO_URL,
        },
        {
            id: 3,
            title: '平台介绍图文二',
            coverImage: DEFAULT_COVER,
            contentType: 1,
            content: '<p>平台介绍图文二的静态正文。</p>',
            videoUrl: '',
        },
        {
            id: 4,
            title: '平台介绍视频二',
            subtitle: '快速熟悉平台使用方式',
            coverImage: DEFAULT_COVER,
            contentType: 2,
            content: '',
            videoUrl: DEMO_VIDEO_URL,
        },
    ],
    mode: [
        {
            id: 1,
            title: '营销模式图文一',
            coverImage: DEFAULT_COVER,
            contentType: 1,
            content: '<p>这里是营销模式图文内容,后续将替换为后台配置的正文。</p>',
            videoUrl: '',
        },
        {
            id: 2,
            title: '营销模式视频一',
            subtitle: '拆解常见营销模式要点',
            coverImage: DEFAULT_COVER,
            contentType: 2,
            content: '',
            videoUrl: DEMO_VIDEO_URL,
        },
        {
            id: 3,
            title: '营销模式图文二',
            coverImage: DEFAULT_COVER,
            contentType: 1,
            content: '<p>营销模式图文二的静态正文。</p>',
            videoUrl: '',
        },
        {
            id: 4,
            title: '营销模式视频二',
            subtitle: '对照案例理解模式落地',
            coverImage: DEFAULT_COVER,
            contentType: 2,
            content: '',
            videoUrl: DEMO_VIDEO_URL,
        },
    ],
    action: [
        {
            id: 1,
            title: '营销动作图文一',
            coverImage: DEFAULT_COVER,
            contentType: 1,
            content: '<p>这里是营销动作图文内容,后续将替换为后台配置的正文。</p>',
            videoUrl: '',
        },
        {
            id: 2,
            title: '营销动作视频一',
            subtitle: '掌握关键营销动作步骤',
            coverImage: DEFAULT_COVER,
            contentType: 2,
            content: '',
            videoUrl: DEMO_VIDEO_URL,
        },
        {
            id: 3,
            title: '营销动作图文二',
            coverImage: DEFAULT_COVER,
            contentType: 1,
            content: '<p>营销动作图文二的静态正文。</p>',
            videoUrl: '',
        },
        {
            id: 4,
            title: '营销动作视频二',
            subtitle: '结合场景执行营销动作',
            coverImage: DEFAULT_COVER,
            contentType: 2,
            content: '',
            videoUrl: DEMO_VIDEO_URL,
        },
    ],
}
/**
 * 按入口类型和内容主键读取一条静态详情。
 */
export function getProjectContentDetail(type, id) {
    const list = PROJECT_CONTENT_MAP[type] || []
    const numericId = Number(id)
    return list.find((item) => item.id === numericId) || null
}
traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue
New file
@@ -0,0 +1,105 @@
<template>
    <view class="container">
        <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
        <view class="nav-bar">
            <text class="nav-title">养生课程</text>
        </view>
        <view class="content">
            <image class="entry-image" src="/static/image/healthCoursesHub/gongfa.png" mode="widthFix"
                hover-class="entry-image--pressed" hover-stay-time="80" @tap="goHealthCourses"></image>
            <image class="entry-image" src="/static/image/healthCoursesHub/kepu.png" mode="widthFix"
                hover-class="entry-image--pressed" hover-stay-time="80" @tap="goHealthPopularization"></image>
            <image class="entry-image" src="/static/image/healthCoursesHub/zhishi.png" mode="widthFix"
                hover-class="entry-image--pressed" hover-stay-time="80" @tap="goHealthKnowledge"></image>
        </view>
    </view>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const statusBarHeight = ref(0)
onMounted(() => {
    uni.getSystemInfo({
        success: (res) => {
            statusBarHeight.value = res.statusBarHeight || 0
        }
    })
})
/**
 * 进入原养生课程列表,作为「养生功法」二级页。
 */
const goHealthCourses = () => {
    uni.navigateTo({
        url: '/pages/healthCourses/healthCourses'
    })
}
/**
 * 进入复制后的养生科普二级页。
 */
const goHealthPopularization = () => {
    uni.navigateTo({
        url: '/pages/healthPopularization/healthPopularization'
    })
}
/**
 * 进入原健康科普页,作为「健康知识」二级页。
 */
const goHealthKnowledge = () => {
    uni.navigateTo({
        url: '/pages/publish/publish'
    })
}
</script>
<style>
page, html {
    background-color: #ffffff;
}
</style>
<style scoped lang="scss">
.container {
    width: 750rpx;
    min-height: 100vh;
    background-color: #ffffff;
}
.status-bar-placeholder {
    width: 100%;
}
.nav-bar {
    width: 100%;
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    .nav-title {
        font-size: 36rpx;
        font-weight: 600;
        color: #1a1a1a;
    }
}
.content {
    padding: 32rpx 40rpx 0;
    box-sizing: border-box;
    .entry-image {
        display: block;
        width: 670rpx;
        margin-bottom: 0;
        &--pressed {
            opacity: 0.92;
        }
    }
}
</style>
traditionalApp/pages/healthPopularization/healthPopularization.vue
New file
@@ -0,0 +1,205 @@
<template>
    <view class="container">
        <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
        <view class="nav-bar">
            <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
            <text class="nav-title">养生科普</text>
            <view class="nav-right-placeholder"></view>
        </view>
        <view class="content">
            <view v-if="articleList.length" class="article-list">
                <view class="article-item" v-for="item in articleList" :key="item.id" @tap="goToDetail(item.id)">
                    <image class="article-img" :src="item.coverImage || defaultCoverImage" mode="aspectFill"></image>
                    <view class="article-shadow">
                        <text class="article-text">{{ item.title }}</text>
                    </view>
                </view>
            </view>
            <view v-else-if="loading" class="status-text">加载中...</view>
            <view v-else class="status-text">暂无养生科普内容</view>
        </view>
    </view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { onPullDownRefresh } from '@dcloudio/uni-app'
import { getHealthPopularizationList } from './service'
interface PopularizationListItem {
    id: number | string
    title: string
    coverImage?: string
    contentType?: number
    clickCount?: number
}
const statusBarHeight = ref(0);
const loading = ref(false)
const articleList = ref<PopularizationListItem[]>([])
const defaultCoverImage = '/static/image/home/zixun.png'
/**
 * 页面加载时初始化状态栏高度并拉取养生科普列表。
 */
onMounted(() => {
    uni.getSystemInfo({
        success: (res) => {
            statusBarHeight.value = res.statusBarHeight || 0;
        }
    });
    loadPopularizationList()
});
/**
 * 下拉刷新养生科普列表,并在结束后关闭原生刷新状态。
 */
onPullDownRefresh(async () => {
    try {
        await loadPopularizationList()
    } finally {
        uni.stopPullDownRefresh()
    }
})
/**
 * 返回上一页;没有历史栈时回到养生课程中转页。
 */
function goBack() {
    const currentPages = getCurrentPages()
    if (currentPages.length > 1) {
        uni.navigateBack()
        return
    }
    uni.switchTab({
        url: '/pages/healthCoursesHub/healthCoursesHub'
    })
}
/**
 * 拉取管理后台已启用的养生科普列表数据。
 */
async function loadPopularizationList() {
    loading.value = true
    try {
        const response = await getHealthPopularizationList()
        if (response?.code !== 0 || !Array.isArray(response.data)) {
            articleList.value = []
            return
        }
        articleList.value = response.data.filter((item: PopularizationListItem) => !!item?.id)
    } catch {
        articleList.value = []
    } finally {
        loading.value = false
    }
}
/**
 * 跳转养生科普详情页。
 */
const goToDetail = (popularizationId?: number | string | null) => {
    uni.navigateTo({
        url: popularizationId
            ? `/pages/articleDetail/articleDetail?id=${popularizationId}`
            : '/pages/articleDetail/articleDetail'
    });
};
</script>
<style>
page, html {
    background-color: #fff;
}
</style>
<style lang="scss" scoped>
.container {
    min-height: 100vh;
    background-color: #fff;
}
.nav-bar {
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 32rpx;
}
.back-icon {
    width: 40rpx;
    height: 40rpx;
    flex-shrink: 0;
}
.nav-title {
    flex: 1;
    text-align: center;
    font-size: 36rpx;
    font-weight: 600;
    color: #333;
}
.nav-right-placeholder {
    width: 40rpx;
    flex-shrink: 0;
}
.content {
    margin-top: 32rpx;
    padding: 0 32rpx;
    padding-bottom: 40rpx;
}
.status-text {
    text-align: center;
    font-size: 24rpx;
    color: #C0C4CC;
    padding: 80rpx 0;
}
.article-list {
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
}
.article-item {
    width: 328rpx;
    height: 328rpx;
    border-radius: 16rpx;
    position: relative;
    overflow: hidden;
    margin-bottom: 24rpx;
}
.article-img {
    width: 100%;
    height: 100%;
}
.article-shadow {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 64rpx;
    background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 100%);
    display: flex;
    align-items: center;
    padding: 0 16rpx;
    box-sizing: border-box;
}
.article-text {
    color: #FFFFFF;
    font-size: 24rpx;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    width: 100%;
}
</style>
traditionalApp/pages/healthPopularization/service.js
New file
@@ -0,0 +1,8 @@
import request from '@/utils/request.js'
/**
 * 获取 APP 养生科普列表(与原健康科普共用接口)。
 */
export const getHealthPopularizationList = () => {
    return request.get('/api/app/health-popularizations', {}, false, false)
}
traditionalApp/pages/passwordInput/passwordInput.vue
New file
@@ -0,0 +1,185 @@
<template>
    <view class="container">
        <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
        <view class="nav-bar">
            <image class="back-icon" src="http://obs.haizhentong.com/file/left-c8c6ad1c120949bab803e2b648a2e69b.png"
                mode="aspectFit" @tap="handleBack"></image>
            <view class="nav-right-placeholder"></view>
        </view>
        <view class="content">
            <view class="page-title">直播间密码</view>
            <view class="input-wrap">
                <input
                    class="password-input"
                    type="text"
                    v-model="password"
                    placeholder="请输入直播间密码"
                    placeholder-class="input-placeholder"
                    :focus="autoFocus"
                    @confirm="handleConfirm"
                />
            </view>
            <view class="confirm-btn" :class="{ disabled: !password }" @tap="handleConfirm">
                确认
            </view>
        </view>
    </view>
</template>
<script setup lang="ts">
    import { onLoad } from '@dcloudio/uni-app'
    import { ref } from 'vue'
    const OPERATOR_TRAINING_PASSWORD = '123456'
    const statusBarHeight = ref(0)
    const password = ref('')
    const autoFocus = ref(true)
    const targetUrl = ref('')
    onLoad((options) => {
        uni.getSystemInfo({
            success: (res) => {
                statusBarHeight.value = res.statusBarHeight || 0
            }
        })
        if (options?.targetUrl) {
            targetUrl.value = decodeURIComponent(options.targetUrl)
        }
    })
    const handleBack = () => {
        uni.navigateBack()
    }
    const handleConfirm = () => {
        if (!password.value) {
            return
        }
        if (password.value !== OPERATOR_TRAINING_PASSWORD) {
            uni.showToast({
                title: '密码错误,请重新输入',
                icon: 'none'
            })
            return
        }
        if (targetUrl.value) {
            uni.redirectTo({
                url: targetUrl.value,
                fail: () => {
                    uni.navigateTo({
                        url: targetUrl.value
                    })
                }
            })
        } else {
            uni.navigateBack()
        }
    }
</script>
<style>
    page,
    html {
        background: #fff;
    }
</style>
<style scoped lang="scss">
    .container {
        width: 750rpx;
        min-height: 100vh;
        background-color: #ffffff;
        position: relative;
    }
    .status-bar-placeholder {
        width: 100%;
    }
    .nav-bar {
        width: 100%;
        height: 88rpx;
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding-left: 32rpx;
        padding-right: 32rpx;
        box-sizing: border-box;
        z-index: 100;
        background-color: #fff;
        .back-icon {
            flex-shrink: 0;
            width: 19rpx;
            height: 32rpx;
        }
        .nav-right-placeholder {
            width: 19rpx;
        }
    }
    .content {
        padding: 0 40rpx;
        box-sizing: border-box;
    }
    .page-title {
        font-size: 68rpx;
        font-family: 'PingFang SC', 'PingFang-SC', sans-serif;
        font-weight: 600;
        color: #1a1a1a;
        line-height: 84rpx;
        margin-left: 0;
        margin-top: 140rpx;
        margin-bottom: 60rpx;
    }
    .input-wrap {
        width: 670rpx;
        height: 108rpx;
        border: 1rpx solid #e5e5e5;
        border-radius: 16rpx;
        display: flex;
        align-items: center;
        padding: 0 32rpx;
        box-sizing: border-box;
        background: #ffffff;
        margin-bottom: 60rpx;
        .password-input {
            width: 100%;
            height: 100%;
            font-size: 32rpx;
            color: #333;
        }
        .input-placeholder {
            color: #999;
            font-size: 32rpx;
        }
    }
    .confirm-btn {
        width: 670rpx;
        height: 104rpx;
        background: linear-gradient(90deg, #c63520 0%, #c63520 100%);
        border-radius: 16rpx;
        color: #ffffff;
        font-size: 34rpx;
        font-weight: 500;
        display: flex;
        align-items: center;
        justify-content: center;
        &.disabled {
            opacity: 0.5;
        }
    }
</style>
traditionalApp/static/image/healthCoursesHub/gongfa.png
traditionalApp/static/image/healthCoursesHub/kepu.png
traditionalApp/static/image/healthCoursesHub/zhishi.png
traditionalApp/static/image/home/weizan-live.png
traditionalApp/static/image/mine/dingdan.png
traditionalApp/static/image/mine/tongji.png
traditionalApp/static/image/mine/yinghangka.png
traditionalApp/static/image/mine/yinghanka.png
traditionalApp/static/image/mine/zhibodingdan.png
traditionalApp/static/image/mine/zhibotongji.png
traditionalApp/static/image/project/dongzuo.png
traditionalApp/static/image/project/jieshao.png
traditionalApp/static/image/project/moshi.png
traditionalApp/static/image/tabBar/Yproject.png
traditionalApp/static/image/tabBar/project.png