From e782b94370da4b44e0f308f723b7d94148575a37 Mon Sep 17 00:00:00 2001
From: hejianhao <15708179461@qq.com>
Date: 星期二, 18 八月 2026 10:11:35 +0800
Subject: [PATCH] feat0806: 提交遗漏的APP文件
---
traditionalApp/pages/bankCard/service.js | 15
traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue | 105 ++++
traditionalApp/static/image/mine/yinghangka.png | 0
traditionalApp/static/image/tabBar/Yproject.png | 0
traditionalApp/static/image/mine/dingdan.png | 0
traditionalApp/pages/healthPopularization/service.js | 8
traditionalApp/static/image/healthCoursesHub/gongfa.png | 0
traditionalApp/static/image/project/jieshao.png | 0
traditionalApp/static/image/mine/zhibodingdan.png | 0
traditionalApp/static/image/project/dongzuo.png | 0
traditionalApp/pages/passwordInput/passwordInput.vue | 185 ++++++++
traditionalApp/static/image/home/weizan-live.png | 0
traditionalApp/pages/good/projectDetail.vue | 204 ++++++++
traditionalApp/static/image/tabBar/project.png | 0
traditionalApp/static/image/mine/tongji.png | 0
traditionalApp/static/image/mine/zhibotongji.png | 0
traditionalApp/static/image/mine/yinghanka.png | 0
traditionalApp/static/image/project/moshi.png | 0
traditionalApp/pages/good/static-content.js | 125 +++++
traditionalApp/pages/healthPopularization/healthPopularization.vue | 205 ++++++++
traditionalApp/static/image/healthCoursesHub/zhishi.png | 0
traditionalApp/static/image/healthCoursesHub/kepu.png | 0
traditionalApp/pages/bankCard/bankCard.vue | 289 ++++++++++++
traditionalApp/pages/good/projectList.vue | 210 +++++++++
24 files changed, 1,346 insertions(+), 0 deletions(-)
diff --git a/traditionalApp/pages/bankCard/bankCard.vue b/traditionalApp/pages/bankCard/bankCard.vue
new file mode 100644
index 0000000..6e13785
--- /dev/null
+++ b/traditionalApp/pages/bankCard/bankCard.vue
@@ -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>
diff --git a/traditionalApp/pages/bankCard/service.js b/traditionalApp/pages/bankCard/service.js
new file mode 100644
index 0000000..1d40f6c
--- /dev/null
+++ b/traditionalApp/pages/bankCard/service.js
@@ -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)
+}
diff --git a/traditionalApp/pages/good/projectDetail.vue b/traditionalApp/pages/good/projectDetail.vue
new file mode 100644
index 0000000..5da54c0
--- /dev/null
+++ b/traditionalApp/pages/good/projectDetail.vue
@@ -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>
diff --git a/traditionalApp/pages/good/projectList.vue b/traditionalApp/pages/good/projectList.vue
new file mode 100644
index 0000000..639a8e3
--- /dev/null
+++ b/traditionalApp/pages/good/projectList.vue
@@ -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>
diff --git a/traditionalApp/pages/good/static-content.js b/traditionalApp/pages/good/static-content.js
new file mode 100644
index 0000000..dc4d21c
--- /dev/null
+++ b/traditionalApp/pages/good/static-content.js
@@ -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
+}
diff --git a/traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue b/traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue
new file mode 100644
index 0000000..48956ac
--- /dev/null
+++ b/traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue
@@ -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>
diff --git a/traditionalApp/pages/healthPopularization/healthPopularization.vue b/traditionalApp/pages/healthPopularization/healthPopularization.vue
new file mode 100644
index 0000000..140f81a
--- /dev/null
+++ b/traditionalApp/pages/healthPopularization/healthPopularization.vue
@@ -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>
diff --git a/traditionalApp/pages/healthPopularization/service.js b/traditionalApp/pages/healthPopularization/service.js
new file mode 100644
index 0000000..419cab1
--- /dev/null
+++ b/traditionalApp/pages/healthPopularization/service.js
@@ -0,0 +1,8 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取 APP 养生科普列表(与原健康科普共用接口)。
+ */
+export const getHealthPopularizationList = () => {
+ return request.get('/api/app/health-popularizations', {}, false, false)
+}
diff --git a/traditionalApp/pages/passwordInput/passwordInput.vue b/traditionalApp/pages/passwordInput/passwordInput.vue
new file mode 100644
index 0000000..37520a1
--- /dev/null
+++ b/traditionalApp/pages/passwordInput/passwordInput.vue
@@ -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>
diff --git a/traditionalApp/static/image/healthCoursesHub/gongfa.png b/traditionalApp/static/image/healthCoursesHub/gongfa.png
new file mode 100644
index 0000000..acb5549
--- /dev/null
+++ b/traditionalApp/static/image/healthCoursesHub/gongfa.png
Binary files differ
diff --git a/traditionalApp/static/image/healthCoursesHub/kepu.png b/traditionalApp/static/image/healthCoursesHub/kepu.png
new file mode 100644
index 0000000..904f5ad
--- /dev/null
+++ b/traditionalApp/static/image/healthCoursesHub/kepu.png
Binary files differ
diff --git a/traditionalApp/static/image/healthCoursesHub/zhishi.png b/traditionalApp/static/image/healthCoursesHub/zhishi.png
new file mode 100644
index 0000000..9082009
--- /dev/null
+++ b/traditionalApp/static/image/healthCoursesHub/zhishi.png
Binary files differ
diff --git a/traditionalApp/static/image/home/weizan-live.png b/traditionalApp/static/image/home/weizan-live.png
new file mode 100644
index 0000000..9c0a2a5
--- /dev/null
+++ b/traditionalApp/static/image/home/weizan-live.png
Binary files differ
diff --git a/traditionalApp/static/image/mine/dingdan.png b/traditionalApp/static/image/mine/dingdan.png
new file mode 100644
index 0000000..001b40d
--- /dev/null
+++ b/traditionalApp/static/image/mine/dingdan.png
Binary files differ
diff --git a/traditionalApp/static/image/mine/tongji.png b/traditionalApp/static/image/mine/tongji.png
new file mode 100644
index 0000000..c42d718
--- /dev/null
+++ b/traditionalApp/static/image/mine/tongji.png
Binary files differ
diff --git a/traditionalApp/static/image/mine/yinghangka.png b/traditionalApp/static/image/mine/yinghangka.png
new file mode 100644
index 0000000..1719c33
--- /dev/null
+++ b/traditionalApp/static/image/mine/yinghangka.png
Binary files differ
diff --git a/traditionalApp/static/image/mine/yinghanka.png b/traditionalApp/static/image/mine/yinghanka.png
new file mode 100644
index 0000000..1719c33
--- /dev/null
+++ b/traditionalApp/static/image/mine/yinghanka.png
Binary files differ
diff --git a/traditionalApp/static/image/mine/zhibodingdan.png b/traditionalApp/static/image/mine/zhibodingdan.png
new file mode 100644
index 0000000..001b40d
--- /dev/null
+++ b/traditionalApp/static/image/mine/zhibodingdan.png
Binary files differ
diff --git a/traditionalApp/static/image/mine/zhibotongji.png b/traditionalApp/static/image/mine/zhibotongji.png
new file mode 100644
index 0000000..c42d718
--- /dev/null
+++ b/traditionalApp/static/image/mine/zhibotongji.png
Binary files differ
diff --git a/traditionalApp/static/image/project/dongzuo.png b/traditionalApp/static/image/project/dongzuo.png
new file mode 100644
index 0000000..7acb7df
--- /dev/null
+++ b/traditionalApp/static/image/project/dongzuo.png
Binary files differ
diff --git a/traditionalApp/static/image/project/jieshao.png b/traditionalApp/static/image/project/jieshao.png
new file mode 100644
index 0000000..6fe626d
--- /dev/null
+++ b/traditionalApp/static/image/project/jieshao.png
Binary files differ
diff --git a/traditionalApp/static/image/project/moshi.png b/traditionalApp/static/image/project/moshi.png
new file mode 100644
index 0000000..29d6597
--- /dev/null
+++ b/traditionalApp/static/image/project/moshi.png
Binary files differ
diff --git a/traditionalApp/static/image/tabBar/Yproject.png b/traditionalApp/static/image/tabBar/Yproject.png
new file mode 100644
index 0000000..fee3bf3
--- /dev/null
+++ b/traditionalApp/static/image/tabBar/Yproject.png
Binary files differ
diff --git a/traditionalApp/static/image/tabBar/project.png b/traditionalApp/static/image/tabBar/project.png
new file mode 100644
index 0000000..3656855
--- /dev/null
+++ b/traditionalApp/static/image/tabBar/project.png
Binary files differ
--
Gitblit v1.7.1