| New file |
| | |
| | | <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> |
| New file |
| | |
| | | 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) |
| | | } |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | 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 |
| | | } |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | import request from '@/utils/request.js' |
| | | |
| | | /** |
| | | * 获取 APP 养生科普列表(与原健康科普共用接口)。 |
| | | */ |
| | | export const getHealthPopularizationList = () => { |
| | | return request.get('/api/app/health-popularizations', {}, false, false) |
| | | } |
| New file |
| | |
| | | <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> |